Merge origin/release/test and preserve procurement direction with request unlock

This commit is contained in:
2026-10-26 13:51:37 +08:00
170 changed files with 3122 additions and 2458 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()
@@ -559,13 +559,13 @@ describe('UnifiedOrdersView contract', () => {
it('binds dimension and weight evidence to their formal server records without mixing galleries', () => {
expect(source).toContain('<el-table :data="actualMeasurementRows" row-key="id"')
expect(source).toContain('<el-table-column label="箱号" prop="boxCode"')
expect(source).toContain("row.boxCode || '—'")
expect(source).toContain("row.boxCode?.split('#').pop() || '—'")
expect(source).toContain("`箱号 ${row.boxCode || row.itemNo || '箱规编码待补充'} 的尺寸图片`")
expect(source).not.toContain('row.boxCode || row.itemNo || row.id')
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')
@@ -2701,10 +2701,21 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<el-table-column v-if="measurementStrategyEditable" label="操作" width="110" fixed="left">
<template #default="{ row }"><el-button type="primary" plain size="small" :loading="measurementOuterSavingId === String(row.id)" :disabled="Boolean(measurementOuterSavingId)" @click="saveWarehouseOuterDimensions(row as Record<string, unknown>)">保存外箱</el-button></template>
</el-table-column>
<el-table-column label="箱号" prop="boxCode" min-width="190" fixed="left" show-overflow-tooltip>
<template #default="{ row }"><strong class="measurement-box-code">{{ row.boxCode || '—' }}</strong></template>
<el-table-column label="尺寸图片" width="148" fixed="left">
<template #default="{ row }">
<MeasurementEvidenceThumbnail
:customer-id="String(detail.summary.tenantId)"
:inbound-id="String(detail.summary.inboundId)"
:file-id="row.intelligentImageFileId"
:alt="`箱号 ${row.boxCode || row.itemNo || '箱规编码待补充'} 的尺寸图片`"
empty-text="未绑定尺寸图片"
@open="openMeasurementEvidence(row.intelligentImageFileId, `尺寸图片 · ${row.boxCode || row.itemNo || '箱规编码待补充'}`)"
/>
</template>
</el-table-column>
<el-table-column label="箱号" prop="boxCode" width="100" fixed="left" show-overflow-tooltip>
<template #default="{ row }"><strong class="measurement-box-code">{{ row.boxCode?.split('#').pop() || '—' }}</strong></template>
</el-table-column>
<el-table-column label="#" prop="itemNo" min-width="70" fixed="left" />
<el-table-column label="仓库修改外箱(长)" width="190" align="center">
<template #default="{ row }"><el-input-number v-if="measurementStrategyEditable" v-model="row.outerLengthCm" class="measurement-outer-dimension-input" aria-label="仓库修改外箱长" :min="0.001" :precision="3" :controls="false" placeholder="不修改" /><template v-else>{{ formatNumber(row.outerLengthCm) }}</template></template>
</el-table-column>
@@ -2718,45 +2729,40 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<el-table-column v-for="column in actualMeasuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120">
<template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template>
</el-table-column>
<el-table-column label="尺寸图片" min-width="148">
<template #default="{ row }">
<MeasurementEvidenceThumbnail
:customer-id="String(detail.summary.tenantId)"
:inbound-id="String(detail.summary.inboundId)"
:file-id="row.intelligentImageFileId"
:alt="`箱号 ${row.boxCode || row.itemNo || '箱规编码待补充'} 的尺寸图片`"
empty-text="未绑定尺寸图片"
@open="openMeasurementEvidence(row.intelligentImageFileId, `尺寸图片 · ${row.boxCode || row.itemNo || '箱规编码待补充'}`)"
/>
</template>
</el-table-column>
</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="托盘类型" 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>
@@ -2811,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" />
@@ -2855,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" />
@@ -2991,14 +2995,13 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</AppDialog>
<SmartLoadingDialog v-model="smartLoadingOpen" />
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="batchApprovalOpen" title="批量审核订单" confirm-text="确认批量审核" confirm-type="success" width="min(1100px, calc(100vw - 28px))" fullscreenable :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders">
<el-alert title="请逐票核对入仓码、客户、中文品名、渠道和目的仓;审核通过后订单进入待测量。" type="info" :closable="false" show-icon />
<LoadErrorState v-if="batchApprovalError" compact title="批量审核预览加载失败" :message="batchApprovalError.message" :loading="batchApprovalLoading" @retry="openBatchApproval"><template #details><ApiErrorMeta :details="batchApprovalError" /></template></LoadErrorState>
<template v-else>
<section class="density-dialog-section" data-tone="summary">
<section class="density-dialog-section">
<h3 class="density-dialog-section__title">本次审核范围</h3>
<div class="batch-approval-summary"><span>预览 {{ batchApprovalPreviewRows.length }} 票</span><strong>已勾选 {{ batchApprovalSelectedRows.length }} 票</strong></div>
</section>
<section class="density-dialog-section" data-tone="source">
<section class="density-dialog-section">
<h3 class="density-dialog-section__title">待审核订单</h3>
<el-table ref="batchApprovalTableRef" class="batch-approval-table" :data="batchApprovalPreviewRows" row-key="orderId" border stripe max-height="520" empty-text="暂无可审核订单" @selection-change="handleBatchApprovalSelection">
<el-table-column type="selection" width="48" fixed="left" :selectable="(row: UnifiedOrderBatchApprovalPreviewItem) => row.canApprove" />
@@ -3025,7 +3028,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<template v-else-if="coLoadDetailSummary">
<div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailSummary) }}</el-tag><span>{{ coLoadDetailSummary.memberCount }} 个成员</span></div>
<el-alert v-if="coLoadDetailHasMissingInboundCodes" type="error" :closable="false" show-icon title="同柜组成员入仓码尚未返回,请刷新后重试" />
<section v-else-if="coLoadDetailMembers.length" class="density-dialog-section" data-tone="source">
<section v-else-if="coLoadDetailMembers.length" class="density-dialog-section">
<h3 class="density-dialog-section__title">同柜成员</h3>
<div class="coload-detail-scroll">
<div class="workbench-evidence"><table class="coload-detail-table">
@@ -3265,19 +3268,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); }
@@ -3307,10 +3297,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>