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:
2026-08-03 16:34:22 +08:00
parent 76cd2b6c0f
commit 3532d0111b
23 changed files with 1433 additions and 44 deletions
+11 -2
View File
@@ -68,7 +68,16 @@ Page({
const streak = storage.validateStreak()
const stats = storage.getTotalStats()
const monthKey = `${this.data.currentYear}-${String(this.data.currentMonth).padStart(2, '0')}`
const records = (storage.getRecordsByMonth(monthKey) || []).map(r => ({ ...r, durationText: util.formatTime(r.duration) }))
// Decorate each record with a display string for circuit (循环) sessions.
const withText = (r) => {
const base = { ...r, durationText: util.formatTime(r.duration) }
if (r.mode === 'circuit') {
const rest = r.restPerSet ? ` · 休${r.restPerSet}s` : ''
base.circuitText = `循环 ${r.sets}组×${r.holdPerSet}s${rest}`
}
return base
}
const records = (storage.getRecordsByMonth(monthKey) || []).map(withText)
// Each month is already sorted newest-first in storage. To get top 30 most recent,
// walk months in reverse chrono order and concat — O(months*30) instead of O(n log n).
@@ -77,7 +86,7 @@ Page({
const historyList = []
for (const mk of monthKeys) {
for (const r of byMonth[mk]) {
historyList.push({ ...r, durationText: util.formatTime(r.duration) })
historyList.push(withText(r))
if (historyList.length >= 30) break
}
if (historyList.length >= 30) break
+6 -6
View File
@@ -24,7 +24,7 @@
</view>
<!-- 统计卡片 (有数据时才显示) -->
<ui-card variant="in" hidden="{{totalSessions === 0}}">
<ui-card variant="in" index="0" hidden="{{totalSessions === 0}}">
<view class="stats">
<view class="stat-item">
<image class="stat-icon" src="{{icons.rankFill}}" mode="aspectFit"></image>
@@ -45,7 +45,7 @@
</ui-card>
<!-- 训练勋章 (里程碑进度条) -->
<ui-card variant="in" hidden="{{totalSessions === 0}}">
<ui-card variant="in" index="1" hidden="{{totalSessions === 0}}">
<view class="badge-section">
<view class="badge-header">
<view class="badge-title-wrap">
@@ -88,7 +88,7 @@
<!-- 训练趋势 -->
<block wx:if="{{contentMode === 'trend'}}">
<ui-card variant="in" hidden="{{totalSessions === 0}}">
<ui-card variant="in" index="2" hidden="{{totalSessions === 0}}">
<view class="trend-section">
<view class="trend-header">
<view class="trend-title-wrap">
@@ -134,7 +134,7 @@
<!-- 日历热力图 -->
<block wx:if="{{contentMode === 'records'}}">
<ui-card variant="in" hidden="{{totalSessions === 0}}">
<ui-card variant="in" index="2" hidden="{{totalSessions === 0}}">
<view class="calendar-section">
<view class="month-picker">
<view class="month-arrow" bindtap="onPrevMonth">
@@ -159,7 +159,7 @@
</ui-card>
<!-- 历史记录 -->
<ui-card variant="in" hidden="{{totalSessions === 0}}">
<ui-card variant="in" index="3" hidden="{{totalSessions === 0}}">
<view class="history-section">
<view class="history-header">
<view class="history-title-wrap">
@@ -190,7 +190,7 @@
></image>
<view class="history-left">
<text class="history-date">{{item.date}}</text>
<text class="history-plan">{{item.day === 0 ? '自由训练' : '第 ' + item.day + ' 天'}}</text>
<text class="history-plan">{{item.mode === 'circuit' ? item.circuitText : (item.day === 0 ? '自由训练' : '第 ' + item.day + ' 天')}}</text>
</view>
<view class="history-right">
<text class="history-duration">{{item.durationText}}</text>