gongxiangp2/docs/数据统计-趋势可视化升级设计提案.md
chenpei c8556a281b feat: 数据统计趋势可视化升级 + 修复部署脚本 stderr bug
1. 趋势可视化升级(方案 A):
   - 新增 TrendLineChart 组件(自绘 SVG 折线图,无第三方依赖)
   - 每日/月度趋势从纯表格升级为面积折线图,默认图表视图
   - 数据补零 + 超过 90 天自动按周降采样
   - 统计摘要:峰值/均值/合计/环比,均值参考线,峰值标注
   - hover tooltip + 视图切换(图表/表格),表格视图完整保留
   - 修复 P1-2:每日趋势标题写死'近31天'改为动态显示真实天数
   - 表格视图去掉 .slice(-31) 截断,显示完整范围

2. 部署脚本 git-incremental-deploy.ps1 stderr bug 修复:
   - PowerShell 在 ErrorActionPreference=Stop 下把 git/plink 的 stderr
     提示当致命错误,导致部署在 Git Sync 阶段误中断
   - 在原生程序调用局部作用域改为 Continue,真实错误仍由 LASTEXITCODE 捕获
2026-06-27 14:21:45 +08:00

11 KiB
Raw Permalink Blame History

数据统计页 · 趋势可视化升级 设计提案

创建时间:2026-06-27 范围:仅 components/admin/DataStatisticsTab.tsx 的趋势展示区 + 必要的后端口径微调 原则:只做加法和体验升级,不删任何现有功能;不改数据库、不动业务逻辑


1. 现状诊断(已通过代码摸底确认)

1.1 趋势区当前形态

┌─ 每日趋势(近31天)─────┐  ┌─ 月度趋势 ─────────────┐
│ 纯 <table> 表格          │  │ 纯 <table> 表格          │
│ 日期 | 上传数            │  │ 月份 | 上传数            │
│ 2026-06-01 | 12          │  │ 2026-01 | 320           │
│ 2026-06-02 | 8           │  │ 2026-02 | 410           │
│ ...(最多31行,可滚动) │  │ ...(可滚动)           │
└──────────────────────────┘  └──────────────────────────┘

问题:

  1. 信息密度低:纯表格,看不出趋势走向(涨/跌/峰值/低谷),需要肉眼逐行比对数字
  2. 缺对比基准:只看到绝对值,不知道"这个数是高还是低"——没有环比/同比/均值线
  3. P1-2 bug:每日趋势标题写死"近31天",但数据实际是后端按筛选时间范围返回的日期桶,选"全部"时表格可能是几年前的31天,标题与数据口径不符
  4. 交互弱:不能hover看明细、不能缩放、点击日期不能联动其它图表

1.2 数据基础(已确认)

后端 lib/admin-material-stats.ts 返回的趋势数据:

dailyUploads:  { bucket: '2026-06-01', count: number }[]  // 按筛选范围内的天
monthlyUploads: { bucket: '2026-06', count: number }[]    // 按筛选范围内的月

数据已经是干净的时序数组,无需后端改动即可做折线图。唯一的口径问题是 dailyUploads 只返回"有数据的天"(稀疏),绘图时需要补零。

1.3 技术约束

  • 项目无图表库(recharts/d3/echarts 都没装),现有柱状图是手写 div + height%
  • 引入新库 = 增加 bundle 体积 + 构建依赖。需权衡"自己画 SVG"vs"引库"

2. 设计目标

  1. 一眼看出趋势:用折线/面积图替代表格,涨跌、峰值、低谷立刻可见
  2. 提供对比基准:加环比(vs 上一周期)、移动平均线,让数字有参照系
  3. 修复 P1-2:每日趋势标题与数据口径一致
  4. 保持轻量:不引入重量级图表库,自己用 SVG 画(与现有柱状图风格一致)
  5. 可降级:表格作为辅助视图保留(切换"图表/表格"),照顾想看精确数字的场景

3. 提案:三档方案(成本递增,任选一档)

方案 A:最小改动(推荐先做)

成本:约 1 个前端组件 + 0 后端改动

把"每日趋势"和"月度趋势"两个表格,各替换为自绘 SVG 折线/面积图,并保留一个"切换表格视图"的按钮。

每日趋势                                  [📈 图表] [📊 表格]  ← 切换
┌──────────────────────────────────────────────────────────────┐
│      ╱╲      ╱╲                                             │
│   ╱╳╯  ╲    ╱  ╲     ╱╲    ← 面积折线(sky 渐变填充)       │
│  ╱      ╲__╱    ╲___╱  ╲___                                 │
│ ─────────── 均值线(虚线)─────────────────                  │
│ 06-01        06-10        06-20        06-30  ← x轴         │
│ 峰值 06-08 (42条)  ·  均值 18.3条/天  ·  合计 549条          │ ← 统计摘要
└──────────────────────────────────────────────────────────────┘
hover 任意点 → tooltip 显示 "2026-06-08:42 条"

新增信息(表格完全没有的):

  • 📍 峰值日期标注(哪天最高)
  • 📍 均值参考线(虚线,一眼判断高低)
  • 📍 统计摘要条(峰值/均值/合计/环比%)
  • 📍 hover tooltip(精确值)

修复 P1-2:标题动态显示真实范围,如"每日趋势(2026-06-01 ~ 2026-06-27,共27天)"。

方案 B:中等改动(A + 同比对比)

成本:A + 前端补零逻辑 + 可能的后端口径微调

在方案 A 基础上,给月度趋势加同比折线(今年 vs 去年同期):

月度趋势(含同比)
┌────────────────────────────────────────────────┐
│   ─── 今年(实线)                              │
│   ┄┄┄ 去年同期(虚线)   ← 需后端返回去年同期数据│
│   1月  2月  3月  4月  5月  6月                  │
│   环比 +12.3%  同比 +5.1%                       │
└────────────────────────────────────────────────┘

后端改动(可选):getMaterialUploadStats 增加 previousMonthlyUploads(取筛选范围向前平移一年的同长度区间)。不改表结构,只多一次聚合查询。

方案 C:大改动(B + 交互联动)

成本:B + 图表点击联动 + 时间粒度切换

  • 点击折线上某月 → 自动把上方"部门/素材类型分布"切换到该月范围
  • 加"日/周/月"粒度切换器(当前只有日和月两张表)
  • 区域选择缩放(拖选一段时间放大看)

4. 推荐落地路径

强烈建议先做方案 A,理由:

维度 方案 A 方案 B 方案 C
视觉提升 ★★★★ 大 ★★★★★ 很大 ★★★★★ 很大
信息增量 中(均值/峰值/摘要) 高(同比) 很高(联动)
后端改动 0 小(多一次聚合) 中
新代码量 ~250 行(1个SVG组件) +100 行 +300 行
bundle 体积 0(自己画SVG) 0 0
回归风险 低(纯展示层) 中(碰后端) 高(联动状态多)
用户感知 立竿见影 需有去年数据才显价值 依赖使用习惯

方案 A 的性价比最高:零后端风险、零依赖、视觉提升立竿见影。做完 A 看实际效果,再决定要不要追加 B/C。


5. 方案 A 详细技术设计

5.1 新增组件:TrendLineChart

位置:components/admin/TrendLineChart.tsx(独立出来,便于复用和测试)

Props:

interface TrendPoint { bucket: string; count: number }
interface TrendLineChartProps {
  data: TrendPoint[]              // 后端返回的 dailyUploads / monthlyUploads
  granularity: 'day' | 'month'    // 决定 x 轴标签格式和补零策略
  accent: 'sky' | 'teal'          // 与现有柱状图配色呼应
  startDate?: string              // 用于动态标题和补零范围
  endDate?: string
}

5.2 SVG 折线图核心算法

1. 数据补零:把 data 按 [startDate, endDate] 补齐成连续日期序列
   (后端只返回有数据的天,绘图需要连续 x 轴)
   - day: 按天补,跨度大时自动降采样到"每周一个点"
   - month: 按月补

2. 坐标映射:
   - x = (index / (n-1)) * width
   - y = height - (count / maxCount) * height

3. 路径生成:
   - 折线: M x0,y0 L x1,y1 ...
   - 面积: 折线 + 回到底边的闭合路径,fill 用渐变(opacity 0.15→0)

4. 均值参考线:
   - avg = sum(count) / n
   - y = height - (avg / maxCount) * height
   - 虚线 stroke-dasharray="4 4"

5. 峰值标注:
   - 找 max count 的点,上方画圆点 + 文字标签

6. hover tooltip:
   - 监听 mousemove,找最近的数据点,浮层显示 "日期:N条"

5.3 统计摘要条(图表上方)

峰值 06-08(42条)  ·  均值 18.3条/天  ·  合计 549条  ·  环比 +12.3% ▲
  • 峰值/均值/合计:前端从 data 算
  • 环比:对比"后半段 vs 前半段"的均值变化(无需后端,纯前端切分)

5.4 视图切换

图表右上角 [📈 图表] [📊 表格] 切换按钮(用现有 rounded-apple 风格)。

  • 默认图表视图
  • 切到表格视图 = 当前形态(保留,零删减,满足"都不删")

5.5 标题修复(P1-2)

// 修改前(写死):
<h3>每日趋势(近31天)</h3>

// 修改后(动态):
const dayCount = dailyUploads.length
const rangeText = startDate && endDate 
  ? `${startDate} ~ ${endDate},共${dayCount}天` 
  : `全部时间,共${dayCount}天`
<h3>每日趋势(${rangeText})</h3>

6. 配色与视觉规范(对齐已完成的 UI 重做)

延续上一轮 UI 重做的 Apple 基线:

  • 折线/面积主色:sky-500(每日)/ teal-500(月度),与两个柱状图呼应
  • 面积填充:from-sky-500/20 to-transparent 渐变
  • 均值线:gray-400 虚线
  • 峰值点:sky-600 实心圆 + 白色描边
  • tooltip:rounded-apple 白底 + shadow-apple-lg
  • 字体:tabular-nums(数字等宽对齐)

7. 改动清单(方案 A)

文件 改动 风险
components/admin/TrendLineChart.tsx 新增(~250行 SVG 折线组件) 低,新文件
components/admin/DataStatisticsTab.tsx 引入 TrendLineChart,替换两个趋势表格的渲染,加视图切换按钮,修 P1-2 标题 低,只动渲染层
后端 lib/admin-material-stats.ts 不改 -
数据库 / Prisma 不改 -

不涉及:接口签名、响应结构、权限、数据写入、其它页面。


8. 验证清单(做完后)

  1. npx tsc --noEmit 类型通过
  2. npm test 现有测试不回归
  3. 切换"今天/近7天/近30天/本月/上月/全部"6个预设,图表正确补零、标题正确显示
  4. 表格视图切换正常,数据与图表一致
  5. hover tooltip 正确显示日期和数值
  6. 峰值/均值/合计/环比 4 个摘要数字计算正确
  7. shadow 环境实测视觉

9. 待你确认的开放问题

  1. 方案选 A / B / C?(我推荐 A)
  2. 视图切换默认显示图表还是表格?(我推荐默认图表)
  3. 每日趋势跨度很大时(比如选"全部",跨2年=730天),怎么处理?
    • 选项:(a) 自动降采样到每周一个点 (b) 横向滚动 (c) 强制只显示最近N天
    • 我推荐 (a) 降采样,保证图表可读
  4. 要不要这个提案先作为文档存档,等你 review 后再决定是否实施?