diff --git a/cloudfunctions/tts/index.js b/cloudfunctions/tts/index.js index 5e4b7c4..b60ab2d 100644 --- a/cloudfunctions/tts/index.js +++ b/cloudfunctions/tts/index.js @@ -24,6 +24,7 @@ const PROMPTS = { halfway: '已完成一半啦,坚持就是胜利!', last30: '最后30秒,保持呼吸,稳住姿势!', last10: '最后10秒,再加把劲!', + goal: '目标达成,继续挑战!', complete: '太棒了!今天的目标已完成,继续加油!' } diff --git a/config.js b/config.js index 78ec93a..3d327af 100644 --- a/config.js +++ b/config.js @@ -4,7 +4,7 @@ */ module.exports = { /** 应用版本号,外显在设置页「关于」 */ - version: 'v2.5', + version: 'v2.6', /** 最后更新日期,外显在设置页「关于」 */ updatedAt: '2026-07-09', diff --git a/pages/timer/timer.js b/pages/timer/timer.js index 0d82d0d..a5b3f3d 100644 --- a/pages/timer/timer.js +++ b/pages/timer/timer.js @@ -17,6 +17,7 @@ Page({ isPaused: false, isCompleted: false, overtime: 0, + goalReached: false, todayTarget: 0, planDay: 1, isFreeMode: false, @@ -79,7 +80,8 @@ Page({ remaining: target, todayTarget: target, planDay: isFreeMode ? 0 : planDay, - isFreeMode + isFreeMode, + goalReached: false }) if (this._timer) { @@ -104,17 +106,14 @@ Page({ }) this._remind(tick) }, - onComplete: () => { - const elapsed = this._timer.elapsed + onGoalReached: () => { + // Target hit - don't end. Voice prompt only (no vibration, per + // design); keep counting so the user can train to failure. const s = storage.getSettings() - if (s.vibrate) { - try { wx.vibrateLong() } catch (e) {} - } if (s.voiceGuide !== false) { - voice.play('complete') + voice.play('goal') } - // Auto-save + show big completion modal - this._saveAndShowCompletion(elapsed) + this.setData({ goalReached: true }) } }) }, @@ -124,7 +123,7 @@ Page({ * - halfway: once when elapsed reaches half of duration * - 30s: once when remaining hits 30 * - 10s: once when remaining hits 10 - * - complete: fired in onComplete above + * - complete: fired in finishTraining when the user ends the session * * Vibration and voice are independently gated by `vibrate` / `voiceGuide` * settings — user can mute haptics but keep voice (e.g. in office) or vice versa. @@ -196,8 +195,8 @@ Page({ }, /** - * Detect which celebration level to show. Called at onComplete (BEFORE - * saveRecord fires in finishTraining), so stats reflect pre-save state. + * Detect which celebration level to show. Called from _saveAndShowCompletion + * (runs in finishTraining, BEFORE saveRecord), so stats reflect pre-save state. * - first: user's very first training ever * - milestone: streak will hit a memorable number after this save * - record: this duration beats the user's previous max @@ -312,18 +311,24 @@ Page({ setTimeout(() => { wx.navigateBack() }, 1500) return } + // Completion voice (replaces the old auto-onComplete cue). No vibration. + const s = storage.getSettings() + if (s.voiceGuide !== false) { + voice.play('complete') + } this._saveAndShowCompletion(elapsed) }, /** * Persist the just-finished training and pop the big completion modal. - * Called from both onComplete (natural finish) and finishTraining - * (user ended early). Detection of "first / record / milestone" uses - * pre-save stats so the level is decided before the data changes. + * Called from finishTraining (the user manually ends; the timer keeps + * running past the goal until they stop). Detection of "first / record + * / milestone" uses pre-save stats so the level is decided before the + * data changes. */ _saveAndShowCompletion(elapsed) { - // Unified re-entry guard: covers both the natural-complete (onComplete) - // and manual-stop (finishTraining) paths so the save never runs twice. + // Re-entry guard: protects the finishTraining path so the save never + // runs twice even if the user double-taps end. if (this._saving) return this._saving = true @@ -421,6 +426,7 @@ Page({ isPaused: false, remaining: this.data.duration, overtime: 0, + goalReached: false, // Re-init countUp so the big number rolls again on next start _countUpTask: null }) diff --git a/pages/timer/timer.wxml b/pages/timer/timer.wxml index 2e7ef82..f4eb3a9 100644 --- a/pages/timer/timer.wxml +++ b/pages/timer/timer.wxml @@ -46,7 +46,7 @@ 💪 - + +{{overtime}}s @@ -61,6 +61,10 @@ 今日目标 {{duration}} 秒 · 第 {{planDay}} 天 + + + 目标达成,继续挑战! + 保持姿势,核心收紧! @@ -145,7 +149,7 @@ +{{completionOvertime}} - 秒超时 + 秒超长发挥 diff --git a/utils/timer.js b/utils/timer.js index 312d2c6..ce29628 100644 --- a/utils/timer.js +++ b/utils/timer.js @@ -1,7 +1,7 @@ class Timer { constructor(options = {}) { this.onTick = options.onTick || (() => {}) - this.onComplete = options.onComplete || (() => {}) + this.onGoalReached = options.onGoalReached || (() => {}) this._duration = 0 this._remaining = 0 @@ -11,7 +11,7 @@ class Timer { this._intervalId = null this._running = false this._paused = false - this._completed = false + this._goalReached = false this._appShowBound = false // WeChat freezes setInterval while the mini-program is in background. @@ -29,7 +29,7 @@ class Timer { this._duration = duration this._remaining = duration this._elapsed = 0 - this._completed = false + this._goalReached = false this._running = true this._paused = false this._startTime = Date.now() @@ -81,15 +81,18 @@ class Timer { duration: this._duration }) - if (this._remaining <= 0 && !this._completed) { - this._completed = true - this.onComplete({ elapsed: this._elapsed, duration: this._duration }) + // Goal reached: fire once but DON'T stop - the user keeps training past + // the target until they manually end. elapsed keeps climbing, so + // overtime (elapsed - duration) grows and the UI shows the surplus. + if (this._remaining <= 0 && !this._goalReached) { + this._goalReached = true + this.onGoalReached({ elapsed: this._elapsed, duration: this._duration }) } } get isRunning() { return this._running && !this._paused } get isPaused() { return this._paused } - get isCompleted() { return this._completed } + get isGoalReached() { return this._goalReached } get remaining() { return this._remaining } get elapsed() { return this._elapsed } } diff --git a/utils/voice.js b/utils/voice.js index 74fc3d9..307b6d7 100644 --- a/utils/voice.js +++ b/utils/voice.js @@ -22,6 +22,7 @@ const PROMPTS = { halfway: '已完成一半啦,坚持就是胜利!', last30: '最后30秒,保持呼吸,稳住姿势!', last10: '最后10秒,再加把劲!', + goal: '目标达成,继续挑战!', complete: '太棒了!今天的目标已完成,继续加油!' }