feat(fleet): integrate trip management and fee coverage workflows

This commit is contained in:
2026-10-03 04:35:55 +08:00
parent bca08f8749
commit dc35288a1b
87 changed files with 3774 additions and 233 deletions
@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { CoveredService, FeeCoveragePreview, FeeCoverageSelection, OriginalFeeSource } from '../feeCoverageTypes'
import { coveredServiceLabels } from '../feeCoverageTypes'
const props = defineProps<{ preview: FeeCoveragePreview | null; disabled?: boolean; loading?: boolean }>()
const selection = defineModel<FeeCoverageSelection[]>({ default: () => [] })
function selected(id: string) { return selection.value.find(row => row.financeFeeId === id)?.services || [] }
function inboundCode(source: OriginalFeeSource) { return props.preview?.cargo.find(row => row.sourceInboundId === source.sourceInboundId)?.inboundCode || source.inboundCode || '—' }
function change(id: string, services: CoveredService[]) {
const source = props.preview?.feeSources.find(row => row.financeFeeId === id)
if (!source) return
selection.value = [...selection.value.filter(row => row.financeFeeId !== id), ...(services.length ? [{ financeFeeId: id, sourceVersion: source.sourceVersion, sourceHash: source.sourceHash, services: [...services].sort() }] : [])].sort((a, b) => a.financeFeeId.localeCompare(b.financeFeeId))
}
</script>
<template>
<section aria-label="原运费服务覆盖" class="coverage-panel">
<h3>原运费服务覆盖</h3>
<p>仅勾选已与客户明确约定包含的服务。原费用金额供核对,不是本次新增金额;同托多票须分别确认。</p>
<p v-if="loading">正在读取原费用…</p>
<p v-else-if="!preview">原费用尚未读取,请重新打开后核对。</p>
<p v-else-if="!preview.sourceComplete">{{ preview.blockReason }};当前不能引用原费用。</p>
<p v-else-if="!preview?.feeSources.length">暂无可引用的已确认原费用。</p>
<div v-for="source in preview?.feeSources || []" :key="source.financeFeeId" class="coverage-row">
<span>{{ inboundCode(source) }} · {{ source.feeItemName || '—' }} · {{ source.currency }} {{ source.amount }} · 版本 {{ source.sourceVersion }}</span>
<el-checkbox-group :model-value="selected(source.financeFeeId)" :disabled="disabled || !preview?.sourceComplete" @update:model-value="change(source.financeFeeId, $event as CoveredService[])">
<el-checkbox v-for="(label, code) in coveredServiceLabels" :key="code" :value="code">已含{{ label }}</el-checkbox>
</el-checkbox-group>
</div>
</section>
</template>
<style scoped>
.coverage-panel { margin-bottom: 12px; }
.coverage-panel p { margin: 6px 0; color: var(--el-text-color-secondary); font-size: 12px; }
.coverage-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 6px 0; border-bottom: 1px solid var(--el-border-color-lighter); font-size: 12px; }
.coverage-row > span { min-width: 0; overflow-wrap: anywhere; }
</style>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { physicalWeightLb, totalPhysicalWeightLb } from '../dispatchPhysicalWeight'
import '@/styles/adminDensity.css'
import { computed, reactive, ref, watch } from 'vue'
import { GripVertical, Plus } from '@lucide/vue'
@@ -87,15 +88,14 @@ const assigned = computed(() => assignedIds.value.map((id) => palletSnapshot(id)
const assignedPackages = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
const assignedVolume = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
const selectedVehicleSpec = computed(() => dispatchVehicleSpecs.find((item) => item.value === selectedVehicle.value))
function weightKg(item?: DispatchPalletCandidateDto) {
const grossWeightKg = Number(item?.grossWeightKg)
if (Number.isFinite(grossWeightKg) && grossWeightKg > 0) return grossWeightKg
const volumeWeightKg = Number(item?.volumeWeightKg)
return Number.isFinite(volumeWeightKg) && volumeWeightKg > 0 ? volumeWeightKg : 0
const weightLb = (item?: DispatchPalletCandidateDto) => physicalWeightLb(item?.grossWeightKg)
const orderWeightLb = (pallets: DispatchPalletCandidateDto[]) => totalPhysicalWeightLb(pallets)
const containerWeightLb = (container: LoadingContainer) => totalPhysicalWeightLb(container.palletIds.map(id => palletSnapshot(id)))
const formatWeightLb = (weight: number | null) => weight === null ? '待确认' : `${number(weight, 0)}LB`
const containerOverweight = (container: LoadingContainer) => {
const weight = containerWeightLb(container)
return weight !== null && weight > container.maxWeightLb * 1.1
}
const weightLb = (item?: DispatchPalletCandidateDto) => weightKg(item) * 2.2046226218
const orderWeightLb = (pallets: DispatchPalletCandidateDto[]) => pallets.reduce((sum, item) => sum + weightLb(item), 0)
const containerWeightLb = (container: LoadingContainer) => container.palletIds.reduce((sum, id) => sum + weightLb(palletSnapshot(id)), 0)
function palletId(item: DispatchPalletCandidateDto) {
return String(item.planPalletId || item.warehousePalletId || '')
@@ -234,9 +234,13 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string)
ElMessage.warning('该装车托盘已达到设置的托数')
return
}
const nextWeight = containerWeightLb(target) + weightLb(item)
if (nextWeight > target.maxWeightLb * 1.1) ElMessage.error(`${target.vehicleLabel}重量将达到 ${nextWeight.toFixed(0)}LB,已超过限重10%,请注意调整`)
else if (nextWeight > target.maxWeightLb) ElMessage.warning(`${target.vehicleLabel}当前重量 ${nextWeight.toFixed(0)}LB,已超过额定限重 ${target.maxWeightLb}LB`)
const currentWeight = containerWeightLb(target), incomingWeight = weightLb(item)
if (currentWeight === null || incomingWeight === null) ElMessage.warning('存在未确认的实际毛重,请在正式配载前补齐;材积重不能用于载重校验')
else {
const nextWeight = currentWeight + incomingWeight
if (nextWeight > target.maxWeightLb * 1.1) ElMessage.error(`${target.vehicleLabel}重量将达到 ${nextWeight.toFixed(0)}LB,已超过限重10%,请注意调整`)
else if (nextWeight > target.maxWeightLb) ElMessage.warning(`${target.vehicleLabel}当前重量 ${nextWeight.toFixed(0)}LB,已超过额定限重 ${target.maxWeightLb}LB`)
}
const anchor = assigned.value[0]
if (anchor?.customerId && anchor.customerId !== item.customerId) {
ElMessage.warning('一次排车只能装同一客户的托盘')
@@ -434,7 +438,7 @@ watch([warehouseTab, () => query.orderType], () => {
><template #default="{ row }">{{ orderType(row.first.orderType) }}</template></el-table-column
>
<el-table-column label="重量(lb)" width="100" align="right"
><template #default="{ row }">{{ number(orderWeightLb(row.pallets), 0) }}LB</template></el-table-column
><template #default="{ row }">{{ formatWeightLb(orderWeightLb(row.pallets)) }}</template></el-table-column
>
<el-table-column label="托盘数量" width="90" align="right"
><template #default="{ row }">{{ row.pallets.length }}</template></el-table-column
@@ -479,7 +483,7 @@ watch([warehouseTab, () => query.orderType], () => {
<GripVertical :size="16" class="drag-handle" />
<div class="pallet-copy">
<strong>{{ text(item.palletNo) }}</strong
><span>{{ Number(item.packageCount || 0) }}箱 · {{ number(weightLb(item), 0) }}LB</span>
><span>{{ Number(item.packageCount || 0) }}箱 · {{ formatWeightLb(weightLb(item)) }}</span>
<div v-if="relabelCartonMarks(item).length" class="carton-marks">
<div v-for="mark in relabelCartonMarks(item)" :key="`relabel-${mark}`" class="carton-mark-line">
<span>{{ mark }}</span
@@ -518,7 +522,7 @@ watch([warehouseTab, () => query.orderType], () => {
:key="container.id"
class="loading-container"
:class="{
'loading-container--overweight': containerWeightLb(container) > container.maxWeightLb * 1.1,
'loading-container--overweight': containerOverweight(container),
}"
@dragover.prevent
@drop.prevent="dropInto($event, container.id)"
@@ -527,7 +531,7 @@ watch([warehouseTab, () => query.orderType], () => {
<strong>{{ container.vehicleLabel }}</strong
><span
>托数 <b>{{ container.palletIds.length }}</b
>/{{ container.capacity }} · 重量 <b>{{ number(containerWeightLb(container), 0) }}LB</b>/{{ container.maxWeightLb }}LB <em v-if="containerWeightLb(container) > container.maxWeightLb * 1.1">超限10%</em></span
>/{{ container.capacity }} · 重量 <b>{{ formatWeightLb(containerWeightLb(container)) }}</b>/{{ container.maxWeightLb }}LB <em v-if="containerOverweight(container)">超限10%</em></span
><el-button class="density-action" data-tone="danger" :aria-label="`删除装车车型${index + 1}`" @click="deleteContainer(container)">删除</el-button>
</div>
<div class="container-body">