Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
5 changed files with 43 additions and 67 deletions
Showing only changes of commit 628a7d781e - Show all commits
@@ -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>