shared: - Add class-filter, error-state, route-error, section-error-boundary, widget-boundary components - Add ui/alert component - Add constants directory - Add breached-password, export-utils, permission-bitmap, rate-limit, resolve-action-error, route-permissions, route-resolver, type-guards lib - Add i18n messages (en, zh-CN) for invitation-codes, parent, questions, rbac tests: - Add integration tests for elective - Add tests/setup/empty-stub scripts: - Add update-md.cjs, tmp_append_en.ps1, tmp_merge_en.ps1 utilities
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
/**
|
||
* 题目内容解析纯函数(共享层)
|
||
*
|
||
* 从 `unknown` 类型的题目内容中安全提取文本、选项、正确答案等。
|
||
* 所有函数均为纯函数,无副作用,便于单测。
|
||
*
|
||
* 这些函数被 homework、error-book 等多个模块共享使用,
|
||
* 避免各模块重复实现题目内容解析逻辑。
|
||
*/
|
||
|
||
export type QuestionOption = {
|
||
id: string
|
||
text: string
|
||
isCorrect?: boolean
|
||
}
|
||
|
||
export const isRecord = (v: unknown): v is Record<string, unknown> =>
|
||
typeof v === "object" && v !== null
|
||
|
||
export const getQuestionText = (content: unknown): string => {
|
||
if (!isRecord(content)) return ""
|
||
return typeof content.text === "string" ? content.text : ""
|
||
}
|
||
|
||
/**
|
||
* 从题目内容中递归提取纯文本预览。
|
||
*
|
||
* 支持以下内容格式:
|
||
* - 字符串:直接返回(可截断)
|
||
* - 数组:递归提取每个节点的 text 和 children,拼接为纯文本
|
||
* - 对象:提取 text 属性
|
||
* - 其他:返回 fallback
|
||
*
|
||
* @param content 题目内容(unknown 类型)
|
||
* @param fallback 无法提取时的回退文本,默认空字符串
|
||
* @param maxLength 最大长度,0 表示不截断,默认 0
|
||
* @returns 提取的纯文本
|
||
*/
|
||
export function extractQuestionPreview(
|
||
content: unknown,
|
||
fallback = "",
|
||
maxLength = 0,
|
||
): string {
|
||
const text = extractTextRecursive(content)
|
||
if (text) {
|
||
return maxLength > 0 ? text.slice(0, maxLength) : text
|
||
}
|
||
return fallback
|
||
}
|
||
|
||
function extractTextRecursive(content: unknown): string {
|
||
if (typeof content === "string") return content
|
||
if (Array.isArray(content)) {
|
||
const parts: string[] = []
|
||
for (const node of content) {
|
||
const text = extractTextRecursive(node)
|
||
if (text) parts.push(text)
|
||
}
|
||
return parts.join("")
|
||
}
|
||
if (isRecord(content)) {
|
||
if (typeof content.text === "string") return content.text
|
||
if (Array.isArray(content.children)) {
|
||
return extractTextRecursive(content.children)
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
export const getOptions = (content: unknown): QuestionOption[] => {
|
||
if (!isRecord(content)) return []
|
||
const raw = content.options
|
||
if (!Array.isArray(raw)) return []
|
||
const out: QuestionOption[] = []
|
||
for (const item of raw) {
|
||
if (!isRecord(item)) continue
|
||
const id = typeof item.id === "string" ? item.id : ""
|
||
const text = typeof item.text === "string" ? item.text : ""
|
||
if (!id || !text) continue
|
||
const isCorrect = item.isCorrect === true
|
||
out.push({ id, text, isCorrect })
|
||
}
|
||
return out
|
||
}
|
||
|
||
export const getChoiceCorrectIds = (content: unknown): string[] => {
|
||
return getOptions(content)
|
||
.filter((o): o is QuestionOption & { isCorrect: true } => o.isCorrect === true)
|
||
.map((o) => o.id)
|
||
}
|
||
|
||
export const getJudgmentCorrectAnswer = (content: unknown): boolean | null => {
|
||
if (!isRecord(content)) return null
|
||
return typeof content.correctAnswer === "boolean" ? content.correctAnswer : null
|
||
}
|
||
|
||
export const getTextCorrectAnswers = (content: unknown): string[] => {
|
||
if (!isRecord(content)) return []
|
||
const raw = content.correctAnswer
|
||
if (typeof raw === "string") return [raw]
|
||
if (Array.isArray(raw)) return raw.filter((x): x is string => typeof x === "string")
|
||
return []
|
||
}
|
||
|
||
export type QuestionType = "single_choice" | "multiple_choice" | "judgment" | "text" | string
|
||
|
||
export const extractAnswerValue = (studentAnswer: unknown): unknown => {
|
||
if (isRecord(studentAnswer) && "answer" in studentAnswer) return studentAnswer.answer
|
||
return studentAnswer
|
||
}
|
||
|
||
/**
|
||
* 从题目内容中提取正确答案,统一序列化为可存储的 JSON 格式。
|
||
*
|
||
* - 选择题:返回正确选项 ID 数组
|
||
* - 判断题:返回布尔值
|
||
* - 填空题:返回正确答案字符串数组
|
||
* - 其他题型或无标准答案:返回 null
|
||
*
|
||
* @param questionType 题目类型
|
||
* @param content 题目内容(unknown 类型)
|
||
* @returns 序列化后的正确答案,或 null
|
||
*/
|
||
export function extractCorrectAnswer(
|
||
questionType: string,
|
||
content: unknown,
|
||
): unknown {
|
||
if (questionType === "single_choice" || questionType === "multiple_choice") {
|
||
const ids = getChoiceCorrectIds(content)
|
||
return ids.length > 0 ? ids : null
|
||
}
|
||
|
||
if (questionType === "judgment") {
|
||
const answer = getJudgmentCorrectAnswer(content)
|
||
return answer // 可能是 true/false/null
|
||
}
|
||
|
||
if (questionType === "text") {
|
||
const answers = getTextCorrectAnswers(content)
|
||
return answers.length > 0 ? answers : null
|
||
}
|
||
|
||
return null
|
||
}
|