import 'package:flutter/material.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_typography.dart'; import '../../../../app/i18n/app_localizations.dart'; import '../../../../shared/widgets/empty_state.dart'; import '../../../../core/services/notification_service.dart'; import '../../../../core/providers/notification_badge_manager.dart'; /// A8. 消息模块 — 通知 + 公告 /// /// Tab 0: 全部(用户通知,不过滤类型) /// Tab 1: 系统(type=SYSTEM) /// Tab 2: 公告(独立接口 /api/v1/announcements) /// Tab 3: 活动(type=ACTIVITY) /// /// 注意:通知和公告是两套独立的后端接口,标记已读/全部已读 /// 需要根据当前 Tab 调用对应的接口。 class MessagePage extends StatefulWidget { const MessagePage({super.key}); @override State createState() => _MessagePageState(); } class _MessagePageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final NotificationService _notificationService = NotificationService(); List _items = []; bool _isLoading = false; String? _error; // 当前是公告 Tab(index=2),需要使用公告 API bool get _isAnnouncementsTab => _tabController.index == 2; // 按 Tab index 对应的通知类型过滤(仅对通知 Tab 有效) static const _tabTypeFilter = { 0: null, 1: NotificationType.system, 2: null, // 公告 Tab,类型无意义 3: NotificationType.activity, }; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); _tabController.addListener(() { if (!_tabController.indexIsChanging) { _load(); } }); _load(); } @override void dispose() { _tabController.dispose(); super.dispose(); } // ── 数据加载 ────────────────────────────────────────────────────────────── Future _load() async { setState(() { _isLoading = true; _error = null; }); try { final List result; if (_isAnnouncementsTab) { // Tab 2:公告,使用独立的公告接口 final resp = await _notificationService.getAnnouncements(page: 1, limit: 50); result = resp.notifications; } else { // Tab 0/1/3:用户通知 final resp = await _notificationService.getNotifications( type: _tabTypeFilter[_tabController.index], page: 1, limit: 50, ); result = resp.notifications; } if (mounted) setState(() { _items = result; _isLoading = false; }); } catch (e) { if (mounted) setState(() { _error = e.toString(); _isLoading = false; }); } } // ── 操作 ────────────────────────────────────────────────────────────────── Future _markAllAsRead() async { final bool success; if (_isAnnouncementsTab) { success = await _notificationService.markAllAnnouncementsAsRead(); } else { // 通知全部已读(后端有 PUT /api/v1/notifications/read-all) success = await _notificationService.markAllNotificationsAsRead(); } if (success) { NotificationBadgeManager().clearCount(); _load(); } } Future _markAsRead(NotificationItem item) async { if (item.isRead) return; final bool success; if (_isAnnouncementsTab) { success = await _notificationService.markAnnouncementAsRead(item.id); } else { success = await _notificationService.markAsRead(item.id); } if (success) { NotificationBadgeManager().decrementCount(); _load(); } } void _openDetail(NotificationItem item) { _markAsRead(item); // 传递 NotificationItem 让详情页直接渲染,无需二次请求 Navigator.pushNamed(context, '/message/detail', arguments: item); } // ── Build ───────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: Text(context.t('message.title')), actions: [ TextButton( onPressed: _markAllAsRead, child: Text( context.t('notification.markAllRead'), style: AppTypography.labelSmall.copyWith(color: AppColors.primary), ), ), ], bottom: TabBar( controller: _tabController, isScrollable: true, tabAlignment: TabAlignment.start, tabs: [ Tab(text: context.t('common.all')), Tab(text: context.t('notification.system')), Tab(text: context.t('notification.announcement')), Tab(text: context.t('notification.activity')), ], ), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _error != null ? _buildErrorView() : _items.isEmpty ? EmptyState.noMessages(context) : RefreshIndicator( onRefresh: _load, child: ListView.separated( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: _items.length, separatorBuilder: (_, __) => const Divider(indent: 76), itemBuilder: (_, i) => _buildItem(_items[i]), ), ), ); } Widget _buildErrorView() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(context.t('notification.loadFailed'), style: AppTypography.bodyMedium), const SizedBox(height: 16), ElevatedButton( onPressed: _load, child: Text(context.t('notification.retry')), ), ], ), ); } Widget _buildItem(NotificationItem item) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), leading: Container( width: 44, height: 44, decoration: BoxDecoration( color: _iconColor(item.type).withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(_iconData(item.type), size: 22, color: _iconColor(item.type)), ), title: Row( children: [ Expanded( child: Text( item.title, style: AppTypography.labelMedium.copyWith( fontWeight: item.isRead ? FontWeight.w400 : FontWeight.w600, ), ), ), if (item.publishedAt != null) Text(_formatTime(context, item.publishedAt!), style: AppTypography.caption), ], ), subtitle: Padding( padding: const EdgeInsets.only(top: 4), child: Text( item.content, style: AppTypography.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), trailing: !item.isRead ? Container( width: 8, height: 8, decoration: const BoxDecoration( color: AppColors.primary, shape: BoxShape.circle, ), ) : null, onTap: () => _openDetail(item), ); } // ── 工具方法 ────────────────────────────────────────────────────────────── String _formatTime(BuildContext context, DateTime time) { final now = DateTime.now(); final diff = now.difference(time); if (diff.inMinutes < 1) return context.t('time.justNow'); if (diff.inMinutes < 60) return context.t('time.minutesAgo').replaceAll('{n}', '${diff.inMinutes}'); if (diff.inHours < 24) return context.t('time.hoursAgo').replaceAll('{n}', '${diff.inHours}'); if (diff.inDays < 7) return context.t('time.daysAgo').replaceAll('{n}', '${diff.inDays}'); return '${time.month}/${time.day}'; } IconData _iconData(NotificationType type) { switch (type) { case NotificationType.system: return Icons.settings_rounded; case NotificationType.activity: return Icons.swap_horiz_rounded; case NotificationType.reward: return Icons.card_giftcard_rounded; case NotificationType.upgrade: return Icons.system_update_rounded; case NotificationType.announcement: return Icons.campaign_rounded; } } Color _iconColor(NotificationType type) { switch (type) { case NotificationType.system: return AppColors.primary; case NotificationType.activity: return AppColors.success; case NotificationType.reward: return AppColors.warning; case NotificationType.upgrade: return AppColors.info; case NotificationType.announcement: return AppColors.info; } } }