import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../services/water_service.dart'; /// 报警推送列表页面 class AlertPage extends StatefulWidget { const AlertPage({super.key}); @override State createState() => _AlertPageState(); } class _AlertPageState extends State { final WaterService _service = WaterService.instance; List _alerts = []; bool _isLoading = true; String? _error; bool _unreadOnly = false; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _isLoading = true; _error = null; }); try { final data = await _service.getAlertList(unreadOnly: _unreadOnly); if (mounted) { setState(() { _alerts = data; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _error = e.toString(); _isLoading = false; }); } } } int get _unreadCount => _alerts.where((a) => !a.isRead).length; Future _markRead(AlertItem alert) async { if (alert.isRead) return; await _service.markAlertRead(alert.id); if (mounted) { setState(() => alert.isRead = true); } } Future _markAllRead() async { await _service.markAllAlertsRead(); if (mounted) { setState(() { for (final a in _alerts) { a.isRead = true; } }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('已全部标记为已读')), ); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('报警推送'), centerTitle: true, actions: [ if (_unreadCount > 0) TextButton( onPressed: _markAllRead, child: Text('全部已读 ($_unreadCount)'), ), IconButton( icon: const Icon(Icons.refresh), onPressed: _isLoading ? null : _loadData, ), ], ), body: Column( children: [ // 筛选栏 _buildFilterBar(theme), // 列表 Expanded(child: _buildBody(theme)), ], ), ); } Widget _buildFilterBar(ThemeData theme) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ _FilterTab(label: '全部', selected: !_unreadOnly, onTap: () { if (_unreadOnly) { setState(() => _unreadOnly = false); _loadData(); } }), const SizedBox(width: 12), _FilterTab( label: '未读', selected: _unreadOnly, badge: _unreadCount, onTap: () { if (!_unreadOnly) { setState(() => _unreadOnly = true); _loadData(); } }, ), ], ), ); } 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: 16), FilledButton.icon( onPressed: _loadData, icon: const Icon(Icons.refresh), label: const Text('重试'), ), ], ), ); } if (_alerts.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.notifications_none, size: 48, color: Colors.grey.shade400), const SizedBox(height: 16), Text( _unreadOnly ? '没有未读报警' : '暂无报警信息', style: theme.textTheme.titleMedium, ), ], ), ); } return RefreshIndicator( onRefresh: _loadData, child: ListView.builder( padding: const EdgeInsets.all(12), itemCount: _alerts.length, itemBuilder: (context, index) { final alert = _alerts[index]; return _AlertCard( alert: alert, onTap: () => _showAlertDetail(context, alert), ); }, ), ); } void _showAlertDetail(BuildContext context, AlertItem alert) { _markRead(alert); showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) => _AlertDetailSheet(alert: alert), ); } } /// 报警卡片 class _AlertCard extends StatelessWidget { final AlertItem alert; final VoidCallback onTap; const _AlertCard({required this.alert, required this.onTap}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final levelColor = Color(alert.level.color); final dateFormat = DateFormat('MM-dd HH:mm'); return Card( margin: const EdgeInsets.only(bottom: 8), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: alert.isRead ? Colors.grey.shade200 : levelColor.withAlpha(100), ), ), color: alert.isRead ? null : levelColor.withAlpha(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(14), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 级别图标 Container( width: 40, height: 40, decoration: BoxDecoration( color: levelColor.withAlpha(30), shape: BoxShape.circle, ), child: Icon( alert.level == AlertLevel.critical ? Icons.error : alert.level == AlertLevel.warning ? Icons.warning_amber : Icons.info_outline, color: levelColor, size: 22, ), ), const SizedBox(width: 12), // 内容 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: levelColor.withAlpha(30), borderRadius: BorderRadius.circular(4), ), child: Text( alert.level.label, style: TextStyle(fontSize: 10, color: levelColor, fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), Expanded( child: Text( alert.title, style: theme.textTheme.titleSmall?.copyWith( fontWeight: alert.isRead ? FontWeight.normal : FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 6), Text( alert.content, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Row( children: [ Icon(Icons.location_on_outlined, size: 12, color: Colors.grey.shade500), const SizedBox(width: 2), Text(alert.location, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)), const SizedBox(width: 12), Icon(Icons.access_time, size: 12, color: Colors.grey.shade500), const SizedBox(width: 2), Text( dateFormat.format(alert.createTime), style: TextStyle(fontSize: 11, color: Colors.grey.shade500), ), ], ), ], ), ), // 未读标记 if (!alert.isRead) Container( width: 8, height: 8, margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( shape: BoxShape.circle, color: levelColor, ), ), ], ), ), ), ); } } /// 报警详情底部弹窗 class _AlertDetailSheet extends StatelessWidget { final AlertItem alert; const _AlertDetailSheet({required this.alert}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final levelColor = Color(alert.level.color); final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss'); return DraggableScrollableSheet( initialChildSize: 0.65, minChildSize: 0.4, maxChildSize: 0.85, expand: false, builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 拖拽指示器 Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 20), // 级别标签 Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: levelColor.withAlpha(30), borderRadius: BorderRadius.circular(6), ), child: Text( '${alert.level.label}报警', style: TextStyle(color: levelColor, fontWeight: FontWeight.w600), ), ), const SizedBox(height: 12), // 标题 Text( alert.title, style: theme.textTheme.titleLarge, ), const SizedBox(height: 16), // 详情信息 _DetailRow(icon: Icons.location_on, label: '位置', value: alert.location), _DetailRow(icon: Icons.devices, label: '设备', value: alert.deviceName), _DetailRow(icon: Icons.access_time, label: '时间', value: dateFormat.format(alert.createTime)), _DetailRow(icon: Icons.person, label: '处理人', value: alert.handlerName), const Divider(height: 24), // 内容 Text('详细描述', style: theme.textTheme.titleSmall), const SizedBox(height: 8), Text( alert.content, style: theme.textTheme.bodyMedium?.copyWith(height: 1.6), ), const SizedBox(height: 24), // 操作按钮 Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.phone), label: const Text('联系处理人'), ), ), const SizedBox(width: 12), Expanded( child: FilledButton.icon( onPressed: () { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('已派单处理')), ); }, icon: const Icon(Icons.assignment), label: const Text('派单处理'), ), ), ], ), const SizedBox(height: 20), ], ), ); }, ); } } class _DetailRow extends StatelessWidget { final IconData icon; final String label; final String value; const _DetailRow({required this.icon, required this.label, required this.value}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( children: [ Icon(icon, size: 16, color: Colors.grey.shade600), const SizedBox(width: 8), SizedBox( width: 60, child: Text(label, style: TextStyle(fontSize: 13, color: Colors.grey.shade600)), ), Expanded( child: Text(value, style: const TextStyle(fontSize: 13)), ), ], ), ); } } class _FilterTab extends StatelessWidget { final String label; final bool selected; final VoidCallback onTap; final int? badge; const _FilterTab({required this.label, required this.selected, required this.onTap, this.badge}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: selected ? theme.colorScheme.primaryContainer : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: TextStyle( fontSize: 13, color: selected ? theme.colorScheme.primary : Colors.grey.shade700, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, ), ), if (badge != null && badge! > 0) ...[ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(8), ), child: Text( '$badge', style: const TextStyle(fontSize: 10, color: Colors.white), ), ), ], ], ), ), ); } }