- 实现知识领域和过程的背诵练习功能 - 矩阵布局:知识领域格子横跨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>
117 lines
3.2 KiB
TypeScript
117 lines
3.2 KiB
TypeScript
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)
|
||
}
|
||
}
|