Files
wx_pbzc/pages/records/records.wxml
T
lc 3532d0111b 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(静默降级不报错)
2026-08-03 16:34:22 +08:00

244 lines
12 KiB
Plaintext

<view class="container page-enter" style="{{themeStyle}}">
<!-- 加载骨架屏 (200ms 最小显示) -->
<block wx:if="{{loading}}">
<ui-skeleton variant="card" />
<view style="height: 24rpx;" />
<ui-skeleton variant="card" />
</block>
<!-- 真实内容 (loading 时隐藏) -->
<view hidden="{{loading}}">
<!-- 首次空状态: 完全没训练记录时显示引导卡 -->
<view class="first-train-prompt" wx:if="{{totalSessions === 0}}">
<view class="empty-illus first-train-illus">
<view class="empty-illus-body"></view>
<view class="empty-illus-arm empty-illus-arm--l"></view>
<view class="empty-illus-arm empty-illus-arm--r"></view>
<view class="empty-illus-bubble">开始</view>
</view>
<text class="first-train-title">开始你的第一次训练</text>
<text class="first-train-sub">每一次坚持都值得被记录</text>
<view class="first-train-cta" bindtap="onStartTrain">
<text>开始训练</text>
</view>
</view>
<!-- 统计卡片 (有数据时才显示) -->
<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>
<text class="stat-num">{{totalSessions}}次</text>
<text class="stat-label">训练次数</text>
</view>
<view class="stat-item">
<image class="stat-icon" src="{{icons.timeFill}}" mode="aspectFit"></image>
<text class="stat-num">{{totalDurationText}}</text>
<text class="stat-label">累计时长</text>
</view>
<view class="stat-item">
<image class="stat-icon" src="{{icons.crownFill}}" mode="aspectFit"></image>
<text class="stat-num">{{maxDurationText}}</text>
<text class="stat-label">最长记录</text>
</view>
</view>
</ui-card>
<!-- 训练勋章 (里程碑进度条) -->
<ui-card variant="in" index="1" hidden="{{totalSessions === 0}}">
<view class="badge-section">
<view class="badge-header">
<view class="badge-title-wrap">
<image class="badge-title-icon" src="{{icons.crownFill}}" mode="aspectFit"></image>
<text class="section-title">训练勋章</text>
</view>
</view>
<view class="badge-status">
<text class="badge-status-days">累计训练 <text class="badge-days-num">{{totalDays}}</text> 天</text>
<text class="badge-status-dot" wx:if="{{nextBadgeHint}}">·</text>
<text class="badge-status-next" wx:if="{{nextBadgeHint}}">{{nextBadgeHint}}</text>
</view>
<view class="badge-track">
<view class="badge-segment" wx:for="{{badgeSegments}}" wx:key="index"
style="left: {{item.left}}%; width: {{item.width}}%;">
<view class="badge-segment-fill" style="width: {{item.progress}}%;"></view>
</view>
<view class="badge-node {{item.unlocked ? 'unlocked' : ''}} {{item.justUnlocked ? 'celebrate' : ''}}"
wx:for="{{trainingBadges}}" wx:key="days"
style="left: {{item.pos}}%;"
data-days="{{item.days}}"
bindtap="onBadgeTap">
<image class="badge-node-icon" src="{{item.unlocked ? icons[item.iconWhite] : icons[item.icon]}}" mode="aspectFit"></image>
<view class="badge-check" wx:if="{{item.unlocked}}"></view>
</view>
</view>
<view class="badge-labels">
<text class="badge-label {{item.unlocked ? 'unlocked' : ''}}"
wx:for="{{trainingBadges}}" wx:key="days"
style="left: {{item.pos}}%;">{{item.label}}</text>
</view>
</view>
</ui-card>
<!-- 内容分区:日历与记录共享月份状态,趋势按需展示,减少默认页长度。 -->
<view class="records-content-tabs" hidden="{{totalSessions === 0}}">
<view class="records-content-tab {{contentMode === 'records' ? 'active' : ''}}" data-mode="records" bindtap="onToggleContentMode">记录日历</view>
<view class="records-content-tab {{contentMode === 'trend' ? 'active' : ''}}" data-mode="trend" bindtap="onToggleContentMode">趋势</view>
</view>
<!-- 训练趋势 -->
<block wx:if="{{contentMode === 'trend'}}">
<ui-card variant="in" index="2" hidden="{{totalSessions === 0}}">
<view class="trend-section">
<view class="trend-header">
<view class="trend-title-wrap">
<image class="history-title-icon" src="{{icons.targetFill}}" mode="aspectFit"></image>
<text class="section-title">训练趋势</text>
</view>
<view class="rec-seg">
<view class="rec-seg-item {{trendMode === 'week' ? 'active' : ''}}" data-mode="week" bindtap="onToggleTrendMode">近7天</view>
<view class="rec-seg-item {{trendMode === 'month' ? 'active' : ''}}" data-mode="month" bindtap="onToggleTrendMode">近6月</view>
</view>
</view>
<view class="trend-summary">
<view class="trend-summary-item">
<text class="trend-summary-value">{{trendSummary.totalText}}</text>
<text class="trend-summary-label">累计时长</text>
</view>
<view class="trend-summary-item">
<text class="trend-summary-value">{{trendSummary.activeCount}}</text>
<text class="trend-summary-label">{{trendSummary.activeLabel}}</text>
</view>
<view class="trend-summary-item">
<text class="trend-summary-value">{{trendSummary.streakCount}}</text>
<text class="trend-summary-label">连续打卡</text>
</view>
</view>
<trend-chart chart-data="{{trendData}}"></trend-chart>
<view class="trend-highlight">
<image class="trend-highlight-icon" src="{{icons.sparkleFill}}" mode="aspectFit"></image>
<text class="trend-highlight-title">{{trendSummary.peakTitle}}</text>
<text class="trend-highlight-text">{{trendSummary.peakText}}</text>
</view>
<view class="best-trend-header">
<view class="trend-title-wrap">
<image class="history-title-icon" src="{{icons.crownFill}}" mode="aspectFit"></image>
<text class="best-trend-title">个人最佳趋势</text>
</view>
<text class="best-trend-best">{{bestTrendSummary.peakText}}</text>
</view>
<trend-chart chart-data="{{bestTrendData}}" variant="peak"></trend-chart>
</view>
</ui-card>
</block>
<!-- 日历热力图 -->
<block wx:if="{{contentMode === 'records'}}">
<ui-card variant="in" index="2" hidden="{{totalSessions === 0}}">
<view class="calendar-section">
<view class="month-picker">
<view class="month-arrow" bindtap="onPrevMonth">
<image class="month-arrow-img" src="{{icons.back}}" mode="aspectFit"></image>
</view>
<view class="month-title-wrap">
<image class="month-icon" src="{{icons.calendarFill}}" mode="aspectFit"></image>
<text class="month-title">{{currentYear}}年 {{currentMonth}}月</text>
</view>
<view class="month-arrow" bindtap="onNextMonth">
<image class="month-arrow-img" src="{{icons.right}}" mode="aspectFit"></image>
</view>
</view>
<calendar-heatmap
year="{{currentYear}}"
month="{{currentMonth}}"
records="{{monthRecords}}"
primaryColor="{{theme.primary}}"
binddaytap="onDayTap"
></calendar-heatmap>
</view>
</ui-card>
<!-- 历史记录 -->
<ui-card variant="in" index="3" hidden="{{totalSessions === 0}}">
<view class="history-section">
<view class="history-header">
<view class="history-title-wrap">
<image class="history-title-icon" src="{{icons.formFill}}" mode="aspectFit"></image>
<text class="section-title">{{listMode === 'month' ? currentYear + '年' + currentMonth + '月记录' : '最近记录'}}</text>
</view>
<view class="rec-seg">
<view class="rec-seg-item {{listMode === 'month' ? 'active' : ''}}" data-mode="month" bindtap="onToggleListMode">本月</view>
<view class="rec-seg-item {{listMode === 'recent' ? 'active' : ''}}" data-mode="recent" bindtap="onToggleListMode">最近</view>
</view>
</view>
<text class="history-hint" wx:if="{{displayList.length > 0}}">长按可删除</text>
<view wx:if="{{displayList.length === 0}}" class="empty-state">
<view class="empty-illus">
<view class="empty-illus-body"></view>
<view class="empty-illus-arm empty-illus-arm--l"></view>
<view class="empty-illus-arm empty-illus-arm--r"></view>
<view class="empty-illus-bubble">开始</view>
</view>
<text class="empty-text">{{listMode === 'month' ? '本月还没有训练记录' : '还没有训练记录'}}</text>
<text class="empty-sub">{{listMode === 'month' ? '换个有训练的月份看看吧' : '完成第一次训练,开启你的坚持!'}}</text>
</view>
<view class="history-list">
<view class="history-item" wx:for="{{displayList}}" wx:key="id" data-id="{{item.id}}" bindlongpress="onLongPressDelete">
<image class="history-item-icon"
src="{{item.duration >= 120 ? icons.hotFill : item.duration >= 60 ? icons.likeFill : icons.roundCheckFill}}"
mode="aspectFit"
></image>
<view class="history-left">
<text class="history-date">{{item.date}}</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>
<view class="history-delete-btn" catch:tap="onDeleteRecord" data-id="{{item.id}}">
<image class="history-delete-icon" src="{{icons.deleteActive}}" mode="aspectFit"></image>
<text class="history-delete-text">删除</text>
</view>
</view>
</view>
</view>
</view>
</ui-card>
</block>
</view>
<!-- /hidden wrapper -->
</view>
<!-- 日期详情弹窗:放在 .container 外。 .page-enter 的 transform 让 .container
成为 fixed 的包含块,mask 降级相对 .container(高>>视口),body 居中到
.container 中心(视口外),真机看不到。移到 .container 外,fixed 相对视口;
style 注入 themeStyle 拿暗黑变量。 -->
<view wx:if="{{showDayDetail}}" class="day-detail-mask" style="{{themeStyle}}" catchtouchmove="onNoop">
<view class="day-detail-mask__bg" bindtap="onCloseDayDetail"></view>
<view class="day-detail-body" catchtap="onNoop">
<view class="detail-date-row">
<image class="detail-icon" src="{{icons.calendarFill}}" mode="aspectFit"></image>
<text class="detail-date">{{dayDetail.date}}</text>
</view>
<view class="detail-grid">
<view class="detail-item">
<image class="detail-item-icon" src="{{icons.rankFill}}" mode="aspectFit"></image>
<text class="detail-num">{{dayDetail.sessions}}次</text>
<text class="detail-label">训练次数</text>
</view>
<view class="detail-item">
<image class="detail-item-icon" src="{{icons.timeFill}}" mode="aspectFit"></image>
<text class="detail-num">{{dayDetail.durationText}}</text>
<text class="detail-label">总时长</text>
</view>
<view class="detail-item">
<image class="detail-item-icon" src="{{icons.hotFill}}" mode="aspectFit"></image>
<text class="detail-num">{{dayDetail.calories}}</text>
<text class="detail-label">约消耗 (千卡)</text>
</view>
</view>
<view class="detail-close" bindtap="onCloseDayDetail">
<text>关闭</text>
</view>
</view>
</view>