md2ui 1.0.18 → 1.0.20

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 (80) hide show
  1. package/README.md +51 -58
  2. package/bin/build.js +95 -9
  3. package/bin/md2ui.js +102 -13
  4. package/package.json +24 -10
  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 +88 -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/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
  14. package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -0
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. package/src/App.vue +111 -12
  38. package/src/components/AppSidebar.vue +181 -21
  39. package/src/components/CodeBlockNodeView.vue +72 -0
  40. package/src/components/DocContent.vue +25 -14
  41. package/src/components/EditorContent.vue +257 -0
  42. package/src/components/EditorToolbar.vue +264 -0
  43. package/src/components/ImageZoom.vue +88 -5
  44. package/src/components/MathBlockNodeView.vue +160 -0
  45. package/src/components/MathInlineNodeView.vue +145 -0
  46. package/src/components/MermaidNodeView.vue +157 -0
  47. package/src/components/MobileSearch.vue +97 -0
  48. package/src/components/TableOfContents.vue +174 -32
  49. package/src/components/TopBar.vue +69 -4
  50. package/src/components/TreeNode.vue +232 -39
  51. package/src/components/WelcomePage.vue +2 -2
  52. package/src/composables/useDocHash.js +9 -1
  53. package/src/composables/useDocManager.js +452 -105
  54. package/src/composables/useDocTree.js +33 -2
  55. package/src/composables/useExportWord.js +73 -10
  56. package/src/composables/useFileWatcher.js +45 -0
  57. package/src/composables/useFrontmatter.js +2 -2
  58. package/src/composables/useMarkdown.js +450 -52
  59. package/src/composables/useMermaidCache.js +15 -0
  60. package/src/composables/useScroll.js +354 -27
  61. package/src/composables/useSearch.js +12 -11
  62. package/src/config.js +1 -4
  63. package/src/extensions/CodeBlockCustom.js +113 -0
  64. package/src/extensions/MathBlock.js +107 -0
  65. package/src/extensions/MathInline.js +100 -0
  66. package/src/extensions/MermaidBlock.js +73 -0
  67. package/src/extensions/TableControls.js +670 -0
  68. package/src/services/DocService.js +168 -0
  69. package/src/style.css +2416 -36
  70. package/src/utils/imageConverter.js +129 -0
  71. package/vite-plugin-doc-api.js +369 -0
  72. package/vite.config.js +7 -2
  73. package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
  74. 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
  75. 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
  76. 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
  77. package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
  78. package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
  79. package/src/api/docs.js +0 -106
  80. package/src/components/SearchPanel.vue +0 -90
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <node-view-wrapper class="math-block-wrapper" data-type="mathBlock">
3
+ <!-- 预览模式 -->
4
+ <div v-if="!editing" class="math-block-preview" contenteditable="false" @click.stop="startEdit">
5
+ <div class="math-block-edit-btn" title="编辑公式">
6
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
7
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
8
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
9
+ <path d="m15 5 4 4" />
10
+ </svg>
11
+ </div>
12
+ <div v-if="renderedHtml && !renderError" class="math-block-rendered" v-html="renderedHtml"></div>
13
+ <div v-else-if="renderError" class="math-block-error-tip">
14
+ <span>公式渲染失败</span>
15
+ <button class="math-block-error-edit" @click.stop="startEdit">编辑公式</button>
16
+ </div>
17
+ <div v-else class="math-block-empty" @click.stop="startEdit">
18
+ <span>点击输入数学公式</span>
19
+ </div>
20
+ </div>
21
+ <!-- 编辑模式:上方编辑区 + 下方实时预览 -->
22
+ <div v-else class="math-block-editor" contenteditable="false">
23
+ <div class="math-block-editor-header">
24
+ <span class="math-block-editor-label">LATEX</span>
25
+ <div class="math-block-editor-actions">
26
+ <button class="math-block-editor-delete" @click.stop="deleteBlock" title="删除公式块">
27
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
28
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
29
+ <path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
30
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
31
+ </svg>
32
+ </button>
33
+ <button class="math-block-editor-done" @click.stop="finishEdit" title="完成编辑">
34
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
35
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
36
+ <polyline points="20 6 9 17 4 12" />
37
+ </svg>
38
+ <span>完成</span>
39
+ </button>
40
+ </div>
41
+ </div>
42
+ <textarea
43
+ ref="textareaRef"
44
+ class="math-block-editor-textarea"
45
+ :value="latex"
46
+ @input="onInput"
47
+ @keydown.tab.prevent="onTab"
48
+ @keydown.escape.prevent="finishEdit"
49
+ spellcheck="false"
50
+ placeholder="输入 LaTeX 公式,如 \int_0^\infty e^{-x} dx"
51
+ ></textarea>
52
+ <!-- 实时预览 -->
53
+ <div class="math-block-live-preview">
54
+ <div v-if="liveHtml && !liveError" class="math-block-live-rendered" v-html="liveHtml"></div>
55
+ <div v-else-if="liveError" class="math-block-live-error">{{ liveError }}</div>
56
+ <div v-else class="math-block-live-placeholder">预览区域</div>
57
+ </div>
58
+ </div>
59
+ </node-view-wrapper>
60
+ </template>
61
+
62
+ <script setup>
63
+ import { ref, computed, watch, onMounted, nextTick } from 'vue'
64
+ import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
65
+ import katex from 'katex'
66
+
67
+ const props = defineProps(nodeViewProps)
68
+
69
+ const editing = ref(false)
70
+ const renderedHtml = ref('')
71
+ const renderError = ref(false)
72
+ const liveHtml = ref('')
73
+ const liveError = ref('')
74
+ const textareaRef = ref(null)
75
+
76
+ // 从节点属性获取 LaTeX 代码
77
+ const latex = computed(() => props.node.attrs.latex || '')
78
+
79
+ // 渲染 KaTeX
80
+ function renderKatex(text, displayMode = true) {
81
+ const trimmed = (text || '').trim()
82
+ if (!trimmed) return { html: '', error: '' }
83
+ try {
84
+ const html = katex.renderToString(trimmed, { throwOnError: true, displayMode })
85
+ return { html, error: '' }
86
+ } catch (e) {
87
+ return { html: '', error: e.message || '渲染失败' }
88
+ }
89
+ }
90
+
91
+ function updatePreview() {
92
+ const { html, error } = renderKatex(latex.value)
93
+ renderedHtml.value = html
94
+ renderError.value = !!error
95
+ }
96
+
97
+ function updateLivePreview(text) {
98
+ const { html, error } = renderKatex(text || latex.value)
99
+ liveHtml.value = html
100
+ liveError.value = error
101
+ }
102
+
103
+ function startEdit() {
104
+ editing.value = true
105
+ updateLivePreview()
106
+ nextTick(() => {
107
+ if (textareaRef.value) {
108
+ textareaRef.value.focus()
109
+ autoResize()
110
+ }
111
+ })
112
+ }
113
+
114
+ function finishEdit() {
115
+ editing.value = false
116
+ updatePreview()
117
+ }
118
+
119
+ function deleteBlock() {
120
+ const pos = props.getPos()
121
+ props.editor.chain().focus().deleteRange({ from: pos, to: pos + props.node.nodeSize }).run()
122
+ }
123
+
124
+ function onInput(e) {
125
+ const newLatex = e.target.value
126
+ props.updateAttributes({ latex: newLatex })
127
+ autoResize()
128
+ updateLivePreview(newLatex)
129
+ }
130
+
131
+ function onTab(e) {
132
+ const ta = e.target
133
+ const start = ta.selectionStart
134
+ const end = ta.selectionEnd
135
+ const val = ta.value
136
+ const newVal = val.substring(0, start) + ' ' + val.substring(end)
137
+ ta.value = newVal
138
+ ta.selectionStart = ta.selectionEnd = start + 2
139
+ onInput({ target: ta })
140
+ }
141
+
142
+ function autoResize() {
143
+ nextTick(() => {
144
+ if (textareaRef.value) {
145
+ textareaRef.value.style.height = 'auto'
146
+ textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px'
147
+ }
148
+ })
149
+ }
150
+
151
+ watch(latex, () => {
152
+ if (!editing.value) {
153
+ updatePreview()
154
+ }
155
+ })
156
+
157
+ onMounted(() => {
158
+ updatePreview()
159
+ })
160
+ </script>
@@ -0,0 +1,145 @@
1
+ <template>
2
+ <node-view-wrapper as="span" class="math-inline-wrapper" :class="{ 'math-inline-editing': showEditor }">
3
+ <!-- 渲染态:显示 KaTeX 结果,点击进入编辑 -->
4
+ <span
5
+ v-if="renderedHtml && !renderError"
6
+ class="math-inline-rendered"
7
+ :class="{ 'math-inline-selected': selected }"
8
+ v-html="renderedHtml"
9
+ @click.stop="openEditor"
10
+ title="点击编辑公式"
11
+ ></span>
12
+ <!-- 渲染失败:显示源码 -->
13
+ <span
14
+ v-else
15
+ class="math-inline-error"
16
+ @click.stop="openEditor"
17
+ title="公式有误,点击编辑"
18
+ >{{ latex }}</span>
19
+
20
+ <!-- 编辑浮层 -->
21
+ <div v-if="showEditor" ref="popoverRef" class="math-inline-popover" @click.stop>
22
+ <div class="math-inline-popover-header">
23
+ <span class="math-inline-popover-label">行内公式</span>
24
+ <div class="math-inline-popover-actions">
25
+ <button class="math-inline-popover-btn delete" @click="deleteNode" title="删除公式">
26
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
27
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
28
+ <path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
29
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
30
+ </svg>
31
+ </button>
32
+ <button class="math-inline-popover-btn done" @click="closeEditor" title="完成">
33
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
34
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
35
+ <polyline points="20 6 9 17 4 12" />
36
+ </svg>
37
+ </button>
38
+ </div>
39
+ </div>
40
+ <input
41
+ ref="inputRef"
42
+ class="math-inline-popover-input"
43
+ :value="latex"
44
+ @input="onInput"
45
+ @keydown.enter.prevent="closeEditor"
46
+ @keydown.escape.prevent="closeEditor"
47
+ spellcheck="false"
48
+ placeholder="LaTeX 公式"
49
+ />
50
+ <!-- 实时预览 -->
51
+ <div v-if="liveHtml" class="math-inline-popover-preview" v-html="liveHtml"></div>
52
+ <div v-else-if="liveError" class="math-inline-popover-preview math-inline-popover-error">{{ liveError }}</div>
53
+ </div>
54
+ </node-view-wrapper>
55
+ </template>
56
+
57
+ <script setup>
58
+ import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
59
+ import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
60
+ import katex from 'katex'
61
+
62
+ const props = defineProps(nodeViewProps)
63
+
64
+ const showEditor = ref(false)
65
+ const renderedHtml = ref('')
66
+ const renderError = ref(false)
67
+ const liveHtml = ref('')
68
+ const liveError = ref('')
69
+ const inputRef = ref(null)
70
+ const popoverRef = ref(null)
71
+
72
+ const latex = computed(() => props.node.attrs.latex || '')
73
+
74
+ // 渲染 KaTeX
75
+ function renderKatex(text) {
76
+ if (!text.trim()) return { html: '', error: '' }
77
+ try {
78
+ const html = katex.renderToString(text.trim(), { throwOnError: true, displayMode: false })
79
+ return { html, error: '' }
80
+ } catch (e) {
81
+ return { html: '', error: e.message || '渲染失败' }
82
+ }
83
+ }
84
+
85
+ function updateRender() {
86
+ const { html, error } = renderKatex(latex.value)
87
+ renderedHtml.value = html
88
+ renderError.value = !!error
89
+ }
90
+
91
+ function updateLivePreview(text) {
92
+ const { html, error } = renderKatex(text)
93
+ liveHtml.value = html
94
+ liveError.value = error
95
+ }
96
+
97
+ function openEditor() {
98
+ showEditor.value = true
99
+ updateLivePreview(latex.value)
100
+ nextTick(() => {
101
+ if (inputRef.value) {
102
+ inputRef.value.focus()
103
+ inputRef.value.select()
104
+ }
105
+ })
106
+ }
107
+
108
+ function closeEditor() {
109
+ showEditor.value = false
110
+ updateRender()
111
+ }
112
+
113
+ function deleteNode() {
114
+ const pos = props.getPos()
115
+ props.editor.chain().focus().deleteRange({ from: pos, to: pos + props.node.nodeSize }).run()
116
+ }
117
+
118
+ function onInput(e) {
119
+ const newLatex = e.target.value
120
+ props.updateAttributes({ latex: newLatex })
121
+ updateLivePreview(newLatex)
122
+ }
123
+
124
+ // 点击外部关闭浮层
125
+ function handleClickOutside(e) {
126
+ if (showEditor.value && popoverRef.value && !popoverRef.value.contains(e.target)) {
127
+ closeEditor()
128
+ }
129
+ }
130
+
131
+ onMounted(() => {
132
+ updateRender()
133
+ document.addEventListener('mousedown', handleClickOutside)
134
+ })
135
+
136
+ onUnmounted(() => {
137
+ document.removeEventListener('mousedown', handleClickOutside)
138
+ })
139
+
140
+ watch(latex, () => {
141
+ if (!showEditor.value) {
142
+ updateRender()
143
+ }
144
+ })
145
+ </script>
@@ -0,0 +1,157 @@
1
+ <template>
2
+ <node-view-wrapper class="mermaid-block-wrapper" data-type="mermaidBlock">
3
+ <!-- 预览模式:渲染图表 -->
4
+ <div v-if="!editing" class="mermaid-preview">
5
+ <div class="mermaid-edit-btn" title="编辑 Mermaid 代码" @click.stop="startEdit">
6
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
7
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
8
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
9
+ <path d="m15 5 4 4" />
10
+ </svg>
11
+ </div>
12
+ <div v-if="svgContent" class="mermaid-svg zoomable-image" v-html="svgContent" style="cursor: zoom-in" title="点击放大查看"></div>
13
+ <div v-else-if="renderError" class="mermaid-error-tip">
14
+ <span>图表渲染失败</span>
15
+ <button class="mermaid-error-edit" @click.stop="startEdit">编辑代码</button>
16
+ </div>
17
+ <div v-else class="mermaid-loading">渲染中...</div>
18
+ </div>
19
+ <!-- 编辑模式:代码编辑器 -->
20
+ <div v-else class="mermaid-editor">
21
+ <div class="mermaid-editor-header">
22
+ <span class="mermaid-editor-label">MERMAID</span>
23
+ <button class="mermaid-editor-done" @click.stop="finishEdit" title="完成编辑">
24
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
25
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
26
+ <polyline points="20 6 9 17 4 12" />
27
+ </svg>
28
+ <span>完成</span>
29
+ </button>
30
+ </div>
31
+ <textarea
32
+ ref="textareaRef"
33
+ class="mermaid-editor-textarea"
34
+ :value="code"
35
+ @input="onInput"
36
+ @keydown.tab.prevent="onTab"
37
+ spellcheck="false"
38
+ ></textarea>
39
+ </div>
40
+ </node-view-wrapper>
41
+ </template>
42
+
43
+ <script setup>
44
+ import { ref, computed, watch, onMounted, nextTick } from 'vue'
45
+ import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
46
+ import mermaid from 'mermaid'
47
+ import { getMermaidCache, setMermaidCache } from '../composables/useMermaidCache.js'
48
+
49
+ const props = defineProps(nodeViewProps)
50
+
51
+ const editing = ref(false)
52
+ const svgContent = ref('')
53
+ const renderError = ref(false)
54
+ const textareaRef = ref(null)
55
+
56
+ // 从节点内容获取代码文本
57
+ const code = computed(() => props.node.textContent || '')
58
+
59
+ // 渲染 Mermaid 图表
60
+ async function renderChart() {
61
+ const text = code.value.trim()
62
+ if (!text) {
63
+ svgContent.value = ''
64
+ renderError.value = false
65
+ return
66
+ }
67
+ // 优先使用缓存
68
+ const cached = getMermaidCache(text)
69
+ if (cached) {
70
+ svgContent.value = cached
71
+ renderError.value = false
72
+ return
73
+ }
74
+ try {
75
+ const id = 'mermaid-editor-' + Math.random().toString(36).substr(2, 9)
76
+ const { svg } = await mermaid.render(id, text)
77
+ svgContent.value = svg
78
+ renderError.value = false
79
+ // 写入缓存
80
+ setMermaidCache(text, svg)
81
+ } catch (e) {
82
+ console.error('Mermaid 编辑器渲染失败:', e)
83
+ svgContent.value = ''
84
+ renderError.value = true
85
+ document.querySelectorAll('[id^="mermaid-editor-"][id$="-svg"]').forEach(el => {
86
+ if (el.closest('.mermaid-block-wrapper') === null) el.remove()
87
+ })
88
+ }
89
+ }
90
+
91
+ // 进入编辑模式
92
+ function startEdit() {
93
+ editing.value = true
94
+ nextTick(() => {
95
+ if (textareaRef.value) {
96
+ textareaRef.value.focus()
97
+ autoResize()
98
+ }
99
+ })
100
+ }
101
+
102
+ // 完成编辑,回到预览模式
103
+ function finishEdit() {
104
+ editing.value = false
105
+ renderChart()
106
+ }
107
+
108
+ // 输入处理:更新节点内容
109
+ function onInput(e) {
110
+ const newText = e.target.value
111
+ const { tr } = props.editor.state
112
+ const pos = props.getPos()
113
+ // 替换节点内部全部文本
114
+ tr.replaceWith(
115
+ pos + 1,
116
+ pos + props.node.nodeSize - 1,
117
+ newText ? props.editor.schema.text(newText) : []
118
+ )
119
+ props.editor.view.dispatch(tr)
120
+ autoResize()
121
+ }
122
+
123
+ // Tab 键插入两个空格
124
+ function onTab(e) {
125
+ const ta = e.target
126
+ const start = ta.selectionStart
127
+ const end = ta.selectionEnd
128
+ const val = ta.value
129
+ const newVal = val.substring(0, start) + ' ' + val.substring(end)
130
+ // 先更新 textarea 显示
131
+ ta.value = newVal
132
+ ta.selectionStart = ta.selectionEnd = start + 2
133
+ // 同步到节点
134
+ onInput({ target: ta })
135
+ }
136
+
137
+ // 自动调整 textarea 高度
138
+ function autoResize() {
139
+ nextTick(() => {
140
+ if (textareaRef.value) {
141
+ textareaRef.value.style.height = 'auto'
142
+ textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px'
143
+ }
144
+ })
145
+ }
146
+
147
+ // 监听代码变化(外部更新时重新渲染)
148
+ watch(code, () => {
149
+ if (!editing.value) {
150
+ renderChart()
151
+ }
152
+ })
153
+
154
+ onMounted(() => {
155
+ renderChart()
156
+ })
157
+ </script>
@@ -0,0 +1,97 @@
1
+ <template>
2
+ <div class="mobile-search-page">
3
+ <!-- 搜索输入区 -->
4
+ <div class="mobile-search-header">
5
+ <div class="mobile-search-input-wrapper">
6
+ <Search :size="16" class="mobile-search-icon" />
7
+ <input
8
+ ref="inputRef"
9
+ type="text"
10
+ class="mobile-search-input"
11
+ placeholder="搜索文档..."
12
+ v-model="searchQuery"
13
+ @input="doSearch(searchQuery)"
14
+ @keydown.escape="$emit('close')"
15
+ @keydown.enter="handleEnter"
16
+ @keydown.up.prevent="moveSelection(-1)"
17
+ @keydown.down.prevent="moveSelection(1)"
18
+ />
19
+ <button v-if="searchQuery" class="mobile-search-clear" @click="clearSearch">
20
+ <X :size="14" />
21
+ </button>
22
+ </div>
23
+ <button class="mobile-search-cancel" @click="$emit('close')">取消</button>
24
+ </div>
25
+ <!-- 搜索结果 -->
26
+ <div class="mobile-search-results">
27
+ <div v-if="!searchReady && indexBuilding" class="mobile-search-tip">
28
+ 正在构建索引...
29
+ </div>
30
+ <div v-else-if="searchQuery && searchResults.length === 0" class="mobile-search-empty">
31
+ 没有找到相关文档
32
+ </div>
33
+ <template v-else-if="searchResults.length > 0">
34
+ <div
35
+ v-for="(item, index) in searchResults"
36
+ :key="item.key"
37
+ :class="['mobile-search-item', { active: index === selectedIndex }]"
38
+ @click="selectResult(item)"
39
+ >
40
+ <FileText :size="14" class="mobile-search-item-icon" />
41
+ <span class="mobile-search-item-title">{{ item.title }}</span>
42
+ </div>
43
+ </template>
44
+ <div v-else class="mobile-search-tip">输入关键词搜索文档内容</div>
45
+ </div>
46
+ </div>
47
+ </template>
48
+
49
+ <script setup>
50
+ import { ref, watch, nextTick, onMounted } from 'vue'
51
+ import { Search, X, FileText } from 'lucide-vue-next'
52
+ import { useSearch } from '../composables/useSearch.js'
53
+
54
+ const emit = defineEmits(['close', 'select'])
55
+
56
+ const { searchQuery, searchResults, searchReady, indexBuilding, doSearch, openSearch } = useSearch()
57
+
58
+ const inputRef = ref(null)
59
+ const selectedIndex = ref(0)
60
+
61
+ // 页面挂载时自动聚焦并构建索引
62
+ onMounted(async () => {
63
+ await openSearch()
64
+ await nextTick()
65
+ inputRef.value?.focus()
66
+ })
67
+
68
+ // 搜索结果变化时重置选中
69
+ watch(searchResults, () => {
70
+ selectedIndex.value = 0
71
+ })
72
+
73
+ function clearSearch() {
74
+ searchQuery.value = ''
75
+ searchResults.value = []
76
+ inputRef.value?.focus()
77
+ }
78
+
79
+ function moveSelection(delta) {
80
+ const len = searchResults.value.length
81
+ if (len === 0) return
82
+ selectedIndex.value = (selectedIndex.value + delta + len) % len
83
+ }
84
+
85
+ function handleEnter() {
86
+ if (searchResults.value.length > 0) {
87
+ selectResult(searchResults.value[selectedIndex.value])
88
+ }
89
+ }
90
+
91
+ function selectResult(item) {
92
+ // 清理搜索状态
93
+ searchQuery.value = ''
94
+ searchResults.value = []
95
+ emit('select', item.key)
96
+ }
97
+ </script>