This commit is contained in:
2026-10-22 11:56:54 +08:00
parent 090431b409
commit 3e77c3976e
40 changed files with 976 additions and 241 deletions
@@ -190,6 +190,28 @@ describe('UnifiedOrdersView list request coordination', () => {
}
})
it.each(['全部', '测量数据审核'])('keeps the %s tab bar mounted without a loading text row during query', async (label) => {
const wrapper = mountView()
await flushPromises()
if (label !== '全部') {
await wrapper.findAll('[role="tab"]').find(tab => tab.text() === label)!.trigger('click')
await flushPromises()
}
const tabList = wrapper.get('[role="tablist"]').element
let resolveCounts!: (value: { counts: {} }) => void
vi.mocked(getPlatformUnifiedOrderStatusCounts).mockImplementationOnce(() => new Promise(resolve => { resolveCounts = resolve }))
wrapper.getComponent({ name: 'QueryToolbar' }).vm.$emit('search')
await flushPromises()
expect(wrapper.get('[role="tablist"]').element).toBe(tabList)
expect(wrapper.get('[role="tablist"]').attributes('aria-busy')).toBe('true')
expect(wrapper.find('.order-status-feedback').exists()).toBe(false)
resolveCounts({ counts: {} })
await flushPromises()
expect(wrapper.get('[role="tablist"]').element).toBe(tabList)
expect(wrapper.get('[role="tablist"]').attributes('aria-busy')).toBe('false')
wrapper.unmount()
})
it.each(['success', 'failure'] as const)('ignores an older tab request %s after the current tab has loaded', async (outcome) => {
const wrapper = mountView()
await flushPromises()
@@ -565,7 +565,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('label="尺寸图片"')
expect(source).toContain(':file-id="row.intelligentImageFileId"')
expect(source).toContain('openMeasurementEvidence(row.intelligentImageFileId')
expect(source).toContain('v-for="weightResult in measurementDetail.weightResults"')
expect(source).toContain('<el-table :data="measurementDetail?.weightResults ?? []" row-key="id" border stripe empty-text="暂无称重图片与确认结果">')
expect(source).toContain(':file-id="weightResult.fileId"')
expect(source).toContain('openMeasurementEvidence(weightResult.fileId')
expect(source).toContain('measurementDetail?.actualGrossWeightKg')
@@ -2732,30 +2732,37 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</el-table>
</div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).volume) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatMeasurementVolume(measurementTotals(actualMeasurementRows).weightVolume) }}</span></div>
<section class="weight-evidence" aria-label="称重图片与服务端确认重量">
<div class="weight-evidence__heading">
<div><h4>称重图片与确认重量</h4></div>
<dl><dt>实际总毛重</dt><dd>{{ formatNumber(measurementDetail?.actualGrossWeightKg) }} kg</dd></dl>
<section class="measurement-section" aria-label="称重图片与服务端确认重量">
<div class="measurement-section-title"><h3>称重图片与确认重量</h3></div>
<div class="detail-table-wrap">
<el-table :data="measurementDetail?.weightResults ?? []" row-key="id" border stripe empty-text="暂无称重图片与确认结果">
<el-table-column label="称重图片" width="148">
<template #default="{ row: weightResult }">
<MeasurementEvidenceThumbnail
:customer-id="String(detail.summary.tenantId)"
:inbound-id="String(detail.summary.inboundId)"
:file-id="weightResult.fileId"
:alt="`称重图片 · ${weightResult.palletType || '未记录类型'}`"
empty-text="未绑定称重图片"
@open="openMeasurementEvidence(weightResult.fileId, `称重图片 · ${weightResult.palletType || '未记录类型'}`)"
/>
</template>
</el-table-column>
<el-table-column label="托盘类型" min-width="160">
<template #default="{ row: weightResult }">{{ weightResult.palletType || '未记录类型' }}</template>
</el-table-column>
<el-table-column label="服务端确认毛重(kg)" min-width="180" align="right">
<template #default="{ row: weightResult }">{{ formatNumber(weightResult.grossWeightKg) }}</template>
</el-table-column>
<el-table-column label="托盘皮重(kg)" min-width="140" align="right">
<template #default="{ row: weightResult }">{{ formatNumber(weightResult.palletTareWeightKg) }}</template>
</el-table-column>
<el-table-column label="货物重量(kg)" min-width="140" align="right">
<template #default="{ row: weightResult }">{{ formatNumber(weightResult.netWeightKg) }}</template>
</el-table-column>
</el-table>
</div>
<div v-if="measurementDetail?.weightResults.length" class="weight-evidence-grid">
<article v-for="weightResult in measurementDetail.weightResults" :key="weightResult.id" class="weight-evidence-card">
<MeasurementEvidenceThumbnail
:customer-id="String(detail.summary.tenantId)"
:inbound-id="String(detail.summary.inboundId)"
:file-id="weightResult.fileId"
:alt="`称重结果 ${weightResult.id} 的称重图片`"
empty-text="未绑定称重图片"
@open="openMeasurementEvidence(weightResult.fileId, `称重图片 · ${weightResult.palletType || '未记录类型'}`)"
/>
<dl>
<div><dt>托盘类型</dt><dd>{{ weightResult.palletType || '未记录类型' }}</dd></div>
<div><dt>服务端确认毛重</dt><dd>{{ formatNumber(weightResult.grossWeightKg) }} kg</dd></div>
<div><dt>托盘皮重</dt><dd>{{ formatNumber(weightResult.palletTareWeightKg) }} kg</dd></div>
<div><dt>货物重量</dt><dd>{{ formatNumber(weightResult.netWeightKg) }} kg</dd></div>
</dl>
</article>
</div>
<p v-else class="compact-empty">暂无称重图片与确认结果</p>
<div class="measurement-total"><strong>合计</strong><span>实际总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }} kg</span></div>
</section>
</div>
</section>
@@ -2810,8 +2817,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
@keydown="handleTabKeydown($event, index)"
><span>{{ tab.label }}</span><span v-if="tabCount(tab.value) !== undefined" class="order-status-count">({{ tabCount(tab.value) }})</span></button>
</div>
<div v-if="statusCountsLoading" class="order-status-feedback" role="status">页签数量加载中…</div>
<div v-else-if="statusCountsError" class="order-status-feedback" role="status">
<div v-if="statusCountsError" class="order-status-feedback" role="status">
<span>{{ statusCountsError.message }}</span>
<el-button link type="primary" @click="loadStatusCounts">重试数量</el-button>
<ApiErrorMeta :details="statusCountsError" />
@@ -2854,8 +2860,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
><span>{{ tab.label }}</span><span v-if="tabCount(tab.value) !== undefined" class="order-status-count">({{ tabCount(tab.value) }})</span></button>
</div>
<div v-if="statusCountsLoading" class="order-status-feedback" role="status">页签数量加载中…</div>
<div v-else-if="statusCountsError" class="order-status-feedback" role="status">
<div v-if="statusCountsError" class="order-status-feedback" role="status">
<span>{{ statusCountsError.message }}</span>
<el-button link type="primary" @click="loadStatusCounts">重试数量</el-button>
<ApiErrorMeta :details="statusCountsError" />
@@ -3264,19 +3269,6 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.measurement-total { display: flex; min-height: var(--admin-control-height, 32px); flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--admin-space-1, 4px) 20px; padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); border: 1px solid var(--workbench-border); border-top: 0; background: var(--admin-label-bg, #f8fafc); color: #475467; font-size: var(--admin-font-caption, 12px); font-variant-numeric: tabular-nums; }
.measurement-total strong { margin-right: auto; color: var(--workbench-text); font-size: var(--admin-font-body, 13px); }
.weight-evidence { margin-top: var(--admin-space-5, 12px); padding: var(--admin-space-4, 10px) 0 0; border-top: 1px solid var(--workbench-border); background: var(--admin-surface, #fff); }
.weight-evidence__heading { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); margin-bottom: var(--admin-space-3, 8px); }
.weight-evidence__heading h4 { margin: 0; color: var(--workbench-text); font-size: var(--admin-font-section, 14px); }
.weight-evidence__heading p { margin: 5px 0 0; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.weight-evidence__heading dl { margin: 0; text-align: right; }
.weight-evidence__heading dt { color: var(--workbench-muted); font-size: var(--admin-font-caption, 11px); }
.weight-evidence__heading dd { margin: 0; color: var(--workbench-text); font-size: var(--admin-font-body, 13px); font-weight: 600; }
.weight-evidence-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--admin-space-3, 8px); }
.weight-evidence-card { display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: center; min-width: 0; padding: var(--admin-space-3, 8px); border: 1px solid var(--workbench-border); background: var(--admin-surface, #fff); }
.weight-evidence-card > dl { display: grid; gap: var(--admin-space-1, 4px); margin: 0; padding: 0 0 0 var(--admin-space-3, 8px); }
.weight-evidence-card > dl > div { display: flex; min-width: 0; justify-content: space-between; gap: var(--admin-space-5, 12px); }
.weight-evidence-card dt { color: var(--workbench-muted); font-size: var(--admin-font-caption, 11px); }
.weight-evidence-card dd { min-width: 0; margin: 0; color: var(--workbench-text); font-size: var(--admin-font-caption, 12px); font-weight: 650; text-align: right; overflow-wrap: anywhere; }
.measurement-original-preview { display: block; width: 100%; max-height: min(72vh, 760px); object-fit: contain; border-radius: 8px; background: #f2f4f7; }
.hold-section { padding-bottom: var(--admin-space-5, 12px); border-bottom: 1px solid var(--workbench-border); }
.other-notes-section { padding-top: var(--admin-space-4, 10px); }
@@ -3306,10 +3298,8 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.notes-list span { display: block; margin-top: 5px; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
@media (max-width: 960px) { .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { flex-wrap: wrap; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } }
@media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } }
@media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--workbench-border); border-left: 0; } }
@media (max-width: 720px) { .measurement-section-title { align-items: stretch; flex-direction: column; } .measurement-section-toggle { align-items: flex-start; flex-wrap: wrap; } .measurement-section-toggle > svg { margin-left: auto; } .measurement-section-mode { width: 100%; flex-basis: auto; } }
@media (max-width: 420px) { .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } }
</style>
<style src="@/styles/adminDensity.css"></style>