topfans/docs/superpowers/specs/2026-06-02-data-dashboard-frontend-design.md
zheng020 e3f28a82c9 docs: 数据看板 spec 补充 effectScope 与 6/7 映射说明
应用 spec reviewer 的两条建议:澄清 composable 用 effectScope 释放
资源(非依赖 onUnmounted),并显式说明 6 组件消费 7 接口的映射
(CollectionMatrix 内部消费 3 个)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 01:20:47 +08:00

342 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 数据看板前端 - 设计文档
> **配套文档**: `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 数据接入
**6 组件 ↔ 7 接口映射**
- CrystalOverview → 1 个接口today-overview
- IncomeCurve → 1 个接口income-curve
- ExhibitionCenter → 1 个接口exhibition-summary
- LikeIncomeBoard → 1 个接口like-income-by-level
- CollectionMatrix容器**3 个接口**top-assets、level-distribution、upgrade-progress子组件各消费其一
**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` 缓存
- **资源释放**composable 内部用 `effectScope` 包裹请求与状态dashboard.vue 在 `onUnmounted` 中调用 composable 暴露的 `dispose()` 释放 effectScope确保 `setTimeout`/Promise 回调不污染已卸载组件
### 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 |