diff --git a/src/components/practice/CelebrationAnimation.tsx b/src/components/practice/CelebrationAnimation.tsx
new file mode 100644
index 0000000..01f2547
--- /dev/null
+++ b/src/components/practice/CelebrationAnimation.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useState } from 'react'
+
+interface CelebrationAnimationProps {
+ onComplete?: () => void
+}
+
+export function CelebrationAnimation({ onComplete }: CelebrationAnimationProps) {
+ const [confetti] = useState(() =>
+ Array.from({ length: 30 }, (_, i) => ({
+ id: i,
+ left: Math.random() * 100,
+ delay: Math.random() * 0.5,
+ duration: 1.5 + Math.random() * 0.5,
+ color: ['#f59e0b', '#3b82f6', '#ef4444', '#10b981', '#8b5cf6'][Math.floor(Math.random() * 5)],
+ }))
+ )
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ onComplete?.()
+ }, 2000)
+ return () => clearTimeout(timer)
+ }, [onComplete])
+
+ return (
+
+ {/* 恭喜文字 */}
+
+
+ {/* 彩带 */}
+ {confetti.map((item) => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/pages/ProcessDetailPage.tsx b/src/pages/ProcessDetailPage.tsx
index b577e94..4d0cfd9 100644
--- a/src/pages/ProcessDetailPage.tsx
+++ b/src/pages/ProcessDetailPage.tsx
@@ -5,6 +5,7 @@ import { getProcessDetail, processes } from '@/data'
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { InputArea } from '@/components/practice/InputArea'
import { normalizeAnswer } from '@/utils/practice'
+import { CelebrationAnimation } from '@/components/practice/CelebrationAnimation'
type IttoSection = 'inputs' | 'tools' | 'outputs'
type CharStatus = 'pending' | 'correct' | 'error'
@@ -102,6 +103,9 @@ export function ProcessDetailPage() {
const longPressTimerRef = useRef(null)
const autoAdvanceTimerRef = useRef(null)
+ // 庆祝动画
+ const [showCelebration, setShowCelebration] = useState(false)
+
const currentPracticeItem = useMemo(
() => practiceItems.find((item) => item.id === currentPracticeId) ?? null,
[practiceItems, currentPracticeId]
@@ -234,7 +238,15 @@ export function ProcessDetailPage() {
setAnsweredItems((prev) => new Set(prev).add(currentPracticeItem.id))
clearAutoAdvanceTimer()
if (currentPracticeIndex === practiceItems.length - 1) {
- autoAdvanceTimerRef.current = window.setTimeout(() => exitPractice(), 500)
+ // 最后一个条目,显示庆祝动画
+ autoAdvanceTimerRef.current = window.setTimeout(() => {
+ setShowCelebration(true)
+ // 庆祝动画结束后退出练习
+ setTimeout(() => {
+ setShowCelebration(false)
+ exitPractice()
+ }, 2000)
+ }, 500)
} else {
autoAdvanceTimerRef.current = window.setTimeout(() => {
setCurrentPracticeId(practiceItems[currentPracticeIndex + 1].id)
@@ -743,6 +755,11 @@ export function ProcessDetailPage() {
) : }
+
+ {/* 庆祝动画 */}
+ {showCelebration && (
+ setShowCelebration(false)} />
+ )}
)
}
diff --git a/src/pages/ProcessPracticePage.tsx b/src/pages/ProcessPracticePage.tsx
index 3b0bcae..86f5b57 100644
--- a/src/pages/ProcessPracticePage.tsx
+++ b/src/pages/ProcessPracticePage.tsx
@@ -9,6 +9,7 @@ import {
import { PracticeMatrix } from '@/components/practice/PracticeMatrix'
import { InputArea } from '@/components/practice/InputArea'
import { HintInfo } from '@/components/practice/HintInfo'
+import { CelebrationAnimation } from '@/components/practice/CelebrationAnimation'
type CharStatus = 'pending' | 'correct' | 'error'
@@ -62,6 +63,9 @@ export default function ProcessPracticePage() {
const [inputLocked, setInputLocked] = useState(false)
const latestInputRef = useRef([])
+ // 庆祝动画
+ const [showCelebration, setShowCelebration] = useState(false)
+
// 初始化输入框
useEffect(() => {
const currentCell = cellSequence.find((c) => c.id === currentCellId)
@@ -184,9 +188,22 @@ export default function ProcessPracticePage() {
if (isCorrect) {
setAnsweredCells((prev) => new Map(prev).set(currentCellId, true))
- setTimeout(() => {
- moveToNextCell()
- }, 300)
+
+ // 检查是否完成所有格子
+ const currentIndex = cellSequence.findIndex((c) => c.id === currentCellId)
+ const isLastCell = currentIndex === cellSequence.length - 1
+
+ if (isLastCell) {
+ // 最后一个格子,显示庆祝动画
+ setTimeout(() => {
+ setShowCelebration(true)
+ }, 300)
+ } else {
+ // 不是最后一个,继续下一个
+ setTimeout(() => {
+ moveToNextCell()
+ }, 300)
+ }
} else if (isComplete) {
setLastErrorTimestamp(Date.now())
}
@@ -495,6 +512,11 @@ export default function ProcessPracticePage() {
aria-live="polite"
aria-atomic="true"
/>
+
+ {/* 庆祝动画 */}
+ {showCelebration && (
+ setShowCelebration(false)} />
+ )}
)
}