@tnotesjs/core 0.1.19 → 0.1.20

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