fix(练习): 修复中文输入法和字符验证问题

- 移除 input maxLength 限制,支持输入法多字符输入
- 使用 ref 保存最新输入状态,避免闭包导致的状态滞后
- 重构验证逻辑,修复字符对比错误(对比原始答案而非标准化答案)
- 修复输入法确认后验证使用旧数据的问题

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-03-01 15:15:37 +00:00
parent 977187b2d5
commit a38e275642
2 changed files with 34 additions and 27 deletions

View File

@@ -134,7 +134,6 @@ export function InputArea({
!isComposing && isError && 'border-red-500', !isComposing && isError && 'border-red-500',
inputLocked && 'cursor-not-allowed opacity-50' inputLocked && 'cursor-not-allowed opacity-50'
)} )}
maxLength={1}
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { import {
generateCellSequence, generateCellSequence,
@@ -39,6 +39,7 @@ export default function ProcessPracticePage() {
expiresAt: number expiresAt: number
} | null>(null) } | null>(null)
const [inputLocked, setInputLocked] = useState(false) const [inputLocked, setInputLocked] = useState(false)
const latestInputRef = useRef<string[]>([])
// 初始化输入框 // 初始化输入框
useEffect(() => { useEffect(() => {
@@ -49,6 +50,11 @@ export default function ProcessPracticePage() {
} }
}, [currentCellId, cellSequence]) }, [currentCellId, cellSequence])
// 同步当前输入快照,供输入法确认后复用
useEffect(() => {
latestInputRef.current = userInput
}, [userInput])
// 切换到指定格子 // 切换到指定格子
const switchToCell = useCallback( const switchToCell = useCallback(
(cell: CellInfo) => { (cell: CellInfo) => {
@@ -94,55 +100,57 @@ export default function ProcessPracticePage() {
switchToCell(prevCell) switchToCell(prevCell)
}, [currentCellId, cellSequence, switchToCell]) }, [currentCellId, cellSequence, switchToCell])
// 输入验证 // 统一的输入验证逻辑
const handleInputChange = useCallback( const validateInput = useCallback(
(newInput: string[]) => { (input: string[]) => {
setUserInput(newInput)
// 等待输入法确认后再验证
if (isComposing) return
const currentCell = cellSequence.find((c) => c.id === currentCellId) const currentCell = cellSequence.find((c) => c.id === currentCellId)
if (!currentCell || !currentCellId) return if (!currentCell || !currentCellId) return
// 使用原始答案长度渲染横线,使用标准化答案验证
const originalAnswer = currentCell.answer const originalAnswer = currentCell.answer
const normalizedInput = normalizeAnswer( const normalizedInput = normalizeAnswer(
newInput.join(''), input.join(''),
currentCell.type === 'knowledge-area' currentCell.type === 'knowledge-area'
) )
const normalizedAnswer = currentCell.normalizedAnswer const normalizedAnswer = currentCell.normalizedAnswer
const normalizeChar = (char: string) =>
normalizeAnswer(char, false) || char
// 逐字符验证状态(基于原始答案) const newCharStatuses = input.map((char, i) => {
const newCharStatuses = newInput.map((char, i) => {
if (!char) return 'pending' as CharStatus if (!char) return 'pending' as CharStatus
// 对比时使用标准化后的字符 const expectedChar = originalAnswer[i] || ''
const normalizedChar = normalizeAnswer(char, false) if (!expectedChar) return 'error' as CharStatus
const expectedChar = normalizedAnswer[i] return normalizeChar(char) === normalizeChar(expectedChar)
return normalizedChar === expectedChar
? ('correct' as CharStatus) ? ('correct' as CharStatus)
: ('error' as CharStatus) : ('error' as CharStatus)
}) })
setCharStatuses(newCharStatuses) setCharStatuses(newCharStatuses)
// 完整答案验证
const isComplete = const isComplete =
newInput.every((c) => c !== '') && input.every((c) => c !== '') && input.length === originalAnswer.length
newInput.length === originalAnswer.length
const isCorrect = isComplete && normalizedInput === normalizedAnswer const isCorrect = isComplete && normalizedInput === normalizedAnswer
if (isCorrect) { if (isCorrect) {
// 答对:标记格子,延迟跳转(给用户反馈时间)
setAnsweredCells((prev) => new Map(prev).set(currentCellId, true)) setAnsweredCells((prev) => new Map(prev).set(currentCellId, true))
setTimeout(() => { setTimeout(() => {
moveToNextCell() moveToNextCell()
}, 300) }, 300)
} else if (isComplete) { } else if (isComplete) {
// 答错:记录错误时间戳,触发红线动画
setLastErrorTimestamp(Date.now()) setLastErrorTimestamp(Date.now())
} }
}, },
[isComposing, currentCellId, cellSequence, moveToNextCell] [cellSequence, currentCellId, moveToNextCell]
)
const handleInputChange = useCallback(
(newInput: string[]) => {
latestInputRef.current = newInput
setUserInput(newInput)
if (isComposing) return
validateInput(newInput)
},
[isComposing, validateInput]
) )
// 输入法状态管理 // 输入法状态管理
@@ -152,11 +160,11 @@ export default function ProcessPracticePage() {
const handleCompositionEnd = useCallback(() => { const handleCompositionEnd = useCallback(() => {
setIsComposing(false) setIsComposing(false)
// 输入法确认后立即验证(使用当前完整输入) // 输入法确认后根据最新快照重新验证
requestAnimationFrame(() => { requestAnimationFrame(() => {
handleInputChange(userInput) validateInput(latestInputRef.current)
}) })
}, [userInput, handleInputChange]) }, [validateInput])
// 批量粘贴处理 // 批量粘贴处理
const handlePaste = useCallback( const handlePaste = useCallback(