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() { // 1. Cancel any pending push BEFORE clearing, to prevent the // scheduled _doPush from reading the freshly-reset defaults // and recreating the cloud doc via add(). cloud.cancelPendingPush() // 2. Clear all local keys (including user_profile which was // previously missed) wx.removeStorageSync('training_records') wx.removeStorageSync('current_streak') wx.removeStorageSync('user_settings') wx.removeStorageSync('app_theme') wx.removeStorageSync('custom_plans') wx.removeStorageSync('user_profile') // 3. Clear cloud doc (awaited — order matters: clear cloud // after cancelling push, before resetting defaults) try { await cloud.clearAll() } catch (e) {} // 4. Reinstate default settings and streak wx.setStorageSync('user_settings', { planId: 'beginner', voiceGuide: true, vibrate: true, planStartDate: storage.getToday() }) wx.setStorageSync('current_streak', { count: 0, lastDate: '' }) // 5. Reset theme and UI (include nickName/avatarUrl to clear // profile state now that user_profile is gone) themeMod.setTheme('orange') this.setData({ currentPlanId: 'beginner', voiceGuide: true, vibrate: true, currentThemeId: 'orange', nickName: '', avatarUrl: '' }) themeMod.applyThemeToPage(this) 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 }) } }) } })