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.
@@ -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
+ }