优化
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { DOMWrapper, enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import {
|
||||
confirmWeightResult,
|
||||
correctWeightResult,
|
||||
@@ -92,11 +93,9 @@ function mountPanel(permissions = allWeightPermissions) {
|
||||
})
|
||||
}
|
||||
|
||||
async function openWeightDialog(wrapper: VueWrapper, reviewEvidence = true) {
|
||||
async function openWeightDialog(wrapper: VueWrapper) {
|
||||
await wrapper.get('button[aria-label="查看称重图片 1"]').trigger('click')
|
||||
await flushPromises()
|
||||
const evidence = document.body.querySelector<HTMLInputElement>('.ocr-review-check input[type="checkbox"]')
|
||||
if (reviewEvidence && evidence) await new DOMWrapper(evidence).setValue(true)
|
||||
}
|
||||
|
||||
function dialogWeightInput() {
|
||||
@@ -117,12 +116,6 @@ function dialogConfirmButton() {
|
||||
return new DOMWrapper(button)
|
||||
}
|
||||
|
||||
function dialogEvidenceCheckbox() {
|
||||
const input = document.body.querySelector<HTMLInputElement>('.ocr-review-check input[type="checkbox"]')
|
||||
if (!input) throw new Error('OCR evidence checkbox is missing')
|
||||
return new DOMWrapper(input)
|
||||
}
|
||||
|
||||
describe('WeightOcrPanel confirmation behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -178,7 +171,9 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
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.textContent).toContain('当前托盘皮重')
|
||||
expect(document.body.textContent).toContain('OCR 识别毛重')
|
||||
expect(document.body.textContent).toContain('12.500 KG')
|
||||
expect(document.body.querySelector('input[aria-label="当前图片确认重量"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -215,17 +210,21 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(deleteWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', 0)
|
||||
})
|
||||
|
||||
it('requires image review before confirming even a high-confidence OCR value', async () => {
|
||||
it('shows only the key weights and allows direct confirmation', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
await openWeightDialog(wrapper, false)
|
||||
await openWeightDialog(wrapper)
|
||||
|
||||
expect(document.body.textContent).toContain('OCR 原文')
|
||||
expect(document.body.textContent).toContain('285.5')
|
||||
expect(document.body.textContent).toContain('98%')
|
||||
expect(document.body.textContent).toContain('OCR 可能误读数字或小数点')
|
||||
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
|
||||
await dialogEvidenceCheckbox().setValue(true)
|
||||
const dialogText = document.body.textContent || ''
|
||||
expect(dialogText).toContain('当前托盘皮重')
|
||||
expect(dialogText).toContain('12.000 KG')
|
||||
expect(dialogText).toContain('OCR 识别毛重')
|
||||
expect(dialogText).toContain('285.500 KG')
|
||||
expect(dialogText).toContain('确认含托盘毛重')
|
||||
for (const hiddenLabel of ['托盘编码', '托盘类型', '托盘容量', '识别状态', '置信度', '服务端毛重', '服务端净重', 'OCR 原文']) {
|
||||
expect(dialogText).not.toContain(hiddenLabel)
|
||||
}
|
||||
expect(dialogConfirmButton().attributes('disabled')).toBeUndefined()
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
@@ -255,6 +254,18 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(wrapper.text()).toContain('285.500 KG')
|
||||
})
|
||||
|
||||
it('closes the confirmation dialog immediately after a successful confirmation', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
await openWeightDialog(wrapper)
|
||||
|
||||
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires a pallet selection before opening the file picker', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
@@ -292,22 +303,24 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(getWeightPalletOptions).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requires explicit image review before confirming a low-confidence OCR value', async () => {
|
||||
it('keeps a low-confidence OCR result focused on the editable weight', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
weightResults: [{ ...pendingWeight, ocrRawText: '5360', ocrWeightValue: 5360, confidence: 0.51863 }],
|
||||
})
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
await openWeightDialog(wrapper, false)
|
||||
await openWeightDialog(wrapper)
|
||||
|
||||
expect(document.body.textContent).toContain('OCR 可能误读数字或小数点')
|
||||
expect(document.body.textContent).toContain('OCR 原文')
|
||||
expect(document.body.textContent).toContain('5360')
|
||||
expect(document.body.textContent).toContain('52%')
|
||||
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
|
||||
const dialogText = document.body.textContent || ''
|
||||
expect(dialogText).toContain('OCR 识别毛重')
|
||||
expect(dialogText).toContain('5360.000 KG')
|
||||
expect(dialogText).toContain('确认含托盘毛重')
|
||||
expect(dialogText).not.toContain('置信度')
|
||||
expect(dialogText).not.toContain('OCR 原文')
|
||||
expect(dialogText).not.toContain('OCR 可能误读数字或小数点')
|
||||
expect(dialogConfirmButton().attributes('disabled')).toBeUndefined()
|
||||
|
||||
await dialogEvidenceCheckbox().setValue(true)
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
@@ -338,16 +351,16 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await flushPromises()
|
||||
await openWeightDialog(wrapper)
|
||||
|
||||
await dialogWeightInput().setValue('10.125')
|
||||
await dialogWeightInput().setValue('30.125')
|
||||
await dialogUnit(1).trigger('click')
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 10.125,
|
||||
adjustedWeightValue: 30.125,
|
||||
adjustedWeightUnit: 'LB',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 10.125 LB',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 30.125 LB',
|
||||
})
|
||||
expect(correctWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -364,7 +377,8 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(confirmWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('accepts the smallest LB value that remains representable in kilograms', async () => {
|
||||
it('rejects a positive gross weight that does not exceed the pallet tare', async () => {
|
||||
const warning = vi.spyOn(ElMessage, 'warning')
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
await openWeightDialog(wrapper)
|
||||
@@ -374,12 +388,8 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 0.002,
|
||||
adjustedWeightUnit: 'LB',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 0.002 LB',
|
||||
})
|
||||
expect(confirmWeightResult).not.toHaveBeenCalled()
|
||||
expect(warning).toHaveBeenCalledWith('称重毛重必须大于托盘皮重 12.000 KG')
|
||||
})
|
||||
|
||||
it('clamps a value outside the safe three-decimal JavaScript range', async () => {
|
||||
@@ -407,16 +417,16 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await openWeightDialog(wrapper)
|
||||
const input = dialogWeightInput()
|
||||
|
||||
await input.setValue('1.2345')
|
||||
expect((input.element as HTMLInputElement).value).toBe('1.235')
|
||||
await input.setValue('13.2345')
|
||||
expect((input.element as HTMLInputElement).value).toBe('13.235')
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 1.235,
|
||||
adjustedWeightValue: 13.235,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 1.235 KG',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 13.235 KG',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -487,16 +497,15 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await openWeightDialog(wrapper)
|
||||
|
||||
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
|
||||
await dialogWeightInput().setValue('1.500')
|
||||
await dialogEvidenceCheckbox().setValue(true)
|
||||
await dialogWeightInput().setValue('13.500')
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 1.5,
|
||||
adjustedWeightValue: 13.5,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 —,人工确认 1.500 KG',
|
||||
adjustmentReason: 'OCR 原值 —,人工确认 13.500 KG',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -556,7 +565,10 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
})
|
||||
|
||||
it('keeps the edited draft available when confirmation fails', async () => {
|
||||
vi.mocked(confirmWeightResult).mockRejectedValueOnce(new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 409))
|
||||
const errorMessage = vi.spyOn(ElMessage, 'error')
|
||||
vi.mocked(confirmWeightResult).mockRejectedValueOnce(
|
||||
new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 409, undefined, 'request-1'),
|
||||
)
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
await openWeightDialog(wrapper)
|
||||
@@ -567,7 +579,13 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect((input.element as HTMLInputElement).value).toBe('286.125')
|
||||
expect(wrapper.text()).toContain('数据版本已变化,请刷新后重试')
|
||||
expect(wrapper.text()).not.toContain('操作未完成')
|
||||
expect(errorMessage).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: '数据版本已变化,请刷新后重试(请求 ID:request-1)',
|
||||
duration: 5_000,
|
||||
showClose: true,
|
||||
}))
|
||||
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
|
||||
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,12 +20,13 @@ describe('WeightOcrPanel contract', () => {
|
||||
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)')
|
||||
expect(source).toContain('deleteWeightResult')
|
||||
expect(source).toContain('confirmWeightResult')
|
||||
expect(source).toContain('class="weight-card__value"')
|
||||
expect(source).toContain('v-model="weightDrafts[previewResult.id].value"')
|
||||
expect(source).toContain('识别重量')
|
||||
expect(source).toContain('当前托盘皮重')
|
||||
expect(source).toContain('OCR 识别毛重')
|
||||
expect(source).toContain('确认含托盘毛重')
|
||||
expect(source).toContain('payload.adjustedWeightValue = Number(draft.value)')
|
||||
expect(source).toContain('payload.adjustmentReason = buildAdjustmentReason(row)')
|
||||
expect(source).toContain('货物总重量')
|
||||
@@ -38,11 +39,15 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain("['image/jpeg', 'image/png', 'image/bmp']")
|
||||
expect(source).toContain('5 * 1024 * 1024')
|
||||
expect(source).toContain('ocrRawText: getWeightOcrRawText(upload)')
|
||||
const template = source.match(/<template>([\s\S]*?)<\/template>/)?.[1] || ''
|
||||
expect(template).not.toContain('置信度')
|
||||
expect(template).not.toContain('OCR 原文')
|
||||
expect(template).not.toContain('提供方')
|
||||
expect(template).not.toContain('文件 ID')
|
||||
expect(source).not.toContain('ocr-review-check')
|
||||
expect(source).not.toContain('OCR 可能误读数字或小数点')
|
||||
expect(source).not.toContain('<span>置信度</span>')
|
||||
expect(source).not.toContain('<span>OCR 原文</span>')
|
||||
expect(source).not.toContain('<span>托盘编码</span>')
|
||||
expect(source).not.toContain('<span>托盘类型</span>')
|
||||
expect(source).not.toContain('<span>托盘容量</span>')
|
||||
expect(source).not.toContain('<span>服务端毛重</span>')
|
||||
expect(source).not.toContain('<span>服务端净重</span>')
|
||||
})
|
||||
|
||||
it('gates every action with the implemented permissions and editable statuses', () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from '../api/weightOcr'
|
||||
import type { WeightPalletOption, WeightResultResource, WeightWorkspaceResource } from '../types'
|
||||
import WeightImageThumbnail from './WeightImageThumbnail.vue'
|
||||
import { inboundProcessStatusLabels, inboundStatusLabel, weightRecognitionStatusLabels } from '../../status'
|
||||
import { inboundProcessStatusLabels, inboundStatusLabel } from '../../status'
|
||||
|
||||
const props = defineProps<{ customerId?: string; tenantId?: string; inboundId: string }>()
|
||||
const emit = defineEmits<{
|
||||
@@ -42,16 +42,13 @@ const palletError = ref<ApiErrorDetails | null>(null)
|
||||
const loading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const actionError = ref<ApiErrorDetails | null>(null)
|
||||
const activeAction = ref('')
|
||||
const previewOpen = ref(false)
|
||||
const previewLoading = ref(false)
|
||||
const previewError = ref<ApiErrorDetails | null>(null)
|
||||
const previewUrl = ref('')
|
||||
const previewShowsRecognition = ref(false)
|
||||
const previewResultId = ref('')
|
||||
const previewSnapshot = ref<WeightResultResource | null>(null)
|
||||
const previewEvidenceReviewed = ref(false)
|
||||
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB' }>>({})
|
||||
let loadSequence = 0
|
||||
|
||||
@@ -72,25 +69,12 @@ const previewConfirmDisabled = computed(() => {
|
||||
return !row
|
||||
|| !isWeightDraftValid(row)
|
||||
|| Boolean(activeAction.value)
|
||||
|| (requiresEvidenceReview(row) && !previewEvidenceReviewed.value)
|
||||
})
|
||||
|
||||
function formatWeight(value: number | null, unit = 'KG') {
|
||||
return value == null ? '—' : `${Number(value).toFixed(3)} ${unit}`
|
||||
}
|
||||
|
||||
function formatConfidence(value: number | null) {
|
||||
return value == null ? '未返回' : `${Math.round(value * 100)}%`
|
||||
}
|
||||
|
||||
function recognitionLabel(status: string) {
|
||||
return inboundStatusLabel(status, weightRecognitionStatusLabels, '未知状态')
|
||||
}
|
||||
|
||||
function requiresEvidenceReview(row: WeightResultResource) {
|
||||
return row.recognitionStatus !== 'CONFIRMED' && row.adjustedWeightValue == null
|
||||
}
|
||||
|
||||
function actionKey(action: string, id: string) {
|
||||
return `${action}:${id}`
|
||||
}
|
||||
@@ -130,6 +114,23 @@ function isWeightDraftValid(row: WeightResultResource) {
|
||||
&& value <= maximumSafeWeight)
|
||||
}
|
||||
|
||||
function draftGrossWeightKg(row: WeightResultResource) {
|
||||
const draft = weightDrafts.value[row.id]
|
||||
if (!draft || !Number.isFinite(Number(draft.value))) return null
|
||||
const value = Number(draft.value)
|
||||
return draft.unit === 'LB' ? value * 0.45359237 : value
|
||||
}
|
||||
|
||||
function showActionError(error: unknown, fallback: string) {
|
||||
const details = getApiErrorDetails(error, fallback)
|
||||
const requestReference = details.requestId ? `(请求 ID:${details.requestId})` : ''
|
||||
ElMessage.error({
|
||||
message: `${details.message}${requestReference}`,
|
||||
duration: 5_000,
|
||||
showClose: true,
|
||||
})
|
||||
}
|
||||
|
||||
function cardWeight(row: WeightResultResource) {
|
||||
if (row.recognitionStatus === 'CONFIRMED') return formatWeight(row.netWeightKg)
|
||||
const draft = weightDrafts.value[row.id]
|
||||
@@ -233,7 +234,6 @@ async function handleFileChange(event: Event) {
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
actionError.value = null
|
||||
try {
|
||||
const upload = await uploadWeightImage(customerId.value, props.inboundId, file)
|
||||
const created = await createWeightResult(customerId.value, props.inboundId, {
|
||||
@@ -251,7 +251,7 @@ async function handleFileChange(event: Event) {
|
||||
ensureWeightDraft(created)
|
||||
emitWorkspaceChanged()
|
||||
} catch (error) {
|
||||
actionError.value = getApiErrorDetails(error, '称重图片上传失败,请检查后重试')
|
||||
showActionError(error, '称重图片上传失败,请检查后重试')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
@@ -263,13 +263,13 @@ async function confirmResult(row: WeightResultResource, closeAfterSuccess = fals
|
||||
ElMessage.warning('请输入大于 0 的有效重量')
|
||||
return
|
||||
}
|
||||
if (requiresEvidenceReview(row) && !previewEvidenceReviewed.value) {
|
||||
ElMessage.warning('请先对照图片核对小数点和重量单位')
|
||||
const grossWeightKg = draftGrossWeightKg(row)
|
||||
if (grossWeightKg == null || grossWeightKg <= Number(row.palletTareWeightKg)) {
|
||||
ElMessage.warning(`称重毛重必须大于托盘皮重 ${formatWeight(row.palletTareWeightKg)}`)
|
||||
return
|
||||
}
|
||||
const key = actionKey('confirm', row.id)
|
||||
activeAction.value = key
|
||||
actionError.value = null
|
||||
try {
|
||||
const payload = { version: row.version } as Parameters<typeof confirmWeightResult>[3]
|
||||
if (hasWeightDraftChanged(row)) {
|
||||
@@ -282,13 +282,13 @@ async function confirmResult(row: WeightResultResource, closeAfterSuccess = fals
|
||||
payload.adjustmentReason = buildAdjustmentReason(row)
|
||||
}
|
||||
await confirmWeightResult(customerId.value, props.inboundId, row.id, payload)
|
||||
if (closeAfterSuccess) previewOpen.value = false
|
||||
ElMessage.success('重量已确认')
|
||||
await loadWorkspace()
|
||||
if (closeAfterSuccess) previewOpen.value = false
|
||||
emitWorkspaceChanged()
|
||||
return true
|
||||
} catch (error) {
|
||||
actionError.value = getApiErrorDetails(error, '重量确认失败,请刷新后重试')
|
||||
showActionError(error, '重量确认失败,请刷新后重试')
|
||||
} finally {
|
||||
activeAction.value = ''
|
||||
}
|
||||
@@ -304,8 +304,6 @@ async function openPreview(row: WeightResultResource, localUrl = '') {
|
||||
releasePreview()
|
||||
previewResultId.value = row.id
|
||||
previewSnapshot.value = row
|
||||
previewEvidenceReviewed.value = false
|
||||
previewShowsRecognition.value = Boolean(localUrl)
|
||||
previewError.value = null
|
||||
previewOpen.value = true
|
||||
if (localUrl) {
|
||||
@@ -338,7 +336,6 @@ async function removeResult(row: WeightResultResource) {
|
||||
}
|
||||
const key = actionKey('delete', row.id)
|
||||
activeAction.value = key
|
||||
actionError.value = null
|
||||
try {
|
||||
await deleteWeightResult(customerId.value, props.inboundId, row.id, row.version)
|
||||
if (previewResultId.value === row.id) previewOpen.value = false
|
||||
@@ -346,7 +343,7 @@ async function removeResult(row: WeightResultResource) {
|
||||
await loadWorkspace()
|
||||
emitWorkspaceChanged()
|
||||
} catch (error) {
|
||||
actionError.value = getApiErrorDetails(error, '称重图片删除失败,请刷新后重试')
|
||||
showActionError(error, '称重图片删除失败,请刷新后重试')
|
||||
} finally {
|
||||
activeAction.value = ''
|
||||
}
|
||||
@@ -362,7 +359,6 @@ watch(
|
||||
palletError.value = null
|
||||
weightDrafts.value = {}
|
||||
loadError.value = null
|
||||
actionError.value = null
|
||||
previewOpen.value = false
|
||||
releasePreview()
|
||||
void loadWorkspace()
|
||||
@@ -373,7 +369,6 @@ watch(
|
||||
|
||||
watch(previewOpen, (open) => {
|
||||
if (!open) {
|
||||
previewEvidenceReviewed.value = false
|
||||
releasePreview()
|
||||
}
|
||||
})
|
||||
@@ -387,7 +382,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
<header class="weight-heading">
|
||||
<div>
|
||||
<h3 id="weight-ocr-title"><Scale :size="18" aria-hidden="true" />称重图片</h3>
|
||||
<p>先选择托盘,再上传图片核对含托盘毛重;皮重与货物净重由服务端计算。</p>
|
||||
</div>
|
||||
<div class="weight-heading__actions">
|
||||
<div v-if="workspace" class="weight-summary" aria-live="polite">
|
||||
@@ -472,21 +466,9 @@ defineExpose({ reload: loadWorkspace })
|
||||
当前入库状态为 {{ inboundStatusLabel(workspace.processStatus, inboundProcessStatusLabels, '未知状态') }},称重数据仅供查看。
|
||||
</el-alert>
|
||||
|
||||
<LoadErrorState
|
||||
v-if="actionError"
|
||||
title="操作未完成"
|
||||
:message="actionError.message"
|
||||
:show-retry="false"
|
||||
compact
|
||||
class="weight-action-error"
|
||||
>
|
||||
<template #details><ApiErrorMeta :details="actionError" /></template>
|
||||
</LoadErrorState>
|
||||
|
||||
<div v-loading="loading" class="weight-list" aria-live="polite">
|
||||
<div v-if="!loading && rows.length === 0" class="weight-empty">
|
||||
<strong>尚未上传称重图片</strong>
|
||||
<p>上传 JPG、PNG 或 BMP 图片,单张不超过 5MB。</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="weight-grid">
|
||||
@@ -538,7 +520,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
:show-confirm="Boolean(previewResult && previewResult.recognitionStatus !== 'CONFIRMED' && canConfirm && editable)"
|
||||
confirm-text="确认重量"
|
||||
confirm-type="success"
|
||||
width="min(680px, calc(100vw - 28px))"
|
||||
width="min(640px, calc(100vw - 28px))"
|
||||
@confirm="previewResult && confirmResult(previewResult, true)"
|
||||
>
|
||||
<LoadErrorState
|
||||
@@ -552,35 +534,11 @@ defineExpose({ reload: loadWorkspace })
|
||||
</LoadErrorState>
|
||||
<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>
|
||||
</div>
|
||||
</dl>
|
||||
<el-alert
|
||||
v-if="previewResult.recognitionStatus !== 'CONFIRMED' && requiresEvidenceReview(previewResult)"
|
||||
class="ocr-review-alert"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="OCR 可能误读数字或小数点,确认前请对照原图核对。"
|
||||
/>
|
||||
<div v-if="previewResult.recognitionStatus === 'CONFIRMED'" class="preview-weight is-confirmed">
|
||||
<span>货物净重</span><strong>{{ formatWeight(previewResult.netWeightKg) }}</strong>
|
||||
<div class="weight-key-facts" aria-label="称重关键数据">
|
||||
<div><span>当前托盘皮重</span><strong>{{ formatWeight(previewResult.palletTareWeightKg) }}</strong></div>
|
||||
<div><span>OCR 识别毛重</span><strong>{{ formatWeight(previewResult.ocrWeightValue, previewResult.ocrWeightUnit || 'KG') }}</strong></div>
|
||||
</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>
|
||||
<div v-if="weightDrafts[previewResult.id]" class="preview-weight">
|
||||
<label>
|
||||
<span>确认含托盘毛重</span>
|
||||
<el-input-number
|
||||
@@ -590,21 +548,15 @@ defineExpose({ reload: loadWorkspace })
|
||||
:max="maximumSafeWeight"
|
||||
:precision="3"
|
||||
:controls="false"
|
||||
:disabled="Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
:disabled="previewResult.recognitionStatus === 'CONFIRMED' || Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
/>
|
||||
</label>
|
||||
<el-segmented
|
||||
v-model="weightDrafts[previewResult.id].unit"
|
||||
aria-label="当前图片重量单位"
|
||||
:options="['KG', 'LB']"
|
||||
:disabled="Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
:disabled="previewResult.recognitionStatus === 'CONFIRMED' || Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
/>
|
||||
<el-checkbox
|
||||
v-if="requiresEvidenceReview(previewResult)"
|
||||
v-model="previewEvidenceReviewed"
|
||||
class="ocr-review-check"
|
||||
:disabled="Boolean(activeAction)"
|
||||
>已对照图片核对小数点和重量单位</el-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
</AppDialog>
|
||||
@@ -616,7 +568,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding-bottom: 18px; border-bottom: 1px solid var(--cc-border); }
|
||||
.weight-heading h3 { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 15px; }
|
||||
.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; }
|
||||
@@ -625,10 +576,9 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-summary strong { color: #067647; font-size: 18px; }
|
||||
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
.weight-state-note, .weight-action-error { margin-bottom: 14px; }
|
||||
.weight-list { min-height: 120px; margin-top: 16px; }
|
||||
.weight-empty { display: grid; min-height: 180px; place-items: center; align-content: center; gap: 8px; padding: 24px; color: var(--cc-muted); text-align: center; }
|
||||
.weight-list { min-height: 0; margin-top: 14px; }
|
||||
.weight-empty { display: flex; min-height: 0; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 4px; background: var(--cc-surface-muted); color: var(--cc-muted); }
|
||||
.weight-empty strong { color: #344054; font-size: 14px; }
|
||||
.weight-empty p { margin: 0; font-size: 12px; }
|
||||
.weight-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 14px; }
|
||||
.weight-card { position: relative; min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; }
|
||||
.weight-card__body { padding: 12px; text-align: center; }
|
||||
@@ -640,22 +590,14 @@ defineExpose({ reload: loadWorkspace })
|
||||
.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; }
|
||||
.ocr-evidence dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.ocr-evidence dd { margin: 4px 0 0; color: #344054; font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||
.ocr-evidence__raw { grid-column: 1 / -1; }
|
||||
.ocr-review-alert { margin-top: 12px; }
|
||||
.weight-preview { display: block; width: 100%; max-height: min(50vh, 460px); object-fit: contain; border-radius: 6px; background: #f2f4f7; }
|
||||
.weight-key-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 14px; padding: 14px 0; border-block: 1px solid var(--cc-border); }
|
||||
.weight-key-facts div { display: grid; min-width: 0; gap: 4px; }
|
||||
.weight-key-facts span { color: var(--cc-muted); font-size: 12px; }
|
||||
.weight-key-facts strong { color: #101828; font-size: 18px; }
|
||||
.preview-weight { display: grid; grid-template-columns: minmax(0, 1fr) 120px; align-items: end; gap: 12px; margin-top: 14px; }
|
||||
.preview-weight__recognized { grid-column: 1 / -1; color: var(--cc-muted); font-size: 12px; }
|
||||
.preview-weight label { display: grid; gap: 6px; color: #475467; font-size: 12px; }
|
||||
.preview-weight :deep(.el-input-number), .preview-weight :deep(.el-segmented) { width: 100%; }
|
||||
.ocr-review-check { grid-column: 1 / -1; white-space: normal; }
|
||||
.ocr-review-check :deep(.el-checkbox__label) { line-height: 1.5; white-space: normal; }
|
||||
.preview-weight.is-confirmed { display: flex; align-items: baseline; justify-content: space-between; padding: 12px 0 0; }
|
||||
.preview-weight.is-confirmed span { color: var(--cc-muted); font-size: 12px; }
|
||||
.preview-weight.is-confirmed strong { color: #067647; font-size: 20px; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.weight-heading { flex-direction: column; }
|
||||
@@ -668,11 +610,10 @@ defineExpose({ reload: loadWorkspace })
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.weight-grid { grid-template-columns: 1fr; }
|
||||
.weight-grid { grid-template-columns: 1fr; }
|
||||
.preview-weight { grid-template-columns: 1fr; }
|
||||
.preview-weight__recognized { grid-column: 1; }
|
||||
.ocr-evidence { grid-template-columns: 1fr; }
|
||||
.ocr-evidence__raw { grid-column: 1; }
|
||||
.weight-key-facts { grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.weight-key-facts strong { font-size: 15px; }
|
||||
.weight-card__facts div { align-items: flex-start; flex-direction: column; gap: 2px; }
|
||||
.weight-card__facts dd { text-align: left; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user