Selaa lähdekoodia

doing #19407 完善费用申请单

chengc 2 viikkoa sitten
vanhempi
commit
fd9fe5a427

+ 4 - 4
lib/features/expense_apply/expense_apply_create_page.dart

@@ -607,7 +607,7 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
                               d.acctSubjectName.isNotEmpty) ...[
                             const SizedBox(height: 4),
                             Text(
-                              d.acctSubjectName,
+                              '${l10n.get('acctSubject')}: ${d.acctSubjectId}/${d.acctSubjectName}',
                               maxLines: 1,
                               overflow: TextOverflow.ellipsis,
                               style: TextStyle(
@@ -619,7 +619,7 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
                           if (d.projectId > 0 && d.projectName.isNotEmpty) ...[
                             const SizedBox(height: 4),
                             Text(
-                              '${d.projectId}/${d.projectName}',
+                              '${l10n.get('project')}: ${d.projectId}/${d.projectName}',
                               maxLines: 1,
                               overflow: TextOverflow.ellipsis,
                               style: TextStyle(
@@ -632,7 +632,7 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
                               d.costDeptName.isNotEmpty) ...[
                             const SizedBox(height: 4),
                             Text(
-                              '${d.costDeptId}/${d.costDeptName}',
+                              '${l10n.get('costDept')}: ${d.costDeptId}/${d.costDeptName}',
                               maxLines: 1,
                               overflow: TextOverflow.ellipsis,
                               style: TextStyle(
@@ -645,7 +645,7 @@ class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
                               d.endDate.isNotEmpty) ...[
                             const SizedBox(height: 4),
                             Text(
-                              '${d.startDate} ~ ${d.endDate}',
+                              '${l10n.get('estimatedDate')}: ${d.startDate} ~ ${d.endDate}',
                               maxLines: 1,
                               overflow: TextOverflow.ellipsis,
                               style: TextStyle(

+ 19 - 11
lib/features/expense_apply/expense_apply_detail_page.dart

@@ -12,6 +12,7 @@ import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import 'expense_apply_model.dart';
+import 'widgets/expense_apply_detail_view_dialog.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../shared/models/bill_attachment.dart';
 import 'expense_apply_api.dart';
@@ -159,9 +160,9 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
         const SizedBox(height: 16),
         FormFieldRow(label: l10n.get('date'), value: du.DateUtils.formatDate(app.createTime), readOnly: true, showArrow: false),
         const SizedBox(height: 16),
-        FormFieldRow(label: l10n.get('applicant'), value: app.applicantName, readOnly: true, showArrow: false),
+        FormFieldRow(label: l10n.get('applicant'), value: app.applicantId.isNotEmpty ? '${app.applicantId}/${app.applicantName}' : app.applicantName, readOnly: true, showArrow: false),
         const SizedBox(height: 16),
-        FormFieldRow(label: l10n.get('department'), value: app.deptName, readOnly: true, showArrow: false),
+        FormFieldRow(label: l10n.get('department'), value: app.deptId.isNotEmpty ? '${app.deptId}/${app.deptName}' : app.deptName, readOnly: true, showArrow: false),
         const SizedBox(height: 16),
         SizedBox(
           height: 24,
@@ -171,7 +172,7 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
           ]),
         ),
         const SizedBox(height: 16),
-        FormFieldRow(label: l10n.get('applyReason'), value: app.purpose, readOnly: true, showArrow: false),
+        FormFieldRow(label: l10n.get('applyReason'), value: app.purpose, readOnly: true, showArrow: false, bold: true),
         const SizedBox(height: 16),
         FormFieldRow(label: l10n.get('remark'), value: app.remark.isNotEmpty ? app.remark : '-', readOnly: true, showArrow: false),
       ],
@@ -199,11 +200,13 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
             final catLabel = d.categoryName.isNotEmpty
                 ? '${d.expenseCategory}/${d.categoryName}'
                 : d.expenseCategory;
-            return Container(
-              margin: const EdgeInsets.symmetric(vertical: 8),
-              padding: const EdgeInsets.all(12),
-              decoration: BoxDecoration(color: colors.bgPage, borderRadius: BorderRadius.circular(8)),
-              child: Row(
+            return GestureDetector(
+              onTap: () => _showExpenseDetailDialog(context, d),
+              child: Container(
+                margin: const EdgeInsets.symmetric(vertical: 8),
+                padding: const EdgeInsets.all(12),
+                decoration: BoxDecoration(color: colors.bgPage, borderRadius: BorderRadius.circular(8)),
+                child: Row(
                 crossAxisAlignment: CrossAxisAlignment.center,
                 children: [
                   Expanded(
@@ -231,7 +234,7 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
                         if (d.acctSubjectId.isNotEmpty) ...[
                           const SizedBox(height: 4),
                           Text(
-                            '${l10n.get('acctSubject')}: ${d.acctSubjectId}${d.acctSubjectName.isNotEmpty ? '/$d.acctSubjectName' : ''}',
+                            '${l10n.get('acctSubject')}: ${d.acctSubjectId}${d.acctSubjectName.isNotEmpty ? '/${d.acctSubjectName}' : ''}',
                             maxLines: 1,
                             overflow: TextOverflow.ellipsis,
                             style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary),
@@ -240,7 +243,7 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
                         if (d.projectId.isNotEmpty && d.projectName.isNotEmpty) ...[
                           const SizedBox(height: 4),
                           Text(
-                            '${d.projectId}/${d.projectName}',
+                            '${l10n.get('project')}: ${d.projectId}/${d.projectName}',
                             maxLines: 1,
                             overflow: TextOverflow.ellipsis,
                             style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary),
@@ -287,7 +290,8 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
                   ),
                 ],
               ),
-            );
+            ),
+          );
           }),
         if (app.details.isNotEmpty) ...[
           const SizedBox(height: 8),
@@ -429,6 +433,10 @@ class _ExpenseApplyDetailPageState extends ConsumerState<ExpenseApplyDetailPage>
       child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: color)),
     );
   }
+
+  void _showExpenseDetailDialog(BuildContext context, ExpenseApplyDetailModel d) {
+    ExpenseApplyDetailViewDialog.show(context, d);
+  }
 }
 
 /// 附件缩略图 — 自动调用 DownloadAttachment 加载图片

+ 1 - 1
lib/features/expense_apply/expense_apply_model.dart

@@ -78,7 +78,7 @@ class ExpenseApplyModel {
       deptId: json['dept'] as String? ?? '',
       deptName: json['deptName'] as String? ?? json['dep'] as String? ?? '',
       estimatedAmount: (json['estimatedAmount'] as num?)?.toDouble() ?? 0.0,
-      urgency: json['urgency'] as String? ?? 'normal',
+      urgency: json['urgency'] as String? ?? json['priority'] as String? ?? 'normal',
       purpose: json['reason'] as String? ?? json['purpose'] as String? ?? '',
       remark: json['rem'] as String? ?? '',
       effectiveDate: json['effDd'] != null

+ 108 - 0
lib/features/expense_apply/widgets/expense_apply_detail_view_dialog.dart

@@ -0,0 +1,108 @@
+import 'package:flutter/material.dart';
+import 'package:tdesign_flutter/tdesign_flutter.dart';
+import '../../../core/i18n/app_localizations.dart';
+import '../../../core/theme/app_colors_extension.dart';
+import '../../../core/utils/date_utils.dart' as du;
+import '../expense_apply_model.dart';
+
+/// 查看费用明细弹窗(只读)。
+///
+/// 使用 [TDSlidePopupRoute] 从底部滑出,展示所有明细字段。
+class ExpenseApplyDetailViewDialog extends StatelessWidget {
+  final ExpenseApplyDetailModel detail;
+  final AppLocalizations l10n;
+
+  const ExpenseApplyDetailViewDialog({
+    super.key,
+    required this.detail,
+    required this.l10n,
+  });
+
+  /// 显示弹窗。
+  static void show(BuildContext context, ExpenseApplyDetailModel detail) {
+    final l10n = AppLocalizations.of(context);
+    FocusScope.of(context).unfocus();
+    Navigator.push(
+      context,
+      TDSlidePopupRoute<void>(
+        slideTransitionFrom: SlideTransitionFrom.bottom,
+        isDismissible: true,
+        builder: (_) => ExpenseApplyDetailViewDialog(detail: detail, l10n: l10n),
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final d = detail;
+
+    final catLabel = d.categoryName.isNotEmpty
+        ? '${d.expenseCategory}/${d.categoryName}'
+        : d.expenseCategory;
+
+    return Container(
+      decoration: BoxDecoration(
+        color: colors.bgCard,
+        borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
+      ),
+      child: SafeArea(
+        top: false,
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            // 标题栏
+            Padding(
+              padding: const EdgeInsets.fromLTRB(20, 20, 12, 8),
+              child: Row(
+                children: [
+                  Expanded(child: Center(child: Text(l10n.get('expenseDetails'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)))),
+                  GestureDetector(
+                    onTap: () => Navigator.pop(context),
+                    child: Icon(Icons.close, color: colors.textSecondary),
+                  ),
+                ],
+              ),
+            ),
+            // 内容
+            Flexible(
+              child: SingleChildScrollView(
+                padding: const EdgeInsets.all(20),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    _row(l10n.get('expenseCategory'), catLabel, colors),
+                    _row(l10n.get('estimatedAmount'), '¥${d.estimatedAmount.toStringAsFixed(2)}', colors),
+                    _row(l10n.get('acctSubject'), d.acctSubjectId.isNotEmpty ? '${d.acctSubjectId}${d.acctSubjectName.isNotEmpty ? '/${d.acctSubjectName}' : ''}' : '-', colors),
+                    _row(l10n.get('project'), d.projectId.isNotEmpty && d.projectName.isNotEmpty ? '${d.projectId}/${d.projectName}' : '-', colors),
+                    _row(l10n.get('costDept'), d.costDeptId.isNotEmpty && d.costDeptName.isNotEmpty ? '${d.costDeptId}/${d.costDeptName}' : '-', colors),
+                    _row(l10n.get('estimatedDate'), d.estimatedStartDate != null ? '${du.DateUtils.formatDate(d.estimatedStartDate!)}${d.estimatedEndDate != null ? ' ~ ${du.DateUtils.formatDate(d.estimatedEndDate!)}' : ''}' : '-', colors),
+                    _row(l10n.get('expenseNo'), d.bxNo.isNotEmpty ? d.bxNo : '-', colors),
+                    _row(l10n.get('remark'), d.remark.isNotEmpty ? d.remark : '-', colors),
+                  ],
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _row(String label, String value, AppColorsExtension colors) {
+    return Padding(
+      padding: const EdgeInsets.only(bottom: 16),
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          SizedBox(
+            width: 100,
+            child: Text(label, style: TextStyle(fontSize: 14, color: colors.textSecondary)),
+          ),
+          const SizedBox(width: 12),
+          Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
+        ],
+      ),
+    );
+  }
+}

+ 31 - 43
lib/shared/widgets/form_field_row.dart

@@ -1,6 +1,7 @@
 import '../../core/theme/app_colors.dart';
 import 'package:flutter/material.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/i18n/app_localizations.dart';
 
 /// 表单字段行,左侧标签 + 右侧值 + 可选箭头。
 ///
@@ -14,6 +15,7 @@ class FormFieldRow extends StatelessWidget {
   final bool required;
   final VoidCallback? onTap;
   final VoidCallback? onClear;
+  final bool bold;
 
   const FormFieldRow({
     super.key,
@@ -25,6 +27,7 @@ class FormFieldRow extends StatelessWidget {
     this.required = false,
     this.onTap,
     this.onClear,
+    this.bold = false,
   });
 
   @override
@@ -40,56 +43,41 @@ class FormFieldRow extends StatelessWidget {
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: [
             Text.rich(
-              TextSpan(
+              TextSpan(children: [
+                TextSpan(text: label, style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.textSecondary)),
+                if (required) TextSpan(text: ' *', style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.danger)),
+              ]),
+            ),
+            const SizedBox(width: 12),
+            Expanded(
+              child: Row(
+                mainAxisSize: MainAxisSize.min,
                 children: [
-                  TextSpan(
-                    text: label,
-                    style: TextStyle(
-                      fontSize: AppFontSizes.subtitle,
-                      color: colors.textSecondary,
+                  Expanded(
+                    child: Text(
+                      hasValue ? value! : (hint ?? AppLocalizations.of(context).get('pleaseSelectOrFill')),
+                      maxLines: 1,
+                      softWrap: false,
+                      overflow: TextOverflow.ellipsis,
+                      textAlign: TextAlign.end,
+                      style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: bold ? FontWeight.w600 : FontWeight.normal, color: hasValue ? colors.textPrimary : colors.textPlaceholder),
                     ),
                   ),
-                  if (required)
-                    TextSpan(
-                      text: ' *',
-                      style: TextStyle(
-                        fontSize: AppFontSizes.subtitle,
-                        color: colors.danger,
+                  if (hasValue && onClear != null)
+                    GestureDetector(
+                      onTap: onClear,
+                      child: const Padding(
+                        padding: EdgeInsets.only(left: 4),
+                        child: Icon(Icons.close, size: 16, color: Color(0xFFBBBBBB)),
                       ),
-                    ),
+                    )
+                  else if (showArrow && !readOnly) ...[
+                    const SizedBox(width: 4),
+                    Icon(Icons.chevron_right, size: 14, color: colors.textPlaceholder),
+                  ],
                 ],
               ),
             ),
-            Row(
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                Text(
-                  hasValue ? value! : (hint ?? '请选择或填写'),
-                  style: TextStyle(
-                    fontSize: AppFontSizes.subtitle,
-                    color: hasValue
-                        ? (readOnly ? colors.textPrimary : colors.textPrimary)
-                        : colors.textPlaceholder,
-                  ),
-                ),
-                if (hasValue && onClear != null)
-                  GestureDetector(
-                    onTap: onClear,
-                    child: const Padding(
-                      padding: EdgeInsets.only(left: 8),
-                      child: Icon(Icons.close, size: 16, color: Color(0xFFBBBBBB)),
-                    ),
-                  )
-                else if (showArrow && !readOnly) ...[
-                  const SizedBox(width: 4),
-                  Icon(
-                    Icons.chevron_right,
-                    size: 14,
-                    color: colors.textPlaceholder,
-                  ),
-                ],
-              ],
-            ),
           ],
         ),
       ),