From 3b15c4b597bf2a9a0330f0f13e276ff06240d38b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 7 Sep 2026 11:20:43 +0800 Subject: [PATCH] feat: add inbound hold controls --- src/modules/inbounds/core/api.test.ts | 32 +++- src/modules/inbounds/core/api.ts | 60 ++++++++ src/modules/inbounds/core/types.ts | 27 ++++ src/modules/inbounds/index.ts | 2 + .../unified-orders/api/unifiedOrders.test.ts | 4 +- .../unified-orders/api/unifiedOrders.ts | 6 +- .../pages/UnifiedOrdersView.coLoad.test.ts | 1 + .../UnifiedOrdersView.fulfillment.test.ts | 77 +++++++++- .../pages/UnifiedOrdersView.test.ts | 13 ++ .../pages/UnifiedOrdersView.vue | 143 +++++++++++++++++- 10 files changed, 352 insertions(+), 13 deletions(-) diff --git a/src/modules/inbounds/core/api.test.ts b/src/modules/inbounds/core/api.test.ts index e871d88a..26f5529f 100644 --- a/src/modules/inbounds/core/api.test.ts +++ b/src/modules/inbounds/core/api.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from './api' +import { downloadInboundCartonLabels, getPlatformInbound, getPlatformInboundHold, holdPlatformInbound, listPlatformInbounds, releasePlatformInbound } from './api' vi.mock('@/shared/api/http', () => ({ ApiResponseError: class ApiResponseError extends Error { @@ -116,4 +116,34 @@ describe('platform inbound query API', () => { message: '箱号映射不完整', code: 'A0422', }) }) + + it('loads and normalizes the tenant-scoped hold state and immutable history', async () => { + vi.mocked(http.get).mockResolvedValue({ data: { + inboundId: 19001, tenantId: 10002, holdFlag: true, currentHoldReason: ' 待核对货款 ', version: '4', + canHold: false, canRelease: true, actionBlockReason: null, + history: [{ id: 88001, action: 'HOLD', reason: ' 待核对货款 ', evidenceFileId: 99001, operatorName: ' 财务A ', operatedAt: '2026-09-07T10:00:00' }], + } }) + + await expect(getPlatformInboundHold('10002', '19001')).resolves.toEqual({ + inboundId: '19001', tenantId: '10002', holdFlag: true, currentHoldReason: '待核对货款', version: 4, + canHold: false, canRelease: true, actionBlockReason: null, + history: [{ id: '88001', action: 'HOLD', reason: '待核对货款', evidenceFileId: '99001', operatorName: '财务A', operatedAt: '2026-09-07T10:00:00' }], + }) + expect(http.get).toHaveBeenCalledWith('/platform/inbounds/19001/hold', { params: { tenantId: '10002' } }) + }) + + it('posts hold and release commands with versions, reasons and independent idempotency keys', async () => { + const result = { data: { inboundId: '19001', tenantId: '10002', holdFlag: true, currentHoldReason: '待核对货款', version: 4, canHold: false, canRelease: true, actionBlockReason: null, history: [] } } + vi.mocked(http.post).mockResolvedValue(result) + + await holdPlatformInbound('10002', '19001', { expectedVersion: 3, reason: '待核对货款' }, 'hold-key') + expect(http.post).toHaveBeenNthCalledWith(1, '/platform/inbounds/19001/hold', { expectedVersion: 3, reason: '待核对货款' }, { + params: { tenantId: '10002' }, headers: { 'Idempotency-Key': 'hold-key' }, + }) + + await releasePlatformInbound('10002', '19001', { expectedVersion: 4, reason: '货款已核对' }, 'release-key') + expect(http.post).toHaveBeenNthCalledWith(2, '/platform/inbounds/19001/hold/release', { expectedVersion: 4, reason: '货款已核对' }, { + params: { tenantId: '10002' }, headers: { 'Idempotency-Key': 'release-key' }, + }) + }) }) diff --git a/src/modules/inbounds/core/api.ts b/src/modules/inbounds/core/api.ts index a7261187..f97c6342 100644 --- a/src/modules/inbounds/core/api.ts +++ b/src/modules/inbounds/core/api.ts @@ -2,6 +2,8 @@ import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/s import type { InboundFileSummary, InboundInternalNote, + InboundHoldCommand, + InboundHoldDetail, InboundListQuery, InboundOrderDetail, InboundOrderSummary, @@ -111,6 +113,32 @@ function normalizeDetail(raw: InboundOrderDetail): InboundOrderDetail { } } +function normalizeHoldDetail(raw: InboundHoldDetail): InboundHoldDetail { + return { + ...raw, + inboundId: String(raw.inboundId), + tenantId: String(raw.tenantId), + holdFlag: Boolean(raw.holdFlag), + currentHoldReason: raw.currentHoldReason?.trim() || null, + version: integerValue(raw.version) || 0, + canHold: Boolean(raw.canHold), + canRelease: Boolean(raw.canRelease), + actionBlockReason: raw.actionBlockReason?.trim() || null, + history: (raw.history || []).map((item) => ({ + ...item, + id: String(item.id), + reason: item.reason?.trim() || '', + evidenceFileId: id(item.evidenceFileId), + operatorName: item.operatorName?.trim() || null, + })), + } +} + +export function createInboundHoldIdempotencyKey(action: 'hold' | 'release') { + const uuid = globalThis.crypto?.randomUUID?.() + return uuid ? `inbound-${action}-${uuid}` : `inbound-${action}-${Date.now()}-${Math.random().toString(36).slice(2)}` +} + function queryParams(tenantId: string | undefined, query: InboundListQuery) { return Object.fromEntries( Object.entries({ tenantId, ...query }).filter(([, value]) => value !== undefined && value !== ''), @@ -132,6 +160,38 @@ export async function getPlatformInbound(tenantId: string, inboundId: string) { return normalizeDetail(unwrapApiResponse(response)) } +export async function getPlatformInboundHold(tenantId: string, inboundId: string) { + const response = await http.get, ApiEnvelope>( + `${basePath}/${encodeURIComponent(inboundId)}/hold`, + { params: { tenantId } }, + ) + return normalizeHoldDetail(unwrapApiResponse(response)) +} + +async function postPlatformInboundHold( + tenantId: string, + inboundId: string, + action: 'hold' | 'release', + payload: InboundHoldCommand, + idempotencyKey = createInboundHoldIdempotencyKey(action), +) { + const suffix = action === 'release' ? '/release' : '' + const response = await http.post, ApiEnvelope>( + `${basePath}/${encodeURIComponent(inboundId)}/hold${suffix}`, + payload, + { params: { tenantId }, headers: { 'Idempotency-Key': idempotencyKey } }, + ) + return normalizeHoldDetail(unwrapApiResponse(response)) +} + +export function holdPlatformInbound(tenantId: string, inboundId: string, payload: InboundHoldCommand, idempotencyKey?: string) { + return postPlatformInboundHold(tenantId, inboundId, 'hold', payload, idempotencyKey) +} + +export function releasePlatformInbound(tenantId: string, inboundId: string, payload: InboundHoldCommand, idempotencyKey?: string) { + return postPlatformInboundHold(tenantId, inboundId, 'release', payload, idempotencyKey) +} + export async function downloadInboundCartonLabels(tenantId: string, inboundId: string, cartonIndexes?: number[]) { const response = await http.post( `${basePath}/${encodeURIComponent(inboundId)}/carton-label-pdf`, diff --git a/src/modules/inbounds/core/types.ts b/src/modules/inbounds/core/types.ts index 9d14394c..a6294d0a 100644 --- a/src/modules/inbounds/core/types.ts +++ b/src/modules/inbounds/core/types.ts @@ -124,6 +124,33 @@ export interface InboundOrderDetail { statusHistory: InboundStatusHistory[] } +export interface InboundHoldHistoryEntry { + id: string + action: 'HOLD' | 'RELEASE' + reason: string + evidenceFileId: string | null + operatorName: string | null + operatedAt: string +} + +export interface InboundHoldDetail { + inboundId: string + tenantId: string + holdFlag: boolean + currentHoldReason: string | null + version: number + canHold: boolean + canRelease: boolean + actionBlockReason: string | null + history: InboundHoldHistoryEntry[] +} + +export interface InboundHoldCommand { + expectedVersion: number + reason: string + evidenceFileId?: string +} + export interface InboundDropshipPallet { palletId: string palletNo: string | null diff --git a/src/modules/inbounds/index.ts b/src/modules/inbounds/index.ts index 3cff1a9e..981896c7 100644 --- a/src/modules/inbounds/index.ts +++ b/src/modules/inbounds/index.ts @@ -1,4 +1,5 @@ export { getInboundMeasurement, saveInboundMeasurementDraft } from './measurement/api' +export { createInboundHoldIdempotencyKey, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound } from './core/api' export { default as InboundMeasurementPanel } from './measurement/InboundMeasurementPanel.vue' export { default as InboundMeasurementDialog } from './measurement/InboundMeasurementDialog.vue' export { default as MeasurementEvidenceThumbnail } from './measurement/components/MeasurementEvidenceThumbnail.vue' @@ -13,3 +14,4 @@ export { export type { InboundFeeSnapshot } from './fee/types' export { downloadWeightImage } from './weight/api/weightOcr' export type { InboundMeasurementDetail } from './measurement/types' +export type { InboundHoldCommand, InboundHoldDetail, InboundHoldHistoryEntry } from './core/types' diff --git a/src/modules/unified-orders/api/unifiedOrders.test.ts b/src/modules/unified-orders/api/unifiedOrders.test.ts index 683b2706..d5446fff 100644 --- a/src/modules/unified-orders/api/unifiedOrders.test.ts +++ b/src/modules/unified-orders/api/unifiedOrders.test.ts @@ -73,11 +73,11 @@ describe('unified orders API contract', () => { it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => { vi.mocked(http.get).mockResolvedValue({ data: { - records: [{ orderId: '90001', orderVersion: '7', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', channelCode: ' NY_EXX ', channelName: '纽约渠道', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: '0.4', expectedVolumeCbm: '0.45', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.46', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }], + records: [{ orderId: '90001', orderVersion: '7', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', channelCode: ' NY_EXX ', channelName: '纽约渠道', holdFlag: true, currentHoldReason: '待核对货款', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: '0.4', expectedVolumeCbm: '0.45', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.46', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }], page: 1, size: 20, total: 1, pages: 1, } }) await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({ - records: [{ orderId: '90001', orderVersion: 7, tenantId: '208', inboundId: '77', inboundVersion: 3, channelCode: 'NY_EXX', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: 0.4, expectedVolumeCbm: 0.45, actualGrossWeightKg: 12.5, actualVolumeCbm: 0.46, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }], + records: [{ orderId: '90001', orderVersion: 7, tenantId: '208', inboundId: '77', inboundVersion: 3, channelCode: 'NY_EXX', holdFlag: true, currentHoldReason: '待核对货款', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: 0.4, expectedVolumeCbm: 0.45, actualGrossWeightKg: 12.5, actualVolumeCbm: 0.46, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }], }) }) diff --git a/src/modules/unified-orders/api/unifiedOrders.ts b/src/modules/unified-orders/api/unifiedOrders.ts index c0be359b..61fde28d 100644 --- a/src/modules/unified-orders/api/unifiedOrders.ts +++ b/src/modules/unified-orders/api/unifiedOrders.ts @@ -119,8 +119,8 @@ export interface UnifiedOrderResource { updatedAt: string | null expectedArrivalAt: string | null feeReviewStatus: string | null - holdCargo?: boolean | null - holdReason?: string | null + holdFlag: boolean | null + currentHoldReason: string | null companyName?: string | null businessType?: string | null pickupMethod?: string | null @@ -413,6 +413,8 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource { receivingWarehouseId: id(raw.receivingWarehouseId), destinationWarehouseId: id(raw.destinationWarehouseId), inboundId: id(raw.inboundId), + holdFlag: raw.holdFlag === null || raw.holdFlag === undefined ? null : Boolean(raw.holdFlag), + currentHoldReason: optionalText(raw.currentHoldReason), inboundVersion: nullableInteger(raw.inboundVersion), relation: raw.relation ? { ...raw.relation, diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts index 808d5bf7..0aa5a1b2 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts @@ -26,6 +26,7 @@ const rows: UnifiedOrderResource[] = ['5001', '5002'].map((orderId, index) => ({ relation: null, internalOrderStatus: 'WAIT_SPLIT', customerId: `600${index}`, customerName: `客户${index + 1}`, channelCode: 'NY_EXX', channelName: '纽约渠道', receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8', inboundId: `700${index}`, inboundVersion: 1, inboundCode: `IN-${index + 1}`, expectedShipDate: null, totalPackageCount: 5, + holdFlag: false, currentHoldReason: null, locationCodes: null, actualGrossWeightKg: 10, actualVolumeCbm: 1, measurement: null, preload: null, loading: null, delivery: null, transport: null, createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null, chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null, measurementCompletedAt: null, inboundCompletedAt: null, diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts index 59cb5675..6f67f55c 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts @@ -1,18 +1,21 @@ import { flushPromises, shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations' -import { getPlatformUnifiedOrder } from '@/modules/unified-orders/api/unifiedOrders' +import { getPlatformUnifiedOrder, listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders' +import { getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound } from '@/modules/inbounds' import UnifiedOrdersView from './UnifiedOrdersView.vue' vi.mock('vue-router', () => ({ useRoute: () => ({ params: { orderId: '5001' }, query: { tenantId: '6001' } }), useRouter: () => ({ push: vi.fn() }), })) -vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) +const { canPermission } = vi.hoisted(() => ({ canPermission: vi.fn((..._permissions: string[]) => true) })) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) })) vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) })) vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) })) vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(), + getPlatformInboundHold: vi.fn(), holdPlatformInbound: vi.fn(), releasePlatformInbound: vi.fn(), InboundFeePanel: { template: '
' }, InboundMeasurementDialog: { template: '
' }, MeasurementEvidenceThumbnail: { template: '
' }, })) vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '
' } })) @@ -34,6 +37,7 @@ const detail = { internalOrderStatus: 'WAIT_SPLIT', customerId: '6001', customerName: '测试客户', channelName: '纽约渠道', receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8', inboundId: '7001', inboundVersion: 1, inboundCode: 'NYEXX2608310001', expectedShipDate: null, totalPackageCount: 2, + holdFlag: false, currentHoldReason: null, locationCodes: null, actualGrossWeightKg: null, actualVolumeCbm: null, measurement: null, preload: null, loading: null, delivery: null, transport: null, coLoadGroup: null, createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null, chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null, @@ -56,6 +60,11 @@ function mountDetail() { ApiErrorMeta: true, StatusBadge: true, 'el-button': { template: '' }, + 'el-table': { props: ['data'], template: '
' }, + 'el-table-column': { template: '
' }, + 'el-tag': { template: '' }, + 'el-empty': { props: ['description'], template: '
{{ description }}
' }, + 'el-alert': { props: ['title'], template: '
{{ title }}
' }, }, }, }) @@ -64,7 +73,22 @@ function mountDetail() { describe('UnifiedOrdersView downstream fulfillment detail', () => { beforeEach(() => { vi.clearAllMocks() + canPermission.mockImplementation(() => true) vi.mocked(getPlatformUnifiedOrder).mockResolvedValue(detail as never) + vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 0 }) + vi.mocked(getPlatformInboundHold).mockResolvedValue({ + inboundId: '7001', tenantId: '6001', holdFlag: false, currentHoldReason: null, version: 3, + canHold: true, canRelease: false, actionBlockReason: null, + history: [{ id: '8801', action: 'RELEASE', reason: '货款已核对', evidenceFileId: '9901', operatorName: '操作员A', operatedAt: '2026-09-07T10:00:00' }], + }) + vi.mocked(holdPlatformInbound).mockResolvedValue({ + inboundId: '7001', tenantId: '6001', holdFlag: true, currentHoldReason: '等待客户补款', version: 4, + canHold: false, canRelease: true, actionBlockReason: null, history: [], + }) + vi.mocked(releasePlatformInbound).mockResolvedValue({ + inboundId: '7001', tenantId: '6001', holdFlag: false, currentHoldReason: null, version: 4, + canHold: true, canRelease: false, actionBlockReason: null, history: [], + }) vi.mocked(getLoadingAssociationSummary).mockResolvedValue({ inboundId: '7001', inboundCode: 'NYEXX2608310001', loadingPlanId: null, loadingPlanNo: null, containerBookingId: null, cabinetNo: null, containerNo: null, loadingStatus: null, taskId: null, @@ -112,4 +136,53 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => { expect(wrapper.text()).not.toContain('9001') expect(wrapper.text()).not.toContain('9004') }) + + it('loads server hold capabilities and history only after opening the other tab', async () => { + const wrapper = mountDetail() + await flushPromises() + expect(getPlatformInboundHold).not.toHaveBeenCalled() + await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click') + await flushPromises() + expect(getPlatformInboundHold).toHaveBeenCalledWith('6001', '7001') + expect(wrapper.text()).toContain('未扣货') + const loaded = (wrapper.vm as unknown as { holdDetail: { history: Array<{ reason: string; evidenceFileId: string }> } }).holdDetail + expect(loaded.history).toEqual([expect.objectContaining({ reason: '货款已核对', evidenceFileId: '9901' })]) + expect(wrapper.findAll('button').map((button) => button.text())).toContain('扣货') + }) + + it('submits the required reason and server version, then refreshes detail and list', async () => { + const wrapper = mountDetail() + await flushPromises() + await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click') + await flushPromises() + const vm = wrapper.vm as unknown as { holdReason: string; openHoldDialog(action: 'HOLD'): void; submitHoldAction(): Promise } + vm.openHoldDialog('HOLD') + vm.holdReason = ' 等待客户补款 ' + await vm.submitHoldAction() + expect(holdPlatformInbound).toHaveBeenCalledWith('6001', '7001', { expectedVersion: 3, reason: '等待客户补款' }) + expect(getPlatformUnifiedOrder).toHaveBeenCalledTimes(2) + expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1) + }) + + it('stays read-only without the independent hold permission', async () => { + canPermission.mockImplementation((...permissions: string[]) => permissions[0] !== 'platform:firstleg:inbound:hold') + const wrapper = mountDetail() + await flushPromises() + await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click') + await flushPromises() + expect(getPlatformInboundHold).toHaveBeenCalledWith('6001', '7001') + expect(wrapper.text()).toContain('未扣货') + expect(wrapper.findAll('button').map((button) => button.text())).not.toEqual(expect.arrayContaining(['扣货', '解除扣货'])) + }) + + it('stays read-only and does not request hold detail before an inbound exists', async () => { + vi.mocked(getPlatformUnifiedOrder).mockResolvedValue({ ...detail, summary: { ...detail.summary, inboundId: null, inboundCode: null, holdFlag: null, currentHoldReason: null } } as never) + const wrapper = mountDetail() + await flushPromises() + await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click') + await flushPromises() + expect(getPlatformInboundHold).not.toHaveBeenCalled() + expect(wrapper.text()).toContain('订单尚未生成入库单,扣货状态仅可查看') + expect(wrapper.findAll('button').map((button) => button.text())).not.toEqual(expect.arrayContaining(['扣货', '解除扣货'])) + }) }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 3d28931c..e0c4aad8 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -79,6 +79,19 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('fbaCode: trimmed(activeFilters.fbaCode)') }) + it('keeps hold state in the other tab and consumes only frozen server capabilities', () => { + for (const marker of [ + 'summary.holdFlag', 'summary.currentHoldReason', 'getPlatformInboundHold', + 'holdDetail.value?.canHold', 'holdDetail.value?.canRelease', "can('platform:firstleg:inbound:hold')", + '扣货历史', '已关联凭证', '订单尚未生成入库单,扣货状态仅可查看', + ]) expect(source).toContain(marker) + expect(source).toContain("await Promise.all([loadDetail(true), loadOrders()])") + expect(source).not.toContain('detail.summary.holdCargo') + expect(source).not.toContain('detail.summary.holdReason') + expect(source).not.toContain('evidenceFileId: hold') + expect(source).not.toContain('type="file"') + }) + it('uses tabs as the only order-status filter and keeps advanced controls inside QueryToolbar', () => { expect(source).toContain('mainStatus: selectedStatus.value || undefined') expect(source).toContain('