md2ui 1.0.18 → 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
@@ -1,33 +1,64 @@
1
1
  <template>
2
- <div>
2
+ <div class="tree-node-root">
3
3
  <!-- 文件夹节点 -->
4
4
  <div
5
5
  v-if="item.type === 'folder'"
6
6
  class="nav-item nav-folder"
7
- :class="{ expanded: item.expanded }"
7
+ :class="{ expanded: item.expanded, 'drag-over': isDragOver }"
8
8
  :style="{ paddingLeft: `${20 + item.level * 16}px` }"
9
9
  @click="$emit('toggle', item)"
10
- @mouseenter="showTooltip($event, item.label)"
11
- @mouseleave="hideTooltip"
10
+ @mouseenter="onMouseEnter($event, item.label)"
11
+ @mouseleave="onMouseLeave"
12
+ @dragover.prevent="onFolderDragOver"
13
+ @dragleave="onFolderDragLeave"
14
+ @drop.prevent="onFolderDrop"
12
15
  >
13
16
  <ChevronRight v-if="!item.expanded" class="nav-icon chevron-icon" :size="16" />
14
17
  <ChevronDown v-else class="nav-icon chevron-icon" :size="16" />
15
18
  <Folder v-if="!item.expanded" class="nav-icon folder-icon" :size="16" />
16
19
  <FolderOpen v-else class="nav-icon folder-icon" :size="16" />
17
20
  <span class="nav-label">{{ item.label }}</span>
21
+ <span class="nav-item-actions" @click.stop>
22
+ <button class="nav-item-btn" @click="$emit('show-create', $event, item.key)" title="新建">
23
+ <Plus :size="12" />
24
+ </button>
25
+ <button class="nav-item-btn" @click="startRename($event)" title="重命名">
26
+ <Pencil :size="12" />
27
+ </button>
28
+ <button class="nav-item-btn nav-item-btn-danger" @click="startDelete($event)" title="删除">
29
+ <Trash2 :size="12" />
30
+ </button>
31
+ </span>
18
32
  </div>
19
33
 
20
- <!-- 递归渲染子节点 -->
21
- <template v-if="item.type === 'folder' && item.expanded && item.children">
22
- <TreeNode
23
- v-for="child in item.children"
24
- :key="child.key"
25
- :item="child"
26
- :currentDoc="currentDoc"
27
- @toggle="$emit('toggle', $event)"
28
- @select="$emit('select', $event)"
29
- />
30
- </template>
34
+ <!-- 递归渲染子节点(可拖拽排序) -->
35
+ <draggable
36
+ v-if="item.type === 'folder' && item.expanded && item.children"
37
+ :list="item.children"
38
+ :group="{ name: 'doc-tree', pull: true, put: true }"
39
+ item-key="key"
40
+ :animation="200"
41
+ ghost-class="drag-ghost"
42
+ chosen-class="drag-chosen"
43
+ drag-class="drag-active"
44
+ handle=".nav-item"
45
+ :fallback-on-body="true"
46
+ :swap-threshold="0.65"
47
+ @end="onDragEnd"
48
+ >
49
+ <template #item="{ element }">
50
+ <TreeNode
51
+ :item="element"
52
+ :currentDoc="currentDoc"
53
+ @toggle="$emit('toggle', $event)"
54
+ @select="$emit('select', $event)"
55
+ @show-create="(e, k) => $emit('show-create', e, k)"
56
+ @rename="(payload) => $emit('rename', payload)"
57
+ @delete="(payload) => $emit('delete', payload)"
58
+ @drag-end="$emit('drag-end', $event)"
59
+ />
60
+ </template>
61
+ </draggable>
31
62
 
32
63
  <!-- 文件节点 -->
33
64
  <div
@@ -36,40 +67,200 @@
36
67
  :class="{ active: currentDoc === item.key }"
37
68
  :style="{ paddingLeft: `${20 + item.level * 16}px` }"
38
69
  @click="$emit('select', item.key)"
39
- @mouseenter="showTooltip($event, item.label)"
40
- @mouseleave="hideTooltip"
70
+ @mouseenter="onMouseEnter($event, item.label)"
71
+ @mouseleave="onMouseLeave"
41
72
  >
42
73
  <FileText class="nav-icon file-icon" :size="16" />
43
74
  <span class="nav-label">{{ item.label }}</span>
75
+ <span class="nav-item-actions" @click.stop>
76
+ <button class="nav-item-btn" @click="startRename($event)" title="重命名">
77
+ <Pencil :size="12" />
78
+ </button>
79
+ <button class="nav-item-btn nav-item-btn-danger" @click="startDelete($event)" title="删除">
80
+ <Trash2 :size="12" />
81
+ </button>
82
+ </span>
44
83
  </div>
84
+
85
+ <!-- 重命名气泡 -->
86
+ <Teleport to="body">
87
+ <div v-if="renameVisible" class="popover-overlay" @click="cancelRename">
88
+ <div class="popover-bubble" :style="popoverStyle" @click.stop>
89
+ <input
90
+ ref="renameInputRef"
91
+ v-model="renameName"
92
+ class="popover-input"
93
+ placeholder="请输入新名称"
94
+ @keydown.enter="confirmRename"
95
+ @keydown.escape="cancelRename"
96
+ />
97
+ <div v-if="renameError" class="popover-error">{{ renameError }}</div>
98
+ <div class="popover-actions">
99
+ <button class="popover-btn popover-btn-cancel" @click="cancelRename">取消</button>
100
+ <button class="popover-btn popover-btn-confirm" @click="confirmRename">确定</button>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </Teleport>
105
+
106
+ <!-- 删除确认气泡 -->
107
+ <Teleport to="body">
108
+ <div v-if="deleteVisible" class="popover-overlay" @click="deleteVisible = false">
109
+ <div class="popover-bubble" :style="popoverStyle" @click.stop>
110
+ <div class="popover-message">
111
+ 确定删除「<strong>{{ item.label }}</strong>」?
112
+ <template v-if="item.type === 'folder'">
113
+ <br /><span class="popover-warning">目录下所有内容将一并删除</span>
114
+ </template>
115
+ </div>
116
+ <div class="popover-actions">
117
+ <button class="popover-btn popover-btn-cancel" @click="deleteVisible = false">取消</button>
118
+ <button class="popover-btn popover-btn-danger" @click="confirmDelete">删除</button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </Teleport>
45
123
  </div>
46
124
  </template>
47
125
 
48
126
  <script setup>
49
- import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText } from 'lucide-vue-next'
50
-
51
- defineProps({
52
- item: {
53
- type: Object,
54
- required: true
55
- },
56
- currentDoc: {
57
- type: String,
58
- required: true
59
- }
127
+ import { ref, nextTick } from 'vue'
128
+ import draggable from 'vuedraggable'
129
+ import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText, Plus, Pencil, Trash2 } from 'lucide-vue-next'
130
+
131
+ const props = defineProps({
132
+ item: { type: Object, required: true },
133
+ currentDoc: { type: String, required: true }
60
134
  })
61
135
 
62
- defineEmits(['toggle', 'select'])
136
+ const emit = defineEmits(['toggle', 'select', 'show-create', 'rename', 'delete', 'drag-end'])
137
+
138
+ // ===== 拖拽相关 =====
139
+ const isDragOver = ref(false)
140
+ let dragOverTimer = null
141
+
142
+ function onFolderDragOver() {
143
+ isDragOver.value = true
144
+ clearTimeout(dragOverTimer)
145
+ }
146
+
147
+ function onFolderDragLeave() {
148
+ dragOverTimer = setTimeout(() => { isDragOver.value = false }, 100)
149
+ }
150
+
151
+ function onFolderDrop() {
152
+ isDragOver.value = false
153
+ }
154
+
155
+ function onDragEnd(evt) {
156
+ emit('drag-end', evt)
157
+ }
158
+
159
+ // ===== 气泡定位 =====
160
+ const popoverStyle = ref({})
161
+ const bubbleRef = ref(null)
162
+
163
+ // 记录触发按钮的位置
164
+ let triggerRect = null
165
+
166
+ function calcPopoverPos(event) {
167
+ const btn = event.currentTarget
168
+ triggerRect = btn.getBoundingClientRect()
169
+ // 先设置一个不可见的初始位置,等渲染后再修正
170
+ popoverStyle.value = {
171
+ position: 'fixed',
172
+ left: `${Math.max(8, triggerRect.left - 80)}px`,
173
+ top: '0px',
174
+ zIndex: 9999,
175
+ visibility: 'hidden'
176
+ }
177
+ }
178
+
179
+ function adjustPopoverPos() {
180
+ nextTick(() => {
181
+ const bubble = document.querySelector('.popover-bubble')
182
+ if (!bubble || !triggerRect) return
183
+ const bRect = bubble.getBoundingClientRect()
184
+ let left = Math.max(8, triggerRect.left - 80)
185
+ // 默认向上弹出
186
+ let top = triggerRect.top - bRect.height - 6
187
+ // 上方空间不够,改为向下
188
+ if (top < 10) {
189
+ top = triggerRect.bottom + 6
190
+ }
191
+ // 向下弹出后底部仍然溢出,贴底显示
192
+ if (top + bRect.height > window.innerHeight - 10) {
193
+ top = window.innerHeight - bRect.height - 10
194
+ }
195
+ // 右侧溢出修正
196
+ if (left + bRect.width > window.innerWidth - 10) {
197
+ left = window.innerWidth - bRect.width - 10
198
+ }
199
+ popoverStyle.value = {
200
+ position: 'fixed',
201
+ left: `${left}px`,
202
+ top: `${top}px`,
203
+ zIndex: 9999,
204
+ visibility: 'visible'
205
+ }
206
+ })
207
+ }
208
+
209
+ // ===== 重命名 =====
210
+ const renameVisible = ref(false)
211
+ const renameInputRef = ref(null)
212
+ const renameName = ref('')
213
+ const renameError = ref('')
63
214
 
215
+ function startRename(event) {
216
+ deleteVisible.value = false
217
+ renameName.value = props.item.label
218
+ renameError.value = ''
219
+ calcPopoverPos(event)
220
+ renameVisible.value = true
221
+ nextTick(() => {
222
+ adjustPopoverPos()
223
+ renameInputRef.value?.focus()
224
+ renameInputRef.value?.select()
225
+ })
226
+ }
227
+
228
+ function confirmRename() {
229
+ const newName = renameName.value.trim()
230
+ if (!newName) { renameError.value = '名称不能为空'; return }
231
+ if (/[\\/:*?"<>|]/.test(newName)) { renameError.value = '名称包含非法字符'; return }
232
+ if (newName === props.item.label) { renameVisible.value = false; return }
233
+ renameError.value = ''
234
+ emit('rename', { item: props.item, newName })
235
+ renameVisible.value = false
236
+ }
237
+
238
+ function cancelRename() {
239
+ renameVisible.value = false
240
+ }
241
+
242
+ // ===== 删除 =====
243
+ const deleteVisible = ref(false)
244
+
245
+ function startDelete(event) {
246
+ renameVisible.value = false
247
+ calcPopoverPos(event)
248
+ deleteVisible.value = true
249
+ adjustPopoverPos()
250
+ }
251
+
252
+ function confirmDelete() {
253
+ emit('delete', props.item)
254
+ deleteVisible.value = false
255
+ }
256
+
257
+ // ===== Tooltip =====
64
258
  let tooltipEl = null
65
259
 
66
- function showTooltip(event, text) {
260
+ function onMouseEnter(event, text) {
67
261
  hideTooltip()
68
-
69
262
  const target = event.currentTarget
70
263
  const label = target.querySelector('.nav-label')
71
-
72
- // 只有文字被截断时才显示 tooltip
73
264
  if (label && label.scrollWidth <= label.clientWidth) return
74
265
 
75
266
  tooltipEl = document.createElement('div')
@@ -89,18 +280,22 @@ function showTooltip(event, text) {
89
280
  cursor: text;
90
281
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
91
282
  `
92
-
93
283
  document.body.appendChild(tooltipEl)
94
-
95
284
  const rect = target.getBoundingClientRect()
96
285
  tooltipEl.style.left = `${rect.right + 8}px`
97
286
  tooltipEl.style.top = `${rect.top}px`
98
-
99
- // 防止超出屏幕右侧
100
287
  const tooltipRect = tooltipEl.getBoundingClientRect()
101
288
  if (tooltipRect.right > window.innerWidth - 10) {
102
289
  tooltipEl.style.left = `${rect.left - tooltipRect.width - 8}px`
103
290
  }
291
+ // 底部溢出修正
292
+ if (tooltipRect.bottom > window.innerHeight - 10) {
293
+ tooltipEl.style.top = `${window.innerHeight - tooltipRect.height - 10}px`
294
+ }
295
+ }
296
+
297
+ function onMouseLeave() {
298
+ hideTooltip()
104
299
  }
105
300
 
106
301
  function hideTooltip() {
@@ -110,5 +305,3 @@ function hideTooltip() {
110
305
  }
111
306
  }
112
307
  </script>
113
-
114
-
@@ -23,11 +23,11 @@
23
23
  </button>
24
24
  </div>
25
25
  <!-- GitHub -->
26
- <a class="welcome-github" href="https://github.com/devsneed/md2ui" target="_blank">
26
+ <a class="welcome-github" href="https://github.com/xiaoyaodev/md2ui" target="_blank">
27
27
  <GitHubIcon :size="16" />
28
28
  <span>GitHub</span>
29
29
  <span class="welcome-github-sep"></span>
30
- <span class="welcome-github-repo">devsneed/md2ui</span>
30
+ <span class="welcome-github-repo">xiaoyaodev/md2ui</span>
31
31
  <ExternalLink :size="12" class="welcome-github-arrow" />
32
32
  </a>
33
33
  </div>
@@ -33,9 +33,17 @@ function toBase62(h1, h2) {
33
33
  return result.padStart(8, '0').slice(0, 8)
34
34
  }
35
35
 
36
+ // 去掉路径中每一段的序号前缀(如 "01-快速开始" → "快速开始")
37
+ // 保证重编号不影响外链地址
38
+ export function stripOrderPrefix(key) {
39
+ return key.split('/').map(seg => seg.replace(/^\d+-/, '')).join('/')
40
+ }
41
+
36
42
  // 根据文档 key 生成 8 位 base62 短 hash(同步、确定性、零依赖)
43
+ // 哈希计算前会去掉序号前缀,使得重编号不影响外链
37
44
  export function docHash(key) {
38
- const [h1, h2] = fnv1a64(key)
45
+ const stripped = stripOrderPrefix(key)
46
+ const [h1, h2] = fnv1a64(stripped)
39
47
  return toBase62(h1, h2)
40
48
  }
41
49