From 86ba48c415354e1e60c64039d67fe23df77e9e81 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 7 Sep 2026 20:25:14 +0800 Subject: [PATCH] fix(overseas): harden receiving warehouse change --- .../pages/LastMileOrdersView.behavior.test.ts | 50 ++++++++++++++++++- .../pages/LastMileOrdersView.vue | 36 +++++++------ 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index c3cda92f..68a4e37a 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -1,9 +1,12 @@ import ElementPlus from 'element-plus' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { createPinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import DataTable from '@/shared/components/DataTable.vue' +import AppDialog from '@/shared/components/AppDialog.vue' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import { listAllPlatformWarehouses } from '@/modules/warehouses' import { @@ -61,6 +64,10 @@ const receivingWarehouse = { ...fbaWarehouse, id: '9007199254740996', warehouseCode: 'LAX02', warehouseName: '洛杉矶二号接柜仓', warehouseType: 'OVERSEAS', } +const currentReceivingWarehouse = { + ...receivingWarehouse, + id: summary.overseasWarehouseId, warehouseCode: summary.overseasWarehouseCode, warehouseName: summary.overseasWarehouseName, +} function selectedRow(overrides: Record = {}) { return { id: '2092873463568596994', customerId, sourceContainerBookingId: bookingId, orderNo: 'LMO-260827-0001', ...overrides } } @@ -309,24 +316,55 @@ describe('LastMileOrdersView pending order and container conversion', () => { }) it('shows the receiving warehouse and changes it from ACTIVE OVERSEAS master data with versioning', async () => { - vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([receivingWarehouse] as never) + vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([currentReceivingWarehouse, receivingWarehouse] as never) const wrapper = await mountView() await wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子'))!.trigger('click') await flushPromises() expect(wrapper.text()).toContain('海外接柜仓') expect(wrapper.text()).toContain('LAX01 · 洛杉矶接柜仓') + expect(wrapper.get('.container-workbench-card__warehouse strong').attributes('title')).toBe('LAX01 · 洛杉矶接柜仓') + expect(listAllPlatformWarehouses).not.toHaveBeenCalled() await button(wrapper, '修改接柜仓')!.trigger('click') await flushPromises() + const receivingDialog = wrapper.findAllComponents(AppDialog).find((item) => item.props('title') === '修改海外接柜仓')! + expect(receivingDialog.props('width')).toBe('min(600px, calc(100vw - 28px))') expect(listAllPlatformWarehouses).toHaveBeenLastCalledWith({ warehouseType: 'OVERSEAS', status: 'ACTIVE' }) + expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1) const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择 ACTIVE 海外接柜仓')! + expect(warehouseSelect.props('modelValue')).toBe('') + expect(wrapper.text()).not.toContain('9007199254740995') + const warehouseOptions = wrapper.findAllComponents({ name: 'ElOption' }) + expect(warehouseOptions.find((item) => item.props('value') === summary.overseasWarehouseId)?.props('disabled')).toBe(true) + const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement + warehouseSelect.vm.$emit('update:modelValue', summary.overseasWarehouseId) + await wrapper.vm.$nextTick() + expect(confirm.disabled).toBe(true) + expect(wrapper.text()).toContain('新的海外接柜仓必须与当前仓不同') warehouseSelect.vm.$emit('update:modelValue', '9007199254740996') await wrapper.vm.$nextTick() - const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement + expect(confirm.disabled).toBe(false) confirm.click() await flushPromises() expect(changeContainerReceivingWarehouse).toHaveBeenCalledWith(bookingId, { overseasWarehouseId: '9007199254740996', expectedVersion: 7, remark: undefined, }, expect.stringMatching(/^container-receiving-warehouse-/)) + expect(listContainerPlanProgress).toHaveBeenCalledTimes(2) + }) + + it('keeps the receiving-warehouse confirmation disabled while candidates are loading', async () => { + let resolveCandidates!: (value: typeof receivingWarehouse[]) => void + vi.mocked(listAllPlatformWarehouses).mockReturnValueOnce(new Promise((resolve) => { resolveCandidates = resolve }) as never) + const wrapper = await mountView() + await wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子'))!.trigger('click') + await flushPromises() + await button(wrapper, '修改接柜仓')!.trigger('click') + await wrapper.vm.$nextTick() + const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement + expect(confirm.disabled).toBe(true) + expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1) + resolveCandidates([receivingWarehouse]) + await flushPromises() + expect(confirm.disabled).toBe(true) }) it('does not expose a receiving-warehouse edit when the server locks the container', async () => { @@ -390,6 +428,14 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect(changeContainerReceivingWarehouse).not.toHaveBeenCalled() }) + it('keeps dense cabinet fields and actions on one line with ellipsis at 1920px', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/LastMileOrdersView.vue'), 'utf8') + expect(source).toContain('.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }') + expect(source).toContain('.container-workbench-card__warehouse strong { min-width: 0; overflow: hidden; color: var(--cc-text); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }') + expect(source).toContain('.container-workbench-card__actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; white-space: nowrap; }') + expect(source).toContain("{ key: 'actions', label: '操作', width: 110, fixed: 'left'") + }) + it('loads only active FBA master data and submits exact string fields for a cross-customer transfer', async () => { const wrapper = await openPending() await selectRows(wrapper, [selectedRow(), selectedRow({ id: '2092873463568596995', customerId: '2092618717861048322' })]) diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index 482767bb..d5a11868 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -90,6 +90,10 @@ const fields = computed(() => statusTab.value === '柜子' ? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key)) : allFields) const selectedContainer = computed(() => containerOptions.value.find((row) => row.sourceContainerBookingId === selectedContainerBookingId.value)) +const selectedReceivingWarehouse = computed(() => receivingWarehouseOptions.value.find((row) => ( + row.warehouseId === receivingWarehouseForm.warehouseId + && row.warehouseId !== receivingWarehouseTarget.value?.overseasWarehouseId +))) interface TransferWarehouseOption { warehouseId: string @@ -304,7 +308,7 @@ async function loadReceivingWarehouseOptions() { function openReceivingWarehouseChange(container: ContainerPlanProgressSummaryDto) { if (!container.canChangeReceivingWarehouse) return receivingWarehouseTarget.value = container - Object.assign(receivingWarehouseForm, { warehouseId: container.overseasWarehouseId || '', remark: '' }) + Object.assign(receivingWarehouseForm, { warehouseId: '', remark: '' }) receivingWarehouseIdempotencyKey = newReceivingWarehouseIdempotencyKey() receivingWarehouseOptions.value = [] receivingWarehouseError.value = null @@ -315,13 +319,14 @@ function openReceivingWarehouseChange(container: ContainerPlanProgressSummaryDto } async function submitReceivingWarehouseChange() { const target = receivingWarehouseTarget.value - const selected = receivingWarehouseOptions.value.find((warehouse) => warehouse.warehouseId === receivingWarehouseForm.warehouseId) + const selected = selectedReceivingWarehouse.value if (!target || !selected) { - ElMessage.warning('请选择有效的海外接柜仓') + ElMessage.warning('请选择与当前仓不同的有效海外接柜仓') return } actionLoading.value = true actionError.value = null + receivingWarehouseActionError.value = null try { await changeContainerReceivingWarehouse(target.sourceContainerBookingId, { overseasWarehouseId: selected.warehouseId, @@ -508,11 +513,11 @@ onMounted(() => { void load() })
- {{ container.containerNo || '柜号待补充' }} + {{ container.containerNo || '柜号待补充' }} - 海外接柜仓{{ receivingWarehouseLabel(container) }} - {{ containerSnapshotBlockReason(container) }} - {{ container.receivingWarehouseLockReason }} + 海外接柜仓{{ receivingWarehouseLabel(container) }} + {{ containerSnapshotBlockReason(container) }} + {{ container.receivingWarehouseLockReason }} 计划到仓{{ formatDateTime(container.plannedArrivalAt) }} @@ -576,7 +581,7 @@ onMounted(() => { void load() }) - + @@ -596,9 +601,10 @@ onMounted(() => { void load() }) - + + @@ -613,6 +619,7 @@ onMounted(() => { void load() }) .last-mile-toolbar :deep(.query-toolbar) { margin-top: 12px; padding: 12px 0 16px; border: 0; border-top: 1px solid var(--cc-border); border-radius: 0; } .container-convert-hint { margin: 0 0 16px; color: var(--cc-muted); line-height: 1.6; } .container-detail-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 16px; margin-bottom: 12px; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: 6px; background: var(--cc-surface); font-size: 13px; } +.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .container-workbench { padding: 18px; } .container-workbench__header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; } .container-workbench__header > div { display: grid; gap: 4px; } @@ -622,21 +629,22 @@ onMounted(() => { void load() }) .container-workbench-card { display: grid; min-width: 0; gap: 14px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; color: inherit; font: inherit; text-align: left; transition: border-color .16s ease, box-shadow .16s ease; } .container-workbench-card:hover { border-color: var(--cc-primary); box-shadow: 0 6px 18px rgb(15 23 42 / 8%); } .container-workbench-card__top { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.container-workbench-card__top strong { min-width: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 16px; } +.container-workbench-card__top strong { min-width: 0; overflow: hidden; color: var(--cc-text); font-size: 16px; text-overflow: ellipsis; white-space: nowrap; } .container-workbench-card__top > span { flex: 0 0 auto; color: var(--cc-primary); font-size: 13px; } .container-workbench-card__arrival { display: grid; gap: 4px; } .container-workbench-card__arrival small { color: var(--cc-muted); font-size: 12px; } .container-workbench-card__arrival strong { color: var(--cc-text); font-size: 15px; } .container-workbench-card__warehouse { display: grid; gap: 4px; } .container-workbench-card__warehouse small { color: var(--cc-muted); font-size: 12px; } -.container-workbench-card__warehouse strong { overflow-wrap: anywhere; color: var(--cc-text); font-size: 14px; } -.container-workbench-card__warehouse .is-locked { color: #c2410c; } +.container-workbench-card__warehouse strong { min-width: 0; overflow: hidden; color: var(--cc-text); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; } +.container-workbench-card__warehouse .is-locked { min-width: 0; overflow: hidden; color: #c2410c; text-overflow: ellipsis; white-space: nowrap; } .container-workbench-card__pending { display: flex; align-items: baseline; gap: 8px; padding: 12px 14px; border-left: 3px solid #f59e0b; background: #fffaf0; } .container-workbench-card__pending strong { color: #b45309; font-size: 28px; line-height: 1; } .container-workbench-card__pending span { color: #92400e; font-size: 13px; } .container-workbench-card__facts { display: flex; flex-wrap: wrap; gap: 6px 12px; color: var(--cc-muted); font-size: 12px; } .container-workbench-card__facts .is-warning { color: #c2410c; } -.container-workbench-card__actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; } +.container-workbench-card__actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; white-space: nowrap; } +.container-workbench-card__actions :deep(.el-button + .el-button) { margin-left: 0; } .container-workbench__pagination { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--cc-border); color: var(--cc-muted); font-size: 13px; } @media (max-width: 1380px) { .container-workbench__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }