feat(self-operated): add transit column and streamline dashboard ranking explanations

This commit is contained in:
2026-10-14 14:09:24 +08:00
parent adfb1a5cb3
commit 2a9c23ebc6
8 changed files with 32 additions and 19 deletions
@@ -93,8 +93,8 @@ describe('warehouse operating dashboard', () => {
data.noOutboundSkus = { ...data.noOutboundSkus, items: [], eligibleCount: '0', unassessableCount: '2741', dataStatus: 'PARTIAL' }
mocks.dashboard.mockResolvedValue(data)
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
expect(wrapper.findAll('.dashboard-chart-actions button').filter(node => node.text() === '待评估原因')).toHaveLength(2)
await findButton(wrapper, '待评估原因').trigger('click'); await flushPromises()
expect(wrapper.findAll('.dashboard-chart-actions button').filter(node => node.text() === '说明')).toHaveLength(3)
await wrapper.findAll('.dashboard-chart-actions button').filter(node => node.text() === '说明')[1]!.trigger('click'); await flushPromises()
expect(wrapper.text()).toContain('全仓库存分布的待核验原因')
expect(wrapper.text()).toContain('待评估 2711 SKU')
expect(wrapper.text()).toContain('待评估 2741 SKU')
@@ -241,7 +241,7 @@ describe('warehouse operating dashboard', () => {
expect(wrapper.findAllComponents(DashboardTrend)).toHaveLength(1)
expect(wrapper.text()).toContain('所选时段无出库库存Top10')
expect(wrapper.text()).not.toContain('近30日无出库')
expect(wrapper.findAllComponents(DashboardBars).slice(1).every(chart => chart.text().includes('2026-10-10 至 2026-10-14'))).toBe(true)
expect(wrapper.findAllComponents(DashboardBars).slice(1).every(chart => !chart.text().includes('2026-10-10 至 2026-10-14'))).toBe(true)
expect(wrapper.text()).toContain('区间已采集出库量')
expect(wrapper.text()).toContain('已知小计 128')
expect(wrapper.text()).toContain('另 1 SKU 待核验')
@@ -301,6 +301,8 @@ describe('warehouse operating dashboard', () => {
mocks.dashboard.mockResolvedValue({ ...dashboardFixture, hotSkus: { ...dashboardFixture.hotSkus, windowStartDate: '2026-10-13', windowEndDateExclusive: '2026-10-14', calculationWindowDays: 1, eligibleCount: '0', items: [], dataStatus: 'UNKNOWN', reasonCodes: ['SALES_WINDOW_INCOMPLETE'] } })
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
expect(wrapper.findAllComponents(DashboardBars)[1]!.props('rows')).toEqual([])
expect(wrapper.find('.dashboard-rankings').text()).not.toContain('未采集日期不计为零出库')
await findButton(wrapper, '说明').trigger('click'); await flushPromises()
expect(wrapper.text()).toContain('未采集日期不计为零出库')
await findButton(wrapper, '查看Top100').trigger('click')
expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ startDate: '2026-10-13', endDate: '2026-10-13', rangePreset: 'CUSTOM' })
@@ -309,7 +311,7 @@ describe('warehouse operating dashboard', () => {
mocks.dashboard.mockResolvedValue({ ...dashboardFixture, hotSkus: { ...dashboardFixture.hotSkus, windowStartDate: '2026-10-01', windowEndDateExclusive: '2026-10-14', calculationWindowDays: 13, dataStatus: 'PARTIAL', reasonCodes: ['SALES_WINDOW_INCOMPLETE'], items: [{ ...dashboardFixture.hotSkus.items[0], windowOutboundQuantity: '37', value: '37', averageDailyOutboundQuantity: null, inventoryCoverageDays: null }] } })
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
expect(wrapper.text()).toContain('所选时段热销Top10')
expect(wrapper.text()).toContain('按所选时段已采集量排名')
expect(wrapper.text()).toContain('数据未采齐,排名仅供参考')
const hot = wrapper.findAllComponents(DashboardBars)[1]!
expect(hot.props('rows')[0]).toMatchObject({ value: '37' })
expect(hot.props('rows')[0].detail).toContain('13天窗口已采集出库 37 件')
@@ -234,16 +234,20 @@ onUnmounted(() => { dataFence.cancel(); trendFence.cancel(); poller.stop(); clea
<section v-for="ranking in rankingNames" :key="ranking.key" class="dashboard-chart">
<DashboardBars ranked :details-width="ranking.key === 'hotSkus' ? '1280px' : undefined" :rows="rankingRows(data[ranking.key])" :title="ranking.label" :unit="ranking.unit" :disabled="contextExpired" @select="index => showTrend(data![ranking.key].items[Number(index)]!, data![ranking.key])">
<template #actions>
<ChartDetailsDialog v-if="ranking.key !== 'hotSkus'" inline button-label="待评估原因" title="采集覆盖与待评估原因" width="1040px" :reset-on="data">
<InventoryRankingDiagnostics :data="data" :warehouse="runtime?.warehouses.find(item => item.warehouseCode === warehouseCode)?.warehouseName || warehouseCode" :historical="contextExpired || stockExpired" />
</ChartDetailsDialog>
<el-button link type="primary" :disabled="contextExpired || ranking.key !== 'hotSkus' && stockExpired" @click="allRanking(ranking.key)">{{ ranking.all }}</el-button>
</template>
<template #intro>
<ChartDetailsDialog inline button-label="说明" :title="`${ranking.label} · 统计说明`" width="1040px" :reset-on="data">
<p>{{ data[ranking.key].windowStartDate }} 至 {{ data[ranking.key].windowEndDateExclusive }}(截止日不含);符合条件 {{ quantityText(data[ranking.key].eligibleCount) }} SKU,待评估 {{ quantityText(data[ranking.key].unassessableCount) }}。</p>
<p>{{ contextExpired || ranking.key !== 'hotSkus' && stockExpired ? '历史/待刷新 · ' : '' }}{{ label(data[ranking.key].dataStatus) }} · {{ reasonsText(data[ranking.key].reasonCodes) }}</p>
<p v-if="ranking.key === 'hotSkus' && data.hotSkus.dataStatus !== 'COMPLETE'">按所选时段已采集量排名,数据尚未采齐,排名可能随采集变化;未采集日期不计为零出库。</p>
<p v-else-if="ranking.key !== 'hotSkus' && data[ranking.key].dataStatus !== 'COMPLETE'">本榜仍需所选区间完整销量与库存观察资料;资料不足的 SKU 暂不评估,未采集日期不计为零出库。</p>
<InventoryRankingDiagnostics v-if="ranking.key !== 'hotSkus'" :data="data" :warehouse="runtime?.warehouses.find(item => item.warehouseCode === warehouseCode)?.warehouseName || warehouseCode" :historical="contextExpired || stockExpired" />
</ChartDetailsDialog>
<el-button link type="primary" :disabled="contextExpired || ranking.key !== 'hotSkus' && stockExpired" @click="allRanking(ranking.key)">{{ ranking.all }}</el-button>
</template>
<template #intro>
<div class="dashboard-ranking-status" role="status">
<span v-if="contextExpired || ranking.key !== 'hotSkus' && stockExpired">历史数据 · 待刷新</span>
<span v-else-if="data[ranking.key].dataStatus !== 'COMPLETE'">{{ ranking.key === 'hotSkus' ? '数据未采齐,排名仅供参考' : Number(data[ranking.key].unassessableCount) > 0 ? `${quantityText(data[ranking.key].unassessableCount)} 个SKU待评估` : '数据待核验' }}</span>
</div>
</template>
<template v-if="ranking.key === 'hotSkus'" #details="{ select }"><HotSkuDetails :ranking="data.hotSkus" :disabled="contextExpired" @select="index => select(rankingRows(data!.hotSkus)[index]!)" /></template>
</DashboardBars>
@@ -273,6 +277,7 @@ onUnmounted(() => { dataFence.cancel(); trendFence.cancel(); poller.stop(); clea
.dashboard-metric-status { color: var(--metric-color); }
.dashboard-kpis p { margin: var(--admin-space-1) 0; }
.dashboard-main-charts { display: grid; grid-template-columns: 2fr 1fr; gap: var(--admin-space-4); }
.dashboard-ranking-status { min-height: 24px; margin-bottom: var(--admin-space-2); color: var(--el-color-warning-dark-2); font-size: var(--admin-font-caption); }
.dashboard-rankings { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--admin-space-4); }
.dashboard-chart { min-width: 0; padding: 18px; background: var(--admin-surface); border: 1px solid #e5eaf4; border-radius: 8px; animation: dashboard-reveal .6s ease-out both; }
.dashboard-chart p { color: #76849c; font-size: var(--admin-font-caption); line-height: 1.7; }