import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:airhub_app/theme/design_tokens.dart'; import 'package:airhub_app/widgets/animated_gradient_background.dart'; import 'package:airhub_app/features/notification/domain/entities/app_notification.dart'; import 'package:airhub_app/features/notification/presentation/controllers/notification_controller.dart'; /// 消息通知页面 — 接入真实 API class NotificationPage extends ConsumerStatefulWidget { const NotificationPage({super.key}); @override ConsumerState createState() => _NotificationPageState(); } class _NotificationPageState extends ConsumerState { /// 当前展开的通知 id(null 表示全部折叠) int? _expandedId; void _toggleNotification(AppNotification notif) { setState(() { if (_expandedId == notif.id) { _expandedId = null; } else { _expandedId = notif.id; // Mark as read when expanded if (!notif.isRead) { ref.read(notificationControllerProvider.notifier).markAsRead(notif.id); } } }); } @override Widget build(BuildContext context) { final notificationsAsync = ref.watch(notificationControllerProvider); return Scaffold( backgroundColor: Colors.transparent, body: Stack( children: [ const AnimatedGradientBackground(), Column( children: [ _buildHeader(context), Expanded( child: notificationsAsync.when( loading: () => const Center( child: CircularProgressIndicator(color: Colors.white), ), error: (error, _) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '加载失败', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 16, ), ), const SizedBox(height: 12), GestureDetector( onTap: () => ref.read(notificationControllerProvider.notifier).refresh(), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(20), ), child: const Text('重试', style: TextStyle(color: Colors.white)), ), ), ], ), ), data: (notifications) { if (notifications.isEmpty) { return _buildEmptyState(); } return ShaderMask( shaderCallback: (Rect rect) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black, Colors.black, Colors.transparent, ], stops: [0.0, 0.03, 0.95, 1.0], ).createShader(rect); }, blendMode: BlendMode.dstIn, child: ListView.builder( padding: EdgeInsets.only( top: 8, left: 20, right: 20, bottom: 40 + MediaQuery.of(context).padding.bottom, ), itemCount: notifications.length, itemBuilder: (context, index) { return _buildNotificationCard(notifications[index]); }, ), ); }, ), ), ], ), ], ), ); } Widget _buildHeader(BuildContext context) { return Container( padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top + 20, left: AppSpacing.lg, right: AppSpacing.lg, bottom: AppSpacing.md, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( onTap: () => Navigator.pop(context), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.iconBtnBg, borderRadius: BorderRadius.circular(AppRadius.button), ), child: const Icon( Icons.arrow_back_ios_new, color: AppColors.textPrimary, size: 18, ), ), ), Text('消息通知', style: AppTextStyles.title), const SizedBox(width: 40), ], ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 100, height: 100, decoration: const BoxDecoration( color: Color(0x1A9CA3AF), shape: BoxShape.circle, ), child: const Icon( Icons.notifications_none_rounded, size: 48, color: AppColors.textSecondary, ), ), const SizedBox(height: 20), const Text( '暂无新消息', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.textSecondary, ), ), const SizedBox(height: 8), const Text( '新的通知会在这里显示', style: TextStyle( fontSize: 13, color: AppColors.textHint, ), ), ], ), ); } Widget _buildNotificationCard(AppNotification notif) { final isExpanded = _expandedId == notif.id; final isUnread = !notif.isRead; final timeStr = _formatTime(notif.createdAt); return Padding( padding: const EdgeInsets.only(bottom: 12), child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, decoration: BoxDecoration( color: isExpanded ? const Color(0xD9FFFFFF) : const Color(0xB3FFFFFF), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0x66FFFFFF)), boxShadow: const [AppShadows.card], ), child: Column( children: [ GestureDetector( onTap: () => _toggleNotification(notif), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildNotifIcon(notif), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( notif.title, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Text( timeStr, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), const SizedBox(height: 4), Text( notif.description, style: const TextStyle( fontSize: 13, color: Color(0xFF6B7280), height: 1.5, ), ), ], ), ), const SizedBox(width: 8), Column( children: [ AnimatedRotation( turns: isExpanded ? 0.25 : 0, duration: const Duration(milliseconds: 300), child: const Icon( Icons.chevron_right, color: AppColors.textHint, size: 20, ), ), if (isUnread) ...[ const SizedBox(height: 6), Container( width: 8, height: 8, decoration: const BoxDecoration( color: AppColors.notificationDot, shape: BoxShape.circle, ), ), ], ], ), ], ), ), ), // ── 展开详情区域(只动画高度,宽度始终满宽,避免文字竖排) ── ClipRect( child: AnimatedSize( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: isExpanded ? Container( width: double.infinity, decoration: const BoxDecoration( color: Color(0x80F9FAFB), border: Border( top: BorderSide( color: Color(0x0D000000), ), ), ), padding: const EdgeInsets.all(20), child: Text( notif.content, style: const TextStyle( fontSize: 14, color: Color(0xFF374151), height: 1.7, ), ), ) : const SizedBox(width: double.infinity, height: 0), ), ), ], ), ), ); } Widget _buildNotifIcon(AppNotification notif) { final isSystem = notif.type == 'system'; final isActivity = notif.type == 'activity'; IconData icon; if (isSystem) { icon = Icons.info_outline; } else if (isActivity) { icon = Icons.card_giftcard; } else { icon = Icons.devices; } return Container( width: 40, height: 40, decoration: BoxDecoration( color: isSystem ? const Color(0xFFEFF6FF) : isActivity ? const Color(0xFFFFF7ED) : const Color(0xFFF0FDF4), borderRadius: BorderRadius.circular(20), ), alignment: Alignment.center, child: notif.imageUrl.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.network( notif.imageUrl, width: 40, height: 40, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Icon( icon, size: 20, color: isSystem ? const Color(0xFF3B82F6) : const Color(0xFFF97316), ), ), ) : Icon( icon, size: 20, color: isSystem ? const Color(0xFF3B82F6) : isActivity ? const Color(0xFFF97316) : const Color(0xFF22C55E), ), ); } String _formatTime(String createdAt) { try { final dt = DateTime.parse(createdAt); final now = DateTime.now(); final diff = now.difference(dt); if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前'; if (diff.inHours < 24) return '${diff.inHours}小时前'; if (diff.inDays < 2) return '昨天'; if (diff.inDays < 7) return '${diff.inDays}天前'; return '${dt.month}月${dt.day}日'; } catch (_) { return createdAt; } } }