@tnotesjs/core 0.1.0

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.

Potentially problematic release.


This version of @tnotesjs/core might be problematic. Click here for more details.

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