diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index f43cde9a..8a8b2567 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -27,6 +27,7 @@ export interface LastMileOrderQuery { orderNo?: string relabelNo?: string orderType?: string + sailingStage?: 'BEFORE_DEPARTURE' | 'AFTER_DEPARTURE' inboundCode?: string lxCode?: string containerNo?: string diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index 60002795..15119a87 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -12,6 +12,7 @@ import { listAllPlatformWarehouses } from '@/modules/warehouses' import { changeContainerReceivingWarehouse, createManualInboundPlan, + downloadLastMileOrders, convertLastMileContainerToInboundPlans, getContainerInboundPlanPreview, getContainerPlanProgress, @@ -27,6 +28,7 @@ import LastMileOrdersView from './LastMileOrdersView.vue' const canAccess = vi.hoisted(() => vi.fn<(permissionCode: string) => boolean>(() => true)) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canAccess }) })) +vi.mock('@/shared/downloadCsv', () => ({ saveCsv: vi.fn() })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() })) vi.mock('../api/lastMileOrders', () => ({ @@ -140,7 +142,7 @@ async function mountView() { async function openPending() { const wrapper = await mountView() - const pendingTab = wrapper.findAll('[role="tab"]').find((item) => item.text() === '待分配') + const pendingTab = wrapper.findAll('[role="tab"]').find((item) => item.text() === '开船前') await pendingTab!.trigger('click') await flushPromises() return wrapper @@ -148,7 +150,7 @@ async function openPending() { async function openHiddenContainerWorkbench(wrapper: Awaited>) { const view = wrapper.vm as unknown as { - statusTab: '全部' | '待分配' | '柜子' + statusTab: '全部' | '开船前' | '开船后' | '柜子' changeStatusTab(): void } view.statusTab = '柜子' @@ -187,7 +189,7 @@ describe('LastMileOrdersView pending order and container conversion', () => { it('supports keyboard status switching and keeps selection and queries in sync', async () => { const wrapper = await mountView() const tabs = wrapper.findAll('[role="tab"]') - expect(tabs.map((tab) => tab.text())).toEqual(['全部', '待分配']) + expect(tabs.map((tab) => tab.text())).toEqual(['全部', '开船前', '开船后']) expect(tabs[0]!.attributes('aria-selected')).toBe('true') expect(tabs[1]!.attributes('tabindex')).toBe('-1') await selectRows(wrapper, [selectedRow()]) @@ -196,7 +198,7 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect(tabs[1]!.attributes('aria-selected')).toBe('true') expect(tabs[1]!.classes()).toContain('is-active') expect(document.activeElement).toBe(tabs[1]!.element) - expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, currentStatus: 'PENDING_ASSIGNMENT' })) + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, orderType: 'FIRST_LEG', sailingStage: 'BEFORE_DEPARTURE', currentStatus: undefined })) expect((wrapper.vm as unknown as { selectedRows: unknown[] }).selectedRows).toEqual([]) const callCount = vi.mocked(listLastMileOrders).mock.calls.length await tabs[1]!.trigger('click') @@ -205,10 +207,31 @@ describe('LastMileOrdersView pending order and container conversion', () => { await tabs[1]!.trigger('keydown', { key: 'Home' }) await flushPromises() expect(tabs[0]!.attributes('aria-selected')).toBe('true') - expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ currentStatus: undefined })) + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ currentStatus: undefined, sailingStage: undefined, orderType: undefined })) await tabs[0]!.trigger('keydown', { key: 'End' }) await flushPromises() - expect(tabs[1]!.attributes('aria-selected')).toBe('true') + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ sailingStage: 'AFTER_DEPARTURE', orderType: 'FIRST_LEG' })) + expect(tabs[2]!.attributes('aria-selected')).toBe('true') + }) + + it('exports the same stage and filters as the list and restores other types in all', async () => { + const wrapper = await mountView() + const toolbar = wrapper.findComponent(QueryToolbar) + toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), orderType: 'RETURN_RELABEL', detained: 'false' }) + await wrapper.vm.$nextTick() + for (const [label, stage] of [['开船前', 'BEFORE_DEPARTURE'], ['开船后', 'AFTER_DEPARTURE']] as const) { + await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === label)!.trigger('click') + await flushPromises() + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ orderType: 'FIRST_LEG', sailingStage: stage, detained: false })) + expect(toolbar.props('availableFields')?.some((field) => field.key === 'orderType')).toBe(false) + await button(wrapper, '导出订单')!.trigger('click') + await flushPromises() + expect(downloadLastMileOrders).toHaveBeenLastCalledWith(vi.mocked(listLastMileOrders).mock.lastCall![0]) + } + await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === '全部')!.trigger('click') + await flushPromises() + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ orderType: 'RETURN_RELABEL', sailingStage: undefined })) + expect(toolbar.props('availableFields')?.some((field) => field.key === 'orderType')).toBe(true) }) it('preserves advanced conditions when collapsed and clears them on reset', async () => { @@ -448,7 +471,7 @@ describe('LastMileOrdersView pending order and container conversion', () => { expect(wrapper.text()).not.toContain('按订单') expect(wrapper.text()).not.toContain('待分配整柜') expect(wrapper.text()).not.toContain('柜子工作台') - expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 10, currentStatus: 'PENDING_ASSIGNMENT' })) + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 10, currentStatus: undefined, orderType: 'FIRST_LEG', sailingStage: 'BEFORE_DEPARTURE' })) expect(listContainerPlanProgress).not.toHaveBeenCalled() }) @@ -481,9 +504,9 @@ describe('LastMileOrdersView pending order and container conversion', () => { ])) expect(availableFields.map((field) => field.key)).not.toEqual(expect.arrayContaining(['planNo', 'inboundOrderNo'])) - await wrapper.findAll('[role="tab"]').find((item) => item.text() === '待分配')!.trigger('click') + await wrapper.findAll('[role="tab"]').find((item) => item.text() === '开船前')!.trigger('click') await flushPromises() - expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ currentStatus: 'PENDING_ASSIGNMENT', lxCode: 'LX-SEARCH' })) + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ currentStatus: 'DELIVERED', sailingStage: 'BEFORE_DEPARTURE', orderType: 'FIRST_LEG', lxCode: 'LX-SEARCH' })) await openHiddenContainerWorkbench(wrapper) expect(listContainerPlanProgress).toHaveBeenLastCalledWith({ diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index c123010c..da79f351 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -15,7 +15,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue' -import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode } from '../api/lastMileOrders' +import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode, type LastMileOrderQuery } from '../api/lastMileOrders' import OrderReferenceText from '../components/OrderReferenceText.vue' import { reviewCargoDiscard } from '../api/lastMileOrders' import WarehouseTransferReviewDialog from '../components/WarehouseTransferReviewDialog.vue' @@ -71,7 +71,7 @@ async function submitReturnPackingPlan() { } const actionLoading = ref(false) const exportLoading = ref(false) -async function exportOrders() { exportLoading.value = true; try { const { createdAt: range, ...rest } = filters.value; saveCsv(await downloadLastMileOrders({ ...rest, createdFrom: Array.isArray(range) && range[0] ? `${String(range[0]).slice(0, 10)}T00:00:00` : undefined, createdTo: Array.isArray(range) && range[1] ? `${String(range[1]).slice(0, 10)}T23:59:59` : undefined }) as Blob, '尾程订单.csv') } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '尾程订单导出失败,请重试').message) } finally { exportLoading.value = false } } +async function exportOrders() { exportLoading.value = true; try { saveCsv(await downloadLastMileOrders(orderQuery()) as Blob, '尾程订单.csv') } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '尾程订单导出失败,请重试').message) } finally { exportLoading.value = false } } const actionError = ref(null) const actionTarget = ref(null) const inboundPlanOpen = ref(false) @@ -150,8 +150,8 @@ const receivingWarehouseTarget = ref(nul const receivingWarehouseForm = reactive({ warehouseId: '', remark: '' }) let receivingWarehouseIdempotencyKey = '' let containerOptionRequest = 0 -const statusTab = ref<'全部' | '待分配' | '柜子'>('全部') -const statusTabs = ['全部', '待分配'] as const +const statusTab = ref<'全部' | '开船前' | '开船后' | '柜子'>('全部') +const statusTabs = ['全部', '开船前', '开船后'] as const const showAdvancedFilters = ref(false) const filters = ref({ orderNo: '', relabelNo: '', orderType: '', warehouse: '', inboundCode: '', lxCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] }) const allFields: QueryField[] = [ @@ -184,10 +184,11 @@ const allFields: QueryField[] = [ { key: 'detained', type: 'select', label: '是否暂扣', width: 140, placeholder: '请选择是否暂扣', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] }, { key: 'createdAt', type: 'daterange', label: '创建时间', width: 280 }, ] +const availableFields = computed(() => allFields.filter((field) => statusTab.value === '全部' || field.key !== 'orderType')) const fields = computed(() => statusTab.value === '柜子' ? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key)) : showAdvancedFilters.value - ? allFields + ? availableFields.value : allFields.filter((field) => ['orderNo', 'inboundCode', 'warehouse'].includes(field.key))) const selectedContainer = computed(() => containerOptions.value.find((row) => row.sourceContainerBookingId === selectedContainerBookingId.value)) const selectedReceivingWarehouse = computed(() => receivingWarehouseOptions.value.find((row) => ( @@ -333,6 +334,29 @@ function changeContainerPageSize(value: number) { page.value = 1 void load() } +function orderQuery(): LastMileOrderQuery { + const dateRange = Array.isArray(filters.value.createdAt) ? filters.value.createdAt.map(String) : [] + const booleanValue = (value: unknown) => value === 'true' ? true : value === 'false' ? false : undefined + return { + page: page.value, + size: pageSize.value, + orderNo: String(filters.value.orderNo || '').trim() || undefined, + relabelNo: String(filters.value.relabelNo || '').trim() || undefined, + orderType: statusTab.value === '全部' ? String(filters.value.orderType || '').trim() || undefined : 'FIRST_LEG', + sailingStage: statusTab.value === '开船前' ? 'BEFORE_DEPARTURE' : statusTab.value === '开船后' ? 'AFTER_DEPARTURE' : undefined, + warehouse: String(filters.value.warehouse || '').trim() || undefined, + inboundCode: String(filters.value.inboundCode || '').trim() || undefined, + lxCode: String(filters.value.lxCode || '').trim() || undefined, + sourceOrderNo: String(filters.value.sourceOrderNo || '').trim() || undefined, + containerNo: String(filters.value.containerNo || '').trim() || undefined, + currentStatus: String(filters.value.status || '').trim() || undefined, + transferWarehouseCode: String(filters.value.transferWarehouseCode || '').trim() || undefined, + abnormal: booleanValue(filters.value.abnormal), + detained: booleanValue(filters.value.detained), + createdFrom: dateRange[0] ? `${dateRange[0]}T00:00:00` : undefined, + createdTo: dateRange[1] ? `${dateRange[1]}T23:59:59` : undefined, + } +} async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model) loading.value = true @@ -359,26 +383,7 @@ async function load(model: QueryModel = filters.value) { return } try { - const dateRange = Array.isArray(filters.value.createdAt) ? filters.value.createdAt.map(String) : [] - const booleanValue = (value: unknown) => value === 'true' ? true : value === 'false' ? false : undefined - const result = await listLastMileOrders({ - page: page.value, - size: pageSize.value, - orderNo: String(filters.value.orderNo || '').trim() || undefined, - relabelNo: String(filters.value.relabelNo || '').trim() || undefined, - orderType: String(filters.value.orderType || '').trim() || undefined, - warehouse: String(filters.value.warehouse || '').trim() || undefined, - inboundCode: String(filters.value.inboundCode || '').trim() || undefined, - lxCode: String(filters.value.lxCode || '').trim() || undefined, - sourceOrderNo: String(filters.value.sourceOrderNo || '').trim() || undefined, - containerNo: String(filters.value.containerNo || '').trim() || undefined, - currentStatus: statusTab.value === '待分配' ? 'PENDING_ASSIGNMENT' : String(filters.value.status || '').trim() || undefined, - transferWarehouseCode: String(filters.value.transferWarehouseCode || '').trim() || undefined, - abnormal: booleanValue(filters.value.abnormal), - detained: booleanValue(filters.value.detained), - createdFrom: dateRange[0] ? `${dateRange[0]}T00:00:00` : undefined, - createdTo: dateRange[1] ? `${dateRange[1]}T23:59:59` : undefined, - }) + const result = await listLastMileOrders(orderQuery()) rows.value = result.records.map(toLastMileOrderListItem) total.value = result.total } catch (error) { @@ -1034,7 +1039,7 @@ onMounted(() => { void load() }) - + - + diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 342753d5..11e51888 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -375,7 +375,7 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(value).toContain("listAllPlatformWarehouses({ warehouseType: 'FBA', status: 'ACTIVE' })") expect(value).toContain('请选择目的 FBA 仓') expect(value).toContain('aria-label="尾程订单状态"') - expect(value).toContain("const statusTabs = ['全部', '待分配'] as const") + expect(value).toContain("const statusTabs = ['全部', '开船前', '开船后'] as const") expect(value).toContain('class="density-status-tabs" role="tablist"') const columns = value.slice(value.indexOf('const columns:'), value.indexOf('const selectedContainerIds')) expect(columns.indexOf("label: '入库单号'")).toBeLessThan(columns.indexOf("label: '出库单号'"))