diff --git a/src/pages/ProcessDetailPage.tsx b/src/pages/ProcessDetailPage.tsx index 356587f..0eab0ec 100644 --- a/src/pages/ProcessDetailPage.tsx +++ b/src/pages/ProcessDetailPage.tsx @@ -22,7 +22,7 @@ export function ProcessDetailPage() { const { id } = useParams() const location = useLocation() const navigate = useNavigate() - const processDetail = id ? getProcessDetail(id) : null + const processDetail = useMemo(() => (id ? getProcessDetail(id) : null), [id]) // ITTO 显示/隐藏状态管理 const [visible, setVisible] = useState>(() => { @@ -124,11 +124,39 @@ export function ProcessDetailPage() { return () => { clearLongPressTimer(); clearAutoAdvanceTimer() } }, [clearLongPressTimer, clearAutoAdvanceTimer]) + useEffect(() => { latestInputRef.current = userInput }, [userInput]) + + // ── 开始 / 退出练习(必须在 validateInput 之前定义,避免闭包捕获 undefined)── + const exitPractice = useCallback(() => { + clearAutoAdvanceTimer() + clearLongPressTimer() + setIsPracticeMode(false) + setAnsweredItems(new Set()) + setCurrentPracticeId(null) + setUserInput([]) + setCharStatuses([]) + setIsComposing(false) + isComposingRef.current = false + setLastErrorTimestamp(null) + setShowAnswer(false) + setInputLocked(false) + }, [clearAutoAdvanceTimer, clearLongPressTimer]) + + const startPractice = useCallback(() => { + if (practiceItems.length === 0) return + clearAutoAdvanceTimer() + setIsPracticeMode(true) + setAnsweredItems(new Set()) + setCurrentPracticeId(practiceItems[0].id) + }, [practiceItems, clearAutoAdvanceTimer]) + // 切换到某个练习项时重置输入状态,并聚焦第一个输入框 + // 依赖 currentPracticeId 和名称长度,而非 currentPracticeItem 对象引用(避免每次渲染重置) + const currentPracticeNameLength = currentPracticeItem?.name.length ?? 0 useEffect(() => { - if (!isPracticeMode || !currentPracticeItem) return - setUserInput(new Array(currentPracticeItem.name.length).fill('')) - setCharStatuses(new Array(currentPracticeItem.name.length).fill('pending')) + if (!isPracticeMode || !currentPracticeId || currentPracticeNameLength === 0) return + setUserInput(new Array(currentPracticeNameLength).fill('')) + setCharStatuses(new Array(currentPracticeNameLength).fill('pending')) setLastErrorTimestamp(null) setShowAnswer(false) setInputLocked(false) @@ -139,9 +167,7 @@ export function ProcessDetailPage() { ) as HTMLInputElement firstInput?.focus() }, 150) - }, [isPracticeMode, currentPracticeItem]) - - useEffect(() => { latestInputRef.current = userInput }, [userInput]) + }, [isPracticeMode, currentPracticeId, currentPracticeNameLength]) // 切换过程时自动退出练习(避免定时器跨过程触发) useEffect(() => { @@ -183,7 +209,7 @@ export function ProcessDetailPage() { setLastErrorTimestamp(Date.now()) } }, - [currentPracticeItem, currentPracticeIndex, practiceItems, clearAutoAdvanceTimer] + [currentPracticeItem, currentPracticeIndex, practiceItems, clearAutoAdvanceTimer, exitPractice] ) const handleInputChange = useCallback( @@ -251,30 +277,6 @@ export function ProcessDetailPage() { setInputLocked(false) }, [clearLongPressTimer]) - // ── 开始 / 退出练习 ─────────────────────────────────────────────────────── - const startPractice = useCallback(() => { - if (practiceItems.length === 0) return - clearAutoAdvanceTimer() - setIsPracticeMode(true) - setAnsweredItems(new Set()) - setCurrentPracticeId(practiceItems[0].id) - }, [practiceItems, clearAutoAdvanceTimer]) - - const exitPractice = useCallback(() => { - clearAutoAdvanceTimer() - clearLongPressTimer() - setIsPracticeMode(false) - setAnsweredItems(new Set()) - setCurrentPracticeId(null) - setUserInput([]) - setCharStatuses([]) - setIsComposing(false) - isComposingRef.current = false - setLastErrorTimestamp(null) - setShowAnswer(false) - setInputLocked(false) - }, [clearAutoAdvanceTimer, clearLongPressTimer]) - if (!processDetail) { return (