diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index b3eb6a7f..58b79d13 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -1086,7 +1086,7 @@ onMounted(() => { void loadCustomers() }) -

基础资料

+

基础资料

@@ -1095,7 +1095,7 @@ onMounted(() => { void loadCustomers() })
-

联系与商务

仅用于企业业务联系,不会修改企业管理员登录手机号;登录手机号请在成员/账号管理中单独维护,修改会影响登录。

+

联系与商务

仅用于企业业务联系,不会修改企业管理员登录手机号;登录手机号请在成员/账号管理中单独维护,修改会影响登录。

@@ -1103,7 +1103,7 @@ onMounted(() => { void loadCustomers() })
-

企业认证资料

营业执照和18位统一社会信用代码为必填业务项;法人身份证、货代资质证书和银行开户许可证均为选填。

支持 PDF、JPG、PNG,单个文件不超过 5MB;* 为提交审核必填项。

{{ statusMeta(editorStatus).label }}
+

企业认证资料

营业执照和18位统一社会信用代码为必填业务项;法人身份证、货代资质证书和银行开户许可证均为选填。

支持 PDF、JPG、PNG,单个文件不超过 5MB;* 为提交审核必填项。

{{ statusMeta(editorStatus).label }}
{{ form.unifiedSocialCreditCode }}{{ creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照共同保存;法人身份证为选填。' }}
diff --git a/src/modules/customers/pages/OverseasStorageFeesView.vue b/src/modules/customers/pages/OverseasStorageFeesView.vue index af760d5a..f7717373 100644 --- a/src/modules/customers/pages/OverseasStorageFeesView.vue +++ b/src/modules/customers/pages/OverseasStorageFeesView.vue @@ -93,9 +93,9 @@ onMounted(() => { void load() }) -
+
-
+

{{ groupIndex === 0 ? '仓租(CBM/天)' : '退货仓储费(托/天)' }} · {{ group.length }}档

新增阶梯
diff --git a/src/modules/inbounds/measurement/measurementDensity.css b/src/modules/inbounds/measurement/measurementDensity.css index 1e7ea3ce..0eddcf71 100644 --- a/src/modules/inbounds/measurement/measurementDensity.css +++ b/src/modules/inbounds/measurement/measurementDensity.css @@ -4,8 +4,9 @@ .inbound-measurement-workspace-dialog.inbound-density .measurement-summary-context { flex: 1 1 auto; } .inbound-measurement-workspace-dialog.inbound-density .measurement-summary-context p > span { flex-shrink: 0; } .inbound-measurement-workspace-dialog.inbound-density .measurement-summary-status { flex-wrap: wrap; } -.inbound-measurement-workspace-dialog.inbound-density .measurement-items-section { min-width: 0; margin: var(--admin-space-5, 12px) 0 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid var(--admin-dialog-work-border); border-radius: var(--admin-dialog-section-radius); background: var(--admin-dialog-work-bg); } +.inbound-measurement-workspace-dialog.inbound-density .measurement-items-section { min-width: 0; margin: var(--admin-space-5, 12px) 0 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid var(--cc-border); border-radius: var(--admin-dialog-section-radius); background: var(--admin-surface, #fff); } +.inbound-measurement-workspace-dialog.inbound-density .measurement-deviation-alert { background: var(--admin-surface, #fff); } .inbound-measurement-workspace-dialog.inbound-density .measurement-subheading { color: #18795d; font-size: var(--admin-font-section, 14px); } .inbound-measurement-workspace-dialog.inbound-density .measurement-declaration { min-width: 0; margin-top: var(--admin-space-3, 8px); padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid var(--admin-dialog-source-border); border-radius: var(--admin-dialog-section-radius); background: var(--admin-dialog-source-bg); } diff --git a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md index 42106cbf..ca34d513 100644 --- a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md +++ b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md @@ -429,3 +429,5 @@ | 页面 | 操作 | 原型/确认依据 | 承载与实现 | 验收 | |---|---|---|---|---| | 排车列表:装车中、装车后、派送中、配送完成 | 管理资料 | 用户管理资料截图及同车/派送单资料共用确认;签收完成即可,其他可补 | 1100px居中可全屏弹框,签收文件/油耗图片/装车资料三个页签;装车中及装车后默认打开装车资料,可同步上传,确认装车且资料齐全后自动派送;按关联派送单切换,共用原派送资料编辑器;油耗沿本车附加照片保存 | 自动回归单独报告;1920×1080、100%待人工截图确认 | + +| 仓库派送排车列表/详情 → 维护车辆信息 | 提交审核入口合并 | 2026-10-21 用户确认:隐藏调整卸货顺序,提交审核移入车辆维护弹框,不需要备注 | 沿用原居中弹框;底部保存/提交审核,提交先保存本次资料再按新版本送审;去掉维护和提交备注输入,保留审核驳回原因 | 定向自动检查单独报告;1920×1080、100%待人工截图确认 | diff --git a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts index ee7dd20b..3b795447 100644 --- a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts @@ -49,6 +49,7 @@ it('reads existing delivery files in one dialog and saves receipt without requir it('switches customer scope exactly and keeps unsaved delivery edits when switching tabs', async () => { const wrapper = setup(); await flushPromises() await wrapper.findAll('[role="tab"]').find(tab => tab.text() === '装车资料')!.trigger('click') + expect(wrapper.getComponent(DeliveryDocumentsDialog).props('categories')).toEqual(['LOADING_PHOTO', 'BOL']) await wrapper.getComponent(DeliveryDocumentsDialog).find('input').setValue('BOL-CHANGED') await wrapper.findAll('[role="tab"]').find(tab => tab.text() === '油耗图片')!.trigger('click') expect(getDeliveryDocuments).toHaveBeenCalledTimes(1) diff --git a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue index d718f6f9..89e030a6 100644 --- a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue +++ b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue @@ -66,8 +66,9 @@ watch(() => [open.value, props.dispatchNo] as const, ([visible]) => {
+

关联派送单资料

- +

{{ access.can(permission.deliveryList) ? '暂无关联派送单' : '当前账号无派送资料查看权限' }}

完成装车并补齐装车资料后,进入派送中管理{{ tab === 'FUEL' ? '油耗图片' : '签收文件' }}。

diff --git a/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.test.ts b/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.test.ts index 4e459d5e..a2aec516 100644 --- a/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.test.ts @@ -62,6 +62,17 @@ it('shows existing photos without exposing upload for a read-only account', asyn expect(api.uploadLoadingPhoto).not.toHaveBeenCalled(); wrapper.unmount() }) +it('allows choosing another image after the server definitively rejects the file', async () => { + vi.mocked(api.uploadLoadingPhoto).mockRejectedValueOnce(new ApiResponseError('图片内容无效', 'A0400')) + const wrapper = setup(); await flushPromises(); await upload(wrapper) + expect(wrapper.text()).toContain('图片内容无效') + expect(wrapper.find('[role="status"]').exists()).toBe(false) + expect(api.bindLoadingPhoto).not.toHaveBeenCalled() + await upload(wrapper) + expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(2) + expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount() +}) + it('allows saving photos while loading and keeps the loading stage', async () => { vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ stage: 'LOADING' })) vi.mocked(api.bindLoadingPhoto).mockResolvedValue(data({ stage: 'LOADING', recordVersion: '1', photos: [photo] })) diff --git a/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.vue b/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.vue index e0090ee3..17dba294 100644 --- a/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.vue +++ b/src/modules/overseas-warehouse/components/DispatchLoadingDocuments.vue @@ -69,7 +69,13 @@ async function upload(slot: LoadingPhotoSlot, event: Event) { try { ensureScope(); pending.value = { slot, key: crypto.randomUUID() }; persist() pending.value.photo = await uploadLoadingPhoto(number, slot, file, pending.value.key); ensureScope(); persist(); await bind() - } catch (cause) { if (active) error.value = getApiErrorDetails(cause, '装车照片上传或保存失败,请核对原请求') } + } catch (cause) { + if (active) { + error.value = getApiErrorDetails(cause, '装车照片上传或保存失败,请核对原请求') + // 文件校验明确拒绝时尚未创建上传意图,允许重新选择;未知结果仍保留原请求。 + if (pending.value && !pending.value.photo && /^(?:A0|HTTP_)?400$/.test(String(error.value.code))) { pending.value = null; persist() } + } + } finally { if (active) busy.value = false } } onMounted(() => { diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts index 71f66c16..9b834b9e 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts @@ -54,7 +54,7 @@ function field(wrapper: Pick, label: string) { describe('dispatch workflow native layout', () => { it('submits all edited resources, dates and zero version through the continuous fullscreen form', async () => { const { dialog } = await render('EDIT') - expect(dialog.props('title')).toBe('维护车辆信息 · PC/1') + expect(dialog.props('title')).toBe('维护车辆 · PC/1') expect(dialog.getComponent(DispatchResourceFields).props('continuous')).toBe(true) await field(dialog, '车牌号').get('input').setValue('CA-NEW') field(dialog, '实际托位容量').getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 22) @@ -76,7 +76,7 @@ describe('dispatch workflow native layout', () => { expect(dialog.text()).toContain('workflow-request') expect(field(dialog, '车牌号').get('input').attributes('disabled')).toBeDefined() await wrapper.setProps({ dispatchNo: 'PC/2', action: 'CANCEL' }); await flushPromises() - expect(dialog.props('title')).toBe('维护车辆信息 · PC/1') + expect(dialog.props('title')).toBe('维护车辆 · PC/1') await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises() expect((field(dialog, '车牌号').get('input').element as HTMLInputElement).value).toBe('CA-PRESERVED') expect((dialog.get('textarea[aria-label="本次操作备注"]').element as HTMLTextAreaElement).value).toBe('保留本次输入') diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts index 87bd2157..411baee8 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts @@ -12,7 +12,7 @@ vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: identity }) }) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('./DispatchResourceFields.vue', () => ({ default: { template: '
' } })) vi.mock('../api/dispatches', () => ({ markDispatchException: vi.fn(), getDispatch: vi.fn(), getDispatchItems: vi.fn(), getDispatchRoute: vi.fn(), reorderDispatchRoute: vi.fn(), removeDispatchItem: vi.fn(), getDispatchLoading: vi.fn(), getDispatchCommand: vi.fn(), listDispatches: vi.fn(), listDispatchCandidates: vi.fn(), completeDispatchLoading: vi.fn(), reviewDispatch: vi.fn(), reviewDispatchLoading: vi.fn(), returnDispatchToReview: vi.fn(), cancelDispatch: vi.fn(), updateDispatch: vi.fn(), addDispatchCargo: vi.fn(), submitDispatchReview: vi.fn() })) -interface State { exception: { exceptionType: string; returnDirection: string; description: string; imageFileIds: string[] }; submit(): Promise; retryError(): Promise; measurements: Array<{ weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null; palletSpaces: number | null }>; form: { reason: string; decision: string; remark: string }; detail: DispatchDetail } +interface State { exception: { exceptionType: string; returnDirection: string; description: string; imageFileIds: string[] }; submit(submitReviewAfterSave?: boolean): Promise; retryError(): Promise; measurements: Array<{ weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null; palletSpaces: number | null }>; form: { reason: string; decision: string; remark: string }; detail: DispatchDetail } async function open(action: DispatchAction, allowed = true) { vi.mocked(api.getDispatch).mockResolvedValue(detail({ actions: { [action]: { allowed, blockCode: null, blockReason: allowed ? null : '前置审核未通过' } } })) const wrapper = mount(DispatchWorkflowDialog, { props: { modelValue: true, dispatchNo: 'PC/1', action }, global: { plugins: [ElementPlus] } }); await flushPromises() @@ -215,3 +215,65 @@ it('does not submit an exception when the server disallows the action', async () state.exception.exceptionType = 'REFUSED'; await state.submit() expect(api.markDispatchException).not.toHaveBeenCalled(); wrapper.unmount() }) + +it('submits review from vehicle maintenance after saving the current inputs and reading the new version', async () => { + const { wrapper, state } = await open('EDIT') + const gate = { allowed: true, blockCode: null, blockReason: null } + expect(wrapper.find('[aria-label="本次操作备注"]').exists()).toBe(false) + expect(wrapper.findAll('button').some(button => button.text() === '保存')).toBe(true) + expect(wrapper.findAll('button').some(button => button.text() === '提交审核')).toBe(true) + vi.mocked(api.updateDispatch).mockImplementation(async (_, body) => command('EDIT', body.idempotencyKey)) + vi.mocked(api.submitDispatchReview).mockImplementation(async (_, body) => ({ ...command('SUBMIT_REVIEW', body.idempotencyKey), recordVersion: 2 })) + vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { EDIT: gate, SUBMIT_REVIEW: gate } })).mockResolvedValueOnce(detail({ recordVersion: 2 })) + await state.submit(true) + expect(api.updateDispatch).toHaveBeenCalledWith('PC/1', expect.objectContaining({ recordVersion: 0, vehicle: expect.any(Object), driver: expect.any(Object) })) + expect(api.submitDispatchReview).toHaveBeenCalledWith('PC/1', expect.objectContaining({ recordVersion: 1, remark: null })) + expect(vi.mocked(api.updateDispatch).mock.invocationCallOrder[0]).toBeLessThan(vi.mocked(api.submitDispatchReview).mock.invocationCallOrder[0]!) + expect(wrapper.emitted('update:modelValue')).toContainEqual([false]); wrapper.unmount() +}) +it('save alone never submits review and a failed save cannot advance review', async () => { + const first = await open('EDIT') + vi.mocked(api.updateDispatch).mockImplementationOnce(async (_, body) => command('EDIT', body.idempotencyKey)) + vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1 })) + await first.state.submit(); expect(api.submitDispatchReview).not.toHaveBeenCalled(); first.wrapper.unmount() + const second = await open('EDIT') + vi.mocked(api.updateDispatch).mockRejectedValueOnce(new ApiResponseError('车辆资料不完整', 'A0400')) + await second.state.submit(true); expect(api.submitDispatchReview).not.toHaveBeenCalled() + expect(second.wrapper.text()).toContain('车辆资料不完整'); second.wrapper.unmount() +}) +it('rechecks review eligibility after save and leaves the saved editor open if blocked', async () => { + const { wrapper, state } = await open('EDIT') + vi.mocked(api.updateDispatch).mockImplementationOnce(async (_, body) => command('EDIT', body.idempotencyKey)) + vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { EDIT: { allowed: true, blockCode: null, blockReason: null }, SUBMIT_REVIEW: { allowed: false, blockCode: 'MISSING', blockReason: '预约资料未齐全' } } })) + await state.submit(true) + expect(api.submitDispatchReview).not.toHaveBeenCalled(); expect(state.detail.recordVersion).toBe(1) + expect(wrapper.text()).toContain('预约资料未齐全'); expect(wrapper.emitted('update:modelValue')).toBeUndefined(); wrapper.unmount() +}) +it('retains the follow-up review intent through an unknown save and does not save twice on recovery', async () => { + const { wrapper, state } = await open('EDIT') + vi.mocked(api.updateDispatch).mockRejectedValueOnce(new Error('save outcome unknown')) + await state.submit(true) + const pending = useDispatchCommandRecovery(() => identity.id).pending.value! + expect(pending.submitReviewAfterSave).toBe(true) + expect(JSON.parse(sessionStorage.getItem(`dispatch-command:${identity.id}`)!).submitReviewAfterSave).toBe(true) + vi.mocked(api.getDispatchCommand).mockResolvedValueOnce(command('EDIT', String(pending.body.idempotencyKey))) + vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { SUBMIT_REVIEW: { allowed: true, blockCode: null, blockReason: null } } })).mockResolvedValueOnce(detail({ recordVersion: 2 })) + vi.mocked(api.submitDispatchReview).mockImplementationOnce(async (_, body) => ({ ...command('SUBMIT_REVIEW', body.idempotencyKey), recordVersion: 2 })) + await state.submit() + expect(api.updateDispatch).toHaveBeenCalledTimes(1); expect(api.submitDispatchReview).toHaveBeenCalledTimes(1) + expect(useDispatchCommandRecovery(() => identity.id).pending.value).toBeNull(); wrapper.unmount() +}) +it('recovers an unknown review submission without saving the vehicle again', async () => { + const { wrapper, state } = await open('EDIT') + vi.mocked(api.updateDispatch).mockImplementationOnce(async (_, body) => command('EDIT', body.idempotencyKey)) + vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { SUBMIT_REVIEW: { allowed: true, blockCode: null, blockReason: null } } })) + vi.mocked(api.submitDispatchReview).mockRejectedValueOnce(new Error('review outcome unknown')) + await state.submit(true) + const pending = useDispatchCommandRecovery(() => identity.id).pending.value! + expect(pending.action).toBe('SUBMIT_REVIEW'); expect(pending.body).toMatchObject({ recordVersion: 1, remark: null }) + vi.mocked(api.getDispatchCommand).mockResolvedValueOnce({ ...command('SUBMIT_REVIEW', String(pending.body.idempotencyKey)), recordVersion: 2 }) + vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 2 })) + await state.submit() + expect(api.updateDispatch).toHaveBeenCalledTimes(1); expect(api.submitDispatchReview).toHaveBeenCalledTimes(1) + expect(wrapper.emitted('update:modelValue')).toContainEqual([false]); wrapper.unmount() +}) diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue index 9ba4dd14..0fa8cca7 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue @@ -45,8 +45,10 @@ let editorRequestKey = '' const cargoAction = computed(() => ['ADD_CARGO', 'REMOVE_CARGO'].includes(currentAction.value)) const cargoEntryMessage = '货物安排统一在智能排车中修改;如有未确认操作,请从智能排车恢复原请求。' const allowed = computed(() => !cargoAction.value && !factsStale.value && !pending.value && canDispatchAction(detail.value?.actions, currentAction.value, access.can) && (currentOperation.value !== 'ROUTE' || route.value?.edit.allowed === true)) -const width = computed(() => ['EDIT', 'LOADING_COMPLETE'].includes(currentAction.value) ? '1120px' : ['DISPATCH_REVIEW', 'LOADING_REVIEW', 'MARK_EXCEPTION'].includes(currentAction.value) ? '760px' : '560px') -const frozen = computed(() => busy.value || Boolean(pending.value)) +const vehicleEditor = computed(() => currentOperation.value !== 'ROUTE' && (currentAction.value === 'EDIT' || props.action === 'EDIT' && currentAction.value === 'SUBMIT_REVIEW')) +const submitting = ref(false) +const width = computed(() => vehicleEditor.value || currentAction.value === 'LOADING_COMPLETE' ? '1120px' : ['DISPATCH_REVIEW', 'LOADING_REVIEW', 'MARK_EXCEPTION'].includes(currentAction.value) ? '760px' : '560px') +const frozen = computed(() => submitting.value || busy.value || Boolean(pending.value)) const evidenceBusy = reactive({ EXCEPTION: false, VEHICLE: false, DRIVER_PHOTO: false, DRIVER: false }) const uploadingEvidence = computed(() => Object.values(evidenceBusy).some(Boolean)) @@ -115,6 +117,7 @@ function payload(): object { if (!measurements.value.length || measurements.value.some(row => row.weightKg == null || row.volumeCbm == null || row.palletSpaces == null || row.deliveryIndex == null || row.weightKg <= 0 || row.volumeCbm <= 0 || row.palletSpaces <= 0 || !Number.isInteger(row.palletSpaces) || !Number.isInteger(row.deliveryIndex) || row.deliveryIndex < 0)) throw new DispatchInputError('请核对每托真实托位、重量、体积和卸货站;不能以参考值代替实测') return { recordVersion: requireVersion(preview.value.recordVersion), idempotencyKey: common.idempotencyKey, manifestHash: preview.value.manifestHash, measurements: measurements.value.map(row => ({ warehousePalletId: row.warehousePalletId, palletSpaces: row.palletSpaces!, weightKg: row.weightKg!, volumeCbm: row.volumeCbm!, deliveryIndex: row.deliveryIndex! })), remark: form.remark || null } satisfies LoadingComplete } + case 'SUBMIT_REVIEW': return { ...common, remark: null } default: return { ...common, remark: form.remark || null } } } @@ -133,20 +136,35 @@ async function send(command: PendingDispatchCommand): Promise { default: throw new DispatchInputError('此操作不属于当前后台作业入口') } } -async function submit() { - if (busy.value || loading.value || uploadingEvidence.value) return +async function submit(submitReviewAfterSave = false) { + if (submitting.value || busy.value || loading.value || uploadingEvidence.value) return + submitting.value = true error.value = null try { if (cargoAction.value) throw new DispatchInputError(cargoEntryMessage) if (pending.value?.action === 'CREATE') throw new DispatchInputError('请从排车列表的原请求入口恢复新建排车') if (!pending.value) { - recovery.begin(props.dispatchNo, props.action, payload(), { operation: currentOperation.value }) + recovery.begin(props.dispatchNo, props.action, payload(), { operation: currentOperation.value, ...(submitReviewAfterSave && vehicleEditor.value ? { submitReviewAfterSave: true } : {}) }) editorRequestKey = String(recovery.pending.value?.body.idempotencyKey || '') } const routeCommand = pending.value?.operation === 'ROUTE' const reviewReturned = ['DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(pending.value?.action || '') && pending.value?.body.decision === 'REJECTED' - const result = await recovery.execute(send, api.getDispatch, api.getDispatchCommand) + const continueReview = pending.value?.action === 'EDIT' && pending.value.submitReviewAfterSave === true + const readCurrent = async (no: string) => { + const value = await api.getDispatch(no) + if (value.dispatchNo === currentNo.value) detail.value = value + return value + } + let result = await recovery.execute(send, readCurrent, api.getDispatchCommand) if (!result) return + if (continueReview) { + emit('saved') + if (!canDispatchAction(detail.value?.actions, 'SUBMIT_REVIEW', access.can)) throw new DispatchInputError(detail.value?.actions.SUBMIT_REVIEW?.blockReason || '车辆信息已保存,当前不能提交审核,请核对资料') + recovery.begin(result.dispatchNo, 'SUBMIT_REVIEW', { recordVersion: requireVersion(detail.value?.recordVersion), idempotencyKey: crypto.randomUUID(), remark: null }) + editorRequestKey = String(pending.value?.body.idempotencyKey || '') + result = await recovery.execute(send, readCurrent, api.getDispatchCommand) + if (!result) return + } ElMessage.success(reviewReturned ? '已退回客服处理(车辆信息),修改后需重新提交调度审核' : result.action === 'LOADING_COMPLETE' ? result.stage === 'DELIVERING' ? '装车完成、资料齐全,已进入派送中' : '装车完成,请在装车后补齐装车资料' : routeCommand ? '卸货顺序调整已确认,请重新核对两级审核' : result.alreadyArranged ? '已引用本车原派送安排,未重复生成单据和费用' : result.replayed ? '原操作结果已核对' : `${dispatchActionLabels[result.action]}已确认`) emit('saved'); open.value = false } catch (cause) { @@ -157,7 +175,7 @@ async function submit() { emit('rejected', error.value) await refreshFacts() } - } + } finally { submitting.value = false } } async function refreshFacts() { const sequence = ++generation, no = currentNo.value @@ -185,7 +203,7 @@ function moveStop(index: number, offset: number) { const target = index + offset watch([open, () => props.dispatchNo, () => props.action, () => props.operation], ([value]) => { if (value) void load(); else { generation++ } }, { immediate: true }) - -

操作说明

操作开始天数结束天数价格
删除以上
-

本次备注

+

本次备注

+