1451 lines
35 KiB
Markdown
1451 lines
35 KiB
Markdown
# 视觉与动效全量升级 — 实施计划
|
|
|
|
> **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 文件的根 `<view class="container ...">` 改为:
|
|
|
|
**pages/index/index.wxml** 第 1 行:
|
|
```xml
|
|
<view class="container page-enter" style="{{themeStyle}}">
|
|
```
|
|
|
|
**pages/timer/timer.wxml** 第 1 行:
|
|
```xml
|
|
<view class="container timer-page page-enter" style="{{themeStyle}}">
|
|
```
|
|
|
|
**pages/records/records.wxml** 第 1 行:
|
|
```xml
|
|
<view class="container page-enter" style="{{themeStyle}}">
|
|
```
|
|
|
|
**pages/leaderboard/leaderboard.wxml** 第 1 行:
|
|
```xml
|
|
<view class="container page-enter" style="{{themeStyle}}">
|
|
```
|
|
|
|
**pages/settings/settings.wxml** 第 1 行:
|
|
```xml
|
|
<view class="container page-enter" style="{{themeStyle}}">
|
|
```
|
|
|
|
### 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
|
|
<view class="skeleton {{active ? 'skeleton--active' : ''}}" wx:if="{{_minDisplayShown || !active}}">
|
|
<!-- card variant -->
|
|
<block wx:if="{{variant === 'card'}}">
|
|
<view class="sk-card">
|
|
<view class="sk-bar sk-bar--title"></view>
|
|
<view class="sk-bar sk-bar--body"></view>
|
|
<view class="sk-bar sk-bar--small"></view>
|
|
</view>
|
|
</block>
|
|
|
|
<!-- list-row variant -->
|
|
<block wx:elif="{{variant === 'list-row'}}">
|
|
<view class="sk-list" wx:for="{{count}}" wx:key="*this">
|
|
<view class="sk-circle"></view>
|
|
<view class="sk-list-text">
|
|
<view class="sk-bar sk-bar--title"></view>
|
|
<view class="sk-bar sk-bar--small"></view>
|
|
</view>
|
|
</view>
|
|
</block>
|
|
|
|
<!-- circle variant (timer) -->
|
|
<block wx:elif="{{variant === 'circle'}}">
|
|
<view class="sk-circle sk-circle--big">
|
|
<view class="sk-bar sk-bar--center"></view>
|
|
</view>
|
|
</block>
|
|
</view>
|
|
```
|
|
|
|
### 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: 验证
|
|
|
|
- 在某个页面临时放 `<ui-skeleton variant="card" />`
|
|
- 看到一张带 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
|
|
<!-- Loading -->
|
|
<view class="status-wrap" wx:if="{{loading}}">
|
|
<text class="status-text">加载中...</text>
|
|
</view>
|
|
```
|
|
|
|
替换为:
|
|
|
|
```xml
|
|
<!-- Loading skeleton -->
|
|
<view class="leaderboard-skeleton" wx:if="{{loading}}">
|
|
<ui-skeleton variant="list-row" count="{{5}}" />
|
|
</view>
|
|
```
|
|
|
|
### 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 行 `<view class="container page-enter" style="{{themeStyle}}">` 后立即插入:
|
|
|
|
```xml
|
|
<block wx:if="{{loading}}">
|
|
<ui-skeleton variant="card" />
|
|
<view style="height: 24rpx;" />
|
|
<ui-skeleton variant="card" />
|
|
</block>
|
|
<block wx:else>
|
|
<!-- existing content -->
|
|
```
|
|
|
|
但 `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 行 `<view class="container timer-page page-enter" style="{{themeStyle}}">` 之后插入:
|
|
|
|
```xml
|
|
<!-- 全屏彩带完成庆祝 -->
|
|
<view class="confetti" wx:if="{{isCompleted}}">
|
|
<view class="confetti-piece c1"></view>
|
|
<view class="confetti-piece c2"></view>
|
|
<view class="confetti-piece c3"></view>
|
|
<view class="confetti-piece c4"></view>
|
|
<view class="confetti-piece c5"></view>
|
|
<view class="confetti-piece c6"></view>
|
|
<view class="confetti-piece c7"></view>
|
|
<view class="confetti-piece c8"></view>
|
|
<view class="confetti-piece c9"></view>
|
|
<view class="confetti-piece c10"></view>
|
|
</view>
|
|
```
|
|
|
|
### 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
|
|
<view class="dock" style="{{themeStyle}}">
|
|
<view class="dock-inner">
|
|
<view
|
|
class="dock-item {{selected === 0 ? 'active' : ''}}"
|
|
data-index="0"
|
|
bindtap="switchTab"
|
|
>
|
|
<view class="dock-icon" style="background-image: url({{selected === 0 ? svgIcons.homeFill : svgIcons.home}})"></view>
|
|
<text class="dock-text">训练</text>
|
|
</view>
|
|
|
|
<view
|
|
class="dock-item {{selected === 1 ? 'active' : ''}}"
|
|
data-index="1"
|
|
bindtap="switchTab"
|
|
>
|
|
<view class="dock-icon" style="background-image: url({{selected === 1 ? svgIcons.calendarFill : svgIcons.calendar}})"></view>
|
|
<text class="dock-text">记录</text>
|
|
</view>
|
|
|
|
<view
|
|
class="dock-item {{selected === 2 ? 'active' : ''}}"
|
|
data-index="2"
|
|
bindtap="switchTab"
|
|
>
|
|
<view class="dock-icon" style="background-image: url({{selected === 2 ? svgIcons.rankFill : svgIcons.rank}})"></view>
|
|
<text class="dock-text">排行</text>
|
|
</view>
|
|
|
|
<view
|
|
class="dock-item {{selected === 3 ? 'active' : ''}}"
|
|
data-index="3"
|
|
bindtap="switchTab"
|
|
>
|
|
<view class="dock-icon" style="background-image: url({{selected === 3 ? svgIcons.settingsFill : svgIcons.settings}})"></view>
|
|
<text class="dock-text">设置</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
```
|
|
|
|
### 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
|
|
<view
|
|
wx:for="{{rankedList}}"
|
|
wx:key="openid"
|
|
class="rank-item {{item.openid === myEntry.openid ? 'is-me' : ''}} rank-item--enter"
|
|
style="animation-delay: {{index < 10 ? index * 60 : 0}}ms;"
|
|
>
|
|
```
|
|
|
|
### 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
|
|
<!-- Empty -->
|
|
<view class="status-wrap" wx:elif="{{empty}}">
|
|
<image class="empty-icon" src="{{icons.peopleFill}}" mode="aspectFit"></image>
|
|
<text class="empty-text">暂无排行数据</text>
|
|
<text class="empty-sub">快去训练吧!</text>
|
|
</view>
|
|
```
|
|
|
|
替换为:
|
|
|
|
```xml
|
|
<!-- Empty with CSS illustration -->
|
|
<view class="empty-state" wx:elif="{{empty}}">
|
|
<view class="empty-illus">
|
|
<view class="empty-illus-body"></view>
|
|
<view class="empty-illus-arm empty-illus-arm--l"></view>
|
|
<view class="empty-illus-arm empty-illus-arm--r"></view>
|
|
<view class="empty-illus-bubble">加油</view>
|
|
</view>
|
|
<text class="empty-text">暂无排行数据</text>
|
|
<text class="empty-sub">快去训练吧,争当第一名!</text>
|
|
</view>
|
|
```
|
|
|
|
### 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
|
|
<view wx:if="{{historyList.length === 0}}" class="empty-state">
|
|
<view class="empty-illus">
|
|
<view class="empty-illus-body"></view>
|
|
<view class="empty-illus-arm empty-illus-arm--l"></view>
|
|
<view class="empty-illus-arm empty-illus-arm--r"></view>
|
|
<view class="empty-illus-bubble">开始</view>
|
|
</view>
|
|
<text class="empty-text">还没有训练记录</text>
|
|
<text class="empty-sub">完成第一次训练,开启你的坚持!</text>
|
|
</view>
|
|
```
|
|
|
|
### 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 "配色方案" 区块之后插入深色模式卡片
|
|
|
|
找到 `<text class="section-title">配色方案</text>` 之后,定位到"训练计划选择"之前,插入:
|
|
|
|
```xml
|
|
<!-- 深色模式 -->
|
|
<ui-card variant="in">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.skinFill}}" mode="aspectFit"></image>
|
|
<text class="section-title">深色模式</text>
|
|
</view>
|
|
<view class="theme-list">
|
|
<view
|
|
class="theme-item {{darkModePref === item.key ? 'active' : ''}}"
|
|
wx:for="{{darkModeOptions}}"
|
|
wx:key="key"
|
|
data-key="{{item.key}}"
|
|
bindtap="onSelectDarkMode"
|
|
>
|
|
<text class="theme-name">{{item.name}}</text>
|
|
<image class="theme-check-img" wx:if="{{darkModePref === item.key}}" src="{{icons.check}}" mode="aspectFit"></image>
|
|
</view>
|
|
</view>
|
|
</ui-card>
|
|
```
|
|
|
|
### 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 <commit-hash>
|
|
|
|
# 或完全回滚(还原到 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
|
|
```
|