Commit 14c26670 authored by 张宏's avatar 张宏

3333

parent 70c95cd5
......@@ -24,13 +24,15 @@ class DeviceTopologyInfoBO {
final int deviceId;
final String deviceName;
final String deviceCode;
final String locationText;
final String deviceLocation;
final String statusText;
const DeviceTopologyInfoBO({
required this.deviceId,
required this.deviceName,
required this.deviceCode,
required this.locationText,
required this.deviceLocation,
required this.statusText
});
factory DeviceTopologyInfoBO.fromJson(Map<String, dynamic> json) {
......@@ -38,7 +40,8 @@ class DeviceTopologyInfoBO {
deviceId: json['deviceId'] as int? ?? 0,
deviceName: json['deviceName'] as String? ?? '',
deviceCode: json['deviceCode'] as String? ?? '',
locationText: json['locationText'] as String? ?? '',
deviceLocation: json['deviceLocation'] as String? ?? '',
statusText: json['statusText'] as String? ?? ''
);
}
}
......@@ -51,6 +54,7 @@ class TreeNodeBO {
final String status;
final String statusText;
final String deviceCode;
final int? gatewayId;
const TreeNodeBO({
required this.deviceId,
......@@ -60,6 +64,7 @@ class TreeNodeBO {
required this.status,
required this.statusText,
required this.deviceCode,
this.gatewayId,
});
factory TreeNodeBO.fromJson(Map<String, dynamic> json) {
......@@ -71,6 +76,7 @@ class TreeNodeBO {
status: json['status'] as String? ?? '',
statusText: json['statusText'] as String? ?? '',
deviceCode: json['deviceCode'] as String? ?? '',
gatewayId: json['gatewayId'] as int?,
);
}
}
\ No newline at end of file
......@@ -63,10 +63,15 @@ class DeviceCubit extends Cubit<DeviceState> {
emit(state.copyWith(isPoweringOn: true));
try {
await _service.powerOn(deviceId: _deviceId);
emit(state.copyWith(isPoweringOn: false));
emit(state.copyWith(
isPoweringOn: false,
operationMessage: '远程送电成功',
));
} catch (e) {
emit(state.copyWith(isPoweringOn: false));
rethrow;
emit(state.copyWith(
isPoweringOn: false,
operationMessage: '远程送电失败: $e',
));
}
}
......@@ -75,10 +80,19 @@ class DeviceCubit extends Cubit<DeviceState> {
emit(state.copyWith(isPoweringOff: true));
try {
await _service.powerOff(deviceId: _deviceId);
emit(state.copyWith(isPoweringOff: false));
emit(state.copyWith(
isPoweringOff: false,
operationMessage: '远程断电成功',
));
} catch (e) {
emit(state.copyWith(isPoweringOff: false));
rethrow;
emit(state.copyWith(
isPoweringOff: false,
operationMessage: '远程断电失败: $e',
));
}
}
void clearOperationMessage() {
emit(state.copyWith(operationMessage: null));
}
}
\ No newline at end of file
......@@ -30,6 +30,7 @@ class DeviceState {
final DeviceInfo? deviceInfo;
final bool isPoweringOn;
final bool isPoweringOff;
final String? operationMessage;
const DeviceState({
this.isLoading = false,
......@@ -37,6 +38,7 @@ class DeviceState {
this.deviceInfo,
this.isPoweringOn = false,
this.isPoweringOff = false,
this.operationMessage,
});
DeviceState copyWith({
......@@ -45,6 +47,7 @@ class DeviceState {
DeviceInfo? deviceInfo,
bool? isPoweringOn,
bool? isPoweringOff,
String? operationMessage,
}) {
return DeviceState(
isLoading: isLoading ?? this.isLoading,
......@@ -52,6 +55,7 @@ class DeviceState {
deviceInfo: deviceInfo ?? this.deviceInfo,
isPoweringOn: isPoweringOn ?? this.isPoweringOn,
isPoweringOff: isPoweringOff ?? this.isPoweringOff,
operationMessage: operationMessage ?? this.operationMessage,
);
}
}
\ No newline at end of file
......@@ -18,65 +18,76 @@ class DeviceDetailView extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => DeviceCubit(deviceId: deviceId),
child: Scaffold(
backgroundColor: const Color.fromRGBO(242, 243, 245, 1),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios,
color: Color.fromRGBO(100, 116, 139, 1)),
onPressed: () {
context.popRoute();
},
),
title: Text(
'设备控制',
style: TextStyle(
color: const Color.fromRGBO(10, 13, 20, 1),
fontSize: 32.sp,
fontWeight: FontWeight.w200,
child: BlocListener<DeviceCubit, DeviceState>(
listener: (context, state) {
final message = state.operationMessage;
if (message != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
context.read<DeviceCubit>().clearOperationMessage();
}
},
child: Scaffold(
backgroundColor: const Color.fromRGBO(242, 243, 245, 1),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios,
color: Color.fromRGBO(100, 116, 139, 1)),
onPressed: () {
context.maybePop();
},
),
title: Text(
'设备控制',
style: TextStyle(
color: const Color.fromRGBO(10, 13, 20, 1),
fontSize: 32.sp,
fontWeight: FontWeight.w200,
),
),
centerTitle: true,
),
centerTitle: true,
),
body: BlocBuilder<DeviceCubit, DeviceState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state.error != null) {
return Center(
child: Text(
'加载失败: ${state.error}',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
body: BlocBuilder<DeviceCubit, DeviceState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state.error != null) {
return Center(
child: Text(
'加载失败: ${state.error}',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(255, 100, 101, 1),
),
),
);
}
final deviceInfo = state.deviceInfo;
if (deviceInfo == null) {
return const SizedBox.shrink();
}
final cubit = context.read<DeviceCubit>();
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: Column(
children: [
StateInfoBlock(deviceInfo: deviceInfo),
SizedBox(height: 10.h),
DeviceParmsBlock(deviceInfo: deviceInfo),
SizedBox(height: 10.h),
PowerCharBlock(deviceInfo: deviceInfo),
SizedBox(height: 10.h),
_buildBottomButtons(context, cubit, state),
SizedBox(height: 30.h),
],
),
);
}
final deviceInfo = state.deviceInfo;
if (deviceInfo == null) {
return const SizedBox.shrink();
}
final cubit = context.read<DeviceCubit>();
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: Column(
children: [
StateInfoBlock(deviceInfo: deviceInfo),
SizedBox(height: 10.h),
DeviceParmsBlock(deviceInfo: deviceInfo),
SizedBox(height: 10.h),
PowerCharBlock(deviceInfo: deviceInfo),
SizedBox(height: 10.h),
_buildBottomButtons(context, cubit, state),
SizedBox(height: 30.h),
],
),
);
},
},
),
),
),
);
......@@ -103,7 +114,7 @@ class DeviceDetailView extends StatelessWidget {
child: TextButton(
onPressed: state.isPoweringOn
? null
: () => _handlePowerOn(context, cubit),
: () => cubit.remotePowerOn(),
child: state.isPoweringOn
? SizedBox(
width: 32.sp,
......@@ -140,7 +151,7 @@ class DeviceDetailView extends StatelessWidget {
child: TextButton(
onPressed: state.isPoweringOff
? null
: () => _handlePowerOff(context, cubit),
: () => cubit.remotePowerOff(),
child: state.isPoweringOff
? SizedBox(
width: 32.sp,
......@@ -162,38 +173,4 @@ class DeviceDetailView extends StatelessWidget {
),
);
}
Future<void> _handlePowerOn(BuildContext context, DeviceCubit cubit) async {
try {
await cubit.remotePowerOn();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('远程送电成功')),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('远程送电失败: $e')),
);
}
}
}
Future<void> _handlePowerOff(BuildContext context, DeviceCubit cubit) async {
try {
await cubit.remotePowerOff();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('远程断电成功')),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('远程断电失败: $e')),
);
}
}
}
}
\ No newline at end of file
......@@ -22,6 +22,7 @@ class DeviceListItem extends StatelessWidget {
borderRadius: BorderRadius.circular(20.r),
),
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
margin: EdgeInsets.only(bottom: 20.h),
child: Row(
children: [
Container(
......
......@@ -80,14 +80,18 @@ class InspectionDeviceView extends StatelessWidget {
lastInspectionTime: state.lastInspectionTime,
icon: state.icon,
),
SizedBox(height: 20.h),
InspectionHistoryCard(
historyList: state.inspectionHistory,
),
// SizedBox(height: 20.h),
InspectionItemsCard(
items: state.inspectionItems,
),
if (state.inspectionHistory.isNotEmpty) ...[
SizedBox(height: 20.h),
InspectionHistoryCard(
historyList: state.inspectionHistory,
),
],
if (state.inspectionItems.isNotEmpty) ...[
SizedBox(height: 20.h),
InspectionItemsCard(
items: state.inspectionItems,
),
],
SizedBox(height: 30.h),
StartInspectionButton(deviceId: state.deviceId),
SizedBox(height: 20.h),
......
......@@ -76,14 +76,18 @@ class DeviceOverviewCard extends StatelessWidget {
Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 10.h),
decoration: BoxDecoration(
color: const Color.fromRGBO(225, 245, 238, 1.0),
color: deviceStatus == '1'
? const Color.fromRGBO(225, 245, 238, 1.0)
: const Color.fromRGBO(255, 235, 238, 1.0),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
deviceStatus,
deviceStatus == '1' ? '在线' : '离线',
style: TextStyle(
fontSize: 28.sp,
color: const Color.fromRGBO(26, 188, 156, 1.0),
color: deviceStatus == '1'
? const Color.fromRGBO(26, 188, 156, 1.0)
: const Color.fromRGBO(231, 76, 60, 1.0),
fontWeight: FontWeight.bold,
),
),
......
......@@ -18,6 +18,7 @@ class InspectionRecordItem extends StatelessWidget {
borderRadius: BorderRadius.circular(20.r),
),
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
margin: EdgeInsets.only(bottom: 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
......
......@@ -41,111 +41,104 @@ class InspectionTopologyCubit extends Cubit<InspectionTopologyState> {
final rootNode = _buildTopologyTree(nodes);
// 计算连接数: 父节点到每个子节点一条连接
int connectionCount = 0;
for (final child in rootNode.children) {
connectionCount++; // 根到子
connectionCount += child.children.length; // 子到孙
}
emit(InspectionTopologyState(
deviceName: info.deviceName,
deviceModel: '${info.deviceCode}·${info.locationText}',
deviceModel: '${info.deviceCode}·${info.deviceLocation}',
topologyNodes: [rootNode],
deviceCount: _countAllNodes(rootNode),
connectionCount: connectionCount,
connectionCount: _countConnections(rootNode),
// statusText:
));
}
/// 将 API 返回的扁平节点列表转换为树结构 + 自动计算位置
/// 根据 gatewayId 将扁平节点列表构建为树结构,并自动计算布局位置
TopologyNode _buildTopologyTree(List<TreeNodeBO> nodes) {
if (nodes.isEmpty) {
return TopologyNode(
return const TopologyNode(
id: '0',
name: '未知设备',
status: 'offline',
children: [],
x: 0.5,
y: 0.15,
y: 0.08,
);
}
// 第一个节点作为根节点
final root = nodes.first;
final children = nodes.length > 1 ? nodes.sublist(1) : <TreeNodeBO>[];
// 如果子节点 <= 6,平分到第二层;否则将前6个放第二层,其余作为叶子
final int maxLevel2 = 6;
final level2Nodes = children.take(maxLevel2).toList();
final leafNodes = children.length > maxLevel2
? children.sublist(maxLevel2)
: <TreeNodeBO>[];
final rootNode = TopologyNode(
id: root.deviceId.toString(),
name: root.deviceName,
status: root.status,
x: 0.5,
y: 0.15,
children: level2Nodes.asMap().entries.map((entry) {
final idx = entry.key;
final node = entry.value;
// 在第二层均匀分布
final xPos = (level2Nodes.length == 1)
? 0.5
: 0.15 + (0.7 * idx / (level2Nodes.length - 1));
return TopologyNode(
id: node.deviceId.toString(),
name: node.deviceName,
status: node.status,
x: xPos,
y: 0.45,
children: [],
);
}).toList(),
);
// 补充叶子节点:附着到最近的第二层节点上
if (leafNodes.isNotEmpty && rootNode.children.isNotEmpty) {
final leafNode = leafNodes.first;
final lastLevel2Idx = rootNode.children.length - 1;
final lastLevel2 = rootNode.children[lastLevel2Idx];
final updatedChildren = List<TopologyNode>.from(rootNode.children);
updatedChildren[lastLevel2Idx] = TopologyNode(
id: lastLevel2.id,
name: lastLevel2.name,
status: lastLevel2.status,
x: lastLevel2.x,
y: lastLevel2.y,
children: leafNodes.asMap().entries.map((entry) {
final idx = entry.key;
final leaf = entry.value;
final xPos = (leafNodes.length == 1)
? lastLevel2.x
: lastLevel2.x -
0.1 +
(0.2 * idx / (leafNodes.length - 1));
return TopologyNode(
id: leaf.deviceId.toString(),
name: leaf.deviceName,
status: leaf.status,
x: xPos,
y: 0.75,
children: [],
);
}).toList(),
// 构建子节点映射表:parent deviceId -> [child TreeNodeBO]
final childrenMap = <int, List<TreeNodeBO>>{};
final nodeMap = <int, TreeNodeBO>{};
for (final node in nodes) {
nodeMap[node.deviceId] = node;
final parentId = node.gatewayId ?? 0;
childrenMap.putIfAbsent(parentId, () => []).add(node);
}
// 查找根节点:gatewayId 为 null/0,或引用的父节点不存在于列表中
final nodeIds = nodeMap.keys.toSet();
final roots = nodes.where((n) =>
n.gatewayId == null ||
n.gatewayId == 0 ||
!nodeIds.contains(n.gatewayId),
).toList();
if (roots.isEmpty) {
// fallback:取第一个节点作为根
roots.add(nodes.first);
}
// 递归构建节点并计算布局位置
TopologyNode buildNode(TreeNodeBO node, int depth, double xCenter, double xSpan) {
final children = childrenMap[node.deviceId] ?? [];
final childNodes = <TopologyNode>[];
if (children.isNotEmpty) {
for (int i = 0; i < children.length; i++) {
final childX = children.length == 1
? xCenter
: xCenter - xSpan / 2 + (xSpan * i / (children.length - 1));
childNodes.add(buildNode(children[i], depth + 1, childX, xSpan * 0.55));
}
}
return TopologyNode(
id: node.deviceId.toString(),
name: node.deviceName,
status: node.status,
statusText: node.statusText,
gatewayId: node.gatewayId?.toString(),
x: xCenter.clamp(0.05, 0.95),
y: (0.08 + depth * 0.17).clamp(0.02, 0.98),
children: childNodes,
);
}
if (roots.length == 1) {
return buildNode(roots.first, 0, 0.5, 0.7);
} else {
// 多个根节点:创建一个虚拟根,将所有根均匀分布在第二层
final virtualChildren = <TopologyNode>[];
for (int i = 0; i < roots.length; i++) {
final xPos = 0.1 + (0.8 * i / (roots.length - 1));
virtualChildren.add(buildNode(roots[i], 1, xPos, 0.35));
}
return TopologyNode(
id: rootNode.id,
name: rootNode.name,
status: rootNode.status,
x: rootNode.x,
y: rootNode.y,
children: updatedChildren,
id: '0',
name: '根节点',
status: 'online',
x: 0.5,
y: 0.08,
children: virtualChildren,
);
}
}
return rootNode;
/// 递归计算连接数
int _countConnections(TopologyNode node) {
int count = 0;
for (final child in node.children) {
count += 1 + _countConnections(child);
}
return count;
}
int _countAllNodes(TopologyNode node) {
......
......@@ -10,6 +10,7 @@ class InspectionTopologyState extends Equatable {
final int deviceCount;
final int connectionCount;
final TopologyNode? selectedNode;
final String statusText;
const InspectionTopologyState({
this.isLoading = false,
......@@ -20,6 +21,7 @@ class InspectionTopologyState extends Equatable {
this.deviceCount = 0,
this.connectionCount = 0,
this.selectedNode,
this.statusText = ''
});
InspectionTopologyState copyWith({
......@@ -31,6 +33,7 @@ class InspectionTopologyState extends Equatable {
int? deviceCount,
int? connectionCount,
TopologyNode? selectedNode,
String? statusText,
}) {
return InspectionTopologyState(
isLoading: isLoading ?? this.isLoading,
......@@ -41,6 +44,7 @@ class InspectionTopologyState extends Equatable {
deviceCount: deviceCount ?? this.deviceCount,
connectionCount: connectionCount ?? this.connectionCount,
selectedNode: selectedNode,
statusText: statusText ?? this.statusText,
);
}
......@@ -54,6 +58,7 @@ class InspectionTopologyState extends Equatable {
deviceCount,
connectionCount,
selectedNode,
statusText
];
}
......@@ -64,6 +69,8 @@ class TopologyNode extends Equatable {
final List<TopologyNode> children;
final double x;
final double y;
final String? gatewayId;
final String? statusText;
const TopologyNode({
required this.id,
......@@ -72,8 +79,10 @@ class TopologyNode extends Equatable {
required this.children,
required this.x,
required this.y,
this.gatewayId,
this.statusText,
});
@override
List<Object?> get props => [id, name, status, children, x, y];
List<Object?> get props => [id, name, status, children, x, y, gatewayId, statusText];
}
\ No newline at end of file
......@@ -118,6 +118,7 @@ class InspectionTopologyPage extends StatelessWidget {
if (state.selectedNode != null)
DeviceDetailDialog(
node: state.selectedNode!,
curentState: state,
onClose: () =>
context.read<InspectionTopologyCubit>().selectNode(null),
),
......
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:smart_hotel_app/routes/app_router.gr.dart';
import '../cubit/inspection_topology_state.dart';
class DeviceDetailDialog extends StatelessWidget {
final TopologyNode node;
final InspectionTopologyState curentState;
final VoidCallback onClose;
const DeviceDetailDialog({
super.key,
required this.node,
required this.curentState,
required this.onClose,
});
......@@ -87,7 +91,7 @@ class DeviceDetailDialog extends StatelessWidget {
),
SizedBox(height: 12.h),
Text(
'传感器',
curentState.deviceModel,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(148, 163, 184, 1),
......@@ -106,7 +110,7 @@ class DeviceDetailDialog extends StatelessWidget {
borderRadius: BorderRadius.circular(40.r),
),
child: Text(
'正常',
node.statusText?? '-',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
......@@ -141,7 +145,7 @@ class DeviceDetailDialog extends StatelessWidget {
),
),
Text(
'dev-${node.id.padLeft(3, '0')}',
'${node.id}',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
......@@ -179,7 +183,14 @@ class DeviceDetailDialog extends StatelessWidget {
width: double.infinity,
height: 104.h,
child: ElevatedButton(
onPressed: () {},
onPressed: () {
final deviceId = int.tryParse(node.id) ?? 0;
if (deviceId > 0) {
context.pushRoute(
DeviceDetailRoute(deviceId: deviceId),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromRGBO(59, 130, 246, 1),
shape: RoundedRectangleBorder(
......
# 智慧酒店 App 接口对接方案
# 智慧酒店 App 接口对接方案
......@@ -1153,6 +1153,7 @@ class xxxBO extends Equatable {
{
"deviceId": 0,
"deviceName": "string",
"gatewayId":0, // 父级id
"deviceType": "string",
"icon": "string",
"status": "string",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment