feat: 循环训练模式 + 语音补全 / 屏幕常亮 / 卡片渲染修复
循环训练(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(静默降级不报错)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# 2026-07-30 工作日志
|
||||
|
||||
## 朋友提供的勋章体系 PRD 评审(纯分析,未改代码)
|
||||
- 方案为三轴:毅力试炼(连续 streak 天数 3/7/15/21/30/66/100)、力量殿堂(累计时长 30min/3h/10h/50h)、极限时刻(单次时长 1/2/5min)。
|
||||
- 结论:方向合理、数据齐(streak.count / totalDuration / maxDuration 现有 storage 均已有)、不浪费现有工作(现有 trainingDayBadges 可降级为"总天数纪念线")。
|
||||
- 三处必须修:
|
||||
- P0 护盾机制只提名字没设计——streak 线生死线,漏一天归零会劝退,需补"消耗品断签保护"规格。
|
||||
- P0 文案/视觉全用 emoji,与用户既定"不想用 emoji"冲突,须换矢量图标(belt/shield/stopwatch 缺,需新增)。
|
||||
- P1 需拍板"首页 C 位放 streak 还是累计天数"(用户早前确认过累计天数可中断,PRD 把 streak 提为核心线,不矛盾但需决策)。
|
||||
- 落地注意:14 枚超单进度条容量→需勋章墙/分 Tab;config 阈值写秒;勋章同获不退(streak 断了不回收)。
|
||||
- 用户明确要求:本次只分析、不改代码。
|
||||
@@ -0,0 +1,51 @@
|
||||
# 2026-07-31 工作日志
|
||||
|
||||
## 排行榜新增「耐力榜」(endurance)
|
||||
- 需求:全局单次最久 top10,显示该次日期;本人未上榜则在底部 my-bar 显示最佳成绩+产生时间。
|
||||
- 云函数 `cloudfunctions/leaderboard/index.js`:
|
||||
- `_buildFromScan` 新增 `endurance` 分支:遍历每人全部 records 取 `max(duration)`,记下 `bestDate`(该条 r.date);`ranked`/`myEntry` 映射均透传 `bestDate`。
|
||||
- `exports.main` 放行 `endurance`;`_rebuildSnapshots` 循环加入 `endurance`(随定时器每 4 分钟重算,存 top500)。
|
||||
- `bestDate` 经 `publicEntry` 的 `...publicData` 展开自动进快照/响应,data.js 无需改。
|
||||
- 客户端 `pages/leaderboard/leaderboard.js`:
|
||||
- `periods` 加 `{key:'endurance',label:'耐力',maxRank:10}`(日/月/年保持 config.leaderboardMaxRank=50)。
|
||||
- `fetchRank` 按当前周期取 `maxRank`(不再写死)。
|
||||
- `_applyResult` 计算 `subText`(耐力=格式化 bestDate 去秒,其余=`N次`);新增 `_formatBestDate` 辅助。
|
||||
- `_applyLocal` 加耐力兜底分支(本地 records 取 max+日期);分享 map 加 `endurance:'耐力'`。
|
||||
- wxml 5 处 `{{x.sessions}}次` → `{{x.subText}}`(领奖台3/列表1/my-bar1)。
|
||||
- wxss `.podium-base__label` 加 `nowrap`+省略号,防窄屏日期串换行撑破领奖台底座。
|
||||
- 校验:`node --check` 云函数+客户端均通过;grep 确认无 `sessions}}次` 残留。
|
||||
- **待办**:改云函数后须重新部署(定时器随部署注册),否则 endurance 周期不生效。改动尚未提交推送,等用户确认。
|
||||
|
||||
## 领奖台底座日期时间拆两行(耐力榜)
|
||||
- 用户要求领奖台底座的「单次日期」拆成日期+时间两行。
|
||||
- `leaderboard.js`:`_applyResult`/`_applyLocal` 给每条耐力榜数据补 `subDate`(YYYY-MM-DD)、`subTime`(HH:MM) 两字段(新增 `_splitBestDate` 从 bestDate 截断);`subText` 仍保留供列表/my-bar 单行使用。
|
||||
- `leaderboard.wxml`:领奖台 3 处 `<text>` 改为 `<view>` 容器 + `wx:if activePeriod==='endurance'` 渲染两行 `podium-base__date`/`podium-base__time`,否则单行 `subText`。
|
||||
- `leaderboard.wxss`:`.podium-base__label` 由单行绝对定位 text 改为 flex column 居中 view(去掉 nowrap/ellipsis),新增 `.podium-base__date`(20rpx)/`.podium-base__time`(18rpx) 缩小字号以适配最矮的第3名底座(48rpx)。
|
||||
- 校验:`node --check` 通过,grep 确认 subDate/subTime/_splitBestDate 已接入。
|
||||
|
||||
## 领奖台第3名贴顶修复 + 顶部文案
|
||||
- 第3名底座仅 48rpx,两行日期时间文字溢出贴顶。统一抬高三阶底座高度:first 96 / second 64→80 / third 48→64(梯度 96/80/64)。
|
||||
- `.podium-base__label` bottom 8→6rpx;耐力榜两行字号缩小:date 20→18rpx、time 18→16rpx。两级余量足以抗微信字体缩放放大。
|
||||
- 领奖台顶部新增哲理文案(仅耐力榜显示,`wx:if activePeriod==='endurance'`):
|
||||
「平板支撑不是一场和时间的恋爱。撑得越久,不一定越好;真正重要的,是每一秒都没有辜负身体。」
|
||||
- 文案配置进 `config.js` 新增 `leaderboardPodiumSlogan`;`leaderboard.js` data 挂 `podiumSlogan: config.leaderboardPodiumSlogan`;wxml 在 `<block wx:else>` 内 podium 前插入 `.podium-slogan` 块;wxss 新增 `.podium-slogan` 样式(居中、淡色、引用感卡片)。
|
||||
- 校验:`node --check` 客户端 + config 均通过;grep 确认 podiumSlogan / leaderboardPodiumSlogan / podium-slogan 三处链路完整。
|
||||
|
||||
## 顶部文案配色与样式微调
|
||||
- 首版 `.podium-slogan` 为灰底灰字,用户认为不好看 → 改为「极淡橙渐变底 + 主色描边 + 主色微光阴影 + 顶部主色大引号(`::before` `\201C`)点睛」金句卡片质感(用 `--primary-rgb`/`--text` 变量,深色模式自适应)。
|
||||
- 用户再要求:去掉引号、文案改斜体。已删除 `.podium-slogan::before` 块;主块加 `font-style: italic`,并把 padding-top 从 26rpx 收到 22rpx 抵消去掉引号后的顶部留白。纯样式改动,无逻辑、无需重新部署云函数。
|
||||
|
||||
## 首页去掉 toolbar 上方小提示卡片
|
||||
- 删除首页底部随机姿势提示卡片(homeTip):wxml 移除 tip-card 整块、wxss 清理 .tip-card/.tip-icon/.tip-text、js 移除 data.homeTip + 随机取 postureTips 逻辑 + setData 引用。
|
||||
- `config.postureTips` 仍被训练页(timer)复用,保留不动。纯前端,编译即可生效,无需重新部署云函数。
|
||||
- 提交:`deaff72`(含 config.js v3.02→v3.1 升版 + 顶部文案标点修正,均无害)。
|
||||
|
||||
## 三个 P2 小修(体检发现)
|
||||
1. **离线兜底缺"N次"**:`pages/leaderboard/leaderboard.js` `_applyLocal` 非 endurance 分支 entry 漏 `subText`,补 `subText: \`${sessions}次\``,与云端 `_applyResult` 对齐。断网/未部署时底部成绩栏不再留白。
|
||||
2. **同天二次训练文案虚高 1 天**:`pages/timer/timer.js` `_detectCelebrationLevel` 未判断 streak.lastDate 是否已为今天。updateStreak 对同天再次训练不递增,故 `nextStreak` 加 `lastIsToday` 判断:是则取 `streak.count`,否则取 `count+1`。只错文案、存储连胜数本就对。用 `storage.getToday().substring(0,10)` 比 lastDate(timer.js 未引入 util,故未用 util.dateOnly)。
|
||||
3. **热力图日期解析脆弱**:`components/calendar-heatmap/calendar-heatmap.js` line 64 原 `parseInt(r.date.split('-')[2])` 靠 parseInt 侥幸解析 `DD HH:MM:SS`。改为 `parseInt(util.dateOnly(r.date).split('-')[2], 10)`,与全局日期规范统一(该组件已引入 util)。
|
||||
- 校验:`node --check` 三文件均通过;grep 确认三处改动已落实。纯前端,无需重新部署云函数。尚未提交推送,等用户确认。
|
||||
|
||||
## 配置澄清
|
||||
- `config.js` 的 `leaderboardMaxRank: 50` 是**客户端显示行数上限**(日/月/年榜 50、耐力榜 10)。
|
||||
- 云函数 `cloudfunctions/leaderboard/index.js:11` `SNAPSHOT_TOP = 500` 是**快照每周期存储人数上限**(写死在云函数,不读 config)。底部「我的成绩」栏靠 findMyEntry 在存下的完整 500 里找本人,故排 51~500 也能在底部显示;仅走缓存快照时受 500 限制(force 实时重算路径在完整全量榜找人,不受 500 限)。两数字各管一摊。
|
||||
@@ -0,0 +1,188 @@
|
||||
# 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.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 四次迭代)
|
||||
|
||||
用户反馈"循环训练缺少正常训练时候的那些语音提示"。**根因有两处,都是设计遗漏**:
|
||||
1. **`_circuitCue` 从未调用 `_remind`**:单组模式 `Timer.onTick` 里有 `this._remind(tick)`(负责 halfway/last30/last10 三条语音),circuit 的 `onTick` 只调 `_circuitCue`,里面仅有"最后一组末 5 秒震动"。
|
||||
2. **`_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()` 的 `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: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。
|
||||
|
||||
### 候选修法(待用户拍板,尚未实施)
|
||||
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.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)` 脉冲首次真正生效。
|
||||
@@ -9,6 +9,8 @@
|
||||
- 加新图标或新彩色容器时,先想清楚底色:浅色底用 `*Fill`(主题色),彩色/主色底用 `*White` 变体。
|
||||
- 主按钮 `ui-btn--primary` 背景是橙渐变,`playFill`(橙) 放上去即隐形——须用 `playWhite`。ghost/outline 按钮底色浅或灰,用 `*Fill`/灰色变体即可。
|
||||
- `components/ui-btn` 图标尺寸 `1.3em`(随按钮字号自适应),不要回到 `1em` 以免显得过小。
|
||||
- **需要「随状态换色」的小装饰件,一律用纯 CSS 画,别用 icon**:图标色改不动,而状态灯/圆点要在 白/绿/灰 间切换。`timer` 页循环训练徽章的 `.circuit-phase-dot` 即是此解(14rpx CSS 圆点 + BEM 状态类控制 background)。
|
||||
- **`timer` 页新增顶部元素必须 `position:relative; z-index:1`**:`.breathe-ring`(呼吸光晕,`z-index:0`)在 DOM 里排在页面上方元素之后,同 stacking 上下文下会反盖上去,把主色光晕晕染到白卡片上。循环训练进度面板 `.circuit-bar` 踩过。
|
||||
- 深色模式有两套事实源,改配色前先统一:`theme.json` 的 dark 与 `app.wxss`/`utils/theme.js` 要一致(已统一到 `#131316` 系)。
|
||||
- **微信圆形头像/图片裁剪陷阱(用户基础库实测)**:本项目的圆形头像在用户机器上表现如下——
|
||||
- 单独给 `<image>` 加 `border-radius:50%`(无 overflow 父层)→ 圆框里露方图,失效。
|
||||
@@ -30,14 +32,50 @@
|
||||
|
||||
## 排行榜架构(2026-07-28 落地 P0/P1/P2 后)
|
||||
- **预计算快照**:`cloudfunctions/leaderboard` 的 `exports.main` 现在区分两种调用——
|
||||
- **定时触发器**(`config.json` 的 `triggers`:`snapshotTimer` / `0 */4 * * * * *`,每 4 分钟)进入 `_rebuildSnapshots()`:扫描一次 `plank_data`(`_fetchLatestByOpenid`,走 updatedAt 索引+投影),复用 `_buildFromScan()` 算 day/month/year 三榜,各自写进 `leaderboard_snapshot` 集合(doc._id=周期,字段 `period/ranked/myOpenid/updatedAt`;首次写自动建集合)。
|
||||
- **定时触发器**(`config.json` 的 `triggers`:`snapshotTimer` / `0 */4 * * * * *`,每 4 分钟)进入 `_rebuildSnapshots()`:扫描一次 `plank_data`(`_fetchLatestByOpenid`,走 updatedAt 索引+投影),复用 `_buildFromScan()` 算 day/month/year/endurance 四榜,各自写进 `leaderboard_snapshot` 集合(doc._id=周期,字段 `period/ranked/myOpenid/updatedAt`;首次写自动建集合)。
|
||||
- **客户端请求**:默认走 `_serveFromSnapshot()`(1 次 `get` ≈ 50ms),快照缺失/过期(>6min)才 `_computeBoard()` 实时重算并回写。**首开/冷启动已从 2-3s 降到 ≈50ms。**
|
||||
- **force 语义**:仅训练后那一次(客户端 `onShow` 读 `_lb_force_refresh` 置 `force=true`)走 `_computeBoard(force:true,persist:true)` 实时重算+回写快照;其他 99% 请求读快照,已满足"force 不再绕过云缓存"诉求。
|
||||
- **P1 启动预热**:`app.js` `onLaunch` 在 `cloud.init()` 后 fire-and-forget 调一次 `leaderboard`(day),提前暖云实例/快照。
|
||||
- **头像延迟**:云函数内 `getTempFileURL` 预解析已落地(在 `_buildFromScan` 里),前端不再懒加载,故"文字先出头像后出"的 1-2s 延迟已大幅消除;快照每 4min 重写,临时 URL 始终新鲜。
|
||||
- **改动注意**:要生效必须**重新部署 `leaderboard` 云函数**(定时器随部署注册)。`plank_data.updatedAt` 单字段索引需在控制台已建(用户已建)。响应结构 `{period,ranked,myOpenid,myEntry,updatedAt}` 不变,客户端 `leaderboard.js` 无需改逻辑(仅注释更新)。
|
||||
- **耐力榜(第 4 个 tab,2026-07-31 新增)**:全局单次最久。`_buildFromScan` 的 `endurance` 分支遍历每人全部 records 取 `max(duration)` 并记下该条 `r.date` 作"产生时间"→ `bestDate` 字段靠 `publicEntry` 的 `...publicData` 展开透传进快照/响应(无需改 data.js)。客户端 `periods` 加 `{key:'endurance',label:'耐力',maxRank:10}`;行/领奖台/my-bar 的副标签统一改用 `subText`(耐力=格式化日期 `YYYY-MM-DD HH:MM`,其余=`N次`)。本人未进 top10 时由既有 my-bar(`myEntry.rank > rankedList.length`)在底部显示其最佳成绩+日期——依赖快照存 top500、`findMyEntry` 在完整 500 条里定位本人,故即便排 200 也能被找到。**改云函数后须重新部署**(定时器随部署注册 `endurance`)。
|
||||
|
||||
- **TTS 语音词条是「两份白名单」,必须同步**:`utils/voice.js` 的 `PROMPTS` 与 `cloudfunctions/tts/index.js` 的 `PROMPTS` 各存一份。客户端有、云端没有 → 云函数返回 `Unknown prompt key`,`_fetchUrl` 得 `success:false` 后返回 null,`play()` 静默 return(**不报错、不弹 toast**,只是不出声,极难察觉)。加词条后**必须重新部署 tts 云函数**。词条设计上要**避免包含数字/变量**(如"第3组"),否则得按 N 合成 N 份音频;用"下一组,准备开始"这类通用句即可覆盖任意组数。
|
||||
- **`voice.play` 会截断前一条语音**(`_playSeq` 机制:新 play 令旧的作废)。凡是可能同一 tick 触发两条语音的地方,必须定优先级。已知必撞场景:circuit 的 halfway 触发点 = `sets×hold/2`,**偶数组时必然落在第 sets/2 组最后一秒**,与 rest 阶段切换同刻 → 用 `_playProgressVoice` 设 `_voiceBusyUntil = now+2600`,让阶段语音在窗口内让位。类似地,每组 <10 秒时不播过场语音(一句话约 2 秒,否则连珠炮)。
|
||||
- **circuit 的进度播报基准必须是整场(`sets × holdPerSet`),不能是每组**:否则 4 组会播 4 次"已完成一半啦"。判据用 `<=` 而非 `===`,容忍小程序后台回前台的跳秒补偿。只在 `phase==='working'` 求值——休息时 `workTotal` 冻结,天然不会重复触发。
|
||||
|
||||
- **`wx.setKeepScreenOn` 是「小程序级」不是「页面级」,且切后台会失效**(训练页已用,`pages/timer/timer.js` 的 `_keepScreenOn`):
|
||||
- 开了不关 → 用户离开训练页后,首页/记录/榜单全程常亮耗电,只有**退出小程序**才自动失效。故 `onUnload` **必须**显式 `keepScreenOn:false`。
|
||||
- 反过来,**切后台再回前台会被系统清掉**,所以 `onShow` 里要 `if (this.data.isRunning) 重新武装`,否则后半程又开始息屏丢语音/震动。
|
||||
- `fail` 必须静默(部分 Android ROM 省电模式拒绝该调用),低基础库用 `typeof wx.setKeepScreenOn !== 'function'` 降级。
|
||||
- 背景:息屏/后台后 JS 定时器被节流,`Timer`/`CircuitTimer` 靠 `Date.now` 补偿秒数不会错,但**实时提示(语音/震动)错过就没了**——平板支撑用户手撑地不碰屏,60 秒必息屏,所以常亮实际是在修 bug 而非锦上添花。
|
||||
|
||||
- **自定义组件宿主节点默认是 inline —— 页面侧给组件标签加 border/box-shadow/transform 会碎裂或静默失效**:本项目 `components/` 下 7 个组件**没有任何一个**写了 `:host { display: block }`(全局 grep `:host`/`display:block` 零匹配),组件 wxss 只给内部那个 view(如 `.ui-card`)写样式。宿主节点(页面 wxml 里的 `<ui-card>` 标签)因此是 inline 盒。
|
||||
- 页面 wxss **可以**命中宿主节点上的 class(style isolation 只隔离组件内部),所以样式确实生效了——但按 inline 盒规则渲染:
|
||||
- `border` → inline 盒被内部 block 打断成前/后两个**零宽行盒**,只在碎片上画左右边框(各 4rpx 贴合成一根竖线)→ 视觉是 block **左上角、左下角各一条竖条**,而不是包一圈。首页 `.just-completed`(训练完成 3 秒高亮)就是这个症状。
|
||||
- `transform: scale()` → **完全无效**(不适用于非替换 inline 元素),脉冲/缩放动画静默不生效。
|
||||
- `box-shadow` → 同样只画在零宽碎片上,等于看不见。
|
||||
- **诊断特征**:只要是"某个自定义组件标签上加了装饰样式,结果出现零散短线/动画不动",先怀疑宿主 inline,而不是怀疑选择器没命中。
|
||||
- **修法**:组件 wxss 加 `:host { display: block; }`(小程序支持 `:host`)。改前需回归各页面该组件的间距布局(inline→block 会吃掉行内空白、宽度变 100%);`ui-btn` 这类可能被当行内用的组件要单独评估。
|
||||
- **同源坑**:`ui-card.wxss` 的 `.ui-card.in:nth-child(1..4)` 交错入场延迟也是失效的——`.ui-card` 在组件内部永远是根下第 1 个子元素,恒匹配 `nth-child(1)`。跨组件边界做"第 N 个"选择器一律不可靠,要交错必须由页面传 index 或内联 `animation-delay`。
|
||||
- **已修(2026-08-03,仅 `ui-card`)**:加了 `:host { display:block; margin-bottom:24rpx; box-sizing:border-box }`,并把 `margin-bottom` 从内部 `.ui-card` **上提到宿主**——否则页面侧加的边框会把 24rpx 外边距一起圈进框里,框比卡片高一截。交错延迟改为 `index` 属性驱动的内联 `animation-delay`(`50 + min(index,3)*100` ms),14 处调用点全部显式传 `index`;`index` 默认 0,漏传也只是退回旧行为,零回归。
|
||||
- **其余 6 个组件宿主仍是 inline,不要盲目批量加 `:host{display:block}`**:`ui-btn` 在 `pages/timer/timer.wxml` 里靠 `custom-style="margin-right:28rpx"` 并排布局,改 block 会直接撑成整行。要改必须逐个回归。
|
||||
- **页面侧给组件标签做描边,优先用 `box-shadow: 0 0 0 Nrpx <color>` 而不是 `border`**:border 会改变宿主盒尺寸(撑大或在 border-box 下向内压窄内容),高亮出现/消失时布局跳动;box-shadow 不占布局空间。但宿主自身没有圆角,必须显式补上与内部卡片一致的 `border-radius`,否则描边是直角框套在圆角卡片外面。
|
||||
|
||||
## 约定
|
||||
- UI 动词说法:index=首页/训练首页,timer=训练页,leaderboard=排行榜,records=记录,settings=设置。
|
||||
- 美化按"见效快优先"分批做,每轮 node --check + grep 残留再交付。
|
||||
- **git 推送需走 sandbox 外**:本机 Bash 工具默认 sandbox 隔离网络,`git push` 到 github.com 会 `SSL connection timeout`(约 5 分钟才报错)。必须用 `dangerouslyDisableSandbox: true` 在沙箱外执行推送(或 `run_in_background` + 沙箱外)才能连上。无 SSH key,remote 仅 HTTPS。
|
||||
|
||||
## 功能:循环训练 circuit(2026-08-03 落地,简化版 + 配置持久化)
|
||||
- 定位:**「自由训练」的循环形态**,不是新计划体系。不碰现有 `utils/plan.js` 的 hold 模式与每日计划进度。
|
||||
- 入口:首页「循环训练 · 分组练习」ghost 按钮 → 底部配置弹窗(stepper 调 每组时长/组数/休息/每周目标)→ `saveCircuitConfig` + 跳 `timer?circuit=1&hold=&sets=&rest=&sessions=`。
|
||||
- 配置持久化:`utils/storage.js` 的 `getCircuitConfig/saveCircuitConfig`(本地 `circuit_config` 键,含默认值+范围校验,**不进云同步**)。下次打开弹窗预填上次值。
|
||||
- 状态机:`utils/circuitTimer.js`(新)`CircuitTimer` 类,接口仿 `Timer`:`start/stop/pause/resume` + `onTick/onPhaseChange/onComplete`;`stop()` 返回 `workTotal`(有效撑总秒,不含休息)。
|
||||
- **关键不变量 1:`duration` 始终 = 有效撑总秒(组数×每组秒,不含休息)** → 排行榜/里程碑/统计零改动。
|
||||
- **关键不变量 2:循环记录 `planId` 用 `'circuit'`** → `getPlanDay` 不会把它算进每日计划进度;但 `updateStreak` 仍计入连胜(用户确实练了)。
|
||||
- 记录落库字段:`mode:'circuit'` + `sets/holdPerSet/restPerSet/sessionsPerWeek`(records 页按 `r.mode` 显示「循环 N组×Ms」)。
|
||||
- 阶段提示:TTS 只有 6 个固定 key 无"第N组/休息"词 → circuit 阶段切换用**震动 + 屏上 phaseTip 文本**,完成复用 `complete` 语音。
|
||||
- 进度环:circuit 模式轨道恒中性灰(否则每阶段都按"累计≥阶段长"误判变绿)。
|
||||
- 未做(可选后续):周频次硬追踪(仅存 sessionsPerWeek 作显示)、逐周进阶、设置页统一管理。
|
||||
- 验证:6 个 JS 全过 `node --check`;未动云函数/榜单/云同步。
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# 循环训练(简化版 + 配置持久化)实现概览
|
||||
|
||||
## 做了什么
|
||||
在「自由训练」并列新增**「循环训练」入口**,让用户自定义 每组时长 / 组数 / 组间休息(外加每周目标次数,仅作显示),并把这些设置**本地持久化**——下次打开直接沿用,无需重填。这正是你要的"简化版 + 至少要有循环训练设置的持久化"。
|
||||
|
||||
## 核心设计
|
||||
- **数据模型零入侵**:循环记录以 `planId:'circuit'` 落库,`getPlanDay` 不会把它算进每日计划进度;`duration` 仍存"有效撑总秒 = 组数×每组秒"(不含休息),排行榜 / 里程碑 / 统计零改动。
|
||||
- **状态机独立**:`utils/circuitTimer.js` 是 `idle→working→resting→…→done` 的 FSM,复用现有 `Timer` 的 `Date.now` + `onAppShow` 前台补偿风格;`stop()` 返回有效撑总秒,直接复用 timer 页现有保存逻辑。
|
||||
- **配置持久化**:`storage.getCircuitConfig / saveCircuitConfig` 存本地,含默认值与范围校验,不进云同步(纯设备偏好)。
|
||||
- **阶段提示**:语音只有固定 6 个 key、无"第N组 / 休息"词,故阶段切换用**震动 + 屏上文本**提示,完成复用 `complete` 语音——不过度依赖 TTS。进度环在循环模式恒中性灰(避免每阶段都"已达标"变绿误导)。
|
||||
|
||||
## 改动文件
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `utils/storage.js` | 加 circuit 配置读写(本地持久化) |
|
||||
| `utils/circuitTimer.js` | 新:循环状态机 |
|
||||
| `pages/timer/timer.{js,wxml,wxss}` | circuit 模式分支、状态条、阶段提示、完成/保存 |
|
||||
| `pages/index/index.{js,wxml,wxss}` | 「循环训练」入口 + 配置弹窗(stepper + 持久化) |
|
||||
| `utils/icons.js` | 加 `repeat` / `repeatFill` 图标 |
|
||||
| `pages/records/records.{js,wxml}` | 循环记录显示「循环 N组×Ms」 |
|
||||
|
||||
## 验证
|
||||
- 6 个 JS 文件全部通过 `node --check`。
|
||||
- 未改动 `plan.js` / 云函数 / 榜单 / 云同步逻辑,首提风险低。
|
||||
|
||||
## 未做(可选后续)
|
||||
- 周频次硬追踪(仅存 `sessionsPerWeek` 作显示,未做"本周 X/N 次"计数)
|
||||
- 逐周自动进阶
|
||||
- 设置页统一管理入口
|
||||
|
||||
## 真机注意
|
||||
- 环形进度用 countUp 动画,不进固定 rpx 盒,规避字体缩放溢出(首页 `_readFontScale` 思路已避开)。
|
||||
- 主色 / 渐变底图标用 White 变体;`repeatFill` 为橙主题色,在 ghost 按钮浅底 OK。
|
||||
Reference in New Issue
Block a user