expense_list_page.dart 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../shell/nav_bar_config.dart';
  5. import '../../core/utils/date_utils.dart' as du;
  6. import '../../core/utils/responsive.dart';
  7. import '../../shared/widgets/filter_tabs.dart';
  8. import '../../shared/widgets/list_card.dart';
  9. import '../../shared/widgets/status_tag.dart';
  10. import '../../shared/widgets/empty_state.dart';
  11. import '../../shared/widgets/loading_widget.dart';
  12. import '../../core/i18n/app_localizations.dart';
  13. import 'expense_list_controller.dart';
  14. class ExpenseListPage extends ConsumerStatefulWidget {
  15. const ExpenseListPage({super.key});
  16. @override
  17. ConsumerState<ExpenseListPage> createState() => _ExpenseListPageState();
  18. }
  19. class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
  20. final _scrollCtrl = ScrollController();
  21. static const _tabLabels = ['全部', '草稿', '审批中', '已通过', '已拒绝', '已撤回'];
  22. static const _tabKeys = [
  23. '',
  24. 'draft',
  25. 'pending',
  26. 'approved',
  27. 'rejected',
  28. 'revoked',
  29. ];
  30. @override
  31. void initState() {
  32. super.initState();
  33. _scrollCtrl.addListener(_onScroll);
  34. }
  35. int _getSelectedIndex(String currentKey) {
  36. final idx = _tabKeys.indexOf(currentKey);
  37. return idx >= 0 ? idx : 0;
  38. }
  39. void _onScroll() {
  40. if (_scrollCtrl.position.pixels >=
  41. _scrollCtrl.position.maxScrollExtent - 100) {
  42. ref.read(expensePageProvider.notifier).state++;
  43. }
  44. }
  45. @override
  46. void dispose() {
  47. _scrollCtrl.removeListener(_onScroll);
  48. _scrollCtrl.dispose();
  49. super.dispose();
  50. }
  51. @override
  52. Widget build(BuildContext context) {
  53. final status = ref.watch(expenseStatusFilterProvider);
  54. final itemsAsync = ref.watch(expenseListProvider);
  55. final r = ResponsiveHelper.of(context);
  56. final l10n = AppLocalizations.of(context);
  57. ref
  58. .read(navBarConfigProvider.notifier)
  59. .update(
  60. NavBarConfig(
  61. title: l10n.get('expenseList'),
  62. showBack: true,
  63. onBack: () => context.pop(),
  64. ),
  65. );
  66. return Column(
  67. children: [
  68. FilterTabs(
  69. tabs: _tabLabels,
  70. selectedIndex: _getSelectedIndex(status),
  71. onChanged: (index) {
  72. ref.read(expenseStatusFilterProvider.notifier).state =
  73. _tabKeys[index];
  74. },
  75. ),
  76. Expanded(
  77. child: Center(
  78. child: ConstrainedBox(
  79. constraints: BoxConstraints(maxWidth: r.listMaxWidth),
  80. child: itemsAsync.when(
  81. loading: () => const LoadingWidget(),
  82. error: (_, __) => const EmptyState(message: '加载失败'),
  83. data: (items) => items.isEmpty
  84. ? const EmptyState(message: '暂无报销单')
  85. : RefreshIndicator(
  86. onRefresh: () async {
  87. ref.invalidate(expenseListProvider);
  88. },
  89. child: ListView.builder(
  90. controller: _scrollCtrl,
  91. padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
  92. itemCount: items.length,
  93. itemBuilder: (_, i) => Padding(
  94. padding: const EdgeInsets.only(bottom: 16),
  95. child: ListCard(
  96. cardNo: items[i].reportNo,
  97. amount:
  98. '¥${items[i].totalAmount.toStringAsFixed(2)}',
  99. description:
  100. '${items[i].expenseType} — ${items[i].applicantName}',
  101. date: du.DateUtils.formatDate(
  102. items[i].createTime,
  103. ),
  104. statusTag: StatusTag.fromStatus(items[i].status),
  105. onTap: () => context.push(
  106. '/expense/detail/${items[i].id}',
  107. ),
  108. ),
  109. ),
  110. ),
  111. ),
  112. ),
  113. ),
  114. ),
  115. ),
  116. ],
  117. );
  118. }
  119. }