Files
wx_pbzc/docs/superpowers/specs/2026-07-08-visual-upgrade-design.md
T
lc 53b57e8bd4 docs: 添加视觉与动效全量升级设计 spec
完整方案:深色模式 + 骨架屏 + 浮动 TabBar + 数字滚动
+ 全屏彩带 + 页面级转场 + 排名位移动画 + 空状态插画
零预算,不引第三方包,2 个工作日完成
2026-07-08 11:26:44 +08:00

374 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 视觉与动效全量升级 — 设计文档
**日期**: 2026-07-08
**项目**: 平板支撑训练小程序 (pbzc)
**类型**: 视觉/动效/UX 升级(不引第三方包,零预算)
**预计工时**: 2 个工作日
---
## 1. 背景与目标
### 1.1 现状
项目已完成核心功能闭环:训练 / 记录 / 排行 / 设置 4 大页面 + 自定义 TabBar + 云函数同步 + Canvas 进度环 + 5 套主题色 + 7 套 CSS 动画 + 5 个自研组件。
### 1.2 问题
与同类精品小程序(美团/京东/小红书)相比,仍存在 6 个"显低级"的视觉缺口:
1. 背景纯灰(`#F5F5F5`)有强 Web 页面感
2. 加载中只有"加载中..."文字,无骨架屏
3. 页面跳转是默认微信闪切,无过渡
4. 排行榜头像全是占位图标
5. 数字无更新动画
6. TabBar 贴底矩形,无悬浮 Dock 感
### 1.3 目标
不增加第三方依赖(无 Lottie / 无 npm 包),通过纯 CSS / 动画 / 少量 JS 工具,将小程序从"主流小程序"提升至"独立精品 App"视觉水平。
### 1.4 非目标
- ❌ 不做头像上传(需要腾讯云 COS,零预算约束下跳过)
- ❌ 不做商业化后端
- ❌ 不改训练 / 计时 / 同步 / 排行核心业务逻辑
- ❌ 不重写已有组件的实现方式
---
## 2. 架构
### 2.1 总思路
- 复用现有 `utils/theme.js` 的 CSS 变量体系
- 深色模式 = 新增 `--*-dark` 变量 + 第二个可选参数
- 新增 1 个组件 + 2 个工具函数 + 改 ~10 个现有文件
- 业务 JS 0 改动,回滚 5 分钟
### 2.2 文件改动清单
| 类型 | 路径 | 说明 |
|------|------|------|
| 🆕 新增 | `components/ui-skeleton/ui-skeleton.{js,json,wxml,wxss}` | 骨架屏组件(3 variant |
| 🆕 新增 | `utils/countUp.js` | 数字滚动工具 |
| 🆕 新增 | `utils/darkMode.js` | 系统主题监听 |
| ✏️ 修改 | `app.json` | 加 `darkmode: true` + theme JSON 配置 |
| ✏️ 修改 | `app.wxss` | 渐变背景 + 深色变量 |
| ✏️ 修改 | `utils/theme.js` | `getThemeStyle(theme, isDark)` 支持第二参数 |
| ✏️ 修改 | `custom-tab-bar/index.wxml` + `.wxss` | 悬浮 Dock 样式 |
| ✏️ 修改 | `pages/leaderboard/leaderboard.wxml` | 加载状态改骨架 + 排名位移动画 |
| ✏️ 修改 | `pages/records/records.wxml` | 加载状态改骨架 |
| ✏️ 修改 | `pages/timer/timer.wxml` + `.wxss` | 完成全屏彩带 + 数字滚动 |
| ✏️ 修改 | `pages/index/index.wxss` | 适配渐变背景 + 页面级入场动画 |
| ✏️ 修改 | `pages/settings/settings.wxml` | 加深色模式开关 |
| ✏️ 修改 | `app.wxss` 中追加 `.container.page-enter` 动画类 | 页面级转场动画(fade + translateY |
| ✏️ 修改 | 2 个 empty 状态 WXMLleaderboard/records | 用 CSS 简笔 SVG 替代空状态文字 |
### 2.3 依赖关系
```
app.json (darkmode: true)
→ app.onLaunch → 读 wx.getSystemInfoSync().theme
→ utils/darkMode.js
→ utils/theme.js → getThemeStyle(theme, isDark)
→ 各页面 onLoad → applyThemeToPage() 注入 themeStyle
→ 组件通过 CSS 变量级联自动适配
```
---
## 3. 组件详细规格
### 3.1 新组件 `ui-skeleton`
**Props 接口**
```js
properties: {
variant: { type: String, value: 'card' }, // 'card' | 'list-row' | 'circle'
count: { type: Number, value: 1 }, // 仅 list-row 生效
active: { type: Boolean, value: true } // 是否播放闪烁动画
}
```
**3 种 variant 渲染规则**
- `card`:模拟 `ui-card` —— 24rpx 圆角矩形 + 3 行灰条(标题宽度 60%,正文 100%,小字 40%
- `list-row`:模拟排行榜行 —— 64rpx 圆形 + 2 行文字条
- `circle`:模拟计时器进度环 —— 360rpx 大圆 + 中心 200×32rpx 灰条
**动画**`@keyframes shimmer`opacity 0.4 ↔ 0.81.4s ease-in-out infinite。
**最小显示时长**:组件挂载时起 200ms 定时器,避免骨架闪一下又消失。
### 3.2 `utils/countUp.js`
**API**
```js
function countUp({ from, to, duration, decimals = 0, onUpdate, onComplete })
// 返回 { cancel() }
// 内部: setInterval(fn, 16) + easeOut (1 - (1-t)^3)
```
**使用场景**
- 计时器归零瞬间:30 → 0
- 排行榜加载完:"我的时长" 0 → 真实值
- 完成庆祝:"本次 +30s" 飘字
**不在以下场景使用**(避免干扰):
- 计时器运行中(每秒变一次,肉眼够用)
- 用户已停留的页面(防打扰)
**取消机制**
- `onUnload` 必调 `cancel()`
- 页面实例持 `_countUpTask`,新一次 countUp 前 cancel 上一个
- 防止 setData 到已销毁页面
### 3.3 `utils/darkMode.js`
**API**
```js
function getInitialDarkMode() // boolean
function watchDarkMode(onChange) // 注册系统主题变化回调
```
**读取优先级**(在 `getInitialDarkMode` 内):
```js
const pref = wx.getStorageSync('dark_mode_pref') || 'system'
if (pref === 'system') return sysTheme === 'dark'
return pref === 'dark'
```
**系统变化监听**`wx.onThemeChange(cb)`,兼容失败时退化为只读一次。
**主题切换**:用户改档位后,遍历 `getCurrentPages()`,每页调 `applyThemeToPage()` 重渲染。
### 3.4 theme.js 扩展
**改动最小化**:仅扩展 `getThemeStyle` 签名。
```js
function getThemeStyle(theme, isDark = false) {
const t = theme || getCurrentTheme()
if (isDark) {
return `${BASE_VARS_DARK}--primary:${t.primary};...`
}
return `${BASE_VARS}--primary:${t.primary};...` // 现有逻辑
}
```
**深色变量**`BASE_VARS_DARK`,在 app.wxss 中体现):
```css
--bg-dark: #0F0F12;
--card-bg-dark: #1C1C1E;
--text-dark: #E5E5E7;
--text-secondary-dark: #98989F;
--border-dark: #2C2C2E;
--success-dark: #30D158;
```
### 3.5 现有组件最小改动
| 组件 | 改动 |
|------|------|
| `ui-card` | 仅 CSS 变量跟随,0 JS 改动 |
| `ui-btn` | 加 `::before` 伪元素涟漪 + 已有的 `transform: scale(0.96)` |
| `progress-ring` | 完成瞬间数字走 `countUp` 滚动到目标值 |
### 3.6 页面级转场动画
**实现方式**:不使用 `wx.pageContainer`(增加兼容性负担),改用 CSS 入场动画 + 极短的 `animation-delay` 错开元素。
`app.wxss` 追加:
```css
.container.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); }
}
```
**触发**:每个页面的根 `view``class="container page-enter"`。微信原生 page transition 的 ~100ms 渐隐 + 这个 350ms 滑入组合,肉眼感受是"自然过渡"。
**为什么不用 `wx.pageContainer`**:需要路由配置侵入式改动,调试成本高。350ms CSS 动画覆盖了 80% 体验。
### 3.7 排名位移动画
**场景**:排行榜 `rankedList` 渲染时,行依次滑入。
**实现**:在 `leaderboard.wxml``.rank-item``style="animation-delay: {{index * 60}}ms"`,配合 CSS
```css
.rank-item {
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); }
}
```
**约束**:超过 10 行不再延迟(避免最后一行等 600ms)。
### 3.8 空状态插画
**当前**`<text>暂无排行数据</text>` + 小人图标。
**升级**:用 CSS 画一个 200×200rpx 的简笔平板支撑小人(body + 两条手臂 + 文字气泡),关键样式:
```css
.empty-illus {
width: 200rpx; height: 200rpx;
position: relative;
}
.empty-illus::before { /* 横躺的身体 */ }
.empty-illus::after { /* 头顶气泡 "加油" */ }
```
**优先级**:低。如果工时不够,可降级为"用 emoji 大字 + 主色圆形背景"占位。
---
## 4. 数据流
### 4.1 主题状态流
```
系统: wx.getSystemInfoSync().theme
→ getInitialDarkMode() → boolean
→ getThemeStyle(theme, isDark) → CSS 变量字符串
→ setData({ themeStyle })
→ 页面 <view style="{{themeStyle}}"> 生效
```
### 4.2 用户手动覆盖
- 设置页 3 档开关:`system` / `light` / `dark`
- 存储:`wx.setStorageSync('dark_mode_pref', value)`
- 切换时:调 `applyThemeToPage` 重渲染所有已打开页面
### 4.3 数字滚动流
```
setData({ displayTime: 30 })
→ countUp({ from: 0, to: 30, duration: 800 })
→ 每 16ms onUpdate(mid)
→ setData({ displayTime: mid })
```
### 4.4 骨架屏流
```
Page.onLoad
→ setData({ loading: true })
→ <ui-skeleton /> 渲染
→ wx.cloud.callFunction 异步返回
→ setData({ loading: false, realData: ... })
→ <ui-skeleton wx:if="{{loading}}" /> 消失
→ <实际数据 wx:else> 出现
```
---
## 5. 错误处理与边界
### 5.1 深色模式
| 情况 | 处理 |
|------|------|
| 系统切换时停留在某页 | `wx.onThemeChange` → 遍历 `getCurrentPages()` 重渲染 |
| 老用户无 `dark_mode_pref` | 默认 `'system'`,行为同升级前 |
| 旧设备不支持 `onThemeChange` | try-catch,退化为只读一次 |
| 主题同步云端失败 | 不影响本地,不抛错 |
### 5.2 countUp
| 情况 | 处理 |
|------|------|
| 页面销毁时还在滚动 | `onUnload` 必调 `cancel()` |
| 连续触发(开始/暂停反复) | 每次新 countUp 前 cancel 上一个 |
| from === to | 跳过动画,直接落到目标 |
| duration ≤ 0 | 跳过动画 |
### 5.3 骨架屏
| 情况 | 处理 |
|------|------|
| 加载 < 200ms | 最小显示定时器,避免闪一下 |
| 加载失败 | 骨架消失,进 empty 态 |
| 重复 setData loading | `if (this.data.loading) return` 短路 |
### 5.4 浮动 TabBar
| 情况 | 处理 |
|------|------|
| iPhone X+ 安全区 | `env(safe-area-inset-bottom)` + `margin-bottom: 16rpx` |
| 点击当前 tab | 已有 `isSelected` 判断,避免重复 |
### 5.5 回滚
所有改动只新增文件 + 增量改 CSS,不改核心业务 JS:
```bash
git checkout HEAD -- app.wxss app.json custom-tab-bar/* \
pages/*/*.wxss pages/*/*.wxml
rm -rf components/ui-skeleton/ utils/countUp.js utils/darkMode.js
```
---
## 6. 验收清单
### 6.1 视觉
- [ ] 浅色模式背景是带主题色极淡径向渐变(非纯灰)
- [ ] 深色模式所有文字/卡片/边框自动转深色(不发白)
- [ ] 5 套主题 × 2 套明暗 = 10 种组合正常
- [ ] TabBar 悬浮、圆角、阴影明显
- [ ] 计时器数字完成时滚动归零
- [ ] 完成时全屏彩带飘落
- [ ] 页面级入场动画(fade + translateY)所有页面正常
- [ ] 排行榜行依次滑入(前 10 行)
### 6.2 性能
- [ ] 首页首屏 < 500ms
- [ ] 切深色 < 100ms
- [ ] countUp 不掉帧
- [ ] 包体积增量 < 5KB
### 6.3 功能
- [ ] 排行榜/记录页:先骨架再真实数据,无闪
- [ ] 排行榜行依次滑入
- [ ] 2 个空状态有插画
- [ ] 设置页深色开关:切到 dark → 已打开页面立即响应
- [ ] 系统切深色:当前页立即响应
- [ ] countUp 取消 0 报错(开始/停止循环 10 次 console 无错)
### 6.4 设备兼容
- [ ] iOS 真机
- [ ] Android 微信 8.0.3+
- [ ] 开发者工具基础库 2.32.3+
---
## 7. 测试方法
无单测框架,端到端肉眼 + Console 验证:
1. **本地开发者工具**:每个页面截图前后对比
2. **真机预览**:预览码 / 体验码
3. **Console 监听**`countUp` / `darkMode` 关键路径加 log
4. **Storage 检查**:开发者工具 Storage 面板看 `dark_mode_pref`
---
## 8. 风险评估
| 风险点 | 等级 | 原因 |
|--------|------|------|
| 深色模式不生效 | 🟢 低 | 复用 theme.js 已有机制 |
| 浮动 TabBar 高度不对 | 🟢 低 | 已有自研 TabBar |
| countUp 性能差 | 🟢 低 | 16ms + 线性插值 |
| 包体积超标 | 🟢 极低 | 0 第三方包 |
| 回归业务逻辑 | 🟢 极低 | 不改 timer/cloud/storage JS |
---
## 9. 交付物
- 1 个新组件 `components/ui-skeleton/`
- 2 个新工具 `utils/countUp.js``utils/darkMode.js`
- 改动 ~10 个现有文件
- 1 份升级前/后对比截图
---
## 10. 不在本次范围
- 头像上传(需 COS,零预算约束跳过)
- Lottie 动画(需引入 50KB 包)
- 视频示范(需 VOD
- 社区/打卡广场(需后端)
- 商业化(支付 / 备案 / 会员)