智慧水务管理系统 - 精河县供水工程综合管理平台

monitor_page.dart 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import 'package:flutter/material.dart';
  2. import 'package:intl/intl.dart';
  3. import '../../services/water_service.dart';
  4. /// 实时监测列表页面
  5. class MonitorPage extends StatefulWidget {
  6. const MonitorPage({super.key});
  7. @override
  8. State<MonitorPage> createState() => _MonitorPageState();
  9. }
  10. class _MonitorPageState extends State<MonitorPage> {
  11. final WaterService _service = WaterService.instance;
  12. List<MonitorItem> _items = [];
  13. bool _isLoading = true;
  14. String? _error;
  15. MonitorType? _filterType;
  16. @override
  17. void initState() {
  18. super.initState();
  19. _loadData();
  20. }
  21. Future<void> _loadData() async {
  22. setState(() {
  23. _isLoading = true;
  24. _error = null;
  25. });
  26. try {
  27. final data = await _service.getMonitorList();
  28. if (mounted) {
  29. setState(() {
  30. _items = data;
  31. _isLoading = false;
  32. });
  33. }
  34. } catch (e) {
  35. if (mounted) {
  36. setState(() {
  37. _error = e.toString();
  38. _isLoading = false;
  39. });
  40. }
  41. }
  42. }
  43. List<MonitorItem> get _filteredItems {
  44. if (_filterType == null) return _items;
  45. return _items.where((item) => item.type == _filterType).toList();
  46. }
  47. int _countByStatus(DeviceStatus status) {
  48. return _items.where((item) => item.status == status).length;
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. final theme = Theme.of(context);
  53. return Scaffold(
  54. appBar: AppBar(
  55. title: const Text('实时监测'),
  56. centerTitle: true,
  57. actions: [
  58. IconButton(
  59. icon: const Icon(Icons.refresh),
  60. onPressed: _isLoading ? null : _loadData,
  61. ),
  62. ],
  63. ),
  64. body: Column(
  65. children: [
  66. // 状态概览栏
  67. if (!_isLoading && _error == null) _buildStatusBar(theme),
  68. // 类型筛选
  69. if (!_isLoading && _error == null) _buildFilterBar(theme),
  70. // 列表内容
  71. Expanded(
  72. child: _buildBody(theme),
  73. ),
  74. ],
  75. ),
  76. );
  77. }
  78. Widget _buildStatusBar(ThemeData theme) {
  79. final online = _countByStatus(DeviceStatus.online);
  80. final offline = _countByStatus(DeviceStatus.offline);
  81. final warning = _countByStatus(DeviceStatus.warning);
  82. return Container(
  83. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  84. color: theme.colorScheme.surfaceContainerHighest.withAlpha(100),
  85. child: Row(
  86. children: [
  87. _StatusBadge(label: '在线', count: online, color: Colors.green),
  88. const SizedBox(width: 16),
  89. _StatusBadge(label: '离线', count: offline, color: Colors.grey),
  90. const SizedBox(width: 16),
  91. _StatusBadge(label: '告警', count: warning, color: Colors.orange),
  92. const Spacer(),
  93. Text(
  94. '共 ${_items.length} 个监测点',
  95. style: theme.textTheme.bodySmall,
  96. ),
  97. ],
  98. ),
  99. );
  100. }
  101. Widget _buildFilterBar(ThemeData theme) {
  102. return SingleChildScrollView(
  103. scrollDirection: Axis.horizontal,
  104. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  105. child: Row(
  106. children: [
  107. _FilterChip(
  108. label: '全部',
  109. selected: _filterType == null,
  110. onTap: () => setState(() => _filterType = null),
  111. ),
  112. const SizedBox(width: 8),
  113. ...MonitorType.values.map((type) {
  114. return Padding(
  115. padding: const EdgeInsets.only(right: 8),
  116. child: _FilterChip(
  117. label: type.label,
  118. selected: _filterType == type,
  119. onTap: () => setState(() => _filterType = type),
  120. ),
  121. );
  122. }),
  123. ],
  124. ),
  125. );
  126. }
  127. Widget _buildBody(ThemeData theme) {
  128. if (_isLoading) {
  129. return const Center(child: CircularProgressIndicator());
  130. }
  131. if (_error != null) {
  132. return Center(
  133. child: Column(
  134. mainAxisSize: MainAxisSize.min,
  135. children: [
  136. Icon(Icons.error_outline, size: 48, color: theme.colorScheme.error),
  137. const SizedBox(height: 16),
  138. Text('加载失败', style: theme.textTheme.titleMedium),
  139. const SizedBox(height: 8),
  140. Text(_error!, style: theme.textTheme.bodySmall),
  141. const SizedBox(height: 16),
  142. FilledButton.icon(
  143. onPressed: _loadData,
  144. icon: const Icon(Icons.refresh),
  145. label: const Text('重试'),
  146. ),
  147. ],
  148. ),
  149. );
  150. }
  151. final items = _filteredItems;
  152. if (items.isEmpty) {
  153. return Center(
  154. child: Column(
  155. mainAxisSize: MainAxisSize.min,
  156. children: [
  157. Icon(Icons.sensors_off, size: 48, color: Colors.grey.shade400),
  158. const SizedBox(height: 16),
  159. Text('暂无监测数据', style: theme.textTheme.titleMedium),
  160. ],
  161. ),
  162. );
  163. }
  164. return RefreshIndicator(
  165. onRefresh: _loadData,
  166. child: ListView.builder(
  167. padding: const EdgeInsets.all(12),
  168. itemCount: items.length,
  169. itemBuilder: (context, index) => _MonitorCard(item: items[index]),
  170. ),
  171. );
  172. }
  173. }
  174. /// 监测点卡片
  175. class _MonitorCard extends StatelessWidget {
  176. final MonitorItem item;
  177. const _MonitorCard({required this.item});
  178. @override
  179. Widget build(BuildContext context) {
  180. final theme = Theme.of(context);
  181. final statusColor = Color(item.status.color);
  182. final timeFormat = DateFormat('HH:mm:ss');
  183. return Card(
  184. margin: const EdgeInsets.only(bottom: 10),
  185. elevation: 0,
  186. shape: RoundedRectangleBorder(
  187. borderRadius: BorderRadius.circular(12),
  188. side: BorderSide(color: theme.colorScheme.outlineVariant.withAlpha(100)),
  189. ),
  190. child: Padding(
  191. padding: const EdgeInsets.all(16),
  192. child: Column(
  193. crossAxisAlignment: CrossAxisAlignment.start,
  194. children: [
  195. // 标题行
  196. Row(
  197. children: [
  198. Container(
  199. width: 8,
  200. height: 8,
  201. decoration: BoxDecoration(
  202. shape: BoxShape.circle,
  203. color: statusColor,
  204. ),
  205. ),
  206. const SizedBox(width: 8),
  207. Expanded(
  208. child: Text(
  209. item.name,
  210. style: theme.textTheme.titleMedium?.copyWith(
  211. fontWeight: FontWeight.w600,
  212. ),
  213. ),
  214. ),
  215. Container(
  216. padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  217. decoration: BoxDecoration(
  218. color: statusColor.withAlpha(30),
  219. borderRadius: BorderRadius.circular(4),
  220. ),
  221. child: Text(
  222. item.type.label,
  223. style: TextStyle(fontSize: 11, color: statusColor),
  224. ),
  225. ),
  226. const SizedBox(width: 8),
  227. Container(
  228. padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  229. decoration: BoxDecoration(
  230. color: statusColor.withAlpha(30),
  231. borderRadius: BorderRadius.circular(4),
  232. ),
  233. child: Text(
  234. item.statusLabel,
  235. style: TextStyle(fontSize: 11, color: statusColor),
  236. ),
  237. ),
  238. ],
  239. ),
  240. const SizedBox(height: 12),
  241. // 数据行
  242. if (item.status != DeviceStatus.offline) ...[
  243. Wrap(
  244. spacing: 16,
  245. runSpacing: 8,
  246. children: [
  247. if (item.flow != null)
  248. _DataChip(icon: Icons.water_drop, label: '流量', value: '${item.flow!.toStringAsFixed(1)} m³/h'),
  249. if (item.pressure != null)
  250. _DataChip(icon: Icons.speed, label: '压力', value: '${item.pressure!.toStringAsFixed(2)} MPa'),
  251. if (item.level != null)
  252. _DataChip(icon: Icons.straighten, label: '液位', value: '${item.level!.toStringAsFixed(1)} m'),
  253. if (item.ph != null)
  254. _DataChip(icon: Icons.science, label: 'pH', value: item.ph!.toStringAsFixed(1)),
  255. if (item.turbidity != null)
  256. _DataChip(icon: Icons.blur_on, label: '浊度', value: '${item.turbidity!.toStringAsFixed(1)} NTU'),
  257. if (item.chlorine != null)
  258. _DataChip(icon: Icons.bubble_chart, label: '余氯', value: '${item.chlorine!.toStringAsFixed(2)} mg/L'),
  259. ],
  260. ),
  261. const SizedBox(height: 8),
  262. ] else
  263. Padding(
  264. padding: const EdgeInsets.symmetric(vertical: 8),
  265. child: Text(
  266. '设备离线,暂无数据',
  267. style: TextStyle(color: Colors.grey.shade500, fontSize: 13),
  268. ),
  269. ),
  270. // 更新时间
  271. Row(
  272. mainAxisAlignment: MainAxisAlignment.end,
  273. children: [
  274. Icon(Icons.access_time, size: 12, color: Colors.grey.shade500),
  275. const SizedBox(width: 4),
  276. Text(
  277. '更新于 ${timeFormat.format(item.updateTime)}',
  278. style: TextStyle(fontSize: 11, color: Colors.grey.shade500),
  279. ),
  280. ],
  281. ),
  282. ],
  283. ),
  284. ),
  285. );
  286. }
  287. }
  288. class _DataChip extends StatelessWidget {
  289. final IconData icon;
  290. final String label;
  291. final String value;
  292. const _DataChip({required this.icon, required this.label, required this.value});
  293. @override
  294. Widget build(BuildContext context) {
  295. return Row(
  296. mainAxisSize: MainAxisSize.min,
  297. children: [
  298. Icon(icon, size: 14, color: Colors.grey.shade600),
  299. const SizedBox(width: 4),
  300. Text(
  301. '$label: ',
  302. style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
  303. ),
  304. Text(
  305. value,
  306. style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
  307. ),
  308. ],
  309. );
  310. }
  311. }
  312. class _StatusBadge extends StatelessWidget {
  313. final String label;
  314. final int count;
  315. final Color color;
  316. const _StatusBadge({required this.label, required this.count, required this.color});
  317. @override
  318. Widget build(BuildContext context) {
  319. return Row(
  320. mainAxisSize: MainAxisSize.min,
  321. children: [
  322. Container(
  323. width: 8,
  324. height: 8,
  325. decoration: BoxDecoration(shape: BoxShape.circle, color: color),
  326. ),
  327. const SizedBox(width: 4),
  328. Text('$label $count', style: const TextStyle(fontSize: 13)),
  329. ],
  330. );
  331. }
  332. }
  333. class _FilterChip extends StatelessWidget {
  334. final String label;
  335. final bool selected;
  336. final VoidCallback onTap;
  337. const _FilterChip({required this.label, required this.selected, required this.onTap});
  338. @override
  339. Widget build(BuildContext context) {
  340. final theme = Theme.of(context);
  341. return InkWell(
  342. onTap: onTap,
  343. borderRadius: BorderRadius.circular(16),
  344. child: Container(
  345. padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
  346. decoration: BoxDecoration(
  347. color: selected ? theme.colorScheme.primaryContainer : Colors.grey.shade100,
  348. borderRadius: BorderRadius.circular(16),
  349. border: selected
  350. ? Border.all(color: theme.colorScheme.primary, width: 1)
  351. : null,
  352. ),
  353. child: Text(
  354. label,
  355. style: TextStyle(
  356. fontSize: 13,
  357. color: selected ? theme.colorScheme.primary : Colors.grey.shade700,
  358. fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
  359. ),
  360. ),
  361. ),
  362. );
  363. }
  364. }