diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index f7bb11ad..b8fe7009 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -11,7 +11,7 @@ describe('adminNavigation', () => { it('opens all self-operated read pages with list permission and keeps sync alone insufficient', () => { const read = filterAdminNavigation(adminNavigation, new Set(['platform:self-operated-analytics:list'])) expect(getNavigationLeaves(read).map(item => item.path)).toEqual([ - '/dashboard', '/self-operated/sales', '/self-operated/inventory', '/self-operated/collection', + '/dashboard', '/self-operated/overview', '/self-operated/sales', '/self-operated/inventory', '/self-operated/collection', ]) const sync = filterAdminNavigation(adminNavigation, new Set(['platform:self-operated-analytics:sync'])) expect(getNavigationLeaves(sync).map(item => item.path)).toEqual(['/dashboard']) @@ -35,12 +35,12 @@ describe('adminNavigation', () => { { title: '海外业务', children: ['采购订单', '本土卡车', '本土散板'] }, { title: '客户服务', children: ['异常订单'] }, { title: '财务管理', children: ['客户账户', '客户应收应付', '应收款统计表', '客户对账单', '结算单应收', '结算单应付'] }, - { title: '自营业务', children: ['销量分析', '库存分析', '数据采集'] }, + { title: '自营业务', children: ['经营总览', '销量分析', '库存分析', '数据采集'] }, { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心'] }, { title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] }, ]) expect(visible).toHaveLength(13) - expect(visible.flatMap(node => node.children ?? [])).toHaveLength(55) + expect(visible.flatMap(node => node.children ?? [])).toHaveLength(56) expect(adminNavigation.find((item) => item.key === 'notifications')).toBeUndefined() }) diff --git a/src/modules/self-operated/components/DashboardBars.vue b/src/modules/self-operated/components/DashboardBars.vue index ce1c81b8..cb02d57a 100644 --- a/src/modules/self-operated/components/DashboardBars.vue +++ b/src/modules/self-operated/components/DashboardBars.vue @@ -3,30 +3,41 @@ import { computed, ref } from 'vue' import type { Quantity } from '../types' import { quantityText } from '../state' export interface BarRow { key: string; label: string; secondary?: string; value: Quantity; detail: string } -const props = defineProps<{ rows: BarRow[]; unit: string; disabled?: boolean; title: string }>() +const props = defineProps<{ rows: BarRow[]; unit: string; disabled?: boolean; title: string; ranked?: boolean }>() const emit = defineEmits<{ select: [key: string] }>() const focused = ref('') const maximum = computed(() => Math.max(1, ...props.rows.map(row => Number(row.value ?? 0)))) +const palette = ['#5575ed', '#3da9ba', '#e8a54b', '#b083dc', '#df8397', '#8a9ab4', '#a2adbf'] +function color(index: number) { return props.ranked ? palette[0] : palette[index % palette.length] } - 暂无符合条件的 SKU;请核对覆盖与待评估资料。 - - {{ row.label }}{{ quantityText(row.value) }} {{ unit }} + —暂无符合条件的 SKU;请核对覆盖与待评估资料。 + + {{ String(index + 1).padStart(2, '0') }}{{ row.label }}{{ quantityText(row.value) }} {{ unit }} {{ row.secondary }} - + {{ rows.find(row => row.key === focused)?.detail }} diff --git a/src/modules/self-operated/components/DashboardCharts.test.ts b/src/modules/self-operated/components/DashboardCharts.test.ts new file mode 100644 index 00000000..865f2a33 --- /dev/null +++ b/src/modules/self-operated/components/DashboardCharts.test.ts @@ -0,0 +1,41 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import DashboardTrend from './DashboardTrend.vue' +import DashboardBars from './DashboardBars.vue' +import type { DailyPoint } from '../types' + +function point(date: string, value: number | null, dataStatus = 'COMPLETE'): DailyPoint { + return { businessDate: date, outboundQuantity: dataStatus === 'COMPLETE' ? value : null, observedQuantity: value, dataStatus, reasonCodes: [], quantityUnit: 'PCS' } +} +describe('dashboard chart integrity and interaction', () => { + it('fills only contiguous known segments and keeps unknown days unavailable for drilldown', async () => { + const wrapper = mount(DashboardTrend, { props: { points: [point('2026-10-01', 2), point('2026-10-02', 4), point('2026-10-03', null, 'UNKNOWN'), point('2026-10-04', 1, 'PARTIAL')] } }) + const areas = wrapper.findAll('.trend-area').filter(path => path.attributes('d')) + expect(areas).toHaveLength(1) + expect(areas[0]!.attributes('d')).not.toContain('770') + const days = wrapper.findAll('g') + await days[2]!.trigger('click') + expect(wrapper.emitted('select')).toBeUndefined() + await days[3]!.trigger('keydown', { key: 'Enter' }) + expect(wrapper.emitted('select')).toEqual([['2026-10-04']]) + expect(days[3]!.find('.trend-point').attributes('fill')).toBe('var(--el-bg-color)') + await wrapper.setProps({ points: [point('2026-10-05', 7)] }) + expect(wrapper.find('.trend-tooltip').exists()).toBe(false) + wrapper.unmount() + }) + it('preserves zero and unknown bars, exact relative widths and disabled selection', async () => { + const wrapper = mount(DashboardBars, { props: { title: '分布', unit: 'SKU', rows: [ + { key: 'a', label: 'A', value: 20, detail: '' }, { key: 'b', label: 'B', value: 5, detail: '' }, + { key: 'zero', label: '零值', value: 0, detail: '' }, { key: 'unknown', label: '未知', value: null, detail: '' }, + ] } }) + expect(wrapper.findAll('.bar-fill').map(bar => bar.attributes('style'))).toEqual(['width: 100%;', 'width: 25%;', 'width: 0%;']) + await wrapper.findAll('button')[3]!.trigger('click') + expect(wrapper.emitted('select')).toBeUndefined() + await wrapper.findAll('button')[2]!.trigger('click') + expect(wrapper.emitted('select')).toEqual([['zero']]) + await wrapper.setProps({ disabled: true }) + await wrapper.findAll('button')[0]!.trigger('click') + expect(wrapper.emitted('select')).toHaveLength(1) + wrapper.unmount() + }) +}) diff --git a/src/modules/self-operated/components/DashboardTrend.vue b/src/modules/self-operated/components/DashboardTrend.vue index cb9cb450..7bca3106 100644 --- a/src/modules/self-operated/components/DashboardTrend.vue +++ b/src/modules/self-operated/components/DashboardTrend.vue @@ -1,38 +1,66 @@ - 缺失日期断线;空心点为未完整日;× 表示未知。今日为已采集累计。 - - 0{{ maximum }} 件 - + 完整日未完整日× 未知 · 缺失日期断线今日为已采集累计 + + + + 0{{ quantityText(maximum) }} 件 + + + {{ text(point) }} - + + × {{ point.businessDate.slice(5) }} - {{ text(focused) }} + {{ text(focused) }} 所选日期暂无可靠出库资料,未知不按零显示。 查看每日数值与覆盖原因日期正式量已采集量状态原因{{ point.businessDate }}{{ quantityText(point.outboundQuantity) }}{{ quantityText(point.observedQuantity) }}{{ label(point.dataStatus) }}{{ reasonsText(point.reasonCodes) }} diff --git a/src/modules/self-operated/operation-matrix.md b/src/modules/self-operated/operation-matrix.md index e038b2b4..10fe76a8 100644 --- a/src/modules/self-operated/operation-matrix.md +++ b/src/modules/self-operated/operation-matrix.md @@ -24,6 +24,19 @@ 自动测试与人工视觉验收分别记录;无真实截图不宣称视觉通过。 +## 经营总览图形表现(2026-10-13 用户截图确认) + +用户明确要求经营总览强化图表与动效。保留 `经营总览图表设计.md` 的五图、窗口、未知与下钻规则,以本轮截图为视觉修改依据。 + +| 操作/区域 | 当前问题与本轮承载结论 | 验证边界 | +| --- | --- | --- | +| 六项指标 | 普通文字带改为彩色细顶边指标带、大数字和辅助状态;保留已知小计与待核验数量 | 数值和未知口径不变;待人工截图确认 | +| 全仓出库趋势 | 继续日折线;增加网格、分段面积、绘制动效、悬停/键盘指引 | 缺日断线,部分日虚线与空心点;不插值补齐;待人工截图确认 | +| 库存分布/三榜 | 保留横向条形;分布分色,排行名次标记,柱条展开动效 | 不引入健康评级、风险率或虚假占位条;待人工截图确认 | +| 首次加载/更新 | 骨架加载、指标和图形入场;减少动态效果偏好下禁用动画 | 原版本、关闭取消、过期提示及权限保持;待人工截图确认 | +| 顶部区域/默认仓库 | 用户补充要求顶部提示紧凑且默认展示仓库;标题与操作合并一行,统计口径和采集时间默认折叠;首次自动加载接口首个仓库,深链指定仓库优先,重置保留仓库 | 保留过期/失败提示及切仓入口;不硬编码仓库;待人工截图确认 | +| 统一搜索/切仓 | 2026-10-13 用户以客户账户截图要求统一标题和右侧操作栏;复用 integrated DataTable 的 toolbar-start/toolbar-actions/filters,关闭结果区,不增加空表;QueryToolbar 接入筛选布局设置。切仓取消旧请求、清旧图后自动加载新仓,重置仍保留当前仓 | 自动查询只读本地聚合接口;验证迟到响应不覆盖、加载与重置;待人工截图确认 | + ## 数据采集样式统一(2026-10-12 当前截图确认) 用户确认销量、库存样式已完成,本轮以两页截图中的“顶部操作栏 → 灰底筛选带 → 提示区 → 表格”为样板,仅统一采集页。采集模式、合成样本声明及更新时间移入主表 notices,顶部保留查看销量/库存的跨页入口,未选仓库时禁用;概览与任务表之间保留单一标准间距。沿用既有配置弹框、任务内嵌详情和恢复确认,不修改采集接口、资格或状态机。修改后待人工1920×1080、100%截图确认。 diff --git a/src/modules/self-operated/pages/CollectionView.vue b/src/modules/self-operated/pages/CollectionView.vue index 07014504..852669c4 100644 --- a/src/modules/self-operated/pages/CollectionView.vue +++ b/src/modules/self-operated/pages/CollectionView.vue @@ -1,6 +1,6 @@ diff --git a/src/modules/self-operated/pages/OverviewView.test.ts b/src/modules/self-operated/pages/OverviewView.test.ts index 1ff885ef..a8ec2692 100644 --- a/src/modules/self-operated/pages/OverviewView.test.ts +++ b/src/modules/self-operated/pages/OverviewView.test.ts @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import OverviewView from './OverviewView.vue' import DashboardBars from '../components/DashboardBars.vue' import DashboardTrend from '../components/DashboardTrend.vue' +import DataTable from '@/shared/components/DataTable.vue' import { dashboardFixture } from './dashboard.fixture' const mocks = vi.hoisted(() => ({ dashboard: vi.fn(), overview: vi.fn(), daily: vi.fn(), push: vi.fn(), query: {} as Record, permitted: true })) @@ -15,7 +16,7 @@ const query = defineComponent({ props: ['modelValue'], emits: ['update:modelValu const button = defineComponent({ props: ['disabled'], emits: ['click'], setup(props, { slots, emit }) { return () => h('button', { disabled: props.disabled, onClick: () => emit('click') }, slots.default?.()) } }) const dialog = defineComponent({ props: ['modelValue', 'width'], emits: ['update:modelValue'], setup(props, { slots }) { return () => props.modelValue ? h('section', [h('button', { onClick: () => undefined }, '趋势弹框'), slots.default?.()]) : null } }) const error = defineComponent({ props: ['message'], setup(props) { return () => h('p', props.message) } }) -const options = { global: { stubs: { AdminPage: passthrough, QueryToolbar: query, AppDialog: dialog, LoadErrorState: error, ApiErrorMeta: true, SkuTrend: true, 'el-button': button } } } +const options = { global: { directives: { loading: () => undefined }, stubs: { AdminPage: passthrough, QueryToolbar: query, AppDialog: dialog, LoadErrorState: error, ApiErrorMeta: true, SkuTrend: true, 'el-button': button, 'el-popover': true, 'el-table-column': true, 'el-table': true, 'el-option': true, 'el-select': true } } } const wrappers: ReturnType[] = [] function page() { const wrapper = mount(OverviewView, options); wrappers.push(wrapper); return wrapper } function findButton(wrapper: ReturnType, text: string) { return wrapper.findAll('button').find(node => node.text() === text)! } @@ -29,6 +30,46 @@ beforeEach(() => { }) afterEach(() => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); vi.useRealTimers(); vi.restoreAllMocks() }) describe('warehouse operating dashboard', () => { + it('does not replace an invalid warehouse-less return with the default warehouse', async () => { + mocks.query = { context: JSON.stringify(dashboardFixture.context), overviewReturn: JSON.stringify({ rangePreset: 'THIS_MONTH', scroll: 760 }) } + mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A', warehouseName: 'A仓' }], observedAt: '2026-10-13T00:00:00Z' }) + const wrapper = page(); await flushPromises() + expect(wrapper.text()).toContain('经营总览返回参数无效') + expect(wrapper.findComponent(query).props('modelValue').warehouseCode).toBe('') + expect(mocks.dashboard).not.toHaveBeenCalled() + await choose(wrapper, { warehouseCode: 'WH-A' }) + expect(mocks.dashboard).toHaveBeenCalledOnce() + expect(mocks.dashboard.mock.calls[0]?.[0]).not.toHaveProperty('queryAsOf') + }) + it('uses the common title/actions/filter surface without an empty result table', async () => { + const wrapper = page(); await flushPromises() + expect(wrapper.findComponent(DataTable).props()).toMatchObject({ appearance: 'integrated', resultsVisible: false, columnSettingsMode: 'hide' }) + expect(wrapper.find('.data-table__toolbar-start h2').text()).toBe('经营总览') + expect(wrapper.find('.data-table__toolbar-actions').text()).toContain('整体刷新') + expect(wrapper.find('.data-table__filters').findComponent(query).exists()).toBe(true) + expect(wrapper.findComponent(query).attributes('preference-key')).toBe('self-operated-overview-query') + expect(wrapper.find('el-table-stub').exists()).toBe(false) + }) + it('automatically loads the first warehouse and keeps it selected on reset', async () => { + mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A', warehouseName: '仓A' }, { warehouseCode: 'WH-A' }, { warehouseCode: 'WH-B' }] }) + const wrapper = page(); await flushPromises() + expect(wrapper.findComponent(query).props('modelValue').warehouseCode).toBe('WH-A') + expect(mocks.dashboard).toHaveBeenCalledTimes(1) + expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'LAST_15_DAYS' }) + expect(wrapper.find('.dashboard-data-notes').attributes('open')).toBeUndefined() + await choose(wrapper, { rangePreset: 'TODAY' }) + wrapper.findComponent(query).vm.$emit('reset'); await flushPromises() + expect(wrapper.findComponent(query).props('modelValue')).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'LAST_15_DAYS' }) + expect(mocks.dashboard.mock.calls.at(-1)?.[0]).not.toHaveProperty('queryAsOf') + }) + it('preserves a warehouse supplied by a direct link instead of replacing it with the default', async () => { + mocks.query = { warehouseCode: 'WH-B' } + mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A' }, { warehouseCode: 'WH-B' }] }) + const wrapper = page(); await flushPromises() + expect(mocks.dashboard).toHaveBeenCalledTimes(1) + expect(mocks.dashboard.mock.calls[0]?.[0].warehouseCode).toBe('WH-B') + expect(wrapper.findComponent(query).props('modelValue').warehouseCode).toBe('WH-B') + }) it('restores the saved warehouse, range, fixed context and scroll when returning from a drilldown', async () => { const scroll = vi.spyOn(window, 'scrollTo').mockImplementation(() => undefined) mocks.query = { warehouseCode: 'WH-A', context: JSON.stringify(dashboardFixture.context), overviewReturn: JSON.stringify({ rangePreset: 'THIS_MONTH', scroll: 760 }) } @@ -48,7 +89,7 @@ describe('warehouse operating dashboard', () => { }) it('keeps sales available when inventory is unavailable, with no fabricated distribution', async () => { mocks.dashboard.mockResolvedValue({ ...dashboardFixture, context: { ...dashboardFixture.context, inventoryDrilldownExpiresAt: null }, coverageDistribution: { ...dashboardFixture.coverageDistribution, candidateSkuCount: null, buckets: [], dataStatus: 'UNKNOWN', reasonCodes: ['INVENTORY_UNAVAILABLE'] } }) - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.text()).toContain('无可用库存分布资料') expect(wrapper.text()).toContain('库存资料不可用') expect(wrapper.findComponent(DashboardTrend).exists()).toBe(true) @@ -56,7 +97,7 @@ describe('warehouse operating dashboard', () => { }) it('keeps inventory present without published sales and never derives a daily average from partial sales', async () => { mocks.dashboard.mockResolvedValue({ ...dashboardFixture, context: { ...dashboardFixture.context, salesRunId: null, salesScopeHash: null }, kpis: { ...dashboardFixture.kpis, averageDailyOutboundQuantity30d: { ...dashboardFixture.kpis.averageDailyOutboundQuantity30d, value: null, knownSubtotal: null, dataStatus: 'UNKNOWN', reasonCodes: ['SALES_UNAVAILABLE'] }, physicalQuantity: { ...dashboardFixture.kpis.physicalQuantity, value: '128', dataStatus: 'COMPLETE' } }, salesTrend: { ...dashboardFixture.salesTrend, points: [] } }) - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.findAll('.dashboard-kpis strong')[1]!.text()).toBe('—') expect(wrapper.findAll('.dashboard-kpis strong')[2]!.text()).toBe('128') expect(wrapper.text()).toContain('销量资料不可用') @@ -65,7 +106,7 @@ describe('warehouse operating dashboard', () => { it('does not request business data without a selected warehouse and renders six metrics/five charts from one snapshot', async () => { const wrapper = page(); await flushPromises() expect(mocks.dashboard).not.toHaveBeenCalled() - await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + await choose(wrapper, { warehouseCode: 'WH-A' }) expect(mocks.dashboard).toHaveBeenCalledTimes(1) expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'LAST_15_DAYS' }) expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6) @@ -77,27 +118,46 @@ describe('warehouse operating dashboard', () => { expect(wrapper.text()).not.toContain('lingxing-primary') }) it('changes only trend range while pinning versions, then explicitly refreshes the whole snapshot', async () => { - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) await choose(wrapper, { rangePreset: 'TODAY' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() expect(mocks.dashboard.mock.calls.at(-1)?.[0]).toMatchObject({ rangePreset: 'TODAY', salesRunId: '11', inventoryRunId: '12', queryAsOf: dashboardFixture.context.queryAsOf }) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(mocks.dashboard.mock.calls.at(-1)?.[0]).not.toHaveProperty('salesRunId') }) - it('clears the old warehouse and ignores its late response', async () => { + it('automatically loads the new warehouse, shows loading and ignores the old late response', async () => { let finish!: (value: unknown) => void + let finishNew!: (value: unknown) => void mocks.dashboard.mockImplementationOnce(() => new Promise(resolve => { finish = resolve })) - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + mocks.dashboard.mockImplementationOnce(() => new Promise(resolve => { finishNew = resolve })) + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) await choose(wrapper, { warehouseCode: 'WH-B' }) expect(wrapper.find('.dashboard-kpis').exists()).toBe(false) - await findButton(wrapper, '查询').trigger('click'); await flushPromises() + expect(wrapper.find('.dashboard-placeholder').exists()).toBe(true) + expect(mocks.dashboard).toHaveBeenCalledTimes(2) + expect(mocks.dashboard.mock.calls[1]?.[0]).toMatchObject({ warehouseCode: 'WH-B', rangePreset: 'LAST_15_DAYS' }) + expect(mocks.dashboard.mock.calls[1]?.[0]).not.toHaveProperty('queryAsOf') + finishNew(dashboardFixture); await flushPromises() + expect(wrapper.find('.dashboard-kpis').exists()).toBe(true) finish({ ...dashboardFixture, kpis: { ...dashboardFixture.kpis, physicalQuantity: { ...dashboardFixture.kpis.physicalQuantity, value: '999999' } } }); await flushPromises() expect(wrapper.text()).not.toContain('999999') expect(mocks.dashboard.mock.calls[0]?.[1].aborted).toBe(true) }) + it('retains the switched warehouse when the query toolbar clears its fields on reset', async () => { + mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A' }, { warehouseCode: 'WH-B' }] }) + const wrapper = page(); await flushPromises() + await choose(wrapper, { warehouseCode: 'WH-B' }) + const toolbar = wrapper.findComponent(query) + toolbar.props('modelValue').warehouseCode = '' + toolbar.props('modelValue').rangePreset = '' + toolbar.vm.$emit('reset'); await flushPromises() + expect(toolbar.props('modelValue')).toMatchObject({ warehouseCode: 'WH-B', rangePreset: 'LAST_15_DAYS' }) + expect(mocks.dashboard).toHaveBeenCalledTimes(3) + expect(mocks.dashboard.mock.calls.at(-1)?.[0]).toMatchObject({ warehouseCode: 'WH-B', rangePreset: 'LAST_15_DAYS' }) + }) it('preserves same-SKU owner identity in the 1120 dialog and cancels closed requests', async () => { let finish!: (value: unknown) => void mocks.daily.mockImplementationOnce(() => new Promise(resolve => { finish = resolve })) - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) wrapper.findAllComponents(DashboardBars)[1]!.vm.$emit('select', '1'); await flushPromises() expect(mocks.daily.mock.calls[0]?.[0]).toMatchObject({ customerCode: 'OWNER-B', sku: 'SKU-X', rangePreset: 'CUSTOM', startDate: '2026-09-13', endDate: '2026-10-12', salesRunId: '11' }) expect(wrapper.findComponent(dialog).props('width')).toBe('1120px') @@ -107,7 +167,7 @@ describe('warehouse operating dashboard', () => { expect(wrapper.text()).not.toContain('LATE-SKU') }) it('emits exact bucket/day/ranking navigation and keeps unknown day navigation disabled', async () => { - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) wrapper.findAllComponents(DashboardBars)[0]!.vm.$emit('select', 'DAYS_GT_90'); await flushPromises() expect(mocks.push.mock.calls.at(-1)?.[0]).toMatchObject({ path: '/self-operated/inventory', query: { coverageBucket: 'DAYS_GT_90', bucketRuleVersion: 'COVERAGE_DISTRIBUTION_V1', from: 'overview' } }) const chart = wrapper.findComponent(DashboardTrend) @@ -123,7 +183,7 @@ describe('warehouse operating dashboard', () => { it('expires inventory before sales while retaining stock-independent SKU trends', async () => { vi.useFakeTimers({ toFake: ['Date', 'setInterval', 'clearInterval'] }); vi.setSystemTime(new Date('2026-10-13T00:00:00Z')) mocks.dashboard.mockResolvedValue({ ...dashboardFixture, context: { ...dashboardFixture.context, inventoryDrilldownExpiresAt: '2026-10-13T00:00:01Z' } }) - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) await vi.advanceTimersByTimeAsync(2000) expect(wrapper.text()).toContain('原图库存已过期或不可用') expect(wrapper.findAllComponents(DashboardBars)[0]!.props('disabled')).toBe(true) @@ -134,7 +194,7 @@ describe('warehouse operating dashboard', () => { expect(mocks.push).not.toHaveBeenCalled() }) it('retains prior figures on failed refresh and does not render unknown quantities as zero', async () => { - const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() + const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) mocks.dashboard.mockRejectedValue(new Error('offline')) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(wrapper.text()).toContain('展示上次查询') diff --git a/src/modules/self-operated/pages/OverviewView.vue b/src/modules/self-operated/pages/OverviewView.vue index d9b99491..ff9898fd 100644 --- a/src/modules/self-operated/pages/OverviewView.vue +++ b/src/modules/self-operated/pages/OverviewView.vue @@ -1,9 +1,11 @@
暂无符合条件的 SKU;请核对覆盖与待评估资料。
{{ rows.find(row => row.key === focused)?.detail }}
缺失日期断线;空心点为未完整日;× 表示未知。今日为已采集累计。
{{ text(focused) }}
所选日期暂无可靠出库资料,未知不按零显示。