This commit is contained in:
2026-10-08 17:08:03 +08:00
parent cc37a42298
commit 7b0df1db37
44 changed files with 574 additions and 405 deletions
@@ -104,7 +104,7 @@ function handleSubmitted() {
</div>
<template #footer>
<div class="measurement-dialog-footer">
<span v-if="measurementPanelRef?.submissionBlockReason" class="measurement-dialog-footer__hint">
<span v-if="measurementPanelRef?.submissionBlockReason" class="measurement-dialog-footer__hint" role="status">
{{ measurementPanelRef.submissionBlockReason }}
</span>
<el-button :disabled="Boolean(measurementPanelRef?.saving || measurementPanelRef?.submitting)" @click="requestClose">取消</el-button>
@@ -147,15 +147,6 @@ function handleSubmitted() {
.inbound-measurement-workspace-dialog .measurement-panel { border: 0; border-radius: 0; box-shadow: none; }
.inbound-measurement-workspace-dialog .measurement-panel__heading .measurement-panel__actions,
.inbound-measurement-workspace-dialog .measurement-panel__footer { display: none; }
.inbound-measurement-workspace-dialog .measurement-summary-section { margin: 0 -12px; padding: 10px 12px 13px; border: 1px solid #cce8df; border-left: 4px solid #20a477; border-radius: 8px; background: #f4fbf8; }
.inbound-measurement-workspace-dialog .measurement-summary-context { min-height: auto; }
.inbound-measurement-workspace-dialog .measurement-subheading { color: #18795d; font-size: 15px; }
.inbound-measurement-workspace-dialog .measurement-metrics { padding: 0; border: 1px solid #cce8df; border-radius: 8px; background: #fff; overflow: hidden; }
.inbound-measurement-workspace-dialog .measurement-metrics > div { min-height: 58px; padding: 9px 14px; border-left: 1px solid #e8ecf2; }
.inbound-measurement-workspace-dialog .measurement-metrics > div:first-child { border-left: 0; }
.inbound-measurement-workspace-dialog .measurement-declaration { margin-top: 12px; padding: 10px 12px 0; border: 1px solid #d9e2fa; border-left: 4px solid #647fec; border-radius: 8px; background: #f7f9ff; }
.inbound-measurement-workspace-dialog .measurement-items-section { margin-top: 12px; padding: 10px 12px 13px; border: 1px solid #eadfc9; border-left: 4px solid #c79643; border-radius: 8px; background: #fffbf4; }
.inbound-measurement-workspace-dialog .measurement-items-section .measurement-section__heading { margin-bottom: 8px; }
@media (max-width: 720px) {
.inbound-measurement-workspace-dialog.el-dialog { width: calc(100vw - 20px) !important; max-width: calc(100vw - 20px); }
.inbound-measurement-workspace-dialog .el-dialog__header { padding: 16px; }
@@ -266,7 +266,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('材积重')
@@ -274,7 +274,7 @@ describe('InboundMeasurementPanel', () => {
expect(text).toContain('平台实测')
expect(text).toContain('差异 +1.5 kg')
expect(text).toContain('差异 -0.2 m³')
expect(text).toContain('箱规明细')
expect(text).toContain('平台实测箱规')
expect(text).not.toContain('租户')
expect(text).not.toContain('数据版本')
@@ -287,27 +287,58 @@ describe('InboundMeasurementPanel', () => {
expect(source).toContain('<section class="measurement-declaration"')
expect(source).not.toContain('<el-collapse-item name="declaration"')
expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
expect(source).toContain('.measurement-data-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));')
expect(source).toContain('.measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));')
expect(source).toContain('.measurement-table { width: 100%; font-size: var(--admin-font-caption, 12px); }')
expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)')
expect(source).toContain('defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlockReason, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })')
})
it('aligns declared and measured values and keeps the declaration outside the measured section', async () => {
const wrapper = await mountPanel()
const rows = wrapper.findAll('.measurement-comparison tbody tr')
expect(rows.map(row => row.findAll('td').map(cell => cell.text()))).toEqual([
['10', '10', '一致'], ['118.5', '120', '差异 +1.5 kg'], ['0.8', '0.6', '差异 -0.2 m³'],
])
expect(wrapper.find('.measurement-summary-section .measurement-declaration').exists()).toBe(false)
expect(wrapper.find('.measurement-declaration').element.parentElement).toBe(wrapper.find('.measurement-summary-section').element.parentElement)
})
it('shows pending comparison rather than a negative count when no boxes have been measured', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({
items: [], actualPackageCount: null, actualGrossWeightKg: null, actualVolumeCbm: null,
}))
const wrapper = await mountPanel()
const rows = wrapper.findAll('.measurement-comparison tbody tr')
for (const row of rows) {
expect(row.findAll('td')[1].text()).toBe('未测量')
expect(row.findAll('td')[2].text()).toBe('待测量')
}
})
it('groups titles and related fields into shared header rows without duplicating submission warnings', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ measurementId: null, items: [], weightResults: [] }))
const wrapper = await mountPanel()
expect(wrapper.find('.measurement-summary-header h4').text()).toBe('申报与实测对比')
expect(wrapper.find('.measurement-declaration-header').text()).toContain('DOC-001')
expect(wrapper.find('.measurement-section__heading .measurement-goods-name input').exists()).toBe(true)
expect(wrapper.find('.measurement-section__heading .measurement-section-actions').text()).toContain('手动录入箱规')
expect(wrapper.findAll('.el-alert').some(alert => alert.text().includes('请先完成箱规测量'))).toBe(false)
expect(wrapper.find('.measurement-panel__footer').text()).toContain('请先完成箱规测量')
})
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')
const comparison = wrapper.find('.measurement-comparison')
expect(comparison.text()).toContain('118.5')
expect(comparison.text()).toContain('0.8')
expect(comparison.findAll('tbody tr')[0].text()).toContain('10')
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)
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(1)
expect(declaration.findAll('input')).toHaveLength(0)
expect(wrapper.find('.measurement-items-section .measurement-goods-name input').exists()).toBe(true)
expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('蓝牙音箱')
expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('50 × 40 × 30')
})
@@ -315,7 +346,7 @@ describe('InboundMeasurementPanel', () => {
it('does not render differences when the corresponding declaration is unavailable', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ originalGrossWeightKg: null, originalVolumeCbm: null }))
const wrapper = await mountPanel()
expect(wrapper.find('.measurement-metrics').text()).not.toContain('差异')
expect(wrapper.findAll('.measurement-comparison tbody tr').slice(1).map(row => row.findAll('td')[2].text())).toEqual(['—', '—'])
})
it('shows all declaration fields as plain text when measurement is not editable', async () => {
@@ -325,7 +356,8 @@ describe('InboundMeasurementPanel', () => {
expect(declaration.find('input').exists()).toBe(false)
expect(declaration.text()).toContain('蓝牙音箱')
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(5)
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(1)
expect(wrapper.find('.measurement-items-section .measurement-goods-name').text()).toContain('蓝牙音箱')
})
it('saves the corrected actual goods name without overriding declaration facts', async () => {
@@ -369,8 +401,8 @@ describe('InboundMeasurementPanel', () => {
wrapper.vm.updateWeightEvidence(125.75, 115.75, true)
await flushPromises()
expect(wrapper.find('.measurement-metrics').text()).toContain('125.75')
expect(wrapper.find('.measurement-metrics').text()).toContain('115.75')
expect(wrapper.find('.measurement-summary-section').text()).toContain('125.75')
expect(wrapper.find('.measurement-summary-section').text()).toContain('115.75')
expect((countInput.element as HTMLInputElement).value).toBe('9')
})
@@ -515,6 +547,35 @@ describe('InboundMeasurementPanel', () => {
expect(wrapper.emitted('changed')).toHaveLength(1)
})
it('explains missing measurement prerequisites and avoids duplicate empty source status', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({
measurementId: null, items: [], sourceType: null, weightResults: [],
}))
const wrapper = await mountPanel()
expect(wrapper.vm.maySubmit).toBe(false)
expect(wrapper.vm.submissionBlockReason).toContain('请先完成箱规测量')
expect(wrapper.vm.submissionBlockReason).toContain('称重图片')
expect(wrapper.find('.measurement-summary-status').text()).not.toContain('尚未测量')
expect(wrapper.find('.measurement-panel__footer [role="status"]').text()).toContain('请先完成箱规测量')
})
it('explains order review and submitted-state blockers before measurement prerequisites', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'PREPARING', measurementId: null }))
const pendingOrder = await mountPanel()
expect(pendingOrder.vm.submissionBlockReason).toContain('订单尚待客服审核')
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'MEASURE_PENDING' }))
const pendingMeasurement = await mountPanel()
expect(pendingMeasurement.vm.submissionBlockReason).toContain('不能提交测量审核')
})
it('keeps dropship pallet edits in the platform section and declaration facts read-only', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ fulfillmentType: 'ONE_CLICK_DROPSHIP', expectedPalletCount: 2 }))
const wrapper = await mountPanel()
expect(wrapper.find('.measurement-declaration input').exists()).toBe(false)
expect(wrapper.find('.measurement-items-section .measurement-expected-pallets input').exists()).toBe(true)
expect(wrapper.find('.measurement-items-section .measurement-goods-name input').exists()).toBe(true)
})
it('requires at least one confirmed weight image before submitting for review', async () => {
const messageSpy = vi.spyOn(ElMessage, 'warning')
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ weightResults: [] }))
@@ -119,6 +119,13 @@ const mayMaintain = computed(() => canEdit.value && countEditableProcess.value &
const manualEntryEditableProcess = computed(() => ['PREPARING', 'WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
const mayCreateManualEntry = computed(() => canManualEntry.value && manualEntryEditableProcess.value && !loading.value && !saving.value && !submitting.value && !isDirty.value)
const confirmedPackageCount = computed(() => form.items.reduce((sum, item) => sum + (Number.isFinite(item.packageCount) ? item.packageCount : 0), 0))
// No scan rows means no measured count yet, rather than a measured zero.
const comparisonMetrics = computed(() => [
{ label: '箱数', declared: detail.value?.originalPackageCount, actual: form.items.length ? confirmedPackageCount.value : null, digits: 0, unit: '' },
{ label: '毛重(kg)', declared: detail.value?.originalGrossWeightKg, actual: detail.value?.actualGrossWeightKg, digits: 3, unit: ' kg' },
{ label: '总体积(m³)', declared: detail.value?.originalVolumeCbm, actual: detail.value?.actualVolumeCbm, digits: 4, unit: ' m³' },
])
const expectedPackageCount = computed(() => detail.value?.originalPackageCount ?? null)
const packageCountDifference = computed(() => expectedPackageCount.value == null ? null : confirmedPackageCount.value - expectedPackageCount.value)
const packageCountMatched = computed(() => packageCountDifference.value === null || packageCountDifference.value === 0)
@@ -127,7 +134,10 @@ const isDropship = computed(() => detail.value?.fulfillmentType === 'ONE_CLICK_D
const expectedPalletCountValid = computed(() => !isDropship.value || (Number.isInteger(form.expectedPalletCount) && Number(form.expectedPalletCount) >= 1))
const maySubmit = computed(() => canSubmit.value && submitEditableProcess.value && Boolean(form.measurementId) && packageCountMatched.value && hasConfirmedWeightResult.value && expectedPalletCountValid.value && !loading.value && !saving.value)
const submissionBlockReason = computed(() => {
if (!canSubmit.value || !submitEditableProcess.value || !form.measurementId) return ''
if (!canSubmit.value || !detail.value || loading.value) return ''
if (detail.value.processStatus === 'PREPARING') return '订单尚待客服审核,审核通过后才能提交测量审核'
if (!submitEditableProcess.value) return `当前${processStatusLabel(detail.value.processStatus)},不能提交测量审核`
if (!form.measurementId) return hasConfirmedWeightResult.value ? '请先完成箱规测量并保存测量数据' : '请先完成箱规测量,并上传确认至少一张称重图片'
if (!hasConfirmedWeightResult.value) return '即使只有一箱,也必须上传并确认至少一张称重图片后才能提交审核'
if (!packageCountMatched.value) return '确认箱数合计必须与订单总箱数一致后才能提交审核'
if (!expectedPalletCountValid.value) return '请先填写并保存有效的预计托数后再提交审核'
@@ -675,15 +685,6 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
<el-alert v-else-if="!canEdit" type="info" :closable="false" show-icon title="当前账号仅可查看测量数据" />
<el-alert
v-if="submissionBlockReason && !hasConfirmedWeightResult"
type="warning"
:closable="false"
show-icon
class="measurement-alert"
:title="submissionBlockReason"
/>
<el-alert v-if="conflictDetected" type="warning" :closable="false" show-icon class="measurement-alert">
<template #title>数据版本已变化,当前修改尚未丢失</template>
<p>{{ operationError?.message }}</p>
@@ -699,13 +700,14 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
<legend class="sr-only">测量草稿</legend>
<section class="measurement-section measurement-summary-section">
<header class="measurement-summary-header">
<h4 class="measurement-subheading">申报与实测对比</h4>
<div class="measurement-summary-context">
<p><span>客户</span>{{ displayCustomerName }}</p>
<p><span>渠道</span>{{ detail.channelName || detail.channelCode || '渠道未填写' }}</p>
</div>
<div class="measurement-summary-status">
<el-tag effect="light">{{ processStatusLabel(detail.processStatus) }}</el-tag>
<span>{{ sourceLabel }}</span>
<span v-if="detail.sourceType && form.items.length">{{ sourceLabel }}</span>
</div>
</header>
@@ -720,57 +722,66 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
<p v-for="warning in measurementDeviationWarnings" :key="warning.key">{{ warning.text }}</p>
</el-alert>
<h4 class="measurement-subheading">平台实测</h4>
<dl class="measurement-metrics" aria-label="平台实测结果汇总">
<div :class="{ 'measurement-metric--warning': !packageCountMatched }">
<dt>实测箱数</dt>
<dd>{{ formatNumber(confirmedPackageCount, 0) }} <small v-if="formatDifference(confirmedPackageCount, expectedPackageCount, 0)">/ {{ formatDifference(confirmedPackageCount, expectedPackageCount, 0) }}</small></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="measurement-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>
<table class="measurement-comparison" aria-label="客户申报与平台实测对比">
<thead><tr><th scope="col">指标</th><th scope="col">客户申报</th><th scope="col">平台实测</th><th scope="col">差异(实测 − 申报)</th></tr></thead>
<tbody>
<tr v-for="metric in comparisonMetrics" :key="metric.label">
<th scope="row">{{ metric.label }}</th>
<td>{{ formatNumber(metric.declared, metric.digits) }}</td>
<td>{{ metric.actual == null ? '未测量' : formatNumber(metric.actual, metric.digits) }}</td>
<td :class="{ 'measurement-comparison__difference': metric.actual != null && metric.declared != null && metric.actual !== metric.declared }">
{{ metric.actual == null ? '待测量' : metric.declared == null ? '—' : formatDifference(metric.actual, metric.declared, metric.digits, metric.unit) || '一致' }}
</td>
</tr>
</tbody>
</table>
<div class="measurement-extra-facts">
<span>货物净重 <strong>{{ formatNumber(detail.actualNetWeightKg) }}</strong> kg</span>
<span>材积重 <strong>{{ formatNumber(detail.volumeWeightKg) }}</strong> kg</span>
<el-tooltip content="净重、材积重为平台数据,不与客户申报毛重比较"><span tabindex="0">平台补充数据</span></el-tooltip>
</div>
</section>
<section class="measurement-declaration" aria-labelledby="measurement-declaration-title">
<h4 id="measurement-declaration-title">客户申报</h4>
<div class="measurement-data-grid">
<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>
<label v-if="isDropship && mayMaintain"><span>预计托数</span><el-input-number v-model="form.expectedPalletCount" placeholder="请输入预计托数" :min="1" :max="1000" :step="1" step-strictly controls-position="right" /></label>
<div v-else-if="isDropship" class="measurement-readonly-field"><span>预计托数</span><strong>{{ detail.expectedPalletCount ?? '—' }}</strong></div>
<div class="measurement-readonly-field"><span>原单据号码</span><strong>{{ detail.originalDocumentNo || '—' }}</strong></div>
</div>
<el-table
v-if="detail.latestDeclaration?.items.length"
:data="detail.latestDeclaration.items"
border
stripe
size="small"
class="measurement-declaration-table"
aria-label="客户申报箱规明细"
>
<el-table-column prop="itemNo" label="#" width="70" />
<el-table-column prop="cargoName" label="货物品名" min-width="150" show-overflow-tooltip />
<el-table-column prop="packageCount" label="申报箱数" width="100" />
<el-table-column label="申报尺寸 长×宽×高(cm)" min-width="190">
<template #default="{ row }">{{ formatNumber(row.lengthCm) }} × {{ formatNumber(row.widthCm) }} × {{ formatNumber(row.heightCm) }}</template>
</el-table-column>
</el-table>
</section>
<section class="measurement-declaration" aria-labelledby="measurement-declaration-title">
<header class="measurement-declaration-header">
<h4 id="measurement-declaration-title">客户申报明细</h4>
<div class="measurement-readonly-field measurement-document-number"><span>原单据号码</span><strong>{{ detail.originalDocumentNo || '—' }}</strong></div>
</header>
<el-table
v-if="detail.latestDeclaration?.items.length"
:data="detail.latestDeclaration.items"
border
stripe
size="small"
class="measurement-declaration-table"
aria-label="客户申报箱规明细"
>
<el-table-column prop="itemNo" label="#" width="70" />
<el-table-column prop="cargoName" label="货物品名" min-width="150" show-overflow-tooltip />
<el-table-column prop="packageCount" label="申报箱数" width="100" />
<el-table-column label="申报尺寸 长×宽×高(cm)" min-width="190">
<template #default="{ row }">{{ formatNumber(row.lengthCm) }} × {{ formatNumber(row.widthCm) }} × {{ formatNumber(row.heightCm) }}</template>
</el-table-column>
</el-table>
</section>
<section class="measurement-section measurement-items-section">
<div class="measurement-section__heading">
<div><h3>箱规明细</h3></div>
<div><h3>平台实测箱规</h3></div>
<div class="measurement-data-grid measurement-platform-fields">
<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>
<label v-if="isDropship && mayMaintain" class="measurement-expected-pallets"><span>预计托数</span><el-input-number v-model="form.expectedPalletCount" placeholder="请输入预计托数" :min="1" :max="1000" :step="1" step-strictly controls-position="right" /></label>
<div v-else-if="isDropship" class="measurement-readonly-field measurement-expected-pallets"><span>预计托数</span><strong>{{ detail.expectedPalletCount ?? '—' }}</strong></div>
</div>
<div class="measurement-section-actions">
<el-button v-if="canManualEntry" type="primary" plain :disabled="!mayCreateManualEntry" @click="openManualEntry"><Plus :size="15" />手动录入箱规</el-button>
</div>
</div>
<el-table
v-if="form.items.length"
:data="form.items"
@@ -854,7 +865,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
</form>
<footer v-if="canEdit || canSubmit" class="measurement-panel__footer">
<span v-if="!hasConfirmedWeightResult"><AlertTriangle :size="15" />至少上传并确认一张称重图片后才能提交审核</span>
<span v-if="submissionBlockReason" role="status"><AlertTriangle :size="15" />{{ submissionBlockReason }}</span>
<span v-else-if="isDirty"><AlertTriangle :size="15" />有尚未保存的修改</span>
<span v-else>所有修改已保存</span>
<div>
@@ -906,6 +917,14 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
</template>
<style scoped>
.measurement-comparison { width: 100%; border-collapse: collapse; table-layout: fixed; background: var(--admin-surface, #fff); font-size: var(--admin-font-body, 13px); }
.measurement-comparison th, .measurement-comparison td { padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); border: 1px solid var(--cc-border); text-align: left; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.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-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; }
.measurement-panel__heading > div:first-child { display: flex; min-width: 0; align-items: flex-start; gap: 11px; }
.measurement-panel__icon { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 8px; background: #eef4ff; color: var(--cc-primary); }
@@ -914,6 +933,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
.measurement-panel h3 { font-size: var(--admin-font-section, 14px); }
.measurement-panel__heading p, .measurement-section__heading p { margin: var(--admin-space-1, 4px) 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); line-height: 1.55; }
.measurement-panel__actions, .measurement-panel__footer > div { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; gap: var(--admin-space-3, 8px); }
.measurement-platform-fields { margin-bottom: var(--admin-space-5, 12px); }
.measurement-declaration-table { margin-top: var(--admin-space-5, 12px); }
.measurement-panel__actions :deep(.el-button), .measurement-panel__footer :deep(.el-button) { margin-left: 0; }
.measurement-skeleton { padding: var(--admin-space-7, 24px); }
@@ -931,15 +951,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
.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; }
.measurement-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 2px 0; }
.measurement-subheading { margin: 0; color: #344054; font-size: var(--admin-font-caption, 12px); font-weight: 700; }
.measurement-metrics > div { display: grid; min-width: 0; min-height: 58px; align-content: center; gap: var(--admin-space-1, 4px); padding: 5px 18px; border-left: 1px solid var(--cc-border); }
.measurement-metrics > div:first-child { padding-left: 0; border-left: 0; }
.measurement-metrics dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.measurement-metrics dd { min-width: 0; margin: 0; color: var(--cc-text); font-size: 18px; font-weight: 700; overflow-wrap: anywhere; }
.measurement-metrics small { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); font-weight: 500; }
.measurement-metric--primary dd { color: var(--cc-primary); font-size: 20px; }
.measurement-metric--warning dd { color: var(--el-color-warning-dark-2); }
.measurement-declaration { padding-top: var(--admin-space-5, 12px); border-top: 1px solid var(--cc-border); }
.measurement-declaration h4 { margin: 0 0 var(--admin-space-4, 10px); color: #344054; font-size: var(--admin-font-caption, 12px); font-weight: 700; }
.measurement-section__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: var(--admin-space-6, 16px); }
@@ -947,7 +959,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
.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(5, minmax(0, 1fr)); gap: var(--admin-space-5, 12px); }
.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%; }
.measurement-data-grid label { display: flex; min-width: 0; flex-direction: column; gap: 5px; color: #344054; font-size: var(--admin-font-caption, 12px); font-weight: 600; }
@@ -982,8 +994,6 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
.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)); }
.measurement-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.measurement-metrics > div:nth-child(3) { padding-left: 0; border-left: 0; }
}
@media (max-width: 720px) {
@@ -1,15 +1,38 @@
/* 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-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-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; }
@media (min-width: 961px) {
.inbound-density.admin-density-dialog .measurement-dialog-weight { padding-bottom: var(--admin-space-4, 10px); }
.inbound-density.admin-density-dialog .measurement-section { padding: var(--admin-space-4, 10px) 0; }
.inbound-density.admin-density-dialog .measurement-section { padding-block: var(--admin-space-4, 10px); }
.inbound-density.admin-density-dialog .measurement-summary-section { gap: var(--admin-space-3, 8px); }
.inbound-density.admin-density-dialog .measurement-section__heading { align-items: center; margin-bottom: var(--admin-space-3, 8px); }
.inbound-density.admin-density-dialog .measurement-metrics { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.inbound-density.admin-density-dialog .measurement-metrics > div { min-height: var(--admin-row-height, 36px); padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); display: flex; align-items: baseline; gap: var(--admin-space-3, 8px); }
.inbound-density.admin-density-dialog .measurement-metrics dd { font-size: var(--admin-font-section, 14px); }
.inbound-density.admin-density-dialog .measurement-metrics small { font-size: var(--admin-font-caption, 12px); }
.inbound-density.admin-density-dialog .measurement-data-grid { gap: var(--admin-space-3, 8px) var(--admin-space-5, 12px); }
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-header h4,
.inbound-measurement-workspace-dialog.inbound-density .measurement-declaration-header h4 { flex-shrink: 0; margin: 0; }
.inbound-measurement-workspace-dialog.inbound-density .measurement-declaration-header { display: flex; align-items: baseline; gap: var(--admin-space-6, 16px); }
.inbound-measurement-workspace-dialog.inbound-density .measurement-document-number { display: flex; flex: 1; align-items: baseline; gap: var(--admin-space-3, 8px); }
.inbound-measurement-workspace-dialog.inbound-density .measurement-document-number > span { flex-shrink: 0; }
.inbound-measurement-workspace-dialog.inbound-density .measurement-document-number strong { min-width: 0; min-height: 0; padding: 0; }
.inbound-measurement-workspace-dialog.inbound-density .measurement-items-section .measurement-section__heading { flex-wrap: wrap; gap: var(--admin-space-3, 8px) var(--admin-space-6, 16px); }
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields { display: flex; flex: 1 1 520px; align-items: center; flex-wrap: wrap; margin: 0; gap: var(--admin-space-5, 12px); }
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields > label,
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields > .measurement-readonly-field { display: flex; flex-direction: row; align-items: center; gap: var(--admin-space-3, 8px); }
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields > .measurement-goods-name { flex: 1 1 340px; }
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields > .measurement-expected-pallets { flex: 0 0 180px; }
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields > * > span { flex-shrink: 0; }
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields .el-input,
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields .el-input-number { min-width: 0; flex: 1; width: auto; }
.inbound-measurement-workspace-dialog.inbound-density .measurement-section-actions { margin-left: auto; }
.inbound-density.admin-density-dialog .measurement-declaration { padding-top: var(--admin-space-3, 8px); }
.inbound-density.admin-density-dialog .measurement-declaration h4 { margin-bottom: var(--admin-space-2, 6px); font-size: var(--admin-font-body, 13px); }
.inbound-density.admin-density-dialog .measurement-declaration h4 { font-size: var(--admin-font-body, 13px); }
.inbound-density.admin-density-dialog .measurement-panel > .el-alert { margin: var(--admin-space-3, 8px) 0; }
.inbound-density.admin-density-dialog .measurement-summary-header span { font-size: var(--admin-font-caption, 12px); }
.inbound-density.admin-density-dialog .measurement-table .el-table__header th { padding: var(--admin-space-1, 4px) 0; font-size: var(--admin-font-body, 13px); }