md2ui 1.0.8 → 1.0.9
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/build.js +3 -4
- package/package.json +1 -1
- package/src/App.vue +2 -2
- package/src/composables/useDocManager.js +59 -18
- package/src/composables/useDocTree.js +16 -0
- package/src/style.css +4 -4
package/bin/build.js
CHANGED
|
@@ -232,7 +232,7 @@ async function renderMarkdownToHtml(markdown, currentDocKey, docsList) {
|
|
|
232
232
|
|
|
233
233
|
// 纯锚点
|
|
234
234
|
if (decoded.startsWith('#')) {
|
|
235
|
-
const anchor =
|
|
235
|
+
const anchor = decoded.slice(1)
|
|
236
236
|
return `<a href="#${anchor}"${titleAttr}>${text}</a>`
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -243,9 +243,8 @@ async function renderMarkdownToHtml(markdown, currentDocKey, docsList) {
|
|
|
243
243
|
const doc = findDocInTree(docsList, targetKey)
|
|
244
244
|
if (doc) {
|
|
245
245
|
const hash = docHash(doc.key)
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
return `<a href="${url}" data-doc-key="${doc.key}"${anchorSlug ? ` data-anchor="${anchorSlug}"` : ''}${titleAttr}>${text}</a>`
|
|
246
|
+
const url = anchor ? `/${hash}.html#${anchor}` : `/${hash}.html`
|
|
247
|
+
return `<a href="${url}" data-doc-key="${doc.key}"${anchor ? ` data-anchor="${anchor}"` : ''}${titleAttr}>${text}</a>`
|
|
249
248
|
}
|
|
250
249
|
return `<a href="javascript:void(0)" class="broken-link" title="文档未找到: ${decoded}">${text}</a>`
|
|
251
250
|
}
|
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
/>
|
|
54
54
|
<!-- 桌面端 TOC -->
|
|
55
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" />
|
|
56
|
+
<TableOfContents v-if="!isMobile" :tocItems="tocItems" :activeHeading="activeHeading" :collapsed="tocCollapsed" :width="tocWidth" @toggle="tocCollapsed = !tocCollapsed" @scroll-to="(id) => scrollToHeading(id, { push: true })" />
|
|
57
57
|
<button v-if="!isMobile && tocCollapsed && tocItems.length > 0" class="expand-btn expand-btn-right" @click="tocCollapsed = false" title="展开目录">
|
|
58
58
|
<ChevronLeft :size="14" />
|
|
59
59
|
</button>
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
:showWelcome="showWelcome"
|
|
68
68
|
@toggle="mobileTocOpen = !mobileTocOpen"
|
|
69
69
|
@close="mobileTocOpen = false"
|
|
70
|
-
@scroll-to="(id) => { scrollToHeading(id); mobileTocOpen = false }"
|
|
70
|
+
@scroll-to="(id) => { scrollToHeading(id, { push: true }); mobileTocOpen = false }"
|
|
71
71
|
/>
|
|
72
72
|
<!-- 返回顶部 -->
|
|
73
73
|
<transition name="fade">
|
|
@@ -4,7 +4,7 @@ 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
|
-
import { findDoc, findFirstDoc, findDocByHash, expandParents, flattenDocsList, expandAll, collapseAll } from './useDocTree.js'
|
|
7
|
+
import { findDoc, findFirstDoc, findReadmeDoc, findDocByHash, expandParents, flattenDocsList, expandAll, collapseAll } from './useDocTree.js'
|
|
8
8
|
|
|
9
9
|
// 等待内容区域图片加载完成
|
|
10
10
|
async function waitForContentImages(timeoutMs = 3000) {
|
|
@@ -36,23 +36,38 @@ export function useDocManager() {
|
|
|
36
36
|
} = useScroll()
|
|
37
37
|
const { isMobile, mobileDrawerOpen } = useMobile()
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// 获取当前滚动位置
|
|
40
|
+
function getScrollTop() {
|
|
41
|
+
const el = document.querySelector('.content')
|
|
42
|
+
return el ? el.scrollTop : 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 构造 history state,保留滚动位置
|
|
46
|
+
function makeState(scrollTop) {
|
|
47
|
+
return { scrollTop: scrollTop ?? getScrollTop() }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 滚动处理,同步锚点到 URL(replaceState 保留滚动位置)
|
|
40
51
|
function handleScroll(e) {
|
|
41
52
|
_handleScroll(e)
|
|
42
53
|
if (activeHeading.value && currentDoc.value) {
|
|
43
|
-
history.replaceState(
|
|
54
|
+
history.replaceState(makeState(), '', `/${docHash(currentDoc.value)}#${activeHeading.value}`)
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
// push: true 表示用户主动点击锚点,产生可回退的历史条目
|
|
48
59
|
function scrollToHeading(id, { push = false } = {}) {
|
|
60
|
+
if (push && currentDoc.value) {
|
|
61
|
+
// 先把当前滚动位置写入即将被覆盖的历史条目
|
|
62
|
+
history.replaceState(makeState(), '', window.location.href)
|
|
63
|
+
}
|
|
49
64
|
_scrollToHeading(id)
|
|
50
65
|
if (currentDoc.value) {
|
|
51
66
|
const url = `/${docHash(currentDoc.value)}#${id}`
|
|
52
67
|
if (push) {
|
|
53
|
-
history.pushState(
|
|
68
|
+
history.pushState(makeState(), '', url)
|
|
54
69
|
} else {
|
|
55
|
-
history.replaceState(
|
|
70
|
+
history.replaceState(makeState(), '', url)
|
|
56
71
|
}
|
|
57
72
|
}
|
|
58
73
|
}
|
|
@@ -63,26 +78,33 @@ export function useDocManager() {
|
|
|
63
78
|
buildIndex(docsList.value)
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
//
|
|
67
|
-
function goHome() {
|
|
81
|
+
// 回到欢迎页(isPopstate: true 表示由浏览器回退触发,不操作 history)
|
|
82
|
+
function goHome({ isPopstate = false } = {}) {
|
|
68
83
|
currentDoc.value = ''
|
|
69
84
|
showWelcome.value = true
|
|
70
85
|
htmlContent.value = ''
|
|
71
86
|
tocItems.value = []
|
|
72
|
-
|
|
87
|
+
if (!isPopstate) {
|
|
88
|
+
// 用户主动点击,保存旧条目滚动位置后 push
|
|
89
|
+
history.replaceState(makeState(), '', window.location.href)
|
|
90
|
+
history.pushState(makeState(0), '', '/')
|
|
91
|
+
}
|
|
73
92
|
if (isMobile.value) mobileDrawerOpen.value = false
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
// 加载文档
|
|
77
|
-
async function loadDoc(key, { replace = false, anchor = '' } = {}) {
|
|
96
|
+
async function loadDoc(key, { replace = false, anchor = '', keepState = false } = {}) {
|
|
78
97
|
currentDoc.value = key
|
|
79
98
|
showWelcome.value = false
|
|
80
99
|
const hash = docHash(key)
|
|
81
100
|
const url = anchor ? `/${hash}#${anchor}` : `/${hash}`
|
|
82
101
|
if (replace) {
|
|
83
|
-
|
|
102
|
+
// keepState: popstate 回退时保留浏览器已有的 state(含 scrollTop)
|
|
103
|
+
if (!keepState) history.replaceState(makeState(0), '', url)
|
|
84
104
|
} else {
|
|
85
|
-
|
|
105
|
+
// push 前先保存当前滚动位置到旧条目
|
|
106
|
+
history.replaceState(makeState(), '', window.location.href)
|
|
107
|
+
history.pushState(makeState(0), '', url)
|
|
86
108
|
}
|
|
87
109
|
const doc = findDoc(docsList.value, key)
|
|
88
110
|
if (!doc) return
|
|
@@ -175,22 +197,35 @@ export function useDocManager() {
|
|
|
175
197
|
async function loadFromUrl() {
|
|
176
198
|
const pathname = window.location.pathname.replace(/^\//, '')
|
|
177
199
|
const anchor = window.location.hash.replace('#', '')
|
|
200
|
+
const savedScroll = history.state?.scrollTop
|
|
178
201
|
if (!pathname) {
|
|
179
202
|
if (currentDoc.value) {
|
|
180
|
-
//
|
|
181
|
-
goHome()
|
|
203
|
+
// 浏览器回退到首页,不操作 history
|
|
204
|
+
goHome({ isPopstate: true })
|
|
182
205
|
} else if (docsList.value.length === 0) {
|
|
183
206
|
showWelcome.value = false
|
|
184
207
|
renderMarkdown('# 当前目录没有 Markdown 文档\n\n请在当前目录下添加 `.md` 文件,然后刷新页面。')
|
|
208
|
+
} else {
|
|
209
|
+
// 优先定位到 README,没有则定位到第一篇文档
|
|
210
|
+
const readme = findReadmeDoc(docsList.value)
|
|
211
|
+
const target = readme || findFirstDoc(docsList.value)
|
|
212
|
+
if (target) {
|
|
213
|
+
expandParents(docsList.value, target.key)
|
|
214
|
+
await loadDoc(target.key, { replace: true })
|
|
215
|
+
}
|
|
185
216
|
}
|
|
186
217
|
return
|
|
187
218
|
}
|
|
188
219
|
const doc = findDocByHash(docsList.value, pathname, docHash)
|
|
189
220
|
if (!doc) return
|
|
190
|
-
//
|
|
221
|
+
// 同一文档内的锚点变化(含回退)
|
|
191
222
|
if (doc.key === currentDoc.value) {
|
|
192
|
-
|
|
193
|
-
|
|
223
|
+
await nextTick()
|
|
224
|
+
if (savedScroll != null) {
|
|
225
|
+
// 有保存的滚动位置,直接恢复(回退场景)
|
|
226
|
+
const contentEl = document.querySelector('.content')
|
|
227
|
+
if (contentEl) contentEl.scrollTo({ top: savedScroll, behavior: 'smooth' })
|
|
228
|
+
} else if (anchor) {
|
|
194
229
|
_scrollToHeading(decodeURIComponent(anchor))
|
|
195
230
|
} else {
|
|
196
231
|
const contentEl = document.querySelector('.content')
|
|
@@ -199,8 +234,14 @@ export function useDocManager() {
|
|
|
199
234
|
return
|
|
200
235
|
}
|
|
201
236
|
expandParents(docsList.value, doc.key)
|
|
202
|
-
await loadDoc(doc.key, { replace: true, anchor: anchor ? decodeURIComponent(anchor) : '' })
|
|
203
|
-
if (
|
|
237
|
+
await loadDoc(doc.key, { replace: true, keepState: savedScroll != null, anchor: anchor ? decodeURIComponent(anchor) : '' })
|
|
238
|
+
if (savedScroll != null) {
|
|
239
|
+
await nextTick()
|
|
240
|
+
const contentEl = document.querySelector('.content')
|
|
241
|
+
if (contentEl) contentEl.scrollTo({ top: savedScroll })
|
|
242
|
+
} else if (anchor) {
|
|
243
|
+
await nextTick(); await waitForContentImages(); _scrollToHeading(decodeURIComponent(anchor))
|
|
244
|
+
}
|
|
204
245
|
}
|
|
205
246
|
|
|
206
247
|
return {
|
|
@@ -24,6 +24,22 @@ export function findFirstDoc(items) {
|
|
|
24
24
|
return null
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// 查找 README 文档(不区分大小写,优先根目录)
|
|
28
|
+
export function findReadmeDoc(items) {
|
|
29
|
+
// 先在当前层级找
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
if (item.type === 'file' && /^readme$/i.test(item.key?.split('/').pop() || '')) return item
|
|
32
|
+
}
|
|
33
|
+
// 再递归子目录找
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
if (item.type === 'folder' && item.children) {
|
|
36
|
+
const found = findReadmeDoc(item.children)
|
|
37
|
+
if (found) return found
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
// 根据 hash 查找文档
|
|
28
44
|
export function findDocByHash(items, hash, docHash) {
|
|
29
45
|
for (const item of items) {
|
package/src/style.css
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
--color-text: #1a1e1b;
|
|
8
8
|
--color-text-secondary: #505856;
|
|
9
9
|
--color-text-tertiary: #848e89;
|
|
10
|
-
--color-border:
|
|
11
|
-
--color-border-light:
|
|
10
|
+
--color-border: rgba(66, 184, 131, 0.25);
|
|
11
|
+
--color-border-light: rgba(66, 184, 131, 0.15);
|
|
12
12
|
--color-accent: #42b883;
|
|
13
13
|
--color-accent-hover: #359d6e;
|
|
14
14
|
--color-accent-bg: rgba(66, 184, 131, 0.1);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
--color-code-border: transparent;
|
|
19
19
|
--color-pre-bg: #ffffff;
|
|
20
20
|
--color-pre-header: #f7f9f8;
|
|
21
|
-
--color-pre-border:
|
|
21
|
+
--color-pre-border: rgba(66, 184, 131, 0.25);
|
|
22
22
|
--color-pre-text: #1a1e1b;
|
|
23
23
|
--color-blockquote-border: #42b883;
|
|
24
24
|
--color-blockquote-bg: rgba(66, 184, 131, 0.03);
|
|
@@ -1074,7 +1074,7 @@ body {
|
|
|
1074
1074
|
align-items: center;
|
|
1075
1075
|
padding: 8px 16px;
|
|
1076
1076
|
background: var(--color-bg);
|
|
1077
|
-
border-bottom:
|
|
1077
|
+
border-bottom: 1px solid var(--color-border);
|
|
1078
1078
|
flex-shrink: 0;
|
|
1079
1079
|
gap: 12px;
|
|
1080
1080
|
z-index: 100;
|