From 83374ba6ccbab3aa7fdcdd58071ba5e14ecaaba5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 16:37:36 +0800 Subject: [PATCH] fix(admin): keep co-load validation action scoped --- .../pages/UnifiedOrdersView.coLoad.test.ts | 59 ++++++++++++++++--- .../pages/UnifiedOrdersView.test.ts | 7 ++- .../pages/UnifiedOrdersView.vue | 12 ++-- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts index 89a238b7..25c89700 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts @@ -2,7 +2,7 @@ import { flushPromises, shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { ElMessageBox } from 'element-plus' import { ApiResponseError } from '@/shared/api/http' -import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders' +import { listPlatformUnifiedOrders, type UnifiedOrderResource } from '@/modules/unified-orders/api/unifiedOrders' import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups' import UnifiedOrdersView from './UnifiedOrdersView.vue' @@ -21,7 +21,7 @@ vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({ cancelPlatformCoLoadGroup: vi.fn(), createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(), })) -const rows = ['5001', '5002'].map((orderId, index) => ({ +const rows: UnifiedOrderResource[] = ['5001', '5002'].map((orderId, index) => ({ orderId, tenantId: `600${index}`, orderNo: `ORD-${index + 1}`, mainStatus: 'WAIT_SPLIT' as const, nextAction: null, relation: null, internalOrderStatus: 'WAIT_SPLIT', customerId: `600${index}`, customerName: `客户${index + 1}`, channelCode: 'NY_EXX', channelName: '纽约渠道', receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8', @@ -38,7 +38,7 @@ function mountView() { AdminPage: { template: '
' }, ListPageLayout: { template: '
' }, QueryToolbar: { template: '
' }, - DataTable: { name: 'DataTable', props: ['data'], emits: ['selection-change'], template: '
' }, + DataTable: { name: 'DataTable', props: ['data', 'columns'], emits: ['selection-change'], template: '
' }, AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText', 'confirmDisabled', 'confirmLoading'], emits: ['confirm', 'update:modelValue'], methods: { close() { this.$emit('update:modelValue', false) } }, template: '
' }, ApiErrorMeta: { props: ['details'], template: '
{{ details.code }} {{ details.requestId }} {{ details.traceId }}
' }, CopyButton: { props: ['value', 'label'], template: '' }, @@ -76,28 +76,42 @@ describe('UnifiedOrdersView co-load group behavior', () => { it.each([ ['平台收货仓不同', [{ ...rows[0], receivingWarehouseId: '8', receivingWarehouseName: '义乌仓' }, { ...rows[1], receivingWarehouseId: '9', receivingWarehouseName: '东莞仓' }], '所选订单的平台收货仓不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:东莞仓;渠道:纽约渠道),不能设为同柜组'], ['渠道不同', [{ ...rows[0], channelCode: 'NY_EXX', channelName: '纽约渠道' }, { ...rows[1], channelCode: 'LA_SEA', channelName: '洛杉矶渠道' }], '所选订单的渠道不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:义乌仓;渠道:洛杉矶渠道),不能设为同柜组'], - ['已有同柜组', [rows[0], { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } }], '所选订单中已有同柜组,请先解除原同柜组'], - ])('blocks create immediately when %s', async (_case, selectedRows, reason) => { + ['已有同柜组', [rows[0], { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } }], '以下入仓码已有同柜组:IN-2(仓库:义乌仓;渠道:纽约渠道),请先解除原同柜组'], + ])('blocks a bypassed invalid create when %s without rendering qualification errors', async (_case, selectedRows, reason) => { vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 }) const wrapper = mountView() await flushPromises() await wrapper.get('[data-testid="select"]').trigger('click') const createButton = wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))! expect(createButton.attributes('disabled')).toBeDefined() - expect(wrapper.text()).toContain(reason) + expect(wrapper.text()).not.toContain(reason) await createButton.trigger('click') expect(createPlatformCoLoadGroup).not.toHaveBeenCalled() }) + it('keeps zero-selection and one-selection silent while the create button stays disabled', async () => { + const wrapper = mountView() + await flushPromises() + expect(wrapper.text()).not.toContain('请至少勾选 2 票订单后再设为同柜组') + expect(wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.attributes('disabled')).toBeDefined() + + ;(wrapper.vm as unknown as { handleOrderSelectionChange: (selected: typeof rows) => void }).handleOrderSelectionChange([rows[0]]) + await wrapper.vm.$nextTick() + expect(wrapper.text()).not.toContain('当前仅选择 1 票') + expect(wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.attributes('disabled')).toBeDefined() + expect(wrapper.text()).not.toContain('5001') + expect(wrapper.text()).not.toContain('7000') + }) + it.each([ ['平台收货仓', [{ ...rows[0], receivingWarehouseId: null, receivingWarehouseName: null }, rows[1]], '以下入仓码缺少平台收货仓:IN-1(仓库:未配置仓库;渠道:纽约渠道),请刷新后重试'], ['渠道', [{ ...rows[0], channelCode: null, channelName: null }, rows[1]], '以下入仓码缺少渠道:IN-1(仓库:义乌仓;渠道:未配置渠道),请刷新后重试'], - ])('lists the affected inbound code and display names when %s is missing', async (_case, selectedRows, reason) => { + ])('keeps a bypassed missing-field selection disabled and silent when %s is missing', async (_case, selectedRows, reason) => { vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 }) const wrapper = mountView() await flushPromises() await wrapper.get('[data-testid="select"]').trigger('click') - expect(wrapper.text()).toContain(reason) + expect(wrapper.text()).not.toContain(reason) expect(createPlatformCoLoadGroup).not.toHaveBeenCalled() }) @@ -118,10 +132,37 @@ describe('UnifiedOrdersView co-load group behavior', () => { const wrapper = mountView() await flushPromises() await wrapper.get('[data-testid="select"]').trigger('click') - expect(wrapper.text()).toContain('所选订单的渠道不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:义乌仓;渠道:纽约渠道),不能设为同柜组') + expect(wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.attributes('disabled')).toBeDefined() + expect(wrapper.text()).not.toContain('所选订单的渠道不同') expect(createPlatformCoLoadGroup).not.toHaveBeenCalled() }) + it('keeps every row selectable for shared batch actions while only disabling invalid co-load creation', async () => { + const selectedRows: UnifiedOrderResource[] = [ + { ...rows[0], mainStatus: 'PENDING_REVIEW', nextAction: 'APPROVE_ORDER', receivingWarehouseId: '8' }, + { ...rows[1], mainStatus: 'PENDING_REVIEW', nextAction: 'APPROVE_ORDER', receivingWarehouseId: '9' }, + { ...rows[1], orderId: '5003', inboundCode: 'IN-3', mainStatus: 'PENDING_REVIEW', nextAction: 'APPROVE_ORDER', receivingWarehouseId: '10', channelCode: 'LA_SEA' }, + ] + vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 3, pages: 1 }) + const wrapper = mountView() + await flushPromises() + const table = wrapper.findComponent({ name: 'DataTable' }) + const selectable = (table.props('columns') as Array<{ key: string; selectable?: (row: UnifiedOrderResource) => boolean }>).find((column) => column.key === 'selection')!.selectable! + const missingChannel = { ...rows[1], channelCode: null, channelName: null } + const grouped = { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } } + + expect(selectedRows.every((row) => selectable(row))).toBe(true) + expect(selectable(missingChannel)).toBe(true) + expect(selectable(grouped)).toBe(true) + await table.vm.$emit('selection-change', selectedRows) + await flushPromises() + expect(wrapper.findAll('button').find((button) => button.text().includes('批量审核通过'))!.attributes('disabled')).toBeUndefined() + expect(wrapper.findAll('button').find((button) => button.text().includes('批量审核通过'))!.text()).toContain('(3)') + expect(wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.attributes('disabled')).toBeDefined() + expect(wrapper.text()).not.toContain('平台收货仓不同') + expect(wrapper.text()).not.toContain('渠道不同') + }) + it('uses inbound codes as the only row identifier and shows an explicit pending state', async () => { vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], inboundCode: null }], page: 1, size: 20, total: 2, pages: 1 }) const wrapper = mountView() diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index fbf84f5f..34fae74f 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -179,7 +179,12 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('所选订单的渠道不同:') expect(source).toContain('row.channelCode!.trim()') expect(source).not.toContain('String(row.channelName || \'\').trim()') - expect(source).toContain('所选订单中已有同柜组,请先解除原同柜组') + expect(source).not.toContain('请至少勾选 2 票订单后再设为同柜组') + expect(source).not.toContain('当前仅选择 1 票:') + expect(source).toContain('以下入仓码已有同柜组:') + expect(source).toContain('selectable: () => !batchApprovalSaving.value && !coLoadSaving.value') + expect(source).not.toContain('isCoLoadRowSelectable') + expect(source).not.toContain(' { diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index c535f81c..b607f766 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -828,7 +828,8 @@ function coLoadSelectionMembersLabel(rows: UnifiedOrderResource[]) { const coLoadSelectionProblem = computed(() => { if (selectedOrders.value.length < 2) return null - if (selectedOrders.value.some((row) => row.coLoadGroup)) return '所选订单中已有同柜组,请先解除原同柜组' + const groupedRows = selectedOrders.value.filter((row) => row.coLoadGroup && row.coLoadGroup.status !== 'CANCELLED') + if (groupedRows.length) return `以下入仓码已有同柜组:${coLoadSelectionMembersLabel(groupedRows)},请先解除原同柜组` const missingWarehouseRows = selectedOrders.value.filter((row) => !String(row.receivingWarehouseId || '').trim()) if (missingWarehouseRows.length) return `以下入仓码缺少平台收货仓:${coLoadSelectionMembersLabel(missingWarehouseRows)},请刷新后重试` const warehouseIds = selectedOrders.value.map((row) => String(row.receivingWarehouseId).trim()) @@ -894,10 +895,7 @@ function openCoLoadDetail(row: UnifiedOrderResource) { } function openCoLoadCreate() { - if (!canCreateCoLoadGroup.value) { - if (coLoadSelectionProblem.value) ElMessage.warning(coLoadSelectionProblem.value) - return - } + if (!canCreateCoLoadGroup.value) return coLoadError.value = null coLoadCreateOpen.value = true } @@ -1432,8 +1430,7 @@ onBeforeUnmount(() => { @@ -1587,7 +1584,6 @@ onBeforeUnmount(() => { .coload-detail-table td span { color: var(--cc-muted); } .coload-detail-table :deep(.el-button) { min-height: 24px; padding: 2px 0; } .coload-toolbar-action { display: flex; min-width: 0; align-items: center; gap: 8px; } -.coload-toolbar-action small { max-width: 360px; overflow: hidden; color: var(--el-color-danger); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } .subsequent-fulfillment { min-height: 72px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--cc-border); } .subsequent-fulfillment h3 { margin: 0 0 10px; font-size: 15px; } .fulfillment-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0 0 10px; border: 1px solid var(--cc-border); border-radius: 6px; overflow: hidden; }