From 1b1f9b22fe9efddf2c17e21f8b94f50d33c20ab8 Mon Sep 17 00:00:00 2001 From: zheng020 Date: Tue, 2 Jun 2026 19:06:58 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=95=B0=E6=8D=AE=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E8=AE=BE=E8=AE=A1=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 锁定 composables 方案、文件结构、组件契约、loading/error 策略, 并附实施顺序与风险记录。后端 dashboardService 尚未实现,前端先用 mock。 Co-Authored-By: Claude Opus 4.7 --- ...26-06-02-data-dashboard-frontend-design.md | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-02-data-dashboard-frontend-design.md diff --git a/docs/superpowers/specs/2026-06-02-data-dashboard-frontend-design.md b/docs/superpowers/specs/2026-06-02-data-dashboard-frontend-design.md new file mode 100644 index 0000000..ed92e9f --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-data-dashboard-frontend-design.md @@ -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 })` 获取全部数据 +- 顶部 `` 含 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 契约 + +**签名**: +```js +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 一个 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) → ` +- 文案区分: + - 顶部水晶/收益:永不为空(接口兜底返回 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 |