@tnotesjs/core 0.1.22 → 0.1.23

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.
Files changed (101) hide show
  1. package/README.md +87 -87
  2. package/package.json +1 -1
  3. package/types/config.ts +60 -60
  4. package/types/index.ts +11 -11
  5. package/types/note.ts +33 -33
  6. package/vitepress/assets/icons/icon__check.svg +3 -3
  7. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  8. package/vitepress/assets/icons/icon__close.svg +1 -1
  9. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  10. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  11. package/vitepress/assets/icons/icon__copy.svg +4 -4
  12. package/vitepress/assets/icons/icon__focus.svg +1 -1
  13. package/vitepress/assets/icons/icon__fold.svg +3 -3
  14. package/vitepress/assets/icons/icon__folder.svg +1 -1
  15. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  16. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  17. package/vitepress/assets/icons/icon__github.svg +3 -3
  18. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  19. package/vitepress/assets/icons/icon__next.svg +1 -1
  20. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  21. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  22. package/vitepress/assets/icons/icon__prev.svg +1 -1
  23. package/vitepress/assets/icons/icon__restore.svg +1 -1
  24. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  25. package/vitepress/assets/icons/icon__search.svg +1 -1
  26. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  27. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  28. package/vitepress/assets/icons/icon__totop.svg +5 -5
  29. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  30. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  31. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  32. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  33. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  34. package/vitepress/assets/icons/index.ts +39 -39
  35. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  36. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  37. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -209
  38. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  39. package/vitepress/components/Discussions/Discussions.vue +242 -242
  40. package/vitepress/components/EnWordList/EnWordList.vue +664 -664
  41. package/vitepress/components/EnWordList/RightClickMenu.vue +87 -87
  42. package/vitepress/components/Footprints/Footprints.vue +472 -472
  43. package/vitepress/components/Layout/AboutModal.vue +329 -329
  44. package/vitepress/components/Layout/AboutPanel.vue +485 -485
  45. package/vitepress/components/Layout/ContentCollapse.vue +591 -591
  46. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  47. package/vitepress/components/Layout/DocBeforeControls.vue +145 -145
  48. package/vitepress/components/Layout/DocFooter.vue +233 -233
  49. package/vitepress/components/Layout/ImagePreview.vue +485 -485
  50. package/vitepress/components/Layout/Layout.vue +744 -744
  51. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  52. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  53. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -170
  54. package/vitepress/components/Layout/SidebarResizeHandle.vue +311 -311
  55. package/vitepress/components/Layout/Swiper.vue +167 -167
  56. package/vitepress/components/Layout/ToggleFullContent.vue +45 -45
  57. package/vitepress/components/Layout/ToggleSidebar.vue +35 -35
  58. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  59. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  60. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  61. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  62. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  63. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  64. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  65. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  66. package/vitepress/components/MarkMap/MarkMap.vue +562 -562
  67. package/vitepress/components/Mermaid/Mermaid.vue +644 -644
  68. package/vitepress/components/NotesTable/NotesTable.vue +175 -175
  69. package/vitepress/components/NotesTable/README.md +67 -67
  70. package/vitepress/components/Settings/Settings.vue +711 -711
  71. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  72. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  73. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -945
  74. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  75. package/vitepress/components/constants.ts +109 -109
  76. package/vitepress/components/notesConfig.data.ts +73 -73
  77. package/vitepress/components/sidebar.data.ts +59 -59
  78. package/vitepress/components/tnotes-config.data.ts +21 -21
  79. package/vitepress/components/utils.ts +26 -26
  80. package/vitepress/config/index.ts +169 -169
  81. package/vitepress/configs/constants.ts +26 -26
  82. package/vitepress/configs/head.config.ts +25 -25
  83. package/vitepress/configs/index.ts +9 -9
  84. package/vitepress/configs/markdown-it.d.ts +23 -23
  85. package/vitepress/configs/markdown.config.ts +368 -368
  86. package/vitepress/configs/theme.config.ts +108 -108
  87. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  88. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  89. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  90. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  91. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  92. package/vitepress/theme/index.ts +137 -137
  93. package/vitepress/theme/styles/base.scss +66 -64
  94. package/vitepress/theme/styles/components/404.scss +31 -31
  95. package/vitepress/theme/styles/components/collapse.scss +172 -172
  96. package/vitepress/theme/styles/components/markmap.scss +101 -101
  97. package/vitepress/theme/styles/components/swiper.scss +255 -255
  98. package/vitepress/theme/styles/index.scss +25 -25
  99. package/vitepress/theme/styles/layout.scss +78 -78
  100. package/vitepress/theme/styles/utilities.scss +39 -39
  101. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,591 +1,591 @@
1
- <template>
2
- <div></div>
3
- </template>
4
-
5
- <script setup lang="ts">
6
- import { useRoute, onContentUpdated } from 'vitepress'
7
- import { onMounted, onUnmounted } from 'vue'
8
-
9
- import { icon__collapse } from '../../assets/icons'
10
-
11
- const route = useRoute()
12
-
13
- // 存储折叠状态的 localStorage key 前缀
14
- const COLLAPSE_STATE_PREFIX = 'tnotes_collapse_state_'
15
-
16
- // 防止重复初始化的标志
17
- let isInitializing = false
18
- let reinitTimer: ReturnType<typeof setTimeout> | null = null
19
-
20
- // 获取当前笔记的唯一标识
21
- function getCurrentNoteKey(): string {
22
- return route.path.replace(/\//g, '_')
23
- }
24
-
25
- // 保存折叠状态
26
- function saveCollapseState(key: string, collapsed: boolean) {
27
- if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
28
- return
29
- }
30
- const noteKey = getCurrentNoteKey()
31
- const storageKey = `${COLLAPSE_STATE_PREFIX}${noteKey}_${key}`
32
- localStorage.setItem(storageKey, collapsed ? '1' : '0')
33
- }
34
-
35
- // 获取折叠状态
36
- function getCollapseState(key: string): boolean {
37
- if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
38
- return false
39
- }
40
- const noteKey = getCurrentNoteKey()
41
- const storageKey = `${COLLAPSE_STATE_PREFIX}${noteKey}_${key}`
42
- return localStorage.getItem(storageKey) === '1'
43
- }
44
-
45
- // 切换折叠状态
46
- function toggleCollapse(
47
- button: HTMLElement,
48
- content: HTMLElement,
49
- key: string
50
- ) {
51
- const isCollapsed = content.classList.contains('collapsed')
52
- content.classList.toggle('collapsed')
53
- button.classList.toggle('collapsed')
54
-
55
- // 保存状态
56
- saveCollapseState(key, !isCollapsed)
57
- }
58
-
59
- // 初始化 TOC 折叠功能
60
- function initTocCollapse() {
61
- const vpDoc = document.querySelector('.vp-doc')
62
- if (!vpDoc) {
63
- return false
64
- }
65
-
66
- // 方案1: 查找 region:toc 注释
67
- const walker = document.createTreeWalker(vpDoc, NodeFilter.SHOW_COMMENT, null)
68
-
69
- let tocStartComment: Comment | null = null
70
- let tocEndComment: Comment | null = null
71
-
72
- while (walker.nextNode()) {
73
- const comment = walker.currentNode as Comment
74
- const commentText = comment.textContent?.trim()
75
-
76
- if (commentText === 'region:toc') {
77
- tocStartComment = comment
78
- } else if (commentText === 'endregion:toc') {
79
- tocEndComment = comment
80
- break
81
- }
82
- }
83
-
84
- // 如果找到注释,使用注释方式处理
85
- if (tocStartComment && tocEndComment) {
86
- return initTocCollapseByComments(tocStartComment, tocEndComment)
87
- }
88
-
89
- // 方案2: 如果注释不存在(生产构建可能移除注释),使用结构化查找
90
- return initTocCollapseByStructure(vpDoc)
91
- }
92
-
93
- // 通过注释初始化 TOC 折叠
94
- function initTocCollapseByComments(
95
- tocStartComment: Comment,
96
- tocEndComment: Comment
97
- ): boolean {
98
- // 获取 TOC 区域的所有内容
99
- const tocElements: Node[] = []
100
- let current: Node | null = tocStartComment.nextSibling
101
-
102
- while (current && current !== tocEndComment) {
103
- tocElements.push(current)
104
- current = current.nextSibling
105
- }
106
-
107
- if (tocElements.length === 0) {
108
- return false
109
- }
110
-
111
- // 创建折叠容器
112
- const collapseWrapper = document.createElement('div')
113
- collapseWrapper.className = 'toc-collapse-wrapper'
114
-
115
- // 创建折叠头部(可点击区域)
116
- const collapseHeader = document.createElement('div')
117
- collapseHeader.className = 'collapse-header toc-collapse-header'
118
- collapseHeader.setAttribute('title', '点击折叠/展开目录')
119
-
120
- // 创建标签(折叠时显示)
121
- const collapseLabel = document.createElement('span')
122
- collapseLabel.className = 'collapse-label'
123
- collapseLabel.textContent = '目录'
124
-
125
- // 创建折叠按钮
126
- const collapseButton = document.createElement('button')
127
- collapseButton.className = 'collapse-toggle toc-collapse-toggle'
128
-
129
- // 使用 SVG 图标
130
- const collapseIcon = document.createElement('img')
131
- collapseIcon.src = icon__collapse
132
- collapseIcon.alt = 'collapse icon'
133
- collapseIcon.className = 'collapse-icon'
134
- collapseButton.appendChild(collapseIcon)
135
-
136
- // 组装头部
137
- collapseHeader.appendChild(collapseLabel)
138
- collapseHeader.appendChild(collapseButton)
139
-
140
- // 创建内容容器
141
- const contentWrapper = document.createElement('div')
142
- contentWrapper.className = 'collapse-content toc-collapse-content'
143
-
144
- // 移动 TOC 内容到容器中
145
- tocElements.forEach((el) => {
146
- contentWrapper.appendChild(el)
147
- })
148
-
149
- // 组装结构
150
- collapseWrapper.appendChild(collapseHeader)
151
- collapseWrapper.appendChild(contentWrapper)
152
-
153
- // 插入到 TOC 开始注释之后
154
- tocStartComment.parentNode?.insertBefore(
155
- collapseWrapper,
156
- tocStartComment.nextSibling
157
- )
158
-
159
- // 恢复折叠状态(默认展开)
160
- const isCollapsed = getCollapseState('toc')
161
- if (isCollapsed) {
162
- contentWrapper.classList.add('collapsed')
163
- collapseHeader.classList.add('collapsed')
164
- }
165
-
166
- // 绑定点击事件到整个头部,支持文本选择
167
- let mouseDownTime = 0
168
- let mouseDownX = 0
169
- let mouseDownY = 0
170
-
171
- collapseHeader.addEventListener('mousedown', (e) => {
172
- mouseDownTime = Date.now()
173
- mouseDownX = e.clientX
174
- mouseDownY = e.clientY
175
- })
176
-
177
- collapseHeader.addEventListener('mouseup', (e) => {
178
- const mouseUpTime = Date.now()
179
- const duration = mouseUpTime - mouseDownTime
180
- const moveX = Math.abs(e.clientX - mouseDownX)
181
- const moveY = Math.abs(e.clientY - mouseDownY)
182
-
183
- // 判断是否为点击行为:
184
- // 1. 持续时间小于 200ms
185
- // 2. 鼠标移动距离小于 5px
186
- // 3. 没有选中文本
187
- const isClick = duration < 200 && moveX < 5 && moveY < 5
188
- const hasSelection = window.getSelection()?.toString().length ?? 0 > 0
189
-
190
- if (isClick && !hasSelection) {
191
- const isCollapsed = contentWrapper.classList.contains('collapsed')
192
- contentWrapper.classList.toggle('collapsed')
193
- collapseHeader.classList.toggle('collapsed')
194
- saveCollapseState('toc', !isCollapsed)
195
- }
196
- })
197
-
198
- return true
199
- }
200
-
201
- // 通过结构化查找初始化 TOC 折叠(用于生产环境)
202
- function initTocCollapseByStructure(vpDoc: Element): boolean {
203
- // 在生产环境中,尝试查找第一个 h2 之前的所有 ul/ol 元素作为 TOC
204
- // 通常 TOC 会在文档开头,第一个 h2 之前
205
- const firstH2 = vpDoc.querySelector('h2')
206
- if (!firstH2) {
207
- return false
208
- }
209
-
210
- // 查找第一个 h2 之前的所有列表元素
211
- const tocElements: Element[] = []
212
- let current = firstH2.previousElementSibling
213
-
214
- while (current) {
215
- // 如果是列表,可能是 TOC
216
- if (current.tagName === 'UL' || current.tagName === 'OL') {
217
- tocElements.unshift(current) // 添加到开头保持顺序
218
- }
219
- // 如果遇到其他块级元素(如 p, h1 等),停止查找
220
- else if (
221
- current.tagName === 'P' ||
222
- current.tagName === 'H1' ||
223
- current.tagName === 'DIV'
224
- ) {
225
- // 如果是空的 p 标签或只有空白,继续
226
- if (current.textContent?.trim()) {
227
- break
228
- }
229
- }
230
- current = current.previousElementSibling
231
- }
232
-
233
- if (tocElements.length === 0) {
234
- return false
235
- }
236
-
237
- // 保存第一个 TOC 元素的位置信息
238
- const firstTocElement = tocElements[0]
239
- const insertParent = firstTocElement.parentNode
240
- const insertBefore = firstTocElement
241
-
242
- // 创建折叠容器
243
- const collapseWrapper = document.createElement('div')
244
- collapseWrapper.className = 'toc-collapse-wrapper'
245
-
246
- // 创建折叠头部
247
- const collapseHeader = document.createElement('div')
248
- collapseHeader.className = 'collapse-header toc-collapse-header'
249
- collapseHeader.setAttribute('title', '点击折叠/展开目录')
250
-
251
- // 创建标签
252
- const collapseLabel = document.createElement('span')
253
- collapseLabel.className = 'collapse-label'
254
- collapseLabel.textContent = '目录'
255
-
256
- // 创建折叠按钮
257
- const collapseButton = document.createElement('button')
258
- collapseButton.className = 'collapse-toggle toc-collapse-toggle'
259
-
260
- // 使用 SVG 图标
261
- const collapseIcon = document.createElement('img')
262
- collapseIcon.src = icon__collapse
263
- collapseIcon.alt = 'collapse icon'
264
- collapseIcon.className = 'collapse-icon'
265
- collapseButton.appendChild(collapseIcon)
266
-
267
- // 组装头部
268
- collapseHeader.appendChild(collapseLabel)
269
- collapseHeader.appendChild(collapseButton)
270
-
271
- // 创建内容容器
272
- const contentWrapper = document.createElement('div')
273
- contentWrapper.className = 'collapse-content toc-collapse-content'
274
-
275
- // 组装结构
276
- collapseWrapper.appendChild(collapseHeader)
277
- collapseWrapper.appendChild(contentWrapper)
278
-
279
- // 先插入折叠容器
280
- insertParent?.insertBefore(collapseWrapper, insertBefore)
281
-
282
- // 再移动 TOC 内容到容器中
283
- tocElements.forEach((el) => {
284
- contentWrapper.appendChild(el)
285
- })
286
-
287
- // 恢复折叠状态
288
- const isCollapsed = getCollapseState('toc')
289
- if (isCollapsed) {
290
- contentWrapper.classList.add('collapsed')
291
- collapseHeader.classList.add('collapsed')
292
- }
293
-
294
- // 绑定点击事件
295
- let mouseDownTime = 0
296
- let mouseDownX = 0
297
- let mouseDownY = 0
298
-
299
- collapseHeader.addEventListener('mousedown', (e) => {
300
- mouseDownTime = Date.now()
301
- mouseDownX = e.clientX
302
- mouseDownY = e.clientY
303
- })
304
-
305
- collapseHeader.addEventListener('mouseup', (e) => {
306
- const mouseUpTime = Date.now()
307
- const duration = mouseUpTime - mouseDownTime
308
- const moveX = Math.abs(e.clientX - mouseDownX)
309
- const moveY = Math.abs(e.clientY - mouseDownY)
310
-
311
- const isClick = duration < 200 && moveX < 5 && moveY < 5
312
- const hasSelection = window.getSelection()?.toString().length ?? 0 > 0
313
-
314
- if (isClick && !hasSelection) {
315
- const isCollapsed = contentWrapper.classList.contains('collapsed')
316
- contentWrapper.classList.toggle('collapsed')
317
- collapseHeader.classList.toggle('collapsed')
318
- saveCollapseState('toc', !isCollapsed)
319
- }
320
- })
321
-
322
- return true
323
- }
324
-
325
- // 初始化二级标题折叠功能
326
- function initH2Collapse() {
327
- const vpDoc = document.querySelector('.vp-doc')
328
- if (!vpDoc) {
329
- return
330
- }
331
-
332
- const h2Elements = vpDoc.querySelectorAll('h2')
333
-
334
- h2Elements.forEach((h2) => {
335
- // 跳过已经处理过的
336
- if (h2.querySelector('.collapse-toggle')) return
337
-
338
- // 获取 h2 的 id 作为唯一标识
339
- const h2Id = h2.id || `h2_${Array.from(h2Elements).indexOf(h2)}`
340
-
341
- // 先清除可能存在的旧状态(避免状态冲突)
342
- h2.classList.remove('collapsible-h2', 'collapsed')
343
- h2.removeAttribute('title')
344
-
345
- // 为 h2 添加可点击的类
346
- h2.classList.add('collapsible-h2')
347
- h2.setAttribute('title', '点击折叠/展开章节')
348
-
349
- // 创建折叠按钮
350
- const collapseButton = document.createElement('button')
351
- collapseButton.className = 'collapse-toggle h2-collapse-toggle'
352
-
353
- // 使用 SVG 图标
354
- const collapseIcon = document.createElement('img')
355
- collapseIcon.src = icon__collapse
356
- collapseIcon.alt = 'collapse icon'
357
- collapseIcon.className = 'collapse-icon'
358
- collapseButton.appendChild(collapseIcon)
359
-
360
- // 收集 h2 后面的内容直到下一个 h2
361
- const contentElements: Element[] = []
362
- let nextSibling = h2.nextElementSibling
363
-
364
- while (nextSibling && nextSibling.tagName !== 'H2') {
365
- contentElements.push(nextSibling)
366
- nextSibling = nextSibling.nextElementSibling
367
- }
368
-
369
- // 如果没有内容,不添加折叠按钮
370
- if (contentElements.length === 0) return
371
-
372
- // 创建内容容器
373
- const contentWrapper = document.createElement('div')
374
- contentWrapper.className = 'collapse-content h2-collapse-content'
375
-
376
- // 移动内容到容器中
377
- contentElements.forEach((el) => {
378
- contentWrapper.appendChild(el)
379
- })
380
-
381
- // 将折叠按钮插入到 h2 内部的末尾
382
- h2.appendChild(collapseButton)
383
-
384
- // 将内容容器插入到 h2 后面
385
- h2.parentNode?.insertBefore(contentWrapper, h2.nextSibling)
386
-
387
- // 恢复折叠状态(默认展开)
388
- const isCollapsed = getCollapseState(`h2_${h2Id}`)
389
- // console.log(
390
- // `[initH2] h2Id: ${h2Id}, isCollapsed from localStorage: ${isCollapsed}`
391
- // )
392
-
393
- if (isCollapsed) {
394
- contentWrapper.classList.add('collapsed')
395
- h2.classList.add('collapsed')
396
- // console.log(`[initH2] Added collapsed class to h2 and contentWrapper`)
397
- // console.log(`[initH2] h2.classList:`, h2.classList.toString())
398
- // console.log(
399
- // `[initH2] contentWrapper.classList:`,
400
- // contentWrapper.classList.toString()
401
- // )
402
- }
403
-
404
- // 绑定点击事件到整个 h2,支持文本选择
405
- let mouseDownTime = 0
406
- let mouseDownX = 0
407
- let mouseDownY = 0
408
-
409
- h2.addEventListener('mousedown', (e) => {
410
- // 如果点击的是链接,不记录时间(让链接正常工作)
411
- const target = e.target as HTMLElement
412
- if (target.tagName === 'A' || target.closest('a')) {
413
- return
414
- }
415
- mouseDownTime = Date.now()
416
- mouseDownX = e.clientX
417
- mouseDownY = e.clientY
418
- })
419
-
420
- h2.addEventListener('mouseup', (e) => {
421
- // 不阻止锚点链接的默认行为
422
- const target = e.target as HTMLElement
423
- if (target.tagName === 'A' || target.closest('a')) {
424
- return
425
- }
426
-
427
- const mouseUpTime = Date.now()
428
- const duration = mouseUpTime - mouseDownTime
429
- const moveX = Math.abs(e.clientX - mouseDownX)
430
- const moveY = Math.abs(e.clientY - mouseDownY)
431
-
432
- // 判断是否为点击行为:
433
- // 1. 持续时间小于 200ms
434
- // 2. 鼠标移动距离小于 5px
435
- // 3. 没有选中文本
436
- const isClick = duration < 200 && moveX < 5 && moveY < 5
437
- const hasSelection = window.getSelection()?.toString().length ?? 0 > 0
438
-
439
- if (isClick && !hasSelection) {
440
- // console.log(`[H2 Click] h2Id: ${h2Id}`)
441
- // console.log('[H2 Click] contentWrapper:', contentWrapper)
442
- // console.log('[H2 Click] h2 element:', h2)
443
- // console.log(
444
- // '[H2 Click] isCollapsed before toggle:',
445
- // contentWrapper.classList.contains('collapsed')
446
- // )
447
-
448
- const isCollapsed = contentWrapper.classList.contains('collapsed')
449
- contentWrapper.classList.toggle('collapsed')
450
- h2.classList.toggle('collapsed')
451
-
452
- // 强制触发重绘(浏览器优化可能导致样式不更新)
453
- void h2.offsetHeight
454
-
455
- saveCollapseState(`h2_${h2Id}`, !isCollapsed)
456
-
457
- // console.log(
458
- // '[H2 Click] isCollapsed after toggle:',
459
- // contentWrapper.classList.contains('collapsed')
460
- // )
461
- // console.log('[H2 Click] h2.classList:', h2.classList.toString())
462
- }
463
- })
464
- })
465
- }
466
-
467
- // 初始化所有折叠功能
468
- function initAllCollapse() {
469
- // 如果正在初始化,跳过
470
- if (isInitializing) {
471
- return
472
- }
473
-
474
- isInitializing = true
475
-
476
- // 延迟执行以确保 DOM 已经渲染完成
477
- const attempts = [100, 300, 500, 1000]
478
- let attemptIndex = 0
479
-
480
- function tryInit() {
481
- const tocSuccess = initTocCollapse()
482
- initH2Collapse()
483
-
484
- // 如果 TOC 初始化失败且还有重试次数,继续尝试
485
- if (!tocSuccess && attemptIndex < attempts.length - 1) {
486
- attemptIndex++
487
- setTimeout(tryInit, attempts[attemptIndex])
488
- } else {
489
- // 初始化完成,释放锁
490
- isInitializing = false
491
- }
492
- }
493
-
494
- setTimeout(tryInit, attempts[attemptIndex])
495
- }
496
-
497
- // 清理折叠功能
498
- function cleanupCollapse() {
499
- // 清理 TOC 折叠容器(需要先移出内容再删除容器)
500
- const tocWrappers = document.querySelectorAll('.toc-collapse-wrapper')
501
-
502
- tocWrappers.forEach((wrapper) => {
503
- const contentContainer = wrapper.querySelector('.toc-collapse-content')
504
- const parent = wrapper.parentElement
505
-
506
- // 将 TOC 内容移回原位
507
- if (contentContainer && parent) {
508
- while (contentContainer.firstChild) {
509
- parent.insertBefore(contentContainer.firstChild, wrapper)
510
- }
511
- }
512
-
513
- // 删除整个包装器
514
- wrapper.remove()
515
- })
516
-
517
- // 清理 H2 折叠
518
- document.querySelectorAll('.h2-collapse-toggle').forEach((el) => el.remove())
519
-
520
- // 清理 H2 标题上的类名和属性
521
- document.querySelectorAll('h2.collapsible-h2').forEach((h2) => {
522
- h2.classList.remove('collapsible-h2', 'collapsed')
523
- h2.removeAttribute('title')
524
- })
525
-
526
- document.querySelectorAll('.h2-collapse-content').forEach((el) => {
527
- // 将内容移回原位
528
- const parent = el.parentElement
529
- while (el.firstChild) {
530
- parent?.insertBefore(el.firstChild, el)
531
- }
532
- el.remove()
533
- })
534
- } // 清除所有折叠状态
535
- function clearAllCollapseStates() {
536
- if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
537
- return
538
- }
539
- const keys = Object.keys(localStorage).filter((key) =>
540
- key.startsWith(COLLAPSE_STATE_PREFIX)
541
- )
542
- keys.forEach((key) => localStorage.removeItem(key))
543
-
544
- // 重新初始化
545
- cleanupCollapse()
546
- initAllCollapse()
547
- }
548
-
549
- // 组件挂载时初始化
550
- onMounted(() => {
551
- // 确保在客户端环境中才初始化
552
- if (typeof window !== 'undefined') {
553
- // 不直接调用 initAllCollapse,让 onContentUpdated 处理
554
- // 因为首次加载时 onContentUpdated 也会触发
555
-
556
- // 添加全局函数,方便调试
557
- ;(window as any).clearAllCollapseStates = clearAllCollapseStates
558
- }
559
- })
560
-
561
- // 内容更新时重新初始化(支持 HMR 和路由切换)
562
- onContentUpdated(() => {
563
- // 清除之前的定时器
564
- if (reinitTimer) {
565
- clearTimeout(reinitTimer)
566
- }
567
-
568
- // 使用防抖,避免短时间内多次触发
569
- reinitTimer = setTimeout(() => {
570
- cleanupCollapse()
571
- initAllCollapse()
572
- reinitTimer = null
573
- }, 150)
574
- })
575
-
576
- // 组件卸载时清理
577
- onUnmounted(() => {
578
- cleanupCollapse()
579
-
580
- // 清除定时器
581
- if (reinitTimer) {
582
- clearTimeout(reinitTimer)
583
- reinitTimer = null
584
- }
585
-
586
- // 清理全局函数
587
- if (typeof window !== 'undefined') {
588
- delete (window as any).clearAllCollapseStates
589
- }
590
- })
591
- </script>
1
+ <template>
2
+ <div></div>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { useRoute, onContentUpdated } from 'vitepress'
7
+ import { onMounted, onUnmounted } from 'vue'
8
+
9
+ import { icon__collapse } from '../../assets/icons'
10
+
11
+ const route = useRoute()
12
+
13
+ // 存储折叠状态的 localStorage key 前缀
14
+ const COLLAPSE_STATE_PREFIX = 'tnotes_collapse_state_'
15
+
16
+ // 防止重复初始化的标志
17
+ let isInitializing = false
18
+ let reinitTimer: ReturnType<typeof setTimeout> | null = null
19
+
20
+ // 获取当前笔记的唯一标识
21
+ function getCurrentNoteKey(): string {
22
+ return route.path.replace(/\//g, '_')
23
+ }
24
+
25
+ // 保存折叠状态
26
+ function saveCollapseState(key: string, collapsed: boolean) {
27
+ if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
28
+ return
29
+ }
30
+ const noteKey = getCurrentNoteKey()
31
+ const storageKey = `${COLLAPSE_STATE_PREFIX}${noteKey}_${key}`
32
+ localStorage.setItem(storageKey, collapsed ? '1' : '0')
33
+ }
34
+
35
+ // 获取折叠状态
36
+ function getCollapseState(key: string): boolean {
37
+ if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
38
+ return false
39
+ }
40
+ const noteKey = getCurrentNoteKey()
41
+ const storageKey = `${COLLAPSE_STATE_PREFIX}${noteKey}_${key}`
42
+ return localStorage.getItem(storageKey) === '1'
43
+ }
44
+
45
+ // 切换折叠状态
46
+ function toggleCollapse(
47
+ button: HTMLElement,
48
+ content: HTMLElement,
49
+ key: string
50
+ ) {
51
+ const isCollapsed = content.classList.contains('collapsed')
52
+ content.classList.toggle('collapsed')
53
+ button.classList.toggle('collapsed')
54
+
55
+ // 保存状态
56
+ saveCollapseState(key, !isCollapsed)
57
+ }
58
+
59
+ // 初始化 TOC 折叠功能
60
+ function initTocCollapse() {
61
+ const vpDoc = document.querySelector('.vp-doc')
62
+ if (!vpDoc) {
63
+ return false
64
+ }
65
+
66
+ // 方案1: 查找 region:toc 注释
67
+ const walker = document.createTreeWalker(vpDoc, NodeFilter.SHOW_COMMENT, null)
68
+
69
+ let tocStartComment: Comment | null = null
70
+ let tocEndComment: Comment | null = null
71
+
72
+ while (walker.nextNode()) {
73
+ const comment = walker.currentNode as Comment
74
+ const commentText = comment.textContent?.trim()
75
+
76
+ if (commentText === 'region:toc') {
77
+ tocStartComment = comment
78
+ } else if (commentText === 'endregion:toc') {
79
+ tocEndComment = comment
80
+ break
81
+ }
82
+ }
83
+
84
+ // 如果找到注释,使用注释方式处理
85
+ if (tocStartComment && tocEndComment) {
86
+ return initTocCollapseByComments(tocStartComment, tocEndComment)
87
+ }
88
+
89
+ // 方案2: 如果注释不存在(生产构建可能移除注释),使用结构化查找
90
+ return initTocCollapseByStructure(vpDoc)
91
+ }
92
+
93
+ // 通过注释初始化 TOC 折叠
94
+ function initTocCollapseByComments(
95
+ tocStartComment: Comment,
96
+ tocEndComment: Comment
97
+ ): boolean {
98
+ // 获取 TOC 区域的所有内容
99
+ const tocElements: Node[] = []
100
+ let current: Node | null = tocStartComment.nextSibling
101
+
102
+ while (current && current !== tocEndComment) {
103
+ tocElements.push(current)
104
+ current = current.nextSibling
105
+ }
106
+
107
+ if (tocElements.length === 0) {
108
+ return false
109
+ }
110
+
111
+ // 创建折叠容器
112
+ const collapseWrapper = document.createElement('div')
113
+ collapseWrapper.className = 'toc-collapse-wrapper'
114
+
115
+ // 创建折叠头部(可点击区域)
116
+ const collapseHeader = document.createElement('div')
117
+ collapseHeader.className = 'collapse-header toc-collapse-header'
118
+ collapseHeader.setAttribute('title', '点击折叠/展开目录')
119
+
120
+ // 创建标签(折叠时显示)
121
+ const collapseLabel = document.createElement('span')
122
+ collapseLabel.className = 'collapse-label'
123
+ collapseLabel.textContent = '目录'
124
+
125
+ // 创建折叠按钮
126
+ const collapseButton = document.createElement('button')
127
+ collapseButton.className = 'collapse-toggle toc-collapse-toggle'
128
+
129
+ // 使用 SVG 图标
130
+ const collapseIcon = document.createElement('img')
131
+ collapseIcon.src = icon__collapse
132
+ collapseIcon.alt = 'collapse icon'
133
+ collapseIcon.className = 'collapse-icon'
134
+ collapseButton.appendChild(collapseIcon)
135
+
136
+ // 组装头部
137
+ collapseHeader.appendChild(collapseLabel)
138
+ collapseHeader.appendChild(collapseButton)
139
+
140
+ // 创建内容容器
141
+ const contentWrapper = document.createElement('div')
142
+ contentWrapper.className = 'collapse-content toc-collapse-content'
143
+
144
+ // 移动 TOC 内容到容器中
145
+ tocElements.forEach((el) => {
146
+ contentWrapper.appendChild(el)
147
+ })
148
+
149
+ // 组装结构
150
+ collapseWrapper.appendChild(collapseHeader)
151
+ collapseWrapper.appendChild(contentWrapper)
152
+
153
+ // 插入到 TOC 开始注释之后
154
+ tocStartComment.parentNode?.insertBefore(
155
+ collapseWrapper,
156
+ tocStartComment.nextSibling
157
+ )
158
+
159
+ // 恢复折叠状态(默认展开)
160
+ const isCollapsed = getCollapseState('toc')
161
+ if (isCollapsed) {
162
+ contentWrapper.classList.add('collapsed')
163
+ collapseHeader.classList.add('collapsed')
164
+ }
165
+
166
+ // 绑定点击事件到整个头部,支持文本选择
167
+ let mouseDownTime = 0
168
+ let mouseDownX = 0
169
+ let mouseDownY = 0
170
+
171
+ collapseHeader.addEventListener('mousedown', (e) => {
172
+ mouseDownTime = Date.now()
173
+ mouseDownX = e.clientX
174
+ mouseDownY = e.clientY
175
+ })
176
+
177
+ collapseHeader.addEventListener('mouseup', (e) => {
178
+ const mouseUpTime = Date.now()
179
+ const duration = mouseUpTime - mouseDownTime
180
+ const moveX = Math.abs(e.clientX - mouseDownX)
181
+ const moveY = Math.abs(e.clientY - mouseDownY)
182
+
183
+ // 判断是否为点击行为:
184
+ // 1. 持续时间小于 200ms
185
+ // 2. 鼠标移动距离小于 5px
186
+ // 3. 没有选中文本
187
+ const isClick = duration < 200 && moveX < 5 && moveY < 5
188
+ const hasSelection = window.getSelection()?.toString().length ?? 0 > 0
189
+
190
+ if (isClick && !hasSelection) {
191
+ const isCollapsed = contentWrapper.classList.contains('collapsed')
192
+ contentWrapper.classList.toggle('collapsed')
193
+ collapseHeader.classList.toggle('collapsed')
194
+ saveCollapseState('toc', !isCollapsed)
195
+ }
196
+ })
197
+
198
+ return true
199
+ }
200
+
201
+ // 通过结构化查找初始化 TOC 折叠(用于生产环境)
202
+ function initTocCollapseByStructure(vpDoc: Element): boolean {
203
+ // 在生产环境中,尝试查找第一个 h2 之前的所有 ul/ol 元素作为 TOC
204
+ // 通常 TOC 会在文档开头,第一个 h2 之前
205
+ const firstH2 = vpDoc.querySelector('h2')
206
+ if (!firstH2) {
207
+ return false
208
+ }
209
+
210
+ // 查找第一个 h2 之前的所有列表元素
211
+ const tocElements: Element[] = []
212
+ let current = firstH2.previousElementSibling
213
+
214
+ while (current) {
215
+ // 如果是列表,可能是 TOC
216
+ if (current.tagName === 'UL' || current.tagName === 'OL') {
217
+ tocElements.unshift(current) // 添加到开头保持顺序
218
+ }
219
+ // 如果遇到其他块级元素(如 p, h1 等),停止查找
220
+ else if (
221
+ current.tagName === 'P' ||
222
+ current.tagName === 'H1' ||
223
+ current.tagName === 'DIV'
224
+ ) {
225
+ // 如果是空的 p 标签或只有空白,继续
226
+ if (current.textContent?.trim()) {
227
+ break
228
+ }
229
+ }
230
+ current = current.previousElementSibling
231
+ }
232
+
233
+ if (tocElements.length === 0) {
234
+ return false
235
+ }
236
+
237
+ // 保存第一个 TOC 元素的位置信息
238
+ const firstTocElement = tocElements[0]
239
+ const insertParent = firstTocElement.parentNode
240
+ const insertBefore = firstTocElement
241
+
242
+ // 创建折叠容器
243
+ const collapseWrapper = document.createElement('div')
244
+ collapseWrapper.className = 'toc-collapse-wrapper'
245
+
246
+ // 创建折叠头部
247
+ const collapseHeader = document.createElement('div')
248
+ collapseHeader.className = 'collapse-header toc-collapse-header'
249
+ collapseHeader.setAttribute('title', '点击折叠/展开目录')
250
+
251
+ // 创建标签
252
+ const collapseLabel = document.createElement('span')
253
+ collapseLabel.className = 'collapse-label'
254
+ collapseLabel.textContent = '目录'
255
+
256
+ // 创建折叠按钮
257
+ const collapseButton = document.createElement('button')
258
+ collapseButton.className = 'collapse-toggle toc-collapse-toggle'
259
+
260
+ // 使用 SVG 图标
261
+ const collapseIcon = document.createElement('img')
262
+ collapseIcon.src = icon__collapse
263
+ collapseIcon.alt = 'collapse icon'
264
+ collapseIcon.className = 'collapse-icon'
265
+ collapseButton.appendChild(collapseIcon)
266
+
267
+ // 组装头部
268
+ collapseHeader.appendChild(collapseLabel)
269
+ collapseHeader.appendChild(collapseButton)
270
+
271
+ // 创建内容容器
272
+ const contentWrapper = document.createElement('div')
273
+ contentWrapper.className = 'collapse-content toc-collapse-content'
274
+
275
+ // 组装结构
276
+ collapseWrapper.appendChild(collapseHeader)
277
+ collapseWrapper.appendChild(contentWrapper)
278
+
279
+ // 先插入折叠容器
280
+ insertParent?.insertBefore(collapseWrapper, insertBefore)
281
+
282
+ // 再移动 TOC 内容到容器中
283
+ tocElements.forEach((el) => {
284
+ contentWrapper.appendChild(el)
285
+ })
286
+
287
+ // 恢复折叠状态
288
+ const isCollapsed = getCollapseState('toc')
289
+ if (isCollapsed) {
290
+ contentWrapper.classList.add('collapsed')
291
+ collapseHeader.classList.add('collapsed')
292
+ }
293
+
294
+ // 绑定点击事件
295
+ let mouseDownTime = 0
296
+ let mouseDownX = 0
297
+ let mouseDownY = 0
298
+
299
+ collapseHeader.addEventListener('mousedown', (e) => {
300
+ mouseDownTime = Date.now()
301
+ mouseDownX = e.clientX
302
+ mouseDownY = e.clientY
303
+ })
304
+
305
+ collapseHeader.addEventListener('mouseup', (e) => {
306
+ const mouseUpTime = Date.now()
307
+ const duration = mouseUpTime - mouseDownTime
308
+ const moveX = Math.abs(e.clientX - mouseDownX)
309
+ const moveY = Math.abs(e.clientY - mouseDownY)
310
+
311
+ const isClick = duration < 200 && moveX < 5 && moveY < 5
312
+ const hasSelection = window.getSelection()?.toString().length ?? 0 > 0
313
+
314
+ if (isClick && !hasSelection) {
315
+ const isCollapsed = contentWrapper.classList.contains('collapsed')
316
+ contentWrapper.classList.toggle('collapsed')
317
+ collapseHeader.classList.toggle('collapsed')
318
+ saveCollapseState('toc', !isCollapsed)
319
+ }
320
+ })
321
+
322
+ return true
323
+ }
324
+
325
+ // 初始化二级标题折叠功能
326
+ function initH2Collapse() {
327
+ const vpDoc = document.querySelector('.vp-doc')
328
+ if (!vpDoc) {
329
+ return
330
+ }
331
+
332
+ const h2Elements = vpDoc.querySelectorAll('h2')
333
+
334
+ h2Elements.forEach((h2) => {
335
+ // 跳过已经处理过的
336
+ if (h2.querySelector('.collapse-toggle')) return
337
+
338
+ // 获取 h2 的 id 作为唯一标识
339
+ const h2Id = h2.id || `h2_${Array.from(h2Elements).indexOf(h2)}`
340
+
341
+ // 先清除可能存在的旧状态(避免状态冲突)
342
+ h2.classList.remove('collapsible-h2', 'collapsed')
343
+ h2.removeAttribute('title')
344
+
345
+ // 为 h2 添加可点击的类
346
+ h2.classList.add('collapsible-h2')
347
+ h2.setAttribute('title', '点击折叠/展开章节')
348
+
349
+ // 创建折叠按钮
350
+ const collapseButton = document.createElement('button')
351
+ collapseButton.className = 'collapse-toggle h2-collapse-toggle'
352
+
353
+ // 使用 SVG 图标
354
+ const collapseIcon = document.createElement('img')
355
+ collapseIcon.src = icon__collapse
356
+ collapseIcon.alt = 'collapse icon'
357
+ collapseIcon.className = 'collapse-icon'
358
+ collapseButton.appendChild(collapseIcon)
359
+
360
+ // 收集 h2 后面的内容直到下一个 h2
361
+ const contentElements: Element[] = []
362
+ let nextSibling = h2.nextElementSibling
363
+
364
+ while (nextSibling && nextSibling.tagName !== 'H2') {
365
+ contentElements.push(nextSibling)
366
+ nextSibling = nextSibling.nextElementSibling
367
+ }
368
+
369
+ // 如果没有内容,不添加折叠按钮
370
+ if (contentElements.length === 0) return
371
+
372
+ // 创建内容容器
373
+ const contentWrapper = document.createElement('div')
374
+ contentWrapper.className = 'collapse-content h2-collapse-content'
375
+
376
+ // 移动内容到容器中
377
+ contentElements.forEach((el) => {
378
+ contentWrapper.appendChild(el)
379
+ })
380
+
381
+ // 将折叠按钮插入到 h2 内部的末尾
382
+ h2.appendChild(collapseButton)
383
+
384
+ // 将内容容器插入到 h2 后面
385
+ h2.parentNode?.insertBefore(contentWrapper, h2.nextSibling)
386
+
387
+ // 恢复折叠状态(默认展开)
388
+ const isCollapsed = getCollapseState(`h2_${h2Id}`)
389
+ // console.log(
390
+ // `[initH2] h2Id: ${h2Id}, isCollapsed from localStorage: ${isCollapsed}`
391
+ // )
392
+
393
+ if (isCollapsed) {
394
+ contentWrapper.classList.add('collapsed')
395
+ h2.classList.add('collapsed')
396
+ // console.log(`[initH2] Added collapsed class to h2 and contentWrapper`)
397
+ // console.log(`[initH2] h2.classList:`, h2.classList.toString())
398
+ // console.log(
399
+ // `[initH2] contentWrapper.classList:`,
400
+ // contentWrapper.classList.toString()
401
+ // )
402
+ }
403
+
404
+ // 绑定点击事件到整个 h2,支持文本选择
405
+ let mouseDownTime = 0
406
+ let mouseDownX = 0
407
+ let mouseDownY = 0
408
+
409
+ h2.addEventListener('mousedown', (e) => {
410
+ // 如果点击的是链接,不记录时间(让链接正常工作)
411
+ const target = e.target as HTMLElement
412
+ if (target.tagName === 'A' || target.closest('a')) {
413
+ return
414
+ }
415
+ mouseDownTime = Date.now()
416
+ mouseDownX = e.clientX
417
+ mouseDownY = e.clientY
418
+ })
419
+
420
+ h2.addEventListener('mouseup', (e) => {
421
+ // 不阻止锚点链接的默认行为
422
+ const target = e.target as HTMLElement
423
+ if (target.tagName === 'A' || target.closest('a')) {
424
+ return
425
+ }
426
+
427
+ const mouseUpTime = Date.now()
428
+ const duration = mouseUpTime - mouseDownTime
429
+ const moveX = Math.abs(e.clientX - mouseDownX)
430
+ const moveY = Math.abs(e.clientY - mouseDownY)
431
+
432
+ // 判断是否为点击行为:
433
+ // 1. 持续时间小于 200ms
434
+ // 2. 鼠标移动距离小于 5px
435
+ // 3. 没有选中文本
436
+ const isClick = duration < 200 && moveX < 5 && moveY < 5
437
+ const hasSelection = window.getSelection()?.toString().length ?? 0 > 0
438
+
439
+ if (isClick && !hasSelection) {
440
+ // console.log(`[H2 Click] h2Id: ${h2Id}`)
441
+ // console.log('[H2 Click] contentWrapper:', contentWrapper)
442
+ // console.log('[H2 Click] h2 element:', h2)
443
+ // console.log(
444
+ // '[H2 Click] isCollapsed before toggle:',
445
+ // contentWrapper.classList.contains('collapsed')
446
+ // )
447
+
448
+ const isCollapsed = contentWrapper.classList.contains('collapsed')
449
+ contentWrapper.classList.toggle('collapsed')
450
+ h2.classList.toggle('collapsed')
451
+
452
+ // 强制触发重绘(浏览器优化可能导致样式不更新)
453
+ void h2.offsetHeight
454
+
455
+ saveCollapseState(`h2_${h2Id}`, !isCollapsed)
456
+
457
+ // console.log(
458
+ // '[H2 Click] isCollapsed after toggle:',
459
+ // contentWrapper.classList.contains('collapsed')
460
+ // )
461
+ // console.log('[H2 Click] h2.classList:', h2.classList.toString())
462
+ }
463
+ })
464
+ })
465
+ }
466
+
467
+ // 初始化所有折叠功能
468
+ function initAllCollapse() {
469
+ // 如果正在初始化,跳过
470
+ if (isInitializing) {
471
+ return
472
+ }
473
+
474
+ isInitializing = true
475
+
476
+ // 延迟执行以确保 DOM 已经渲染完成
477
+ const attempts = [100, 300, 500, 1000]
478
+ let attemptIndex = 0
479
+
480
+ function tryInit() {
481
+ const tocSuccess = initTocCollapse()
482
+ initH2Collapse()
483
+
484
+ // 如果 TOC 初始化失败且还有重试次数,继续尝试
485
+ if (!tocSuccess && attemptIndex < attempts.length - 1) {
486
+ attemptIndex++
487
+ setTimeout(tryInit, attempts[attemptIndex])
488
+ } else {
489
+ // 初始化完成,释放锁
490
+ isInitializing = false
491
+ }
492
+ }
493
+
494
+ setTimeout(tryInit, attempts[attemptIndex])
495
+ }
496
+
497
+ // 清理折叠功能
498
+ function cleanupCollapse() {
499
+ // 清理 TOC 折叠容器(需要先移出内容再删除容器)
500
+ const tocWrappers = document.querySelectorAll('.toc-collapse-wrapper')
501
+
502
+ tocWrappers.forEach((wrapper) => {
503
+ const contentContainer = wrapper.querySelector('.toc-collapse-content')
504
+ const parent = wrapper.parentElement
505
+
506
+ // 将 TOC 内容移回原位
507
+ if (contentContainer && parent) {
508
+ while (contentContainer.firstChild) {
509
+ parent.insertBefore(contentContainer.firstChild, wrapper)
510
+ }
511
+ }
512
+
513
+ // 删除整个包装器
514
+ wrapper.remove()
515
+ })
516
+
517
+ // 清理 H2 折叠
518
+ document.querySelectorAll('.h2-collapse-toggle').forEach((el) => el.remove())
519
+
520
+ // 清理 H2 标题上的类名和属性
521
+ document.querySelectorAll('h2.collapsible-h2').forEach((h2) => {
522
+ h2.classList.remove('collapsible-h2', 'collapsed')
523
+ h2.removeAttribute('title')
524
+ })
525
+
526
+ document.querySelectorAll('.h2-collapse-content').forEach((el) => {
527
+ // 将内容移回原位
528
+ const parent = el.parentElement
529
+ while (el.firstChild) {
530
+ parent?.insertBefore(el.firstChild, el)
531
+ }
532
+ el.remove()
533
+ })
534
+ } // 清除所有折叠状态
535
+ function clearAllCollapseStates() {
536
+ if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
537
+ return
538
+ }
539
+ const keys = Object.keys(localStorage).filter((key) =>
540
+ key.startsWith(COLLAPSE_STATE_PREFIX)
541
+ )
542
+ keys.forEach((key) => localStorage.removeItem(key))
543
+
544
+ // 重新初始化
545
+ cleanupCollapse()
546
+ initAllCollapse()
547
+ }
548
+
549
+ // 组件挂载时初始化
550
+ onMounted(() => {
551
+ // 确保在客户端环境中才初始化
552
+ if (typeof window !== 'undefined') {
553
+ // 不直接调用 initAllCollapse,让 onContentUpdated 处理
554
+ // 因为首次加载时 onContentUpdated 也会触发
555
+
556
+ // 添加全局函数,方便调试
557
+ ;(window as any).clearAllCollapseStates = clearAllCollapseStates
558
+ }
559
+ })
560
+
561
+ // 内容更新时重新初始化(支持 HMR 和路由切换)
562
+ onContentUpdated(() => {
563
+ // 清除之前的定时器
564
+ if (reinitTimer) {
565
+ clearTimeout(reinitTimer)
566
+ }
567
+
568
+ // 使用防抖,避免短时间内多次触发
569
+ reinitTimer = setTimeout(() => {
570
+ cleanupCollapse()
571
+ initAllCollapse()
572
+ reinitTimer = null
573
+ }, 150)
574
+ })
575
+
576
+ // 组件卸载时清理
577
+ onUnmounted(() => {
578
+ cleanupCollapse()
579
+
580
+ // 清除定时器
581
+ if (reinitTimer) {
582
+ clearTimeout(reinitTimer)
583
+ reinitTimer = null
584
+ }
585
+
586
+ // 清理全局函数
587
+ if (typeof window !== 'undefined') {
588
+ delete (window as any).clearAllCollapseStates
589
+ }
590
+ })
591
+ </script>