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); }
+}