({idx + 1})
diff --git a/src/modules/exams/editor/editor-to-structure.ts b/src/modules/exams/editor/editor-to-structure.ts
index 002f3a3..64610ab 100644
--- a/src/modules/exams/editor/editor-to-structure.ts
+++ b/src/modules/exams/editor/editor-to-structure.ts
@@ -55,17 +55,18 @@ const parseOptions = (
nodes: JSONContent[]
): Array<{ id: string; text: string; isCorrect?: boolean }> => {
const options: Array<{ id: string; text: string; isCorrect?: boolean }> = []
+ const seenIds = new Set()
for (const n of nodes) {
if (n.type === "orderedList" || n.type === "bulletList") {
if (Array.isArray(n.content)) {
n.content.forEach((item, idx) => {
const text = extractText(item).trim()
const match = text.match(/^([A-Z])[.、)]\s*(.+)$/)
- if (match) {
- options.push({ id: match[1]!, text: match[2]! })
- } else {
- options.push({ id: String.fromCharCode(65 + idx), text })
- }
+ const id = match ? match[1]! : String.fromCharCode(65 + idx)
+ // 同一题目内选项 id 去重,避免多个列表合并后出现重复 "A"
+ if (seenIds.has(id)) return
+ seenIds.add(id)
+ options.push({ id, text: match ? match[2]! : text })
})
}
}
diff --git a/src/modules/exams/editor/selection-toolbar.tsx b/src/modules/exams/editor/selection-toolbar.tsx
index f48c566..18cda46 100644
--- a/src/modules/exams/editor/selection-toolbar.tsx
+++ b/src/modules/exams/editor/selection-toolbar.tsx
@@ -94,12 +94,36 @@ export function SelectionToolbar({
// 即使不在块内也允许显示(可标记为题目/分组)
void inBlock
- const view = editor.view
- const start = view.coordsAtPos(from)
- const end = view.coordsAtPos(to)
- const top = Math.min(start.top, end.top) - 44 // 浮在选区上方
- const left = (start.left + end.right) / 2
- setCoords({ top, left })
+ // 使用浏览器原生选区的可见矩形(相对视口),避免长文本选区起点
+ // 在视口外时 coordsAtPos 返回负坐标导致工具栏跑到页面顶部
+ const domSelection = window.getSelection()
+ let top: number
+ let left: number
+
+ if (domSelection && domSelection.rangeCount > 0) {
+ const rect = domSelection.getRangeAt(0).getBoundingClientRect()
+ // 选区不可见(滚动出视口)时 rect 为空矩形,此时不显示工具栏
+ if (rect.width === 0 && rect.height === 0) {
+ setCoords(null)
+ setHasSelection(false)
+ return
+ }
+ top = rect.top - 44 // 浮在选区上方
+ left = rect.left + rect.width / 2
+ } else {
+ // 降级:使用 ProseMirror 的 coordsAtPos
+ const view = editor.view
+ const start = view.coordsAtPos(from)
+ const end = view.coordsAtPos(to)
+ top = Math.min(start.top, end.top) - 44
+ left = (start.left + end.right) / 2
+ }
+
+ // 限制在视口内,避免超出顶部或左右边界
+ const clampedTop = Math.max(8, Math.min(top, window.innerHeight - 60))
+ const clampedLeft = Math.max(120, Math.min(left, window.innerWidth - 120))
+
+ setCoords({ top: clampedTop, left: clampedLeft })
setHasSelection(true)
}
@@ -128,7 +152,12 @@ export function SelectionToolbar({
const chain = editor.chain().focus()
if (hasTextSelection) {
// 选中文本时:包裹为题目块
- chain.wrapInQuestion({ type, score: type === "composite" ? 5 : 2 }).run()
+ // 注意:在 isolating 节点(如复合题)内,wrapIn 可能失败,
+ // 此时降级为插入空题目块
+ const success = chain.wrapInQuestion({ type, score: type === "composite" ? 5 : 2 }).run()
+ if (!success) {
+ chain.insertQuestion({ type, score: type === "composite" ? 5 : 2 }).run()
+ }
} else {
// 未选中文本时:插入空题目块
chain.insertQuestion({ type, score: type === "composite" ? 5 : 2 }).run()