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:
@@ -34,6 +34,12 @@ Page({
|
||||
presets: [30, 60, 90, 120, 180, 300],
|
||||
customDuration: 60,
|
||||
customInput: '',
|
||||
// --- Circuit (循环训练) config panel (persisted via storage.saveCircuitConfig) ---
|
||||
showCircuitPicker: false,
|
||||
cHold: 30,
|
||||
cSets: 4,
|
||||
cRest: 15,
|
||||
cSessions: 3,
|
||||
icons: iconsMod.build()
|
||||
},
|
||||
|
||||
@@ -206,6 +212,60 @@ Page({
|
||||
wx.navigateTo({ url: `/pages/timer/timer?free=${dur}` })
|
||||
},
|
||||
|
||||
/**
|
||||
* 循环训练入口:打开配置弹窗,预填上次保存的设置(getCircuitConfig 含默认值
|
||||
* + 校验),用户改完确认后持久化并跳计时器。设置因此被保存下来——下次打开直接
|
||||
* 沿用,无需重填。
|
||||
*/
|
||||
onCircuitTrain() {
|
||||
const cfg = storage.getCircuitConfig()
|
||||
this.setData({
|
||||
showCircuitPicker: true,
|
||||
cHold: cfg.holdPerSet,
|
||||
cSets: cfg.sets,
|
||||
cRest: cfg.restPerSet,
|
||||
cSessions: cfg.sessionsPerWeek
|
||||
})
|
||||
},
|
||||
|
||||
onCloseCircuitPicker() {
|
||||
this.setData({ showCircuitPicker: false })
|
||||
},
|
||||
|
||||
onCircuitStep(e) {
|
||||
const field = e.currentTarget.dataset.field
|
||||
const delta = Number(e.currentTarget.dataset.delta) || 0
|
||||
const keyMap = { hold: 'cHold', sets: 'cSets', rest: 'cRest', sessions: 'cSessions' }
|
||||
const key = keyMap[field]
|
||||
if (!key) return
|
||||
const bounds = {
|
||||
hold: [5, 600],
|
||||
sets: [1, 50],
|
||||
rest: [0, 600],
|
||||
sessions: [1, 14]
|
||||
}
|
||||
let v = this.data[key] + delta
|
||||
const min = bounds[field][0]
|
||||
const max = bounds[field][1]
|
||||
if (v < min) v = min
|
||||
if (v > max) v = max
|
||||
this.setData({ [key]: v })
|
||||
},
|
||||
|
||||
onConfirmCircuit() {
|
||||
const cfg = {
|
||||
holdPerSet: this.data.cHold,
|
||||
sets: this.data.cSets,
|
||||
restPerSet: this.data.cRest,
|
||||
sessionsPerWeek: this.data.cSessions
|
||||
}
|
||||
storage.saveCircuitConfig(cfg)
|
||||
this.setData({ showCircuitPicker: false })
|
||||
wx.navigateTo({
|
||||
url: `/pages/timer/timer?circuit=1&hold=${cfg.holdPerSet}&sets=${cfg.sets}&rest=${cfg.restPerSet}&sessions=${cfg.sessionsPerWeek}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 分享给朋友。微信在用户点击右上角"···"菜单里的"转发给朋友"、
|
||||
* 或者页面内任何 `<button open-type="share">` / `<button open-type="share">`
|
||||
|
||||
+66
-2
@@ -10,7 +10,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 连续打卡卡片:火焰连胜 + 分段进度 -->
|
||||
<ui-card variant="gradient in" class="{{justCompleted ? 'just-completed' : ''}}">
|
||||
<ui-card variant="gradient in" index="0" class="{{justCompleted ? 'just-completed' : ''}}">
|
||||
<view class="header-card">
|
||||
<!-- 上区:连胜视觉重心 + 计划徽章 -->
|
||||
<view class="streak-row">
|
||||
@@ -54,7 +54,7 @@
|
||||
</ui-card>
|
||||
|
||||
<!-- 今日目标卡片 -->
|
||||
<ui-card variant="in">
|
||||
<ui-card variant="in" index="1">
|
||||
<view class="target-section">
|
||||
<view class="target-header">
|
||||
<image class="section-icon" src="{{icons.targetFill}}" mode="aspectFit"></image>
|
||||
@@ -99,6 +99,15 @@
|
||||
icon-src="{{icons.timeFill}}"
|
||||
bindtap="onFreeTrain"
|
||||
></ui-btn>
|
||||
|
||||
<ui-btn
|
||||
variant="ghost"
|
||||
size="md"
|
||||
block
|
||||
text="循环训练 · 分组练习"
|
||||
icon-src="{{icons.repeatFill}}"
|
||||
bindtap="onCircuitTrain"
|
||||
></ui-btn>
|
||||
</view>
|
||||
|
||||
<!-- 时间选择弹窗 -->
|
||||
@@ -142,4 +151,59 @@
|
||||
></ui-btn>
|
||||
<text class="picker-cancel" bindtap="onClosePicker">取消</text>
|
||||
</ui-modal>
|
||||
|
||||
<!-- 循环训练配置弹窗:每组时长 / 组数 / 休息 / 每周次数,保存后持久化 -->
|
||||
<ui-modal
|
||||
visible="{{showCircuitPicker}}"
|
||||
position="bottom"
|
||||
bind:close="onCloseCircuitPicker"
|
||||
>
|
||||
<text class="picker-title">循环训练设置</text>
|
||||
<text class="picker-sub">自定义每组时长、组数与组间休息</text>
|
||||
|
||||
<view class="circuit-row">
|
||||
<text class="circuit-row-label">每组时长</text>
|
||||
<view class="stepper">
|
||||
<view class="stepper-btn" data-field="hold" data-delta="-5" bindtap="onCircuitStep">−</view>
|
||||
<text class="stepper-val">{{cHold}}秒</text>
|
||||
<view class="stepper-btn" data-field="hold" data-delta="5" bindtap="onCircuitStep">+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="circuit-row">
|
||||
<text class="circuit-row-label">组数</text>
|
||||
<view class="stepper">
|
||||
<view class="stepper-btn" data-field="sets" data-delta="-1" bindtap="onCircuitStep">−</view>
|
||||
<text class="stepper-val">{{cSets}}组</text>
|
||||
<view class="stepper-btn" data-field="sets" data-delta="1" bindtap="onCircuitStep">+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="circuit-row">
|
||||
<text class="circuit-row-label">组间休息</text>
|
||||
<view class="stepper">
|
||||
<view class="stepper-btn" data-field="rest" data-delta="-5" bindtap="onCircuitStep">−</view>
|
||||
<text class="stepper-val">{{cRest}}秒</text>
|
||||
<view class="stepper-btn" data-field="rest" data-delta="5" bindtap="onCircuitStep">+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="circuit-row">
|
||||
<text class="circuit-row-label">每周目标</text>
|
||||
<view class="stepper">
|
||||
<view class="stepper-btn" data-field="sessions" data-delta="-1" bindtap="onCircuitStep">−</view>
|
||||
<text class="stepper-val">{{cSessions}}次/周</text>
|
||||
<view class="stepper-btn" data-field="sessions" data-delta="1" bindtap="onCircuitStep">+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<ui-btn
|
||||
variant="primary"
|
||||
size="lg"
|
||||
block
|
||||
text="保存并开始"
|
||||
bindtap="onConfirmCircuit"
|
||||
></ui-btn>
|
||||
<text class="picker-cancel" bindtap="onCloseCircuitPicker">取消</text>
|
||||
</ui-modal>
|
||||
</view>
|
||||
|
||||
+65
-2
@@ -404,11 +404,74 @@
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* ---- circuit (循环训练) config panel ---- */
|
||||
.picker-sub {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin: -18rpx 0 24rpx;
|
||||
}
|
||||
|
||||
.circuit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 8rpx;
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
}
|
||||
|
||||
.circuit-row-label {
|
||||
font-size: 30rpx;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stepper-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-soft);
|
||||
color: var(--primary);
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
transition: transform 0.12s ease, opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.stepper-btn:active {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stepper-val {
|
||||
min-width: 124rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ---- 训练完成高亮 (3 秒后淡出) ---- */
|
||||
/* 这个 class 落在 <ui-card> 宿主节点上(ui-card.wxss 已把宿主设为 display:block,
|
||||
否则 border/box-shadow 只会碎成两根竖条、transform 完全不生效)。
|
||||
描边用 box-shadow 的 spread 而非 border:border 会撑大宿主 8rpx,高亮出现/消失
|
||||
时整页布局跳动;box-shadow 不占布局空间。宿主自身无圆角,必须补上与内部卡片
|
||||
一致的 24rpx,否则描边会是直角框套在圆角卡片外面。 */
|
||||
.just-completed {
|
||||
animation: justCompletedPulse 1s ease-in-out 2;
|
||||
border: 4rpx solid var(--primary) !important;
|
||||
box-shadow: 0 4rpx 24rpx rgba(var(--primary-rgb), 0.3) !important;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 0 0 4rpx var(--primary), 0 4rpx 24rpx rgba(var(--primary-rgb), 0.3);
|
||||
}
|
||||
|
||||
@keyframes justCompletedPulse {
|
||||
|
||||
Reference in New Issue
Block a user