Files
ittoview/src/utils/practice.ts
ittoview cc8dd1e751 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>
2026-03-01 14:28:59 +00:00

117 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
knowledgeAreas,
processGroups,
processes,
} from '@/data'
import type { Process } from '@/types/itto'
export interface CellInfo {
id: string // 格子唯一标识
type: 'knowledge-area' | 'process'
knowledgeAreaId: string
processGroupId?: string // 知识领域格子无此字段
processId?: string // 过程格子才有
answer: string // 正确答案(原始)
normalizedAnswer: string // 标准化答案(用于比对)
order: number // 全局顺序
}
/**
* 答案标准化函数
* @param str 原始字符串
* @param isKnowledgeArea 是否为知识领域(只对知识领域去除"项目"前缀)
*/
export function normalizeAnswer(
str: string,
isKnowledgeArea: boolean = false
): string {
let normalized = str
.replace(/\s+/g, '') // 去除空格
.toLowerCase() // 转小写(如有英文)
.replace(/[,。、;:""''()【】]/g, '') // 去除中文标点
// 只对知识领域去除"项目"前缀
if (isKnowledgeArea) {
normalized = normalized.replace(/^项目/, '')
}
return normalized
}
/**
* 生成格子顺序列表
* 顺序KA01 → P1.1 → P1.2 → ... → P1.7 → KA02 → P2.1 → ...
*/
export function generateCellSequence(): CellInfo[] {
const sequence: CellInfo[] = []
let order = 0
// 确保数据源按 order 字段排序
const sortedKAs = [...knowledgeAreas].sort((a, b) => a.order - b.order)
const sortedPGs = [...processGroups].sort((a, b) => a.order - b.order)
sortedKAs.forEach((ka) => {
// 1. 添加知识领域格子
sequence.push({
id: `ka-${ka.id}`,
type: 'knowledge-area',
knowledgeAreaId: ka.id,
answer: ka.name.replace('项目', ''), // "项目整合管理" -> "整合管理"
normalizedAnswer: normalizeAnswer(ka.name, true), // 知识领域去除"项目"
order: order++,
})
// 2. 添加该知识领域下的所有过程格子(按过程组顺序)
sortedPGs.forEach((pg) => {
const kaProcesses = processes
.filter((p) => p.knowledgeAreaId === ka.id && p.processGroupId === pg.id)
.sort((a, b) => a.order - b.order)
kaProcesses.forEach((p) => {
sequence.push({
id: `process-${p.id}`,
type: 'process',
knowledgeAreaId: ka.id,
processGroupId: pg.id,
processId: p.id,
answer: p.name,
normalizedAnswer: normalizeAnswer(p.name, false), // 过程不去除"项目"
order: order++,
})
})
})
})
return sequence
}
/**
* 获取指定知识领域和过程组下的所有过程
*/
export function getProcessesByKaAndPg(
knowledgeAreaId: string,
processGroupId: string
): Process[] {
return processes
.filter(
(p) =>
p.knowledgeAreaId === knowledgeAreaId &&
p.processGroupId === processGroupId
)
.sort((a, b) => a.order - b.order)
}
/**
* 无障碍通告函数
*/
export function announceToScreenReader(message: string): void {
const liveRegion = document.getElementById('aria-live-region')
if (liveRegion) {
liveRegion.textContent = message
// 清空,以便下次通告
setTimeout(() => {
liveRegion.textContent = ''
}, 1000)
}
}