> **视觉基线更新(2026-08-15)**:本提案落地时,图表与配色须遵循 `DESIGN.md` 的白皮书语言(全站浅色单主题),不得引入深色样式。
# 数据统计页 · 趋势可视化升级 设计提案
> 创建时间:2026-06-27
> 范围:仅 `components/admin/DataStatisticsTab.tsx` 的趋势展示区 + 必要的后端口径微调
> 原则:只做加法和体验升级,不删任何现有功能;不改数据库、不动业务逻辑
---
## 1. 现状诊断(已通过代码摸底确认)
### 1.1 趋势区当前形态
```
┌─ 每日趋势(近31天)─────┐ ┌─ 月度趋势 ─────────────┐
│ 纯
表格 │ │ 纯 表格 │
│ 日期 | 上传数 │ │ 月份 | 上传数 │
│ 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` 返回的趋势数据:
```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**:
```ts
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)
```ts
// 修改前(写死):
每日趋势(近31天)
// 修改后(动态):
const dayCount = dailyUploads.length
const rangeText = startDate && endDate
? `${startDate} ~ ${endDate},共${dayCount}天`
: `全部时间,共${dayCount}天`
每日趋势(${rangeText})
```
---
## 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 后再决定是否实施?**