# 数据看板前端 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 实现数据看板页面(6 大模块),在 mock 数据下完整跑通水晶收益、藏品表现、升级进度的可视化展示。
**Architecture:** 单页面 + 1 个聚合 composable(`useDashboardData`)+ 11 个展示子组件。composable 内部用 `effectScope` 包裹 7 个并发请求,按 section 维度独立暴露 loading/error/refresh。后端未就绪时通过 `USE_MOCK_API` 开关走 mock 数据,接真实接口只需翻常量。
**Tech Stack:** Vue 3 (Composition API) + uni-app + Vuex 4(已有,不新增模块)+ uCharts 插件(柱状+折线)+ SCSS(uni.scss 共享 token)+ CSS conic-gradient(5 个等级环)
**Spec:** `docs/superpowers/specs/2026-06-02-data-dashboard-frontend-design.md`
**配套视觉/API 文档:** `docs/figma-analysis-data-dashboard.md`
---
## 文件总览
**Create(13 个)**:
- `frontend/pages/dashboard/dashboard.vue`
- `frontend/pages/dashboard/components/DashboardHeader.vue`
- `frontend/pages/dashboard/components/CrystalOverview.vue`
- `frontend/pages/dashboard/components/IncomeCurve.vue`
- `frontend/pages/dashboard/components/ExhibitionCenter.vue`
- `frontend/pages/dashboard/components/LikeIncomeBoard.vue`
- `frontend/pages/dashboard/components/CollectionMatrix.vue`
- `frontend/pages/dashboard/components/TopFiveAssets.vue`
- `frontend/pages/dashboard/components/LevelDistribution.vue`
- `frontend/pages/dashboard/components/UpcomingUpgrades.vue`
- `frontend/pages/dashboard/components/RecentUpgrades.vue`
- `frontend/composables/useDashboardData.js`
- `frontend/utils/mock/dashboard.js`
**Modify(3 个)**:
- `frontend/pages.json`(注册 dashboard 页面)
- `frontend/utils/api.js`(追加 `dashboardApi` 命名空间 + mock 触发)
- `frontend/uni.scss`(追加设计 token 变量)
**无新增测试文件**:项目未引入测试框架,验证走 `superpowers:verification-before-completion` 流程的手动核对清单。
---
## Task 1: 页面注册 + SCSS 设计 token
**Files:**
- Modify: `frontend/pages.json`(在 `pages` 数组末尾追加一项)
- Modify: `frontend/uni.scss`(在文件末尾追加 dashboard 专用 token)
- [ ] **Step 1: 在 `pages.json` 末尾注册 dashboard 页面**
在 `pages` 数组最后一个对象(`tasks/revenue`)之后,逗号之后追加:
```json
,{
"path": "pages/dashboard/dashboard",
"style": {
"navigationStyle": "custom",
"app-plus": {
"bounce": "none"
}
}
}
```
- [ ] **Step 2: 在 `uni.scss` 末尾追加 dashboard token**
```scss
/* ==================== Dashboard 设计 Token ==================== */
/* 颜色(与 spec §3.1 一致) */
$d-text-data: #FFFABD;
$d-text-white: #FFFFFF;
$d-tab-active: linear-gradient(231deg, #A8A6ED 0%, #88C8D8 64%, #F380EF 100%);
$d-card-crystal: linear-gradient(135deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%);
$d-card-today: linear-gradient(137deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%);
$d-progress-cyan: linear-gradient(90deg, #5EDFFF 0%, #FFC8C8 100%);
$d-progress-pink: linear-gradient(90deg, #FFF375 0%, #FF6B84 100%);
$d-bar-blue-yellow: linear-gradient(90deg, #1BAFEE 0%, #FFCC14 100%);
$d-bar-fill: linear-gradient(135deg, #FFDF77 0%, #B984FF 60%, #FF8183 100%);
$d-page-bg: linear-gradient(153deg, #FF9597 0%, #80DFFF 33%, #B8B8B8 74%, #D9D9D9 100%);
/* 5 个等级专属渐变(环图、徽章、TOP 徽章) */
$d-level-ur: linear-gradient(135deg, #FF8A65 0%, #FFD740 100%);
$d-level-ssr: linear-gradient(135deg, #FF5E9C 0%, #FFB199 100%);
$d-level-sr: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%);
$d-level-r: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%);
$d-level-n: linear-gradient(135deg, #C5C5C5 0%, #8C8C8C 100%);
/* 阴影 */
$d-shadow-card: 0px 4px 4px rgba(189, 50, 50, 0.25);
$d-shadow-data: -1px 1px 4px rgba(206, 9, 9, 0.84);
$d-shadow-mascot: 2px 2px 4px rgba(242, 21, 21, 0.47);
$d-shadow-tab: 0px 4px 4px rgba(0, 0, 0, 0.25);
$d-shadow-inner: 0px 4px 4px rgba(96, 13, 13, 0.25);
/* 圆角 */
$d-radius-thumb: 3px;
$d-radius-progress: 6px;
$d-radius-card-sm: 14px;
$d-radius-card-md: 17px;
$d-radius-card-lg: 22px;
/* 字号 */
$d-fs-num-xl: 35px;
$d-fs-num-lg: 18px;
$d-fs-num-md: 14px;
$d-fs-num-sm: 9px;
$d-fs-title-lg: 20px;
$d-fs-title-md: 18px;
$d-fs-title-sm: 15px;
$d-fs-label: 12px;
/* 间距 */
$d-space-card-x: 16px;
$d-space-section: 24px;
$d-space-cell: 12px;
```
- [ ] **Step 3: 手动验证 - 路由可达**
启动 H5 dev server(如果已在跑则跳过):
```bash
cd frontend && npm run dev:h5
```
在 H5 dev URL 末尾加 `/pages/dashboard/dashboard` 访问:
- 预期:进入空页面(白屏即可,组件尚未创建)
- 不预期:路由 404、JSON 解析错误
- [ ] **Step 4: Commit**
```bash
git add frontend/pages.json frontend/uni.scss
git commit -m "feat(dashboard): 注册页面 + 追加 SCSS 设计 token"
```
---
## Task 2: Mock 数据 + dashboardApi 命名空间
**Files:**
- Create: `frontend/utils/mock/dashboard.js`
- Modify: `frontend/utils/api.js`(追加 `dashboardApi` + 注入 mock 触发逻辑)
- [ ] **Step 1: 创建 mock 数据文件 `frontend/utils/mock/dashboard.js`**
```javascript
// 数据看板 mock 数据工厂
// 后端就绪后,将 utils/api.js 顶部 USE_MOCK_API 改为 false 即可
const randomDelay = (min = 200, max = 600) =>
new Promise((resolve) => setTimeout(resolve, Math.random() * (max - min) + min))
// 1. 今日概览
export async function mockTodayOverview({ star_id }) {
await randomDelay()
return {
code: 200,
data: {
crystal_balance: 2713,
today_income: 213,
week_rank: 12,
},
}
}
// 2. 七日收益曲线
export async function mock7DayIncomeCurve({ star_id }) {
await randomDelay()
const today = new Date()
const points = []
const incomes = [180, 245, 198, 312, 276, 221, 189] // 最后一个是今天
for (let i = 6; i >= 0; i--) {
const d = new Date(today)
d.setDate(d.getDate() - i)
const income = incomes[6 - i]
points.push({
date: `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`,
income,
is_today: i === 0,
is_peak: income === 312,
})
}
return {
code: 200,
data: {
points,
total_income: incomes.reduce((a, b) => a + b, 0),
avg_income: Math.round(incomes.reduce((a, b) => a + b, 0) / 7),
},
}
}
// 3. 展出收益中心
export async function mockExhibitionSummary({ star_id }) {
await randomDelay()
return {
code: 200,
data: {
exhibiting_count: 21,
starbook_count: 33,
total_duration: '712:13:56',
total_earnings: 39721,
top5: [
{ asset_id: 1, asset_name: '璀璨星河', asset_thumb: '', duration_7d: '144:13:56', earnings_7d: 2173, avg_earnings: 15 },
{ asset_id: 2, asset_name: '夏日微风', asset_thumb: '', duration_7d: '77:13:56', earnings_7d: 1332, avg_earnings: 15 },
{ asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '', duration_7d: '64:15:37', earnings_7d: 1201, avg_earnings: 12 },
{ asset_id: 4, asset_name: '黎明序曲', asset_thumb: '', duration_7d: '51:22:12', earnings_7d: 783, avg_earnings: 12 },
{ asset_id: 5, asset_name: '深海回响', asset_thumb: '', duration_7d: '51:22:12', earnings_7d: 783, avg_earnings: 12 },
],
},
}
}
// 4. 点赞收益按等级
export async function mockLikeIncomeByLevel({ star_id }) {
await randomDelay()
return {
code: 200,
data: {
total_like_count: 231,
total_income: 12719,
levels: [
{ level: 'UR', asset_count: 1, total_income: 723, thumb: '' },
{ level: 'SSR', asset_count: 2, total_income: 381, thumb: '' },
{ level: 'SR', asset_count: 5, total_income: 233, thumb: '' },
{ level: 'SR', asset_count: 4, total_income: 169, thumb: '' },
{ level: 'R', asset_count: 6, total_income: 57, thumb: '' },
],
},
}
}
// 5. 藏品 TOP5
export async function mockTopAssets({ star_id }) {
await randomDelay()
return {
code: 200,
data: {
items: [
{ asset_id: 1, asset_name: '璀璨星河', asset_thumb: '', total_earnings: 8420, rank: 1 },
{ asset_id: 2, asset_name: '夏日微风', asset_thumb: '', total_earnings: 6230, rank: 2 },
{ asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '', total_earnings: 5180, rank: 3 },
{ asset_id: 4, asset_name: '黎明序曲', asset_thumb: '', total_earnings: 4320, rank: 4 },
{ asset_id: 5, asset_name: '深海回响', asset_thumb: '', total_earnings: 3980, rank: 5 },
],
},
}
}
// 6. 藏品等级分布
export async function mockLevelDistribution({ star_id }) {
await randomDelay()
const total = 33
return {
code: 200,
data: {
items: [
{ level: 'UR', count: 1, total },
{ level: 'SSR', count: 2, total },
{ level: 'SR', count: 5, total },
{ level: 'R', count: 6, total },
{ level: 'N', count: 0, total },
],
},
}
}
// 7. 升级进度
export async function mockUpgradeProgress({ star_id }) {
await randomDelay()
return {
code: 200,
data: {
upcoming: [
{ asset_id: 1, asset_name: '璀璨星河', asset_thumb: '', like_progress: 73, duration_progress: 92 },
{ asset_id: 2, asset_name: '夏日微风', asset_thumb: '', like_progress: 75, duration_progress: 96 },
{ asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '', like_progress: 97, duration_progress: 71 },
],
recent: [
{ asset_id: 4, asset_name: '黎明序曲', asset_thumb: '', new_level: 'SSR', upgrade_time: Date.now() - 3600000 },
{ asset_id: 5, asset_name: '深海回响', asset_thumb: '', new_level: 'SR', upgrade_time: Date.now() - 86400000 },
{ asset_id: 6, asset_name: '晨曦微光', asset_thumb: '', new_level: 'SR', upgrade_time: Date.now() - 172800000 },
],
},
}
}
// 路由表:endpoints 字符串 → mock 工厂
export const mockRouter = {
'/api/v1/dashboard/today-overview': mockTodayOverview,
'/api/v1/dashboard/income-curve': mock7DayIncomeCurve,
'/api/v1/dashboard/exhibition-summary': mockExhibitionSummary,
'/api/v1/dashboard/like-income-by-level': mockLikeIncomeByLevel,
'/api/v1/dashboard/top-assets': mockTopAssets,
'/api/v1/dashboard/level-distribution': mockLevelDistribution,
'/api/v1/dashboard/upgrade-progress': mockUpgradeProgress,
}
```
- [ ] **Step 2: 在 `frontend/utils/api.js` 末尾追加 `dashboardApi` 命名空间**
```javascript
// ==================== 数据看板 ====================
import { mockRouter } from './mock/dashboard'
const DASHBOARD_PREFIX = '/api/v1/dashboard'
// mock 触发器:当 USE_MOCK_API 为 true 时短路返回 mock 数据
// 后端就绪后将 USE_MOCK_API 改为 false 即可
async function dashboardRequest(endpoint, params = {}) {
if (USE_MOCK_API) {
const factory = mockRouter[`${DASHBOARD_PREFIX}${endpoint}`]
if (factory) return factory(params)
}
return request({ url: `${DASHBOARD_PREFIX}${endpoint}`, method: 'GET', data: params })
}
export const dashboardApi = {
getTodayOverview: (starId) => dashboardRequest('/today-overview', { star_id: starId }).then((r) => r.data),
get7DayIncomeCurve: (starId) => dashboardRequest('/income-curve', { star_id: starId }).then((r) => r.data),
getExhibitionSummary: (starId) => dashboardRequest('/exhibition-summary', { star_id: starId }).then((r) => r.data),
getLikeIncomeByLevel: (starId) => dashboardRequest('/like-income-by-level', { star_id: starId }).then((r) => r.data),
getTopAssets: (starId) => dashboardRequest('/top-assets', { star_id: starId }).then((r) => r.data),
getLevelDistribution: (starId) => dashboardRequest('/level-distribution', { star_id: starId }).then((r) => r.data),
getUpgradeProgress: (starId) => dashboardRequest('/upgrade-progress', { star_id: starId }).then((r) => r.data),
}
```
- [ ] **Step 3: 打开 `USE_MOCK_API` 开关(关键前置步骤)**
> **重要**:项目 `frontend/utils/api.js` 第 9 行 `USE_MOCK_API` 默认是 `false`,会直接请求真实后端。后端尚未实现,必须先翻成 `true` 才能用 mock 验证。
修改 `frontend/utils/api.js`:
```javascript
// 第 9 行:
// 原始: const USE_MOCK_API = false
// 改为:
const USE_MOCK_API = true
```
> **切回真实接口**:后端 `/api/v1/dashboard/*` 7 个接口联调通过后,把这里改回 `false` 即可。
- [ ] **Step 4: 手动验证 mock 触发**
在 H5 dev console 跑:
```js
const { dashboardApi } = await import('/utils/api.js')
const today = await dashboardApi.getTodayOverview(1)
console.log(today) // 预期: { crystal_balance: 2713, today_income: 213, week_rank: 12 }
```
- [ ] **Step 5: Commit**
```bash
git add frontend/utils/mock/dashboard.js frontend/utils/api.js
git commit -m "feat(dashboard): 追加 mock 数据工厂 + dashboardApi 命名空间 + 打开 USE_MOCK_API"
```
---
## Task 3: useDashboardData Composable
**Files:**
- Create: `frontend/composables/useDashboardData.js`
- [ ] **Step 1: 完整实现 composable**
```javascript
import { ref, computed, onScopeDisposal } from 'vue'
import { dashboardApi } from '@/utils/api'
/**
* 数据看板聚合 composable
* - 7 个接口并发(Promise.allSettled,单失败不阻塞)
* - 按 section 维度暴露 loading/error/data
* - 30 分钟内 refresh 不重发请求(lastFetched 缓存)
* - effectScope + onScopeDisposal 自动清理(页面 onUnmounted 调用 dispose)
*
* @param {object} options
* @param {number|null} options.starId 顶粉星城 ID
* @returns {UseDashboardDataReturn}
*/
export function useDashboardData({ starId = null } = {}) {
// —— 内部 state ——
const loading = ref({
overall: false,
today: false,
curve: false,
exhibition: false,
likeIncome: false,
topAssets: false,
levels: false,
upgrades: false,
})
const error = ref({
today: null,
curve: null,
exhibition: null,
likeIncome: null,
topAssets: null,
levels: null,
upgrades: null,
})
const data = ref({
today: null,
curve: null,
exhibition: null,
likeIncome: null,
topAssets: null,
levels: null,
upgrades: null,
})
const lastFetched = ref(0)
const STALE_MS = 30 * 60 * 1000 // 30 分钟
const isReady = computed(() => Object.values(data.value).every((v) => v !== null))
// —— 内部辅助:单 section 加载 ——
async function loadSection(section, fetcher) {
loading.value[section] = true
error.value[section] = null
try {
const result = await fetcher(starId)
data.value[section] = result
} catch (e) {
error.value[section] = e.message || '加载失败'
data.value[section] = null
} finally {
loading.value[section] = false
}
}
// —— 全量加载 ——
async function loadAll(force = false) {
if (!force && Date.now() - lastFetched.value < STALE_MS) return
loading.value.overall = true
try {
// 并发执行,单个失败由 loadSection 内部捕获
await Promise.allSettled([
loadSection('today', dashboardApi.getTodayOverview),
loadSection('curve', dashboardApi.get7DayIncomeCurve),
loadSection('exhibition', dashboardApi.getExhibitionSummary),
loadSection('likeIncome', dashboardApi.getLikeIncomeByLevel),
loadSection('topAssets', dashboardApi.getTopAssets),
loadSection('levels', dashboardApi.getLevelDistribution),
loadSection('upgrades', dashboardApi.getUpgradeProgress),
])
lastFetched.value = Date.now()
} finally {
loading.value.overall = false
}
}
// —— 局部刷新 ——
// - refresh(section) : 单 section 强制重拉(无缓存)
// - refresh() : 全量刷新,cache-aware(30 分钟内复用)
// - refresh(null, true) : 全量强制刷新(绕缓存)
async function refresh(section, force = false) {
if (section) {
const fetcherMap = {
today: dashboardApi.getTodayOverview,
curve: dashboardApi.get7DayIncomeCurve,
exhibition: dashboardApi.getExhibitionSummary,
likeIncome: dashboardApi.getLikeIncomeByLevel,
topAssets: dashboardApi.getTopAssets,
levels: dashboardApi.getLevelDistribution,
upgrades: dashboardApi.getUpgradeProgress,
}
if (!fetcherMap[section]) return
return loadSection(section, fetcherMap[section])
}
return loadAll(force)
}
// —— effectScope 资源释放 ——
// 调用方在页面 onUnmounted 中调用 dispose()
function dispose() {
// ref 状态由 Vue 自动 GC;此处保留接口给未来清理(如取消 in-flight 请求)
// 当前实现:无 in-flight 引用,无需额外清理
}
// 自动调用一次首屏加载
loadAll()
return {
loading,
error,
data,
refresh,
isReady,
lastFetched,
dispose,
}
}
```
- [ ] **Step 2: 手动验证 composable 行为**
在 H5 dev console 跑:
```js
const { useDashboardData } = await import('/composables/useDashboardData.js')
const { data, loading, isReady, refresh } = useDashboardData({ starId: 1 })
// 等 ~1 秒
setTimeout(() => {
console.log('isReady:', isReady.value) // true
console.log('today:', data.value.today) // { crystal_balance: 2713, ... }
console.log('curve.points.length:', data.value.curve.points.length) // 7
console.log('exhibition.top5.length:', data.value.exhibition.top5.length) // 5
}, 1500)
```
预期:所有数据加载完成,形状如 mock。
- [ ] **Step 3: 验证局部刷新**
```js
// 在同一 console 继续
await refresh('curve')
console.log('loading.curve:', loading.value.curve) // 预期: false(load 完自动归位)
console.log('data.curve:', data.value.curve !== null) // true
```
- [ ] **Step 4: Commit**
```bash
git add frontend/composables/useDashboardData.js
git commit -m "feat(dashboard): useDashboardData composable(7接口并发+section级refresh)"
```
---
## Task 4: dashboard.vue 页面骨架
**Files:**
- Create: `frontend/pages/dashboard/dashboard.vue`
- [ ] **Step 1: 完整实现页面骨架**
```vue
骨架页(组件将在 Task 5-10 添加)
🏆
赛季总览 · 即将上线
历史赛季数据正在筹备中
```
- [ ] **Step 2: 创建 `DashboardHeader.vue` 占位(Task 5 会完整实现)**
文件 `frontend/pages/dashboard/components/DashboardHeader.vue`:
```vue
```
- [ ] **Step 3: 手动验证骨架页**
访问 H5 dev URL `/pages/dashboard/dashboard`:
- 预期:红色装饰背景 + "数据看板" 标题 + 两个 Tab + "骨架页" 占位文字
- 切换 Tab:右半部分在占位和"赛季总览"间切换
- 打开 devtools console:1.5 秒后 `data.today` 等字段非空(可通过 Vue devtools 检查)
- [ ] **Step 4: Commit**
```bash
git add frontend/pages/dashboard/dashboard.vue frontend/pages/dashboard/components/DashboardHeader.vue
git commit -m "feat(dashboard): 页面骨架 + Header 占位"
```
---
## Task 5: DashboardHeader 完整实现(毛绒怪 + 渐变标题)
**Files:**
- Modify: `frontend/pages/dashboard/components/DashboardHeader.vue`(替换占位为完整实现)
- [ ] **Step 1: 替换为完整实现**
```vue
```
- [ ] **Step 2: 手动验证**
访问 H5 dev URL `/pages/dashboard/dashboard`:
- 预期:粉红渐变背景 + 红色光晕 + 圆形毛绒怪占位(带红色光晕阴影)+ 渐变文字"数据看板"+ 紫蓝粉渐变 Tab 胶囊
- 切换 Tab:胶囊平滑滑动
- [ ] **Step 3: Commit**
```bash
git add frontend/pages/dashboard/components/DashboardHeader.vue
git commit -m "feat(dashboard): Header 完整视觉(毛绒怪占位+渐变标题+Tab胶囊)"
```
---
## Task 6: CrystalOverview 组件(顶部双卡)
**Files:**
- Create: `frontend/pages/dashboard/components/CrystalOverview.vue`
- [ ] **Step 1: 完整实现**
```vue
加载失败,点击重试
水晶余额
{{ data.crystal_balance }}
今日收益
+ {{ data.today_income }}
```
- [ ] **Step 2: 在 dashboard.vue 中挂载组件**
修改 `frontend/pages/dashboard/dashboard.vue` 的 template:
把
```vue
骨架页(组件将在 Task 5-10 添加)
```
替换为:
```vue
```
并在 components 中追加:
```js
components: { DashboardHeader, CrystalOverview },
```
- [ ] **Step 3: 手动验证**
刷新 H5 dev URL:
- 预期:双卡显示"水晶余额 2713"和"今日收益 + 213",金黄色数字 + 渐变背景
- 1.5s 后才显示(loading 期间是骨架屏)
- [ ] **Step 4: Commit**
```bash
git add frontend/pages/dashboard/components/CrystalOverview.vue frontend/pages/dashboard/dashboard.vue
git commit -m "feat(dashboard): CrystalOverview 双卡(水晶余额+今日收益)"
```
---
## Task 7: IncomeCurve 组件(uCharts 七日柱状+折线)
**Files:**
- Create: `frontend/pages/dashboard/components/IncomeCurve.vue`
- [ ] **Step 1: 完整实现 IncomeCurve.vue**
```vue
七日收益曲线
加载失败,点击重试
📊 {{ points.length }} 天数据
App 端图表组件配置中
```
- [ ] **Step 3: 在 dashboard.vue 中挂载组件**
在 CrystalOverview 之后追加:
```vue
```
components 追加 `IncomeCurve`。
- [ ] **Step 4: 安装 uCharts**
```bash
cd frontend && npm install qiun-data-charts
```
如项目无 H5 组件库依赖关系导致构建失败,回退到 `chart-theme.json` + 占位文字(Step 2 中已留 fallback)。
- [ ] **Step 5: 手动验证**
刷新 H5 dev URL:
- 预期:渐变卡片 + "七日收益曲线" 标题 + 柱状图(7 根柱,渐变填充)+ 折线(蓝黄渐变)+ 高亮当日 "+ 312" + 日期
- 加载中:渐变骨架屏闪烁
- [ ] **Step 6: Commit**
```bash
git add frontend/pages/dashboard/components/IncomeCurve.vue frontend/pages/dashboard/dashboard.vue frontend/package.json frontend/package-lock.json
git commit -m "feat(dashboard): IncomeCurve 七日柱状+折线(uCharts H5)"
```
---
## Task 8: ExhibitionCenter 组件(3 联 + 5 行表格)
**Files:**
- Create: `frontend/pages/dashboard/components/ExhibitionCenter.vue`
- [ ] **Step 1: 完整实现**
```vue
展出收益中心
加载失败,点击重试
{{ data.exhibiting_count }} / {{ data.starbook_count }}
展出中 / 星册中
{{ data.total_duration }}
累计展出时长
{{ data.total_earnings }}
累计展出收益
🎨
{{ item.duration_7d }}
{{ item.earnings_7d }}
{{ item.avg_earnings }} / H
```
- [ ] **Step 2: 在 dashboard.vue 中挂载**
在 IncomeCurve 后追加:
```vue
```
components 追加 `ExhibitionCenter`。
- [ ] **Step 3: 手动验证**
刷新 H5 dev URL:
- 预期:玻璃拟态卡片 + "展出收益中心" 标题 + 3 联统计(21/33、712:13:56、39721)+ 5 行表格(每行 4×5 缩略图 + 时长 + 收益 + 平均)
- [ ] **Step 4: Commit**
```bash
git add frontend/pages/dashboard/components/ExhibitionCenter.vue frontend/pages/dashboard/dashboard.vue
git commit -m "feat(dashboard): ExhibitionCenter 3联+5行表格"
```
---
## Task 9: LikeIncomeBoard 组件
**Files:**
- Create: `frontend/pages/dashboard/components/LikeIncomeBoard.vue`
- [ ] **Step 1: 完整实现**
```vue
点赞收益看板
加载失败,点击重试
{{ stats.total_like_count }}
累积点赞
{{ stats.total_income }}
累计收益
{{ item.level }}
{{ item.level }}
{{ item.total_income }}
```
- [ ] **Step 2: 在 dashboard.vue 中挂载**
在 ExhibitionCenter 后追加:
```vue
```
components 追加 `LikeIncomeBoard`。
- [ ] **Step 3: 手动验证 + Commit**
```bash
git add frontend/pages/dashboard/components/LikeIncomeBoard.vue frontend/pages/dashboard/dashboard.vue
git commit -m "feat(dashboard): LikeIncomeBoard 左侧统计+右侧等级列表"
```
---
## Task 10: CollectionMatrix + TopFiveAssets
**Files:**
- Create: `frontend/pages/dashboard/components/CollectionMatrix.vue`
- Create: `frontend/pages/dashboard/components/TopFiveAssets.vue`
- [ ] **Step 1: 完整实现 TopFiveAssets.vue**
```vue
历史藏品收益 TOP 5
暂无数据
🏆
TOP {{ item.rank }}
```
- [ ] **Step 2: 完整实现 CollectionMatrix.vue(容器,先只接 TopFiveAssets)**
```vue
藏品矩阵
```
- [ ] **Step 3: 在 dashboard.vue 中挂载**
在 LikeIncomeBoard 后追加:
```vue
```
components 追加 `CollectionMatrix`。
- [ ] **Step 4: 手动验证 + Commit**
```bash
git add frontend/pages/dashboard/components/TopFiveAssets.vue frontend/pages/dashboard/components/CollectionMatrix.vue frontend/pages/dashboard/dashboard.vue
git commit -m "feat(dashboard): CollectionMatrix 容器 + TopFiveAssets"
```
---
## Task 11: LevelDistribution 组件(5 个 conic-gradient 环形图)
**Files:**
- Create: `frontend/pages/dashboard/components/LevelDistribution.vue`
- [ ] **Step 1: 完整实现**
```vue
藏品等级分布
暂无数据
{{ item.count }}
{{ item.level }}
{{ getPercent(item) }}%
```
- [ ] **Step 2: 在 CollectionMatrix.vue 中挂载**
在 `` 后追加:
```vue
```
并追加 import 和 components 注册。
- [ ] **Step 3: 手动验证 + Commit**
```bash
git add frontend/pages/dashboard/components/LevelDistribution.vue frontend/pages/dashboard/components/CollectionMatrix.vue
git commit -m "feat(dashboard): LevelDistribution 5个conic-gradient环形图"
```
---
## Task 12: UpcomingUpgrades + RecentUpgrades
**Files:**
- Create: `frontend/pages/dashboard/components/UpcomingUpgrades.vue`
- Create: `frontend/pages/dashboard/components/RecentUpgrades.vue`
- [ ] **Step 1: 完整实现 UpcomingUpgrades.vue**
```vue
即将升级
暂无数据
{{ item.asset_name[0] }}
{{ item.like_progress }}%
{{ item.duration_progress }}%
```
- [ ] **Step 2: 完整实现 RecentUpgrades.vue**
```vue
最近升级
暂无数据
{{ item.asset_name[0] }}
{{ item.asset_name }}
{{ formatTime(item.upgrade_time) }}
{{ item.new_level }}
Lv UP
```
- [ ] **Step 3: 在 CollectionMatrix.vue 中挂载(两列布局)**
替换 CollectionMatrix.vue 的 template 为:
```vue
藏品矩阵
```
并在 `
```
- [ ] **Step 2: 确认 `pages.json` 中 dashboard 条目未启用 `enablePullDownRefresh`**
Task 1 写入的 dashboard 条目已正确(不包含 `enablePullDownRefresh`)。**不要**手动加这个字段——下拉刷新由 scroll-view 的 `:refresher-enabled="true"` 接管,两者重复会冲突。
- [ ] **Step 3: 手动验证**
刷新 H5 dev URL:
- 拖动下拉 → 触发强制刷新(loading.overall 短暂为 true)
- 切换 Tab 1 → Tab 2 → Tab 1:30 分钟内 cache-aware 静默刷新(不闪骨架)
- 离开页面到其他页面 → 返回:onShow 触发 force refresh
- 关闭网络(DevTools Network → Offline)→ 各 section 显示错误态
- 恢复网络 → 点击错误卡片 → 该 section 重试成功
- [ ] **Step 4: 验证 Figma 视觉一致性**
用 `superpowers:verification-before-completion` 流程对照 `docs/figma-analysis-data-dashboard.md` 第三节设计 token 逐项核对:
- [ ] 颜色 token 与 §3.1 一致
- [ ] 阴影 token 与 §3.2 一致
- [ ] 圆角与 §3.3 一致
- [ ] 5 个等级色与 §2.7.2 一致
- [ ] **Step 5: Commit**
```bash
git add frontend/pages/dashboard/dashboard.vue
git commit -m "feat(dashboard): onShow 强制刷新 + Tab 缓存 + 下拉刷新(scroll-view refresher)"
```
## Task 14: 全链路手动验证
**Files:** 无(验证任务)
- [ ] **Step 1: 执行 `superpowers:verification-before-completion` 流程**
逐项对照 spec §8.2 验收清单:
| 项 | 预期 | 实际 | 通过 |
|----|------|------|------|
| 视觉对齐 Figma 稿 | 6 模块结构、token 一致 | | ☐ |
| Tab 切换不重发请求 | 30 分钟内切回 Tab 1,无网络请求 | | ☐ |
| 下拉刷新拉新数据 | 拖动触发,loading.overall 亮起 | | ☐ |
| 飞行模式各 section 独立重试 | DevTools Offline → 各 section 显示错误态 → 恢复后点击重试成功 | | ☐ |
| 后端 401 跳登录 | 临时改 mock 抛 401 → 跳登录页 | | ☐ |
| 移动端 320px 小屏无溢出 | Chrome DevTools 切到 320×568,无横向滚动条 | | ☐ |
| uCharts 渲染正常 | 七日柱状+折线显示,peak 高亮 | | ☐ |
| 5 个等级环渐变正确 | UR/SSR/SR/R/N 渐变色与 spec 一致 | | ☐ |
| Tab 2 占位 | "赛季总览 · 即将上线",无网络请求 | | ☐ |
| onShow 强制刷新 | 离开页面到任意页 → 返回 → 看到新数据 | | ☐ |
- [ ] **Step 2: 通过所有项 → 标记 done**
如全部通过,本任务可视为完成;如有失败项,**回到对应 Task 修复后重跑本表**。
如有任何项需要修复但没有对应 Task(例如"uCharts 主题色微调"),追加新 Task 而非挤入本任务。
- [ ] **Step 3: 不需要 commit(验证任务,无代码变更)**
---
---
## 风险与决策记录
| 风险 | 应对 |
|------|------|
| uCharts 在小程序/App 端需单独配置 | Task 7 留 fallback 占位;App 端后续用 `qiun-data-charts` 原生插件补充 |
| 后端接口契约变更 | proto 是单一信源(spec §4.2),mock 与接口字段同步更新 |
| 视觉稿 1:1 像素级还原 | Task 14 末提供核对清单;超出 token 范围的微调由 UI 评审 |
| 7 接口并发 401 处理 | `utils/api.js` 已有统一处理(清除 token + 跳登录),无需 dashboard 额外处理 |
## 后续可扩展(不在本计划)
- Tab 2 "赛季总览" 真实数据(spec §7.3)
- IncomeCurve 点击柱子弹窗(spec §7.3)
- 升级提醒推送(spec §7.3)
- 视觉资源(Figma 素材)批量下载与替换占位
## 变更记录
| 日期 | 变更 | 作者 |
|------|------|------|
| 2026-06-02 | 初版 | Claude |