This commit is contained in:
2026-10-21 17:28:04 +08:00
parent 5e8daad5a0
commit fde16235ea
12 changed files with 233 additions and 62 deletions
@@ -19,8 +19,9 @@ const props = defineProps<{ dispatchNo: string; pickupApproved?: boolean; pickup
const access = useAccessPreviewStore(), detail = ref<TransportDetail | null>(null), error = ref<ApiErrorDetails | null>(null), busy = ref(false)
const vehicles = ref<VehicleDetail[]>([]), drivers = ref<DriverDetail[]>([]), vehicleId = ref(''), driverId = ref(''), reason = ref('')
const occupancy = ref<Record<string, boolean>>({}), candidateBusy = ref(false)
const selected = ref<PickupTask | null>(null), pickupOpen = ref(false), actual = ref(1), evidence = ref<PickupEvidence[]>([]), truckEvidence = ref<PickupEvidence[]>([])
const selected = ref<PickupTask | null>(null), pickupOpen = ref(false), actual = ref<number>(), evidence = ref<PickupEvidence[]>([]), truckEvidence = ref<PickupEvidence[]>([])
const arrivalMode = ref(false)
const validPickupQuantity = computed(() => typeof actual.value === 'number' && Number.isInteger(actual.value) && actual.value >= 1 && actual.value <= 5000)
const editingResources = ref(props.editResources === true)
const pickupPhotos = (row: PickupTask, phase: 'PICKUP' | 'ARRIVAL') => row.evidence.filter(file => ((file as PickupEvidence & { phase?: string }).phase || 'PICKUP') === phase)
const pickupStage = computed(() => trip.value?.status === 'COMPLETED' ? '取货派送完成' : trip.value?.status === 'CANCELLED' ? '已取消' : detail.value?.pickups.some(row => row.status === 'ASSIGNED') ? props.pickupApproved ? '装货中' : ({ DRAFT: '待提交审核', DISPATCH_PENDING: '待调度审核', LOADING_PENDING: '待理货审核', REJECTED: '审核退回' }[props.pickupReviewState || 'DRAFT'] || '待审核') : '派送中')
@@ -76,7 +77,8 @@ async function command(action: 'depart' | 'complete' | 'arrive' | 'cancel', row?
pending.value = { suffix, kind: action === 'cancel' ? 'cancel' : undefined, body: { recordVersion: trip.value.recordVersion, idempotencyKey: crypto.randomUUID(), reason: reason.value.trim() || message, ...(action === 'complete' && prefix === 'truck' ? { evidenceFileIds: truckEvidence.value.map(file => file.fileId) } : {}) } }
await execute()
}
function editPickup(row: PickupTask, arrival = false) { selected.value = row; arrivalMode.value = arrival; actual.value = Number(row.actualPackages ?? row.packageCount); evidence.value = []; pickupOpen.value = true }
/** 实际取货数量由现场填写,预报箱数不能自动作为实提事实。 */
function editPickup(row: PickupTask, arrival = false) { selected.value = row; arrivalMode.value = arrival; actual.value = undefined; evidence.value = []; pickupOpen.value = true }
async function download(file: PickupEvidence, row?: PickupTask) {
try { const blob = await downloadTransportEvidence(props.dispatchNo, file.fileId, row); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = file.fileName; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000) }
catch (cause) { error.value = getApiErrorDetails(cause, '凭证下载失败') }
@@ -91,7 +93,7 @@ async function upload(event: Event, pickup: boolean) {
finally { busy.value = false; input.value = '' }
}
async function picked() {
if (!selected.value || !trip.value || !Number.isInteger(actual.value) || actual.value <= 0 || !evidence.value.length || locked.value) return
if (!selected.value || !trip.value || (!arrivalMode.value && !validPickupQuantity.value) || !evidence.value.length || locked.value) return
const row = selected.value
const common = { recordVersion: trip.value.recordVersion, idempotencyKey: crypto.randomUUID(), evidenceFileIds: evidence.value.map(file => file.fileId) }
pending.value = { suffix: `pickup/${encodeURIComponent(row.customerId)}/${encodeURIComponent(row.transferNo)}/${arrivalMode.value ? 'arrive' : 'picked'}`, body: arrivalMode.value ? { ...common, reason: reason.value.trim() || '上传到仓照片,确认取货派送完成' } : { ...common, actualPackages: actual.value, remark: reason.value.trim() } }; await execute()
@@ -138,10 +140,13 @@ onBeforeRouteLeave(() => !locked.value || window.confirm('运输请求尚待核
<section v-if="detail.taskType === 'PICKUP'" class="transport-pickups" aria-label="取货明细">
<div class="transport-section-heading"><h2 class="workbench-section-title"><Package :size="16" aria-hidden="true" />取货明细</h2><span class="workbench-meta">{{ detail.pickups.length }} 单 · 预报 {{ detail.pickups.reduce((total, row) => total + row.packageCount, 0) }} 箱</span></div>
<el-table :data="detail.pickups" size="small" class="density-table" empty-text="暂无取货明细">
<el-table-column v-if="!readonly" label="操作" width="160"><template #default="{ row }"><template v-if="editable && props.pickupApproved && ['PLANNED', 'IN_TRANSIT'].includes(trip.status) && ['ASSIGNED', 'PICKED'].includes(row.status)"><el-button v-if="row.status === 'ASSIGNED'" link type="primary" :disabled="locked" @click="editPickup(row as PickupTask)">上传取货照片</el-button><el-button v-if="row.status === 'PICKED' && trip.status === 'IN_TRANSIT'" link type="primary" :disabled="locked" @click="editPickup(row as PickupTask, true)">上传到仓照片</el-button></template><span v-else class="workbench-meta">—</span></template></el-table-column>
<el-table-column v-if="!readonly" label="操作" width="160"><template #default="{ row }"><template v-if="editable && props.pickupApproved && ['PLANNED', 'IN_TRANSIT'].includes(trip.status) && ['ASSIGNED', 'PICKED'].includes(row.status)"><el-button v-if="row.status === 'ASSIGNED'" link type="primary" :disabled="locked" @click="editPickup(row as PickupTask)">上传装货照片</el-button><el-button v-if="row.status === 'PICKED' && trip.status === 'IN_TRANSIT'" link type="primary" :disabled="locked" @click="editPickup(row as PickupTask, true)">上传到仓照片</el-button></template><span v-else class="workbench-meta">—</span></template></el-table-column>
<el-table-column prop="transferNo" label="转运单号" width="210" />
<el-table-column label="取货地址" min-width="280"><template #default="{ row }">{{ row.pickupAddress || '—' }}</template></el-table-column>
<el-table-column label="联系人" min-width="120"><template #default="{ row }">{{ row.contactName || '—' }}</template></el-table-column>
<el-table-column label="联系电话" min-width="160"><template #default="{ row }">{{ row.contactPhone || '—' }}</template></el-table-column>
<el-table-column label="状态" width="120"><template #default="{ row }"><el-tag :type="trip.status === 'CANCELLED' ? 'info' : row.status === 'ARRIVED' ? 'success' : 'primary'">{{ trip.status === 'CANCELLED' ? '已取消' : pickupStatusLabels[row.status] }}</el-tag></template></el-table-column>
<el-table-column label="预报/实提(箱)" width="145" align="right"><template #default="{ row }">{{ row.packageCount }} / {{ row.actualPackages ?? '—' }}</template></el-table-column>
<el-table-column label="预报/实际取货(箱)" width="180" align="right"><template #default="{ row }">{{ row.packageCount }} / {{ row.actualPackages ?? '—' }}</template></el-table-column>
<el-table-column label="取货照片" min-width="110"><template #default="{ row }"><el-button v-for="(file, index) in pickupPhotos(row as PickupTask, 'PICKUP')" :key="file.fileId" link @click="download(file, row as PickupTask)">照片{{ index + 1 }}</el-button><span v-if="!pickupPhotos(row as PickupTask, 'PICKUP').length">—</span></template></el-table-column>
<el-table-column label="到仓照片" min-width="110"><template #default="{ row }"><el-button v-for="(file, index) in pickupPhotos(row as PickupTask, 'ARRIVAL')" :key="file.fileId" link @click="download(file, row as PickupTask)">照片{{ index + 1 }}</el-button><span v-if="!pickupPhotos(row as PickupTask, 'ARRIVAL').length">—</span></template></el-table-column>
</el-table>
@@ -149,8 +154,18 @@ onBeforeRouteLeave(() => !locked.value || window.confirm('运输请求尚待核
<div v-if="detail.taskType === 'LOCAL_TRUCK' && trip.status === 'IN_TRANSIT' && editable"><label>完成凭证 <input type="file" :disabled="locked" @change="upload($event, false)" /></label><p v-for="file in truckEvidence" :key="file.fileId">{{ file.fileName }}</p></div>
<el-button v-for="file in detail.completionEvidence || []" :key="file.fileId" link @click="download(file)">{{ file.fileName }}</el-button>
</template>
<AppDialog class="admin-density-dialog" v-model="pickupOpen" :title="arrivalMode ? '上传到仓照片' : '上传取货照片'" width="560px" :show-close="!locked" :close-on-click-modal="!locked" :close-on-press-escape="!locked">
<p>{{ selected?.transferNo }}</p><el-form label-position="left" label-width="85px" :disabled="locked"><el-form-item v-if="!arrivalMode" label="实提箱数" required><el-input-number v-model="actual" :min="1" :precision="0" /></el-form-item><el-form-item label="照片" required><input type="file" accept="image/*" :disabled="locked" @change="upload($event, true)" /></el-form-item><p v-for="file in evidence" :key="file.fileId">{{ file.fileName }}</p></el-form><template #footer><el-button type="primary" :disabled="locked || !evidence.length" @click="picked">{{ arrivalMode ? '确认到仓' : '确认取货' }}</el-button></template>
<AppDialog class="admin-density-dialog" v-model="pickupOpen" :title="arrivalMode ? '上传到仓照片' : '上传装货照片'" width="560px" :show-close="!locked" :close-on-click-modal="!locked" :close-on-press-escape="!locked">
<p>{{ selected?.transferNo }}</p>
<el-form label-position="left" label-width="150px" :disabled="locked">
<el-form-item label="预报数量(箱)">{{ selected?.packageCount ?? '—' }}</el-form-item>
<el-form-item label="实际取货数量(箱)" :required="!arrivalMode">
<span v-if="arrivalMode">{{ selected?.actualPackages ?? '—' }}</span>
<el-input-number v-else v-model="actual" :min="1" :max="5000" :precision="0" placeholder="填写实际取货箱数" aria-label="实际取货数量(箱)" />
</el-form-item>
<el-form-item label="照片" required><input type="file" accept="image/*" :disabled="locked" @change="upload($event, true)" /></el-form-item>
<p v-for="file in evidence" :key="file.fileId">{{ file.fileName }}</p>
</el-form>
<template #footer><el-button type="primary" :disabled="locked || !evidence.length || (!arrivalMode && !validPickupQuantity)" @click="picked">{{ arrivalMode ? '确认到仓' : '确认取货' }}</el-button></template>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="releaseOpen" :title="releaseKind === 'VEHICLE' ? '结束车辆任务' : '结束司机任务'" width="560px" :show-close="!locked" :close-on-click-modal="!locked"><el-form label-position="left" label-width="100px" :disabled="locked"><el-form-item label="实际结束时间" required><el-date-picker v-model="actualEnd" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" /></el-form-item><el-form-item label="结束地点" required><el-input v-model="endLocation" /></el-form-item></el-form><template #footer><el-button type="primary" :disabled="locked || !actualEnd || !endLocation.trim()" @click="release">确认结束</el-button></template></AppDialog>
</section>