import 'dart:async'; import 'package:flutter/material.dart'; import '../../../../shared/theme/design_tokens.dart'; import '../../../../shared/widgets/notification/notification_detail_bottom_sheet.dart'; import '../../data/models/notification_item.dart'; import '../../data/models/notification_payload.dart'; import '../../data/repositories/notification_repository.dart'; import '../bloc/notification_bloc.dart'; import '../widgets/notification_list_item.dart'; class NotificationCenterScreen extends StatefulWidget { const NotificationCenterScreen({ super.key, required this.repository, this.onNavigateToRoute, this.onOpenUrl, this.onUnreadCountChanged, }); final NotificationRepository repository; final void Function(String route, {String? entityId, String? tab})? onNavigateToRoute; final void Function(String url)? onOpenUrl; final Future Function()? onUnreadCountChanged; @override State createState() => _NotificationCenterScreenState(); } class _NotificationCenterScreenState extends State { late NotificationBloc _bloc; @override void initState() { super.initState(); _bloc = NotificationBloc(repository: widget.repository); _bloc.handleEvent(LoadNotifications()); _bloc.addListener(_onStateChanged); } void _onStateChanged() { setState(() {}); } @override void dispose() { _bloc.removeListener(_onStateChanged); _bloc.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final state = _bloc.state; return Scaffold( appBar: AppBar( title: const Text('通知'), centerTitle: true, actions: [ if (state.items.any((item) => !item.isRead)) TextButton( onPressed: _onMarkAllRead, child: Text('全部已读', style: TextStyle(color: colors.primary)), ), ], ), body: RefreshIndicator( onRefresh: () => _bloc.handleEvent(RefreshNotifications()), child: _buildBody(state, colors), ), ); } Widget _buildBody(NotificationState state, ColorScheme colors) { if (state.status == NotificationStatus.loading && state.items.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (state.status == NotificationStatus.error && state.items.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: colors.error), const SizedBox(height: AppSpacing.md), Text('加载失败', style: TextStyle(color: colors.onSurfaceVariant)), const SizedBox(height: AppSpacing.sm), FilledButton( onPressed: () => _bloc.handleEvent(LoadNotifications()), child: const Text('重试'), ), ], ), ); } if (state.items.isEmpty) { return ListView( children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.5, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.notifications_none_outlined, size: 64, color: colors.outline, ), const SizedBox(height: AppSpacing.md), Text( '暂无通知', style: TextStyle( color: colors.onSurfaceVariant, fontSize: 16, ), ), ], ), ), ), ], ); } return ListView.builder( itemCount: state.items.length + (state.hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == state.items.length && state.hasMore) { _bloc.handleEvent(LoadMoreNotifications()); return const Padding( padding: EdgeInsets.all(AppSpacing.lg), child: Center(child: CircularProgressIndicator()), ); } final item = state.items[index]; return NotificationListItem( item: item, onTap: () => _handleNotificationTap(context, item), ); }, ); } Future _handleNotificationTap( BuildContext context, NotificationItem item, ) async { final wasUnread = !item.isRead; if (!item.isRead) { await _bloc.handleEvent(MarkNotificationRead(notificationId: item.id)); final updatedIndex = _bloc.state.items.indexWhere((n) => n.id == item.id); if (wasUnread && updatedIndex >= 0 && _bloc.state.items[updatedIndex].isRead) { await widget.onUnreadCountChanged?.call(); } } if (context.mounted) { await showNotificationDetailBottomSheet( context: context, item: item, onMarkRead: () async {}, ); } _executePayload(item.payload); } void _executePayload(NotificationPayload payload) { switch (payload) { case NotificationPayloadNone(): break; case NotificationPayloadRoute(:final route, :final entityId, :final tab): widget.onNavigateToRoute?.call(route, entityId: entityId, tab: tab); case NotificationPayloadUrl(:final url): widget.onOpenUrl?.call(url); } } void _onMarkAllRead() { unawaited(_markAllRead()); } Future _markAllRead() async { final unreadBefore = _bloc.state.items.any((item) => !item.isRead); await _bloc.handleEvent(MarkAllNotificationsRead()); final unreadAfter = _bloc.state.items.any((item) => !item.isRead); if (unreadBefore && !unreadAfter) { await widget.onUnreadCountChanged?.call(); } } }