Files
wx_pbzc/utils/timer.js
T
lc dd9cd1859c 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>
2026-07-09 09:37:32 +08:00

101 lines
2.9 KiB
JavaScript

class Timer {
constructor(options = {}) {
this.onTick = options.onTick || (() => {})
this.onGoalReached = options.onGoalReached || (() => {})
this._duration = 0
this._remaining = 0
this._elapsed = 0
this._startTime = 0
this._pausedTime = 0
this._intervalId = null
this._running = false
this._paused = false
this._goalReached = false
this._appShowBound = false
// WeChat freezes setInterval while the mini-program is in background.
// When the user returns, force an immediate tick so the UI jumps to
// the actual elapsed time (instead of staying on the last pre-background
// frame) and any missed halfway / 30s / 10s reminders can still fire.
this._onAppShow = () => {
if (this._running && !this._paused) {
this._tick()
}
}
}
start(duration) {
this._duration = duration
this._remaining = duration
this._elapsed = 0
this._goalReached = false
this._running = true
this._paused = false
this._startTime = Date.now()
this._tick()
this._intervalId = setInterval(() => { this._tick() }, 1000)
// Register foreground listener (idempotent — we keep a flag so we don't
// double-register if start() is called multiple times in one session)
if (!this._appShowBound && typeof wx.onAppShow === 'function') {
try {
wx.onAppShow(this._onAppShow)
this._appShowBound = true
} catch (e) {}
}
}
pause() {
if (!this._running || this._paused) return
this._paused = true
this._pausedTime = Date.now()
clearInterval(this._intervalId)
this._intervalId = null
}
resume() {
if (!this._running || !this._paused) return
this._startTime += Date.now() - this._pausedTime
this._paused = false
this._tick()
this._intervalId = setInterval(() => { this._tick() }, 1000)
}
stop() {
this._running = false
this._paused = false
clearInterval(this._intervalId)
this._intervalId = null
return this._elapsed
}
_tick() {
if (!this._running || this._paused) return
this._elapsed = Math.floor((Date.now() - this._startTime) / 1000)
this._remaining = Math.max(0, this._duration - this._elapsed)
this.onTick({
remaining: this._remaining,
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 isGoalReached() { return this._goalReached }
get remaining() { return this._remaining }
get elapsed() { return this._elapsed }
}
module.exports = Timer