Files
wx_pbzc/.workbuddy/memory/2026-08-03.md
T
lc bce796d893 feat: 首页按钮样式调整(小圆角/并排/高度对齐)
- 主按钮与自由/循环训练按钮圆角统一为 16rpx(直角→折中方案)
- 自由训练/循环训练两个次按钮改为并排一行,文字精简为「自由训练」「循环训练」
- 两个次按钮 size 由 md 改为 xl,与主按钮高度一致
- 新增 preview-buttons.html 对照预览
- 更新工作记忆
2026-08-03 17:04:05 +08:00

22 KiB
Raw Blame History

2026-08-03 工作日志

规划:分阶段训练(循环训练 circuit)功能

用户想在平板支撑小程序加入"分阶段训练":自定义 每组时长 / 每次组数 / 每周训练次数。 分析现有代码后结论:

  • 现有 utils/plan.js 是"单组持续撑"模型(hold 模式),计划按天线性加秒;设置页已能自定义公式参数(totalDays/startTarget/increment/cycleDays),但无"组数/休息/周频次"概念。
  • 用户要的是"循环训练(circuit)"范式,当前数据模型与计时器都表达不了,但云同步/计划选择/进度条/记录页骨架可复用。

已确认的设计决策(与用户拍板)

  1. 双模式并存:保留现有3个预设计划和老用户进度(mode:'hold'),新增 circuit 类型(mode:'circuit')。不破坏榜单/已有数据。
  2. 弹性训练:任意一天都能练;sessionsPerWeek 只用于显示「本周 X/N 次」(自然周周一重置),不卡进度(getPlanDay 已按训练日推进)。
  3. 扁平 routine 先上:整个计划每次 routine 相同(如 4组×30秒),靠"完成次数"和"每周次数"做进度;逐周进阶作为后续增强,MVP 不做。

约定细节

  • 默认组间休息 15 秒(编辑器可改)。
  • 循环预设示例:"新手 4组×20秒·休息15秒·3次/周·4周"。
  • duration 始终存"有效撑总秒数 = sets×holdPerSet"(不含休息),保证排行榜/里程碑零改动。

计划改动清单(原方案,较完整,本次未走此路线)

  • utils/plan.js:加 mode + circuit 目标解析 + 1 个循环预设
  • utils/storage.jssaveRecord 支持 sets/holdPerSet/restPerSet/mode
  • pages/timer + 新 utils/circuitTimer.jscircuit 状态机与 UI
  • pages/index:目标显示"4组×30秒·休息15秒" + 本周进度
  • pages/settings:编辑器加"循环模式"输入项
  • pages/records:展示"4组×30秒"
  • 榜单/云函数:不动

方案简化(用户两次追问"还有没有再简单一点的方案")

原方案把 circuit 做成完整"计划管理系统"(mode/预设/进度进阶/周频次追踪/设置页编辑器) 过度设计。 最终定位为「自由训练」的循环形态,而非新计划体系:

  • 首页加「循环训练」入口:输入 每组时长/组数/休息秒数 → 跳 timer?circuit=1&hold=&sets=&rest=
  • timer 内用新 utils/circuitTimer.js(类 Timer 接口: start/stop/pause/resume + onTick/onPhaseChange) 跑 工作/休息 状态机;UI 显示「第X/Y组」+ 阶段 + 组内剩余
  • 首页主「开始训练」仍走原 hold 计划,circuit 不替换每日目标 → 完全不动 index 的计划逻辑
  • storage.saveRecord 加可选 sets/holdPerSet/restPerSet/modeduration 仍=有效撑总秒(sets×hold),榜单零改
  • records 页:circuit 记录显示「4组×30秒」
  • 砍掉plan.js mode、circuit 预设、设置页编辑器、plan-day 进阶、周频次追踪(周频次可仅作为 picker 里可选提示,先不做追踪)
  • 唯一"真新逻辑"= circuitTimer 状态机;其余皆小 UI。风险与原方案同级(无新权限/域名)。

用户最终确认:先做简化版,但循环训练设置要持久化(下次打开不重填)。

已实现:简化版循环训练 + 配置持久化(2026-08-03 落地)

改动文件

  • utils/storage.js:新增 CIRCUIT_CONFIG_KEY / getCircuitConfig / saveCircuitConfig(本地持久化,含默认值 {holdPerSet:30, sets:4, restPerSet:15, sessionsPerWeek:3} + 范围校验,不进云同步——纯设备偏好)。
  • utils/circuitTimer.js(新):CircuitTimer 状态机 idle→working→resting→…→done,类 Timer 接口(start/stop/pause/resume + onTick/onPhaseChange/onComplete),stop() 返回 workTotal(有效撑总秒,不含休息),复用 Date.now+onAppShow 前台补偿。
  • pages/timer/timer.{js,wxml,wxss}_init 检测 options.circuit_initCircuit 建 FSM + circuit 字段;新增 _circuitCue/_circuitPhase/_onCircuitComplete/_vibrateOnce/_int;阶段切换走震动 + phaseTip 文本TTS 无"第N组/休息"词,刻意不堆语音),完成复用 complete 语音;_saveAndShowCompletionrecord.mode='circuit' + sets/holdPerSet/restPerSet/sessionsPerWeekplanId'circuit'(避免 getPlanDay 把循环记录算进每日计划进度);onTrainAgain 在 circuit 模式重建 FSMcompletion overtime 在 circuit 隐藏(每阶段是固定目标,无"超长发挥"概念)。
  • pages/index/index.{js,wxml,wxss}:加「循环训练 · 分组练习」ghost 按钮(repeatFill 图标);底部配置弹窗(stepper 调 每组时长/组数/休息/每周目标),确认后 saveCircuitConfig + 跳 timer?circuit=1&hold=&sets=&rest=&sessions=
  • utils/icons.js:加 repeat/repeatFillMaterial repeat 路径,build 自动给 gray + primary 两变体)。
  • pages/records/records.{js,wxml}:circuit 记录显示「循环 N组×Ms(·休Rs)」,基于 r.mode

关键不变量(续做时牢记)

  • duration 始终 = 有效撑总秒(组数×每组秒,不含休息)→ 排行榜/里程碑/统计零改动。
  • 循环记录 planId:'circuit',不污染每日计划 getPlanDay;连胜 updateStreak 仍计入(用户确实练了)。
  • 进度环在 circuit 模式轨道恒中性灰(否则每阶段都按"累计≥阶段长"误判变绿)。

验证

6 个 JS 全过 node --check。未动 plan.js / 云函数 / 榜单 / 云同步。

未做(可选后续)

  • 周频次硬追踪(仅存 sessionsPerWeek 作显示,未做"本周 X/N 次"计数)
  • 逐周自动进阶
  • 设置页统一管理入口

修复+重做:循环训练页顶部进度面板(2026-08-03 二次迭代)

用户反馈"顶部只有『1/3组,撑住』太简单难看"。根因:上一轮只写了 .circuit-bar 的 WXML 结构,timer.wxss 里一行样式都没写(grep 确认零匹配),所以渲染出来就是两段裸文字。

新设计(信息层次分工)

  • 顶部面板管宏观「第几组」,中间进度环管微观「本组还剩几秒」——不再重复展示同一信息。
  • 结构:组进度胶囊点阵 → 大数字 2/4 组 + 阶段徽章 → 副行「已撑 42 秒 · 目标 120 秒」。
  • 点阵三态:is-done(实心主色) / is-active(加宽+主色渐变+发光呼吸) / is-next(休息时高亮下一组,success 绿呼吸)。
  • 徽章四态 BEM--work(主色渐变实心+白灯白字) / --rest(success 绿淡底) / --idle / --pause(中性灰)。休息态整张卡边框也转绿,视觉降温。
  • 待机态显示总组数 4 组 + "准备开始" + "每组30秒·休息15秒";运行态显示 2/4 组 + 累计秒数。

实现要点(踩坑记录)

  • 状态指示灯用纯 CSS 圆点,不用 icon:项目里 SVG 图标颜色烤死在 data URI,CSS 改不动;而这个灯需要随状态换色(白/绿/灰),纯 CSS 是唯一干净解。
  • 点阵用 flex:1 + max-width,任意组数都均分不溢出;is-activeflex:2 加宽突出。组数 >15 (DOTS_MAX) 退化为线性进度条 .circuit-track
  • 面板不设固定 height,靠 padding 撑开——真机大字体档会放大文字但不放大 rpx 盒子,固定高度必裁字(项目老坑)。
  • .circuit-bar 必须 position:relative; z-index:1.breathe-ring(z-index:0) 在 DOM 里排在面板之后,不抬层橙色光晕会晕染到白卡片上。
  • 暂停时 .is-paused 冻结所有 dot 呼吸动画——静止本身就是"已暂停"信号。
  • dots 三态在 onTick 里算(与 isResting 同批 setData,不会状态不同步):resting 时 CircuitTimer 的 setIndex 仍指向"刚做完那组",故 dotsDone=setIndexdotsActive=0dotsNext=setIndex+1
  • _initCircuit 新增 circuitDots(仅 [1..sets] 供 wx:for) / circuitShowDots / circuitTotalWork(sets×hold)。
  • hint-section 的 circuit idle 文案改为「循环训练 · 共需撑 N 秒」,避免与面板副文案重复。

改动文件:pages/timer/timer.{js,wxml,wxss}node --check 通过,无旧类名残留。

微调:组进度点阵改为按组数严格等分铺满(2026-08-03 三次迭代)

用户反馈"点阵只占顶部一部分,应该按组数平均分配到整行"。

  • 根因.circuit-dotmax-width: 64rpx 给每段宽度封顶,.circuit-dots 默认 justify-content:flex-start → 组数少时(3-4 组)点阵只占左边一截。此外 is-activeflex:2 加宽,本身就破坏了"按组数等分"。
  • 修法.circuit-dotflex:1 1 0; min-width:0删掉 max-width.circuit-dotswidth:100%。当前组/下一组不再靠"加宽"强调,改为 增高12rpx → 18rpx,圆角同步 6→9rpx)+ 渐变/发光/呼吸,宽度恒等分。
  • .circuit-dotsmin-height:18rpx 锁行高,避免 idle(无 active,行高 12rpx)→ running(有 active18rpx)时整块跳 6rpx。
  • transition 从 flex 改为 height(强调维度变了)。
  • 仅改 pages/timer/timer.wxss,无 JS/WXML 改动。

修复:循环训练缺少过程语音(2026-08-03 四次迭代)

用户反馈"循环训练缺少正常训练时候的那些语音提示"。根因有两处,都是设计遗漏

  1. _circuitCue 从未调用 _remind:单组模式 Timer.onTick 里有 this._remind(tick)(负责 halfway/last30/last10 三条语音),circuit 的 onTick 只调 _circuitCue,里面仅有"最后一组末 5 秒震动"。
  2. _circuitPhase 当初刻意不放语音(注释写明"TTS 无第N组/休息词条")。 结果整场只剩 2 声:onStartstart + _onCircuitCompletecomplete

修法

  • 新增 3 个 TTS 词条utils/voice.jscloudfunctions/tts/index.js 的 PROMPTS 必须同步):restStart(休息一下,调整呼吸) / nextSet(下一组,准备开始) / lastSet(最后一组,全力冲刺!)。刻意不含组号数字 → 固定 3 条覆盖任意组数,无需按 N 合成 N 份音频。
  • _circuitCue 加整场进度播报:基准是 total = sets × holdPerSet整场有效秒,不是每组)——否则 4 组会播 4 次"已完成一半啦",语义全错。判据用 done >= total/2left <= 30 / <= 10<= 而非 ===,容忍后台回前台跳秒)。只在 phase==='working' 求值(休息时 workTotal 冻结,天然不会重复触发)。门槛沿用 _remindhalfway 需 total>=10、last30 需 total>60、last10 需 total>20。
  • _circuitPhase 加阶段语音 + chatty 闸门holdPerSet >= 10 才播 nextSet/restStart(一句话约 2 秒,短组会变语音连珠炮);restStart 另需 restPerSet >= 5lastSet 无条件播(唯一值得打断的节点)。第 1 组不播(onStartstart 同刻已响)。
  • _playProgressVoice 优先窗口(关键时序坑)halfway 触发点 = sets×hold/2组数为偶数时必然落在第 sets/2 组的最后一秒,与 rest 阶段切换同一 tick;voice.play_playSeq 会让后播的截断先播的 → 默认 4 组配置下"已完成一半啦"必被 restStart 掐断。故进度语音播放时设 this._voiceBusyUntil = Date.now()+2600_circuitPhase 在窗口内让位不播。_initCircuit 需重置 _voiceBusyUntil = 0(否则"再来一次"会误静音首条过场语音)。
  • preload 分模式voice.HOLD_KEYS / voice.CIRCUIT_KEYS 导出,onLoadisCircuitMode 取,避免单组训练白白预载 3 条循环语音。

部署要求

必须重新部署 tts 云函数,否则 3 个新 key 云端返回 Unknown prompt key。降级是安全的(_fetchUrl 拿到 success:false → 返回 null → play 静默 return,不报错、不弹 toast),只是这 3 条不出声。首次播放会各触发一次 TTS 合成(一次性费用,之后走 fileID + 本地文件双层缓存)。

改动文件:utils/voice.jscloudfunctions/tts/index.jspages/timer/timer.js。三个 node --check 全过,两边词条 grep 核对一致。

分析(未改代码):训练时禁止息屏

用户问"训练过程中能否禁止屏幕息屏"。结论:能,wx.setKeepScreenOn({keepScreenOn}),基础库 1.4.0+,无授权/无域名/无审核风险。项目当前 零处调用grep setKeepScreenOn 无匹配)。

为什么这不只是体验优化,而是修 bug

息屏/后台后小程序 JS 定时器被系统节流挂起 → 语音播报(halfway/last30/last10、circuit 的 restStart/nextSet/lastSet)与震动全部丢失Timer/CircuitTimerDate.now 前台补偿,秒数不会错,但中段提示是废的。平板支撑用户手撑地上不碰屏幕,60 秒必息屏 → 现状几乎每次训练的过程提示都收不到。

挂钩位置(关键)

  • wx.setKeepScreenOn 作用域是整个小程序,跨页面持续生效,只有退出小程序才自动失效 → 在 pages/timer 打开后,若不手动关,用户返回首页/记录页/排行榜也会一直常亮耗电。
  • 建议:onStart 开启 → onUnload 必须关闭(timer.js 现有 onLoad/onShow/onUnloadonHide)。暂停时保持常亮(用户可能只是短暂调整姿势)。完成弹窗期间也保持,离开页面才关。
  • wx.setKeepScreenOn 是异步 APIfail 要静默吞掉(部分 Android ROM 省电模式会拒绝),不弹 toast。

已知限制(需向用户说明)

  • 只阻止自动息屏;用户手动按电源键仍会锁屏。
  • 部分 Android 厂商 ROM 在省电模式/低电量下会忽略该设置。
  • 不影响亮度,屏幕可能仍会自动调暗(如需要可配 wx.setScreenBrightness,但必须在 onUnload 恢复,否则污染用户系统亮度——不建议做)。

建议的落地范围

约 5 行,只动 pages/timer/timer.js;可选在设置页加「训练时屏幕常亮」开关(默认开),与 voiceGuide 同样存在 settings 里。

已实现:训练时屏幕常亮(2026-08-03 五次迭代)

用户确认落地,且明确「先不要加开关」→ 默认恒开,不入 settings。

实现

仅改 pages/timer/timer.js,新增 _keepScreenOn(on) 方法 + 4 个挂钩点:

  • _keepScreenOn(on)typeof wx.setKeepScreenOn !== 'function' 时直接 return(低基础库降级),fail(){} 静默吞掉(Android 省电模式会拒绝,弹 toast 只会让用户困惑)。
  • onStart() 正常启动分支 → 开启;暂停时不关(用户多半在调整姿势,闪屏更烦);完成弹窗期间不关(让用户看成绩)。
  • onStart()isPaused resume 分支 → 也调一次(暂停期间若切过后台,标志位已被系统清掉)。
  • onShow()if (this.data.isRunning) this._keepScreenOn(true) 重新武装。这是最容易漏的一点:退出/切后台会清掉 app 级标志,回来不重设则后半程又静音。完成后 isRunning 已是 false(第 831 行 setData),不会误开。
  • onUnload()必须关。标志是 app 级不是页面级,不关则用户返回首页/记录/榜单全程常亮耗电。

hold 与 circuit 两种模式共用这一套(都走 onStart/onUnload),无需分别处理。node --check 通过。

分析(未改代码):首页打卡卡片左上/左下出现两条竖条

用户反馈"训练结束返回首页,顶部连续打卡卡片左上角和左下角各显示一个竖条,2-3 秒后消失"。

根因

pages/index/index.wxss.just-completed(训练完成高亮)把 border: 4rpx solid var(--primary) !important + box-shadow 加在了 <ui-card> 宿主节点上(index.wxml:13class="{{justCompleted ? 'just-completed' : ''}}")。而 components/ui-card/ui-card.wxss 只给内部 .ui-card view 写样式,宿主节点没有任何 display 声明 → 宿主是 inline 盒

inline 盒内包 block 子元素时,inline 盒被打断成"前/后"两个零宽行盒,border 只画在这两个碎片上;左右各 4rpx 贴合 → 视觉上就是 block 上方一条、下方一条竖线,且都贴左侧 = 用户看到的"左上角+左下角两个竖条"。

时序对上:index.js:125-129onShowjustCompleted=truesetTimeout 3000ms 后置回 false —— 正是那 2-3 秒。

连带失效(同一根因,用户还没察觉)

  • .just-completedanimation: justCompletedPulsetransform: scale从未生效 —— transform 对非替换 inline 元素无效。
  • box-shadow 同样只画在两个零宽碎片上,橙色光晕看不见。
  • 全局 grep :host / display:blockcomponents/ 零匹配 → 项目里所有自定义组件宿主都是 inline,任何在页面侧给组件标签加 border/shadow/transform 的写法都会踩同一个坑。
  • 附带发现:ui-card.wxss.ui-card.in:nth-child(1..4) 交错入场延迟也是失效的 —— .ui-card 是组件内部根节点下唯一的第 1 个子元素,永远匹配 nth-child(1),所有卡片延迟恒为 0.05s。要做交错必须由页面侧传 index 或用 animation-delay 内联 style。

候选修法(待用户拍板,尚未实施)

  1. 推荐components/ui-card/ui-card.wxss:host { display: block; }(微信小程序支持 :host 选择器)。一处修复所有 ui-card 用法,border/shadow/transform 全部恢复正常。风险:宿主从 inline 变 block 可能影响其他页面既有布局,需回归各页面卡片间距。
  2. 保守版:只把 .just-completed 的高亮样式改成作用于内部元素(如页面侧改成给 .header-card 加高亮),不动组件。影响面最小但治标不治本。
  3. 顺带可给其余 6 个自定义组件统一补 :host { display: block; }(需逐个回归,尤其 ui-btn 若被当行内元素用则不能改)。

已实现:修复竖条 + 恢复交错入场(2026-08-03 六次迭代,用户选方案 A)

改动

  • components/ui-card/ui-card.wxss:新增 :host { display:block; margin-bottom:24rpx; box-sizing:border-box },并把 margin-bottom:24rpx 从内部 .ui-card 移除(上提到宿主)——否则页面侧加的描边会把 24rpx 外边距一起圈进去,框比卡片高一截。删掉失效的 .ui-card.in:nth-child(1..4) 规则。
  • components/ui-card/ui-card.js:加 index 属性(Number,默认 0)。
  • components/ui-card/ui-card.wxmlstyle="animation-delay:{{50 + (index>3?3:(index>0?index:0))*100}}ms;{{customStyle}}"customStyle 必须写在最后才能覆盖内联 delay。>3 钳到 3,避免设置页第 7 张卡等 0.65s。
  • pages/index/index.wxss.just-completedborder: 4rpx solidbox-shadow: 0 0 0 4rpx var(--primary), 0 4rpx 24rpx rgba(--primary-rgb,.3),加 border-radius: 24rpx,去掉两个 !important(宿主上已无竞争样式)。
  • 14 处调用点补 indexindex 页 0-1records 页 0,1,2(trend 分支),2(records 分支,与 trend 互斥故同号),3settings 页 0-6。

校验与风险

  • ui-card.jsonstyleIsolation 覆盖(默认 isolated),:host 有效。customStyle 从未用在 ui-card 上(只有 ui-btn 在 timer 页用),改动无冲突。
  • hidden="{{...}}"(records 5 张卡)安全:微信内置 [hidden]{display:none!important}!important,优先级高于 :hostdisplay:block:host 未加 !important)。
  • 待真机确认:inline→block 会吃掉标签间行内空白,各页卡片间距可能略收紧几 px(属修正,非回归);transform:scale(1.02) 脉冲首次真正生效。

提交与推送

今日全部改动已合为一个提交推送到 origin/main

  • commit 3532d01 feat: 循环训练模式 + 语音补全 / 屏幕常亮 / 卡片渲染修复23 files, +1433 / -44
  • 前一个提交是 76cd2b6。推送后 git rev-list --left-right --count origin/main...main0 0,工作区干净。
  • 未拆分成多个 commit 的原因pages/timer/timer.js 被循环训练 / 顶部面板 / 语音 / 常亮四轮改动同时触及,按文件粒度无法干净切分,hunk 级拆分成本高且易错。改为一次提交、commit message 分四段写清各自根因。
  • .workbuddy/memory/*.md 一并提交(该目录本就在版本控制内,已有 2026-07-21/22/28 三份历史日志被跟踪)。
  • 推送按项目惯例走沙箱外(dangerouslyDisableSandbox),沙箱内会 SSL 超时。

⚠️ 部署待办

cloudfunctions/tts 尚未重新部署。新增的 restStart / nextSet / lastSet 三条词条在云端词表白名单里还不存在,线上会返回 Unknown prompt key → 客户端静默降级(不报错、不弹窗,只是不出声)。循环训练的过场语音要生效必须先部署这个云函数。

首页按钮改直角(2026-08-03 末尾)

用户偏好无圆角按钮。把 pages/index/index.wxml 中 5 个 ui-btn 全加 custom-style="border-radius:0;"(主训练按钮 xl-primary、两个 ghost 次按钮、两个弹窗 primary-lg 按钮)。

  • 实现要点ui-btn 是全局共用组件且 style isolation: isolated,页面 wxss 改不到内部圆角;但组件暴露 customStyle 属性(直接挂到内部 <button>style),内联 border-radius:0 可覆盖组件内置的 48rpx仅影响首页、不动全局与其他页面
  • 生成 preview-buttons.html 对照预览(圆角 vs 直角)。
  • 用户反馈直角不好看 → 改为折中 border-radius:10rpx812 中间值)5 处 custom-style 已更新。仅首页、不动全局。预览页「改后」栏同步更新为 10rpx 效果。
  • 布局调整:首页「自由训练 / 循环训练」两个 ghost 次按钮从竖排改为并排一行。wxml 用 <view class="action-row"> 包住两个 <ui-btn class="action-cell">index.wxss 加 .action-row{display:flex;gap:16rpx} + .action-row .action-cell{flex:1;min-width:0}(宿主节点为 flex 项,flex:1 各占一半,保留 block 让内部按钮撑满宿主)。按钮文字精简为「自由训练」「循环训练」。
  • 高度对齐:自由/循环两个 ghost 次按钮 sizemd(80rpx) 改为 xl(108rpx),与主按钮 size="xl" 高度一致(圆角保持 16rpx)。预览页 btn--mdbtn--xl 同步。仅首页、不动全局组件。