import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/services/notification_service.dart'; import '../../providers/notification_providers.dart'; import '../../providers/user_providers.dart'; /// 通知收件箱页面 /// /// 展示当前用户的所有通知列表,支持: /// - 下拉刷新 /// - 点击查看详情并自动标记已读 /// - 右上角"全部已读"按钮 /// - 空状态 / 错误状态 / 加载状态 /// /// 适配 2.0 mining-app 的 dark/light 主题。 class NotificationInboxPage extends ConsumerStatefulWidget { const NotificationInboxPage({super.key}); @override ConsumerState createState() => _NotificationInboxPageState(); } class _NotificationInboxPageState extends ConsumerState { /// 通知列表 List _notifications = []; /// 未读数量 int _unreadCount = 0; /// 是否正在加载 bool _isLoading = true; /// 错误信息 String? _error; // ── 品牌色 ── static const Color _orange = Color(0xFFFF6B00); @override void initState() { super.initState(); _loadNotifications(); } /// 从后端加载通知列表 Future _loadNotifications() async { setState(() { _isLoading = true; _error = null; }); try { final accountSequence = ref.read(currentAccountSequenceProvider); if (accountSequence == null || accountSequence.isEmpty) { setState(() { _error = '用户未登录'; _isLoading = false; }); return; } final notificationService = ref.read(notificationServiceProvider); final response = await notificationService.getNotifications( userSerialNum: accountSequence, ); if (mounted) { setState(() { _notifications = response.notifications; _unreadCount = response.unreadCount; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _error = '加载通知失败'; _isLoading = false; }); } } } /// 标记单条通知为已读 Future _markAsRead(NotificationItem notification) async { if (notification.isRead) return; final accountSequence = ref.read(currentAccountSequenceProvider); if (accountSequence == null || accountSequence.isEmpty) return; final notificationService = ref.read(notificationServiceProvider); final success = await notificationService.markAsRead( userSerialNum: accountSequence, notificationId: notification.id, ); if (success && mounted) { setState(() { final index = _notifications.indexWhere((n) => n.id == notification.id); if (index != -1) { _notifications[index] = NotificationItem( id: notification.id, title: notification.title, content: notification.content, type: notification.type, priority: notification.priority, imageUrl: notification.imageUrl, linkUrl: notification.linkUrl, publishedAt: notification.publishedAt, isRead: true, readAt: DateTime.now(), requiresForceRead: notification.requiresForceRead, ); _unreadCount = (_unreadCount - 1).clamp(0, _unreadCount); } }); // 同步更新全局未读角标 ref.read(notificationBadgeProvider.notifier).decrementCount(); } } /// 标记所有通知为已读 Future _markAllAsRead() async { if (_unreadCount == 0) return; final accountSequence = ref.read(currentAccountSequenceProvider); if (accountSequence == null || accountSequence.isEmpty) return; final notificationService = ref.read(notificationServiceProvider); final success = await notificationService.markAllAsRead( userSerialNum: accountSequence, ); if (success && mounted) { setState(() { _notifications = _notifications.map((n) { if (!n.isRead) { return NotificationItem( id: n.id, title: n.title, content: n.content, type: n.type, priority: n.priority, imageUrl: n.imageUrl, linkUrl: n.linkUrl, publishedAt: n.publishedAt, isRead: true, readAt: DateTime.now(), requiresForceRead: n.requiresForceRead, ); } return n; }).toList(); _unreadCount = 0; }); // 同步清空全局未读角标 ref.read(notificationBadgeProvider.notifier).clearCount(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('已全部标记为已读'), backgroundColor: _orange, ), ); } } } /// 显示通知详情弹窗 void _showNotificationDetail(NotificationItem notification) { // 先标记为已读 _markAsRead(notification); final isDark = Theme.of(context).brightness == Brightness.dark; showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: isDark ? const Color(0xFF1F2937) : Colors.white, title: Row( children: [ Text( notification.typeIcon, style: const TextStyle(fontSize: 20), ), const SizedBox(width: 8), Expanded( child: Text( notification.title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: isDark ? const Color(0xFFE5E7EB) : const Color(0xFF1F2937), ), ), ), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( notification.content, style: TextStyle( fontSize: 14, color: isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280), height: 1.5, ), ), const SizedBox(height: 16), Text( _formatTime(notification.publishedAt), style: TextStyle( fontSize: 12, color: isDark ? const Color(0xFF6B7280) : const Color(0xFFBDBDBD), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text( '关闭', style: TextStyle(color: _orange), ), ), ], ), ); } /// 格式化相对时间 String _formatTime(DateTime? dateTime) { if (dateTime == null) return ''; final now = DateTime.now(); final diff = now.difference(dateTime); if (diff.inMinutes < 1) { return '刚刚'; } else if (diff.inHours < 1) { return '${diff.inMinutes}分钟前'; } else if (diff.inDays < 1) { return '${diff.inHours}小时前'; } else if (diff.inDays < 7) { return '${diff.inDays}天前'; } else { return '${dateTime.month}月${dateTime.day}日'; } } /// 获取类型颜色 Color _getTypeColor(NotificationType type) { switch (type) { case NotificationType.system: return const Color(0xFF6B7280); case NotificationType.activity: return _orange; case NotificationType.reward: return const Color(0xFFF59E0B); case NotificationType.upgrade: return const Color(0xFF10B981); case NotificationType.announcement: return const Color(0xFF3B82F6); } } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: isDark ? const Color(0xFF111827) : const Color(0xFFF3F4F6), body: SafeArea( child: Column( children: [ _buildAppBar(isDark), Expanded( child: _isLoading ? const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(_orange), ), ) : _error != null ? _buildErrorView(isDark) : _notifications.isEmpty ? _buildEmptyView(isDark) : _buildNotificationList(isDark), ), ], ), ), ); } /// 自定义 AppBar Widget _buildAppBar(bool isDark) { return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), color: isDark ? const Color(0xFF1F2937) : Colors.white, child: Row( children: [ GestureDetector( onTap: () => context.pop(), child: Container( width: 40, height: 40, alignment: Alignment.center, child: Icon( Icons.arrow_back, size: 24, color: isDark ? const Color(0xFFE5E7EB) : const Color(0xFF1F2937), ), ), ), const SizedBox(width: 8), Expanded( child: Text( '通知中心', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: isDark ? const Color(0xFFE5E7EB) : const Color(0xFF1F2937), ), ), ), // 全部已读按钮 if (_unreadCount > 0) GestureDetector( onTap: _markAllAsRead, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: _orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), child: const Text( '全部已读', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: _orange, ), ), ), ), ], ), ); } /// 空状态视图 Widget _buildEmptyView(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.notifications_none, size: 64, color: isDark ? Colors.grey.shade700 : Colors.grey.shade300, ), const SizedBox(height: 16), Text( '暂无通知', style: TextStyle( fontSize: 16, color: isDark ? Colors.grey.shade600 : Colors.grey.shade500, ), ), ], ), ); } /// 错误状态视图 Widget _buildErrorView(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 64, color: isDark ? Colors.grey.shade700 : Colors.grey.shade300, ), const SizedBox(height: 16), Text( _error ?? '加载失败', style: TextStyle( fontSize: 16, color: isDark ? Colors.grey.shade600 : Colors.grey.shade500, ), ), const SizedBox(height: 16), ElevatedButton( onPressed: _loadNotifications, style: ElevatedButton.styleFrom( backgroundColor: _orange, ), child: const Text('重试'), ), ], ), ); } /// 通知列表(支持下拉刷新) Widget _buildNotificationList(bool isDark) { return RefreshIndicator( onRefresh: _loadNotifications, color: _orange, child: ListView.separated( padding: const EdgeInsets.all(16), itemCount: _notifications.length, separatorBuilder: (context, index) => const SizedBox(height: 12), itemBuilder: (context, index) { final notification = _notifications[index]; return _buildNotificationCard(notification, isDark); }, ), ); } /// 单条通知卡片 Widget _buildNotificationCard(NotificationItem notification, bool isDark) { return GestureDetector( onTap: () => _showNotificationDetail(notification), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: notification.isRead ? (isDark ? const Color(0xFF1F2937) : Colors.white) : (isDark ? const Color(0xFF374151) : const Color(0xFFFFF3E0)), borderRadius: BorderRadius.circular(12), border: Border.all( color: notification.isRead ? (isDark ? const Color(0xFF374151) : const Color(0xFFE0E0E0)) : _orange.withValues(alpha: 0.3), width: 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.15 : 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 类型图标圆圈 Container( width: 40, height: 40, decoration: BoxDecoration( color: _getTypeColor(notification.type).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), ), child: Center( child: Text( notification.typeIcon, style: const TextStyle(fontSize: 20), ), ), ), const SizedBox(width: 12), // 内容区域 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // 未读红点 if (!notification.isRead) Container( width: 8, height: 8, margin: const EdgeInsets.only(right: 8), decoration: const BoxDecoration( color: _orange, shape: BoxShape.circle, ), ), // 标题 Expanded( child: Text( notification.title, style: TextStyle( fontSize: 15, fontWeight: notification.isRead ? FontWeight.w500 : FontWeight.w600, color: isDark ? const Color(0xFFE5E7EB) : const Color(0xFF1F2937), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 4), // 内容预览 Text( notification.content, style: TextStyle( fontSize: 13, color: notification.isRead ? (isDark ? const Color(0xFF6B7280) : const Color(0xFFBDBDBD)) : (isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280)), height: 1.4, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), // 类型标签 + 时间 Row( children: [ Text( notification.typeName, style: TextStyle( fontSize: 11, color: _getTypeColor(notification.type), fontWeight: FontWeight.w500, ), ), const SizedBox(width: 8), Text( _formatTime(notification.publishedAt), style: TextStyle( fontSize: 11, color: isDark ? const Color(0xFF6B7280) : const Color(0xFFBDBDBD), ), ), ], ), ], ), ), // 右箭头 Icon( Icons.chevron_right, size: 20, color: isDark ? const Color(0xFF6B7280) : const Color(0xFFBDBDBD), ), ], ), ), ); } }