Files
NextEdu/src/modules/questions/utils/parse-content.ts
SpecialX f3c223d914 feat(settings,questions,school,textbooks): add brand config, question components, school dialogs, textbooks hooks
settings:

- Add actions-brand, brand-config, data-access-brand for brand management

- Add admin-file-upload-card, admin-notification-config-card, admin-school-info-card, admin-security-policy-card

- Add ai-provider-delete-dialog, ai-provider-selector, brand-config-card

- Add security-recent-logins-section, security-two-factor-section

- Add config/profile-overview-config, data-access-profile-overview, lib/system-settings-utils

questions:

- Add batch-operations, import-export-buttons, knowledge-point-selector, options-editor

- Add question-bank-results-client, question-cascade-filter, question-content-renderer, utils

school:

- Add grade-delete-dialog, grade-form-dialog, grade-list-toolbar, grade-overview-cards

- Add use-grade-data hook

textbooks:

- Add textbook-form-fields component

- Add use-kp-create, use-kp-delete, use-kp-update hooks
2026-07-03 10:26:00 +08:00

129 lines
3.6 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.
/**
* 题目内容类型定义与类型守卫。
*
* 题目 content 存储为 JSONunknown实际结构为
* - { text: string } — 简答题/判断题
* - { text: string, options: QuestionOption[] } — 选择题
* - { text: string, options: QuestionOption[], answer?: string, explanation?: string } — 完整结构
*
* 所有从 unknown 到具体类型的转换必须使用本文件的类型守卫函数,
* 禁止使用 `as` 断言。
*/
/** 选择题选项 */
export interface QuestionOption {
id: string
text: string
isCorrect: boolean
}
/** 题目内容结构化类型 */
export interface QuestionContent {
text: string
options?: QuestionOption[]
answer?: string
explanation?: string
}
/** 类型守卫:判断值是否为字符串 */
function isString(value: unknown): value is string {
return typeof value === "string"
}
/** 类型守卫:判断值是否为非 null 对象 */
function isNonNullObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
/** 类型守卫:判断值是否为 QuestionOption */
function isQuestionOption(value: unknown): value is QuestionOption {
if (!isNonNullObject(value)) return false
const id = value.id
const text = value.text
const isCorrect = value.isCorrect
return (
(isString(id) || id === undefined) &&
isString(text) &&
typeof isCorrect === "boolean"
)
}
/** 类型守卫:判断值是否为 QuestionOption 数组 */
function isQuestionOptionArray(value: unknown): value is QuestionOption[] {
return Array.isArray(value) && value.every(isQuestionOption)
}
/**
* 从 unknown 安全解析为 QuestionContent。
*
* 处理以下情况:
* - 字符串:转为 { text: string }
* - 对象:提取 text/options/answer/explanation 字段
* - null/undefined返回 { text: "" }
* - 其他JSON.stringify 后转为 { text }
*
* @example
* const content = parseQuestionContent(question.content)
* console.log(content.text) // 题干文本
* console.log(content.options) // 选项列表(选择题)
*/
export function parseQuestionContent(raw: unknown): QuestionContent {
if (isString(raw)) {
return { text: raw }
}
if (isNonNullObject(raw)) {
const text = isString(raw.text) ? raw.text : ""
const optionsRaw = raw.options
const options = isQuestionOptionArray(optionsRaw)
? optionsRaw.map((opt) => ({
id: opt.id ?? opt.text,
text: opt.text,
isCorrect: opt.isCorrect,
}))
: undefined
const answer = isString(raw.answer) ? raw.answer : undefined
const explanation = isString(raw.explanation) ? raw.explanation : undefined
return { text, options, answer, explanation }
}
if (raw == null) {
return { text: "" }
}
try {
return { text: JSON.stringify(raw) }
} catch {
return { text: "" }
}
}
/**
* 从 QuestionContent 提取纯文本预览(截断到指定长度)。
*
* @param raw - 原始 contentunknown
* @param maxLength - 最大长度,默认 80
*/
export function getQuestionPreview(raw: unknown, maxLength = 80): string {
const content = parseQuestionContent(raw)
return content.text.slice(0, maxLength)
}
/**
* 从 QuestionContent 提取选项列表(用于表单回填)。
*
* @returns 选项数组,无选项时返回 undefined
*/
export function getQuestionOptions(raw: unknown): QuestionOption[] | undefined {
const content = parseQuestionContent(raw)
return content.options
}
/**
* 从 QuestionContent 提取纯文本(用于 AI 变体生成等场景)。
*/
export function getQuestionText(raw: unknown): string {
return parseQuestionContent(raw).text
}