循环训练(circuit)
- 新增 utils/circuitTimer.js 状态机: 工作->休息->...->完成, stop() 返回有效撑总秒(不含休息)
- 首页新增「循环训练·分组练习」入口 + stepper 配置弹窗
- 配置本地持久化 circuit_config(每组时长/组数/休息/每周目标), 下次自动预填
- 记录以 planId:'circuit' + mode:'circuit' 落库, duration 语义不变 -> 排行榜/统计/每日计划进度零改动
- 记录页展示「循环 N组xMs·休Rs」
计时器顶部循环进度面板
- 组进度点阵按组数等分铺满(flex:1 1 0, 去掉 max-width 封顶), 当前组以增高而非加宽强调
- 大号「X/Y 组」+ 四态阶段徽章(撑住/休息中/准备开始/已暂停) + 累计秒数副行
- 状态灯用纯 CSS 圆点(SVG 图标颜色烤死在 data URI 里, CSS 改不动)
- 超过 15 组自动降级为线性进度条; 面板不设固定高度, 规避真机大字体档裁字
修复: 循环训练全程无语音
- 根因: circuit 的 onTick 走 _circuitCue(只有震动), 从未调用 _remind
- 接入整场进度播报, 基准为 sets x holdPerSet(而非每组), 避免 4 组播 4 次「已完成一半」
- 新增 restStart/nextSet/lastSet 三条不含数字的词条, 客户端与 tts 云函数词表同步
- 修复必然撞车的时序: 偶数组时 halfway 与休息切换同一 tick, 加 2.6s 优先窗口让位
- 防吵闸门: 每组 <10s 不播过场语音, 休息 <5s 不播; 预载按模式取词条
训练期间屏幕常亮
- wx.setKeepScreenOn: onStart 开 / onUnload 关 / onShow 在 isRunning 时重新武装
- 切后台再回来该标志会被系统清掉, 故必须重新武装, 否则后半程丢语音与震动
- 低基础库与 Android 省电模式静默降级, 不弹 toast
修复: 首页打卡卡片左侧两个竖条
- 根因: ui-card 宿主节点无 display 声明为 inline, 页面侧加的 border 被打断成两个零宽行盒碎片
- ui-card 加 :host{display:block}, margin-bottom 从内部 view 上提到宿主
- 高亮由 border 改为 box-shadow spread 描边(不占布局, 无跳动) + 补 border-radius
- 连带修复失效的交错入场: nth-child 跨组件边界恒匹配 1, 改为 index 属性驱动内联 animation-delay, 14 处调用点补序号
注意: tts 云函数需重新部署, 否则新增的 3 条词条返回 Unknown prompt key(静默降级不报错)
19 KiB
2026-08-03 工作日志
规划:分阶段训练(循环训练 circuit)功能
用户想在平板支撑小程序加入"分阶段训练":自定义 每组时长 / 每次组数 / 每周训练次数。 分析现有代码后结论:
- 现有
utils/plan.js是"单组持续撑"模型(hold 模式),计划按天线性加秒;设置页已能自定义公式参数(totalDays/startTarget/increment/cycleDays),但无"组数/休息/周频次"概念。 - 用户要的是"循环训练(circuit)"范式,当前数据模型与计时器都表达不了,但云同步/计划选择/进度条/记录页骨架可复用。
已确认的设计决策(与用户拍板)
- 双模式并存:保留现有3个预设计划和老用户进度(mode:'hold'),新增 circuit 类型(mode:'circuit')。不破坏榜单/已有数据。
- 弹性训练:任意一天都能练;sessionsPerWeek 只用于显示「本周 X/N 次」(自然周周一重置),不卡进度(
getPlanDay已按训练日推进)。 - 扁平 routine 先上:整个计划每次 routine 相同(如 4组×30秒),靠"完成次数"和"每周次数"做进度;逐周进阶作为后续增强,MVP 不做。
约定细节
- 默认组间休息 15 秒(编辑器可改)。
- 循环预设示例:"新手 4组×20秒·休息15秒·3次/周·4周"。
duration始终存"有效撑总秒数 = sets×holdPerSet"(不含休息),保证排行榜/里程碑零改动。
计划改动清单(原方案,较完整,本次未走此路线)
- utils/plan.js:加 mode + circuit 目标解析 + 1 个循环预设
- utils/storage.js:saveRecord 支持 sets/holdPerSet/restPerSet/mode
- pages/timer + 新 utils/circuitTimer.js:circuit 状态机与 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/mode;duration 仍=有效撑总秒(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语音;_saveAndShowCompletion写record.mode='circuit'+sets/holdPerSet/restPerSet/sessionsPerWeek,planId用'circuit'(避免getPlanDay把循环记录算进每日计划进度);onTrainAgain在 circuit 模式重建 FSM;completion overtime 在 circuit 隐藏(每阶段是固定目标,无"超长发挥"概念)。 - pages/index/index.{js,wxml,wxss}:加「循环训练 · 分组练习」ghost 按钮(
repeatFill图标);底部配置弹窗(stepper 调 每组时长/组数/休息/每周目标),确认后saveCircuitConfig+ 跳timer?circuit=1&hold=&sets=&rest=&sessions=。 - utils/icons.js:加
repeat/repeatFill(Material 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-active用flex: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=setIndex、dotsActive=0、dotsNext=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-dot有max-width: 64rpx给每段宽度封顶,.circuit-dots默认justify-content:flex-start→ 组数少时(3-4 组)点阵只占左边一截。此外is-active用flex:2加宽,本身就破坏了"按组数等分"。 - 修法:
.circuit-dot改flex:1 1 0; min-width:0,删掉max-width;.circuit-dots加width:100%。当前组/下一组不再靠"加宽"强调,改为 增高(12rpx → 18rpx,圆角同步 6→9rpx)+ 渐变/发光/呼吸,宽度恒等分。 .circuit-dots加min-height:18rpx锁行高,避免 idle(无 active,行高 12rpx)→ running(有 active,18rpx)时整块跳 6rpx。- transition 从
flex改为height(强调维度变了)。 - 仅改
pages/timer/timer.wxss,无 JS/WXML 改动。
修复:循环训练缺少过程语音(2026-08-03 四次迭代)
用户反馈"循环训练缺少正常训练时候的那些语音提示"。根因有两处,都是设计遗漏:
_circuitCue从未调用_remind:单组模式Timer.onTick里有this._remind(tick)(负责 halfway/last30/last10 三条语音),circuit 的onTick只调_circuitCue,里面仅有"最后一组末 5 秒震动"。_circuitPhase当初刻意不放语音(注释写明"TTS 无第N组/休息词条")。 结果整场只剩 2 声:onStart的start+_onCircuitComplete的complete。
修法
- 新增 3 个 TTS 词条(
utils/voice.js与cloudfunctions/tts/index.js的 PROMPTS 必须同步):restStart(休息一下,调整呼吸) /nextSet(下一组,准备开始) /lastSet(最后一组,全力冲刺!)。刻意不含组号数字 → 固定 3 条覆盖任意组数,无需按 N 合成 N 份音频。 _circuitCue加整场进度播报:基准是total = sets × holdPerSet(整场有效秒,不是每组)——否则 4 组会播 4 次"已完成一半啦",语义全错。判据用done >= total/2和left <= 30 / <= 10(<=而非===,容忍后台回前台跳秒)。只在phase==='working'求值(休息时 workTotal 冻结,天然不会重复触发)。门槛沿用_remind:halfway 需 total>=10、last30 需 total>60、last10 需 total>20。_circuitPhase加阶段语音 +chatty闸门:holdPerSet >= 10才播nextSet/restStart(一句话约 2 秒,短组会变语音连珠炮);restStart另需restPerSet >= 5;lastSet无条件播(唯一值得打断的节点)。第 1 组不播(onStart的start同刻已响)。_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导出,onLoad按isCircuitMode取,避免单组训练白白预载 3 条循环语音。
部署要求
必须重新部署 tts 云函数,否则 3 个新 key 云端返回 Unknown prompt key。降级是安全的(_fetchUrl 拿到 success:false → 返回 null → play 静默 return,不报错、不弹 toast),只是这 3 条不出声。首次播放会各触发一次 TTS 合成(一次性费用,之后走 fileID + 本地文件双层缓存)。
改动文件:utils/voice.js、cloudfunctions/tts/index.js、pages/timer/timer.js。三个 node --check 全过,两边词条 grep 核对一致。
分析(未改代码):训练时禁止息屏
用户问"训练过程中能否禁止屏幕息屏"。结论:能,wx.setKeepScreenOn({keepScreenOn}),基础库 1.4.0+,无授权/无域名/无审核风险。项目当前 零处调用(grep setKeepScreenOn 无匹配)。
为什么这不只是体验优化,而是修 bug
息屏/后台后小程序 JS 定时器被系统节流挂起 → 语音播报(halfway/last30/last10、circuit 的 restStart/nextSet/lastSet)与震动全部丢失。Timer/CircuitTimer 有 Date.now 前台补偿,秒数不会错,但中段提示是废的。平板支撑用户手撑地上不碰屏幕,60 秒必息屏 → 现状几乎每次训练的过程提示都收不到。
挂钩位置(关键)
wx.setKeepScreenOn作用域是整个小程序,跨页面持续生效,只有退出小程序才自动失效 → 在pages/timer打开后,若不手动关,用户返回首页/记录页/排行榜也会一直常亮耗电。- 建议:
onStart开启 →onUnload必须关闭(timer.js 现有onLoad/onShow/onUnload,无onHide)。暂停时保持常亮(用户可能只是短暂调整姿势)。完成弹窗期间也保持,离开页面才关。 wx.setKeepScreenOn是异步 API,fail要静默吞掉(部分 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()的isPausedresume 分支 → 也调一次(暂停期间若切过后台,标志位已被系统清掉)。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:13 的 class="{{justCompleted ? 'just-completed' : ''}}")。而 components/ui-card/ui-card.wxss 只给内部 .ui-card view 写样式,宿主节点没有任何 display 声明 → 宿主是 inline 盒。
inline 盒内包 block 子元素时,inline 盒被打断成"前/后"两个零宽行盒,border 只画在这两个碎片上;左右各 4rpx 贴合 → 视觉上就是 block 上方一条、下方一条竖线,且都贴左侧 = 用户看到的"左上角+左下角两个竖条"。
时序对上:index.js:125-129 在 onShow 置 justCompleted=true,setTimeout 3000ms 后置回 false —— 正是那 2-3 秒。
连带失效(同一根因,用户还没察觉)
.just-completed的animation: justCompletedPulse(transform: scale)从未生效 ——transform对非替换 inline 元素无效。box-shadow同样只画在两个零宽碎片上,橙色光晕看不见。- 全局 grep
:host/display:block在components/零匹配 → 项目里所有自定义组件宿主都是 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。
候选修法(待用户拍板,尚未实施)
- 推荐:
components/ui-card/ui-card.wxss加:host { display: block; }(微信小程序支持:host选择器)。一处修复所有 ui-card 用法,border/shadow/transform 全部恢复正常。风险:宿主从 inline 变 block 可能影响其他页面既有布局,需回归各页面卡片间距。 - 保守版:只把
.just-completed的高亮样式改成作用于内部元素(如页面侧改成给.header-card加高亮),不动组件。影响面最小但治标不治本。 - 顺带可给其余 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.wxml:style="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-completed:border: 4rpx solid→box-shadow: 0 0 0 4rpx var(--primary), 0 4rpx 24rpx rgba(--primary-rgb,.3),加border-radius: 24rpx,去掉两个!important(宿主上已无竞争样式)。- 14 处调用点补
index:index 页 0-1;records 页 0,1,2(trend 分支),2(records 分支,与 trend 互斥故同号),3;settings 页 0-6。
校验与风险
ui-card.json无styleIsolation覆盖(默认 isolated),:host有效。customStyle从未用在 ui-card 上(只有 ui-btn 在 timer 页用),改动无冲突。hidden="{{...}}"(records 5 张卡)安全:微信内置[hidden]{display:none!important}带!important,优先级高于:host的display:block(:host未加 !important)。- 待真机确认:inline→block 会吃掉标签间行内空白,各页卡片间距可能略收紧几 px(属修正,非回归);
transform:scale(1.02)脉冲首次真正生效。