|
|
@@ -113,14 +113,14 @@
|
|
|
<div class="toc-list" v-if="tocItems.length > 0">
|
|
|
<div
|
|
|
v-for="(item, index) in tocItems"
|
|
|
- :key="index"
|
|
|
+ :key="item.index || index"
|
|
|
class="toc-item"
|
|
|
:class="['toc-level-' + item.level]"
|
|
|
@click="scrollToHeading(item)"
|
|
|
>
|
|
|
<span class="toc-bullet">{{ getTocBullet(item.level) }}</span>
|
|
|
- <span class="toc-text">{{ item.text }}</span>
|
|
|
- <span class="toc-page" v-if="item.page">{{ item.page }}</span>
|
|
|
+ <span class="toc-text">{{ item.title || item.text }}</span>
|
|
|
+ <span class="toc-page" v-if="item.pageNum || item.page">{{ item.pageNum || item.page }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="toc-empty" v-else>
|
|
|
@@ -495,50 +495,18 @@ const newSourceFile = reactive({
|
|
|
// 文档结构块(用于生成目录等)
|
|
|
const blocks = ref([])
|
|
|
|
|
|
-// 目录数据(从文档结构中提取 toc_item 元素)
|
|
|
-const tocItems = computed(() => {
|
|
|
- const items = []
|
|
|
- if (!blocks.value || blocks.value.length === 0) return items
|
|
|
-
|
|
|
- for (const block of blocks.value) {
|
|
|
- // 遍历块中的元素,查找 toc_item 类型
|
|
|
- if (block.elements && Array.isArray(block.elements)) {
|
|
|
- for (const el of block.elements) {
|
|
|
- if (el.type === 'toc_item') {
|
|
|
- const text = el.content || el.text || ''
|
|
|
- if (text.trim()) {
|
|
|
- // 从文本中推断层级(根据开头数字格式)
|
|
|
- const level = detectTocLevel(text.trim())
|
|
|
- items.push({
|
|
|
- level: level,
|
|
|
- text: text.trim(),
|
|
|
- page: el.style?.tocPageNum || '',
|
|
|
- blockId: block.id
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return items
|
|
|
-})
|
|
|
+// 目录数据(从 API 获取)
|
|
|
+const tocItems = ref([])
|
|
|
|
|
|
-// 检测目录项层级(根据编号格式推断)
|
|
|
-function detectTocLevel(text) {
|
|
|
- // 一级标题:1 xxx, 第一章 xxx
|
|
|
- if (/^(\d+)\s+[^\d]/.test(text) || /^第[一二三四五六七八九十\d]+[章节部篇]/.test(text)) {
|
|
|
- return 1
|
|
|
- }
|
|
|
- // 二级标题:1.1 xxx, 1-1 xxx
|
|
|
- if (/^\d+\.\d+\s+/.test(text) || /^\d+-\d+\s+/.test(text)) {
|
|
|
- return 2
|
|
|
- }
|
|
|
- // 三级标题:1.1.1 xxx
|
|
|
- if (/^\d+\.\d+\.\d+\s+/.test(text)) {
|
|
|
- return 3
|
|
|
+// 加载文档目录
|
|
|
+async function loadToc(documentId) {
|
|
|
+ try {
|
|
|
+ const items = await documentApi.getToc(documentId)
|
|
|
+ tocItems.value = items || []
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('获取文档目录失败:', error)
|
|
|
+ tocItems.value = []
|
|
|
}
|
|
|
- // 默认一级
|
|
|
- return 1
|
|
|
}
|
|
|
|
|
|
// 获取目录项的项目符号
|
|
|
@@ -547,11 +515,62 @@ function getTocBullet(level) {
|
|
|
return bullets[Math.min(level - 1, bullets.length - 1)]
|
|
|
}
|
|
|
|
|
|
-// 滚动到指定标题
|
|
|
+// 滚动到指定章节(通过标题文本匹配)
|
|
|
function scrollToHeading(item) {
|
|
|
- if (!item.blockId) return
|
|
|
+ // 优先通过 anchorId 查找
|
|
|
+ if (item.anchorId) {
|
|
|
+ const anchorEl = document.querySelector(`[data-element-id="${item.anchorId}"]`)
|
|
|
+ if (anchorEl) {
|
|
|
+ anchorEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
+ highlightElement(anchorEl)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- const blockEl = document.querySelector(`[data-block-id="${item.blockId}"]`)
|
|
|
+ // 通过标题文本在文档内容中查找
|
|
|
+ const titleText = item.title || item.text
|
|
|
+ if (!titleText) return
|
|
|
+
|
|
|
+ // 在文档内容区域中查找匹配的文本
|
|
|
+ const editorContent = document.querySelector('.editor-content')
|
|
|
+ if (!editorContent) return
|
|
|
+
|
|
|
+ // 查找包含该标题文本的元素
|
|
|
+ const walker = document.createTreeWalker(
|
|
|
+ editorContent,
|
|
|
+ NodeFilter.SHOW_TEXT,
|
|
|
+ null,
|
|
|
+ false
|
|
|
+ )
|
|
|
+
|
|
|
+ let node
|
|
|
+ while ((node = walker.nextNode())) {
|
|
|
+ if (node.textContent && node.textContent.includes(titleText)) {
|
|
|
+ const parent = node.parentElement
|
|
|
+ if (parent) {
|
|
|
+ parent.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
+ highlightElement(parent)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.warning('未找到对应章节')
|
|
|
+}
|
|
|
+
|
|
|
+// 高亮元素
|
|
|
+function highlightElement(el) {
|
|
|
+ el.classList.add('highlight-block')
|
|
|
+ setTimeout(() => {
|
|
|
+ el.classList.remove('highlight-block')
|
|
|
+ }, 2000)
|
|
|
+}
|
|
|
+
|
|
|
+// 旧的滚动方法(用于 blockId)
|
|
|
+function scrollToBlock(blockId) {
|
|
|
+ if (!blockId) return
|
|
|
+
|
|
|
+ const blockEl = document.querySelector(`[data-block-id="${blockId}"]`)
|
|
|
if (blockEl) {
|
|
|
blockEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
// 高亮一下
|
|
|
@@ -760,10 +779,15 @@ async function fetchTemplateData() {
|
|
|
const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
|
|
|
if (baseDocumentId) {
|
|
|
try {
|
|
|
- const structuredDoc = await documentApi.getStructured(baseDocumentId)
|
|
|
+ // 并行获取结构化内容和目录
|
|
|
+ const [structuredDoc] = await Promise.all([
|
|
|
+ documentApi.getStructured(baseDocumentId),
|
|
|
+ loadToc(baseDocumentId)
|
|
|
+ ])
|
|
|
+
|
|
|
// 将结构化文档的 blocks 和 images 合并渲染
|
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
|
- blocks.value = structuredDoc.blocks // 保存 blocks 用于目录
|
|
|
+ blocks.value = structuredDoc.blocks // 保存 blocks
|
|
|
documentContent.value = renderStructuredDocument(structuredDoc)
|
|
|
// 提取文档中的实体
|
|
|
entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
@@ -780,6 +804,7 @@ async function fetchTemplateData() {
|
|
|
}
|
|
|
} else {
|
|
|
blocks.value = []
|
|
|
+ tocItems.value = []
|
|
|
documentContent.value = emptyPlaceholder
|
|
|
entities.value = []
|
|
|
}
|