fix(exams): fix toolbar tracking and prevent list-to-options for non-choice questions

Two fixes:

1. Selection toolbar tracking: add pointerup listener so the toolbar
   updates its position immediately after the user finishes dragging
   the selection, not just on selectionUpdate events which can lag.

2. List-to-options conversion: parseOptions now only converts
   orderedList/bulletList to A/B/C options for choice question types
   (single_choice, multiple_choice, judgment). For text/composite
   questions, list content is preserved as part of the question stem
   text, preventing unwanted 'A.' prefixes.
This commit is contained in:
SpecialX
2026-06-24 15:13:49 +08:00
parent e9429935b9
commit 90f7d395f2
2 changed files with 29 additions and 11 deletions

View File

@@ -51,9 +51,15 @@ const collectImages = (
return imgs return imgs
} }
/** 选择题类型:只有这些类型才会把列表解析为选项 */
const CHOICE_TYPES = new Set(["single_choice", "multiple_choice", "judgment"])
const parseOptions = ( const parseOptions = (
nodes: JSONContent[] nodes: JSONContent[],
questionType: RichQuestionType
): Array<{ id: string; text: string; isCorrect?: boolean }> => { ): Array<{ id: string; text: string; isCorrect?: boolean }> => {
// 只有选择题才解析选项,填空/简答/复合题的列表保持为普通文本
if (!CHOICE_TYPES.has(questionType)) return []
const options: Array<{ id: string; text: string; isCorrect?: boolean }> = [] const options: Array<{ id: string; text: string; isCorrect?: boolean }> = []
const seenIds = new Set<string>() const seenIds = new Set<string>()
for (const n of nodes) { for (const n of nodes) {
@@ -106,16 +112,19 @@ const buildQuestion = (qb: JSONContent): EditorQuestion => {
return true return true
}) })
const text = extractText({ // 选择题:过滤掉列表(列表作为选项单独处理)
type: "doc", // 非选择题:保留列表内容在文本中(列表是题干的一部分)
content: nonQuestionBlocks.filter( const isChoice = CHOICE_TYPES.has(type)
const textNodes = isChoice
? nonQuestionBlocks.filter(
(n) => (n) =>
n.type !== "orderedList" && n.type !== "orderedList" &&
n.type !== "bulletList" && n.type !== "bulletList" &&
n.type !== "image" n.type !== "image"
), )
}) : nonQuestionBlocks.filter((n) => n.type !== "image")
const options = parseOptions(nonQuestionBlocks) const text = extractText({ type: "doc", content: textNodes })
const options = parseOptions(nonQuestionBlocks, type)
const blanks = collectBlanks(nonQuestionBlocks) const blanks = collectBlanks(nonQuestionBlocks)
const images = collectImages(nonQuestionBlocks) const images = collectImages(nonQuestionBlocks)
const content: RichQuestionContent = { text: text.trim() } const content: RichQuestionContent = { text: text.trim() }

View File

@@ -128,6 +128,14 @@ export function SelectionToolbar({
} }
editor.on("selectionUpdate", updatePosition) editor.on("selectionUpdate", updatePosition)
// 鼠标释放后立即更新位置(选区拖动结束时)
const handlePointerUp = () => {
// 微延迟确保选区已更新
requestAnimationFrame(updatePosition)
}
document.addEventListener("pointerup", handlePointerUp)
editor.on("blur", () => { editor.on("blur", () => {
// 延迟以允许点击工具栏按钮 // 延迟以允许点击工具栏按钮
setTimeout(() => { setTimeout(() => {
@@ -140,6 +148,7 @@ export function SelectionToolbar({
return () => { return () => {
editor.off("selectionUpdate", updatePosition) editor.off("selectionUpdate", updatePosition)
document.removeEventListener("pointerup", handlePointerUp)
} }
}, [editor]) }, [editor])