feat(self-operated): add charts and continuous collection status

This commit is contained in:
2026-10-13 20:44:31 +08:00
parent 698ca34529
commit 4c4a3db1eb
33 changed files with 780 additions and 211 deletions
@@ -1,3 +1,4 @@
vi.mock('vue-echarts', () => ({ default: { name: 'VChart', props: ['option'], template: '<div />' } }))
import { defineComponent, h } from 'vue'
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -14,7 +15,7 @@ vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({
const passthrough = defineComponent({ setup(_, { slots }) { return () => h('div', slots.default?.()) } })
const query = defineComponent({ props: ['modelValue', 'fields'], emits: ['update:modelValue', 'search'], setup(_, { emit }) { return () => h('button', { onClick: () => emit('search') }, '查询') } })
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 dialog = defineComponent({ props: ['modelValue', 'width', 'title', 'appendToBody'], 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 popover = defineComponent({ props: ['trigger', 'placement', 'title', 'width'], setup(_, { slots }) { return () => h('div', slots.reference?.()) } })
const options = { global: { directives: { loading: () => undefined }, stubs: { AdminPage: passthrough, QueryToolbar: query, AppDialog: dialog, LoadErrorState: error, ApiErrorMeta: true, SkuTrend: true, 'el-button': button, 'el-popover': popover, 'el-table-column': true, 'el-table': true, 'el-option': true, 'el-select': true } } }
@@ -31,6 +32,19 @@ beforeEach(() => {
})
afterEach(() => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); vi.useRealTimers(); vi.restoreAllMocks() })
describe('warehouse operating dashboard', () => {
it('offers coverage evidence for both inventory rankings even when they are empty', async () => {
const data = structuredClone(dashboardFixture)
data.longCoverageSkus = { ...data.longCoverageSkus, items: [], eligibleCount: '0', unassessableCount: '2711', dataStatus: 'PARTIAL' }
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.text()).toContain('全仓库存分布的待核验原因')
expect(wrapper.text()).toContain('待评估 2711 SKU')
expect(wrapper.text()).toContain('待评估 2741 SKU')
expect(mocks.dashboard).toHaveBeenCalledOnce()
})
it('uses one date range field, blocks incomplete ranges and clears obsolete validation errors', async () => {
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A', rangePreset: 'CUSTOM' })
const toolbar = wrapper.findComponent(query)
@@ -201,8 +215,9 @@ describe('warehouse operating dashboard', () => {
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')
wrapper.findComponent(dialog).vm.$emit('update:modelValue', false); await flushPromises()
expect(wrapper.findAllComponents(dialog).find(item => item.props('title') === 'SKU每日出库趋势')!.props('width')).toBe('1120px')
for (const modal of wrapper.findAllComponents(dialog)) expect(modal.props('appendToBody')).not.toBeUndefined()
wrapper.findAllComponents(dialog).find(item => item.props('title') === 'SKU每日出库趋势')!.vm.$emit('update:modelValue', false); await flushPromises()
finish({ context: dashboardFixture.context, summary: { ...dashboardFixture.salesTrend.summary, sku: 'LATE-SKU' }, points: [] }); await flushPromises()
expect(mocks.daily.mock.calls[0]?.[1].aborted).toBe(true)
expect(wrapper.text()).not.toContain('LATE-SKU')
@@ -234,10 +249,11 @@ describe('warehouse operating dashboard', () => {
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)
await chart.findAll('g')[1]!.trigger('keydown', { key: 'Enter' })
await chart.find('.chart-disclosure button').trigger('click')
await chart.findAll('tbody button')[1]!.trigger('click')
expect(mocks.push).toHaveBeenCalledTimes(1)
expect(chart.text()).toContain('销量未覆盖')
await chart.findAll('g')[0]!.trigger('keydown', { key: 'Enter' })
await chart.findAll('tbody button')[0]!.trigger('click')
expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ analysis: 'SKU', rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-10' })
await findButton(wrapper, '查看Top100').trigger('click')
expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ analysis: 'HOT', rangePreset: 'CUSTOM', startDate: '2026-09-13', endDate: '2026-10-12' })