expense_list_page.dart 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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.dart';
  8. import '../../core/utils/date_utils.dart' as du;
  9. import '../../core/utils/responsive.dart';
  10. import '../../shared/widgets/list_card.dart';
  11. import '../../shared/widgets/status_tag.dart';
  12. import '../../shared/widgets/empty_state.dart';
  13. import '../../shared/widgets/loading_widget.dart';
  14. import '../../core/i18n/app_localizations.dart';
  15. import 'expense_list_controller.dart';
  16. class ExpenseListPage extends ConsumerStatefulWidget {
  17. const ExpenseListPage({super.key});
  18. @override
  19. ConsumerState<ExpenseListPage> createState() => _ExpenseListPageState();
  20. }
  21. class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
  22. with TickerProviderStateMixin {
  23. static const _tabLabels = ['全部', '草稿', '审批中', '已通过', '已拒绝', '已撤回'];
  24. static const _tabKeys = [
  25. '',
  26. 'draft',
  27. 'pending',
  28. 'approved',
  29. 'rejected',
  30. 'revoked',
  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(expenseStatusFilterProvider.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(expenseStatusFilterProvider);
  52. final l10n = AppLocalizations.of(context);
  53. // Sync TabController with external filter changes
  54. final targetIdx = _tabKeys.indexOf(status);
  55. if (targetIdx >= 0 &&
  56. _tabCtrl.index != targetIdx &&
  57. !_tabCtrl.indexIsChanging) {
  58. WidgetsBinding.instance.addPostFrameCallback((_) {
  59. if (mounted) _tabCtrl.animateTo(targetIdx);
  60. });
  61. }
  62. ref
  63. .read(navBarConfigProvider.notifier)
  64. .update(
  65. NavBarConfig(
  66. title: l10n.get('expenseList'),
  67. showBack: true,
  68. onBack: () => context.pop(),
  69. ),
  70. );
  71. return Column(
  72. children: [
  73. Container(
  74. color: AppColors.bgCard,
  75. padding: const EdgeInsets.symmetric(horizontal: 8),
  76. child: TDTabBar(
  77. tabs: _tabLabels.map((l) => TDTab(text: l)).toList(),
  78. controller: _tabCtrl,
  79. isScrollable: true,
  80. labelColor: AppColors.primary,
  81. unselectedLabelColor: AppColors.textSecondary,
  82. outlineType: TDTabBarOutlineType.filled,
  83. showIndicator: true,
  84. indicatorColor: AppColors.primary,
  85. indicatorHeight: 3,
  86. labelPadding: const EdgeInsets.symmetric(horizontal: 12),
  87. onTap: (index) {
  88. ref.read(expenseStatusFilterProvider.notifier).state =
  89. _tabKeys[index];
  90. },
  91. ),
  92. ),
  93. Expanded(
  94. child: TabBarView(
  95. controller: _tabCtrl,
  96. children: List.generate(_tabKeys.length, (tabIdx) {
  97. return _buildTabContent(tabIdx);
  98. }),
  99. ),
  100. ),
  101. ],
  102. );
  103. }
  104. Widget _buildTabContent(int tabIdx) {
  105. final r = ResponsiveHelper.of(context);
  106. return Center(
  107. child: ConstrainedBox(
  108. constraints: BoxConstraints(maxWidth: r.listMaxWidth),
  109. child: _ExpenseTabContent(statusKey: _tabKeys[tabIdx]),
  110. ),
  111. );
  112. }
  113. }
  114. class _ExpenseTabContent extends ConsumerWidget {
  115. final String statusKey;
  116. const _ExpenseTabContent({required this.statusKey});
  117. @override
  118. Widget build(BuildContext context, WidgetRef ref) {
  119. final itemsAsync = ref.watch(expenseListProvider);
  120. return itemsAsync.when(
  121. loading: () => const LoadingWidget(),
  122. error: (_, _) => const EmptyState(message: '加载失败'),
  123. data: (items) {
  124. if (items.isEmpty) {
  125. return EasyRefresh(
  126. header: TDRefreshHeader(),
  127. onRefresh: () async {
  128. ref.invalidate(expenseListProvider);
  129. },
  130. child: ListView(
  131. children: const [
  132. SizedBox(height: 120),
  133. EmptyState(message: '暂无报销单'),
  134. ],
  135. ),
  136. );
  137. }
  138. return EasyRefresh(
  139. header: TDRefreshHeader(),
  140. onRefresh: () async {
  141. ref.invalidate(expenseListProvider);
  142. },
  143. child: ListView.builder(
  144. padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
  145. itemCount: items.length,
  146. itemBuilder: (_, i) => Padding(
  147. padding: const EdgeInsets.only(bottom: 16),
  148. child: ListCard(
  149. cardNo: items[i].reportNo,
  150. amount: '¥${items[i].totalAmount.toStringAsFixed(2)}',
  151. description:
  152. '${items[i].expenseType} — ${items[i].applicantName}',
  153. date: du.DateUtils.formatDate(
  154. items[i].createTime,
  155. ),
  156. statusTag: StatusTag.fromStatus(items[i].status),
  157. onTap: () => context.push(
  158. '/expense/detail/${items[i].id}',
  159. ),
  160. ),
  161. ),
  162. ),
  163. );
  164. },
  165. );
  166. }
  167. }