feat(练习): 新增过程背诵练习模块

- 实现知识领域和过程的背诵练习功能
- 矩阵布局:知识领域格子横跨5列,过程按过程组分列
- 动态输入框:根据答案长度自动调整横线数量
- 实时验证:逐字符验证,错误标红,正确后自动跳转
- 辅助信息:知识领域显示裁剪因素,过程显示主要作用
- 长按显示答案:支持触摸、鼠标和键盘(空格键)
- TAB键切换:按顺序切换格子,自动跳过空单元格
- 支持输入法和批量粘贴
- 完整的无障碍支持(aria-live、tabIndex、scrollIntoView)
- 进度跟踪:顶部显示答题进度条

新增文件:
- src/utils/practice.ts - 工具函数
- src/hooks/useLongPress.ts - 长按 Hook
- src/components/practice/ - 练习组件
- src/pages/ProcessPracticePage.tsx - 练习页面

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

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-03-01 14:28:59 +00:00
parent dd76db193c
commit cc8dd1e751
10 changed files with 1064 additions and 0 deletions

62
src/hooks/useLongPress.ts Normal file
View File

@@ -0,0 +1,62 @@
import { useCallback, useRef } from 'react'
interface UseLongPressOptions {
onLongPress: (cellId: string) => void
onLongPressEnd: () => void
delay?: number
}
/**
* 自定义长按 Hook
* 支持触摸、鼠标和键盘(空格键)
*/
export function useLongPress(
cellId: string,
{ onLongPress, onLongPressEnd, delay = 600 }: UseLongPressOptions
) {
const timerRef = useRef<number>()
const isLongPressRef = useRef(false)
const start = useCallback(() => {
isLongPressRef.current = false
timerRef.current = window.setTimeout(() => {
isLongPressRef.current = true
onLongPress(cellId)
}, delay)
}, [cellId, onLongPress, delay])
const cancel = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
// 只有在长按成功触发后才调用 onLongPressEnd
if (isLongPressRef.current) {
onLongPressEnd()
}
isLongPressRef.current = false
}, [onLongPressEnd])
return {
onPointerDown: start,
onPointerUp: cancel,
onPointerLeave: cancel,
onPointerCancel: cancel,
onContextMenu: (e: React.MouseEvent) => {
e.preventDefault()
cancel()
},
// 键盘支持(空格键长按)
onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === ' ' && !e.repeat) {
e.preventDefault()
start()
}
},
onKeyUp: (e: React.KeyboardEvent) => {
if (e.key === ' ') {
e.preventDefault()
cancel()
}
},
}
}