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] } 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 @@ 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 @@