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
@@ -39,7 +39,8 @@ describe('platform container loading API', () => {
.mockResolvedValueOnce({ code: '200', data: container })
.mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, inboundDetailId: 3, orderId: 4, warehouseId: 5, destinationWarehouseId: 6, availablePackageCount: '7' }] })
expect((await getContainer('9')).id).toBe(String(container.id))
expect(await listSmartLoadingCandidates('5')).toMatchObject([{ accountId: '1', inboundDetailId: '3', destinationWarehouseId: '6', availablePackageCount: 7 }])
expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundDetailId: '3', destinationWarehouseId: '6', availablePackageCount: 7 }])
expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/candidates', { params: { bookingId: '7', warehouseId: '5' } })
})
it('sends plan and item versions plus operation and idempotency keys', async () => {
@@ -76,8 +76,8 @@ export async function updateContainerTransport(containerId: string, version: num
return container(unwrapApiResponse(response))
}
export async function listSmartLoadingCandidates(warehouseId: string) {
const response = await http.get<ApiEnvelope<SmartLoadingCandidate[]>, ApiEnvelope<SmartLoadingCandidate[]>>(`${smartLoadingPath}/candidates`, { params: { warehouseId } })
export async function listSmartLoadingCandidates(bookingId: string, warehouseId: string) {
const response = await http.get<ApiEnvelope<SmartLoadingCandidate[]>, ApiEnvelope<SmartLoadingCandidate[]>>(`${smartLoadingPath}/candidates`, { params: { bookingId, warehouseId } })
return unwrapApiResponse(response).map((raw) => ({
...raw, accountId: id(raw.accountId), inboundId: id(raw.inboundId), inboundDetailId: id(raw.inboundDetailId),
orderId: id(raw.orderId), warehouseId: id(raw.warehouseId), destinationWarehouseId: optionalId(raw.destinationWarehouseId),
@@ -65,7 +65,7 @@ describe('GlobalSmartLoadingView', () => {
expect(listContainers).toHaveBeenCalledWith({ page: 1, size: 100 })
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.warehouseId)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId)
expect(wrapper.text()).toContain('候选订单与货物')
expect(wrapper.text()).toContain('ORD-2')
expect(wrapper.text()).toContain('CB-1 装柜方案')
@@ -146,7 +146,7 @@ describe('GlobalSmartLoadingView', () => {
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(pushed.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(pushed.warehouseId)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(pushed.id, pushed.warehouseId)
expect(wrapper.text()).toContain('CB-2')
expect(wrapper.text()).toContain('拣货中')
expect(wrapper.text()).toContain('智能推荐')
@@ -172,7 +172,7 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
selectionLoading.value = true
try {
const [candidateResult, planResult] = await Promise.all([
listSmartLoadingCandidates(container.warehouseId),
listSmartLoadingCandidates(container.id, container.warehouseId),
getSmartLoadingPlanByBooking(container.id).then((value) => ({ value, missing: false })).catch((error) => {
const details = getApiErrorDetails(error)
if (details.code === 'A0404') return { value: null, missing: true }
@@ -42,7 +42,7 @@ describe('SmartLoadingWorkbenchView', () => {
await flushPromises()
expect(getContainer).toHaveBeenCalledWith(container.id)
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith('8')
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, '8')
expect(wrapper.text()).toContain('LP-99')
expect(wrapper.text()).toContain('ORD-1')
expect(wrapper.text()).toContain('按 110% 上限')
@@ -57,7 +57,7 @@ describe('SmartLoadingWorkbenchView', () => {
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.warehouseId)
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId)
expect(wrapper.text()).toContain('拣货中')
expect(wrapper.text()).toContain('智能推荐')
expect(wrapper.text()).toContain('ORD-1')
@@ -108,7 +108,7 @@ async function load() {
if (detail.code === 'A0404') { plan.value = null; noDraft.value = true }
else throw error
}
candidates.value = await listSmartLoadingCandidates(container.value.warehouseId)
candidates.value = await listSmartLoadingCandidates(container.value.id, container.value.warehouseId)
} catch (error) { loadError.value = getApiErrorDetails(error, '智能排柜工作台加载失败') }
finally { loading.value = false }
}
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { createOceanBooking, listOceanBookings, performOceanBookingAction, requestOceanBookingChange } from './platformOceanBookings'
import { createOceanBooking, listOceanBookings, performOceanBookingAction, releaseOceanBooking, requestOceanBookingChange } from './platformOceanBookings'
import { http } from '@/shared/api/http'
const detailResponse = (version = 0) => ({
@@ -22,7 +22,7 @@ describe('ocean bookings api', () => {
it('uses version zero for create and extracts the id from the returned Detail', async () => {
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: detailResponse() })
const created = await createOceanBooking({ expectedVersion: 0, data: { customerId: '456', customerName: '客户', parties: [], containerRequirements: [] } })
const created = await createOceanBooking({ expectedVersion: 0, data: { customerId: '456', customerName: '客户', channelCode: 'SEA', channelName: '海运', warehouseId: '8', warehouseName: '义乌仓', parties: [], containerRequirements: [] } })
expect(post.mock.calls[0]?.[1]).toMatchObject({ expectedVersion: 0 })
expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } })
expect(created.id).toBe('123')
@@ -38,5 +38,9 @@ describe('ocean bookings api', () => {
await requestOceanBookingChange('123', { expectedVersion: 4, vesselName: 'VESSEL', voyageNo: 'V001', reason: '调整船期' })
expect(post.mock.calls[1]?.[0]).toContain('/123/change-request')
expect(post.mock.calls[1]?.[1]).toMatchObject({ expectedVersion: 4, vesselName: 'VESSEL', voyageNo: 'V001', reason: '调整船期' })
await releaseOceanBooking('123', { expectedVersion: 4, billOfLadingNo: 'BL-1', vesselName: 'VESSEL', voyageNo: 'V001', estimatedSailingAt: '2026-08-12T10:00:00', estimatedArrivalAt: '2026-09-01T10:00:00' })
expect(post.mock.calls[2]?.[0]).toContain('/123/release')
expect(post.mock.calls[2]?.[1]).toMatchObject({ billOfLadingNo: 'BL-1', vesselName: 'VESSEL', voyageNo: 'V001', estimatedSailingAt: '2026-08-12T10:00:00', estimatedArrivalAt: '2026-09-01T10:00:00' })
})
})
@@ -1,5 +1,5 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { OceanBookingActionRequest, OceanBookingChangeRequest, OceanBookingDetail, OceanBookingPage, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus } from '../types'
import type { OceanBookingActionRequest, OceanBookingChangeRequest, OceanBookingDetail, OceanBookingPage, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus, OceanBookingReleaseRequest } from '../types'
const basePath = '/platform/v1/ocean-bookings'
function pageNumber(value: number | string, fallback: number) { const n = Number(value); return Number.isFinite(n) ? Math.trunc(n) : fallback }
@@ -45,7 +45,8 @@ function idempotencyKey() { return `ocean-booking-${Date.now()}-${Math.random().
export async function createOceanBooking(payload: OceanBookingSaveRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(basePath, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export async function updateOceanBooking(id: string, payload: OceanBookingSaveRequest) { const response = await http.put<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export type OceanBookingAction = 'submit' | 'confirm' | 'release' | 'fail' | 'cancel' | 'change-request' | 'change-confirm' | 'change-fail'
export async function performOceanBookingAction(id: string, action: Exclude<OceanBookingAction, 'change-request'>, request: OceanBookingActionRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/${action}`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export async function performOceanBookingAction(id: string, action: Exclude<OceanBookingAction, 'change-request' | 'release'>, request: OceanBookingActionRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/${action}`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export async function releaseOceanBooking(id: string, request: OceanBookingReleaseRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/release`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export async function requestOceanBookingChange(id: string, request: OceanBookingChangeRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/change-request`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export async function printOceanBooking(id: string, type: 'SHIPPING_INSTRUCTION' | 'BILL_CONFIRMATION') { return http.get<Blob, Blob>(`${basePath}/${encodeURIComponent(id)}/print`, { params: { type }, responseType: 'blob' }) }
export type { OceanBookingResource, OceanBookingStatus }
@@ -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>
+4 -1
View File
@@ -1,10 +1,11 @@
export type OceanBookingStatus = 'DRAFT' | 'SUBMITTED' | 'CONFIRMED' | 'RELEASED' | 'FAILED' | 'CHANGE_PENDING' | 'CANCELLED'
export interface OceanBookingParty { id?: string; partyType: string; partyName: string; contactName?: string | null; contactPhone?: string | null; address?: string | null }
export interface OceanBookingContainer { id?: string; containerType: string; quantity: number; containerKind?: string | null }
export interface OceanBookingContainer { id?: string; containerType: string; quantity: number; containerKind?: string | null; capacityCbm: number; maxWeightKg: number }
export interface OceanBookingContainerAllocation extends OceanBookingContainer { allocatedQuantity?: number | null }
export interface OceanBookingDraftData {
customerId: string; customerName: string; parties: OceanBookingParty[]; containerRequirements: OceanBookingContainer[]
channelCode: string; channelName: string; warehouseId: string; warehouseName: string
billOfLadingNo?: string | null
shippingCompanyId?: string | null; shippingCompanyName?: string | null; bookingAgentId?: string | null; bookingAgentName?: string | null
departurePortCode?: string | null; departurePortName?: string | null; destinationPortCode?: string | null; destinationPortName?: string | null
@@ -14,6 +15,7 @@ export interface OceanBookingDraftData {
export interface OceanBookingResource {
id: string; bookingNo: string | null; status: OceanBookingStatus; version: number
customerId: string | null; customerName: string | null; billOfLadingNo: string | null
channelCode: string | null; channelName: string | null; warehouseId: string | null; warehouseName: string | null
shippingCompanyId: string | null; shippingCompanyName: string | null; bookingAgentId: string | null; bookingAgentName: string | null
vesselName: string | null; voyageNo: string | null; departurePortCode: string | null; departurePortName: string | null
destinationPortCode: string | null; destinationPortName: string | null; placeOfDelivery: string | null
@@ -32,6 +34,7 @@ export interface OceanBookingPage { records: OceanBookingResource[]; page: numbe
export interface OceanBookingQuery { page: number; size: number; keyword?: string; status?: OceanBookingStatus; customerId?: string }
export interface OceanBookingSaveRequest { expectedVersion: number; data: OceanBookingDraftData }
export interface OceanBookingActionRequest { expectedVersion: number; reason?: string | null }
export interface OceanBookingReleaseRequest { expectedVersion: number; billOfLadingNo: string; vesselName: string; voyageNo: string; estimatedSailingAt: string; estimatedArrivalAt: string; reason?: string | null }
export interface OceanBookingChangeRequest extends OceanBookingActionRequest {
vesselName: string
voyageNo: string
@@ -103,7 +103,8 @@ describe('unified order workflow API contract', () => {
unitWeightKg: '10.5', unitVolumeCbm: '0.25',
}] })
const container = await getContainerBooking('2086085512165634050')
const candidates = await listSmartLoadingCandidates(container.warehouseId!)
const candidates = await listSmartLoadingCandidates(container.id, container.warehouseId!)
expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/candidates', { params: { bookingId: container.id, warehouseId: container.warehouseId } })
expect(container).toMatchObject({ id: '2086085512165634050', warehouseId: '2086085512165634051', capacityCbm: 68 })
expect(candidates[0]).toMatchObject({
accountId: '2086085512165634052', inboundId: '2086085512165634053',
+2 -2
View File
@@ -339,9 +339,9 @@ export async function getContainerBooking(bookingId: string | number) {
return normalizeContainer(unwrapApiResponse(response))
}
export async function listSmartLoadingCandidates(warehouseId: string | number) {
export async function listSmartLoadingCandidates(bookingId: string | number, warehouseId: string | number) {
const response = await http.get<ApiEnvelope<Record<string, unknown>[]>, ApiEnvelope<Record<string, unknown>[]>>(
`${smartLoadingBasePath}/candidates`, { params: { warehouseId: String(warehouseId) } },
`${smartLoadingBasePath}/candidates`, { params: { bookingId: String(bookingId), warehouseId: String(warehouseId) } },
)
return unwrapApiResponse(response).map(normalizeCandidate)
}
@@ -166,7 +166,7 @@ async function load() {
if (error instanceof ApiResponseError && ['A0404', 'NOT_FOUND'].includes(error.code)) plan.value = null
else throw Object.assign(error instanceof Error ? error : new Error(details.message), { details })
}
candidates.value = nextContainer.warehouseId ? await listSmartLoadingCandidates(nextContainer.warehouseId) : []
candidates.value = nextContainer.warehouseId ? await listSmartLoadingCandidates(nextContainer.id, nextContainer.warehouseId) : []
} catch (error) {
container.value = null
plan.value = null
@@ -185,7 +185,7 @@ async function runAction(name: string, operation: () => Promise<SmartLoadingPlan
plan.value = await operation()
ElMessage.success(success)
emit('updated')
if (container.value?.warehouseId) candidates.value = await listSmartLoadingCandidates(container.value.warehouseId)
if (container.value?.warehouseId) candidates.value = await listSmartLoadingCandidates(container.value.id, container.value.warehouseId)
return true
} catch (error) {
actionError.value = getApiErrorDetails(error, `${success}失败,请重试`)