This commit is contained in:
2026-10-27 17:39:19 +08:00
parent b5d8ccdffe
commit c148912b73
49 changed files with 1134 additions and 464 deletions
@@ -10,6 +10,7 @@ import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import * as api from '../api/dispatches'
import { getTransport } from '../api/transportTasks'
import type { MarkDispatchException, DispatchExceptionType, DispatchReturnDirection, CommandResult, DispatchAction, DispatchDetail, Driver, LoadingComplete, LoadingPreview, ReasonCommand, ReviewCommand, UpdateDispatch, Vehicle, VersionCommand } from '../dispatchContracts'
import type { DispatchRoute, DispatchRouteCommand, DispatchRouteStop } from '../dispatchContracts'
import { DispatchInputError, requireVersion } from '../dispatchContracts'
@@ -32,6 +33,7 @@ const currentNo = computed(() => pending.value?.dispatchNo || props.dispatchNo)
const currentOperation = computed(() => pending.value ? pending.value.operation : props.operation)
const route = ref<DispatchRoute | null>(null), stops = ref<DispatchRouteStop[]>([])
const detail = ref<DispatchDetail | null>(null), working = ref(false), error = ref<ApiErrorDetails | null>(null)
const pickupDestinations = ref<string[]>([])
const factsStale = ref(false)
const preview = ref<LoadingPreview | null>(null)
const loading = computed(() => working.value)
@@ -66,7 +68,7 @@ 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 vehicleEditor = computed(() => currentOperation.value !== 'ROUTE' && (currentAction.value === 'EDIT' || props.action === 'EDIT' && currentAction.value === 'SUBMIT_REVIEW'))
const submitting = ref(false)
const width = computed(() => vehicleEditor.value ? '1120px' : isReview.value ? '1100px' : currentAction.value === 'MARK_EXCEPTION' ? '760px' : '560px')
const width = computed(() => vehicleEditor.value ? '1120px' : isReview.value ? '1100px' : currentAction.value === 'SUBMIT_REVIEW' ? '820px' : currentAction.value === 'MARK_EXCEPTION' ? '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))
@@ -75,7 +77,7 @@ async function load() {
const sequence = ++generation, no = currentNo.value, action = currentAction.value
if (editorRequestKey && pending.value?.body.idempotencyKey === editorRequestKey && detail.value?.dispatchNo === no) { working.value = false; return }
working.value = true; factsStale.value = false; error.value = null; detail.value = null; preview.value = null
route.value = null; stops.value = []
route.value = null; stops.value = []; pickupDestinations.value = []
try {
if (cargoAction.value) return
if (action === 'CREATE') throw new DispatchInputError('请从排车列表进入新建排车')
@@ -83,6 +85,13 @@ async function load() {
if (sequence !== generation) return
if (value.dispatchNo !== no) throw new DispatchInputError('排车业务号不一致,请重新打开')
detail.value = value
// 仓储转运取货显示原单取货地址,不把排车摘要当作起运地。
if (action === 'SUBMIT_REVIEW' && value.taskType === 'PICKUP') {
const transport = await getTransport(no)
if (sequence !== generation) return
if (transport.dispatchNo !== no) throw new DispatchInputError('取货资料与排车单不一致')
pickupDestinations.value = [...new Set(transport.pickups.map(row => row.pickupAddress?.trim()).filter((address): address is string => Boolean(address)))]
}
form.maintenance = value.vehicleInfo.maintenance ? { ...value.vehicleInfo.maintenance } : blankVehicleMaintenance()
fillDestinationAppointments(value)
Object.assign(exception, { exceptionType: '', returnDirection: '', description: '', imageFileIds: [] })
@@ -138,7 +147,11 @@ function payload(): object {
if (!measurements.value.length || measurements.value.some(row => row.deliveryIndex == null || !Number.isInteger(row.deliveryIndex) || row.deliveryIndex < 1)) throw new DispatchInputError('托盘卸货站尚未匹配,请调度核对路线')
return { recordVersion: requireVersion(preview.value.recordVersion), idempotencyKey: common.idempotencyKey, manifestHash: preview.value.manifestHash, remark: form.remark || null } satisfies LoadingComplete
}
case 'SUBMIT_REVIEW': return { ...common, remark: null }
case 'SUBMIT_REVIEW':
if (!detail.value.vehicleInfo.vehicle?.vehicleId || !detail.value.vehicleInfo.driver?.driverId) {
throw new DispatchInputError('车辆信息尚未维护完整,请先点击“维护车辆”并保存车辆、司机后再提交审核')
}
return { ...common, remark: null }
default: return { ...common, remark: form.remark || null }
}
}
@@ -257,6 +270,18 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],
</template>
</DispatchVehicleMaintenanceForm>
</template>
<template v-else-if="currentAction === 'SUBMIT_REVIEW'">
<section class="density-dialog-section" data-tone="source" aria-label="提交审核依据">
<h3 class="density-dialog-section__title">审核依据</h3>
<table class="density-table"><tbody>
<tr><th scope="row">车牌号</th><td>{{ detail.vehicleInfo.vehicle?.plate || '未维护' }}</td><th scope="row">司机</th><td>{{ detail.vehicleInfo.driver?.name || '未维护' }}</td><th scope="row">联系电话</th><td>{{ detail.vehicleInfo.driver?.phone || '—' }}</td></tr>
<tr v-if="detail.taskType === 'PICKUP'"><th scope="row">取货目的地</th><td colspan="5"><div v-for="address in pickupDestinations" :key="address">{{ address }}</div><span v-if="!pickupDestinations.length">—</span></td></tr>
<tr v-else><th scope="row">起运地</th><td>{{ detail.dispatchInfo.route?.split(' → ')[0] || '—' }}</td><th scope="row">目的地</th><td colspan="3">{{ detail.dispatchInfo.route?.split(' → ').slice(1).join(' → ') || '—' }}</td></tr>
<tr><th scope="row">计划发车</th><td>{{ reviewTime(detail.dispatchInfo.plannedDepartureAt) }}</td><th scope="row">预计送达</th><td>{{ reviewTime(detail.dispatchInfo.estimatedArrivalAt) }}</td><th scope="row">货量</th><td>{{ detail.counts.palletCount }} 托 / {{ detail.counts.packageCount }} 箱</td></tr>
</tbody></table>
<el-alert v-if="!detail.vehicleInfo.vehicle?.vehicleId || !detail.vehicleInfo.driver?.driverId" title="车辆信息尚未维护完整,请先点击“维护车辆”并保存车辆、司机后再提交审核。" type="warning" :closable="false" show-icon />
</section>
</template>
<template v-else-if="currentAction === 'DISPATCH_REVIEW' || currentAction === 'LOADING_REVIEW'">
<section class="density-dialog-section" data-tone="source" aria-label="本车审核依据">
<h3 class="density-dialog-section__title">车辆与货物</h3>
@@ -278,7 +303,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 />
<section v-if="isReview" class="density-dialog-section" data-tone="source" aria-label="审核照片">
<section v-if="isReview || currentAction === 'SUBMIT_REVIEW' && !vehicleEditor" class="density-dialog-section" data-tone="source" aria-label="审核照片">
<h3 class="density-dialog-section__title">车辆司机照片</h3>
<div class="workflow-photo-grid density-dialog-sections" aria-label="车辆司机照片">
<DispatchEvidenceFiles v-for="field in dispatchReviewAttachmentFields" :key="field.key" photo-card readonly :model-value="reviewEvidence[field.key]" :dispatch-no="currentNo" :kind="field.kind" :label="field.label" />