md2ui 1.0.0
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 +230 -0
- package/bin/md2ui.js +168 -0
- package/index.html +14 -0
- package/package.json +46 -0
- package/public/README.md +106 -0
- package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +51 -0
- package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +57 -0
- package/public/docs/02-Mermaid/345/233/276/350/241/250.md +102 -0
- 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 +55 -0
- 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 +63 -0
- 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 +73 -0
- package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +80 -0
- package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +92 -0
- package/public/logo.svg +6 -0
- package/src/App.vue +171 -0
- package/src/api/docs.js +103 -0
- package/src/components/ImageZoom.vue +285 -0
- package/src/components/Logo.vue +44 -0
- package/src/components/TableOfContents.vue +44 -0
- package/src/components/TreeNode.vue +61 -0
- package/src/composables/useMarkdown.js +128 -0
- package/src/composables/useResize.js +50 -0
- package/src/composables/useScroll.js +77 -0
- package/src/main.js +5 -0
- package/src/style.css +784 -0
- package/vite.config.js +9 -0
package/src/api/docs.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// 构建目录树结构(支持多层嵌套)
|
|
2
|
+
function buildTree(files) {
|
|
3
|
+
const root = { children: [] }
|
|
4
|
+
|
|
5
|
+
files.forEach(file => {
|
|
6
|
+
const parts = file.relativePath.split('/')
|
|
7
|
+
let currentLevel = root
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
10
|
+
const folderName = parts[i]
|
|
11
|
+
const folderPath = parts.slice(0, i + 1).join('/')
|
|
12
|
+
|
|
13
|
+
let folder = currentLevel.children.find(item => item.key === folderPath)
|
|
14
|
+
|
|
15
|
+
if (!folder) {
|
|
16
|
+
const match = folderName.match(/^(\d+)-(.+)$/)
|
|
17
|
+
const folderLabel = match ? match[2] : folderName
|
|
18
|
+
const cleanFolderLabel = folderLabel.replace(/[。.]$/, '')
|
|
19
|
+
folder = {
|
|
20
|
+
type: 'folder',
|
|
21
|
+
key: folderPath,
|
|
22
|
+
label: cleanFolderLabel,
|
|
23
|
+
order: match ? parseInt(match[1]) : 999,
|
|
24
|
+
level: i,
|
|
25
|
+
children: [],
|
|
26
|
+
expanded: false
|
|
27
|
+
}
|
|
28
|
+
currentLevel.children.push(folder)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
currentLevel = folder
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
currentLevel.children.push({
|
|
35
|
+
type: 'file',
|
|
36
|
+
...file
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
function sortChildren(node) {
|
|
41
|
+
if (node.children) {
|
|
42
|
+
node.children.sort((a, b) => a.order - b.order)
|
|
43
|
+
node.children.forEach(child => sortChildren(child))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sortChildren(root)
|
|
48
|
+
return root.children
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 获取文档列表
|
|
52
|
+
export async function getDocsList() {
|
|
53
|
+
// 尝试 CLI 模式:检查是否有用户文档 API
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch('/@user-docs-list')
|
|
56
|
+
if (response.ok) {
|
|
57
|
+
return await response.json()
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// 忽略错误,继续使用开发模式
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 开发模式:扫描 public/docs 目录
|
|
64
|
+
try {
|
|
65
|
+
const modules = import.meta.glob('/public/docs/**/*.md')
|
|
66
|
+
const files = []
|
|
67
|
+
|
|
68
|
+
for (const path in modules) {
|
|
69
|
+
const relativePath = path.replace('/public/docs/', '').replace('.md', '')
|
|
70
|
+
const parts = relativePath.split('/')
|
|
71
|
+
const fileName = parts[parts.length - 1]
|
|
72
|
+
|
|
73
|
+
const match = fileName.match(/^(\d+)-(.+)$/)
|
|
74
|
+
if (match) {
|
|
75
|
+
const [, order, name] = match
|
|
76
|
+
const cleanName = name.replace(/[。.]$/, '')
|
|
77
|
+
files.push({
|
|
78
|
+
key: relativePath,
|
|
79
|
+
label: cleanName,
|
|
80
|
+
order: parseInt(order),
|
|
81
|
+
path: `/docs/${relativePath}.md`,
|
|
82
|
+
relativePath,
|
|
83
|
+
level: parts.length - 1
|
|
84
|
+
})
|
|
85
|
+
} else {
|
|
86
|
+
const cleanFileName = fileName.replace(/[。.]$/, '')
|
|
87
|
+
files.push({
|
|
88
|
+
key: relativePath,
|
|
89
|
+
label: cleanFileName,
|
|
90
|
+
order: 999,
|
|
91
|
+
path: `/docs/${relativePath}.md`,
|
|
92
|
+
relativePath,
|
|
93
|
+
level: parts.length - 1
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return buildTree(files)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('获取文档列表失败:', error)
|
|
101
|
+
return []
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<teleport to="body">
|
|
3
|
+
<div
|
|
4
|
+
v-if="visible"
|
|
5
|
+
class="image-zoom-overlay"
|
|
6
|
+
@click="handleOverlayClick"
|
|
7
|
+
@wheel="handleWheel"
|
|
8
|
+
@mousedown="handleMouseDown"
|
|
9
|
+
@mousemove="handleMouseMove"
|
|
10
|
+
@mouseup="handleMouseUp"
|
|
11
|
+
@mouseleave="handleMouseUp"
|
|
12
|
+
>
|
|
13
|
+
<div class="image-zoom-container">
|
|
14
|
+
<!-- 工具栏 -->
|
|
15
|
+
<div class="zoom-toolbar">
|
|
16
|
+
<button class="zoom-btn" @click="handleZoomIn" title="放大">
|
|
17
|
+
<ZoomIn :size="16" />
|
|
18
|
+
</button>
|
|
19
|
+
<button class="zoom-btn" @click="handleZoomOut" title="缩小">
|
|
20
|
+
<ZoomOut :size="16" />
|
|
21
|
+
</button>
|
|
22
|
+
<button class="zoom-btn" @click="resetZoom" title="重置">
|
|
23
|
+
<RotateCcw :size="16" />
|
|
24
|
+
</button>
|
|
25
|
+
<span class="zoom-level">{{ Math.round(scale * 100) }}%</span>
|
|
26
|
+
<button class="zoom-btn close-btn" @click="close" title="关闭">
|
|
27
|
+
<X :size="16" />
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- 图片容器 -->
|
|
32
|
+
<div
|
|
33
|
+
class="image-wrapper"
|
|
34
|
+
:style="{
|
|
35
|
+
transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
|
|
36
|
+
transformOrigin: 'center center'
|
|
37
|
+
}"
|
|
38
|
+
v-html="imageContent"
|
|
39
|
+
></div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</teleport>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup>
|
|
46
|
+
import { ref } from 'vue'
|
|
47
|
+
import { ZoomIn, ZoomOut, RotateCcw, X } from 'lucide-vue-next'
|
|
48
|
+
|
|
49
|
+
// Props
|
|
50
|
+
const props = defineProps({
|
|
51
|
+
visible: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false
|
|
54
|
+
},
|
|
55
|
+
imageContent: {
|
|
56
|
+
type: String,
|
|
57
|
+
default: ''
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Emits
|
|
62
|
+
const emit = defineEmits(['close'])
|
|
63
|
+
|
|
64
|
+
// 缩放和拖拽状态
|
|
65
|
+
const scale = ref(1)
|
|
66
|
+
const translateX = ref(0)
|
|
67
|
+
const translateY = ref(0)
|
|
68
|
+
const isDragging = ref(false)
|
|
69
|
+
const lastMouseX = ref(0)
|
|
70
|
+
const lastMouseY = ref(0)
|
|
71
|
+
|
|
72
|
+
// 缩放控制
|
|
73
|
+
const minScale = 0.1
|
|
74
|
+
const maxScale = 10
|
|
75
|
+
const scaleStep = 0.2
|
|
76
|
+
|
|
77
|
+
// 放大
|
|
78
|
+
function handleZoomIn() {
|
|
79
|
+
if (scale.value < maxScale) {
|
|
80
|
+
scale.value = Math.min(scale.value + scaleStep, maxScale)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 缩小
|
|
85
|
+
function handleZoomOut() {
|
|
86
|
+
if (scale.value > minScale) {
|
|
87
|
+
scale.value = Math.max(scale.value - scaleStep, minScale)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 重置缩放
|
|
92
|
+
function resetZoom() {
|
|
93
|
+
scale.value = 1
|
|
94
|
+
translateX.value = 0
|
|
95
|
+
translateY.value = 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 关闭
|
|
99
|
+
function close() {
|
|
100
|
+
resetZoom()
|
|
101
|
+
emit('close')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 处理遮罩层点击
|
|
105
|
+
function handleOverlayClick(event) {
|
|
106
|
+
if (event.target.classList.contains('image-zoom-overlay')) {
|
|
107
|
+
close()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 处理滚轮缩放
|
|
112
|
+
function handleWheel(event) {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
|
|
115
|
+
const delta = event.deltaY > 0 ? -scaleStep : scaleStep
|
|
116
|
+
const newScale = Math.max(minScale, Math.min(maxScale, scale.value + delta))
|
|
117
|
+
|
|
118
|
+
if (newScale !== scale.value) {
|
|
119
|
+
scale.value = newScale
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 处理鼠标按下
|
|
124
|
+
function handleMouseDown(event) {
|
|
125
|
+
if (event.target.closest('.zoom-toolbar')) return
|
|
126
|
+
|
|
127
|
+
isDragging.value = true
|
|
128
|
+
lastMouseX.value = event.clientX
|
|
129
|
+
lastMouseY.value = event.clientY
|
|
130
|
+
event.preventDefault()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 处理鼠标移动
|
|
134
|
+
function handleMouseMove(event) {
|
|
135
|
+
if (!isDragging.value) return
|
|
136
|
+
|
|
137
|
+
const deltaX = event.clientX - lastMouseX.value
|
|
138
|
+
const deltaY = event.clientY - lastMouseY.value
|
|
139
|
+
|
|
140
|
+
translateX.value += deltaX
|
|
141
|
+
translateY.value += deltaY
|
|
142
|
+
|
|
143
|
+
lastMouseX.value = event.clientX
|
|
144
|
+
lastMouseY.value = event.clientY
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 处理鼠标释放
|
|
148
|
+
function handleMouseUp() {
|
|
149
|
+
isDragging.value = false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 监听键盘事件
|
|
153
|
+
function handleKeyDown(event) {
|
|
154
|
+
if (!props.visible) return
|
|
155
|
+
|
|
156
|
+
switch (event.key) {
|
|
157
|
+
case 'Escape':
|
|
158
|
+
close()
|
|
159
|
+
break
|
|
160
|
+
case '+':
|
|
161
|
+
case '=':
|
|
162
|
+
handleZoomIn()
|
|
163
|
+
break
|
|
164
|
+
case '-':
|
|
165
|
+
handleZoomOut()
|
|
166
|
+
break
|
|
167
|
+
case '0':
|
|
168
|
+
resetZoom()
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 添加键盘监听
|
|
174
|
+
if (typeof window !== 'undefined') {
|
|
175
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<style scoped>
|
|
180
|
+
.image-zoom-overlay {
|
|
181
|
+
position: fixed;
|
|
182
|
+
top: 0;
|
|
183
|
+
left: 0;
|
|
184
|
+
right: 0;
|
|
185
|
+
bottom: 0;
|
|
186
|
+
background: rgba(0, 0, 0, 0.95);
|
|
187
|
+
z-index: 9999;
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: center;
|
|
191
|
+
cursor: grab;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.image-zoom-overlay:active {
|
|
195
|
+
cursor: grabbing;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.image-zoom-container {
|
|
199
|
+
position: relative;
|
|
200
|
+
width: 100%;
|
|
201
|
+
height: 100%;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
pointer-events: none;
|
|
206
|
+
overflow: auto;
|
|
207
|
+
padding: 40px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.zoom-toolbar {
|
|
211
|
+
position: absolute;
|
|
212
|
+
top: 20px;
|
|
213
|
+
right: 20px;
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
background: rgba(255, 255, 255, 0.95);
|
|
218
|
+
padding: 8px 12px;
|
|
219
|
+
border-radius: 8px;
|
|
220
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
221
|
+
z-index: 10000;
|
|
222
|
+
pointer-events: auto;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.zoom-btn {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
width: 32px;
|
|
230
|
+
height: 32px;
|
|
231
|
+
border: none;
|
|
232
|
+
background: transparent;
|
|
233
|
+
color: #4a5568;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
border-radius: 4px;
|
|
236
|
+
transition: all 0.15s;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.zoom-btn:hover {
|
|
240
|
+
background: #f7fafc;
|
|
241
|
+
color: #2d3748;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.close-btn {
|
|
245
|
+
color: #e53e3e;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.close-btn:hover {
|
|
249
|
+
background: #fed7d7;
|
|
250
|
+
color: #c53030;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.zoom-level {
|
|
254
|
+
font-size: 12px;
|
|
255
|
+
font-weight: 600;
|
|
256
|
+
color: #4a5568;
|
|
257
|
+
padding: 0 8px;
|
|
258
|
+
min-width: 50px;
|
|
259
|
+
text-align: center;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.image-wrapper {
|
|
263
|
+
transition: transform 0.2s ease-out;
|
|
264
|
+
cursor: grab;
|
|
265
|
+
background: rgba(255, 255, 255, 0.98);
|
|
266
|
+
padding: 20px;
|
|
267
|
+
border-radius: 8px;
|
|
268
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
269
|
+
pointer-events: auto;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.image-wrapper:active {
|
|
273
|
+
cursor: grabbing;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* 确保SVG在放大时保持清晰 */
|
|
277
|
+
.image-wrapper :deep(svg) {
|
|
278
|
+
display: block !important;
|
|
279
|
+
background: white;
|
|
280
|
+
border-radius: 4px;
|
|
281
|
+
min-width: 800px;
|
|
282
|
+
width: auto !important;
|
|
283
|
+
height: auto !important;
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="logo-container" @click="$emit('go-home')" title="返回首页">
|
|
3
|
+
<svg class="logo-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
4
|
+
<rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
5
|
+
<line x1="8" y1="8" x2="16" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
6
|
+
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
7
|
+
<line x1="8" y1="16" x2="13" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
8
|
+
</svg>
|
|
9
|
+
<span class="logo-text">md2ui</span>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
defineEmits(['go-home'])
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<style scoped>
|
|
18
|
+
.logo-container {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 10px;
|
|
22
|
+
color: #111827;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
transition: opacity 0.2s;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.logo-container:hover {
|
|
28
|
+
opacity: 0.7;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.logo-container:active {
|
|
32
|
+
opacity: 0.5;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.logo-icon {
|
|
36
|
+
flex-shrink: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.logo-text {
|
|
40
|
+
font-size: 16px;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
letter-spacing: -0.02em;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside class="toc-sidebar" v-if="tocItems.length > 0 && !collapsed" :style="{ width: width + 'px' }">
|
|
3
|
+
<div class="toc-header">
|
|
4
|
+
<List :size="16" />
|
|
5
|
+
<span>目录</span>
|
|
6
|
+
</div>
|
|
7
|
+
<nav class="toc-nav">
|
|
8
|
+
<a
|
|
9
|
+
v-for="item in tocItems"
|
|
10
|
+
:key="item.id"
|
|
11
|
+
:href="`#${item.id}`"
|
|
12
|
+
:class="['toc-item', `toc-level-${item.level}`, { active: activeHeading === item.id }]"
|
|
13
|
+
@click.prevent="$emit('scroll-to', item.id)"
|
|
14
|
+
>
|
|
15
|
+
{{ item.text }}
|
|
16
|
+
</a>
|
|
17
|
+
</nav>
|
|
18
|
+
</aside>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup>
|
|
22
|
+
import { List } from 'lucide-vue-next'
|
|
23
|
+
|
|
24
|
+
defineProps({
|
|
25
|
+
tocItems: {
|
|
26
|
+
type: Array,
|
|
27
|
+
default: () => []
|
|
28
|
+
},
|
|
29
|
+
activeHeading: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: ''
|
|
32
|
+
},
|
|
33
|
+
collapsed: {
|
|
34
|
+
type: Boolean,
|
|
35
|
+
default: false
|
|
36
|
+
},
|
|
37
|
+
width: {
|
|
38
|
+
type: Number,
|
|
39
|
+
default: 240
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
defineEmits(['scroll-to'])
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- 文件夹节点 -->
|
|
4
|
+
<div
|
|
5
|
+
v-if="item.type === 'folder'"
|
|
6
|
+
class="nav-item nav-folder"
|
|
7
|
+
:class="{ expanded: item.expanded }"
|
|
8
|
+
:style="{ paddingLeft: `${20 + item.level * 16}px` }"
|
|
9
|
+
@click="$emit('toggle', item)"
|
|
10
|
+
>
|
|
11
|
+
<ChevronRight v-if="!item.expanded" class="nav-icon chevron-icon" :size="16" />
|
|
12
|
+
<ChevronDown v-else class="nav-icon chevron-icon" :size="16" />
|
|
13
|
+
<Folder v-if="!item.expanded" class="nav-icon folder-icon" :size="16" />
|
|
14
|
+
<FolderOpen v-else class="nav-icon folder-icon" :size="16" />
|
|
15
|
+
<span>{{ item.label }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- 递归渲染子节点 -->
|
|
19
|
+
<template v-if="item.type === 'folder' && item.expanded && item.children">
|
|
20
|
+
<TreeNode
|
|
21
|
+
v-for="child in item.children"
|
|
22
|
+
:key="child.key"
|
|
23
|
+
:item="child"
|
|
24
|
+
:currentDoc="currentDoc"
|
|
25
|
+
@toggle="$emit('toggle', $event)"
|
|
26
|
+
@select="$emit('select', $event)"
|
|
27
|
+
/>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<!-- 文件节点 -->
|
|
31
|
+
<div
|
|
32
|
+
v-if="item.type === 'file'"
|
|
33
|
+
class="nav-item"
|
|
34
|
+
:class="{ active: currentDoc === item.key }"
|
|
35
|
+
:style="{ paddingLeft: `${20 + item.level * 16}px` }"
|
|
36
|
+
@click="$emit('select', item.key)"
|
|
37
|
+
>
|
|
38
|
+
<FileText class="nav-icon file-icon" :size="16" />
|
|
39
|
+
<span>{{ item.label }}</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script setup>
|
|
45
|
+
import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText } from 'lucide-vue-next'
|
|
46
|
+
|
|
47
|
+
defineProps({
|
|
48
|
+
item: {
|
|
49
|
+
type: Object,
|
|
50
|
+
required: true
|
|
51
|
+
},
|
|
52
|
+
currentDoc: {
|
|
53
|
+
type: String,
|
|
54
|
+
required: true
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
defineEmits(['toggle', 'select'])
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { ref, nextTick } from 'vue'
|
|
2
|
+
import { marked } from 'marked'
|
|
3
|
+
import mermaid from 'mermaid'
|
|
4
|
+
|
|
5
|
+
// 初始化 Mermaid - 使用 neutral 主题
|
|
6
|
+
mermaid.initialize({
|
|
7
|
+
startOnLoad: false,
|
|
8
|
+
theme: 'neutral',
|
|
9
|
+
securityLevel: 'loose'
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
// 自定义 marked 渲染器,处理 Mermaid 代码块
|
|
13
|
+
const renderer = new marked.Renderer()
|
|
14
|
+
const originalCodeRenderer = renderer.code.bind(renderer)
|
|
15
|
+
|
|
16
|
+
renderer.code = function(code, language) {
|
|
17
|
+
if (language === 'mermaid') {
|
|
18
|
+
const id = 'mermaid-' + Math.random().toString(36).substr(2, 9)
|
|
19
|
+
return `<div class="mermaid" id="${id}">${code}</div>`
|
|
20
|
+
}
|
|
21
|
+
return originalCodeRenderer(code, language)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
marked.setOptions({
|
|
25
|
+
renderer,
|
|
26
|
+
breaks: true,
|
|
27
|
+
gfm: true,
|
|
28
|
+
headerIds: true,
|
|
29
|
+
mangle: false
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export function useMarkdown() {
|
|
33
|
+
const htmlContent = ref('')
|
|
34
|
+
const tocItems = ref([])
|
|
35
|
+
|
|
36
|
+
// 渲染 Markdown
|
|
37
|
+
async function renderMarkdown(markdown) {
|
|
38
|
+
htmlContent.value = marked.parse(markdown)
|
|
39
|
+
await renderMermaid()
|
|
40
|
+
wrapTables()
|
|
41
|
+
addImageZoomHandlers()
|
|
42
|
+
extractTOC()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 为表格添加滚动容器
|
|
46
|
+
function wrapTables() {
|
|
47
|
+
nextTick(() => {
|
|
48
|
+
const tables = document.querySelectorAll('.markdown-content table')
|
|
49
|
+
tables.forEach(table => {
|
|
50
|
+
// 检查是否已经被包裹
|
|
51
|
+
if (!table.parentElement.classList.contains('table-wrapper')) {
|
|
52
|
+
const wrapper = document.createElement('div')
|
|
53
|
+
wrapper.className = 'table-wrapper'
|
|
54
|
+
table.parentNode.insertBefore(wrapper, table)
|
|
55
|
+
wrapper.appendChild(table)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 渲染 Mermaid 图表
|
|
62
|
+
async function renderMermaid() {
|
|
63
|
+
await nextTick()
|
|
64
|
+
const mermaidElements = document.querySelectorAll('.mermaid')
|
|
65
|
+
|
|
66
|
+
for (const element of mermaidElements) {
|
|
67
|
+
try {
|
|
68
|
+
const id = element.id
|
|
69
|
+
const code = element.textContent
|
|
70
|
+
const { svg } = await mermaid.render(id + '-svg', code)
|
|
71
|
+
element.innerHTML = svg
|
|
72
|
+
|
|
73
|
+
// 为Mermaid图表添加可点击样式
|
|
74
|
+
element.classList.add('zoomable-image')
|
|
75
|
+
element.style.cursor = 'zoom-in'
|
|
76
|
+
element.title = '点击放大查看'
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Mermaid 渲染失败:', error)
|
|
79
|
+
element.innerHTML = `<pre class="mermaid-error">图表渲染失败\n${error.message}</pre>`
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 为图片和Mermaid图表添加放大功能
|
|
85
|
+
function addImageZoomHandlers() {
|
|
86
|
+
nextTick(() => {
|
|
87
|
+
// 为所有图片添加放大功能
|
|
88
|
+
const images = document.querySelectorAll('.markdown-content img')
|
|
89
|
+
images.forEach(img => {
|
|
90
|
+
img.classList.add('zoomable-image')
|
|
91
|
+
img.style.cursor = 'zoom-in'
|
|
92
|
+
img.title = '点击放大查看'
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 提取文档大纲
|
|
98
|
+
function extractTOC() {
|
|
99
|
+
tocItems.value = []
|
|
100
|
+
|
|
101
|
+
nextTick(() => {
|
|
102
|
+
const headings = document.querySelectorAll('.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6')
|
|
103
|
+
|
|
104
|
+
headings.forEach((heading, index) => {
|
|
105
|
+
const level = parseInt(heading.tagName.substring(1))
|
|
106
|
+
const text = heading.textContent.trim()
|
|
107
|
+
const id = heading.id || `heading-${index}`
|
|
108
|
+
|
|
109
|
+
if (!heading.id) {
|
|
110
|
+
heading.id = id
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
tocItems.value.push({
|
|
114
|
+
id,
|
|
115
|
+
text,
|
|
116
|
+
level
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
htmlContent,
|
|
124
|
+
tocItems,
|
|
125
|
+
renderMarkdown,
|
|
126
|
+
addImageZoomHandlers
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useResize() {
|
|
4
|
+
const sidebarWidth = ref(320)
|
|
5
|
+
const tocWidth = ref(240)
|
|
6
|
+
const isResizing = ref(false)
|
|
7
|
+
const resizeType = ref('')
|
|
8
|
+
|
|
9
|
+
// 开始拖拽
|
|
10
|
+
function startResize(type, e) {
|
|
11
|
+
isResizing.value = true
|
|
12
|
+
resizeType.value = type
|
|
13
|
+
document.body.style.cursor = 'col-resize'
|
|
14
|
+
document.body.style.userSelect = 'none'
|
|
15
|
+
|
|
16
|
+
const handleMouseMove = (e) => {
|
|
17
|
+
if (!isResizing.value) return
|
|
18
|
+
|
|
19
|
+
if (resizeType.value === 'left') {
|
|
20
|
+
const newWidth = e.clientX
|
|
21
|
+
if (newWidth >= 200 && newWidth <= 400) {
|
|
22
|
+
sidebarWidth.value = newWidth
|
|
23
|
+
}
|
|
24
|
+
} else if (resizeType.value === 'right') {
|
|
25
|
+
const newWidth = window.innerWidth - e.clientX
|
|
26
|
+
if (newWidth >= 200 && newWidth <= 400) {
|
|
27
|
+
tocWidth.value = newWidth
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleMouseUp = () => {
|
|
33
|
+
isResizing.value = false
|
|
34
|
+
resizeType.value = ''
|
|
35
|
+
document.body.style.cursor = ''
|
|
36
|
+
document.body.style.userSelect = ''
|
|
37
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
38
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
42
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
sidebarWidth,
|
|
47
|
+
tocWidth,
|
|
48
|
+
startResize
|
|
49
|
+
}
|
|
50
|
+
}
|