83 lines
8.4 KiB
Vue
83 lines
8.4 KiB
Vue
<script setup lang="ts">
|
||||
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|||
|
|
import { ElMessage } from 'element-plus'
|
|||
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
|||
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|||
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|||
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|||
|
|
import { getTripLoading, confirmTripLoading, getTripCapacityPlan, confirmTripCapacityPlan } from '../api/trips'
|
|||
|
|
import type { FleetTrip, TripLoading, TripCapacityPlanning, TripMeasurement } from '../tripTypes'
|
|||
|
|
|
|||
|
|
const props = defineProps<{ trip: FleetTrip; editable: boolean; planning?: boolean }>()
|
|||
|
|
const emit = defineEmits<{ saved: [loading: TripLoading]; planned: [plan: TripCapacityPlanning] }>()
|
|||
|
|
type PalletRow = { warehousePalletId: string; palletNo: string; destinations: string; palletSpaces: number; weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null }
|
|||
|
|
const info = ref<TripLoading | TripCapacityPlanning | null>(null), rows = ref<PalletRow[]>([]), busy = ref(false), loading = ref(false), error = ref<ApiErrorDetails | null>(null)
|
|||
|
|
let sequence = 0
|
|||
|
|
const columns = computed<DataTableColumn[]>(() => [{ key: 'palletNo', prop: 'palletNo', label: '实物托盘', minWidth: 150 },
|
|||
|
|
{ key: 'destinations', prop: 'destinations', label: '货物目的地', minWidth: 160 }, { key: 'spaces', label: '占用标准托位', slotName: 'spaces', width: 175 },
|
|||
|
|
{ key: 'weight', label: props.planning ? '预计重量(kg)' : '实测重量(kg)', slotName: 'weight', width: 190 }, { key: 'volume', label: props.planning ? '预计体积(m³)' : '实测体积(m³)', slotName: 'volume', width: 190 },
|
|||
|
|
{ key: 'stop', label: '卸货站', slotName: 'stop', minWidth: 200 }])
|
|||
|
|
const savedLoad = computed(() => info.value && ('capacityPlan' in info.value ? info.value.capacityPlan : info.value.confirmedLoad))
|
|||
|
|
const segments = computed(() => info.value && 'availableSegments' in info.value ? info.value.availableSegments : savedLoad.value?.segments || [])
|
|||
|
|
const stalePlan = computed(() => info.value && 'planValid' in info.value && !!info.value.capacityPlan && !info.value.planValid)
|
|||
|
|
function apply(value: TripLoading | TripCapacityPlanning) {
|
|||
|
|
info.value = value
|
|||
|
|
const pallets = new Map<string, PalletRow>()
|
|||
|
|
for (const cargo of value.cargo) {
|
|||
|
|
if (!cargo.warehousePalletId) continue
|
|||
|
|
const existing = pallets.get(cargo.warehousePalletId)
|
|||
|
|
if (existing) { if (cargo.destinationLabel && !existing.destinations.split(' / ').includes(cargo.destinationLabel)) existing.destinations += ` / ${cargo.destinationLabel}`; continue }
|
|||
|
|
const saved = ('capacityPlan' in value ? value.capacityPlan : value.confirmedLoad)?.pallets.find(row => row.warehousePalletId === cargo.warehousePalletId)
|
|||
|
|
const index = props.trip.stops.findIndex(stop => !!cargo.destinationKey && stop.destinationKey === cargo.destinationKey)
|
|||
|
|
pallets.set(cargo.warehousePalletId, { warehousePalletId: cargo.warehousePalletId, palletNo: cargo.palletNo || '—', destinations: cargo.destinationLabel || '—',
|
|||
|
|
palletSpaces: saved?.palletSpaces ?? 1, weightKg: saved?.weightKg ?? null, volumeCbm: saved?.volumeCbm ?? null, deliveryIndex: saved?.deliveryIndex ?? (index > 0 ? index : null) })
|
|||
|
|
}
|
|||
|
|
rows.value = [...pallets.values()]
|
|||
|
|
}
|
|||
|
|
async function load() {
|
|||
|
|
const current = ++sequence; loading.value = true; error.value = null
|
|||
|
|
try { const result = await (props.planning ? getTripCapacityPlan(props.trip.tripNo) : getTripLoading(props.trip.tripNo)); if (current === sequence) apply(result) }
|
|||
|
|
catch (cause) { if (current === sequence) error.value = getApiErrorDetails(cause, props.planning ? '计划容量加载失败' : '装载信息加载失败') }
|
|||
|
|
finally { if (current === sequence) loading.value = false }
|
|||
|
|
}
|
|||
|
|
async function save() {
|
|||
|
|
if (busy.value || !info.value) return
|
|||
|
|
if (!rows.value.length || rows.value.some(row => row.weightKg == null || row.volumeCbm == null || row.deliveryIndex == null)) { ElMessage.warning(props.planning ? '请填写每托预计重量、体积和卸货站' : '请填写每托实测重量、体积和卸货站'); return }
|
|||
|
|
const current = sequence, snapshot = info.value
|
|||
|
|
const pallets: TripMeasurement[] = rows.value.map(row => ({ warehousePalletId: row.warehousePalletId, palletSpaces: row.palletSpaces,
|
|||
|
|
weightKg: row.weightKg!, volumeCbm: row.volumeCbm!, deliveryIndex: row.deliveryIndex! }))
|
|||
|
|
busy.value = true; error.value = null
|
|||
|
|
try {
|
|||
|
|
const body = { recordVersion: snapshot.recordVersion, manifestHash: snapshot.manifestHash, pallets, reason: props.planning ? '核对本车自有货计划载荷' : '核对实物托盘及实际装载' }
|
|||
|
|
if (props.planning) {
|
|||
|
|
const result = await confirmTripCapacityPlan(props.trip.tripNo, body)
|
|||
|
|
if (current === sequence) { apply(result); emit('planned', result); ElMessage.success('计划容量已核对') }
|
|||
|
|
} else {
|
|||
|
|
const result = await confirmTripLoading(props.trip.tripNo, body)
|
|||
|
|
if (current === sequence) { apply(result); emit('saved', result); ElMessage.success('实际装载已确认') }
|
|||
|
|
}
|
|||
|
|
} catch (cause) { if (current === sequence) error.value = getApiErrorDetails(cause, props.planning ? '计划容量确认失败' : '装载确认失败') }
|
|||
|
|
finally { busy.value = false }
|
|||
|
|
}
|
|||
|
|
watch(() => [props.trip.tripNo, props.trip.recordVersion, props.planning], () => { if (info.value?.tripNo !== props.trip.tripNo || info.value.recordVersion !== props.trip.recordVersion || !!props.planning !== ('capacityPlan' in info.value)) void load() }, { immediate: true })
|
|||
|
|
onBeforeUnmount(() => { sequence++ })
|
|||
|
|
</script>
|
|||
|
|
<template>
|
|||
|
|
<div v-loading="loading">
|
|||
|
|
<LoadErrorState v-if="error" :title="planning ? '计划容量操作未完成' : '装载操作未完成'" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
|||
|
|
<p v-if="planning">先核对本车自有货的预计载荷,查看每段剩余托位、载重和容积。实际装载完成后仍需按实测值确认。</p>
|
|||
|
|
<p v-if="stalePlan" role="status">货物明细已变化,请重新核对计划容量。</p>
|
|||
|
|
<p v-if="info?.blockReason" role="status">{{ info.blockReason }}</p>
|
|||
|
|
<p v-else-if="!planning && savedLoad">已确认实际装载;车辆、司机或计划调整后需要重新确认。</p>
|
|||
|
|
<p v-else-if="!planning">按实物托盘核对,同一托盘中的多客户货物只计一次。发车仍由海外 PDA 完成。</p>
|
|||
|
|
<DataTable :data="rows" :columns="columns" row-key="warehousePalletId" empty-text="关联排车单后显示实物托盘">
|
|||
|
|
<template #spaces="{ row }"><el-input-number v-model="row.palletSpaces" :min="1" :precision="0" :disabled="!editable || busy" placeholder="占用托位" /></template>
|
|||
|
|
<template #weight="{ row }"><el-input-number v-model="row.weightKg" :min="0.001" :precision="3" :disabled="!editable || busy" :placeholder="planning ? '预计重量' : '实际重量'" /></template>
|
|||
|
|
<template #volume="{ row }"><el-input-number v-model="row.volumeCbm" :min="0.001" :precision="3" :disabled="!editable || busy" :placeholder="planning ? '预计体积' : '实际体积'" /></template>
|
|||
|
|
<template #stop="{ row }"><el-select v-model="row.deliveryIndex" :disabled="!editable || busy" placeholder="选择卸货站"><el-option v-for="(stop, index) in trip.stops.slice(1)" :key="index" :value="index + 1" :label="stop.name" /></el-select></template>
|
|||
|
|
<template #toolbar-actions><el-button @click="load">{{ planning ? '重新读取容量计划' : '重新读取装载' }}</el-button><el-button v-if="editable" type="primary" :loading="busy" :disabled="!!info?.blockReason || !rows.length" @click="save">{{ planning ? '确认计划容量' : '确认实际装载' }}</el-button></template>
|
|||
|
|
</DataTable>
|
|||
|
|
<table v-if="savedLoad && !stalePlan" class="density-table"><thead><tr><th>行驶区间</th><th>占用托位</th><th>重量(kg)</th><th>体积(m³)</th><th v-if="planning">剩余托位 / kg / m³</th></tr></thead><tbody><tr v-for="segment in segments" :key="segment.fromIndex"><td>{{ trip.stops[segment.fromIndex]?.name }} → {{ trip.stops[segment.toIndex]?.name }}</td><td>{{ segment.occupied.palletSpaces }} / {{ trip.maxPalletSpaces }}</td><td>{{ segment.occupied.weightKg }} / {{ trip.maxLoadKg }}</td><td>{{ segment.occupied.volumeCbm }} / {{ trip.maxVolumeCbm }}</td><td v-if="planning">{{ segment.remaining.palletSpaces }} / {{ segment.remaining.weightKg }} / {{ segment.remaining.volumeCbm }}</td></tr></tbody></table>
|
|||
|
|
</div>
|
|||
|
|
</template>
|