overtime_list_page.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:tdesign_flutter/tdesign_flutter.dart';
  5. import 'package:easy_refresh/easy_refresh.dart';
  6. import '../shell/nav_bar_config.dart';
  7. import '../../core/theme/app_colors_extension.dart';
  8. import '../../core/utils/date_utils.dart' as du;
  9. import '../../shared/widgets/list_card.dart';
  10. import '../../shared/widgets/status_tag.dart';
  11. import '../../shared/widgets/empty_state.dart';
  12. import '../../shared/widgets/skeleton_list_card.dart';
  13. import '../../shared/widgets/filter_bar.dart';
  14. import '../../core/i18n/app_localizations.dart';
  15. import 'overtime_list_controller.dart';
  16. import 'overtime_model.dart';
  17. class OvertimeListPage extends ConsumerStatefulWidget {
  18. const OvertimeListPage({super.key});
  19. @override
  20. ConsumerState<OvertimeListPage> createState() => _OvertimeListPageState();
  21. }
  22. class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
  23. with TickerProviderStateMixin {
  24. static const _tabLabels = ['全部', '草稿', '审批中', '已通过', '已拒绝'];
  25. static const _tabKeys = [
  26. '',
  27. 'draft',
  28. 'pending',
  29. 'approved',
  30. 'rejected',
  31. ];
  32. late final TabController _tabCtrl;
  33. @override
  34. void initState() {
  35. super.initState();
  36. _tabCtrl = TabController(length: _tabLabels.length, vsync: this);
  37. _tabCtrl.addListener(() {
  38. if (!_tabCtrl.indexIsChanging) {
  39. ref.read(overtimeStatusFilterProvider.notifier).state =
  40. _tabKeys[_tabCtrl.index];
  41. }
  42. });
  43. }
  44. @override
  45. void dispose() {
  46. _tabCtrl.dispose();
  47. super.dispose();
  48. }
  49. @override
  50. Widget build(BuildContext context) {
  51. final status = ref.watch(overtimeStatusFilterProvider);
  52. final dateStart = ref.watch(overtimeDateStartProvider);
  53. final dateEnd = ref.watch(overtimeDateEndProvider);
  54. final otTypeFilter = ref.watch(overtimeTypeFilterProvider);
  55. final l10n = AppLocalizations.of(context);
  56. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  57. // Sync TabController with external filter changes
  58. final targetIdx = _tabKeys.indexOf(status);
  59. if (targetIdx >= 0 &&
  60. _tabCtrl.index != targetIdx &&
  61. !_tabCtrl.indexIsChanging) {
  62. WidgetsBinding.instance.addPostFrameCallback((_) {
  63. if (mounted) _tabCtrl.animateTo(targetIdx);
  64. });
  65. }
  66. final filterGroups = [
  67. FilterGroup(title: '日期范围', type: FilterGroupType.dateRange, sections: [
  68. FilterSection(
  69. label: '起始日期',
  70. type: FilterSectionType.dateRange,
  71. startDate: dateStart,
  72. endDate: dateEnd,
  73. onStartChanged: (v) =>
  74. ref.read(overtimeDateStartProvider.notifier).state = v,
  75. onEndChanged: (v) =>
  76. ref.read(overtimeDateEndProvider.notifier).state = v,
  77. ),
  78. FilterSection(
  79. label: '结束日期',
  80. type: FilterSectionType.dateRange,
  81. startDate: dateStart,
  82. endDate: dateEnd,
  83. onStartChanged: (v) =>
  84. ref.read(overtimeDateStartProvider.notifier).state = v,
  85. onEndChanged: (v) =>
  86. ref.read(overtimeDateEndProvider.notifier).state = v,
  87. ),
  88. ]),
  89. FilterGroup(title: '其它', type: FilterGroupType.other, sections: [
  90. FilterSection(
  91. label: '加班类型',
  92. type: FilterSectionType.singleSelect,
  93. options: const [
  94. FilterOption(value: 'workday', label: '工作日加班'),
  95. FilterOption(value: 'weekend', label: '休息日加班'),
  96. FilterOption(value: 'holiday', label: '节假日加班'),
  97. ],
  98. selectedValue: otTypeFilter,
  99. onChanged: (v) =>
  100. ref.read(overtimeTypeFilterProvider.notifier).state = v,
  101. ),
  102. ]),
  103. ];
  104. final hasFilter = FilterBar.hasActiveFilter(filterGroups);
  105. final onFilterReset = () {
  106. ref.read(overtimeDateStartProvider.notifier).state = null;
  107. ref.read(overtimeDateEndProvider.notifier).state = null;
  108. ref.read(overtimeTypeFilterProvider.notifier).state = null;
  109. };
  110. ref
  111. .read(navBarConfigProvider.notifier)
  112. .update(
  113. NavBarConfig(
  114. title: l10n.get('overtimeList'),
  115. showBack: true,
  116. showRight: true,
  117. rightWidget: GestureDetector(
  118. onTap: () => FilterBar.show(
  119. context,
  120. groups: filterGroups,
  121. onReset: onFilterReset,
  122. onConfirm: () {},
  123. ),
  124. child: Stack(
  125. children: [
  126. Icon(TDIcons.filter, color: hasFilter ? colors.primary : colors.textPrimary),
  127. if (hasFilter)
  128. Positioned(
  129. right: -2,
  130. top: -2,
  131. child: Container(
  132. width: 6,
  133. height: 6,
  134. decoration: BoxDecoration(
  135. color: colors.danger,
  136. shape: BoxShape.circle,
  137. ),
  138. ),
  139. ),
  140. ],
  141. ),
  142. ),
  143. onBack: () => context.pop(),
  144. ),
  145. );
  146. return Column(
  147. children: [
  148. Container(
  149. color: colors.bgCard,
  150. padding: const EdgeInsets.symmetric(horizontal: 8),
  151. child: TDTabBar(
  152. tabs: _tabLabels.map((l) => TDTab(text: l)).toList(),
  153. controller: _tabCtrl,
  154. isScrollable: true,
  155. labelColor: colors.primary,
  156. unselectedLabelColor: colors.textSecondary,
  157. outlineType: TDTabBarOutlineType.filled,
  158. showIndicator: true,
  159. indicatorColor: colors.primary,
  160. indicatorHeight: 3,
  161. dividerHeight: 0,
  162. labelPadding: const EdgeInsets.symmetric(horizontal: 12),
  163. onTap: (index) {
  164. ref.invalidate(overtimeListProvider);
  165. ref.read(overtimeStatusFilterProvider.notifier).state =
  166. _tabKeys[index];
  167. },
  168. ),
  169. ),
  170. Expanded(
  171. child: Container(
  172. color: colors.bgPage,
  173. child: TabBarView(
  174. controller: _tabCtrl,
  175. children: List.generate(_tabKeys.length, (tabIdx) {
  176. return _buildTabContent(tabIdx);
  177. }),
  178. ),
  179. ),
  180. ),
  181. ],
  182. );
  183. }
  184. Widget _buildTabContent(int tabIdx) {
  185. return _OvertimeTabContent(statusKey: _tabKeys[tabIdx]);
  186. }
  187. }
  188. class _OvertimeTabContent extends ConsumerWidget {
  189. final String statusKey;
  190. const _OvertimeTabContent({required this.statusKey});
  191. @override
  192. Widget build(BuildContext context, WidgetRef ref) {
  193. final itemsAsync = ref.watch(overtimeListProvider(statusKey));
  194. if (itemsAsync.isLoading && !itemsAsync.hasValue) {
  195. return const SkeletonLoadingList();
  196. }
  197. return EasyRefresh(
  198. header: TDRefreshHeader(),
  199. onRefresh: () async {
  200. ref.read(overtimeRefreshProvider.notifier).state++;
  201. },
  202. child: _buildContent(itemsAsync, context, ref),
  203. );
  204. }
  205. Widget _buildContent(
  206. AsyncValue<List<OvertimeModel>> itemsAsync,
  207. BuildContext context,
  208. WidgetRef ref,
  209. ) {
  210. if (itemsAsync.isReloading) {
  211. final oldItems = itemsAsync.valueOrNull ?? [];
  212. if (oldItems.isEmpty) return const SkeletonLoadingList();
  213. return ListView.builder(
  214. padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
  215. itemCount: oldItems.length,
  216. itemBuilder: (_, i) => Padding(
  217. padding: const EdgeInsets.only(bottom: 16),
  218. child: ListCard(
  219. cardNo: oldItems[i].applicationNo,
  220. description: '${oldItems[i].otType} · ${oldItems[i].compensationType}',
  221. amount: '${oldItems[i].otHours.toStringAsFixed(1)}小时',
  222. date: du.DateUtils.formatDate(oldItems[i].otDate),
  223. statusTag: StatusTag.fromStatus(oldItems[i].status),
  224. onTap: () => context.push('/overtime/detail/${oldItems[i].id}'),
  225. ),
  226. ),
  227. );
  228. }
  229. if (itemsAsync.hasError) {
  230. return ListView(
  231. children: const [
  232. SizedBox(height: 120),
  233. EmptyState(message: '加载失败'),
  234. ],
  235. );
  236. }
  237. final items = itemsAsync.requireValue;
  238. if (items.isEmpty) {
  239. return ListView(
  240. children: const [
  241. SizedBox(height: 120),
  242. EmptyState(message: '暂无加班记录'),
  243. ],
  244. );
  245. }
  246. return ListView.builder(
  247. padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
  248. itemCount: items.length,
  249. itemBuilder: (_, i) => Padding(
  250. padding: const EdgeInsets.only(bottom: 16),
  251. child: ListCard(
  252. cardNo: items[i].applicationNo,
  253. description: '${items[i].otType} · ${items[i].compensationType}',
  254. amount: '${items[i].otHours.toStringAsFixed(1)}小时',
  255. date: du.DateUtils.formatDate(items[i].otDate),
  256. statusTag: StatusTag.fromStatus(items[i].status),
  257. onTap: () => context.push('/overtime/detail/${items[i].id}'),
  258. ),
  259. ),
  260. );
  261. }
  262. }