diff --git a/pages/timer/timer.js b/pages/timer/timer.js index da5ff6c..fabc52f 100644 --- a/pages/timer/timer.js +++ b/pages/timer/timer.js @@ -20,6 +20,9 @@ Page({ todayTarget: 0, planDay: 1, isFreeMode: false, + celebrationLevel: 'normal', // normal / first / milestone / record + celebrationMessage: '', + confettiPieces: [], icons: iconsMod.build() }, @@ -92,7 +95,16 @@ Page({ this._remind(tick) }, onComplete: () => { - this.setData({ status: 'completed', isCompleted: true }) + 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) {} @@ -162,6 +174,53 @@ Page({ } }, + /** + * Detect which celebration level to show. Called at onComplete (BEFORE + * saveRecord fires in finishTraining), 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 + * - normal: everything else + */ + _detectCelebrationLevel(elapsed) { + const stats = storage.getTotalStats() + const streak = storage.getStreak() + const nextStreak = (streak.lastDate && streak.count >= 1) ? streak.count + 1 : 1 + + if (stats.totalSessions === 0) { + return { level: 'first', message: '🎉 第一次训练!坚持就是胜利' } + } + if ([7, 14, 30, 50, 100, 365].includes(nextStreak)) { + return { level: 'milestone', message: `🔥 连续打卡 ${nextStreak} 天!` } + } + if (elapsed > stats.maxDuration) { + return { level: 'record', message: `🏆 新纪录!${elapsed}秒` } + } + return { level: 'normal', message: '完成!' } + }, + + /** + * Build the confetti piece list. More pieces + more colors for higher + * celebration levels. Pieces are pre-positioned so the layout doesn't + * shift when wx:for renders them. + */ + _buildConfettiPieces(level) { + const counts = { normal: 12, record: 24, milestone: 32, first: 40 } + const count = counts[level] || counts.normal + const palette = ['#FF6B35', '#FFD93D', '#4CAF50', '#2979FF', '#EC407A', '#7E57C2', '#FF8C5A', '#5C9CFF', '#6EC072', '#A08AD6'] + const pieces = [] + for (let i = 0; i < count; i++) { + pieces.push({ + left: Math.round((i / count) * 96 + 2), + color: palette[i % palette.length], + delay: Math.round(Math.random() * 300) / 1000, + rotate: Math.round((Math.random() * 90) - 45), + size: level === 'first' ? 40 : level === 'milestone' ? 36 : 32 + }) + } + return pieces + }, + onUnload() { voice.stop() voice.destroy() diff --git a/pages/timer/timer.wxml b/pages/timer/timer.wxml index ddfe3c3..bf40106 100644 --- a/pages/timer/timer.wxml +++ b/pages/timer/timer.wxml @@ -2,16 +2,17 @@ - - - - - - - - - - + + + + + + {{celebrationMessage}} diff --git a/pages/timer/timer.wxss b/pages/timer/timer.wxss index a288696..a7b1cff 100644 --- a/pages/timer/timer.wxss +++ b/pages/timer/timer.wxss @@ -155,24 +155,56 @@ .confetti-piece { position: absolute; top: -20rpx; - width: 16rpx; - height: 32rpx; border-radius: 4rpx; animation: confettiFall 2.4s ease-out forwards; } -.c1 { left: 10%; background: #FF6B35; animation-delay: 0.00s; transform: rotate(15deg); } -.c2 { left: 22%; background: #FFD93D; animation-delay: 0.10s; transform: rotate(-20deg); } -.c3 { left: 34%; background: #4CAF50; animation-delay: 0.20s; transform: rotate(30deg); } -.c4 { left: 46%; background: #2979FF; animation-delay: 0.05s; transform: rotate(-10deg); } -.c5 { left: 58%; background: #EC407A; animation-delay: 0.15s; transform: rotate(45deg); } -.c6 { left: 70%; background: #7E57C2; animation-delay: 0.25s; transform: rotate(-30deg); } -.c7 { left: 82%; background: #FF8C5A; animation-delay: 0.08s; transform: rotate(20deg); } -.c8 { left: 14%; background: #5C9CFF; animation-delay: 0.18s; transform: rotate(-40deg); } -.c9 { left: 38%; background: #6EC072; animation-delay: 0.28s; transform: rotate(50deg); } -.c10 { left: 90%; background: #FFD93D; animation-delay: 0.12s; transform: rotate(-25deg); } +/* Higher celebration levels: faster / longer fall */ +.confetti-piece--milestone, +.confetti-piece--first, +.confetti-piece--record { + animation-duration: 3.2s; +} + +.confetti-piece--first { + animation-duration: 4s; +} @keyframes confettiFall { 0% { opacity: 1; transform: translateY(0) rotate(0deg); } 100% { opacity: 0.6; transform: translateY(100vh) rotate(720deg); } } + +/* ---- achievement banner ---- */ +.achievement-banner { + position: fixed; + top: 160rpx; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, var(--primary), var(--primary-light)); + color: #FFFFFF; + font-size: 32rpx; + font-weight: 700; + padding: 24rpx 48rpx; + border-radius: 48rpx; + box-shadow: 0 8rpx 24rpx rgba(var(--primary-rgb), 0.4); + z-index: 10000; + animation: achievementIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both, + achievementOut 0.4s ease-in 3.5s both; + white-space: nowrap; +} + +.achievement-text { + display: block; + line-height: 1; +} + +@keyframes achievementIn { + 0% { opacity: 0; transform: translateX(-50%) scale(0.5); } + 100% { opacity: 1; transform: translateX(-50%) scale(1); } +} + +@keyframes achievementOut { + 0% { opacity: 1; transform: translateX(-50%) scale(1); } + 100% { opacity: 0; transform: translateX(-50%) scale(0.8); } +}