md2ui 1.0.16 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -55
- package/bin/build.js +82 -7
- package/bin/md2ui.js +80 -4
- package/package.json +23 -9
- package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
- package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
- package/public/docs/02-Markdown/346/270/262/346/237/223/00-/345/237/272/347/241/200/350/257/255/346/263/225.md +86 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/05-/346/225/260/345/255/246/345/205/254/345/274/217.md +47 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/00-/344/270/211/346/240/217/345/270/203/345/261/200.md +33 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/01-/347/233/256/345/275/225/346/240/221/345/257/274/350/210/252.md +43 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/02-/346/226/207/346/241/243/345/244/247/347/272/262.md +51 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/03-/344/270/212/344/270/213/347/257/207/345/257/274/350/210/252.md +29 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/04-/347/253/231/345/206/205/351/223/276/346/216/245.md +39 -0
- package/public/docs/04-/346/220/234/347/264/242/345/212/237/350/203/275/00-/345/205/250/346/226/207/346/220/234/347/264/242.md +46 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/00-/347/274/226/350/276/221/345/231/250/345/237/272/347/241/200.md +65 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/01-/350/207/252/345/212/250/344/277/235/345/255/230.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/00-/351/230/205/350/257/273/350/277/233/345/272/246.md +43 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/01-/345/233/276/347/211/207/346/224/276/345/244/247.md +40 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/02-/350/277/224/345/233/236/351/241/266/351/203/250.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
- package/public/docs/07-/347/247/273/345/212/250/347/253/257/351/200/202/351/205/215/00-/345/223/215/345/272/224/345/274/217/345/270/203/345/261/200.md +37 -0
- package/public/docs/08-/346/226/207/346/241/243/347/256/241/347/220/206/00-/346/226/260/345/273/272/344/270/216/345/210/240/351/231/244.md +47 -0
- package/public/docs/09-/345/257/274/345/207/272/345/212/237/350/203/275/00-/345/257/274/345/207/272Word.md +77 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/00-CLI/345/267/245/345/205/267.md +52 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/01-SSG/351/235/231/346/200/201/346/236/204/345/273/272.md +44 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +58 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/00-/344/270/200/347/272/247/346/226/207/346/241/243.md +20 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/00-/344/272/214/347/272/247/346/226/207/346/241/243.md +13 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/01-/346/267/261/345/261/202/345/265/214/345/245/227/00-/344/270/211/347/272/247/346/226/207/346/241/243.md +23 -0
- package/src/App.vue +130 -6
- package/src/components/AppSidebar.vue +181 -21
- package/src/components/CodeBlockNodeView.vue +72 -0
- package/src/components/DocContent.vue +25 -14
- package/src/components/EditorContent.vue +257 -0
- package/src/components/EditorToolbar.vue +264 -0
- package/src/components/ImageZoom.vue +199 -2
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +149 -0
- package/src/components/TableBubbleMenu.vue +177 -0
- package/src/components/TableOfContents.vue +138 -32
- package/src/components/TopBar.vue +69 -4
- package/src/components/TreeNode.vue +232 -39
- package/src/components/WelcomePage.vue +2 -2
- package/src/composables/useDocHash.js +9 -1
- package/src/composables/useDocManager.js +325 -68
- package/src/composables/useDocTree.js +56 -1
- package/src/composables/useExportPdf.js +102 -0
- package/src/composables/useExportWord.js +73 -10
- package/src/composables/useFileWatcher.js +45 -0
- package/src/composables/useFrontmatter.js +2 -2
- package/src/composables/useMarkdown.js +529 -42
- package/src/composables/useScroll.js +47 -5
- package/src/config.js +1 -1
- package/src/extensions/CodeBlockCustom.js +113 -0
- package/src/extensions/MathBlock.js +107 -0
- package/src/extensions/MathInline.js +100 -0
- package/src/extensions/MermaidBlock.js +73 -0
- package/src/extensions/TableControls.js +670 -0
- package/src/services/DocService.js +184 -0
- package/src/style.css +2194 -39
- package/vite-plugin-doc-api.js +368 -0
- package/vite.config.js +2 -1
- package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/01-/347/233/256/345/275/225/347/273/223/346/236/204.md +0 -55
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +0 -63
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/03-/351/203/250/347/275/262/346/226/271/346/241/210.md +0 -73
- package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
- package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
- package/src/api/docs.js +0 -106
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<teleport to="body">
|
|
3
|
+
<div
|
|
4
|
+
v-if="show"
|
|
5
|
+
ref="menuRef"
|
|
6
|
+
class="table-bubble-menu"
|
|
7
|
+
:style="menuStyle"
|
|
8
|
+
>
|
|
9
|
+
<div class="table-bubble-group">
|
|
10
|
+
<button class="table-bubble-btn" @click="addColumnBefore" title="在左侧插入列">
|
|
11
|
+
<BetweenVerticalStart :size="14" />
|
|
12
|
+
</button>
|
|
13
|
+
<button class="table-bubble-btn" @click="addColumnAfter" title="在右侧插入列">
|
|
14
|
+
<BetweenVerticalEnd :size="14" />
|
|
15
|
+
</button>
|
|
16
|
+
<button class="table-bubble-btn danger" @click="deleteColumn" title="删除当前列">
|
|
17
|
+
<Columns3 :size="14" />
|
|
18
|
+
<X :size="10" class="table-bubble-badge" />
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="table-bubble-divider"></div>
|
|
22
|
+
<div class="table-bubble-group">
|
|
23
|
+
<button class="table-bubble-btn" @click="addRowBefore" title="在上方插入行">
|
|
24
|
+
<BetweenHorizontalStart :size="14" />
|
|
25
|
+
</button>
|
|
26
|
+
<button class="table-bubble-btn" @click="addRowAfter" title="在下方插入行">
|
|
27
|
+
<BetweenHorizontalEnd :size="14" />
|
|
28
|
+
</button>
|
|
29
|
+
<button class="table-bubble-btn danger" @click="deleteRow" title="删除当前行">
|
|
30
|
+
<RowsIcon :size="14" />
|
|
31
|
+
<X :size="10" class="table-bubble-badge" />
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="table-bubble-divider"></div>
|
|
35
|
+
<div class="table-bubble-group">
|
|
36
|
+
<button class="table-bubble-btn" @click="mergeCells" title="合并单元格">
|
|
37
|
+
<TableCellsMerge :size="14" />
|
|
38
|
+
</button>
|
|
39
|
+
<button class="table-bubble-btn" @click="splitCell" title="拆分单元格">
|
|
40
|
+
<TableCellsSplit :size="14" />
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="table-bubble-divider"></div>
|
|
44
|
+
<button class="table-bubble-btn danger" @click="deleteTable" title="删除表格">
|
|
45
|
+
<Trash2 :size="14" />
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</teleport>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup>
|
|
52
|
+
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
|
53
|
+
import {
|
|
54
|
+
BetweenVerticalStart, BetweenVerticalEnd, Columns3,
|
|
55
|
+
BetweenHorizontalStart, BetweenHorizontalEnd, Rows3 as RowsIcon,
|
|
56
|
+
TableCellsMerge, TableCellsSplit,
|
|
57
|
+
Trash2, X,
|
|
58
|
+
} from 'lucide-vue-next'
|
|
59
|
+
|
|
60
|
+
const props = defineProps({
|
|
61
|
+
editor: { type: Object, required: true }
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const menuRef = ref(null)
|
|
65
|
+
const menuStyle = ref({})
|
|
66
|
+
|
|
67
|
+
const show = computed(() => {
|
|
68
|
+
if (!props.editor) return false
|
|
69
|
+
if (props.editor.isActive('codeBlock') || props.editor.isActive('mermaidBlock')) return false
|
|
70
|
+
return props.editor.isActive('table')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// 计算菜单位置:定位到表格上方
|
|
74
|
+
function updatePosition() {
|
|
75
|
+
if (!show.value || !props.editor) return
|
|
76
|
+
|
|
77
|
+
const { view } = props.editor
|
|
78
|
+
const { state } = view
|
|
79
|
+
|
|
80
|
+
// 找到当前所在的 table 节点
|
|
81
|
+
let tablePos = null
|
|
82
|
+
const { $from } = state.selection
|
|
83
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
84
|
+
if ($from.node(d).type.name === 'table') {
|
|
85
|
+
tablePos = $from.start(d) - 1
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (tablePos === null) return
|
|
90
|
+
|
|
91
|
+
// 获取表格 DOM 元素的位置
|
|
92
|
+
const dom = view.nodeDOM(tablePos)
|
|
93
|
+
if (!dom) return
|
|
94
|
+
|
|
95
|
+
const tableRect = dom.getBoundingClientRect()
|
|
96
|
+
const menuEl = menuRef.value
|
|
97
|
+
if (!menuEl) return
|
|
98
|
+
|
|
99
|
+
const menuWidth = menuEl.offsetWidth
|
|
100
|
+
// 水平居中于表格上方
|
|
101
|
+
let left = tableRect.left + (tableRect.width - menuWidth) / 2
|
|
102
|
+
let top = tableRect.top - menuEl.offsetHeight - 8
|
|
103
|
+
|
|
104
|
+
// 边界修正
|
|
105
|
+
if (left < 8) left = 8
|
|
106
|
+
if (left + menuWidth > window.innerWidth - 8) left = window.innerWidth - menuWidth - 8
|
|
107
|
+
if (top < 8) top = tableRect.bottom + 8 // 表格上方放不下就放下方
|
|
108
|
+
|
|
109
|
+
menuStyle.value = {
|
|
110
|
+
position: 'fixed',
|
|
111
|
+
left: `${left}px`,
|
|
112
|
+
top: `${top}px`,
|
|
113
|
+
zIndex: 100,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 监听 show 变化和编辑器事务来更新位置
|
|
118
|
+
watch(show, async (val) => {
|
|
119
|
+
if (val) {
|
|
120
|
+
await nextTick()
|
|
121
|
+
updatePosition()
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
let updateTimer = null
|
|
126
|
+
function onTransaction() {
|
|
127
|
+
if (!show.value) return
|
|
128
|
+
if (updateTimer) cancelAnimationFrame(updateTimer)
|
|
129
|
+
updateTimer = requestAnimationFrame(updatePosition)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onMounted(() => {
|
|
133
|
+
if (props.editor) {
|
|
134
|
+
props.editor.on('transaction', onTransaction)
|
|
135
|
+
// 滚动时也更新位置
|
|
136
|
+
const scrollEl = document.querySelector('.editor-content')
|
|
137
|
+
if (scrollEl) scrollEl.addEventListener('scroll', updatePosition, { passive: true })
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
onUnmounted(() => {
|
|
142
|
+
if (props.editor) {
|
|
143
|
+
props.editor.off('transaction', onTransaction)
|
|
144
|
+
}
|
|
145
|
+
const scrollEl = document.querySelector('.editor-content')
|
|
146
|
+
if (scrollEl) scrollEl.removeEventListener('scroll', updatePosition)
|
|
147
|
+
if (updateTimer) cancelAnimationFrame(updateTimer)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
function addColumnBefore() {
|
|
151
|
+
props.editor.chain().focus().addColumnBefore().run()
|
|
152
|
+
}
|
|
153
|
+
function addColumnAfter() {
|
|
154
|
+
props.editor.chain().focus().addColumnAfter().run()
|
|
155
|
+
}
|
|
156
|
+
function deleteColumn() {
|
|
157
|
+
props.editor.chain().focus().deleteColumn().run()
|
|
158
|
+
}
|
|
159
|
+
function addRowBefore() {
|
|
160
|
+
props.editor.chain().focus().addRowBefore().run()
|
|
161
|
+
}
|
|
162
|
+
function addRowAfter() {
|
|
163
|
+
props.editor.chain().focus().addRowAfter().run()
|
|
164
|
+
}
|
|
165
|
+
function deleteRow() {
|
|
166
|
+
props.editor.chain().focus().deleteRow().run()
|
|
167
|
+
}
|
|
168
|
+
function mergeCells() {
|
|
169
|
+
props.editor.chain().focus().mergeCells().run()
|
|
170
|
+
}
|
|
171
|
+
function splitCell() {
|
|
172
|
+
props.editor.chain().focus().splitCell().run()
|
|
173
|
+
}
|
|
174
|
+
function deleteTable() {
|
|
175
|
+
props.editor.chain().focus().deleteTable().run()
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
@@ -5,45 +5,151 @@
|
|
|
5
5
|
<List :size="16" />
|
|
6
6
|
<span>目录</span>
|
|
7
7
|
</div>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
8
|
+
<div class="toc-header-actions">
|
|
9
|
+
<button class="toc-action-btn" @click="expandAll" title="全部展开">
|
|
10
|
+
<ChevronsDownUp :size="14" />
|
|
11
|
+
</button>
|
|
12
|
+
<button class="toc-action-btn" @click="collapseAll" title="全部收起">
|
|
13
|
+
<ChevronsUpDown :size="14" />
|
|
14
|
+
</button>
|
|
15
|
+
<button class="toc-toggle" @click="$emit('toggle')" title="收起目录">
|
|
16
|
+
<ChevronRight :size="14" />
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
11
19
|
</div>
|
|
12
|
-
<nav class="toc-nav">
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
<nav ref="tocNavRef" class="toc-nav">
|
|
21
|
+
<template v-for="(item, index) in tocItems" :key="item.id">
|
|
22
|
+
<!-- 顶级标题(h1/h2):可点击折叠 -->
|
|
23
|
+
<a
|
|
24
|
+
v-if="isTopLevel(item)"
|
|
25
|
+
:href="`#${item.id}`"
|
|
26
|
+
:data-toc-id="item.id"
|
|
27
|
+
:class="['toc-item', `toc-level-${item.level}`, { active: activeHeading === item.id }]"
|
|
28
|
+
@click.prevent="$emit('scroll-to', item.id)"
|
|
29
|
+
>
|
|
30
|
+
<span class="toc-item-text">{{ item.text }}</span>
|
|
31
|
+
<button
|
|
32
|
+
v-if="hasChildren(index)"
|
|
33
|
+
class="toc-fold-btn"
|
|
34
|
+
@click.prevent.stop="toggleSection(item.id)"
|
|
35
|
+
:title="collapsedSections.has(item.id) ? '展开子目录' : '收起子目录'"
|
|
36
|
+
>
|
|
37
|
+
<ChevronRight :size="12" :class="{ 'toc-fold-icon-open': !collapsedSections.has(item.id) }" class="toc-fold-icon" />
|
|
38
|
+
</button>
|
|
39
|
+
</a>
|
|
40
|
+
<!-- 子标题(h3/h4):受父级折叠控制 -->
|
|
41
|
+
<a
|
|
42
|
+
v-else-if="!isSectionCollapsed(index)"
|
|
43
|
+
:href="`#${item.id}`"
|
|
44
|
+
:data-toc-id="item.id"
|
|
45
|
+
:class="['toc-item', `toc-level-${item.level}`, { active: activeHeading === item.id }]"
|
|
46
|
+
@click.prevent="$emit('scroll-to', item.id)"
|
|
47
|
+
>
|
|
48
|
+
{{ item.text }}
|
|
49
|
+
</a>
|
|
50
|
+
</template>
|
|
22
51
|
</nav>
|
|
23
52
|
</aside>
|
|
24
53
|
</template>
|
|
25
54
|
|
|
26
55
|
<script setup>
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
type: String,
|
|
36
|
-
default: ''
|
|
37
|
-
},
|
|
38
|
-
collapsed: {
|
|
39
|
-
type: Boolean,
|
|
40
|
-
default: false
|
|
41
|
-
},
|
|
42
|
-
width: {
|
|
43
|
-
type: Number,
|
|
44
|
-
default: 240
|
|
45
|
-
}
|
|
56
|
+
import { ref, watch, nextTick } from 'vue'
|
|
57
|
+
import { List, ChevronRight, ChevronsDownUp, ChevronsUpDown } from 'lucide-vue-next'
|
|
58
|
+
|
|
59
|
+
const props = defineProps({
|
|
60
|
+
tocItems: { type: Array, default: () => [] },
|
|
61
|
+
activeHeading: { type: String, default: '' },
|
|
62
|
+
collapsed: { type: Boolean, default: false },
|
|
63
|
+
width: { type: Number, default: 240 }
|
|
46
64
|
})
|
|
47
65
|
|
|
48
66
|
defineEmits(['scroll-to', 'toggle'])
|
|
67
|
+
|
|
68
|
+
// 目录导航容器 ref
|
|
69
|
+
const tocNavRef = ref(null)
|
|
70
|
+
|
|
71
|
+
// 记录被收起的顶级标题 id
|
|
72
|
+
const collapsedSections = ref(new Set())
|
|
73
|
+
|
|
74
|
+
// 判断是否为顶级标题(h1 或 h2)
|
|
75
|
+
function isTopLevel(item) {
|
|
76
|
+
return item.level <= 2
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 判断某个顶级标题后面是否有子标题
|
|
80
|
+
function hasChildren(index) {
|
|
81
|
+
const next = props.tocItems[index + 1]
|
|
82
|
+
return next && next.level > props.tocItems[index].level
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 找到某个子标题对应的父级顶级标题
|
|
86
|
+
function findParentId(index) {
|
|
87
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
88
|
+
if (isTopLevel(props.tocItems[i])) {
|
|
89
|
+
return props.tocItems[i].id
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 判断某个子标题是否被折叠
|
|
96
|
+
function isSectionCollapsed(index) {
|
|
97
|
+
const parentId = findParentId(index)
|
|
98
|
+
return parentId ? collapsedSections.value.has(parentId) : false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 切换某个顶级标题的折叠状态
|
|
102
|
+
function toggleSection(id) {
|
|
103
|
+
const next = new Set(collapsedSections.value)
|
|
104
|
+
if (next.has(id)) {
|
|
105
|
+
next.delete(id)
|
|
106
|
+
} else {
|
|
107
|
+
next.add(id)
|
|
108
|
+
}
|
|
109
|
+
collapsedSections.value = next
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 全部展开
|
|
113
|
+
function expandAll() {
|
|
114
|
+
collapsedSections.value = new Set()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 全部收起
|
|
118
|
+
function collapseAll() {
|
|
119
|
+
const ids = new Set()
|
|
120
|
+
props.tocItems.forEach((item, index) => {
|
|
121
|
+
if (isTopLevel(item) && hasChildren(index)) {
|
|
122
|
+
ids.add(item.id)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
collapsedSections.value = ids
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 滚动时自动展开当前激活标题所在的折叠分组,并将目录项滚动到可视区域
|
|
129
|
+
watch(() => props.activeHeading, (id) => {
|
|
130
|
+
if (!id) return
|
|
131
|
+
const index = props.tocItems.findIndex(item => item.id === id)
|
|
132
|
+
if (index === -1) return
|
|
133
|
+
// 如果激活的是子标题,找到其父级并展开
|
|
134
|
+
if (!isTopLevel(props.tocItems[index])) {
|
|
135
|
+
const parentId = findParentId(index)
|
|
136
|
+
if (parentId && collapsedSections.value.has(parentId)) {
|
|
137
|
+
const next = new Set(collapsedSections.value)
|
|
138
|
+
next.delete(parentId)
|
|
139
|
+
collapsedSections.value = next
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 等 DOM 更新后,将激活项滚动到目录可视区域
|
|
143
|
+
nextTick(() => {
|
|
144
|
+
const el = tocNavRef.value?.querySelector(`[data-toc-id="${id}"]`)
|
|
145
|
+
if (el) {
|
|
146
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// 文档切换时重置折叠状态
|
|
152
|
+
watch(() => props.tocItems, () => {
|
|
153
|
+
collapsedSections.value = new Set()
|
|
154
|
+
})
|
|
49
155
|
</script>
|
|
@@ -51,7 +51,48 @@
|
|
|
51
51
|
</div>
|
|
52
52
|
<!-- 右侧操作 -->
|
|
53
53
|
<div class="top-bar-actions">
|
|
54
|
-
<
|
|
54
|
+
<button
|
|
55
|
+
v-if="!showWelcome"
|
|
56
|
+
class="export-word-btn"
|
|
57
|
+
:disabled="exporting"
|
|
58
|
+
@click="handleExport"
|
|
59
|
+
title="导出为 Word 文档"
|
|
60
|
+
>
|
|
61
|
+
<FileDown :size="14" />
|
|
62
|
+
<span>{{ exporting ? '导出中...' : '导出 Word' }}</span>
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
v-if="!showWelcome"
|
|
66
|
+
class="export-word-btn"
|
|
67
|
+
:disabled="copying"
|
|
68
|
+
@click="handleCopyMarkdown"
|
|
69
|
+
title="复制 Markdown 源码"
|
|
70
|
+
>
|
|
71
|
+
<ClipboardCopy :size="14" />
|
|
72
|
+
<span>{{ copying ? '已复制' : '复制 MD' }}</span>
|
|
73
|
+
</button>
|
|
74
|
+
<div class="mode-switch">
|
|
75
|
+
<button
|
|
76
|
+
class="mode-switch-btn"
|
|
77
|
+
:class="{ active: !editMode }"
|
|
78
|
+
@click="emit('toggle-edit', false)"
|
|
79
|
+
title="查看模式"
|
|
80
|
+
>
|
|
81
|
+
<Eye :size="14" />
|
|
82
|
+
<span>查看</span>
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
class="mode-switch-btn"
|
|
86
|
+
:class="{ active: editMode, 'mode-edit': editMode }"
|
|
87
|
+
@click="emit('toggle-edit', true)"
|
|
88
|
+
title="编辑模式"
|
|
89
|
+
>
|
|
90
|
+
<Pencil :size="14" />
|
|
91
|
+
<span>编辑</span>
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
<span class="top-bar-divider"></span>
|
|
95
|
+
<a href="https://github.com/xiaoyaodev/md2ui" target="_blank" class="top-bar-github" title="GitHub">
|
|
55
96
|
<Github :size="15" />
|
|
56
97
|
</a>
|
|
57
98
|
</div>
|
|
@@ -60,15 +101,24 @@
|
|
|
60
101
|
|
|
61
102
|
<script setup>
|
|
62
103
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
63
|
-
import { Search, Github, FileText } from 'lucide-vue-next'
|
|
104
|
+
import { Search, Github, FileText, Eye, Pencil, FileDown, ClipboardCopy } from 'lucide-vue-next'
|
|
64
105
|
import Logo from './Logo.vue'
|
|
65
106
|
import { useSearch } from '../composables/useSearch.js'
|
|
107
|
+
import { useExportWord } from '../composables/useExportWord.js'
|
|
66
108
|
|
|
67
|
-
defineProps({
|
|
109
|
+
const props = defineProps({
|
|
110
|
+
editMode: { type: Boolean, default: false },
|
|
111
|
+
showWelcome: { type: Boolean, default: true },
|
|
112
|
+
docTitle: { type: String, default: '文档' },
|
|
113
|
+
rawMarkdown: { type: String, default: '' }
|
|
114
|
+
})
|
|
68
115
|
|
|
69
|
-
const emit = defineEmits(['go-home', 'select-search'])
|
|
116
|
+
const emit = defineEmits(['go-home', 'select-search', 'toggle-edit'])
|
|
70
117
|
|
|
71
118
|
const { searchQuery, searchResults, searchReady, indexBuilding, doSearch, openSearch, closeSearch } = useSearch()
|
|
119
|
+
const { exporting, exportToWord } = useExportWord()
|
|
120
|
+
|
|
121
|
+
const copying = ref(false)
|
|
72
122
|
|
|
73
123
|
const inputRef = ref(null)
|
|
74
124
|
const searchWrapperRef = ref(null)
|
|
@@ -126,6 +176,21 @@ function selectResult(item) {
|
|
|
126
176
|
inputRef.value?.blur()
|
|
127
177
|
}
|
|
128
178
|
|
|
179
|
+
function handleExport() {
|
|
180
|
+
exportToWord(props.docTitle)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function handleCopyMarkdown() {
|
|
184
|
+
if (!props.rawMarkdown) return
|
|
185
|
+
try {
|
|
186
|
+
await navigator.clipboard.writeText(props.rawMarkdown)
|
|
187
|
+
copying.value = true
|
|
188
|
+
setTimeout(() => { copying.value = false }, 1500)
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error('复制失败:', e)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
129
194
|
// 全局快捷键 Ctrl+K
|
|
130
195
|
function handleGlobalKeydown(e) {
|
|
131
196
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|