import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../services/water_service.dart'; /// 实时监测列表页面 class MonitorPage extends StatefulWidget { const MonitorPage({super.key}); @override State createState() => _MonitorPageState(); } class _MonitorPageState extends State { final WaterService _service = WaterService.instance; List _items = []; bool _isLoading = true; String? _error; MonitorType? _filterType; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _isLoading = true; _error = null; }); try { final data = await _service.getMonitorList(); if (mounted) { setState(() { _items = data; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _error = e.toString(); _isLoading = false; }); } } } List get _filteredItems { if (_filterType == null) return _items; return _items.where((item) => item.type == _filterType).toList(); } int _countByStatus(DeviceStatus status) { return _items.where((item) => item.status == status).length; } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('实时监测'), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _isLoading ? null : _loadData, ), ], ), body: Column( children: [ // 状态概览栏 if (!_isLoading && _error == null) _buildStatusBar(theme), // 类型筛选 if (!_isLoading && _error == null) _buildFilterBar(theme), // 列表内容 Expanded( child: _buildBody(theme), ), ], ), ); } Widget _buildStatusBar(ThemeData theme) { final online = _countByStatus(DeviceStatus.online); final offline = _countByStatus(DeviceStatus.offline); final warning = _countByStatus(DeviceStatus.warning); return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), color: theme.colorScheme.surfaceContainerHighest.withAlpha(100), child: Row( children: [ _StatusBadge(label: '在线', count: online, color: Colors.green), const SizedBox(width: 16), _StatusBadge(label: '离线', count: offline, color: Colors.grey), const SizedBox(width: 16), _StatusBadge(label: '告警', count: warning, color: Colors.orange), const Spacer(), Text( '共 ${_items.length} 个监测点', style: theme.textTheme.bodySmall, ), ], ), ); } Widget _buildFilterBar(ThemeData theme) { return SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ _FilterChip( label: '全部', selected: _filterType == null, onTap: () => setState(() => _filterType = null), ), const SizedBox(width: 8), ...MonitorType.values.map((type) { return Padding( padding: const EdgeInsets.only(right: 8), child: _FilterChip( label: type.label, selected: _filterType == type, onTap: () => setState(() => _filterType = type), ), ); }), ], ), ); } Widget _buildBody(ThemeData theme) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, size: 48, color: theme.colorScheme.error), const SizedBox(height: 16), Text('加载失败', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text(_error!, style: theme.textTheme.bodySmall), const SizedBox(height: 16), FilledButton.icon( onPressed: _loadData, icon: const Icon(Icons.refresh), label: const Text('重试'), ), ], ), ); } final items = _filteredItems; if (items.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.sensors_off, size: 48, color: Colors.grey.shade400), const SizedBox(height: 16), Text('暂无监测数据', style: theme.textTheme.titleMedium), ], ), ); } return RefreshIndicator( onRefresh: _loadData, child: ListView.builder( padding: const EdgeInsets.all(12), itemCount: items.length, itemBuilder: (context, index) => _MonitorCard(item: items[index]), ), ); } } /// 监测点卡片 class _MonitorCard extends StatelessWidget { final MonitorItem item; const _MonitorCard({required this.item}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final statusColor = Color(item.status.color); final timeFormat = DateFormat('HH:mm:ss'); return Card( margin: const EdgeInsets.only(bottom: 10), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: theme.colorScheme.outlineVariant.withAlpha(100)), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: statusColor, ), ), const SizedBox(width: 8), Expanded( child: Text( item.name, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: statusColor.withAlpha(30), borderRadius: BorderRadius.circular(4), ), child: Text( item.type.label, style: TextStyle(fontSize: 11, color: statusColor), ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: statusColor.withAlpha(30), borderRadius: BorderRadius.circular(4), ), child: Text( item.statusLabel, style: TextStyle(fontSize: 11, color: statusColor), ), ), ], ), const SizedBox(height: 12), // 数据行 if (item.status != DeviceStatus.offline) ...[ Wrap( spacing: 16, runSpacing: 8, children: [ if (item.flow != null) _DataChip(icon: Icons.water_drop, label: '流量', value: '${item.flow!.toStringAsFixed(1)} m³/h'), if (item.pressure != null) _DataChip(icon: Icons.speed, label: '压力', value: '${item.pressure!.toStringAsFixed(2)} MPa'), if (item.level != null) _DataChip(icon: Icons.straighten, label: '液位', value: '${item.level!.toStringAsFixed(1)} m'), if (item.ph != null) _DataChip(icon: Icons.science, label: 'pH', value: item.ph!.toStringAsFixed(1)), if (item.turbidity != null) _DataChip(icon: Icons.blur_on, label: '浊度', value: '${item.turbidity!.toStringAsFixed(1)} NTU'), if (item.chlorine != null) _DataChip(icon: Icons.bubble_chart, label: '余氯', value: '${item.chlorine!.toStringAsFixed(2)} mg/L'), ], ), const SizedBox(height: 8), ] else Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( '设备离线,暂无数据', style: TextStyle(color: Colors.grey.shade500, fontSize: 13), ), ), // 更新时间 Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Icon(Icons.access_time, size: 12, color: Colors.grey.shade500), const SizedBox(width: 4), Text( '更新于 ${timeFormat.format(item.updateTime)}', style: TextStyle(fontSize: 11, color: Colors.grey.shade500), ), ], ), ], ), ), ); } } class _DataChip extends StatelessWidget { final IconData icon; final String label; final String value; const _DataChip({required this.icon, required this.label, required this.value}); @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: Colors.grey.shade600), const SizedBox(width: 4), Text( '$label: ', style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), Text( value, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), ], ); } } class _StatusBadge extends StatelessWidget { final String label; final int count; final Color color; const _StatusBadge({required this.label, required this.count, required this.color}); @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: BoxDecoration(shape: BoxShape.circle, color: color), ), const SizedBox(width: 4), Text('$label $count', style: const TextStyle(fontSize: 13)), ], ); } } class _FilterChip extends StatelessWidget { final String label; final bool selected; final VoidCallback onTap; const _FilterChip({required this.label, required this.selected, required this.onTap}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), decoration: BoxDecoration( color: selected ? theme.colorScheme.primaryContainer : Colors.grey.shade100, borderRadius: BorderRadius.circular(16), border: selected ? Border.all(color: theme.colorScheme.primary, width: 1) : null, ), child: Text( label, style: TextStyle( fontSize: 13, color: selected ? theme.colorScheme.primary : Colors.grey.shade700, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, ), ), ), ); } }