Commit 95546f15 authored by 张宏's avatar 张宏

设备拓扑及相关样式调整

parent 3c72b94e
......@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:smart_hotel_app/routes/app_router.gr.dart';
import 'package:smart_hotel_app/views/report/rule/rule_management_view.dart';
import 'package:smart_hotel_app/views/report/device/device_list_view.dart';
import 'package:smart_hotel_app/views/home/inspection_topology/inspection_topology_view.dart';
@AutoRouterConfig(replaceInRouteName: 'View,Route')
class AppRouter extends $AppRouter {
......@@ -34,5 +35,6 @@ class AppRouter extends $AppRouter {
AutoRoute(page: AbnormalListRoute.page),
AutoRoute(page: ReportRuleManagementRoute.page),
AutoRoute(page: ReportDeviceListRoute.page),
AutoRoute(page: InspectionTopologyRoute.page),
];
}
This diff is collapsed.
......@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:smart_hotel_app/routes/app_router.gr.dart';
import 'cubit/inspection_device_cubit.dart';
import 'cubit/inspection_device_state.dart';
import 'widget/device_overview_card.dart';
......@@ -31,24 +32,29 @@ class InspectionDeviceView extends StatelessWidget {
context.popRoute();
},
),
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'设备拓扑图',
style: TextStyle(
title: GestureDetector(
onTap: () {
context.pushRoute(const InspectionTopologyRoute());
},
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'设备拓扑图',
style: TextStyle(
color: const Color.fromRGBO(100, 116, 139, 1),
fontSize: 32.sp,
fontWeight: FontWeight.w200,
),
),
Icon(
Icons.arrow_forward,
color: const Color.fromRGBO(100, 116, 139, 1),
fontSize: 32.sp,
fontWeight: FontWeight.w200,
),
),
// Icon(
// Icons.arrow_forward,
// color: const Color.fromRGBO(100, 116, 139, 1),
// ),
],
],
),
),
centerTitle: true,
centerTitle: false,
),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 20.h),
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'inspection_topology_state.dart';
class InspectionTopologyCubit extends Cubit<InspectionTopologyState> {
InspectionTopologyCubit()
: super(const InspectionTopologyState(
deviceName: '',
deviceModel: '',
topologyNodes: [],
)) {
_loadTopologyData();
}
void selectNode(TopologyNode? node) {
emit(state.copyWith(selectedNode: node));
}
void _loadTopologyData() {
final rootNode = TopologyNode(
id: '1',
name: '智能空开',
status: 'online',
x: 0.5,
y: 0.15,
children: [
TopologyNode(
id: '2',
name: '串口分配器',
status: 'online',
x: 0.25,
y: 0.45,
children: [
TopologyNode(
id: '5',
name: '环境采集',
status: 'online',
x: 0.15,
y: 0.75,
children: [],
),
TopologyNode(
id: '6',
name: '人体存在',
status: 'online',
x: 0.35,
y: 0.75,
children: [],
),
],
),
TopologyNode(
id: '3',
name: '网关',
status: 'online',
x: 0.5,
y: 0.45,
children: [
TopologyNode(
id: '7',
name: '环境采集',
status: 'online',
x: 0.45,
y: 0.75,
children: [],
),
TopologyNode(
id: '8',
name: '人体存在',
status: 'online',
x: 0.55,
y: 0.75,
children: [],
),
],
),
TopologyNode(
id: '4',
name: 'WIFI',
status: 'online',
x: 0.75,
y: 0.45,
children: [
TopologyNode(
id: '9',
name: '环境采集',
status: 'online',
x: 0.65,
y: 0.75,
children: [],
),
TopologyNode(
id: '10',
name: '人体存在',
status: 'online',
x: 0.85,
y: 0.75,
children: [],
),
],
),
],
);
emit(InspectionTopologyState(
deviceName: '客房305 智能空开',
deviceModel: 'A-301智能空开·客房301',
topologyNodes: [rootNode],
deviceCount: 10,
connectionCount: 11,
));
}
}
import 'package:flutter/material.dart';
import 'package:equatable/equatable.dart';
class InspectionTopologyState extends Equatable {
final String deviceName;
final String deviceModel;
final List<TopologyNode> topologyNodes;
final int deviceCount;
final int connectionCount;
final TopologyNode? selectedNode;
const InspectionTopologyState({
required this.deviceName,
required this.deviceModel,
required this.topologyNodes,
this.deviceCount = 10,
this.connectionCount = 11,
this.selectedNode,
});
InspectionTopologyState copyWith({
String? deviceName,
String? deviceModel,
List<TopologyNode>? topologyNodes,
int? deviceCount,
int? connectionCount,
TopologyNode? selectedNode,
}) {
return InspectionTopologyState(
deviceName: deviceName ?? this.deviceName,
deviceModel: deviceModel ?? this.deviceModel,
topologyNodes: topologyNodes ?? this.topologyNodes,
deviceCount: deviceCount ?? this.deviceCount,
connectionCount: connectionCount ?? this.connectionCount,
selectedNode: selectedNode,
);
}
@override
List<Object?> get props => [deviceName, deviceModel, topologyNodes, deviceCount, connectionCount, selectedNode];
}
class TopologyNode extends Equatable {
final String id;
final String name;
final String status;
final List<TopologyNode> children;
final double x;
final double y;
const TopologyNode({
required this.id,
required this.name,
required this.status,
required this.children,
required this.x,
required this.y,
});
@override
List<Object?> get props => [id, name, status, children, x, y];
}
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'cubit/inspection_topology_cubit.dart';
import 'cubit/inspection_topology_state.dart';
import 'widget/device_header_widget.dart';
import 'widget/status_legend_widget.dart';
import 'widget/network_topology_widget.dart';
import 'widget/device_detail_dialog.dart';
@RoutePage()
class InspectionTopologyView extends StatelessWidget {
const InspectionTopologyView({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => InspectionTopologyCubit(),
child: const InspectionTopologyPage(),
);
}
}
class InspectionTopologyPage extends StatelessWidget {
const InspectionTopologyPage({super.key});
@override
Widget build(BuildContext context) {
return 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(15, 23, 42, 1),
fontSize: 32.sp,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
),
body: BlocBuilder<InspectionTopologyCubit, InspectionTopologyState>(
builder: (context, state) {
return Stack(
children: [
SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 24.h),
child: Column(
children: [
SizedBox(height: 10.h),
StatusLegendWidget(),
SizedBox(height: 20.h),
DeviceHeaderWidget(
deviceName: state.deviceName,
deviceModel: state.deviceModel,
),
NetworkTopologyWidget(
topologyNodes: state.topologyNodes,
deviceCount: state.deviceCount,
connectionCount: state.connectionCount,
onNodeTap: (node) =>
context.read<InspectionTopologyCubit>().selectNode(node),
),
],
),
),
if (state.selectedNode != null)
DeviceDetailDialog(
node: state.selectedNode!,
onClose: () =>
context.read<InspectionTopologyCubit>().selectNode(null),
),
],
);
},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../cubit/inspection_topology_state.dart';
class DeviceDetailDialog extends StatelessWidget {
final TopologyNode node;
final VoidCallback onClose;
const DeviceDetailDialog({
super.key,
required this.node,
required this.onClose,
});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.transparent,
child: Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: onClose,
child: Container(
color: Colors.black.withOpacity(0.5),
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(48.r),
topRight: Radius.circular(48.r),
),
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: EdgeInsets.only(top: 20.h),
width: 120.w,
height: 8.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(203, 213, 225, 1),
borderRadius: BorderRadius.circular(4.r),
),
),
Padding(
padding: EdgeInsets.all(48.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 80.w,
height: 80.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(239, 246, 255, 1),
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(
_getDeviceIcon(node.name),
size: 80.sp,
color: const Color.fromRGBO(59, 130, 246, 1),
),
),
SizedBox(width: 32.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getDeviceFullName(node.name),
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.w600,
color: const Color.fromRGBO(15, 23, 42, 1),
),
),
SizedBox(height: 12.h),
Text(
'传感器',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(148, 163, 184, 1),
),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 12.h,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(209, 250, 229, 1),
borderRadius: BorderRadius.circular(40.r),
),
child: Text(
'正常',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
color: const Color.fromRGBO(16, 185, 129, 1),
),
),
),
],
),
SizedBox(height: 48.h),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 32.w,
vertical: 32.h,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(241, 245, 249, 1),
borderRadius: BorderRadius.circular(32.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'设备ID',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(148, 163, 184, 1),
),
),
Text(
'dev-${node.id.padLeft(3, '0')}',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
color: const Color.fromRGBO(15, 23, 42, 1),
),
),
],
),
SizedBox(height: 32.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'连接方式',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(148, 163, 184, 1),
),
),
Text(
'自动检测',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
color: const Color.fromRGBO(15, 23, 42, 1),
),
),
],
),
],
),
),
SizedBox(height: 48.h),
SizedBox(
width: double.infinity,
height: 104.h,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromRGBO(59, 130, 246, 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40.r),
),
),
child: Text(
'查看详情',
style: TextStyle(
fontSize: 40.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
],
),
),
),
),
],
),
);
}
IconData _getDeviceIcon(String name) {
if (name.contains('环境') || name.contains('温湿')) {
return Icons.thermostat;
} else if (name.contains('人体')) {
return Icons.person_search;
} else if (name.contains('空开')) {
return Icons.power;
} else if (name.contains('网关')) {
return Icons.router;
} else if (name.contains('WIFI')) {
return Icons.wifi;
} else if (name.contains('串口')) {
return Icons.usb;
}
return Icons.devices;
}
String _getDeviceFullName(String name) {
if (name == '环境采集') {
return '温湿度传感器';
}
return name;
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class DeviceHeaderWidget extends StatelessWidget {
final String deviceName;
final String deviceModel;
const DeviceHeaderWidget({
super.key,
required this.deviceName,
required this.deviceModel,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 32.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24.r),
topRight: Radius.circular(24.r),
),
),
child: Row(
children: [
Container(
width: 64.w,
height: 64.h,
decoration: BoxDecoration(
color: const Color.fromRGBO(219, 234, 254, 1),
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(
Icons.power,
size: 50.sp,
color: const Color.fromRGBO(59, 130, 246, 1),
),
),
SizedBox(width: 24.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
deviceName,
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.w600,
color: const Color.fromRGBO(15, 23, 42, 1),
),
),
SizedBox(height: 12.h),
Text(
deviceModel,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(100, 116, 139, 1),
),
),
],
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../cubit/inspection_topology_state.dart';
class NetworkTopologyWidget extends StatelessWidget {
final List<TopologyNode> topologyNodes;
final int deviceCount;
final int connectionCount;
final Function(TopologyNode) onNodeTap;
const NetworkTopologyWidget({
super.key,
required this.topologyNodes,
required this.deviceCount,
required this.connectionCount,
required this.onNodeTap,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 32.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24.r),
bottomRight: Radius.circular(24.r),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'网络拓扑',
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.w600,
color: const Color.fromRGBO(15, 23, 42, 1),
),
),
Text(
'共$deviceCount个设备,$connectionCount条连接',
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(148, 163, 184, 1),
),
),
],
),
SizedBox(height: 32.h),
SizedBox(
width: double.infinity,
height: 800.h,
child: ClipRRect(
borderRadius: BorderRadius.circular(32.r),
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
color: const Color.fromRGBO(241, 245, 249, 1),
),
if (topologyNodes.isNotEmpty)
CustomPaint(
size: Size.infinite,
painter: TopologyPainter(nodes: topologyNodes),
),
if (topologyNodes.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
final List<Widget> clickableNodes = [];
_collectNodes(
topologyNodes[0],
clickableNodes,
constraints.maxWidth,
constraints.maxHeight,
);
return Stack(children: clickableNodes);
},
),
],
),
),
),
],
),
);
}
void _collectNodes(
TopologyNode node,
List<Widget> nodes,
double maxWidth,
double maxHeight,
) {
final centerX = maxWidth * node.x;
final centerY = maxHeight * node.y;
final nodeWidth = 150.w;
// final nodeHeight = node.name.length > 4 ? 160.h : 100.h;
final nodeHeight = 80.h;
nodes.add(
Positioned(
left: centerX - nodeWidth / 2,
top: centerY - nodeHeight / 2,
width: nodeWidth,
height: nodeHeight,
child: GestureDetector(
onTap: () => onNodeTap(node),
child: Container(
decoration: BoxDecoration(
color: const Color.fromRGBO(239, 246, 255, 1),
border: Border.all(
color: const Color.fromRGBO(59, 130, 246, 1),
width: 1.w,
),
borderRadius: BorderRadius.circular(60.r),
),
child: Center(
child: Text(
node.name,
style: TextStyle(
color: const Color.fromRGBO(71, 85, 105, 1),
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
),
),
),
);
for (var child in node.children) {
_collectNodes(child, nodes, maxWidth, maxHeight);
}
}
}
class TopologyPainter extends CustomPainter {
final List<TopologyNode> nodes;
TopologyPainter({required this.nodes});
@override
void paint(Canvas canvas, Size size) {
if (nodes.isEmpty) return;
final paint = Paint()
..color = const Color.fromRGBO(203, 213, 225, 1)
..strokeWidth = 3.w
..style = PaintingStyle.stroke;
_drawConnections(canvas, size, nodes[0], paint);
}
void _drawConnections(Canvas canvas, Size size, TopologyNode node, Paint paint) {
final centerX = size.width * node.x;
final centerY = size.height * node.y;
for (var child in node.children) {
final childCenterX = size.width * child.x;
final childCenterY = size.height * child.y;
canvas.drawLine(
Offset(centerX, centerY),
Offset(childCenterX, childCenterY),
paint,
);
_drawConnections(canvas, size, child, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class StatusLegendWidget extends StatelessWidget {
const StatusLegendWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'状态图例',
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.w600,
color: const Color.fromRGBO(15, 23, 42, 1),
),
),
SizedBox(height: 24.h),
Wrap(
spacing: 32.w,
runSpacing: 20.h,
children: [
_buildLegendItem(const Color.fromRGBO(59, 130, 246, 1), '开启'),
_buildLegendItem(const Color.fromRGBO(226, 232, 240, 1), '关闭'),
_buildLegendItem(const Color.fromRGBO(16, 185, 129, 1), '在线'),
_buildLegendItem(const Color.fromRGBO(71, 85, 105, 1), '离线'),
_buildLegendItem(const Color.fromRGBO(251, 191, 36, 1), '告警'),
_buildLegendItem(const Color.fromRGBO(239, 68, 68, 1), '故障'),
],
),
],
),
);
}
Widget _buildLegendItem(Color color, String label) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32.w,
height: 32.h,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
SizedBox(width: 12.w),
Text(
label,
style: TextStyle(
fontSize: 24.sp,
color: const Color.fromRGBO(71, 85, 105, 1),
),
),
],
);
}
}
......@@ -19,12 +19,14 @@ class ServiceIndexView extends StatelessWidget {
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: Container(
// color: const Color.fromRGBO(242, 243, 245, 1),
color: Color.fromRGBO(242, 243, 245, 1.0),
width: double.infinity,
color: const Color.fromRGBO(242, 243, 245, 1),
padding: EdgeInsets.symmetric(horizontal: 28.w),
child: BlocBuilder<ServiceIndexCubit, ServiceIndexState>(
builder: (context, state) {
return Column(
......
......@@ -6,48 +6,58 @@ class ServiceHeadBlock extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 150.h,
padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight, left: 20.w, right: 20.w),
decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(20.r),
color: Colors.white,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'客房服务看板',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(0, 0, 0, 1),
fontFamily: 'FZLTXH',
),
final screenWidth = MediaQuery.of(context).size.width;
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: -28.w,
right: -28.w,
top: 0,
child: Container(
height: 150.h,
color: Colors.white,
),
Container(
width: 64.w,
height: 64.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color.fromRGBO(73, 149, 234, 1),
width: 1.w,
),
Container(
height: 150.h,
padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight, left: 20.w, right: 20.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'客房服务看板',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: const Color.fromRGBO(0, 0, 0, 1),
fontFamily: 'FZLTXH',
),
),
),
child: CircleAvatar(
radius: 40.w,
backgroundColor: Colors.white,
child: Icon(
Icons.person,
color: const Color.fromRGBO(73, 149, 234, 1),
size: 48.sp,
Container(
width: 64.w,
height: 64.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color.fromRGBO(73, 149, 234, 1),
width: 1.w,
),
),
child: CircleAvatar(
radius: 40.w,
backgroundColor: Colors.white,
child: Icon(
Icons.person,
color: const Color.fromRGBO(73, 149, 234, 1),
size: 48.sp,
),
),
),
),
],
),
],
),
),
],
);
}
}
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