import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../shared/widgets/empty_state.dart'; import '../shell/nav_bar_config.dart'; import '../../core/i18n/app_localizations.dart'; import '../../shared/widgets/message_item.dart'; import '../../shared/widgets/skeleton_list_card.dart'; import 'message_controller.dart'; import 'message_model.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; /// 消息通知聚合页 /// /// 展示 5 种消息类型: /// - 审批待办(📋)→ 详情页 + 审批操作栏 /// - 审批结果(✅/❌)→ 详情查看结果 /// - 撤回通知(↩)→ 消息列表 /// - 系统公告(📢)→ 公告详情 /// - 过期提醒(⏰)→ 对应详情页 /// /// 左滑操作:标记已读(蓝)+ 删除(红)。已读消息不可左滑。 class MessageListPage extends ConsumerWidget { const MessageListPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final colors = Theme.of(context).extension()!; final messagesAsync = ref.watch(messageListProvider); final unreadCount = ref.watch(unreadCountProvider); final location = GoRouterState.of(context).uri.toString(); final l10n = AppLocalizations.of(context); if (location.startsWith('/messages')) { ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('messageNotifications'), showBack: true, leadingIcon: Icons.close, // 仅在有未读时显示"全部已读"按钮 showRight: unreadCount > 0, rightWidget: unreadCount > 0 ? GestureDetector( onTap: () { TDMessage.showMessage( context: context, content: l10n.get('markAllRead'), theme: MessageTheme.success, icon: true, duration: 2000, ); }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Text( l10n.get('markAllRead'), style: TextStyle( fontSize: AppFontSizes.body, color: colors.primary, ), ), ), ) : null, ), ); } // 首次加载:展示骨架屏,不包裹 EasyRefresh if (messagesAsync.isLoading && !messagesAsync.hasValue) { return SkeletonLoadingList( cardBuilder: () => const SkeletonMessageCard(), ); } return EasyRefresh( header: TDRefreshHeader(), onRefresh: () async { ref.invalidate(messageListProvider); await ref.read(messageListProvider.future); }, child: _buildContent(context, ref, messagesAsync, l10n), ); } /// 根据加载状态构建列表内容 /// /// - [AsyncValue.isReloading]:下拉刷新中,展示旧数据 + header loading /// - [AsyncValue.hasError]:错误状态 /// - 空数据:空状态 /// - 正常数据:消息列表 Widget _buildContent( BuildContext context, WidgetRef ref, AsyncValue> messagesAsync, AppLocalizations l10n, ) { // 下拉刷新中:保留旧数据 + EasyRefresh header loading if (messagesAsync.isReloading) { final oldItems = messagesAsync.valueOrNull ?? []; if (oldItems.isEmpty) { return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: EmptyState(message: l10n.get('noMessages')), ); } return ListView.separated( padding: const EdgeInsets.fromLTRB( AppSpacing.md, AppSpacing.md, AppSpacing.md, AppSpacing.lg, ), itemCount: oldItems.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (_, i) => _buildItem(context, ref, oldItems[i], l10n), ); } // 错误 if (messagesAsync.hasError) { return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: EmptyState(message: l10n.get('loadFailed')), ); } // 空数据 final messages = messagesAsync.requireValue; if (messages.isEmpty) { return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: EmptyState(message: l10n.get('noMessages')), ); } // 正常列表 return ListView.separated( padding: const EdgeInsets.fromLTRB( AppSpacing.md, AppSpacing.md, AppSpacing.md, AppSpacing.lg, ), itemCount: messages.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (_, i) => _buildItem(context, ref, messages[i], l10n), ); } Widget _buildItem( BuildContext context, WidgetRef ref, MessageModel msg, AppLocalizations l10n, ) { final colors = Theme.of(context).extension()!; final (icon, iconColor, iconBg) = _messageIconProps(msg.msgType, colors); return MessageItem( icon: icon, iconColor: iconColor, iconBackground: iconBg, title: msg.title, time: _formatTime(msg.createTime), sender: _messageSender(msg.msgType, l10n), summary: msg.content, unread: !msg.isRead, onTap: () => _navigateToBiz(context, msg), // 仅未读消息展示左滑操作 onToggleRead: msg.isRead ? null : () { TDMessage.showMessage( context: context, content: '${l10n.get("markRead")}:${msg.title}', theme: MessageTheme.success, icon: true, duration: 2000, ); }, onDelete: () { showDialog( context: context, builder: (ctx) => TDAlertDialog( title: l10n.get('confirm'), content: l10n.getString( 'confirmAction', args: {'action': l10n.get('delete')}, ), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.of(ctx).pop(), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirm'), action: () { Navigator.of(ctx).pop(); TDMessage.showMessage( context: context, content: '${l10n.get("deletedToast")}${msg.title}', theme: MessageTheme.warning, icon: true, duration: 2000, ); }, ), ), ); }, ); } /// 按消息类型返回(图标, 图标色, 图标背景色) (IconData, Color, Color) _messageIconProps( String msgType, AppColorsExtension colors, ) { switch (msgType) { case 'announcement': return (Icons.campaign_outlined, colors.warning, colors.warningBg); case 'approval_result': return (Icons.check_circle_outlined, colors.success, colors.successBg); case 'withdraw_notice': return (Icons.undo_outlined, colors.statusGray, colors.swipeDeleteBg); case 'expiry_reminder': return (Icons.timer_off_outlined, colors.danger, colors.dangerBg); case 'approval_notice': default: return (Icons.assignment_outlined, colors.primary, colors.primaryLight); } } /// 按消息类型返回发送者标签 String _messageSender(String msgType, AppLocalizations l10n) { switch (msgType) { case 'announcement': return l10n.get('systemNotice'); case 'approval_notice': return l10n.get('approvalNotice'); case 'approval_result': return l10n.get('systemMessage'); case 'withdraw_notice': return l10n.get('systemMessage'); case 'expiry_reminder': return l10n.get('systemMessage'); default: return l10n.get('systemMessage'); } } /// 格式化时间为 MM-DD HH:mm String _formatTime(DateTime time) { final month = time.month.toString().padLeft(2, '0'); final day = time.day.toString().padLeft(2, '0'); final hour = time.hour.toString().padLeft(2, '0'); final minute = time.minute.toString().padLeft(2, '0'); return '$month-$day $hour:$minute'; } /// 按 BizType 路由跳转 void _navigateToBiz(BuildContext context, MessageModel msg) { if (msg.bizType == null || msg.bizId == null) return; switch (msg.bizType) { case 'expense': context.push('/expense/detail/${msg.bizId}'); break; case 'overtime': context.push('/overtime/detail/${msg.bizId}'); break; case 'vehicle': context.push('/vehicle/detail/${msg.bizId}'); break; case 'announcement': context.push('/announcement/detail/${msg.bizId}'); break; case 'expense_application': context.push('/expense-apply/detail/${msg.bizId}'); break; } } }