Files
wx_pbzc/pages/settings/settings.wxml
T
lc a7ed017267 feat(voice): 语音参数移入 config.js + 设置页男女声切换
- config: 新增 tts.female/male 两组参数(voiceType/volume/speed),
  直接改数字即可调音色,无需改逻辑代码
- storage: getSettings 兜底 voiceGender(旧用户迁移为女声)
- voice: 新增 _getTtsParams 读 config+性别;缓存 key 含完整参数,
  改音色/音量/性别自动重新合成,无需手动清缓存
- settings: 训练偏好加"语音音色"segmented(女声/男声)
- tts 云函数: _synthesize 用传入 opts 合成;_upload 路径含 voiceType
  (男/女不同文件);需重新部署

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 10:08:09 +08:00

302 lines
11 KiB
Plaintext

<view class="container page-enter" style="{{themeStyle}}">
<!-- 个人资料 -->
<ui-card variant="in">
<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" src="{{avatarUrl || icons.peopleFill}}" mode="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">
<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 {{currentThemeId === item.id ? 'active' : ''}}"
wx:for="{{themes}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onSelectTheme"
>
<view class="theme-color-dot" style="background: {{item.primary}};"></view>
<text class="theme-name">{{item.name}}</text>
<image class="theme-check-img" wx:if="{{currentThemeId === item.id}}" src="{{icons.check}}" mode="aspectFit"></image>
</view>
</view>
</ui-card>
<!-- 深色模式 -->
<ui-card variant="in">
<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">
<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">
<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>
</ui-card>
<!-- 数据管理 -->
<ui-card variant="in">
<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">
<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>
<view class="about-footer">
<text class="about-copy">Made with ❤️ for better fitness</text>
</view>
</ui-card>
<!-- 训练计划编辑器:用普通 view 不用 ui-modal,
因为 WeChat 自定义组件根元素上 position: fixed 会被吃,导致 body 被推到容器末尾。
普通 view 的 position: fixed 在小程序里 100% 生效。 -->
<view wx:if="{{showPlanEditor}}" class="plan-editor-mask" catchtouchmove="onNoop">
<view class="plan-editor-mask__bg" bindtap="onCloseEditor"></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-save" bindtap="onSavePlan">
<text>保存</text>
</view>
</view>
<text class="editor-cancel" bindtap="onCloseEditor">取消</text>
</view>
</view>
<!-- 清除数据确认弹窗:用自定义 ui-modal 替代 wx.showModal
避免开发者工具中 wx.showModal 首次点击不关闭的问题 -->
<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>