From 87c6d2c4ca2443778f39d7c131f93235098dc8ce Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 14 Aug 2026 10:09:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/DomesticContainersView.test.ts | 5 ++ .../pages/DomesticContainersView.vue | 23 ++++--- .../pages/domesticContainersSummary.css | 67 +++++++++++++++++++ src/shared/components/QueryToolbar.test.ts | 12 +++- src/shared/components/QueryToolbar.vue | 49 +++++++++----- 5 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 src/modules/domestic-containers/pages/domesticContainersSummary.css diff --git a/src/modules/domestic-containers/pages/DomesticContainersView.test.ts b/src/modules/domestic-containers/pages/DomesticContainersView.test.ts index d2674603..1d3af183 100644 --- a/src/modules/domestic-containers/pages/DomesticContainersView.test.ts +++ b/src/modules/domestic-containers/pages/DomesticContainersView.test.ts @@ -2,9 +2,14 @@ import { describe, expect, it } from 'vitest' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' const source = readFileSync(resolve(process.cwd(), 'src/modules/domestic-containers/pages/DomesticContainersView.vue'), 'utf8') +const summaryStyles = readFileSync(resolve(process.cwd(), 'src/modules/domestic-containers/pages/domesticContainersSummary.css'), 'utf8') describe('DomesticContainersView', () => { it('uses formal shared list and centered dialog components', () => { expect(source).toContain('QueryToolbar'); expect(source).toContain('DataTable'); expect(source).toContain('AppDialog'); expect(source).toContain("fixed: 'left'"); expect(source).toContain('width="min(720px, calc(100vw - 28px))"') }) it('gates create actions with backend permissions', () => { expect(source).toContain("access.can('platform:domestic-container:inbound:create')"); expect(source).toContain("access.can('platform:domestic-container:outbound:create')") }) it('loads only active domestic warehouse candidates and surfaces errors', () => { expect(source).toContain("listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })"); expect(source).toContain('国内仓候选加载失败') }) it('keeps idempotency key stable through retry and handles conflicts by refreshing inventory', () => { expect(source).toContain('idempotencyKey.value'); expect(source).toContain("submitError.value.code === 'A0409'"); expect(source).toContain('await loadCandidates()'); expect(source).not.toContain('localStorage') }) + it('reuses the order-list tab panel and keyboard interaction', () => { expect(source).toContain('class="business-query-panel"'); expect(source).toContain('role="tablist"'); expect(source).toContain('role="tab"'); expect(source).toContain('handleTabKeydown'); expect(source).toContain(".status-tab.is-active::after{background:var(--cc-primary)}"); expect(source).not.toContain(' { expect(source).toContain("key: 'warehouseId', type: 'select', width: 210"); expect(source).toContain("key: 'orderNo', type: 'input', width: 220"); expect(source).toContain("key: 'containerNo', type: 'input', width: 220"); expect(source).toContain("key: 'cargoName', type: 'input', width: 220"); expect(source).toContain("key: 'status', type: 'select', width: 180") }) + it('uses the shared query toolbar flow without a page-level layout override', () => { expect(source).not.toContain('.query-toolbar__filters') }) + it('matches the prototype four-column inventory summary', () => { expect(source).toContain("import './domesticContainersSummary.css'"); expect(summaryStyles).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))'); expect(summaryStyles).toContain('border: 1px solid #e7edf5'); expect(summaryStyles).toContain('font-size: 20px'); expect(summaryStyles).toContain('.inventory-summary + .data-table > .data-table__toolbar') }) }) diff --git a/src/modules/domestic-containers/pages/DomesticContainersView.vue b/src/modules/domestic-containers/pages/DomesticContainersView.vue index 1ac6dc7c..db22bc55 100644 --- a/src/modules/domestic-containers/pages/DomesticContainersView.vue +++ b/src/modules/domestic-containers/pages/DomesticContainersView.vue @@ -16,9 +16,15 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses' import { createDomesticContainerInbound, createDomesticContainerOutbound, getDomesticContainerInventory, listDomesticContainerInbounds, listDomesticContainerInventories, listDomesticContainerOutbounds } from '../api/domesticContainers' import type { ContainerOrder, ContainerOrderStatus, InventoryRecord, InventorySummary } from '../types' +import './domesticContainersSummary.css' type TabName = 'inbound' | 'outbound' | 'inventory' const router = useRouter(); const access = useAccessPreviewStore(); const activeTab = ref('inbound') +const businessTabs: Array<{ value: TabName; label: string }> = [ + { value: 'inbound', label: '整柜入库单' }, + { value: 'outbound', label: '整柜出库单' }, + { value: 'inventory', label: '整柜库存' }, +] const loading = ref(false); const loadError = ref(null); const rows = ref<(ContainerOrder | InventoryRecord)[]>([]); const page = ref(1); const pageSize = ref(20); const total = ref(0) const summary = ref({ inboundBoxes: 0, outboundBoxes: 0, availableBoxes: 0, inboundVolumeCbm: 0 }) const warehouses = ref([]); const warehouseLoading = ref(false); const warehouseError = ref(null) @@ -26,10 +32,10 @@ const filters = ref({ warehouseId: '', orderNo: '', containerNo: '', const appliedFilters = ref({ ...filters.value }) const warehouseOptions = computed(() => warehouses.value.map((item) => ({ label: item.warehouseName, value: String(item.id) }))) const queryFields = computed(() => { - const base: QueryField[] = [{ key: 'warehouseId', type: 'select', label: '国内仓', placeholder: warehouseError.value ? '国内仓加载失败' : '全部国内仓', options: warehouseOptions.value, props: { loading: warehouseLoading.value, disabled: Boolean(warehouseError.value) } }] - if (activeTab.value !== 'inventory') base.push({ key: 'orderNo', type: 'input', label: '单号', placeholder: activeTab.value === 'inbound' ? '入库单号' : '出库单号' }) - base.push({ key: 'containerNo', type: 'input', label: '集装箱号', placeholder: '集装箱号' }, { key: 'cargoName', type: 'input', label: '货物名称', placeholder: '货物名称' }) - base.push({ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: activeTab.value === 'inventory' ? [{ label: '在库', value: 'IN_STOCK' }, { label: '部分出库', value: 'PARTIALLY_OUTBOUND' }, { label: '已出库', value: 'OUTBOUND' }] : [{ label: activeTab.value === 'inbound' ? '待入库' : '待出库', value: 'PENDING' }, { label: activeTab.value === 'inbound' ? '已入库' : '已出库', value: 'COMPLETED' }] }) + const base: QueryField[] = [{ key: 'warehouseId', type: 'select', width: 210, label: '国内仓', placeholder: warehouseError.value ? '国内仓加载失败' : '全部国内仓', options: warehouseOptions.value, props: { loading: warehouseLoading.value, disabled: Boolean(warehouseError.value) } }] + if (activeTab.value !== 'inventory') base.push({ key: 'orderNo', type: 'input', width: 220, label: '单号', placeholder: activeTab.value === 'inbound' ? '入库单号' : '出库单号' }) + base.push({ key: 'containerNo', type: 'input', width: 220, label: '集装箱号', placeholder: '集装箱号' }, { key: 'cargoName', type: 'input', width: 220, label: '货物名称', placeholder: '货物名称' }) + base.push({ key: 'status', type: 'select', width: 180, label: '状态', placeholder: '全部状态', options: activeTab.value === 'inventory' ? [{ label: '在库', value: 'IN_STOCK' }, { label: '部分出库', value: 'PARTIALLY_OUTBOUND' }, { label: '已出库', value: 'OUTBOUND' }] : [{ label: activeTab.value === 'inbound' ? '待入库' : '待出库', value: 'PENDING' }, { label: activeTab.value === 'inbound' ? '已入库' : '已出库', value: 'COMPLETED' }] }) return base }) const columns = computed(() => activeTab.value === 'inventory' ? [ @@ -43,7 +49,8 @@ function queryParams() { const model = appliedFilters.value; return { page: page async function load() { loading.value = true; loadError.value = null; try { const result = activeTab.value === 'inbound' ? await listDomesticContainerInbounds(queryParams()) : activeTab.value === 'outbound' ? await listDomesticContainerOutbounds(queryParams()) : await listDomesticContainerInventories(queryParams()); rows.value = result.records; total.value = result.total; if ('summary' in result) summary.value = result.summary } catch (cause) { loadError.value = getApiErrorDetails(cause, '整柜业务加载失败') } finally { loading.value = false } } async function loadWarehouses() { warehouseLoading.value = true; warehouseError.value = null; try { warehouses.value = (await listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })).filter((item) => item.warehouseType === 'DOMESTIC' && item.status === 'ACTIVE') } catch (cause) { warehouseError.value = getApiErrorDetails(cause, '国内仓候选加载失败') } finally { warehouseLoading.value = false } } function search(model = filters.value) { appliedFilters.value = { ...model }; if (page.value === 1) void load(); else page.value = 1 } -function switchTab(name: string | number) { activeTab.value = name as TabName; filters.value = { warehouseId: filters.value.warehouseId || '', orderNo: '', containerNo: '', cargoName: '', status: '' }; appliedFilters.value = { ...filters.value }; page.value = 1; void load() } +function switchTab(name: TabName) { if (activeTab.value === name) return; activeTab.value = name; filters.value = { warehouseId: filters.value.warehouseId || '', orderNo: '', containerNo: '', cargoName: '', status: '' }; appliedFilters.value = { ...filters.value }; page.value = 1; void load() } +function handleTabKeydown(event: KeyboardEvent, index: number) { let nextIndex = index; if (event.key === 'ArrowRight') nextIndex = (index + 1) % businessTabs.length; else if (event.key === 'ArrowLeft') nextIndex = (index - 1 + businessTabs.length) % businessTabs.length; else if (event.key === 'Home') nextIndex = 0; else if (event.key === 'End') nextIndex = businessTabs.length - 1; else return; event.preventDefault(); const tab = businessTabs[nextIndex]; if (!tab) return; switchTab(tab.value); (event.currentTarget as HTMLElement).parentElement?.querySelectorAll('[role="tab"]')[nextIndex]?.focus() } function viewInventory(row: ContainerOrder | InventoryRecord) { if ('inventoryStatus' in row) void router.push({ name: 'domestic-container-inventory-detail', params: { inventoryId: String(row.id) } }); else void findAndOpenInventory(row) } async function findAndOpenInventory(order: ContainerOrder) { try { const result = await listDomesticContainerInventories({ page: 1, size: 20, warehouseId: String(order.warehouseId), containerNo: order.containerNo }); const inventory = result.records.find((item) => String(item.warehouseId) === String(order.warehouseId) && item.containerNo === order.containerNo); if (!inventory) return ElMessage.warning('该单据尚未形成整柜库存'); await router.push({ name: 'domestic-container-inventory-detail', params: { inventoryId: String(inventory.id) } }) } catch (cause) { loadError.value = getApiErrorDetails(cause, '整柜库存定位失败') } } @@ -63,9 +70,9 @@ async function submit() { if (!await formRef.value?.validate().catch(() => false watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWarehouses(), load()]) }) -