This commit is contained in:
2026-10-21 17:49:04 +08:00
parent 22e60248d2
commit 4f5cec1b94
20 changed files with 192 additions and 56 deletions
@@ -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<CommandResult> {
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 })
</script>
<template>
<AppDialog v-model="open" class="admin-density-dialog admin-density admin-workbench is-dialog" :title="(currentOperation === 'ROUTE' ? '调整卸货顺序' : dispatchActionLabels[currentAction]) + ' · ' + (currentNo || '—')" :width="width" :fullscreenable="width !== '560px'" :loading="loading" :confirm-loading="busy || uploadingEvidence" :confirm-disabled="cargoAction || loading || uploadingEvidence || !pending && !allowed" :confirm-text="cargoAction ? '请从智能排车修改' : pending?.result ? '刷新页面资料' : pending ? '核对并重试原请求' : currentAction === 'MARK_EXCEPTION' ? '保存' : '确认提交'" @confirm="submit">
<AppDialog v-model="open" class="admin-density-dialog admin-density admin-workbench is-dialog" :title="(currentOperation === 'ROUTE' ? '调整卸货顺序' : dispatchActionLabels[currentAction]) + ' · ' + (currentNo || '—')" :width="width" :fullscreenable="width !== '560px'" :loading="loading" :confirm-loading="submitting || busy || uploadingEvidence" :confirm-disabled="cargoAction || loading || uploadingEvidence || !pending && !allowed" :confirm-text="cargoAction ? '请从智能排车修改' : pending?.result ? '刷新页面资料' : pending ? '核对并重试原请求' : currentAction === 'MARK_EXCEPTION' ? '保存' : '确认提交'" @confirm="submit()">
<div class="density-dialog-sections">
<el-alert v-if="cargoAction" :title="cargoEntryMessage" type="warning" :closable="false" show-icon />
<el-alert v-if="dispatchRejectionReason(detail?.reviews)" title="已退回客服处理,请核对原因后重新提交调度审核。" :description="dispatchRejectionReason(detail?.reviews)" type="warning" :closable="false" show-icon />
@@ -204,7 +222,7 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],
<el-form-item label="异常说明"><el-input v-model="exception.description" type="textarea" :rows="3" :maxlength="1000" show-word-limit /></el-form-item>
<DispatchEvidenceFiles v-model="exception.imageFileIds" :dispatch-no="currentNo" kind="EXCEPTION" label="异常图片" save-hint="上传后点击保存,图片与本次异常一起登记。" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.EXCEPTION = $event" />
</template>
<template v-else-if="currentAction === 'EDIT'">
<template v-else-if="vehicleEditor">
<section class="density-dialog-section" data-tone="work" aria-label="本车资源登记"><h3 class="density-dialog-section__title">车辆与司机</h3><DispatchResourceFields continuous v-model:vehicle="vehicle" v-model:driver="driver" :disabled="frozen || loading || uploadingEvidence || !allowed" /></section>
<section class="density-dialog-section" data-tone="work" aria-label="本车时间登记"><h3 class="density-dialog-section__title">排车时间</h3><table class="density-table density-table--editable workflow-editor-table"><tbody><tr><td><el-form-item label="计划发车时间"><el-date-picker v-model="form.plannedDepartureAt" :disabled="frozen || loading || uploadingEvidence || !allowed" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择计划发车时间" /></el-form-item></td><td><el-form-item label="预计到达时间"><el-date-picker v-model="form.estimatedArrivalAt" :disabled="frozen || loading || uploadingEvidence || !allowed" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择预计到达时间" /></el-form-item></td></tr></tbody></table></section>
</template>
@@ -222,7 +240,7 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],
</template>
<el-alert v-else-if="currentAction === 'RETURN_TO_REVIEW'" title="仅零真实扫码允许退回装车前,保留原排车号,修改后两级审核重新提交。" type="warning" :closable="false" show-icon />
<el-alert v-else-if="currentAction === 'CANCEL'" title="确认取消本排车单;服务端将核对真实作业和来源占用,不自动回库。" type="warning" :closable="false" show-icon />
<template v-if="currentOperation !== 'ROUTE' && currentAction === 'EDIT'">
<template v-if="vehicleEditor">
<DispatchEvidenceFiles v-model="vehicle.evidenceFileIds" :dispatch-no="currentNo" kind="VEHICLE" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.VEHICLE = $event" />
<DispatchEvidenceFiles v-if="driver.evidence" v-model="driver.evidence.fileIds" :dispatch-no="currentNo" kind="DRIVER" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.DRIVER = $event" />
</template>
@@ -230,9 +248,14 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],
<DispatchEvidenceFiles v-for="field in dispatchReviewAttachmentFields" :key="field.key" v-model="reviewAttachments[field.key]" :reference-ids="reviewEvidence[field.key]" :dispatch-no="currentNo" :kind="field.kind" :label="field.label" save-hint="上传后提交审核,附件与本次审核结果一起保存。" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy[field.kind] = $event" />
</template>
<section v-if="currentOperation === 'ROUTE' || ['CANCEL', 'RETURN_TO_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(currentAction)" class="density-dialog-section" data-tone="work" aria-label="操作原因与意见"><h3 class="density-dialog-section__title">操作说明</h3><table class="density-table density-table--editable"><tbody><tr><td><el-form-item :label="currentOperation === 'ROUTE' || form.decision === 'REJECTED' || ['CANCEL', 'RETURN_TO_REVIEW'].includes(currentAction) ? '原因(必填)' : '审核意见(选填)'"><el-input v-model="form.reason" :disabled="frozen || loading || uploadingEvidence || !allowed" type="textarea" :rows="3" /></el-form-item></td></tr></tbody></table></section>
<section v-else-if="currentAction !== 'MARK_EXCEPTION'" class="density-dialog-section" data-tone="work" aria-label="本次操作备注"><h3 class="density-dialog-section__title">本次备注</h3><table class="density-table density-table--editable"><tbody><tr><td><el-form-item label="备注"><el-input v-model="form.remark" aria-label="本次操作备注" :disabled="frozen || loading || uploadingEvidence || !allowed" type="textarea" :rows="3" /></el-form-item></td></tr></tbody></table></section>
<section v-else-if="!['MARK_EXCEPTION', 'EDIT', 'SUBMIT_REVIEW'].includes(currentAction)" class="density-dialog-section" data-tone="work" aria-label="本次操作备注"><h3 class="density-dialog-section__title">本次备注</h3><table class="density-table density-table--editable"><tbody><tr><td><el-form-item label="备注"><el-input v-model="form.remark" aria-label="本次操作备注" :disabled="frozen || loading || uploadingEvidence || !allowed" type="textarea" :rows="3" /></el-form-item></td></tr></tbody></table></section>
</el-form>
</div>
<template v-if="vehicleEditor" #footer="{ close }">
<el-button :disabled="submitting || busy || uploadingEvidence" @click="close">取消</el-button>
<el-button v-if="!pending" :disabled="submitting || loading || uploadingEvidence || !allowed" @click="submit()">保存</el-button>
<el-button type="primary" :loading="submitting || busy" :disabled="loading || uploadingEvidence || !pending && !allowed" @click="submit(true)">{{ pending?.result ? '刷新页面资料' : pending ? '核对并重试原请求' : '提交审核' }}</el-button>
</template>
</AppDialog>
</template>
<style scoped>