gcx/frontend/genex-mobile/lib/features/message/presentation/pages/message_page.dart

261 lines
7.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 + 消息详情
/// 接入后端真实 API
class MessagePage extends StatefulWidget {
const MessagePage({super.key});
@override
State<MessagePage> createState() => _MessagePageState();
}
class _MessagePageState extends State<MessagePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final NotificationService _notificationService = NotificationService();
List<NotificationItem> _notifications = [];
bool _isLoading = false;
String? _error;
NotificationType? _currentFilter;
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
_onTabChanged(_tabController.index);
}
});
_loadNotifications();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _onTabChanged(int index) {
switch (index) {
case 0:
_currentFilter = null;
break;
case 1:
_currentFilter = NotificationType.system;
break;
case 2:
_currentFilter = NotificationType.announcement;
break;
case 3:
_currentFilter = NotificationType.activity;
break;
}
_loadNotifications();
}
Future<void> _loadNotifications() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final response = await _notificationService.getNotifications(
type: _currentFilter,
);
if (mounted) {
setState(() {
_notifications = response.notifications;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
}
Future<void> _markAllAsRead() async {
final success = await _notificationService.markAllAnnouncementsAsRead();
if (success) {
NotificationBadgeManager().clearCount();
_loadNotifications();
}
}
Future<void> _markAsRead(NotificationItem item) async {
if (item.isRead) return;
final success = await _notificationService.markAsRead(item.id);
if (success) {
NotificationBadgeManager().decrementCount();
_loadNotifications();
}
}
@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()
: _notifications.isEmpty
? EmptyState.noMessages(context)
: RefreshIndicator(
onRefresh: _loadNotifications,
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _notifications.length,
separatorBuilder: (_, __) => const Divider(indent: 76),
itemBuilder: (context, index) {
return _buildMessageItem(_notifications[index]);
},
),
),
);
}
Widget _buildErrorView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(context.t('notification.loadFailed'),
style: AppTypography.bodyMedium),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadNotifications,
child: Text(context.t('notification.retry')),
),
],
),
);
}
Widget _buildMessageItem(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(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: () {
_markAsRead(item);
Navigator.pushNamed(context, '/message/detail');
},
);
}
String _formatTime(DateTime time) {
final now = DateTime.now();
final diff = now.difference(time);
if (diff.inMinutes < 1) return '刚刚';
if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
if (diff.inHours < 24) return '${diff.inHours}小时前';
if (diff.inDays < 7) return '${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;
}
}
}