统一后台状态列顺序、详情布局与测量计费展示

This commit is contained in:
2026-10-28 10:32:33 +08:00
parent 8e97fd6a9f
commit e41e9e6d3f
120 changed files with 436 additions and 238 deletions
@@ -1,8 +1,9 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiResponseError } from '@/shared/api/http'
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
import { downloadPlatformProductImage, getPlatformUnifiedOrder, listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
import { downloadPlatformInboundHoldEvidence, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from '@/modules/inbounds'
import { calculateInboundFee, confirmInboundFee, downloadPlatformInboundHoldEvidence, getLatestInboundFee, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound, sendInboundFee, uploadPlatformInboundHoldEvidence } from '@/modules/inbounds'
import UnifiedOrdersView from './UnifiedOrdersView.vue'
const { routeQuery, routerPush } = vi.hoisted(() => ({ routeQuery: { tenantId: '6001', from: '' }, routerPush: vi.fn() }))
@@ -17,6 +18,9 @@ vi.mock('@/modules/iam', async importOriginal => ({
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
calculateInboundFee: vi.fn(), confirmInboundFee: vi.fn(), sendInboundFee: vi.fn(), getLatestInboundFee: vi.fn(),
createFeeActionIdempotencyKey: vi.fn((action: string) => `fee-${action}`),
createFeeAdjustmentIdempotencyKey: vi.fn(() => 'fee-adjust'),
downloadPlatformInboundHoldEvidence: vi.fn(), getPlatformInboundHold: vi.fn(), holdPlatformInbound: vi.fn(), releasePlatformInbound: vi.fn(), uploadPlatformInboundHoldEvidence: vi.fn(),
InboundFeePanel: { template: '<div />' }, InboundMeasurementDialog: { template: '<div />' }, MeasurementEvidenceThumbnail: { template: '<div />' },
}))
@@ -168,6 +172,58 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => {
wrapper.unmount()
})
it.each([
{ confirmedVolume: 3.894, hasReference: true },
{ confirmedVolume: 4.25, hasReference: true },
{ confirmedVolume: 4.25, hasReference: false },
])('defaults billing to the saved outer volume and sends $confirmedVolume (reference: $hasReference)', async ({ confirmedVolume, hasReference }) => {
const fee = { unitPrice: 13.6, chargeableQuantity: 650.298, chargeUnit: 'KG', roundingScale: 4, rateCode: 'RATE', feeStatus: 'CALCULATED' }
vi.mocked(getLatestInboundFee).mockResolvedValue(fee as never)
if (!hasReference) vi.mocked(getLatestInboundFee).mockRejectedValueOnce(new ApiResponseError('暂无费用快照', 'A0404', 200))
vi.mocked(calculateInboundFee).mockResolvedValue(fee as never)
vi.mocked(confirmInboundFee).mockResolvedValue({ ...fee, feeStatus: 'CONFIRMED' } as never)
vi.mocked(sendInboundFee).mockResolvedValue({ ...fee, feeStatus: 'SENT' } as never)
const wrapper = mountDetail()
await flushPromises()
const vm = wrapper.vm as unknown as {
measurementDetail: Record<string, unknown>
measurementSectionModes: { outer: string; actual: string }
feeConfirmVolume: number
feeConfirmWeight: number
feeConfirmRows: { source: string; volume: number }[]
openFeeConfirmDialog(): Promise<void>
executeConfirmAndSendFee(): Promise<void>
}
vm.measurementDetail = {
actualPackageCount: 22, actualGrossWeightKg: 570.5, actualVolumeCbm: 3.6894,
items: [{ id: '9001', itemNo: 'B1', packageCount: 22, lengthCm: 62, widthCm: 52, heightCm: 52,
outerLengthCm: 63, outerWidthCm: 53, outerHeightCm: 53,
calculatedOuterLengthCm: 63, calculatedOuterWidthCm: 53, calculatedOuterHeightCm: 53,
unitVolumeCbm: 0.177, totalVolumeCbm: 3.894, volumeWeightKg: 650.298,
coefficientMode: 'ACTUAL', calculationRuleVersion: 'WAREHOUSE_OUTER_OVERRIDE_V1' }],
}
vm.measurementSectionModes.outer = 'ACTUAL'
vm.measurementSectionModes.actual = 'ACTUAL'
await vm.openFeeConfirmDialog()
expect(vm.feeConfirmRows.find(row => row.source === '实际测量数据')?.volume).toBeCloseTo(3.6894)
expect(vm.feeConfirmRows.find(row => row.source === '外箱规数据')?.volume).toBeCloseTo(3.894)
expect(vm.feeConfirmVolume).toBeCloseTo(3.894)
expect(vm.feeConfirmWeight).toBeCloseTo(570.5)
if (!hasReference) expect(calculateInboundFee).toHaveBeenCalledWith('6001', '7001', {
selectedWeightKg: 570.5, selectedVolumeCbm: 3.894, weightSource: 'ACTUAL', volumeSource: 'OUTER',
}, 'fee-calculate')
vm.feeConfirmVolume = confirmedVolume
await vm.executeConfirmAndSendFee()
expect(calculateInboundFee).toHaveBeenCalledWith('6001', '7001', {
selectedWeightKg: 570.5, selectedVolumeCbm: confirmedVolume, weightSource: 'ACTUAL', volumeSource: 'OUTER',
}, 'fee-calculate')
expect(confirmInboundFee).toHaveBeenCalledWith('6001', '7001', 'fee-confirm')
expect(sendInboundFee).toHaveBeenCalledWith('6001', '7001', 'fee-send')
wrapper.unmount()
})
it('downloads one preview for cargo rows sharing the same product image', async () => {
vi.mocked(getPlatformUnifiedOrder).mockResolvedValue({
...detail,
@@ -508,7 +508,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('const outerTotals = measurementTotals(outerMeasurementRows.value)')
expect(source).toContain("ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则')")
expect(source).toContain("weightSource: 'ACTUAL'")
expect(source).toContain("volumeSource: 'ACTUAL'")
expect(source).toContain("volumeSource: 'OUTER'")
expect(source).not.toContain('测量审核通过后才能确认并发送费用')
expect(source).not.toContain("reviewStatus !== 'APPROVED'")
expect(source).toContain('const hasActualMeasurementData = computed(() => (measurementDetail.value?.items?.length ?? 0) > 0)')
@@ -530,7 +530,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('const measuredNetWeight = Number(measurementDetail.value?.actualNetWeightKg)')
expect(source).not.toContain('const actualVolume = Number(measurementDetail.value?.actualVolumeCbm)')
expect(source).toContain('feeConfirmWeight.value = Number(actualTotals.gross) > 0')
expect(source).toContain('feeConfirmVolume.value = Number(actualTotals.volume) > 0')
expect(source).toContain('feeConfirmVolume.value = Number(outerTotals.volume)')
expect(source).toContain("{ source: '实际测量数据', count: actualTotals.count, weight: actualWeight, volume: actualVolume }")
const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange'))
expect(sendFeeSource).not.toContain('saveInboundMeasurementDraft')
@@ -673,8 +673,10 @@ const columns = computed<DataTableColumn[]>(() => [
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value && !exportSubmitting.value,
} satisfies DataTableColumn] : []),
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 160, slotName: 'inboundCode' },
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 120, slotName: 'mainStatus', afterColumn: 'inboundCode' },
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 150, slotName: 'measurementReviewStatus', afterColumn: 'inboundCode' },
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field', defaultVisible: false, afterColumn: 'inboundCode' },
{ key: 'orderType', label: '订单类型', minWidth: 120, slotName: 'orderType' },
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 120, slotName: 'mainStatus' },
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 120, slotName: 'field' },
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 100, slotName: 'field' },
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 100, slotName: 'field' },
@@ -687,13 +689,11 @@ const columns = computed<DataTableColumn[]>(() => [
{ key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托数', minWidth: 90, slotName: 'field' },
{ key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间', minWidth: 140, slotName: 'field' },
{ key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 160, slotName: 'field', defaultVisible: false },
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 150, slotName: 'measurementReviewStatus' },
{ key: 'actualGrossWeightKg', prop: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 120, slotName: 'field' },
{ key: 'totalNetWeightKg', prop: 'totalNetWeightKg', label: '货物净重(kg)', minWidth: 120, slotName: 'field' },
{ key: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
{ key: 'expectedShipDate', prop: 'expectedShipDate', label: '预计出运日期', minWidth: 150, slotName: 'field' },
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field', defaultVisible: false },
{ key: 'skuNos', prop: 'skuNos', label: 'SKU 编码', minWidth: 180, slotName: 'field', defaultVisible: false },
{ key: 'totalQuantity', prop: 'totalQuantity', label: '货物总数量', minWidth: 120, slotName: 'field', defaultVisible: false },
{ key: 'expressCompany', prop: 'expressCompany', label: '快递公司', minWidth: 140, slotName: 'field', defaultVisible: false },
@@ -1752,7 +1752,7 @@ async function openFeeConfirmDialog() {
feeConfirmWeight.value = Number(actualTotals.gross) > 0
? Number(actualTotals.gross)
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
feeConfirmVolume.value = Number(actualTotals.volume) > 0 ? Number(actualTotals.volume) : null
feeConfirmVolume.value = Number(outerTotals.volume)
feeConfirmReference.value = null
feeConfirmPriceMissing.value = false
feeConfirmUnitPrice.value = null
@@ -1773,7 +1773,7 @@ async function openFeeConfirmDialog() {
selectedWeightKg: Number(feeConfirmWeight.value),
selectedVolumeCbm: Number(feeConfirmVolume.value),
weightSource: 'ACTUAL',
volumeSource: 'ACTUAL',
volumeSource: 'OUTER',
}, createFeeActionIdempotencyKey('calculate'))
}
feeConfirmReference.value = snapshot
@@ -1843,7 +1843,7 @@ async function executeConfirmAndSendFee() {
let snapshot: InboundFeeSnapshot
try {
snapshot = await calculateInboundFee(tenantId, inboundId, {
selectedWeightKg, selectedVolumeCbm, weightSource: 'ACTUAL', volumeSource: 'ACTUAL',
selectedWeightKg, selectedVolumeCbm, weightSource: 'ACTUAL', volumeSource: 'OUTER',
}, attempt.calculateKey)
} catch (error) {
if (getApiErrorDetails(error, '费用试算失败').code !== 'A0404') throw error