This commit is contained in:
2026-08-14 17:38:06 +08:00
parent d1b4a4d3f3
commit 010204da57
12 changed files with 259 additions and 33 deletions
@@ -3,6 +3,7 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
ImagePlus,
RefreshCw,
Scale,
Trash2,
} from '@lucide/vue'
@@ -17,10 +18,11 @@ import {
deleteWeightResult,
downloadWeightImage,
getWeightOcrRawText,
getWeightPalletOptions,
getWeightWorkspace,
uploadWeightImage,
} from '../api/weightOcr'
import type { WeightResultResource, WeightWorkspaceResource } from '../types'
import type { WeightPalletOption, WeightResultResource, WeightWorkspaceResource } from '../types'
import WeightImageThumbnail from './WeightImageThumbnail.vue'
import { inboundProcessStatusLabels, inboundStatusLabel, weightRecognitionStatusLabels } from '../../status'
@@ -33,6 +35,10 @@ const emit = defineEmits<{
const access = useAccessPreviewStore()
const fileInput = ref<HTMLInputElement>()
const workspace = ref<WeightWorkspaceResource | null>(null)
const palletOptions = ref<WeightPalletOption[]>([])
const selectedPalletId = ref('')
const palletLoading = ref(false)
const palletError = ref<ApiErrorDetails | null>(null)
const loading = ref(false)
const uploading = ref(false)
const loadError = ref<ApiErrorDetails | null>(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 })
<header class="weight-heading">
<div>
<h3 id="weight-ocr-title"><Scale :size="18" aria-hidden="true" />称重图片</h3>
<p>上传后直接核对重量,确认结果自动计入实际总毛重。</p>
<p>先选择托盘,再上传图片核对含托盘毛重;皮重与货物净重由服务端计算。</p>
</div>
<div class="weight-heading__actions">
<div v-if="workspace" class="weight-summary" aria-live="polite">
<span>已确认总毛重</span>
<span>货物总重量</span>
<strong>{{ formatWeight(workspace.actualGrossWeightKg) }}</strong>
</div>
<div class="pallet-picker">
<el-select
v-model="selectedPalletId"
aria-label="选择称重托盘"
placeholder="选择称重托盘"
filterable
:loading="palletLoading"
:disabled="loading || uploading || !editable || !canEdit || Boolean(palletError)"
>
<el-option
v-for="option in palletOptions"
:key="option.palletId"
:label="`${option.palletCode} · ${option.palletType}`"
:value="option.palletId"
/>
</el-select>
<small v-if="selectedPallet">皮重 {{ formatWeight(selectedPallet.tareWeightKg) }} · 容量 {{ selectedPallet.capacityCbm }} m³</small>
</div>
<input
ref="fileInput"
class="visually-hidden-input"
@@ -369,7 +424,7 @@ defineExpose({ reload: loadWorkspace })
v-if="canEdit"
type="primary"
:loading="uploading"
:disabled="loading || !editable"
:disabled="loading || palletLoading || !editable || !selectedPalletId || Boolean(palletError)"
@click="chooseFile"
><ImagePlus :size="16" />上传称重图片</el-button>
</div>
@@ -392,6 +447,27 @@ defineExpose({ reload: loadWorkspace })
</LoadErrorState>
<template v-else>
<LoadErrorState
v-if="palletError"
title="可选托盘加载失败"
:message="palletError.message"
:loading="palletLoading"
compact
class="weight-action-error"
@retry="loadPalletOptions"
>
<template #details><ApiErrorMeta :details="palletError" /></template>
</LoadErrorState>
<el-alert
v-else-if="!palletLoading && palletOptions.length === 0 && editable && canEdit"
type="warning"
:closable="false"
show-icon
class="weight-action-error"
title="当前仓库暂无可选托盘,暂时不能上传称重图片。"
>
<el-button size="small" @click="loadPalletOptions"><RefreshCw :size="14" />重新加载</el-button>
</el-alert>
<el-alert v-if="workspace && !editable" type="info" :closable="false" show-icon class="weight-state-note">
当前入库状态为 {{ inboundStatusLabel(workspace.processStatus, inboundProcessStatusLabels, '未知状态') }},称重数据仅供查看。
</el-alert>
@@ -440,6 +516,12 @@ defineExpose({ reload: loadWorkspace })
:aria-label="`查看称重图片 ${index + 1},重量 ${cardWeight(row)}`"
@click="openPreview(row)"
><strong>{{ cardWeight(row) }}</strong></button>
<dl class="weight-card__facts">
<div><dt>托盘</dt><dd>{{ row.palletCode }}</dd></div>
<div><dt>含托盘毛重</dt><dd>{{ formatWeight(row.grossWeightKg) }}</dd></div>
<div><dt>托盘皮重</dt><dd>{{ formatWeight(row.palletTareWeightKg) }}</dd></div>
<div><dt>货物净重</dt><dd>{{ formatWeight(row.netWeightKg) }}</dd></div>
</dl>
</div>
</article>
</div>
@@ -471,8 +553,14 @@ defineExpose({ reload: loadWorkspace })
<template v-else-if="previewResult">
<img v-if="previewUrl" class="weight-preview" :src="previewUrl" alt="当前入库单的称重图片" />
<dl class="ocr-evidence" aria-label="OCR 识别证据">
<div><dt>托盘编码</dt><dd>{{ previewResult.palletCode }}</dd></div>
<div><dt>托盘类型</dt><dd>{{ previewResult.palletType }}</dd></div>
<div><dt>托盘皮重</dt><dd>{{ formatWeight(previewResult.palletTareWeightKg) }}</dd></div>
<div><dt>托盘容量</dt><dd>{{ previewResult.palletCapacityCbm }} m³</dd></div>
<div><dt>识别状态</dt><dd>{{ recognitionLabel(previewResult.recognitionStatus) }}</dd></div>
<div><dt>置信度</dt><dd>{{ formatConfidence(previewResult.confidence) }}</dd></div>
<div><dt>服务端毛重</dt><dd>{{ formatWeight(previewResult.grossWeightKg) }}</dd></div>
<div><dt>服务端净重</dt><dd>{{ formatWeight(previewResult.netWeightKg) }}</dd></div>
<div class="ocr-evidence__raw"><dt>OCR 原文</dt><dd>{{ previewResult.ocrRawText || '未识别到有效重量' }}</dd></div>
<div v-if="previewResult.adjustmentReason" class="ocr-evidence__raw">
<dt>人工修正</dt><dd>{{ previewResult.adjustmentReason }}</dd>
@@ -487,14 +575,14 @@ defineExpose({ reload: loadWorkspace })
title="OCR 可能误读数字或小数点,确认前请对照原图核对。"
/>
<div v-if="previewResult.recognitionStatus === 'CONFIRMED'" class="preview-weight is-confirmed">
<span>确认重量</span><strong>{{ formatWeight(previewResult.finalWeightKg) }}</strong>
<span>货物净重</span><strong>{{ formatWeight(previewResult.netWeightKg) }}</strong>
</div>
<div v-else-if="weightDrafts[previewResult.id]" class="preview-weight">
<span v-if="previewShowsRecognition" class="preview-weight__recognized">
识别重量 {{ formatWeight(previewResult.ocrWeightValue, previewResult.ocrWeightUnit || '') }}
</span>
<label>
<span>确认重量</span>
<span>确认含托盘毛重</span>
<el-input-number
v-model="weightDrafts[previewResult.id].value"
aria-label="当前图片确认重量"
@@ -530,6 +618,8 @@ defineExpose({ reload: loadWorkspace })
.weight-heading h3 svg { color: var(--cc-primary); }
.weight-heading p { margin: 6px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.6; }
.weight-heading__actions { display: flex; flex: 0 0 auto; align-items: center; gap: 16px; }
.pallet-picker { display: grid; min-width: 260px; gap: 4px; }
.pallet-picker small { color: var(--cc-muted); font-size: 11px; line-height: 1.4; }
.weight-summary { display: grid; gap: 2px; text-align: right; }
.weight-summary span { color: var(--cc-muted); font-size: 12px; }
.weight-summary strong { color: #067647; font-size: 18px; }
@@ -546,6 +636,10 @@ defineExpose({ reload: loadWorkspace })
.weight-card__value { width: 100%; padding: 0; border: 0; background: transparent; color: inherit; cursor: pointer; text-align: center; }
.weight-card__value strong { color: #101828; font-size: 17px; }
.weight-card__value:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 4px; }
.weight-card__facts { display: grid; gap: 6px; margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--cc-border); }
.weight-card__facts div { display: flex; justify-content: space-between; gap: 8px; font-size: 11px; line-height: 1.4; }
.weight-card__facts dt { color: var(--cc-muted); }
.weight-card__facts dd { min-width: 0; margin: 0; color: #344054; overflow-wrap: anywhere; text-align: right; }
.weight-preview { display: block; width: 100%; max-height: min(58vh, 560px); object-fit: contain; border-radius: 6px; background: #f2f4f7; }
.ocr-evidence { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 18px; margin: 14px 0 0; padding: 12px 0; border-block: 1px solid var(--cc-border); }
.ocr-evidence div { min-width: 0; }
@@ -566,6 +660,8 @@ defineExpose({ reload: loadWorkspace })
@media (max-width: 720px) {
.weight-heading { flex-direction: column; }
.weight-heading__actions { width: 100%; align-items: stretch; flex-direction: column; }
.pallet-picker { width: 100%; min-width: 0; }
.pallet-picker :deep(.el-select) { width: 100%; }
.weight-heading__actions :deep(.el-button) { width: 100%; min-height: 44px; }
.weight-summary { text-align: left; }
.weight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -577,5 +673,7 @@ defineExpose({ reload: loadWorkspace })
.preview-weight__recognized { grid-column: 1; }
.ocr-evidence { grid-template-columns: 1fr; }
.ocr-evidence__raw { grid-column: 1; }
.weight-card__facts div { align-items: flex-start; flex-direction: column; gap: 2px; }
.weight-card__facts dd { text-align: left; }
}
</style>