Files
NextEdu/src/shared/lib/question-content.ts
SpecialX 0e63c24ed9
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat(shared,tests): add error boundaries, lib utils, i18n messages, and integration tests
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
2026-07-03 10:26:38 +08:00

145 lines
4.4 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.
/**
* 题目内容解析纯函数(共享层)
*
* 从 `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
}