This commit is contained in:
2026-10-29 13:37:22 +08:00
parent ac2ecb4663
commit 8970ad52fa
15 changed files with 79 additions and 41 deletions
@@ -133,7 +133,7 @@ async function candidates() {
try {
const [v, d] = await Promise.all([listVehicles({ page: 1, size: 100, region: 'OVERSEAS', status: 'IN_SERVICE' }), listDrivers({ page: 1, size: 100, region: 'OVERSEAS', active: true, reviewStatus: 'APPROVED', expectedEnd: trip.value.expectedDriverEndAt })])
const [vo, dr] = await Promise.all([transportOccupancy(props.dispatchNo, 'VEHICLE', v.records.map(row => row.id)), transportOccupancy(props.dispatchNo, 'DRIVER', d.records.map(row => row.id))])
occupancy.value = Object.fromEntries([...vo.map(row => [`V:${row.resourceId}`, row.available]), ...dr.map(row => [`D:${row.resourceId}`, row.available])]); vehicles.value = v.records; drivers.value = d.records
occupancy.value = Object.fromEntries([...vo.map(row => [`V:${row.resourceId}`, row.available]), ...dr.map(row => [`D:${row.resourceId}`, row.available])]); vehicles.value = v.records.filter(row => occupancy.value[`V:${row.id}`] === true); drivers.value = d.records.filter(row => occupancy.value[`D:${row.id}`] === true && row.qualification.canDispatch)
} catch (cause) { error.value = getApiErrorDetails(cause, '资源候选加载失败') }
finally { candidateBusy.value = false }
}
@@ -206,6 +206,10 @@ onBeforeUnmount(clearTruckPreviewUrls)
<el-button link :disabled="locked" aria-label="刷新运输任务" @click="load"><RefreshCw :size="14" />刷新</el-button>
</div>
</header>
<dl v-if="detail.cargo" class="transport-summary-cards" aria-label="货物数量统计">
<div><dt>托数</dt><dd>{{ detail.cargo.palletCount }} 托</dd></div><div><dt>箱数</dt><dd>{{ detail.cargo.packageCount }} 箱</dd></div>
<div><dt>总重量</dt><dd>{{ detail.cargo.totalWeightKg ?? '—' }} kg</dd></div><div><dt>总体积</dt><dd>{{ detail.cargo.totalVolumeCbm ?? '—' }} m³</dd></div>
</dl>
<section class="workbench-panel is-compact transport-resources" aria-label="车辆信息">
<div class="transport-section-heading"><h2 class="workbench-section-title">车辆信息</h2><el-button v-if="canChangeResources && editable && !resourceFormOpen" link type="primary" :disabled="locked" @click="editingResources = true">维护车辆信息</el-button></div>
<dl class="transport-facts">
@@ -213,12 +217,23 @@ onBeforeUnmount(clearTruckPreviewUrls)
<div><dt>计划发车</dt><dd>{{ time(trip.plannedDepartureAt) }}</dd></div>
<div><dt>预计结束</dt><dd>{{ time(trip.expectedDriverEndAt) }}</dd></div>
<template v-if="!resourceFormOpen"><div><dt>车辆</dt><dd>{{ trip.vehiclePlate || '待分配' }}</dd></div><div><dt>司机</dt><dd>{{ trip.driverName || '待分配' }}<span v-if="trip.driverPhone" class="transport-phone">{{ trip.driverPhone }}</span></dd></div></template>
<div><dt>实际发车</dt><dd>{{ time(trip.departedAt) }}</dd></div>
<div><dt>实际完成</dt><dd>{{ time(trip.completedAt) }}</dd></div>
<div><dt>最近更新</dt><dd>{{ time(trip.updatedAt) }}</dd></div>
<div><dt>车辆容量</dt><dd>{{ trip.maxPalletSpaces ?? '—' }} 托 / {{ trip.maxLoadKg ?? '—' }} kg / {{ trip.maxVolumeCbm ?? '—' }} m³</dd></div>
<div><dt>驾驶证号</dt><dd>{{ detail.resources?.driver?.evidence?.licenseNo || '—' }}</dd></div>
<div><dt>驾驶证有效期</dt><dd>{{ detail.resources?.driver?.evidence?.licenseExpiresOn || '—' }}</dd></div>
<div v-if="detail.orderNo"><dt>订单号</dt><dd>{{ detail.orderNo }}</dd></div>
<div v-if="detail.taskType === 'LOCAL_TRUCK'"><dt>路线</dt><dd>{{ trip.stops.map(stop => stop.name).join(' → ') }}</dd></div>
</dl>
<div v-if="readonly || ['COMPLETED', 'CANCELLED'].includes(trip.status)" class="transport-review-photos" aria-label="车辆司机资料">
<DispatchEvidenceFiles photo-card :model-value="detail.resources?.vehicle?.evidenceFileIds || []" :dispatch-no="dispatchNo" kind="VEHICLE" label="车辆照片" readonly />
<DispatchEvidenceFiles photo-card :model-value="detail.resources?.driver?.evidence?.photoFileIds || []" :dispatch-no="dispatchNo" kind="DRIVER_PHOTO" label="司机照片" readonly />
<DispatchEvidenceFiles photo-card :model-value="detail.resources?.driver?.evidence?.fileIds || []" :dispatch-no="dispatchNo" kind="DRIVER" label="驾驶证照片" :master-license="detail.resources?.driver?.driverId && detail.resources.driver.evidence?.masterLicenseFileId ? { driverId: detail.resources.driver.driverId, fileId: detail.resources.driver.evidence.masterLicenseFileId } : null" readonly />
</div>
<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: boolean) => 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: boolean) => 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>
<el-form-item label="车辆"><el-select v-model="vehicleId" filterable placeholder="选择车辆" @visible-change="(value: boolean) => 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" :label="`${row.plateNumber} · ${row.maxPalletSpaces}托位`" /></el-select></el-form-item>
<el-form-item label="司机"><el-select v-model="driverId" filterable placeholder="选择司机" @visible-change="(value: boolean) => 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" :label="`${row.details.name} · ${row.details.phone}`" /></el-select></el-form-item>
<section v-if="['PICKUP', 'LOCAL_TRUCK'].includes(detail.taskType)" class="transport-review-photos" aria-label="调度审核照片">
<DispatchEvidenceFiles photo-card v-model="vehiclePhotos" :dispatch-no="dispatchNo" kind="VEHICLE" label="车辆图片" :master-resource="vehicleId ? { kind: 'VEHICLE', id: vehicleId } : null" @available="masterPhotoAvailable.vehicle = $event" :disabled="locked" @busy="photoBusy.vehicle = $event" />
<DispatchEvidenceFiles photo-card v-model="driverPhotos" :dispatch-no="dispatchNo" kind="DRIVER_PHOTO" label="司机照片" :master-resource="driverId ? { kind: 'DRIVER', id: driverId } : null" @available="masterPhotoAvailable.driver = $event" :disabled="locked" @busy="photoBusy.driver = $event" />
@@ -228,6 +243,12 @@ onBeforeUnmount(clearTruckPreviewUrls)
<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.cargo" class="workbench-panel is-compact" aria-label="货物与取送信息">
<h2 class="workbench-section-title">货物与取送信息</h2><dl class="transport-facts">
<div><dt>取货地</dt><dd>{{ detail.cargo.origin }}</dd></div><div><dt>取货地址</dt><dd>{{ detail.cargo.originAddress || '—' }}</dd></div><div><dt>取货联系人</dt><dd>{{ detail.cargo.originContactName }} {{ detail.cargo.originContactPhone }}</dd></div>
<div><dt>送达地</dt><dd>{{ detail.cargo.destination }}</dd></div><div><dt>送达地址</dt><dd>{{ detail.cargo.destinationAddress || '—' }}</dd></div><div><dt>送达联系人</dt><dd>{{ detail.cargo.destinationContactName }} {{ detail.cargo.destinationContactPhone }}</dd></div>
</dl>
</section>
<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>
<div v-if="!detail.pickups.length" class="workbench-meta">暂无取货明细</div>
@@ -276,6 +297,11 @@ onBeforeUnmount(clearTruckPreviewUrls)
</template>
<style scoped>
.transport-summary-cards { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 12px 0; }
.transport-summary-cards > div { padding: 16px; border: 1px solid var(--cc-border, #e3eaf3); border-radius: 8px; background: #f8fbff; }
.transport-summary-cards dt { color: var(--cc-text-muted, #788699); font-size: 12px; }
.transport-summary-cards dd { margin: 8px 0 0; color: #243951; font-size: 18px; font-weight: 600; }
.transport-execution { display: grid; gap: var(--admin-space-5); min-width: 0; padding: var(--admin-space-5); }
.transport-toolbar, .transport-section-heading, .transport-identity { display: flex; align-items: center; gap: var(--admin-space-4); }
.transport-toolbar, .transport-section-heading { justify-content: space-between; flex-wrap: wrap; }