fix(admin): validate explicit booking container limits

This commit is contained in:
2026-08-12 18:55:18 +08:00
parent 1be13c3bf5
commit 971ac91a23
4 changed files with 35 additions and 9 deletions
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import { containerRequirementError, createBlankContainerRequirement } from './containerRequirements'
describe('ocean booking container requirements', () => {
it('does not guess capacity or weight for a new container type', () => {
expect(createBlankContainerRequirement()).toEqual({
containerType: '40HC', containerKind: 'FCL', capacityCbm: null, maxWeightKg: null, quantity: 1,
})
})
it('rejects missing or non-positive capacity, weight and quantity', () => {
expect(containerRequirementError([createBlankContainerRequirement()])).toContain('容量、限重和数量必须大于 0')
expect(containerRequirementError([{ containerType: 'UNKNOWN', containerKind: 'FCL', capacityCbm: 1, maxWeightKg: 1, quantity: 0 }])).toContain('UNKNOWN')
expect(containerRequirementError([{ containerType: 'UNKNOWN', containerKind: 'FCL', capacityCbm: 1, maxWeightKg: 1, quantity: 1 }])).toBeNull()
})
})
@@ -0,0 +1,10 @@
import type { OceanBookingContainer } from './types'
export function createBlankContainerRequirement(): OceanBookingContainer {
return { containerType: '40HC', containerKind: 'FCL', capacityCbm: null, maxWeightKg: null, quantity: 1 }
}
export function containerRequirementError(requirements: OceanBookingContainer[]) {
const invalid = requirements.find((item) => !(Number(item.capacityCbm) > 0) || !(Number(item.maxWeightKg) > 0) || !(Number(item.quantity) > 0))
return invalid ? `箱型 ${invalid.containerType || '未命名'} 的容量、限重和数量必须大于 0,请补充后再提交` : null
}
@@ -48,10 +48,9 @@ describe('OceanBookingDetailView interaction contract', () => {
expect(source).toContain('v-model="container.capacityCbm"')
expect(source).toContain('v-model="container.maxWeightKg"')
expect(source).toContain('releaseOceanBooking(id.value, releaseRequest)')
expect(source).toContain('capacityCbm: null')
expect(source).toContain('maxWeightKg: null')
expect(source).toContain('createBlankContainerRequirement()')
expect(source).toContain('containerRequirementError(form.data.containerRequirements)')
expect(source).toContain('function validateContainerRequirements()')
expect(source).toContain('容量、限重和数量必须大于 0')
expect(source).not.toContain('capacityCbm: 68')
expect(source).not.toContain('maxWeightKg: 28000')
})
@@ -32,6 +32,7 @@ import type {
OceanBookingStatus,
} from '../types'
import { oceanBookingStatusLabel } from '../status'
import { containerRequirementError, createBlankContainerRequirement } from '../containerRequirements'
const partyTypes = [
{ value: 'SHIPPER', label: '发货人' },
@@ -85,7 +86,7 @@ function blankParties(): OceanBookingParty[] {
function blankData(): OceanBookingDraftData {
return {
customerId: '', customerName: '', channelCode: '', channelName: '', warehouseId: '', warehouseName: '', parties: blankParties(),
containerRequirements: [{ containerType: '40HC', containerKind: 'FCL', capacityCbm: null, maxWeightKg: null, quantity: 1 }],
containerRequirements: [createBlankContainerRequirement()],
}
}
const form = reactive<OceanBookingSaveRequest>({ expectedVersion: 0, data: blankData() })
@@ -151,7 +152,7 @@ function applyDetail(value: OceanBookingDetail) {
estimatedSailingAt: value.estimatedSailingAt, estimatedArrivalAt: value.estimatedArrivalAt,
remark: value.remark,
parties: value.parties.length ? value.parties.map(({ partyType, partyName, contactName, contactPhone, address }) => ({ partyType, partyName, contactName, contactPhone, address })) : blankParties(),
containerRequirements: value.containerRequirements.length ? value.containerRequirements.map(({ containerType, containerKind, capacityCbm, maxWeightKg, quantity }) => ({ containerType, containerKind, capacityCbm: capacityCbm ?? null, maxWeightKg: maxWeightKg ?? null, quantity: quantity ?? null })) : [{ containerType: '40HC', containerKind: 'FCL', capacityCbm: null, maxWeightKg: null, quantity: 1 }],
containerRequirements: value.containerRequirements.length ? value.containerRequirements.map(({ containerType, containerKind, capacityCbm, maxWeightKg, quantity }) => ({ containerType, containerKind, capacityCbm: capacityCbm ?? null, maxWeightKg: maxWeightKg ?? null, quantity: quantity ?? null })) : [createBlankContainerRequirement()],
}
}
async function load() {
@@ -230,14 +231,14 @@ async function runAction(action: OceanBookingAction) {
} finally { actionSaving.value = false }
}
function validateContainerRequirements() {
const invalid = form.data.containerRequirements.find((container) => !(Number(container.capacityCbm) > 0) || !(Number(container.maxWeightKg) > 0) || !(Number(container.quantity) > 0))
if (invalid) {
actionError.value = { message: `箱型 ${invalid.containerType || '未命名'} 的容量、限重和数量必须大于 0,请补充后再提交` }
const message = containerRequirementError(form.data.containerRequirements)
if (message) {
actionError.value = { message }
return false
}
return true
}
function addContainer() { form.data.containerRequirements.push({ containerType: '40HC', containerKind: 'FCL', capacityCbm: null, maxWeightKg: null, quantity: 1 }) }
function addContainer() { form.data.containerRequirements.push(createBlankContainerRequirement()) }
function removeContainer(index: number) { if (form.data.containerRequirements.length > 1) form.data.containerRequirements.splice(index, 1) }
onMounted(() => { void load(); void loadOptions() })