Merge origin/release/test and preserve local frontend changes
This commit is contained in:
@@ -53,6 +53,10 @@ describe('InboundTaskList measurement scope', () => {
|
||||
expect(source).toContain('@submitted="handleMeasurementSubmitted"')
|
||||
expect(source).not.toContain('<WeightOcrPanel')
|
||||
expect(source).not.toContain('<InboundMeasurementPanel')
|
||||
expect(source).toContain("import AppDialog from '@/shared/components/AppDialog.vue'")
|
||||
expect(source).toContain(':show-actions="false"')
|
||||
expect(source).toContain('<template #footer="{ close }">')
|
||||
expect(source).toContain('reviewPanelRef?.confirmApprove()')
|
||||
})
|
||||
|
||||
it('loads only active customers and exposes a searchable name-only select', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { listPlatformWarehouses } from '@/modules/warehouses'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
@@ -36,6 +37,7 @@ const props = defineProps<{ mode: InboundTaskMode; title?: string }>()
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
const reviewPanelRef = ref<InstanceType<typeof MeasurementReviewPanel> | null>(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const loading = ref(false)
|
||||
@@ -416,12 +418,14 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
|
||||
<el-dialog
|
||||
<AppDialog
|
||||
v-if="isReview"
|
||||
v-model="reviewDialogOpen"
|
||||
:title="selectedInbound ? `${taskLabel} · ${selectedInbound.inboundCode || '入仓码待补充'}` : taskLabel"
|
||||
width="min(1560px, calc(100vw - 48px))"
|
||||
top="4vh"
|
||||
fullscreenable
|
||||
:show-confirm="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:close-on-click-modal="!reviewNavigationLoading"
|
||||
@@ -436,6 +440,8 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
</el-alert>
|
||||
<MeasurementReviewPanel
|
||||
v-if="selectedInbound"
|
||||
ref="reviewPanelRef"
|
||||
:show-actions="false"
|
||||
:tenant-id="selectedInbound.tenantId"
|
||||
:inbound-id="selectedInbound.id"
|
||||
:fulfillment-type="selectedInbound.fulfillmentType"
|
||||
@@ -446,7 +452,16 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
:approval-completed="reviewApproved"
|
||||
@changed="handleTaskChanged"
|
||||
/>
|
||||
</el-dialog>
|
||||
<template #footer="{ close }">
|
||||
<div class="review-dialog-footer">
|
||||
<el-button :disabled="reviewNavigationLoading" @click="close">关闭</el-button>
|
||||
<template v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview">
|
||||
<el-button type="primary" plain @click="reviewPanelRef?.enableWeightCorrection()">修改重量</el-button>
|
||||
<el-button type="primary" :loading="reviewPanelRef?.actionLoading === 'approve' || reviewNavigationLoading" :disabled="reviewApproved" @click="reviewPanelRef?.confirmApprove()">{{ reviewApproved ? '审核已通过' : reviewPanelRef?.approvalButtonLabel }}</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</AppDialog>
|
||||
|
||||
<InboundMeasurementDialog
|
||||
v-if="!isReview && selectedInbound"
|
||||
@@ -463,6 +478,8 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.review-dialog-footer { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: var(--admin-space-4, 10px); }
|
||||
.review-dialog-footer :deep(.el-button) { margin-left: 0; }
|
||||
.inbound-task-page { min-width: 0; }
|
||||
.state-stack { display: grid; gap: var(--admin-space-5, 12px); }
|
||||
.review-navigation-error { margin-bottom: 14px; }
|
||||
@@ -479,7 +496,6 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
|
||||
<style>
|
||||
.inbound-task-dialog.admin-density-workspace.el-dialog:not(.is-fullscreen) { height: auto; max-height: 92vh; overflow: hidden; }
|
||||
.inbound-task-dialog .el-dialog__body { max-height: calc(92vh - 62px); }
|
||||
@media (max-width: 520px) {
|
||||
.inbound-task-dialog .el-dialog__body { padding: 14px; }
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@ onBeforeUnmount(() => {
|
||||
<h2>入库码:{{ title }}</h2>
|
||||
<StatusBadge v-if="detail" :status="statusLabel" />
|
||||
</div>
|
||||
<p>按基本信息、货物测量、审核、费用和托盘库位顺序处理,所有结果以服务端最新状态为准。</p>
|
||||
</div>
|
||||
<el-button :loading="loading" :disabled="!customerId || !inboundId" @click="refreshWorkspace">
|
||||
<RefreshCw :size="16" />刷新
|
||||
@@ -235,7 +234,6 @@ onBeforeUnmount(() => {
|
||||
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 18px 24px 16px; border-bottom: 1px solid var(--cc-border); }
|
||||
.workspace-title-row { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 10px; }
|
||||
.workspace-title-row h2 { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 16px; line-height: 1.35; }
|
||||
.workspace-header p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
||||
.workspace-state { margin: 24px; }
|
||||
.workspace-skeleton { padding: 0; }
|
||||
.workflow-tabs { min-width: 0; padding: 0 24px 24px; }
|
||||
|
||||
@@ -235,11 +235,13 @@ describe('InboundFeePanel', () => {
|
||||
|
||||
it('treats A0404 as a neutral not-yet-generated state', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockRejectedValue(new ApiResponseError('费用快照不存在', 'A0404'))
|
||||
mountPanel(['firstleg:fba-pricing:view'])
|
||||
const wrapper = mountPanel(['firstleg:fba-pricing:view'])
|
||||
await flushPromises()
|
||||
|
||||
expect(document.body.textContent).toContain('尚未生成费用快照')
|
||||
expect(document.body.textContent).toContain('测量审核完成后')
|
||||
expect(document.body.textContent).not.toContain('测量审核完成后')
|
||||
await wrapper.get('button[aria-label="查看计费说明"]').trigger('mouseenter')
|
||||
await vi.waitFor(() => expect(document.body.textContent).toContain('测量审核完成后'))
|
||||
expect(document.querySelector('[role="alert"]')).toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import InlineHelp from '@/shared/components/InlineHelp.vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Check, CircleDollarSign, PencilLine, RefreshCw, Send } from '@lucide/vue'
|
||||
@@ -379,12 +380,11 @@ defineExpose({ reload: loadFee })
|
||||
<template>
|
||||
<section class="inbound-fee-panel" :aria-busy="loading" aria-labelledby="inbound-fee-title">
|
||||
<header class="fee-heading" :class="{ 'fee-heading--compact': compact }">
|
||||
<h3 v-if="compact" id="inbound-fee-title" class="fee-heading-title--compact">头程自动初算</h3>
|
||||
<h3 v-if="compact" id="inbound-fee-title" class="fee-heading-title--compact">头程自动初算 <InlineHelp title="计费说明"><p>测量审核完成后,系统会自动生成初算费用;人工确认后才发布。</p><p>实际重量与体积重量取大后计费,金额以服务端快照为准。</p></InlineHelp></h3>
|
||||
<div v-else class="fee-heading-copy">
|
||||
<span class="fee-heading-icon" aria-hidden="true"><CircleDollarSign :size="20" /></span>
|
||||
<div>
|
||||
<h3 id="inbound-fee-title">入库费用快照</h3>
|
||||
<p>测量审核后的头程费用,人工确认后才发布。</p>
|
||||
<h3 id="inbound-fee-title">入库费用快照 <InlineHelp title="计费说明"><p>测量审核完成后,系统会自动生成初算费用;人工确认后才发布。</p><p>实际重量与体积重量取大后计费,金额以服务端快照为准。</p></InlineHelp></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fee-heading-actions">
|
||||
@@ -470,7 +470,6 @@ defineExpose({ reload: loadFee })
|
||||
</el-alert>
|
||||
|
||||
<el-empty v-else-if="notFound" :image-size="64" description="尚未生成费用快照">
|
||||
<p class="empty-note">测量审核完成后,系统会自动生成初算费用;人工确认后才发布。</p>
|
||||
</el-empty>
|
||||
|
||||
<div v-else-if="snapshot" class="fee-content">
|
||||
@@ -504,7 +503,7 @@ defineExpose({ reload: loadFee })
|
||||
|
||||
<section class="calculation-section" aria-labelledby="calculation-title">
|
||||
<div class="section-title">
|
||||
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际重量与体积重量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<div><h4 id="calculation-title">计费依据</h4></div>
|
||||
<div class="calculation-tags">
|
||||
<el-tag effect="plain">订单报关:{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
||||
<el-tag effect="plain">报价税制:{{ pricingTaxModeLabel(snapshot.pricingTaxMode) }}</el-tag>
|
||||
|
||||
@@ -874,7 +874,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<el-dialog class="admin-density-dialog" v-model="imagePreviewOpen" title="当前有效测量图片" width="min(720px, 92vw)" destroy-on-close @closed="releaseImagePreview">
|
||||
<AppDialog class="admin-density-dialog" v-model="imagePreviewOpen" :title="detail?.inboundCode ? `当前有效测量图片 · ${detail.inboundCode}` : '当前有效测量图片'" width="min(720px, 92vw)" fullscreenable :show-footer="false" :close-on-click-modal="true" destroy-on-close append-to-body @closed="releaseImagePreview">
|
||||
<div class="measurement-image-preview">
|
||||
<el-skeleton v-if="imagePreviewLoading" :rows="5" animated />
|
||||
<LoadErrorState v-else-if="imagePreviewError" title="图片加载失败" :message="imagePreviewError.message" :show-retry="false">
|
||||
@@ -883,29 +883,40 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
<img v-else-if="imagePreviewUrl" :src="imagePreviewUrl" alt="当前扫描箱测量图片" />
|
||||
<el-empty v-else description="暂无可预览图片" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</AppDialog>
|
||||
|
||||
<el-dialog class="admin-density-dialog" v-model="manualEntryOpen" title="手动录入箱规" width="min(680px, 92vw)" destroy-on-close>
|
||||
<el-dialog class="admin-density-dialog" v-model="manualEntryOpen" :title="detail?.inboundCode ? `手动录入箱规 · ${detail.inboundCode}` : '手动录入箱规'" width="min(680px, 92vw)" destroy-on-close append-to-body>
|
||||
<el-alert v-if="manualEntryErrors.length" type="error" :closable="false" show-icon class="manual-entry-alert" title="请修正以下问题">
|
||||
<ul><li v-for="error in manualEntryErrors" :key="error">{{ error }}</li></ul>
|
||||
</el-alert>
|
||||
<el-form label-position="top" class="manual-entry-form" @submit.prevent="submitManualEntry">
|
||||
<div class="measurement-form-grid">
|
||||
<el-form-item label="箱号 *"><el-input v-model="manualForm.boxCode" placeholder="请输入已打印箱唛上的箱号" /></el-form-item>
|
||||
<el-form-item label="测量长(cm) *"><el-input-number v-model="manualForm.length" :min="0.001" :precision="3" controls-position="right" placeholder="请输入测量长" /></el-form-item>
|
||||
<el-form-item label="测量宽(cm) *"><el-input-number v-model="manualForm.width" :min="0.001" :precision="3" controls-position="right" placeholder="请输入测量宽" /></el-form-item>
|
||||
<el-form-item label="测量高(cm) *"><el-input-number v-model="manualForm.height" :min="0.001" :precision="3" controls-position="right" placeholder="请输入测量高" /></el-form-item>
|
||||
<el-form-item label="单箱毛重(kg) *"><el-input-number v-model="manualForm.weight" :min="0.001" :precision="3" controls-position="right" placeholder="请输入单箱毛重" /></el-form-item>
|
||||
<el-form-item label="测量时间 *"><el-date-picker v-model="manualForm.measuredAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm:ss" /></el-form-item>
|
||||
<el-form-item label="设备/来源编号"><el-input v-model="manualForm.machineNo" maxlength="64" placeholder="请输入设备编号(选填)" /></el-form-item>
|
||||
<el-form-item label="Shipment ID"><el-input v-model="manualForm.scannedShipmentId" maxlength="64" placeholder="请输入扫描运单号(选填)" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="当前箱测量图片 *">
|
||||
<el-upload class="manual-entry-upload" :auto-upload="false" :limit="1" accept="image/*" :on-change="selectManualImage" :on-remove="() => { manualEntryImage = null }">
|
||||
<el-button><Upload :size="15" />选择图片</el-button>
|
||||
<template #tip><div class="el-upload__tip">支持图片格式,大小不超过 5 MB</div></template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form class="manual-entry-form" @submit.prevent="submitManualEntry">
|
||||
<table class="density-table density-table--editable manual-entry-table" aria-label="手动录入箱规">
|
||||
<colgroup><col class="manual-entry-label-column" /><col /><col class="manual-entry-label-column" /><col /></colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="manual-box-code">箱号 *</label></th><td><el-input id="manual-box-code" v-model="manualForm.boxCode" placeholder="请输入已打印箱唛上的箱号" /></td>
|
||||
<th scope="row"><label for="manual-length">测量长(cm) *</label></th><td><el-input-number id="manual-length" v-model="manualForm.length" :min="0.001" :precision="3" controls-position="right" placeholder="请输入测量长" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="manual-width">测量宽(cm) *</label></th><td><el-input-number id="manual-width" v-model="manualForm.width" :min="0.001" :precision="3" controls-position="right" placeholder="请输入测量宽" /></td>
|
||||
<th scope="row"><label for="manual-height">测量高(cm) *</label></th><td><el-input-number id="manual-height" v-model="manualForm.height" :min="0.001" :precision="3" controls-position="right" placeholder="请输入测量高" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="manual-weight">单箱毛重(kg) *</label></th><td><el-input-number id="manual-weight" v-model="manualForm.weight" :min="0.001" :precision="3" controls-position="right" placeholder="请输入单箱毛重" /></td>
|
||||
<th scope="row"><label for="manual-time">测量时间 *</label></th><td><el-date-picker id="manual-time" v-model="manualForm.measuredAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm:ss" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="manual-machine">设备/来源编号</label></th><td><el-input id="manual-machine" v-model="manualForm.machineNo" maxlength="64" placeholder="请输入设备编号(选填)" /></td>
|
||||
<th scope="row"><label for="manual-shipment">Shipment ID</label></th><td><el-input id="manual-shipment" v-model="manualForm.scannedShipmentId" maxlength="64" placeholder="请输入扫描运单号(选填)" /></td>
|
||||
</tr>
|
||||
<tr><th scope="row">当前箱测量图片 *</th><td colspan="3">
|
||||
<el-upload class="manual-entry-upload" :auto-upload="false" :limit="1" accept="image/*" :on-change="selectManualImage" :on-remove="() => { manualEntryImage = null }">
|
||||
<el-button><Upload :size="15" />选择图片</el-button>
|
||||
<template #tip><div class="el-upload__tip">支持图片格式,大小不超过 5 MB</div></template>
|
||||
</el-upload>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="manualEntrySubmitting" @click="manualEntryOpen = false">取消</el-button>
|
||||
@@ -922,7 +933,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
.measurement-comparison thead th { background: #f8fafc; }
|
||||
.measurement-comparison thead th:nth-child(2) { background: #f7f9ff; color: #465fc4; }
|
||||
.measurement-comparison thead th:nth-child(3) { background: #f4fbf8; color: #18795d; }
|
||||
.measurement-comparison__difference { color: var(--el-color-warning-dark-2); font-weight: 600; }
|
||||
.measurement-comparison__difference { color: var(--el-color-danger-dark-2); font-weight: 600; }
|
||||
.measurement-extra-facts { display: flex; flex-wrap: wrap; gap: var(--admin-space-3, 8px) var(--admin-space-6, 16px); color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
|
||||
.measurement-extra-facts strong { color: var(--cc-text); }
|
||||
.measurement-panel { overflow: hidden; }
|
||||
@@ -947,7 +958,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
.measurement-summary-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
|
||||
.measurement-summary-context { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: var(--admin-space-3, 8px) 20px; }
|
||||
.measurement-summary-header span { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }
|
||||
.measurement-deviation-alert { margin: 0; width: auto; }
|
||||
.measurement-deviation-alert { --el-color-warning: var(--el-color-danger-dark-2); --el-color-warning-dark-2: var(--el-color-danger-dark-2); margin: 0; width: auto; }
|
||||
.measurement-deviation-alert p { margin: 3px 0 0; line-height: 1.55; }
|
||||
.measurement-summary-context p { display: inline-flex; min-width: 0; align-items: baseline; gap: var(--admin-space-2, 6px); margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: var(--admin-font-caption, 12px); font-weight: 600; }
|
||||
.measurement-summary-status { display: flex; flex: 0 0 auto; align-items: center; gap: 9px; }
|
||||
@@ -958,7 +969,6 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
.measurement-section__heading > div { min-width: 0; }
|
||||
.measurement-section-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--admin-space-3, 8px); }.measurement-section-actions :deep(.el-button) { margin-left: 0; }
|
||||
.warehouse-suggestion-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-6, 16px); }.warehouse-suggestion-form :deep(.el-select),.warehouse-suggestion-form :deep(.el-input-number) { width: 100%; }.warehouse-suggestion-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }.suggestion-preview { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: var(--admin-space-1, 4px) 0 0; border: 1px solid var(--cc-border); border-radius: 6px; }.suggestion-preview div { padding: var(--admin-space-5, 12px); border-right: 1px solid var(--cc-border); }.suggestion-preview div:last-child { border-right: 0; }.suggestion-preview dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }.suggestion-preview dd { margin: 5px 0 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); font-weight: 650; }
|
||||
.measurement-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-6, 16px); }
|
||||
.measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--admin-space-5, 12px); }
|
||||
.measurement-form label { display: flex; min-width: 0; flex-direction: column; gap: 7px; color: #344054; font-size: var(--admin-font-caption, 12px); font-weight: 650; }
|
||||
.measurement-form :deep(.el-input-number), .measurement-form :deep(.el-input) { width: 100%; }
|
||||
@@ -971,7 +981,8 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
.measurement-image-preview img { display: block; width: auto; max-width: 100%; max-height: 68vh; object-fit: contain; }
|
||||
.manual-entry-alert { margin-bottom: var(--admin-space-6, 16px); }
|
||||
.manual-entry-alert ul { margin: var(--admin-space-2, 6px) 0 0; padding-left: 18px; }
|
||||
.manual-entry-form :deep(.el-input-number), .manual-entry-form :deep(.el-date-editor) { width: 100%; }
|
||||
.manual-entry-table { table-layout: fixed; }
|
||||
.manual-entry-label-column { width: 18%; }
|
||||
.manual-entry-upload { width: 100%; }
|
||||
.measurement-items-section { background: var(--admin-surface, #fff); }
|
||||
.measurement-table { width: 100%; font-size: var(--admin-font-caption, 12px); }
|
||||
@@ -991,7 +1002,6 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.measurement-form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.measurement-data-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.measurement-row-details { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@@ -1003,7 +1013,10 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
.measurement-tags { justify-content: flex-start; }
|
||||
.measurement-section { padding-right: 14px; padding-left: 14px; }
|
||||
.measurement-panel > :deep(.el-alert), .measurement-alert { margin-right: 14px; margin-left: 14px; }
|
||||
.measurement-form-grid, .measurement-data-grid { grid-template-columns: 1fr; }
|
||||
.measurement-data-grid { grid-template-columns: 1fr; }
|
||||
.manual-entry-table colgroup { display: none; }
|
||||
.manual-entry-table tr { display: grid; grid-template-columns: 36% minmax(0, 1fr); }
|
||||
.manual-entry-table :is(th, td) { min-width: 0; }
|
||||
.measurement-row-details { grid-template-columns: 1fr; padding-left: 54px; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* Measurement workspace: keep all sections inside the panel's content bounds. */
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-section { min-width: 0; margin: 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid #cce8df; border-left: 4px solid #20a477; border-radius: 8px; background: #f4fbf8; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-section { min-width: 0; margin: 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid var(--admin-dialog-summary-border); border-radius: var(--admin-dialog-section-radius); background: var(--admin-dialog-summary-bg); }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-header { flex-wrap: wrap; align-items: center; gap: var(--admin-space-3, 8px) var(--admin-space-6, 16px); }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-context { flex: 1 1 auto; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-context p > span { flex-shrink: 0; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-status { flex-wrap: wrap; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-items-section { min-width: 0; margin: var(--admin-space-5, 12px) 0 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid #eadfc9; border-left: 4px solid #c79643; border-radius: 8px; background: #fffbf4; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-items-section { min-width: 0; margin: var(--admin-space-5, 12px) 0 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid var(--admin-dialog-work-border); border-radius: var(--admin-dialog-section-radius); background: var(--admin-dialog-work-bg); }
|
||||
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-subheading { color: #18795d; font-size: var(--admin-font-section, 14px); }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-declaration { min-width: 0; margin-top: var(--admin-space-3, 8px); padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid #d9e2fa; border-left: 4px solid #647fec; border-radius: 8px; background: #f7f9ff; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-declaration { min-width: 0; margin-top: var(--admin-space-3, 8px); padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); border: 1px solid var(--admin-dialog-source-border); border-radius: var(--admin-dialog-section-radius); background: var(--admin-dialog-source-bg); }
|
||||
|
||||
@media (min-width: 961px) {
|
||||
.inbound-density.admin-density-dialog .measurement-dialog-weight { padding-bottom: var(--admin-space-4, 10px); }
|
||||
|
||||
@@ -461,7 +461,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId +
|
||||
</DataTable>
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
<InboundCodeRelationshipDialog v-if="relationshipDialogOpen" v-model="relationshipDialogOpen" :inbound-code="relationshipDialogInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
|
||||
<InboundCodeRelationshipDialog dense v-if="relationshipDialogOpen" v-model="relationshipDialogOpen" :inbound-code="relationshipDialogInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -143,19 +143,17 @@ describe('MeasurementReviewPanel', () => {
|
||||
|
||||
const summary = wrapper.get('.review-data').text()
|
||||
expect(summary).toContain('审核数据总览')
|
||||
const comparisonRows = wrapper.findAll('.review-comparison__row')
|
||||
expect(comparisonRows).toHaveLength(2)
|
||||
expect(comparisonRows[0].text()).toContain('客户申报')
|
||||
expect(comparisonRows[0].text()).toContain('箱数9')
|
||||
expect(comparisonRows[0].text()).toContain('毛重118 kg')
|
||||
expect(comparisonRows[0].text()).not.toContain('实际品名')
|
||||
expect(comparisonRows[0].text()).not.toContain('原单据号码')
|
||||
expect(comparisonRows[1].text()).toContain('平台实测')
|
||||
expect(comparisonRows[1].text()).toContain('箱数10 差异 +1')
|
||||
expect(comparisonRows[1].text()).toContain('毛重120 kg · 差异 +2 kg')
|
||||
expect(comparisonRows[1].text()).toContain('货物净重110 kg')
|
||||
expect(comparisonRows[1].text()).toContain('体积2.2 m³ · 差异 +0.2 m³')
|
||||
expect(comparisonRows[1].text()).toContain('体积重367.4 kg')
|
||||
const comparisonRows = wrapper.findAll('.review-comparison tbody tr')
|
||||
expect(comparisonRows.map(row => row.findAll('th, td').map(cell => cell.text()))).toEqual([
|
||||
['箱数', '9', '10', '差异 +1'],
|
||||
['毛重(kg)', '118', '120', '差异 +2 kg'],
|
||||
['体积(m³)', '2', '2.2', '差异 +0.2 m³'],
|
||||
])
|
||||
expect(wrapper.get('.review-comparison').text()).not.toContain('实际品名')
|
||||
expect(wrapper.get('.review-comparison').text()).not.toContain('原单据号码')
|
||||
const extraFacts = wrapper.get('.review-extra-facts').text().replace(/\s/g, '')
|
||||
expect(extraFacts).toContain('货物净重110kg')
|
||||
expect(extraFacts).toContain('体积重367.4kg')
|
||||
|
||||
const items = wrapper.get('.review-items').text()
|
||||
expect(items).toContain('箱规测量明细')
|
||||
@@ -174,7 +172,7 @@ describe('MeasurementReviewPanel', () => {
|
||||
expect(source).toContain(':file-id="row.intelligentImageFileId"')
|
||||
expect(source).toContain('openImagePreview(row.intelligentImageFileId')
|
||||
expect(source).toContain('downloadInboundMeasurementImage')
|
||||
expect(source).toContain('.review-table :deep(.el-table__header .cell) { white-space: nowrap; }')
|
||||
expect(source).not.toContain('.review-table :deep(.el-table__header .cell) { white-space: nowrap; }')
|
||||
expect(source).toContain('.review-table :deep(.review-time-cell .cell) { white-space: nowrap; }')
|
||||
})
|
||||
|
||||
@@ -183,7 +181,10 @@ describe('MeasurementReviewPanel', () => {
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('.review-comparison').text()).not.toContain('差异')
|
||||
const rows = wrapper.findAll('.review-comparison tbody tr')
|
||||
expect(rows[0].findAll('td').at(-1)?.text()).toBe('一致')
|
||||
expect(rows[1].findAll('td').map(cell => cell.text())).toEqual(['—', '120', '—'])
|
||||
expect(rows[2].findAll('td').map(cell => cell.text())).toEqual(['—', '2.2', '—'])
|
||||
})
|
||||
|
||||
it('reuses WeightOcrPanel and enables only its review correction mode', async () => {
|
||||
@@ -216,6 +217,25 @@ describe('MeasurementReviewPanel', () => {
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('exposes the same guarded actions when a dialog owns the fixed footer', async () => {
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
|
||||
const wrapper = mount(MeasurementReviewPanel, {
|
||||
props: { customerId: '10001', inboundId: '90001', showActions: false },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.review-toolbar__actions').exists()).toBe(false)
|
||||
expect(wrapper.get('.review-toolbar__context').text()).toContain('客户:柏悦客户')
|
||||
expect(wrapper.vm.approvalButtonLabel).toBe('通过并进入待分柜')
|
||||
wrapper.vm.enableWeightCorrection()
|
||||
await flushPromises()
|
||||
expect(wrapper.getComponent({ name: 'WeightOcrPanel' }).props('correctionMode')).toBe(true)
|
||||
await wrapper.vm.confirmApprove()
|
||||
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 7 })
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('emits only after approval succeeds and blocks duplicate submissions', async () => {
|
||||
let finishApproval: (() => void) | undefined
|
||||
vi.mocked(approveMeasurement).mockImplementation(() => new Promise((resolve) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Check, Pencil, RotateCcw } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { downloadInboundMeasurementImage } from '../measurement/api'
|
||||
import MeasurementEvidenceThumbnail from '../measurement/components/MeasurementEvidenceThumbnail.vue'
|
||||
@@ -14,7 +15,7 @@ import WeightOcrPanel from '../weight/components/WeightOcrPanel.vue'
|
||||
import { approveMeasurement, getMeasurementReviewDetail } from './api'
|
||||
import type { MeasurementReviewDetail } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
customerId?: string
|
||||
/** @deprecated 仅用于兼容尚未完成 customerId 映射的上层集成。 */
|
||||
tenantId?: string
|
||||
@@ -25,7 +26,8 @@ const props = defineProps<{
|
||||
channelCode?: string | null
|
||||
postApproveLoading?: boolean
|
||||
approvalCompleted?: boolean
|
||||
}>()
|
||||
showActions?: boolean
|
||||
}>(), { showActions: true })
|
||||
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
|
||||
@@ -61,9 +63,9 @@ function formatNumber(value: number | null | undefined, maximumFractionDigits =
|
||||
}
|
||||
|
||||
function formatDifference(actual: number | null | undefined, declared: number | null | undefined, maximumFractionDigits = 3, unit = '') {
|
||||
if (actual == null || declared == null || !Number.isFinite(actual) || !Number.isFinite(declared)) return null
|
||||
if (actual == null || declared == null || !Number.isFinite(actual) || !Number.isFinite(declared)) return '—'
|
||||
const difference = actual - declared
|
||||
if (difference === 0) return null
|
||||
if (difference === 0) return '一致'
|
||||
const sign = difference > 0 ? '+' : ''
|
||||
return `差异 ${sign}${formatNumber(difference, maximumFractionDigits)}${unit}`
|
||||
}
|
||||
@@ -129,6 +131,10 @@ async function handleWeightChanged() {
|
||||
await loadReview()
|
||||
}
|
||||
|
||||
function enableWeightCorrection() {
|
||||
correctionMode.value = true
|
||||
}
|
||||
|
||||
async function confirmApprove() {
|
||||
if (!detail.value || !isPending.value || !canReview.value || actionLoading.value || props.postApproveLoading || props.approvalCompleted) return
|
||||
try {
|
||||
@@ -168,15 +174,15 @@ onMounted(() => {
|
||||
})
|
||||
onBeforeUnmount(releaseImagePreview)
|
||||
|
||||
defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
defineExpose({ confirmApprove, enableWeightCorrection, approvalButtonLabel, isPending, canReview, actionLoading })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="review-panel" aria-labelledby="measurement-review-title">
|
||||
<section class="review-panel admin-density density-dialog-sections" 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>
|
||||
<div v-if="showActions" class="review-toolbar__actions">
|
||||
<el-button v-if="isPending && canReview" type="primary" plain @click="enableWeightCorrection"><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">
|
||||
@@ -198,33 +204,21 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
<template v-else-if="detail">
|
||||
<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"><h3 id="review-data-title">审核数据总览</h3></header>
|
||||
|
||||
<div class="review-comparison">
|
||||
<div class="review-comparison__row review-comparison__row--declared">
|
||||
<strong>客户申报</strong>
|
||||
<dl class="review-declaration" aria-label="客户申报信息">
|
||||
<div><dt>箱数</dt><dd>{{ formatNumber(detail.originalPackageCount, 0) }}</dd></div>
|
||||
<div><dt>毛重</dt><dd>{{ formatNumber(detail.originalGrossWeightKg) }} <small>kg</small></dd></div>
|
||||
<div><dt>体积</dt><dd>{{ formatNumber(detail.originalVolumeCbm, 4) }} <small>m³</small></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="review-comparison__row">
|
||||
<strong>平台实测</strong>
|
||||
<dl class="review-metrics" aria-label="平台实测结果汇总">
|
||||
<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>
|
||||
<div><dt>体积重</dt><dd>{{ formatNumber(detail.volumeWeightKg) }} <small>kg</small></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<section class="review-data density-dialog-section" data-tone="summary" aria-labelledby="review-data-title">
|
||||
<header class="review-section-heading density-dialog-section__title"><h3 id="review-data-title">审核数据总览</h3></header>
|
||||
<table class="review-comparison density-table" aria-label="客户申报与平台实测对比">
|
||||
<thead><tr><th scope="col">指标</th><th scope="col">客户申报</th><th scope="col">平台实测</th><th scope="col">差异(实测 − 申报)</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><th scope="row">箱数</th><td>{{ formatNumber(detail.originalPackageCount, 0) }}</td><td>{{ formatNumber(detail.actualPackageCount, 0) }}</td><td>{{ formatDifference(detail.actualPackageCount, detail.originalPackageCount, 0) }}</td></tr>
|
||||
<tr><th scope="row">毛重(kg)</th><td>{{ formatNumber(detail.originalGrossWeightKg) }}</td><td>{{ formatNumber(detail.actualGrossWeightKg) }}</td><td>{{ formatDifference(detail.actualGrossWeightKg, detail.originalGrossWeightKg, 3, ' kg') }}</td></tr>
|
||||
<tr><th scope="row">体积(m³)</th><td>{{ formatNumber(detail.originalVolumeCbm, 4) }}</td><td>{{ formatNumber(detail.actualVolumeCbm, 4) }}</td><td>{{ formatDifference(detail.actualVolumeCbm, detail.originalVolumeCbm, 4, ' m³') }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="review-extra-facts"><span>货物净重 <strong>{{ formatNumber(detail.actualNetWeightKg) }}</strong> kg</span><span>体积重 <strong>{{ formatNumber(detail.volumeWeightKg) }}</strong> kg</span></div>
|
||||
</section>
|
||||
|
||||
<section class="review-items" aria-labelledby="review-items-title">
|
||||
<header class="review-section-heading">
|
||||
<section class="review-items density-dialog-section" data-tone="source" aria-labelledby="review-items-title">
|
||||
<header class="review-section-heading density-dialog-section__title">
|
||||
<div>
|
||||
<h3 id="review-items-title">箱规测量明细</h3>
|
||||
</div>
|
||||
@@ -271,8 +265,8 @@ 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>
|
||||
<section class="review-items review-weight-items density-dialog-section" data-tone="source" aria-labelledby="review-weight-title">
|
||||
<header class="review-section-heading density-dialog-section__title"><h3 id="review-weight-title">称重明细</h3><span>{{ weightCount }} 条</span></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" />
|
||||
@@ -299,7 +293,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
/>
|
||||
</template>
|
||||
|
||||
<el-dialog class="admin-density-dialog" v-model="imagePreviewOpen" :title="imagePreviewTitle" width="min(760px, 92vw)" destroy-on-close append-to-body @closed="releaseImagePreview">
|
||||
<AppDialog class="admin-density-dialog" v-model="imagePreviewOpen" :title="imagePreviewTitle" width="min(760px, 92vw)" fullscreenable :show-footer="false" :close-on-click-modal="true" destroy-on-close append-to-body @closed="releaseImagePreview">
|
||||
<div class="review-image-preview">
|
||||
<el-skeleton v-if="imagePreviewLoading" :rows="5" animated />
|
||||
<div v-else-if="imagePreviewError" class="review-image-preview__error">
|
||||
@@ -309,76 +303,42 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
<img v-else-if="imagePreviewUrl" :src="imagePreviewUrl" alt="当前箱规测量原图" />
|
||||
<el-empty v-else description="暂无可预览图片" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</AppDialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<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: linear-gradient(100deg, #f3f6ff 0%, #fff 48%, #eefaf7 100%); }
|
||||
.review-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); position: sticky; top: 0; z-index: 3; flex-wrap: wrap; padding: var(--admin-space-3, 8px) 0; border-bottom: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
|
||||
.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; }
|
||||
.review-toolbar__actions :deep(.el-button) { margin-left: 0; }
|
||||
.review-toolbar__status > .review-toolbar__context { padding-left: var(--admin-space-3, 8px); border-left: 1px solid var(--cc-border); font-size: var(--admin-font-body, 13px); line-height: 1.55; overflow-wrap: anywhere; }
|
||||
.review-alert-title { display: inline-flex; align-items: center; gap: var(--admin-space-3, 8px); flex-wrap: wrap; }
|
||||
.review-data, .review-items { min-width: 0; padding: 0 0 var(--admin-space-5, 12px); border-bottom: 1px solid var(--cc-border); }
|
||||
.review-data, .review-items { min-width: 0; }
|
||||
.review-section-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); margin-bottom: var(--admin-space-3, 8px); }
|
||||
.review-section-heading > div { min-width: 0; }
|
||||
.review-section-heading h3 { margin: 0; color: var(--cc-text); font-size: var(--admin-font-section, 14px); line-height: 1.35; }
|
||||
.review-section-heading > span { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); white-space: nowrap; }
|
||||
.review-comparison { border-top: 1px solid var(--cc-border); }
|
||||
.review-comparison__row { display: grid; grid-template-columns: 78px minmax(0, 1fr); align-items: center; min-width: 0; padding: var(--admin-space-3, 8px) 0; }
|
||||
.review-comparison__row + .review-comparison__row { border-top: 1px dashed var(--cc-border); }
|
||||
.review-comparison__row > strong { color: #344054; font-size: var(--admin-font-caption, 12px); }
|
||||
.review-metrics, .review-declaration { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--admin-space-3, 8px) var(--admin-space-6, 16px); min-width: 0; margin: 0; }
|
||||
.review-declaration { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.review-metrics > div, .review-declaration > div { display: flex; min-width: 0; align-items: baseline; gap: var(--admin-space-2, 6px); }
|
||||
.review-metrics dt, .review-declaration dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); line-height: 1.4; }
|
||||
.review-metrics dd { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: var(--admin-font-section, 14px); font-weight: 600; line-height: 1.35; }
|
||||
.review-metrics dd small, .review-declaration dd small { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); font-weight: 500; }
|
||||
.review-metrics .review-metric--primary dd { color: #067647; }
|
||||
.review-declaration dd { margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: var(--admin-font-body, 13px); font-weight: 650; line-height: 1.4; }
|
||||
.review-comparison { table-layout: fixed; }
|
||||
.review-extra-facts { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--admin-space-5, 12px); margin-top: var(--admin-space-3, 8px); color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
|
||||
.review-extra-facts strong { color: var(--cc-text); }
|
||||
.review-table-scroll { width: 100%; min-width: 0; overflow: hidden; }
|
||||
.review-table { width: 100%; font-size: var(--admin-font-caption, 12px); }
|
||||
.review-table :deep(.el-table__header th) { padding: var(--admin-space-3, 8px) 0; background: #f6f8fa; color: #475467; font-size: var(--admin-font-caption, 12px); font-weight: 700; }
|
||||
.review-table :deep(.el-table__header .cell) { white-space: nowrap; }
|
||||
.review-table :deep(.el-table__cell) { padding: 7px 0; }
|
||||
.review-table { width: 100%; }
|
||||
.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: var(--admin-font-body, 13px); }
|
||||
.review-declaration dd, .review-metrics dd { font-size: var(--admin-font-body, 13px); }
|
||||
.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: var(--admin-font-caption, 12px); }
|
||||
.review-weight-totals span { padding: 4px 8px; border-radius: 7px; background: #f0faf5; }
|
||||
.review-weight-totals strong { margin-left: 6px; font-size: var(--admin-font-body, 13px); }
|
||||
.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-value { color: var(--el-color-success-dark-2); font-weight: 600; }
|
||||
.review-location { 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); }
|
||||
@media (max-width: 720px) {
|
||||
.review-toolbar { align-items: stretch; flex-direction: column; margin: -14px -14px 0; padding: 12px 14px; }
|
||||
.review-toolbar { align-items: stretch; flex-direction: column; }
|
||||
.review-toolbar__status > .review-toolbar__context { width: 100%; padding-top: 8px; padding-left: 0; border-top: 1px solid var(--cc-border); border-left: 0; }
|
||||
.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)); }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.review-toolbar { margin: -10px -10px 0; padding: 10px; }
|
||||
.review-comparison__row { grid-template-columns: 1fr; gap: 6px; }
|
||||
.review-metrics, .review-declaration { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px 10px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -685,6 +685,17 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await openWeightDialog(wrapper)
|
||||
|
||||
await dialogWeightInput().setValue('286.125')
|
||||
const expand = document.body.querySelector<HTMLButtonElement>('button[aria-label="展开全屏"]')
|
||||
expect(expand).not.toBeNull()
|
||||
await new DOMWrapper(expand!).trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.body.querySelector('.el-dialog.is-fullscreen')).not.toBeNull()
|
||||
expect(dialogWeightInput().element.value).toBe('286.125')
|
||||
const collapse = document.body.querySelector<HTMLButtonElement>('button[aria-label="退出全屏"]')
|
||||
await new DOMWrapper(collapse!).trigger('click')
|
||||
await flushPromises()
|
||||
expect(dialogWeightInput().element.value).toBe('286.125')
|
||||
expect(confirmWeightResult).not.toHaveBeenCalled()
|
||||
await dialogConfirmButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
|
||||
@@ -649,14 +649,15 @@ defineExpose({ reload: loadWorkspace })
|
||||
|
||||
<AppDialog class="admin-density-dialog"
|
||||
v-model="previewOpen"
|
||||
title="确认称重图片"
|
||||
:title="workspace?.inboundCode ? `确认称重图片 · ${workspace.inboundCode}` : '确认称重图片'"
|
||||
:loading="previewLoading"
|
||||
:confirm-loading="Boolean(previewResult && isActionLoading('confirm', previewResult.id))"
|
||||
:confirm-disabled="previewConfirmDisabled"
|
||||
:show-confirm="Boolean(previewResult && previewCanConfirm && (previewResult.recognitionStatus !== 'CONFIRMED' || reviewCorrectionEnabled))"
|
||||
:confirm-text="previewConfirmText"
|
||||
confirm-type="success"
|
||||
width="min(640px, calc(100vw - 28px))"
|
||||
width="min(920px, calc(100vw - 28px))"
|
||||
fullscreenable
|
||||
@confirm="previewResult && confirmResult(previewResult, true)"
|
||||
>
|
||||
<LoadErrorState
|
||||
@@ -668,37 +669,44 @@ defineExpose({ reload: loadWorkspace })
|
||||
>
|
||||
<template #details><ApiErrorMeta :details="previewError" /></template>
|
||||
</LoadErrorState>
|
||||
<template v-else-if="previewResult">
|
||||
<div v-else-if="previewResult" class="weight-preview-layout">
|
||||
<img v-if="previewUrl" class="weight-preview" :src="previewUrl" alt="当前入库单的称重图片" />
|
||||
<div class="weight-key-facts" aria-label="称重关键数据">
|
||||
<div><span>托盘类型</span><strong>{{ previewResult.palletType || '未记录类型' }}</strong></div>
|
||||
<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-if="weightDrafts[previewResult.id]" class="preview-weight">
|
||||
<label>
|
||||
<span>确认含托盘毛重</span>
|
||||
<el-input-number
|
||||
v-model="weightDrafts[previewResult.id].value"
|
||||
aria-label="当前图片确认重量"
|
||||
:min="minimumWeight[weightDrafts[previewResult.id].unit]"
|
||||
:max="maximumSafeWeight"
|
||||
:precision="3"
|
||||
:controls="false"
|
||||
:disabled="Boolean(activeAction) || !previewInputEditable" placeholder="请输入确认重量" />
|
||||
</label>
|
||||
<el-segmented
|
||||
v-model="weightDrafts[previewResult.id].unit"
|
||||
aria-label="当前图片重量单位"
|
||||
:options="['KG', 'LB']"
|
||||
:disabled="Boolean(activeAction) || !previewInputEditable"
|
||||
/>
|
||||
</div>
|
||||
<label v-if="reviewCorrectionEnabled && previewResult.recognitionStatus === 'CONFIRMED' && weightDrafts[previewResult.id]" class="correction-reason">
|
||||
<span>修改原因 *</span>
|
||||
<el-input v-model="weightDrafts[previewResult.id].reason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请填写本次毛重修正原因" />
|
||||
</label>
|
||||
</template>
|
||||
<section class="density-dialog-section" data-tone="work">
|
||||
<h3 class="density-dialog-section__title">称重数据</h3>
|
||||
<table class="density-table density-table--editable weight-confirm-table" aria-label="称重关键数据">
|
||||
<colgroup><col class="weight-confirm-label-column" /><col /></colgroup>
|
||||
<tbody>
|
||||
<tr><th scope="row"><span>托盘类型</span></th><td>{{ previewResult.palletType || '未记录类型' }}</td></tr>
|
||||
<tr><th scope="row">当前托盘皮重</th><td>{{ formatWeight(previewResult.palletTareWeightKg) }}</td></tr>
|
||||
<tr><th scope="row">OCR 识别毛重</th><td>{{ formatWeight(previewResult.ocrWeightValue, previewResult.ocrWeightUnit || 'KG') }}</td></tr>
|
||||
<template v-if="weightDrafts[previewResult.id]">
|
||||
<tr><th scope="row"><label for="weight-confirm-value">确认含托盘毛重</label></th><td>
|
||||
<el-input-number
|
||||
id="weight-confirm-value"
|
||||
v-model="weightDrafts[previewResult.id].value"
|
||||
aria-label="当前图片确认重量"
|
||||
:min="minimumWeight[weightDrafts[previewResult.id].unit]"
|
||||
:max="maximumSafeWeight"
|
||||
:precision="3"
|
||||
:controls="false"
|
||||
:disabled="Boolean(activeAction) || !previewInputEditable" placeholder="请输入确认重量" />
|
||||
</td></tr>
|
||||
<tr><th scope="row">重量单位</th><td>
|
||||
<el-segmented
|
||||
v-model="weightDrafts[previewResult.id].unit"
|
||||
aria-label="当前图片重量单位"
|
||||
:options="['KG', 'LB']"
|
||||
:disabled="Boolean(activeAction) || !previewInputEditable"
|
||||
/>
|
||||
</td></tr>
|
||||
<tr v-if="reviewCorrectionEnabled && previewResult.recognitionStatus === 'CONFIRMED'"><th scope="row"><label for="weight-correction-reason">修改原因 *</label></th><td>
|
||||
<el-input id="weight-correction-reason" v-model="weightDrafts[previewResult.id].reason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请填写本次毛重修正原因" />
|
||||
</td></tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</section>
|
||||
</template>
|
||||
@@ -744,14 +752,9 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-data-table .weight-net-value { color: #16875b; font-weight: 750; }
|
||||
.weight-location { display: inline-block; padding: 3px 7px; border: 1px solid #e2e8f2; border-radius: 5px; background: #f3f6fb; color: #53627a; }
|
||||
.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: var(--admin-space-3, 8px) var(--admin-space-5, 12px); margin-top: var(--admin-space-3, 8px); padding: var(--admin-space-3, 8px) 0; border-block: 1px solid var(--cc-border); }
|
||||
.weight-key-facts div { display: grid; min-width: 0; gap: var(--admin-space-1, 4px); }
|
||||
.weight-key-facts span { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
|
||||
.weight-key-facts strong { color: #101828; font-size: var(--admin-font-section, 14px); }
|
||||
.preview-weight { display: grid; grid-template-columns: minmax(0, 1fr) 120px; align-items: end; gap: var(--admin-space-5, 12px); margin-top: 14px; }
|
||||
.preview-weight label { display: grid; gap: var(--admin-space-2, 6px); color: #475467; font-size: var(--admin-font-caption, 12px); }
|
||||
.preview-weight :deep(.el-input-number), .preview-weight :deep(.el-segmented) { width: 100%; }
|
||||
.correction-reason { display: grid; gap: var(--admin-space-2, 6px); margin-top: 14px; color: #475467; font-size: var(--admin-font-caption, 12px); }
|
||||
.weight-preview-layout { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(320px, 1fr); align-items: start; gap: var(--admin-space-5, 12px); }
|
||||
.weight-confirm-table { table-layout: fixed; }
|
||||
.weight-confirm-label-column { width: 42%; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.weight-heading { flex-direction: column; }
|
||||
@@ -761,13 +764,11 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-heading__actions :deep(.el-button) { width: 100%; min-height: 44px; }
|
||||
.weight-summary-group { width: 100%; justify-content: space-between; }
|
||||
.weight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.weight-preview-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.weight-grid { grid-template-columns: 1fr; }
|
||||
.preview-weight { grid-template-columns: 1fr; }
|
||||
.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