This commit is contained in:
2026-09-03 11:13:28 +08:00
parent 9b3664c532
commit 2f7c122c44
5 changed files with 334 additions and 66 deletions
@@ -70,8 +70,25 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
actualVolumeCbm: 0.6,
volumeWeightKg: 100.2,
actualGoodsName: '蓝牙音箱',
latestDeclaration: {
referenceId: 'DOC-001',
totalPackageCount: 10,
grossWeightKg: 118.5,
volumeCbm: 0.8,
goodsName: '蓝牙音箱',
items: [{
inboundDetailId: '8101',
itemNo: 1,
cargoName: '蓝牙音箱',
packageCount: 10,
lengthCm: 50,
widthCm: 40,
heightCm: 30,
}],
},
items: [{
id: '9101',
inboundDetailId: '8101',
boxCode: 'RK202608040001#10-1',
itemNo: '001',
boxSpecification: '50×40×30cm',
@@ -162,6 +179,33 @@ describe('InboundMeasurementPanel', () => {
expect(wrapper.find('button[aria-label="预览测量图片"]').exists()).toBe(true)
})
it('warns when declared dimensions or gross weight differ from measurements by at least ten percent', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({
actualGrossWeightKg: 132,
items: [{
...measurement().items[0],
lengthCm: 55,
widthCm: 36,
}],
}))
const wrapper = await mountPanel()
const alert = wrapper.find('.measurement-deviation-alert')
expect(alert.exists()).toBe(true)
expect(alert.text()).toContain('下单箱规与测量数据差异达到 10%')
expect(alert.text()).toContain('长偏差 10%')
expect(alert.text()).toContain('下单 50 cm → 实测 55 cm')
expect(alert.text()).toContain('宽偏差 10%')
expect(alert.text()).toContain('重量偏差 11.4%')
expect(alert.text()).not.toContain('高偏差')
})
it('does not warn when every declared and measured value differs by less than ten percent', async () => {
const wrapper = await mountPanel()
expect(wrapper.find('.measurement-deviation-alert').exists()).toBe(false)
})
it('opens the manual carton entry form in an editable measurement state', async () => {
const wrapper = await mountPanel()
const button = wrapper.findAll('button').find((candidate) => candidate.text().includes('手动录入箱规'))
@@ -113,6 +113,64 @@ const registrationOpen = computed(() => hasContext.value && canView.value && Boo
const requiredPermission = computed(() => canEdit.value ? 'platform:firstleg:inbound:measure:edit' : null)
const sourceLabel = computed(() => ({ THIRD_PARTY: '第三方设备', MANUAL: '平台人工', OPENAPI: '第三方设备' }[detail.value?.sourceType || ''] || (detail.value?.sourceType ? '未知来源' : '尚未测量')))
interface MeasurementDeviationWarning {
key: string
text: string
}
const measurementDeviationWarnings = computed<MeasurementDeviationWarning[]>(() => {
const resource = detail.value
if (!resource) return []
const warnings: MeasurementDeviationWarning[] = []
const declaredItems = resource.latestDeclaration?.items || []
const dimensions = [
{ key: 'lengthCm', label: '长' },
{ key: 'widthCm', label: '宽' },
{ key: 'heightCm', label: '高' },
] as const
for (const dimension of dimensions) {
const differences = resource.items.flatMap((measuredItem) => {
const declaredItem = declaredItems.find((candidate) => (
measuredItem.inboundDetailId != null
&& candidate.inboundDetailId != null
&& String(candidate.inboundDetailId) === String(measuredItem.inboundDetailId)
)) || declaredItems.find((candidate) => candidate.itemNo != null && Number(candidate.itemNo) === Number(measuredItem.itemNo))
if (!declaredItem) return []
const declaredValue = declaredItem[dimension.key]
const measuredValue = measuredItem[dimension.key]
const ratio = deviationRatio(measuredValue, declaredValue)
if (ratio == null || ratio < 0.1 - 1e-9) return []
return [{
measuredItem,
declaredValue,
measuredValue,
ratio,
}]
}).sort((left, right) => right.ratio - left.ratio)
const largest = differences[0]
if (!largest) continue
const boxLabel = largest.measuredItem.boxCode || `第 ${largest.measuredItem.itemNo} 条箱规`
const countSuffix = differences.length > 1 ? `,共 ${differences.length} 条箱规` : ''
warnings.push({
key: dimension.key,
text: `${dimension.label}偏差 ${formatPercentage(largest.ratio)}(${boxLabel}:下单 ${formatNumber(largest.declaredValue)} cm → 实测 ${formatNumber(largest.measuredValue)} cm${countSuffix})`,
})
}
const declaredGrossWeight = resource.latestDeclaration?.grossWeightKg ?? resource.originalGrossWeightKg
const weightRatio = deviationRatio(resource.actualGrossWeightKg, declaredGrossWeight)
if (weightRatio != null && weightRatio >= 0.1 - 1e-9) {
warnings.push({
key: 'grossWeightKg',
text: `重量偏差 ${formatPercentage(weightRatio)}(下单 ${formatNumber(declaredGrossWeight)} kg → 实测 ${formatNumber(resource.actualGrossWeightKg)} kg)`,
})
}
return warnings
})
const { captureBaseline } = useUnsavedWorkRegistration({
key: 'p2-inbound-measurement-editor',
label: computed(() => `入仓码 ${detail.value?.inboundCode || '待补充'} 的测量草稿`),
@@ -419,6 +477,15 @@ function formatNumber(value: number | null | undefined, maximumFractionDigits =
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits }).format(value)
}
function deviationRatio(actual: number | null | undefined, declared: number | null | undefined) {
if (actual == null || declared == null || !Number.isFinite(actual) || !Number.isFinite(declared) || declared <= 0) return null
return Math.abs(actual - declared) / Math.abs(declared)
}
function formatPercentage(ratio: number) {
return `${formatNumber(ratio * 100, 1)}%`
}
function formatDifference(actual: number | null | undefined, declared: number | null | undefined, maximumFractionDigits = 3, unit = '') {
if (actual == null || declared == null || !Number.isFinite(actual) || !Number.isFinite(declared)) return null
const difference = actual - declared
@@ -539,6 +606,17 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
</div>
</header>
<el-alert
v-if="measurementDeviationWarnings.length"
class="measurement-deviation-alert"
type="warning"
:closable="false"
show-icon
title="下单箱规与测量数据差异达到 10%"
>
<p v-for="warning in measurementDeviationWarnings" :key="warning.key">{{ warning.text }}</p>
</el-alert>
<h4 class="measurement-subheading">平台实测</h4>
<dl class="measurement-metrics" aria-label="平台实测结果汇总">
<div :class="{ 'measurement-metric--warning': !packageCountMatched }">
@@ -702,6 +780,8 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
.measurement-summary-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.measurement-summary-context { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 8px 20px; }
.measurement-summary-header span { color: var(--cc-muted); font-size: 11px; }
.measurement-deviation-alert { margin: 0; width: auto; }
.measurement-deviation-alert p { margin: 3px 0 0; line-height: 1.55; }
.measurement-summary-context p { display: inline-flex; min-width: 0; align-items: baseline; gap: 6px; margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 12px; font-weight: 600; }
.measurement-summary-status { display: flex; flex: 0 0 auto; align-items: center; gap: 9px; }
.measurement-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 2px 0; }
+21
View File
@@ -13,6 +13,7 @@ export type InboundMeasurementProcessStatus =
export interface InboundMeasurementItem {
id: MeasurementIdentifier
inboundDetailId?: MeasurementIdentifier | null
boxCode: string | null
itemNo: string
boxSpecification: string
@@ -36,6 +37,25 @@ export interface InboundMeasurementItem {
remark: string | null
}
export interface InboundMeasurementDeclarationItem {
inboundDetailId: MeasurementIdentifier | null
itemNo: number | null
cargoName: string | null
packageCount: number | null
lengthCm: number | null
widthCm: number | null
heightCm: number | null
}
export interface InboundMeasurementDeclarationSnapshot {
referenceId: string | null
totalPackageCount: number | null
grossWeightKg: number | null
volumeCbm: number | null
goodsName: string | null
items: InboundMeasurementDeclarationItem[]
}
export interface InboundMeasurementWeightResult {
id: MeasurementIdentifier
fileId: MeasurementIdentifier
@@ -88,6 +108,7 @@ export interface InboundMeasurementDetail {
actualVolumeCbm: number | null
volumeWeightKg: number | null
actualGoodsName: string | null
latestDeclaration?: InboundMeasurementDeclarationSnapshot | null
items: InboundMeasurementItem[]
weightResults: InboundMeasurementWeightResult[]
}