message_list_page.dart 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import 'package:easy_refresh/easy_refresh.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import 'package:tdesign_flutter/tdesign_flutter.dart';
  6. import '../../core/theme/app_colors.dart';
  7. import '../../shared/widgets/empty_state.dart';
  8. import '../shell/nav_bar_config.dart';
  9. import '../../core/i18n/app_localizations.dart';
  10. import '../../shared/widgets/loading_widget.dart';
  11. import '../../shared/widgets/message_item.dart';
  12. import 'message_controller.dart';
  13. import 'message_model.dart';
  14. class MessageListPage extends ConsumerWidget {
  15. const MessageListPage({super.key});
  16. @override
  17. Widget build(BuildContext context, WidgetRef ref) {
  18. final messagesAsync = ref.watch(messageListProvider);
  19. final location = GoRouterState.of(context).uri.toString();
  20. final l10n = AppLocalizations.of(context);
  21. if (location.startsWith('/messages')) {
  22. ref
  23. .read(navBarConfigProvider.notifier)
  24. .update(
  25. NavBarConfig(
  26. title: l10n.get('tabMessages'),
  27. showBack: true,
  28. leadingIcon: Icons.close,
  29. ),
  30. );
  31. }
  32. return EasyRefresh(
  33. header: TDRefreshHeader(),
  34. onRefresh: () async {
  35. ref.invalidate(messageListProvider);
  36. await ref.read(messageListProvider.future);
  37. },
  38. child: messagesAsync.when(
  39. loading: () => const SingleChildScrollView(
  40. physics: AlwaysScrollableScrollPhysics(),
  41. child: LoadingWidget(),
  42. ),
  43. error: (_, _) => SingleChildScrollView(
  44. physics: const AlwaysScrollableScrollPhysics(),
  45. child: EmptyState(message: l10n.get('loadFailed')),
  46. ),
  47. data: (messages) => messages.isEmpty
  48. ? SingleChildScrollView(
  49. physics: const AlwaysScrollableScrollPhysics(),
  50. child: EmptyState(message: l10n.get('noMessages')),
  51. )
  52. : ListView.separated(
  53. padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
  54. itemCount: messages.length,
  55. separatorBuilder: (_, _) => const SizedBox(height: 12),
  56. itemBuilder: (_, i) => _buildItem(context, messages[i]),
  57. ),
  58. ),
  59. );
  60. }
  61. Widget _buildItem(BuildContext context, MessageModel msg) {
  62. final l10n = AppLocalizations.of(context);
  63. final (icon, iconColor, iconBg) = _messageIconProps(msg.msgType);
  64. final sender = _messageSender(msg.msgType, l10n);
  65. return MessageItem(
  66. icon: icon,
  67. iconColor: iconColor,
  68. iconBackground: iconBg,
  69. title: msg.title,
  70. time: _formatTime(msg.createTime, l10n),
  71. sender: sender,
  72. summary: msg.content,
  73. unread: !msg.isRead,
  74. onTap: () => _navigateToBiz(context, msg),
  75. onPin: () {
  76. TDMessage.showMessage(
  77. context: context,
  78. content: '${l10n.get("pinnedToast")}${msg.title}',
  79. theme: MessageTheme.info,
  80. icon: true,
  81. duration: 3000,
  82. );
  83. },
  84. onToggleRead: () {
  85. final action = msg.isRead
  86. ? l10n.get('markUnread')
  87. : l10n.get('markRead');
  88. TDMessage.showMessage(
  89. context: context,
  90. content: '$action:${msg.title}',
  91. theme: MessageTheme.success,
  92. icon: true,
  93. duration: 3000,
  94. );
  95. },
  96. onDelete: () {
  97. TDMessage.showMessage(
  98. context: context,
  99. content: '${l10n.get("deletedToast")}${msg.title}',
  100. theme: MessageTheme.warning,
  101. icon: true,
  102. duration: 3000,
  103. );
  104. },
  105. );
  106. }
  107. (IconData, Color, Color) _messageIconProps(String msgType) {
  108. switch (msgType) {
  109. case 'announcement':
  110. return (Icons.campaign, AppColors.warning, AppColors.warningBg);
  111. case 'approval_result':
  112. return (
  113. Icons.check_circle_outline,
  114. AppColors.success,
  115. AppColors.successBg,
  116. );
  117. case 'approval_notice':
  118. default:
  119. return (
  120. Icons.assignment_outlined,
  121. AppColors.primary,
  122. AppColors.primaryLight,
  123. );
  124. }
  125. }
  126. String _messageSender(String msgType, AppLocalizations l10n) {
  127. switch (msgType) {
  128. case 'announcement':
  129. return l10n.get('systemNotice');
  130. case 'approval_notice':
  131. return l10n.get('approvalNotice');
  132. case 'approval_result':
  133. return l10n.get('systemMessage');
  134. default:
  135. return l10n.get('systemMessage');
  136. }
  137. }
  138. String _formatTime(DateTime time, AppLocalizations l10n) {
  139. final now = DateTime.now();
  140. final diff = now.difference(time);
  141. if (diff.inMinutes < 60)
  142. return '${diff.inMinutes}${l10n.get("minutesAgo")}';
  143. if (diff.inHours < 24) return '${diff.inHours}${l10n.get("hoursAgo")}';
  144. if (diff.inDays < 7) return '${diff.inDays}${l10n.get("daysAgo")}';
  145. return '${time.month}/${time.day}';
  146. }
  147. void _navigateToBiz(BuildContext context, MessageModel msg) {
  148. if (msg.bizType == null || msg.bizId == null) return;
  149. switch (msg.bizType) {
  150. case 'expense':
  151. context.push('/expense/detail/${msg.bizId}');
  152. break;
  153. case 'overtime':
  154. context.push('/overtime/detail/${msg.bizId}');
  155. break;
  156. case 'vehicle':
  157. context.push('/vehicle/detail/${msg.bizId}');
  158. break;
  159. case 'announcement':
  160. context.push('/announcement/detail/${msg.bizId}');
  161. break;
  162. case 'expense_application':
  163. context.push('/expense-apply/detail/${msg.bizId}');
  164. break;
  165. }
  166. }
  167. }