优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { Truck, Package, RefreshCw } from '@lucide/vue'
|
||||
@@ -10,7 +10,8 @@ import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/sh
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { listVehicles, listDrivers, pickupStatusLabels, type VehicleDetail, type DriverDetail, type PickupTask, type PickupEvidence } from '@/modules/overseas-master-data'
|
||||
import { listVehicles, listDrivers, getDriver, pickupStatusLabels, type VehicleDetail, type DriverDetail, type PickupTask, type PickupEvidence } from '@/modules/overseas-master-data'
|
||||
import DispatchEvidenceFiles from './DispatchEvidenceFiles.vue'
|
||||
import { getTransport, assignTransport, transportOccupancy, cancelTransport, releaseTransport, commandTransport, uploadTransport, downloadTransportEvidence, transportLabels, type TransportDetail } from '../api/transportTasks'
|
||||
import { dispatchActionPermissions as permission } from '../dispatchPresentation'
|
||||
import { formatOverseasDateTime as time } from '../lastMilePresentation'
|
||||
@@ -19,6 +20,11 @@ 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 vehiclePhotos = ref<string[]>([]), driverPhotos = ref<string[]>([]), licensePhotos = ref<string[]>([])
|
||||
const masterLicense = ref<{ driverId: string; fileId: string } | null>(null), licenseLoading = ref(false)
|
||||
const photoBusy = reactive({ vehicle: false, driver: false, license: false })
|
||||
const activeMasterLicense = computed(() => licensePhotos.value.length ? null : masterLicense.value)
|
||||
const photosComplete = computed(() => detail.value?.taskType !== 'PICKUP' || !!(vehiclePhotos.value.length && driverPhotos.value.length && (licensePhotos.value.length || activeMasterLicense.value)))
|
||||
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)
|
||||
@@ -31,10 +37,35 @@ const trip = computed(() => detail.value?.execution.trip)
|
||||
const editable = computed(() => !props.readonly && access.can(permission.EDIT))
|
||||
const canChangeResources = computed(() => trip.value?.status === 'PLANNED' && !detail.value?.pickups.some(row => row.status !== 'ASSIGNED'))
|
||||
const resourceFormOpen = computed(() => canChangeResources.value && editable.value && (editingResources.value || !trip.value?.vehicleId || !trip.value?.driverId))
|
||||
const locked = computed(() => busy.value || !!pending.value)
|
||||
const locked = computed(() => busy.value || !!pending.value || licenseLoading.value || Object.values(photoBusy).some(Boolean))
|
||||
/** 本车已保存的照片随资源带出;换车不能继续显示原车照片。 */
|
||||
function restoreVehiclePhotos() {
|
||||
const saved = detail.value?.resources?.vehicle
|
||||
vehiclePhotos.value = saved?.vehicleId === vehicleId.value ? [...saved.evidenceFileIds] : []
|
||||
}
|
||||
watch(vehicleId, restoreVehiclePhotos, { flush: 'sync' })
|
||||
/** 切换司机后重读其主档驾驶证,迟到响应不得覆盖新司机或新单据。 */
|
||||
watch(() => [driverId.value, resourceFormOpen.value, detail.value] as const, async ([id, open], _, onCleanup) => {
|
||||
let current = true
|
||||
onCleanup(() => { current = false })
|
||||
licenseLoading.value = false
|
||||
if (!open || detail.value?.taskType !== 'PICKUP') return
|
||||
const saved = detail.value.resources?.driver
|
||||
const same = saved?.driverId === id ? saved.evidence : null
|
||||
driverPhotos.value = [...(same?.photoFileIds || [])]
|
||||
licensePhotos.value = (same?.fileIds || []).filter(file => file !== same?.masterLicenseFileId)
|
||||
masterLicense.value = same?.masterLicenseFileId ? { driverId: id!, fileId: same.masterLicenseFileId } : null
|
||||
if (!id || licensePhotos.value.length) return
|
||||
licenseLoading.value = true
|
||||
try {
|
||||
const driver = await getDriver(id, trip.value?.expectedDriverEndAt)
|
||||
if (current) masterLicense.value = driver.licenseFileId ? { driverId: id, fileId: driver.licenseFileId } : null
|
||||
} catch (cause) { if (current) error.value = getApiErrorDetails(cause, '驾驶证带出失败,可重新读取或上传照片') }
|
||||
finally { if (current) licenseLoading.value = false }
|
||||
}, { immediate: true })
|
||||
async function load() {
|
||||
error.value = null
|
||||
try { detail.value = await getTransport(props.dispatchNo); vehicleId.value = trip.value?.vehicleId || ''; driverId.value = trip.value?.driverId || '' }
|
||||
try { detail.value = await getTransport(props.dispatchNo); vehicleId.value = trip.value?.vehicleId || ''; driverId.value = trip.value?.driverId || ''; restoreVehiclePhotos() }
|
||||
catch (cause) { error.value = getApiErrorDetails(cause, '运输任务加载失败') }
|
||||
}
|
||||
watch(() => props.dispatchNo, () => { detail.value = null; pending.value = null; void load() }, { immediate: true })
|
||||
@@ -50,10 +81,10 @@ async function candidates() {
|
||||
finally { candidateBusy.value = false }
|
||||
}
|
||||
async function assign() {
|
||||
if (!trip.value || locked.value || !vehicleId.value || !driverId.value) return
|
||||
if (!trip.value || locked.value || !vehicleId.value || !driverId.value || !photosComplete.value) return
|
||||
busy.value = true; error.value = null
|
||||
try { detail.value = await assignTransport(props.dispatchNo, { recordVersion: trip.value.recordVersion, vehicleId: vehicleId.value, driverId: driverId.value, reason: reason.value.trim() || '分配运输车辆及司机' }); editingResources.value = false; emit('saved'); ElMessage.success('车辆司机已分配') }
|
||||
catch (cause) { await load(); error.value = getApiErrorDetails(cause, '分配未完成') }
|
||||
try { detail.value = await assignTransport(props.dispatchNo, { recordVersion: trip.value.recordVersion, vehicleId: vehicleId.value, driverId: driverId.value, reason: reason.value.trim() || '分配运输车辆及司机', ...(detail.value?.taskType === 'PICKUP' ? { dispatchRecordVersion: detail.value.dispatchRecordVersion, reviewPhotos: { vehicleFileIds: [...vehiclePhotos.value], driverPhotoFileIds: [...driverPhotos.value], licenseFileIds: [...licensePhotos.value] }, masterLicenseFileId: activeMasterLicense.value?.fileId || null } : {}) }); editingResources.value = false; emit('saved'); ElMessage.success('车辆司机及审核照片已保存') }
|
||||
catch (cause) { error.value = getApiErrorDetails(cause, '保存未完成,已上传照片保留,请核对后重试') }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
/** 每次业务命令保留原版本与幂等键,结果未知只重试原请求。 */
|
||||
@@ -134,7 +165,13 @@ onBeforeRouteLeave(() => !locked.value || window.confirm('运输请求尚待核
|
||||
<el-form v-if="resourceFormOpen" label-position="left" :disabled="locked" class="density-form transport-resource-form">
|
||||
<el-form-item label="车辆"><el-select v-model="vehicleId" filterable placeholder="选择车辆" @visible-change="value => value && !vehicles.length && candidates()"><el-option v-if="vehicleId && !vehicles.some(row => row.id === vehicleId)" :value="vehicleId" :label="trip.vehiclePlate || '当前车辆'" /><el-option v-for="row in vehicles" :key="row.id" :value="row.id" :disabled="occupancy[`V:${row.id}`] !== true" :label="`${row.plateNumber} · ${row.maxPalletSpaces}托位`" /></el-select></el-form-item>
|
||||
<el-form-item label="司机"><el-select v-model="driverId" filterable placeholder="选择司机" @visible-change="value => value && !drivers.length && candidates()"><el-option v-if="driverId && !drivers.some(row => row.id === driverId)" :value="driverId" :label="`${trip.driverName || '当前司机'} · ${trip.driverPhone || ''}`" /><el-option v-for="row in drivers" :key="row.id" :value="row.id" :disabled="occupancy[`D:${row.id}`] !== true || !row.qualification.canDispatch" :label="`${row.details.name} · ${row.details.phone}`" /></el-select></el-form-item>
|
||||
<div class="density-actions"><el-button type="primary" :loading="busy || candidateBusy" :disabled="locked || !vehicleId || !driverId" @click="assign">保存车辆司机</el-button><el-button v-if="trip.vehicleId && trip.driverId" :disabled="locked" @click="editingResources = false; vehicleId = trip.vehicleId; driverId = trip.driverId">收起</el-button></div>
|
||||
<section v-if="detail.taskType === 'PICKUP'" class="transport-review-photos" aria-label="调度审核照片">
|
||||
<DispatchEvidenceFiles photo-card v-model="vehiclePhotos" :dispatch-no="dispatchNo" kind="VEHICLE" label="车辆图片" :disabled="locked" @busy="photoBusy.vehicle = $event" />
|
||||
<DispatchEvidenceFiles photo-card v-model="driverPhotos" :dispatch-no="dispatchNo" kind="DRIVER_PHOTO" label="司机照片" :disabled="locked" @busy="photoBusy.driver = $event" />
|
||||
<DispatchEvidenceFiles photo-card v-model="licensePhotos" :dispatch-no="dispatchNo" kind="DRIVER" label="驾驶证照片" :master-license="activeMasterLicense" :reference-ids="activeMasterLicense ? [activeMasterLicense.fileId] : []" :disabled="locked" @busy="photoBusy.license = $event" />
|
||||
<p class="transport-photo-hint workbench-meta">请补齐车辆图片、司机照片和驾驶证照片;保存后供调度员和理货员审核。</p>
|
||||
</section>
|
||||
<div class="density-actions transport-resource-save"><el-button type="primary" :loading="busy || candidateBusy" :disabled="locked || !vehicleId || !driverId || !photosComplete" @click="assign">保存车辆司机</el-button><el-button v-if="trip.vehicleId && trip.driverId" :disabled="locked" @click="editingResources = false; vehicleId = trip.vehicleId; driverId = trip.driverId; restoreVehiclePhotos()">收起</el-button></div>
|
||||
</el-form>
|
||||
</section>
|
||||
<section v-if="detail.taskType === 'PICKUP'" class="transport-pickups" aria-label="取货明细">
|
||||
@@ -184,6 +221,8 @@ onBeforeRouteLeave(() => !locked.value || window.confirm('运输请求尚待核
|
||||
.transport-facts dd { margin: 0; color: var(--cc-text); overflow-wrap: anywhere; }
|
||||
.transport-phone { margin-left: var(--admin-space-3); color: var(--cc-muted); }
|
||||
.transport-resource-form { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(240px, 1fr) auto; align-items: start; gap: var(--admin-space-6); margin-top: var(--admin-space-5); }
|
||||
.transport-review-photos { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--admin-space-5); }
|
||||
.transport-photo-hint, .transport-resource-save { grid-column: 1 / -1; }
|
||||
.transport-pickups { display: grid; gap: var(--admin-space-4); min-width: 0; }
|
||||
@media (max-width: 1100px) { .transport-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } .transport-resource-form { grid-template-columns: minmax(200px, 1fr) minmax(200px, 1fr); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user