2e2594d154
- 修复 cloud.clearAll 静默失败导致重启后数据恢复 - 清除数据弹窗改用自定义 ui-modal 替代 wx.showModal - TTS 语音合成增加 fileID 持久化缓存,重启不再重新合成 - 排行榜限制前15名,maxRank 参数化到 config.js - 新增 config.js 统一管理版本号/更新日期/开发者/排行榜限制 - progress-ring 消除 getSystemInfoSync 弃用警告 - voice.js 增加 InnerAudioContext 错误监听 - storage/util 完善用户资料、打卡日期精度、记录日志
395 lines
11 KiB
JavaScript
395 lines
11 KiB
JavaScript
const storage = require('../../utils/storage')
|
|
const themeMod = require('../../utils/theme')
|
|
const planMod = require('../../utils/plan')
|
|
const iconsMod = require('../../utils/icons')
|
|
const cloud = require('../../utils/cloud')
|
|
const config = require('../../config')
|
|
|
|
/**
|
|
* Build the simplified plans list shown in the settings UI by overlaying
|
|
* user-customized plans on top of the 3 presets. Each item carries
|
|
* everything the editor needs to repopulate its form.
|
|
*/
|
|
const buildPlansList = (customPlans) => {
|
|
const presets = planMod.plans // { beginner, intermediate, advanced }
|
|
const custom = customPlans || {}
|
|
const order = ['beginner', 'intermediate', 'advanced']
|
|
return order.map((id) => {
|
|
const p = custom[id] || presets[id]
|
|
return {
|
|
id: p.id,
|
|
name: p.name,
|
|
desc: p.description,
|
|
isCustom: !!custom[id],
|
|
totalDays: p.totalDays,
|
|
startTarget: p.startTarget,
|
|
increment: p.increment,
|
|
cycleDays: p.cycleDays
|
|
}
|
|
})
|
|
}
|
|
|
|
const MAX_DAYS = 365
|
|
const MIN_DAYS = 1
|
|
const MAX_TARGET = 3600
|
|
const MIN_TARGET = 5
|
|
const MAX_INCREMENT = 300
|
|
const MAX_CYCLE = 30
|
|
|
|
const _toInt = (v, fallback) => {
|
|
const n = parseInt(v, 10)
|
|
return Number.isFinite(n) ? n : fallback
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
theme: { primary: '#FF6B35', primaryLight: '#FF8C5A', primaryBg: '#FFF3ED', primaryRgb: '255,107,53' },
|
|
themeStyle: themeMod.BASE_VARS,
|
|
themes: themeMod.THEMES,
|
|
currentThemeId: 'orange',
|
|
plans: [],
|
|
currentPlanId: 'beginner',
|
|
voiceGuide: true,
|
|
vibrate: true,
|
|
icons: iconsMod.build(),
|
|
nickName: '',
|
|
avatarUrl: '',
|
|
// Editor state
|
|
showPlanEditor: false,
|
|
editingPlanId: '',
|
|
editingIsCustom: false,
|
|
editingDraft: null, // { name, totalDays, startTarget, increment, cycleDays }
|
|
editorPreview: [], // [{ day, target }, ...]
|
|
// Clear-data confirmation
|
|
showClearConfirm: false,
|
|
// App info from config
|
|
appVersion: config.version,
|
|
appUpdatedAt: config.updatedAt,
|
|
appDeveloper: config.developer
|
|
},
|
|
|
|
onLoad() {
|
|
const s = storage.getSettings()
|
|
const theme = themeMod.getCurrentTheme()
|
|
themeMod.applyThemeToPage(this)
|
|
const profile = storage.getProfile()
|
|
this.setData({
|
|
currentPlanId: s.planId,
|
|
voiceGuide: s.voiceGuide,
|
|
vibrate: s.vibrate,
|
|
currentThemeId: theme.id,
|
|
theme,
|
|
icons: iconsMod.build(theme),
|
|
plans: buildPlansList(storage.getCustomPlans()),
|
|
nickName: profile.nickname || '',
|
|
avatarUrl: profile.avatarUrl || ''
|
|
})
|
|
},
|
|
|
|
onShow() {
|
|
try {
|
|
const tb = this.getTabBar()
|
|
if (tb) tb.setData({ selected: 3 })
|
|
} catch (e) {}
|
|
themeMod.applyThemeToPage(this)
|
|
const theme = themeMod.getCurrentTheme()
|
|
const profile = storage.getProfile()
|
|
this.setData({
|
|
currentThemeId: theme.id,
|
|
theme,
|
|
icons: iconsMod.build(theme),
|
|
plans: buildPlansList(storage.getCustomPlans()),
|
|
nickName: profile.nickname || '',
|
|
avatarUrl: profile.avatarUrl || ''
|
|
})
|
|
},
|
|
|
|
// --- Profile handlers ---
|
|
|
|
onChooseAvatar(e) {
|
|
const avatarUrl = e.detail.avatarUrl
|
|
if (!avatarUrl) return
|
|
this.setData({ avatarUrl })
|
|
const saved = storage.saveProfile({
|
|
nickname: this.data.nickName,
|
|
avatarUrl
|
|
})
|
|
this.setData({
|
|
nickName: saved.nickname || this.data.nickName,
|
|
avatarUrl: saved.avatarUrl
|
|
})
|
|
wx.showToast({ title: '头像已更新', icon: 'success', duration: 1200 })
|
|
},
|
|
|
|
onNicknameBlur(e) {
|
|
const value = (e.detail.value || '').trim()
|
|
if (!value) return
|
|
this.setData({ nickName: value })
|
|
storage.saveProfile({
|
|
nickname: value,
|
|
avatarUrl: this.data.avatarUrl
|
|
})
|
|
wx.showToast({ title: '昵称已保存', icon: 'success', duration: 1200 })
|
|
},
|
|
|
|
onSelectTheme(e) {
|
|
const id = e.currentTarget.dataset.id
|
|
const theme = themeMod.setTheme(id)
|
|
this.setData({ currentThemeId: id })
|
|
try {
|
|
const tb = this.getTabBar()
|
|
if (tb) tb.setData({ themeStyle: themeMod.getThemeStyle(theme) })
|
|
} catch (e) {}
|
|
themeMod.applyThemeToPage(this)
|
|
},
|
|
|
|
onSelectPlan(e) {
|
|
const planId = e.currentTarget.dataset.id
|
|
const s = storage.getSettings()
|
|
if (s.planId === planId) return
|
|
|
|
wx.showModal({
|
|
title: '切换训练计划',
|
|
content: '切换计划会重置训练进度,原计划的历史记录仍会保留在训练记录中。确定继续吗?',
|
|
confirmText: '确定切换',
|
|
success: (res) => {
|
|
if (!res.confirm) return
|
|
s.planId = planId
|
|
s.planStartDate = storage.getToday()
|
|
storage.saveSettings(s)
|
|
this.setData({ currentPlanId: planId })
|
|
wx.showToast({ title: '计划已切换', icon: 'success', duration: 1200 })
|
|
}
|
|
})
|
|
},
|
|
|
|
onToggleVoice(e) {
|
|
const s = storage.getSettings()
|
|
s.voiceGuide = e.detail.value
|
|
storage.saveSettings(s)
|
|
this.setData({ voiceGuide: e.detail.value })
|
|
},
|
|
|
|
onToggleVibrate(e) {
|
|
const s = storage.getSettings()
|
|
s.vibrate = e.detail.value
|
|
storage.saveSettings(s)
|
|
this.setData({ vibrate: e.detail.value })
|
|
},
|
|
|
|
onCopyWechat() {
|
|
wx.setClipboardData({
|
|
data: config.developer,
|
|
success: () => {
|
|
wx.showToast({ title: '已复制', icon: 'success', duration: 1500 })
|
|
}
|
|
})
|
|
},
|
|
|
|
// Show custom confirmation instead of wx.showModal to avoid the dev-tools
|
|
// bug where the native dialog refuses to dismiss on first tap.
|
|
onClearData() {
|
|
this.setData({ showClearConfirm: true })
|
|
},
|
|
|
|
onCancelClear() {
|
|
this.setData({ showClearConfirm: false })
|
|
},
|
|
|
|
onConfirmClear() {
|
|
this.setData({ showClearConfirm: false })
|
|
this._doClearData()
|
|
},
|
|
|
|
async _doClearData() {
|
|
wx.removeStorageSync('training_records')
|
|
wx.removeStorageSync('current_streak')
|
|
wx.removeStorageSync('user_settings')
|
|
wx.removeStorageSync('app_theme')
|
|
wx.removeStorageSync('custom_plans')
|
|
try { await cloud.clearAll() } catch (e) {}
|
|
wx.setStorageSync('user_settings', {
|
|
planId: 'beginner',
|
|
voiceGuide: true,
|
|
vibrate: true,
|
|
planStartDate: storage.getToday()
|
|
})
|
|
wx.setStorageSync('current_streak', { count: 0, lastDate: '' })
|
|
themeMod.setTheme('orange')
|
|
this.setData({
|
|
currentPlanId: 'beginner',
|
|
voiceGuide: true,
|
|
vibrate: true,
|
|
currentThemeId: 'orange'
|
|
})
|
|
themeMod.applyThemeToPage(this)
|
|
// Reset plans list — custom_plans are gone
|
|
this.setData({ plans: buildPlansList(storage.getCustomPlans()) })
|
|
wx.showToast({ title: '已清除', icon: 'success', duration: 1500 })
|
|
},
|
|
|
|
// -------- Plan editor --------
|
|
|
|
onTapEditPlan(e) {
|
|
// stopPropagation: prevent the row's onSelectPlan from also firing
|
|
const id = e.currentTarget.dataset.id
|
|
if (!id) return
|
|
const plan = planMod.getPlan(id, storage.getCustomPlans())
|
|
const isCustom = !!storage.getCustomPlans()[id]
|
|
const draft = {
|
|
name: plan.name,
|
|
totalDays: plan.totalDays,
|
|
startTarget: plan.startTarget,
|
|
increment: plan.increment,
|
|
cycleDays: plan.cycleDays
|
|
}
|
|
this.setData({
|
|
showPlanEditor: true,
|
|
editingPlanId: id,
|
|
editingIsCustom: isCustom,
|
|
editingDraft: draft,
|
|
editorPreview: plan.days
|
|
})
|
|
},
|
|
|
|
onCloseEditor() {
|
|
this.setData({ showPlanEditor: false })
|
|
},
|
|
|
|
// Swallow bubble for inner touches (e.g. tapping the sheet body
|
|
// shouldn't dismiss the modal — only the backdrop should).
|
|
onNoop() { /* no-op */ },
|
|
|
|
_recomputePreview(draft) {
|
|
return planMod.generatePlanDays({
|
|
totalDays: draft.totalDays,
|
|
startTarget: draft.startTarget,
|
|
increment: draft.increment,
|
|
cycleDays: draft.cycleDays
|
|
})
|
|
},
|
|
|
|
onEditName(e) {
|
|
const draft = { ...this.data.editingDraft, name: e.detail.value }
|
|
this.setData({ editingDraft: draft })
|
|
},
|
|
|
|
onEditTotalDays(e) {
|
|
const draft = {
|
|
...this.data.editingDraft,
|
|
totalDays: _toInt(e.detail.value, 0)
|
|
}
|
|
this.setData({
|
|
editingDraft: draft,
|
|
editorPreview: this._recomputePreview(draft)
|
|
})
|
|
},
|
|
|
|
onEditStartTarget(e) {
|
|
const draft = {
|
|
...this.data.editingDraft,
|
|
startTarget: _toInt(e.detail.value, 0)
|
|
}
|
|
this.setData({
|
|
editingDraft: draft,
|
|
editorPreview: this._recomputePreview(draft)
|
|
})
|
|
},
|
|
|
|
onEditIncrement(e) {
|
|
const draft = {
|
|
...this.data.editingDraft,
|
|
increment: _toInt(e.detail.value, 0)
|
|
}
|
|
this.setData({
|
|
editingDraft: draft,
|
|
editorPreview: this._recomputePreview(draft)
|
|
})
|
|
},
|
|
|
|
onEditCycleDays(e) {
|
|
const draft = {
|
|
...this.data.editingDraft,
|
|
cycleDays: _toInt(e.detail.value, 0)
|
|
}
|
|
this.setData({
|
|
editingDraft: draft,
|
|
editorPreview: this._recomputePreview(draft)
|
|
})
|
|
},
|
|
|
|
_validateDraft(draft) {
|
|
if (!draft.name || !draft.name.trim()) return '请填写计划名称'
|
|
if (draft.totalDays < MIN_DAYS || draft.totalDays > MAX_DAYS) {
|
|
return `总天数需在 ${MIN_DAYS}-${MAX_DAYS} 之间`
|
|
}
|
|
if (draft.startTarget < MIN_TARGET || draft.startTarget > MAX_TARGET) {
|
|
return `起始时长需在 ${MIN_TARGET}-${MAX_TARGET} 秒之间`
|
|
}
|
|
if (draft.increment < 0 || draft.increment > MAX_INCREMENT) {
|
|
return `每次增加需在 0-${MAX_INCREMENT} 秒之间`
|
|
}
|
|
if (draft.cycleDays < 1 || draft.cycleDays > MAX_CYCLE) {
|
|
return `每几天增加需在 1-${MAX_CYCLE} 之间`
|
|
}
|
|
return null
|
|
},
|
|
|
|
onSavePlan() {
|
|
const { editingPlanId, editingDraft } = this.data
|
|
if (!editingPlanId || !editingDraft) return
|
|
const err = this._validateDraft(editingDraft)
|
|
if (err) {
|
|
wx.showToast({ title: err, icon: 'none', duration: 1800 })
|
|
return
|
|
}
|
|
const planData = {
|
|
id: editingPlanId,
|
|
name: editingDraft.name.trim(),
|
|
totalDays: editingDraft.totalDays,
|
|
startTarget: editingDraft.startTarget,
|
|
increment: editingDraft.increment,
|
|
cycleDays: editingDraft.cycleDays,
|
|
days: this._recomputePreview(editingDraft),
|
|
description: `${editingDraft.totalDays}天计划 · ${editingDraft.startTarget}秒起步`
|
|
}
|
|
storage.saveCustomPlan(editingPlanId, planData)
|
|
this.setData({
|
|
showPlanEditor: false,
|
|
plans: buildPlansList(storage.getCustomPlans())
|
|
})
|
|
wx.showToast({ title: '已保存', icon: 'success', duration: 1200 })
|
|
},
|
|
|
|
onResetPlan() {
|
|
const { editingPlanId } = this.data
|
|
if (!editingPlanId) return
|
|
wx.showModal({
|
|
title: '恢复默认',
|
|
content: '将恢复此计划的默认设置,自定义内容会丢失。确定继续吗?',
|
|
confirmText: '确定恢复',
|
|
confirmColor: '#E53935',
|
|
success: (res) => {
|
|
if (!res.confirm) return
|
|
storage.resetCustomPlan(editingPlanId)
|
|
// Reopen editor pointed at the preset version
|
|
const preset = planMod.getPlan(editingPlanId)
|
|
const draft = {
|
|
name: preset.name,
|
|
totalDays: preset.totalDays,
|
|
startTarget: preset.startTarget,
|
|
increment: preset.increment,
|
|
cycleDays: preset.cycleDays
|
|
}
|
|
this.setData({
|
|
editingIsCustom: false,
|
|
editingDraft: draft,
|
|
editorPreview: preset.days,
|
|
plans: buildPlansList(storage.getCustomPlans())
|
|
})
|
|
wx.showToast({ title: '已恢复默认', icon: 'success', duration: 1200 })
|
|
}
|
|
})
|
|
}
|
|
})
|