应用 spec reviewer 的两条建议:澄清 composable 用 effectScope 释放 资源(非依赖 onUnmounted),并显式说明 6 组件消费 7 接口的映射 (CollectionMatrix 内部消费 3 个)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
14 KiB
数据看板前端 - 设计文档
配套文档:
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、错误、刷新。
理由:
- dashboard 数据完全页面内,不跨页面共享(水晶余额走单独的
getUserProfileApi,已在utils/api.js中) - 移动端内存友好——页面 onUnmounted 时 composable 自动清理状态
- 与项目既有
composables/useHolographicPreview.js等 Vue 3 模式一致 - 减少一个 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 个 section;Tab 2 渲染占位
- 监听
onShow:Tab 1 切回时如有缓存则用旧数据 + 后台静默刷新
Props: 无
Key state: activeTab: 'crystal' | 'season'
Key event: onShow → refresh({ force: true })
4.2 useDashboardData.js Composable 契约
签名:
const {
loading, // { overall, today, curve, exhibition, likeIncome, topAssets, levels, upgrades }
error, // 同 shape,string | null
data, // { today, curve, exhibition, likeIncome, topAssets, levels, upgrades }
refresh, // (section?: string | { section, force }) => Promise
isReady, // computed: 所有 data 字段非空
lastFetched, // timestamp,30 分钟内 refresh 不重发请求
} = useDashboardData({ starId })
关键行为:
- 首次调用:7 个接口
Promise.allSettled并发 - 单 section 失败 → 该 section
data保持null、error有值、UI 显示重试 refresh('curve'):只重拉 curve 一个 sectionrefresh({ 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(项目已有该文件):
/* 颜色 */
$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 个工厂函数:
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 |