@tnotesjs/core 0.1.18 → 0.1.20

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 (114) hide show
  1. package/README.md +87 -87
  2. package/package.json +1 -1
  3. package/types/config.ts +61 -61
  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 +213 -213
  38. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  39. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  40. package/vitepress/components/Discussions/Discussions.vue +211 -211
  41. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  42. package/vitepress/components/EnWordList/EnWordList.vue +543 -543
  43. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  44. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  45. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  46. package/vitepress/components/Footprints/Footprints.vue +377 -377
  47. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  48. package/vitepress/components/Layout/AboutModal.vue +105 -105
  49. package/vitepress/components/Layout/AboutPanel.vue +270 -270
  50. package/vitepress/components/Layout/ContentCollapse.vue +603 -603
  51. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  52. package/vitepress/components/Layout/DocBeforeControls.vue +149 -149
  53. package/vitepress/components/Layout/DocFooter.vue +233 -233
  54. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  55. package/vitepress/components/Layout/ImagePreview.vue +281 -281
  56. package/vitepress/components/Layout/Layout.module.scss +661 -661
  57. package/vitepress/components/Layout/Layout.vue +621 -621
  58. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  59. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  60. package/vitepress/components/Layout/SidebarNavBefore.vue +171 -171
  61. package/vitepress/components/Layout/SidebarResizeHandle.vue +319 -319
  62. package/vitepress/components/Layout/Swiper.vue +167 -167
  63. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  64. package/vitepress/components/Layout/ToggleFullContent.vue +35 -35
  65. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  66. package/vitepress/components/Layout/ToggleSidebar.vue +26 -26
  67. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  68. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  69. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  70. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  71. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  72. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  73. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  74. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  75. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  76. package/vitepress/components/MarkMap/MarkMap.vue +405 -405
  77. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  78. package/vitepress/components/Mermaid/Mermaid.vue +365 -365
  79. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  80. package/vitepress/components/NotesTable/NotesTable.vue +98 -98
  81. package/vitepress/components/NotesTable/README.md +67 -67
  82. package/vitepress/components/Settings/Settings.module.scss +375 -375
  83. package/vitepress/components/Settings/Settings.vue +334 -334
  84. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  85. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  86. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -946
  87. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  88. package/vitepress/components/constants.ts +109 -109
  89. package/vitepress/components/notesConfig.data.ts +73 -73
  90. package/vitepress/components/sidebar.data.ts +59 -59
  91. package/vitepress/components/tnotes-config.data.ts +21 -21
  92. package/vitepress/components/utils.ts +26 -26
  93. package/vitepress/config/index.ts +169 -169
  94. package/vitepress/configs/constants.ts +26 -26
  95. package/vitepress/configs/head.config.ts +25 -25
  96. package/vitepress/configs/index.ts +9 -9
  97. package/vitepress/configs/markdown-it.d.ts +23 -23
  98. package/vitepress/configs/markdown.config.ts +366 -366
  99. package/vitepress/configs/theme.config.ts +108 -108
  100. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  101. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  102. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  103. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  104. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  105. package/vitepress/theme/index.ts +137 -137
  106. package/vitepress/theme/styles/base.scss +64 -59
  107. package/vitepress/theme/styles/components/404.scss +31 -31
  108. package/vitepress/theme/styles/components/collapse.scss +172 -185
  109. package/vitepress/theme/styles/components/markmap.scss +101 -101
  110. package/vitepress/theme/styles/components/swiper.scss +255 -255
  111. package/vitepress/theme/styles/index.scss +25 -25
  112. package/vitepress/theme/styles/layout.scss +78 -78
  113. package/vitepress/theme/styles/utilities.scss +39 -39
  114. 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>