md2ui 1.0.16 → 1.0.19
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 +3 -55
- package/bin/build.js +82 -7
- package/bin/md2ui.js +80 -4
- package/package.json +23 -9
- 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 +86 -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/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/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 +130 -6
- 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 +199 -2
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +149 -0
- package/src/components/TableBubbleMenu.vue +177 -0
- package/src/components/TableOfContents.vue +138 -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 +325 -68
- package/src/composables/useDocTree.js +56 -1
- package/src/composables/useExportPdf.js +102 -0
- 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 +529 -42
- package/src/composables/useScroll.js +47 -5
- package/src/config.js +1 -1
- 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 +184 -0
- package/src/style.css +2194 -39
- package/vite-plugin-doc-api.js +368 -0
- package/vite.config.js +2 -1
- 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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { ref, computed, nextTick } from 'vue'
|
|
2
|
-
import
|
|
1
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
2
|
+
import * as docService from '../services/DocService.js'
|
|
3
3
|
import { useMarkdown } from './useMarkdown.js'
|
|
4
4
|
import { useSearch } from './useSearch.js'
|
|
5
5
|
import { useScroll } from './useScroll.js'
|
|
6
6
|
import { useMobile } from './useMobile.js'
|
|
7
7
|
import { findDoc, findFirstDoc, findReadmeDoc, findDocByHash, expandParents, flattenDocsList, expandAll, collapseAll } from './useDocTree.js'
|
|
8
|
+
import { stripOrderPrefix } from './useDocHash.js'
|
|
8
9
|
|
|
9
10
|
// 等待内容区域图片加载完成
|
|
10
11
|
async function waitForContentImages(timeoutMs = 3000) {
|
|
@@ -21,33 +22,39 @@ export function useDocManager() {
|
|
|
21
22
|
// 文档状态
|
|
22
23
|
const docsList = ref([])
|
|
23
24
|
const currentDoc = ref('')
|
|
24
|
-
// 如果 URL 有路径,说明是刷新已有文档页面,初始不显示欢迎页,避免闪烁
|
|
25
25
|
const hasInitialPath = window.location.pathname.replace(/^\//, '') !== ''
|
|
26
26
|
const showWelcome = ref(!hasInitialPath)
|
|
27
|
+
const lastModified = ref('')
|
|
28
|
+
|
|
29
|
+
// 编辑模式(从 sessionStorage 恢复)
|
|
30
|
+
const editMode = ref(sessionStorage.getItem('editMode') === 'true')
|
|
31
|
+
const rawMarkdown = ref('')
|
|
32
|
+
const currentDocFilePath = ref('')
|
|
27
33
|
|
|
28
34
|
// composables
|
|
29
|
-
const { htmlContent, tocItems, renderMarkdown, docHash } = useMarkdown()
|
|
35
|
+
const { htmlContent, tocItems, renderMarkdown, extractTOCFromMarkdown, docHash } = useMarkdown()
|
|
30
36
|
const { buildIndex } = useSearch()
|
|
31
37
|
const {
|
|
32
38
|
scrollProgress, showBackToTop, activeHeading,
|
|
33
39
|
handleScroll: _handleScroll,
|
|
34
40
|
scrollToHeading: _scrollToHeading,
|
|
35
|
-
scrollToTop
|
|
41
|
+
scrollToTop,
|
|
42
|
+
setTocItems
|
|
36
43
|
} = useScroll()
|
|
37
44
|
const { isMobile, mobileDrawerOpen } = useMobile()
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
setTocItems(tocItems)
|
|
47
|
+
|
|
48
|
+
// ===== 滚动 & 历史 =====
|
|
40
49
|
function getScrollTop() {
|
|
41
50
|
const el = document.querySelector('.content')
|
|
42
51
|
return el ? el.scrollTop : 0
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
// 构造 history state,保留滚动位置
|
|
46
54
|
function makeState(scrollTop) {
|
|
47
55
|
return { scrollTop: scrollTop ?? getScrollTop() }
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
// 滚动处理,同步锚点到 URL(replaceState 保留滚动位置)
|
|
51
58
|
function handleScroll(e) {
|
|
52
59
|
_handleScroll(e)
|
|
53
60
|
if (activeHeading.value && currentDoc.value) {
|
|
@@ -55,10 +62,8 @@ export function useDocManager() {
|
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
// push: true 表示用户主动点击锚点,产生可回退的历史条目
|
|
59
65
|
function scrollToHeading(id, { push = false } = {}) {
|
|
60
66
|
if (push && currentDoc.value) {
|
|
61
|
-
// 先把当前滚动位置写入即将被覆盖的历史条目
|
|
62
67
|
history.replaceState(makeState(), '', window.location.href)
|
|
63
68
|
}
|
|
64
69
|
_scrollToHeading(id)
|
|
@@ -72,37 +77,40 @@ export function useDocManager() {
|
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
//
|
|
80
|
+
// ===== 文档列表加载 =====
|
|
76
81
|
async function loadDocsList() {
|
|
77
|
-
docsList.value = await
|
|
82
|
+
docsList.value = await docService.fetchDocsList()
|
|
83
|
+
restoreExpandedState()
|
|
78
84
|
buildIndex(docsList.value)
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
//
|
|
87
|
+
// ===== 导航 =====
|
|
82
88
|
function goHome({ isPopstate = false } = {}) {
|
|
83
89
|
currentDoc.value = ''
|
|
84
90
|
showWelcome.value = true
|
|
85
91
|
htmlContent.value = ''
|
|
86
92
|
tocItems.value = []
|
|
93
|
+
editMode.value = false
|
|
94
|
+
lastModified.value = ''
|
|
95
|
+
sessionStorage.setItem('editMode', 'false')
|
|
96
|
+
document.title = 'md2ui'
|
|
87
97
|
if (!isPopstate) {
|
|
88
|
-
// 用户主动点击,保存旧条目滚动位置后 push
|
|
89
98
|
history.replaceState(makeState(), '', window.location.href)
|
|
90
99
|
history.pushState(makeState(0), '', '/')
|
|
91
100
|
}
|
|
92
101
|
if (isMobile.value) mobileDrawerOpen.value = false
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
// 加载文档
|
|
96
104
|
async function loadDoc(key, { replace = false, anchor = '', keepState = false } = {}) {
|
|
97
105
|
currentDoc.value = key
|
|
98
106
|
showWelcome.value = false
|
|
107
|
+
lastContentHash = ''
|
|
108
|
+
docService.resetContentEtag()
|
|
99
109
|
const hash = docHash(key)
|
|
100
110
|
const url = anchor ? `/${hash}#${anchor}` : `/${hash}`
|
|
101
111
|
if (replace) {
|
|
102
|
-
// keepState: popstate 回退时保留浏览器已有的 state(含 scrollTop)
|
|
103
112
|
if (!keepState) history.replaceState(makeState(0), '', url)
|
|
104
113
|
} else {
|
|
105
|
-
// push 前先保存当前滚动位置到旧条目
|
|
106
114
|
history.replaceState(makeState(), '', window.location.href)
|
|
107
115
|
history.pushState(makeState(0), '', url)
|
|
108
116
|
}
|
|
@@ -112,33 +120,75 @@ export function useDocManager() {
|
|
|
112
120
|
const response = await fetch(doc.path)
|
|
113
121
|
if (response.ok) {
|
|
114
122
|
const content = await response.text()
|
|
115
|
-
|
|
123
|
+
rawMarkdown.value = content
|
|
124
|
+
// 捕获最后修改时间
|
|
125
|
+
const lm = response.headers.get('x-last-modified')
|
|
126
|
+
lastModified.value = lm || ''
|
|
127
|
+
// 提取文件路径供轮询使用
|
|
128
|
+
currentDocFilePath.value = doc.path.replace(/^\/@user-docs\//, '')
|
|
129
|
+
if (editMode.value) {
|
|
130
|
+
extractTOCFromMarkdown(content, tocItems)
|
|
131
|
+
} else {
|
|
132
|
+
await renderMarkdown(content, key, docsList.value)
|
|
133
|
+
}
|
|
116
134
|
const contentEl = document.querySelector('.content')
|
|
117
135
|
if (contentEl) contentEl.scrollTop = 0
|
|
136
|
+
// 动态更新页面标题(SEO + 浏览器标签页)
|
|
137
|
+
const docTitle = findDoc(docsList.value, key)?.label || ''
|
|
138
|
+
document.title = docTitle ? `${docTitle} - md2ui` : 'md2ui'
|
|
118
139
|
}
|
|
119
140
|
} catch (error) {
|
|
120
141
|
console.error('加载文档失败:', error)
|
|
121
142
|
}
|
|
122
143
|
}
|
|
123
144
|
|
|
124
|
-
// 加载第一篇文档
|
|
125
145
|
function loadFirstDoc() {
|
|
126
146
|
const first = findFirstDoc(docsList.value)
|
|
127
147
|
if (first) loadDoc(first.key)
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
// 选择文档(移动端自动关闭抽屉)
|
|
131
150
|
function handleDocSelect(key) {
|
|
132
151
|
loadDoc(key)
|
|
133
152
|
if (isMobile.value) mobileDrawerOpen.value = false
|
|
134
153
|
}
|
|
135
154
|
|
|
136
|
-
// 文件夹操作
|
|
137
|
-
function toggleFolder(item) { item.expanded = !item.expanded }
|
|
138
|
-
function onExpandAll() { expandAll(docsList.value) }
|
|
139
|
-
function onCollapseAll() { collapseAll(docsList.value) }
|
|
155
|
+
// ===== 文件夹操作 =====
|
|
156
|
+
function toggleFolder(item) { item.expanded = !item.expanded; saveExpandedState() }
|
|
157
|
+
function onExpandAll() { expandAll(docsList.value); saveExpandedState() }
|
|
158
|
+
function onCollapseAll() { collapseAll(docsList.value); saveExpandedState() }
|
|
140
159
|
|
|
141
|
-
|
|
160
|
+
function saveExpandedState() {
|
|
161
|
+
const expanded = []
|
|
162
|
+
function collect(items) {
|
|
163
|
+
for (const item of items) {
|
|
164
|
+
if (item.type === 'folder') {
|
|
165
|
+
if (item.expanded) expanded.push(item.key)
|
|
166
|
+
if (item.children) collect(item.children)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
collect(docsList.value)
|
|
171
|
+
sessionStorage.setItem('expandedFolders', JSON.stringify(expanded))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function restoreExpandedState() {
|
|
175
|
+
const raw = sessionStorage.getItem('expandedFolders')
|
|
176
|
+
if (!raw) return
|
|
177
|
+
try {
|
|
178
|
+
const expanded = new Set(JSON.parse(raw))
|
|
179
|
+
function apply(items) {
|
|
180
|
+
for (const item of items) {
|
|
181
|
+
if (item.type === 'folder') {
|
|
182
|
+
if (expanded.has(item.key)) item.expanded = true
|
|
183
|
+
if (item.children) apply(item.children)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
apply(docsList.value)
|
|
188
|
+
} catch { /* ignore */ }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ===== 内容区点击处理 =====
|
|
142
192
|
function handleContentClick(event, { onZoom }) {
|
|
143
193
|
const target = event.target
|
|
144
194
|
const link = target.closest('a')
|
|
@@ -148,6 +198,7 @@ export function useDocManager() {
|
|
|
148
198
|
event.preventDefault()
|
|
149
199
|
const anchor = link.dataset.anchor || ''
|
|
150
200
|
expandParents(docsList.value, docKey)
|
|
201
|
+
saveExpandedState()
|
|
151
202
|
loadDoc(docKey).then(async () => {
|
|
152
203
|
if (anchor) { await nextTick(); await waitForContentImages(); scrollToHeading(anchor) }
|
|
153
204
|
})
|
|
@@ -160,20 +211,20 @@ export function useDocManager() {
|
|
|
160
211
|
}
|
|
161
212
|
return
|
|
162
213
|
}
|
|
163
|
-
// 图片/Mermaid 放大(收集所有可放大元素,支持左右切换)
|
|
164
214
|
const isImg = target.tagName === 'IMG' && target.classList.contains('zoomable-image')
|
|
165
|
-
const mermaidEl = target.closest('.mermaid')
|
|
215
|
+
const mermaidEl = target.closest('.mermaid') || target.closest('.mermaid-svg')
|
|
166
216
|
const isMermaid = mermaidEl && mermaidEl.classList.contains('zoomable-image')
|
|
167
217
|
if (isImg || isMermaid) {
|
|
168
218
|
const container = document.querySelector('.markdown-content')
|
|
169
219
|
if (!container) return
|
|
170
|
-
// 收集所有可放大元素
|
|
171
220
|
const allZoomable = [...container.querySelectorAll('.zoomable-image')]
|
|
172
221
|
const images = allZoomable.map(el => {
|
|
173
222
|
if (el.tagName === 'IMG') {
|
|
174
223
|
return `<img src="${el.src}" alt="${el.alt || ''}" style="max-width: 100%; height: auto;" />`
|
|
175
224
|
}
|
|
176
|
-
|
|
225
|
+
const clone = el.cloneNode(true)
|
|
226
|
+
clone.querySelectorAll('.mermaid-copy-btn, .image-copy-btn').forEach(btn => btn.remove())
|
|
227
|
+
return clone.innerHTML
|
|
177
228
|
})
|
|
178
229
|
const clickedEl = isImg ? target : mermaidEl
|
|
179
230
|
const index = allZoomable.indexOf(clickedEl)
|
|
@@ -181,14 +232,13 @@ export function useDocManager() {
|
|
|
181
232
|
}
|
|
182
233
|
}
|
|
183
234
|
|
|
184
|
-
//
|
|
235
|
+
// ===== 计算属性 =====
|
|
185
236
|
const currentDocTitle = computed(() => {
|
|
186
237
|
if (!currentDoc.value) return '文档'
|
|
187
238
|
const doc = findDoc(docsList.value, currentDoc.value)
|
|
188
239
|
return doc?.label || '文档'
|
|
189
240
|
})
|
|
190
241
|
|
|
191
|
-
// 上一篇/下一篇
|
|
192
242
|
const prevDoc = computed(() => {
|
|
193
243
|
if (!currentDoc.value) return null
|
|
194
244
|
const flat = flattenDocsList(docsList.value)
|
|
@@ -203,26 +253,139 @@ export function useDocManager() {
|
|
|
203
253
|
return idx >= 0 && idx < flat.length - 1 ? flat[idx + 1] : null
|
|
204
254
|
})
|
|
205
255
|
|
|
206
|
-
// 搜索结果选中
|
|
207
256
|
function handleSearchSelect(key) {
|
|
208
257
|
expandParents(docsList.value, key)
|
|
258
|
+
saveExpandedState()
|
|
209
259
|
loadDoc(key)
|
|
210
260
|
}
|
|
211
261
|
|
|
212
|
-
//
|
|
262
|
+
// ===== 编辑模式 =====
|
|
263
|
+
function toggleEditMode() {
|
|
264
|
+
editMode.value = !editMode.value
|
|
265
|
+
sessionStorage.setItem('editMode', editMode.value)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
watch(editMode, async (newVal, oldVal) => {
|
|
269
|
+
if (oldVal && !newVal && rawMarkdown.value && currentDoc.value) {
|
|
270
|
+
await renderMarkdown(rawMarkdown.value, currentDoc.value, docsList.value)
|
|
271
|
+
}
|
|
272
|
+
if (newVal && rawMarkdown.value) {
|
|
273
|
+
extractTOCFromMarkdown(rawMarkdown.value, tocItems)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
watch(rawMarkdown, (md) => {
|
|
278
|
+
if (editMode.value && md) {
|
|
279
|
+
extractTOCFromMarkdown(md, tocItems)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// ===== 轮询回调(供 useFileWatcher 调用) =====
|
|
284
|
+
|
|
285
|
+
// 树结构指纹,快速判断是否有变化
|
|
286
|
+
function treeFingerprint(items) {
|
|
287
|
+
const parts = []
|
|
288
|
+
for (const item of items) {
|
|
289
|
+
parts.push(item.key)
|
|
290
|
+
if (item.type === 'folder' && item.children) {
|
|
291
|
+
parts.push('{', treeFingerprint(item.children), '}')
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return parts.join(',')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getExpandedKeys(items) {
|
|
298
|
+
const keys = new Set()
|
|
299
|
+
for (const item of items) {
|
|
300
|
+
if (item.type === 'folder') {
|
|
301
|
+
if (item.expanded) keys.add(item.key)
|
|
302
|
+
if (item.children) for (const k of getExpandedKeys(item.children)) keys.add(k)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return keys
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function applyExpandedState(items, expandedKeys) {
|
|
309
|
+
for (const item of items) {
|
|
310
|
+
if (item.type === 'folder') {
|
|
311
|
+
if (expandedKeys.has(item.key)) item.expanded = true
|
|
312
|
+
if (item.children) applyExpandedState(item.children, expandedKeys)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 刷新文档列表(轮询回调 或 手动调用)
|
|
318
|
+
async function reloadDocsList(newTree) {
|
|
319
|
+
if (!newTree) {
|
|
320
|
+
// 手动调用(创建/删除/重命名后),强制拉取最新
|
|
321
|
+
docService.resetListEtag()
|
|
322
|
+
newTree = await docService.fetchDocsList()
|
|
323
|
+
}
|
|
324
|
+
// 结构没变就跳过
|
|
325
|
+
if (docsList.value.length > 0 && treeFingerprint(docsList.value) === treeFingerprint(newTree)) {
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
// 保存展开状态 → 替换 → 恢复
|
|
329
|
+
const expandedKeys = getExpandedKeys(docsList.value)
|
|
330
|
+
docsList.value = newTree
|
|
331
|
+
applyExpandedState(docsList.value, expandedKeys)
|
|
332
|
+
if (currentDoc.value) expandParents(docsList.value, currentDoc.value)
|
|
333
|
+
buildIndex(docsList.value)
|
|
334
|
+
saveExpandedState()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 刷新当前文档内容(轮询回调)
|
|
338
|
+
let lastContentHash = ''
|
|
339
|
+
async function reloadCurrentDoc(content) {
|
|
340
|
+
if (!currentDoc.value) return
|
|
341
|
+
// 哈希比对,内容没变就跳过
|
|
342
|
+
let hash = 0
|
|
343
|
+
for (let i = 0; i < content.length; i++) {
|
|
344
|
+
hash = ((hash << 5) - hash + content.charCodeAt(i)) | 0
|
|
345
|
+
}
|
|
346
|
+
const hashStr = String(hash)
|
|
347
|
+
if (hashStr === lastContentHash) return
|
|
348
|
+
lastContentHash = hashStr
|
|
349
|
+
rawMarkdown.value = content
|
|
350
|
+
// 编辑模式下只更新 rawMarkdown(编辑器组件会自行比对内容决定是否刷新)
|
|
351
|
+
if (editMode.value) return
|
|
352
|
+
await renderMarkdown(content, currentDoc.value, docsList.value)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ===== 写操作 =====
|
|
356
|
+
async function saveDocContent({ path: filePath, content }) {
|
|
357
|
+
try {
|
|
358
|
+
const ok = await docService.saveDoc(filePath, content)
|
|
359
|
+
if (ok) {
|
|
360
|
+
rawMarkdown.value = content
|
|
361
|
+
if (!editMode.value) {
|
|
362
|
+
await renderMarkdown(content, currentDoc.value, docsList.value)
|
|
363
|
+
}
|
|
364
|
+
return true
|
|
365
|
+
}
|
|
366
|
+
return false
|
|
367
|
+
} catch (e) {
|
|
368
|
+
console.error('保存失败:', e)
|
|
369
|
+
return false
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function getCurrentDocPath() {
|
|
374
|
+
return currentDocFilePath.value
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ===== URL 路由 =====
|
|
213
378
|
async function loadFromUrl() {
|
|
214
379
|
const pathname = window.location.pathname.replace(/^\//, '')
|
|
215
380
|
const anchor = window.location.hash.replace('#', '')
|
|
216
381
|
const savedScroll = history.state?.scrollTop
|
|
217
382
|
if (!pathname) {
|
|
218
383
|
if (currentDoc.value) {
|
|
219
|
-
// 浏览器回退到首页,不操作 history
|
|
220
384
|
goHome({ isPopstate: true })
|
|
221
385
|
} else if (docsList.value.length === 0) {
|
|
222
386
|
showWelcome.value = false
|
|
223
387
|
renderMarkdown('# 当前目录没有 Markdown 文档\n\n请在当前目录下添加 `.md` 文件,然后刷新页面。')
|
|
224
388
|
} else {
|
|
225
|
-
// 优先定位到 README,没有则定位到第一篇文档
|
|
226
389
|
const readme = findReadmeDoc(docsList.value)
|
|
227
390
|
const target = readme || findFirstDoc(docsList.value)
|
|
228
391
|
if (target) {
|
|
@@ -234,11 +397,9 @@ export function useDocManager() {
|
|
|
234
397
|
}
|
|
235
398
|
const doc = findDocByHash(docsList.value, pathname, docHash)
|
|
236
399
|
if (!doc) return
|
|
237
|
-
// 同一文档内的锚点变化(含回退)
|
|
238
400
|
if (doc.key === currentDoc.value) {
|
|
239
401
|
await nextTick()
|
|
240
402
|
if (savedScroll != null) {
|
|
241
|
-
// 有保存的滚动位置,直接恢复(回退场景)
|
|
242
403
|
const contentEl = document.querySelector('.content')
|
|
243
404
|
if (contentEl) contentEl.scrollTo({ top: savedScroll, behavior: 'smooth' })
|
|
244
405
|
} else if (anchor) {
|
|
@@ -250,6 +411,7 @@ export function useDocManager() {
|
|
|
250
411
|
return
|
|
251
412
|
}
|
|
252
413
|
expandParents(docsList.value, doc.key)
|
|
414
|
+
saveExpandedState()
|
|
253
415
|
await loadDoc(doc.key, { replace: true, keepState: savedScroll != null, anchor: anchor ? decodeURIComponent(anchor) : '' })
|
|
254
416
|
if (savedScroll != null) {
|
|
255
417
|
await nextTick()
|
|
@@ -260,38 +422,133 @@ export function useDocManager() {
|
|
|
260
422
|
}
|
|
261
423
|
}
|
|
262
424
|
|
|
425
|
+
// ===== 文档管理操作 =====
|
|
426
|
+
async function createDoc({ parentKey, name, type }) {
|
|
427
|
+
const relativePath = parentKey ? `${parentKey}/${name}` : name
|
|
428
|
+
const apiPath = type === 'file' ? `${relativePath}.md` : relativePath
|
|
429
|
+
try {
|
|
430
|
+
await docService.createDoc(apiPath, type)
|
|
431
|
+
await reloadDocsList()
|
|
432
|
+
if (parentKey) expandParents(docsList.value, relativePath)
|
|
433
|
+
if (type === 'file') await loadDoc(relativePath)
|
|
434
|
+
return { ok: true }
|
|
435
|
+
} catch (e) {
|
|
436
|
+
return { ok: false, message: e.message }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function renameDoc(item, newName) {
|
|
441
|
+
const parts = item.key.split('/')
|
|
442
|
+
parts[parts.length - 1] = newName
|
|
443
|
+
const newKey = parts.join('/')
|
|
444
|
+
const oldPath = item.type === 'file' ? `${item.key}.md` : item.key
|
|
445
|
+
const newPath = item.type === 'file' ? `${newKey}.md` : newKey
|
|
446
|
+
try {
|
|
447
|
+
await docService.renameDoc(oldPath, newPath)
|
|
448
|
+
const wasCurrentDoc = item.type === 'file' && item.key === currentDoc.value
|
|
449
|
+
const wasInFolder = item.type === 'folder' && currentDoc.value.startsWith(item.key + '/')
|
|
450
|
+
await reloadDocsList()
|
|
451
|
+
if (wasCurrentDoc) {
|
|
452
|
+
expandParents(docsList.value, newKey)
|
|
453
|
+
await loadDoc(newKey, { replace: true })
|
|
454
|
+
}
|
|
455
|
+
if (wasInFolder) {
|
|
456
|
+
const newDocKey = currentDoc.value.replace(item.key + '/', newKey + '/')
|
|
457
|
+
expandParents(docsList.value, newDocKey)
|
|
458
|
+
await loadDoc(newDocKey, { replace: true })
|
|
459
|
+
}
|
|
460
|
+
return { ok: true }
|
|
461
|
+
} catch (e) {
|
|
462
|
+
return { ok: false, message: e.message }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function deleteDoc(item) {
|
|
467
|
+
const apiPath = item.type === 'file' ? `${item.key}.md` : item.key
|
|
468
|
+
try {
|
|
469
|
+
await docService.deleteDoc(apiPath)
|
|
470
|
+
if (item.type === 'file' && item.key === currentDoc.value) goHome()
|
|
471
|
+
if (item.type === 'folder' && currentDoc.value.startsWith(item.key + '/')) goHome()
|
|
472
|
+
await reloadDocsList()
|
|
473
|
+
return { ok: true }
|
|
474
|
+
} catch (e) {
|
|
475
|
+
return { ok: false, message: e.message }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ===== 拖拽排序 =====
|
|
480
|
+
function calcPadWidth(count) { return count < 100 ? 2 : String(count - 1).length }
|
|
481
|
+
function orderPrefix(index, padWidth) { return String(index).padStart(padWidth, '0') }
|
|
482
|
+
function stripPrefix(name) { return name.replace(/^\d+-/, '') }
|
|
483
|
+
|
|
484
|
+
function collectReorderItems(items, parentPath = '') {
|
|
485
|
+
const result = []
|
|
486
|
+
const padWidth = calcPadWidth(items.length)
|
|
487
|
+
items.forEach((item, index) => {
|
|
488
|
+
const prefix = orderPrefix(index, padWidth)
|
|
489
|
+
const pureName = stripPrefix(item.label || item.key.split('/').pop())
|
|
490
|
+
const newName = `${prefix}-${pureName}`
|
|
491
|
+
const oldName = item.key.split('/').pop()
|
|
492
|
+
const oldFsName = item.type === 'file' ? `${oldName}.md` : oldName
|
|
493
|
+
const newFsName = item.type === 'file' ? `${newName}.md` : newName
|
|
494
|
+
const oldPath = parentPath ? `${parentPath}/${oldFsName}` : oldFsName
|
|
495
|
+
const newPath = parentPath ? `${parentPath}/${newFsName}` : newFsName
|
|
496
|
+
if (oldPath !== newPath) result.push({ oldPath, newPath })
|
|
497
|
+
})
|
|
498
|
+
return result
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function collectAllReorderLevels(items, parentPath = '') {
|
|
502
|
+
const levels = []
|
|
503
|
+
items.forEach((item) => {
|
|
504
|
+
if (item.type === 'folder' && item.children) {
|
|
505
|
+
const folderPath = parentPath ? `${parentPath}/${item.key.split('/').pop()}` : item.key.split('/').pop()
|
|
506
|
+
levels.push(...collectAllReorderLevels(item.children, folderPath))
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
const currentLevelItems = collectReorderItems(items, parentPath)
|
|
510
|
+
if (currentLevelItems.length > 0) levels.push(currentLevelItems)
|
|
511
|
+
return levels
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function reorderDocs() {
|
|
515
|
+
const levels = collectAllReorderLevels(docsList.value)
|
|
516
|
+
if (levels.length === 0) return { ok: true }
|
|
517
|
+
const currentPureName = currentDoc.value ? stripOrderPrefix(currentDoc.value) : ''
|
|
518
|
+
try {
|
|
519
|
+
for (const levelItems of levels) {
|
|
520
|
+
if (levelItems.length === 0) continue
|
|
521
|
+
await docService.reorderDocs(levelItems)
|
|
522
|
+
}
|
|
523
|
+
await reloadDocsList()
|
|
524
|
+
if (currentPureName) {
|
|
525
|
+
const flat = flattenDocsList(docsList.value)
|
|
526
|
+
const match = flat.find(d => stripOrderPrefix(d.key) === currentPureName)
|
|
527
|
+
if (match) {
|
|
528
|
+
expandParents(docsList.value, match.key)
|
|
529
|
+
await loadDoc(match.key, { replace: true })
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { ok: true }
|
|
533
|
+
} catch (e) {
|
|
534
|
+
console.error('重编号失败:', e)
|
|
535
|
+
return { ok: false, message: e.message }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ===== 导出 =====
|
|
263
540
|
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
handleScroll,
|
|
276
|
-
scrollToHeading,
|
|
277
|
-
scrollToTop,
|
|
278
|
-
// 文档操作
|
|
279
|
-
loadDocsList,
|
|
280
|
-
loadFromUrl,
|
|
281
|
-
goHome,
|
|
282
|
-
loadDoc,
|
|
283
|
-
loadFirstDoc,
|
|
284
|
-
handleDocSelect,
|
|
285
|
-
handleContentClick,
|
|
286
|
-
handleSearchSelect,
|
|
287
|
-
// 文件夹操作
|
|
288
|
-
toggleFolder,
|
|
289
|
-
onExpandAll,
|
|
290
|
-
onCollapseAll,
|
|
291
|
-
// 导航
|
|
292
|
-
prevDoc,
|
|
293
|
-
nextDoc,
|
|
294
|
-
// 工具
|
|
541
|
+
docsList, currentDoc, currentDocTitle, showWelcome, htmlContent, tocItems,
|
|
542
|
+
editMode, rawMarkdown, currentDocFilePath, lastModified,
|
|
543
|
+
scrollProgress, showBackToTop, activeHeading,
|
|
544
|
+
handleScroll, scrollToHeading, scrollToTop,
|
|
545
|
+
loadDocsList, loadFromUrl, goHome, loadDoc, loadFirstDoc,
|
|
546
|
+
handleDocSelect, handleContentClick, handleSearchSelect,
|
|
547
|
+
toggleEditMode, reloadDocsList, reloadCurrentDoc,
|
|
548
|
+
saveDoc: saveDocContent, getCurrentDocPath,
|
|
549
|
+
toggleFolder, onExpandAll, onCollapseAll,
|
|
550
|
+
prevDoc, nextDoc,
|
|
551
|
+
createDoc, deleteDoc, renameDoc, reorderDocs,
|
|
295
552
|
docHash
|
|
296
553
|
}
|
|
297
554
|
}
|
|
@@ -55,7 +55,11 @@ export function findDocByHash(items, hash, docHash) {
|
|
|
55
55
|
// 展开文档所在的所有父级文件夹
|
|
56
56
|
export function expandParents(items, targetKey) {
|
|
57
57
|
for (const item of items) {
|
|
58
|
-
if (item.
|
|
58
|
+
if (item.key === targetKey) {
|
|
59
|
+
// 如果目标本身是文件夹,也展开它
|
|
60
|
+
if (item.type === 'folder') item.expanded = true
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
59
63
|
if (item.type === 'folder' && item.children) {
|
|
60
64
|
if (expandParents(item.children, targetKey)) {
|
|
61
65
|
item.expanded = true
|
|
@@ -94,3 +98,54 @@ export function collapseAll(items) {
|
|
|
94
98
|
}
|
|
95
99
|
})
|
|
96
100
|
}
|
|
101
|
+
|
|
102
|
+
// 查找节点的父级列表(用于插入/删除操作)
|
|
103
|
+
// 返回 { parent: 父节点children数组, index: 节点在数组中的索引 }
|
|
104
|
+
export function findParent(items, targetKey) {
|
|
105
|
+
for (let i = 0; i < items.length; i++) {
|
|
106
|
+
if (items[i].key === targetKey) {
|
|
107
|
+
return { parent: items, index: i }
|
|
108
|
+
}
|
|
109
|
+
if (items[i].type === 'folder' && items[i].children) {
|
|
110
|
+
const found = findParent(items[i].children, targetKey)
|
|
111
|
+
if (found) return found
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 查找文件夹节点
|
|
118
|
+
export function findFolder(items, key) {
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
if (item.type === 'folder' && item.key === key) return item
|
|
121
|
+
if (item.type === 'folder' && item.children) {
|
|
122
|
+
const found = findFolder(item.children, key)
|
|
123
|
+
if (found) return found
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 收集所有已展开文件夹的 key
|
|
130
|
+
export function collectExpandedKeys(items) {
|
|
131
|
+
const keys = new Set()
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
if (item.type === 'folder') {
|
|
134
|
+
if (item.expanded) keys.add(item.key)
|
|
135
|
+
if (item.children) {
|
|
136
|
+
for (const k of collectExpandedKeys(item.children)) keys.add(k)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return keys
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 根据 key 集合恢复展开状态
|
|
144
|
+
export function restoreExpandedKeys(items, keys) {
|
|
145
|
+
for (const item of items) {
|
|
146
|
+
if (item.type === 'folder') {
|
|
147
|
+
if (keys.has(item.key)) item.expanded = true
|
|
148
|
+
if (item.children) restoreExpandedKeys(item.children, keys)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|