From abdea48e8de60ffb43fa1f9be590779fbdf9a472 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 30 Aug 2026 18:21:12 +0800 Subject: [PATCH] refactor(admin): use dialog for inbound relationships --- .../inbounds/components/InboundTaskList.vue | 12 +-- .../inbounds/pages/InboundOrdersView.vue | 10 +-- .../pages/InventoryTransactionsView.vue | 8 +- .../pages/UnifiedOrdersView.vue | 12 +-- ... => InboundCodeRelationshipDialog.test.ts} | 78 +++++++++++++------ ....vue => InboundCodeRelationshipDialog.vue} | 8 +- 6 files changed, 80 insertions(+), 48 deletions(-) rename src/shared/components/{InboundCodeRelationshipDrawer.test.ts => InboundCodeRelationshipDialog.test.ts} (73%) rename src/shared/components/{InboundCodeRelationshipDrawer.vue => InboundCodeRelationshipDialog.vue} (81%) diff --git a/src/modules/inbounds/components/InboundTaskList.vue b/src/modules/inbounds/components/InboundTaskList.vue index 8780a06f..0c5dfc0f 100644 --- a/src/modules/inbounds/components/InboundTaskList.vue +++ b/src/modules/inbounds/components/InboundTaskList.vue @@ -12,7 +12,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' -import InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' +import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import { listPlatformInbounds } from '../core/api' import type { InboundOrderSummary } from '../core/types' @@ -45,8 +45,8 @@ const warehouseOptions = ref>([]) const selectedInbound = ref(null) const measurementDialogOpen = ref(false) const reviewDialogOpen = ref(false) -const relationshipOpen = ref(false) -const relationshipInboundCode = ref('') +const relationshipDialogOpen = ref(false) +const relationshipDialogInboundCode = ref('') const batchReviewLoading = ref(false) const selectedReviewRows = ref([]) let requestSerial = 0 @@ -196,8 +196,8 @@ function openDetail(row: InboundOrderSummary) { function openRelationship(row: InboundOrderSummary) { const code = row.inboundCode?.trim() if (!code) return - relationshipInboundCode.value = code - relationshipOpen.value = true + relationshipDialogInboundCode.value = code + relationshipDialogOpen.value = true } function handleTaskChanged() { @@ -391,7 +391,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria @submitted="handleMeasurementSubmitted" @closed="clearSelectedInbound" /> - + diff --git a/src/modules/inbounds/pages/InboundOrdersView.vue b/src/modules/inbounds/pages/InboundOrdersView.vue index 1962ce3e..b621da84 100644 --- a/src/modules/inbounds/pages/InboundOrdersView.vue +++ b/src/modules/inbounds/pages/InboundOrdersView.vue @@ -10,7 +10,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' -import InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' +import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' @@ -43,8 +43,8 @@ const pageSize = ref(20) const loading = ref(false) const loadError = ref(null) const printingInboundId = ref('') -const relationshipOpen = ref(false) -const relationshipInboundCode = ref('') +const relationshipDialogOpen = ref(false) +const relationshipDialogInboundCode = ref('') const result = ref({ records: [] as InboundOrderSummary[], total: 0 }) const filters = ref({ customerId: '', inboundCode: '', warehouseId: '', deliveryWarehouseId: '', processStatus: '', orderStatus: '', measureReviewStatus: '', customerName: '', destinationCode: '', feeStatus: '', feeReviewStatus: '', holdFlag: '', expectedArrivalRange: [] }) const appliedFilters = ref({ ...filters.value, expectedArrivalRange: [] }) @@ -289,7 +289,7 @@ function openDetail(row: InboundOrderSummary) { } void router.push({ name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId } }) } -function openRelationship(row: InboundOrderSummary) { const code = row.inboundCode?.trim(); if (!code) return; relationshipInboundCode.value = code; relationshipOpen.value = true } +function openRelationship(row: InboundOrderSummary) { const code = row.inboundCode?.trim(); if (!code) return; relationshipDialogInboundCode.value = code; relationshipDialogOpen.value = true } async function printCartonLabels(row: InboundOrderSummary) { if (printingInboundId.value) return printingInboundId.value = row.id @@ -460,7 +460,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId + - + diff --git a/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue b/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue index de7625a1..53f5e529 100644 --- a/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue @@ -5,11 +5,11 @@ import { useRoute, useRouter } from 'vue-router' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { useAccessPreviewStore } from '@/core/access/accessPreview' import AdminPage from '@/shared/components/AdminPage.vue'; import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'; import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'; import ListPageLayout from '@/shared/components/ListPageLayout.vue'; import LoadErrorState from '@/shared/components/LoadErrorState.vue'; import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' -import InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' +import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue' import { listInventoryLedger } from '../api/inventory'; import { formatOverseasDateTime } from '../lastMilePresentation'; import type { InventoryTransactionListItem } from '../types' const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore(); const rows = ref([]); const page = ref(1); const pageSize = ref(10); const total = ref(0); const loading = ref(false); const loadError = ref(null) -const relationshipOpen = ref(false); const relationshipInboundCode = ref('') +const relationshipDialogOpen = ref(false); const relationshipDialogInboundCode = ref('') const filters = ref({ occurredDate: [], customerName: '', warehouse: '', sku: '', businessType: '', businessOrderNo: '', inboundCode: '', inboundOrderNo: '', outboundOrderNo: '' }) const fields = computed(() => [ { key: 'occurredDate', type: 'daterange', label: '发生日期', width: 260 }, @@ -27,7 +27,7 @@ const columns: DataTableColumn[] = [ { key: 'operator', prop: 'operator', label: '操作人', minWidth: 120 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }, ] function text(value: unknown) { return String(value ?? '').trim() || '—' } -function openRelationship(row: InventoryTransactionListItem) { if (row.inboundCode === '—') return; relationshipInboundCode.value = row.inboundCode; relationshipOpen.value = true } +function openRelationship(row: InventoryTransactionListItem) { if (row.inboundCode === '—') return; relationshipDialogInboundCode.value = row.inboundCode; relationshipDialogOpen.value = true } async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); const range = Array.isArray(filters.value.occurredDate) ? filters.value.occurredDate.map(String) : []; loading.value = true; loadError.value = null; try { const result = await listInventoryLedger({ page: page.value, size: pageSize.value, customerName: String(filters.value.customerName || '').trim() || undefined, destinationWarehouseCode: String(filters.value.warehouse || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, businessType: String(filters.value.businessType || '').trim() || undefined, businessOrderNo: String(filters.value.businessOrderNo || '').trim() || undefined, inboundCode: String(filters.value.inboundCode || '').trim() || undefined, inboundOrderNo: String(filters.value.inboundOrderNo || '').trim() || undefined, outboundOrderNo: String(filters.value.outboundOrderNo || '').trim() || undefined, occurredFrom: range[0] ? `${range[0]}T00:00:00` : undefined, occurredTo: range[1] ? `${range[1]}T23:59:59` : undefined }); rows.value = result.records.map((item) => ({ id: String(item.id), occurredAt: formatOverseasDateTime(item.occurredAt), owner: text(item.customerName || '客户信息待补充'), warehouse: text(item.destinationWarehouseCode), sku: text(item.skuCode), productName: '—', businessType: text(item.businessType), businessOrderNo: text(item.businessOrderNo), inboundCode: text(item.inboundCode), inboundOrderNo: text(item.inboundOrderNo), outboundOrderNo: text(item.outboundOrderNo), inboundQuantity: item.deltaPackages > 0 ? item.deltaPackages : 0, outboundQuantity: item.deltaPackages < 0 ? Math.abs(item.deltaPackages) : 0, balance: Number(item.afterPackages || 0), operator: item.operatorId ? '操作人名称待补充' : '—', remark: text(item.remark) })); total.value = result.total } catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '进销存记录加载失败,请重试') } finally { loading.value = false } } function search(model: QueryModel) { page.value = 1; void load(model) }; function reset(model: QueryModel) { page.value = 1; void load(model) }; function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted(load) @@ -36,6 +36,6 @@ function search(model: QueryModel) { page.value = 1; void load(model) }; functio
- + diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 0171f270..4f96a6ff 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -36,7 +36,7 @@ import DetailPageLayout from '@/shared/components/DetailPageLayout.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import LineClampText from '@/shared/components/LineClampText.vue' -import InboundCodeRelationshipDrawer from '@/shared/components/InboundCodeRelationshipDrawer.vue' +import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import StatusBadge from '@/shared/components/StatusBadge.vue' import { @@ -141,8 +141,8 @@ const noteContent = ref('') const noteOrder = ref(null) const relationOpen = ref(false) const relationOrder = ref(null) -const inboundRelationshipOpen = ref(false) -const inboundRelationshipCode = ref('') +const inboundRelationshipDialogOpen = ref(false) +const inboundRelationshipDialogCode = ref('') const measurementDetail = ref(null) const measurementLoading = ref(false) const measurementError = ref(null) @@ -793,8 +793,8 @@ function handleOrderSelectionChange(rows: UnifiedOrderResource[]) { function openInboundRelationship(row: UnifiedOrderResource) { const code = row.inboundCode?.trim() if (!code) return - inboundRelationshipCode.value = code - inboundRelationshipOpen.value = true + inboundRelationshipDialogCode.value = code + inboundRelationshipDialogOpen.value = true } function coLoadGroup(row: UnifiedOrderResource) { @@ -1419,7 +1419,7 @@ onBeforeUnmount(() => { :inbound-code="relationOrder.inboundCode" /> - + diff --git a/src/shared/components/InboundCodeRelationshipDrawer.test.ts b/src/shared/components/InboundCodeRelationshipDialog.test.ts similarity index 73% rename from src/shared/components/InboundCodeRelationshipDrawer.test.ts rename to src/shared/components/InboundCodeRelationshipDialog.test.ts index fcf998db..e22053a4 100644 --- a/src/shared/components/InboundCodeRelationshipDrawer.test.ts +++ b/src/shared/components/InboundCodeRelationshipDialog.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import { resolve } from 'node:path' import { defineComponent } from 'vue' import { flushPromises, mount } from '@vue/test-utils' @@ -9,7 +9,7 @@ import { getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation, } from '@/shared/api/inboundCodeRelations' -import InboundCodeRelationshipDrawer from './InboundCodeRelationshipDrawer.vue' +import InboundCodeRelationshipDialog from './InboundCodeRelationshipDialog.vue' vi.mock('@/shared/api/inboundCodeRelations', () => ({ getLoadingAssociationSummary: vi.fn(), @@ -17,15 +17,15 @@ vi.mock('@/shared/api/inboundCodeRelations', () => ({ getPlatformInboundCodeAssociation: vi.fn(), })) -const source = readFileSync(resolve(process.cwd(), 'src/shared/components/InboundCodeRelationshipDrawer.vue'), 'utf8') -const AppDrawerStub = defineComponent({ +const source = readFileSync(resolve(process.cwd(), 'src/shared/components/InboundCodeRelationshipDialog.vue'), 'utf8') +const AppDialogStub = defineComponent({ props: { modelValue: Boolean, title: String, loading: Boolean }, emits: ['update:modelValue'], template: '', }) -function mountDrawer() { - return mount(InboundCodeRelationshipDrawer, { +function mountDialog() { + return mount(InboundCodeRelationshipDialog, { props: { modelValue: true, inboundCode: 'NYEXX2608300027', @@ -35,9 +35,9 @@ function mountDrawer() { }, global: { stubs: { - AppDrawer: AppDrawerStub, + AppDialog: AppDialogStub, ApiErrorMeta: true, - LoadErrorState: true, + LoadErrorState: { props: ['title', 'message'], template: '
{{ title }} · {{ message }}
' }, StatusBadge: true, 'el-alert': true, 'el-empty': { props: ['description'], template: '

{{ description }}

' }, @@ -48,7 +48,7 @@ function mountDrawer() { }) } -describe('InboundCodeRelationshipDrawer contract', () => { +describe('InboundCodeRelationshipDialog contract', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(getPlatformInboundCodeAssociation).mockResolvedValue({ @@ -73,7 +73,7 @@ describe('InboundCodeRelationshipDrawer contract', () => { }) it('loads all three sections on an initially open mount and reloads after closing and reopening', async () => { - const wrapper = mountDrawer() + const wrapper = mountDialog() await flushPromises() expect(getPlatformInboundCodeAssociation).toHaveBeenCalledWith('NYEXX2608300027') @@ -106,20 +106,30 @@ describe('InboundCodeRelationshipDrawer contract', () => { inboundCode: 'NYEXX2608300027', found: false, customer: null, overseasWarehouse: null, overseasReceivingStatus: null, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [], }) - const wrapper = mountDrawer() + const wrapper = mountDialog() await flushPromises() expect(wrapper.text()).toContain('服务端未找到该入仓码的海外关联数据') expect(wrapper.text()).toContain('FL2093975748931063810') expect(wrapper.text()).toContain('LP-260830-0016') }) + it('keeps a failed section local while the other successful sections stay visible', async () => { + vi.mocked(getOverseasInboundCodeRelationship).mockRejectedValueOnce(new Error('overseas unavailable')) + const wrapper = mountDialog() + await flushPromises() + expect(wrapper.text()).toContain('海外关联加载失败') + expect(wrapper.text()).toContain('FL2093975748931063810') + expect(wrapper.text()).toContain('LP-260830-0016') + expect(wrapper.text()).not.toContain('洛杉矶海外仓') + }) + it('distinguishes an unassigned overseas warehouse from a missing warehouse name', async () => { vi.mocked(getOverseasInboundCodeRelationship).mockResolvedValueOnce({ inboundCode: 'NYEXX2608300027', found: true, customer: { customerId: '90071992547409932', customerName: '纽约客户' }, overseasWarehouse: null, overseasReceivingStatus: 'PENDING_ASSIGNMENT', taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [], }) - const unassigned = mountDrawer() + const unassigned = mountDialog() await flushPromises() expect(unassigned.text()).toContain('尚未分配海外仓') expect(unassigned.text()).not.toContain('仓库名称待补充') @@ -131,7 +141,7 @@ describe('InboundCodeRelationshipDrawer contract', () => { overseasWarehouse: { warehouseId: '90071992547409939', warehouseCode: 'LAX', warehouseName: '' }, overseasReceivingStatus: 'PENDING_ASSIGNMENT', taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [], }) - const missingName = mountDrawer() + const missingName = mountDialog() await flushPromises() expect(missingName.text()).toContain('仓库名称待补充') expect(missingName.text()).not.toContain('尚未分配海外仓') @@ -165,10 +175,11 @@ describe('InboundCodeRelationshipDrawer contract', () => { expect(source).toContain(':title="display(baseAssociation.inboundCode)"') }) - it('keeps sections and status badges within the drawer at desktop and narrow widths', () => { - expect(source).toContain('class="inbound-relationship-drawer"') - expect(source).toContain(':global(.inbound-relationship-drawer .el-drawer__body){box-sizing:border-box;min-width:0;max-width:100%;overflow-x:hidden;padding-right:calc(var(--el-drawer-padding-primary) + 8px)}') - expect(source).toContain(':global(.inbound-relationship-drawer .app-drawer__body){box-sizing:border-box;width:auto;max-width:100%;min-width:0;overflow-x:hidden}') + it('keeps sections and status badges within the dialog at desktop and narrow widths', () => { + expect(source).toContain('class="inbound-relationship-dialog"') + expect(source).toContain('width="min(1060px, 94vw)"') + expect(source).toContain(':global(.inbound-relationship-dialog .el-dialog__body){box-sizing:border-box;min-width:0;max-height:calc(90vh - 112px);overflow-x:hidden;overflow-y:auto;padding-right:28px}') + expect(source).toContain(':global(.inbound-relationship-dialog .app-dialog__body){box-sizing:border-box;width:auto;max-width:100%;min-width:0;overflow-x:hidden}') expect(source).toContain('.relationship-sections{display:grid;width:auto;max-width:100%;min-width:0;grid-template-columns:minmax(0,1fr)') expect(source).toContain('.relationship-section{box-sizing:border-box;width:100%;min-width:0') expect(source).toContain('flex-wrap:wrap') @@ -177,8 +188,8 @@ describe('InboundCodeRelationshipDrawer contract', () => { expect(source).toContain('.inventory-table{width:100%;max-width:100%;min-width:0') }) - it('mounts the containment class through the real AppDrawer and Element Plus drawer DOM', async () => { - const wrapper = mount(InboundCodeRelationshipDrawer, { + it('mounts the containment class through the real AppDialog and Element Plus dialog DOM', async () => { + const wrapper = mount(InboundCodeRelationshipDialog, { attachTo: document.body, props: { modelValue: true, @@ -191,15 +202,36 @@ describe('InboundCodeRelationshipDrawer contract', () => { }) await flushPromises() - const drawer = document.querySelector('.inbound-relationship-drawer') - const elementBody = drawer?.querySelector('.el-drawer__body') - const appBody = drawer?.querySelector('.app-drawer__body') - expect(drawer).not.toBeNull() + const dialog = document.querySelector('.inbound-relationship-dialog') + const elementBody = dialog?.querySelector('.el-dialog__body') + const appBody = dialog?.querySelector('.app-dialog__body') + expect(dialog).not.toBeNull() expect(elementBody).not.toBeNull() expect(appBody).not.toBeNull() expect(appBody?.querySelectorAll('.relationship-section')).toHaveLength(3) expect(appBody?.querySelectorAll('.relationship-status')).toHaveLength(3) + expect(dialog?.querySelector('.el-drawer__body')).toBeNull() wrapper.unmount() }) + + it('removes the retired Drawer component and all Admin consumer references', () => { + const root = process.cwd() + expect(existsSync(resolve(root, 'src/shared/components/InboundCodeRelationshipDrawer.vue'))).toBe(false) + expect(existsSync(resolve(root, 'src/shared/components/InboundCodeRelationshipDrawer.test.ts'))).toBe(false) + const consumerFiles = [ + 'src/modules/unified-orders/pages/UnifiedOrdersView.vue', + 'src/modules/inbounds/pages/InboundOrdersView.vue', + 'src/modules/inbounds/components/InboundTaskList.vue', + 'src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue', + ] + const consumerSources = consumerFiles.map((file) => readFileSync(resolve(root, file), 'utf8')) + const consumers = consumerSources.join('\n') + expect(consumers).not.toContain('InboundCodeRelationshipDrawer') + expect(consumers).not.toMatch(/relationshipOpen|relationshipInboundCode|inboundRelationshipOpen/) + consumerSources.forEach((consumer) => { + expect(consumer).toContain("import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'") + expect(consumer).toContain(' [props.modelValue, props.inboundCode, props.canReadBase, props.canRe - +