diff --git a/docs/superpowers/plans/2026-06-02-data-dashboard-frontend.md b/docs/superpowers/plans/2026-06-02-data-dashboard-frontend.md new file mode 100644 index 0000000..1870e5a --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-data-dashboard-frontend.md @@ -0,0 +1,2982 @@ +# 数据看板前端 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(15 个)**: +- `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/pages/dashboard/components/SectionSkeleton.vue` +- `frontend/pages/dashboard/components/EmptyState.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: 手动验证 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 4: Commit** + +```bash +git add frontend/utils/mock/dashboard.js frontend/utils/api.js +git commit -m "feat(dashboard): 追加 mock 数据工厂 + dashboardApi 命名空间" +``` + +--- + +## 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 + } + } + + // —— 局部刷新 —— + async function refresh(section) { + if (!section) { + return loadAll(true) + } + 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 + await loadSection(section, fetcherMap[section]) + } + + // —— 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 + + + + + +``` + +- [ ] **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 + + + + + +``` + +- [ ] **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` +- Create: `frontend/pages/dashboard/chart-theme.json`(uCharts 主题配置) + +- [ ] **Step 1: 创建 uCharts 主题配置 `frontend/pages/dashboard/chart-theme.json`** + +> **注意**:此文件路径在 Vue 项目中通过 `?url` 或 `import` 引入。H5 用 `import`,小程序/App 走条件编译。 + +```json +{ + "type": "barline", + "color": ["#FFCC14", "#1BAFEE"], + "padding": [16, 16, 8, 16], + "dataLabel": false, + "legend": { "show": false }, + "xAxis": { + "disableGrid": true, + "axisLine": false, + "fontColor": "#FFFFFF", + "fontSize": 9 + }, + "yAxis": { + "data": [{ "min": 0 }], + "disableGrid": true, + "axisLine": false, + "fontColor": "#FFFFFF", + "fontSize": 9 + }, + "extra": { + "bar": { + "type": "group", + "width": 18, + "activeBgColor": "#000000", + "activeBgOpacity": 0.1, + "linear": true, + "color": ["#FFDF77", "#B984FF", "#FF8183"] + }, + "line": { + "type": "curve", + "width": 2, + "activeType": "hollow" + } + } +} +``` + +- [ ] **Step 2: 完整实现 IncomeCurve.vue** + +```vue + + + + + +``` + +- [ ] **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/pages/dashboard/chart-theme.json 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 + + + + + +``` + +- [ ] **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 + + + + + +``` + +- [ ] **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 + + + + + +``` + +- [ ] **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 + + + + + +``` + +- [ ] **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 + + + + + +``` + +- [ ] **Step 2: 完整实现 RecentUpgrades.vue** + +```vue + + + + + +``` + +- [ ] **Step 3: 在 CollectionMatrix.vue 中挂载(两列布局)** + +替换 CollectionMatrix.vue 的 template 为: +```vue + +``` + +并在 ` +``` + +- [ ] **Step 2: 完整实现 EmptyState.vue** + +```vue + + + + + +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/pages/dashboard/components/SectionSkeleton.vue frontend/pages/dashboard/components/EmptyState.vue +git commit -m "feat(dashboard): SectionSkeleton + EmptyState 通用组件" +``` + +> **注**:本任务暂不替换各 section 内的内联骨架(已能用),后续若需要统一可在各组件中替换 `` 为 ``。 + +--- + +## Task 14: onShow 刷新 + Tab 缓存 + 下拉刷新 + +**Files:** +- Modify: `frontend/pages/dashboard/dashboard.vue` + +- [ ] **Step 1: 加入 onShow 静默刷新 + Tab 缓存** + +完整替换 `