@tnotesjs/core 0.1.15 → 0.1.17

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 (117) hide show
  1. package/README.md +87 -87
  2. package/dist/{chunk-FVKQPBPP.js → chunk-4MO7F7Q4.js} +2005 -1769
  3. package/dist/cli/index.js +1 -1
  4. package/dist/vitepress/config/index.js +212 -4
  5. package/package.json +1 -1
  6. package/types/config.ts +61 -61
  7. package/types/index.ts +11 -11
  8. package/types/note.ts +33 -33
  9. package/vitepress/assets/icons/icon__check.svg +3 -3
  10. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  11. package/vitepress/assets/icons/icon__close.svg +1 -1
  12. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  13. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  14. package/vitepress/assets/icons/icon__copy.svg +4 -4
  15. package/vitepress/assets/icons/icon__focus.svg +1 -1
  16. package/vitepress/assets/icons/icon__fold.svg +3 -3
  17. package/vitepress/assets/icons/icon__folder.svg +1 -1
  18. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  19. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  20. package/vitepress/assets/icons/icon__github.svg +3 -3
  21. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  22. package/vitepress/assets/icons/icon__next.svg +1 -1
  23. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  24. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  25. package/vitepress/assets/icons/icon__prev.svg +1 -1
  26. package/vitepress/assets/icons/icon__restore.svg +1 -1
  27. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  28. package/vitepress/assets/icons/icon__search.svg +1 -1
  29. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  30. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  31. package/vitepress/assets/icons/icon__totop.svg +5 -5
  32. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  33. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  34. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  35. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  37. package/vitepress/assets/icons/index.ts +39 -39
  38. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  39. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -373
  40. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -115
  41. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
  42. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  43. package/vitepress/components/Discussions/Discussions.vue +211 -211
  44. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  45. package/vitepress/components/EnWordList/EnWordList.vue +543 -543
  46. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  47. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  48. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  49. package/vitepress/components/Footprints/Footprints.vue +377 -377
  50. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  51. package/vitepress/components/Layout/AboutModal.vue +105 -105
  52. package/vitepress/components/Layout/AboutPanel.vue +270 -270
  53. package/vitepress/components/Layout/ContentCollapse.vue +603 -603
  54. package/vitepress/components/Layout/CustomSidebar.vue +817 -608
  55. package/vitepress/components/Layout/DocBeforeControls.vue +149 -149
  56. package/vitepress/components/Layout/DocFooter.vue +233 -233
  57. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  58. package/vitepress/components/Layout/ImagePreview.vue +281 -281
  59. package/vitepress/components/Layout/Layout.module.scss +661 -661
  60. package/vitepress/components/Layout/Layout.vue +621 -559
  61. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  62. package/vitepress/components/Layout/SidebarItems.vue +1083 -268
  63. package/vitepress/components/Layout/SidebarNavBefore.vue +171 -108
  64. package/vitepress/components/Layout/SidebarResizeHandle.vue +319 -0
  65. package/vitepress/components/Layout/Swiper.vue +167 -167
  66. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  67. package/vitepress/components/Layout/ToggleFullContent.vue +35 -35
  68. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  69. package/vitepress/components/Layout/ToggleSidebar.vue +26 -36
  70. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  71. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  72. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  73. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  74. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  75. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -0
  76. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  77. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  78. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  79. package/vitepress/components/MarkMap/MarkMap.vue +405 -405
  80. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  81. package/vitepress/components/Mermaid/Mermaid.vue +365 -365
  82. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  83. package/vitepress/components/NotesTable/NotesTable.vue +98 -98
  84. package/vitepress/components/NotesTable/README.md +67 -67
  85. package/vitepress/components/Settings/Settings.module.scss +375 -433
  86. package/vitepress/components/Settings/Settings.vue +333 -306
  87. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  88. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  89. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -946
  90. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  91. package/vitepress/components/constants.ts +109 -91
  92. package/vitepress/components/notesConfig.data.ts +73 -73
  93. package/vitepress/components/sidebar.data.ts +59 -59
  94. package/vitepress/components/tnotes-config.data.ts +21 -21
  95. package/vitepress/components/utils.ts +26 -26
  96. package/vitepress/config/index.ts +169 -167
  97. package/vitepress/configs/constants.ts +26 -26
  98. package/vitepress/configs/head.config.ts +25 -25
  99. package/vitepress/configs/index.ts +9 -9
  100. package/vitepress/configs/markdown-it.d.ts +23 -23
  101. package/vitepress/configs/markdown.config.ts +366 -366
  102. package/vitepress/configs/theme.config.ts +108 -108
  103. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  104. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  105. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  106. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -0
  107. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  108. package/vitepress/theme/index.ts +137 -137
  109. package/vitepress/theme/styles/base.scss +51 -50
  110. package/vitepress/theme/styles/components/404.scss +31 -31
  111. package/vitepress/theme/styles/components/collapse.scss +185 -175
  112. package/vitepress/theme/styles/components/markmap.scss +101 -101
  113. package/vitepress/theme/styles/components/swiper.scss +255 -255
  114. package/vitepress/theme/styles/index.scss +25 -25
  115. package/vitepress/theme/styles/layout.scss +78 -62
  116. package/vitepress/theme/styles/utilities.scss +39 -39
  117. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,608 +1,817 @@
1
- <template>
2
- <div class="custom-sidebar-wrapper">
3
- <nav class="nav" ref="navRef">
4
- <!-- 使用递归组件渲染侧边栏,支持任意层级嵌套 -->
5
- <SidebarItems
6
- :items="sidebarGroups"
7
- :depth="0"
8
- :max-depth="maxDepth"
9
- :show-note-id="showNoteId"
10
- :base="base"
11
- :current-path="route.path"
12
- />
13
- </nav>
14
- </div>
15
- </template>
16
-
17
- <script setup lang="ts">
18
- import { useRoute, useData } from 'vitepress'
19
- import { ref, onMounted, watch, computed } from 'vue'
20
-
21
- import SidebarItems from './SidebarItems.vue'
22
- import { SIDEBAR_SHOW_NOTE_ID_KEY, SIDEBAR_MAX_DEPTH_KEY } from '../constants'
23
- // @ts-expect-error - VitePress Data Loader
24
- import { data as sidebarConfig } from '../sidebar.data'
25
- // @ts-expect-error - VitePress Data Loader
26
- import { data as tnotesConfig } from '../tnotes-config.data'
27
-
28
- // 支持递归的侧边栏项类型
29
- interface SidebarItem {
30
- text: string
31
- link?: string
32
- items?: SidebarItem[]
33
- collapsed?: boolean
34
- }
35
-
36
- const route = useRoute()
37
- const { site } = useData()
38
- const sidebarGroups = ref<SidebarItem[]>([])
39
- const navRef = ref<HTMLElement | null>(null)
40
- const currentFocusIndex = ref(0)
41
-
42
- // 最大解析层级(默认 3 层)
43
- const maxDepth = computed(() => {
44
- if (typeof window === 'undefined') {
45
- return tnotesConfig.sidebarMaxDepth ?? 3
46
- }
47
-
48
- const savedMaxDepth = localStorage.getItem(SIDEBAR_MAX_DEPTH_KEY)
49
- if (savedMaxDepth !== null) {
50
- return parseInt(savedMaxDepth, 10)
51
- }
52
-
53
- return tnotesConfig.sidebarMaxDepth ?? 3
54
- })
55
-
56
- // 获取配置:是否显示笔记 ID
57
- const showNoteId = computed(() => {
58
- if (typeof window === 'undefined') {
59
- return tnotesConfig.sidebarShowNoteId ?? false
60
- }
61
-
62
- const savedShowNoteId = localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
63
- if (savedShowNoteId !== null) {
64
- return savedShowNoteId === 'true'
65
- }
66
-
67
- return tnotesConfig.sidebarShowNoteId ?? false
68
- })
69
-
70
- // 获取 base 路径
71
- const base = computed(() => site.value.base || '/')
72
-
73
- // 加载 sidebar 数据
74
- function loadSidebar() {
75
- if (sidebarConfig && sidebarConfig['/notes/']) {
76
- sidebarGroups.value = processItems(sidebarConfig['/notes/'])
77
- }
78
- }
79
-
80
- // 递归处理侧边栏项,添加 collapsed 状态
81
- function processItems(items: any[]): SidebarItem[] {
82
- return items.map((item) => ({
83
- ...item,
84
- collapsed: item.collapsed ?? true,
85
- items: item.items ? processItems(item.items) : undefined,
86
- }))
87
- }
88
-
89
- // 判断项是否有子项
90
- function hasChildren(item: SidebarItem): boolean {
91
- return !!(item.items && item.items.length > 0)
92
- }
93
-
94
- // 获取项的唯一 key
95
- function getItemKey(item: SidebarItem): string {
96
- return item.link || item.text
97
- }
98
-
99
- // 切换项的展开/折叠状态(支持递归)
100
- function toggleItem(item: SidebarItem) {
101
- item.collapsed = !item.collapsed
102
- }
103
-
104
- // 递归查找并切换项
105
- function toggleItemRecursive(items: SidebarItem[], text: string): boolean {
106
- for (const item of items) {
107
- if (item.text === text) {
108
- item.collapsed = !item.collapsed
109
- return true
110
- }
111
- if (item.items && toggleItemRecursive(item.items, text)) {
112
- return true
113
- }
114
- }
115
- return false
116
- }
117
-
118
- // 切换组展开/折叠(保留兼容性)
119
- function toggleGroup(groupText: string) {
120
- toggleItemRecursive(sidebarGroups.value, groupText)
121
- }
122
-
123
- // 递归展开/折叠所有项
124
- function setAllCollapsed(items: SidebarItem[], collapsed: boolean) {
125
- items.forEach((item) => {
126
- if (item.items) {
127
- item.collapsed = collapsed
128
- setAllCollapsed(item.items, collapsed)
129
- }
130
- })
131
- }
132
-
133
- // 检查是否有任何一级章节处于展开状态
134
- function hasAnyFirstLevelExpanded(): boolean {
135
- return sidebarGroups.value.some((group) => !group.collapsed)
136
- }
137
-
138
- // 展开全部
139
- function expandAll() {
140
- setAllCollapsed(sidebarGroups.value, false)
141
- }
142
-
143
- // 折叠全部
144
- function collapseAll() {
145
- setAllCollapsed(sidebarGroups.value, true)
146
- }
147
-
148
- // 智能切换:如果有展开的一级章节则折叠全部,否则展开全部
149
- function toggleExpandCollapse() {
150
- if (hasAnyFirstLevelExpanded()) {
151
- collapseAll()
152
- } else {
153
- expandAll()
154
- }
155
- }
156
-
157
- // 获取当前笔记的所有出现位置
158
- function getCurrentNotePositions(): HTMLElement[] {
159
- const currentPath = route.path
160
- const elements: HTMLElement[] = []
161
-
162
- if (!navRef.value) {
163
- console.log('❌ [getCurrentNotePositions] navRef is null')
164
- return elements
165
- }
166
-
167
- console.log('🔍 [getCurrentNotePositions] Current route path:', currentPath)
168
-
169
- // 查找所有激活的笔记项
170
- const activeItems = navRef.value.querySelectorAll('.nav-item.active')
171
- console.log(
172
- '🔍 [getCurrentNotePositions] Active nav-items:',
173
- activeItems.length
174
- )
175
-
176
- activeItems.forEach((item, index) => {
177
- const href = item.getAttribute('href')
178
- console.log(`🔍 [${index}] Active item href:`, href)
179
- elements.push(item as HTMLElement)
180
- })
181
-
182
- console.log('🎯 [getCurrentNotePositions] Found positions:', elements.length)
183
- return elements
184
- }
185
-
186
- // 展开指定元素的父级分组
187
- function expandParentGroup(element: HTMLElement) {
188
- console.log('📂 [expandParentGroup] Starting to expand parent groups')
189
-
190
- // 查找所有父级 group 元素(从最近的开始)
191
- let currentElement: HTMLElement | null = element
192
- const groupsToExpand: string[] = []
193
-
194
- // 向上遍历,收集所有父级 group 的标题文本
195
- while (currentElement) {
196
- const groupElement: HTMLElement | null =
197
- currentElement.closest<HTMLElement>('.group')
198
- if (!groupElement) break
199
-
200
- const groupTitle = groupElement.querySelector('.group-title span')
201
- if (groupTitle) {
202
- const groupText = groupTitle.textContent?.trim()
203
- if (groupText) {
204
- console.log('📌 [expandParentGroup] Found parent group:', groupText)
205
- groupsToExpand.push(groupText)
206
- }
207
- }
208
-
209
- // 继续向上查找
210
- currentElement =
211
- groupElement.parentElement?.closest<HTMLElement>('.group') || null
212
- }
213
-
214
- console.log(
215
- '📋 [expandParentGroup] Groups to expand (inner to outer):',
216
- groupsToExpand
217
- )
218
-
219
- // 从最外层开始展开,逐层向内
220
- // 但是搜索时要确保在正确的上下文中搜索
221
- if (groupsToExpand.length === 0) return
222
-
223
- // 反转数组,从最外层开始处理
224
- const outerToInner = [...groupsToExpand].reverse()
225
- console.log(
226
- '📋 [expandParentGroup] Processing order (outer to inner):',
227
- outerToInner
228
- )
229
-
230
- // 第一层必须从根开始搜索
231
- let currentContext: SidebarItem[] | null = null
232
-
233
- for (let i = 0; i < outerToInner.length; i++) {
234
- const groupText = outerToInner[i]
235
- console.log(`🔄 [expandParentGroup] [${i}] Expanding: "${groupText}"`)
236
-
237
- if (i === 0) {
238
- // 第一层从根搜索
239
- console.log(` 🌳 Searching from root`)
240
- const found = expandGroupRecursive(sidebarGroups.value, groupText)
241
- if (found) {
242
- // 找到后,获取这个分组的 items 作为下一层的搜索上下文
243
- const foundGroup = findGroupByText(sidebarGroups.value, groupText)
244
- if (foundGroup?.items) {
245
- currentContext = foundGroup.items
246
- console.log(
247
- ` ✅ Found and set context for next level (${foundGroup.items.length} items)`
248
- )
249
- }
250
- }
251
- } else {
252
- // 后续层从上一层的上下文中搜索
253
- if (currentContext) {
254
- console.log(
255
- ` 🔍 Searching in context (${currentContext.length} items)`
256
- )
257
- const found = expandGroupRecursive(currentContext, groupText)
258
- if (found) {
259
- const foundGroup = findGroupByText(currentContext, groupText)
260
- if (foundGroup?.items) {
261
- currentContext = foundGroup.items
262
- console.log(
263
- ` ✅ Found and set context for next level (${foundGroup.items.length} items)`
264
- )
265
- }
266
- }
267
- }
268
- }
269
- }
270
- }
271
-
272
- // 查找分组(不展开,只返回引用)
273
- function findGroupByText(
274
- items: SidebarItem[],
275
- targetText: string
276
- ): SidebarItem | null {
277
- for (const item of items) {
278
- if (item.text === targetText) {
279
- return item
280
- }
281
- if (item.items) {
282
- const found = findGroupByText(item.items, targetText)
283
- if (found) return found
284
- }
285
- }
286
- return null
287
- }
288
-
289
- // 递归查找并展开分组
290
- function expandGroupRecursive(
291
- items: SidebarItem[],
292
- targetText: string,
293
- depth: number = 0
294
- ): boolean {
295
- const indent = ' '.repeat(depth)
296
- console.log(
297
- `${indent}🔍 [expandGroupRecursive] Searching for "${targetText}" at depth ${depth}`
298
- )
299
-
300
- for (const item of items) {
301
- console.log(`${indent} 📝 Checking item: "${item.text}"`)
302
-
303
- if (item.text === targetText) {
304
- console.log(`${indent} ✅ Found target! Setting collapsed = false`)
305
- item.collapsed = false
306
- return true
307
- }
308
-
309
- if (item.items) {
310
- console.log(
311
- `${indent} 📂 Item has ${item.items.length} children, searching...`
312
- )
313
- const found = expandGroupRecursive(item.items, targetText, depth + 1)
314
- if (found) {
315
- console.log(
316
- `${indent} ✅ Target found in children, expanding current item "${item.text}"`
317
- )
318
- // 如果在子项中找到了,也展开当前项
319
- item.collapsed = false
320
- return true
321
- }
322
- }
323
- }
324
-
325
- console.log(
326
- `${indent}❌ [expandGroupRecursive] Target "${targetText}" not found at depth ${depth}`
327
- )
328
- return false
329
- }
330
-
331
- // 滚动到指定元素
332
- function scrollToElement(element: HTMLElement) {
333
- if (!element || !navRef.value) {
334
- // console.log('❌ [scrollToElement] No element or navRef')
335
- return
336
- }
337
-
338
- const navContainer = navRef.value.closest('.VPSidebar')
339
- if (!navContainer) {
340
- // console.log(' [scrollToElement] No VPSidebar container found')
341
- return
342
- }
343
-
344
- // console.log('📍 [scrollToElement] Scrolling to element')
345
-
346
- // 计算元素相对于容器的位置
347
- const elementRect = element.getBoundingClientRect()
348
- const containerRect = navContainer.getBoundingClientRect()
349
-
350
- // 计算需要滚动的距离(将元素放在容器中间)
351
- const scrollTop =
352
- navContainer.scrollTop +
353
- elementRect.top -
354
- containerRect.top -
355
- containerRect.height / 2 +
356
- elementRect.height / 2
357
-
358
- navContainer.scrollTo({
359
- top: scrollTop,
360
- behavior: 'smooth',
361
- })
362
-
363
- // 添加临时高亮动画
364
- element.classList.add('focus-highlight')
365
- setTimeout(() => {
366
- element.classList.remove('focus-highlight')
367
- }, 1000)
368
- }
369
-
370
- // 聚焦到当前笔记(支持多个位置切换)
371
- function focusCurrentNote() {
372
- console.log('🎯 [focusCurrentNote] Called')
373
- const positions = getCurrentNotePositions()
374
-
375
- if (positions.length === 0) {
376
- console.log('❌ [focusCurrentNote] No positions found')
377
- return
378
- }
379
-
380
- // 循环切换聚焦位置
381
- currentFocusIndex.value = (currentFocusIndex.value + 1) % positions.length
382
- const targetElement = positions[currentFocusIndex.value]
383
-
384
- console.log(
385
- `🎯 [focusCurrentNote] Focusing position ${currentFocusIndex.value + 1}/${
386
- positions.length
387
- }`
388
- )
389
-
390
- // 展开该笔记所在的分组
391
- expandParentGroup(targetElement)
392
-
393
- // 滚动到该笔记
394
- setTimeout(() => {
395
- scrollToElement(targetElement)
396
- }, 100)
397
- }
398
-
399
- // 展开当前激活笔记的所有父级分组
400
- function expandActiveItemParents() {
401
- expandActiveItemParentsRecursive(sidebarGroups.value)
402
- }
403
-
404
- // 递归展开包含激活项的父级
405
- function expandActiveItemParentsRecursive(items: SidebarItem[]): boolean {
406
- let hasActive = false
407
-
408
- for (const item of items) {
409
- if (item.link) {
410
- // 检查当前项是否激活
411
- const fullLink = getFullLink(item.link)
412
- const decodedRoutePath = decodeURIComponent(route.path)
413
- const decodedFullLink = decodeURIComponent(fullLink)
414
- const itemActive =
415
- decodedRoutePath === decodedFullLink ||
416
- decodedRoutePath === decodedFullLink + '.html'
417
-
418
- if (itemActive) {
419
- hasActive = true
420
- }
421
- } else if (item.items) {
422
- const childHasActive = expandActiveItemParentsRecursive(item.items)
423
- if (childHasActive) {
424
- item.collapsed = false
425
- hasActive = true
426
- }
427
- }
428
- }
429
-
430
- return hasActive
431
- }
432
-
433
- // 获取完整链接(包含 base)
434
- function getFullLink(link: string) {
435
- const cleanLink = link.startsWith('/') ? link.slice(1) : link
436
- return base.value + cleanLink
437
- }
438
-
439
- // 滚动到当前激活的笔记
440
- function scrollToActiveItem() {
441
- // 等待 DOM 更新
442
- setTimeout(() => {
443
- const positions = getCurrentNotePositions()
444
- if (positions.length > 0) {
445
- // 展开所有包含当前笔记的分组
446
- expandActiveItemParents()
447
-
448
- // 滚动到第一个位置
449
- setTimeout(() => {
450
- scrollToElement(positions[0])
451
- }, 100)
452
- }
453
- }, 300)
454
- }
455
-
456
- // 暴露函数给父组件使用
457
- defineExpose({
458
- expandAll,
459
- collapseAll,
460
- toggleExpandCollapse,
461
- hasAnyFirstLevelExpanded,
462
- focusCurrentNote,
463
- })
464
-
465
- onMounted(() => {
466
- loadSidebar()
467
-
468
- // 调试:打印配置信息
469
- // console.log('🔧 [CustomSidebar] showNoteId:', showNoteId.value)
470
- // if (typeof window !== 'undefined') {
471
- // console.log(
472
- // '🔧 [CustomSidebar] localStorage value:',
473
- // localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
474
- // )
475
- // }
476
- // console.log(
477
- // '🔧 [CustomSidebar] tnotesConfig value:',
478
- // tnotesConfig.sidebarShowNoteId
479
- // )
480
-
481
- // 自动滚动到当前激活的笔记
482
- scrollToActiveItem()
483
- })
484
-
485
- // 监听 sidebarConfig 的变化(HMR 会更新这个导入的数据)
486
- watch(
487
- () => sidebarConfig,
488
- () => {
489
- // console.log('🔄 [CustomSidebar] Sidebar config changed, reloading...')
490
- loadSidebar()
491
- },
492
- { deep: true }
493
- )
494
-
495
- // 监听 tnotesConfig 的变化
496
- watch(
497
- () => tnotesConfig,
498
- () => {
499
- // console.log(
500
- // '🔄 [CustomSidebar] TNotes config changed, sidebarShowNoteId:',
501
- // tnotesConfig.sidebarShowNoteId
502
- // )
503
- },
504
- { deep: true }
505
- )
506
-
507
- // 监听路由变化,自动展开当前激活项所在的组并滚动
508
- watch(
509
- () => route.path,
510
- () => {
511
- // 重置聚焦索引
512
- currentFocusIndex.value = 0
513
-
514
- // 展开并滚动到当前笔记
515
- expandActiveItemParents()
516
- scrollToActiveItem()
517
- }
518
- )
519
- </script>
520
-
521
- <style scoped>
522
- /* 自定义 sidebar 容器,适配 VitePress 的 sidebar-nav-before 插槽 */
523
- .custom-sidebar-wrapper {
524
- /* 不需要设置 position 和尺寸,因为它在 VitePress 的 sidebar 容器内 */
525
- }
526
-
527
- .nav {
528
- font-size: 14px;
529
- line-height: 2;
530
- }
531
-
532
- .group {
533
- margin-bottom: 16px;
534
- }
535
-
536
- .group-title {
537
- display: flex;
538
- justify-content: space-between;
539
- align-items: center;
540
- width: 100%;
541
- padding: 6px 0;
542
- font-weight: 600;
543
- color: var(--vp-c-text-1);
544
- background: none;
545
- border: none;
546
- cursor: pointer;
547
- text-align: left;
548
- transition: color 0.25s;
549
- }
550
-
551
- .group-title:hover {
552
- color: var(--vp-c-brand-1);
553
- }
554
-
555
- .arrow {
556
- font-size: 10px;
557
- transform: rotate(90deg);
558
- transition: transform 0.25s;
559
- }
560
-
561
- .arrow.collapsed {
562
- transform: rotate(0deg);
563
- }
564
-
565
- .nav-item {
566
- display: block;
567
- padding: 4px;
568
- color: var(--vp-c-text-2);
569
- text-decoration: none;
570
- border-radius: 4px;
571
- font-size: 14px;
572
- line-height: 24px;
573
- transition: all 0.25s;
574
- }
575
-
576
- .nav-item:hover {
577
- color: var(--vp-c-brand-1);
578
- background-color: var(--vp-c-default-soft);
579
- }
580
-
581
- .nav-item.active {
582
- color: var(--vp-c-brand-1);
583
- font-weight: 600;
584
- }
585
-
586
- /* 聚焦高亮动画 */
587
- .nav-item.focus-highlight {
588
- animation: focusPulse 1s ease-in-out;
589
- }
590
-
591
- @keyframes focusPulse {
592
- 0%,
593
- 100% {
594
- background-color: transparent;
595
- }
596
- 50% {
597
- background-color: var(--vp-c-brand-soft);
598
- }
599
- }
600
- </style>
601
-
602
- <!-- 全局样式:隐藏 VitePress 默认的 sidebar nav -->
603
- <style>
604
- /* 隐藏 VitePress 默认的 sidebar 导航内容(保留容器) */
605
- .VPSidebarNav {
606
- display: none !important;
607
- }
608
- </style>
1
+ <template>
2
+ <div class="custom-sidebar-wrapper">
3
+ <nav class="nav" ref="navRef">
4
+ <!-- 使用递归组件渲染侧边栏,支持任意层级嵌套 -->
5
+ <SidebarItems
6
+ :items="sidebarGroups"
7
+ :depth="0"
8
+ :max-depth="maxDepth"
9
+ :show-note-id="showNoteId"
10
+ :base="base"
11
+ :current-path="route.path"
12
+ :is-dev="isDev"
13
+ :sidebar-density="sidebarDensity"
14
+ :done-prefix="donePrefix"
15
+ :undone-prefix="undonePrefix"
16
+ @rename-group="renameGroup"
17
+ @delete-group="deleteGroup"
18
+ @create-note-in-group="createNoteInGroup"
19
+ @create-notes-in-group="createNotesInGroup"
20
+ @rename-note="renameNote"
21
+ @delete-note="deleteNote"
22
+ @create-note-around="createNoteAround"
23
+ @open-note-about="openNoteAbout"
24
+ @reorder-sidebar="reorderSidebar"
25
+ />
26
+ </nav>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { useRoute, useData } from 'vitepress'
32
+ import { ref, onMounted, watch, computed } from 'vue'
33
+
34
+ import SidebarItems from './SidebarItems.vue'
35
+ import {
36
+ SIDEBAR_SHOW_NOTE_ID_KEY,
37
+ SIDEBAR_MAX_DEPTH_KEY,
38
+ SIDEBAR_DENSITY_KEY,
39
+ SIDEBAR_DONE_PREFIX_KEY,
40
+ SIDEBAR_UNDONE_PREFIX_KEY,
41
+ } from '../constants'
42
+ // @ts-expect-error - VitePress Data Loader
43
+ import { data as sidebarConfig } from '../sidebar.data'
44
+ // @ts-expect-error - VitePress Data Loader
45
+ import { data as tnotesConfig } from '../tnotes-config.data'
46
+
47
+ // 支持递归的侧边栏项类型
48
+ interface SidebarItem {
49
+ text: string
50
+ link?: string
51
+ items?: SidebarItem[]
52
+ collapsed?: boolean
53
+ }
54
+
55
+ const route = useRoute()
56
+ const { site } = useData()
57
+ const sidebarGroups = ref<SidebarItem[]>([])
58
+ const navRef = ref<HTMLElement | null>(null)
59
+ const currentFocusIndex = ref(0)
60
+
61
+ const emit = defineEmits<{
62
+ 'open-note-about': [noteIndex: string]
63
+ }>()
64
+
65
+ interface GroupActionPayload {
66
+ groupPath: string[]
67
+ text: string
68
+ }
69
+
70
+ interface NoteActionPayload {
71
+ noteIndex: string
72
+ text: string
73
+ link?: string
74
+ placement?: 'before' | 'after'
75
+ }
76
+
77
+ interface SidebarActionResult {
78
+ success: boolean
79
+ message?: string
80
+ redirectUrl?: string
81
+ newUrl?: string
82
+ }
83
+
84
+ interface ReorderPayload {
85
+ dragType: 'group' | 'note'
86
+ groupPath?: string[]
87
+ noteIndex?: string
88
+ targetType: 'group' | 'note'
89
+ targetGroupPath?: string[]
90
+ targetNoteIndex?: string
91
+ placement?: 'before' | 'after'
92
+ }
93
+
94
+ type SidebarDensity = 'compact' | 'default' | 'loose'
95
+
96
+ const DEFAULT_SIDEBAR_DENSITY: SidebarDensity = 'default'
97
+ const DEFAULT_DONE_PREFIX = '✅'
98
+ const DEFAULT_UNDONE_PREFIX = '⏰'
99
+
100
+ // 最大解析层级(默认 3 层)
101
+ const maxDepth = computed(() => {
102
+ if (typeof window === 'undefined') {
103
+ return tnotesConfig.sidebarMaxDepth ?? 3
104
+ }
105
+
106
+ const savedMaxDepth = localStorage.getItem(SIDEBAR_MAX_DEPTH_KEY)
107
+ if (savedMaxDepth !== null) {
108
+ return parseInt(savedMaxDepth, 10)
109
+ }
110
+
111
+ return tnotesConfig.sidebarMaxDepth ?? 3
112
+ })
113
+
114
+ // 获取配置:是否显示笔记 ID
115
+ const showNoteId = computed(() => {
116
+ if (typeof window === 'undefined') {
117
+ return tnotesConfig.sidebarShowNoteId ?? false
118
+ }
119
+
120
+ const savedShowNoteId = localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
121
+ if (savedShowNoteId !== null) {
122
+ return savedShowNoteId === 'true'
123
+ }
124
+
125
+ return tnotesConfig.sidebarShowNoteId ?? false
126
+ })
127
+
128
+ const sidebarDensity = computed<SidebarDensity>(() => {
129
+ if (typeof window === 'undefined') return DEFAULT_SIDEBAR_DENSITY
130
+
131
+ return normalizeSidebarDensity(localStorage.getItem(SIDEBAR_DENSITY_KEY))
132
+ })
133
+
134
+ const donePrefix = computed(() => {
135
+ if (typeof window === 'undefined') return DEFAULT_DONE_PREFIX
136
+
137
+ const savedPrefix = localStorage.getItem(SIDEBAR_DONE_PREFIX_KEY)
138
+ return savedPrefix !== null ? savedPrefix : DEFAULT_DONE_PREFIX
139
+ })
140
+
141
+ const undonePrefix = computed(() => {
142
+ if (typeof window === 'undefined') return DEFAULT_UNDONE_PREFIX
143
+
144
+ const savedPrefix = localStorage.getItem(SIDEBAR_UNDONE_PREFIX_KEY)
145
+ return savedPrefix !== null ? savedPrefix : DEFAULT_UNDONE_PREFIX
146
+ })
147
+
148
+ // 获取 base 路径
149
+ const base = computed(() => site.value.base || '/')
150
+
151
+ const isDev = computed(() => {
152
+ if (typeof window === 'undefined') return false
153
+ return (
154
+ window.location.hostname === 'localhost' ||
155
+ window.location.hostname === '127.0.0.1'
156
+ )
157
+ })
158
+
159
+ // 加载 sidebar 数据
160
+ function loadSidebar() {
161
+ if (sidebarConfig && sidebarConfig['/notes/']) {
162
+ sidebarGroups.value = processItems(sidebarConfig['/notes/'])
163
+ }
164
+ }
165
+
166
+ // 递归处理侧边栏项,添加 collapsed 状态
167
+ function processItems(items: any[]): SidebarItem[] {
168
+ return items.map((item) => ({
169
+ ...item,
170
+ collapsed: item.collapsed ?? true,
171
+ items: item.items ? processItems(item.items) : undefined,
172
+ }))
173
+ }
174
+
175
+ function stripHeadingNumber(text: string): string {
176
+ return text.replace(/^\d+(?:\.\d+)*\.\s*/, '')
177
+ }
178
+
179
+ function stripNoteTitlePrefix(text: string): string {
180
+ return text
181
+ .replace(/^[✅⏰]\s*/, '')
182
+ .replace(/^\d{4}\.?\s*/, '')
183
+ .trim()
184
+ }
185
+
186
+ function normalizeSidebarDensity(value: string | null): SidebarDensity {
187
+ if (value === 'compact' || value === 'default' || value === 'loose') {
188
+ return value
189
+ }
190
+
191
+ return DEFAULT_SIDEBAR_DENSITY
192
+ }
193
+
194
+ function isCurrentNote(noteIndex: string): boolean {
195
+ const decodedRoutePath = decodeURIComponent(route.path)
196
+ return decodedRoutePath.includes(`/notes/${noteIndex}.`)
197
+ }
198
+
199
+ function resolveUrl(path: string): string {
200
+ const normalizedBase = base.value.endsWith('/') ? base.value : `${base.value}/`
201
+ return normalizedBase + path.replace(/^\/+/, '')
202
+ }
203
+
204
+ async function requestSidebarAction(
205
+ path: string,
206
+ body: unknown,
207
+ ): Promise<SidebarActionResult> {
208
+ const response = await fetch(path, {
209
+ method: 'POST',
210
+ headers: {
211
+ 'Content-Type': 'application/json',
212
+ },
213
+ body: JSON.stringify(body),
214
+ })
215
+
216
+ const result = (await response.json().catch(() => null)) as
217
+ | SidebarActionResult
218
+ | null
219
+
220
+ if (!response.ok || !result?.success) {
221
+ throw new Error(result?.message || response.statusText || '操作失败')
222
+ }
223
+
224
+ setTimeout(() => {
225
+ loadSidebar()
226
+ }, 150)
227
+
228
+ return result
229
+ }
230
+
231
+ async function runSidebarAction(action: () => Promise<void>) {
232
+ if (!isDev.value) return
233
+
234
+ try {
235
+ await action()
236
+ } catch (error) {
237
+ alert(error instanceof Error ? error.message : String(error))
238
+ }
239
+ }
240
+
241
+ function renameGroup(payload: GroupActionPayload) {
242
+ void runSidebarAction(async () => {
243
+ const newTitle = window.prompt('请输入新的目录名称', stripHeadingNumber(payload.text))
244
+ if (newTitle === null) return
245
+
246
+ await requestSidebarAction('/__tnotes_sidebar_rename_group', {
247
+ groupPath: payload.groupPath,
248
+ newTitle,
249
+ })
250
+ })
251
+ }
252
+
253
+ function deleteGroup(payload: GroupActionPayload) {
254
+ void runSidebarAction(async () => {
255
+ const confirmed = window.confirm(
256
+ `确定删除目录「${payload.groupPath.join(' / ')}」及其下所有笔记吗?`,
257
+ )
258
+ if (!confirmed) return
259
+
260
+ const result = await requestSidebarAction('/__tnotes_sidebar_delete_group', {
261
+ groupPath: payload.groupPath,
262
+ })
263
+
264
+ if (result.redirectUrl) {
265
+ window.location.replace(resolveUrl(result.redirectUrl))
266
+ }
267
+ })
268
+ }
269
+
270
+ function createNoteInGroup(payload: GroupActionPayload) {
271
+ void runSidebarAction(async () => {
272
+ await requestSidebarAction('/__tnotes_sidebar_create_note', {
273
+ groupPath: payload.groupPath,
274
+ count: 1,
275
+ })
276
+ })
277
+ }
278
+
279
+ function createNotesInGroup(payload: GroupActionPayload) {
280
+ void runSidebarAction(async () => {
281
+ const input = window.prompt('请输入新增笔记数量(1-100)', '1')
282
+ if (input === null) return
283
+
284
+ const count = Number(input)
285
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
286
+ throw new Error('新增多篇笔记的数量必须是 1-100 的整数')
287
+ }
288
+
289
+ await requestSidebarAction('/__tnotes_sidebar_create_notes', {
290
+ groupPath: payload.groupPath,
291
+ count,
292
+ })
293
+ })
294
+ }
295
+
296
+ function renameNote(payload: NoteActionPayload) {
297
+ void runSidebarAction(async () => {
298
+ if (!payload.noteIndex) return
299
+
300
+ const newTitle = window.prompt(
301
+ '请输入新的笔记名称',
302
+ stripNoteTitlePrefix(payload.text),
303
+ )
304
+ if (newTitle === null) return
305
+
306
+ const result = await requestSidebarAction('/__tnotes_rename_note', {
307
+ noteIndex: payload.noteIndex,
308
+ newTitle,
309
+ })
310
+
311
+ if (result.newUrl && isCurrentNote(payload.noteIndex)) {
312
+ window.location.replace(resolveUrl(result.newUrl))
313
+ }
314
+ })
315
+ }
316
+
317
+ function deleteNote(payload: NoteActionPayload) {
318
+ void runSidebarAction(async () => {
319
+ if (!payload.noteIndex) return
320
+
321
+ const confirmed = window.confirm(
322
+ `确定删除笔记「${stripHeadingNumber(payload.text)}」吗?`,
323
+ )
324
+ if (!confirmed) return
325
+
326
+ const result = await requestSidebarAction('/__tnotes_sidebar_delete_note', {
327
+ noteIndex: payload.noteIndex,
328
+ })
329
+
330
+ if (result.redirectUrl && isCurrentNote(payload.noteIndex)) {
331
+ window.location.replace(resolveUrl(result.redirectUrl))
332
+ }
333
+ })
334
+ }
335
+
336
+ function createNoteAround(payload: NoteActionPayload) {
337
+ void runSidebarAction(async () => {
338
+ if (!payload.noteIndex || !payload.placement) return
339
+
340
+ await requestSidebarAction('/__tnotes_sidebar_create_note', {
341
+ targetNoteIndex: payload.noteIndex,
342
+ placement: payload.placement,
343
+ count: 1,
344
+ })
345
+ })
346
+ }
347
+
348
+ function openNoteAbout(payload: NoteActionPayload) {
349
+ if (!payload.noteIndex) return
350
+ emit('open-note-about', payload.noteIndex)
351
+ }
352
+
353
+ function reorderSidebar(payload: ReorderPayload) {
354
+ void runSidebarAction(async () => {
355
+ await requestSidebarAction('/__tnotes_sidebar_reorder', payload)
356
+ })
357
+ }
358
+
359
+ // 判断项是否有子项
360
+ function hasChildren(item: SidebarItem): boolean {
361
+ return !!(item.items && item.items.length > 0)
362
+ }
363
+
364
+ // 获取项的唯一 key
365
+ function getItemKey(item: SidebarItem): string {
366
+ return item.link || item.text
367
+ }
368
+
369
+ // 切换项的展开/折叠状态(支持递归)
370
+ function toggleItem(item: SidebarItem) {
371
+ item.collapsed = !item.collapsed
372
+ }
373
+
374
+ // 递归查找并切换项
375
+ function toggleItemRecursive(items: SidebarItem[], text: string): boolean {
376
+ for (const item of items) {
377
+ if (item.text === text) {
378
+ item.collapsed = !item.collapsed
379
+ return true
380
+ }
381
+ if (item.items && toggleItemRecursive(item.items, text)) {
382
+ return true
383
+ }
384
+ }
385
+ return false
386
+ }
387
+
388
+ // 切换组展开/折叠(保留兼容性)
389
+ function toggleGroup(groupText: string) {
390
+ toggleItemRecursive(sidebarGroups.value, groupText)
391
+ }
392
+
393
+ // 递归展开/折叠所有项
394
+ function setAllCollapsed(items: SidebarItem[], collapsed: boolean) {
395
+ items.forEach((item) => {
396
+ if (item.items) {
397
+ item.collapsed = collapsed
398
+ setAllCollapsed(item.items, collapsed)
399
+ }
400
+ })
401
+ }
402
+
403
+ // 检查是否有任何一级章节处于展开状态
404
+ function hasAnyFirstLevelExpanded(): boolean {
405
+ return sidebarGroups.value.some((group) => !group.collapsed)
406
+ }
407
+
408
+ // 展开全部
409
+ function expandAll() {
410
+ setAllCollapsed(sidebarGroups.value, false)
411
+ }
412
+
413
+ // 折叠全部
414
+ function collapseAll() {
415
+ setAllCollapsed(sidebarGroups.value, true)
416
+ }
417
+
418
+ // 智能切换:如果有展开的一级章节则折叠全部,否则展开全部
419
+ function toggleExpandCollapse() {
420
+ if (hasAnyFirstLevelExpanded()) {
421
+ collapseAll()
422
+ } else {
423
+ expandAll()
424
+ }
425
+ }
426
+
427
+ // 获取当前笔记的所有出现位置
428
+ function getCurrentNotePositions(): HTMLElement[] {
429
+ const elements: HTMLElement[] = []
430
+
431
+ if (!navRef.value) {
432
+ return elements
433
+ }
434
+
435
+ const activeItems = navRef.value.querySelectorAll('.nav-item.active')
436
+
437
+ activeItems.forEach((item) => {
438
+ elements.push(item as HTMLElement)
439
+ })
440
+
441
+ return elements
442
+ }
443
+
444
+ // 展开指定元素的父级分组
445
+ function expandParentGroup(element: HTMLElement) {
446
+ // 查找所有父级 group 元素(从最近的开始)
447
+ let currentElement: HTMLElement | null = element
448
+ const groupsToExpand: string[] = []
449
+
450
+ // 向上遍历,收集所有父级 group 的标题文本
451
+ while (currentElement) {
452
+ const groupElement: HTMLElement | null =
453
+ currentElement.closest<HTMLElement>('.group')
454
+ if (!groupElement) break
455
+
456
+ const groupTitleText = groupElement.querySelector('.group-title-text')
457
+ if (groupTitleText) {
458
+ const groupText = groupTitleText.textContent?.trim()
459
+ if (groupText) {
460
+ groupsToExpand.push(groupText)
461
+ }
462
+ }
463
+
464
+ // 继续向上查找
465
+ currentElement =
466
+ groupElement.parentElement?.closest<HTMLElement>('.group') || null
467
+ }
468
+
469
+ // 从最外层开始展开,逐层向内
470
+ // 但是搜索时要确保在正确的上下文中搜索
471
+ if (groupsToExpand.length === 0) return
472
+
473
+ // 反转数组,从最外层开始处理
474
+ const outerToInner = [...groupsToExpand].reverse()
475
+
476
+ // 第一层必须从根开始搜索
477
+ let currentContext: SidebarItem[] | null = null
478
+
479
+ for (let i = 0; i < outerToInner.length; i++) {
480
+ const groupText = outerToInner[i]
481
+
482
+ if (i === 0) {
483
+ // 第一层从根搜索
484
+ const found = expandGroupRecursive(sidebarGroups.value, groupText)
485
+ if (found) {
486
+ // 找到后,获取这个分组的 items 作为下一层的搜索上下文
487
+ const foundGroup = findGroupByText(sidebarGroups.value, groupText)
488
+ if (foundGroup?.items) {
489
+ currentContext = foundGroup.items
490
+ }
491
+ }
492
+ } else {
493
+ // 后续层从上一层的上下文中搜索
494
+ if (currentContext) {
495
+ const found = expandGroupRecursive(currentContext, groupText)
496
+ if (found) {
497
+ const foundGroup = findGroupByText(currentContext, groupText)
498
+ if (foundGroup?.items) {
499
+ currentContext = foundGroup.items
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ // 查找分组(不展开,只返回引用)
508
+ function findGroupByText(
509
+ items: SidebarItem[],
510
+ targetText: string
511
+ ): SidebarItem | null {
512
+ for (const item of items) {
513
+ if (item.text === targetText) {
514
+ return item
515
+ }
516
+ if (item.items) {
517
+ const found = findGroupByText(item.items, targetText)
518
+ if (found) return found
519
+ }
520
+ }
521
+ return null
522
+ }
523
+
524
+ // 递归查找并展开分组
525
+ function expandGroupRecursive(
526
+ items: SidebarItem[],
527
+ targetText: string,
528
+ ): boolean {
529
+ for (const item of items) {
530
+ if (item.text === targetText) {
531
+ item.collapsed = false
532
+ return true
533
+ }
534
+
535
+ if (item.items) {
536
+ const found = expandGroupRecursive(item.items, targetText)
537
+ if (found) {
538
+ // 如果在子项中找到了,也展开当前项
539
+ item.collapsed = false
540
+ return true
541
+ }
542
+ }
543
+ }
544
+
545
+ return false
546
+ }
547
+
548
+ // 滚动到指定元素
549
+ function scrollToElement(element: HTMLElement) {
550
+ if (!element || !navRef.value) {
551
+ // console.log('❌ [scrollToElement] No element or navRef')
552
+ return
553
+ }
554
+
555
+ const navContainer = navRef.value.closest('.VPSidebar')
556
+ if (!navContainer) {
557
+ // console.log('❌ [scrollToElement] No VPSidebar container found')
558
+ return
559
+ }
560
+
561
+ // console.log('📍 [scrollToElement] Scrolling to element')
562
+
563
+ // 计算元素相对于容器的位置
564
+ const elementRect = element.getBoundingClientRect()
565
+ const containerRect = navContainer.getBoundingClientRect()
566
+
567
+ // 计算需要滚动的距离(将元素放在容器中间)
568
+ const scrollTop =
569
+ navContainer.scrollTop +
570
+ elementRect.top -
571
+ containerRect.top -
572
+ containerRect.height / 2 +
573
+ elementRect.height / 2
574
+
575
+ navContainer.scrollTo({
576
+ top: scrollTop,
577
+ behavior: 'smooth',
578
+ })
579
+
580
+ // 添加临时高亮动画
581
+ element.classList.add('focus-highlight')
582
+ setTimeout(() => {
583
+ element.classList.remove('focus-highlight')
584
+ }, 1000)
585
+ }
586
+
587
+ // 聚焦到当前笔记(支持多个位置切换)
588
+ function focusCurrentNote() {
589
+ const positions = getCurrentNotePositions()
590
+
591
+ if (positions.length === 0) {
592
+ return
593
+ }
594
+
595
+ // 循环切换聚焦位置
596
+ currentFocusIndex.value = (currentFocusIndex.value + 1) % positions.length
597
+ const targetElement = positions[currentFocusIndex.value]
598
+
599
+ // 展开该笔记所在的分组
600
+ expandParentGroup(targetElement)
601
+
602
+ // 滚动到该笔记
603
+ setTimeout(() => {
604
+ scrollToElement(targetElement)
605
+ }, 100)
606
+ }
607
+
608
+ // 展开当前激活笔记的所有父级分组
609
+ function expandActiveItemParents() {
610
+ expandActiveItemParentsRecursive(sidebarGroups.value)
611
+ }
612
+
613
+ // 递归展开包含激活项的父级
614
+ function expandActiveItemParentsRecursive(items: SidebarItem[]): boolean {
615
+ let hasActive = false
616
+
617
+ for (const item of items) {
618
+ if (item.link) {
619
+ // 检查当前项是否激活
620
+ const fullLink = getFullLink(item.link)
621
+ const decodedRoutePath = decodeURIComponent(route.path)
622
+ const decodedFullLink = decodeURIComponent(fullLink)
623
+ const itemActive =
624
+ decodedRoutePath === decodedFullLink ||
625
+ decodedRoutePath === decodedFullLink + '.html'
626
+
627
+ if (itemActive) {
628
+ hasActive = true
629
+ }
630
+ } else if (item.items) {
631
+ const childHasActive = expandActiveItemParentsRecursive(item.items)
632
+ if (childHasActive) {
633
+ item.collapsed = false
634
+ hasActive = true
635
+ }
636
+ }
637
+ }
638
+
639
+ return hasActive
640
+ }
641
+
642
+ // 获取完整链接(包含 base)
643
+ function getFullLink(link: string) {
644
+ const cleanLink = link.startsWith('/') ? link.slice(1) : link
645
+ return base.value + cleanLink
646
+ }
647
+
648
+ // 滚动到当前激活的笔记
649
+ function scrollToActiveItem() {
650
+ // 等待 DOM 更新
651
+ setTimeout(() => {
652
+ const positions = getCurrentNotePositions()
653
+ if (positions.length > 0) {
654
+ // 展开所有包含当前笔记的分组
655
+ expandActiveItemParents()
656
+
657
+ // 滚动到第一个位置
658
+ setTimeout(() => {
659
+ scrollToElement(positions[0])
660
+ }, 100)
661
+ }
662
+ }, 300)
663
+ }
664
+
665
+ // 暴露函数给父组件使用
666
+ defineExpose({
667
+ expandAll,
668
+ collapseAll,
669
+ toggleExpandCollapse,
670
+ hasAnyFirstLevelExpanded,
671
+ focusCurrentNote,
672
+ })
673
+
674
+ onMounted(() => {
675
+ loadSidebar()
676
+
677
+ // 调试:打印配置信息
678
+ // console.log('🔧 [CustomSidebar] showNoteId:', showNoteId.value)
679
+ // if (typeof window !== 'undefined') {
680
+ // console.log(
681
+ // '🔧 [CustomSidebar] localStorage value:',
682
+ // localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
683
+ // )
684
+ // }
685
+ // console.log(
686
+ // '🔧 [CustomSidebar] tnotesConfig value:',
687
+ // tnotesConfig.sidebarShowNoteId
688
+ // )
689
+
690
+ // 自动滚动到当前激活的笔记
691
+ scrollToActiveItem()
692
+ })
693
+
694
+ // 监听 sidebarConfig 的变化(HMR 会更新这个导入的数据)
695
+ watch(
696
+ () => sidebarConfig,
697
+ () => {
698
+ // console.log('🔄 [CustomSidebar] Sidebar config changed, reloading...')
699
+ loadSidebar()
700
+ },
701
+ { deep: true }
702
+ )
703
+
704
+ // 监听 tnotesConfig 的变化
705
+ watch(
706
+ () => tnotesConfig,
707
+ () => {
708
+ // console.log(
709
+ // '🔄 [CustomSidebar] TNotes config changed, sidebarShowNoteId:',
710
+ // tnotesConfig.sidebarShowNoteId
711
+ // )
712
+ },
713
+ { deep: true }
714
+ )
715
+
716
+ // 监听路由变化,自动展开当前激活项所在的组并滚动
717
+ watch(
718
+ () => route.path,
719
+ () => {
720
+ // 重置聚焦索引
721
+ currentFocusIndex.value = 0
722
+
723
+ // 展开并滚动到当前笔记
724
+ expandActiveItemParents()
725
+ scrollToActiveItem()
726
+ }
727
+ )
728
+ </script>
729
+
730
+ <style scoped>
731
+ /* 自定义 sidebar 容器,适配 VitePress 的 sidebar-nav-before 插槽 */
732
+ .custom-sidebar-wrapper {
733
+ /* 不需要设置 position 和尺寸,因为它在 VitePress 的 sidebar 容器内 */
734
+ }
735
+
736
+ .nav {
737
+ font-size: 14px;
738
+ line-height: 2;
739
+ }
740
+
741
+ .group {
742
+ margin-bottom: 16px;
743
+ }
744
+
745
+ .group-title {
746
+ display: flex;
747
+ justify-content: space-between;
748
+ align-items: center;
749
+ width: 100%;
750
+ padding: 6px 0;
751
+ font-weight: 600;
752
+ color: var(--vp-c-text-1);
753
+ background: none;
754
+ border: none;
755
+ cursor: pointer;
756
+ text-align: left;
757
+ transition: color 0.25s;
758
+ }
759
+
760
+ .group-title:hover {
761
+ color: var(--vp-c-brand-1);
762
+ }
763
+
764
+ .arrow {
765
+ font-size: 10px;
766
+ transform: rotate(90deg);
767
+ transition: transform 0.25s;
768
+ }
769
+
770
+ .arrow.collapsed {
771
+ transform: rotate(0deg);
772
+ }
773
+
774
+ .nav-item {
775
+ display: block;
776
+ padding: 4px;
777
+ color: var(--vp-c-text-2);
778
+ text-decoration: none;
779
+ border-radius: 4px;
780
+ font-size: 14px;
781
+ line-height: 24px;
782
+ transition: all 0.25s;
783
+ }
784
+
785
+ .nav-item:hover {
786
+ color: var(--vp-c-brand-1);
787
+ background-color: var(--vp-c-default-soft);
788
+ }
789
+
790
+ .nav-item.active {
791
+ color: var(--vp-c-brand-1);
792
+ font-weight: 600;
793
+ }
794
+
795
+ /* 聚焦高亮动画 */
796
+ .nav-item.focus-highlight {
797
+ animation: focusPulse 1s ease-in-out;
798
+ }
799
+
800
+ @keyframes focusPulse {
801
+ 0%,
802
+ 100% {
803
+ background-color: transparent;
804
+ }
805
+ 50% {
806
+ background-color: var(--vp-c-brand-soft);
807
+ }
808
+ }
809
+ </style>
810
+
811
+ <!-- 全局样式:隐藏 VitePress 默认的 sidebar nav -->
812
+ <style>
813
+ /* 隐藏 VitePress 默认的 sidebar 导航内容(保留容器) */
814
+ .VPSidebarNav {
815
+ display: none !important;
816
+ }
817
+ </style>