feat: 训练完成全屏弹窗 (大数字+countUp+超时+3按钮)
替换原来不显眼的 wx.showToast:
- 全屏遮罩 + 大圆角卡片,scale-in 弹性入场
- 大数字 180rpx countUp 从 0 滚到实际秒数(1.2s)
- 超时标签 +Xs 秒超时(OvertimePop 弹性出现,延迟 1.1s 等数字到末尾)
- 连续打卡显示 🔥
- 3 按钮:再来一次(留在当前页重置) / 查看记录(跳记录页) / 回到首页
- 装饰 emoji 圆形 1.6s 循环脉冲
- onComplete 触发时自动保存,不需要再手动点结束
This commit is contained in:
+100
-17
@@ -23,6 +23,14 @@ Page({
|
||||
celebrationLevel: 'normal', // normal / first / milestone / record
|
||||
celebrationMessage: '',
|
||||
confettiPieces: [],
|
||||
// Completion modal (full-screen summary after training ends)
|
||||
showCompletion: false,
|
||||
completionElapsed: 0,
|
||||
completionTarget: 0,
|
||||
completionOvertime: 0,
|
||||
completionStreak: 0,
|
||||
completionLevel: 'normal',
|
||||
completionMessage: '',
|
||||
icons: iconsMod.build()
|
||||
},
|
||||
|
||||
@@ -96,24 +104,15 @@ Page({
|
||||
},
|
||||
onComplete: () => {
|
||||
const elapsed = this._timer.elapsed
|
||||
const { level, message } = this._detectCelebrationLevel(elapsed)
|
||||
const confettiPieces = this._buildConfettiPieces(level)
|
||||
this.setData({
|
||||
status: 'completed',
|
||||
isCompleted: true,
|
||||
celebrationLevel: level,
|
||||
celebrationMessage: message,
|
||||
confettiPieces
|
||||
})
|
||||
const s = storage.getSettings()
|
||||
if (s.vibrate) {
|
||||
try { wx.vibrateLong() } catch (e) {}
|
||||
}
|
||||
// 4th voice prompt: completion
|
||||
if (s.voiceGuide !== false) {
|
||||
voice.play('complete')
|
||||
}
|
||||
wx.showToast({ title: '目标完成! 太棒了!', icon: 'success', duration: 2000 })
|
||||
// Auto-save + show big completion modal
|
||||
this._saveAndShowCompletion(elapsed)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -271,8 +270,9 @@ Page({
|
||||
},
|
||||
|
||||
finishTraining() {
|
||||
// Guard against double-tap: stop button is still visible during the
|
||||
// 1.5s navigateBack delay, so a second tap would create a duplicate.
|
||||
// Guard against double-tap: completion modal shows immediately, so
|
||||
// a second tap on the underlying 结束 button is harmless — but we
|
||||
// still need to guard the save itself.
|
||||
if (this._saving) return
|
||||
this._saving = true
|
||||
|
||||
@@ -283,6 +283,20 @@ Page({
|
||||
setTimeout(() => { wx.navigateBack() }, 1500)
|
||||
return
|
||||
}
|
||||
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.
|
||||
*/
|
||||
_saveAndShowCompletion(elapsed) {
|
||||
const { level, message } = this._detectCelebrationLevel(elapsed)
|
||||
const confettiPieces = this._buildConfettiPieces(level)
|
||||
|
||||
// Save record + streak
|
||||
const today = storage.getToday()
|
||||
storage.saveRecord({
|
||||
date: today,
|
||||
@@ -292,11 +306,80 @@ Page({
|
||||
})
|
||||
storage.updateStreak(today)
|
||||
|
||||
// Flag the just-completed training so the index page can briefly
|
||||
// highlight the "今日已完成" badge when the user lands back there.
|
||||
// Flag for the index page's "训练完成" highlight
|
||||
try { wx.setStorageSync('_last_train_at', Date.now()) } catch (e) {}
|
||||
|
||||
wx.showToast({ title: `已记录 ${elapsed}秒`, icon: 'none', duration: 1500 })
|
||||
setTimeout(() => { wx.navigateBack() }, 1500)
|
||||
const finalStreak = storage.getStreak().count
|
||||
|
||||
this.setData({
|
||||
// Background effects keep playing behind the modal
|
||||
status: 'completed',
|
||||
isCompleted: true,
|
||||
celebrationLevel: level,
|
||||
celebrationMessage: message,
|
||||
confettiPieces,
|
||||
// Modal state
|
||||
showCompletion: true,
|
||||
completionElapsed: 0,
|
||||
completionTarget: this.data.duration,
|
||||
completionOvertime: Math.max(0, elapsed - this.data.duration),
|
||||
completionStreak: finalStreak,
|
||||
completionLevel: level,
|
||||
completionMessage: message
|
||||
})
|
||||
|
||||
// Animate the big number from 0 to elapsed
|
||||
if (this._completionCountUp) this._completionCountUp.cancel()
|
||||
this._completionCountUp = countUp({
|
||||
from: 0,
|
||||
to: elapsed,
|
||||
duration: 1200,
|
||||
onUpdate: (v) => this.setData({ completionElapsed: v }),
|
||||
onComplete: () => { this._completionCountUp = null }
|
||||
})
|
||||
},
|
||||
|
||||
onCloseCompletion() {
|
||||
if (this._completionCountUp) {
|
||||
this._completionCountUp.cancel()
|
||||
this._completionCountUp = null
|
||||
}
|
||||
this.setData({ showCompletion: false })
|
||||
setTimeout(() => { wx.navigateBack() }, 280)
|
||||
},
|
||||
|
||||
onViewRecords() {
|
||||
if (this._completionCountUp) {
|
||||
this._completionCountUp.cancel()
|
||||
this._completionCountUp = null
|
||||
}
|
||||
this.setData({ showCompletion: false })
|
||||
setTimeout(() => {
|
||||
wx.navigateBack({
|
||||
success: () => {
|
||||
setTimeout(() => wx.switchTab({ url: '/pages/records/records' }), 100)
|
||||
}
|
||||
})
|
||||
}, 280)
|
||||
},
|
||||
|
||||
onTrainAgain() {
|
||||
// Reset the timer to idle so the user can start a new session right
|
||||
// here (no need to navigate back and re-tap 开始).
|
||||
if (this._completionCountUp) {
|
||||
this._completionCountUp.cancel()
|
||||
this._completionCountUp = null
|
||||
}
|
||||
this.setData({
|
||||
showCompletion: false,
|
||||
isCompleted: false,
|
||||
status: 'idle',
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
remaining: this.data.duration,
|
||||
overtime: 0,
|
||||
// Re-init countUp so the big number rolls again on next start
|
||||
_countUpTask: null
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user