# 视觉与动效全量升级 — 实施计划 > **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:** 将平板支撑训练小程序从"主流小程序"提升至"独立精品 App"视觉水平,包含深色模式、骨架屏、悬浮 TabBar、数字滚动、页面转场、排名动画、全屏彩带、空状态插画等 8 项升级。 **Architecture:** 复用现有 `utils/theme.js` CSS 变量体系;新增 1 个组件 (`ui-skeleton`) + 2 个工具 (`countUp`, `darkMode`);改 10 个现有文件;零第三方依赖,零预算。 **Tech Stack:** 微信小程序原生 (WXML/WXSS/JS)、CSS3 keyframes、Canvas 2D、`wx.onThemeChange`、`wx.getSystemInfoSync`。 **Spec:** `docs/superpowers/specs/2026-07-08-visual-upgrade-design.md` --- ## 文件结构 ### 新增 - `utils/countUp.js` — 数字滚动工具 - `utils/darkMode.js` — 系统主题监听 - `components/ui-skeleton/ui-skeleton.js` — 骨架屏组件逻辑 - `components/ui-skeleton/ui-skeleton.json` — 组件配置 - `components/ui-skeleton/ui-skeleton.wxml` — 组件模板 - `components/ui-skeleton/ui-skeleton.wxss` — 组件样式 ### 修改 - `app.json` — 加 `darkmode: true` 与 theme 配置 - `app.wxss` — 加深色变量、渐变背景、页面入场动画类 - `utils/theme.js` — 扩展 `getThemeStyle(theme, isDark)` 支持深色 - `custom-tab-bar/index.wxml` + `index.wxss` — 悬浮 Dock 样式 - `pages/index/index.wxml` + `.wxss` — 适配渐变 + 入场动画 - `pages/timer/timer.wxml` + `.wxss` + `.js` — countUp + 全屏彩带 - `pages/leaderboard/leaderboard.wxml` + `.wxss` — 骨架屏 + 排名动画 + 空状态 - `pages/records/records.wxml` + `.wxss` — 骨架屏 + 空状态 - `pages/settings/settings.wxml` + `.js` — 深色模式开关 ### 不改 - `cloudfunctions/`、`utils/storage.js`、`utils/plan.js`、`utils/timer.js`、`utils/voice.js`、`utils/util.js`、`utils/cloud.js`、训练业务逻辑 --- ## Task 顺序与依赖 ``` Task 1 (深色模式基础设施) ↓ Task 2 (渐变背景) ↓ Task 3 (页面级入场动画) ↓ Task 4 (ui-skeleton 组件) ↓ Task 5 (骨架屏 → leaderboard) ↓ Task 6 (骨架屏 → records) ↓ Task 7 (countUp 工具) ↓ Task 8 (timer 接入 countUp) ↓ Task 9 (timer 全屏彩带) ↓ Task 9.5 (ui-btn 涟漪) ↓ Task 10 (浮动 Dock TabBar) ↓ Task 11 (排名位移动画) ↓ Task 12 (空状态插画) ↓ Task 13 (设置页深色开关) ↓ Task 14 (最终验收) ``` --- ## Task 1: 深色模式基础设施 **Files:** - Create: `utils/darkMode.js` - Modify: `utils/theme.js` - Modify: `app.wxss` - Modify: `app.json` - Modify: `app.js` ### Step 1: 创建 `utils/darkMode.js` ```js /** * 系统深色模式监听 — 提供初始值与变更回调 * 旧设备 wx.onThemeChange 失败时退化为只读一次 */ const STORAGE_KEY = 'dark_mode_pref' function _readSystemTheme() { try { const info = wx.getSystemInfoSync() return info.theme === 'dark' ? true : false } catch (e) { return false } } function getInitialDarkMode() { const pref = wx.getStorageSync(STORAGE_KEY) || 'system' if (pref === 'system') return _readSystemTheme() return pref === 'dark' } function getPref() { return wx.getStorageSync(STORAGE_KEY) || 'system' } function setPref(value) { if (!['system', 'light', 'dark'].includes(value)) return wx.setStorageSync(STORAGE_KEY, value) } function watchDarkMode(onChange) { if (typeof wx.onThemeChange !== 'function') return try { wx.onThemeChange(({ theme }) => { const pref = getPref() if (pref === 'system') onChange(theme === 'dark') }) } catch (e) { // 旧设备不支持,忽略 } } module.exports = { getInitialDarkMode, getPref, setPref, watchDarkMode } ``` ### Step 2: 扩展 `utils/theme.js` 在 `utils/theme.js` 中替换 `getThemeStyle` 函数(第 97-100 行): ```js const getThemeStyle = (theme, isDark = false) => { const t = theme || getCurrentTheme() const baseVars = isDark ? '--text:#E5E5E7;--text-secondary:#98989F;--bg:#0F0F12;--card-bg:#1C1C1E;--border:#2C2C2E;--success:#30D158;' : '--text:#333333;--text-secondary:#999999;--bg:#F5F5F5;--card-bg:#FFFFFF;--border:#EEEEEE;--success:#4CAF50;' return `${baseVars}--primary:${t.primary};--primary-light:${t.primaryLight};--primary-bg:${t.primaryBg};--primary-rgb:${t.primaryRgb};` } ``` 在 `applyThemeToPage` 函数(第 102-111 行)改为: ```js const applyThemeToPage = (self) => { const darkMod = require('./darkMode') const theme = getCurrentTheme() const isDark = darkMod.getInitialDarkMode() const nextStyle = getThemeStyle(theme, isDark) if (self.data && self.data.themeStyle === nextStyle) { return } self.setData({ theme, themeStyle: nextStyle, isDark }) _setNavBarColor(theme.primary) } ``` ### Step 3: 在 `app.wxss` 顶部追加深色安全区 `app.wxss` 第 1-14 行替换为: ```css page { --text: #333333; --text-secondary: #999999; --bg: #F5F5F5; --card-bg: #FFFFFF; --success: #4CAF50; --border: #EEEEEE; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; font-size: 28rpx; color: var(--text); background-color: var(--bg) !important; transition: background-color 0.3s ease, color 0.3s ease; } ``` ### Step 4: 修改 `app.json` 启用深色模式 `app.json` 在第 28 行 `sitemapLocation` 之后加一行(注意末尾无逗号): ```json { "pages": [...], "window": {...}, "tabBar": {...}, "darkmode": true, "themeLocation": "theme.json", "sitemapLocation": "sitemap.json" } ``` ### Step 5: 创建 `theme.json` 新建 `theme.json`(与 `app.json` 同级): ```json { "light": { "navigationBarBackgroundColor": "#FF6B35", "navigationBarTextStyle": "white", "backgroundColor": "#F5F5F5", "backgroundColorTop": "#F5F5F5", "backgroundColorBottom": "#F5F5F5" }, "dark": { "navigationBarBackgroundColor": "#FF6B35", "navigationBarTextStyle": "white", "backgroundColor": "#0F0F12", "backgroundColorTop": "#0F0F12", "backgroundColorBottom": "#0F0F12" } } ``` ### Step 6: 修改 `app.js` 启动监听 在 `app.js` 的 `onLaunch` 函数开头(第 6 行)插入: ```js async onLaunch() { // Init dark mode listener const darkMod = require('./utils/darkMode') darkMod.watchDarkMode((isDark) => { // System theme changed; refresh all open pages const pages = getCurrentPages() pages.forEach(p => { if (p && typeof p.applyThemeToPage === 'function') { p.applyThemeToPage() } }) }) // Init cloud storage (no-op if not configured) cloud.init() // ... rest of existing onLaunch ``` ### Step 7: 验证 在微信开发者工具中: 1. 重新编译 2. 顶部模拟器设置切到「深色模式」(或「Dark」) 3. 首页背景应变成 `#0F0F12` 深色 4. Console 应无报错 ### Step 8: 提交 ```bash git add utils/darkMode.js utils/theme.js app.wxss app.json theme.json app.js git commit -m "feat: 深色模式基础设施 (system theme + 跟随系统)" ``` --- ## Task 2: 渐变背景 **Files:** - Modify: `app.wxss` ### Step 1: 在 `app.wxss` 的 `page` 选择器后追加渐变背景 `app.wxss` 找到 `page { ... }` 块(第 1-14 行),在闭合 `}` 后追加: ```css page { --text: #333333; --text-secondary: #999999; --bg: #F5F5F5; --bg-gradient-start: #F5F5F5; --bg-gradient-end: #FAFAFA; --card-bg: #FFFFFF; --success: #4CAF50; --border: #EEEEEE; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; font-size: 28rpx; color: var(--text); background: linear-gradient(180deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%) !important; transition: background-color 0.3s ease, color 0.3s ease; } /* dark mode overrides */ @media (prefers-color-scheme: dark) { page { --bg-gradient-start: #0F0F12; --bg-gradient-end: #14141A; } } ``` ### Step 2: 验证 - 浅色:背景从 `#F5F5F5` 渐变到 `#FAFAFA`(极淡,几乎看不出但是有渐变感) - 深色:背景从 `#0F0F12` 渐变到 `#14141A` - 切换瞬间平滑过渡 ### Step 3: 提交 ```bash git add app.wxss git commit -m "feat: 渐变背景 + 渐变变量" ``` --- ## Task 3: 页面级入场动画 **Files:** - Modify: `app.wxss` - Modify: 5 个页面 WXML: `pages/index/index.wxml`、`pages/timer/timer.wxml`、`pages/records/records.wxml`、`pages/leaderboard/leaderboard.wxml`、`pages/settings/settings.wxml` ### Step 1: 在 `app.wxss` 末尾追加页面入场动画 ```css /* ---- page-level transition ---- */ .page-enter { animation: pageIn 0.35s cubic-bezier(0.2, 0, 0.2, 1) both; } @keyframes pageIn { from { opacity: 0; transform: translateY(20rpx); } to { opacity: 1; transform: translateY(0); } } ``` ### Step 2: 给每个页面的根 `view` 加 `page-enter` 类 5 个页面 WXML 文件的根 `` 改为: **pages/index/index.wxml** 第 1 行: ```xml ``` **pages/timer/timer.wxml** 第 1 行: ```xml ``` **pages/records/records.wxml** 第 1 行: ```xml ``` **pages/leaderboard/leaderboard.wxml** 第 1 行: ```xml ``` **pages/settings/settings.wxml** 第 1 行: ```xml ``` ### Step 3: 验证 - 切换 Tab 4 个页面,每个页面有 ~20rpx 向上滑动 + 渐入效果 - 不应阻塞交互 ### Step 4: 提交 ```bash git add app.wxss pages/*/index.wxml pages/*/*.wxml git commit -m "feat: 页面级入场动画 (fade + translateY)" ``` --- ## Task 4: 新建 `ui-skeleton` 骨架屏组件 **Files:** - Create: `components/ui-skeleton/ui-skeleton.js` - Create: `components/ui-skeleton/ui-skeleton.json` - Create: `components/ui-skeleton/ui-skeleton.wxml` - Create: `components/ui-skeleton/ui-skeleton.wxss` ### Step 1: `components/ui-skeleton/ui-skeleton.js` ```js Component({ properties: { variant: { type: String, value: 'card' }, count: { type: Number, value: 1 }, active: { type: Boolean, value: true } }, data: { _minDisplayShown: false }, lifetimes: { attached() { // 200ms 最小显示,避免骨架闪一下 this._minTimer = setTimeout(() => { this.setData({ _minDisplayShown: true }) }, 200) }, detached() { if (this._minTimer) clearTimeout(this._minTimer) } } }) ``` ### Step 2: `components/ui-skeleton/ui-skeleton.json` ```json { "component": true, "usingComponents": {} } ``` ### Step 3: `components/ui-skeleton/ui-skeleton.wxml` ```xml ``` ### Step 4: `components/ui-skeleton/ui-skeleton.wxss` ```css .skeleton { width: 100%; } .skeleton--active .sk-bar, .skeleton--active .sk-circle { animation: skShimmer 1.4s ease-in-out infinite; background: linear-gradient(90deg, rgba(0, 0, 0, 0.06) 0%, rgba(0, 0, 0, 0.12) 50%, rgba(0, 0, 0, 0.06) 100%); background-size: 200% 100%; } @keyframes skShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .sk-bar { height: 24rpx; border-radius: 8rpx; margin-bottom: 16rpx; background-color: rgba(0, 0, 0, 0.08); } .sk-bar--title { width: 60%; height: 32rpx; } .sk-bar--body { width: 100%; } .sk-bar--small { width: 40%; height: 20rpx; margin-bottom: 0; } .sk-bar--center { width: 60%; height: 40rpx; } .sk-card { background: var(--card-bg, #FFFFFF); border-radius: 24rpx; padding: 32rpx; box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06); } .sk-circle { width: 64rpx; height: 64rpx; border-radius: 50%; background-color: rgba(0, 0, 0, 0.08); flex-shrink: 0; } .sk-circle--big { width: 360rpx; height: 360rpx; display: flex; align-items: center; justify-content: center; margin: 0 auto; } .sk-list { display: flex; align-items: center; padding: 24rpx 0; gap: 24rpx; } .sk-list-text { flex: 1; display: flex; flex-direction: column; gap: 8rpx; } /* dark mode */ @media (prefers-color-scheme: dark) { .sk-card { background: var(--card-bg, #1C1C1E); } .skeleton--active .sk-bar, .skeleton--active .sk-circle { background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.10) 50%, rgba(255, 255, 255, 0.05) 100%); background-size: 200% 100%; } .sk-bar, .sk-circle { background-color: rgba(255, 255, 255, 0.06); } } ``` ### Step 5: 验证 - 在某个页面临时放 `` - 看到一张带 3 条灰色横条的占位卡片 - 切换深色模式,占位颜色应自动转深 ### Step 6: 提交 ```bash git add components/ui-skeleton/ git commit -m "feat: 新增 ui-skeleton 骨架屏组件 (card / list-row / circle)" ``` --- ## Task 5: 骨架屏应用到 leaderboard 页面 **Files:** - Modify: `pages/leaderboard/leaderboard.wxml` ### Step 1: 替换加载状态区块 `pages/leaderboard/leaderboard.wxml` 第 13-16 行: ```xml 加载中... ``` 替换为: ```xml ``` ### Step 2: 验证 - 进入排行页,先看到 5 行骨架,数据到达后切换到真实列表 - 切换 < 200ms 时骨架不闪烁(被 200ms 最小定时器遮蔽) ### Step 3: 提交 ```bash git add pages/leaderboard/leaderboard.wxml git commit -m "feat: leaderboard 页面接入骨架屏" ``` --- ## Task 6: 骨架屏应用到 records 页面 **Files:** - Modify: `pages/records/records.wxml` ### Step 1: 在统计卡片和日历热力图位置插入骨架 `pages/records/records.wxml` 第 1-2 行 `` 后立即插入: ```xml ``` 但 `loading` 字段可能不存在于 records 页。需先在 `pages/records/records.js` 顶部加 `loading: true,` 初始值,并在 `onShow` 末尾 setData 加载完成后置 false。 ### Step 2: 在 `pages/records/records.js` 加 loading 状态 文件顶部 data 段加: ```js data: { loading: true, // ... existing fields } ``` 找到 `onShow` 末尾,`setData` 调用后追加: ```js this.setData({ loading: false }) ``` (具体位置需根据实际 JS 文件确认,详见 `pages/records/records.js` 第 ~40 行附近) ### Step 3: 验证 - 进入记录页,先看到 2 张骨架卡片 - 数据加载完切换到真实统计+日历 ### Step 4: 提交 ```bash git add pages/records/records.wxml pages/records/records.js git commit -m "feat: records 页面接入骨架屏" ``` --- ## Task 7: 创建 `utils/countUp.js` **Files:** - Create: `utils/countUp.js` ### Step 1: 完整内容 ```js /** * 数字滚动工具 — 替代第三方库 * 用 setInterval(fn, 16) 模拟 60fps,兼容性最稳 * 使用 easeOut 缓动: 1 - (1-t)^3 * * 使用: * const task = countUp({ from: 0, to: 30, duration: 800, onUpdate: setDisplayTime }) * // 不再需要时: * task.cancel() */ function countUp({ from = 0, to, duration = 800, decimals = 0, onUpdate, onComplete } = {}) { // 边界:数值相同 或 时长 <= 0,跳过动画 if (from === to || duration <= 0) { if (typeof onUpdate === 'function') onUpdate(to) if (typeof onComplete === 'function') onComplete() return { cancel: () => {} } } const start = Date.now() const delta = to - from let cancelled = false const tick = () => { if (cancelled) return const elapsed = Date.now() - start let t = Math.min(elapsed / duration, 1) // easeOut cubic t = 1 - Math.pow(1 - t, 3) const current = from + delta * t const value = decimals > 0 ? Number(current.toFixed(decimals)) : Math.round(current) if (typeof onUpdate === 'function') onUpdate(value) if (elapsed >= duration) { if (typeof onComplete === 'function') onComplete() return } setTimeout(tick, 16) } setTimeout(tick, 16) return { cancel() { cancelled = true } } } module.exports = { countUp } ``` ### Step 2: 验证(开发者工具 Console 测试) 临时在某页 onLoad 中执行: ```js const { countUp } = require('../../utils/countUp') countUp({ from: 0, to: 100, duration: 1000, onUpdate: (v) => console.log('count:', v) }) ``` Console 应看到数字 0 → 100 平滑递增,约 1 秒完成。 ### Step 3: 提交 ```bash git add utils/countUp.js git commit -m "feat: utils/countUp 数字滚动工具 (easeOut cubic)" ``` --- ## Task 8: timer 页面接入 countUp **Files:** - Modify: `pages/timer/timer.js` ### Step 1: 在 timer.js 引入 countUp 文件顶部加: ```js const { countUp } = require('../../utils/countUp') ``` ### Step 2: 在 data 中加 `_countUpTask` 持有任务 data 段加: ```js _countUpTask: null ``` ### Step 3: 在 `onStart` (或开始训练的入口) 触发 countUp 找到计时器开始函数,在 `setData({ displayTime: duration })` 之前改为: ```js // 取消上一个 task if (this.data._countUpTask) this.data._countUpTask.cancel() const task = countUp({ from: 0, to: duration, duration: 600, onUpdate: (v) => this.setData({ displayTime: v }), onComplete: () => { this.setData({ _countUpTask: null }) } }) this.setData({ _countUpTask: task }) ``` ### Step 4: 在 `onUnload` 取消 ```js onUnload() { if (this.data._countUpTask) this.data._countUpTask.cancel() } ``` ### Step 5: 验证 - 进入计时页,看到 0 滚到目标秒数 - 反复开始/停止/开始 10 次,Console 无 setData 报错 ### Step 6: 提交 ```bash git add pages/timer/timer.js git commit -m "feat: timer 页面接入 countUp 数字滚动" ``` --- ## Task 9: timer 完成全屏彩带 **Files:** - Modify: `pages/timer/timer.wxml` - Modify: `pages/timer/timer.wxss` ### Step 1: 在 timer.wxml 顶部 view 内追加彩带容器 `pages/timer/timer.wxml` 在第 1 行 `` 之后插入: ```xml ``` ### Step 2: 在 timer.wxss 末尾追加彩带样式 ```css /* ---- full-screen confetti ---- */ .confetti { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 9999; overflow: hidden; } .confetti-piece { position: absolute; top: -20rpx; width: 16rpx; height: 32rpx; border-radius: 4rpx; animation: confettiFall 2.4s ease-out forwards; } .c1 { left: 10%; background: #FF6B35; animation-delay: 0.00s; transform: rotate(15deg); } .c2 { left: 22%; background: #FFD93D; animation-delay: 0.10s; transform: rotate(-20deg); } .c3 { left: 34%; background: #4CAF50; animation-delay: 0.20s; transform: rotate(30deg); } .c4 { left: 46%; background: #2979FF; animation-delay: 0.05s; transform: rotate(-10deg); } .c5 { left: 58%; background: #EC407A; animation-delay: 0.15s; transform: rotate(45deg); } .c6 { left: 70%; background: #7E57C2; animation-delay: 0.25s; transform: rotate(-30deg); } .c7 { left: 82%; background: #FF8C5A; animation-delay: 0.08s; transform: rotate(20deg); } .c8 { left: 14%; background: #5C9CFF; animation-delay: 0.18s; transform: rotate(-40deg); } .c9 { left: 38%; background: #6EC072; animation-delay: 0.28s; transform: rotate(50deg); } .c10 { left: 90%; background: #FFD93D; animation-delay: 0.12s; transform: rotate(-25deg); } @keyframes confettiFall { 0% { opacity: 1; transform: translateY(0) rotate(0deg); } 100% { opacity: 0.6; transform: translateY(100vh) rotate(720deg); } } ``` ### Step 3: 验证 - 完成训练,看到 10 条彩色碎片从顶部飘落到屏幕外 - 不阻塞其他交互(`pointer-events: none`) ### Step 4: 提交 ```bash git add pages/timer/timer.wxml pages/timer/timer.wxss git commit -m "feat: timer 完成全屏彩带动画" ``` --- ## Task 10: 浮动 Dock TabBar **Files:** - Modify: `custom-tab-bar/index.wxml` - Modify: `custom-tab-bar/index.wxss` ### Step 1: 替换 `custom-tab-bar/index.wxml` 完整内容: ```xml 训练 记录 排行 设置 ``` ### Step 2: 替换 `custom-tab-bar/index.wxss` 完整内容: ```css .dock { position: fixed; bottom: calc(40rpx + env(safe-area-inset-bottom)); left: 50%; transform: translateX(-50%); width: calc(100% - 80rpx); max-width: 600rpx; z-index: 100; } .dock-inner { display: flex; align-items: center; justify-content: space-around; background: var(--card-bg, #FFFFFF); border-radius: 100rpx; padding: 12rpx 0; box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12), 0 2rpx 8rpx rgba(0, 0, 0, 0.06); transition: background-color 0.3s ease; } .dock-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 8rpx 0; transition: transform 0.2s ease; } .dock-item:active { transform: scale(0.92); } .dock-icon { width: 44rpx; height: 44rpx; background-size: contain; background-repeat: no-repeat; background-position: center; margin-bottom: 4rpx; opacity: 0.5; transition: opacity 0.2s ease; } .dock-text { font-size: 20rpx; color: var(--text-secondary, #999); line-height: 1; transition: color 0.2s ease; } .dock-item.active .dock-icon { opacity: 1; } .dock-item.active .dock-text { color: var(--primary, #FF6B35); font-weight: 600; } /* dark mode */ @media (prefers-color-scheme: dark) { .dock-inner { background: var(--card-bg, #1C1C1E); box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.5), 0 2rpx 8rpx rgba(0, 0, 0, 0.3); } } ``` ### Step 3: 验证 - 4 个 Tab 浮在屏幕底部,圆角胶囊状 - 上下留有安全区,不被 Home Indicator 遮挡 - 切深色,自动转深色背景 ### Step 4: 提交 ```bash git add custom-tab-bar/ git commit -m "feat: 浮动 Dock TabBar 样式" ``` --- ## Task 11: 排名位移动画 **Files:** - Modify: `pages/leaderboard/leaderboard.wxml` - Modify: `pages/leaderboard/leaderboard.wxss` ### Step 1: 在 leaderboard.wxml 给每个 rank-item 加动画延迟 `pages/leaderboard/leaderboard.wxml` 第 27-31 行 (rank-item 容器) 改为: ```xml ``` ### Step 2: 在 leaderboard.wxss 末尾追加位移动画 ```css /* ---- rank item entrance ---- */ .rank-item--enter { animation: rankIn 0.4s cubic-bezier(0.2, 0, 0.2, 1) both; } @keyframes rankIn { from { opacity: 0; transform: translateX(-20rpx); } to { opacity: 1; transform: translateX(0); } } ``` ### Step 3: 验证 - 排行榜加载,前 10 行依次滑入 - 第 11 行以后无延迟(避免最后一行等 600ms) - "我的"bar 也参与 ### Step 4: 提交 ```bash git add pages/leaderboard/leaderboard.wxml pages/leaderboard/leaderboard.wxss git commit -m "feat: 排行榜行依次滑入动画" ``` --- ## Task 12: 空状态插画 **Files:** - Modify: `pages/leaderboard/leaderboard.wxml` - Modify: `pages/leaderboard/leaderboard.wxss` - Modify: `pages/records/records.wxml` - Modify: `pages/records/records.wxss` ### Step 1: 替换 leaderboard 空状态 `pages/leaderboard/leaderboard.wxml` 第 19-23 行 (empty 区块): ```xml 暂无排行数据 快去训练吧! ``` 替换为: ```xml 加油 暂无排行数据 快去训练吧,争当第一名! ``` ### Step 2: 在 leaderboard.wxss 末尾追加空状态插画 ```css /* ---- empty state illustration ---- */ .empty-state { display: flex; flex-direction: column; align-items: center; padding: 80rpx 32rpx; } .empty-illus { position: relative; width: 200rpx; height: 200rpx; margin-bottom: 40rpx; } .empty-illus-body { position: absolute; bottom: 30rpx; left: 50%; transform: translateX(-50%); width: 160rpx; height: 30rpx; background: linear-gradient(90deg, var(--primary), var(--primary-light)); border-radius: 16rpx; } .empty-illus-arm { position: absolute; bottom: 36rpx; width: 20rpx; height: 60rpx; background: var(--primary); border-radius: 10rpx; } .empty-illus-arm--l { left: 30rpx; transform: rotate(30deg); } .empty-illus-arm--r { right: 30rpx; transform: rotate(-30deg); } .empty-illus-bubble { position: absolute; top: 0; right: 0; background: var(--primary-bg); color: var(--primary); font-size: 22rpx; font-weight: 600; padding: 8rpx 16rpx; border-radius: 16rpx; line-height: 1; } .empty-text { font-size: 30rpx; color: var(--text); margin-bottom: 12rpx; } .empty-sub { font-size: 26rpx; color: var(--text-secondary); } ``` ### Step 3: 同样改造 records 页面 empty 状态 `pages/records/records.wxml` 第 58-60 行 (empty 区块) 替换为: ```xml 开始 还没有训练记录 完成第一次训练,开启你的坚持! ``` ### Step 4: 在 records.wxss 末尾追加相同插画样式 复制 Task 12 Step 2 的 `.empty-*` 系列样式粘贴到 `pages/records/records.wxss` 末尾(优先复用,如样式已存在可跳过)。 ### Step 5: 验证 - 排行榜 / 记录页无数据时,显示平板支撑小人简笔 + 气泡 - 5 套主题色小人身体都跟随主题色 - 深色模式正常 ### Step 6: 提交 ```bash git add pages/leaderboard/leaderboard.wxml pages/leaderboard/leaderboard.wxss git add pages/records/records.wxml pages/records/records.wxss git commit -m "feat: 空状态插画 (CSS 简笔小人)" ``` --- ## Task 13: 设置页加深色模式开关 **Files:** - Modify: `pages/settings/settings.wxml` - Modify: `pages/settings/settings.js` ### Step 1: 在 settings.wxml "配色方案" 区块之后插入深色模式卡片 找到 `配色方案` 之后,定位到"训练计划选择"之前,插入: ```xml 深色模式 {{item.name}} ``` ### Step 2: 在 settings.js data 段加 darkMode 相关字段 ```js const darkMod = require('../../utils/darkMode') data: { // ... existing fields darkModePref: 'system', darkModeOptions: [ { key: 'system', name: '跟随系统' }, { key: 'light', name: '浅色' }, { key: 'dark', name: '深色' } ] } ``` ### Step 3: 在 settings.js onShow 读 pref ```js onShow() { // ... existing this.setData({ darkModePref: darkMod.getPref() }) } ``` ### Step 4: 在 settings.js 加 onSelectDarkMode 处理函数 ```js onSelectDarkMode(e) { const key = e.currentTarget.dataset.key darkMod.setPref(key) // 重新计算 isDark 并通知所有页面 const isDark = darkMod.getInitialDarkMode() this.setData({ darkModePref: key }) // 触发所有页面重渲染 const pages = getCurrentPages() pages.forEach(p => { if (p && typeof p.applyThemeToPage === 'function') { p.applyThemeToPage() } }) // 同时应用到本页面 this.applyThemeToPage() } ``` ### Step 5: 验证 - 设置页出现 3 个选项:跟随系统 / 浅色 / 深色 - 选"深色",所有页面立即切到深色 - 选"跟随系统",跟随系统切换 - 选"浅色",系统切深色时仍保持浅色 - 重启后选择被记住(Storage 中 `dark_mode_pref`) ### Step 6: 提交 ```bash git add pages/settings/settings.wxml pages/settings/settings.js git commit -m "feat: 设置页深色模式开关 (3 档)" ``` --- ## Task 9.5: ui-btn 涟漪效果 **Files:** - Modify: `components/ui-btn/ui-btn.wxss` ### Step 1: 在 ui-btn.wxss 末尾追加涟漪 ```css /* ---- ripple effect ---- */ .ui-btn { position: relative; overflow: hidden; } .ui-btn::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.35); transform: translate(-50%, -50%); transition: width 0.5s ease, height 0.5s ease, opacity 0.5s ease; opacity: 0; pointer-events: none; } .ui-btn--primary::before, .ui-btn--ghost::before { background: rgba(255, 255, 255, 0.4); } .ui-btn--outline::before, .ui-btn--danger::before { background: rgba(var(--primary-rgb, 255, 107, 53), 0.15); } .ui-btn:active::before { width: 400rpx; height: 400rpx; opacity: 1; transition: 0s; } ``` ### Step 2: 验证 - 点击 ui-btn(任意 variant),从手指触点扩散出圆形涟漪 - 不影响原有 `transform: scale(0.96)` 按下效果 - 深色模式 ripple 仍可见(透明度足够) ### Step 3: 提交 ```bash git add components/ui-btn/ui-btn.wxss git commit -m "feat: ui-btn 按下涟漪效果" ``` --- ## Task 14: 最终验收 **Files:** - (仅验收,无代码改动) ### Step 1: 跑通 6.1 视觉验收清单 对照 spec §6.1,逐项检查: - [ ] 浅色模式背景是渐变(非纯灰) - [ ] 深色模式所有元素正常转深色 - [ ] 5 套主题 × 2 套明暗 = 10 种组合都正常 - [ ] TabBar 悬浮、圆角、阴影明显 - [ ] 计时器数字完成时滚动归零 - [ ] 完成时全屏彩带飘落 - [ ] 页面级入场动画所有页面正常 - [ ] 排行榜行依次滑入(前 10 行) ### Step 2: 跑通 6.2 性能验收 在微信开发者工具: - 编译 → 性能 → 查看首屏时间 - 切换深色:用 Stopwatch 计时,应 < 100ms - 计时器运行时,Console 无 setData 报错 ### Step 3: 跑通 6.3 功能验收 - [ ] 排行榜/记录页:先骨架再真实数据,无闪 - [ ] 排行榜行依次滑入 - [ ] 2 个空状态有插画 - [ ] 设置页深色开关:切到 dark → 已打开页面立即响应 - [ ] 系统切深色:当前页立即响应(选"跟随系统"时) - [ ] countUp 取消 0 报错(开始/停止循环 10 次) ### Step 4: 跑通 6.4 设备兼容 如有真机条件,在 iOS / Android 真机预览全部 Task 效果。 ### Step 5: 包体积检查 ```bash ls -la dist/ 2>/dev/null || du -sh miniprogram/ ``` 体积增量应 < 5KB(无第三方包)。 ### Step 6: 整体提交 ```bash git status # 确认无未提交 git log --oneline -20 # 确认 14 个 commit 都有 ``` ### Step 7: 写一个验收报告 新建 `docs/superpowers/notes/2026-07-08-visual-upgrade-verify.md`,记录: - 哪些验收项通过 / 不通过 - 截图(可选) - 已知问题 / 后续优化建议 --- ## 总结 **14 个 Task**,每个 Task 一次 commit,总 ~16 个 commit。 **回滚策略**(任意 Task 出问题): ```bash # 回滚某个 Task 的所有文件 git revert # 或完全回滚(还原到 main): git checkout HEAD -- app.wxss app.json theme.json app.js git checkout HEAD -- custom-tab-bar/ pages/*/ rm -rf components/ui-skeleton/ utils/countUp.js utils/darkMode.js ```