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

12 KiB
Raw Blame History

视觉与动效全量升级 — 设计文档

日期: 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 接口

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 shimmeropacity 0.4 ↔ 0.81.4s ease-in-out infinite。

最小显示时长:组件挂载时起 200ms 定时器,避免骨架闪一下又消失。

3.2 utils/countUp.js

API

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

function getInitialDarkMode()  // boolean
function watchDarkMode(onChange)  // 注册系统主题变化回调

读取优先级(在 getInitialDarkMode 内):

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 签名。

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 中体现):

--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 追加:

.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); }
}

触发:每个页面的根 viewclass="container page-enter"。微信原生 page transition 的 ~100ms 渐隐 + 这个 350ms 滑入组合,肉眼感受是"自然过渡"。

为什么不用 wx.pageContainer:需要路由配置侵入式改动,调试成本高。350ms CSS 动画覆盖了 80% 体验。

3.7 排名位移动画

场景:排行榜 rankedList 渲染时,行依次滑入。

实现:在 leaderboard.wxml.rank-itemstyle="animation-delay: {{index * 60}}ms",配合 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 + 两条手臂 + 文字气泡),关键样式:

.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:

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.jsutils/darkMode.js
  • 改动 ~10 个现有文件
  • 1 份升级前/后对比截图

10. 不在本次范围

  • 头像上传(需 COS,零预算约束跳过)
  • Lottie 动画(需引入 50KB 包)
  • 视频示范(需 VOD
  • 社区/打卡广场(需后端)
  • 商业化(支付 / 备案 / 会员)