import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:easy_refresh/easy_refresh.dart'; import '../../core/theme/app_colors.dart'; import '../shell/nav_bar_config.dart'; import '../../core/utils/date_utils.dart' as du; import '../../core/utils/responsive.dart'; import '../../shared/widgets/empty_state.dart'; import '../../core/i18n/app_localizations.dart'; import 'announcement_list_controller.dart'; import 'announcement_model.dart'; class AnnouncementListPage extends ConsumerStatefulWidget { const AnnouncementListPage({super.key}); @override ConsumerState createState() => _AnnouncementListPageState(); } class _AnnouncementListPageState extends ConsumerState with TickerProviderStateMixin { late TabController _tabCtrl; static List _getTabLabels(bool isAdmin) => isAdmin ? ['全部', '通知公告', '人事与制度', '放假与活动', '我的草稿'] : ['全部', '通知公告', '人事与制度', '放假与活动']; @override void initState() { super.initState(); final isAdmin = ref.read(isAdminProvider); _tabCtrl = TabController( length: _getTabLabels(isAdmin).length, vsync: this, ); _tabCtrl.addListener(_onTabChanged); } void _onTabChanged() { if (!_tabCtrl.indexIsChanging) { ref.read(announcementTabProvider.notifier).state = _tabCtrl.index; } } @override void dispose() { _tabCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isAdmin = ref.watch(isAdminProvider); final tabs = _getTabLabels(isAdmin); final tabIndex = ref.watch(announcementTabProvider); final r = ResponsiveHelper.of(context); final l10n = AppLocalizations.of(context); // Handle TabController recreation when tab count changes (isAdmin toggle) if (_tabCtrl.length != tabs.length) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final newIdx = tabIndex >= tabs.length ? 0 : tabIndex; _tabCtrl.dispose(); _tabCtrl = TabController( length: tabs.length, vsync: this, initialIndex: newIdx, ); _tabCtrl.addListener(_onTabChanged); setState(() {}); }); // Render a simplified view during this frame to avoid length mismatch return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: r.listMaxWidth), child: Column( children: [ Container( color: AppColors.bgCard, padding: const EdgeInsets.symmetric(horizontal: 8), child: TDTabBar( tabs: tabs.map((l) => TDTab(text: l)).toList(), isScrollable: true, labelColor: AppColors.primary, unselectedLabelColor: AppColors.textSecondary, outlineType: TDTabBarOutlineType.filled, showIndicator: true, indicatorColor: AppColors.primary, indicatorHeight: 3, labelPadding: const EdgeInsets.symmetric(horizontal: 12), ), ), const Expanded(child: SizedBox.shrink()), ], ), ), ); } // Sync TabController with external changes if (_tabCtrl.index != tabIndex && !_tabCtrl.indexIsChanging) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _tabCtrl.animateTo(tabIndex); }); } ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('announcementList'), showBack: true, onBack: () => context.pop(), ), ); return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: r.listMaxWidth), child: Column( children: [ Container( color: AppColors.bgCard, padding: const EdgeInsets.symmetric(horizontal: 8), child: TDTabBar( tabs: tabs.map((l) => TDTab(text: l)).toList(), controller: _tabCtrl, isScrollable: true, labelColor: AppColors.primary, unselectedLabelColor: AppColors.textSecondary, outlineType: TDTabBarOutlineType.filled, showIndicator: true, indicatorColor: AppColors.primary, indicatorHeight: 3, labelPadding: const EdgeInsets.symmetric(horizontal: 12), onTap: (index) { ref.read(announcementTabProvider.notifier).state = index; }, ), ), Expanded( child: TabBarView( controller: _tabCtrl, children: List.generate(tabs.length, (tabIdx) { return _AnnouncementTabContent(tabIndex: tabIdx); }), ), ), ], ), ), ); } } class _AnnouncementTabContent extends ConsumerWidget { final int tabIndex; const _AnnouncementTabContent({required this.tabIndex}); @override Widget build(BuildContext context, WidgetRef ref) { final items = ref.watch(filteredAnnouncementsProvider); if (items.isEmpty) { return EasyRefresh( header: TDRefreshHeader(), onRefresh: () async { ref.invalidate(filteredAnnouncementsProvider); }, child: ListView( children: const [ SizedBox(height: 120), EmptyState(message: '暂无行政公告'), ], ), ); } return EasyRefresh( header: TDRefreshHeader(), onRefresh: () async { ref.invalidate(filteredAnnouncementsProvider); }, child: ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: items.length, itemBuilder: (_, i) => _buildAnnouncementCard(context, items[i]), ), ); } Widget _buildAnnouncementCard(BuildContext context, AnnouncementModel item) { final expired = item.isExpired; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: GestureDetector( onTap: () => context.push('/announcement/detail/${item.id}'), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: expired ? const Color(0xFFF9F9F9) : AppColors.bgCard, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ // 置顶标记 if (item.isTop) Container( margin: const EdgeInsets.only(right: 6), padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 1), decoration: BoxDecoration( color: AppColors.danger, borderRadius: BorderRadius.circular(2), ), child: const Text('置顶', style: TextStyle( fontSize: 10, color: Colors.white)), ), // 标题 Expanded( child: Text( item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: expired ? AppColors.textPlaceholder : AppColors.textPrimary, decoration: expired ? TextDecoration.lineThrough : null, ), ), ), // 已过期标记 if (expired) Container( margin: const EdgeInsets.only(left: 6), padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(3), ), child: const Text('已过期', style: TextStyle( fontSize: 10, color: AppColors.textPlaceholder)), ), // 未读红点 if (!item.isRead && !expired) Container( margin: const EdgeInsets.only(left: 6), width: 8, height: 8, decoration: const BoxDecoration( color: AppColors.danger, shape: BoxShape.circle, ), ), ], ), const SizedBox(height: 8), // 元信息行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ _buildTypeTag(item.typeLabel), const SizedBox(width: 8), Text( item.publisherName, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary), ), ], ), Text( du.DateUtils.formatDateTime(item.publishTime), style: const TextStyle( fontSize: 12, color: AppColors.textPlaceholder), ), ], ), ], ), ), ), ); } Widget _buildTypeTag(String type) { Color bgColor; Color textColor; switch (type) { case '人事与制度': bgColor = AppColors.successBg; textColor = AppColors.success; break; case '放假与活动': bgColor = AppColors.warningBg; textColor = AppColors.warning; break; default: // 通知公告 bgColor = AppColors.primaryLight; textColor = AppColors.primary; } return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(3), ), child: Text(type, style: TextStyle(fontSize: 11, color: textColor)), ); } }