From 010204da57b997a927f69cad294633f43da0d684 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Fri, 14 Aug 2026 17:38:06 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../InboundMeasurementPanel.test.ts | 18 ++-
.../measurement/InboundMeasurementPanel.vue | 9 +-
src/modules/inbounds/measurement/types.ts | 8 +-
.../review/MeasurementReviewPanel.test.ts | 9 +-
.../review/MeasurementReviewPanel.vue | 20 +--
src/modules/inbounds/review/types.ts | 8 +-
.../inbounds/weight/api/weightOcr.test.ts | 13 ++
src/modules/inbounds/weight/api/weightOcr.ts | 9 ++
.../WeightOcrPanel.behavior.test.ts | 60 ++++++++-
.../weight/components/WeightOcrPanel.test.ts | 7 +-
.../weight/components/WeightOcrPanel.vue | 114 ++++++++++++++++--
src/modules/inbounds/weight/types.ts | 17 ++-
12 files changed, 259 insertions(+), 33 deletions(-)
diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts b/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts
index 58f81308..abbbc8d5 100644
--- a/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts
+++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts
@@ -201,7 +201,7 @@ describe('InboundMeasurementPanel', () => {
expect(text).toContain('扫描箱测量数据明细')
expect(text).toContain('订单总箱数')
expect(text).toContain('确认箱数合计')
- expect(text).toContain('实测总毛重')
+ expect(text).toContain('货物总重量')
expect(text).toContain('实测总体积')
expect(text).toContain('实测总材积重')
expect(text).not.toContain('租户')
@@ -271,6 +271,22 @@ describe('InboundMeasurementPanel', () => {
expect(getInboundMeasurement).toHaveBeenCalledTimes(1)
})
+ it('shows a normal A0409 business reason instead of relabeling it as a version conflict', async () => {
+ vi.mocked(updateConfirmedPackageCount).mockRejectedValueOnce(
+ new ApiResponseError('该托盘已被占用,请重新选择', 'A0409', 409),
+ )
+ const wrapper = await mountPanel()
+ const countInput = wrapper.find('.measurement-count-editor input')
+ await countInput.setValue('9')
+ const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据'))
+ await save?.trigger('click')
+ await flushPromises()
+
+ expect(wrapper.text()).toContain('该托盘已被占用,请重新选择')
+ expect(wrapper.text()).toContain('操作未完成')
+ expect(wrapper.text()).not.toContain('数据版本已变化,当前修改尚未丢失')
+ })
+
it('requires the dirty draft to be saved before submitting for review', async () => {
const wrapper = await mountPanel()
const countInput = wrapper.find('.measurement-count-editor input')
diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue
index 3c4c71ce..8b4eecb6 100644
--- a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue
+++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue
@@ -314,11 +314,8 @@ function validateDraft() {
function setOperationError(error: unknown, fallback: string) {
const details = getApiErrorDetails(error, fallback)
operationError.value = details
- conflictDetected.value = details.code === 'A0409'
- || details.code === 'CONFLICT'
- || details.code === 'HTTP_409'
- || details.message.includes('版本')
- || details.message.includes('并发')
+ conflictDetected.value = details.message.includes('版本已变化')
+ || details.message.includes('并发修改')
return details
}
@@ -532,7 +529,7 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
-
+
diff --git a/src/modules/inbounds/measurement/types.ts b/src/modules/inbounds/measurement/types.ts
index cf88c644..7d328dcc 100644
--- a/src/modules/inbounds/measurement/types.ts
+++ b/src/modules/inbounds/measurement/types.ts
@@ -39,6 +39,11 @@ export interface InboundMeasurementItem {
export interface InboundMeasurementWeightResult {
id: MeasurementIdentifier
fileId: MeasurementIdentifier
+ palletId: MeasurementIdentifier
+ palletCode: string
+ palletType: string
+ palletCapacityCbm: number
+ palletTareWeightKg: number
ocrProvider: string | null
ocrRawText: string | null
ocrWeightValue: number | null
@@ -46,7 +51,8 @@ export interface InboundMeasurementWeightResult {
confidence: number | null
adjustedWeightValue: number | null
adjustedWeightUnit: string | null
- finalWeightKg: number | null
+ grossWeightKg: number | null
+ netWeightKg: number | null
recognitionStatus: string
adjustmentReason: string | null
adjustedBy: MeasurementIdentifier | null
diff --git a/src/modules/inbounds/review/MeasurementReviewPanel.test.ts b/src/modules/inbounds/review/MeasurementReviewPanel.test.ts
index 60eb57de..e339e4eb 100644
--- a/src/modules/inbounds/review/MeasurementReviewPanel.test.ts
+++ b/src/modules/inbounds/review/MeasurementReviewPanel.test.ts
@@ -23,7 +23,7 @@ const detail = {
originalDocumentNo: 'ORD-001', originalGrossWeightKg: 100, originalVolumeCbm: 2, originalPackageCount: 10,
actualPackageCount: 10, actualGrossWeightKg: 110, actualVolumeCbm: 2.2, volumeWeightKg: 367.4, actualGoodsName: '测试货物',
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 }],
+ weightResults: [{ id: '2', fileId: 'file-1', palletId: '50001', palletCode: 'TP-001', palletType: '木托盘', palletCapacityCbm: 1.2, palletTareWeightKg: 10, ocrProvider: 'OCR', ocrRawText: '设备屏幕原始识别内容 120.5 KG,包含一段需要自动换行展示的较长文本', ocrWeightValue: 120.5, ocrWeightUnit: 'KG', confidence: 0.98, adjustedWeightValue: 120, adjustedWeightUnit: 'KG', grossWeightKg: 120, netWeightKg: 110, recognitionStatus: 'CONFIRMED', adjustmentReason: '修正 OCR 毛重读数', adjustedBy: '20001', adjustedAt: '2026-08-04T09:58:00', confirmedBy: '20002', confirmedAt: '2026-08-04T10:00:00', version: 1 }],
}
enableAutoUnmount(afterEach)
@@ -48,11 +48,14 @@ describe('MeasurementReviewPanel', () => {
expect(wrapper.text()).toContain('设备实测')
expect(wrapper.text()).toContain('设备采集')
expect(wrapper.text()).not.toContain('DEVICE')
- expect(wrapper.text()).toContain('称重 OCR')
+ expect(wrapper.text()).toContain('货物总重量')
expect(wrapper.text()).toContain('实测长 × 宽 × 高(cm)')
expect(wrapper.text()).toContain('周长超过渠道限制')
expect(wrapper.text()).toContain('OCR 原文:设备屏幕原始识别内容')
- expect(wrapper.text()).toContain('调整原因:去除托盘重量后采用净值')
+ expect(wrapper.text()).toContain('TP-001')
+ expect(wrapper.text()).toContain('托盘皮重')
+ expect(wrapper.text()).toContain('货物重量')
+ expect(wrapper.text()).toContain('调整原因:修正 OCR 毛重读数')
expect(wrapper.findComponent(DataTable).exists()).toBe(true)
expect(wrapper.text()).not.toContain('租户')
expect(wrapper.text()).toContain('通过并进入待分柜')
diff --git a/src/modules/inbounds/review/MeasurementReviewPanel.vue b/src/modules/inbounds/review/MeasurementReviewPanel.vue
index 0e179877..0bce1ccc 100644
--- a/src/modules/inbounds/review/MeasurementReviewPanel.vue
+++ b/src/modules/inbounds/review/MeasurementReviewPanel.vue
@@ -49,7 +49,7 @@ const canRelease = computed(() => access.can('firstleg:inbound:exception-release
const customerId = computed(() => props.customerId || props.tenantId || '')
const isPending = computed(() => detail.value?.measureReviewStatus === 'PENDING' && detail.value?.processStatus === 'MEASURE_PENDING')
const isRejected = computed(() => detail.value?.measureReviewStatus === 'REJECTED' && detail.value?.processStatus === 'MEASURE_REJECTED')
-const confirmedWeights = computed(() => detail.value?.weightResults.filter((item) => item.confirmedAt && item.finalWeightKg !== null).length ?? 0)
+const confirmedWeights = computed(() => detail.value?.weightResults.filter((item) => item.confirmedAt && item.netWeightKg !== null).length ?? 0)
const reviewStatusLabel = computed(() => inboundStatusLabel(detail.value?.measureReviewStatus, inboundReviewStatusLabels, '未提交'))
const measurementColumns: DataTableColumn[] = [
@@ -69,10 +69,13 @@ const measurementColumns: DataTableColumn[] = [
const weightColumns: DataTableColumn[] = [
{ key: 'file', label: '称重文件 / OCR 原文', minWidth: 260, slotName: 'file' },
+ { key: 'pallet', label: '托盘', minWidth: 170, slotName: 'pallet' },
{ key: 'status', label: '识别状态', width: 100, slotName: 'status' },
- { key: 'ocrWeight', label: '原始重量', minWidth: 110, slotName: 'ocrWeight' },
- { key: 'adjustedWeight', label: '调整后重量', minWidth: 120, slotName: 'adjustedWeight' },
- { key: 'finalWeight', label: '最终重量', minWidth: 110, slotName: 'finalWeight' },
+ { key: 'ocrWeight', label: 'OCR 含托盘毛重', minWidth: 140, slotName: 'ocrWeight' },
+ { key: 'adjustedWeight', label: '人工修正毛重', minWidth: 130, slotName: 'adjustedWeight' },
+ { key: 'grossWeight', label: '服务端毛重', minWidth: 115, slotName: 'grossWeight' },
+ { key: 'tareWeight', label: '托盘皮重', minWidth: 110, slotName: 'tareWeight' },
+ { key: 'netWeight', label: '货物重量', minWidth: 110, slotName: 'netWeight' },
{ key: 'confidence', label: '置信度', width: 90, slotName: 'confidence' },
{ key: 'reason', label: '调整原因', minWidth: 180, slotName: 'reason' },
]
@@ -103,7 +106,7 @@ function measurementSourceLabel(value: string | null | undefined) {
function isConflict(error: unknown) {
const details = getApiErrorDetails(error)
- return details.code === 'A0409' || details.code === '409' || /并发|版本|已被更新|状态已变化/.test(details.message)
+ return details.message.includes('版本已变化') || details.message.includes('并发修改')
}
async function loadReview() {
@@ -251,7 +254,7 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
申报箱数{{ formatNumber(detail.originalPackageCount, ' 箱') }}
实测箱数{{ formatNumber(detail.actualPackageCount, ' 箱') }}
申报毛重{{ formatNumber(detail.originalGrossWeightKg, ' kg') }}
- 称重 OCR{{ formatNumber(detail.actualGrossWeightKg, ' kg') }}
+ 货物总重量{{ formatNumber(detail.actualGrossWeightKg, ' kg') }}
申报体积{{ formatNumber(detail.originalVolumeCbm, ' CBM') }}
设备实测体积{{ formatNumber(detail.actualVolumeCbm, ' CBM') }}
材积重{{ formatNumber(detail.volumeWeightKg, ' kg') }}
@@ -289,10 +292,13 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
称重图片识别结果
{{ detail.weightResults.length }} 张
{{ row.fileId || '未关联文件' }}OCR 原文:{{ row.ocrRawText }}
+ {{ row.palletCode }}{{ row.palletType }} · {{ formatNumber(row.palletCapacityCbm, ' m³') }}
{{ recognitionStatusLabel(row.recognitionStatus) }}
{{ formatNumber(row.ocrWeightValue, ` ${row.ocrWeightUnit || ''}`) }}
{{ formatNumber(row.adjustedWeightValue, ` ${row.adjustedWeightUnit || ''}`) }}
- {{ formatNumber(row.finalWeightKg, ' kg') }}
+ {{ formatNumber(row.grossWeightKg, ' kg') }}
+ {{ formatNumber(row.palletTareWeightKg, ' kg') }}
+ {{ formatNumber(row.netWeightKg, ' kg') }}
{{ row.confidence === null ? '-' : `${Math.round(row.confidence * 100)}%` }}
{{ row.adjustmentReason ? `调整原因:${row.adjustmentReason}` : '-' }}
diff --git a/src/modules/inbounds/review/types.ts b/src/modules/inbounds/review/types.ts
index fc36e82d..09c0982b 100644
--- a/src/modules/inbounds/review/types.ts
+++ b/src/modules/inbounds/review/types.ts
@@ -28,6 +28,11 @@ export interface MeasurementReviewItem {
export interface MeasurementReviewWeightResult {
id: string
fileId: string | null
+ palletId: string
+ palletCode: string
+ palletType: string
+ palletCapacityCbm: number
+ palletTareWeightKg: number
ocrProvider: string | null
ocrRawText: string | null
ocrWeightValue: number | null
@@ -35,7 +40,8 @@ export interface MeasurementReviewWeightResult {
confidence: number | null
adjustedWeightValue: number | null
adjustedWeightUnit: string | null
- finalWeightKg: number | null
+ grossWeightKg: number | null
+ netWeightKg: number | null
recognitionStatus: string | null
adjustmentReason: string | null
adjustedBy: string | null
diff --git a/src/modules/inbounds/weight/api/weightOcr.test.ts b/src/modules/inbounds/weight/api/weightOcr.test.ts
index 31994205..53829841 100644
--- a/src/modules/inbounds/weight/api/weightOcr.test.ts
+++ b/src/modules/inbounds/weight/api/weightOcr.test.ts
@@ -8,6 +8,7 @@ import {
downloadWeightImage,
downloadWeightThumbnail,
getWeightOcrRawText,
+ getWeightPalletOptions,
getWeightWorkspace,
normalizeWeightConfidence,
retryWeightRecognition,
@@ -81,12 +82,24 @@ describe('inbound weight OCR API', () => {
)
})
+ it('loads same-warehouse pallet snapshots through Firstleg', async () => {
+ const options = [{ palletId: '11', palletCode: 'TP-11', palletType: '木托盘', capacityCbm: 1.2, tareWeightKg: 12 }]
+ vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: options })
+
+ await expect(getWeightPalletOptions('10001', '20001')).resolves.toEqual(options)
+ expect(http.get).toHaveBeenCalledWith(
+ '/platform/v1/first-leg/inbounds/20001/weight-pallet-options',
+ { params: { tenantId: '10001' } },
+ )
+ })
+
it('registers, corrects, and confirms a weight result using the implemented contracts', async () => {
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1' } })
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', version: 1 } })
const createPayload = {
fileId: '9007199254740995',
+ palletId: '11',
ocrProvider: 'FILE_WEIGHT_OCR',
ocrRawText: null,
ocrWeightValue: 12.5,
diff --git a/src/modules/inbounds/weight/api/weightOcr.ts b/src/modules/inbounds/weight/api/weightOcr.ts
index 1d48ef37..8caafc52 100644
--- a/src/modules/inbounds/weight/api/weightOcr.ts
+++ b/src/modules/inbounds/weight/api/weightOcr.ts
@@ -5,6 +5,7 @@ import type {
CorrectWeightResultRequest,
CreateWeightResultRequest,
WeightImageUploadResult,
+ WeightPalletOption,
WeightResultResource,
WeightWorkspaceResource,
} from '../types'
@@ -59,6 +60,14 @@ export async function getWeightWorkspace(tenantId: string, inboundId: string) {
return unwrapApiResponse(response)
}
+export async function getWeightPalletOptions(tenantId: string, inboundId: string) {
+ const response = await http.get, ApiEnvelope>(
+ `${measurementBasePath}/${segment(inboundId)}/weight-pallet-options`,
+ { params: { tenantId } },
+ )
+ return unwrapApiResponse(response)
+}
+
export async function uploadWeightImage(tenantId: string, inboundId: string, file: File) {
const body = new FormData()
body.append('file', file)
diff --git a/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts b/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts
index f0b04e39..286383bb 100644
--- a/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts
+++ b/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts
@@ -14,6 +14,7 @@ import {
downloadWeightImage,
downloadWeightThumbnail,
getWeightWorkspace,
+ getWeightPalletOptions,
uploadWeightImage,
} from '../api/weightOcr'
import type { WeightResultResource, WeightWorkspaceResource } from '../types'
@@ -30,6 +31,7 @@ vi.mock('../api/weightOcr', async () => {
downloadWeightImage: vi.fn(),
downloadWeightThumbnail: vi.fn(),
getWeightWorkspace: vi.fn(),
+ getWeightPalletOptions: vi.fn(),
uploadWeightImage: vi.fn(),
}
})
@@ -37,6 +39,11 @@ vi.mock('../api/weightOcr', async () => {
const pendingWeight: WeightResultResource = {
id: '30001',
fileId: '40001',
+ palletId: '50001',
+ palletCode: 'TP-001',
+ palletType: '木托盘',
+ palletCapacityCbm: 1.2,
+ palletTareWeightKg: 12,
ocrProvider: 'FILE_WEIGHT_OCR',
ocrRawText: '285.5',
ocrWeightValue: 285.5,
@@ -44,7 +51,8 @@ const pendingWeight: WeightResultResource = {
confidence: 0.98,
adjustedWeightValue: null,
adjustedWeightUnit: null,
- finalWeightKg: null,
+ grossWeightKg: 285.5,
+ netWeightKg: null,
recognitionStatus: 'PENDING_CONFIRM',
adjustmentReason: null,
adjustedBy: null,
@@ -120,6 +128,9 @@ describe('WeightOcrPanel confirmation behavior', () => {
vi.clearAllMocks()
document.body.innerHTML = ''
vi.mocked(getWeightWorkspace).mockResolvedValue(workspace)
+ vi.mocked(getWeightPalletOptions).mockResolvedValue([{
+ palletId: '50001', palletCode: 'TP-001', palletType: '木托盘', capacityCbm: 1.2, tareWeightKg: 12,
+ }])
vi.mocked(downloadWeightImage).mockResolvedValue(new Blob(['image'], { type: 'image/jpeg' }))
vi.mocked(downloadWeightThumbnail).mockResolvedValue(new Blob(['thumb'], { type: 'image/jpeg' }))
vi.stubGlobal('URL', {
@@ -129,7 +140,8 @@ describe('WeightOcrPanel confirmation behavior', () => {
vi.mocked(confirmWeightResult).mockResolvedValue({
...pendingWeight,
recognitionStatus: 'CONFIRMED',
- finalWeightKg: 285.5,
+ grossWeightKg: 297.5,
+ netWeightKg: 285.5,
version: 1,
})
})
@@ -154,6 +166,8 @@ describe('WeightOcrPanel confirmation behavior', () => {
})
const wrapper = mountPanel()
await flushPromises()
+ await wrapper.find('.pallet-picker .el-select').trigger('click')
+ await new DOMWrapper(document.body).find('.el-select-dropdown__item').trigger('click')
const input = wrapper.get('input[type="file"]')
const file = new File(['image'], 'weight.jpg', { type: 'image/jpeg' })
Object.defineProperty(input.element, 'files', { configurable: true, value: [file] })
@@ -162,6 +176,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
await flushPromises()
expect(uploadWeightImage).toHaveBeenCalledWith('10001', '20001', file)
+ expect(createWeightResult).toHaveBeenCalledWith('10001', '20001', expect.objectContaining({ palletId: '50001' }))
expect(document.body.textContent).toContain('确认称重图片')
expect(document.body.textContent).toContain('识别重量 12.500 KG')
expect(document.body.querySelector('input[aria-label="当前图片确认重量"]')).not.toBeNull()
@@ -225,7 +240,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
.mockResolvedValueOnce({
...workspace,
actualGrossWeightKg: 285.5,
- weightResults: [{ ...pendingWeight, recognitionStatus: 'CONFIRMED', finalWeightKg: 285.5, version: 1 }],
+ weightResults: [{ ...pendingWeight, recognitionStatus: 'CONFIRMED', grossWeightKg: 297.5, netWeightKg: 285.5, version: 1 }],
})
const wrapper = mountPanel()
await flushPromises()
@@ -236,10 +251,47 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(wrapper.emitted('summaryChanged')).toEqual([[285.5]])
expect(wrapper.emitted('changed')).toHaveLength(1)
- expect(wrapper.text()).toContain('已确认总毛重')
+ expect(wrapper.text()).toContain('货物总重量')
expect(wrapper.text()).toContain('285.500 KG')
})
+ it('requires a pallet selection before opening the file picker', async () => {
+ const wrapper = mountPanel()
+ await flushPromises()
+ const click = vi.spyOn(wrapper.get('input[type="file"]').element as HTMLInputElement, 'click')
+
+ await wrapper.findAll('button').find((button) => button.text().includes('上传称重图片'))?.trigger('click')
+
+ expect(click).not.toHaveBeenCalled()
+ })
+
+ it('shows an actionable empty state when the warehouse has no pallet options', async () => {
+ vi.mocked(getWeightPalletOptions).mockResolvedValueOnce([])
+ const wrapper = mountPanel()
+ await flushPromises()
+
+ expect(wrapper.text()).toContain('当前仓库暂无可选托盘')
+ expect(wrapper.findAll('button').some((button) => button.text().includes('重新加载'))).toBe(true)
+ })
+
+ it('shows pallet loading failures without hiding existing weight results', async () => {
+ vi.mocked(getWeightPalletOptions).mockRejectedValueOnce(new ApiResponseError('托盘服务暂不可用', 'A0500', 503))
+ const wrapper = mountPanel()
+ await flushPromises()
+
+ expect(wrapper.text()).toContain('托盘服务暂不可用')
+ expect(wrapper.findAll('.weight-card')).toHaveLength(1)
+ })
+
+ it('does not load weight or pallet data without measurement view permission', async () => {
+ const wrapper = mountPanel([])
+ await flushPromises()
+
+ expect(wrapper.text()).toContain('缺少入库测量查看权限')
+ expect(getWeightWorkspace).not.toHaveBeenCalled()
+ expect(getWeightPalletOptions).not.toHaveBeenCalled()
+ })
+
it('requires explicit image review before confirming a low-confidence OCR value', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
diff --git a/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts b/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts
index df442d78..583fffa2 100644
--- a/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts
+++ b/src/modules/inbounds/weight/components/WeightOcrPanel.test.ts
@@ -16,6 +16,8 @@ describe('WeightOcrPanel contract', () => {
it('covers upload, immediate preview, inline correction, deletion, confirmation, and server totals', () => {
expect(source).toContain('uploadWeightImage')
+ expect(source).toContain('getWeightPalletOptions')
+ expect(source).toContain('palletId: selectedPalletId.value')
expect(source).toContain('downloadWeightImage')
expect(source).toContain('await openPreview(created, URL.createObjectURL(file))')
expect(source).toContain('previewShowsRecognition.value = Boolean(localUrl)')
@@ -26,7 +28,9 @@ describe('WeightOcrPanel contract', () => {
expect(source).toContain('识别重量')
expect(source).toContain('payload.adjustedWeightValue = Number(draft.value)')
expect(source).toContain('payload.adjustmentReason = buildAdjustmentReason(row)')
- expect(source).toContain('已确认总毛重')
+ expect(source).toContain('货物总重量')
+ expect(source).toContain('palletTareWeightKg')
+ expect(source).toContain('netWeightKg')
expect(source).toContain("emit('summaryChanged', workspace.value?.actualGrossWeightKg ?? null)")
})
@@ -53,6 +57,7 @@ describe('WeightOcrPanel contract', () => {
expect(source).toContain('()
const workspace = ref(null)
+const palletOptions = ref([])
+const selectedPalletId = ref('')
+const palletLoading = ref(false)
+const palletError = ref(null)
const loading = ref(false)
const uploading = ref(false)
const loadError = ref(null)
@@ -58,6 +64,7 @@ const canConfirm = computed(() => access.can('platform:firstleg:inbound:weight:c
const canView = computed(() => access.can('platform:firstleg:inbound:measure:view'))
const editable = computed(() => Boolean(workspace.value && editableStatuses.has(workspace.value.processStatus)))
const rows = computed(() => workspace.value?.weightResults ?? [])
+const selectedPallet = computed(() => palletOptions.value.find((option) => option.palletId === selectedPalletId.value) ?? null)
const previewResult = computed(() => rows.value.find((row) => row.id === previewResultId.value)
|| previewSnapshot.value)
const previewConfirmDisabled = computed(() => {
@@ -124,7 +131,7 @@ function isWeightDraftValid(row: WeightResultResource) {
}
function cardWeight(row: WeightResultResource) {
- if (row.recognitionStatus === 'CONFIRMED') return formatWeight(row.finalWeightKg)
+ if (row.recognitionStatus === 'CONFIRMED') return formatWeight(row.netWeightKg)
const draft = weightDrafts.value[row.id]
return formatWeight(draft?.value ?? null, draft?.unit ?? 'KG')
}
@@ -176,11 +183,36 @@ async function loadWorkspace() {
}
}
+async function loadPalletOptions() {
+ if (!customerId.value || !props.inboundId || !canView.value) {
+ palletOptions.value = []
+ selectedPalletId.value = ''
+ return
+ }
+ palletLoading.value = true
+ palletError.value = null
+ try {
+ const result = await getWeightPalletOptions(customerId.value, props.inboundId)
+ palletOptions.value = result
+ if (!result.some((option) => option.palletId === selectedPalletId.value)) selectedPalletId.value = ''
+ } catch (error) {
+ palletOptions.value = []
+ selectedPalletId.value = ''
+ palletError.value = getApiErrorDetails(error, '可选托盘加载失败,请稍后重试')
+ } finally {
+ palletLoading.value = false
+ }
+}
+
function chooseFile() {
if (!editable.value) {
ElMessage.warning('当前入库状态不允许上传称重图片')
return
}
+ if (!selectedPalletId.value) {
+ ElMessage.warning('请先选择本次称重使用的托盘')
+ return
+ }
fileInput.value?.click()
}
@@ -206,6 +238,7 @@ async function handleFileChange(event: Event) {
const upload = await uploadWeightImage(customerId.value, props.inboundId, file)
const created = await createWeightResult(customerId.value, props.inboundId, {
fileId: String(upload.fileId),
+ palletId: selectedPalletId.value,
ocrProvider: 'FILE_WEIGHT_OCR',
ocrRawText: getWeightOcrRawText(upload),
ocrWeightValue: upload.weightValue,
@@ -295,7 +328,7 @@ async function removeResult(row: WeightResultResource) {
try {
await ElMessageBox.confirm(
row.recognitionStatus === 'CONFIRMED'
- ? '删除后将重新计算实际总毛重,确认删除这张称重图片吗?'
+ ? '删除后将重新计算货物总重量,确认删除这张称重图片吗?'
: '确认删除这张称重图片吗?',
'删除称重图片',
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
@@ -324,12 +357,16 @@ watch(
() => {
++loadSequence
workspace.value = null
+ palletOptions.value = []
+ selectedPalletId.value = ''
+ palletError.value = null
weightDrafts.value = {}
loadError.value = null
actionError.value = null
previewOpen.value = false
releasePreview()
void loadWorkspace()
+ void loadPalletOptions()
},
{ immediate: true },
)
@@ -350,13 +387,31 @@ defineExpose({ reload: loadWorkspace })
+
+
+
+
+ 重新加载
+
当前入库状态为 {{ inboundStatusLabel(workspace.processStatus, inboundProcessStatusLabels, '未知状态') }},称重数据仅供查看。
@@ -440,6 +516,12 @@ defineExpose({ reload: loadWorkspace })
:aria-label="`查看称重图片 ${index + 1},重量 ${cardWeight(row)}`"
@click="openPreview(row)"
>{{ cardWeight(row) }}
+
+ - 托盘
- {{ row.palletCode }}
+ - 含托盘毛重
- {{ formatWeight(row.grossWeightKg) }}
+ - 托盘皮重
- {{ formatWeight(row.palletTareWeightKg) }}
+ - 货物净重
- {{ formatWeight(row.netWeightKg) }}
+
@@ -471,8 +553,14 @@ defineExpose({ reload: loadWorkspace })
+ - 托盘编码
- {{ previewResult.palletCode }}
+ - 托盘类型
- {{ previewResult.palletType }}
+ - 托盘皮重
- {{ formatWeight(previewResult.palletTareWeightKg) }}
+ - 托盘容量
- {{ previewResult.palletCapacityCbm }} m³
- 识别状态
- {{ recognitionLabel(previewResult.recognitionStatus) }}
- 置信度
- {{ formatConfidence(previewResult.confidence) }}
+ - 服务端毛重
- {{ formatWeight(previewResult.grossWeightKg) }}
+ - 服务端净重
- {{ formatWeight(previewResult.netWeightKg) }}
- OCR 原文
- {{ previewResult.ocrRawText || '未识别到有效重量' }}
- 人工修正
- {{ previewResult.adjustmentReason }}
@@ -487,14 +575,14 @@ defineExpose({ reload: loadWorkspace })
title="OCR 可能误读数字或小数点,确认前请对照原图核对。"
/>
- 确认重量{{ formatWeight(previewResult.finalWeightKg) }}
+ 货物净重{{ formatWeight(previewResult.netWeightKg) }}
识别重量 {{ formatWeight(previewResult.ocrWeightValue, previewResult.ocrWeightUnit || '') }}