This commit is contained in:
2026-09-23 17:47:54 +08:00
parent 10e20f896a
commit c25aaaca53
4 changed files with 94 additions and 25 deletions
@@ -52,13 +52,8 @@ const isRejected = computed(() => detail.value?.measureReviewStatus === 'REJECTE
const reviewStatusLabel = computed(() => inboundStatusLabel(detail.value?.measureReviewStatus, inboundReviewStatusLabels, '未提交'))
const displayCustomerName = computed(() => detail.value?.companyName?.trim() || props.customerName?.trim() || '客户未命名')
const displayChannelName = computed(() => detail.value?.channelName?.trim() || detail.value?.channelCode?.trim() || props.channelName?.trim() || props.channelCode?.trim() || '渠道未填写')
const sourceLabels: Record<string, string> = {
THIRD_PARTY: '第三方设备',
DEVICE: '第三方设备',
OPENAPI: '第三方设备',
MANUAL: '平台人工',
}
const sourceLabel = computed(() => sourceLabels[detail.value?.sourceType || ''] || (detail.value?.sourceType ? '未知来源' : '尚未记录'))
const measurementCount = computed(() => detail.value?.items.length || 0)
const weightCount = computed(() => detail.value?.weightResults.length || 0)
function formatNumber(value: number | null | undefined, maximumFractionDigits = 3) {
if (value === null || value === undefined || !Number.isFinite(value)) return '—'
@@ -180,18 +175,18 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
<section class="review-panel" aria-labelledby="measurement-review-title">
<h2 id="measurement-review-title" class="sr-only">测量审核内容</h2>
<div class="review-toolbar" aria-label="测量审核操作">
<div class="review-toolbar__actions">
<el-button v-if="isPending && canReview" type="primary" plain @click="correctionMode = true"><Pencil :size="16" />修改重量</el-button>
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve' || postApproveLoading" :disabled="approvalCompleted" @click="confirmApprove"><Check :size="16" />{{ approvalCompleted ? '审核已通过' : approvalButtonLabel }}</el-button>
</div>
<div class="review-toolbar__status">
<StatusBadge v-if="detail" :status="reviewStatusLabel" />
<span v-if="isPending">通过后进入待分柜</span>
<span v-if="isPending">通过后进入{{ approvalTargetLabel }}</span>
<span v-else-if="isRejected">历史记录为审核驳回,仅供查看</span>
<span v-else>当前记录不可审核</span>
<span v-if="detail" class="review-toolbar__context">客户:{{ displayCustomerName }} · 渠道:{{ displayChannelName }}</span>
</div>
<div class="review-toolbar__actions">
<el-button v-if="isPending && canReview" type="primary" plain @click="correctionMode = true"><Pencil :size="16" />修改重量</el-button>
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve' || postApproveLoading" :disabled="approvalCompleted" @click="confirmApprove"><Check :size="16" />{{ approvalCompleted ? '审核已通过' : approvalButtonLabel }}</el-button>
<el-button :loading="loading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
</div>
<el-button class="review-refresh" :loading="loading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
</div>
<el-alert v-if="loadError" type="error" :closable="false" show-icon role="alert">
@@ -204,12 +199,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
<el-alert v-if="detail.reviewReason" type="warning" :closable="false" show-icon :title="`最近审核说明:${detail.reviewReason}`" />
<section class="review-data" aria-labelledby="review-data-title">
<header class="review-section-heading">
<div>
<h3 id="review-data-title">审核数据总览</h3>
</div>
<el-tag effect="plain">{{ sourceLabel }}</el-tag>
</header>
<header class="review-section-heading"><h3 id="review-data-title">审核数据总览</h3></header>
<div class="review-comparison">
<div class="review-comparison__row review-comparison__row--declared">
@@ -223,7 +213,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
<div class="review-comparison__row">
<strong>平台实测</strong>
<dl class="review-metrics" aria-label="平台实测结果汇总">
<div><dt>箱数</dt><dd>{{ formatNumber(detail.actualPackageCount, 0) }} <small v-if="formatDifference(detail.actualPackageCount, detail.originalPackageCount, 0)">{{ formatDifference(detail.actualPackageCount, detail.originalPackageCount, 0) }}</small></dd></div>
<div><dt>箱数</dt><dd>{{ formatNumber(detail.actualPackageCount, 0) }}{{ formatDifference(detail.actualPackageCount, detail.originalPackageCount, 0) ? ` ${formatDifference(detail.actualPackageCount, detail.originalPackageCount, 0)}` : '' }}</dd></div>
<div><dt>毛重</dt><dd>{{ formatNumber(detail.actualGrossWeightKg) }} <small>kg<span v-if="formatDifference(detail.actualGrossWeightKg, detail.originalGrossWeightKg, 3, ' kg')"> · {{ formatDifference(detail.actualGrossWeightKg, detail.originalGrossWeightKg, 3, ' kg') }}</span></small></dd></div>
<div class="review-metric--primary"><dt>货物净重</dt><dd>{{ formatNumber(detail.actualNetWeightKg) }} <small>kg</small></dd></div>
<div><dt>体积</dt><dd>{{ formatNumber(detail.actualVolumeCbm, 4) }} <small>m³<span v-if="formatDifference(detail.actualVolumeCbm, detail.originalVolumeCbm, 4, ' m³')"> · {{ formatDifference(detail.actualVolumeCbm, detail.originalVolumeCbm, 4, ' m³') }}</span></small></dd></div>
@@ -238,12 +228,12 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
<div>
<h3 id="review-items-title">箱规测量明细</h3>
</div>
<span>{{ detail.items.length }} 条</span>
<span>{{ measurementCount }} 条</span>
</header>
<div v-if="detail.items.length" class="review-table-scroll">
<el-table :data="detail.items" border stripe table-layout="fixed" class="review-table">
<el-table-column type="index" label="#" width="44" align="center" />
<el-table-column label="箱号" width="240" show-overflow-tooltip>
<el-table-column label="箱号" width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.boxCode || row.itemNo || '—' }}</template>
</el-table-column>
<el-table-column label="测量图片" width="98" align="center">
@@ -281,12 +271,31 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
<el-empty v-else description="暂无箱规测量明细" :image-size="48" />
</section>
<section class="review-items review-weight-items" aria-labelledby="review-weight-title">
<header class="review-section-heading"><h3 id="review-weight-title">⚖ 称重明细</h3><div class="review-weight-totals"><span>实测毛重 <strong>{{ formatNumber(detail.actualGrossWeightKg, 3) }} KG</strong></span><span>货物重量 <strong>{{ formatNumber(detail.actualNetWeightKg, 3) }} KG</strong></span></div></header>
<div v-if="weightCount" class="review-table-scroll">
<el-table :data="detail.weightResults" border stripe table-layout="fixed" class="review-table review-weight-table">
<el-table-column type="index" label="#" width="44" align="center" />
<el-table-column label="称重图片" width="112" align="center"><template #default="{ row }"><MeasurementEvidenceThumbnail :customer-id="customerId" :inbound-id="inboundId" :file-id="row.fileId" :alt="`${row.palletCode || '托盘'}称重图片`" empty-text="无图片" @open="openImagePreview(row.fileId, row.palletCode || '托盘')" /></template></el-table-column>
<el-table-column prop="palletCode" label="托盘编码" min-width="120" />
<el-table-column prop="palletType" label="托盘类型" min-width="120" />
<el-table-column label="库位" min-width="130"><template #default="{ row }"><span class="review-location">{{ row.locationCode || '—' }}<template v-if="row.locationCode"> · {{ row.slotStatus === 'BOUND' ? '待上架' : row.slotStatus || '待上架' }}</template></span></template></el-table-column>
<el-table-column label="毛重(KG)" min-width="120" align="right"><template #default="{ row }">{{ formatNumber(row.grossWeightKg, 3) }}</template></el-table-column>
<el-table-column label="托盘皮重(kg)" min-width="120" align="right"><template #default="{ row }">{{ formatNumber(row.palletTareWeightKg, 3) }}</template></el-table-column>
<el-table-column label="货物重量(kg)" min-width="120" align="right"><template #default="{ row }"><strong class="review-weight-value">{{ formatNumber(row.netWeightKg, 3) }}</strong></template></el-table-column>
<el-table-column label="称重时间" min-width="150"><template #default="{ row }">{{ formatDate(row.confirmedAt || row.adjustedAt) }}</template></el-table-column>
</el-table>
</div>
<el-empty v-else description="暂无称重明细" :image-size="48" />
</section>
<WeightOcrPanel
review-mode
:correction-mode="correctionMode"
:customer-id="customerId"
:inbound-id="inboundId"
@changed="handleWeightChanged"
:class="{ 'review-weight-editor--hidden': !correctionMode }"
/>
</template>
@@ -306,7 +315,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
<style scoped>
.review-panel { display: grid; gap: var(--admin-space-5, 12px); color: var(--cc-text); }
.review-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; position: sticky; top: -12px; z-index: 3; flex-wrap: wrap; margin: -var(--admin-space-5, 12px) -var(--admin-space-6, 16px) 0; padding: var(--admin-space-3, 8px) var(--admin-space-6, 16px); border-bottom: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.review-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; position: sticky; top: -12px; z-index: 3; flex-wrap: wrap; margin: -var(--admin-space-5, 12px) -var(--admin-space-6, 16px) 0; padding: var(--admin-space-3, 8px) var(--admin-space-6, 16px); border-bottom: 1px solid var(--cc-border); background: linear-gradient(100deg, #f3f6ff 0%, #fff 48%, #eefaf7 100%); }
.review-toolbar__status, .review-toolbar__actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3, 8px); }
.review-toolbar__status > span { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.review-toolbar__actions { justify-content: flex-end; }
@@ -337,6 +346,23 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
.review-table :deep(.el-table__cell) { padding: 7px 0; }
.review-table :deep(.review-time-cell .cell) { white-space: nowrap; }
.review-table :deep(.measurement-evidence-thumb) { min-width: 72px; border-radius: var(--admin-radius, 4px); }
.review-data { border: 1px solid #dfe5ef; border-radius: 8px; overflow: hidden; background: #fff; }
.review-data .review-section-heading { margin: 0; padding: 10px 14px 6px; border-bottom: 0; }
.review-comparison__row { padding: 10px 14px; }
.review-comparison__row--declared { background: linear-gradient(90deg, #ddf5e9, #eaf9f1); box-shadow: inset 4px 0 #25a879; }
.review-comparison__row--declared > strong { color: #16845d; }
.review-comparison__row + .review-comparison__row { border-top: 1px dashed #dfe4ed; }
.review-comparison__row > strong { font-size: 14px; }
.review-declaration dd, .review-metrics dd { font-size: 16px; }
.review-metric--primary dd, .review-weight-value, .review-weight-totals strong { color: #138154 !important; font-weight: 750; }
.review-weight-items { padding-top: 0; }
.review-weight-totals { display: flex; gap: 18px; color: var(--cc-muted); font-size: 12px; }
.review-weight-totals span { padding: 4px 8px; border-radius: 7px; background: #f0faf5; }
.review-weight-totals strong { margin-left: 6px; font-size: 16px; }
.review-weight-table :deep(.el-table__header th) { background: linear-gradient(180deg, #f1f4fc, #e9eef8); }
.review-weight-table :deep(.el-table__cell) { padding: 9px 0; }
.review-location { display: inline-block; padding: 3px 7px; border: 1px solid #e2e8f2; border-radius: 5px; background: #f3f6fb; color: #53627a; white-space: nowrap; }
.review-weight-editor--hidden { display: none; }
.review-image-preview { display: grid; min-height: 240px; place-items: center; }
.review-image-preview img { display: block; width: auto; max-width: 100%; max-height: 70vh; object-fit: contain; }
.review-image-preview__error { display: grid; justify-items: center; gap: var(--admin-space-3, 8px); color: var(--el-color-danger); }
@@ -346,6 +372,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
.review-toolbar__actions { display: grid; width: 100%; grid-template-columns: 1fr; justify-content: stretch; }
.review-toolbar__actions :deep(.el-button) { width: 100%; min-height: 44px; }
.review-section-heading { align-items: stretch; flex-direction: column; }
.review-weight-totals { flex-wrap: wrap; gap: 6px; }
.review-comparison__row { grid-template-columns: 78px minmax(0, 1fr); align-items: start; }
.review-metrics, .review-declaration { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}