expense_detail_page.dart 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:tdesign_flutter/tdesign_flutter.dart';
  4. import 'package:go_router/go_router.dart';
  5. import '../../core/theme/app_colors.dart';
  6. import '../../shared/models/user_model.dart';
  7. import '../../shared/widgets/app_card.dart';
  8. import '../../shared/widgets/approval_timeline.dart';
  9. import '../../shared/widgets/approval_actions.dart';
  10. import 'expense_model.dart';
  11. import 'expense_list_controller.dart';
  12. class ExpenseDetailPage extends ConsumerWidget {
  13. final String id;
  14. const ExpenseDetailPage({super.key, required this.id});
  15. @override
  16. Widget build(BuildContext context, WidgetRef ref) {
  17. final expense = mockExpenses.firstWhere((e) => e.id == id,
  18. orElse: () => mockExpenses.first);
  19. return Scaffold(
  20. appBar: TDNavBar(
  21. title: '报销单详情',
  22. titleColor: Colors.white,
  23. backgroundColor: const Color(0xFF00ABF3),
  24. centerTitle: false,
  25. ),
  26. body: Column(children: [
  27. Expanded(
  28. child: SingleChildScrollView(
  29. padding: const EdgeInsets.all(12),
  30. child: Column(children: [
  31. _buildStatusHeader(expense),
  32. const SizedBox(height: 12),
  33. _buildInfoSection(expense),
  34. const SizedBox(height: 12),
  35. _buildDetailSection(expense),
  36. const SizedBox(height: 12),
  37. if (expense.approvalRecords.isNotEmpty || expense.approvalChain.isNotEmpty)
  38. AppCard(
  39. child: ApprovalTimeline(
  40. records: expense.approvalRecords,
  41. chain: expense.approvalChain,
  42. currentApproverId: expense.currentApproverId,
  43. ),
  44. ),
  45. ]),
  46. ),
  47. ),
  48. ApprovalActions(
  49. status: expense.status,
  50. userRole: UserRole.employee,
  51. onApprove: () {},
  52. onReject: () {},
  53. onEdit: () => context.push('/expense/apply?id=${expense.id}'),
  54. onWithdraw: () {},
  55. ),
  56. ]),
  57. );
  58. }
  59. Widget _buildStatusHeader(ExpenseModel expense) {
  60. final (icon, color, text) = switch (expense.status) {
  61. 'approved' => (Icons.check_circle, AppColors.success, '已通过'),
  62. 'rejected' => (Icons.cancel, AppColors.error, '已拒绝'),
  63. 'draft' => (Icons.edit, AppColors.textHint, '草稿'),
  64. _ => (Icons.schedule, AppColors.warning, '待审批'),
  65. };
  66. return Container(
  67. padding: const EdgeInsets.all(20),
  68. decoration: BoxDecoration(
  69. color: color.withValues(alpha: 0.08),
  70. borderRadius: BorderRadius.circular(12)),
  71. child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
  72. Icon(icon, color: color, size: 36),
  73. const SizedBox(width: 10),
  74. Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  75. Text(text, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: color)),
  76. if (expense.status == 'pending')
  77. Text('当前审批人:${expense.currentApproverId}',
  78. style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)),
  79. ]),
  80. ]),
  81. );
  82. }
  83. Widget _buildInfoSection(ExpenseModel expense) {
  84. return _buildDetailCard('基本信息', [
  85. TDCell(title: '报销单号', note: expense.reportNo, showBottomBorder: true),
  86. TDCell(title: '申请人', note: '${expense.applicantName} · ${expense.deptName}', showBottomBorder: true),
  87. TDCell(title: '报销类型', note: expense.expenseType, showBottomBorder: true),
  88. TDCell(title: '发票数量', note: '${expense.invoiceCount}张', showBottomBorder: true),
  89. TDCell(title: '总金额', note: '¥${expense.totalAmount.toStringAsFixed(2)}', showBottomBorder: expense.remark.isNotEmpty),
  90. if (expense.remark.isNotEmpty)
  91. TDCell(title: '备注', note: expense.remark, showBottomBorder: false),
  92. ]);
  93. }
  94. Widget _buildDetailSection(ExpenseModel expense) {
  95. if (expense.details.isEmpty) return const SizedBox.shrink();
  96. return _buildDetailCard('报销明细', [
  97. ...expense.details.map((d) => TDCell(
  98. titleWidget: Text(d.expenseDesc, style: const TextStyle(fontSize: 13, color: AppColors.textPrimary)),
  99. description: d.expenseDate.toString().substring(0, 10),
  100. note: '¥${d.totalAmount.toStringAsFixed(2)}',
  101. noteWidget: Text('¥${d.totalAmount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.primary)),
  102. showBottomBorder: true,
  103. )),
  104. Padding(
  105. padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
  106. child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  107. const Text('合计', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
  108. Text('¥${expense.totalAmount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.primary)),
  109. ]),
  110. ),
  111. ]);
  112. }
  113. Widget _buildDetailCard(String title, List<Widget> cells) {
  114. return Container(
  115. margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
  116. decoration: BoxDecoration(
  117. color: AppColors.cardWhite,
  118. borderRadius: BorderRadius.circular(10),
  119. boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 4, offset: const Offset(0, 1))],
  120. ),
  121. child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  122. Padding(
  123. padding: const EdgeInsets.fromLTRB(14, 12, 14, 8),
  124. child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
  125. ),
  126. ...cells,
  127. ]),
  128. );
  129. }
  130. }