From 6a905419fc80d91f18abec43c7c19be9e86bbcc2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 14 Oct 2026 11:22:59 +0800 Subject: [PATCH] fix(self-operated): render dashboard KPIs with omitted unknown values --- .../self-operated/pages/OverviewView.test.ts | 23 ++++++++++++++++--- .../self-operated/pages/OverviewView.vue | 7 +++--- src/modules/self-operated/types.ts | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/modules/self-operated/pages/OverviewView.test.ts b/src/modules/self-operated/pages/OverviewView.test.ts index d635b6a9..09416ec4 100644 --- a/src/modules/self-operated/pages/OverviewView.test.ts +++ b/src/modules/self-operated/pages/OverviewView.test.ts @@ -32,24 +32,41 @@ beforeEach(() => { }) afterEach(() => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); vi.useRealTimers(); vi.restoreAllMocks() }) describe('warehouse operating dashboard', () => { + it.each(['PARTIAL', 'UNKNOWN'])('renders omitted nullable KPI fields for %s instead of rejecting the dashboard', async status => { + const response = structuredClone(dashboardFixture) + for (const metric of Object.values(response.kpis)) { + metric.value = null; metric.knownSubtotal = null; metric.unknownSkuCount = null + metric.outboundQuantity = null; metric.observedQuantity = null; metric.dataStatus = status + } + response.kpis.intervalOutboundQuantity.observedQuantity = '8' + response.kpis.physicalQuantity.knownSubtotal = '128' + // Match a server response serialized with NON_NULL, not an explicit-null fixture. + mocks.dashboard.mockResolvedValue(JSON.parse(JSON.stringify(response, (_, value) => value === null ? undefined : value))) + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) + expect(wrapper.text()).not.toContain('指标结构不完整') + expect(wrapper.findAll('.dashboard-kpis strong').map(node => node.text())).toEqual(['8', '—', '—', '—', '—', '—']) + expect(wrapper.text()).toContain('已知小计 128') + expect(wrapper.findAllComponents(DashboardBars)).toHaveLength(4) + }) + it.each(['intervalOutboundQuantity', 'averageDailyOutboundQuantity', 'physicalQuantity', 'availableQuantity', 'physicalSkuCount', 'noOutboundSkuCount'] as const)('keeps the page usable when the response omits %s and recovers after refresh', async key => { const invalid = structuredClone(dashboardFixture) delete (invalid.kpis as Partial)[key] mocks.dashboard.mockResolvedValue(invalid) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) - expect(wrapper.text()).toContain('经营总览指标返回不完整') + expect(wrapper.text()).toContain('经营总览返回的指标结构不完整') expect(wrapper.find('.dashboard-kpis').exists()).toBe(false) expect(wrapper.findComponent(query).exists()).toBe(true) mocks.dashboard.mockResolvedValue(dashboardFixture) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6) - expect(wrapper.text()).not.toContain('经营总览指标返回不完整') + expect(wrapper.text()).not.toContain('经营总览返回的指标结构不完整') }) it('preserves the last valid result when a refreshed response has a null KPI', async () => { const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) mocks.dashboard.mockResolvedValue({ ...dashboardFixture, kpis: { ...dashboardFixture.kpis, averageDailyOutboundQuantity: null } }) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() - expect(wrapper.text()).toContain('经营总览指标返回不完整,请更新服务后重新查询;展示上次查询') + expect(wrapper.text()).toContain('经营总览返回的指标结构不完整,请重试或联系管理员;展示上次查询') expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6) }) diff --git a/src/modules/self-operated/pages/OverviewView.vue b/src/modules/self-operated/pages/OverviewView.vue index d89613da..6486733a 100644 --- a/src/modules/self-operated/pages/OverviewView.vue +++ b/src/modules/self-operated/pages/OverviewView.vue @@ -100,9 +100,10 @@ async function load(refresh = false) { if (!request.current()) return if (!result?.context || metricNames.some(({ key }) => { const metric = result?.kpis?.[key] - return metric == null || typeof metric !== 'object' || !('value' in metric) + // Unknown quantities may be omitted by the server's NON_NULL serialization. + return metric == null || typeof metric !== 'object' || !['COMPLETE', 'PARTIAL', 'UNKNOWN'].includes(metric.dataStatus) })) { - error.value = `经营总览指标返回不完整,请更新服务后重新查询${data.value ? ';展示上次查询' : ''}` + error.value = `经营总览返回的指标结构不完整,请重试或联系管理员${data.value ? ';展示上次查询' : ''}` return } if (pinned && !sameContext(pinned, result.context)) { error.value = '查询版本不一致,请整体刷新经营总览'; return } @@ -183,7 +184,7 @@ onUnmounted(() => { dataFence.cancel(); trendFence.cancel(); poller.stop(); clea

没有自营经营数据查看权限。