This commit is contained in:
2026-08-18 11:33:01 +08:00
parent 834382e04b
commit 114d5bacc7
34 changed files with 522 additions and 570 deletions
@@ -18,7 +18,15 @@ describe('InboundMeasurementDialog', () => {
expect(source).not.toContain('class="surface-panel measurement-dialog-weight"')
expect(source).toContain('border-bottom: 1px solid var(--cc-border)')
expect(source).toContain('@summary-changed="handleWeightSummaryChanged"')
expect(source).toContain('measurementPanelRef.value?.updateActualGrossWeight(actualGrossWeightKg)')
expect(source).toContain('measurementPanelRef.value?.updateActualWeights(actualGrossWeightKg, actualNetWeightKg)')
})
it('shows the inbound code once in the dialog title and not as a second body heading', () => {
expect(source).toContain('`测量 · ${props.inboundCode?.trim() || props.inboundId}`')
expect(source).toContain(':title="title"')
expect(panelSource).not.toContain('<span>入仓码</span>')
expect(panelSource).not.toContain('<strong>{{ detail.inboundCode')
expect(panelSource).toContain('class="measurement-summary-context"')
})
it('is the only measurement dialog opened by both formal list entries', () => {
@@ -43,8 +43,8 @@ async function beforeClose(done: () => void) {
if (await mayClose()) done()
}
function handleWeightSummaryChanged(actualGrossWeightKg: number | null) {
measurementPanelRef.value?.updateActualGrossWeight(actualGrossWeightKg)
function handleWeightSummaryChanged(actualGrossWeightKg: number | null, actualNetWeightKg: number | null) {
measurementPanelRef.value?.updateActualWeights(actualGrossWeightKg, actualNetWeightKg)
}
function handleUpdated() {
@@ -66,6 +66,7 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
originalPackageCount: 10,
actualPackageCount: 10,
actualGrossWeightKg: 120,
actualNetWeightKg: 110,
actualVolumeCbm: 0.6,
volumeWeightKg: 100.2,
actualGoodsName: '蓝牙音箱',
@@ -216,7 +217,53 @@ describe('InboundMeasurementPanel', () => {
expect(source).toContain('.measurement-data-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));')
expect(source).toContain('.measurement-table { width: 100%; font-size: 12px; }')
expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)')
expect(source).toContain('defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })')
expect(source).toContain('defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })')
})
it('renders customer declaration facts as plain read-only values', async () => {
const wrapper = await mountPanel()
const declaration = wrapper.find('.measurement-declaration')
expect(declaration.text()).toContain('申报箱数')
expect(declaration.text()).toContain('10')
expect(declaration.text()).toContain('申报毛重(kg)')
expect(declaration.text()).toContain('118.5')
expect(declaration.text()).toContain('申报体积(m³)')
expect(declaration.text()).toContain('0.8')
expect(declaration.text()).toContain('原单据号码')
expect(declaration.text()).toContain('DOC-001')
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(4)
expect(declaration.findAll('input')).toHaveLength(1)
expect(declaration.find('.measurement-goods-name input').exists()).toBe(true)
})
it('shows all declaration fields as plain text when measurement is not editable', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'WAIT_SPLIT' }))
const wrapper = await mountPanel()
const declaration = wrapper.find('.measurement-declaration')
expect(declaration.find('input').exists()).toBe(false)
expect(declaration.text()).toContain('蓝牙音箱')
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(5)
})
it('saves the corrected actual goods name without overriding declaration facts', async () => {
const wrapper = await mountPanel()
await wrapper.find('.measurement-goods-name input').setValue('修正后的蓝牙音箱')
const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据'))
await save?.trigger('click')
await flushPromises()
expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({
measurementId: '9001',
version: 2,
actualGoodsName: '修正后的蓝牙音箱',
items: expect.any(Array),
}))
const payload = vi.mocked(saveInboundMeasurementDraft).mock.calls[0]?.[2]
expect(payload).not.toHaveProperty('originalDocumentNo')
expect(payload).not.toHaveProperty('originalGrossWeightKg')
expect(payload).not.toHaveProperty('originalVolumeCbm')
})
it('updates the server weight summary without discarding unsaved measurement edits', async () => {
@@ -224,10 +271,11 @@ describe('InboundMeasurementPanel', () => {
const countInput = wrapper.find('.measurement-count-editor input')
await countInput.setValue('9')
wrapper.vm.updateActualGrossWeight(125.75)
wrapper.vm.updateActualWeights(125.75, 115.75)
await flushPromises()
expect(wrapper.find('.measurement-metrics').text()).toContain('125.75')
expect(wrapper.find('.measurement-metrics').text()).toContain('115.75')
expect((countInput.element as HTMLInputElement).value).toBe('9')
})
@@ -237,6 +285,9 @@ describe('InboundMeasurementPanel', () => {
expect(wrapper.classes()).toContain('surface-panel')
expect(wrapper.find('.measurement-summary-header').exists()).toBe(true)
expect(wrapper.find('.measurement-summary-status').text()).toContain('测量中')
expect(wrapper.find('.measurement-summary-header').text()).not.toContain('RK202608040001')
expect(wrapper.find('.measurement-summary-header').text()).toContain('测试企业')
expect(wrapper.find('.measurement-summary-header').text()).toContain('美西海运')
})
it('saves only the changed confirmed package count with the current measurement version', async () => {
@@ -374,7 +425,6 @@ describe('InboundMeasurementPanel', () => {
await flushPromises()
expect(getInboundMeasurement).toHaveBeenLastCalledWith('10002', '7002')
expect(wrapper.find('.measurement-summary-header').text()).toContain('RK202608040002')
expect(wrapper.find('.measurement-summary-header').text()).toContain('第二企业')
expect(wrapper.find('.measurement-summary-header').text()).not.toContain('测试企业')
})
@@ -37,9 +37,6 @@ interface EditableMeasurementItem extends SaveInboundMeasurementItemRequest {
interface MeasurementFormModel {
measurementId: string | null
version: number
originalDocumentNo: string
originalGrossWeightKg: number | null
originalVolumeCbm: number | null
actualGoodsName: string
items: EditableMeasurementItem[]
}
@@ -79,9 +76,6 @@ let itemSequence = 0
const form = reactive<MeasurementFormModel>({
measurementId: null,
version: 0,
originalDocumentNo: '',
originalGrossWeightKg: null,
originalVolumeCbm: null,
actualGoodsName: '',
items: [],
})
@@ -239,9 +233,6 @@ function editableItem(item: InboundMeasurementItem): EditableMeasurementItem {
function clearForm() {
form.measurementId = null
form.version = 0
form.originalDocumentNo = ''
form.originalGrossWeightKg = null
form.originalVolumeCbm = null
form.actualGoodsName = ''
form.items.splice(0)
baseline.value = JSON.stringify(form)
@@ -252,9 +243,6 @@ function applyDetail(resource: InboundMeasurementDetail) {
detail.value = resource
form.measurementId = resource.measurementId == null ? null : String(resource.measurementId)
form.version = resource.version ?? 0
form.originalDocumentNo = resource.originalDocumentNo || ''
form.originalGrossWeightKg = resource.originalGrossWeightKg
form.originalVolumeCbm = resource.originalVolumeCbm
form.actualGoodsName = resource.actualGoodsName || ''
form.items.splice(0, form.items.length, ...resource.items.map(editableItem))
baseline.value = JSON.stringify(form)
@@ -340,24 +328,13 @@ async function saveDraft() {
conflictDetected.value = false
try {
const originalCounts = new Map((detail.value?.items || []).map((item) => [String(item.id), item.packageCount]))
const originalDraft = detail.value
? { originalDocumentNo: detail.value.originalDocumentNo || '', originalGrossWeightKg: detail.value.originalGrossWeightKg, originalVolumeCbm: detail.value.originalVolumeCbm, actualGoodsName: detail.value.actualGoodsName || '' }
: null
const draftChanged = Boolean(originalDraft && (
originalDraft.originalDocumentNo !== form.originalDocumentNo
|| originalDraft.originalGrossWeightKg !== form.originalGrossWeightKg
|| originalDraft.originalVolumeCbm !== form.originalVolumeCbm
|| originalDraft.actualGoodsName !== form.actualGoodsName
))
const draftChanged = Boolean(detail.value && (detail.value.actualGoodsName || '') !== form.actualGoodsName)
const changedItems = form.items.filter((item) => originalCounts.get(item.itemId) !== item.packageCount)
let version = form.version
if (draftChanged) {
const saved = await saveInboundMeasurementDraft(customerId.value, props.inboundId, {
measurementId: form.measurementId,
version,
originalDocumentNo: form.originalDocumentNo.trim() || null,
originalGrossWeightKg: form.originalGrossWeightKg,
originalVolumeCbm: form.originalVolumeCbm,
actualGoodsName: form.actualGoodsName.trim() || null,
items: form.items.map(({ itemId: _itemId, clientKey: _clientKey, serverValues: _serverValues, ...item }) => item),
})
@@ -446,9 +423,9 @@ function processStatusLabel(status: string | undefined) {
return inboundStatusLabel(status, inboundProcessStatusLabels)
}
function updateActualGrossWeight(actualGrossWeightKg: number | null) {
function updateActualWeights(actualGrossWeightKg: number | null, actualNetWeightKg: number | null) {
if (!detail.value) return
detail.value = { ...detail.value, actualGrossWeightKg }
detail.value = { ...detail.value, actualGrossWeightKg, actualNetWeightKg }
}
function measurementRowClassName({ row }: { row: EditableMeasurementItem }) {
@@ -472,7 +449,7 @@ watch(
onBeforeUnmount(releaseImagePreview)
defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })
defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })
</script>
<template>
@@ -523,10 +500,9 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
<legend class="sr-only">测量草稿</legend>
<section class="measurement-section measurement-summary-section">
<header class="measurement-summary-header">
<div>
<span>入仓码</span>
<strong>{{ detail.inboundCode || '—' }}</strong>
<p>{{ detail.companyName || '客户未命名' }} · {{ detail.channelName || detail.channelCode || '渠道未填写' }}</p>
<div class="measurement-summary-context">
<p><span>客户</span>{{ detail.companyName || '客户未命名' }}</p>
<p><span>渠道</span>{{ detail.channelName || detail.channelCode || '渠道未填写' }}</p>
</div>
<div class="measurement-summary-status">
<el-tag effect="light">{{ processStatusLabel(detail.processStatus) }}</el-tag>
@@ -539,7 +515,8 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
<dt>实测箱数</dt>
<dd>{{ formatNumber(confirmedPackageCount, 0) }} <small>/ 申报 {{ formatNumber(expectedPackageCount, 0) }}</small></dd>
</div>
<div class="measurement-metric--primary"><dt>货物重量</dt><dd>{{ formatNumber(detail.actualGrossWeightKg) }} <small>kg</small></dd></div>
<div><dt>实测毛重</dt><dd>{{ formatNumber(detail.actualGrossWeightKg) }} <small>kg</small></dd></div>
<div class="measurement-metric--primary"><dt>货物重量</dt><dd>{{ formatNumber(detail.actualNetWeightKg) }} <small>kg</small></dd></div>
<div><dt>总体积</dt><dd>{{ formatNumber(detail.actualVolumeCbm, 4) }} <small>m³</small></dd></div>
<div><dt>材积重</dt><dd>{{ formatNumber(detail.volumeWeightKg) }} <small>kg</small></dd></div>
</dl>
@@ -547,11 +524,12 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
<section class="measurement-declaration" aria-labelledby="measurement-declaration-title">
<h4 id="measurement-declaration-title">申报信息</h4>
<div class="measurement-data-grid">
<label><span>申报箱数</span><el-input :model-value="formatNumber(expectedPackageCount, 0)" readonly /></label>
<label><span>申报毛重(kg)</span><el-input-number v-model="form.originalGrossWeightKg" :min="0" :precision="3" controls-position="right" /></label>
<label><span>申报体积(m³)</span><el-input-number v-model="form.originalVolumeCbm" :min="0" :precision="4" controls-position="right" /></label>
<label><span>实际品名</span><el-input v-model="form.actualGoodsName" placeholder="请输入实际品名" maxlength="128" /></label>
<label><span>原单据号码</span><el-input v-model="form.originalDocumentNo" placeholder="请输入原单据号码" maxlength="128" /></label>
<div class="measurement-readonly-field"><span>申报箱数</span><strong>{{ formatNumber(expectedPackageCount, 0) }}</strong></div>
<div class="measurement-readonly-field"><span>申报毛重(kg)</span><strong>{{ formatNumber(detail.originalGrossWeightKg) }}</strong></div>
<div class="measurement-readonly-field"><span>申报体积(m³)</span><strong>{{ formatNumber(detail.originalVolumeCbm, 4) }}</strong></div>
<label v-if="mayMaintain" class="measurement-goods-name"><span>实际品名</span><el-input v-model="form.actualGoodsName" placeholder="请输入实际品名" maxlength="255" /></label>
<div v-else class="measurement-readonly-field measurement-goods-name"><span>实际品名</span><strong>{{ detail.actualGoodsName || '—' }}</strong></div>
<div class="measurement-readonly-field"><span>原单据号码</span><strong>{{ detail.originalDocumentNo || '—' }}</strong></div>
</div>
</section>
</section>
@@ -696,10 +674,9 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
.measurement-section { padding: 16px 20px; border-top: 1px solid var(--cc-border); }
.measurement-summary-section { display: grid; gap: 12px; border-top: 0; }
.measurement-summary-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.measurement-summary-header > div:first-child { display: grid; min-width: 0; gap: 3px; }
.measurement-summary-context { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 8px 20px; }
.measurement-summary-header span { color: var(--cc-muted); font-size: 11px; }
.measurement-summary-header strong { overflow-wrap: anywhere; color: var(--cc-text); font-size: 20px; line-height: 1.3; }
.measurement-summary-header p { margin: 0; color: var(--cc-muted); font-size: 12px; }
.measurement-summary-context p { display: inline-flex; min-width: 0; align-items: baseline; gap: 6px; margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 12px; font-weight: 600; }
.measurement-summary-status { display: flex; flex: 0 0 auto; align-items: center; gap: 9px; }
.measurement-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 2px 0; }
.measurement-metrics > div { display: grid; min-width: 0; min-height: 58px; align-content: center; gap: 4px; padding: 5px 18px; border-left: 1px solid var(--cc-border); }
@@ -718,7 +695,10 @@ defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain,
.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-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-data-grid :deep(.el-input__wrapper) { border-radius: 3px; }
.measurement-readonly-field { display: grid; min-width: 0; align-content: start; gap: 7px; }
.measurement-readonly-field span { color: #344054; font-size: 12px; font-weight: 600; }
.measurement-readonly-field strong { min-height: 32px; padding: 7px 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 13px; line-height: 18px; }
.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; }
+3 -3
View File
@@ -11,9 +11,6 @@ vi.mock('@/shared/api/http', () => ({
const draft: SaveInboundMeasurementDraftRequest = {
measurementId: '9001',
version: 2,
originalDocumentNo: 'DOC-01',
originalGrossWeightKg: 120,
originalVolumeCbm: 1.2,
actualGoodsName: '蓝牙音箱',
items: [{
boxCode: 'RK202608040001#10-1',
@@ -62,6 +59,9 @@ describe('inbound measurement API', () => {
draft,
{ params: { tenantId: '10001' } },
)
expect(draft).not.toHaveProperty('originalDocumentNo')
expect(draft).not.toHaveProperty('originalGrossWeightKg')
expect(draft).not.toHaveProperty('originalVolumeCbm')
})
it('submits the server measurement id and optimistic-lock version', async () => {
+1 -3
View File
@@ -84,6 +84,7 @@ export interface InboundMeasurementDetail {
originalPackageCount: number | null
actualPackageCount: number | null
actualGrossWeightKg: number | null
actualNetWeightKg: number | null
actualVolumeCbm: number | null
volumeWeightKg: number | null
actualGoodsName: string | null
@@ -113,9 +114,6 @@ export interface SaveInboundMeasurementItemRequest {
export interface SaveInboundMeasurementDraftRequest {
measurementId: MeasurementIdentifier | null
version: number
originalDocumentNo: string | null
originalGrossWeightKg: number | null
originalVolumeCbm: number | null
actualGoodsName: string | null
items: SaveInboundMeasurementItemRequest[]
}