Release/test 合并上生产 #2
@@ -57,6 +57,7 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
|
||||
originalDocumentNo: 'DOC-001',
|
||||
originalGrossWeightKg: 118.5,
|
||||
originalVolumeCbm: 0.8,
|
||||
originalPackageCount: 10,
|
||||
actualPackageCount: 10,
|
||||
actualGrossWeightKg: 120,
|
||||
actualVolumeCbm: 0.6,
|
||||
@@ -64,6 +65,7 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
|
||||
actualGoodsName: '蓝牙音箱',
|
||||
items: [{
|
||||
id: '9101',
|
||||
boxCode: 'RK202608040001#10-1',
|
||||
itemNo: '001',
|
||||
boxSpecification: '50×40×30cm',
|
||||
packageCount: 10,
|
||||
@@ -77,6 +79,8 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
|
||||
totalVolumeCbm: 0.6,
|
||||
volumeWeightKg: 100.2,
|
||||
intelligentImageFileId: '88001',
|
||||
machineNo: 'MJ20210327',
|
||||
measuredAt: '2026-08-04T10:30:00+08:00',
|
||||
overLimitWarning: null,
|
||||
outerLengthCm: null,
|
||||
outerWidthCm: null,
|
||||
@@ -143,7 +147,7 @@ describe('InboundMeasurementPanel', () => {
|
||||
const text = wrapper.text()
|
||||
|
||||
expect(text).toContain('原单数据')
|
||||
expect(text).toContain('入库单测量数据明细')
|
||||
expect(text).toContain('扫描箱测量数据明细')
|
||||
expect(text).toContain('实测总箱数')
|
||||
expect(text).toContain('实测总毛重')
|
||||
expect(text).toContain('实测总体积')
|
||||
@@ -152,14 +156,14 @@ describe('InboundMeasurementPanel', () => {
|
||||
|
||||
const labels = wrapper.findAll('.measurement-item__grid label > span').map((label) => label.text())
|
||||
expect(labels.slice(0, 8)).toEqual([
|
||||
'明细编号 *',
|
||||
'箱规 *',
|
||||
'箱规长(cm)*',
|
||||
'箱规宽(cm)*',
|
||||
'箱规高(cm)*',
|
||||
'箱数 *',
|
||||
'箱号',
|
||||
'测量长(cm)*',
|
||||
'测量宽(cm)*',
|
||||
'测量高(cm)*',
|
||||
'确认箱数 *',
|
||||
'每箱毛重(kg)*',
|
||||
'智能图片文件 ID',
|
||||
'机器编号',
|
||||
'测量时间',
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { AlertTriangle, Image, Plus, RefreshCw, Ruler, Save, Send, Trash2 } from '@lucide/vue'
|
||||
import { AlertTriangle, Image, RefreshCw, Ruler, Save, Send } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -75,11 +75,17 @@ const customerId = computed(() => props.customerId || props.tenantId || '')
|
||||
const hasContext = computed(() => Boolean(customerId.value && props.inboundId))
|
||||
const editableProcess = computed(() => ['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
|
||||
const mayMaintain = computed(() => canEdit.value && editableProcess.value && !loading.value && !submitting.value)
|
||||
const maySubmit = computed(() => canSubmit.value && editableProcess.value && Boolean(form.measurementId) && packageCountMatched.value && !loading.value && !saving.value)
|
||||
const confirmedPackageCount = computed(() => form.items.reduce((sum, item) => sum + (Number.isFinite(item.packageCount) ? item.packageCount : 0), 0))
|
||||
const expectedPackageCount = computed(() => detail.value?.originalPackageCount ?? null)
|
||||
const packageCountDifference = computed(() => expectedPackageCount.value == null ? null : confirmedPackageCount.value - expectedPackageCount.value)
|
||||
const packageCountMatched = computed(() => packageCountDifference.value === null || packageCountDifference.value === 0)
|
||||
const packageCountDifferenceLabel = computed(() => {
|
||||
if (packageCountDifference.value == null) return ''
|
||||
return packageCountDifference.value > 0
|
||||
? `超出 ${packageCountDifference.value} 箱`
|
||||
: `还差 ${Math.abs(packageCountDifference.value)} 箱`
|
||||
})
|
||||
const maySubmit = computed(() => canSubmit.value && editableProcess.value && Boolean(form.measurementId) && packageCountMatched.value && !loading.value && !saving.value)
|
||||
const snapshot = computed(() => JSON.stringify(form))
|
||||
const isDirty = computed(() => Boolean(detail.value) && snapshot.value !== baseline.value)
|
||||
const registrationOpen = computed(() => hasContext.value && canView.value && Boolean(detail.value))
|
||||
@@ -105,29 +111,6 @@ function nextItemKey() {
|
||||
return `measurement-item-${itemSequence}`
|
||||
}
|
||||
|
||||
function emptyItem(): EditableMeasurementItem {
|
||||
return {
|
||||
clientKey: nextItemKey(),
|
||||
boxCode: null,
|
||||
itemNo: '',
|
||||
boxSpecification: '',
|
||||
packageCount: 1,
|
||||
lengthCm: 0,
|
||||
widthCm: 0,
|
||||
heightCm: 0,
|
||||
unitWeightKg: 0,
|
||||
intelligentImageFileId: null,
|
||||
machineNo: null,
|
||||
measuredAt: null,
|
||||
overLimitWarning: null,
|
||||
outerLengthCm: null,
|
||||
outerWidthCm: null,
|
||||
outerHeightCm: null,
|
||||
remark: null,
|
||||
serverValues: { girthCm: null, unitVolumeCbm: null, totalWeightKg: null, totalVolumeCbm: null, volumeWeightKg: null },
|
||||
}
|
||||
}
|
||||
|
||||
function editableItem(item: InboundMeasurementItem): EditableMeasurementItem {
|
||||
return {
|
||||
clientKey: nextItemKey(),
|
||||
@@ -219,23 +202,6 @@ async function reloadLatest() {
|
||||
await loadMeasurement()
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
if (form.items.length >= 500) {
|
||||
ElMessage.warning('单个测量批次最多包含 500 条箱规')
|
||||
return
|
||||
}
|
||||
form.items.push(emptyItem())
|
||||
}
|
||||
|
||||
async function removeItem(index: number) {
|
||||
if (saving.value || submitting.value) return
|
||||
if (form.items.length === 1) {
|
||||
ElMessage.warning('测量草稿至少需要一条箱规')
|
||||
return
|
||||
}
|
||||
form.items.splice(index, 1)
|
||||
}
|
||||
|
||||
function isPositive(value: number | null) {
|
||||
return value != null && Number.isFinite(value) && value > 0
|
||||
}
|
||||
@@ -514,7 +480,7 @@ onBeforeUnmount(releaseImagePreview)
|
||||
|
||||
<el-alert v-if="expectedPackageCount !== null && !packageCountMatched" type="warning" :closable="false" show-icon class="measurement-alert">
|
||||
<template #title>确认箱数尚未与订单总箱数匹配</template>
|
||||
当前已确认 {{ confirmedPackageCount }} 箱,订单总箱数 {{ expectedPackageCount }} 箱,还差 {{ Math.abs(packageCountDifference || 0) }} 箱。
|
||||
当前已确认 {{ confirmedPackageCount }} 箱,订单总箱数 {{ expectedPackageCount }} 箱,{{ packageCountDifferenceLabel }}。
|
||||
</el-alert>
|
||||
|
||||
<form class="measurement-form" @submit.prevent="saveDraft">
|
||||
@@ -534,18 +500,14 @@ onBeforeUnmount(releaseImagePreview)
|
||||
|
||||
<section class="measurement-section measurement-items-section">
|
||||
<div class="measurement-section__heading">
|
||||
<div><h3>入库单测量数据明细</h3><p>最多 500 条;保存后由服务端重新计算箱规周长、单箱体积、总毛重、总体积和总材积重。</p></div>
|
||||
<el-button v-if="canEdit" :disabled="!mayMaintain || form.items.length >= 500" @click="addItem"><Plus :size="15" />新增箱规</el-button>
|
||||
<div><h3>扫描箱测量数据明细</h3><p>每条记录对应一个实际扫描箱;人工只确认该条记录代表的箱数,设备数据由服务端计算汇总。</p></div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!form.items.length" description="暂无箱规明细">
|
||||
<el-button v-if="mayMaintain" type="primary" @click="addItem"><Plus :size="15" />添加第一条箱规</el-button>
|
||||
</el-empty>
|
||||
<el-empty v-if="!form.items.length" description="暂无扫描箱测量数据" />
|
||||
|
||||
<article v-for="(item, index) in form.items" :key="item.clientKey" class="measurement-item" :class="{ 'has-error': validationErrors[item.clientKey]?.length }">
|
||||
<header>
|
||||
<div><strong>扫描箱 {{ index + 1 }}</strong><span>{{ item.boxCode || '未关联箱号' }} · {{ item.machineNo || '人工录入' }}</span></div>
|
||||
<el-button v-if="canEdit" link type="danger" :disabled="!mayMaintain || form.items.length === 1" @click="removeItem(index)"><Trash2 :size="15" />删除</el-button>
|
||||
</header>
|
||||
<div class="measurement-item__grid">
|
||||
<label><span>箱号</span><el-input :model-value="item.boxCode || '—'" readonly /></label>
|
||||
@@ -621,7 +583,7 @@ onBeforeUnmount(releaseImagePreview)
|
||||
.measurement-context__identity span { color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
||||
.measurement-tags { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 7px; }
|
||||
.measurement-tags > span:last-child { color: var(--cc-muted); font-size: 11px; }
|
||||
.measurement-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 16px 0 0; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.measurement-metrics { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); margin: 16px 0 0; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.measurement-metrics > div { min-width: 0; padding: 13px 15px; border-right: 1px solid var(--cc-border); }
|
||||
.measurement-metrics > div:last-child { border-right: 0; }
|
||||
.measurement-metrics dt { color: var(--cc-muted); font-size: 11px; }
|
||||
@@ -638,6 +600,9 @@ onBeforeUnmount(releaseImagePreview)
|
||||
.measurement-form-grid, .measurement-item__grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 15px; }
|
||||
.measurement-form label, .measurement-item label { display: flex; min-width: 0; flex-direction: column; gap: 7px; color: #344054; font-size: 12px; font-weight: 650; }
|
||||
.measurement-form :deep(.el-input-number), .measurement-form :deep(.el-input) { width: 100%; }
|
||||
.measurement-image-field :deep(.el-button) { width: 100%; margin: 0; }
|
||||
.measurement-image-preview { display: grid; min-height: 240px; place-items: center; }
|
||||
.measurement-image-preview img { display: block; width: auto; max-width: 100%; max-height: 68vh; object-fit: contain; }
|
||||
.measurement-items-section { background: #fafbfc; }
|
||||
.measurement-item { overflow: hidden; margin-top: 14px; border: 1px solid var(--cc-border); border-radius: 9px; background: #fff; }
|
||||
.measurement-item.has-error { border-color: var(--el-color-danger-light-5); }
|
||||
|
||||
@@ -16,6 +16,7 @@ const draft: SaveInboundMeasurementDraftRequest = {
|
||||
originalVolumeCbm: 1.2,
|
||||
actualGoodsName: '蓝牙音箱',
|
||||
items: [{
|
||||
boxCode: 'RK202608040001#10-1',
|
||||
itemNo: '001',
|
||||
boxSpecification: '50×40×30cm',
|
||||
packageCount: 10,
|
||||
@@ -24,6 +25,8 @@ const draft: SaveInboundMeasurementDraftRequest = {
|
||||
heightCm: 30,
|
||||
unitWeightKg: 12,
|
||||
intelligentImageFileId: '88001',
|
||||
machineNo: 'MJ20210327',
|
||||
measuredAt: '2026-08-04T10:30:00+08:00',
|
||||
overLimitWarning: null,
|
||||
outerLengthCm: null,
|
||||
outerWidthCm: null,
|
||||
|
||||
@@ -20,9 +20,9 @@ const detail = {
|
||||
inboundId: '90001', inboundCode: 'RK90001', companyName: '柏悦客户', channelCode: 'SEA', channelName: '海运',
|
||||
processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING', recommendedPalletType: 'STANDARD', reviewReason: null, reviewerId: null, reviewerName: null,
|
||||
measurementId: '70001', sourceType: 'DEVICE', measurementStatus: 'SUBMITTED', version: 4,
|
||||
originalDocumentNo: 'ORD-001', originalGrossWeightKg: 100, originalVolumeCbm: 2,
|
||||
originalDocumentNo: 'ORD-001', originalGrossWeightKg: 100, originalVolumeCbm: 2, originalPackageCount: 10,
|
||||
actualPackageCount: 10, actualGrossWeightKg: 110, actualVolumeCbm: 2.2, volumeWeightKg: 367.4, actualGoodsName: '测试货物',
|
||||
items: [{ id: '1', itemNo: '001', boxSpecification: '60×40×30', packageCount: 10, lengthCm: 60, widthCm: 40, heightCm: 30, girthCm: 240, unitWeightKg: 11, unitVolumeCbm: 0.072, totalWeightKg: 110, totalVolumeCbm: 0.72, volumeWeightKg: 120.24, intelligentImageFileId: '30001', overLimitWarning: '周长超过渠道限制,请人工核验尺寸后再作出审核决定', outerLengthCm: 61, outerWidthCm: 41, outerHeightCm: 31, remark: '外箱存在轻微变形' }],
|
||||
items: [{ id: '1', boxCode: 'RK90001#10-1', itemNo: '001', boxSpecification: '60×40×30', packageCount: 10, lengthCm: 60, widthCm: 40, heightCm: 30, girthCm: 240, unitWeightKg: 11, unitVolumeCbm: 0.072, totalWeightKg: 110, totalVolumeCbm: 0.72, volumeWeightKg: 120.24, intelligentImageFileId: '30001', machineNo: 'MJ20210327', measuredAt: '2026-08-04T10:30:00+08:00', overLimitWarning: '周长超过渠道限制,请人工核验尺寸后再作出审核决定', outerLengthCm: 61, outerWidthCm: 41, outerHeightCm: 31, remark: '外箱存在轻微变形' }],
|
||||
weightResults: [{ id: '2', fileId: 'file-1', ocrProvider: 'OCR', ocrRawText: '设备屏幕原始识别内容 110.5 KG,包含一段需要自动换行展示的较长文本', ocrWeightValue: 110.5, ocrWeightUnit: 'KG', confidence: 0.98, adjustedWeightValue: 110, adjustedWeightUnit: 'KG', finalWeightKg: 110, recognitionStatus: 'CONFIRMED', adjustmentReason: '去除托盘重量后采用净值', adjustedBy: '20001', adjustedAt: '2026-08-04T09:58:00', confirmedBy: '20002', confirmedAt: '2026-08-04T10:00:00', version: 1 }],
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,13 @@ const reviewStatusLabel = computed(() => {
|
||||
})
|
||||
|
||||
const measurementColumns: DataTableColumn[] = [
|
||||
{ key: 'boxSpecification', label: '箱规', minWidth: 130, slotName: 'boxSpecification' },
|
||||
{ key: 'packageCount', label: '箱数', width: 80, slotName: 'packageCount' },
|
||||
{ key: 'dimensions', label: '长 × 宽 × 高(cm)', minWidth: 170, slotName: 'dimensions' },
|
||||
{ key: 'outerDimensions', label: '外箱长 × 宽 × 高(cm)', minWidth: 190, slotName: 'outerDimensions' },
|
||||
{ key: 'girthCm', label: '周长(cm)', minWidth: 105, slotName: 'girthCm' },
|
||||
{ key: 'unitWeightKg', label: '每箱毛重(kg)', minWidth: 125, slotName: 'unitWeightKg' },
|
||||
{ key: 'boxCode', label: '扫描箱号', minWidth: 190, slotName: 'boxCode' },
|
||||
{ key: 'packageCount', label: '确认箱数', width: 90, slotName: 'packageCount' },
|
||||
{ key: 'dimensions', label: '实测长 × 宽 × 高(cm)', minWidth: 190, slotName: 'dimensions' },
|
||||
{ key: 'unitWeightKg', label: '实测重量(kg)', minWidth: 115, slotName: 'unitWeightKg' },
|
||||
{ key: 'machineNo', label: '机器编号', minWidth: 130, slotName: 'machineNo' },
|
||||
{ key: 'measuredAt', label: '测量时间', minWidth: 150, slotName: 'measuredAt' },
|
||||
{ key: 'intelligentImageFileId', label: '当前图片', minWidth: 110, slotName: 'image' },
|
||||
{ key: 'unitVolumeCbm', label: '每箱体积(m³)', minWidth: 125, slotName: 'unitVolumeCbm' },
|
||||
{ key: 'totalWeightKg', label: '总毛重(kg)', minWidth: 110, slotName: 'totalWeightKg' },
|
||||
{ key: 'totalVolumeCbm', label: '总体积(m³)', minWidth: 110, slotName: 'totalVolumeCbm' },
|
||||
@@ -258,12 +259,15 @@ onMounted(() => {
|
||||
<div class="review-section">
|
||||
<div class="section-heading"><div><h3>三组数据对比</h3><p>{{ detail.companyName || '-' }} · {{ detail.channelName || detail.channelCode || '-' }}</p></div><span class="section-count">{{ detail.items.length }} 条箱规</span></div>
|
||||
<DataTable :data="detail.items" :columns="measurementColumns" empty-text="暂无设备测量明细" border stripe>
|
||||
<template #boxSpecification="{ row }">{{ (row as MeasurementReviewItem).boxSpecification || (row as MeasurementReviewItem).itemNo || '-' }}</template>
|
||||
<template #boxCode="{ row }">{{ (row as MeasurementReviewItem).boxCode || (row as MeasurementReviewItem).itemNo || '-' }}</template>
|
||||
<template #packageCount="{ row }">{{ (row as MeasurementReviewItem).packageCount ?? '-' }}</template>
|
||||
<template #dimensions="{ row }">{{ [(row as MeasurementReviewItem).lengthCm, (row as MeasurementReviewItem).widthCm, (row as MeasurementReviewItem).heightCm].map((value) => formatNumber(value)).join(' × ') }}</template>
|
||||
<template #outerDimensions="{ row }">{{ [(row as MeasurementReviewItem).outerLengthCm, (row as MeasurementReviewItem).outerWidthCm, (row as MeasurementReviewItem).outerHeightCm].map((value) => formatNumber(value)).join(' × ') }}</template>
|
||||
<template #girthCm="{ row }">{{ formatNumber((row as MeasurementReviewItem).girthCm) }}</template>
|
||||
<template #unitWeightKg="{ row }">{{ formatNumber((row as MeasurementReviewItem).unitWeightKg) }}</template>
|
||||
<template #machineNo="{ row }">{{ (row as MeasurementReviewItem).machineNo || '-' }}</template>
|
||||
<template #measuredAt="{ row }">{{ formatDate((row as MeasurementReviewItem).measuredAt) }}</template>
|
||||
<template #image="{ row }">{{ (row as MeasurementReviewItem).intelligentImageFileId ? '已关联' : '暂无' }}</template>
|
||||
<template #unitVolumeCbm="{ row }">{{ formatNumber((row as MeasurementReviewItem).unitVolumeCbm) }}</template>
|
||||
<template #totalWeightKg="{ row }">{{ formatNumber((row as MeasurementReviewItem).totalWeightKg) }}</template>
|
||||
<template #totalVolumeCbm="{ row }">{{ formatNumber((row as MeasurementReviewItem).totalVolumeCbm) }}</template>
|
||||
|
||||
Reference in New Issue
Block a user