@tnotesjs/core 0.1.22 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +87 -87
  2. package/package.json +1 -1
  3. package/types/config.ts +60 -60
  4. package/types/index.ts +11 -11
  5. package/types/note.ts +33 -33
  6. package/vitepress/assets/icons/icon__check.svg +3 -3
  7. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  8. package/vitepress/assets/icons/icon__close.svg +1 -1
  9. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  10. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  11. package/vitepress/assets/icons/icon__copy.svg +4 -4
  12. package/vitepress/assets/icons/icon__focus.svg +1 -1
  13. package/vitepress/assets/icons/icon__fold.svg +3 -3
  14. package/vitepress/assets/icons/icon__folder.svg +1 -1
  15. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  16. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  17. package/vitepress/assets/icons/icon__github.svg +3 -3
  18. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  19. package/vitepress/assets/icons/icon__next.svg +1 -1
  20. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  21. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  22. package/vitepress/assets/icons/icon__prev.svg +1 -1
  23. package/vitepress/assets/icons/icon__restore.svg +1 -1
  24. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  25. package/vitepress/assets/icons/icon__search.svg +1 -1
  26. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  27. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  28. package/vitepress/assets/icons/icon__totop.svg +5 -5
  29. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  30. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  31. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  32. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  33. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  34. package/vitepress/assets/icons/index.ts +39 -39
  35. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  36. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  37. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -209
  38. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  39. package/vitepress/components/Discussions/Discussions.vue +242 -242
  40. package/vitepress/components/EnWordList/EnWordList.vue +664 -664
  41. package/vitepress/components/EnWordList/RightClickMenu.vue +87 -87
  42. package/vitepress/components/Footprints/Footprints.vue +472 -472
  43. package/vitepress/components/Layout/AboutModal.vue +329 -329
  44. package/vitepress/components/Layout/AboutPanel.vue +485 -485
  45. package/vitepress/components/Layout/ContentCollapse.vue +591 -591
  46. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  47. package/vitepress/components/Layout/DocBeforeControls.vue +145 -145
  48. package/vitepress/components/Layout/DocFooter.vue +233 -233
  49. package/vitepress/components/Layout/ImagePreview.vue +485 -485
  50. package/vitepress/components/Layout/Layout.vue +744 -744
  51. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  52. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  53. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -170
  54. package/vitepress/components/Layout/SidebarResizeHandle.vue +311 -311
  55. package/vitepress/components/Layout/Swiper.vue +167 -167
  56. package/vitepress/components/Layout/ToggleFullContent.vue +45 -45
  57. package/vitepress/components/Layout/ToggleSidebar.vue +35 -35
  58. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  59. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  60. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  61. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  62. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  63. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  64. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  65. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  66. package/vitepress/components/MarkMap/MarkMap.vue +562 -562
  67. package/vitepress/components/Mermaid/Mermaid.vue +644 -644
  68. package/vitepress/components/NotesTable/NotesTable.vue +175 -175
  69. package/vitepress/components/NotesTable/README.md +67 -67
  70. package/vitepress/components/Settings/Settings.vue +711 -711
  71. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  72. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  73. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -945
  74. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  75. package/vitepress/components/constants.ts +109 -109
  76. package/vitepress/components/notesConfig.data.ts +73 -73
  77. package/vitepress/components/sidebar.data.ts +59 -59
  78. package/vitepress/components/tnotes-config.data.ts +21 -21
  79. package/vitepress/components/utils.ts +26 -26
  80. package/vitepress/config/index.ts +169 -169
  81. package/vitepress/configs/constants.ts +26 -26
  82. package/vitepress/configs/head.config.ts +25 -25
  83. package/vitepress/configs/index.ts +9 -9
  84. package/vitepress/configs/markdown-it.d.ts +23 -23
  85. package/vitepress/configs/markdown.config.ts +368 -368
  86. package/vitepress/configs/theme.config.ts +108 -108
  87. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  88. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  89. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  90. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  91. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  92. package/vitepress/theme/index.ts +137 -137
  93. package/vitepress/theme/styles/base.scss +66 -64
  94. package/vitepress/theme/styles/components/404.scss +31 -31
  95. package/vitepress/theme/styles/components/collapse.scss +172 -172
  96. package/vitepress/theme/styles/components/markmap.scss +101 -101
  97. package/vitepress/theme/styles/components/swiper.scss +255 -255
  98. package/vitepress/theme/styles/index.scss +25 -25
  99. package/vitepress/theme/styles/layout.scss +78 -78
  100. package/vitepress/theme/styles/utilities.scss +39 -39
  101. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,817 +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
- :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>
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>