fix(过程详情): 修复练习模式输入框完全无法输入的问题

根本原因:
1. getProcessDetail(id) 每次渲染产生新对象 → practiceItems/currentPracticeItem
   引用不稳定 → reset useEffect 每次渲染触发 → userInput 被立即清空
2. exitPractice 定义在 validateInput 之后 → 闭包捕获到 undefined

修复:
- 用 useMemo([id]) 稳定 processDetail 引用
- 将 exitPractice 移至 validateInput 之前定义
- reset useEffect 依赖改为 [isPracticeMode, currentPracticeId, currentPracticeNameLength]
  使用原始类型值避免对象引用不稳定触发误重置

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

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-03-02 07:54:21 +00:00
parent 83a3791f25
commit 19f0ee7bc4

View File

@@ -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<Record<IttoSection, boolean>>(() => {
@@ -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 (
<div className="flex flex-col items-center justify-center py-20">