261 lines
7.5 KiB
Dart
261 lines
7.5 KiB
Dart
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;
|
||
}
|
||
}
|
||
}
|