expense_detail_view_dialog.dart 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import 'package:flutter/material.dart';
  2. import 'package:tdesign_flutter/tdesign_flutter.dart';
  3. import '../../../core/i18n/app_localizations.dart';
  4. import '../../../core/theme/app_colors_extension.dart';
  5. import '../../../core/utils/amount_utils.dart';
  6. import '../expense_model.dart';
  7. /// 查看费用报销明细弹窗(只读)。
  8. ///
  9. /// 使用 [TDSlidePopupRoute] 从底部滑出,展示报销明细的所有字段。
  10. class ExpenseDetailViewDialog extends StatelessWidget {
  11. final ExpenseDetailModel detail;
  12. final AppLocalizations l10n;
  13. const ExpenseDetailViewDialog({
  14. super.key,
  15. required this.detail,
  16. required this.l10n,
  17. });
  18. /// 显示弹窗。
  19. static void show(BuildContext context, ExpenseDetailModel detail) {
  20. final l10n = AppLocalizations.of(context);
  21. FocusScope.of(context).unfocus();
  22. Navigator.push(
  23. context,
  24. TDSlidePopupRoute<void>(
  25. slideTransitionFrom: SlideTransitionFrom.bottom,
  26. isDismissible: true,
  27. builder: (_) => ExpenseDetailViewDialog(detail: detail, l10n: l10n),
  28. ),
  29. );
  30. }
  31. @override
  32. Widget build(BuildContext context) {
  33. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  34. final d = detail;
  35. final catLabel = d.categoryName.isNotEmpty
  36. ? '${d.expenseCategory}/${d.categoryName}'
  37. : d.expenseCategory;
  38. return ConstrainedBox(
  39. constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.8),
  40. child: Container(
  41. decoration: BoxDecoration(
  42. color: colors.bgCard,
  43. borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
  44. ),
  45. child: SafeArea(
  46. top: false,
  47. child: Column(
  48. mainAxisSize: MainAxisSize.min,
  49. children: [
  50. // 标题栏
  51. Padding(
  52. padding: const EdgeInsets.fromLTRB(20, 20, 12, 8),
  53. child: Row(
  54. children: [
  55. const SizedBox(width: 24),
  56. Expanded(child: Center(child: Text(l10n.get('expenseDetails'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)))),
  57. GestureDetector(
  58. onTap: () => Navigator.pop(context),
  59. child: Icon(Icons.close, color: colors.textSecondary),
  60. ),
  61. ],
  62. ),
  63. ),
  64. // 内容
  65. Flexible(
  66. child: SingleChildScrollView(
  67. padding: const EdgeInsets.all(20),
  68. child: Column(
  69. crossAxisAlignment: CrossAxisAlignment.start,
  70. children: [
  71. _row(l10n.get('expenseCategory'), catLabel, colors),
  72. _row(l10n.get('amountInclTax'), formatAmount(d.totalAmount), colors),
  73. if (d.approvedAmount > 0)
  74. _row(l10n.get('approvedAmount'), formatAmount(d.approvedAmount), colors),
  75. _row(l10n.get('amountExcludingTax'), formatAmount(d.amount), colors),
  76. if (d.taxAmount > 0)
  77. _row(l10n.get('taxAmount'), formatAmount(d.taxAmount), colors),
  78. if (d.taxRate > 0)
  79. _row(l10n.get('taxRate'), '${d.taxRate.toStringAsFixed(0)}%', colors),
  80. _row(l10n.get('acctSubject'), d.acctSubjectId.isNotEmpty ? '${d.acctSubjectId}${d.acctSubjectName.isNotEmpty ? '/${d.acctSubjectName}' : ''}' : '-', colors),
  81. _row(l10n.get('project'), d.projectId.isNotEmpty ? '${d.projectId}${d.projectName.isNotEmpty ? '/${d.projectName}' : ''}' : '-', colors),
  82. _row(l10n.get('costDept'), d.costDeptId.isNotEmpty ? '${d.costDeptId}${d.costDeptName.isNotEmpty ? '/${d.costDeptName}' : ''}' : '-', colors),
  83. _row(l10n.get('customerVendor'), d.customerVendorId.isNotEmpty ? '${d.customerVendorId}${d.customerVendorName.isNotEmpty ? '/${d.customerVendorName}' : ''}' : '-', colors),
  84. _row(l10n.get('expenseApplyNo'), d.aeNo.isNotEmpty ? d.aeNo : '-', colors),
  85. _row(l10n.get('applyDate'), d.aeDd.isNotEmpty ? _formatDate(d.aeDd) : '-', colors),
  86. _row(l10n.get('applicant'), d.sqMan.isNotEmpty ? '${d.sqMan}${d.sqManName.isNotEmpty ? '/${d.sqManName}' : ''}' : '-', colors),
  87. _row(l10n.get('bankName'), d.bankName.isNotEmpty ? d.bankName : '-', colors),
  88. _row(l10n.get('bankAccountName'), d.bankAccountName.isNotEmpty ? d.bankAccountName : '-', colors),
  89. _row(l10n.get('bankAccount'), d.bankAccount.isNotEmpty ? d.bankAccount : '-', colors),
  90. _row(l10n.get('remark'), d.remark.isNotEmpty ? d.remark : '-', colors),
  91. ],
  92. ),
  93. ),
  94. ),
  95. ],
  96. ),
  97. ),
  98. ),
  99. );
  100. }
  101. String _formatDate(String raw) {
  102. final dt = DateTime.tryParse(raw);
  103. if (dt == null) return raw;
  104. return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
  105. }
  106. Widget _row(String label, String value, AppColorsExtension colors) {
  107. return Padding(
  108. padding: const EdgeInsets.only(bottom: 16),
  109. child: Row(
  110. crossAxisAlignment: CrossAxisAlignment.start,
  111. children: [
  112. SizedBox(
  113. width: 100,
  114. child: Text(label, style: TextStyle(fontSize: 14, color: colors.textSecondary)),
  115. ),
  116. const SizedBox(width: 12),
  117. Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
  118. ],
  119. ),
  120. );
  121. }
  122. }