style(admin): unify dialog layouts and dense readonly presentation

This commit is contained in:
2026-10-20 10:11:31 +08:00
parent 7ff7c9914f
commit 3267679758
214 changed files with 10931 additions and 2654 deletions
@@ -1,16 +1,18 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/core/auth/auth'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AppDialog from '@/shared/components/AppDialog.vue'
import LineClampText from '@/shared/components/LineClampText.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import * as api from '../api/dispatches'
import type { AddCargo, ArrangeDelivery, ArrangeGroup, ArrangementPreview, Candidate, CommandResult, DispatchAction, DispatchDetail, Driver, LoadingComplete, LoadingPreview, ReasonCommand, ReviewCommand, UpdateDispatch, Vehicle, VersionCommand } from '../dispatchContracts'
import type { DispatchItems, DispatchRoute, DispatchRouteCommand, DispatchRouteStop } from '../dispatchContracts'
import { DispatchInputError, requireVersion, requireBatchVersions } from '../dispatchContracts'
import { DispatchInputError, requireVersion, requireBatchVersions, requireSelectionGroup, expandSelectionGroups, selectionGroupBlockReason } from '../dispatchContracts'
import { canDispatchAction, dispatchActionLabels } from '../dispatchPresentation'
import { useDispatchCommandRecovery, type PendingDispatchCommand } from '../dispatchCommandRecovery'
import DispatchResourceFields from './DispatchResourceFields.vue'
@@ -30,13 +32,14 @@ const items = ref<DispatchItems | null>(null), itemId = ref(''), route = ref<Dis
const detail = ref<DispatchDetail | null>(null), loading = ref(false), error = ref<ApiErrorDetails | null>(null)
const preview = ref<LoadingPreview | null>(null), arrangement = ref<ArrangementPreview | null>(null), groups = ref<ArrangeGroup[]>([])
const arrangeFields = ref<InstanceType<typeof DispatchArrangeFields> | null>(null)
const candidates = ref<Candidate[]>([]), selected = ref<string[]>([]), candidateKeyword = ref('')
const candidates = ref<Candidate[]>([]), selected = ref<Array<{ sourceKey: string; groupKey: string; groupVersion: string }>>([]), candidateKeyword = ref('')
const measurements = ref<Array<{ warehousePalletId: string; palletNo: string; palletSpaces: number | null; weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null }>>([])
const blankVehicle = (): Vehicle => ({ mode: 'MASTER', vehicleId: null, plate: null, carrierName: null, capacity: { palletSpaces: null, maxLoadKg: null, maxVolumeCbm: null }, evidenceFileIds: [] })
const blankDriver = (): Driver => ({ mode: 'MASTER', driverId: null, name: null, phone: null, evidence: { licenseNo: null, licenseExpiresOn: null, fileIds: [], expectedTaskEndAt: null } })
const vehicle = ref<Vehicle>(blankVehicle()), driver = ref<Driver>(blankDriver())
const form = reactive({ plannedDepartureAt: '', estimatedArrivalAt: '', remark: '', decision: 'APPROVED' as 'APPROVED' | 'REJECTED', reason: '' })
let generation = 0
let editorRequestKey = ''
const allowed = computed(() => !pending.value && canDispatchAction(detail.value?.actions, currentAction.value, access.can) && (currentOperation.value !== 'ROUTE' || route.value?.edit.allowed === true))
const width = computed(() => ['EDIT', 'LOADING_COMPLETE', 'ARRANGE_DELIVERY', 'ADD_CARGO', 'REMOVE_CARGO'].includes(currentAction.value) ? '1120px' : ['DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(currentAction.value) ? '760px' : '560px')
const frozen = computed(() => busy.value || Boolean(pending.value))
@@ -45,6 +48,7 @@ const uploadingEvidence = computed(() => evidenceBusy.VEHICLE || evidenceBusy.DR
async function load() {
const sequence = ++generation, no = currentNo.value, action = currentAction.value
if (editorRequestKey && pending.value?.body.idempotencyKey === editorRequestKey && detail.value?.dispatchNo === no) { loading.value = false; return }
loading.value = true; error.value = null; detail.value = null; preview.value = null; arrangement.value = null; selected.value = []
items.value = null; itemId.value = ''; route.value = null; stops.value = []
try {
@@ -110,6 +114,15 @@ function requirePlanPallet(row: Candidate): string {
if (!row.selection.planPalletId) throw new DispatchInputError('批次分配缺少真实计划来源,请核对')
return row.selection.planPalletId
}
function isCargoSelected(row: Candidate): boolean { return selected.value.some(value => value.groupKey === row.selectionGroup?.groupKey) }
function selectCargo(row: Candidate, checked: unknown) {
try {
if (checked !== true) { selected.value = selected.value.filter(value => value.groupKey !== row.selectionGroup?.groupKey && value.sourceKey !== row.sourceKey); return }
const group = requireSelectionGroup(row)
selected.value = selected.value.filter(value => value.groupKey !== group.groupKey && value.sourceKey !== row.sourceKey)
if (checked === true) selected.value.push({ sourceKey: row.sourceKey, groupKey: group.groupKey, groupVersion: group.groupVersion })
} catch (cause) { error.value = getApiErrorDetails(cause, cause instanceof Error ? cause.message : '完整选货组无效') }
}
function payload(): object {
if (!detail.value || !allowed.value) throw new DispatchInputError(detail.value?.actions[currentAction.value]?.blockReason || '当前操作未获得服务端允许,请刷新核对')
const common = { recordVersion: requireVersion(detail.value.recordVersion), idempotencyKey: crypto.randomUUID() }
@@ -126,10 +139,14 @@ function payload(): object {
}
case 'EDIT': return { ...common, vehicle: vehicle.value, driver: driver.value, plannedDepartureAt: form.plannedDepartureAt || null, estimatedArrivalAt: form.estimatedArrivalAt || null, remark: form.remark || null } satisfies UpdateDispatch
case 'ADD_CARGO': {
const chosen = candidates.value.filter(row => selected.value.includes(row.sourceKey))
const chosen = selected.value.map(value => {
const row = candidates.value.find(candidate => candidate.sourceKey === value.sourceKey)
if (!row || row.selectionGroup?.groupKey !== value.groupKey || row.selectionGroup.groupVersion !== value.groupVersion) throw new DispatchInputError('选货组组成已变化,请重新读取并选择货物')
return row
})
requireBatchVersions(chosen.flatMap(row => row.appointmentBatches))
if (!chosen.length || chosen.some(row => row.blockReasons.length || !row.sourceVersion || row.sourceVersion !== row.selection.sourceVersion || row.appointmentBatches.some(batch => batch.customerId !== row.customerId))) throw new DispatchInputError('请选择来源完整、预约客户一致且可排的货物')
return { ...common, cargo: chosen.map(row => row.selection), batchAllocations: chosen.flatMap(row => row.appointmentBatches.flatMap(batch => batch.sources.map(source => ({ planPalletId: requirePlanPallet(row), batchKey: batch.batchKey, customerId: batch.customerId, ...source, recordVersion: batch.recordVersion })))) } satisfies AddCargo
return { ...common, cargo: expandSelectionGroups(chosen), batchAllocations: chosen.flatMap(row => row.appointmentBatches.flatMap(batch => batch.sources.map(source => ({ planPalletId: requirePlanPallet(row), batchKey: batch.batchKey, customerId: batch.customerId, ...source, recordVersion: batch.recordVersion })))) } satisfies AddCargo
}
case 'DISPATCH_REVIEW': case 'LOADING_REVIEW':
if (form.decision === 'REJECTED' && !form.reason.trim()) throw new DispatchInputError('驳回时必须填写原因')
@@ -181,7 +198,10 @@ async function submit() {
error.value = null
try {
if (pending.value?.action === 'CREATE') throw new DispatchInputError('请从排车列表的原请求入口恢复新建排车')
if (!pending.value) recovery.begin(props.dispatchNo, props.action, payload(), { operation: currentOperation.value, ...(props.action === 'REMOVE_CARGO' ? { itemId: itemId.value } : {}) })
if (!pending.value) {
recovery.begin(props.dispatchNo, props.action, payload(), { operation: currentOperation.value, ...(props.action === 'REMOVE_CARGO' ? { itemId: itemId.value } : {}) })
editorRequestKey = String(recovery.pending.value?.body.idempotencyKey || '')
}
const routeCommand = pending.value?.operation === 'ROUTE'
const result = await recovery.execute(send, api.getDispatch, api.getDispatchCommand)
if (!result) return
@@ -193,24 +213,61 @@ 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" :title="currentOperation === 'ROUTE' ? '调整卸货顺序' : dispatchActionLabels[currentAction]" :width="width" :confirm-loading="busy" :confirm-disabled="loading || uploadingEvidence || !pending && !allowed" :confirm-text="pending ? '核对并重试原请求' : '确认提交'" @confirm="submit">
<p>排车单号 {{ currentNo || '—' }}</p><p v-if="pending" role="status">原操作结果尚待核对,当前使用原版本和原请求内容。</p>
<LoadErrorState v-if="error" title="排车操作未完成" :message="error.message" :loading="loading" @retry="pending ? submit() : load()"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<el-button v-if="pending?.rejected" :disabled="busy" @click="recovery.discardRejected(); load()">按明确驳回结果重新核对</el-button>
<p v-if="detail?.actions[currentAction]?.allowed !== true && !pending">{{ detail?.actions[currentAction]?.blockReason || '当前操作资格未返回,请刷新核对' }}</p>
<el-form v-if="detail" class="density-form" label-position="top">
<template v-if="currentOperation === 'ROUTE' && route"><p>起运仓:{{ route.stops[0]?.name || '—' }} · {{ route.stops[0]?.address || '—' }}</p><p>{{ route.edit.blockReason || '仅调整本车既有卸货点,外接卸货点一并保留。调整后须重新审核。' }}</p><ol><li v-for="(stop, index) in stops" :key="stop.destinationKey || stop.index">{{ stop.name }} · {{ stop.address || '—' }} <el-button :disabled="frozen || index === 0" @click="moveStop(index, -1)">上移</el-button><el-button :disabled="frozen || index === stops.length - 1" @click="moveStop(index, 1)">下移</el-button></li></ol></template><template v-else-if="currentAction === 'REMOVE_CARGO' && items"><el-table :data="items.items" border max-height="420"><el-table-column label="操作" width="140" fixed="left"><template #default="{ row }"><el-radio v-model="itemId" :value="row.dispatchItemId" :disabled="frozen || row.remove.allowed !== true || row.pickedPackages > 0">移除此项</el-radio></template></el-table-column><el-table-column prop="customerName" label="客户" /><el-table-column prop="orderNo" label="来源订单" /><el-table-column prop="inboundCode" label="入仓码" /><el-table-column prop="palletNo" label="托盘码" /><el-table-column prop="scheduledPackages" label="本次箱数" /><el-table-column prop="pickedPackages" label="真实已拣" /><el-table-column label="移货资格"><template #default="{ row }">{{ row.remove.blockReason || '可移除' }}</template></el-table-column></el-table></template><template v-else-if="currentAction === 'EDIT'"><DispatchResourceFields v-model:vehicle="vehicle" v-model:driver="driver" :disabled="frozen" /><div class="form-grid"><el-form-item label="计划发车时间"><el-date-picker v-model="form.plannedDepartureAt" :disabled="frozen" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择计划发车时间" /></el-form-item><el-form-item label="预计到达时间"><el-date-picker v-model="form.estimatedArrivalAt" :disabled="frozen" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择预计到达时间" /></el-form-item></div></template>
<template v-else-if="currentAction === 'DISPATCH_REVIEW' || currentAction === 'LOADING_REVIEW'"><p>{{ detail.vehicleInfo.vehicle?.plate || '—' }} · {{ detail.vehicleInfo.driver?.name || '—' }} · {{ detail.counts.palletCount }}托/{{ detail.counts.packageCount }}箱</p><p v-for="appointment in detail.dispatchInfo.fbaAppointments" :key="appointment.batchKey">{{ appointment.destinationWarehouseCode }} · {{ appointment.appointmentNo || '—' }} · {{ appointment.status }}</p><p>请核对本车资源与本趟证据;审核通过不代替后续现场扫码。</p><el-form-item label="审核结果"><el-radio-group v-model="form.decision" :disabled="frozen"><el-radio value="APPROVED">审核通过</el-radio><el-radio value="REJECTED">驳回</el-radio></el-radio-group></el-form-item></template>
<template v-else-if="currentAction === 'ADD_CARGO'"><div class="density-actions"><el-input v-model="candidateKeyword" :disabled="frozen" placeholder="订单号/入仓码" /><el-button :disabled="frozen" @click="searchCargo">查询</el-button></div><el-table :data="candidates" border max-height="380"><el-table-column label="选择" width="70"><template #default="{ row }"><el-checkbox :model-value="selected.includes(row.sourceKey)" @change="selected = $event === true ? [...selected, row.sourceKey] : selected.filter(key => key !== row.sourceKey)" :disabled="frozen || row.blockReasons.length > 0" :aria-label="`选择货物 ${row.palletNo || row.orderNo || '待核实'}`" /></template></el-table-column><el-table-column prop="customerName" label="客户" /><el-table-column prop="orderNo" label="订单号" /><el-table-column prop="palletNo" label="托盘码" /><el-table-column prop="availablePackages" label="可排箱数" /><el-table-column label="目的仓"><template #default="{ row }">{{ row.destination.warehouseCode || row.destination.address || '—' }}</template></el-table-column></el-table></template>
<template v-else-if="currentAction === 'LOADING_COMPLETE' && preview"><p>{{ preview.loadingComplete.blockReason || 'PDA真实拣货已核平,请核对每托实际装载。' }}</p><el-table :data="measurements" border><el-table-column prop="palletNo" label="实物托盘码" min-width="170" /><el-table-column label="实际托位"><template #default="{ row }"><el-input-number v-model="row.palletSpaces" :disabled="frozen" :min="1" placeholder="请确认" /></template></el-table-column><el-table-column label="实测重量(kg)"><template #default="{ row }"><el-input-number v-model="row.weightKg" :disabled="frozen" :min="0.001" placeholder="请核对真实实测" /></template></el-table-column><el-table-column label="实测体积(m³)"><template #default="{ row }"><el-input-number v-model="row.volumeCbm" :disabled="frozen" :min="0.001" placeholder="请核对真实实测" /></template></el-table-column><el-table-column label="已匹配卸货顺序"><template #default="{ row }">{{ row.deliveryIndex ?? '尚未匹配,请调度核对' }}</template></el-table-column></el-table><p v-for="pallet in preview.pallets" :key="pallet.warehousePalletId">{{ pallet.palletNo }}:<span v-for="evidence in pallet.evidence" :key="evidence.field + evidence.sourceType">{{ evidence.actual ? '实测依据' : '仅供参考' }} {{ evidence.sourceBusinessNo || '—' }} {{ evidence.value ?? '—' }};</span></p></template>
<DispatchArrangeFields v-else-if="currentAction === 'ARRANGE_DELIVERY' && arrangement" ref="arrangeFields" v-model="groups" :preview="arrangement.groups" :disabled="frozen" />
<p v-else-if="currentAction === 'DEPART'">确认本车实际发车。装车完成及全部客户派送安排由服务端再次核验。</p><p v-else-if="currentAction === 'RETURN_TO_REVIEW'">仅零真实扫码允许退回装车前,保留原排车号,修改后两级审核重新提交。</p><p v-else-if="currentAction === 'CANCEL'">确认取消本排车单;服务端将核对真实作业和来源占用,不自动回库。</p>
<template v-if="currentOperation !== 'ROUTE' && ['EDIT', 'DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(currentAction)"><DispatchEvidenceFiles v-model="vehicle.evidenceFileIds" :dispatch-no="currentNo" kind="VEHICLE" :readonly="currentAction !== 'EDIT'" :disabled="frozen" @busy="evidenceBusy.VEHICLE = $event" /><DispatchEvidenceFiles v-if="driver.evidence" v-model="driver.evidence.fileIds" :dispatch-no="currentNo" kind="DRIVER" :readonly="currentAction !== 'EDIT'" :disabled="frozen" @busy="evidenceBusy.DRIVER = $event" /></template><el-form-item v-if="currentOperation === 'ROUTE' || ['REMOVE_CARGO', 'CANCEL', 'RETURN_TO_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(currentAction)" :label="currentOperation === 'ROUTE' || form.decision === 'REJECTED' || ['REMOVE_CARGO', 'CANCEL', 'RETURN_TO_REVIEW'].includes(currentAction) ? '原因(必填)' : '审核意见(选填)'"><el-input v-model="form.reason" :disabled="frozen" type="textarea" :rows="3" placeholder="请输入操作原因或审核意见" /></el-form-item>
<el-form-item v-else-if="currentAction !== 'ARRANGE_DELIVERY' && currentAction !== 'ADD_CARGO'" label="备注"><el-input v-model="form.remark" :disabled="frozen" type="textarea" :rows="3" placeholder="请输入本次备注(选填)" /></el-form-item>
</el-form>
<AppDialog v-model="open" class="admin-density-dialog admin-density" :title="(currentOperation === 'ROUTE' ? '调整卸货顺序' : dispatchActionLabels[currentAction]) + ' · ' + (currentNo || '—')" :width="width" :fullscreenable="width !== '560px'" :loading="loading" :confirm-loading="busy || uploadingEvidence" :confirm-disabled="loading || uploadingEvidence || !pending && !allowed" :confirm-text="pending ? '核对并重试原请求' : '确认提交'" @confirm="submit">
<div class="density-dialog-sections">
<el-alert v-if="pending" role="status" title="原操作结果尚待核对,当前使用原版本和原请求内容。" type="warning" :closable="false" show-icon />
<LoadErrorState v-if="error" title="排车操作未完成" :message="error.message" :loading="loading || busy || uploadingEvidence" @retry="pending ? submit() : load()"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<el-button v-if="pending?.rejected" :disabled="busy || loading || uploadingEvidence" @click="recovery.discardRejected(); load()">按明确驳回结果重新核对</el-button>
<el-alert v-if="detail && !allowed && !pending" :title="detail?.actions[currentAction]?.blockReason || '当前操作未获得允许,请核对权限与作业状态'" type="warning" :closable="false" show-icon />
<el-form v-if="detail" class="density-form density-dialog-sections" label-position="left" label-width="130px" :disabled="frozen || loading || uploadingEvidence || !allowed">
<section v-if="currentOperation === 'ROUTE' && route" 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>{{ route.stops[0]?.name || '—' }}</td><th scope="row">地址</th><td>{{ route.stops[0]?.address || '—' }}</td></tr></tbody></table>
<el-alert :title="route.edit.blockReason || '仅调整本车既有卸货点,外接卸货点一并保留。调整后须重新审核。'" type="info" :closable="false" show-icon />
<table class="density-table"><thead><tr><th>操作</th><th>顺序</th><th>卸货点</th><th>地址</th></tr></thead><tbody><tr v-for="(stop, index) in stops" :key="stop.destinationKey || stop.index"><td class="workflow-order-actions"><el-button :disabled="frozen || loading || uploadingEvidence || !allowed || index === 0" @click="moveStop(index, -1)">上移</el-button><el-button :disabled="frozen || loading || uploadingEvidence || !allowed || index === stops.length - 1" @click="moveStop(index, 1)">下移</el-button></td><td>{{ index + 1 }}</td><td>{{ stop.name }}</td><td>{{ stop.address || '—' }}</td></tr></tbody></table>
</section>
<section v-else-if="currentAction === 'REMOVE_CARGO' && items" class="density-dialog-section" data-tone="source" aria-label="本车可移除货物"><h3 class="density-dialog-section__title">本车货物</h3>
<el-table :data="items.items" border><el-table-column label="操作" width="140" fixed="left"><template #default="{ row }"><el-radio v-model="itemId" :value="row.dispatchItemId" :disabled="frozen || loading || uploadingEvidence || !allowed || row.remove.allowed !== true || row.pickedPackages > 0">移除此项</el-radio></template></el-table-column><el-table-column prop="customerName" label="客户" min-width="140" show-overflow-tooltip /><el-table-column prop="orderNo" label="来源订单" min-width="150" show-overflow-tooltip /><el-table-column prop="inboundCode" label="入仓码" min-width="150" show-overflow-tooltip /><el-table-column prop="palletNo" label="托盘码" min-width="150" show-overflow-tooltip /><el-table-column prop="scheduledPackages" label="本次箱数" width="100" /><el-table-column prop="pickedPackages" label="真实已拣" width="100" /><el-table-column label="移货资格" min-width="180" show-overflow-tooltip><template #default="{ row }">{{ row.remove.blockReason || '可移除' }}</template></el-table-column></el-table>
</section>
<template v-else-if="currentAction === 'EDIT'">
<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>
<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><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></tr><tr><th scope="row">托盘数</th><td>{{ detail.counts.palletCount }} 托</td><th scope="row">箱数</th><td>{{ detail.counts.packageCount }} 箱</td></tr></tbody></table>
<table v-if="detail.dispatchInfo.fbaAppointments.length" class="density-table"><thead><tr><th>目的仓</th><th>预约单号</th><th>预约状态</th></tr></thead><tbody><tr v-for="appointment in detail.dispatchInfo.fbaAppointments" :key="appointment.batchKey"><td>{{ appointment.destinationWarehouseCode }}</td><td>{{ appointment.appointmentNo || '—' }}</td><td>{{ appointment.status }}</td></tr></tbody></table>
<el-alert title="请核对本车资源与本趟证据;审核通过不代替后续现场扫码。" type="info" :closable="false" show-icon />
</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"><tbody><tr><td><el-form-item label="审核结果"><el-radio-group v-model="form.decision" :disabled="frozen || loading || uploadingEvidence || !allowed"><el-radio value="APPROVED">审核通过</el-radio><el-radio value="REJECTED">驳回</el-radio></el-radio-group></el-form-item></td></tr></tbody></table></section>
</template>
<section v-else-if="currentAction === 'ADD_CARGO'" class="density-dialog-section" data-tone="source" aria-label="本车添加货物"><h3 class="density-dialog-section__title">选择完整货物组</h3>
<div class="workflow-cargo-query"><el-input v-model="candidateKeyword" :disabled="frozen || loading || uploadingEvidence || !allowed" placeholder="订单号/入仓码" /><el-button :disabled="frozen || loading || uploadingEvidence || !allowed" @click="searchCargo">查询</el-button></div>
<el-table :data="candidates" border><el-table-column label="选择" width="70" fixed="left"><template #default="{ row }"><el-checkbox :model-value="isCargoSelected(row as Candidate)" @change="selectCargo(row as Candidate, $event)" :disabled="frozen || loading || uploadingEvidence || !allowed || (!isCargoSelected(row as Candidate) && Boolean(selectionGroupBlockReason(row as Candidate)))" :aria-label="'选择货物 ' + (row.palletNo || row.orderNo || '待核实')" /></template></el-table-column><el-table-column prop="customerName" label="客户" min-width="140" show-overflow-tooltip /><el-table-column prop="orderNo" label="订单号" min-width="150" show-overflow-tooltip /><el-table-column label="托盘与完整来源组" min-width="240"><template #default="{ row }"><LineClampText :content="row.palletNo || '—'" /><details class="workflow-source-group"><summary>查看完整来源</summary><ul><li v-for="source in row.selectionGroup.sources" :key="source.sourceKey">{{ source.sourceBusinessNo || '—' }} · {{ source.customerName }} · {{ source.packages }}箱</li></ul></details><span>{{ selectionGroupBlockReason(row as Candidate) }}</span></template></el-table-column><el-table-column prop="availablePackages" label="可排箱数" width="100" /><el-table-column label="目的仓" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.destination.warehouseCode || row.destination.address || '—' }}</template></el-table-column></el-table>
</section>
<template v-else-if="currentAction === 'LOADING_COMPLETE' && preview">
<el-alert :title="preview.loadingComplete.blockReason || 'PDA真实拣货已核平,请核对每托实际装载。'" type="info" :closable="false" show-icon />
<section class="density-dialog-section" data-tone="work" aria-label="真实实载登记"><h3 class="density-dialog-section__title">逐托实载</h3><el-table class="density-table--editable" :data="measurements" border><el-table-column prop="palletNo" label="实物托盘码" min-width="170" show-overflow-tooltip /><el-table-column label="实际托位" min-width="140"><template #default="{ row }"><el-input-number v-model="row.palletSpaces" :disabled="frozen || loading || uploadingEvidence || !allowed" :min="1" placeholder="请确认" controls-position="right" /></template></el-table-column><el-table-column label="实测重量(kg)" min-width="170"><template #default="{ row }"><el-input-number v-model="row.weightKg" :disabled="frozen || loading || uploadingEvidence || !allowed" :min="0.001" placeholder="请核对真实实测" controls-position="right" /></template></el-table-column><el-table-column label="实测体积(m³)" min-width="170"><template #default="{ row }"><el-input-number v-model="row.volumeCbm" :disabled="frozen || loading || uploadingEvidence || !allowed" :min="0.001" placeholder="请核对真实实测" controls-position="right" /></template></el-table-column><el-table-column label="已匹配卸货顺序" min-width="170"><template #default="{ row }">{{ row.deliveryIndex ?? '尚未匹配,请调度核对' }}</template></el-table-column></el-table></section>
<section class="density-dialog-section" data-tone="source" aria-label="实载与参考依据"><h3 class="density-dialog-section__title">测量依据</h3><table class="density-table"><thead><tr><th>托盘码</th><th>依据</th><th>来源业务号</th><th>值</th></tr></thead><tbody><template v-for="pallet in preview.pallets" :key="pallet.warehousePalletId"><tr v-for="evidence in pallet.evidence" :key="evidence.field + evidence.sourceType"><td>{{ pallet.palletNo }}</td><td>{{ evidence.actual ? '实测依据' : '仅供参考' }}</td><td>{{ evidence.sourceBusinessNo || '—' }}</td><td>{{ evidence.value ?? '—' }}</td></tr></template></tbody></table></section>
</template>
<DispatchArrangeFields v-else-if="currentAction === 'ARRANGE_DELIVERY' && arrangement" ref="arrangeFields" v-model="groups" :preview="arrangement.groups" :disabled="frozen || loading || uploadingEvidence || !allowed" />
<el-alert v-else-if="currentAction === 'DEPART'" title="确认本车实际发车。装车完成及全部客户派送安排由服务端再次核验。" type="warning" :closable="false" show-icon />
<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' && ['EDIT', 'DISPATCH_REVIEW', 'LOADING_REVIEW'].includes(currentAction)">
<DispatchEvidenceFiles v-model="vehicle.evidenceFileIds" :dispatch-no="currentNo" kind="VEHICLE" :readonly="currentAction !== 'EDIT'" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.VEHICLE = $event" />
<DispatchEvidenceFiles v-if="driver.evidence" v-model="driver.evidence.fileIds" :dispatch-no="currentNo" kind="DRIVER" :readonly="currentAction !== 'EDIT'" :disabled="frozen || loading || uploadingEvidence || !allowed" @busy="evidenceBusy.DRIVER = $event" />
</template>
<section v-if="currentOperation === 'ROUTE' || ['REMOVE_CARGO', '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' || ['REMOVE_CARGO', 'CANCEL', 'RETURN_TO_REVIEW'].includes(currentAction) ? '原因(必填)' : '审核意见(选填)'"><el-input v-model="form.reason" :disabled="frozen || loading || uploadingEvidence || !allowed" type="textarea" :rows="3" placeholder="请输入操作原因或审核意见" /></el-form-item></td></tr></tbody></table></section>
<section v-else-if="currentAction !== 'ARRANGE_DELIVERY' && currentAction !== 'ADD_CARGO'" 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" :disabled="frozen || loading || uploadingEvidence || !allowed" type="textarea" :rows="3" placeholder="请输入本次备注(选填)" /></el-form-item></td></tr></tbody></table></section>
</el-form>
</div>
</AppDialog>
</template>
<style scoped>
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 var(--admin-space-5); }
.form-grid :deep(.el-date-editor) { width: 100%; }
.workflow-editor-table { table-layout: fixed; }
.workflow-editor-table td { width: 50%; }
.workflow-order-actions { width: 160px; white-space: nowrap; }
.workflow-cargo-query { display: flex; gap: var(--admin-space-3); margin-bottom: var(--admin-space-4); }
.workflow-cargo-query > .el-input { max-width: 320px; }
.workflow-source-group > summary { cursor: pointer; }
.workflow-source-group ul { margin: var(--admin-space-2) 0 0; padding-left: 18px; }
</style>