| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- 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 '../../shared/widgets/skeleton_list_card.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<AnnouncementListPage> createState() =>
- _AnnouncementListPageState();
- }
- class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
- with TickerProviderStateMixin {
- late TabController _tabCtrl;
- static List<String> _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,
- dividerHeight: 0,
- 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,
- dividerHeight: 0,
- labelPadding: const EdgeInsets.symmetric(horizontal: 12),
- onTap: (index) {
- ref.invalidate(filteredAnnouncementsProvider);
- 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 itemsAsync = ref.watch(filteredAnnouncementsProvider(tabIndex));
- if (itemsAsync.isLoading && !itemsAsync.hasValue) {
- return SkeletonLoadingList(
- cardBuilder: () => const SkeletonAnnouncementCard(),
- );
- }
- return EasyRefresh(
- header: TDRefreshHeader(),
- onRefresh: () async {
- ref.read(announcementRefreshProvider.notifier).state++;
- },
- child: _buildContent(itemsAsync, context, ref),
- );
- }
- Widget _buildContent(
- AsyncValue<List<AnnouncementModel>> itemsAsync,
- BuildContext context,
- WidgetRef ref,
- ) {
- if (itemsAsync.isReloading) {
- final oldItems = itemsAsync.valueOrNull ?? [];
- if (oldItems.isEmpty) {
- return SkeletonLoadingList(
- cardBuilder: () => const SkeletonAnnouncementCard(),
- );
- }
- return ListView.builder(
- padding: const EdgeInsets.symmetric(vertical: 8),
- itemCount: oldItems.length,
- itemBuilder: (_, i) => _buildAnnouncementCard(context, oldItems[i]),
- );
- }
- if (itemsAsync.hasError) {
- return ListView(
- children: const [
- SizedBox(height: 120),
- EmptyState(message: '加载失败'),
- ],
- );
- }
- final items = itemsAsync.requireValue;
- if (items.isEmpty) {
- return ListView(
- children: const [
- SizedBox(height: 120),
- EmptyState(message: '暂无行政公告'),
- ],
- );
- }
- return 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)),
- );
- }
- }
|