md2ui 1.0.3 → 1.0.8

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/bin/md2ui.js CHANGED
@@ -1,61 +1,84 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // 子命令路由:build 命令走独立的 SSG 构建流程
4
+ const subCommand = process.argv[2]
5
+ if (subCommand === 'build') {
6
+ await import('./build.js')
7
+ process.exit(0)
8
+ }
9
+
3
10
  import { createServer } from 'vite'
4
11
  import { fileURLToPath } from 'url'
5
12
  import { dirname, resolve } from 'path'
6
13
  import fs from 'fs'
14
+ import { exec } from 'child_process'
15
+ import { pathToFileURL } from 'url'
7
16
 
8
- // 获取 CLI 工具所在目录(即 md2ui 包的安装目录)
17
+ // 获取 CLI 工具所在目录
9
18
  const __filename = fileURLToPath(import.meta.url)
10
19
  const __dirname = dirname(__filename)
11
20
  const pkgRoot = resolve(__dirname, '..')
12
21
 
13
- // 导入共享配置
14
- const { config } = await import(resolve(pkgRoot, 'src/config.js'))
15
-
16
22
  // 用户执行命令的目录
17
23
  const userDir = process.cwd()
18
24
 
19
- // 解析命令行参数
20
- const args = process.argv.slice(2)
21
- let port = config.defaultPort
25
+ // 默认配置
26
+ const defaultConfig = {
27
+ title: 'md2ui',
28
+ port: 3000,
29
+ folderExpanded: false,
30
+ github: '',
31
+ footer: '',
32
+ themeColor: '#3eaf7c'
33
+ }
22
34
 
23
- for (let i = 0; i < args.length; i++) {
24
- if (args[i] === '-p' || args[i] === '--port') {
25
- port = parseInt(args[i + 1]) || config.defaultPort
35
+ // 加载用户配置文件(md2ui.config.js .md2uirc.json)
36
+ async function loadUserConfig() {
37
+ // 尝试 md2ui.config.js
38
+ const jsPath = resolve(userDir, 'md2ui.config.js')
39
+ if (fs.existsSync(jsPath)) {
40
+ try {
41
+ const mod = await import(pathToFileURL(jsPath).href)
42
+ console.log(' 配置文件: md2ui.config.js\n')
43
+ return mod.default || mod
44
+ } catch (e) {
45
+ console.warn(' 配置文件加载失败:', e.message, '\n')
46
+ }
26
47
  }
48
+ // 尝试 .md2uirc.json
49
+ const jsonPath = resolve(userDir, '.md2uirc.json')
50
+ if (fs.existsSync(jsonPath)) {
51
+ try {
52
+ const raw = fs.readFileSync(jsonPath, 'utf-8')
53
+ console.log(' 配置文件: .md2uirc.json\n')
54
+ return JSON.parse(raw)
55
+ } catch (e) {
56
+ console.warn(' 配置文件加载失败:', e.message, '\n')
57
+ }
58
+ }
59
+ return {}
27
60
  }
28
61
 
29
- // 虚拟模块插件:动态提供用户目录的文档列表
30
- function docsPlugin() {
31
- const virtualModuleId = 'virtual:user-docs'
32
- const resolvedVirtualModuleId = '\0' + virtualModuleId
33
-
34
- return {
35
- name: 'user-docs-plugin',
36
- resolveId(id) {
37
- if (id === virtualModuleId) {
38
- return resolvedVirtualModuleId
39
- }
40
- },
41
- load(id) {
42
- if (id === resolvedVirtualModuleId) {
43
- // 扫描用户目录下的 md 文件
44
- const docs = scanDocs(userDir)
45
- return `export default ${JSON.stringify(docs)}`
46
- }
62
+ // 解析命令行参数
63
+ function parseArgs() {
64
+ const args = process.argv.slice(2)
65
+ const result = {}
66
+ for (let i = 0; i < args.length; i++) {
67
+ if (args[i] === '-p' || args[i] === '--port') {
68
+ result.port = parseInt(args[i + 1]) || undefined
69
+ i++
47
70
  }
48
71
  }
72
+ return result
49
73
  }
50
74
 
51
75
  // 扫描目录下的 md 文件
52
- function scanDocs(dir, basePath = '', level = 0) {
76
+ function scanDocs(dir, basePath = '', level = 0, folderExpanded = false) {
53
77
  const items = []
54
-
55
78
  if (!fs.existsSync(dir)) return items
56
-
79
+
57
80
  const entries = fs.readdirSync(dir, { withFileTypes: true })
58
- .filter(e => !e.name.startsWith('.') && e.name !== 'node_modules')
81
+ .filter(e => e.name !== 'node_modules')
59
82
  .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
60
83
 
61
84
  for (const entry of entries) {
@@ -63,8 +86,7 @@ function scanDocs(dir, basePath = '', level = 0) {
63
86
  const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name
64
87
 
65
88
  if (entry.isDirectory()) {
66
- const children = scanDocs(fullPath, relativePath, level + 1)
67
- // 只有包含 md 文件的文件夹才加入目录
89
+ const children = scanDocs(fullPath, relativePath, level + 1, folderExpanded)
68
90
  if (children.length > 0) {
69
91
  const match = entry.name.match(/^(\d+)-(.+)$/)
70
92
  items.push({
@@ -73,7 +95,7 @@ function scanDocs(dir, basePath = '', level = 0) {
73
95
  order: match ? parseInt(match[1]) : 999,
74
96
  type: 'folder',
75
97
  level,
76
- expanded: config.folderExpanded,
98
+ expanded: folderExpanded,
77
99
  children
78
100
  })
79
101
  }
@@ -91,25 +113,43 @@ function scanDocs(dir, basePath = '', level = 0) {
91
113
  }
92
114
  }
93
115
 
94
- // 按 order 排序
95
116
  items.sort((a, b) => a.order - b.order)
96
117
  return items
97
118
  }
98
119
 
99
- // 提供用户目录 md 文件的中间件
100
- function serveUserDocs() {
120
+ // 检查目录下是否有 md 文件
121
+ function hasMdFiles(dir) {
122
+ if (!fs.existsSync(dir)) return false
123
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
124
+ .filter(e => e.name !== 'node_modules')
125
+ for (const entry of entries) {
126
+ if (entry.isFile() && entry.name.endsWith('.md')) return true
127
+ if (entry.isDirectory() && hasMdFiles(resolve(dir, entry.name))) return true
128
+ }
129
+ return false
130
+ }
131
+
132
+ // Vite 插件:提供用户文档 API + 配置 API + 热更新
133
+ function md2uiPlugin(siteConfig) {
101
134
  return {
102
- name: 'serve-user-docs',
135
+ name: 'md2ui-server',
103
136
  configureServer(server) {
137
+ // API 中间件
104
138
  server.middlewares.use((req, res, next) => {
105
- // 提供文档列表 API
139
+ // 文档列表 API
106
140
  if (req.url === '/@user-docs-list') {
107
- const docs = scanDocs(userDir)
141
+ const docs = scanDocs(userDir, '', 0, siteConfig.folderExpanded)
108
142
  res.setHeader('Content-Type', 'application/json; charset=utf-8')
109
143
  res.end(JSON.stringify(docs))
110
144
  return
111
145
  }
112
- // 提供文档内容
146
+ // 站点配置 API
147
+ if (req.url === '/@site-config') {
148
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
149
+ res.end(JSON.stringify(siteConfig))
150
+ return
151
+ }
152
+ // 文档内容
113
153
  if (req.url?.startsWith('/@user-docs/')) {
114
154
  const filePath = resolve(userDir, decodeURIComponent(req.url.replace('/@user-docs/', '')))
115
155
  if (fs.existsSync(filePath)) {
@@ -120,52 +160,61 @@ function serveUserDocs() {
120
160
  }
121
161
  next()
122
162
  })
123
- }
124
- }
125
- }
126
163
 
127
- // 检查目录下是否有 md 文件
128
- function hasMdFiles(dir) {
129
- if (!fs.existsSync(dir)) return false
130
-
131
- const entries = fs.readdirSync(dir, { withFileTypes: true })
132
- .filter(e => !e.name.startsWith('.') && e.name !== 'node_modules')
133
-
134
- for (const entry of entries) {
135
- if (entry.isFile() && entry.name.endsWith('.md')) {
136
- return true
137
- }
138
- if (entry.isDirectory()) {
139
- if (hasMdFiles(resolve(dir, entry.name))) {
140
- return true
164
+ // SPA fallback
165
+ return () => {
166
+ server.middlewares.use((req, res, next) => {
167
+ const url = req.url?.split('?')[0] || ''
168
+ if (url.startsWith('/@') || url.startsWith('/src/') || url.startsWith('/node_modules/') || url.match(/\.\w+$/)) {
169
+ next()
170
+ return
171
+ }
172
+ req.url = '/index.html'
173
+ next()
174
+ })
141
175
  }
142
176
  }
143
177
  }
144
- return false
145
178
  }
146
179
 
147
180
  async function start() {
148
181
  console.log(`\n md2ui - Markdown 文档预览工具\n`)
149
182
  console.log(` 扫描目录: ${userDir}\n`)
150
183
 
151
- // 检查是否有 md 文件
152
184
  if (!hasMdFiles(userDir)) {
153
185
  console.log(' 当前目录下没有找到 Markdown 文件 (.md)\n')
154
186
  console.log(' 请在包含 .md 文件的目录中运行此命令\n')
155
187
  process.exit(1)
156
188
  }
157
189
 
190
+ // 加载配置
191
+ const userConfig = await loadUserConfig()
192
+ const cliArgs = parseArgs()
193
+ const siteConfig = { ...defaultConfig, ...userConfig, ...cliArgs }
194
+
158
195
  const server = await createServer({
159
196
  root: pkgRoot,
160
197
  configFile: resolve(pkgRoot, 'vite.config.js'),
161
- plugins: [docsPlugin(), serveUserDocs()],
198
+ plugins: [md2uiPlugin(siteConfig)],
162
199
  server: {
163
- port
200
+ port: siteConfig.port
164
201
  }
165
202
  })
166
203
 
167
204
  await server.listen()
168
205
  server.printUrls()
206
+
207
+ if (siteConfig.title !== defaultConfig.title) {
208
+ console.log(` 站点标题: ${siteConfig.title}`)
209
+ }
210
+ console.log('')
211
+
212
+ // 自动打开浏览器
213
+ const address = server.httpServer.address()
214
+ const url = `http://localhost:${address.port}`
215
+ const platform = process.platform
216
+ const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'
217
+ exec(`${cmd} ${url}`)
169
218
  }
170
219
 
171
220
  start().catch(console.error)
package/index.html CHANGED
@@ -6,9 +6,51 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg">
7
7
  <title>md2ui - Markdown 文档渲染系统</title>
8
8
  <meta name="description" content="将本地 Markdown 文档转换为美观的 HTML 页面">
9
+ <!-- 内联骨架屏样式,防止白屏闪烁 -->
10
+ <style>
11
+ * { margin: 0; padding: 0; box-sizing: border-box; }
12
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; background: #ffffff; }
13
+ .app-skeleton {
14
+ display: flex;
15
+ flex-direction: column;
16
+ height: 100vh;
17
+ }
18
+ .skeleton-topbar {
19
+ height: 49px;
20
+ background: #f6f8fa;
21
+ border-bottom: 1px solid #d0d7de;
22
+ flex-shrink: 0;
23
+ }
24
+ .skeleton-body {
25
+ display: flex;
26
+ flex: 1;
27
+ overflow: hidden;
28
+ }
29
+ .skeleton-sidebar {
30
+ width: 260px;
31
+ background: #f6f8fa;
32
+ border-right: 1px solid #d0d7de;
33
+ flex-shrink: 0;
34
+ }
35
+ .skeleton-content {
36
+ flex: 1;
37
+ }
38
+ @media (max-width: 768px) {
39
+ .skeleton-sidebar { display: none; }
40
+ }
41
+ </style>
9
42
  </head>
10
43
  <body>
11
- <div id="app"></div>
44
+ <div id="app">
45
+ <!-- 骨架屏:Vue 挂载后自动替换 -->
46
+ <div class="app-skeleton">
47
+ <div class="skeleton-topbar"></div>
48
+ <div class="skeleton-body">
49
+ <div class="skeleton-sidebar"></div>
50
+ <div class="skeleton-content"></div>
51
+ </div>
52
+ </div>
53
+ </div>
12
54
  <script type="module" src="/src/main.js"></script>
13
55
  </body>
14
56
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md2ui",
3
- "version": "1.0.3",
3
+ "version": "1.0.8",
4
4
  "type": "module",
5
5
  "description": "将本地 Markdown 文档转换为美观的 HTML 页面",
6
6
  "author": "",
@@ -33,14 +33,15 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@vitejs/plugin-vue": "^5.0.0",
36
- "crypto-js": "^4.2.0",
36
+ "flexsearch": "^0.8.212",
37
+ "github-slugger": "^2.0.0",
38
+ "highlight.js": "^11.11.1",
39
+ "jsdom": "^28.1.0",
37
40
  "lucide-vue-next": "^0.556.0",
38
41
  "marked": "^11.1.1",
39
42
  "mermaid": "^10.6.1",
43
+ "minisearch": "^7.2.0",
40
44
  "vite": "^5.0.0",
41
45
  "vue": "^3.4.0"
42
- },
43
- "devDependencies": {
44
- "@types/crypto-js": "^4.2.2"
45
46
  }
46
47
  }
package/src/App.vue CHANGED
@@ -1,206 +1,142 @@
1
1
  <template>
2
- <div class="container">
3
- <aside v-if="!sidebarCollapsed" class="sidebar" :style="{ width: sidebarWidth + 'px' }">
4
- <div class="logo">
5
- <div class="logo-group">
6
- <Logo @go-home="loadFirstDoc" />
7
- <a href="https://github.com/devsneed/md2ui" target="_blank" class="github-link" title="GitHub">
8
- <Github :size="14" />
9
- </a>
10
- </div>
11
- <button class="sidebar-toggle" @click="sidebarCollapsed = true" title="收起导航">
12
- <PanelLeftClose :size="16" />
13
- </button>
14
- </div>
15
- <nav class="nav-menu">
16
- <div class="nav-section">
17
- <span>文档目录</span>
18
- <div class="nav-actions">
19
- <button class="action-btn" @click="expandAll" title="全部展开">
20
- <ChevronsDownUp :size="14" />
21
- </button>
22
- <button class="action-btn" @click="collapseAll" title="全部收起">
23
- <ChevronsUpDown :size="14" />
24
- </button>
25
- </div>
26
- </div>
27
- <TreeNode
28
- v-for="item in docsList"
29
- :key="item.key"
30
- :item="item"
31
- :currentDoc="currentDoc"
32
- @toggle="toggleFolder"
33
- @select="loadDoc"
34
- />
35
- </nav>
36
- </aside>
37
- <div v-if="!sidebarCollapsed" class="resizer resizer-left" @mousedown="startResize('left', $event)"></div>
38
- <button v-if="sidebarCollapsed" class="expand-btn expand-btn-left" @click="sidebarCollapsed = false" title="展开导航">
39
- <PanelLeftOpen :size="16" />
2
+ <div class="container" :class="{ 'is-mobile': isMobile }">
3
+ <!-- 移动端顶栏 -->
4
+ <MobileHeader
5
+ v-if="isMobile"
6
+ @open-drawer="mobileDrawerOpen = true"
7
+ @go-home="goHome"
8
+ @open-search="openSearch"
9
+ />
10
+ <!-- 桌面端顶栏 -->
11
+ <TopBar
12
+ v-if="!isMobile"
13
+ @select-search="handleSearchSelect"
14
+ @go-home="goHome"
15
+ />
16
+ <!-- 移动端遮罩 -->
17
+ <transition name="fade">
18
+ <div v-if="isMobile && mobileDrawerOpen" class="drawer-overlay" @click="mobileDrawerOpen = false"></div>
19
+ </transition>
20
+ <!-- 主体区域(侧边栏 + 内容 + TOC) -->
21
+ <div class="main-body">
22
+ <!-- 侧边栏 -->
23
+ <AppSidebar
24
+ v-if="!isMobile ? !sidebarCollapsed : true"
25
+ :docsList="docsList"
26
+ :currentDoc="currentDoc"
27
+ :isMobile="isMobile"
28
+ :drawerOpen="mobileDrawerOpen"
29
+ :width="sidebarWidth"
30
+ @go-home="goHome"
31
+ @close-drawer="mobileDrawerOpen = false"
32
+ @collapse="sidebarCollapsed = true"
33
+ @expand-all="onExpandAll"
34
+ @collapse-all="onCollapseAll"
35
+ @toggle-folder="toggleFolder"
36
+ @select-doc="handleDocSelect"
37
+ />
38
+ <!-- 左侧拖拽条 & 展开按钮(桌面端) -->
39
+ <div v-if="!isMobile && !sidebarCollapsed" class="resizer resizer-left" @mousedown="startResize('left', $event)"></div>
40
+ <button v-if="!isMobile && sidebarCollapsed" class="expand-btn expand-btn-left" @click="sidebarCollapsed = false" title="展开导航">
41
+ <ChevronRight :size="14" />
40
42
  </button>
41
- <main class="content" @scroll="handleScroll" @click="handleContentClick">
42
- <article class="markdown-content" v-html="htmlContent"></article>
43
- </main>
44
- <div v-if="!tocCollapsed && tocItems.length > 0" class="resizer resizer-right" @mousedown="startResize('right', $event)"></div>
45
- <TableOfContents :tocItems="tocItems" :activeHeading="activeHeading" :collapsed="tocCollapsed" :width="tocWidth" @toggle="tocCollapsed = !tocCollapsed" @scroll-to="scrollToHeading" />
46
- <button v-if="tocCollapsed && tocItems.length > 0" class="expand-btn expand-btn-right" @click="tocCollapsed = false" title="展开目录">
47
- <PanelRightOpen :size="16" />
43
+ <!-- 内容区 -->
44
+ <DocContent
45
+ :showWelcome="showWelcome"
46
+ :htmlContent="htmlContent"
47
+ :prevDoc="prevDoc"
48
+ :nextDoc="nextDoc"
49
+ @scroll="handleScroll"
50
+ @content-click="onContentClick"
51
+ @start="loadFirstDoc"
52
+ @load-doc="loadDoc"
53
+ />
54
+ <!-- 桌面端 TOC -->
55
+ <div v-if="!isMobile && !tocCollapsed && tocItems.length > 0" class="resizer resizer-right" @mousedown="startResize('right', $event)"></div>
56
+ <TableOfContents v-if="!isMobile" :tocItems="tocItems" :activeHeading="activeHeading" :collapsed="tocCollapsed" :width="tocWidth" @toggle="tocCollapsed = !tocCollapsed" @scroll-to="scrollToHeading" />
57
+ <button v-if="!isMobile && tocCollapsed && tocItems.length > 0" class="expand-btn expand-btn-right" @click="tocCollapsed = false" title="展开目录">
58
+ <ChevronLeft :size="14" />
48
59
  </button>
60
+ </div>
61
+ <!-- 移动端 TOC -->
62
+ <MobileToc
63
+ v-if="isMobile"
64
+ :tocItems="tocItems"
65
+ :activeHeading="activeHeading"
66
+ :open="mobileTocOpen"
67
+ :showWelcome="showWelcome"
68
+ @toggle="mobileTocOpen = !mobileTocOpen"
69
+ @close="mobileTocOpen = false"
70
+ @scroll-to="(id) => { scrollToHeading(id); mobileTocOpen = false }"
71
+ />
72
+ <!-- 返回顶部 -->
49
73
  <transition name="fade">
50
74
  <button v-if="showBackToTop" class="back-to-top" @click="scrollToTop" title="返回顶部">
51
75
  <ArrowUp :size="20" />
52
76
  <span class="progress-text">{{ scrollProgress }}%</span>
53
77
  </button>
54
78
  </transition>
79
+ <!-- 图片放大 -->
55
80
  <ImageZoom :visible="zoomVisible" :imageContent="zoomContent" @close="zoomVisible = false" />
81
+
56
82
  </div>
57
83
  </template>
58
84
 
59
85
  <script setup>
60
86
  import { ref, onMounted } from 'vue'
61
- import { ChevronsDownUp, ChevronsUpDown, ArrowUp, PanelLeftOpen, PanelLeftClose, PanelRightOpen, Github } from 'lucide-vue-next'
62
- import Logo from './components/Logo.vue'
63
- import TreeNode from './components/TreeNode.vue'
87
+ import { ArrowUp, ChevronRight, ChevronLeft } from 'lucide-vue-next'
88
+ import MobileHeader from './components/MobileHeader.vue'
89
+ import TopBar from './components/TopBar.vue'
90
+ import AppSidebar from './components/AppSidebar.vue'
91
+ import DocContent from './components/DocContent.vue'
64
92
  import TableOfContents from './components/TableOfContents.vue'
93
+ import MobileToc from './components/MobileToc.vue'
65
94
  import ImageZoom from './components/ImageZoom.vue'
66
- import { getDocsList } from './api/docs.js'
67
- import { useMarkdown } from './composables/useMarkdown.js'
68
- import { useScroll } from './composables/useScroll.js'
95
+ import { useDocManager } from './composables/useDocManager.js'
69
96
  import { useResize } from './composables/useResize.js'
70
97
 
71
- const docsList = ref([])
72
- const currentDoc = ref('')
98
+ import { useSearch } from './composables/useSearch.js'
99
+ import { useMobile } from './composables/useMobile.js'
100
+
101
+
102
+ // UI 状态
73
103
  const sidebarCollapsed = ref(false)
74
104
  const tocCollapsed = ref(false)
75
105
  const zoomVisible = ref(false)
76
106
  const zoomContent = ref('')
77
107
 
78
- const { htmlContent, tocItems, renderMarkdown } = useMarkdown()
79
- const { scrollProgress, showBackToTop, activeHeading, handleScroll, scrollToHeading, scrollToTop } = useScroll()
80
- const { sidebarWidth, tocWidth, startResize } = useResize()
81
-
82
- async function loadDocsList() {
83
- docsList.value = await getDocsList()
84
- }
85
-
86
- // 加载第一个文档
87
- function loadFirstDoc() {
88
- const firstDoc = findFirstDoc(docsList.value)
89
- if (firstDoc) {
90
- loadDoc(firstDoc.key)
91
- }
92
- }
93
-
94
- async function loadDoc(key) {
95
- currentDoc.value = key
96
- const doc = findDoc(docsList.value, key)
97
- if (!doc) return
98
- try {
99
- const response = await fetch(doc.path)
100
- if (response.ok) {
101
- const content = await response.text()
102
- await renderMarkdown(content)
103
- const contentEl = document.querySelector('.content')
104
- if (contentEl) contentEl.scrollTop = 0
105
- }
106
- } catch (error) {
107
- console.error('加载文档失败:', error)
108
- }
109
- }
110
-
111
- function findDoc(items, key) {
112
- for (const item of items) {
113
- if (item.type === 'file' && item.key === key) return item
114
- if (item.type === 'folder' && item.children) {
115
- const found = findDoc(item.children, key)
116
- if (found) return found
117
- }
118
- }
119
- return null
120
- }
108
+ // composables
109
+ const {
110
+ docsList, currentDoc, showWelcome, htmlContent, tocItems,
111
+ scrollProgress, showBackToTop, activeHeading,
112
+ handleScroll, scrollToHeading, scrollToTop,
113
+ loadDocsList, loadFromUrl, goHome, loadDoc, loadFirstDoc,
114
+ handleDocSelect, handleContentClick, handleSearchSelect,
115
+ toggleFolder, onExpandAll, onCollapseAll,
116
+ prevDoc, nextDoc
117
+ } = useDocManager()
121
118
 
122
- function toggleFolder(item) {
123
- item.expanded = !item.expanded
124
- }
119
+ const { sidebarWidth, tocWidth, startResize } = useResize()
125
120
 
126
- function expandAll() {
127
- function expand(items) {
128
- items.forEach(item => {
129
- if (item.type === 'folder') {
130
- item.expanded = true
131
- if (item.children) expand(item.children)
132
- }
133
- })
134
- }
135
- expand(docsList.value)
136
- }
121
+ const { openSearch } = useSearch()
122
+ const { isMobile, mobileDrawerOpen, mobileTocOpen } = useMobile()
137
123
 
138
- function collapseAll() {
139
- function collapse(items) {
140
- items.forEach(item => {
141
- if (item.type === 'folder') {
142
- item.expanded = false
143
- if (item.children) collapse(item.children)
144
- }
145
- })
146
- }
147
- collapse(docsList.value)
148
- }
149
124
 
150
- function handleContentClick(event) {
151
- const target = event.target
152
- if (target.tagName === 'IMG' && target.classList.contains('zoomable-image')) {
153
- zoomContent.value = `<img src="${target.src}" alt="${target.alt || ''}" style="max-width: 100%; height: auto;" />`
154
- zoomVisible.value = true
155
- return
156
- }
157
- const mermaidEl = target.closest('.mermaid')
158
- if (mermaidEl && mermaidEl.classList.contains('zoomable-image')) {
159
- zoomContent.value = mermaidEl.innerHTML
160
- zoomVisible.value = true
161
- }
162
- }
163
-
164
- // 查找第一个文档
165
- function findFirstDoc(items) {
166
- for (const item of items) {
167
- if (item.type === 'file') return item
168
- if (item.type === 'folder' && item.children) {
169
- const found = findFirstDoc(item.children)
170
- if (found) return found
125
+ // 内容区点击:委托给 docManager,图片放大回调在这里处理
126
+ function onContentClick(event) {
127
+ handleContentClick(event, {
128
+ onZoom(content) {
129
+ zoomContent.value = content
130
+ zoomVisible.value = true
171
131
  }
172
- }
173
- return null
132
+ })
174
133
  }
175
134
 
176
- // 显示无文档提示
177
- function showEmptyMessage() {
178
- renderMarkdown(`# 当前目录没有 Markdown 文档
179
-
180
- 请在当前目录下添加 \`.md\` 文件,然后刷新页面。
181
-
182
- ## 文档组织示例
183
-
184
- \`\`\`
185
- your-docs/
186
- ├── 00-快速开始.md
187
- ├── 01-功能特性.md
188
- └── 02-进阶指南/
189
- ├── 01-目录结构.md
190
- └── 02-自定义配置.md
191
- \`\`\`
192
- `)
193
- }
135
+ // 全局快捷键
136
+ window.addEventListener('popstate', () => loadFromUrl())
194
137
 
195
138
  onMounted(async () => {
196
139
  await loadDocsList()
197
-
198
- // 如果有文档,加载第一个;否则显示提示
199
- const firstDoc = findFirstDoc(docsList.value)
200
- if (firstDoc) {
201
- await loadDoc(firstDoc.key)
202
- } else {
203
- showEmptyMessage()
204
- }
140
+ await loadFromUrl()
205
141
  })
206
142
  </script>
package/src/api/docs.js CHANGED
@@ -64,7 +64,8 @@ export async function getDocsList() {
64
64
 
65
65
  // 开发模式:扫描 public/docs 目录
66
66
  try {
67
- const modules = import.meta.glob('/public/docs/**/*.md')
67
+ // 排除隐藏目录和隐藏文件(以 . 开头的路径段)
68
+ const modules = import.meta.glob('/public/docs/**/*.md', { eager: false })
68
69
  const files = []
69
70
 
70
71
  for (const path in modules) {