diff --git a/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts b/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts index efa2db57..1e49927f 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts +++ b/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts @@ -65,15 +65,16 @@ describe('ContainerDetailView empty return', () => { expect(value).toBe('2026-10-23(默认推算)') expect(value).not.toContain('00:00') }) - it('passes the original container to schedule maintenance and reloads its projection after save', async () => { - const wrapper = await mountDetail('DOMESTIC') - const panel = wrapper.findComponent({ name: 'ContainerPortSchedulePanel' }) - expect(panel.props('container').id).toBe('9001') - vi.mocked(getContainer).mockResolvedValue({ ...base, vesselName: 'REASSIGNED', voyageNo: 'V2', currentPortScheduleId: 'S1' } as never) - panel.vm.$emit('saved'); await flushPromises() - expect(getContainer).toHaveBeenCalledTimes(2) - expect(wrapper.text()).toContain('REASSIGNED / V2') - expect(wrapper.findComponent({ name: 'ContainerPortSchedulePanel' }).props('container').currentPortScheduleId).toBe('S1') + it.each(['DOMESTIC', 'OVERSEAS'] as const)('keeps voyage information without the temporary schedule maintenance entry in %s', async (workbench) => { + vi.mocked(getContainer).mockResolvedValue({ ...base, vesselName: 'VESSEL', voyageNo: 'V2', currentPortScheduleId: 'S1', estimatedSailingAt: '2026-11-05T10:55:00', estimatedPortArrivalAt: '2026-11-08T00:00:00' } as never) + const wrapper = await mountDetail(workbench) + const section = wrapper.get('[aria-labelledby="container-voyage-heading"]') + expect(section.text()).toContain('船名航次信息') + expect(section.text()).toContain('VESSEL / V2') + expect(section.text()).toContain('2026-11-05 10:55') + expect(section.text()).toContain('2026-11-08 00:00') + expect(wrapper.findComponent({ name: 'ContainerPortSchedulePanel' }).exists()).toBe(false) + expect(section.text()).not.toContain('船期维护与历史') }) beforeEach(() => { routeState.query = {} diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue index 2aa81799..00abb91d 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.vue +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -16,7 +16,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import StatusBadge from '@/shared/components/StatusBadge.vue' -import { CarrierScheduleQueryPanel, ContainerPortSchedulePanel } from '@/modules/port-schedules' +import { CarrierScheduleQueryPanel } from '@/modules/port-schedules' import { ContainerFeesPanel, useLoadingListDetail, LoadingListDetailDrawer, PalletLabelPrintDialog, type LoadingListMode } from '@/modules/loading-lists' import { getContainerOutboundDispatch, @@ -648,7 +648,7 @@ onMounted(async () => {
-

船名航次信息

+

船名航次信息

船公司
{{ detail.shippingCompanyName || '—' }}
船名 / 航次
{{ [detail.vesselName, detail.voyageNo].filter(Boolean).join(' / ') || '—' }}
预计开船
{{ formatTime(detail.estimatedSailingAt) }}
预计到港
{{ formatTime(detail.estimatedPortArrivalAt) }}
diff --git a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md index 5fea9003..d5eeff94 100644 --- a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md +++ b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md @@ -476,3 +476,13 @@ 2026-10-29 用户截图调整:排车列表查看路线更名查看轨迹。已完成本土卡车详情补充货物数量卡片、起讫地址与联系人、司机/车辆及证照照片、实际发车和完成时间。维护车辆/司机候选排除其他车次实际占用,当前车次已选资源允许保留。尾程查询占位提示字体12px。自动检查与人工视觉验收分别记录;待人工1920×1080、100%截图确认。 + +## 2026-11-05 PDA拆柜收货与维护车辆问题修复 + +| 页面/问题 | 修复 | 验收 | +|---|---|---| +| PDA拆柜收货按钮重叠 | 底部操作区改为随内容正常排列,解析、校验、保存及结束按钮全部保留;照片上传后返回页面不再清理同一签收会话的未签收草稿。 | PDA定向35项通过,App资源构建通过;真实设备截图待确认 | +| 维护车辆字段与附件 | 移除不可维护的发车时间及主档拖车公司展示;保留可编辑的外配拖车公司字段;预约号回显已登记编号;驾驶证展示优先本趟上传文件,避免保存后重复主档或空白图。 | Admin定向67项通过,修改文件Scoped TypeScript检查通过;真实页面截图待确认 | +| 后端预约号 | 已登记预约号可回显,只有有效收货确认提供确认时间。 | Lastleg定向8项通过;未部署 | + +本轮修改未部署到测试或生产环境;需在真实PDA和Admin页面进行人工截图及业务数据验收。 diff --git a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts index be4e285a..2c6dc790 100644 --- a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.test.ts @@ -23,13 +23,33 @@ it.each(['VEHICLE', 'DRIVER_PHOTO'] as const)('does not count a PDF or failed up expect(wrapper.emitted('busy')?.at(-1)).toEqual([false]) wrapper.unmount() }) -it('previews an unsaved master license through its driver and keeps uploaded license files on the dispatch route', async () => { +it('uses the uploaded license before the master fallback and restores the fallback after removal', async () => { vi.mocked(downloadDriverLicense).mockResolvedValue(new Blob(['png'], { type: 'image/png' })) const wrapper = mount(DispatchEvidenceFiles, { ...options, props: { ...options.props, kind: 'DRIVER', photoCard: true, modelValue: ['301'], referenceIds: ['33'], masterLicense: { driverId: '2', fileId: '33' } } }); await flushPromises() - expect(downloadDriverLicense).toHaveBeenCalledWith('2', '33') + expect(downloadDriverLicense).not.toHaveBeenCalled() expect(api.getDispatchEvidence).toHaveBeenCalledWith('PC/1', 'DRIVER', '301') expect(api.getDispatchEvidence).not.toHaveBeenCalledWith('PC/1', 'DRIVER', '33') - expect(wrapper.findAll('figure img')).toHaveLength(2); wrapper.unmount() + expect(wrapper.findAll('figure img')).toHaveLength(1) + await wrapper.setProps({ modelValue: [] }); await flushPromises() + expect(downloadDriverLicense).toHaveBeenCalledWith('2', '33') + expect(wrapper.findAll('figure img')).toHaveLength(1); wrapper.unmount() +}) +it('keeps saved trip licenses without an additional master image or changes to stored references', async () => { + const wrapper = mount(DispatchEvidenceFiles, { ...options, props: { ...options.props, kind: 'DRIVER', photoCard: true, modelValue: ['33', '301'], referenceIds: ['33'], fallbackFileId: '33' } }); await flushPromises() + expect(wrapper.findAll('figure img')).toHaveLength(1) + expect(api.getDispatchEvidence).not.toHaveBeenCalledWith('PC/1', 'DRIVER', '33') + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + wrapper.unmount() +}) +it.each(['tiny', 'broken'])('shows a recovery message instead of a blank %s thumbnail', async failure => { + const wrapper = mount(DispatchEvidenceFiles, { ...options, props: { ...options.props, photoCard: true } }); await flushPromises() + const image = wrapper.get('figure img') + Object.defineProperties(image.element, { naturalWidth: { value: 1 }, naturalHeight: { value: 1 } }) + await image.trigger(failure === 'tiny' ? 'load' : 'error') + expect(wrapper.find('figure img').exists()).toBe(false) + expect(wrapper.text()).toContain(failure === 'tiny' ? '原图仅 1 × 1 像素' : '照片加载失败') + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + wrapper.unmount() }) async function selectFile(wrapper: ReturnType, file: File) { const input = wrapper.get('input[type="file"]'); Object.defineProperty(input.element, 'files', { configurable: true, value: [file] }); await input.trigger('change'); await flushPromises() } beforeEach(() => { vi.resetAllMocks(); vi.mocked(api.getDispatchEvidence).mockResolvedValue(metadata()); vi.mocked(api.uploadDispatchEvidence).mockResolvedValue(metadata('302')); vi.mocked(api.getDispatchEvidenceContent).mockResolvedValue(new Blob(['png'], { type: 'image/png' })); URL.createObjectURL = vi.fn().mockReturnValue('blob:authenticated'); URL.revokeObjectURL = vi.fn() }) diff --git a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue index 61c63c57..908c48b3 100644 --- a/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue +++ b/src/modules/overseas-warehouse/components/DispatchEvidenceFiles.vue @@ -11,14 +11,21 @@ import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/sh import { getDispatchEvidence, getDispatchEvidenceContent, uploadDispatchEvidence } from '../api/dispatches' import { downloadDriverLicense, getResourcePhoto, downloadResourcePhoto, type ResourcePhotoKind } from '@/modules/overseas-master-data' import { DispatchInputError, type DispatchEvidenceFile, type DispatchEvidenceKind } from '../dispatchContracts' -const props = defineProps<{ dispatchNo: string; kind: DispatchEvidenceKind; disabled?: boolean; readonly?: boolean; photoCard?: boolean; label?: string; saveHint?: string; referenceIds?: string[]; masterResource?: { kind: ResourcePhotoKind; id: string } | null; masterLicense?: { driverId: string; fileId: string } | null }>() +const props = defineProps<{ dispatchNo: string; kind: DispatchEvidenceKind; disabled?: boolean; readonly?: boolean; photoCard?: boolean; label?: string; saveHint?: string; referenceIds?: string[]; fallbackFileId?: string | null; masterResource?: { kind: ResourcePhotoKind; id: string } | null; masterLicense?: { driverId: string; fileId: string } | null }>() const ids = defineModel({ required: true }) const profilePhotoId = ref(null) -const allIds = computed(() => [...new Set([...(props.referenceIds || []), ...ids.value, ...(profilePhotoId.value ? [profilePhotoId.value] : [])])]) +// 主档仅作缺图时的默认来源;已有本趟驾驶证时不再额外展示主档图片。 +const selectedIds = computed(() => { + const values = [...new Set([...(props.referenceIds || []), ...ids.value])] + const fallback = props.fallbackFileId || props.masterLicense?.fileId + return props.kind === 'DRIVER' && fallback && values.some(id => id !== fallback) ? values.filter(id => id !== fallback) : values +}) +const allIds = computed(() => [...new Set([...selectedIds.value, ...(profilePhotoId.value ? [profilePhotoId.value] : [])])]) const emit = defineEmits<{ busy: [value: boolean]; available: [value: boolean] }>() const metadata = ref>({}), error = ref(null), uploading = ref(false), input = ref(null) const imageOpen = ref(false), imageUrl = ref(''), imageName = ref(''), previewLoading = ref(false) const thumbnails = ref>({}) +const thumbnailFailures = ref>({}) let masterBlob: { fileId: string; blob: Blob } | null = null const previewId = ref(''), imageDecoding = ref(false), imageFailure = ref('') const previewBusy = computed(() => previewLoading.value || imageDecoding.value) @@ -45,9 +52,9 @@ function imageFailed(event: Event) { imageFailure.value = '图片无法解码或加载,请重新预览;若仍失败,请核对原附件是否损坏。' } async function load() { - const current = ++generation, no = props.dispatchNo, kind = props.kind, values = [...new Set([...(props.referenceIds || []), ...ids.value])] + const current = ++generation, no = props.dispatchNo, kind = props.kind, values = [...selectedIds.value] for (const url of Object.values(thumbnails.value)) { URL.revokeObjectURL(url); urls.delete(url) } - thumbnails.value = {} + thumbnails.value = {}; thumbnailFailures.value = {} closePreview(); metadata.value = {}; error.value = null; masterBlob = null; profilePhotoId.value = null; emit('available', false) if (!no) return // 未保存且没有本趟照片时展示档案图片;保存由服务端复制,不提交跨归属文件ID。 @@ -89,6 +96,14 @@ async function load() { } catch (cause) { if (current === generation) fail(cause, '照片读取失败,请重试') } })) } +function thumbnailLoaded(id: string, event: Event) { + const image = event.target as HTMLImageElement + if (image.getAttribute('src') !== thumbnails.value[id]) return + if (image.naturalWidth <= 1 && image.naturalHeight <= 1) thumbnailFailures.value[id] = '原图仅 1 × 1 像素,请替换有效照片' +} +function thumbnailFailed(id: string, event: Event) { + if ((event.target as HTMLImageElement).getAttribute('src') === thumbnails.value[id]) thumbnailFailures.value[id] = '照片加载失败,请重新读取或替换' +} function choose(id: string | null = null) { if (props.readonly || props.disabled || uploading.value || !props.dispatchNo) return; replacing = id; input.value?.click() } async function upload(event: Event) { const element = event.target as HTMLInputElement, file = element.files?.[0]; element.value = '' @@ -133,7 +148,7 @@ async function preview(id: string) { } catch (cause) { if (current === previewGeneration) { closePreview(); fail(cause, '本车附件预览失败,原引用已保留') } } finally { if (current === previewGeneration) previewLoading.value = false } } -watch(() => JSON.stringify([props.dispatchNo, props.kind, ids.value, props.referenceIds, props.masterLicense, props.masterResource]), load, { immediate: true }) +watch(() => JSON.stringify([props.dispatchNo, props.kind, ids.value, props.referenceIds, props.fallbackFileId, props.masterLicense, props.masterResource]), load, { immediate: true }) onBeforeUnmount(() => { generation++; closePreview(); for (const url of urls) URL.revokeObjectURL(url); urls.clear(); emit('busy', false) }) - diff --git a/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue b/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue index 57794491..0a76d002 100644 --- a/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue +++ b/src/modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue @@ -31,7 +31,7 @@ function confirmedAt(appointment: DispatchDestinationAppointment) {

车辆与司机

-
+
diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts index ab69481c..8996d019 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts @@ -151,7 +151,7 @@ describe('dispatch workflow native layout', () => { const { dialog } = await render('EDIT', undefined, false, { dispatchInfo: { ...source.dispatchInfo, departedAt: '2026-07-01T15:00:00Z', plannedDepartureAt: '2026-07-01T16:00:00Z', estimatedArrivalAt: '2026-07-01T17:00:00Z', } }) - expect(field(dialog, '发车时间').get('[aria-label="发车时间时区换算"]').text()).toBe('美东2026-07-01 11:00:00美西2026-07-01 08:00:00') + expect(field(dialog, '发车时间')).toBeUndefined() expect(field(dialog, '计划发车时间').get('[aria-label="计划发车时间时区换算"]').text()).toBe('美东2026-07-01 12:00:00美西2026-07-01 09:00:00') expect(field(dialog, '预计到达时间').get('[aria-label="预计到达时间时区换算"]').text()).toBe('美东2026-07-01 13:00:00美西2026-07-01 10:00:00') expect(field(dialog, '预计任务结束').get('[aria-label="预计任务结束时区换算"]').text()).toBe('美东2026-11-01 05:00:00美西2026-11-01 02:00:00') @@ -178,7 +178,7 @@ describe('dispatch workflow native layout', () => { for (const [label, value] of [['派送方式', '外配'], ['提货方式', '一提多卸'], ['装车类型', '混装'], ['车辆类型', '厢式货车']]) field(dialog, label!).getComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', value) await field(dialog, '车队邮箱').get('input').setValue(' fleet@example.test ') await field(dialog, '预约账号').get('input').setValue(' fleet-account ') - expect(field(dialog, '发车时间').get('input').attributes('readonly')).toBeDefined() + expect(field(dialog, '发车时间')).toBeUndefined() expect(dialog.find('textarea[aria-label="本次操作备注"]').exists()).toBe(false) await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() dialog.vm.$emit('confirm'); await flushPromises() @@ -202,7 +202,7 @@ describe('dispatch workflow native layout', () => { await field(dialog,'车牌号').get('input').setValue('CA-PRESERVED') dialog.vm.$emit('confirm');await flushPromises() const original=structuredClone(vi.mocked(api.updateDispatch).mock.calls[0]![1]) - expect(dialog.text()).toContain('workflow-request') + expect((wrapper.vm as unknown as { error: { requestId: string } }).error.requestId).toBe('workflow-request') expect(field(dialog,'车牌号').get('input').attributes('disabled')).toBeUndefined() expect((field(dialog,'车牌号').get('input').element as HTMLInputElement).value).toBe('CA-PRESERVED') await field(dialog,'车牌号').get('input').setValue('CA-CHANGED') diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue index 1c2ab435..073f3a3e 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue @@ -275,7 +275,7 @@ watch([open, () => props.dispatchNo, () => props.action, () => props.operation],
- +
diff --git a/src/modules/overseas-warehouse/pages/InboundDialogs.behavior.test.ts b/src/modules/overseas-warehouse/pages/InboundDialogs.behavior.test.ts index 7f8d3b5d..20c24827 100644 --- a/src/modules/overseas-warehouse/pages/InboundDialogs.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/InboundDialogs.behavior.test.ts @@ -57,7 +57,6 @@ beforeEach(() => { }) async function listPage() { const wrapper = mount(InboundOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } }); await flushPromises(); return wrapper } function popup(wrapper: VueWrapper, title: string) { return wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('title').startsWith(title))! } -async function openEditPlan() { const wrapper = await listPage(); await wrapper.findAll('.el-table__body-wrapper button').find(button => button.text() === '编辑')!.trigger('click'); await flushPromises(); return { wrapper, dialog: popup(wrapper, '编辑入库单') } } async function openEditAppointment() { const wrapper = await listPage(); await wrapper.findAll('.el-table__body-wrapper button').find(button => button.text() === '预约')!.trigger('click'); await flushPromises(); return { wrapper, dialog: popup(wrapper, '编辑预约') } } async function openMove() { const wrapper = mount(InboundOrderDetailView, { attachTo: document.body, global: { plugins: [ElementPlus] } }); await flushPromises(); await wrapper.findAll('.el-table__body-wrapper button').find(button => button.text() === '移库')!.trigger('click'); await flushPromises(); return { wrapper, dialog: popup(wrapper, '托盘移库') } } @@ -140,47 +139,17 @@ describe('inbound appointment native dialogs', () => { }) describe('inbound plan and pallet move native dialogs', () => { - it('hides plan editing for first-leg generated inbound orders', async () => { - vi.mocked(listInboundOrders).mockResolvedValue({ records: [{ ...order, orderType: 'FIRST_LEG' }], total: 1, page: 1, size: 10, pages: 1 }) + it.each(['ONE_CLICK_DROPSHIP', 'FIRST_LEG', 'RELABEL_APPLICATION', 'WAREHOUSE_TRANSFER', 'RETURN_RELABEL', 'DELIVERY_RETURN', 'LOCAL_LOOSEBOARD', 'CARGO_DISCARD', 'REJECTED', 'EXPRESS', 'FUTURE_TYPE', null])('hides plan editing for inbound order type %s even with edit permission', async (orderType) => { + vi.mocked(listInboundOrders).mockResolvedValue({ records: [{ ...order, orderType }], total: 1, page: 1, size: 10, pages: 1 }) const wrapper = await listPage() - expect(wrapper.findAll('.el-table__body-wrapper button').map(button => button.text())).not.toContain('编辑') - expect(wrapper.findAll('.el-table__body-wrapper button').map(button => button.text())).toContain('查看') + const actions = wrapper.findAll('.el-table__body-wrapper button').map(button => button.text()) + expect(actions).not.toContain('编辑') + expect(actions).toContain('查看') + expect(actions).toContain('查看预约') + expect(actions).toContain('预约') + expect(wrapper.findAllComponents(AppDialog).some(dialog => dialog.props('title').startsWith('编辑入库单'))).toBe(false) expect(listReceivingOverseasWarehouses).not.toHaveBeenCalled() - }) - it('shows plan identity and saves the selected master warehouse with the internal version through fullscreen', async () => { - const { dialog } = await openEditPlan() - expect(dialog.get('[aria-label="入库计划来源"]').findAll('input')).toHaveLength(0) - expect(dialog.get('[aria-label="入库计划来源"]').text()).toContain('入库单号') - expect(dialog.get('[aria-label="入库计划来源"]').text()).not.toContain('版本') - const select = dialog.findComponent({ name: 'ElSelect' }) - select.vm.$emit('update:modelValue', warehouse.id); select.vm.$emit('change', warehouse.id); await flushPromises() - await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() - dialog.vm.$emit('confirm'); await flushPromises() - expect(updateInboundPlan).toHaveBeenCalledExactlyOnceWith('IP-1', { customerId: 'C-1', overseasWarehouseId: warehouse.id, warehouseCode: 'LA2', warehouseName: '洛杉矶二仓', version: 0 }) - }) - it('locks a pending plan edit and keeps the warehouse and diagnostic context after failure', async () => { - let reject!: (reason: unknown) => void - vi.mocked(updateInboundPlan).mockReturnValueOnce(new Promise((_, no) => { reject = no })) - const { dialog } = await openEditPlan() - const select = dialog.findComponent({ name: 'ElSelect' }) - select.vm.$emit('update:modelValue', warehouse.id); select.vm.$emit('change', warehouse.id); await flushPromises() - dialog.vm.$emit('confirm'); dialog.vm.$emit('confirm'); await flushPromises() - expect(updateInboundPlan).toHaveBeenCalledTimes(1) - expect(dialog.findComponent({ name: 'ElDialog' }).props()).toMatchObject({ showClose: false, closeOnPressEscape: false }) - expect(select.find('input').attributes('disabled')).toBeDefined() - reject(new ApiResponseError('版本已变化', 'A0409', 409, null, 'plan-popup-request')); await flushPromises() - expect(dialog.props('modelValue')).toBe(true) - expect(select.props('modelValue')).toBe(warehouse.id) - expect(dialog.text()).toContain('plan-popup-request') - }) - it('does not save while the plan warehouse candidates are loading', async () => { - let resolve!: (value: Awaited>) => void - vi.mocked(listReceivingOverseasWarehouses).mockReturnValue(new Promise(done => { resolve = done })) - const { dialog } = await openEditPlan() - expect(dialog.props('loading')).toBe(true) - dialog.vm.$emit('confirm'); await flushPromises(); expect(updateInboundPlan).not.toHaveBeenCalled() - resolve({ records: [warehouse], page: 1, size: 200, total: 1, pages: 1 }); await flushPromises() - expect(dialog.props('loading')).toBe(false) + expect(updateInboundPlan).not.toHaveBeenCalled() }) it('loads only physical usable locations and preserves a valid zero pallet version in the command', async () => { const { dialog } = await openMove() diff --git a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue index 7a8d6d72..f2e3ebe0 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue @@ -19,8 +19,7 @@ import SmartDispatchDialog from '../components/SmartDispatchDialog.vue' import FbaAppointmentFields from '../components/FbaAppointmentFields.vue' import type { PlanningSourceLookup } from '@/modules/appointment-calendar' import { createAppointmentForm, appointmentPayload, appointmentFormError } from '../appointmentForm' -import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment, updateInboundPlan, type InboundOrderSummaryDto } from '../api/inboundOrders' -import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets' +import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment, type InboundOrderSummaryDto } from '../api/inboundOrders' import { overseasWarehousePermissions as permission } from '../permissions' import { formatOverseasDateTime, inboundPlanStatusLabel, lastMileOrderTypeLabel } from '../lastMilePresentation' import type { InboundOrderListItem } from '../types' @@ -68,7 +67,6 @@ const appointmentViewOnly = ref(false) const appointmentReplacementNos = ref([]) const appointmentReadOnly = computed(() => appointmentViewOnly.value || !access.can(permission.inboundOrderAppointment)) const appointmentTitle = computed(() => `${appointmentReadOnly.value ? '查看预约' : appointmentMode.value === 'edit' ? '编辑预约' : '预约'} · ${editingAppointmentNo.value || `${appointmentSelectedCount.value} 条入库单`}`) -const planEditOpen = ref(false); const planEditLoading = ref(false); const planEditSaving = ref(false); const planEditTarget = ref(null); const planEditForm = reactive({ warehouseId: '', warehouseCode: '', warehouseName: '', version: 0 }); const warehouses = ref([]) function selectableRow(source: object) { const row = source as InboundOrderListItem @@ -234,9 +232,6 @@ async function load(model: QueryModel = filters.value) { function search(model: QueryModel) { page.value = 1; void load(model) } function reset(model: QueryModel) { page.value = 1; void load(model) } function openDetail(source: object) { const row = source as InboundOrderListItem; void router.push({ name: 'overseas-inbound-order-detail', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) } -function canEditPlan(row: InboundOrderListItem) { return access.can(permission.inboundOrderEdit) && row.status === '待入库' && row.type !== '头程订单' } -async function openPlanEdit(source: object) { const row = source as InboundOrderListItem; if (!canEditPlan(row)) return; planEditTarget.value = row; planEditOpen.value = true; planEditLoading.value = true; mutationError.value = null; try { Object.assign(planEditForm, { warehouseId: row.warehouseId, warehouseCode: row.destinationWarehouseCode === '—' ? '' : row.destinationWarehouseCode, warehouseName: row.destinationWarehouseName === '—' ? '' : row.destinationWarehouseName, version: row.version }); if (!warehouses.value.length) warehouses.value = (await listReceivingOverseasWarehouses()).records } catch (error) { planEditOpen.value = false; mutationError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') } finally { planEditLoading.value = false } } -async function submitPlanEdit() { if (planEditSaving.value || planEditLoading.value || !access.can(permission.inboundOrderEdit)) return; const target = planEditTarget.value; if (!target || !canEditPlan(target)) return; if (!planEditForm.warehouseId) { ElMessage.warning('请选择目的仓'); return } planEditSaving.value = true; mutationError.value = null; try { await updateInboundPlan(target.orderNo, { customerId: target.customerId, overseasWarehouseId: planEditForm.warehouseId, warehouseCode: planEditForm.warehouseCode, warehouseName: planEditForm.warehouseName, version: planEditForm.version }); planEditOpen.value = false; ElMessage.success('入库单已更新'); await load() } catch (error) { mutationError.value = getApiErrorDetails(error, '入库单更新失败,请重试') } finally { planEditSaving.value = false } } onMounted(() => { void load() }) @@ -251,7 +246,7 @@ onMounted(() => { void load() }) - +
@@ -264,11 +259,6 @@ onMounted(() => { void load() }) -
-

计划信息

入库单号{{ planEditTarget?.orderNo || '—' }}
- -

目的仓登记

-
diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index 714a1000..a604e043 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -297,7 +297,7 @@ const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 330, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left' }, { key: 'orderNo', prop: 'orderNo', label: '尾程单号', minWidth: 170, slotName: 'orderNo' }, - { key: 'dispatchStatus', prop: 'dispatchStatus', label: '排车状态', width: 110, afterColumn: 'orderNo' }, + { key: 'dispatchStatus', prop: 'dispatchStatus', label: '出库排车状态', width: 110, afterColumn: 'orderNo' }, { key: 'relabelStatus', slotName: 'relabelStatus', label: '换标子状态', minWidth: 180, afterColumn: 'orderNo' }, { key: 'status', prop: 'status', label: '当前状态', width: 110, afterColumn: 'orderNo' }, { key: 'abnormal', prop: 'abnormal', label: '是否异常', width: 100, afterColumn: 'orderNo' },