diff --git a/src/modules/exams/actions.ts b/src/modules/exams/actions.ts index b422e22..69e3391 100644 --- a/src/modules/exams/actions.ts +++ b/src/modules/exams/actions.ts @@ -1137,17 +1137,28 @@ const buildTiptapDocFromAiResponse = (data: unknown): unknown => { }) } - // 子题(阅读理解的小题) + // 子题(阅读理解的小题)—— 生成为嵌套的 questionBlock,便于编辑器层级展示与解析 const subQuestions = Array.isArray(contentNode.subQuestions) ? contentNode.subQuestions : [] for (const sub of subQuestions) { if (!isRecord(sub)) continue const subText = typeof sub.text === "string" ? sub.text : "" + const subScore = typeof sub.score === "number" ? sub.score : 0 if (subText) { + // 子题文本按行拆分为段落 + const subLines = subText.split("\n").filter((l) => l.trim().length > 0) + const subInner = + subLines.length > 0 + ? subLines.map((line) => ({ + type: "paragraph", + content: [{ type: "text", text: line }], + })) + : [{ type: "paragraph", content: [{ type: "text", text: " " }] }] inner.push({ - type: "paragraph", - content: [{ type: "text", text: subText }], + type: "questionBlock", + attrs: { questionId: "", type: "text", score: subScore }, + content: subInner, }) } } diff --git a/src/modules/exams/components/exam-rich-form.tsx b/src/modules/exams/components/exam-rich-form.tsx index 7972e29..fccbd29 100644 --- a/src/modules/exams/components/exam-rich-form.tsx +++ b/src/modules/exams/components/exam-rich-form.tsx @@ -329,16 +329,16 @@ function ExamPreview({ structure }: { structure: EditorDoc }) { ): React.ReactNode => { if (node.type === "group") { return ( -
+

{node.title || "大题"}

-
+
{(node.children ?? []).map((child) => renderNode(child, depth + 1))}
@@ -347,39 +347,99 @@ function ExamPreview({ structure }: { structure: EditorDoc }) { const question = structure.questions.find((q) => q.id === node.questionId) if (!question) return null counter.value += 1 + const qNum = counter.value + const isComposite = question.type === "composite" + const typeLabel = question.type === "single_choice" ? "单选" + : question.type === "multiple_choice" ? "多选" + : question.type === "judgment" ? "判断" + : question.type === "composite" ? "复合" + : "简答" + return ( -
- - {counter.value}. - -
-
- {question.content.text || "未命名题目"} - - ({question.score}分) - -
- {question.content.options && question.content.options.length > 0 && ( -
- {question.content.options.map((opt) => ( -
- {opt.id}. - {opt.text} -
- ))} -
- )} +
+ {/* 题目头部:题号 + 题型标签 + 分值 */} +
+ {qNum}. + + {typeLabel} + + + {question.score} 分 +
+ {/* 题干文本 */} + {question.content.text && ( +
+ {question.content.text} +
+ )} + {/* 选项 */} + {question.content.options && question.content.options.length > 0 && ( +
+ {question.content.options.map((opt) => ( +
+ {opt.id}. + {opt.text} +
+ ))} +
+ )} + {/* 复合题子题 */} + {isComposite && question.content.subQuestions && question.content.subQuestions.length > 0 && ( +
+ {question.content.subQuestions.map((sub, idx) => ( +
+
+ + ({idx + 1}) + + {sub.score !== undefined && ( + + {sub.score} 分 + + )} +
+
+ {sub.text} +
+
+ ))} +
+ )} + {/* 图片 */} + {question.content.images && question.content.images.length > 0 && ( +
+ {question.content.images.map((img) => ( + {img.alt + ))} +
+ )}
) } return ( -
- {structure.structure.map((node) => renderNode(node))} +
+ {structure.title && ( +

+ {structure.title} +

+ )} + {structure.structure.length === 0 ? ( +

+ 暂无题目,请在左侧编辑器中添加 +

+ ) : ( + structure.structure.map((node) => renderNode(node)) + )}
) } diff --git a/src/modules/exams/editor/editor-to-structure.ts b/src/modules/exams/editor/editor-to-structure.ts index 0c37dc4..dfb5f43 100644 --- a/src/modules/exams/editor/editor-to-structure.ts +++ b/src/modules/exams/editor/editor-to-structure.ts @@ -84,22 +84,44 @@ const buildQuestion = (qb: JSONContent): EditorQuestion => { : "single_choice") as RichQuestionType const score = typeof attrs.score === "number" ? attrs.score : 0 const inner = qb.content ?? [] + + // 分离出嵌套的 questionBlock(复合题的子题)、选项列表、图片、普通文本 + const subQuestions: Array<{ id: string; text: string; answer?: string; score?: number }> = [] + const nonQuestionBlocks = inner.filter((n) => { + if (n.type === "questionBlock") { + const sub = buildQuestion(n) + const subText = sub.content.text + const subOptions = sub.content.options + ?.map((o) => `${o.id}. ${o.text}`) + .join("\n") + const fullText = [subText, subOptions].filter(Boolean).join("\n") + subQuestions.push({ + id: sub.id, + text: fullText || "", + score: sub.score, + }) + return false + } + return true + }) + const text = extractText({ type: "doc", - content: inner.filter( + content: nonQuestionBlocks.filter( (n) => n.type !== "orderedList" && n.type !== "bulletList" && n.type !== "image" ), }) - const options = parseOptions(inner) - const blanks = collectBlanks(inner) - const images = collectImages(inner) + const options = parseOptions(nonQuestionBlocks) + const blanks = collectBlanks(nonQuestionBlocks) + const images = collectImages(nonQuestionBlocks) const content: RichQuestionContent = { text: text.trim() } if (options.length > 0) content.options = options if (blanks.length > 0) content.blanks = blanks if (images.length > 0) content.images = images + if (subQuestions.length > 0) content.subQuestions = subQuestions return { id, type, score, content } }