@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,1083 +1,1083 @@
1
- <template>
2
- <template v-for="item in items" :key="getItemKey(item)">
3
- <!-- 只有在不超过最大层级时才渲染组 -->
4
- <div
5
- v-if="hasChildren(item) && depth < maxDepth - 1"
6
- class="group"
7
- :class="getDensityClass()"
8
- >
9
- <div
10
- class="sidebar-row group-row"
11
- :class="[getGroupDropClass(item), getDensityClass()]"
12
- :style="getNodeStyle(depth)"
13
- :draggable="isDev"
14
- @dragstart="onGroupDragStart($event, item)"
15
- @dragend="clearDropTarget"
16
- @dragover.prevent.stop="onGroupDragOver($event, item)"
17
- @drop.stop="onGroupDrop($event, item)"
18
- >
19
- <button
20
- class="group-title row-main"
21
- :class="`group-title-level-${depth}`"
22
- @click="toggleItem(item)"
23
- >
24
- <span class="arrow" :class="{ collapsed: item.collapsed }">
25
- <img
26
- :src="
27
- item.collapsed ? icon__sidebar_collapsed : icon__sidebar_opened
28
- "
29
- alt=""
30
- />
31
- </span>
32
-
33
- <span class="group-title-text">{{ item.text }}</span>
34
- </button>
35
-
36
- <div
37
- v-if="isDev"
38
- class="row-actions"
39
- :class="{ 'menu-open': isGroupMenuOpen(item) }"
40
- @click.stop
41
- >
42
- <button
43
- class="action-btn menu-trigger"
44
- title="更多操作"
45
- type="button"
46
- @click="toggleMenu(getGroupMenuKey(item, 'more'))"
47
- >
48
- <span class="ellipsis">...</span>
49
- </button>
50
-
51
- <div
52
- v-if="isMenuOpen(getGroupMenuKey(item, 'more'))"
53
- class="action-menu"
54
- >
55
- <button class="menu-item" type="button" @click="renameGroup(item)">
56
- 重命名目录
57
- </button>
58
- <button
59
- class="menu-item danger"
60
- type="button"
61
- @click="deleteGroup(item)"
62
- >
63
- 删除目录
64
- </button>
65
- </div>
66
-
67
- <button
68
- class="action-btn menu-trigger add-trigger"
69
- title="新增"
70
- type="button"
71
- @click="toggleMenu(getGroupMenuKey(item, 'add'))"
72
- >
73
- +
74
- </button>
75
-
76
- <div
77
- v-if="isMenuOpen(getGroupMenuKey(item, 'add'))"
78
- class="action-menu"
79
- >
80
- <button
81
- class="menu-item"
82
- type="button"
83
- @click="createNoteInGroup(item)"
84
- >
85
- 新增笔记
86
- </button>
87
- <button
88
- class="menu-item"
89
- type="button"
90
- @click="createNotesInGroup(item)"
91
- >
92
- 新增多篇笔记
93
- </button>
94
- </div>
95
- </div>
96
- </div>
97
-
98
- <div v-show="!item.collapsed" class="group-items">
99
- <SidebarItems
100
- :items="getChildItems(item)"
101
- :depth="depth + 1"
102
- :max-depth="maxDepth"
103
- :show-note-id="showNoteId"
104
- :base="base"
105
- :current-path="currentPath"
106
- :item-depth="depth + 1"
107
- :group-path="getGroupPath(item)"
108
- :is-dev="isDev"
109
- :sidebar-density="sidebarDensity"
110
- :done-prefix="donePrefix"
111
- :undone-prefix="undonePrefix"
112
- @rename-group="$emit('rename-group', $event)"
113
- @delete-group="$emit('delete-group', $event)"
114
- @create-note-in-group="$emit('create-note-in-group', $event)"
115
- @create-notes-in-group="$emit('create-notes-in-group', $event)"
116
- @rename-note="$emit('rename-note', $event)"
117
- @delete-note="$emit('delete-note', $event)"
118
- @create-note-around="$emit('create-note-around', $event)"
119
- @open-note-about="$emit('open-note-about', $event)"
120
- @reorder-sidebar="$emit('reorder-sidebar', $event)"
121
- />
122
- </div>
123
- </div>
124
-
125
- <!-- 如果是链接项,或者是超过最大层级的组,都不渲染 -->
126
- <div
127
- v-else-if="!hasChildren(item)"
128
- class="sidebar-row note-row"
129
- :class="[getNoteDropClass(item), getDensityClass()]"
130
- :style="getNodeStyle(actualItemDepth)"
131
- :draggable="isDev"
132
- @dragstart="onNoteDragStart($event, item)"
133
- @dragend="clearDropTarget"
134
- @dragover.prevent.stop="onNoteDragOver($event, item)"
135
- @drop.stop="onNoteDrop($event, item)"
136
- >
137
- <a
138
- :href="getFullLink(item.link)"
139
- :class="[
140
- 'nav-item',
141
- 'row-main',
142
- { active: isActive(item.link) },
143
- `nav-item-${extractNoteIdFromLink(item.link)}`,
144
- `nav-item-level-${actualItemDepth + 1}`,
145
- ]"
146
- :data-note-id="extractNoteIdFromLink(item.link)"
147
- >
148
- {{ getNoteDisplayText(item.text, item.link) }}
149
- </a>
150
-
151
- <div
152
- v-if="isDev"
153
- class="row-actions"
154
- :class="{ 'menu-open': isNoteMenuOpen(item) }"
155
- @click.stop
156
- >
157
- <button
158
- class="action-btn menu-trigger"
159
- title="更多操作"
160
- type="button"
161
- @click="toggleMenu(getNoteMenuKey(item, 'more'))"
162
- >
163
- <span class="ellipsis">...</span>
164
- </button>
165
-
166
- <div
167
- v-if="isMenuOpen(getNoteMenuKey(item, 'more'))"
168
- class="action-menu"
169
- >
170
- <button class="menu-item" type="button" @click="renameNote(item)">
171
- 重命名
172
- </button>
173
- <button
174
- class="menu-item danger"
175
- type="button"
176
- @click="deleteNote(item)"
177
- >
178
- 删除笔记
179
- </button>
180
- <button class="menu-item" type="button" @click="openNoteAbout(item)">
181
- 关于
182
- </button>
183
- </div>
184
-
185
- <button
186
- class="action-btn menu-trigger add-trigger"
187
- title="新增"
188
- type="button"
189
- @click="toggleMenu(getNoteMenuKey(item, 'add'))"
190
- >
191
- +
192
- </button>
193
-
194
- <div
195
- v-if="isMenuOpen(getNoteMenuKey(item, 'add'))"
196
- class="action-menu"
197
- >
198
- <button
199
- class="menu-item"
200
- type="button"
201
- @click="createNoteAround(item, 'before')"
202
- >
203
- 在上方新增笔记
204
- </button>
205
- <button
206
- class="menu-item"
207
- type="button"
208
- @click="createNoteAround(item, 'after')"
209
- >
210
- 在下方新增笔记
211
- </button>
212
- </div>
213
- </div>
214
- </div>
215
- </template>
216
- </template>
217
-
218
- <script setup lang="ts">
219
- import { computed, onBeforeUnmount, ref } from 'vue'
220
-
221
- import {
222
- icon__sidebar_opened,
223
- icon__sidebar_collapsed,
224
- } from '../../assets/icons'
225
-
226
- interface SidebarItem {
227
- text: string
228
- link?: string
229
- items?: SidebarItem[]
230
- collapsed?: boolean
231
- }
232
-
233
- interface Props {
234
- items: SidebarItem[]
235
- depth: number
236
- maxDepth: number
237
- showNoteId: boolean
238
- base: string
239
- currentPath: string
240
- itemDepth?: number // 用于计算链接项的缩进,默认等于 depth
241
- groupPath?: string[]
242
- isDev?: boolean
243
- sidebarDensity?: SidebarDensity
244
- donePrefix?: string
245
- undonePrefix?: string
246
- }
247
-
248
- const props = withDefaults(defineProps<Props>(), {
249
- depth: 0,
250
- maxDepth: 3,
251
- showNoteId: false,
252
- base: '/',
253
- currentPath: '',
254
- itemDepth: undefined,
255
- groupPath: () => [],
256
- isDev: false,
257
- sidebarDensity: 'default',
258
- donePrefix: '✅',
259
- undonePrefix: '⏰',
260
- })
261
-
262
- const emit = defineEmits<{
263
- 'rename-group': [payload: GroupPayload]
264
- 'delete-group': [payload: GroupPayload]
265
- 'create-note-in-group': [payload: GroupPayload]
266
- 'create-notes-in-group': [payload: GroupPayload]
267
- 'rename-note': [payload: NotePayload]
268
- 'delete-note': [payload: NotePayload]
269
- 'create-note-around': [
270
- payload: NotePayload & { placement: 'before' | 'after' },
271
- ]
272
- 'open-note-about': [payload: NotePayload]
273
- 'reorder-sidebar': [payload: ReorderPayload]
274
- }>()
275
-
276
- interface GroupPayload {
277
- groupPath: string[]
278
- text: string
279
- }
280
-
281
- interface NotePayload {
282
- noteIndex: string
283
- text: string
284
- link?: string
285
- }
286
-
287
- interface DragPayload {
288
- type: 'group' | 'note'
289
- groupPath?: string[]
290
- noteIndex?: string
291
- }
292
-
293
- interface DropTarget {
294
- type: 'group' | 'note'
295
- key: string
296
- placement: 'before' | 'after' | 'inside'
297
- }
298
-
299
- interface ReorderPayload {
300
- dragType: 'group' | 'note'
301
- groupPath?: string[]
302
- noteIndex?: string
303
- targetType: 'group' | 'note'
304
- targetGroupPath?: string[]
305
- targetNoteIndex?: string
306
- placement?: 'before' | 'after'
307
- }
308
-
309
- type MenuType = 'more' | 'add'
310
- type SidebarDensity = 'compact' | 'default' | 'loose'
311
- type NoteStatus = 'done' | 'undone' | null
312
-
313
- // 获取实际的 item depth(用于链接项的缩进)
314
- const actualItemDepth = computed(() => props.itemDepth ?? props.depth)
315
- const openMenuKey = ref<string | null>(null)
316
- const dropTarget = ref<DropTarget | null>(null)
317
- let ignoredToggleKey: string | null = null
318
-
319
- const dragDataType = 'application/x-tnotes-sidebar-node'
320
- const dragNoteType = 'application/x-tnotes-sidebar-note'
321
- const dragGroupType = 'application/x-tnotes-sidebar-group'
322
-
323
- if (typeof document !== 'undefined') {
324
- document.addEventListener('click', closeMenuOnDocumentClick, true)
325
- document.addEventListener('dragend', clearDropTarget, true)
326
- document.addEventListener('drop', clearDropTarget, true)
327
- }
328
-
329
- onBeforeUnmount(() => {
330
- if (typeof document !== 'undefined') {
331
- document.removeEventListener('click', closeMenuOnDocumentClick, true)
332
- document.removeEventListener('dragend', clearDropTarget, true)
333
- document.removeEventListener('drop', clearDropTarget, true)
334
- }
335
- })
336
-
337
- // 判断项是否有子项
338
- function hasChildren(item: SidebarItem): boolean {
339
- return !!(item.items && item.items.length > 0)
340
- }
341
-
342
- function getChildItems(item: SidebarItem): SidebarItem[] {
343
- return item.items ?? []
344
- }
345
-
346
- // 获取项的唯一 key
347
- function getItemKey(item: SidebarItem): string {
348
- return item.link || item.text
349
- }
350
-
351
- // 切换项的展开/折叠状态
352
- function toggleItem(item: SidebarItem) {
353
- item.collapsed = !item.collapsed
354
- }
355
-
356
- function getNodeStyle(depth: number) {
357
- return {
358
- paddingLeft: `${depth * getIndentSize()}px`,
359
- }
360
- }
361
-
362
- function getDensityClass(): string {
363
- return `density-${props.sidebarDensity}`
364
- }
365
-
366
- function getIndentSize(): number {
367
- if (props.sidebarDensity === 'compact') return 20
368
- if (props.sidebarDensity === 'loose') return 28
369
- return 24
370
- }
371
-
372
- function getGroupPath(item: SidebarItem): string[] {
373
- return [...props.groupPath, item.text]
374
- }
375
-
376
- function getGroupPayload(item: SidebarItem): GroupPayload {
377
- return {
378
- groupPath: getGroupPath(item),
379
- text: item.text,
380
- }
381
- }
382
-
383
- function getNotePayload(item: SidebarItem): NotePayload {
384
- return {
385
- noteIndex: extractNoteIdFromLink(item.link) || '',
386
- text: item.text,
387
- link: item.link,
388
- }
389
- }
390
-
391
- function getGroupMenuKey(item: SidebarItem, menuType: MenuType): string {
392
- return `group:${menuType}:${getGroupPath(item).join('/')}`
393
- }
394
-
395
- function getGroupDropKey(item: SidebarItem): string {
396
- return `group:${getGroupPath(item).join('/')}`
397
- }
398
-
399
- function getNoteMenuKey(item: SidebarItem, menuType: MenuType): string {
400
- return `note:${menuType}:${extractNoteIdFromLink(item.link) || item.link || item.text}`
401
- }
402
-
403
- function getNoteDropKey(item: SidebarItem): string {
404
- return `note:${extractNoteIdFromLink(item.link) || item.link || item.text}`
405
- }
406
-
407
- function toggleMenu(menuKey: string) {
408
- if (ignoredToggleKey === menuKey) {
409
- ignoredToggleKey = null
410
- return
411
- }
412
-
413
- openMenuKey.value = openMenuKey.value === menuKey ? null : menuKey
414
- }
415
-
416
- function closeMenu() {
417
- openMenuKey.value = null
418
- }
419
-
420
- function isMenuOpen(menuKey: string): boolean {
421
- return openMenuKey.value === menuKey
422
- }
423
-
424
- function closeMenuOnDocumentClick() {
425
- if (!openMenuKey.value) return
426
-
427
- const closedMenuKey = openMenuKey.value
428
- openMenuKey.value = null
429
- ignoredToggleKey = closedMenuKey
430
-
431
- window.setTimeout(() => {
432
- if (ignoredToggleKey === closedMenuKey) {
433
- ignoredToggleKey = null
434
- }
435
- })
436
- }
437
-
438
- function isGroupMenuOpen(item: SidebarItem): boolean {
439
- return (
440
- isMenuOpen(getGroupMenuKey(item, 'more')) ||
441
- isMenuOpen(getGroupMenuKey(item, 'add'))
442
- )
443
- }
444
-
445
- function isNoteMenuOpen(item: SidebarItem): boolean {
446
- return (
447
- isMenuOpen(getNoteMenuKey(item, 'more')) ||
448
- isMenuOpen(getNoteMenuKey(item, 'add'))
449
- )
450
- }
451
-
452
- function renameGroup(item: SidebarItem) {
453
- emit('rename-group', getGroupPayload(item))
454
- closeMenu()
455
- }
456
-
457
- function deleteGroup(item: SidebarItem) {
458
- emit('delete-group', getGroupPayload(item))
459
- closeMenu()
460
- }
461
-
462
- function createNoteInGroup(item: SidebarItem) {
463
- emit('create-note-in-group', getGroupPayload(item))
464
- closeMenu()
465
- }
466
-
467
- function createNotesInGroup(item: SidebarItem) {
468
- emit('create-notes-in-group', getGroupPayload(item))
469
- closeMenu()
470
- }
471
-
472
- function renameNote(item: SidebarItem) {
473
- emit('rename-note', getNotePayload(item))
474
- closeMenu()
475
- }
476
-
477
- function deleteNote(item: SidebarItem) {
478
- emit('delete-note', getNotePayload(item))
479
- closeMenu()
480
- }
481
-
482
- function openNoteAbout(item: SidebarItem) {
483
- emit('open-note-about', getNotePayload(item))
484
- closeMenu()
485
- }
486
-
487
- function createNoteAround(item: SidebarItem, placement: 'before' | 'after') {
488
- emit('create-note-around', {
489
- ...getNotePayload(item),
490
- placement,
491
- })
492
- closeMenu()
493
- }
494
-
495
- function setDragData(event: DragEvent, payload: DragPayload) {
496
- if (!event.dataTransfer) return
497
-
498
- event.dataTransfer.effectAllowed = 'move'
499
- event.dataTransfer.setData(dragDataType, JSON.stringify(payload))
500
- event.dataTransfer.setData(
501
- payload.type === 'note' ? dragNoteType : dragGroupType,
502
- '1',
503
- )
504
- }
505
-
506
- function getDragData(event: DragEvent): DragPayload | null {
507
- const raw = event.dataTransfer?.getData(dragDataType)
508
- if (!raw) return null
509
-
510
- try {
511
- return JSON.parse(raw) as DragPayload
512
- } catch {
513
- return null
514
- }
515
- }
516
-
517
- function getDragTypes(event: DragEvent): string[] {
518
- return Array.from(event.dataTransfer?.types ?? [])
519
- }
520
-
521
- function getDraggedType(event: DragEvent): DragPayload['type'] | null {
522
- const types = getDragTypes(event)
523
-
524
- if (types.includes(dragNoteType)) return 'note'
525
- if (types.includes(dragGroupType)) return 'group'
526
-
527
- return getDragData(event)?.type ?? null
528
- }
529
-
530
- function setDropEffect(event: DragEvent) {
531
- if (event.dataTransfer) {
532
- event.dataTransfer.dropEffect = 'move'
533
- }
534
- }
535
-
536
- function clearDropTarget() {
537
- dropTarget.value = null
538
- }
539
-
540
- function isSamePath(left: string[], right: string[]): boolean {
541
- return (
542
- left.length === right.length &&
543
- left.every((item, index) => item === right[index])
544
- )
545
- }
546
-
547
- function getNoteDropPlacement(event: DragEvent): 'before' | 'after' {
548
- const row = event.currentTarget as HTMLElement | null
549
- if (!row) return 'before'
550
-
551
- const rect = row.getBoundingClientRect()
552
- return event.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
553
- }
554
-
555
- function getGroupDropClass(item: SidebarItem): Record<string, boolean> {
556
- const key = getGroupDropKey(item)
557
- return {
558
- 'drop-before':
559
- dropTarget.value?.type === 'group' &&
560
- dropTarget.value.key === key &&
561
- dropTarget.value.placement === 'before',
562
- 'drop-inside':
563
- dropTarget.value?.type === 'group' &&
564
- dropTarget.value.key === key &&
565
- dropTarget.value.placement === 'inside',
566
- }
567
- }
568
-
569
- function getNoteDropClass(item: SidebarItem): Record<string, boolean> {
570
- const key = getNoteDropKey(item)
571
- return {
572
- 'drop-before':
573
- dropTarget.value?.type === 'note' &&
574
- dropTarget.value.key === key &&
575
- dropTarget.value.placement === 'before',
576
- 'drop-after':
577
- dropTarget.value?.type === 'note' &&
578
- dropTarget.value.key === key &&
579
- dropTarget.value.placement === 'after',
580
- }
581
- }
582
-
583
- function onGroupDragStart(event: DragEvent, item: SidebarItem) {
584
- if (!props.isDev) return
585
- closeMenu()
586
- setDragData(event, {
587
- type: 'group',
588
- groupPath: getGroupPath(item),
589
- })
590
- }
591
-
592
- function onNoteDragStart(event: DragEvent, item: SidebarItem) {
593
- if (!props.isDev) return
594
- const noteIndex = extractNoteIdFromLink(item.link)
595
- if (!noteIndex) return
596
-
597
- closeMenu()
598
- setDragData(event, {
599
- type: 'note',
600
- noteIndex,
601
- })
602
- }
603
-
604
- function onGroupDragOver(event: DragEvent, item: SidebarItem) {
605
- if (!props.isDev) return
606
-
607
- const draggedType = getDraggedType(event)
608
- if (!draggedType) return
609
-
610
- setDropEffect(event)
611
-
612
- const targetGroupPath = getGroupPath(item)
613
- const payload = getDragData(event)
614
- if (
615
- draggedType === 'group' &&
616
- payload?.groupPath &&
617
- isSamePath(payload.groupPath, targetGroupPath)
618
- ) {
619
- clearDropTarget()
620
- return
621
- }
622
-
623
- dropTarget.value = {
624
- type: 'group',
625
- key: getGroupDropKey(item),
626
- placement: draggedType === 'note' ? 'inside' : 'before',
627
- }
628
- }
629
-
630
- function onGroupDrop(event: DragEvent, item: SidebarItem) {
631
- if (!props.isDev) return
632
-
633
- const payload = getDragData(event)
634
- if (!payload) {
635
- clearDropTarget()
636
- return
637
- }
638
-
639
- const targetGroupPath = getGroupPath(item)
640
-
641
- if (payload.type === 'note' && payload.noteIndex) {
642
- emit('reorder-sidebar', {
643
- dragType: 'note',
644
- noteIndex: payload.noteIndex,
645
- targetType: 'group',
646
- targetGroupPath,
647
- })
648
- clearDropTarget()
649
- return
650
- }
651
-
652
- if (payload.type === 'group' && payload.groupPath) {
653
- if (isSamePath(payload.groupPath, targetGroupPath)) {
654
- clearDropTarget()
655
- return
656
- }
657
-
658
- emit('reorder-sidebar', {
659
- dragType: 'group',
660
- groupPath: payload.groupPath,
661
- targetType: 'group',
662
- targetGroupPath,
663
- placement: 'before',
664
- })
665
- }
666
-
667
- clearDropTarget()
668
- }
669
-
670
- function onNoteDragOver(event: DragEvent, item: SidebarItem) {
671
- if (!props.isDev || getDraggedType(event) !== 'note') return
672
-
673
- const targetNoteIndex = extractNoteIdFromLink(item.link)
674
- const payload = getDragData(event)
675
- if (!targetNoteIndex || payload?.noteIndex === targetNoteIndex) {
676
- clearDropTarget()
677
- return
678
- }
679
-
680
- setDropEffect(event)
681
- dropTarget.value = {
682
- type: 'note',
683
- key: getNoteDropKey(item),
684
- placement: getNoteDropPlacement(event),
685
- }
686
- }
687
-
688
- function onNoteDrop(event: DragEvent, item: SidebarItem) {
689
- if (!props.isDev) return
690
-
691
- const payload = getDragData(event)
692
- const targetNoteIndex = extractNoteIdFromLink(item.link)
693
- if (
694
- !payload ||
695
- payload.type !== 'note' ||
696
- !payload.noteIndex ||
697
- !targetNoteIndex
698
- ) {
699
- clearDropTarget()
700
- return
701
- }
702
-
703
- if (payload.noteIndex === targetNoteIndex) {
704
- clearDropTarget()
705
- return
706
- }
707
-
708
- const placement = getNoteDropPlacement(event)
709
- emit('reorder-sidebar', {
710
- dragType: 'note',
711
- noteIndex: payload.noteIndex,
712
- targetType: 'note',
713
- targetNoteIndex,
714
- placement,
715
- })
716
- clearDropTarget()
717
- }
718
-
719
- // 获取完整链接(包含 base)
720
- function getFullLink(link?: string) {
721
- if (!link) return '#'
722
- const cleanLink = link.startsWith('/') ? link.slice(1) : link
723
- return props.base + cleanLink
724
- }
725
-
726
- // 判断链接是否激活
727
- function isActive(link?: string) {
728
- if (!link) return false
729
- const fullLink = getFullLink(link)
730
- const decodedRoutePath = decodeURIComponent(props.currentPath)
731
- const decodedFullLink = decodeURIComponent(fullLink)
732
-
733
- return (
734
- decodedRoutePath === decodedFullLink ||
735
- decodedRoutePath === decodedFullLink + '.html'
736
- )
737
- }
738
-
739
- // 从链接中提取笔记 ID
740
- function extractNoteIdFromLink(link?: string): string | null {
741
- if (!link) return null
742
- const match = link.match(/\/notes\/(\d{4})\./)
743
- return match ? match[1] : null
744
- }
745
-
746
- // 提取文本开头的 emoji
747
- function extractEmoji(text: string): { emoji: string; rest: string } {
748
- const emojiMatch = text.match(
749
- /^([\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}✅❌⏰]+)\s*/u
750
- )
751
-
752
- if (emojiMatch) {
753
- return {
754
- emoji: emojiMatch[1],
755
- rest: text.slice(emojiMatch[0].length),
756
- }
757
- }
758
-
759
- return { emoji: '', rest: text }
760
- }
761
-
762
- function getNoteStatusAndText(text: string): {
763
- status: NoteStatus
764
- rest: string
765
- } {
766
- const { emoji, rest } = extractEmoji(text)
767
-
768
- if (emoji.includes('✅')) {
769
- return { status: 'done', rest }
770
- }
771
-
772
- if (emoji.includes('⏰')) {
773
- return { status: 'undone', rest }
774
- }
775
-
776
- return { status: null, rest: text }
777
- }
778
-
779
- function getStatusPrefix(status: NoteStatus): string {
780
- if (status === 'done') return props.donePrefix
781
- if (status === 'undone') return props.undonePrefix
782
-
783
- return ''
784
- }
785
-
786
- function withStatusPrefix(prefix: string, text: string): string {
787
- return prefix ? `${prefix} ${text}` : text
788
- }
789
-
790
- // 获取笔记显示文本
791
- function getNoteDisplayText(text: string, link?: string): string {
792
- const { status, rest } = getNoteStatusAndText(text)
793
- const statusPrefix = getStatusPrefix(status)
794
-
795
- if (props.showNoteId) {
796
- if (/^\d{4}\./.test(rest)) {
797
- return withStatusPrefix(statusPrefix, rest)
798
- }
799
-
800
- const noteId = extractNoteIdFromLink(link)
801
- if (noteId) {
802
- return withStatusPrefix(statusPrefix, `${noteId}. ${rest}`)
803
- }
804
-
805
- return withStatusPrefix(statusPrefix, rest)
806
- } else {
807
- const cleanRest = rest.replace(/^\d{4}\.\s*/, '')
808
- return withStatusPrefix(statusPrefix, cleanRest)
809
- }
810
- }
811
- </script>
812
-
813
- <style scoped>
814
- .group {
815
- margin-bottom: 2px;
816
- }
817
-
818
- .group.density-compact {
819
- margin-bottom: 0;
820
- }
821
-
822
- .group.density-loose {
823
- margin-bottom: 6px;
824
- }
825
-
826
- .sidebar-row {
827
- position: relative;
828
- display: flex;
829
- align-items: center;
830
- width: 100%;
831
- min-height: 30px;
832
- border-radius: 6px;
833
- transition: background-color 0.18s ease;
834
- }
835
-
836
- .sidebar-row.density-compact {
837
- min-height: 26px;
838
- border-radius: 4px;
839
- }
840
-
841
- .sidebar-row.density-loose {
842
- min-height: 36px;
843
- border-radius: 8px;
844
- }
845
-
846
- .sidebar-row:hover {
847
- background: var(--vp-c-default-soft);
848
- }
849
-
850
- .sidebar-row.drop-before::before,
851
- .sidebar-row.drop-after::after {
852
- position: absolute;
853
- right: 8px;
854
- left: 8px;
855
- z-index: 4;
856
- height: 2px;
857
- background: var(--vp-c-brand-1);
858
- border-radius: 999px;
859
- content: '';
860
- pointer-events: none;
861
- }
862
-
863
- .sidebar-row.drop-before::before {
864
- top: -2px;
865
- }
866
-
867
- .sidebar-row.drop-after::after {
868
- bottom: -2px;
869
- }
870
-
871
- .sidebar-row.drop-inside {
872
- background: var(--vp-c-brand-soft);
873
- outline: 1px solid var(--vp-c-brand-1);
874
- outline-offset: -1px;
875
- }
876
-
877
- .row-main {
878
- min-width: 0;
879
- flex: 1;
880
- }
881
-
882
- .group-title {
883
- display: flex;
884
- align-items: center;
885
- gap: 6px;
886
- width: 100%;
887
- padding: 4px 6px;
888
- font-weight: 600;
889
- color: var(--vp-c-text-1);
890
- background: none;
891
- border: none;
892
- cursor: pointer;
893
- text-align: left;
894
- transition: color 0.25s;
895
- }
896
-
897
- .sidebar-row.density-compact .group-title,
898
- .sidebar-row.density-compact .nav-item {
899
- min-height: 26px;
900
- padding: 2px 5px;
901
- font-size: 13px;
902
- line-height: 18px;
903
- }
904
-
905
- .sidebar-row.density-loose .group-title,
906
- .sidebar-row.density-loose .nav-item {
907
- min-height: 36px;
908
- padding: 6px 8px;
909
- font-size: 14px;
910
- line-height: 22px;
911
- }
912
-
913
- .group-title:hover {
914
- color: var(--vp-c-brand-1);
915
- }
916
-
917
- .group-title-text {
918
- overflow: hidden;
919
- text-overflow: ellipsis;
920
- white-space: nowrap;
921
- }
922
-
923
- .arrow {
924
- display: inline-flex;
925
- width: 14px;
926
- height: 14px;
927
- flex: 0 0 14px;
928
- transition: transform 0.25s;
929
- }
930
-
931
- .arrow:not(.collapsed) {
932
- transform: rotate(90deg);
933
- }
934
-
935
- .arrow img {
936
- width: 14px;
937
- height: 14px;
938
- }
939
-
940
- .nav-item {
941
- display: flex;
942
- align-items: center;
943
- min-height: 30px;
944
- padding: 4px 6px;
945
- color: var(--vp-c-text-2);
946
- text-decoration: none;
947
- border-radius: 4px;
948
- font-size: 14px;
949
- line-height: 20px;
950
- transition: all 0.25s;
951
- overflow: hidden;
952
- text-overflow: ellipsis;
953
- white-space: nowrap;
954
- }
955
-
956
- .nav-item:hover {
957
- color: var(--vp-c-brand-1);
958
- }
959
-
960
- .nav-item.active {
961
- color: var(--vp-c-brand-1);
962
- font-weight: 600;
963
- }
964
-
965
- .row-actions {
966
- position: relative;
967
- display: flex;
968
- align-items: center;
969
- gap: 0;
970
- flex: 0 0 auto;
971
- width: 0;
972
- padding-right: 0;
973
- opacity: 0;
974
- overflow: visible;
975
- pointer-events: none;
976
- transition:
977
- width 0.18s ease,
978
- padding-right 0.18s ease,
979
- gap 0.18s ease,
980
- opacity 0.18s ease;
981
- }
982
-
983
- .sidebar-row:hover .row-actions,
984
- .row-actions:focus-within,
985
- .row-actions.menu-open {
986
- gap: 4px;
987
- width: 52px;
988
- padding-right: 6px;
989
- opacity: 1;
990
- pointer-events: auto;
991
- }
992
-
993
- .action-btn {
994
- display: inline-flex;
995
- align-items: center;
996
- justify-content: center;
997
- width: 22px;
998
- height: 22px;
999
- padding: 0;
1000
- color: var(--vp-c-text-2);
1001
- background: transparent;
1002
- border: none;
1003
- border-radius: 4px;
1004
- cursor: pointer;
1005
- font-size: 11px;
1006
- line-height: 1;
1007
- }
1008
-
1009
- .action-btn:hover {
1010
- color: var(--vp-c-brand-1);
1011
- background: var(--vp-c-bg-soft);
1012
- }
1013
-
1014
- .menu-trigger {
1015
- font-weight: 600;
1016
- font-family: var(--vp-font-family-base);
1017
- }
1018
-
1019
- .ellipsis {
1020
- display: inline-block;
1021
- transform: rotate(90deg);
1022
- transform-origin: center;
1023
- letter-spacing: 1px;
1024
- }
1025
-
1026
- .add-trigger {
1027
- font-size: 15px;
1028
- }
1029
-
1030
- .action-menu {
1031
- position: absolute;
1032
- top: calc(100% + 4px);
1033
- right: 4px;
1034
- z-index: 5;
1035
- display: flex;
1036
- flex-direction: column;
1037
- min-width: 128px;
1038
- padding: 4px;
1039
- background: var(--vp-c-bg-elv);
1040
- border: 1px solid var(--vp-c-divider);
1041
- border-radius: 6px;
1042
- box-shadow: var(--vp-shadow-2);
1043
- }
1044
-
1045
- .menu-item {
1046
- display: block;
1047
- width: 100%;
1048
- padding: 6px 8px;
1049
- color: var(--vp-c-text-1);
1050
- background: transparent;
1051
- border: none;
1052
- border-radius: 4px;
1053
- cursor: pointer;
1054
- font-size: 12px;
1055
- line-height: 18px;
1056
- text-align: left;
1057
- white-space: nowrap;
1058
- }
1059
-
1060
- .menu-item:hover {
1061
- color: var(--vp-c-brand-1);
1062
- background: var(--vp-c-default-soft);
1063
- }
1064
-
1065
- .menu-item.danger:hover {
1066
- color: var(--vp-c-danger-1, #ed3c50);
1067
- }
1068
-
1069
- /* 聚焦高亮动画 */
1070
- .nav-item.focus-highlight {
1071
- animation: focusPulse 1s ease-in-out;
1072
- }
1073
-
1074
- @keyframes focusPulse {
1075
- 0%,
1076
- 100% {
1077
- background-color: transparent;
1078
- }
1079
- 50% {
1080
- background-color: var(--vp-c-brand-soft);
1081
- }
1082
- }
1083
- </style>
1
+ <template>
2
+ <template v-for="item in items" :key="getItemKey(item)">
3
+ <!-- 只有在不超过最大层级时才渲染组 -->
4
+ <div
5
+ v-if="hasChildren(item) && depth < maxDepth - 1"
6
+ class="group"
7
+ :class="getDensityClass()"
8
+ >
9
+ <div
10
+ class="sidebar-row group-row"
11
+ :class="[getGroupDropClass(item), getDensityClass()]"
12
+ :style="getNodeStyle(depth)"
13
+ :draggable="isDev"
14
+ @dragstart="onGroupDragStart($event, item)"
15
+ @dragend="clearDropTarget"
16
+ @dragover.prevent.stop="onGroupDragOver($event, item)"
17
+ @drop.stop="onGroupDrop($event, item)"
18
+ >
19
+ <button
20
+ class="group-title row-main"
21
+ :class="`group-title-level-${depth}`"
22
+ @click="toggleItem(item)"
23
+ >
24
+ <span class="arrow" :class="{ collapsed: item.collapsed }">
25
+ <img
26
+ :src="
27
+ item.collapsed ? icon__sidebar_collapsed : icon__sidebar_opened
28
+ "
29
+ alt=""
30
+ />
31
+ </span>
32
+
33
+ <span class="group-title-text">{{ item.text }}</span>
34
+ </button>
35
+
36
+ <div
37
+ v-if="isDev"
38
+ class="row-actions"
39
+ :class="{ 'menu-open': isGroupMenuOpen(item) }"
40
+ @click.stop
41
+ >
42
+ <button
43
+ class="action-btn menu-trigger"
44
+ title="更多操作"
45
+ type="button"
46
+ @click="toggleMenu(getGroupMenuKey(item, 'more'))"
47
+ >
48
+ <span class="ellipsis">...</span>
49
+ </button>
50
+
51
+ <div
52
+ v-if="isMenuOpen(getGroupMenuKey(item, 'more'))"
53
+ class="action-menu"
54
+ >
55
+ <button class="menu-item" type="button" @click="renameGroup(item)">
56
+ 重命名目录
57
+ </button>
58
+ <button
59
+ class="menu-item danger"
60
+ type="button"
61
+ @click="deleteGroup(item)"
62
+ >
63
+ 删除目录
64
+ </button>
65
+ </div>
66
+
67
+ <button
68
+ class="action-btn menu-trigger add-trigger"
69
+ title="新增"
70
+ type="button"
71
+ @click="toggleMenu(getGroupMenuKey(item, 'add'))"
72
+ >
73
+ +
74
+ </button>
75
+
76
+ <div
77
+ v-if="isMenuOpen(getGroupMenuKey(item, 'add'))"
78
+ class="action-menu"
79
+ >
80
+ <button
81
+ class="menu-item"
82
+ type="button"
83
+ @click="createNoteInGroup(item)"
84
+ >
85
+ 新增笔记
86
+ </button>
87
+ <button
88
+ class="menu-item"
89
+ type="button"
90
+ @click="createNotesInGroup(item)"
91
+ >
92
+ 新增多篇笔记
93
+ </button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <div v-show="!item.collapsed" class="group-items">
99
+ <SidebarItems
100
+ :items="getChildItems(item)"
101
+ :depth="depth + 1"
102
+ :max-depth="maxDepth"
103
+ :show-note-id="showNoteId"
104
+ :base="base"
105
+ :current-path="currentPath"
106
+ :item-depth="depth + 1"
107
+ :group-path="getGroupPath(item)"
108
+ :is-dev="isDev"
109
+ :sidebar-density="sidebarDensity"
110
+ :done-prefix="donePrefix"
111
+ :undone-prefix="undonePrefix"
112
+ @rename-group="$emit('rename-group', $event)"
113
+ @delete-group="$emit('delete-group', $event)"
114
+ @create-note-in-group="$emit('create-note-in-group', $event)"
115
+ @create-notes-in-group="$emit('create-notes-in-group', $event)"
116
+ @rename-note="$emit('rename-note', $event)"
117
+ @delete-note="$emit('delete-note', $event)"
118
+ @create-note-around="$emit('create-note-around', $event)"
119
+ @open-note-about="$emit('open-note-about', $event)"
120
+ @reorder-sidebar="$emit('reorder-sidebar', $event)"
121
+ />
122
+ </div>
123
+ </div>
124
+
125
+ <!-- 如果是链接项,或者是超过最大层级的组,都不渲染 -->
126
+ <div
127
+ v-else-if="!hasChildren(item)"
128
+ class="sidebar-row note-row"
129
+ :class="[getNoteDropClass(item), getDensityClass()]"
130
+ :style="getNodeStyle(actualItemDepth)"
131
+ :draggable="isDev"
132
+ @dragstart="onNoteDragStart($event, item)"
133
+ @dragend="clearDropTarget"
134
+ @dragover.prevent.stop="onNoteDragOver($event, item)"
135
+ @drop.stop="onNoteDrop($event, item)"
136
+ >
137
+ <a
138
+ :href="getFullLink(item.link)"
139
+ :class="[
140
+ 'nav-item',
141
+ 'row-main',
142
+ { active: isActive(item.link) },
143
+ `nav-item-${extractNoteIdFromLink(item.link)}`,
144
+ `nav-item-level-${actualItemDepth + 1}`,
145
+ ]"
146
+ :data-note-id="extractNoteIdFromLink(item.link)"
147
+ >
148
+ {{ getNoteDisplayText(item.text, item.link) }}
149
+ </a>
150
+
151
+ <div
152
+ v-if="isDev"
153
+ class="row-actions"
154
+ :class="{ 'menu-open': isNoteMenuOpen(item) }"
155
+ @click.stop
156
+ >
157
+ <button
158
+ class="action-btn menu-trigger"
159
+ title="更多操作"
160
+ type="button"
161
+ @click="toggleMenu(getNoteMenuKey(item, 'more'))"
162
+ >
163
+ <span class="ellipsis">...</span>
164
+ </button>
165
+
166
+ <div
167
+ v-if="isMenuOpen(getNoteMenuKey(item, 'more'))"
168
+ class="action-menu"
169
+ >
170
+ <button class="menu-item" type="button" @click="renameNote(item)">
171
+ 重命名
172
+ </button>
173
+ <button
174
+ class="menu-item danger"
175
+ type="button"
176
+ @click="deleteNote(item)"
177
+ >
178
+ 删除笔记
179
+ </button>
180
+ <button class="menu-item" type="button" @click="openNoteAbout(item)">
181
+ 关于
182
+ </button>
183
+ </div>
184
+
185
+ <button
186
+ class="action-btn menu-trigger add-trigger"
187
+ title="新增"
188
+ type="button"
189
+ @click="toggleMenu(getNoteMenuKey(item, 'add'))"
190
+ >
191
+ +
192
+ </button>
193
+
194
+ <div
195
+ v-if="isMenuOpen(getNoteMenuKey(item, 'add'))"
196
+ class="action-menu"
197
+ >
198
+ <button
199
+ class="menu-item"
200
+ type="button"
201
+ @click="createNoteAround(item, 'before')"
202
+ >
203
+ 在上方新增笔记
204
+ </button>
205
+ <button
206
+ class="menu-item"
207
+ type="button"
208
+ @click="createNoteAround(item, 'after')"
209
+ >
210
+ 在下方新增笔记
211
+ </button>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </template>
216
+ </template>
217
+
218
+ <script setup lang="ts">
219
+ import { computed, onBeforeUnmount, ref } from 'vue'
220
+
221
+ import {
222
+ icon__sidebar_opened,
223
+ icon__sidebar_collapsed,
224
+ } from '../../assets/icons'
225
+
226
+ interface SidebarItem {
227
+ text: string
228
+ link?: string
229
+ items?: SidebarItem[]
230
+ collapsed?: boolean
231
+ }
232
+
233
+ interface Props {
234
+ items: SidebarItem[]
235
+ depth: number
236
+ maxDepth: number
237
+ showNoteId: boolean
238
+ base: string
239
+ currentPath: string
240
+ itemDepth?: number // 用于计算链接项的缩进,默认等于 depth
241
+ groupPath?: string[]
242
+ isDev?: boolean
243
+ sidebarDensity?: SidebarDensity
244
+ donePrefix?: string
245
+ undonePrefix?: string
246
+ }
247
+
248
+ const props = withDefaults(defineProps<Props>(), {
249
+ depth: 0,
250
+ maxDepth: 3,
251
+ showNoteId: false,
252
+ base: '/',
253
+ currentPath: '',
254
+ itemDepth: undefined,
255
+ groupPath: () => [],
256
+ isDev: false,
257
+ sidebarDensity: 'default',
258
+ donePrefix: '✅',
259
+ undonePrefix: '⏰',
260
+ })
261
+
262
+ const emit = defineEmits<{
263
+ 'rename-group': [payload: GroupPayload]
264
+ 'delete-group': [payload: GroupPayload]
265
+ 'create-note-in-group': [payload: GroupPayload]
266
+ 'create-notes-in-group': [payload: GroupPayload]
267
+ 'rename-note': [payload: NotePayload]
268
+ 'delete-note': [payload: NotePayload]
269
+ 'create-note-around': [
270
+ payload: NotePayload & { placement: 'before' | 'after' },
271
+ ]
272
+ 'open-note-about': [payload: NotePayload]
273
+ 'reorder-sidebar': [payload: ReorderPayload]
274
+ }>()
275
+
276
+ interface GroupPayload {
277
+ groupPath: string[]
278
+ text: string
279
+ }
280
+
281
+ interface NotePayload {
282
+ noteIndex: string
283
+ text: string
284
+ link?: string
285
+ }
286
+
287
+ interface DragPayload {
288
+ type: 'group' | 'note'
289
+ groupPath?: string[]
290
+ noteIndex?: string
291
+ }
292
+
293
+ interface DropTarget {
294
+ type: 'group' | 'note'
295
+ key: string
296
+ placement: 'before' | 'after' | 'inside'
297
+ }
298
+
299
+ interface ReorderPayload {
300
+ dragType: 'group' | 'note'
301
+ groupPath?: string[]
302
+ noteIndex?: string
303
+ targetType: 'group' | 'note'
304
+ targetGroupPath?: string[]
305
+ targetNoteIndex?: string
306
+ placement?: 'before' | 'after'
307
+ }
308
+
309
+ type MenuType = 'more' | 'add'
310
+ type SidebarDensity = 'compact' | 'default' | 'loose'
311
+ type NoteStatus = 'done' | 'undone' | null
312
+
313
+ // 获取实际的 item depth(用于链接项的缩进)
314
+ const actualItemDepth = computed(() => props.itemDepth ?? props.depth)
315
+ const openMenuKey = ref<string | null>(null)
316
+ const dropTarget = ref<DropTarget | null>(null)
317
+ let ignoredToggleKey: string | null = null
318
+
319
+ const dragDataType = 'application/x-tnotes-sidebar-node'
320
+ const dragNoteType = 'application/x-tnotes-sidebar-note'
321
+ const dragGroupType = 'application/x-tnotes-sidebar-group'
322
+
323
+ if (typeof document !== 'undefined') {
324
+ document.addEventListener('click', closeMenuOnDocumentClick, true)
325
+ document.addEventListener('dragend', clearDropTarget, true)
326
+ document.addEventListener('drop', clearDropTarget, true)
327
+ }
328
+
329
+ onBeforeUnmount(() => {
330
+ if (typeof document !== 'undefined') {
331
+ document.removeEventListener('click', closeMenuOnDocumentClick, true)
332
+ document.removeEventListener('dragend', clearDropTarget, true)
333
+ document.removeEventListener('drop', clearDropTarget, true)
334
+ }
335
+ })
336
+
337
+ // 判断项是否有子项
338
+ function hasChildren(item: SidebarItem): boolean {
339
+ return !!(item.items && item.items.length > 0)
340
+ }
341
+
342
+ function getChildItems(item: SidebarItem): SidebarItem[] {
343
+ return item.items ?? []
344
+ }
345
+
346
+ // 获取项的唯一 key
347
+ function getItemKey(item: SidebarItem): string {
348
+ return item.link || item.text
349
+ }
350
+
351
+ // 切换项的展开/折叠状态
352
+ function toggleItem(item: SidebarItem) {
353
+ item.collapsed = !item.collapsed
354
+ }
355
+
356
+ function getNodeStyle(depth: number) {
357
+ return {
358
+ paddingLeft: `${depth * getIndentSize()}px`,
359
+ }
360
+ }
361
+
362
+ function getDensityClass(): string {
363
+ return `density-${props.sidebarDensity}`
364
+ }
365
+
366
+ function getIndentSize(): number {
367
+ if (props.sidebarDensity === 'compact') return 20
368
+ if (props.sidebarDensity === 'loose') return 28
369
+ return 24
370
+ }
371
+
372
+ function getGroupPath(item: SidebarItem): string[] {
373
+ return [...props.groupPath, item.text]
374
+ }
375
+
376
+ function getGroupPayload(item: SidebarItem): GroupPayload {
377
+ return {
378
+ groupPath: getGroupPath(item),
379
+ text: item.text,
380
+ }
381
+ }
382
+
383
+ function getNotePayload(item: SidebarItem): NotePayload {
384
+ return {
385
+ noteIndex: extractNoteIdFromLink(item.link) || '',
386
+ text: item.text,
387
+ link: item.link,
388
+ }
389
+ }
390
+
391
+ function getGroupMenuKey(item: SidebarItem, menuType: MenuType): string {
392
+ return `group:${menuType}:${getGroupPath(item).join('/')}`
393
+ }
394
+
395
+ function getGroupDropKey(item: SidebarItem): string {
396
+ return `group:${getGroupPath(item).join('/')}`
397
+ }
398
+
399
+ function getNoteMenuKey(item: SidebarItem, menuType: MenuType): string {
400
+ return `note:${menuType}:${extractNoteIdFromLink(item.link) || item.link || item.text}`
401
+ }
402
+
403
+ function getNoteDropKey(item: SidebarItem): string {
404
+ return `note:${extractNoteIdFromLink(item.link) || item.link || item.text}`
405
+ }
406
+
407
+ function toggleMenu(menuKey: string) {
408
+ if (ignoredToggleKey === menuKey) {
409
+ ignoredToggleKey = null
410
+ return
411
+ }
412
+
413
+ openMenuKey.value = openMenuKey.value === menuKey ? null : menuKey
414
+ }
415
+
416
+ function closeMenu() {
417
+ openMenuKey.value = null
418
+ }
419
+
420
+ function isMenuOpen(menuKey: string): boolean {
421
+ return openMenuKey.value === menuKey
422
+ }
423
+
424
+ function closeMenuOnDocumentClick() {
425
+ if (!openMenuKey.value) return
426
+
427
+ const closedMenuKey = openMenuKey.value
428
+ openMenuKey.value = null
429
+ ignoredToggleKey = closedMenuKey
430
+
431
+ window.setTimeout(() => {
432
+ if (ignoredToggleKey === closedMenuKey) {
433
+ ignoredToggleKey = null
434
+ }
435
+ })
436
+ }
437
+
438
+ function isGroupMenuOpen(item: SidebarItem): boolean {
439
+ return (
440
+ isMenuOpen(getGroupMenuKey(item, 'more')) ||
441
+ isMenuOpen(getGroupMenuKey(item, 'add'))
442
+ )
443
+ }
444
+
445
+ function isNoteMenuOpen(item: SidebarItem): boolean {
446
+ return (
447
+ isMenuOpen(getNoteMenuKey(item, 'more')) ||
448
+ isMenuOpen(getNoteMenuKey(item, 'add'))
449
+ )
450
+ }
451
+
452
+ function renameGroup(item: SidebarItem) {
453
+ emit('rename-group', getGroupPayload(item))
454
+ closeMenu()
455
+ }
456
+
457
+ function deleteGroup(item: SidebarItem) {
458
+ emit('delete-group', getGroupPayload(item))
459
+ closeMenu()
460
+ }
461
+
462
+ function createNoteInGroup(item: SidebarItem) {
463
+ emit('create-note-in-group', getGroupPayload(item))
464
+ closeMenu()
465
+ }
466
+
467
+ function createNotesInGroup(item: SidebarItem) {
468
+ emit('create-notes-in-group', getGroupPayload(item))
469
+ closeMenu()
470
+ }
471
+
472
+ function renameNote(item: SidebarItem) {
473
+ emit('rename-note', getNotePayload(item))
474
+ closeMenu()
475
+ }
476
+
477
+ function deleteNote(item: SidebarItem) {
478
+ emit('delete-note', getNotePayload(item))
479
+ closeMenu()
480
+ }
481
+
482
+ function openNoteAbout(item: SidebarItem) {
483
+ emit('open-note-about', getNotePayload(item))
484
+ closeMenu()
485
+ }
486
+
487
+ function createNoteAround(item: SidebarItem, placement: 'before' | 'after') {
488
+ emit('create-note-around', {
489
+ ...getNotePayload(item),
490
+ placement,
491
+ })
492
+ closeMenu()
493
+ }
494
+
495
+ function setDragData(event: DragEvent, payload: DragPayload) {
496
+ if (!event.dataTransfer) return
497
+
498
+ event.dataTransfer.effectAllowed = 'move'
499
+ event.dataTransfer.setData(dragDataType, JSON.stringify(payload))
500
+ event.dataTransfer.setData(
501
+ payload.type === 'note' ? dragNoteType : dragGroupType,
502
+ '1',
503
+ )
504
+ }
505
+
506
+ function getDragData(event: DragEvent): DragPayload | null {
507
+ const raw = event.dataTransfer?.getData(dragDataType)
508
+ if (!raw) return null
509
+
510
+ try {
511
+ return JSON.parse(raw) as DragPayload
512
+ } catch {
513
+ return null
514
+ }
515
+ }
516
+
517
+ function getDragTypes(event: DragEvent): string[] {
518
+ return Array.from(event.dataTransfer?.types ?? [])
519
+ }
520
+
521
+ function getDraggedType(event: DragEvent): DragPayload['type'] | null {
522
+ const types = getDragTypes(event)
523
+
524
+ if (types.includes(dragNoteType)) return 'note'
525
+ if (types.includes(dragGroupType)) return 'group'
526
+
527
+ return getDragData(event)?.type ?? null
528
+ }
529
+
530
+ function setDropEffect(event: DragEvent) {
531
+ if (event.dataTransfer) {
532
+ event.dataTransfer.dropEffect = 'move'
533
+ }
534
+ }
535
+
536
+ function clearDropTarget() {
537
+ dropTarget.value = null
538
+ }
539
+
540
+ function isSamePath(left: string[], right: string[]): boolean {
541
+ return (
542
+ left.length === right.length &&
543
+ left.every((item, index) => item === right[index])
544
+ )
545
+ }
546
+
547
+ function getNoteDropPlacement(event: DragEvent): 'before' | 'after' {
548
+ const row = event.currentTarget as HTMLElement | null
549
+ if (!row) return 'before'
550
+
551
+ const rect = row.getBoundingClientRect()
552
+ return event.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
553
+ }
554
+
555
+ function getGroupDropClass(item: SidebarItem): Record<string, boolean> {
556
+ const key = getGroupDropKey(item)
557
+ return {
558
+ 'drop-before':
559
+ dropTarget.value?.type === 'group' &&
560
+ dropTarget.value.key === key &&
561
+ dropTarget.value.placement === 'before',
562
+ 'drop-inside':
563
+ dropTarget.value?.type === 'group' &&
564
+ dropTarget.value.key === key &&
565
+ dropTarget.value.placement === 'inside',
566
+ }
567
+ }
568
+
569
+ function getNoteDropClass(item: SidebarItem): Record<string, boolean> {
570
+ const key = getNoteDropKey(item)
571
+ return {
572
+ 'drop-before':
573
+ dropTarget.value?.type === 'note' &&
574
+ dropTarget.value.key === key &&
575
+ dropTarget.value.placement === 'before',
576
+ 'drop-after':
577
+ dropTarget.value?.type === 'note' &&
578
+ dropTarget.value.key === key &&
579
+ dropTarget.value.placement === 'after',
580
+ }
581
+ }
582
+
583
+ function onGroupDragStart(event: DragEvent, item: SidebarItem) {
584
+ if (!props.isDev) return
585
+ closeMenu()
586
+ setDragData(event, {
587
+ type: 'group',
588
+ groupPath: getGroupPath(item),
589
+ })
590
+ }
591
+
592
+ function onNoteDragStart(event: DragEvent, item: SidebarItem) {
593
+ if (!props.isDev) return
594
+ const noteIndex = extractNoteIdFromLink(item.link)
595
+ if (!noteIndex) return
596
+
597
+ closeMenu()
598
+ setDragData(event, {
599
+ type: 'note',
600
+ noteIndex,
601
+ })
602
+ }
603
+
604
+ function onGroupDragOver(event: DragEvent, item: SidebarItem) {
605
+ if (!props.isDev) return
606
+
607
+ const draggedType = getDraggedType(event)
608
+ if (!draggedType) return
609
+
610
+ setDropEffect(event)
611
+
612
+ const targetGroupPath = getGroupPath(item)
613
+ const payload = getDragData(event)
614
+ if (
615
+ draggedType === 'group' &&
616
+ payload?.groupPath &&
617
+ isSamePath(payload.groupPath, targetGroupPath)
618
+ ) {
619
+ clearDropTarget()
620
+ return
621
+ }
622
+
623
+ dropTarget.value = {
624
+ type: 'group',
625
+ key: getGroupDropKey(item),
626
+ placement: draggedType === 'note' ? 'inside' : 'before',
627
+ }
628
+ }
629
+
630
+ function onGroupDrop(event: DragEvent, item: SidebarItem) {
631
+ if (!props.isDev) return
632
+
633
+ const payload = getDragData(event)
634
+ if (!payload) {
635
+ clearDropTarget()
636
+ return
637
+ }
638
+
639
+ const targetGroupPath = getGroupPath(item)
640
+
641
+ if (payload.type === 'note' && payload.noteIndex) {
642
+ emit('reorder-sidebar', {
643
+ dragType: 'note',
644
+ noteIndex: payload.noteIndex,
645
+ targetType: 'group',
646
+ targetGroupPath,
647
+ })
648
+ clearDropTarget()
649
+ return
650
+ }
651
+
652
+ if (payload.type === 'group' && payload.groupPath) {
653
+ if (isSamePath(payload.groupPath, targetGroupPath)) {
654
+ clearDropTarget()
655
+ return
656
+ }
657
+
658
+ emit('reorder-sidebar', {
659
+ dragType: 'group',
660
+ groupPath: payload.groupPath,
661
+ targetType: 'group',
662
+ targetGroupPath,
663
+ placement: 'before',
664
+ })
665
+ }
666
+
667
+ clearDropTarget()
668
+ }
669
+
670
+ function onNoteDragOver(event: DragEvent, item: SidebarItem) {
671
+ if (!props.isDev || getDraggedType(event) !== 'note') return
672
+
673
+ const targetNoteIndex = extractNoteIdFromLink(item.link)
674
+ const payload = getDragData(event)
675
+ if (!targetNoteIndex || payload?.noteIndex === targetNoteIndex) {
676
+ clearDropTarget()
677
+ return
678
+ }
679
+
680
+ setDropEffect(event)
681
+ dropTarget.value = {
682
+ type: 'note',
683
+ key: getNoteDropKey(item),
684
+ placement: getNoteDropPlacement(event),
685
+ }
686
+ }
687
+
688
+ function onNoteDrop(event: DragEvent, item: SidebarItem) {
689
+ if (!props.isDev) return
690
+
691
+ const payload = getDragData(event)
692
+ const targetNoteIndex = extractNoteIdFromLink(item.link)
693
+ if (
694
+ !payload ||
695
+ payload.type !== 'note' ||
696
+ !payload.noteIndex ||
697
+ !targetNoteIndex
698
+ ) {
699
+ clearDropTarget()
700
+ return
701
+ }
702
+
703
+ if (payload.noteIndex === targetNoteIndex) {
704
+ clearDropTarget()
705
+ return
706
+ }
707
+
708
+ const placement = getNoteDropPlacement(event)
709
+ emit('reorder-sidebar', {
710
+ dragType: 'note',
711
+ noteIndex: payload.noteIndex,
712
+ targetType: 'note',
713
+ targetNoteIndex,
714
+ placement,
715
+ })
716
+ clearDropTarget()
717
+ }
718
+
719
+ // 获取完整链接(包含 base)
720
+ function getFullLink(link?: string) {
721
+ if (!link) return '#'
722
+ const cleanLink = link.startsWith('/') ? link.slice(1) : link
723
+ return props.base + cleanLink
724
+ }
725
+
726
+ // 判断链接是否激活
727
+ function isActive(link?: string) {
728
+ if (!link) return false
729
+ const fullLink = getFullLink(link)
730
+ const decodedRoutePath = decodeURIComponent(props.currentPath)
731
+ const decodedFullLink = decodeURIComponent(fullLink)
732
+
733
+ return (
734
+ decodedRoutePath === decodedFullLink ||
735
+ decodedRoutePath === decodedFullLink + '.html'
736
+ )
737
+ }
738
+
739
+ // 从链接中提取笔记 ID
740
+ function extractNoteIdFromLink(link?: string): string | null {
741
+ if (!link) return null
742
+ const match = link.match(/\/notes\/(\d{4})\./)
743
+ return match ? match[1] : null
744
+ }
745
+
746
+ // 提取文本开头的 emoji
747
+ function extractEmoji(text: string): { emoji: string; rest: string } {
748
+ const emojiMatch = text.match(
749
+ /^([\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}✅❌⏰]+)\s*/u
750
+ )
751
+
752
+ if (emojiMatch) {
753
+ return {
754
+ emoji: emojiMatch[1],
755
+ rest: text.slice(emojiMatch[0].length),
756
+ }
757
+ }
758
+
759
+ return { emoji: '', rest: text }
760
+ }
761
+
762
+ function getNoteStatusAndText(text: string): {
763
+ status: NoteStatus
764
+ rest: string
765
+ } {
766
+ const { emoji, rest } = extractEmoji(text)
767
+
768
+ if (emoji.includes('✅')) {
769
+ return { status: 'done', rest }
770
+ }
771
+
772
+ if (emoji.includes('⏰')) {
773
+ return { status: 'undone', rest }
774
+ }
775
+
776
+ return { status: null, rest: text }
777
+ }
778
+
779
+ function getStatusPrefix(status: NoteStatus): string {
780
+ if (status === 'done') return props.donePrefix
781
+ if (status === 'undone') return props.undonePrefix
782
+
783
+ return ''
784
+ }
785
+
786
+ function withStatusPrefix(prefix: string, text: string): string {
787
+ return prefix ? `${prefix} ${text}` : text
788
+ }
789
+
790
+ // 获取笔记显示文本
791
+ function getNoteDisplayText(text: string, link?: string): string {
792
+ const { status, rest } = getNoteStatusAndText(text)
793
+ const statusPrefix = getStatusPrefix(status)
794
+
795
+ if (props.showNoteId) {
796
+ if (/^\d{4}\./.test(rest)) {
797
+ return withStatusPrefix(statusPrefix, rest)
798
+ }
799
+
800
+ const noteId = extractNoteIdFromLink(link)
801
+ if (noteId) {
802
+ return withStatusPrefix(statusPrefix, `${noteId}. ${rest}`)
803
+ }
804
+
805
+ return withStatusPrefix(statusPrefix, rest)
806
+ } else {
807
+ const cleanRest = rest.replace(/^\d{4}\.\s*/, '')
808
+ return withStatusPrefix(statusPrefix, cleanRest)
809
+ }
810
+ }
811
+ </script>
812
+
813
+ <style scoped>
814
+ .group {
815
+ margin-bottom: 2px;
816
+ }
817
+
818
+ .group.density-compact {
819
+ margin-bottom: 0;
820
+ }
821
+
822
+ .group.density-loose {
823
+ margin-bottom: 6px;
824
+ }
825
+
826
+ .sidebar-row {
827
+ position: relative;
828
+ display: flex;
829
+ align-items: center;
830
+ width: 100%;
831
+ min-height: 30px;
832
+ border-radius: 6px;
833
+ transition: background-color 0.18s ease;
834
+ }
835
+
836
+ .sidebar-row.density-compact {
837
+ min-height: 26px;
838
+ border-radius: 4px;
839
+ }
840
+
841
+ .sidebar-row.density-loose {
842
+ min-height: 36px;
843
+ border-radius: 8px;
844
+ }
845
+
846
+ .sidebar-row:hover {
847
+ background: var(--vp-c-default-soft);
848
+ }
849
+
850
+ .sidebar-row.drop-before::before,
851
+ .sidebar-row.drop-after::after {
852
+ position: absolute;
853
+ right: 8px;
854
+ left: 8px;
855
+ z-index: 4;
856
+ height: 2px;
857
+ background: var(--vp-c-brand-1);
858
+ border-radius: 999px;
859
+ content: '';
860
+ pointer-events: none;
861
+ }
862
+
863
+ .sidebar-row.drop-before::before {
864
+ top: -2px;
865
+ }
866
+
867
+ .sidebar-row.drop-after::after {
868
+ bottom: -2px;
869
+ }
870
+
871
+ .sidebar-row.drop-inside {
872
+ background: var(--vp-c-brand-soft);
873
+ outline: 1px solid var(--vp-c-brand-1);
874
+ outline-offset: -1px;
875
+ }
876
+
877
+ .row-main {
878
+ min-width: 0;
879
+ flex: 1;
880
+ }
881
+
882
+ .group-title {
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 6px;
886
+ width: 100%;
887
+ padding: 4px 6px;
888
+ font-weight: 600;
889
+ color: var(--vp-c-text-1);
890
+ background: none;
891
+ border: none;
892
+ cursor: pointer;
893
+ text-align: left;
894
+ transition: color 0.25s;
895
+ }
896
+
897
+ .sidebar-row.density-compact .group-title,
898
+ .sidebar-row.density-compact .nav-item {
899
+ min-height: 26px;
900
+ padding: 2px 5px;
901
+ font-size: 13px;
902
+ line-height: 18px;
903
+ }
904
+
905
+ .sidebar-row.density-loose .group-title,
906
+ .sidebar-row.density-loose .nav-item {
907
+ min-height: 36px;
908
+ padding: 6px 8px;
909
+ font-size: 14px;
910
+ line-height: 22px;
911
+ }
912
+
913
+ .group-title:hover {
914
+ color: var(--vp-c-brand-1);
915
+ }
916
+
917
+ .group-title-text {
918
+ overflow: hidden;
919
+ text-overflow: ellipsis;
920
+ white-space: nowrap;
921
+ }
922
+
923
+ .arrow {
924
+ display: inline-flex;
925
+ width: 14px;
926
+ height: 14px;
927
+ flex: 0 0 14px;
928
+ transition: transform 0.25s;
929
+ }
930
+
931
+ .arrow:not(.collapsed) {
932
+ transform: rotate(90deg);
933
+ }
934
+
935
+ .arrow img {
936
+ width: 14px;
937
+ height: 14px;
938
+ }
939
+
940
+ .nav-item {
941
+ display: flex;
942
+ align-items: center;
943
+ min-height: 30px;
944
+ padding: 4px 6px;
945
+ color: var(--vp-c-text-2);
946
+ text-decoration: none;
947
+ border-radius: 4px;
948
+ font-size: 14px;
949
+ line-height: 20px;
950
+ transition: all 0.25s;
951
+ overflow: hidden;
952
+ text-overflow: ellipsis;
953
+ white-space: nowrap;
954
+ }
955
+
956
+ .nav-item:hover {
957
+ color: var(--vp-c-brand-1);
958
+ }
959
+
960
+ .nav-item.active {
961
+ color: var(--vp-c-brand-1);
962
+ font-weight: 600;
963
+ }
964
+
965
+ .row-actions {
966
+ position: relative;
967
+ display: flex;
968
+ align-items: center;
969
+ gap: 0;
970
+ flex: 0 0 auto;
971
+ width: 0;
972
+ padding-right: 0;
973
+ opacity: 0;
974
+ overflow: visible;
975
+ pointer-events: none;
976
+ transition:
977
+ width 0.18s ease,
978
+ padding-right 0.18s ease,
979
+ gap 0.18s ease,
980
+ opacity 0.18s ease;
981
+ }
982
+
983
+ .sidebar-row:hover .row-actions,
984
+ .row-actions:focus-within,
985
+ .row-actions.menu-open {
986
+ gap: 4px;
987
+ width: 52px;
988
+ padding-right: 6px;
989
+ opacity: 1;
990
+ pointer-events: auto;
991
+ }
992
+
993
+ .action-btn {
994
+ display: inline-flex;
995
+ align-items: center;
996
+ justify-content: center;
997
+ width: 22px;
998
+ height: 22px;
999
+ padding: 0;
1000
+ color: var(--vp-c-text-2);
1001
+ background: transparent;
1002
+ border: none;
1003
+ border-radius: 4px;
1004
+ cursor: pointer;
1005
+ font-size: 11px;
1006
+ line-height: 1;
1007
+ }
1008
+
1009
+ .action-btn:hover {
1010
+ color: var(--vp-c-brand-1);
1011
+ background: var(--vp-c-bg-soft);
1012
+ }
1013
+
1014
+ .menu-trigger {
1015
+ font-weight: 600;
1016
+ font-family: var(--vp-font-family-base);
1017
+ }
1018
+
1019
+ .ellipsis {
1020
+ display: inline-block;
1021
+ transform: rotate(90deg);
1022
+ transform-origin: center;
1023
+ letter-spacing: 1px;
1024
+ }
1025
+
1026
+ .add-trigger {
1027
+ font-size: 15px;
1028
+ }
1029
+
1030
+ .action-menu {
1031
+ position: absolute;
1032
+ top: calc(100% + 4px);
1033
+ right: 4px;
1034
+ z-index: 5;
1035
+ display: flex;
1036
+ flex-direction: column;
1037
+ min-width: 128px;
1038
+ padding: 4px;
1039
+ background: var(--vp-c-bg-elv);
1040
+ border: 1px solid var(--vp-c-divider);
1041
+ border-radius: 6px;
1042
+ box-shadow: var(--vp-shadow-2);
1043
+ }
1044
+
1045
+ .menu-item {
1046
+ display: block;
1047
+ width: 100%;
1048
+ padding: 6px 8px;
1049
+ color: var(--vp-c-text-1);
1050
+ background: transparent;
1051
+ border: none;
1052
+ border-radius: 4px;
1053
+ cursor: pointer;
1054
+ font-size: 12px;
1055
+ line-height: 18px;
1056
+ text-align: left;
1057
+ white-space: nowrap;
1058
+ }
1059
+
1060
+ .menu-item:hover {
1061
+ color: var(--vp-c-brand-1);
1062
+ background: var(--vp-c-default-soft);
1063
+ }
1064
+
1065
+ .menu-item.danger:hover {
1066
+ color: var(--vp-c-danger-1, #ed3c50);
1067
+ }
1068
+
1069
+ /* 聚焦高亮动画 */
1070
+ .nav-item.focus-highlight {
1071
+ animation: focusPulse 1s ease-in-out;
1072
+ }
1073
+
1074
+ @keyframes focusPulse {
1075
+ 0%,
1076
+ 100% {
1077
+ background-color: transparent;
1078
+ }
1079
+ 50% {
1080
+ background-color: var(--vp-c-brand-soft);
1081
+ }
1082
+ }
1083
+ </style>