This commit is contained in:
2026-08-13 19:00:37 +08:00
parent 41038e1edd
commit 0f0ae612ca
18 changed files with 175 additions and 170 deletions
@@ -67,6 +67,16 @@ const measurementColumns: DataTableColumn[] = [
{ key: 'note', label: '超限提示 / 备注', minWidth: 200, slotName: 'note' },
]
const weightColumns: DataTableColumn[] = [
{ key: 'file', label: '称重文件 / OCR 原文', minWidth: 260, slotName: 'file' },
{ 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: 'confidence', label: '置信度', width: 90, slotName: 'confidence' },
{ key: 'reason', label: '调整原因', minWidth: 180, slotName: 'reason' },
]
function formatNumber(value: number | null | undefined, unit = '') {
if (value === null || value === undefined || Number.isNaN(value)) return '-'
return `${value.toLocaleString('zh-CN', { maximumFractionDigits: 3 })}${unit}`
@@ -214,10 +224,7 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
<template>
<section class="review-panel" aria-labelledby="measurement-review-title">
<header class="review-header">
<div>
<h2 id="measurement-review-title">测量审核</h2>
<p>核对原单、设备实测和称重 OCR 后,再决定是否进入自动计费入库。</p>
</div>
<h2 id="measurement-review-title">测量信息</h2>
<div class="review-header-status">
<StatusBadge v-if="detail" :status="reviewStatusLabel" />
<el-button text :loading="loading || historyLoading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
@@ -232,39 +239,29 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
<el-skeleton v-if="loading && !detail" :rows="8" animated />
<template v-else-if="detail">
<div class="comparison-grid">
<article class="comparison-card">
<span class="comparison-label">原单申报</span>
<strong>{{ detail.originalDocumentNo || detail.inboundCode || '-' }}</strong>
<dl>
<div><dt>申报毛重</dt><dd>{{ formatNumber(detail.originalGrossWeightKg, ' kg') }}</dd></div>
<div><dt>申报体积</dt><dd>{{ formatNumber(detail.originalVolumeCbm, ' CBM') }}</dd></div>
</dl>
</article>
<article class="comparison-card">
<span class="comparison-label">设备实测</span>
<strong>{{ formatNumber(detail.actualPackageCount, ' 箱') }}</strong>
<dl>
<div><dt>实测体积</dt><dd>{{ formatNumber(detail.actualVolumeCbm, ' CBM') }}</dd></div>
<div><dt>材积重</dt><dd>{{ formatNumber(detail.volumeWeightKg, ' kg') }}</dd></div>
</dl>
</article>
<article class="comparison-card">
<span class="comparison-label">称重 OCR</span>
<strong>{{ formatNumber(detail.actualGrossWeightKg, ' kg') }}</strong>
<dl>
<div><dt>已确认图片</dt><dd>{{ confirmedWeights }} / {{ detail.weightResults.length }}</dd></div>
<div><dt>测量来源</dt><dd>{{ measurementSourceLabel(detail.sourceType) }}</dd></div>
</dl>
</article>
</div>
<dl class="review-fields">
<div><dt>入库单号</dt><dd>{{ detail.inboundCode || '-' }}</dd></div>
<div><dt>客户名称</dt><dd>{{ detail.companyName || '-' }}</dd></div>
<div><dt>原单申报</dt><dd>{{ detail.originalDocumentNo || '-' }}</dd></div>
<div><dt>渠道</dt><dd>{{ detail.channelName || detail.channelCode || '-' }}</dd></div>
<div><dt>申报箱数</dt><dd>{{ formatNumber(detail.originalPackageCount, ' 箱') }}</dd></div>
<div><dt>实测箱数</dt><dd>{{ formatNumber(detail.actualPackageCount, ' 箱') }}</dd></div>
<div><dt>申报毛重</dt><dd>{{ formatNumber(detail.originalGrossWeightKg, ' kg') }}</dd></div>
<div><dt>称重 OCR</dt><dd>{{ formatNumber(detail.actualGrossWeightKg, ' kg') }}</dd></div>
<div><dt>申报体积</dt><dd>{{ formatNumber(detail.originalVolumeCbm, ' CBM') }}</dd></div>
<div><dt>设备实测体积</dt><dd>{{ formatNumber(detail.actualVolumeCbm, ' CBM') }}</dd></div>
<div><dt>材积重</dt><dd>{{ formatNumber(detail.volumeWeightKg, ' kg') }}</dd></div>
<div><dt>测量来源</dt><dd>{{ measurementSourceLabel(detail.sourceType) }}</dd></div>
<div><dt>已确认称重图片</dt><dd>{{ confirmedWeights }} / {{ detail.weightResults.length }}</dd></div>
<div><dt>审核状态</dt><dd><StatusBadge :status="reviewStatusLabel" /></dd></div>
</dl>
<div v-if="detail.reviewReason" class="review-reason">
<CircleAlert :size="17" /><span><strong>最近审核说明:</strong>{{ detail.reviewReason }}</span>
</div>
<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>
<div class="section-heading"><h3>测量明细</h3><span class="section-count">{{ detail.items.length }} 条</span></div>
<DataTable :data="detail.items" :columns="measurementColumns" empty-text="暂无设备测量明细" border stripe>
<template #boxCode="{ row }">{{ (row as MeasurementReviewItem).boxCode || (row as MeasurementReviewItem).itemNo || '-' }}</template>
<template #packageCount="{ row }">{{ (row as MeasurementReviewItem).packageCount ?? '-' }}</template>
@@ -285,17 +282,16 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
</div>
<div class="review-section">
<div class="section-heading"><div><h3>称重图片识别结果</h3><p>原始 OCR 值不可覆盖,只有已确认且有效的结果会计入实际重量。</p></div><span class="section-count">{{ detail.weightResults.length }} 张</span></div>
<div class="weight-list">
<div v-for="item in detail.weightResults" :key="item.id" class="weight-row">
<div><strong>{{ item.fileId || '未关联文件' }}</strong><span>{{ item.ocrProvider || 'OCR' }} · {{ recognitionStatusLabel(item.recognitionStatus) }}</span><p v-if="item.ocrRawText">OCR 原文:{{ item.ocrRawText }}</p><p v-if="item.adjustmentReason">调整原因:{{ item.adjustmentReason }}</p></div>
<div><span>原始</span><strong>{{ formatNumber(item.ocrWeightValue, ` ${item.ocrWeightUnit || ''}`) }}</strong></div>
<div><span>调整后</span><strong>{{ formatNumber(item.adjustedWeightValue, ` ${item.adjustedWeightUnit || ''}`) }}</strong></div>
<div><span>最终</span><strong>{{ formatNumber(item.finalWeightKg, ' kg') }}</strong></div>
<div><span>置信度</span><strong>{{ item.confidence === null ? '-' : `${Math.round(item.confidence * 100)}%` }}</strong></div>
</div>
<el-empty v-if="!detail.weightResults.length" description="暂无称重图片识别结果" :image-size="48" />
</div>
<div class="section-heading"><h3>称重图片识别结果</h3><span class="section-count">{{ detail.weightResults.length }} 张</span></div>
<DataTable :data="detail.weightResults" :columns="weightColumns" empty-text="暂无称重图片识别结果" border stripe>
<template #file="{ row }"><div class="table-wrap"><strong>{{ row.fileId || '未关联文件' }}</strong><span v-if="row.ocrRawText">OCR 原文:{{ row.ocrRawText }}</span></div></template>
<template #status="{ row }">{{ recognitionStatusLabel(row.recognitionStatus) }}</template>
<template #ocrWeight="{ row }">{{ formatNumber(row.ocrWeightValue, ` ${row.ocrWeightUnit || ''}`) }}</template>
<template #adjustedWeight="{ row }">{{ formatNumber(row.adjustedWeightValue, ` ${row.adjustedWeightUnit || ''}`) }}</template>
<template #finalWeight="{ row }">{{ formatNumber(row.finalWeightKg, ' kg') }}</template>
<template #confidence="{ row }">{{ row.confidence === null ? '-' : `${Math.round(row.confidence * 100)}%` }}</template>
<template #reason="{ row }">{{ row.adjustmentReason ? `调整原因:${row.adjustmentReason}` : '-' }}</template>
</DataTable>
</div>
<div class="review-actions" :class="{ 'review-actions--disabled': !isPending && !isRejected }">
@@ -336,18 +332,16 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
.review-header p, .section-heading p { margin: 0; color: var(--cc-muted); font-size: 13px; line-height: 1.55; }
.review-header-status { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
.review-alert-title { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.comparison-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.comparison-card { min-width: 0; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--cc-surface); }
.comparison-card strong { display: block; margin: 8px 0 12px; font-size: 21px; line-height: 1.2; overflow-wrap: anywhere; }
.comparison-label { color: var(--cc-muted); font-size: 12px; font-weight: 700; }
dl { display: grid; gap: 7px; margin: 0; }.comparison-card dl div { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }.comparison-card dt { color: var(--cc-muted); }.comparison-card dd { margin: 0; font-weight: 600; text-align: right; }
.review-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 32px; margin: 0; border-top: 1px solid var(--cc-border); }
.review-fields > div { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 12px; min-width: 0; padding: 10px 0; border-bottom: 1px solid var(--cc-border); font-size: 13px; }
.review-fields dt { color: var(--cc-muted); }.review-fields dd { min-width: 0; margin: 0; color: var(--cc-text); font-weight: 600; overflow-wrap: anywhere; }
.review-reason { display: flex; gap: 8px; align-items: flex-start; padding: 12px 14px; border: 1px solid #f3d19e; border-radius: 8px; background: #fdf6ec; color: #8a5a00; font-size: 13px; line-height: 1.55; }
.review-section { display: grid; gap: 13px; padding-top: 2px; }.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }.section-heading h3 { display: flex; align-items: center; gap: 7px; margin: 0 0 4px; font-size: 16px; }.section-count { color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
.measurement-note { overflow-wrap: anywhere; }.measurement-note strong, .measurement-note span { display: block; }.measurement-note strong { color: var(--el-color-warning-dark-2); }.measurement-note strong + span { margin-top: 4px; color: var(--cc-muted); }
.weight-list { display: grid; gap: 8px; }.weight-row { display: grid; grid-template-columns: minmax(190px, 1.8fr) repeat(4, minmax(80px, 1fr)); gap: 12px; align-items: center; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--cc-surface); }.weight-row > div { min-width: 0; }.weight-row strong, .weight-row span, .weight-row p { display: block; overflow-wrap: anywhere; }.weight-row span { margin-top: 3px; color: var(--cc-muted); font-size: 11px; }.weight-row p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; line-height: 1.45; }.weight-row > div:not(:first-child) { text-align: right; }
.table-wrap { display: grid; gap: 4px; min-width: 0; }.table-wrap span { color: var(--cc-muted); font-size: 12px; line-height: 1.45; overflow-wrap: anywhere; }
.review-actions { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px 16px; border: 1px solid #b3d8ff; border-radius: 10px; background: #ecf5ff; }.review-actions > div:first-child { display: grid; gap: 4px; min-width: 0; }.review-actions span { color: #606266; font-size: 12px; line-height: 1.5; }.review-actions--disabled { border-color: var(--cc-border); background: var(--cc-surface-muted); }.action-buttons { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.history-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }.history-list li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 10px; position: relative; padding-bottom: 16px; }.history-list li:not(:last-child)::after { content: ''; position: absolute; top: 16px; bottom: 0; left: 7px; width: 1px; background: var(--cc-border); }.history-marker { z-index: 1; width: 14px; height: 14px; margin-top: 2px; border: 3px solid #909399; border-radius: 50%; background: var(--cc-surface); }.history-marker--success { border-color: #67c23a; }.history-marker--danger { border-color: #f56c6c; }.history-content { min-width: 0; }.history-content > div { display: flex; justify-content: space-between; gap: 12px; }.history-content time, .history-content > span { color: var(--cc-muted); font-size: 11px; }.history-content p { margin: 5px 0 4px; color: var(--cc-text); font-size: 13px; overflow-wrap: anywhere; }
.el-button { min-height: 36px; }.el-button :deep(svg) { flex: 0 0 auto; }.review-panel :deep(.el-dialog__body) { display: grid; gap: 14px; }
@media (max-width: 720px) { .review-header, .section-heading, .review-actions { align-items: flex-start; flex-direction: column; }.review-header-status, .action-buttons { width: 100%; justify-content: space-between; }.comparison-grid { grid-template-columns: 1fr; }.weight-row { grid-template-columns: 1fr 1fr; }.weight-row > div:not(:first-child) { text-align: left; }.review-actions .action-buttons .el-button { flex: 1 1 140px; }.review-actions .action-buttons { justify-content: stretch; }.review-actions .action-buttons :deep(.el-button) { width: 100%; } }
@media (max-width: 380px) { .review-header h2 { font-size: 19px; }.review-header p { font-size: 12px; }.comparison-card { padding: 13px; }.weight-row { grid-template-columns: 1fr; }.review-actions { padding: 13px; } }
@media (max-width: 720px) { .review-header, .section-heading, .review-actions { align-items: flex-start; flex-direction: column; }.review-header-status, .action-buttons { width: 100%; justify-content: space-between; }.review-fields { grid-template-columns: 1fr; gap: 0; }.review-actions .action-buttons .el-button { flex: 1 1 140px; }.review-actions .action-buttons { justify-content: stretch; }.review-actions .action-buttons :deep(.el-button) { width: 100%; } }
@media (max-width: 380px) { .review-header h2 { font-size: 19px; }.review-header p { font-size: 12px; }.review-fields > div { grid-template-columns: 100px minmax(0, 1fr); }.review-actions { padding: 13px; } }
</style>