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