feat(timer): 训练到目标时长不再自动结束,支持超长发挥

- Timer: 到时间触发 onGoalReached(提示)而非自动结束,elapsed/overtime
  继续累加直到用户手动停止
- timer: onGoalReached 播 goal 语音 + goalReached 状态;finishTraining
  补 complete 语音;完成弹窗显示"超长发挥 +X 秒"
- wxml: 超时 badge 训练中也显示;新增 goalReached hint 分支
- voice/tts: 新增 goal 语音 prompt(需部署 tts 云函数)
- config: 版本号 v2.5 -> v2.6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 09:37:32 +08:00
parent 6fffc5aece
commit dd9cd1859c
6 changed files with 42 additions and 27 deletions
+23 -17
View File
@@ -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
})