From 9fcea1ca1c51166e7cf7b2801fa7bee7e7fc022c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 1 Sep 2026 16:14:16 +0800 Subject: [PATCH] refactor(admin): scope truck orders to bookings --- .../trucking/api/platformTrucking.test.ts | 31 +++++------- src/modules/trucking/api/platformTrucking.ts | 9 ++-- .../TruckAppointmentForm.behavior.test.ts | 50 ++++++++----------- .../components/TruckAppointmentForm.test.ts | 34 ++++++------- .../components/TruckAppointmentForm.vue | 38 ++------------ ...ruckAppointmentCreateView.behavior.test.ts | 28 +++++------ .../pages/TruckAppointmentCreateView.test.ts | 11 ++-- .../pages/TruckAppointmentCreateView.vue | 35 +++++++------ .../pages/TruckAppointmentDetailView.test.ts | 21 ++++---- .../pages/TruckAppointmentDetailView.vue | 20 ++------ .../pages/TruckAppointmentsView.test.ts | 13 +++-- .../trucking/pages/TruckAppointmentsView.vue | 10 ++-- src/modules/trucking/types.ts | 22 +------- 13 files changed, 118 insertions(+), 204 deletions(-) diff --git a/src/modules/trucking/api/platformTrucking.test.ts b/src/modules/trucking/api/platformTrucking.test.ts index 2971ae9b..2da168a6 100644 --- a/src/modules/trucking/api/platformTrucking.test.ts +++ b/src/modules/trucking/api/platformTrucking.test.ts @@ -8,15 +8,15 @@ describe('truck appointment API', () => { beforeEach(() => { vi.clearAllMocks() }) it('normalizes page values and maps filters', async () => { - vi.mocked(http.get).mockResolvedValue({ data: { records: [{ id: 1, oceanBookingId: 2, containerBookingId: 3, warehouseId: 4, shippingCompanyId: 5, bookingAgentId: 6, oceanBookingNo: 'OB-1', cabinetNo: 'CAB-1', containerNo: 'MSCU-1', warehouseName: '义乌仓', shippingCompanyName: '船公司', bookingAgentName: '订舱代理', attachmentCount: '2' }], page: '2', size: '20', total: '21', pages: '2' } }) - await expect(listTruckAppointments({ page: 2, size: 20, keyword: ' BY ', status: 'DISPATCHED', containerBookingId: '77' })).resolves.toMatchObject({ page: 2, total: 21, pages: 2, records: [{ id: '1', oceanBookingId: '2', containerBookingId: '3', warehouseId: '4', shippingCompanyId: '5', bookingAgentId: '6', oceanBookingNo: 'OB-1', cabinetNo: 'CAB-1', containerNo: 'MSCU-1', attachmentCount: 2 }] }) - expect(http.get).toHaveBeenCalledWith('/platform/v1/truck-appointments', { params: { page: 2, size: 20, keyword: 'BY', status: 'DISPATCHED', containerBookingId: '77' } }) + vi.mocked(http.get).mockResolvedValue({ data: { records: [{ id: 1, oceanBookingId: 2, warehouseId: 4, shippingCompanyId: 5, bookingAgentId: 6, oceanBookingNo: 'OB-1', warehouseName: '义乌仓', shippingCompanyName: '船公司', bookingAgentName: '订舱代理', attachmentCount: '2' }], page: '2', size: '20', total: '21', pages: '2' } }) + await expect(listTruckAppointments({ page: 2, size: 20, keyword: ' BY ', status: 'DISPATCHED', oceanBookingId: '77' })).resolves.toMatchObject({ page: 2, total: 21, pages: 2, records: [{ id: '1', oceanBookingId: '2', warehouseId: '4', shippingCompanyId: '5', bookingAgentId: '6', oceanBookingNo: 'OB-1', attachmentCount: 2 }] }) + expect(http.get).toHaveBeenCalledWith('/platform/v1/truck-appointments', { params: { page: 2, size: 20, keyword: 'BY', status: 'DISPATCHED', oceanBookingId: '77' } }) }) it('uses the server-owned create candidate contract without rebuilding availability', async () => { - vi.mocked(http.get).mockResolvedValue({ data: { records: [{ oceanBookingId: 11, oceanBookingNo: 'OB-11', billOfLadingNo: 'BL-11', warehouseId: 12, warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', requirementId: 13, containerType: '40HC', requirementQuantity: '2', containerBookingId: 14, cabinetNo: 'CAB-14', containerNo: null, loadingPlanId: 15, loadingPlanNo: 'LP-15', loadingPlanStatus: 'EDITING', bookingDocumentReady: true }], page: '1', size: '200', total: '1', pages: '1' } }) + vi.mocked(http.get).mockResolvedValue({ data: { records: [{ oceanBookingId: 11, oceanBookingNo: 'OB-11', billOfLadingNo: 'BL-11', warehouseId: 12, warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', bookingDocumentReady: true }], page: '1', size: '200', total: '1', pages: '1' } }) await expect(listTruckAppointmentCandidates({ keyword: ' OB ', page: 1, size: 200 })).resolves.toMatchObject({ - records: [{ oceanBookingId: '11', warehouseId: '12', requirementId: '13', containerBookingId: '14', loadingPlanId: '15', requirementQuantity: 2, bookingDocumentReady: true }], + records: [{ oceanBookingId: '11', warehouseId: '12', bookingDocumentReady: true }], total: 1, }) expect(http.get).toHaveBeenCalledWith('/platform/v1/truck-appointments/candidates', { params: { keyword: 'OB', page: 1, size: 200 } }) @@ -27,31 +27,26 @@ describe('truck appointment API', () => { vi.mocked(http.post).mockResolvedValue({ data: '9001' }) vi.mocked(http.post).mockResolvedValue({ data: { id: '9001', status: 'DISPATCHED' } }) await expect(getTruckAppointment('9001')).resolves.toMatchObject({ id: '9001' }) - await expect(createTruckAppointment({ carrierId: '200', oceanBookingId: '300', containerBookingId: '400', loadingPlanId: null, plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0 })).resolves.toMatchObject({ id: '9001' }) - expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/truck-appointments', { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', loadingPlanId: null, plannedLoadingAt: '2026-08-10T10:00:00', currency: 'RMB', expectedVersion: 0 }) + await expect(createTruckAppointment({ carrierId: '200', oceanBookingId: '300', plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0 })).resolves.toMatchObject({ id: '9001' }) + expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/truck-appointments', { carrierId: '200', oceanBookingId: '300', plannedLoadingAt: '2026-08-10T10:00:00', currency: 'RMB', expectedVersion: 0 }) await expect(performTruckAppointmentAction('9001', 'DISPATCH', { expectedVersion: 2 })).resolves.toMatchObject({ status: 'DISPATCHED' }) expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments/9001/dispatch', { expectedVersion: 2, reason: null }, { headers: { 'Idempotency-Key': expect.stringContaining('truck-appointment-dispatch-9001-') } }) }) - it('sends the real ISO container number only with the pickup action', async () => { - vi.mocked(http.post).mockResolvedValue({ data: { id: '9001', status: 'PICKUP_IN_PROGRESS', containerNo: 'MSCU6639870' } }) - await performTruckAppointmentAction('9001', 'PICKUP', { expectedVersion: 3, containerNo: 'MSCU6639870' }) + it('keeps pickup booking-scoped without sending container identity', async () => { + vi.mocked(http.post).mockResolvedValue({ data: { id: '9001', status: 'PICKUP_IN_PROGRESS' } }) + await performTruckAppointmentAction('9001', 'PICKUP', { expectedVersion: 3 }) expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments/9001/pickup', { - expectedVersion: 3, reason: null, containerNo: 'MSCU6639870', + expectedVersion: 3, reason: null, }, { headers: { 'Idempotency-Key': expect.stringContaining('truck-appointment-pickup-9001-') } }) - - await performTruckAppointmentAction('9002', 'PICKUP', { expectedVersion: 1, containerNo: null }) - expect(http.post).toHaveBeenLastCalledWith('/platform/v1/truck-appointments/9002/pickup', { - expectedVersion: 1, reason: null, - }, { headers: { 'Idempotency-Key': expect.stringContaining('truck-appointment-pickup-9002-') } }) }) it('drops legacy customer fields instead of forwarding hidden compatibility data', async () => { vi.mocked(http.post).mockResolvedValueOnce({ data: { id: '9001', status: 'DRAFT', attachmentCount: 0 } }) - const payload = { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0, currency: 'EUR', receivableAmount: '999.00', customerId: 'legacy-customer', customerName: '旧客户快照' } + const payload = { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', loadingPlanId: '500', plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0, currency: 'EUR', receivableAmount: '999.00', customerId: 'legacy-customer', customerName: '旧客户快照' } await createTruckAppointment(payload as never) expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments', { - carrierId: '200', oceanBookingId: '300', containerBookingId: '400', plannedLoadingAt: '2026-08-10T10:00:00', currency: 'RMB', expectedVersion: 0, + carrierId: '200', oceanBookingId: '300', plannedLoadingAt: '2026-08-10T10:00:00', currency: 'RMB', expectedVersion: 0, }) }) diff --git a/src/modules/trucking/api/platformTrucking.ts b/src/modules/trucking/api/platformTrucking.ts index 9074ba2f..b3f9166e 100644 --- a/src/modules/trucking/api/platformTrucking.ts +++ b/src/modules/trucking/api/platformTrucking.ts @@ -41,7 +41,7 @@ function normalizePage(payload: PagePayload): TruckAppointmentPage { } function normalizeResource(resource: TruckAppointmentResource): TruckAppointmentResource { - const idKeys = ['id', 'outboundOrderId', 'oceanBookingId', 'containerBookingId', 'loadingPlanId', 'warehouseId', 'shippingCompanyId', 'bookingAgentId', 'carrierId'] + const idKeys = ['id', 'outboundOrderId', 'oceanBookingId', 'warehouseId', 'shippingCompanyId', 'bookingAgentId', 'carrierId'] const normalized = { ...resource } as Record for (const key of idKeys) { if (normalized[key] !== null && normalized[key] !== undefined) normalized[key] = String(normalized[key]) @@ -56,8 +56,7 @@ export async function listTruckAppointments(query: TruckAppointmentQuery = {}) { ...query, keyword: query.keyword?.trim() || undefined, status: query.status || undefined, - containerBookingId: query.containerBookingId || undefined, - containerType: query.containerType || undefined, + oceanBookingId: query.oceanBookingId || undefined, }, }) return normalizePage(unwrapApiResponse(response)) @@ -73,10 +72,9 @@ type CandidatePagePayload = Omit): TruckAppointmentCandidate { const normalized = { ...candidate } as Record - for (const key of ['oceanBookingId', 'warehouseId', 'requirementId', 'containerBookingId', 'loadingPlanId']) { + for (const key of ['oceanBookingId', 'warehouseId']) { if (normalized[key] !== null && normalized[key] !== undefined) normalized[key] = String(normalized[key]) } - normalized.requirementQuantity = Number(normalized.requirementQuantity || 0) normalized.bookingDocumentReady = normalized.bookingDocumentReady === true return normalized as unknown as TruckAppointmentCandidate } @@ -157,7 +155,6 @@ export async function performTruckAppointmentAction( const body = { expectedVersion: payload.expectedVersion, reason: payload.reason || null, - ...(payload.containerNo ? { containerNo: payload.containerNo } : {}), } const response = await http.post, ApiEnvelope>(`${basePath}/${id}/${action.toLowerCase()}`, body, { headers: { 'Idempotency-Key': operationKey, ...(requestHash ? { 'X-Request-Hash': requestHash } : {}) }, diff --git a/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts b/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts index 7b7ee398..00d8f38f 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts +++ b/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts @@ -18,15 +18,12 @@ vi.mock('../api/platformTrucking', () => ({ listTruckAppointmentCandidates: vi.f const model: SaveTruckAppointmentRequest = { expectedVersion: 0, oceanBookingId: null, - containerBookingId: null, - loadingPlanId: null, carrierId: null, plannedLoadingAt: null, } const candidates = [ - { oceanBookingId: 'booking-1', oceanBookingNo: 'OB-1', billOfLadingNo: 'BL-1', warehouseId: 'WH-1', warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', requirementId: 'requirement-1', containerType: '40HC', requirementQuantity: 2, containerBookingId: 'container-1', cabinetNo: 'CAB-1', containerNo: 'CBHU8888883', loadingPlanId: 'plan-1', loadingPlanNo: 'LP-1', loadingPlanStatus: 'EDITING', bookingDocumentReady: true }, - { oceanBookingId: 'booking-1', oceanBookingNo: 'OB-1', billOfLadingNo: 'BL-1', warehouseId: 'WH-1', warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', requirementId: 'requirement-1', containerType: '40HC', requirementQuantity: 2, containerBookingId: 'container-pending', cabinetNo: 'CAB-X', containerNo: null, loadingPlanId: null, loadingPlanNo: null, loadingPlanStatus: null, bookingDocumentReady: true }, - { oceanBookingId: 'booking-2', oceanBookingNo: 'OB-2', billOfLadingNo: 'BL-2', warehouseId: 'WH-2', warehouseName: '东莞仓', channelCode: 'SEA', channelName: '海运', requirementId: 'requirement-2', containerType: '40GP', requirementQuantity: 1, containerBookingId: 'container-2', cabinetNo: 'CAB-2', containerNo: 'MSCU6639870', loadingPlanId: null, loadingPlanNo: null, loadingPlanStatus: null, bookingDocumentReady: true }, + { oceanBookingId: 'booking-1', oceanBookingNo: 'OB-1', billOfLadingNo: 'BL-1', warehouseId: 'WH-1', warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', bookingDocumentReady: true }, + { oceanBookingId: 'booking-2', oceanBookingNo: 'OB-2', billOfLadingNo: 'BL-2', warehouseId: 'WH-2', warehouseName: '东莞仓', channelCode: 'SEA', channelName: '海运', bookingDocumentReady: true }, ] function detail(id: string) { const candidate = candidates.find((item) => item.oceanBookingId === id)! @@ -36,9 +33,9 @@ function detail(id: string) { containers: [], } } -function resource(id: string, bookingId: string, containerId: string): TruckAppointmentResource { - const candidate = candidates.find((item) => item.oceanBookingId === bookingId && item.containerBookingId === containerId)! - return { id, status: 'DRAFT', oceanBookingId: bookingId, oceanBookingNo: candidate.oceanBookingNo, containerBookingId: containerId, cabinetNo: candidate.cabinetNo, containerNo: candidate.containerNo, relatedContainerType: candidate.containerType } +function resource(id: string, bookingId: string): TruckAppointmentResource { + const candidate = candidates.find((item) => item.oceanBookingId === bookingId)! + return { id, status: 'DRAFT', oceanBookingId: bookingId, oceanBookingNo: candidate.oceanBookingNo, billOfLadingNo: candidate.billOfLadingNo, warehouseId: candidate.warehouseId, warehouseName: candidate.warehouseName } } describe('TruckAppointmentForm booking-only behavior', () => { @@ -48,7 +45,7 @@ describe('TruckAppointmentForm booking-only behavior', () => { vi.mocked(getOceanBooking).mockImplementation(async (id) => detail(id) as never) }) - it('selects a released booking, exposes pending-pickup internal containers, and never emits customer fields', async () => { + it('selects only server-approved released bookings and never exposes container identity', async () => { const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } }) await flushPromises() const selects = wrapper.findAllComponents({ name: 'ElSelect' }) @@ -58,8 +55,8 @@ describe('TruckAppointmentForm booking-only behavior', () => { expect(listTruckAppointmentCandidates).toHaveBeenCalledWith({ page: 1, size: 200 }) expect(getOceanBooking).not.toHaveBeenCalled() const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || '')) - expect(labels.some((label) => label.includes('CAB-1') && label.includes('CBHU8888883'))).toBe(true) - expect(labels.some((label) => label.includes('CAB-X') && label.includes('40HC') && label.includes('待补录'))).toBe(true) + expect(labels.some((label) => label.includes('OB-1') && label.includes('BL-1'))).toBe(true) + expect(labels.some((label) => label.includes('CAB-'))).toBe(false) const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest expect(emitted.oceanBookingId).toBe('booking-1') expect(emitted.billOfLadingNo).toBe('BL-1') @@ -68,23 +65,20 @@ describe('TruckAppointmentForm booking-only behavior', () => { wrapper.unmount() }) - it('clears a selected container when switching to another booking', async () => { + it('replaces frozen booking snapshots when switching the selected booking', async () => { const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } }) await flushPromises() const selects = wrapper.findAllComponents({ name: 'ElSelect' }) selects[0]!.vm.$emit('update:modelValue', 'booking-1') await flushPromises() - selects[1]!.vm.$emit('update:modelValue', 'container-1') - await flushPromises() - expect((wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest).containerBookingId).toBe('container-1') - selects[0]!.vm.$emit('update:modelValue', 'booking-2') await flushPromises() const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest expect(emitted.oceanBookingId).toBe('booking-2') - expect(emitted.containerBookingId).toBeNull() + expect(emitted.billOfLadingNo).toBe('BL-2') + expect(emitted.warehouseId).toBe('WH-2') const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || '')) - expect(labels.some((label) => label.includes('CAB-2'))).toBe(true) + expect(labels.some((label) => label.includes('OB-2'))).toBe(true) wrapper.unmount() }) @@ -95,11 +89,11 @@ describe('TruckAppointmentForm booking-only behavior', () => { if (id === 'booking-1') resolveFirst = resolve as typeof resolveFirst else resolveSecond = resolve as typeof resolveSecond }) as never) - const firstResource = resource('appointment-1', 'booking-1', 'container-1') - const secondResource = resource('appointment-2', 'booking-2', 'container-2') - const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1', containerBookingId: 'container-1' }, resource: firstResource }, global: { plugins: [ElementPlus] } }) + const firstResource = resource('appointment-1', 'booking-1') + const secondResource = resource('appointment-2', 'booking-2') + const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1' }, resource: firstResource }, global: { plugins: [ElementPlus] } }) await flushPromises() - await wrapper.setProps({ modelValue: { ...model, oceanBookingId: 'booking-2', containerBookingId: 'container-2' }, resource: secondResource }) + await wrapper.setProps({ modelValue: { ...model, oceanBookingId: 'booking-2' }, resource: secondResource }) await flushPromises() resolveSecond(detail('booking-2')) @@ -108,8 +102,8 @@ describe('TruckAppointmentForm booking-only behavior', () => { await flushPromises() const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || '')) - expect(labels.some((label) => label.includes('CAB-2') && label.includes('不可重选'))).toBe(true) - expect(labels.some((label) => label.includes('CAB-1'))).toBe(false) + expect(labels.some((label) => label.includes('OB-2') && label.includes('不可重选'))).toBe(true) + expect(labels.some((label) => label.includes('OB-1'))).toBe(false) expect(wrapper.text()).not.toContain('所选订舱缺少有效的订舱确认文件或提单文件') expect(listTruckAppointmentCandidates).not.toHaveBeenCalled() wrapper.unmount() @@ -117,8 +111,8 @@ describe('TruckAppointmentForm booking-only behavior', () => { it('preserves the business code and request trace when booking readiness fails', async () => { vi.mocked(getOceanBooking).mockRejectedValueOnce(new ApiResponseError('订舱版本冲突', 'A0409', 409, undefined, 'req-booking', 'trace-booking')) - const current = resource('appointment-1', 'booking-1', 'container-1') - const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1', containerBookingId: 'container-1' }, resource: current }, global: { plugins: [ElementPlus] } }) + const current = resource('appointment-1', 'booking-1') + const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1' }, resource: current }, global: { plugins: [ElementPlus] } }) await flushPromises() expect(wrapper.text()).toContain('订舱版本冲突') expect(wrapper.text()).toContain('A0409') @@ -129,8 +123,8 @@ describe('TruckAppointmentForm booking-only behavior', () => { it('flags an edit binding whose booking has become unreleased without adding it to create candidates', async () => { vi.mocked(getOceanBooking).mockResolvedValueOnce({ ...detail('booking-1'), status: 'DRAFT' } as never) - const current = resource('appointment-1', 'booking-1', 'container-1') - const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1', containerBookingId: 'container-1' }, resource: current }, global: { plugins: [ElementPlus] } }) + const current = resource('appointment-1', 'booking-1') + const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1' }, resource: current }, global: { plugins: [ElementPlus] } }) await flushPromises() expect(wrapper.text()).toContain('当前绑定订舱已不是放舱状态') expect(listTruckAppointmentCandidates).not.toHaveBeenCalled() diff --git a/src/modules/trucking/components/TruckAppointmentForm.test.ts b/src/modules/trucking/components/TruckAppointmentForm.test.ts index de199600..e0c06eb4 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.test.ts +++ b/src/modules/trucking/components/TruckAppointmentForm.test.ts @@ -6,31 +6,28 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/trucking/compone const typesSource = readFileSync(resolve(process.cwd(), 'src/modules/trucking/types.ts'), 'utf8') describe('TruckAppointmentForm interaction contract', () => { - it('requires a released booking before an allocated internal container', () => { + it('requires one server-approved released booking without exposing a container selector', () => { expect(source).toContain('listTruckAppointmentCandidates({ page: 1, size: 200 })') expect(source).toContain("oceanBookingId: [{ required: true, message: '请选择已放舱订舱'") - expect(source).toContain("containerBookingId: [{ required: true, message: '请选择该订舱下可预约的柜次'") - expect(source.indexOf('label="订舱"')).toBeLessThan(source.indexOf('label="柜次"')) - expect(source).toContain(':disabled="relationLocked || !draft.oceanBookingId || optionsLoading || bookingContainersLoading"') + expect(source).not.toContain('prop="containerBookingId"') + expect(source).not.toContain('label="柜次"') expect(source).not.toContain('关联订舱(可选)') expect(source).not.toContain('关联柜次(可选)') }) - it('uses only server-owned create candidates and never rebuilds availability from booking detail', () => { + it('uses only server-owned booking candidates and never rebuilds availability from booking detail', () => { expect(source).toContain('candidates.value = results[1].value.records') - expect(source).toContain('candidates.value.filter((item) => item.oceanBookingId === draft.oceanBookingId)') - expect(source).toContain('该订舱暂无可预约柜次') + expect(source).toContain('new Map { - expect(source).toContain('draft.containerBookingId = null') - expect(source).toContain('draft.loadingPlanId = null') - expect(source).toContain('draft.containerBookingId = null') - expect(source).toContain('candidate.loadingPlanId') + it('does not write legacy container or loading-plan fields', () => { + expect(source).not.toContain('draft.containerBookingId') + expect(source).not.toContain('draft.loadingPlanId') + expect(typesSource).not.toContain("'containerBookingId', 'loadingPlanId'") }) it('freezes warehouse and bill of lading snapshots without retaining customer data', () => { @@ -56,12 +53,11 @@ describe('TruckAppointmentForm interaction contract', () => { expect(source).toContain('请选择有效的合作中拖车供应商') }) - it('freezes the current edit relation and marks its fallback options as non-selectable', () => { + it('freezes the current edit booking and marks its fallback option as non-selectable', () => { expect(source).toContain('const relationLocked = computed(() => Boolean(props.resource))') expect(source).toContain('(当前绑定,不可重选)') expect(source).toContain(':disabled="relationLocked"') - expect(source).toContain(' { @@ -72,10 +68,10 @@ describe('TruckAppointmentForm interaction contract', () => { expect(source).toContain("watch(() => props.resource?.id") }) - it('labels the internal cabinet, type and pickup-time physical number separately', () => { - expect(source).toContain('柜次号:${item.cabinetNo') - expect(source).toContain('柜型:${item.containerType') - expect(source).toContain("集装箱号:${item.containerNo || '待补录'}") + it('keeps container identity out of the booking-scoped form', () => { + expect(source).not.toContain('柜次号:') + expect(source).not.toContain('集装箱号:') + expect(source).toContain('真实柜次及集装箱号在货柜与预装流程维护') }) it('keeps the form usable at 390px', () => { diff --git a/src/modules/trucking/components/TruckAppointmentForm.vue b/src/modules/trucking/components/TruckAppointmentForm.vue index ea4b8192..b95d1247 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.vue +++ b/src/modules/trucking/components/TruckAppointmentForm.vue @@ -38,7 +38,6 @@ const rules: FormRules = { if (!draft.billOfLadingNo?.trim()) return callback(new Error('所选订舱缺少有效提单号')) return callback(bookingDocumentReady.value ? undefined : new Error('所选订舱缺少有效的订舱确认文件或提单文件')) }, trigger: 'change' }], - containerBookingId: [{ required: true, message: '请选择该订舱下可预约的柜次', trigger: 'change' }, { validator: (_rule, value, callback) => callback(selectableContainers.value.some((item) => item.containerBookingId === String(value)) || (relationLocked.value && selectedContainerFallback.value?.id === String(value)) ? undefined : new Error('请选择该订舱下仍可预约的柜次')), trigger: 'change' }], carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }, { validator: (_rule, value, callback) => callback(selectableCarriers.value.some((item) => String(item.id) === String(value)) ? undefined : new Error('请选择有效的合作中拖车供应商')), trigger: 'change' }], plannedLoadingAt: [{ required: true, message: '请选择预计装箱时间', trigger: 'change' }], fleetPhone: [phoneRule('车队电话')], driverPhone: [phoneRule('司机电话')], storePhone: [phoneRule('门店电话')], @@ -70,19 +69,6 @@ const selectedReleasedBooking = computed(() => bookings.value.find((item) => ite const bookingDocumentReady = computed(() => relationLocked.value ? bookingDocuments.value.some((document) => document.valid && ['BOOKING_CONFIRMATION', 'BILL_OF_LADING'].includes(document.documentType)) : Boolean(selectedReleasedBooking.value?.bookingDocumentReady)) -const selectableContainers = computed(() => candidates.value.filter((item) => item.oceanBookingId === draft.oceanBookingId)) -function containerOptionLabel(item: TruckAppointmentCandidate) { - return `柜次号:${item.cabinetNo || '待生成'} / 柜型:${item.containerType || '—'} / 集装箱号:${item.containerNo || '待补录'}` -} -const selectedContainerFallback = computed(() => { - const id = draft.containerBookingId - const snapshot = props.resource - if (!id || !relationLocked.value || String(snapshot?.containerBookingId) !== String(id)) return null - return { - id: String(id), - label: `柜次号:${snapshot?.cabinetNo || '待生成'} / 柜型:${snapshot?.relatedContainerType || '—'} / 集装箱号:${snapshot?.containerNo || '待补录'}(当前绑定,不可重选)`, - } -}) const selectedBookingFallback = computed(() => { const id = draft.oceanBookingId const snapshot = props.resource @@ -90,16 +76,6 @@ const selectedBookingFallback = computed(() => { return { id: String(id), label: `${snapshot?.oceanBookingNo || '订舱号缺失'}(当前绑定,不可重选)` } }) function selectCarrier(carrierId: string) { const carrier = selectableCarriers.value.find((item) => String(item.id) === carrierId); draft.carrierId = carrier?.id || null; draft.carrierName = carrier?.name || null } -function selectContainer(containerId: string) { - const item = selectableContainers.value.find((entry) => entry.containerBookingId === containerId) - draft.containerBookingId = item?.containerBookingId || null - relationWarning.value = '' - if (!item) { - draft.loadingPlanId = null - return - } - draft.loadingPlanId = item.loadingPlanId -} async function loadCurrentBookingContext(bookingId: string) { const sequence = ++bookingLoadSequence bookingContainersLoading.value = true @@ -127,8 +103,6 @@ async function selectBooking(bookingId: string) { bookingLoadSequence += 1 const item = bookings.value.find((entry) => entry.id === bookingId) draft.oceanBookingId = item?.id || null - draft.containerBookingId = null - draft.loadingPlanId = null bookingDocuments.value = [] relationWarning.value = '' if (!item) { @@ -155,15 +129,12 @@ async function loadOptions() { optionsLoading.value = false if (relationLocked.value && draft.oceanBookingId) await loadCurrentBookingContext(String(draft.oceanBookingId)) else if (draft.oceanBookingId) { - const candidate = candidates.value.find((item) => item.oceanBookingId === draft.oceanBookingId && item.containerBookingId === draft.containerBookingId) + const candidate = candidates.value.find((item) => item.oceanBookingId === draft.oceanBookingId) if (candidate) { draft.warehouseId = candidate.warehouseId draft.billOfLadingNo = candidate.billOfLadingNo - draft.loadingPlanId = candidate.loadingPlanId } else { draft.oceanBookingId = null - draft.containerBookingId = null - draft.loadingPlanId = null } } } @@ -177,13 +148,12 @@ watch(() => props.resource?.id, (id, previous) => { if (id && id !== previous) v