3532d0111b
循环训练(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(静默降级不报错)
330 lines
12 KiB
Plaintext
330 lines
12 KiB
Plaintext
<view class="container page-enter" style="{{themeStyle}}">
|
|
<!-- 个人资料 -->
|
|
<ui-card variant="in" index="0">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.peopleFill}}" mode="aspectFit"></image>
|
|
<text class="section-title">个人资料</text>
|
|
</view>
|
|
<view class="profile-row">
|
|
<button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
|
<image class="avatar-img {{avatarUrl ? 'has-avatar' : ''}}" src="{{avatarUrl || icons.peopleFill}}" mode="{{avatarUrl ? 'aspectFill' : 'aspectFit'}}"></image>
|
|
<view class="avatar-overlay">
|
|
<text>点击设置</text>
|
|
</view>
|
|
</button>
|
|
<view class="profile-info">
|
|
<text class="profile-name">{{nickName || '未设置'}}</text>
|
|
<text class="profile-sub">排行榜将显示此昵称</text>
|
|
</view>
|
|
</view>
|
|
<view class="setting-row">
|
|
<view class="setting-left">
|
|
<image class="setting-icon" src="{{icons.formFill}}" mode="aspectFit"></image>
|
|
<text class="setting-label">昵称</text>
|
|
</view>
|
|
<input
|
|
type="nickname"
|
|
class="nickname-input"
|
|
placeholder="点击设置昵称"
|
|
value="{{nickName}}"
|
|
bindblur="onNicknameBlur"
|
|
maxlength="16"
|
|
/>
|
|
</view>
|
|
</ui-card>
|
|
|
|
<!-- 配色方案 -->
|
|
<ui-card variant="in" index="1">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.skinFill}}" mode="aspectFit"></image>
|
|
<text class="section-title">配色方案</text>
|
|
</view>
|
|
<view class="theme-cards">
|
|
<view
|
|
class="theme-card {{currentThemeId === item.id ? 'active' : ''}}"
|
|
wx:for="{{themes}}"
|
|
wx:key="id"
|
|
data-id="{{item.id}}"
|
|
bindtap="onSelectTheme"
|
|
>
|
|
<view class="theme-card-swatch" style="background: linear-gradient(135deg, {{item.primary}}, {{item.primaryLight}});">
|
|
<image class="theme-card-check" wx:if="{{currentThemeId === item.id}}" src="{{icons.checkWhite}}" mode="aspectFit"></image>
|
|
</view>
|
|
<text class="theme-card-name {{currentThemeId === item.id ? 'active' : ''}}">{{item.name}}</text>
|
|
</view>
|
|
</view>
|
|
</ui-card>
|
|
|
|
<!-- 深色模式 -->
|
|
<ui-card variant="in" index="2">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.skinFill}}" mode="aspectFit"></image>
|
|
<text class="section-title">深色模式</text>
|
|
</view>
|
|
<view class="theme-list">
|
|
<view
|
|
class="theme-item {{darkModePref === item.key ? 'active' : ''}}"
|
|
wx:for="{{darkModeOptions}}"
|
|
wx:key="key"
|
|
data-key="{{item.key}}"
|
|
bindtap="onSelectDarkMode"
|
|
>
|
|
<text class="theme-name">{{item.name}}</text>
|
|
<image class="theme-check-img" wx:if="{{darkModePref === item.key}}" src="{{icons.check}}" mode="aspectFit"></image>
|
|
</view>
|
|
</view>
|
|
</ui-card>
|
|
|
|
<!-- 训练计划选择 -->
|
|
<ui-card variant="in" index="3">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.formFill}}" mode="aspectFit"></image>
|
|
<text class="section-title">训练计划</text>
|
|
</view>
|
|
<view class="plan-list">
|
|
<view
|
|
class="plan-item {{currentPlanId === item.id ? 'active' : ''}}"
|
|
wx:for="{{plans}}"
|
|
wx:key="id"
|
|
data-id="{{item.id}}"
|
|
bindtap="onSelectPlan"
|
|
>
|
|
<view class="plan-radio">
|
|
<view class="radio-dot" wx:if="{{currentPlanId === item.id}}"></view>
|
|
</view>
|
|
<view class="plan-content">
|
|
<text class="plan-item-name">{{item.name}}</text>
|
|
<text class="plan-item-desc">{{item.desc}}</text>
|
|
</view>
|
|
<view class="plan-actions">
|
|
<view class="plan-edit-btn" catch:tap="onTapEditPlan" data-id="{{item.id}}">
|
|
<image class="plan-edit-img" src="{{icons.edit}}" mode="aspectFit"></image>
|
|
</view>
|
|
<image class="plan-check-img" wx:if="{{currentPlanId === item.id}}" src="{{icons.check}}" mode="aspectFit"></image>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</ui-card>
|
|
|
|
<!-- 训练偏好 -->
|
|
<ui-card variant="in" index="4">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.settingsFill}}" mode="aspectFit"></image>
|
|
<text class="section-title">训练偏好</text>
|
|
</view>
|
|
<view class="setting-row">
|
|
<view class="setting-left">
|
|
<image class="setting-icon" src="{{icons.voiceFill}}" mode="aspectFit"></image>
|
|
<text class="setting-label">语音引导</text>
|
|
</view>
|
|
<switch checked="{{voiceGuide}}" bindchange="onToggleVoice" color="{{theme.primary}}"/>
|
|
</view>
|
|
<view class="setting-row">
|
|
<view class="setting-left">
|
|
<image class="setting-icon" src="{{icons.voiceFill}}" mode="aspectFit"></image>
|
|
<text class="setting-label">语音音色</text>
|
|
</view>
|
|
<view class="segmented">
|
|
<view
|
|
class="segmented-item {{voiceGender === item.key ? 'active' : ''}}"
|
|
wx:for="{{voiceGenderOptions}}"
|
|
wx:key="key"
|
|
data-key="{{item.key}}"
|
|
bindtap="onSelectVoiceGender"
|
|
>{{item.name}}</view>
|
|
</view>
|
|
</view>
|
|
<view class="setting-row">
|
|
<view class="setting-left">
|
|
<image class="setting-icon" src="{{icons.notificationFill}}" mode="aspectFit"></image>
|
|
<text class="setting-label">振动反馈</text>
|
|
</view>
|
|
<switch checked="{{vibrate}}" bindchange="onToggleVibrate" color="{{theme.primary}}"/>
|
|
</view>
|
|
<view class="setting-row">
|
|
<view class="setting-left">
|
|
<image class="setting-icon" src="{{icons.notificationFill}}" mode="aspectFit"></image>
|
|
<text class="setting-label">震动强度</text>
|
|
</view>
|
|
<view class="segmented">
|
|
<view
|
|
class="segmented-item {{vibrateIntensity === item.key ? 'active' : ''}}"
|
|
wx:for="{{vibrateIntensityOptions}}"
|
|
wx:key="key"
|
|
data-key="{{item.key}}"
|
|
bindtap="onSelectVibrateIntensity"
|
|
>{{item.name}}</view>
|
|
</view>
|
|
</view>
|
|
</ui-card>
|
|
|
|
<!-- 数据管理 -->
|
|
<ui-card variant="in" index="5">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.deleteActive}}" mode="aspectFit"></image>
|
|
<text class="section-title">数据管理</text>
|
|
</view>
|
|
<ui-btn
|
|
variant="danger"
|
|
size="md"
|
|
block
|
|
text="清除所有训练记录"
|
|
icon-src="{{icons.delete}}"
|
|
bindtap="onClearData"
|
|
></ui-btn>
|
|
</ui-card>
|
|
|
|
<!-- 关于 -->
|
|
<ui-card variant="in" index="6">
|
|
<view class="section-header">
|
|
<image class="section-icon" src="{{icons.infoFill}}" mode="aspectFit"></image>
|
|
<text class="section-title">关于</text>
|
|
</view>
|
|
<view class="about-list">
|
|
<view class="about-row">
|
|
<text class="about-label">版本</text>
|
|
<text class="about-value">{{appVersion}}</text>
|
|
</view>
|
|
<view class="about-row">
|
|
<text class="about-label">更新日期</text>
|
|
<text class="about-value">{{appUpdatedAt}}</text>
|
|
</view>
|
|
<view class="about-row" bindtap="onCopyWechat">
|
|
<text class="about-label">开发者</text>
|
|
<text class="about-value about-link">{{appDeveloper}}</text>
|
|
</view>
|
|
<view class="about-row" bindtap="onCopyOfficialAccount">
|
|
<text class="about-label">公众号</text>
|
|
<text class="about-value about-link">{{appOfficialAccount}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="about-footer">
|
|
<text class="about-copy">Made with ❤️ for better fitness</text>
|
|
</view>
|
|
</ui-card>
|
|
</view>
|
|
<!-- /container — 关闭在这里,让下面的弹窗 fixed 相对视口而非 .container -->
|
|
|
|
<!-- 训练计划编辑器:用普通 view 不用 ui-modal,
|
|
因为 WeChat 自定义组件根元素上 position: fixed 会被吃,导致 body 被推到容器末尾。
|
|
普通 view 的 position: fixed 在小程序里 100% 生效。 -->
|
|
<view wx:if="{{showPlanEditor}}" class="plan-editor-mask" style="{{themeStyle}}">
|
|
<view class="plan-editor-mask__bg" bindtap="onCloseEditor" catchtouchmove="onNoop"></view>
|
|
<view class="plan-editor-sheet" catchtap="onNoop">
|
|
<view class="plan-editor-handle"></view>
|
|
<view class="editor-title">编辑训练计划</view>
|
|
|
|
<view class="editor-field">
|
|
<text class="editor-label">计划名称</text>
|
|
<input
|
|
class="editor-input"
|
|
type="text"
|
|
value="{{editingDraft.name}}"
|
|
bindinput="onEditName"
|
|
placeholder="例如:初级"
|
|
maxlength="20"
|
|
/>
|
|
</view>
|
|
|
|
<view class="editor-field">
|
|
<text class="editor-label">总天数</text>
|
|
<input
|
|
class="editor-input"
|
|
type="number"
|
|
value="{{editingDraft.totalDays}}"
|
|
bindinput="onEditTotalDays"
|
|
placeholder="1-365"
|
|
/>
|
|
</view>
|
|
|
|
<view class="editor-row">
|
|
<view class="editor-field-half">
|
|
<text class="editor-label">起始时长(秒)</text>
|
|
<input
|
|
class="editor-input"
|
|
type="number"
|
|
value="{{editingDraft.startTarget}}"
|
|
bindinput="onEditStartTarget"
|
|
placeholder="5-3600"
|
|
/>
|
|
</view>
|
|
<view class="editor-field-half">
|
|
<text class="editor-label">每次增加(秒)</text>
|
|
<input
|
|
class="editor-input"
|
|
type="number"
|
|
value="{{editingDraft.increment}}"
|
|
bindinput="onEditIncrement"
|
|
placeholder="0-300"
|
|
/>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="editor-field">
|
|
<text class="editor-label">每几天增加</text>
|
|
<input
|
|
class="editor-input"
|
|
type="number"
|
|
value="{{editingDraft.cycleDays}}"
|
|
bindinput="onEditCycleDays"
|
|
placeholder="1-30"
|
|
/>
|
|
</view>
|
|
|
|
<view class="editor-preview">
|
|
<view class="editor-preview-head">
|
|
<text class="editor-preview-title">预览</text>
|
|
<text class="editor-preview-meta">共 {{editorPreview.length}} 天</text>
|
|
</view>
|
|
<view class="editor-preview-list">
|
|
<view class="editor-preview-row" wx:for="{{editorPreview}}" wx:key="day">
|
|
<text class="editor-preview-day">第 {{item.day}} 天</text>
|
|
<text class="editor-preview-target">{{item.target}} 秒</text>
|
|
</view>
|
|
<view class="editor-preview-empty" wx:if="{{editorPreview.length === 0}}">
|
|
<text>请填写总天数</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="editor-actions">
|
|
<view class="editor-action-reset" wx:if="{{editingIsCustom}}" bindtap="onResetPlan">
|
|
<text>恢复默认</text>
|
|
</view>
|
|
<view class="editor-action-cancel" bindtap="onCloseEditor">
|
|
<text>取消</text>
|
|
</view>
|
|
<view class="editor-action-save" bindtap="onSavePlan">
|
|
<text>保存</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 清除数据确认弹窗:用自定义 ui-modal 替代 wx.showModal
|
|
避免开发者工具中 wx.showModal 首次点击不关闭的问题。
|
|
外层 view 注入 themeStyle,确保弹窗跟随暗黑模式切换。 -->
|
|
<view style="{{themeStyle}}">
|
|
<ui-modal
|
|
visible="{{showClearConfirm}}"
|
|
position="center"
|
|
showHandle="{{false}}"
|
|
bind:close="onCancelClear"
|
|
>
|
|
<view class="confirm-dialog" catchtap="onNoop">
|
|
<text class="confirm-title">清除数据</text>
|
|
<text class="confirm-desc">将删除所有训练记录和连续打卡数据,此操作不可恢复。确定继续吗?</text>
|
|
<view class="confirm-actions">
|
|
<view class="confirm-btn confirm-btn--cancel" bindtap="onCancelClear">
|
|
<text>取消</text>
|
|
</view>
|
|
<view class="confirm-btn confirm-btn--danger" bindtap="onConfirmClear">
|
|
<text>确定清除</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</ui-modal>
|
|
</view>
|
|
|
|
<!-- 公众号名称直接在「关于」展示,点击复制名称,无需弹窗引导 -->
|