expense_detail_view_dialog.dart 5.5 KB

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