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.
- package/README.md +51 -58
- package/bin/build.js +95 -9
- package/bin/md2ui.js +102 -13
- package/package.json +24 -10
- 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 +88 -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/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
- package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -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/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
- 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 +111 -12
- 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 +88 -5
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +157 -0
- package/src/components/MobileSearch.vue +97 -0
- package/src/components/TableOfContents.vue +174 -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 +452 -105
- package/src/composables/useDocTree.js +33 -2
- 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 +450 -52
- package/src/composables/useMermaidCache.js +15 -0
- package/src/composables/useScroll.js +354 -27
- package/src/composables/useSearch.js +12 -11
- package/src/config.js +1 -4
- 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 +168 -0
- package/src/style.css +2416 -36
- package/src/utils/imageConverter.js +129 -0
- package/vite-plugin-doc-api.js +369 -0
- package/vite.config.js +7 -2
- 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
- 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>
|