优化
This commit is contained in:
@@ -161,6 +161,16 @@ describe('InboundFeePanel', () => {
|
||||
expect(document.body.textContent).toContain('没有查看入库费用的权限')
|
||||
})
|
||||
|
||||
it.each(['', ' ', 'null', 'undefined'])('does not request a fee for invalid inboundId %j', async (inboundId) => {
|
||||
const pinia = setupPermissions(['firstleg:fba-pricing:view'])
|
||||
mount(InboundFeePanel, {
|
||||
props: { customerId: '10001', inboundId },
|
||||
global: { plugins: [pinia, ElementPlus] },
|
||||
})
|
||||
await flushPromises()
|
||||
expect(getLatestInboundFee).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('treats A0404 as a neutral not-yet-generated state', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockRejectedValue(new ApiResponseError('费用快照不存在', 'A0404'))
|
||||
mountPanel(['firstleg:fba-pricing:view'])
|
||||
|
||||
@@ -66,6 +66,10 @@ const adjustmentRules: FormRules = {
|
||||
const canView = computed(() => access.can('firstleg:fba-pricing:view'))
|
||||
const canAdjust = computed(() => access.can('firstleg:fba-pricing:adjust'))
|
||||
const customerContextId = computed(() => props.customerId || props.tenantId || '')
|
||||
const normalizedInboundId = computed(() => {
|
||||
const identifier = String(props.inboundId ?? '').trim()
|
||||
return identifier && identifier !== 'null' && identifier !== 'undefined' ? identifier : ''
|
||||
})
|
||||
const adjustmentEligible = computed(() => (
|
||||
snapshot.value?.feeStatus === 'CALCULATED'
|
||||
&& snapshot.value?.nextProcessStatus === 'WAIT_SPLIT'
|
||||
@@ -126,14 +130,14 @@ async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
|
||||
const sequence = ++loadSequence
|
||||
loadError.value = null
|
||||
notFound.value = false
|
||||
if (!customerContextId.value || !props.inboundId || !canView.value) {
|
||||
if (!customerContextId.value || !normalizedInboundId.value || !canView.value) {
|
||||
snapshot.value = null
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getLatestInboundFee(customerContextId.value, props.inboundId)
|
||||
const result = await getLatestInboundFee(customerContextId.value, normalizedInboundId.value)
|
||||
if (sequence !== loadSequence) return
|
||||
snapshot.value = result
|
||||
emit('loaded', result)
|
||||
@@ -197,7 +201,7 @@ async function submitAdjustment() {
|
||||
try {
|
||||
const result = await adjustPublishedInboundFee(
|
||||
customerContextId.value,
|
||||
props.inboundId,
|
||||
normalizedInboundId.value,
|
||||
payload,
|
||||
getAdjustmentKey(payload),
|
||||
)
|
||||
@@ -219,7 +223,7 @@ async function submitAdjustment() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [customerContextId.value, props.inboundId, access.permissionSignature],
|
||||
() => [customerContextId.value, normalizedInboundId.value, access.permissionSignature],
|
||||
() => {
|
||||
adjustOpen.value = false
|
||||
snapshot.value = null
|
||||
@@ -247,7 +251,7 @@ defineExpose({ reload: loadFee })
|
||||
circle
|
||||
aria-label="刷新费用快照"
|
||||
:loading="loading"
|
||||
:disabled="!customerContextId || !inboundId || !canView"
|
||||
:disabled="!customerContextId || !normalizedInboundId || !canView"
|
||||
@click="loadFee()"
|
||||
><RefreshCw :size="16" /></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
@@ -23,6 +23,11 @@ const snapshot = {
|
||||
describe('inbound fee API', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it.each(['', ' ', 'null', 'undefined'])('rejects invalid inboundId %j without issuing HTTP', async (inboundId) => {
|
||||
await expect(getLatestInboundFee('10001', inboundId)).rejects.toThrow('inboundId is required')
|
||||
expect(http.get).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('loads the latest immutable fee snapshot with string IDs', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: snapshot })
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { AdjustPublishedInboundFeeRequest, InboundFeeSnapshot } from './types'
|
||||
|
||||
function feeBasePath(inboundId: string) {
|
||||
return `/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees`
|
||||
const identifier = String(inboundId ?? '').trim()
|
||||
if (!identifier || identifier === 'null' || identifier === 'undefined') throw new TypeError('inboundId is required')
|
||||
return `/platform/v1/inbounds/${encodeURIComponent(identifier)}/fees`
|
||||
}
|
||||
|
||||
export async function getLatestInboundFee(tenantId: string, inboundId: string) {
|
||||
|
||||
@@ -87,7 +87,7 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
|
||||
volumeWeightKg: 100.2,
|
||||
intelligentImageFileId: '88001',
|
||||
machineNo: 'MJ20210327',
|
||||
measuredAt: '2026-08-04T10:30:00+08:00',
|
||||
deviceMeasuredAt: '2026-08-04T10:30:00+08:00',
|
||||
overLimitWarning: null,
|
||||
outerLengthCm: null,
|
||||
outerWidthCm: null,
|
||||
@@ -199,10 +199,13 @@ describe('InboundMeasurementPanel', () => {
|
||||
const text = wrapper.text()
|
||||
|
||||
expect(text).toContain('实测箱数')
|
||||
expect(text).toContain('货物重量')
|
||||
expect(text).toContain('货物净重')
|
||||
expect(text).toContain('总体积')
|
||||
expect(text).toContain('材积重')
|
||||
expect(text).toContain('申报信息')
|
||||
expect(text).toContain('客户申报')
|
||||
expect(text).toContain('平台实测')
|
||||
expect(text).toContain('差异 +1.5 kg')
|
||||
expect(text).toContain('差异 -0.2 m³')
|
||||
expect(text).toContain('箱规明细')
|
||||
expect(text).not.toContain('租户')
|
||||
expect(text).not.toContain('数据版本')
|
||||
@@ -210,6 +213,8 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(wrapper.find('.measurement-table').exists()).toBe(true)
|
||||
expect(wrapper.find('.measurement-item').exists()).toBe(false)
|
||||
expect(wrapper.find('.measurement-table').text()).not.toContain('周长')
|
||||
expect(wrapper.text()).not.toContain('每箱毛重')
|
||||
expect(wrapper.text()).not.toContain('总重量(kg)')
|
||||
const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default)
|
||||
expect(source).toContain('<section class="measurement-declaration"')
|
||||
expect(source).not.toContain('<el-collapse-item name="declaration"')
|
||||
@@ -237,6 +242,12 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(declaration.find('.measurement-goods-name input').exists()).toBe(true)
|
||||
})
|
||||
|
||||
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('差异')
|
||||
})
|
||||
|
||||
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()
|
||||
@@ -396,6 +407,16 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(wrapper.emitted('updated')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('maps the formal deviceMeasuredAt response field into the read-only measurement time', async () => {
|
||||
const wrapper = await mountPanel()
|
||||
await wrapper.get('.el-table__expand-icon').trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('2026-08-04 10:30:00+08:00')
|
||||
const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default)
|
||||
expect(source).toContain('measuredAt: item.deviceMeasuredAt')
|
||||
expect(source).not.toContain('measuredAt: item.measuredAt')
|
||||
})
|
||||
|
||||
it('blocks normal submission when confirmed box counts do not match the order total', async () => {
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ originalPackageCount: 12 }))
|
||||
const wrapper = await mountPanel()
|
||||
|
||||
@@ -214,7 +214,7 @@ function editableItem(item: InboundMeasurementItem): EditableMeasurementItem {
|
||||
unitWeightKg: item.unitWeightKg,
|
||||
intelligentImageFileId: item.intelligentImageFileId == null ? null : String(item.intelligentImageFileId),
|
||||
machineNo: item.machineNo,
|
||||
measuredAt: item.measuredAt,
|
||||
measuredAt: item.deviceMeasuredAt,
|
||||
overLimitWarning: item.overLimitWarning,
|
||||
outerLengthCm: item.outerLengthCm,
|
||||
outerWidthCm: item.outerWidthCm,
|
||||
@@ -398,6 +398,14 @@ function formatNumber(value: number | null | undefined, maximumFractionDigits =
|
||||
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits }).format(value)
|
||||
}
|
||||
|
||||
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
|
||||
const difference = actual - declared
|
||||
if (difference === 0) return null
|
||||
const sign = difference > 0 ? '+' : ''
|
||||
return `差异 ${sign}${formatNumber(difference, maximumFractionDigits)}${unit}`
|
||||
}
|
||||
|
||||
function releaseImagePreview() {
|
||||
if (imagePreviewUrl.value) URL.revokeObjectURL(imagePreviewUrl.value)
|
||||
imagePreviewUrl.value = ''
|
||||
@@ -510,19 +518,20 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<dl class="measurement-metrics" aria-label="测量结果汇总">
|
||||
<h4 class="measurement-subheading">平台实测</h4>
|
||||
<dl class="measurement-metrics" aria-label="平台实测结果汇总">
|
||||
<div :class="{ 'measurement-metric--warning': !packageCountMatched }">
|
||||
<dt>实测箱数</dt>
|
||||
<dd>{{ formatNumber(confirmedPackageCount, 0) }} <small>/ 申报 {{ formatNumber(expectedPackageCount, 0) }}</small></dd>
|
||||
<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</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.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>
|
||||
|
||||
<section class="measurement-declaration" aria-labelledby="measurement-declaration-title">
|
||||
<h4 id="measurement-declaration-title">申报信息</h4>
|
||||
<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>
|
||||
@@ -554,7 +563,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
<dl class="measurement-row-details">
|
||||
<div><dt>机器编号</dt><dd>{{ row.machineNo || '人工录入' }}</dd></div>
|
||||
<div><dt>测量时间</dt><dd>{{ row.measuredAt ? row.measuredAt.replace('T', ' ') : '—' }}</dd></div>
|
||||
<div><dt>每箱毛重</dt><dd>{{ formatNumber(row.unitWeightKg) }} kg</dd></div>
|
||||
<div><dt>单箱体积</dt><dd>{{ formatNumber(row.serverValues.unitVolumeCbm, 4) }} m³</dd></div>
|
||||
<div><dt>箱规周长</dt><dd>{{ formatNumber(row.serverValues.girthCm) }} cm</dd></div>
|
||||
<div><dt>总材积重</dt><dd>{{ formatNumber(row.serverValues.volumeWeightKg) }} kg</dd></div>
|
||||
@@ -583,9 +591,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总重量(kg)" width="112" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.serverValues.totalWeightKg) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总体积(m³)" width="108" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.serverValues.totalVolumeCbm, 4) }}</template>
|
||||
</el-table-column>
|
||||
@@ -679,6 +684,7 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
.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-subheading { margin: 0; color: #344054; font-size: 12px; font-weight: 700; }
|
||||
.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); }
|
||||
.measurement-metrics > div:first-child { padding-left: 0; border-left: 0; }
|
||||
.measurement-metrics dt { color: var(--cc-muted); font-size: 12px; }
|
||||
|
||||
@@ -34,7 +34,7 @@ function segment(value: string) {
|
||||
|
||||
export async function getInboundMeasurement(customerId: string, inboundId: string) {
|
||||
// Device-scanned fields expected from the backend: originalPackageCount and
|
||||
// items[].boxCode, machineNo, measuredAt. Keep this compatibility mapping here.
|
||||
// items[].boxCode, machineNo, deviceMeasuredAt. Keep the response aligned with the backend OpenAPI.
|
||||
const response = await http.get<ApiEnvelope<InboundMeasurementDetail>, ApiEnvelope<InboundMeasurementDetail>>(
|
||||
measurementPath(inboundId),
|
||||
{ params: { tenantId: customerId } },
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface InboundMeasurementItem {
|
||||
volumeWeightKg: number | null
|
||||
intelligentImageFileId: MeasurementIdentifier | null
|
||||
machineNo: string | null
|
||||
measuredAt: string | null
|
||||
deviceMeasuredAt: string | null
|
||||
overLimitWarning: string | null
|
||||
outerLengthCm: number | null
|
||||
outerWidthCm: number | null
|
||||
|
||||
@@ -23,6 +23,15 @@ vi.mock('../weight/components/WeightOcrPanel.vue', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../measurement/components/MeasurementEvidenceThumbnail.vue', () => ({
|
||||
default: {
|
||||
name: 'MeasurementEvidenceThumbnail',
|
||||
props: { customerId: String, inboundId: String, fileId: String, alt: String, emptyText: String },
|
||||
emits: ['open'],
|
||||
template: '<button class="mock-measurement-image" @click="$emit(\'open\')">测量图片</button>',
|
||||
},
|
||||
}))
|
||||
|
||||
const detail: MeasurementReviewDetail = {
|
||||
inboundId: '90001', inboundCode: 'RK90001', companyName: '柏悦客户', channelCode: 'SEA', channelName: '海运',
|
||||
processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING', recommendedPalletType: 'STANDARD', reviewReason: null, reviewerId: null, reviewerName: null,
|
||||
@@ -71,6 +80,8 @@ describe('MeasurementReviewPanel', () => {
|
||||
expect(wrapper.text()).not.toContain('入仓码')
|
||||
expect(wrapper.text()).not.toContain('驳回')
|
||||
expect(wrapper.text()).not.toContain('异常放行')
|
||||
expect(wrapper.text()).not.toContain('审核历史')
|
||||
expect(listMeasurementReviewHistory).not.toHaveBeenCalled()
|
||||
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
|
||||
expect(source).not.toContain('position: sticky')
|
||||
expect(source).toContain('review-toolbar__context')
|
||||
@@ -78,6 +89,7 @@ describe('MeasurementReviewPanel', () => {
|
||||
expect(source).not.toContain('reasonDialog')
|
||||
expect(source).not.toContain('saveInboundMeasurementDraft')
|
||||
expect(source).not.toContain('submitInboundMeasurement')
|
||||
expect(source).not.toContain('listMeasurementReviewHistory')
|
||||
})
|
||||
|
||||
it('uses the list customer context when the review detail has no customer name', async () => {
|
||||
@@ -91,6 +103,73 @@ describe('MeasurementReviewPanel', () => {
|
||||
expect(wrapper.get('.review-toolbar__context').text()).toBe('客户:欧阳的公司 · 渠道:海运')
|
||||
})
|
||||
|
||||
it('shows declared facts, authoritative measurement totals, and box-detail evidence for manual review', async () => {
|
||||
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({
|
||||
...detail,
|
||||
originalPackageCount: 9,
|
||||
originalGrossWeightKg: 118,
|
||||
originalVolumeCbm: 2,
|
||||
actualPackageCount: 10,
|
||||
actualGrossWeightKg: 120,
|
||||
actualNetWeightKg: 110,
|
||||
actualVolumeCbm: 2.2,
|
||||
volumeWeightKg: 367.4,
|
||||
items: [{
|
||||
id: 'item-1', boxCode: 'BOX-001', itemNo: '1', boxSpecification: null, packageCount: 10,
|
||||
lengthCm: 60, widthCm: 40, heightCm: 30, girthCm: 200, unitWeightKg: 12,
|
||||
unitVolumeCbm: 0.072, totalWeightKg: 120, totalVolumeCbm: 0.72, volumeWeightKg: 120.2,
|
||||
intelligentImageFileId: '88001', machineNo: 'DEVICE-01', deviceMeasuredAt: '2026-08-18T10:30:00',
|
||||
overLimitWarning: null, outerLengthCm: null, outerWidthCm: null, outerHeightCm: null, remark: null,
|
||||
}],
|
||||
})
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
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 items = wrapper.get('.review-items').text()
|
||||
expect(items).toContain('箱规测量明细')
|
||||
expect(items).toContain('BOX-001')
|
||||
expect(items).toContain('60 × 40 × 30')
|
||||
expect(items).toContain('120.2')
|
||||
expect(items).toContain('2026-08-18 10:30:00')
|
||||
expect(items).not.toContain('每箱毛重(kg)')
|
||||
expect(items).not.toContain('总重量(kg)')
|
||||
const image = wrapper.getComponent({ name: 'MeasurementEvidenceThumbnail' })
|
||||
expect(image.props('customerId')).toBe('10001')
|
||||
expect(image.props('inboundId')).toBe('90001')
|
||||
expect(image.props('fileId')).toBe('88001')
|
||||
|
||||
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
|
||||
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).toContain('.review-table :deep(.review-time-cell .cell) { white-space: nowrap; }')
|
||||
})
|
||||
|
||||
it('keeps difference labels absent when declared values are unavailable', async () => {
|
||||
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, originalGrossWeightKg: null, originalVolumeCbm: null })
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('.review-comparison').text()).not.toContain('差异')
|
||||
})
|
||||
|
||||
it('reuses WeightOcrPanel and enables only its review correction mode', async () => {
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
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 StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { downloadInboundMeasurementImage } from '../measurement/api'
|
||||
import MeasurementEvidenceThumbnail from '../measurement/components/MeasurementEvidenceThumbnail.vue'
|
||||
import { inboundReviewStatusLabels, inboundStatusLabel } from '../status'
|
||||
import WeightOcrPanel from '../weight/components/WeightOcrPanel.vue'
|
||||
import { approveMeasurement, getMeasurementReviewDetail, listMeasurementReviewHistory } from './api'
|
||||
import type { MeasurementReviewDetail, MeasurementReviewRecord } from './types'
|
||||
import { approveMeasurement, getMeasurementReviewDetail } from './api'
|
||||
import type { MeasurementReviewDetail } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
customerId?: string
|
||||
@@ -25,14 +27,17 @@ const emit = defineEmits<{ changed: [] }>()
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const detail = ref<MeasurementReviewDetail | null>(null)
|
||||
const history = ref<MeasurementReviewRecord[]>([])
|
||||
const loading = ref(false)
|
||||
const historyLoading = ref(false)
|
||||
const actionLoading = ref('')
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const historyError = ref<ApiErrorDetails | null>(null)
|
||||
const correctionMode = ref(false)
|
||||
const requestSerial = ref(0)
|
||||
const imagePreviewOpen = ref(false)
|
||||
const imagePreviewTitle = ref('测量图片')
|
||||
const imagePreviewUrl = ref('')
|
||||
const imagePreviewLoading = ref(false)
|
||||
const imagePreviewError = ref<ApiErrorDetails | null>(null)
|
||||
let imagePreviewRequestSerial = 0
|
||||
|
||||
const canReview = computed(() => access.can('firstleg:inbound:measurement-review'))
|
||||
const customerId = computed(() => props.customerId || props.tenantId || '')
|
||||
@@ -41,15 +46,56 @@ const isRejected = computed(() => detail.value?.measureReviewStatus === 'REJECTE
|
||||
const reviewStatusLabel = computed(() => inboundStatusLabel(detail.value?.measureReviewStatus, inboundReviewStatusLabels, '未提交'))
|
||||
const displayCustomerName = computed(() => detail.value?.companyName?.trim() || props.customerName?.trim() || '客户未命名')
|
||||
const displayChannelName = computed(() => detail.value?.channelName?.trim() || detail.value?.channelCode?.trim() || props.channelName?.trim() || props.channelCode?.trim() || '渠道未填写')
|
||||
const sourceLabels: Record<string, string> = {
|
||||
THIRD_PARTY: '第三方设备',
|
||||
DEVICE: '第三方设备',
|
||||
OPENAPI: '第三方设备',
|
||||
MANUAL: '平台人工',
|
||||
}
|
||||
const sourceLabel = computed(() => sourceLabels[detail.value?.sourceType || ''] || (detail.value?.sourceType ? '未知来源' : '尚未记录'))
|
||||
|
||||
function formatNumber(value: number | null | undefined, maximumFractionDigits = 3) {
|
||||
if (value === null || value === undefined || !Number.isFinite(value)) return '—'
|
||||
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits, minimumFractionDigits: 0 }).format(value)
|
||||
}
|
||||
|
||||
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
|
||||
const difference = actual - declared
|
||||
if (difference === 0) return null
|
||||
const sign = difference > 0 ? '+' : ''
|
||||
return `差异 ${sign}${formatNumber(difference, maximumFractionDigits)}${unit}`
|
||||
}
|
||||
|
||||
function formatDate(value: string | null | undefined) {
|
||||
return value ? value.replace('T', ' ') : '—'
|
||||
}
|
||||
|
||||
function resultLabel(value: string | null | undefined) {
|
||||
if (value === 'APPROVED') return '审核通过'
|
||||
if (value === 'REJECTED') return '审核驳回'
|
||||
return '未知结果'
|
||||
function releaseImagePreview() {
|
||||
imagePreviewRequestSerial += 1
|
||||
if (imagePreviewUrl.value) URL.revokeObjectURL(imagePreviewUrl.value)
|
||||
imagePreviewUrl.value = ''
|
||||
imagePreviewLoading.value = false
|
||||
imagePreviewError.value = null
|
||||
}
|
||||
|
||||
async function openImagePreview(fileId: string | null, itemLabel: string) {
|
||||
if (!fileId) return
|
||||
releaseImagePreview()
|
||||
const serial = ++imagePreviewRequestSerial
|
||||
imagePreviewTitle.value = `测量图片 · ${itemLabel}`
|
||||
imagePreviewOpen.value = true
|
||||
imagePreviewLoading.value = true
|
||||
try {
|
||||
const blob = await downloadInboundMeasurementImage(customerId.value, props.inboundId, fileId)
|
||||
if (serial !== imagePreviewRequestSerial) return
|
||||
imagePreviewUrl.value = URL.createObjectURL(blob)
|
||||
} catch (error) {
|
||||
if (serial !== imagePreviewRequestSerial) return
|
||||
imagePreviewError.value = getApiErrorDetails(error, '测量原图读取失败,请稍后重试')
|
||||
} finally {
|
||||
if (serial === imagePreviewRequestSerial) imagePreviewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function isConflict(error: unknown) {
|
||||
@@ -74,21 +120,8 @@ async function loadReview() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
historyError.value = null
|
||||
historyLoading.value = true
|
||||
try {
|
||||
history.value = (await listMeasurementReviewHistory(customerId.value, props.inboundId)).records
|
||||
} catch (error) {
|
||||
history.value = []
|
||||
historyError.value = getApiErrorDetails(error, '审核历史加载失败')
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
await Promise.all([loadReview(), loadHistory()])
|
||||
await loadReview()
|
||||
}
|
||||
|
||||
async function handleWeightChanged() {
|
||||
@@ -132,6 +165,7 @@ watch(() => [customerId.value, props.inboundId], () => {
|
||||
onMounted(() => {
|
||||
if (!customerId.value || !props.inboundId) loadError.value = { message: '缺少所属企业或入库单标识,无法加载审核数据' }
|
||||
})
|
||||
onBeforeUnmount(releaseImagePreview)
|
||||
|
||||
defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
</script>
|
||||
@@ -150,7 +184,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
<div class="review-toolbar__actions">
|
||||
<el-button v-if="isPending && canReview" type="primary" plain @click="correctionMode = true"><Pencil :size="16" />修改重量</el-button>
|
||||
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" />通过并进入待分柜</el-button>
|
||||
<el-button :loading="loading || historyLoading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
|
||||
<el-button :loading="loading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,6 +197,84 @@ 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">
|
||||
<div>
|
||||
<h3 id="review-data-title">审核数据总览</h3>
|
||||
</div>
|
||||
<el-tag effect="plain">{{ sourceLabel }}</el-tag>
|
||||
</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) }} <small v-if="formatDifference(detail.actualPackageCount, detail.originalPackageCount, 0)">{{ formatDifference(detail.actualPackageCount, detail.originalPackageCount, 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="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>
|
||||
|
||||
<section class="review-items" aria-labelledby="review-items-title">
|
||||
<header class="review-section-heading">
|
||||
<div>
|
||||
<h3 id="review-items-title">箱规测量明细</h3>
|
||||
</div>
|
||||
<span>{{ detail.items.length }} 条</span>
|
||||
</header>
|
||||
<div v-if="detail.items.length" class="review-table-scroll">
|
||||
<el-table :data="detail.items" border stripe table-layout="fixed" class="review-table">
|
||||
<el-table-column type="index" label="#" width="44" align="center" />
|
||||
<el-table-column label="箱号" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.boxCode || row.itemNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="测量图片" width="98" align="center">
|
||||
<template #default="{ row }">
|
||||
<MeasurementEvidenceThumbnail
|
||||
:customer-id="customerId"
|
||||
:inbound-id="inboundId"
|
||||
:file-id="row.intelligentImageFileId"
|
||||
:alt="`箱规 ${row.boxCode || row.itemNo || row.id} 的测量图片`"
|
||||
empty-text="无图片"
|
||||
@open="openImagePreview(row.intelligentImageFileId, row.boxCode || row.itemNo || row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="尺寸 长×宽×高(cm)" width="132" align="center">
|
||||
<template #default="{ row }">{{ formatNumber(row.lengthCm) }} × {{ formatNumber(row.widthCm) }} × {{ formatNumber(row.heightCm) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="packageCount" label="箱数" width="54" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.packageCount, 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单箱体积(m³)" width="106" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.unitVolumeCbm, 4) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总体积(m³)" width="96" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.totalVolumeCbm, 4) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="体积重(kg)" width="96" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.volumeWeightKg) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="测量时间" width="152" class-name="review-time-cell">
|
||||
<template #default="{ row }">{{ formatDate(row.deviceMeasuredAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-empty v-else description="暂无箱规测量明细" :image-size="48" />
|
||||
</section>
|
||||
|
||||
<WeightOcrPanel
|
||||
review-mode
|
||||
:correction-mode="correctionMode"
|
||||
@@ -170,21 +282,19 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
:inbound-id="inboundId"
|
||||
@changed="handleWeightChanged"
|
||||
/>
|
||||
|
||||
<el-collapse class="review-history">
|
||||
<el-collapse-item name="history" :title="`审核历史(${history.length})`">
|
||||
<el-alert v-if="historyError" type="warning" :closable="false" show-icon><template #title><div class="review-alert-title"><span>{{ historyError.message }}</span><ApiErrorMeta :details="historyError" /></div></template><el-button link type="warning" @click="loadHistory">重试</el-button></el-alert>
|
||||
<el-skeleton v-else-if="historyLoading" :rows="3" animated />
|
||||
<el-empty v-else-if="!history.length" description="暂无审核历史" :image-size="48" />
|
||||
<ol v-else class="history-list">
|
||||
<li v-for="item in history" :key="item.id">
|
||||
<div class="history-marker" :class="{ 'history-marker--success': item.reviewResult === 'APPROVED', 'history-marker--danger': item.reviewResult === 'REJECTED' }" />
|
||||
<div class="history-content"><div><strong>{{ resultLabel(item.reviewResult) }}{{ item.exceptionRelease ? ' · 异常放行' : '' }}</strong><time>{{ formatDate(item.reviewedAt) }}</time></div><p>{{ item.releaseReason || item.reviewReason || '未填写说明' }}</p><span>{{ item.reviewerName || '系统' }}</span></div>
|
||||
</li>
|
||||
</ol>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="imagePreviewOpen" :title="imagePreviewTitle" width="min(760px, 92vw)" 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">
|
||||
<span>{{ imagePreviewError.message }}</span>
|
||||
<ApiErrorMeta :details="imagePreviewError" />
|
||||
</div>
|
||||
<img v-else-if="imagePreviewUrl" :src="imagePreviewUrl" alt="当前箱规测量原图" />
|
||||
<el-empty v-else description="暂无可预览图片" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -197,26 +307,45 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
.review-toolbar__actions :deep(.el-button) { margin-left: 0; }
|
||||
.review-toolbar__status > .review-toolbar__context { padding-left: 8px; border-left: 1px solid var(--cc-border); font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
|
||||
.review-alert-title { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.review-history { border-top: 1px solid var(--cc-border); border-bottom: 0; }
|
||||
.review-history :deep(.el-collapse-item__header) { height: 42px; color: var(--cc-muted); font-size: 12px; }
|
||||
.review-history :deep(.el-collapse-item__wrap) { border-bottom: 0; }
|
||||
.history-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.history-list li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 10px; position: relative; padding-bottom: 16px; }
|
||||
.history-list li:not(:last-child)::after { content: ''; position: absolute; top: 16px; bottom: 0; left: 7px; width: 1px; background: var(--cc-border); }
|
||||
.history-marker { z-index: 1; width: 14px; height: 14px; margin-top: 2px; border: 3px solid #909399; border-radius: 50%; background: var(--cc-surface); }
|
||||
.history-marker--success { border-color: #67c23a; }
|
||||
.history-marker--danger { border-color: #f56c6c; }
|
||||
.history-content { min-width: 0; }
|
||||
.history-content > div { display: flex; justify-content: space-between; gap: 12px; }
|
||||
.history-content time, .history-content > span { color: var(--cc-muted); font-size: 11px; }
|
||||
.history-content p { margin: 5px 0 4px; color: var(--cc-text); font-size: 13px; overflow-wrap: anywhere; }
|
||||
.review-data, .review-items { min-width: 0; padding: 0 0 12px; border-bottom: 1px solid var(--cc-border); }
|
||||
.review-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 8px; }
|
||||
.review-section-heading > div { min-width: 0; }
|
||||
.review-section-heading h3 { margin: 0; color: var(--cc-text); font-size: 14px; line-height: 1.35; }
|
||||
.review-section-heading > span { color: var(--cc-muted); font-size: 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: 8px 0; }
|
||||
.review-comparison__row + .review-comparison__row { border-top: 1px dashed var(--cc-border); }
|
||||
.review-comparison__row > strong { color: #344054; font-size: 12px; }
|
||||
.review-metrics, .review-declaration { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px 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: 6px; }
|
||||
.review-metrics dt, .review-declaration dt { color: var(--cc-muted); font-size: 11px; line-height: 1.4; }
|
||||
.review-metrics dd { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 15px; font-weight: 700; line-height: 1.35; }
|
||||
.review-metrics dd small, .review-declaration dd small { color: var(--cc-muted); font-size: 11px; 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: 13px; font-weight: 650; line-height: 1.4; }
|
||||
.review-table-scroll { width: 100%; min-width: 0; overflow: hidden; }
|
||||
.review-table { width: 100%; font-size: 12px; }
|
||||
.review-table :deep(.el-table__header th) { padding: 8px 0; background: #f6f8fa; color: #475467; font-size: 11px; font-weight: 700; }
|
||||
.review-table :deep(.el-table__header .cell) { white-space: nowrap; }
|
||||
.review-table :deep(.el-table__cell) { padding: 7px 0; }
|
||||
.review-table :deep(.review-time-cell .cell) { white-space: nowrap; }
|
||||
.review-table :deep(.measurement-evidence-thumb) { min-width: 72px; border-radius: 4px; }
|
||||
.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: 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__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-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>
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface MeasurementReviewItem {
|
||||
volumeWeightKg: number | null
|
||||
intelligentImageFileId: string | null
|
||||
machineNo: string | null
|
||||
measuredAt: string | null
|
||||
deviceMeasuredAt: string | null
|
||||
overLimitWarning: string | null
|
||||
outerLengthCm: number | null
|
||||
outerWidthCm: number | null
|
||||
|
||||
@@ -81,4 +81,9 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain('@media (max-width: 720px)')
|
||||
expect(source).toContain('@media (max-width: 360px)')
|
||||
})
|
||||
|
||||
it('keeps each weight summary label and value on one compact line', () => {
|
||||
expect(source).toContain('.weight-summary { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }')
|
||||
expect(source).not.toContain('.weight-summary { display: grid;')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -609,16 +609,16 @@ defineExpose({ reload: loadWorkspace })
|
||||
|
||||
<style scoped>
|
||||
.weight-workspace { min-width: 0; color: var(--el-text-color-primary); }
|
||||
.weight-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding-bottom: 18px; border-bottom: 1px solid var(--cc-border); }
|
||||
.weight-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding-bottom: 10px; border-bottom: 1px solid var(--cc-border); }
|
||||
.weight-heading h3 { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 15px; }
|
||||
.weight-heading h3 svg { color: var(--cc-primary); }
|
||||
.weight-heading__actions { display: flex; flex: 0 0 auto; align-items: center; gap: 16px; }
|
||||
.weight-heading__actions { display: flex; flex: 0 0 auto; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 16px; }
|
||||
.pallet-picker { width: clamp(360px, 36vw, 520px); min-width: 0; }
|
||||
.pallet-picker :deep(.el-select) { width: 100%; }
|
||||
.weight-summary { display: grid; gap: 2px; text-align: right; }
|
||||
.weight-summary-group { display: flex; align-items: center; gap: 18px; }
|
||||
.weight-summary { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
|
||||
.weight-summary-group { display: flex; align-items: center; flex-wrap: wrap; gap: 20px; }
|
||||
.weight-summary span { color: var(--cc-muted); font-size: 12px; }
|
||||
.weight-summary strong { color: #067647; font-size: 18px; }
|
||||
.weight-summary strong { color: #067647; font-size: 16px; }
|
||||
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
.weight-action-error { margin-bottom: 14px; }
|
||||
.weight-list { min-height: 0; margin-top: 14px; }
|
||||
@@ -652,7 +652,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
.pallet-picker { width: 100%; min-width: 0; }
|
||||
.pallet-picker :deep(.el-select) { width: 100%; }
|
||||
.weight-heading__actions :deep(.el-button) { width: 100%; min-height: 44px; }
|
||||
.weight-summary { text-align: left; }
|
||||
.weight-summary-group { width: 100%; justify-content: space-between; }
|
||||
.weight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user