md2ui 1.0.19 → 1.0.21
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 +65 -20
- package/bin/build.js +13 -2
- package/bin/md2ui.js +25 -12
- package/package.json +4 -4
- 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 +2 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -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 +4 -4
- package/src/App.vue +36 -61
- package/src/components/ImageZoom.vue +9 -123
- package/src/components/MermaidNodeView.vue +10 -2
- package/src/components/MobileSearch.vue +97 -0
- package/src/components/TableOfContents.vue +42 -6
- package/src/composables/useDocManager.js +134 -44
- package/src/composables/useDocTree.js +26 -50
- package/src/composables/useMarkdown.js +51 -140
- package/src/composables/useMermaidCache.js +15 -0
- package/src/composables/useScroll.js +317 -32
- package/src/composables/useSearch.js +12 -11
- package/src/config.js +1 -4
- package/src/services/DocService.js +0 -16
- package/src/style.css +235 -10
- package/src/utils/imageConverter.js +129 -0
- package/vite-plugin-doc-api.js +158 -157
- package/vite.config.js +5 -1
- package/src/components/SearchPanel.vue +0 -90
- package/src/components/TableBubbleMenu.vue +0 -177
- package/src/composables/useExportPdf.js +0 -102
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ref } from 'vue'
|
|
2
2
|
|
|
3
|
+
// ===== 常量 =====
|
|
4
|
+
// smooth 滚动锁定超时(scrollend 事件优先解锁,此值仅作兜底)
|
|
5
|
+
// 低性能设备或长文档上 smooth 滚动可能超过 800ms,拉长到 1500ms 确保安全
|
|
6
|
+
const SCROLL_LOCK_TIMEOUT_MS = 1500
|
|
7
|
+
|
|
3
8
|
// 单例状态,确保多处调用共享同一份
|
|
4
9
|
const scrollProgress = ref(0)
|
|
5
10
|
const showBackToTop = ref(false)
|
|
@@ -8,6 +13,24 @@ const activeHeading = ref('')
|
|
|
8
13
|
// 外部注入的 tocItems 引用,用于编辑模式下通过文本匹配标题
|
|
9
14
|
let _tocItemsRef = null
|
|
10
15
|
|
|
16
|
+
// 点击目录项后锁定,防止滚动检测覆盖 activeHeading 导致闪动
|
|
17
|
+
let _locked = false
|
|
18
|
+
let _lockTimer = null
|
|
19
|
+
|
|
20
|
+
// 标题元素缓存
|
|
21
|
+
let _headingsCache = null
|
|
22
|
+
|
|
23
|
+
// MutationObserver:监听 .markdown-content 子树变化,自动失效标题缓存
|
|
24
|
+
let _mutationObserver = null
|
|
25
|
+
|
|
26
|
+
// 标记:true 表示 activeHeading 被文档切换清空,不应同步到 URL
|
|
27
|
+
let _suppressHashClear = false
|
|
28
|
+
|
|
29
|
+
// IntersectionObserver 实例
|
|
30
|
+
let _observer = null
|
|
31
|
+
// 记录当前在视口中的标题(id → IntersectionObserverEntry)
|
|
32
|
+
const _visibleHeadings = new Map()
|
|
33
|
+
|
|
11
34
|
export function useScroll() {
|
|
12
35
|
|
|
13
36
|
// 注入 tocItems 引用(由 useDocManager 调用一次)
|
|
@@ -15,18 +38,246 @@ export function useScroll() {
|
|
|
15
38
|
_tocItemsRef = tocItems
|
|
16
39
|
}
|
|
17
40
|
|
|
18
|
-
//
|
|
41
|
+
// 重建标题缓存并重新设置 IntersectionObserver
|
|
42
|
+
let _rebuildRetryTimer = null
|
|
43
|
+
function rebuildHeadingsCache() {
|
|
44
|
+
if (_rebuildRetryTimer) { clearTimeout(_rebuildRetryTimer); _rebuildRetryTimer = null }
|
|
45
|
+
_headingsCache = null
|
|
46
|
+
_setupMutationObserver()
|
|
47
|
+
_setupObserver()
|
|
48
|
+
// 如果 tocItems 有内容但 DOM 中没查到标题(编辑器还没渲染完),延迟重试
|
|
49
|
+
const headings = _headingsCache || []
|
|
50
|
+
const hasTocItems = _tocItemsRef && _tocItemsRef.value && _tocItemsRef.value.length > 0
|
|
51
|
+
if (headings.length === 0 && hasTocItems) {
|
|
52
|
+
_rebuildRetryTimer = setTimeout(() => {
|
|
53
|
+
_headingsCache = null
|
|
54
|
+
_setupMutationObserver()
|
|
55
|
+
_setupObserver()
|
|
56
|
+
}, 200)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 设置 MutationObserver 监听内容区子树变化,自动失效标题缓存
|
|
61
|
+
let _mutationDebounce = null
|
|
62
|
+
function _setupMutationObserver() {
|
|
63
|
+
if (_mutationObserver) {
|
|
64
|
+
_mutationObserver.disconnect()
|
|
65
|
+
_mutationObserver = null
|
|
66
|
+
}
|
|
67
|
+
// 监听 .content 容器,兼容预览模式和编辑模式(编辑模式下可能有多个 .markdown-content)
|
|
68
|
+
const container = document.querySelector('.content')
|
|
69
|
+
if (!container) return
|
|
70
|
+
_mutationObserver = new MutationObserver(() => {
|
|
71
|
+
// 子树变化时失效缓存,下次 _getHeadings 会重新查询
|
|
72
|
+
_headingsCache = null
|
|
73
|
+
// 编辑模式下 tiptap 频繁修改 DOM,跳过 IntersectionObserver 重建避免抖动
|
|
74
|
+
const container = document.querySelector('.content')
|
|
75
|
+
if (container && container.classList.contains('editor-content')) return
|
|
76
|
+
// 防抖重建 IntersectionObserver,确保新增/删除的标题也能被监听
|
|
77
|
+
if (_mutationDebounce) clearTimeout(_mutationDebounce)
|
|
78
|
+
_mutationDebounce = setTimeout(() => {
|
|
79
|
+
_setupObserver()
|
|
80
|
+
}, 800)
|
|
81
|
+
})
|
|
82
|
+
_mutationObserver.observe(container, { childList: true, subtree: true })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _getHeadings() {
|
|
86
|
+
if (_headingsCache) return _headingsCache
|
|
87
|
+
const content = document.querySelector('.content')
|
|
88
|
+
if (!content) return []
|
|
89
|
+
_headingsCache = [...content.querySelectorAll(
|
|
90
|
+
'.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6'
|
|
91
|
+
)]
|
|
92
|
+
// 编辑模式下 tiptap 渲染的标题没有 id,通过 tocItems 文本匹配注入 id
|
|
93
|
+
// 使注入后的标题能直接复用预览模式的 getElementById 锚点逻辑
|
|
94
|
+
_syncHeadingIds(_headingsCache)
|
|
95
|
+
return _headingsCache
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 为缺少 id 的标题元素注入 id(基于 tocItems 文本匹配)
|
|
99
|
+
function _syncHeadingIds(headings) {
|
|
100
|
+
if (!_tocItemsRef || !_tocItemsRef.value || !_tocItemsRef.value.length) return
|
|
101
|
+
// 构建 text → id 的映射(处理重复文本:按出现顺序依次匹配)
|
|
102
|
+
const tocQueue = new Map()
|
|
103
|
+
for (const item of _tocItemsRef.value) {
|
|
104
|
+
if (!tocQueue.has(item.text)) {
|
|
105
|
+
tocQueue.set(item.text, [])
|
|
106
|
+
}
|
|
107
|
+
tocQueue.get(item.text).push(item.id)
|
|
108
|
+
}
|
|
109
|
+
for (const heading of headings) {
|
|
110
|
+
if (heading.id) continue // 已有 id,跳过
|
|
111
|
+
const text = getHeadingText(heading)
|
|
112
|
+
const ids = tocQueue.get(text)
|
|
113
|
+
if (ids && ids.length > 0) {
|
|
114
|
+
heading.id = ids.shift()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 设置 IntersectionObserver 监听标题元素
|
|
120
|
+
function _setupObserver() {
|
|
121
|
+
// 清理旧的 observer
|
|
122
|
+
if (_observer) {
|
|
123
|
+
_observer.disconnect()
|
|
124
|
+
_observer = null
|
|
125
|
+
}
|
|
126
|
+
_visibleHeadings.clear()
|
|
127
|
+
|
|
128
|
+
const content = document.querySelector('.content')
|
|
129
|
+
if (!content) return
|
|
130
|
+
|
|
131
|
+
const headings = _getHeadings()
|
|
132
|
+
if (!headings.length) return
|
|
133
|
+
|
|
134
|
+
// rootMargin: 顶部 0px,底部 -70%,即标题进入视口上方 30% 区域时触发
|
|
135
|
+
_observer = new IntersectionObserver(
|
|
136
|
+
(entries) => {
|
|
137
|
+
if (_locked) return
|
|
138
|
+
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const id = entry.target.id || findTocIdByText(getHeadingText(entry.target))
|
|
141
|
+
if (!id) continue
|
|
142
|
+
if (entry.isIntersecting) {
|
|
143
|
+
_visibleHeadings.set(id, entry)
|
|
144
|
+
} else {
|
|
145
|
+
_visibleHeadings.delete(id)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_resolveActiveHeading()
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
root: content,
|
|
153
|
+
// 上方全部可见,下方只保留 30%(裁掉底部 70%)
|
|
154
|
+
rootMargin: '0px 0px -70% 0px',
|
|
155
|
+
threshold: 0
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
for (const heading of headings) {
|
|
160
|
+
_observer.observe(heading)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 根据可见标题集合确定当前激活标题
|
|
165
|
+
// 策略:取文档顺序中最后一个进入检测区域的标题
|
|
166
|
+
function _resolveActiveHeading() {
|
|
167
|
+
if (_locked) return
|
|
168
|
+
|
|
169
|
+
const headings = _getHeadings()
|
|
170
|
+
if (!headings.length) return
|
|
171
|
+
|
|
172
|
+
const content = document.querySelector('.content')
|
|
173
|
+
|
|
174
|
+
// 边界处理:滚动到底部时,强制激活最后一个标题
|
|
175
|
+
if (content) {
|
|
176
|
+
const atBottom = content.scrollTop + content.clientHeight >= content.scrollHeight - 10
|
|
177
|
+
if (atBottom) {
|
|
178
|
+
// 从后往前找最后一个有 id 的标题
|
|
179
|
+
for (let i = headings.length - 1; i >= 0; i--) {
|
|
180
|
+
const id = headings[i].id || findTocIdByText(getHeadingText(headings[i]))
|
|
181
|
+
if (id) {
|
|
182
|
+
if (!activeHeading.value) _suppressHashClear = false
|
|
183
|
+
activeHeading.value = id
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 正常情况:从可见标题中取文档顺序最后一个
|
|
191
|
+
if (_visibleHeadings.size > 0) {
|
|
192
|
+
let lastId = ''
|
|
193
|
+
for (const heading of headings) {
|
|
194
|
+
const id = heading.id || findTocIdByText(getHeadingText(heading))
|
|
195
|
+
if (id && _visibleHeadings.has(id)) {
|
|
196
|
+
lastId = id
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (lastId) {
|
|
200
|
+
activeHeading.value = lastId
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 没有可见标题时,用传统方式兜底(滚动位置在第一个标题之前)
|
|
206
|
+
if (content && content.scrollTop < 100) {
|
|
207
|
+
if (activeHeading.value) _suppressHashClear = false
|
|
208
|
+
activeHeading.value = ''
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 编辑模式下基于滚动位置计算当前激活标题
|
|
213
|
+
// tiptap 会频繁替换 DOM 元素导致 IntersectionObserver 失效,改用传统方式
|
|
214
|
+
function _resolveActiveHeadingByScroll(container) {
|
|
215
|
+
if (_locked) return
|
|
216
|
+
// 每次都重新查询标题元素,因为 tiptap 可能已替换 DOM
|
|
217
|
+
_headingsCache = null
|
|
218
|
+
const headings = _getHeadings()
|
|
219
|
+
if (!headings.length) return
|
|
220
|
+
|
|
221
|
+
const scrollTop = container.scrollTop
|
|
222
|
+
// 检测区域:视口上方 30%(与 IO 的 rootMargin 策略一致)
|
|
223
|
+
const threshold = container.clientHeight * 0.3
|
|
224
|
+
|
|
225
|
+
// 滚动到底部时,强制激活最后一个标题
|
|
226
|
+
const atBottom = scrollTop + container.clientHeight >= container.scrollHeight - 10
|
|
227
|
+
if (atBottom) {
|
|
228
|
+
for (let i = headings.length - 1; i >= 0; i--) {
|
|
229
|
+
const id = headings[i].id || findTocIdByText(getHeadingText(headings[i]))
|
|
230
|
+
if (id) {
|
|
231
|
+
activeHeading.value = id
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 从后往前找最后一个滚过检测线的标题
|
|
238
|
+
let lastId = ''
|
|
239
|
+
for (const heading of headings) {
|
|
240
|
+
// offsetTop 是相对于 offsetParent 的,需要计算相对于滚动容器的位置
|
|
241
|
+
const headingTop = heading.getBoundingClientRect().top - container.getBoundingClientRect().top
|
|
242
|
+
if (headingTop <= threshold) {
|
|
243
|
+
const id = heading.id || findTocIdByText(getHeadingText(heading))
|
|
244
|
+
if (id) lastId = id
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (lastId) {
|
|
249
|
+
activeHeading.value = lastId
|
|
250
|
+
} else if (scrollTop < 100) {
|
|
251
|
+
activeHeading.value = ''
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 监听滚动:更新进度条、返回顶部按钮,以及处理底部边界
|
|
19
256
|
function handleScroll(e) {
|
|
20
257
|
const element = e.target
|
|
21
258
|
const scrollTop = element.scrollTop
|
|
22
259
|
const scrollHeight = element.scrollHeight - element.clientHeight
|
|
23
|
-
|
|
260
|
+
|
|
24
261
|
if (scrollHeight > 0) {
|
|
25
262
|
scrollProgress.value = Math.round((scrollTop / scrollHeight) * 100)
|
|
26
263
|
showBackToTop.value = scrollTop > 300
|
|
27
264
|
}
|
|
28
|
-
|
|
29
|
-
|
|
265
|
+
|
|
266
|
+
// 编辑模式下 tiptap 会替换 DOM 元素导致 IO 失效,改用滚动位置计算
|
|
267
|
+
if (element.classList.contains('editor-content')) {
|
|
268
|
+
if (!_locked) {
|
|
269
|
+
_resolveActiveHeadingByScroll(element)
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 底部边界检测(IO 的 rootMargin 裁掉了底部 70%,滚到底时可能漏掉)
|
|
275
|
+
if (!_locked) {
|
|
276
|
+
const atBottom = scrollTop + element.clientHeight >= element.scrollHeight - 10
|
|
277
|
+
if (atBottom) {
|
|
278
|
+
_resolveActiveHeading()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
30
281
|
}
|
|
31
282
|
|
|
32
283
|
// 提取标题纯文本(去掉锚点图标等子元素)
|
|
@@ -43,49 +294,52 @@ export function useScroll() {
|
|
|
43
294
|
return item ? item.id : ''
|
|
44
295
|
}
|
|
45
296
|
|
|
46
|
-
//
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const scrollTop = content.scrollTop
|
|
53
|
-
|
|
54
|
-
let currentId = ''
|
|
55
|
-
headings.forEach(heading => {
|
|
56
|
-
const rect = heading.getBoundingClientRect()
|
|
57
|
-
const contentRect = content.getBoundingClientRect()
|
|
58
|
-
const offsetTop = rect.top - contentRect.top + scrollTop
|
|
59
|
-
|
|
60
|
-
if (offsetTop <= scrollTop + 100) {
|
|
61
|
-
// 优先用 id,没有 id 时通过文本匹配 tocItems
|
|
62
|
-
currentId = heading.id || findTocIdByText(getHeadingText(heading))
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
activeHeading.value = currentId
|
|
297
|
+
// 解锁函数
|
|
298
|
+
function _unlock() {
|
|
299
|
+
_locked = false
|
|
300
|
+
if (_lockTimer) { clearTimeout(_lockTimer); _lockTimer = null }
|
|
301
|
+
// 解锁后立即重新解析一次,确保 IO 状态同步
|
|
302
|
+
_resolveActiveHeading()
|
|
67
303
|
}
|
|
68
304
|
|
|
69
|
-
//
|
|
305
|
+
// 滚动到指定标题
|
|
70
306
|
function scrollToHeading(id) {
|
|
71
|
-
|
|
307
|
+
activeHeading.value = id
|
|
308
|
+
// 锁定,防止 smooth 滚动期间检测覆盖
|
|
309
|
+
_locked = true
|
|
310
|
+
if (_lockTimer) clearTimeout(_lockTimer)
|
|
311
|
+
|
|
312
|
+
const content = document.querySelector('.content')
|
|
313
|
+
|
|
314
|
+
// 优先用 scrollend 事件解锁,SCROLL_LOCK_TIMEOUT_MS 兜底
|
|
315
|
+
if (content) {
|
|
316
|
+
content.addEventListener('scrollend', _unlock, { once: true })
|
|
317
|
+
}
|
|
318
|
+
_lockTimer = setTimeout(() => {
|
|
319
|
+
_unlock()
|
|
320
|
+
if (content) content.removeEventListener('scrollend', _unlock)
|
|
321
|
+
}, SCROLL_LOCK_TIMEOUT_MS)
|
|
322
|
+
|
|
72
323
|
let el = document.getElementById(id)
|
|
73
324
|
if (el) {
|
|
74
325
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
75
|
-
|
|
326
|
+
// 触发高亮闪烁动画
|
|
327
|
+
_flashHeading(el)
|
|
76
328
|
return
|
|
77
329
|
}
|
|
78
330
|
// 编辑模式下标题无 id,通过 tocItems 找到文本再匹配 DOM
|
|
331
|
+
// 注意:不调用 _getHeadings() 避免 _syncHeadingIds 修改 tiptap DOM 导致节点重建
|
|
79
332
|
if (_tocItemsRef && _tocItemsRef.value) {
|
|
80
333
|
const tocItem = _tocItemsRef.value.find(t => t.id === id)
|
|
81
334
|
if (tocItem) {
|
|
82
|
-
const content = document.querySelector('.content')
|
|
83
335
|
if (!content) return
|
|
84
|
-
const headings = content.querySelectorAll(
|
|
336
|
+
const headings = [...content.querySelectorAll(
|
|
337
|
+
'.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6'
|
|
338
|
+
)]
|
|
85
339
|
for (const heading of headings) {
|
|
86
340
|
if (getHeadingText(heading) === tocItem.text) {
|
|
87
341
|
heading.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
88
|
-
|
|
342
|
+
_flashHeading(heading)
|
|
89
343
|
return
|
|
90
344
|
}
|
|
91
345
|
}
|
|
@@ -93,6 +347,33 @@ export function useScroll() {
|
|
|
93
347
|
}
|
|
94
348
|
}
|
|
95
349
|
|
|
350
|
+
// 触发标题高亮闪烁动画
|
|
351
|
+
function _flashHeading(el) {
|
|
352
|
+
el.classList.remove('heading-flash')
|
|
353
|
+
void el.offsetWidth // 强制 reflow 重置动画
|
|
354
|
+
el.classList.add('heading-flash')
|
|
355
|
+
el.addEventListener('animationend', () => el.classList.remove('heading-flash'), { once: true })
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 锁定 activeHeading 一段时间(供外部在非 smooth 滚动场景使用,如页面刷新定位锚点)
|
|
359
|
+
function lockHeading(id, durationMs = SCROLL_LOCK_TIMEOUT_MS) {
|
|
360
|
+
activeHeading.value = id
|
|
361
|
+
_locked = true
|
|
362
|
+
if (_lockTimer) clearTimeout(_lockTimer)
|
|
363
|
+
_lockTimer = setTimeout(_unlock, durationMs)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 文档切换时清空 activeHeading(标记为文档切换,不同步清除 URL hash)
|
|
367
|
+
function clearActiveHeading() {
|
|
368
|
+
_suppressHashClear = true
|
|
369
|
+
activeHeading.value = ''
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 查询当前是否为文档切换导致的清空
|
|
373
|
+
function isSuppressHashClear() {
|
|
374
|
+
return _suppressHashClear
|
|
375
|
+
}
|
|
376
|
+
|
|
96
377
|
// 返回顶部
|
|
97
378
|
function scrollToTop() {
|
|
98
379
|
const content = document.querySelector('.content')
|
|
@@ -108,6 +389,10 @@ export function useScroll() {
|
|
|
108
389
|
handleScroll,
|
|
109
390
|
scrollToHeading,
|
|
110
391
|
scrollToTop,
|
|
111
|
-
setTocItems
|
|
392
|
+
setTocItems,
|
|
393
|
+
rebuildHeadingsCache,
|
|
394
|
+
clearActiveHeading,
|
|
395
|
+
isSuppressHashClear,
|
|
396
|
+
lockHeading
|
|
112
397
|
}
|
|
113
398
|
}
|
|
@@ -42,24 +42,25 @@ export function useSearch() {
|
|
|
42
42
|
indexBuilding.value = true
|
|
43
43
|
|
|
44
44
|
const docs = flattenDocs(pendingDocsList)
|
|
45
|
-
const documents = []
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
// 并发加载所有文档内容,大幅提升索引构建速度
|
|
47
|
+
const documents = (await Promise.all(
|
|
48
|
+
docs.map(async (doc) => {
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(doc.path)
|
|
51
|
+
if (!response.ok) return null
|
|
51
52
|
const content = await response.text()
|
|
52
|
-
|
|
53
|
+
return {
|
|
53
54
|
id: doc.key,
|
|
54
55
|
title: doc.label,
|
|
55
56
|
content: content.replace(/^---[\s\S]*?---\n?/, ''), // 去掉 frontmatter
|
|
56
57
|
path: doc.path
|
|
57
|
-
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
return null // 忽略加载失败的文档
|
|
58
61
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
62
|
+
})
|
|
63
|
+
)).filter(Boolean)
|
|
63
64
|
|
|
64
65
|
searchIndex = new MiniSearch({
|
|
65
66
|
fields: ['title', 'content'],
|
package/src/config.js
CHANGED
|
@@ -76,9 +76,6 @@ export async function pollDocContent(docPath) {
|
|
|
76
76
|
if (res.status === 304) return null
|
|
77
77
|
if (res.status === 200) {
|
|
78
78
|
_contentEtag = res.headers.get('etag')
|
|
79
|
-
// 捕获最后修改时间
|
|
80
|
-
const lm = res.headers.get('x-last-modified')
|
|
81
|
-
if (lm) _lastModified = lm
|
|
82
79
|
return await res.text()
|
|
83
80
|
}
|
|
84
81
|
} catch { /* 静默忽略 */ }
|
|
@@ -96,19 +93,6 @@ export function resetListEtag() {
|
|
|
96
93
|
_listEtag = null
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
// ===== 文档最后修改时间 =====
|
|
100
|
-
let _lastModified = null
|
|
101
|
-
|
|
102
|
-
/** 获取当前文档的最后修改时间 */
|
|
103
|
-
export function getLastModified() {
|
|
104
|
-
return _lastModified
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** 重置最后修改时间 */
|
|
108
|
-
export function resetLastModified() {
|
|
109
|
-
_lastModified = null
|
|
110
|
-
}
|
|
111
|
-
|
|
112
96
|
// ===== 写操作 API =====
|
|
113
97
|
|
|
114
98
|
async function postJson(url, body) {
|