Selaa lähdekoodia

fix #19900 #19888 加班申请单、用车申请单

chengc 2 viikkoa sitten
vanhempi
commit
b208e06aa5

+ 9 - 6
assets/i18n/en.json

@@ -98,8 +98,7 @@
     "statusDisabled": "Disabled",
     "statusWaitApprove": "Pending",
     "statusWaitPay": "Pending Payment",
-    "editApply": "Edit Pre-Application",
-    "editExpense": "Edit Expense Report",
+    "editApply": "Modify Expense Application",
     "statusTransferred": "Transferred",
     "statusClosed": "Closed",
     "statusPendingReview": "Pending Review",
@@ -131,6 +130,7 @@
     "saveFailed": "Save failed",
     "submitSuccess": "Submitted successfully",
     "billCreated": "Bill created",
+    "billModified": "Bill modified",
     "submitFailed": "Submit Failed",
     "requestFailed": "Request Failed",
     "unknownError": "Unknown error",
@@ -423,7 +423,7 @@
   "expense": {
     "expenseList": "Expense List",
     "expenseDetail": "Expense Details",
-    "editExpense": "Edit Expense",
+    "editExpense": "Modify Expense",
     "expenseApply": "Reimbursement",
     "searchExpense": "Search expense No. or applicant",
     "noExpenses": "No expense reports",
@@ -472,6 +472,7 @@
     "enterExpenseName": "Enter expense item name",
     "description": "Description",
     "addExpenseDetail": "Add Expense Detail",
+    "editExpenseDetail": "Modify Expense Detail",
     "invoiceUpload": "Invoice Upload",
     "maxInvoices": "Up to 9 invoices",
     "invoiceAttachment": "Invoice Attachment",
@@ -552,7 +553,7 @@
     "weekend": "Weekend",
     "holiday": "Holiday",
     "overtimeRequest": "Overtime Request",
-    "editOvertime": "Edit Overtime",
+    "editOvertime": "Modify Overtime",
     "overtimeApplyNo": "OT Application No.",
     "employee": "Employee",
     "dep": "Department",
@@ -581,8 +582,9 @@
     "confirmEdit": "Confirm Edit",
     "overtimeHoursPositive": "Overtime hours must be greater than 0",
     "addOvertimeDetail": "Add Overtime Detail",
-    "editOvertimeDetail": "Edit Overtime Detail",
+    "editOvertimeDetail": "Modify Overtime Detail",
     "addDetail": "Add Detail",
+    "editDetail": "Modify Detail",
     "overtimeDetails": "Overtime Details",
     "noDetailHint": "Please add overtime detail",
     "selectDept": "Please select department first",
@@ -680,7 +682,7 @@
     "mileageInvalid": "Return mileage cannot be less than departure mileage",
     "business": "Official",
     "vehicleRequest": "Vehicle Request",
-    "editVehicle": "Edit Vehicle",
+    "editVehicle": "Modify Vehicle",
     "vehicleApplyNo": "Vehicle Application No.",
     "applicant": "Applicant",
     "companionInfo": "Companion Info",
@@ -712,6 +714,7 @@
     "vehicleReturned": "Vehicle Returned",
     "selectLicensePlateHint": "Please select license plate",
     "selectDepartTime": "Please select estimated departure time",
+    "selectApplicantHint": "Please select applicant",
     "selectReturnTime": "Please select estimated return time",
     "withdraw": "Withdraw",
     "withdrawn": "Withdrawn",

+ 8 - 5
assets/i18n/zh_CN.json

@@ -96,7 +96,6 @@
     "statusWaitApprove": "待审核",
     "statusWaitPay": "待付款",
     "editApply": "修改报销申请",
-    "editExpense": "修改报销",
     "statusTransferred": "已转收支",
     "statusClosed": "已结案",
     "statusPendingReview": "待审核",
@@ -128,6 +127,7 @@
     "saveFailed": "保存失败",
     "submitSuccess": "提交成功",
     "billCreated": "单据已生成",
+    "billModified": "单据修改成功",
     "submitFailed": "提交失败",
     "requestFailed": "请求失败",
     "unknownError": "未知错误",
@@ -329,6 +329,7 @@
     "overBudgetTriggerApproval": "您的申请金额已超支,提交后将自动触发高管特批流程",
     "expenseDetails": "费用明细",
     "addDetail": "添加费用明细",
+    "editDetail": "修改费用明细",
     "addAtLeastOneDetail": "请至少添加一行费用明细",
     "noDetailHint": "暂无明细,点击上方添加",
     "detailRemark": "明细说明",
@@ -423,7 +424,7 @@
   "expense": {
     "expenseList": "报销列表",
     "expenseDetail": "报销详情",
-    "editExpense": "编辑报销",
+    "editExpense": "修改报销",
     "expenseApply": "报销",
     "searchExpense": "搜索报销单号或报销人",
     "noExpenses": "暂无报销单",
@@ -472,6 +473,7 @@
     "enterExpenseName": "请输入费用项目名称",
     "description": "描述",
     "addExpenseDetail": "添加费用明细",
+    "editExpenseDetail": "修改费用明细",
     "invoiceUpload": "发票上传",
     "maxInvoices": "最多上传9张发票",
     "invoiceAttachment": "发票附件",
@@ -552,7 +554,7 @@
     "weekend": "休息日",
     "holiday": "节假日",
     "overtimeRequest": "加班申请",
-    "editOvertime": "编辑加班申请",
+    "editOvertime": "修改加班申请",
     "overtimeApplyNo": "加班申请单号",
     "employee": "员工",
     "dep": "部门",
@@ -581,7 +583,7 @@
     "confirmEdit": "确认修改",
     "overtimeHoursPositive": "加班时长必须大于0",
     "addOvertimeDetail": "添加加班明细",
-    "editOvertimeDetail": "编辑加班明细",
+    "editOvertimeDetail": "修改加班明细",
     "addDetail": "添加明细",
     "overtimeDetails": "加班明细",
     "noDetailHint": "请添加加班明细",
@@ -680,7 +682,7 @@
     "mileageInvalid": "还车后里程不能小于出车前里程",
     "business": "公务",
     "vehicleRequest": "用车申请",
-    "editVehicle": "编辑用车申请",
+    "editVehicle": "修改用车申请",
     "vehicleApplyNo": "用车申请单号",
     "applicant": "申请人",
     "companionInfo": "同行信息",
@@ -712,6 +714,7 @@
     "vehicleReturned": "已还车",
     "selectLicensePlateHint": "请选择车牌号",
     "selectDepartTime": "请选择预计出车时间",
+    "selectApplicantHint": "请选择申请人",
     "selectReturnTime": "请选择预计还车时间",
     "withdraw": "撤回",
     "withdrawn": "已撤回",

+ 8 - 5
assets/i18n/zh_TW.json

@@ -99,7 +99,6 @@
     "statusWaitApprove": "待審核",
     "statusWaitPay": "待付款",
     "editApply": "修改報銷申請",
-    "editExpense": "修改報銷",
     "statusTransferred": "已轉收支",
     "statusClosed": "已結案",
     "statusPendingReview": "待審核",
@@ -131,6 +130,7 @@
     "saveFailed": "保存失敗",
     "submitSuccess": "提交成功",
     "billCreated": "單據已生成",
+    "billModified": "單據修改成功",
     "submitFailed": "提交失敗",
     "requestFailed": "請求失敗",
     "unknownError": "未知錯誤",
@@ -329,6 +329,7 @@
     "overBudgetTriggerApproval": "您的申請金額已超支,提交后将自動触發高管特批流程",
     "expenseDetails": "費用明細",
     "addDetail": "添加費用明細",
+    "editDetail": "修改費用明細",
     "addAtLeastOneDetail": "請至少添加一行費用明細",
     "noDetailHint": "暫無明細,点击上方添加",
     "detailRemark": "明細說明",
@@ -423,7 +424,7 @@
   "expense": {
     "expenseList": "報销列表",
     "expenseDetail": "報销詳情",
-    "editExpense": "編輯報销",
+    "editExpense": "修改報销",
     "expenseApply": "費用報销",
     "searchExpense": "搜尋報銷單號或報銷人",
     "noExpenses": "暫無報销單",
@@ -472,6 +473,7 @@
     "enterExpenseName": "請輸入費用項目名称",
     "description": "描述",
     "addExpenseDetail": "添加費用明細",
+    "editExpenseDetail": "修改費用明細",
     "invoiceUpload": "發票上传",
     "maxInvoices": "最多上传9张發票",
     "invoiceAttachment": "發票附件",
@@ -552,7 +554,7 @@
     "weekend": "休息日",
     "holiday": "節假日",
     "overtimeRequest": "加班申請",
-    "editOvertime": "編輯加班申請",
+    "editOvertime": "修改加班申請",
     "overtimeApplyNo": "加班申請單號",
     "employee": "員工",
     "dep": "部門",
@@ -581,7 +583,7 @@
     "confirmEdit": "確認修改",
     "overtimeHoursPositive": "加班時長必須大於0",
     "addOvertimeDetail": "添加加班明細",
-    "editOvertimeDetail": "編輯加班明細",
+    "editOvertimeDetail": "修改加班明細",
     "addDetail": "添加明細",
     "overtimeDetails": "加班明細",
     "noDetailHint": "請添加加班明細",
@@ -680,7 +682,7 @@
     "mileageInvalid": "還車后里程不能小于出車前里程",
     "business": "公務",
     "vehicleRequest": "用車申請",
-    "editVehicle": "編輯用車申請",
+    "editVehicle": "修改用車申請",
     "vehicleApplyNo": "用車申請單號",
     "applicant": "申請人",
     "companionInfo": "同行資訊",
@@ -712,6 +714,7 @@
     "vehicleReturned": "已還車",
     "selectLicensePlateHint": "請選擇車牌號",
     "selectDepartTime": "請選擇預計出車時間",
+    "selectApplicantHint": "請選擇申請人",
     "selectReturnTime": "請選擇預計還車時間",
     "withdraw": "撤回",
     "withdrawn": "已撤回",

+ 8 - 0
lib/features/expense/expense_api.dart

@@ -681,6 +681,14 @@ class ExpenseApi {
   }
 
   /// 获取单据状态
+  Future<Map<String, dynamic>> getBillAuditConfig(String bilId) async {
+    final response = await _client.get<Map<String, dynamic>>(
+      '/OA/GetBillAuditConfig',
+      queryParameters: {'bilId': bilId},
+    );
+    return response.data!;
+  }
+
   Future<Map<String, dynamic>> getBillStatus(String billNo) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetBillStatus',

+ 33 - 6
lib/features/expense/expense_detail_page.dart

@@ -86,14 +86,23 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
         }
       }
 
-      // 3. 获取单据状态(非致命)
+      // 3. 获取单据状态 + 审核配置(非致命)
       try {
-        final status = await api.getBillStatus(widget.billNo);
+        final results = await Future.wait([
+          api.getBillStatus(widget.billNo),
+          api.getBillAuditConfig('BX'),
+        ]);
+        final Map<String, dynamic> status = results[0];
+        final Map<String, dynamic> auditConfig = results[1];
+        final canManualSubmit =
+            auditConfig['hasAuditFlow'] == true &&
+            auditConfig['autoSubmit'] != true;
         if (mounted) {
           setState(() {
             final l10n = AppLocalizations.of(context);
             _billStatusBar = BillStatusBar(
               billStatus: status,
+              canManualSubmit: canManualSubmit,
               closedText: l10n.get('statusClosed'),
               transferredText: l10n.get('statusTransferred'),
               onEdit: status['canEdit'] == true
@@ -103,17 +112,35 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
                           if (result == true && mounted) _loadData();
                         })
                   : null,
-              onSubmit: () {
+              onSubmit: () async {
+                final l10n = AppLocalizations.of(context);
                 final dd = _expense?.expenseDate != null
                     ? du.DateUtils.formatDate(_expense!.expenseDate!)
                     : '';
-                api.shSubmit(bilNo: widget.billNo, bilDd: dd);
+                LoadingDialog.show(context, text: l10n.get('submitting'));
+                try {
+                  await api.shSubmit(bilNo: widget.billNo, bilDd: dd);
+                } finally {
+                  if (mounted) LoadingDialog.hide(context);
+                }
+                if (mounted) _loadData();
               },
-              onCancelSubmit: () {
+              onCancelSubmit: () async {
+                final l10n = AppLocalizations.of(context);
                 final dd = _expense?.expenseDate != null
                     ? du.DateUtils.formatDate(_expense!.expenseDate!)
                     : '';
-                api.shSubmit(bilNo: widget.billNo, bilDd: dd, isCancel: true);
+                LoadingDialog.show(context, text: l10n.get('submitting'));
+                try {
+                  await api.shSubmit(
+                    bilNo: widget.billNo,
+                    bilDd: dd,
+                    isCancel: true,
+                  );
+                } finally {
+                  if (mounted) LoadingDialog.hide(context);
+                }
+                if (mounted) _loadData();
               },
               onTapStatusTag: () => _showAuditTrail('BX'),
             );

+ 23 - 12
lib/features/expense/expense_edit_page.dart

@@ -15,6 +15,7 @@ import 'widgets/expense_detail_dialog.dart';
 import '../../shared/widgets/searchable_picker_sheet.dart';
 import '../../shared/widgets/loading_dialog.dart';
 import '../../shared/widgets/action_bar.dart';
+import '../../shared/helpers/audit_flow_helper.dart';
 import 'expense_api.dart';
 import '../../core/i18n/app_localizations.dart';
 import 'expense_model.dart';
@@ -1069,19 +1070,29 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
           final data = _buildSubmitData();
           final api = ref.read(expenseApiProvider);
           final billNo = await api.submit(data);
+          if (!mounted) return;
+          LoadingDialog.hide(context);
+
+          if (billNo != null) {
+            try {
+              final config = await api.getBillAuditConfig('BX');
+              if (!mounted) return;
+              final hasFlow = config['hasAuditFlow'] == true;
+              final autoSubmit = config['autoSubmit'] == true;
+              if (hasFlow && !autoSubmit) {
+                final dd = data['HeadData']['BX_DD']?.toString() ?? '';
+                await AuditFlowHelper.handle(
+                  context: context,
+                  l10n: l10n,
+                  getConfig: () async => config,
+                  onShSubmit: () => api.shSubmit(bilNo: billNo, bilDd: dd),
+                );
+              }
+            } catch (_) {}
+          }
           if (mounted) {
-            LoadingDialog.hide(context);
-            TDToast.showSuccess(l10n.get('submitSuccess'), context: context);
-            // BillSave 成功后异步提交审核流,不阻塞不弹窗
-            if (billNo != null) {
-              api.shSubmit(
-                bilNo: billNo,
-                bilDd: data['HeadData']['BX_DD']?.toString() ?? '',
-              );
-            }
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              if (mounted) GoRouter.of(context).pop(true);
-            });
+            TDToast.showSuccess(l10n.get('billModified'), context: context);
+            GoRouter.of(context).go('/expense/list');
           }
         } catch (e) {
           if (mounted) LoadingDialog.hide(context);

+ 3 - 1
lib/features/expense/widgets/expense_detail_dialog.dart

@@ -465,7 +465,9 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
               Expanded(
                 child: Center(
                   child: Text(
-                    _l10n.get('addExpenseDetail'),
+                    _isEdit
+                        ? _l10n.get('editExpenseDetail')
+                        : _l10n.get('addExpenseDetail'),
                     style: TextStyle(
                       fontSize: AppFontSizes.title,
                       fontWeight: FontWeight.w600,

+ 8 - 0
lib/features/expense_apply/expense_apply_api.dart

@@ -501,6 +501,14 @@ class ExpenseApplyApi {
   }
 
   /// 获取单据状态
+  Future<Map<String, dynamic>> getBillAuditConfig(String bilId) async {
+    final response = await _client.get<Map<String, dynamic>>(
+      '/OA/GetBillAuditConfig',
+      queryParameters: {'bilId': bilId},
+    );
+    return response.data!;
+  }
+
   Future<Map<String, dynamic>> getBillStatus(String billNo) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetBillStatus',

+ 66 - 49
lib/features/expense_apply/expense_apply_detail_page.dart

@@ -98,61 +98,81 @@ class _ExpenseApplyDetailPageState
       debugPrint(
         '[Attach] final state: attachAvailable=$attachAvailable, count=${attachments.length}',
       );
+      // 单据状态 + 审核配置(非关键,尽力加载),与详情合并到一个 setState 避免骨架闪烁
+      Map<String, dynamic>? status;
+      Map<String, dynamic>? auditConfig;
+      try {
+        final results = await Future.wait([
+          api.getBillStatus(widget.billNo),
+          api.getBillAuditConfig('AE'),
+        ]);
+        final Map<String, dynamic> s = results[0];
+        final Map<String, dynamic> c = results[1];
+        status = s;
+        auditConfig = c;
+      } catch (_) {}
+
+      final hasAuditFlow = auditConfig?['hasAuditFlow'] == true;
+      final autoSubmit = auditConfig?['autoSubmit'] == true;
+      final canManualSubmit = hasAuditFlow && !autoSubmit;
+
       if (mounted) {
         setState(() {
           _data = detail;
           _attachments = attachments;
           _attachAvailable = attachAvailable;
           _billFileRights = billFileRights;
+          _billStatusBar = (status != null)
+              ? BillStatusBar(
+                  billStatus: status,
+                  canManualSubmit: canManualSubmit,
+                  transferredText: AppLocalizations.of(
+                    context,
+                  ).get('statusConvertedToExpense'),
+                  onEdit: () async {
+                    final result = await GoRouter.of(
+                      context,
+                    ).push('/expense-apply/edit/${widget.billNo}');
+                    if (result == true && mounted) _loadData();
+                  },
+                  onSubmit: () async {
+                    final l10n = AppLocalizations.of(context);
+                    final a = ref.read(expenseApplyApiProvider);
+                    final applyDate = _data?.expenseApplyDate != null
+                        ? du.DateUtils.formatDate(_data!.expenseApplyDate!)
+                        : '';
+                    LoadingDialog.show(context, text: l10n.get('submitting'));
+                    try {
+                      await a.shSubmit(bilNo: widget.billNo, bilDd: applyDate);
+                    } finally {
+                      if (mounted) LoadingDialog.hide(context);
+                    }
+                    if (mounted) _loadData();
+                  },
+                  onCancelSubmit: () async {
+                    final l10n = AppLocalizations.of(context);
+                    final a = ref.read(expenseApplyApiProvider);
+                    final applyDate = _data?.expenseApplyDate != null
+                        ? du.DateUtils.formatDate(_data!.expenseApplyDate!)
+                        : '';
+                    LoadingDialog.show(context, text: l10n.get('submitting'));
+                    try {
+                      await a.shSubmit(
+                        bilNo: widget.billNo,
+                        bilDd: applyDate,
+                        isCancel: true,
+                      );
+                    } finally {
+                      if (mounted) LoadingDialog.hide(context);
+                    }
+                    if (mounted) _loadData();
+                  },
+                  onTapStatusTag: () => _showAuditTrail('AE'),
+                )
+              : null;
           _loading = false;
         });
       }
-
-      // 单据状态(非关键,尽力加载)
-      try {
-        final status = await api.getBillStatus(widget.billNo);
-        if (mounted) {
-          setState(() {
-            final l10n = AppLocalizations.of(context);
-            _billStatusBar = BillStatusBar(
-              billStatus: status,
-              transferredText: l10n.get('statusConvertedToExpense'),
-              onEdit: () async {
-                final result = await GoRouter.of(
-                  context,
-                ).push('/expense-apply/edit/${widget.billNo}');
-                if (result == true && mounted) _loadData();
-              },
-              onSubmit: () async {
-                final a = ref.read(expenseApplyApiProvider);
-                final applyDate = _data?.expenseApplyDate != null
-                    ? du.DateUtils.formatDate(_data!.expenseApplyDate!)
-                    : '';
-                final success = await a.shSubmit(
-                  bilNo: widget.billNo,
-                  bilDd: applyDate,
-                );
-                if (success && mounted) _loadData();
-              },
-              onCancelSubmit: () async {
-                final a = ref.read(expenseApplyApiProvider);
-                final applyDate = _data?.expenseApplyDate != null
-                    ? du.DateUtils.formatDate(_data!.expenseApplyDate!)
-                    : '';
-                final success = await a.shSubmit(
-                  bilNo: widget.billNo,
-                  bilDd: applyDate,
-                  isCancel: true,
-                );
-                if (success && mounted) _loadData();
-              },
-              onTapStatusTag: () => _showAuditTrail('AE'),
-            );
-          });
-        }
-      } catch (_) {
-        // 获取单据状态失败,忽略
-      }
     } catch (e) {
       if (mounted) {
         setState(() {
@@ -217,9 +237,6 @@ class _ExpenseApplyDetailPageState
                 const SizedBox(height: 16),
                 _buildAttachmentSection(l10n, colors),
                 const SizedBox(height: 24),
-                _billStatusBar?.buildActions(context) ??
-                    const SizedBox.shrink(),
-                const SizedBox(height: 24),
                 _buildPageFooter(colors),
               ],
             ),

+ 23 - 12
lib/features/expense_apply/expense_apply_edit_page.dart

@@ -20,6 +20,7 @@ import '../../core/constants/enums.dart';
 import 'expense_apply_api.dart';
 import 'widgets/expense_apply_detail_dialog.dart';
 import '../../shared/widgets/searchable_picker_sheet.dart';
+import '../../shared/helpers/audit_flow_helper.dart';
 import '../../core/utils/amount_utils.dart';
 
 class ExpenseApplyEditPage extends ConsumerStatefulWidget {
@@ -766,19 +767,29 @@ class _ExpenseApplyEditPageState extends ConsumerState<ExpenseApplyEditPage> {
           final data = _buildSubmitData();
           final api = ref.read(expenseApplyApiProvider);
           final billNo = await api.submit(data);
+          if (!mounted) return;
+          LoadingDialog.hide(context);
+
+          if (billNo != null) {
+            try {
+              final config = await api.getBillAuditConfig('AE');
+              if (!mounted) return;
+              final hasFlow = config['hasAuditFlow'] == true;
+              final autoSubmit = config['autoSubmit'] == true;
+              if (hasFlow && !autoSubmit) {
+                final dd = data['HeadData']['AE_DD']?.toString() ?? '';
+                await AuditFlowHelper.handle(
+                  context: context,
+                  l10n: l10n,
+                  getConfig: () async => config,
+                  onShSubmit: () => api.shSubmit(bilNo: billNo, bilDd: dd),
+                );
+              }
+            } catch (_) {}
+          }
           if (mounted) {
-            LoadingDialog.hide(context);
-            TDToast.showSuccess(l10n.get('submitSuccess'), context: context);
-            // BillSave 成功后异步提交审核流,不阻塞不弹窗
-            if (billNo != null) {
-              api.shSubmit(
-                bilNo: billNo,
-                bilDd: data['HeadData']['AE_DD']?.toString() ?? '',
-              );
-            }
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              if (mounted) GoRouter.of(context).pop(true);
-            });
+            TDToast.showSuccess(l10n.get('billModified'), context: context);
+            GoRouter.of(context).go('/expense-apply/list');
           }
         } catch (e) {
           if (mounted) LoadingDialog.hide(context);

+ 1 - 1
lib/features/expense_apply/widgets/expense_apply_detail_dialog.dart

@@ -357,7 +357,7 @@ class _ExpenseApplyDetailDialogState extends State<ExpenseApplyDetailDialog> {
               Expanded(
                 child: Center(
                   child: Text(
-                    _l10n.get('addDetail'),
+                    _isEdit ? _l10n.get('editDetail') : _l10n.get('addDetail'),
                     style: TextStyle(
                       fontSize: AppFontSizes.title,
                       fontWeight: FontWeight.w600,

+ 56 - 48
lib/features/overtime/overtime_apply_detail_page.dart

@@ -9,6 +9,7 @@ import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/bill_status_bar.dart';
+import '../../shared/widgets/loading_dialog.dart';
 import '../../core/navigation/host_app_channel.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
@@ -56,59 +57,67 @@ class _OvertimeApplyDetailPageState
     try {
       final api = ref.read(overtimeApplyApiProvider);
       final detail = await api.fetchDetail(widget.billNo);
+      if (!mounted) return;
+
+      // 单据状态(非关键,尽力加载),与详情合并到一个 setState 避免中间闪烁
+      Map<String, dynamic>? status;
+      try {
+        status = await api.getBillStatus(widget.billNo);
+      } catch (_) {}
 
       if (mounted) {
         setState(() {
           _data = detail;
-          _loading = false;
-        });
-      }
-
-      // 单据状态(非关键,尽力加载)
-      try {
-        final status = await api.getBillStatus(widget.billNo);
-        if (mounted) {
-          setState(() {
-            _billStatusBar = BillStatusBar(
-              billStatus: status,
-              onEdit: () {
-                context
-                    .push('/overtime-apply/edit/${widget.billNo}')
-                    .then((_) => _loadData());
-              },
-              onSubmit: () {
-                final api = ref.read(overtimeApplyApiProvider);
-                final jbDdStr = _data?.jbDd != null
-                    ? du.DateUtils.formatDate(_data!.jbDd!)
-                    : '';
-                if (jbDdStr.isNotEmpty) {
-                  api
-                      .shSubmit(bilNo: widget.billNo, bilDd: jbDdStr)
-                      .then((_) => _loadData());
-                }
-              },
-              onCancelSubmit: () {
-                final api = ref.read(overtimeApplyApiProvider);
-                final jbDdStr = _data?.jbDd != null
-                    ? du.DateUtils.formatDate(_data!.jbDd!)
-                    : '';
-                if (jbDdStr.isNotEmpty) {
-                  api
-                      .shSubmit(
+          _billStatusBar = (status != null)
+              ? BillStatusBar(
+                  billStatus: status,
+                  onEdit: () {
+                    context
+                        .push('/overtime-apply/edit/${widget.billNo}')
+                        .then((_) => _loadData());
+                  },
+                  onSubmit: () async {
+                    final l10n = AppLocalizations.of(context);
+                    final api = ref.read(overtimeApplyApiProvider);
+                    final jbDdStr = _data?.jbDd != null
+                        ? du.DateUtils.formatDate(_data!.jbDd!)
+                        : '';
+                    if (jbDdStr.isEmpty) return;
+                    LoadingDialog.show(context, text: l10n.get('submitting'));
+                    try {
+                      await api.shSubmit(bilNo: widget.billNo, bilDd: jbDdStr);
+                    } finally {
+                      if (mounted) LoadingDialog.hide(context);
+                    }
+                    if (mounted) _loadData();
+                  },
+                  onCancelSubmit: () async {
+                    final l10n = AppLocalizations.of(context);
+                    final api = ref.read(overtimeApplyApiProvider);
+                    final jbDdStr = _data?.jbDd != null
+                        ? du.DateUtils.formatDate(_data!.jbDd!)
+                        : '';
+                    if (jbDdStr.isEmpty) return;
+                    LoadingDialog.show(context, text: l10n.get('submitting'));
+                    try {
+                      await api.shSubmit(
                         bilNo: widget.billNo,
                         bilDd: jbDdStr,
                         isCancel: true,
-                      )
-                      .then((_) => _loadData());
-                }
-              },
-              onTapStatusTag: () {
-                _showAuditTrail('OT');
-              },
-            );
-          });
-        }
-      } catch (_) {} // ignore non-critical status load failure
+                      );
+                    } finally {
+                      if (mounted) LoadingDialog.hide(context);
+                    }
+                    if (mounted) _loadData();
+                  },
+                  onTapStatusTag: () {
+                    _showAuditTrail('OT');
+                  },
+                )
+              : null;
+          _loading = false;
+        });
+      }
     } catch (e) {
       if (mounted) {
         setState(() {
@@ -171,13 +180,12 @@ class _OvertimeApplyDetailPageState
                 const SizedBox(height: 16),
                 _buildOvertimeDetailSection(app, l10n, colors),
                 const SizedBox(height: 24),
-                _billStatusBar?.buildActions(context) ??
-                    const SizedBox.shrink(),
                 _buildPageFooter(colors),
               ],
             ),
           ),
         ),
+        _billStatusBar?.buildActions(context) ?? const SizedBox.shrink(),
       ],
     );
   }

+ 14 - 9
lib/features/overtime/overtime_apply_edit_page.dart

@@ -14,6 +14,7 @@ import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/nav_bar_config.dart';
+import '../../shared/helpers/audit_flow_helper.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 import 'overtime_apply_api.dart';
@@ -808,16 +809,20 @@ class _OvertimeApplyEditPageState extends ConsumerState<OvertimeApplyEditPage> {
           final data = _buildSubmitData();
           final api = ref.read(overtimeApplyApiProvider);
           final billNo = await api.submit(data);
+          if (!mounted) return;
+          LoadingDialog.hide(context);
+
+          if (billNo != null) {
+            final dd = data['HeadData']['JB_DD']?.toString() ?? '';
+            await AuditFlowHelper.handle(
+              context: context,
+              l10n: l10n,
+              getConfig: () => api.getBillAuditConfig('JB'),
+              onShSubmit: () => api.shSubmit(bilNo: billNo, bilDd: dd),
+            );
+          }
           if (mounted) {
-            LoadingDialog.hide(context);
-            TDToast.showSuccess(l10n.get('submitSuccess'), context: context);
-            // BillSave 成功后异步提交审核流,不阻塞不弹窗
-            if (billNo != null) {
-              api.shSubmit(
-                bilNo: billNo,
-                bilDd: data['HeadData']['JB_DD']?.toString() ?? '',
-              );
-            }
+            TDToast.showSuccess(l10n.get('billModified'), context: context);
             GoRouter.of(context).go('/overtime-apply/list');
           }
         } catch (e) {

+ 4 - 0
lib/features/vehicle/vehicle_apply_create_page.dart

@@ -273,6 +273,7 @@ class _VehicleApplyCreatePageState
         const SizedBox(height: 16),
         FormFieldRow(
           label: l10n.get('applicant'),
+          required: true,
           value: _selectedApplicantId.isNotEmpty
               ? '$_selectedApplicantId/$_selectedApplicantName'
               : '',
@@ -666,6 +667,9 @@ class _VehicleApplyCreatePageState
 
   List<String> _validate(AppLocalizations l10n) {
     final e = <String>[];
+    if (_selectedApplicantId.isEmpty) {
+      e.add(l10n.get('selectApplicantHint'));
+    }
     if (_reasonController.text.trim().isEmpty) {
       e.add(l10n.get('enterVehicleReason'));
     }

+ 32 - 11
lib/features/vehicle/vehicle_apply_detail_page.dart

@@ -11,6 +11,7 @@ import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/amount_utils.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/bill_status_bar.dart';
+import '../../shared/widgets/loading_dialog.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/form_section.dart';
 import 'vehicle_apply_api.dart';
@@ -92,26 +93,46 @@ class _VehicleApplyDetailPageState
                             if (result == true && mounted) _loadData();
                           });
                     },
-                    onSubmit: () {
+                    onSubmit: () async {
+                      final l10n = AppLocalizations.of(context);
                       final dd = _data?.ycDd != null
                           ? du.DateUtils.formatDate(_data!.ycDd!)
                           : '';
-                      api.shSubmit(
-                        bilId: 'YC',
-                        bilNo: widget.billNo,
-                        bilDd: dd,
+                      LoadingDialog.show(
+                        context,
+                        text: l10n.get('submitting'),
                       );
+                      try {
+                        await api.shSubmit(
+                          bilId: 'YC',
+                          bilNo: widget.billNo,
+                          bilDd: dd,
+                        );
+                      } finally {
+                        if (mounted) LoadingDialog.hide(context);
+                      }
+                      if (mounted) _loadData();
                     },
-                    onCancelSubmit: () {
+                    onCancelSubmit: () async {
+                      final l10n = AppLocalizations.of(context);
                       final dd = _data?.ycDd != null
                           ? du.DateUtils.formatDate(_data!.ycDd!)
                           : '';
-                      api.shSubmit(
-                        bilId: 'YC',
-                        bilNo: widget.billNo,
-                        bilDd: dd,
-                        isCancel: true,
+                      LoadingDialog.show(
+                        context,
+                        text: l10n.get('submitting'),
                       );
+                      try {
+                        await api.shSubmit(
+                          bilId: 'YC',
+                          bilNo: widget.billNo,
+                          bilDd: dd,
+                          isCancel: true,
+                        );
+                      } finally {
+                        if (mounted) LoadingDialog.hide(context);
+                      }
+                      if (mounted) _loadData();
                     },
                     onTapStatusTag: () => _showAuditTrail('YC'),
                     onReturn: canReturn

+ 20 - 13
lib/features/vehicle/vehicle_apply_edit_page.dart

@@ -17,6 +17,7 @@ import '../../shared/widgets/loading_dialog.dart';
 import '../../shared/widgets/nav_bar_config.dart';
 import '../../shared/widgets/app_input_dialog.dart';
 import '../../shared/widgets/searchable_picker_sheet.dart';
+import '../../shared/helpers/audit_flow_helper.dart';
 import 'vehicle_apply_api.dart';
 import 'vehicle_apply_list_controller.dart';
 import 'vehicle_apply_model.dart';
@@ -437,6 +438,7 @@ class _VehicleApplyEditPageState extends ConsumerState<VehicleApplyEditPage> {
         const SizedBox(height: 16),
         FormFieldRow(
           label: l10n.get('applicant'),
+          required: true,
           value: _selectedApplicantId.isNotEmpty
               ? '$_selectedApplicantId/$_selectedApplicantName'
               : '',
@@ -762,20 +764,22 @@ class _VehicleApplyEditPageState extends ConsumerState<VehicleApplyEditPage> {
           final data = _buildSubmitData();
           final api = ref.read(vehicleApplyApiProvider);
           final billNo = await api.submit(data);
+          if (!mounted) return;
+          LoadingDialog.hide(context);
+
+          if (billNo != null) {
+            final dd = data['HeadData']['YC_DD']?.toString() ?? '';
+            await AuditFlowHelper.handle(
+              context: context,
+              l10n: l10n,
+              getConfig: () => api.getBillAuditConfig('YC'),
+              onShSubmit: () =>
+                  api.shSubmit(bilId: 'YC', bilNo: billNo, bilDd: dd),
+            );
+          }
           if (mounted) {
-            LoadingDialog.hide(context);
-            TDToast.showSuccess(l10n.get('submitSuccess'), context: context);
-            // BillSave 成功后异步提交审核流,不阻塞不弹窗
-            if (billNo != null) {
-              api.shSubmit(
-                bilId: 'YC',
-                bilNo: billNo,
-                bilDd: data['HeadData']['YC_DD']?.toString() ?? '',
-              );
-            }
-            WidgetsBinding.instance.addPostFrameCallback((_) {
-              if (mounted) GoRouter.of(context).pop(true);
-            });
+            TDToast.showSuccess(l10n.get('billModified'), context: context);
+            GoRouter.of(context).go('/vehicle-apply/list');
           }
         } catch (e) {
           if (mounted) LoadingDialog.hide(context);
@@ -815,6 +819,9 @@ class _VehicleApplyEditPageState extends ConsumerState<VehicleApplyEditPage> {
 
   List<String> _validate(AppLocalizations l10n) {
     final e = <String>[];
+    if (_selectedApplicantId.isEmpty) {
+      e.add(l10n.get('selectApplicantHint'));
+    }
     if (_reasonController.text.trim().isEmpty) {
       e.add(l10n.get('enterVehicleReason'));
     }

+ 1 - 0
lib/shared/helpers/audit_flow_helper.dart

@@ -34,6 +34,7 @@ class AuditFlowHelper {
         return;
       }
 
+      // ignore: use_build_context_synchronously
       final shouldSubmit = await _showDialog(colors, context, l10n);
       if (shouldSubmit == true) {
         await onShSubmit();

+ 12 - 1
lib/shared/widgets/bill_status_bar.dart

@@ -46,6 +46,10 @@ class BillStatusBar {
   /// 已还车状态文本(如"已还车",仅用车申请单使用,优先级最高)
   final String? returnedText;
 
+  /// 是否允许手动提交审核流。为 false 时提交/取消提交按钮不显示。
+  /// 当 ERP 已自动处理存盘立即提交时,无需在详情页再次手动操作。
+  final bool canManualSubmit;
+
   const BillStatusBar({
     this.billStatus,
     this.onEdit,
@@ -56,6 +60,7 @@ class BillStatusBar {
     this.closedText,
     this.transferredText,
     this.returnedText,
+    this.canManualSubmit = true,
   });
 
   // ── 派生状态 ──
@@ -81,11 +86,17 @@ class BillStatusBar {
   }
 
   bool get showSubmit {
+    if (!canManualSubmit) return false;
+    if (_isClosed || _isTransferred) return false;
     final s = _approvalStatus;
     return s == 'SHCOUNT0' || s == 'SHCOUNT3';
   }
 
-  bool get showCancelSubmit => _approvalStatus == 'SHCOUNT1';
+  bool get showCancelSubmit {
+    if (!canManualSubmit) return false;
+    if (_isClosed || _isTransferred) return false;
+    return _approvalStatus == 'SHCOUNT1';
+  }
 
   bool get showReturn {
     if (onReturn == null) return false;