import { useState, useEffect, useCallback, useRef } from 'react' import { motion } from 'framer-motion' import { generateCellSequence, normalizeAnswer, announceToScreenReader, type CellInfo, } from '@/utils/practice' import { PracticeMatrix } from '@/components/practice/PracticeMatrix' import { InputArea } from '@/components/practice/InputArea' import { HintInfo } from '@/components/practice/HintInfo' type CharStatus = 'pending' | 'correct' | 'error' export default function ProcessPracticePage() { // 生成格子顺序 const [cellSequence] = useState(() => generateCellSequence()) // 从 localStorage 加载答题进度 const loadProgress = useCallback(() => { try { const saved = localStorage.getItem('practice-progress') if (saved) { const data = JSON.parse(saved) return { answeredCells: new Map(data.answeredCells || []), currentCellId: data.currentCellId || cellSequence[0]?.id || null, } } } catch (e) { console.error('加载进度失败:', e) } return { answeredCells: new Map(), currentCellId: cellSequence[0]?.id || null, } }, [cellSequence]) // 答题状态 const [answeredCells, setAnsweredCells] = useState>( () => loadProgress().answeredCells ) const [currentCellId, setCurrentCellId] = useState( () => loadProgress().currentCellId ) // 输入状态 const [userInput, setUserInput] = useState([]) const [charStatuses, setCharStatuses] = useState([]) const [isComposing, setIsComposing] = useState(false) const isComposingRef = useRef(false) const [lastErrorTimestamp, setLastErrorTimestamp] = useState( null ) // 长按显示答案 const [showAnswerForCell, setShowAnswerForCell] = useState<{ cellId: string answer: string expiresAt: number } | null>(null) const [inputLocked, setInputLocked] = useState(false) const latestInputRef = useRef([]) // 初始化输入框 useEffect(() => { const currentCell = cellSequence.find((c) => c.id === currentCellId) if (currentCell) { setUserInput(new Array(currentCell.answer.length).fill('')) setCharStatuses(new Array(currentCell.answer.length).fill('pending')) } }, [currentCellId, cellSequence]) // 同步当前输入快照,供输入法确认后复用 useEffect(() => { latestInputRef.current = userInput }, [userInput]) // 保存答题进度到 localStorage useEffect(() => { try { localStorage.setItem( 'practice-progress', JSON.stringify({ answeredCells: Array.from(answeredCells.entries()), currentCellId, }) ) } catch (e) { console.error('保存进度失败:', e) } }, [answeredCells, currentCellId]) // 恢复焦点到第一个空输入框 const restoreFocus = useCallback(() => { setTimeout(() => { const inputs = document.querySelectorAll('.practice-input-area input') const firstEmptyInput = Array.from(inputs).find( (input) => !(input as HTMLInputElement).value ) as HTMLInputElement if (firstEmptyInput) { firstEmptyInput.focus() } else { // 如果所有输入框都有值,聚焦到第一个 (inputs[0] as HTMLInputElement)?.focus() } }, 100) }, []) // 切换到指定格子 const switchToCell = useCallback( (cell: CellInfo) => { setCurrentCellId(cell.id) setUserInput(new Array(cell.answer.length).fill('')) setCharStatuses(new Array(cell.answer.length).fill('pending')) setLastErrorTimestamp(null) // 滚动到可见区域 requestAnimationFrame(() => { const element = document.querySelector( `[data-cell-id="${cell.id}"]` ) as HTMLElement element?.scrollIntoView({ behavior: 'smooth', block: 'center' }) }) // 延迟聚焦到第一个输入框,确保 DOM 已更新 setTimeout(() => { const firstInput = document.querySelector( '.practice-input-area input' ) as HTMLInputElement firstInput?.focus() }, 150) }, [] ) // 移动到下一个格子 const moveToNextCell = useCallback(() => { const currentIndex = cellSequence.findIndex((c) => c.id === currentCellId) if (currentIndex === -1 || currentIndex === cellSequence.length - 1) return const nextCell = cellSequence[currentIndex + 1] switchToCell(nextCell) }, [currentCellId, cellSequence, switchToCell]) // 移动到上一个格子 const moveToPrevCell = useCallback(() => { const currentIndex = cellSequence.findIndex((c) => c.id === currentCellId) if (currentIndex <= 0) return const prevCell = cellSequence[currentIndex - 1] switchToCell(prevCell) }, [currentCellId, cellSequence, switchToCell]) // 统一的输入验证逻辑 const validateInput = useCallback( (input: string[]) => { const currentCell = cellSequence.find((c) => c.id === currentCellId) if (!currentCell || !currentCellId) return const originalAnswer = currentCell.answer const normalizedInput = normalizeAnswer( input.join(''), currentCell.type === 'knowledge-area' ) const normalizedAnswer = currentCell.normalizedAnswer const normalizeChar = (char: string) => normalizeAnswer(char, false) || char const newCharStatuses = input.map((char, i) => { if (!char) return 'pending' as CharStatus const expectedChar = originalAnswer[i] || '' if (!expectedChar) return 'error' as CharStatus return normalizeChar(char) === normalizeChar(expectedChar) ? ('correct' as CharStatus) : ('error' as CharStatus) }) setCharStatuses(newCharStatuses) const isComplete = input.every((c) => c !== '') && input.length === originalAnswer.length const isCorrect = isComplete && normalizedInput === normalizedAnswer if (isCorrect) { setAnsweredCells((prev) => new Map(prev).set(currentCellId, true)) setTimeout(() => { moveToNextCell() }, 300) } else if (isComplete) { setLastErrorTimestamp(Date.now()) } }, [cellSequence, currentCellId, moveToNextCell] ) const handleInputChange = useCallback( (newInput: string[]) => { latestInputRef.current = newInput setUserInput(newInput) if (isComposingRef.current) return validateInput(newInput) }, [validateInput] ) // 输入法状态管理 const handleCompositionStart = useCallback((_index: number) => { isComposingRef.current = true setIsComposing(true) }, []) const handleCompositionEnd = useCallback( (index: number, value: string) => { isComposingRef.current = false setIsComposing(false) // 输入法确认后,需要将当前输入框中的所有字符分散到后续输入框 requestAnimationFrame(() => { const currentInput = latestInputRef.current const composedText = value const newInput = [...currentInput] if (composedText) { const chars = composedText.split('') for ( let i = 0; i < chars.length && index + i < newInput.length; i++ ) { newInput[index + i] = chars[i] } } else { newInput[index] = '' } latestInputRef.current = newInput setUserInput(newInput) validateInput(newInput) }) }, [validateInput] ) // 批量粘贴处理 const handlePaste = useCallback( (e: React.ClipboardEvent) => { e.preventDefault() const pastedText = e.clipboardData.getData('text') const currentCell = cellSequence.find((c) => c.id === currentCellId) if (!currentCell) return // 创建固定长度的数组,不足部分保留为空字符串 const targetLength = currentCell.answer.length const newInput = new Array(targetLength).fill('') const chars = pastedText.split('') for (let i = 0; i < Math.min(chars.length, targetLength); i++) { newInput[i] = chars[i] } handleInputChange(newInput) }, [currentCellId, cellSequence, handleInputChange] ) // 长按显示答案 const handleLongPress = useCallback( (cellId: string) => { const cell = cellSequence.find((c) => c.id === cellId) if (!cell) return // 显示答案,并设置过期时间 setShowAnswerForCell({ cellId, answer: cell.answer, expiresAt: Date.now() + 3000, // 3秒后自动隐藏 }) // 暂时锁定输入区域 setInputLocked(true) // 无障碍通告 announceToScreenReader('答案已显示') }, [cellSequence] ) const handleLongPressEnd = useCallback(() => { // 只有在答案仍显示时才隐藏(避免重复调用) if (showAnswerForCell) { setShowAnswerForCell(null) setInputLocked(false) announceToScreenReader('答案已隐藏') restoreFocus() } }, [showAnswerForCell, restoreFocus]) // 自动过期检查 useEffect(() => { if (!showAnswerForCell) return const remainingTime = showAnswerForCell.expiresAt - Date.now() if (remainingTime <= 0) { setShowAnswerForCell(null) setInputLocked(false) restoreFocus() return } const timer = setTimeout(() => { setShowAnswerForCell(null) setInputLocked(false) announceToScreenReader('答案已自动隐藏') restoreFocus() }, remainingTime) return () => clearTimeout(timer) }, [showAnswerForCell, restoreFocus]) // 点击格子切换(允许回顾已答对的格子) const handleCellClick = useCallback( (cellId: string) => { const cell = cellSequence.find((c) => c.id === cellId) if (cell) { switchToCell(cell) } }, [cellSequence, switchToCell] ) // 计算 tabIndex const getCellTabIndex = useCallback( (cellId: string) => { return cellId === currentCellId ? 0 : -1 }, [currentCellId] ) // 键盘事件监听 useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.key === 'Tab') { e.preventDefault() if (e.shiftKey) { moveToPrevCell() } else { moveToNextCell() } } else if (e.key === 'Escape') { // 清空当前输入 setUserInput(new Array(userInput.length).fill('')) setCharStatuses(new Array(userInput.length).fill('pending')) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [moveToNextCell, moveToPrevCell, userInput.length]) const currentCell = cellSequence.find((c) => c.id === currentCellId) const answeredCount = answeredCells.size const totalCount = cellSequence.length // 清除进度 const handleClearProgress = useCallback(() => { if (confirm('确定要清除所有答题进度吗?')) { setAnsweredCells(new Map()) setCurrentCellId(cellSequence[0]?.id || null) localStorage.removeItem('practice-progress') } }, [cellSequence]) return (
{/* 顶部进度条 */}

过程背诵练习

进度:{answeredCount} / {totalCount}
{/* 主内容区域 */}
{/* 矩阵区域 */}
{/* 底部固定区域(粘附底部,参与文档流) */}
{/* 输入区域 */}
{/* 查看答案按钮 */}
{/* 辅助信息区域 */}
{/* 无障碍通告区域 */}
) }