优化
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user