docs: 数据看板前端设计 spec

锁定 composables 方案、文件结构、组件契约、loading/error 策略,
并附实施顺序与风险记录。后端 dashboardService 尚未实现,前端先用 mock。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zheng020 2026-06-02 19:06:58 +08:00
parent 29173ca55e
commit 1b1f9b22fe

View File

@ -0,0 +1,334 @@
# 数据看板前端 - 设计文档
> **配套文档**: `docs/figma-analysis-data-dashboard.md`Figma 视觉稿 + 后端 API 契约)
> **本文档焦点**: 前端架构与实现策略视觉细节、token、API 字段、SQL 均见配套文档)
## 一、背景与范围
### 1.1 背景
数据看板是顶粉星城个人中心的核心数据可视化页,展示用户的水晶收益、藏品表现、升级进度。当前**后端 `/api/v1/dashboard/*` 7 个接口尚未实现**,前端需在 mock 数据下先行开发完整 UI 与交互。
### 1.2 范围
**In scope**:
- 6 大模块完整 UI水晶概览、七日曲线、展出收益、点赞收益、藏品矩阵含 4 子组件)
- Tab 1 "水晶相关" 完整实现
- Tab 2 "赛季总览" 占位骨架
- 加载/空/错误三态
- mock 数据接入
**Out of scope不在本文档**:
- 后端 dashboardService 实现(详见配套文档第 4 节)
- Figma 素材下载(项目自有 `static/components/` 已含同类资源)
- 数据看板分享、7日曲线交互弹窗、升级提醒等扩展项配套文档 7.3
- 视觉稿 1:1 像素级还原本文档保证结构、token、文案一致像素级由前端开发阶段人工对齐
---
## 二、架构决策
### 2.1 状态管理Composables不引入新 Vuex 模块)
**决策**: 使用 Vue 3 Composition API 的 `useDashboardData` composable 聚合 7 个接口的请求、loading、错误、刷新。
**理由**:
1. dashboard 数据完全页面内,不跨页面共享(水晶余额走单独的 `getUserProfileApi`,已在 `utils/api.js` 中)
2. 移动端内存友好——页面 onUnmounted 时 composable 自动清理状态
3. 与项目既有 `composables/useHolographicPreview.js` 等 Vue 3 模式一致
4. 减少一个 Vuex 模块文件,组件测试更易隔离
**取捨**:
- 失去:跨页面数据共享(不需)、统一 Vuex devtools 视图(影响小)
- 获得移动端页面切换内存自动回收、composable 单元测试简单
### 2.2 API 函数组织:追加到 `utils/api.js`(不新建 `api/` 目录)
**决策**: 7 个 dashboard 接口以 `dashboardApi` 命名空间对象追加到 `utils/api.js` 末尾。
**理由**:
- 项目 24+ 个 `*Api` 函数全在 `utils/api.js`(如 `loginApi`/`getAssetDetailApi`),惯例一致
- 配套文档 5.3 节建议的 `frontend/api/dashboard.js` 路径会引入新目录,与项目惯例冲突
**取捨**:
- 失去dashboard 相关的 API 物理上不分组
- 获得:与项目惯例一致;后续接真实后端时只改 mock 触发逻辑,文件位置不动
### 2.3 图表方案uCharts 插件
**决策**: 引入 `qiun-data-charts`(跨端)/ `@qiun/vue-ucharts`H5实现七日柱状+折线。
**理由**:
- 移动端 Canvas 手绘跨端适配成本高
- uCharts 包体积 +200KB 可接受
- 多端一致,交互能力强(后续可扩展点击柱子查看明细)
**取捨**:
- 失去:零依赖的轻量优势
- 获得:跨端一致、避免重复造轮子
### 2.4 Mock 数据触发:复用 `USE_MOCK_API` 开关
**决策**: 沿用 `utils/api.js` 顶部已有的 `USE_MOCK_API` 常量mock 数据集中在 `utils/mock/dashboard.js`
**理由**:
- 不引入新机制,沿用项目已有模式
- 后续接真实后端只需将该常量翻为 `false`
**取捨**:
- 失去mock 与真实接口文件分离mock 在 `utils/mock/`,真实在 `utils/api.js`
- 获得:触发逻辑统一,未来切真实接口 1 行代码
---
## 三、文件结构
```
frontend/
├── pages/
│ └── dashboard/
│ ├── dashboard.vue # 页面tab 切换、调用 composable、分发 props
│ └── components/
│ ├── DashboardHeader.vue # 装饰背景 + 标题 + Tab
│ ├── CrystalOverview.vue # 水晶余额+今日收益双卡
│ ├── IncomeCurve.vue # uCharts 七日柱状+折线
│ ├── ExhibitionCenter.vue # 展出收益中心3联+5行
│ ├── LikeIncomeBoard.vue # 点赞收益看板
│ ├── CollectionMatrix.vue # 藏品矩阵容器
│ ├── TopFiveAssets.vue # TOP5 横向卡片
│ ├── LevelDistribution.vue # 5 个环形图CSS conic-gradient
│ ├── UpcomingUpgrades.vue # 即将升级(左列)
│ ├── RecentUpgrades.vue # 最近升级(右列)
│ ├── SectionSkeleton.vue # 通用骨架屏(被各 section 复用)
│ └── EmptyState.vue # 通用空态
├── composables/
│ └── useDashboardData.js # 核心:聚合 7 个接口、loading、错误、刷新
├── utils/
│ ├── api.js # 追加 dashboardApi 命名空间 + mock 触发逻辑
│ └── mock/
│ └── dashboard.js # 7 个接口的 mock 数据工厂
└── pages.json # 注册 dashboard 页面
```
---
## 四、组件设计
### 4.1 页面 `dashboard.vue`
**职责**:
- 调用 `useDashboardData({ starId })` 获取全部数据
- 顶部 `<DashboardHeader>` 含 Tab 切换,本地状态 `activeTab`
- Tab 1 渲染所有 6 个 sectionTab 2 渲染占位
- 监听 `onShow`Tab 1 切回时如有缓存则用旧数据 + 后台静默刷新
**Props**: 无
**Key state**: `activeTab: 'crystal' | 'season'`
**Key event**: `onShow``refresh({ force: true })`
### 4.2 `useDashboardData.js` Composable 契约
**签名**:
```js
const {
loading, // { overall, today, curve, exhibition, likeIncome, topAssets, levels, upgrades }
error, // 同 shapestring | null
data, // { today, curve, exhibition, likeIncome, topAssets, levels, upgrades }
refresh, // (section?: string | { section, force }) => Promise
isReady, // computed: 所有 data 字段非空
lastFetched, // timestamp30 分钟内 refresh 不重发请求
} = useDashboardData({ starId })
```
**关键行为**:
- 首次调用7 个接口 `Promise.allSettled` 并发
- 单 section 失败 → 该 section `data` 保持 `null`、`error` 有值、UI 显示重试
- `refresh('curve')`:只重拉 curve 一个 section
- `refresh({ force: true })`:绕过 `lastFetched` 缓存
- `onUnmounted` 自动清空所有 state依赖 Vue 3 组件卸载生命周期)
### 4.3 子组件 Props 契约
| 组件 | Props | 备注 |
|------|-------|------|
| `DashboardHeader` | `activeTab`, `@update:activeTab` | 受控模式 |
| `CrystalOverview` | `{ balance, today }, loading, error, @retry` | 单一对象loading 顶层 |
| `IncomeCurve` | `points: DailyIncomePoint[], loading, error, @retry` | uCharts 主题色 props |
| `ExhibitionCenter` | `summary: ExhibitionIncomeSummary, loading, error, @retry` | 含 3 联 + 5 行 |
| `LikeIncomeBoard` | `stats: { total, income }, levels: LikeIncomeLevelItem[], loading, error, @retry` | |
| `CollectionMatrix` | `topFive, levels, upgrades, loadingMap, errorMap, @retrySection` | 容器,加载态按子组件维度分发 |
| `TopFiveAssets` | `items: TopAssetItem[]` | |
| `LevelDistribution` | `items: AssetLevelItem[]` | 5 个环形 conic-gradient |
| `UpcomingUpgrades` | `items: UpcomingLevelUpItem[]` | |
| `RecentUpgrades` | `items: RecentLevelUpItem[]` | |
| `SectionSkeleton` | `variant: 'card'\|'list'\|'chart'\|'matrix'` | 通用骨架屏 |
| `EmptyState` | `text?: string, icon?: string` | 默认 "暂无数据" |
---
## 五、Loading / Empty / Error 三态
### 5.1 Loading
- **整体首次加载**`loading.overall === true` → 渲染全屏骨架屏(所有 6 个 section 同步显示 `SectionSkeleton`
- **单 section 加载**:该 section 渲染 `SectionSkeleton` 变体,其他 section 保持数据
- **下拉刷新**`loading.overall` 短暂为 true复用骨架屏
### 5.2 Empty
- 各 section 内置判断:`if (data && data.length === 0) → <EmptyState />`
- 文案区分:
- 顶部水晶/收益:永不为空(接口兜底返回 0
- 七日曲线:空态文案"暂无收益记录"
- 展出/点赞/藏品矩阵:空态文案"暂无数据"
- Tab 2 占位:显示插画 + "赛季总览 · 即将上线" 文案
### 5.3 Error
- 单 section 失败 → 该 section 内显示错误占位(红色边框 + "加载失败,点击重试" + 触发 `@retry`
- 整页不弹 toast移动端网络抖动频繁全屏 toast 体验差)
- 接口 401 走 `utils/api.js` 已有的统一处理(清除 token + 跳登录)
---
## 六、视觉规范
### 6.1 设计 Token 落地
将配套文档 3.x 节的 token 翻译成 SCSS 变量,写入 `uni.scss`(项目已有该文件):
```scss
/* 颜色 */
$color-text-data: #FFFABD;
$color-tab-active: linear-gradient(231deg, #A8A6ED 0%, #88C8D8 64%, #F380EF 100%);
$color-card-gradient-1: linear-gradient(135deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%);
$color-progress-cyan: linear-gradient(90deg, #5EDFFF 0%, #FFC8C8 100%);
$color-progress-pink: linear-gradient(90deg, #FFF375 0%, #FF6B84 100%);
$color-bar-blue-yellow: linear-gradient(90deg, #1BAFEE 0%, #FFCC14 100%);
// ... 其他 token
/* 阴影 */
$shadow-card: 0px 4px 4px rgba(189, 50, 50, 0.25);
$shadow-data-text: -1px 1px 4px rgba(206, 9, 9, 0.84);
// ...
/* 圆角 */
$radius-thumb: 3px;
$radius-card-sm: 14px;
$radius-card-lg: 22px;
// ...
/* 字体 */
$font-num-xl: 35px 'Baloo Bhai', sans-serif;
// ...
```
页面/组件内通过 `var(--xxx)` 引用uni.scss 编译后生成 CSS 变量)。
### 6.2 5 种等级环
`LevelDistribution.vue` 用 CSS `conic-gradient` 实现 5 个环形图:
- 等级色UR/SSR/SR/R/N 各自对应一套渐变色(写在 SCSS 变量)
- 外环 17px、内环 5px按配套文档 2.7.2 节)
- 中心数字 + 百分比覆盖在环上
### 6.3 uCharts 主题
`IncomeCurve.vue` 引用 uCharts 主题配置文件 `static/dashboard/chart-theme.json`
- 5 种渐变(黄→紫→粉、青→粉、黄→红、蓝→黄)映射到 UR/SSR/SR/R/N 等级
- 高亮柱is_peak=true用更亮的渐变
### 6.4 资源占位
配套文档 7.1 节的 Figma 资源毛绒怪头像、TOP 徽章、等级徽章等):
- **本期不下载**:项目 `static/components/` 已有同类风格资源
- **占位策略**:用纯 CSS 渐变方块 + 文字标签代替(如 `[UR]` `[SSR]` 文字徽章)
- **后续**:设计资源到位后逐个替换
---
## 七、数据契约(与配套文档 4.2 一致,本节为前端视角)
### 7.1 7 个接口
| 接口 | 方法 | 路径 | 数据形状(前端) |
|------|------|------|------------------|
| 今日概览 | GET | `/api/v1/dashboard/today-overview?star_id=X` | `{ crystal_balance, today_income, week_rank? }` |
| 七日曲线 | GET | `/api/v1/dashboard/income-curve?star_id=X` | `{ points: DailyIncomePoint[], total_income, avg_income }` |
| 展出收益 | GET | `/api/v1/dashboard/exhibition-summary?star_id=X` | `ExhibitionIncomeSummary` |
| 点赞收益 | GET | `/api/v1/dashboard/like-income-by-level?star_id=X` | `{ total_like_count, total_income, levels }` |
| TOP 藏品 | GET | `/api/v1/dashboard/top-assets?star_id=X` | `{ items: TopAssetItem[] }` |
| 等级分布 | GET | `/api/v1/dashboard/level-distribution?star_id=X` | `{ items: AssetLevelItem[] }` |
| 升级进度 | GET | `/api/v1/dashboard/upgrade-progress?star_id=X` | `{ upcoming, recent }` |
类型定义在 `composables/useDashboardData.js` 顶部用 JSDoc 注释表达,配套文档 4.2 节的 proto 字段是单一信源。
### 7.2 Mock 数据形状
`utils/mock/dashboard.js` 导出 7 个工厂函数:
```js
export const mockTodayOverview = (params) => ({ crystal_balance: 2713, today_income: 213, week_rank: 12 })
export const mock7DayIncomeCurve = (params) => ({ points: [...7 items with is_today + is_peak], total_income: 1823, avg_income: 260 })
// ... 其他 5 个
```
工厂内根据 `params.star_id` 返回不同值mock 至少 2 个 star_id 的数据以验证切换)。
---
## 八、测试策略
### 8.1 单元测试
- `composables/useDashboardData.js` 核心:覆盖 4 个场景
- 首次加载 7 个接口并发
- 单 section 失败不阻塞其他
- `refresh('curve')` 只刷一个
- `onUnmounted` 清空 state
- 测试框架:项目未引入测试框架,**本期不引入**;如必要,使用 `@vue/test-utils + vitest` 作为 P2 任务
### 8.2 手动验证清单
- [ ] 视觉对齐 Figma 稿
- [ ] Tab 切换不重发请求
- [ ] 下拉刷新拉新数据
- [ ] 飞行模式下各 section 独立重试
- [ ] 后端 401 走统一跳登录
- [ ] 移动端 320px 小屏无溢出
### 8.3 E2E
- 本期不做(项目无 Playwright 基建)
---
## 九、实施顺序(与配套文档 6.1 一致,标注前端部分)
| 阶段 | 内容 | 估时 | 验收 |
|------|------|------|------|
| M2 | pages.json 注册、空页面路由、composable、API 封装、mock | 1 天 | 页面可访问、loading/empty/error 三态正确 |
| M3 | DashboardHeader + CrystalOverview + IncomeCurve | 2 天 | 视觉稿对齐 |
| M4 | ExhibitionCenter + LikeIncomeBoard | 2 天 | 视觉稿对齐 |
| M5 | CollectionMatrix含 4 个子组件) | 2 天 | 视觉稿对齐 |
| M6 | 联调、tab 切换、刷新、空态、错误处理 | 1 天 | 全链路通过 |
| **总计** | | **8 工作日** | |
---
## 十、风险与决策记录
| 风险/决策 | 影响 | 应对/理由 |
|------|------|------|
| uCharts 跨端版本差异 | IncomeCurve 实现 | `#ifdef H5``#ifdef APP-PLUS` 条件编译,按官方文档配置 |
| 移动端首屏 7 个接口并发 | 网络抖动场景 | `Promise.allSettled` + 单 section 独立重试 |
| 后端接口契约变更 | 联调阶段 | proto 是单一信源,前端 mock 与 proto 字段同步 |
| Figma 资源未下载 | 视觉稿 1:1 还原 | 本期用 CSS 占位;资源到位后逐个替换(不影响架构) |
| `USE_MOCK_API` 开关切换 | 后端就绪时 | `utils/api.js` 顶部 1 行常量改动 |
---
## 十一、变更记录
| 日期 | 变更 | 作者 |
|------|------|------|
| 2026-06-02 | 初版 | Claude |