feat(admin): adapt channel booking and smart loading contracts
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}失败,请重试`)
|
||||
|
||||
Reference in New Issue
Block a user