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.
Files changed (74) hide show
  1. package/README.md +3 -55
  2. package/bin/build.js +82 -7
  3. package/bin/md2ui.js +80 -4
  4. package/package.json +23 -9
  5. package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
  6. package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
  7. 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
  8. package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
  9. package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
  10. package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
  11. package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. package/src/App.vue +130 -6
  35. package/src/components/AppSidebar.vue +181 -21
  36. package/src/components/CodeBlockNodeView.vue +72 -0
  37. package/src/components/DocContent.vue +25 -14
  38. package/src/components/EditorContent.vue +257 -0
  39. package/src/components/EditorToolbar.vue +264 -0
  40. package/src/components/ImageZoom.vue +199 -2
  41. package/src/components/MathBlockNodeView.vue +160 -0
  42. package/src/components/MathInlineNodeView.vue +145 -0
  43. package/src/components/MermaidNodeView.vue +149 -0
  44. package/src/components/TableBubbleMenu.vue +177 -0
  45. package/src/components/TableOfContents.vue +138 -32
  46. package/src/components/TopBar.vue +69 -4
  47. package/src/components/TreeNode.vue +232 -39
  48. package/src/components/WelcomePage.vue +2 -2
  49. package/src/composables/useDocHash.js +9 -1
  50. package/src/composables/useDocManager.js +325 -68
  51. package/src/composables/useDocTree.js +56 -1
  52. package/src/composables/useExportPdf.js +102 -0
  53. package/src/composables/useExportWord.js +73 -10
  54. package/src/composables/useFileWatcher.js +45 -0
  55. package/src/composables/useFrontmatter.js +2 -2
  56. package/src/composables/useMarkdown.js +529 -42
  57. package/src/composables/useScroll.js +47 -5
  58. package/src/config.js +1 -1
  59. package/src/extensions/CodeBlockCustom.js +113 -0
  60. package/src/extensions/MathBlock.js +107 -0
  61. package/src/extensions/MathInline.js +100 -0
  62. package/src/extensions/MermaidBlock.js +73 -0
  63. package/src/extensions/TableControls.js +670 -0
  64. package/src/services/DocService.js +184 -0
  65. package/src/style.css +2194 -39
  66. package/vite-plugin-doc-api.js +368 -0
  67. package/vite.config.js +2 -1
  68. package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
  69. 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
  70. 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
  71. 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
  72. package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
  73. package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
  74. 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
- <button class="toc-toggle" @click="$emit('toggle')" title="收起目录">
9
- <ChevronRight :size="14" />
10
- </button>
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
- <a
14
- v-for="item in tocItems"
15
- :key="item.id"
16
- :href="`#${item.id}`"
17
- :class="['toc-item', `toc-level-${item.level}`, { active: activeHeading === item.id }]"
18
- @click.prevent="$emit('scroll-to', item.id)"
19
- >
20
- {{ item.text }}
21
- </a>
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 { List, ChevronRight } from 'lucide-vue-next'
28
-
29
- defineProps({
30
- tocItems: {
31
- type: Array,
32
- default: () => []
33
- },
34
- activeHeading: {
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
- <a href="https://github.com/devsneed/md2ui" target="_blank" class="top-bar-btn" title="GitHub">
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') {