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:
@@ -6,6 +6,7 @@ const SETTINGS_KEY = 'user_settings'
|
||||
const STREAK_KEY = 'current_streak'
|
||||
const CUSTOM_PLANS_KEY = 'custom_plans'
|
||||
const PROFILE_KEY = 'user_profile'
|
||||
const CIRCUIT_CONFIG_KEY = 'circuit_config'
|
||||
|
||||
let _idSeq = 0
|
||||
|
||||
@@ -230,6 +231,53 @@ const saveProfile = (profile) => {
|
||||
|
||||
const getStreak = () => wx.getStorageSync(STREAK_KEY) || { count: 0, lastDate: '' }
|
||||
|
||||
/**
|
||||
* Circuit (循环) training configuration — persisted locally (device-only,
|
||||
* NOT cloud-synced; it's a per-device training preference, not account
|
||||
* data, so no cloud.pushAll()). This is the "持久化" the simplified
|
||||
* circuit-train feature needs: the user's { 每组时长 / 组数 / 休息 / 每周次数 }
|
||||
* survives restarts and pre-fills the config panel next time.
|
||||
*
|
||||
* Shape: {
|
||||
* holdPerSet: number // seconds held per set (>=5)
|
||||
* sets: number // sets per session (>=1)
|
||||
* restPerSet: number // rest seconds between sets (>=0; 0 = none)
|
||||
* sessionsPerWeek: number // weekly target (>=1) — DISPLAY ONLY in the
|
||||
* simplified version, no progress tracking yet
|
||||
* }
|
||||
* Any missing / out-of-range field falls back to a sensible default, so a
|
||||
* corrupt or partial config never crashes the timer.
|
||||
*/
|
||||
const DEFAULT_CIRCUIT_CONFIG = { holdPerSet: 30, sets: 4, restPerSet: 15, sessionsPerWeek: 3 }
|
||||
|
||||
const _clampInt = (v, min, max, fallback) => {
|
||||
const n = parseInt(v)
|
||||
if (!Number.isFinite(n)) return fallback
|
||||
return Math.max(min, Math.min(max, n))
|
||||
}
|
||||
|
||||
const getCircuitConfig = () => {
|
||||
const raw = wx.getStorageSync(CIRCUIT_CONFIG_KEY)
|
||||
if (!raw || typeof raw !== 'object') return { ...DEFAULT_CIRCUIT_CONFIG }
|
||||
return {
|
||||
holdPerSet: _clampInt(raw.holdPerSet, 5, 600, DEFAULT_CIRCUIT_CONFIG.holdPerSet),
|
||||
sets: _clampInt(raw.sets, 1, 50, DEFAULT_CIRCUIT_CONFIG.sets),
|
||||
restPerSet: _clampInt(raw.restPerSet, 0, 600, DEFAULT_CIRCUIT_CONFIG.restPerSet),
|
||||
sessionsPerWeek: _clampInt(raw.sessionsPerWeek, 1, 14, DEFAULT_CIRCUIT_CONFIG.sessionsPerWeek)
|
||||
}
|
||||
}
|
||||
|
||||
const saveCircuitConfig = (cfg) => {
|
||||
if (!cfg || typeof cfg !== 'object') return
|
||||
const clean = {
|
||||
holdPerSet: _clampInt(cfg.holdPerSet, 5, 600, DEFAULT_CIRCUIT_CONFIG.holdPerSet),
|
||||
sets: _clampInt(cfg.sets, 1, 50, DEFAULT_CIRCUIT_CONFIG.sets),
|
||||
restPerSet: _clampInt(cfg.restPerSet, 0, 600, DEFAULT_CIRCUIT_CONFIG.restPerSet),
|
||||
sessionsPerWeek: _clampInt(cfg.sessionsPerWeek, 1, 14, DEFAULT_CIRCUIT_CONFIG.sessionsPerWeek)
|
||||
}
|
||||
wx.setStorageSync(CIRCUIT_CONFIG_KEY, clean)
|
||||
}
|
||||
|
||||
/**
|
||||
* User-customized training plans, keyed by planId (e.g. 'beginner').
|
||||
* Each value is a full plan object built by `utils/plan.generatePlanDays`
|
||||
@@ -474,6 +522,8 @@ module.exports = {
|
||||
getProfile,
|
||||
saveProfile,
|
||||
getStreak,
|
||||
getCircuitConfig,
|
||||
saveCircuitConfig,
|
||||
validateStreak,
|
||||
recomputeStreak,
|
||||
updateStreak,
|
||||
|
||||
Reference in New Issue
Block a user