import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../core/app_colors.dart'; import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/data_providers.dart'; import '../../services/in_app_notification_service.dart'; class NotificationCenterPage extends ConsumerStatefulWidget { const NotificationCenterPage({super.key}); @override ConsumerState createState() => _NotificationCenterPageState(); } class _NotificationCenterPageState extends ConsumerState { InAppNotificationHistory? _history; bool _loading = true; bool _markingAll = false; String? _error; InAppNotificationService get _service => ref.read(inAppNotificationServiceProvider); @override void initState() { super.initState(); Future.microtask(_load); } Future _load() async { if (mounted) { setState(() { _loading = true; _error = null; }); } try { final history = await _service.getHistory(); if (!mounted) return; setState(() { _history = history; _loading = false; }); ref.invalidate(notificationUnreadCountProvider); } catch (error) { if (mounted) { setState(() { _error = '$error'; _loading = false; }); } } } Future _open(InAppNotification item) async { if (!item.isRead) { await _service.acknowledge(item.id); if (!mounted) return; setState(() { final current = _history; if (current == null) return; _history = InAppNotificationHistory( unreadCount: (current.unreadCount - 1).clamp(0, current.unreadCount), items: current.items .map( (value) => value.id == item.id ? value.copyWith(isRead: true) : value, ) .toList(), ); }); ref.invalidate(notificationUnreadCountProvider); } if (!mounted || item.actionType == null) return; switch (item.actionType) { case 'medication': pushRoute(ref, 'medCheckIn', params: {'id': item.actionTargetId ?? ''}); return; case 'exercise': pushRoute(ref, 'exercisePlan'); return; case 'health': pushRoute(ref, 'trend', params: {'type': item.actionTargetId ?? ''}); return; case 'report': final reportId = item.actionTargetId; if (reportId != null && reportId.isNotEmpty) { pushRoute(ref, 'aiAnalysis', params: {'id': reportId}); } return; } } Future _markAllRead() async { if (_markingAll || (_history?.unreadCount ?? 0) == 0) return; setState(() => _markingAll = true); try { await _service.markAllRead(); if (!mounted) return; final current = _history; if (current != null) { setState( () => _history = InAppNotificationHistory( unreadCount: 0, items: current.items .map((item) => item.copyWith(isRead: true)) .toList(), ), ); } ref.invalidate(notificationUnreadCountProvider); } finally { if (mounted) setState(() => _markingAll = false); } } Future _delete(InAppNotification item) async { try { await _service.delete(item.id); if (!mounted) return; final current = _history; if (current != null) { setState( () => _history = InAppNotificationHistory( unreadCount: item.isRead ? current.unreadCount : (current.unreadCount - 1).clamp(0, current.unreadCount), items: current.items.where((value) => value.id != item.id).toList(), ), ); } ref.invalidate(notificationUnreadCountProvider); } catch (_) { if (mounted) await _load(); } } @override Widget build(BuildContext context) { final items = _history?.items ?? const []; return Scaffold( body: AppBackground( safeArea: true, child: Column( children: [ _Header( unreadCount: _history?.unreadCount ?? 0, markingAll: _markingAll, onBack: () => popRoute(ref), onMarkAllRead: _markAllRead, ), Expanded( child: RefreshIndicator( onRefresh: _load, color: const Color(0xFF438CE1), child: _buildBody(items), ), ), ], ), ), ); } Widget _buildBody(List items) { if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return _MessageState( icon: LucideIcons.wifiOff, title: '通知加载失败', message: '请检查网络连接后重试', buttonText: '重新加载', onPressed: _load, ); } if (items.isEmpty) { return const _MessageState( icon: LucideIcons.bellOff, title: '暂时没有通知', message: '用药、运动、健康指标和报告消息会显示在这里', ); } final today = []; final earlier = []; final now = DateTime.now(); for (final item in items) { final local = item.createdAt.toLocal(); final isToday = local.year == now.year && local.month == now.month && local.day == now.day; (isToday ? today : earlier).add(item); } return ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 6, 16, 32), children: [ if (today.isNotEmpty) ...[ const _SectionTitle('今天'), ...today.map(_buildDismissible), ], if (earlier.isNotEmpty) ...[ const SizedBox(height: 10), const _SectionTitle('更早'), ...earlier.map(_buildDismissible), ], ], ); } Widget _buildDismissible(InAppNotification item) => Padding( padding: const EdgeInsets.only(bottom: 10), child: Dismissible( key: ValueKey(item.id), direction: DismissDirection.endToStart, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 24), decoration: BoxDecoration( color: AppColors.error, borderRadius: BorderRadius.circular(18), ), child: const Icon(LucideIcons.trash2, color: Colors.white), ), onDismissed: (_) => _delete(item), child: _NotificationCard(item: item, onTap: () => _open(item)), ), ); } class _Header extends StatelessWidget { final int unreadCount; final bool markingAll; final VoidCallback onBack; final VoidCallback onMarkAllRead; const _Header({ required this.unreadCount, required this.markingAll, required this.onBack, required this.onMarkAllRead, }); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(8, 8, 12, 10), child: Row( children: [ IconButton( tooltip: '返回', onPressed: onBack, icon: const Icon(LucideIcons.chevronLeft), ), const SizedBox(width: 2), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '通知中心', style: TextStyle( fontSize: 21, fontWeight: FontWeight.w800, color: AppColors.textPrimary, ), ), const SizedBox(height: 2), Text( unreadCount == 0 ? '所有消息均已读' : '$unreadCount 条未读消息', style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ), if (unreadCount > 0) TextButton.icon( onPressed: markingAll ? null : onMarkAllRead, icon: markingAll ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(LucideIcons.checkCheck, size: 17), label: const Text('全部已读'), ), ], ), ); } class _SectionTitle extends StatelessWidget { final String text; const _SectionTitle(this.text); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(4, 6, 4, 10), child: Text( text, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textSecondary, ), ), ); } class _NotificationCard extends StatelessWidget { final InAppNotification item; final VoidCallback onTap; const _NotificationCard({required this.item, required this.onTap}); @override Widget build(BuildContext context) { final visual = _NotificationVisual.of(item); return Material( color: item.isRead ? Colors.white.withValues(alpha: 0.72) : Colors.white.withValues(alpha: 0.96), borderRadius: BorderRadius.circular(18), elevation: item.isRead ? 0 : 1, shadowColor: visual.color.withValues(alpha: 0.12), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), border: Border.all( color: item.isRead ? const Color(0xFFE9EEF5) : visual.color.withValues(alpha: 0.18), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 46, height: 46, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [visual.lightColor, Colors.white], ), borderRadius: BorderRadius.circular(14), ), child: Icon(visual.icon, size: 22, color: visual.color), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( item.title, style: TextStyle( fontSize: 15, fontWeight: item.isRead ? FontWeight.w600 : FontWeight.w800, color: AppColors.textPrimary, ), ), ), if (!item.isRead) Container( width: 8, height: 8, decoration: BoxDecoration( color: visual.color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: visual.color.withValues(alpha: 0.3), blurRadius: 5, ), ], ), ), ], ), const SizedBox(height: 5), Text( item.message, style: const TextStyle( fontSize: 13, height: 1.45, color: AppColors.textSecondary, ), ), const SizedBox(height: 9), Row( children: [ Text( _formatTime(item.createdAt), style: const TextStyle( fontSize: 11, color: AppColors.textHint, ), ), const Spacer(), if (item.actionType != null) Row( children: [ Text( '查看详情', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: visual.color, ), ), const SizedBox(width: 2), Icon( LucideIcons.chevronRight, size: 14, color: visual.color, ), ], ), ], ), ], ), ), ], ), ), ), ); } static String _formatTime(DateTime value) { final local = value.toLocal(); final now = DateTime.now(); final time = '${local.hour.toString().padLeft(2, '0')}:' '${local.minute.toString().padLeft(2, '0')}'; if (local.year == now.year && local.month == now.month && local.day == now.day) { return time; } if (local.year == now.year) return '${local.month}月${local.day}日 $time'; return '${local.year}年${local.month}月${local.day}日 $time'; } } class _NotificationVisual { final IconData icon; final Color color; final Color lightColor; const _NotificationVisual(this.icon, this.color, this.lightColor); factory _NotificationVisual.of(InAppNotification item) { switch (item.actionType) { case 'exercise': return const _NotificationVisual( LucideIcons.activity, Color(0xFF2C9A70), Color(0xFFDDF7EC), ); case 'health': if (item.severity == 'critical') { return const _NotificationVisual( LucideIcons.triangleAlert, Color(0xFFE55353), Color(0xFFFFE7E7), ); } return const _NotificationVisual( LucideIcons.heartPulse, Color(0xFFE8863A), Color(0xFFFFEEDC), ); case 'report': if (item.severity == 'error') { return const _NotificationVisual( LucideIcons.fileWarning, Color(0xFFE55353), Color(0xFFFFE7E7), ); } return const _NotificationVisual( LucideIcons.fileCheck2, Color(0xFF7B68CE), Color(0xFFEDE9FF), ); default: return const _NotificationVisual( LucideIcons.pill, Color(0xFF468AD8), Color(0xFFE4F0FF), ); } } } class _MessageState extends StatelessWidget { final IconData icon; final String title; final String message; final String? buttonText; final VoidCallback? onPressed; const _MessageState({ required this.icon, required this.title, required this.message, this.buttonText, this.onPressed, }); @override Widget build(BuildContext context) => ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(28, 120, 28, 32), children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 34), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.white.withValues(alpha: 0.95), const Color(0xFFF1F7FF).withValues(alpha: 0.9), ], ), borderRadius: BorderRadius.circular(24), border: Border.all(color: const Color(0xFFE5EDF7)), ), child: Column( children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: const Color(0xFFE5F0FF), borderRadius: BorderRadius.circular(20), ), child: Icon(icon, size: 29, color: const Color(0xFF5D91CF)), ), const SizedBox(height: 18), Text( title, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w800, color: AppColors.textPrimary, ), ), const SizedBox(height: 7), Text( message, textAlign: TextAlign.center, style: const TextStyle( fontSize: 13, height: 1.5, color: AppColors.textSecondary, ), ), if (buttonText != null) ...[ const SizedBox(height: 20), FilledButton(onPressed: onPressed, child: Text(buttonText!)), ], ], ), ), ], ); }