feat(ui): 记录页月份联动与多项 UI 美化/修复

- records: 底部列表与月份选择器联动,新增 [本月|最近] 分段控件;标题/空态随模式切换
- leaderboard: 领奖台布局(冠军居中+皇冠)、次数标注于底座且三档水平对齐;头像 cloud:// 批量预解析提速(云函数需重新部署)
- index/timer: 主色底图标改白色变体修复隐形;目标数字按微信字体缩放补偿真机遮挡
- settings: 配色方案横滑渐变色卡;头像上传前压缩至 200px
- theme: 新增 teal 主题;暗黑背景统一对齐
- 圆形头像双保险(image 自身 border-radius)修复月/年榜方图

注: cloudfunctions/leaderboard 改动须在开发者工具重新部署才生效。
This commit is contained in:
2026-07-22 09:36:22 +08:00
parent 70b87d424d
commit 6176f2c341
32 changed files with 924 additions and 197 deletions
+131
View File
@@ -0,0 +1,131 @@
# 微信小程序代码分析报告(平板支撑训练)
审查范围:`app.*``pages/*`5 页)、`utils/*`11 模块)、`cloudfunctions/*`4 个)、`custom-tab-bar``components/*`
整体评价:代码质量较高,日志、容错、云同步去重/合并都做得很到位。以下为发现的问题,按严重程度排列。
> **修复进度**2026-07-21):高优 #1、#2 已修;中优 #4、#5、#6 已修;低优 #7、#9、#10 已修。
> #3、#8 经复核为**误报**,无需修改(见正文)。
---
## 🔴 高(High
### 1. 同一天多次训练会错误跳过计划天数 ✅ 已修复
- **位置**`utils/plan.js``getPlanDay()`
- **问题**
```js
const trainedDays = new Set(eligibleRecords.map(r => r.date)).size
```
`r.date` 是 `"YYYY-MM-DD HH:MM:SS"`(含时分秒)。同一天训练两次会得到两条不同的字符串,
`Set` 计成 2 天,导致 `planDay` 被 +2,**平白跳过一天的进度**。计划进度应按"日历天"而非"训练次数"计算。
- **修复**:改用 `dateOnly` 去重
```js
const trainedDays = new Set(eligibleRecords.map(r => dateOnly(r.date))).size
```
`dateOnly` 已在 `utils/util.js` 中导出,本文件已 `require`
### 2. 系统深色模式切换时,3 个 tab 页不会实时重绘 ✅ 已修复
- **位置**`app.js` 的 `darkMod.watchDarkMode` 回调 + `pages/index|records|leaderboard/*.js`
- **问题**:回调只对有 `applyThemeToPage` 方法的页面生效:
```js
pages.forEach(p => { if (p && typeof p.applyThemeToPage === 'function') p.applyThemeToPage() })
```
但 **index / records / leaderboard 三个页面都没有定义 `applyThemeToPage` 方法**(只有 `settings` 定义了)。
结果:用户在手机系统里切深色/浅色时,这三页不会立刻变色,要等下次 `onShow` 才刷新。
- **修复**:在 app.js 的回调里补上兜底分支(与 settings 保持一致),并对自定义 tabBar 调用 `updateTheme()`
```js
pages.forEach(p => {
if (p && typeof p.applyThemeToPage === 'function') p.applyThemeToPage()
else if (p && p.route) { try { themeMod.applyThemeToPage(p) } catch(e){} }
try { const tb = p.getTabBar && p.getTabBar(); if (tb && tb.updateTheme) tb.updateTheme() } catch(e){}
})
```
(三页 `data` 中本就有 `themeStyle`,且 onLoad/onShow 都套用过主题,兜底可直接生效。)
---
## 🟠 中(Medium
### 3. 排行榜数据可伪造(⚠️ 经复核为误报 — 无需修复)
- **位置**`cloudfunctions/leaderboard/index.js`
- **复核结论**:初审认为"信任客户端上传的 `records.duration`",但实际查看云函数实现后发现——
该函数**只接收 `period` / `maxRank`**,duration 是在服务端从用户自己的 `doc.records` 重新累加出来的
(第 102132 行遍历 `records[monthKey]` 求和),并不读取客户端传入的 `duration`。
因此单个用户最多只能伪造自己那条记录,无法影响他人或全局榜单,属于客户端上报类健身应用的固有信任模型,可接受。
- **结论**:**无需修改**。若未来要做反作弊,可再加单条时长上限 / 频率校验,但属于增强项而非缺陷。
### 4. `admin-dedupe` 云函数无鉴权且具破坏性 ✅ 已修复
- **位置**`cloudfunctions/admin-dedupe/index.js`
- **问题**:该函数以管理员身份运行(wx-server-sdk 默认绕过安全规则),会**批量删除文档**,
但函数体内**没有任何调用者鉴权**。一旦部署,任意小程序用户都能调用它删数据。
- **修复**:在 `exports.main` 入口加 openid 白名单闸门(读环境变量 `ADMIN_OPENIDS`,逗号分隔):
```js
const ctx = cloud.getWXContext()
const caller = (ctx && ctx.OPENID) || ''
const allowlist = (process.env.ADMIN_OPENIDS || '').split(',').map(s => s.trim()).filter(Boolean)
if (!caller || allowlist.length === 0 || !allowlist.includes(caller)) {
return { err: 'forbidden', msg: '无权限执行该操作' }
}
```
- **部署要求**:必须在云函数环境变量中配置 `ADMIN_OPENIDS`(你的微信 openid);未配置时函数**默认拒绝执行(失败安全)**。
- 仍建议用毕即删/禁用该函数,白名单只是兜底闸门。
### 5. Timer 前台监听只注册不注销(监听器泄漏) ✅ 已修复
- **位置**`utils/timer.js` → `start()` 注册 `wx.onAppShow`,但 `stop()/pause()/onUnload` 均未 `wx.offAppShow`
- **问题**:每次进入训练页新建 Timer 都会注册一个 `onAppShow`,旧的不会被移除。
虽有 `if (this._running && !this._paused)` 守卫不会出错,但多次训练后会累积监听器(内存泄漏、潜在性能问题)。
- **修复**:抽出 `_bindAppShow()` / `_unbindAppShow()`,并在 `stop()` 中调用 `wx.offAppShow(this._onAppShow)` 且重置 `_appShowBound = false`。
页面 `onUnload` 已调用 `this._timer.stop()`,故页面销毁时监听一并注销,无遗漏。
### 6. `getTodayTarget` 在空计划天数数组时会崩溃 ✅ 已修复
- **位置**`utils/plan.js` → `getTodayTarget()`
```js
return day ? day.target : plan.days[0].target
```
- **问题**:若 `plan.days` 为空(totalDays=0,极端数据损坏场景),`plan.days[0]` 为 `undefined` → 访问 `.target` 抛异常。
- **修复**
```js
if (!plan.days || plan.days.length === 0) return 0
```
---
## 🟡 低(Low
### 7. `darkMode.js` 使用了已废弃 API ✅ 已修复
- `wx.getSystemInfoSync()` 已废弃,真机会打印警告。已改用 `wx.getWindowInfo()`(同样返回 `theme` 字段)。
### 8. `ui-modal` 的 `customStyle` 属性(⚠️ 误报 — 实际已使用)
- 初审称"wxml 中未使用",但复核 `components/ui-modal/ui-modal.wxml` 第 3 行:
```xml
<view class="ui-modal__body" catchtap="onNoop" style="{{customStyle}}">
```
该属性确实绑定到了 body 的 `style`,属正常可用的样式覆写入口,**不是死属性**。无需修改。
- 注:`timer.wxml` 里 `custom-style="margin-right:28rpx;"` 挂在 `ui-btn` 组件上,与 `ui-modal` 无关,亦无误。
### 9. `onTrainAgain` 用 `setData` 写入下划线前缀字段 ✅ 已修复
- `pages/timer/timer.js``this.setData({ ..., _countUpTask: null })`。
- **修复**:改为实例字段赋值 `this._countUpTask = null`(下划线前缀字段不应写进 page data)。
### 10. `index.js` 的 planProgress 边界为 NaN ✅ 已修复
- `Math.round((Math.min(planDay, plan.totalDays) / plan.totalDays) * 100)`,当 `plan.totalDays` 为 0 时得到 `NaN`。
- **修复**
```js
const planProgress = plan.totalDays > 0 ? Math.round((clampedDay / plan.totalDays) * 100) : 0
```
---
## ✅ 亮点(值得肯定)
- 云同步的"按 openid 定位 + 合并去重 + 永不重复 add"逻辑处理得很扎实,避免了旧版"每次冷启动新建文档导致排行翻倍"的坑。
- 录音/语音、震动提醒、暗色模式、自定义计划编辑器、完成庆祝动画等交互细节都考虑周全,且有完善的降级(云未部署/无密钥时静默回落本地)。
- `custom-tab-bar` 单独监听了系统主题变化,避免了切 tab 闪白,处理到位。
---
## 建议优先处理顺序(最终)
1. ~~#1 计划天数统计 bug~~ ✅
2. ~~#2 深色模式实时刷新~~ ✅
3. ~~#4 admin 函数鉴权~~ ✅(高优安全项,已修;部署时记得配 `ADMIN_OPENIDS`
4. ~~#5 / #6 / #7 / #9 / #10~~
5. #3#8 经复核为误报,无需处理。