@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.
- package/README.md +105 -0
- package/dist/chunk-K3X5OP3N.js +1532 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4199 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +9 -0
- package/package.json +74 -0
- package/types/config.ts +61 -0
- package/types/index.ts +11 -0
- package/types/note.ts +33 -0
- package/vitepress/assets/icons/icon__check.svg +3 -0
- package/vitepress/assets/icons/icon__clipboard.svg +8 -0
- package/vitepress/assets/icons/icon__close.svg +1 -0
- package/vitepress/assets/icons/icon__collapse.svg +1 -0
- package/vitepress/assets/icons/icon__confirm.svg +1 -0
- package/vitepress/assets/icons/icon__copy.svg +4 -0
- package/vitepress/assets/icons/icon__focus.svg +1 -0
- package/vitepress/assets/icons/icon__fold.svg +3 -0
- package/vitepress/assets/icons/icon__folder.svg +1 -0
- package/vitepress/assets/icons/icon__fullscreen.svg +1 -0
- package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -0
- package/vitepress/assets/icons/icon__github.svg +4 -0
- package/vitepress/assets/icons/icon__mindmap.svg +1 -0
- package/vitepress/assets/icons/icon__next.svg +1 -0
- package/vitepress/assets/icons/icon__number_gray.svg +1 -0
- package/vitepress/assets/icons/icon__number_purple.svg +1 -0
- package/vitepress/assets/icons/icon__prev.svg +1 -0
- package/vitepress/assets/icons/icon__restore.svg +1 -0
- package/vitepress/assets/icons/icon__rotate.svg +4 -0
- package/vitepress/assets/icons/icon__search.svg +1 -0
- package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -0
- package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -0
- package/vitepress/assets/icons/icon__totop.svg +6 -0
- package/vitepress/assets/icons/icon__vscode.svg +6 -0
- package/vitepress/assets/icons/icon__zoom_fit.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_in.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_out.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_reset.svg +1 -0
- package/vitepress/assets/icons/index.ts +38 -0
- package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -0
- package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -0
- package/vitepress/components/CodeBlockFullscreen/index.ts +115 -0
- package/vitepress/components/CodeBlockFullscreen/styles.css +64 -0
- package/vitepress/components/Discussions/Discussions.module.scss +32 -0
- package/vitepress/components/Discussions/Discussions.vue +211 -0
- package/vitepress/components/EnWordList/EnWordList.module.scss +124 -0
- package/vitepress/components/EnWordList/EnWordList.vue +543 -0
- package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -0
- package/vitepress/components/EnWordList/RightClickMenu.vue +66 -0
- package/vitepress/components/Footprints/Footprints.module.scss +93 -0
- package/vitepress/components/Footprints/Footprints.vue +377 -0
- package/vitepress/components/Layout/AboutModal.module.scss +233 -0
- package/vitepress/components/Layout/AboutModal.vue +105 -0
- package/vitepress/components/Layout/AboutPanel.vue +266 -0
- package/vitepress/components/Layout/ContentCollapse.vue +603 -0
- package/vitepress/components/Layout/CustomSidebar.vue +605 -0
- package/vitepress/components/Layout/DocBeforeControls.vue +139 -0
- package/vitepress/components/Layout/DocFooter.vue +225 -0
- package/vitepress/components/Layout/ImagePreview.module.scss +201 -0
- package/vitepress/components/Layout/ImagePreview.vue +281 -0
- package/vitepress/components/Layout/Layout.module.scss +661 -0
- package/vitepress/components/Layout/Layout.vue +542 -0
- package/vitepress/components/Layout/NoteStatus.vue +140 -0
- package/vitepress/components/Layout/SidebarItems.vue +263 -0
- package/vitepress/components/Layout/SidebarNavBefore.vue +92 -0
- package/vitepress/components/Layout/Swiper.vue +167 -0
- package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -0
- package/vitepress/components/Layout/ToggleFullContent.vue +34 -0
- package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -0
- package/vitepress/components/Layout/ToggleSidebar.vue +35 -0
- package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -0
- package/vitepress/components/Layout/composables/useNoteConfig.ts +121 -0
- package/vitepress/components/Layout/composables/useNoteSave.ts +173 -0
- package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -0
- package/vitepress/components/Layout/composables/useRedirect.ts +110 -0
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -0
- package/vitepress/components/Layout/homeReadme.data.ts +124 -0
- package/vitepress/components/LoadingPage/LoadingPage.vue +192 -0
- package/vitepress/components/MarkMap/MarkMap.module.scss +159 -0
- package/vitepress/components/MarkMap/MarkMap.vue +404 -0
- package/vitepress/components/Mermaid/Mermaid.module.scss +275 -0
- package/vitepress/components/Mermaid/Mermaid.vue +364 -0
- package/vitepress/components/NotesTable/NotesTable.module.scss +77 -0
- package/vitepress/components/NotesTable/NotesTable.vue +98 -0
- package/vitepress/components/NotesTable/README.md +67 -0
- package/vitepress/components/Settings/Settings.module.scss +433 -0
- package/vitepress/components/Settings/Settings.vue +306 -0
- package/vitepress/components/SidebarCard/MindMapView.vue +483 -0
- package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -0
- package/vitepress/components/SidebarCard/SidebarCard.vue +948 -0
- package/vitepress/components/Tooltip/Tooltip.vue +70 -0
- package/vitepress/components/constants.ts +91 -0
- package/vitepress/components/notesConfig.data.ts +73 -0
- package/vitepress/components/sidebar.data.ts +59 -0
- package/vitepress/components/tnotes-config.data.ts +21 -0
- package/vitepress/components/utils.ts +26 -0
- package/vitepress/config/index.ts +126 -0
- package/vitepress/configs/constants.ts +26 -0
- package/vitepress/configs/head.config.ts +25 -0
- package/vitepress/configs/index.ts +9 -0
- package/vitepress/configs/markdown-it.d.ts +23 -0
- package/vitepress/configs/markdown.config.ts +366 -0
- package/vitepress/configs/theme.config.ts +108 -0
- package/vitepress/plugins/buildProgressPlugin.ts +390 -0
- package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -0
- package/vitepress/plugins/renameNotePlugin.ts +60 -0
- package/vitepress/plugins/updateConfigPlugin.ts +63 -0
- package/vitepress/theme/index.ts +95 -0
- package/vitepress/theme/styles/base.scss +50 -0
- package/vitepress/theme/styles/components/404.scss +31 -0
- package/vitepress/theme/styles/components/collapse.scss +175 -0
- package/vitepress/theme/styles/components/markmap.scss +101 -0
- package/vitepress/theme/styles/components/swiper.scss +255 -0
- package/vitepress/theme/styles/index.scss +25 -0
- package/vitepress/theme/styles/layout.scss +62 -0
- package/vitepress/theme/styles/utilities.scss +39 -0
- 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>
|