This commit is contained in:
2026-08-13 17:43:04 +08:00
parent a1ed15f055
commit 41038e1edd
2 changed files with 39 additions and 85 deletions
@@ -147,7 +147,7 @@ describe('InboundMeasurementPanel', () => {
const wrapper = await mountPanel()
expect(getInboundMeasurement).toHaveBeenCalledWith('10001', '7001')
expect(wrapper.text()).toContain('第三方设备')
expect((wrapper.find('input[readonly]').element as HTMLInputElement).value).toBe('RK202608040001')
expect(wrapper.text()).toContain('RK202608040001')
expect(wrapper.text()).toContain('周长(cm)')
expect(wrapper.find('.measurement-table').text()).toContain('190')
@@ -187,7 +187,7 @@ describe('InboundMeasurementPanel', () => {
const wrapper = await mountPanel()
expect(wrapper.text()).toContain('未知来源')
expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '未知来源')).toBe(true)
expect(wrapper.text()).not.toContain('LEGACY_DEVICE')
})
@@ -195,7 +195,7 @@ describe('InboundMeasurementPanel', () => {
const wrapper = await mountPanel()
const text = wrapper.text()
expect(text).toContain('原单数据')
expect(text).toContain('原单数据与测量汇总')
expect(text).toContain('扫描箱测量数据明细')
expect(text).toContain('订单总箱数')
expect(text).toContain('确认箱数合计')
@@ -208,9 +208,9 @@ describe('InboundMeasurementPanel', () => {
expect(wrapper.find('.measurement-item').exists()).toBe(false)
expect(wrapper.find('.measurement-table').text()).toContain('周长(cm)')
const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default)
expect(source).toContain('.measurement-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));')
expect(source).toContain('.measurement-form-grid, .measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));')
expect(source).toContain('.measurement-table { width: 100%; font-size: 12px; }')
expect(source).toContain('.measurement-order-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));')
expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)')
expect(source).toContain('defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit })')
})
@@ -218,8 +218,8 @@ describe('InboundMeasurementPanel', () => {
const wrapper = await mountPanel()
expect(wrapper.classes()).toContain('surface-panel')
expect(wrapper.find('.measurement-panel__heading').classes()).toContain('panel-heading')
expect(wrapper.find('.status-badge').text()).toBe('测量中')
expect(wrapper.find('.measurement-panel__heading').exists()).toBe(true)
expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '测量中')).toBe(true)
})
it('saves only the changed confirmed package count with the current measurement version', async () => {
@@ -314,8 +314,8 @@ describe('InboundMeasurementPanel', () => {
await flushPromises()
expect(getInboundMeasurement).toHaveBeenLastCalledWith('10002', '7002')
expect(wrapper.text()).toContain('RK202608040002')
expect(wrapper.text()).not.toContain('测试企业')
expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === 'RK202608040002')).toBe(true)
expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '测试企业')).toBe(false)
})
it('renders a permission-specific read-only state without requesting data', async () => {
@@ -1,12 +1,11 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { AlertTriangle, Image, Plus, RefreshCw, Ruler, Save, Send, Upload } from '@lucide/vue'
import { AlertTriangle, Image, Plus, Save, Send, Upload } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { inboundProcessStatusLabels, inboundStatusLabel } from '../status'
import { createManualCartonScan, downloadInboundMeasurementImage, getInboundMeasurement, submitInboundMeasurement, updateConfirmedPackageCount } from './api'
@@ -122,7 +121,6 @@ const isDirty = computed(() => Boolean(detail.value) && snapshot.value !== basel
const registrationOpen = computed(() => hasContext.value && canView.value && Boolean(detail.value))
const requiredPermission = computed(() => canEdit.value ? 'platform:firstleg:inbound:measure:edit' : null)
const sourceLabel = computed(() => ({ THIRD_PARTY: '第三方设备', MANUAL: '平台人工', OPENAPI: '第三方设备' }[detail.value?.sourceType || ''] || (detail.value?.sourceType ? '未知来源' : '尚未测量')))
const sourceTagType = computed(() => detail.value?.sourceType === 'MANUAL' ? 'info' : 'primary')
const { captureBaseline } = useUnsavedWorkRegistration({
key: 'p2-inbound-measurement-editor',
@@ -448,27 +446,6 @@ defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submi
<template>
<section class="surface-panel measurement-panel" aria-label="入库测量草稿">
<header class="panel-heading measurement-panel__heading">
<div>
<span class="measurement-panel__icon"><Ruler :size="18" aria-hidden="true" /></span>
<div>
<h2>测量与箱规</h2>
<p>维护箱规原始值;体积、材积重和汇总结果均由服务端计算。</p>
</div>
</div>
<div class="measurement-panel__actions">
<el-button :loading="loading" :disabled="!hasContext || !canView || saving || submitting" @click="reloadLatest">
<RefreshCw :size="15" />刷新
</el-button>
<el-button v-if="canEdit" type="primary" plain :loading="saving" :disabled="!mayMaintain" @click="saveDraft">
<Save :size="15" />保存确认箱数
</el-button>
<el-button v-if="canSubmit" type="primary" :loading="submitting" :disabled="!maySubmit" @click="submitForReview">
<Send :size="15" />提交审核
</el-button>
</div>
</header>
<el-empty v-if="!hasContext" description="请选择所属企业和入库单后维护测量数据" />
<LoadErrorState
@@ -493,27 +470,6 @@ defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submi
</LoadErrorState>
<template v-else-if="detail">
<div class="measurement-context">
<div class="measurement-context__identity">
<div>
<strong>{{ detail.inboundCode }}</strong>
<span>{{ detail.companyName }} · {{ detail.channelName || detail.channelCode || '未设置渠道' }}</span>
</div>
<div class="measurement-tags">
<el-tag :type="sourceTagType" effect="light">{{ sourceLabel }}</el-tag>
<StatusBadge :status="processStatusLabel(detail.processStatus)" />
<span>版本 {{ form.version }}</span>
</div>
</div>
<dl class="measurement-metrics">
<div><dt>订单总箱数</dt><dd>{{ formatNumber(expectedPackageCount, 0) }}</dd></div>
<div><dt>确认箱数合计</dt><dd>{{ formatNumber(confirmedPackageCount, 0) }}</dd></div>
<div><dt>实测总毛重</dt><dd>{{ formatNumber(detail.actualGrossWeightKg) }} <small>T.K.GW</small></dd></div>
<div><dt>实测总体积</dt><dd>{{ formatNumber(detail.actualVolumeCbm, 4) }} <small>T.CBM</small></dd></div>
<div><dt>实测总材积重</dt><dd>{{ formatNumber(detail.volumeWeightKg) }} <small>KG</small></dd></div>
</dl>
</div>
<el-alert v-if="!countEditableProcess" type="info" :closable="false" show-icon>
<template #title>当前状态为“{{ processStatusLabel(detail.processStatus) }}”,测量数据仅可查看</template>
确认箱数只允许在待测量、测量中、待审核或审核驳回状态维护。
@@ -549,15 +505,26 @@ defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submi
<fieldset :disabled="!mayMaintain || saving">
<legend class="sr-only">测量草稿</legend>
<section class="measurement-section">
<div class="measurement-section__heading">
<div><h3>原单数据</h3><p>原单值仅用于审核对比,不参与前端计算。</p></div>
<div class="measurement-section__heading measurement-panel__heading">
<div><h3>原单数据与测量汇总</h3></div>
</div>
<div class="measurement-data-grid">
<label><span>入仓号</span><el-input :model-value="detail.inboundCode || '—'" readonly /></label>
<label><span>公司</span><el-input :model-value="detail.companyName || '—'" readonly /></label>
<label><span>渠道</span><el-input :model-value="detail.channelName || detail.channelCode || '—'" readonly /></label>
<label><span>测量来源</span><el-input :model-value="sourceLabel" readonly /></label>
<label><span>当前状态</span><el-input :model-value="processStatusLabel(detail.processStatus)" readonly /></label>
<label><span>数据版本</span><el-input :model-value="String(form.version)" readonly /></label>
<label><span>确认箱数合计</span><el-input :model-value="formatNumber(confirmedPackageCount, 0)" readonly /></label>
<label><span>订单总箱数</span><el-input :model-value="formatNumber(expectedPackageCount, 0)" readonly /></label>
<label><span>实测总毛重(T.K.GW)</span><el-input :model-value="formatNumber(detail.actualGrossWeightKg)" readonly /></label>
<label><span>原单总毛重(T.K.GW)</span><el-input :model-value="formatNumber(form.originalGrossWeightKg)" readonly /></label>
<label><span>实测总体积(T.CBM)</span><el-input :model-value="formatNumber(detail.actualVolumeCbm, 4)" readonly /></label>
<label><span>原单总体积(T.CBM)</span><el-input :model-value="formatNumber(form.originalVolumeCbm, 4)" readonly /></label>
<label><span>实测总材积重(kg)</span><el-input :model-value="formatNumber(detail.volumeWeightKg)" readonly /></label>
<label><span>原单据号码(CKW)</span><el-input :model-value="form.originalDocumentNo || '—'" readonly /></label>
<label><span>实际品名</span><el-input :model-value="form.actualGoodsName || '—'" readonly /></label>
</div>
<dl class="measurement-order-summary">
<div><dt>原单据号码(CKW)</dt><dd>{{ form.originalDocumentNo || '—' }}</dd></div>
<div><dt>原单据毛重(T.K.GW)</dt><dd>{{ formatNumber(form.originalGrossWeightKg) }}</dd></div>
<div><dt>原单据总体积(T.CBM)</dt><dd>{{ formatNumber(form.originalVolumeCbm, 4) }}</dd></div>
<div><dt>实际品名</dt><dd>{{ form.actualGoodsName || '—' }}</dd></div>
</dl>
</section>
<section class="measurement-section measurement-items-section">
@@ -701,42 +668,34 @@ defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submi
.measurement-panel__actions, .measurement-panel__footer > div { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.measurement-panel__actions :deep(.el-button), .measurement-panel__footer :deep(.el-button) { margin-left: 0; }
.measurement-skeleton { padding: 24px; }
.measurement-context { padding: 18px 20px; background: #fbfcfe; }
.measurement-context { padding: 16px 20px; border-bottom: 1px solid var(--cc-border); }
.measurement-context__identity { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.measurement-context__identity > div:first-child { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
.measurement-context__identity strong { overflow-wrap: anywhere; color: var(--cc-text); font-size: 15px; }
.measurement-context__identity span { color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
.measurement-tags { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 7px; }
.measurement-tags > span:last-child { color: var(--cc-muted); font-size: 11px; }
.measurement-metrics { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); margin: 16px 0 0; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
.measurement-metrics > div { min-width: 0; padding: 13px 15px; border-right: 1px solid var(--cc-border); }
.measurement-metrics > div:last-child { border-right: 0; }
.measurement-metrics dt { color: var(--cc-muted); font-size: 11px; }
.measurement-metrics dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 17px; font-weight: 700; }
.measurement-metrics small { color: var(--cc-muted); font-size: 10px; font-weight: 500; }
.measurement-metrics { display: none; }
.measurement-panel > :deep(.el-alert), .measurement-alert { margin: 16px 20px 0; width: auto; }
.measurement-alert p { margin: 5px 0 0; line-height: 1.55; }
.measurement-alert ul { margin: 7px 0 0; padding-left: 18px; font-size: 12px; line-height: 1.65; }
.measurement-alert .el-button { margin-top: 9px; }
.measurement-form fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
.measurement-section { padding: 22px 20px; border-top: 1px solid var(--cc-border); }
.measurement-section { padding: 18px 20px; border-top: 1px solid var(--cc-border); }
.measurement-section__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 16px; }
.measurement-section__heading > div { min-width: 0; }
.measurement-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 15px; }
.measurement-form-grid, .measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 24px; }
.measurement-form label { display: flex; min-width: 0; flex-direction: column; gap: 7px; color: #344054; font-size: 12px; font-weight: 650; }
.measurement-form :deep(.el-input-number), .measurement-form :deep(.el-input) { width: 100%; }
.measurement-order-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; }
.measurement-order-summary > div { min-width: 0; padding: 11px 13px; border-right: 1px solid var(--cc-border); }
.measurement-order-summary > div:last-child { border-right: 0; }
.measurement-order-summary dt { color: var(--cc-muted); font-size: 11px; }
.measurement-order-summary dd { margin: 4px 0 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 13px; font-weight: 650; }
.measurement-data-grid label { display: flex; min-width: 0; flex-direction: column; gap: 5px; color: #344054; font-size: 12px; font-weight: 600; }
.measurement-data-grid :deep(.el-input__wrapper) { border-radius: 3px; background: #f5f5f5; box-shadow: 0 0 0 1px #e5e7eb inset; }
.measurement-image-preview { display: grid; min-height: 240px; place-items: center; }
.measurement-image-preview img { display: block; width: auto; max-width: 100%; max-height: 68vh; object-fit: contain; }
.manual-entry-alert { margin-bottom: 16px; }
.manual-entry-alert ul { margin: 6px 0 0; padding-left: 18px; }
.manual-entry-form :deep(.el-input-number), .manual-entry-form :deep(.el-date-editor) { width: 100%; }
.manual-entry-upload { width: 100%; }
.measurement-items-section { background: #fafbfc; }
.measurement-items-section { background: #fff; }
.measurement-table { width: 100%; font-size: 12px; }
.measurement-table :deep(.el-table__header th) { padding: 8px 0; background: #f6f8fa; color: #475467; font-size: 11px; font-weight: 700; }
.measurement-table :deep(.el-table__cell) { padding: 7px 0; }
@@ -747,16 +706,14 @@ defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submi
.measurement-row-details { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px 20px; margin: 0; padding: 4px 18px 8px 62px; }
.measurement-row-details dt { color: var(--cc-muted); font-size: 11px; }
.measurement-row-details dd { margin: 3px 0 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 12px; font-weight: 600; }
.measurement-panel__footer { position: sticky; bottom: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 20px; border-top: 1px solid var(--cc-border); background: rgba(255, 255, 255, .97); box-shadow: 0 -8px 22px rgba(15, 23, 42, .05); }
.measurement-panel__footer { position: sticky; bottom: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 20px; border-top: 1px solid var(--cc-border); background: #fff; }
.measurement-panel__footer > span { display: inline-flex; align-items: center; gap: 6px; color: var(--cc-muted); font-size: 12px; }
.measurement-panel__footer > span:has(svg) { color: #b54708; font-weight: 650; }
.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-order-summary, .measurement-row-details { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.measurement-order-summary > div:nth-child(2) { border-right: 0; }
.measurement-order-summary > div:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); }
.measurement-row-details { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.measurement-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.measurement-metrics > div:nth-child(2) { border-right: 0; }
.measurement-metrics > div:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); }
@@ -769,13 +726,10 @@ defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submi
.measurement-tags { justify-content: flex-start; }
.measurement-context, .measurement-section { padding-right: 14px; padding-left: 14px; }
.measurement-panel > :deep(.el-alert), .measurement-alert { margin-right: 14px; margin-left: 14px; }
.measurement-form-grid { grid-template-columns: 1fr; }
.measurement-form-grid, .measurement-data-grid { grid-template-columns: 1fr; }
.measurement-row-details { grid-template-columns: 1fr; padding-left: 54px; }
}
@media (max-width: 420px) {
.measurement-metrics, .measurement-order-summary { grid-template-columns: 1fr; }
.measurement-metrics > div, .measurement-order-summary > div { border-right: 0; border-bottom: 1px solid var(--cc-border); }
.measurement-metrics > div:last-child, .measurement-order-summary > div:last-child { border-bottom: 0; }
}
</style>