This commit is contained in:
2026-10-20 21:33:46 +08:00
parent 00b1fefecb
commit 8afd04aa55
23 changed files with 449 additions and 69 deletions
@@ -30,7 +30,7 @@ export const unifiedOrderStatusOptions: Array<{ value: UnifiedOrderMainStatus; l
{ value: 'WAIT_DISPATCH', label: '待排车' },
{ value: 'OUTBOUND_DOCUMENT', label: '出库单' },
{ value: 'DELIVERING', label: '派送中' },
{ value: 'SIGNED', label: '签收完成' },
{ value: 'SIGNED', label: '派送完成' },
]
export interface UnifiedOrderMeasurementSummary {
@@ -694,7 +694,7 @@ export async function listPlatformUnifiedOrderFeeDetails(accountId: string, inbo
}
export function unifiedOrderStatusLabel(status: UnifiedOrderMainStatus) {
if (status === 'DELIVERED') return '签收完成'
if (status === 'DELIVERED') return '派送完成'
if (status === 'COMPLETED') return '已完成'
return unifiedOrderStatusOptions.find((item) => item.value === status)?.label || '未知状态'
}
@@ -106,6 +106,56 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => {
})
})
it('keeps unchanged actual and outer volumes on the same server snapshot with three decimal places', async () => {
const wrapper = mountDetail()
await flushPromises()
const vm = wrapper.vm as unknown as {
measurementDetail: Record<string, unknown>
measurementSectionModes: { outer: string; actual: string }
outerMeasurementRows: Record<string, unknown>[]
actualMeasurementRows: Record<string, unknown>[]
measurementTotals(rows: Record<string, unknown>[]): { volume: number; weightVolume: number }
measurementValue(row: Record<string, unknown>, key: string): string
formatMeasurementVolume(value: number | null): string
}
const items = [
{ lengthCm: 60, widthCm: 50, heightCm: 50, packageCount: 10, unitVolumeCbm: 0.15, totalVolumeCbm: 1.5 },
{ lengthCm: 61, widthCm: 51, heightCm: 51, packageCount: 9, unitVolumeCbm: 0.159, totalVolumeCbm: 1.431 },
{ lengthCm: 50, widthCm: 40, heightCm: 40, packageCount: 1, unitVolumeCbm: 0.08, totalVolumeCbm: 0.08 },
].map((row, index) => ({
...row, itemNo: index + 1, coefficientMode: 'ACTUAL',
calculatedOuterLengthCm: row.lengthCm, calculatedOuterWidthCm: row.widthCm, calculatedOuterHeightCm: row.heightCm,
}))
vm.measurementDetail = { items }
vm.measurementSectionModes.outer = 'ACTUAL'
vm.measurementSectionModes.actual = 'ACTUAL'
await flushPromises()
for (const rows of [vm.outerMeasurementRows, vm.actualMeasurementRows]) {
expect(rows.map(row => vm.measurementValue(row, 'totalVolumeCbm'))).toEqual(['1.500', '1.431', '0.080'])
expect(rows.map(row => vm.measurementValue(row, 'unitVolumeCbm'))).toEqual(['0.150', '0.159', '0.080'])
expect(vm.formatMeasurementVolume(vm.measurementTotals(rows).volume)).toBe('3.011')
expect(vm.formatMeasurementVolume(vm.measurementTotals(rows).weightVolume)).toBe('3.011')
}
expect(vm.formatMeasurementVolume(3)).toBe('3.000')
expect(vm.formatMeasurementVolume(0)).toBe('0.000')
expect(vm.formatMeasurementVolume(null)).toBe('—')
// 仓库修正的外箱仍与实测尺寸分开,不能把外箱计费快照当成原始实测体积。
vm.measurementDetail = { items: [{ ...items[1], outerLengthCm: 62,
calculatedOuterLengthCm: 62, unitVolumeCbm: 0.1613, totalVolumeCbm: 1.4517,
calculationRuleVersion: 'WAREHOUSE_OUTER_OVERRIDE_V1' }] }
await flushPromises()
expect(vm.measurementValue(vm.outerMeasurementRows[0]!, 'totalVolumeCbm')).toBe('1.452')
expect(vm.measurementValue(vm.actualMeasurementRows[0]!, 'totalVolumeCbm')).toBe('1.428')
vm.measurementDetail = { items: [{ ...items[1], coefficientMode: 'ADD', calculatedOuterLengthCm: 62,
calculatedOuterWidthCm: 52, calculatedOuterHeightCm: 52, unitVolumeCbm: 0.168, totalVolumeCbm: 1.512 }] }
await flushPromises()
expect(vm.measurementValue(vm.actualMeasurementRows[0]!, 'totalVolumeCbm')).toBe('1.428')
wrapper.unmount()
})
it('downloads one preview for cargo rows sharing the same product image', async () => {
vi.mocked(getPlatformUnifiedOrder).mockResolvedValue({
...detail,
@@ -45,7 +45,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'DELIVERING', label: '派送中' },
{ value: 'SIGNED', label: '签收完成' },
{ value: 'SIGNED', label: '派送完成' },
],
}))
@@ -121,7 +121,7 @@ describe('UnifiedOrdersView list request coordination', () => {
expect(tabs[1]!.text()).toBe('待审核(12)')
expect(tabs[2]!.text()).toBe('待测量数据(0)')
expect(tabs[3]!.text()).toBe('测量数据审核')
expect(tabs.at(-1)!.text()).toBe('签收完成(3)')
expect(tabs.at(-1)!.text()).toBe('派送完成(3)')
expect(tabs.map((tab) => tab.text())).not.toContain('派送完成')
expect(tabs.map((tab) => tab.text())).not.toContain('已完成')
await tabs[1]!.trigger('click')
@@ -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)
@@ -500,10 +500,18 @@ function calculatedMeasurementRow(
const strategyDimensions = matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))
const [lengthCm, widthCm, heightCm] = strategyDimensions
const packageCount = Number(source.packageCount)
const unitVolumeCbm = mode === 'ADD' && rule && !hasOuterOverride
const savedDimensions = [source.calculatedOuterLengthCm, source.calculatedOuterWidthCm, source.calculatedOuterHeightCm]
// 不调整且尺寸与落库快照一致时沿用服务端体积,避免历史三位精度与预览四位取整产生差额。
const useSavedVolume = !customerSource && mode === 'ACTUAL'
&& measurementCalculationModeForRow(source) === 'ACTUAL'
&& savedDimensions.every((value, index) => value != null
&& Math.abs(Number(value) - strategyDimensions[index]!) < 0.000001)
&& Number(source.unitVolumeCbm) > 0 && Number(source.totalVolumeCbm) > 0
const unitVolumeCbm = useSavedVolume ? Number(source.unitVolumeCbm) : mode === 'ADD' && rule && !hasOuterOverride
? Math.round((lengthCm * widthCm * heightCm / 1_000_000) * 1000) / 1000
: roundedUp(lengthCm * widthCm * heightCm / 1_000_000, 4)
const totalVolumeCbm = Number.isFinite(packageCount) ? roundedUp(unitVolumeCbm * packageCount, 4) : null
const totalVolumeCbm = useSavedVolume ? Number(source.totalVolumeCbm)
: Number.isFinite(packageCount) ? roundedUp(unitVolumeCbm * packageCount, 4) : null
const fallbackMeasuredUnitWeight = !customerSource && Number.isFinite(Number(measurementDetail.value?.actualGrossWeightKg))
&& Number(measurementDetail.value?.actualPackageCount) > 0
? Number(measurementDetail.value?.actualGrossWeightKg) / Number(measurementDetail.value?.actualPackageCount)
@@ -517,7 +525,8 @@ function calculatedMeasurementRow(
...source,
lengthCm, widthCm, heightCm, unitWeightKg, unitVolumeCbm, totalWeightKg, totalVolumeCbm,
girthCm: roundedUp(lengthCm + 2 * (widthCm + heightCm), 3),
volumeWeightKg: totalVolumeCbm == null ? null : roundedUp(totalVolumeCbm * 167, 3),
volumeWeightKg: useSavedVolume && Number(source.volumeWeightKg) > 0 ? Number(source.volumeWeightKg)
: totalVolumeCbm == null ? null : roundedUp(totalVolumeCbm * 167, 3),
remark: customerSource ? source.cargoRemark : source.remark,
}
}
@@ -866,6 +875,11 @@ function formatNumber(value: number | null | undefined, digits = 2) {
return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits })
}
function formatMeasurementVolume(value: number | null | undefined) {
return value == null || !Number.isFinite(value) ? '—'
: value.toLocaleString('zh-CN', { minimumFractionDigits: 3, maximumFractionDigits: 3 })
}
function partyValue(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'RECEIVER', key: string) {
const party = parties.find((item) => String(item.partyType || '').toUpperCase() === type)
const value = party?.[key]
@@ -1020,11 +1034,12 @@ function measurementValue(row: Record<string, unknown>, key: string, customerSou
if (customerSource && key === 'unitVolumeCbm') {
const total = Number(row.volumeCbm)
const count = Number(row.packageCount)
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatNumber(total / count, 6) : '—'
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatMeasurementVolume(total / count) : '—'
}
if (customerSource && key === 'remark') return String(row.cargoRemark || '—')
const value = row[key]
if (value === null || value === undefined || value === '') return '—'
if (key === 'unitVolumeCbm' || key === 'totalVolumeCbm') return formatMeasurementVolume(Number(value))
return typeof value === 'number' ? formatNumber(value, 6) : String(value)
}
@@ -2660,17 +2675,17 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</LoadErrorState>
<template v-else>
<section class="measurement-section">
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span v-if="measurementCollapsed.customer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(customerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(customerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.customer" aria-label="客户下单数据计算方式" @change="updateMeasurementSectionMode('customer', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span v-if="measurementCollapsed.customer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).weightVolume) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.customer" aria-label="客户下单数据计算方式" @change="updateMeasurementSectionMode('customer', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
<div v-if="!measurementCollapsed.customer">
<div class="detail-table-wrap"><el-table :data="customerMeasurementRows" border stripe empty-text="暂无客户下单数据"><el-table-column v-for="column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(customerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(customerMeasurementRows).weightVolume, 6) }}</span></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(customerMeasurementRows).weightVolume) }}</span></div>
</div>
</section>
<section class="measurement-section">
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span v-if="measurementCollapsed.outer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button></div>
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span v-if="measurementCollapsed.outer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).weightVolume) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button></div>
<div v-if="!measurementCollapsed.outer">
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="outerMeasurementRows" border stripe empty-text="暂无外箱规数据"><el-table-column label="#" prop="itemNo" min-width="70" /><el-table-column v-for="column in outerColumns.filter((item) => item.key !== 'itemNo')" :key="column.key" :label="column.label" :prop="column.key" min-width="150"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(outerMeasurementRows).weightVolume) }}</span></div>
</div>
</section>
<div class="measurement-rule-selector">
@@ -2683,7 +2698,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<span v-if="!measurementStrategyEditable" class="measurement-rule-selector__hint">{{ measurementStrategyLockedHint }}</span>
</div>
<section class="measurement-section">
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span v-if="measurementCollapsed.actual" class="measurement-section-summary"><span>总件数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(actualMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.actual" aria-label="实际测量数据计算方式" @change="updateMeasurementSectionMode('actual', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span v-if="measurementCollapsed.actual" class="measurement-section-summary"><span>总件数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).weightVolume) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.actual" aria-label="实际测量数据计算方式" @change="updateMeasurementSectionMode('actual', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
<div v-if="!measurementCollapsed.actual">
<div v-loading="measurementLoading" class="detail-table-wrap">
<el-table :data="actualMeasurementRows" row-key="id" border stripe empty-text="暂无实际测量数据">
@@ -2721,7 +2736,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</el-table-column>
</el-table>
</div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(actualMeasurementRows).weightVolume, 6) }}</span></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).weightVolume) }}</span></div>
<section class="weight-evidence" aria-label="称重图片与服务端确认重量">
<div class="weight-evidence__heading">
<div><h4>称重图片与确认重量</h4></div>