feat(admin): adapt channel booking and smart loading contracts

This commit is contained in:
2026-08-12 18:43:54 +08:00
parent 9ebd0b2295
commit 41a131a33b
14 changed files with 58 additions and 26 deletions
@@ -41,6 +41,15 @@ describe('OceanBookingDetailView interaction contract', () => {
]) expect(source).toContain(`placeholder="${placeholder}"`)
})
it('captures channel, warehouse, capacity snapshots and uses the release DTO', () => {
expect(source).toContain('v-model="form.data.channelCode"')
expect(source).toContain('v-model="form.data.channelName"')
expect(source).toContain('form.data.warehouseName = warehouses.find')
expect(source).toContain('v-model="container.capacityCbm"')
expect(source).toContain('v-model="container.maxWeightKg"')
expect(source).toContain('releaseOceanBooking(id.value, releaseRequest)')
})
it('keeps view, create and edit on the formal detail route with a return action', () => {
expect(source).toContain("router.push('/ocean-bookings')")
expect(source).toContain("router.replace(`/ocean-bookings/${created.id}`)")
@@ -14,10 +14,12 @@ import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import PageToolbar from '@/shared/components/PageToolbar.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data'
import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
import {
createOceanBooking,
getOceanBooking,
performOceanBookingAction,
releaseOceanBooking,
requestOceanBookingChange,
updateOceanBooking,
type OceanBookingAction,
@@ -68,6 +70,7 @@ const actionError = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
const formRef = ref<FormInstance>()
const customers = ref<PlatformTenantResource[]>([])
const partners = ref<PlatformPartnerResource[]>([])
const warehouses = ref<PlatformWarehouseResource[]>([])
const optionsLoading = ref(false)
const optionsError = ref('')
const selectableCustomers = computed(() => customers.value.filter((customer) => (
@@ -81,8 +84,8 @@ function blankParties(): OceanBookingParty[] {
}
function blankData(): OceanBookingDraftData {
return {
customerId: '', customerName: '', parties: blankParties(),
containerRequirements: [{ containerType: '40HC', containerKind: 'FCL', quantity: 1 }],
customerId: '', customerName: '', channelCode: '', channelName: '', warehouseId: '', warehouseName: '', parties: blankParties(),
containerRequirements: [{ containerType: '40HC', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 28000, quantity: 1 }],
}
}
const form = reactive<OceanBookingSaveRequest>({ expectedVersion: 0, data: blankData() })
@@ -106,12 +109,14 @@ async function loadOptions() {
optionsLoading.value = true
optionsError.value = ''
try {
const [customerResult, partnerResult] = await Promise.all([
const [customerResult, partnerResult, warehouseResult] = await Promise.all([
listPlatformTenants({ page: 1, size: 200 }),
listPlatformPartners({ page: 1, size: 200, status: 'ACTIVE' }),
listPlatformWarehouses({ page: 1, size: 200, status: 'ACTIVE' }),
])
customers.value = customerResult.records
partners.value = partnerResult.records
warehouses.value = warehouseResult.records
} catch (cause) {
optionsError.value = getApiErrorDetails(cause, '客户及合作单位选项加载失败').message
} finally {
@@ -136,7 +141,7 @@ function applyDetail(value: OceanBookingDetail) {
detail.value = value
form.expectedVersion = value.version
form.data = {
customerId: value.customerId || '', customerName: value.customerName || '', billOfLadingNo: value.billOfLadingNo,
customerId: value.customerId || '', customerName: value.customerName || '', channelCode: value.channelCode || '', channelName: value.channelName || '', warehouseId: value.warehouseId || '', warehouseName: value.warehouseName || '', billOfLadingNo: value.billOfLadingNo,
shippingCompanyId: value.shippingCompanyId, shippingCompanyName: value.shippingCompanyName,
bookingAgentId: value.bookingAgentId, bookingAgentName: value.bookingAgentName,
vesselName: value.vesselName, voyageNo: value.voyageNo,
@@ -146,7 +151,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, quantity }) => ({ containerType, containerKind, quantity })) : [{ containerType: '40HC', containerKind: 'FCL', quantity: 1 }],
containerRequirements: value.containerRequirements.length ? value.containerRequirements.map(({ containerType, containerKind, capacityCbm, maxWeightKg, quantity }) => ({ containerType, containerKind, capacityCbm, maxWeightKg, quantity })) : [{ containerType: '40HC', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 28000, quantity: 1 }],
}
}
async function load() {
@@ -213,13 +218,16 @@ async function runAction(action: OceanBookingAction) {
const reason = await requiredReason(action)
await ElMessageBox.confirm(`确认执行“${actionLabels[action]}”吗?`, actionLabels[action], { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' })
actionSaving.value = true
applyDetail(await performOceanBookingAction(id.value, action, { expectedVersion: detail.value.version, reason }))
if (action === 'release') {
const releaseRequest = { expectedVersion: detail.value.version, billOfLadingNo: form.data.billOfLadingNo || '', vesselName: form.data.vesselName || '', voyageNo: form.data.voyageNo || '', estimatedSailingAt: form.data.estimatedSailingAt || '', estimatedArrivalAt: form.data.estimatedArrivalAt || '', reason }
applyDetail(await releaseOceanBooking(id.value, releaseRequest))
} else applyDetail(await performOceanBookingAction(id.value, action, { expectedVersion: detail.value.version, reason }))
ElMessage.success(`${actionLabels[action]}成功`)
} catch (cause) {
if (cause !== 'cancel' && !(cause instanceof Error && cause.message === 'cancel')) actionError.value = getApiErrorDetails(cause)
} finally { actionSaving.value = false }
}
function addContainer() { form.data.containerRequirements.push({ containerType: '40HC', containerKind: 'FCL', quantity: 1 }) }
function addContainer() { form.data.containerRequirements.push({ containerType: '40HC', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 28000, quantity: 1 }) }
function removeContainer(index: number) { if (form.data.containerRequirements.length > 1) form.data.containerRequirements.splice(index, 1) }
onMounted(() => { void load(); void loadOptions() })
@@ -255,6 +263,9 @@ onMounted(() => { void load(); void loadOptions() })
<el-form-item label="订舱号"><el-input :model-value="detail?.bookingNo || '保存后生成'" disabled /></el-form-item>
<el-form-item label="客户" prop="customerId"><el-select :model-value="form.data.customerId" filterable placeholder="请选择客户" :loading="optionsLoading" :disabled="!isNew" style="width:100%" @update:model-value="selectCustomer(String($event || ''))"><el-option v-for="customer in selectableCustomers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></el-select></el-form-item>
<el-form-item label="状态"><el-input :model-value="detail ? statusLabel(detail.status) : '待创建'" disabled /></el-form-item>
<el-form-item label="渠道编码" prop="channelCode"><el-input v-model="form.data.channelCode" placeholder="请输入渠道编码" /></el-form-item>
<el-form-item label="渠道名称" prop="channelName"><el-input v-model="form.data.channelName" placeholder="请输入渠道名称" /></el-form-item>
<el-form-item label="装柜仓库" prop="warehouseId"><el-select :model-value="form.data.warehouseId" filterable placeholder="请选择装柜仓库" style="width:100%" @update:model-value="(value) => { form.data.warehouseId = String(value || ''); form.data.warehouseName = warehouses.find((item) => String(item.id) === String(value))?.warehouseName || '' }"><el-option v-for="warehouse in warehouses" :key="String(warehouse.id)" :label="warehouse.warehouseName" :value="String(warehouse.id)" /></el-select></el-form-item>
<el-form-item label="提单号"><el-input v-model="form.data.billOfLadingNo" placeholder="请输入提单号(可选)" /></el-form-item>
<el-form-item label="船公司"><el-select :model-value="form.data.shippingCompanyId" filterable clearable placeholder="请选择船公司" :loading="optionsLoading" style="width:100%" @update:model-value="selectPartner('shippingCompany', String($event || ''))"><el-option v-if="form.data.shippingCompanyId && !shippingCompanies.some((partner) => String(partner.id) === String(form.data.shippingCompanyId))" :label="form.data.shippingCompanyName || '当前船公司'" :value="String(form.data.shippingCompanyId)" /><el-option v-for="partner in shippingCompanies" :key="String(partner.id)" :label="partner.partnerName" :value="String(partner.id)" /></el-select></el-form-item>
<el-form-item label="订舱代理"><el-select :model-value="form.data.bookingAgentId" filterable clearable placeholder="请选择订舱代理" :loading="optionsLoading" style="width:100%" @update:model-value="selectPartner('bookingAgent', String($event || ''))"><el-option v-if="form.data.bookingAgentId && !bookingAgents.some((partner) => String(partner.id) === String(form.data.bookingAgentId))" :label="form.data.bookingAgentName || '当前订舱代理'" :value="String(form.data.bookingAgentId)" /><el-option v-for="partner in bookingAgents" :key="String(partner.id)" :label="partner.partnerName" :value="String(partner.id)" /></el-select></el-form-item>
@@ -280,6 +291,8 @@ onMounted(() => { void load(); void loadOptions() })
<div v-for="(container, index) in form.data.containerRequirements" :key="index" class="container-row">
<el-input v-model="container.containerType" :disabled="!editable" placeholder="箱型,如 40HC" />
<el-input v-model="container.containerKind" :disabled="!editable" placeholder="类型,如 FCL" />
<el-input-number v-model="container.capacityCbm" :disabled="!editable" :min="0.01" :precision="2" placeholder="体积 m³" />
<el-input-number v-model="container.maxWeightKg" :disabled="!editable" :min="0.01" :precision="2" placeholder="限重 kg" />
<el-input-number v-model="container.quantity" :disabled="!editable" :min="1" :max="999" />
<el-button v-if="editable" type="danger" plain :disabled="form.data.containerRequirements.length === 1" @click="removeContainer(index)"><Trash2 :size="15" />删除</el-button>
</div>