13 KiB
2026-07-28 工作日志
记录页「训练勋章」UI 改造(去掉 emoji 图标)
用户反馈勋章卡片 UI 问题并要求"不用 emoji 图标"。完成:
utils/icons.js:新增 3 个勋章 PATHS(bolt 闪电 / moon 月亮 / diamond 钻石),并补 white 变体 boltWhite / moonWhite / diamondWhite / crownWhite(crown/hot/trophy 复用已有 PATHS)。config.js:trainingDayBadges每项icon/iconWhite改为 icons.js 的 key(如 'hot','bolt'),不再用 emoji 字符串。pages/records/records.wxml:节点<text>emoji 改<image class="badge-node-icon">,按 unlocked 选icons[item.iconWhite](白) 或icons[item.icon](灰); 新增.badge-names层显示勋章名;把nextBadgeHint从标题右侧移到"累计训练 X 天"下方,避免标题行被拉扯。pages/records/records.wxss:节点.badge-node.unlocked改为实心主题色圆(背景 var(--primary))+ 白色图标;未解锁用灰描边+灰图标(去掉 grayscale/opacity 压暗,保证可见);.badge-node-icon改 32rpx image;新增.badge-names/.badge-name样式;.badge-next改为居中块。pages/records/records.js:去掉"全部勋章已解锁 🎉"里的 emoji。
验证:node --check 三个 JS 全过;项目内已无 emoji。
训练勋章 P0/P1 修复 + 标题/文案对齐调整
用户确认统计口径为"累计天数、可中断",并截图指出标题图标与文字未对齐、图标太小、中间文案拥挤。完成:
config.js:勋章命名全部改为累计语义(一周累计 / 半月累计 / 月度累计 / 双月累计 / 百日累计),注释明确"可中断、非连续打卡"。pages/records/records.js:- 进度算法改为 sqrt 感知映射:
pos(d)=5+sqrt(d)/sqrt(100)*90,节点与填充同映射 → 解锁时填充正好抵达节点、满级时 100% 满格;首枚节点从 20.6% 开始,新手期进展可见。 - 新增
_checkBadgeCelebration():持久化badge_celebrated集合,首次仅初始化不刷屏,后续有新解锁时弹 toast"解锁勋章:XX"并给节点打justUnlocked标记。
- 进度算法改为 sqrt 感知映射:
pages/records/records.wxml:标题下新增.badge-sub"累计训练天数,非连续打卡";节点加celebrate类。pages/records/records.wxss:- 标题 crown 图标从 28rpx 放大到 36rpx,加
align-self:center与margin-top:-2rpx光学对齐;重置.badge-header .section-title的 margin-bottom。 - 新增
.badge-sub样式;调整.badge-days与.badge-next的上下间距,减少中间文案拥挤感。 - 新增
.badge-node.celebratepulse 动画与@keyframes badgePop。
- 标题 crown 图标从 28rpx 放大到 36rpx,加
验证:node --check 通过;旧名/emoji 已无残留。
训练勋章卡片去拥挤化
用户截图反馈勋章卡片整体拥挤、文案过多、节点图标挤压。完成:
pages/records/records.wxml:- 删除
.badge-sub("累计训练天数,非连续打卡")这一行; - 将
.badge-days与.badge-next合并为同一行.badge-status(中间用圆点分隔),减少顶部文案层数; - 删除常驻的
.badge-names层;勋章名改为点击节点时通过onBadgeTaptoast 展示; - 节点加
data-days+bindtap="onBadgeTap"。
- 删除
pages/records/records.js:- 新增
onBadgeTap():点击已解锁节点提示"已解锁:XX",未解锁提示"再练 N 天解锁「XX」"; - 轨道首尾留白从 5% 缩小到 2.5%,让 6 个节点在窄屏分布更舒展。
- 新增
pages/records/records.wxss:- 删除
.badge-sub、.badge-names、.badge-name样式; - 新增
.badge-status及其子元素样式(居中、允许折行、圆点分隔); - 调整
.badge-labels的margin-top与opacity,让天数数字更轻盈; - 保留
.badge-node/.badge-node-icon/.badge-track等核心样式。
- 删除
验证:node --check 通过;旧 class 名(badge-sub/badge-names/badge-name)已无残留。
训练勋章卡片间距与节点尺寸微调
用户再次截图反馈行间距拥挤、勋章图标粘连、节点圆盘遮挡底部天数。完成:
pages/records/records.wxss:- 状态行
.badge-status下边距从 28rpx 增大到 44rpx; - 轨道
.badge-track上下间距从36rpx 0 4rpx调整为44rpx 0 6rpx,整体呼吸感加大; - 节点圆盘从 56rpx 缩小到 44rpx,图标从 32rpx 缩小到 26rpx,避免遮挡下方天数;
- 天数标签层
.badge-labels的margin-top从 14rpx 增大到 22rpx,height从 26rpx 增大到 30rpx,让数字与圆盘彻底分离; - 解锁 pulse 光晕从 8rpx 缩小到 6rpx,匹配更小节点。
- 状态行
验证:node --check 通过。
训练勋章:改回等距节点 + 恢复大图标
用户指出前 3 个节点间距不一致(sqrt 映射副作用),并希望不缩小图标、改用增大区域高度来避免遮挡。完成:
pages/records/records.js:进度算法从 sqrt 感知映射改回等距排列——segWidth=(100-2*TRACK_PAD)/(n-1),pos(i)=TRACK_PAD+i*segWidth,节点与填充同套线性插值。TRACK_PAD 回到 4%;副作用是节点间距完全一致、视觉更整齐,但"新手期进度条动得快"的感知弱化(用户更看重整齐)。pages/records/records.wxss:- 节点圆盘恢复 56rpx、图标恢复 32rpx、celebrate 光晕恢复 8rpx(撤销上一轮的缩小);
- 通过增大区域高度避免遮挡:
.badge-trackmargin 改为48rpx 0 20rpx,.badge-labelsmargin-top 改为 32rpx;圆盘下缘到天数字起始约 52rpx(圆盘半径 28rpx + 留白),不再遮挡; - 状态行
.badge-status下边距保持 44rpx。
验证:node --check 通过。
训练勋章:让"已拥有勋章"直观可见(方案 1 + 3)
用户确认保持等距进度条布局不变,痛点转为"用户无法直观看到自己目前有哪些勋章"。落地方案 1(已解锁名字常驻)+ 方案 3(✓ 角标强化拥有感):
pages/records/records.wxml:- 已解锁节点加
<view class="badge-check" wx:if="{{item.unlocked}}">角标; .badge-label改为item.unlocked ? item.name : item.days——已解锁渲染勋章名、未解锁只渲染天数数字(避免之前 6 名字常驻拥挤,现在只有已解锁的 1–3 个显示名字)。
- 已解锁节点加
pages/records/records.wxss:- 新增
.badge-check(24rpx 主题色圆 + 2rpx 白描边,定位在节点right:-3rpx; bottom:-3rpx)与.badge-check::after(纯 CSS border 旋转 45° 画白勾;因图标 SVG 颜色烤死无法用变量改色,故用伪元素画); .badge-label.unlocked字号 20→22rpx、字重 600→700,让已拥有的勋章名更醒目。
- 新增
验证:node --check 三个 JS 全过;wxml 的 badge-check/badge-label 与 wxss 样式一致。首尾 TRACK_PAD=4%,节点在 4%–96%,✓ 角标不溢出卡片。
训练勋章:新增 80 天「洪荒神登」图标(star)
用户在 config.js 的 trainingDayBadges 加了 80 天「洪荒神登」,临时复用了 diamond 图标(与 60 天「上古神登」撞图标)。完成:
utils/icons.js:PATHS 新增star(Material 五角星路径,24x24 单色);build() 的 Badge white variants 段新增out.starWhite = _svg(PATHS.star, '#FFFFFF')(解锁态白星,坐在主题色圆盘上)。config.js:第 69 行 80 天勋章icon/iconWhite从diamond/diamondWhite改为star/starWhite;并修正上方过时注释(原写"name 一律用累计措辞",实际用户已改成"登"系列幽默命名),改为"口径=累计去重训练日(可中断);name 仅作趣味命名,不暗示连续打卡"。
验证:node --check 通过 icons.js / config.js。注意:勋章现 7 枚,等距每段约 115rpx、节点间隙约 59rpx,仍不重叠(之前评估 8–10 枚才明显拥挤)。
排行榜首开延迟分析(3-4s)+ P1 索引/投影落地
用户反馈排行榜首次打开有时延迟 3-4s 才显示数据。分析结论(已用示意图拆解):
- 根因:排行榜在用户请求时同步全量计算
cloudfunctions/leaderboard/index.js的_fetchLatestByOpenid()全表扫描 plank_data;没有预计算快照。客户端缓存(30min)只缓解"重复进入",云实例缓存(5min, 内存不跨容器)随低流量容器回收频繁变冷。 - 耗时拆解(冷路径):云函数冷启动 ~1.2s + 全表扫描 ~1.5-2.5s + 头像 getTempFileURL ~0.3-0.5s;热路径 <0.6s。
- 关键坑:原查询
_.or([{updatedAt:gte(90d)},{updatedAt:exists(false)}])的exists(false)分支无法走索引 → 实质全集合扫描;且每次读全字段(settings/streak/customPlans/themeId 都在 doc 里),载荷大。 - 修复分层:P0 预计算快照(定时触发器写 leaderboard_snapshot,用户函数只读1 doc);P1 updatedAt 索引+投影;P1 App 启动预热;P2 force 不再绕过云缓存。
用户已在控制台给 plank_data.updatedAt 建单字段索引。我落地 P1 代码侧:
cloudfunctions/leaderboard/index.js的_fetchLatestByOpenid:去掉exists(false)OR 兜底分支(让 gte(90d) 干净走索引),并给查询加.field({records,profile,_openid,updatedAt})投影,跳过 settings/streak/customPlans/themeId 大字段,缩小单次读取载荷。- 取舍:无 updatedAt 的极旧遗留 doc(必已 90+ 天未同步)将不再入榜,可接受。
- 验证:
node --check cloudfunctions/leaderboard/index.js通过。
下一步可选:P0 定时快照(彻底消除首开等待与容器冷启动影响)。
排行榜延迟根治:P0 预计算快照 + P1 预热 + P2 force(已落地)
用户确认做 P0/P1/P2。完成:
cloudfunctions/leaderboard/index.js重写:- 抽
_buildFromScan(latestByOpenid, period, limit, myOpenid):从已抓取 Map 算单周期榜(含头像 getTempFileURL 解析),供实时与定时两条路径共用; - 新增
_persistSnapshot(period, result):写leaderboard_snapshot集合(doc._id=周期,字段 period/ranked/myOpenid/updatedAt);首次写自动建集合; - 新增
_serveFromSnapshot(period, limit, myOpenid):读快照(1 次 get≈50ms),按 openid 计算 myEntry;快照缺失或过期(>6min)返回 null 回退实时; - 新增
_rebuildSnapshots():扫描一次、_buildFromScan复用算 day/month/year 三榜并各自_persistSnapshot(SNAPSHOT_TOP=500=函数 maxRank 上限); - 新增
_computeBoard(period, limit, myOpenid, {force,persist}):实时重算并可选回写快照; exports.main区分:timer 触发(event.type==='timer'||Type||triggerName||MessageType) →_rebuildSnapshots();客户端请求 → force 则_computeBoard(force:true,persist:true)(训练后那次,唯一整表扫描),否则_serveFromSnapshot,缺失/过期则_computeBoard(force:false,persist:true)兜底并回写。- 响应结构保持
{period,ranked,myOpenid,myEntry,updatedAt}不变,客户端_applyResult无改动。
- 抽
cloudfunctions/leaderboard/config.json:加triggers定时器snapshotTimertype=timer config=0 */4 * * * * *(WeChat 7 字段 cron,每 4 分钟触发一次重建快照)。注意:需重新部署该云函数定时器才生效。app.js:onLaunch 在cloud.init()后 fire-and-forget 调一次leaderboard(period=day, maxRank=config.leaderboardMaxRank),提前填充云实例/快照(P1 启动预热);guard 用cloud.enabled;新增require('./config')。pages/leaderboard/leaderboard.js:仅更新过时注释(原"云函数全表扫描冷启动 2-3s"改为"现读预计算快照 1 次 get≈50ms")。force 语义:快照架构下正常打开读快照(已满足"force 不再绕过云缓存"诉求),仅训练后那次 force 触发实时重算+回写。
验证:node --check 通过 index.js / app.js / leaderboard.js / config.js;函数内调用图一致。
效果预期:首开/冷启动排行榜从 2-3s 降到 ≈50ms(1 次 get);容器冷启动不再慢;每 4min 离线刷新三榜,数据最多延迟 4min(训练后 force 立即刷新)。
排行榜快照写入失败修复(wx-server-sdk set 需 { data } 包裹)
用户手动触发云函数,leaderboard_snapshot 集合仍无数据,日志报 parameter.data should be object instead of undefined(day/month/year 三榜 persist 全失败)。
- 排查:本地
node --check通过;本地 mock 跑_persistSnapshot逻辑确认set收到的是对象(data 在 console.log 时存在、rankedLen 能打印);WebSearch 定位根因——wx-server-sdk 写操作(add/update/set)参数必须是{ data: {...} }包裹,SDK 读parameter.data,裸对象会让data.data=undefined 触发该报错(读操作get()不受影响)。此前判断"部署代码未刷新"是错的。 - 修复:
cloudfunctions/leaderboard/index.js的_persistSnapshot改为docRef.set({ data: { period, ranked, myOpenid, updatedAt } })(引用拆分存 docRef + data 包裹 + 诊断日志 + .errMsg 容错)。读取doc.data兼容(SDK 解开 data 外套,文档内容即裸对象)。 day rankedLen=0非 bug:record.date格式为YYYY-MM-DD HH:MM:SS(formatDate 生成,pages/records/records.js:225注释确认),_buildFromScan用r.date.startsWith('2026-07-28')能匹配;0 多为测试时(09:30)当天确实无训练记录,month/year 历史累计有 5 条属正常。- 验证:
node --check通过。 - 待办:用户需重新部署 leaderboard 云函数(含最新 _persistSnapshot),再手动触发/进榜下拉刷新,日志应现
persist ok、集合出现 day/month/year 三文档。