This commit is contained in:
2026-10-20 17:24:50 +08:00
parent f155976f52
commit 3598310237
18 changed files with 203 additions and 120 deletions
@@ -21,7 +21,7 @@ export const unifiedOrderStatusOptions: Array<{ value: UnifiedOrderMainStatus; l
{ value: 'WEIGHT_CONFIRMED', label: '已确认数据' },
{ value: 'WAIT_CUSTOMS_DOCS', label: '待上传资料' },
{ value: 'WAIT_PALLETIZE', label: '一件代发打托' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'WAIT_SPLIT', label: '待装柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'EXPORT_CUSTOMS', label: '出口报关' },
@@ -302,7 +302,7 @@ export interface UnifiedOrderQuery {
tenantId?: string
keyword?: string
orderNo?: string
mainStatus?: UnifiedOrderMainStatus
mainStatus?: UnifiedOrderMainStatus | 'MEASUREMENT_REVIEW'
customerId?: string
receivingWarehouseId?: string
destinationWarehouseId?: string
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import type { UnifiedOrderResource } from './api/unifiedOrders'
import { toMeasurementReviewTask } from './measurementReviewTasks'
describe('measurement review task identity', () => {
it('keeps original tenant, inbound identity and optimistic version for approval', () => {
const task = toMeasurementReviewTask({
orderId: '9007199254740995', tenantId: '9007199254740993', inboundId: '9007199254740994', inboundVersion: 0,
inboundCode: 'IN-1', customerId: '9007199254740993', fulfillmentType: 'ONE_CLICK_DROPSHIP',
measurement: { reviewStatus: 'PENDING', actualGrossWeightKg: 12, actualVolumeCbm: 0.2, chargeableWeightKg: 13, completedAt: '2026-10-20T08:00:00' },
} as UnifiedOrderResource)
expect(task).toMatchObject({ id: '9007199254740994', tenantId: '9007199254740993', orderId: '9007199254740995', version: 0, inboundCode: 'IN-1', measureReviewStatus: 'PENDING', fulfillmentType: 'ONE_CLICK_DROPSHIP', actualGrossWeightKg: 12, actualVolumeCbm: 0.2 })
})
it('rejects an order without an inbound identity instead of approving a different resource', () => {
expect(() => toMeasurementReviewTask({ orderId: '1', inboundId: null } as UnifiedOrderResource)).toThrow('缺少入库单标识')
})
})
@@ -0,0 +1,66 @@
import type { InboundOrderSummary } from '@/modules/inbounds'
import type { UnifiedOrderResource } from './api/unifiedOrders'
/** 将统一订单查询的审核待办映射到现有测量审核操作,保留原始客户、入库单与版本。 */
export function toMeasurementReviewTask(row: UnifiedOrderResource): InboundOrderSummary {
if (row.inboundId == null) throw new Error('测量审核订单缺少入库单标识,请刷新后重试')
return {
id: String(row.inboundId),
tenantId: String(row.tenantId),
orderId: String(row.orderId),
orderNo: row.orderNo,
inboundCode: row.inboundCode,
processStatus: 'MEASURE_PENDING',
orderStatusSnapshot: row.internalOrderStatus,
measureReviewStatus: row.measurement?.reviewStatus ?? null,
feeStatus: null,
feeReviewStatus: row.feeReviewStatus,
notificationStatus: null,
version: row.inboundVersion,
expectedArrivalAt: row.expectedArrivalAt,
customerId: row.customerId == null ? null : String(row.customerId),
customerName: row.customerName,
companyId: null,
companyName: row.companyName ?? null,
warehouseId: row.receivingWarehouseId == null ? null : String(row.receivingWarehouseId),
warehouseName: row.receivingWarehouseName,
deliveryWarehouseId: row.destinationWarehouseId == null ? null : String(row.destinationWarehouseId),
deliveryWarehouseName: row.destinationWarehouseName,
businessType: row.businessType ?? null,
fulfillmentType: row.fulfillmentType ?? null,
channelCode: row.channelCode,
channelName: row.channelName,
taxMode: row.taxMode ?? null,
containerMode: null,
customsInspection: row.customsInspection ?? null,
destinationType: row.destinationType ?? null,
destinationWarehouseId: row.destinationWarehouseId == null ? null : String(row.destinationWarehouseId),
destinationCode: null,
destinationName: row.destinationWarehouseName,
holdFlag: row.holdFlag,
currentHoldReason: row.currentHoldReason,
totalPackageCount: row.totalPackageCount,
actualGrossWeightKg: row.measurement?.actualGrossWeightKg ?? row.actualGrossWeightKg,
actualNetWeightKg: row.totalNetWeightKg,
actualVolumeCbm: row.measurement?.actualVolumeCbm ?? row.actualVolumeCbm,
volumeWeightKg: row.volumetricWeightKg,
chargeableWeightKg: row.measurement?.chargeableWeightKg ?? null,
chargeableVolumeCbm: row.chargeableVolumeCbm,
palletCount: row.totalPalletCount,
locationCodes: row.locationCodes,
measureCompletedAt: row.measurement?.completedAt ?? row.measurementCompletedAt,
inboundedAt: row.inboundCompletedAt,
expressCompanyCode: null,
expressCompanyName: row.expressCompany,
expressTrackingNo: row.expressTrackingNo,
loadingStatus: row.transport?.loadingStatus ?? null,
containerNo: row.transport?.containerNo ?? null,
cabinetNo: row.transport?.cabinetNo ?? null,
vesselName: row.transport?.vesselName ?? null,
voyageNo: row.transport?.voyageNo ?? null,
sailingAt: row.transport?.effectiveSailingAt ?? null,
portArrivalAt: row.transport?.effectivePortArrivalAt ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}
}
@@ -16,7 +16,12 @@ vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
InboundTaskList: { name: 'InboundTaskList', props: ['mode', 'title'], template: '<section><slot name="status-tabs" /></section>' },
InboundTaskList: {
name: 'InboundTaskList', props: ['mode', 'title', 'taskLoader'],
methods: { reload() { return this.taskLoader({ page: 1, size: 20 }) } },
mounted() { void this.reload() },
template: '<section><slot name="filters" :loading="false" /><slot name="status-tabs" /></section>',
},
}))
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
@@ -36,7 +41,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
unifiedOrderStatusOptions: [
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'WAIT_SPLIT', label: '待装柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'DELIVERING', label: '派送中' },
@@ -76,6 +81,37 @@ describe('UnifiedOrdersView list request coordination', () => {
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
})
it('shares applied filters, advanced fields and reset across measurement review and other tabs', async () => {
const wrapper = mountView()
await flushPromises()
const model = { receivingWarehouseId: '9007199254740993', destinationWarehouseId: '200', inboundCode: ' IN-9 ', tenantId: '300', channelName: '海派', containerNo: 'CONT-1', cabinetNo: 'CAB-1', loaded: 'false', fullContainer: 'true', expectedArrivalDate: '2026-10-20', sailingDate: '2026-11-01', feeReviewStatus: 'APPROVED', measurementReviewStatus: 'PENDING', fbaCode: 'SKU-1', createdRange: ['2026-10-01', '2026-10-20'] }
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), ...model })
await flushPromises()
toolbar.vm.$emit('search')
await flushPromises()
const ordinaryQuery = vi.mocked(listPlatformUnifiedOrders).mock.lastCall![0]
await wrapper.findAll('[role="tab"]').find(tab => tab.text().startsWith('测量数据审核'))!.trigger('click')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith({ ...ordinaryQuery, mainStatus: 'MEASUREMENT_REVIEW' })
const reviewToolbar = wrapper.getComponent({ name: 'QueryToolbar' })
expect(reviewToolbar.props('modelValue')).toMatchObject(model)
expect(reviewToolbar.attributes('preference-key')).toBe('unified-orders-query-v2')
expect(reviewToolbar.attributes('available-fields')).toBeDefined()
reviewToolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'MEASUREMENT_REVIEW', receivingWarehouseId: undefined, inboundCode: undefined, channelName: undefined, tenantId: undefined }))
reviewToolbar.vm.$emit('update:modelValue', { ...reviewToolbar.props('modelValue'), inboundCode: 'IN-REVIEW' })
await flushPromises()
reviewToolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'MEASUREMENT_REVIEW', inboundCode: 'IN-REVIEW' }))
await wrapper.findAll('[role="tab"]').find(tab => tab.text().startsWith('待审核'))!.trigger('click')
await flushPromises()
expect(wrapper.getComponent({ name: 'QueryToolbar' }).props('modelValue')).toMatchObject({ inboundCode: 'IN-REVIEW' })
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'PENDING_REVIEW', inboundCode: 'IN-REVIEW' }))
})
it('shows only supported counts, keeps all text-only and never scopes counts by status or pagination', async () => {
vi.mocked(getPlatformUnifiedOrderStatusCounts).mockResolvedValue({ counts: { PENDING_REVIEW: 12, WAIT_MEASURE: 0, SIGNED: 3 } })
const wrapper = mountView()
@@ -166,13 +202,13 @@ describe('UnifiedOrdersView list request coordination', () => {
await flushPromises()
expect(wrapper.getComponent({ name: 'InboundTaskList' }).props()).toMatchObject({ mode: 'review', title: '订单列表' })
expect(wrapper.get('[aria-selected="true"]').text()).toBe('测量数据审核')
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === '待测量数据')!.trigger('click')
await flushPromises()
expect(wrapper.findComponent({ name: 'InboundTaskList' }).exists()).toBe(false)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'WAIT_MEASURE' }))
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
})
it('does not apply draft filters to pagination until query is submitted', async () => {
@@ -62,7 +62,7 @@ describe('UnifiedOrdersView contract', () => {
it('exposes the product main statuses including dropship palletizing', () => {
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
'待审核', '待测量数据', '待客户确认', '已确认数据', '待上传资料', '一件代发打托', '待分柜', '装柜中', '装柜完成', '出口报关', '已开船/待清关', '待拆柜', '待排车', '出库单', '派送中', '签收完成',
'待审核', '待测量数据', '待客户确认', '已确认数据', '待上传资料', '一件代发打托', '待装柜', '装柜中', '装柜完成', '出口报关', '已开船/待清关', '待拆柜', '待排车', '出库单', '派送中', '签收完成',
])
for (const legacyLabel of ['差异处理中', '进入分柜']) {
expect(source).not.toContain(legacyLabel)
@@ -27,6 +27,7 @@ import {
type LegacyOrderImportResult,
} from '@/modules/unified-orders/api/unifiedOrders'
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
import { toMeasurementReviewTask } from '../measurementReviewTasks'
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
import OrderDeclarationEditorDialog from '@/modules/unified-orders/components/OrderDeclarationEditorDialog.vue'
@@ -330,6 +331,7 @@ const showAdvancedFilters = ref(false)
const filters = ref<FilterModel>(emptyFilters())
const appliedFilters = ref<FilterModel>(emptyFilters())
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
const measurementReviewList = ref<InstanceType<typeof InboundTaskList> | null>(null)
const measurementReviewSelected = ref(false)
const selectedTab = computed(() => measurementReviewSelected.value ? 'MEASUREMENT_REVIEW' : selectedStatus.value)
const statusTabList = ref<HTMLElement | null>(null)
@@ -1641,9 +1643,14 @@ function tabCount(value: UnifiedOrderMainStatus | '' | 'MEASUREMENT_REVIEW') {
return value ? statusCounts.value[value] : undefined
}
async function loadMeasurementReviewTasks(pagination: { page: number; size: number }) {
const result = await listPlatformUnifiedOrders({ ...orderCountFilters(), mainStatus: 'MEASUREMENT_REVIEW', ...pagination })
return { ...result, records: result.records.map(toMeasurementReviewTask) }
}
async function loadOrders(refreshCounts = true) {
if (refreshCounts) void loadStatusCounts()
if (measurementReviewSelected.value) return
if (measurementReviewSelected.value) { await measurementReviewList.value?.reload(); return }
selectedApprovalOrders.value = []
selectedOrders.value = []
loading.value = true
@@ -2777,8 +2784,9 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<template v-else>
<div v-if="measurementReviewSelected" id="unified-orders-results" role="tabpanel">
<InboundTaskList mode="review" title="订单列表" @changed="loadStatusCounts">
<template #status-tabs>
<InboundTaskList ref="measurementReviewList" mode="review" title="订单列表" :task-loader="loadMeasurementReviewTasks" @changed="loadStatusCounts">
<template #filters="{ loading: reviewLoading }">
<div class="orders-query-panel integrated-query-group">
<div ref="statusTabList" class="status-tabs density-status-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
<button
v-for="(tab, index) in statusTabs"
@@ -2799,6 +2807,15 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<el-button link type="primary" @click="loadStatusCounts">重试数量</el-button>
<ApiErrorMeta :details="statusCountsError" />
</div>
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :loading="reviewLoading" :search-on-reset="false" @search="search" @reset="reset">
<template #actions>
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
<ChevronUp v-if="showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
</el-button>
</template>
</QueryToolbar>
</div>
</template>
</InboundTaskList>
</div>