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/README.md +104 -38
- package/bin/build.js +609 -0
- package/bin/md2ui.js +112 -63
- package/index.html +43 -1
- package/package.json +6 -5
- package/src/App.vue +106 -170
- package/src/api/docs.js +2 -1
- package/src/components/AppSidebar.vue +102 -0
- package/src/components/DocContent.vue +39 -0
- package/src/components/ImageZoom.vue +4 -2
- package/src/components/Logo.vue +5 -9
- package/src/components/MobileHeader.vue +20 -0
- package/src/components/MobileToc.vue +40 -0
- package/src/components/SearchPanel.vue +90 -0
- package/src/components/TableOfContents.vue +2 -2
- package/src/components/TopBar.vue +144 -0
- package/src/components/WelcomePage.vue +251 -0
- package/src/composables/useDocHash.js +66 -0
- package/src/composables/useDocManager.js +239 -0
- package/src/composables/useDocTree.js +80 -0
- package/src/composables/useFrontmatter.js +41 -0
- package/src/composables/useMarkdown.js +316 -94
- package/src/composables/useMobile.js +29 -0
- package/src/composables/useScroll.js +9 -15
- package/src/composables/useSearch.js +133 -0
- package/src/hljs-theme.css +104 -0
- package/src/main.js +1 -0
- package/src/style.css +935 -213
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
|
|
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
|
|
21
|
-
|
|
25
|
+
// 默认配置
|
|
26
|
+
const defaultConfig = {
|
|
27
|
+
title: 'md2ui',
|
|
28
|
+
port: 3000,
|
|
29
|
+
folderExpanded: false,
|
|
30
|
+
github: '',
|
|
31
|
+
footer: '',
|
|
32
|
+
themeColor: '#3eaf7c'
|
|
33
|
+
}
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 =>
|
|
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:
|
|
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
|
-
//
|
|
100
|
-
function
|
|
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: '
|
|
135
|
+
name: 'md2ui-server',
|
|
103
136
|
configureServer(server) {
|
|
137
|
+
// API 中间件
|
|
104
138
|
server.middlewares.use((req, res, next) => {
|
|
105
|
-
//
|
|
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
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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: [
|
|
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"
|
|
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
|
+
"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
|
-
"
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 {
|
|
62
|
-
import
|
|
63
|
-
import
|
|
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 {
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
79
|
-
const {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
123
|
-
item.expanded = !item.expanded
|
|
124
|
-
}
|
|
119
|
+
const { sidebarWidth, tocWidth, startResize } = useResize()
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|