@tnotesjs/core 0.1.27 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +2 -1
  2. package/package.json +3 -1
  3. package/templates/sub-repo/.gitattributes +30 -0
  4. package/templates/sub-repo/.github/workflows/deploy.yml +87 -0
  5. package/templates/sub-repo/.vitepress/config.mts +12 -0
  6. package/templates/sub-repo/.vitepress/env.d.ts +10 -0
  7. package/templates/sub-repo/.vitepress/theme/index.ts +12 -0
  8. package/templates/sub-repo/.vscode/settings.json +330 -0
  9. package/templates/sub-repo/.vscode/tnotes.code-snippets +110 -0
  10. package/templates/sub-repo/public/favicon.ico +0 -0
  11. package/templates/sub-repo/public/logo.png +0 -0
  12. package/templates/sub-repo/tsconfig.json +45 -0
  13. package/vitepress/assets/icons/icon__setting_dark.svg +4 -0
  14. package/vitepress/assets/icons/icon__setting_light.svg +4 -0
  15. package/vitepress/assets/icons/index.ts +2 -2
  16. package/vitepress/client/localSearchIndexBridge.ts +24 -0
  17. package/vitepress/components/Discussions/Discussions.vue +1 -1
  18. package/vitepress/components/Footprints/Footprints.vue +90 -25
  19. package/vitepress/components/Layout/CustomSidebar.vue +489 -117
  20. package/vitepress/components/Layout/Layout.vue +5 -0
  21. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +10 -2
  22. package/vitepress/components/Layout/SidebarItems.vue +517 -342
  23. package/vitepress/components/Layout/SidebarNavBefore.vue +1 -50
  24. package/vitepress/components/Layout/SidebarResizeHandle.vue +6 -23
  25. package/vitepress/components/Layout/composables/docLayoutLogic.ts +59 -0
  26. package/vitepress/components/Layout/composables/useDocLayout.test.ts +69 -0
  27. package/vitepress/components/Layout/composables/useDocLayout.ts +128 -0
  28. package/vitepress/components/Layout/composables/useSidebarDrag.ts +514 -0
  29. package/vitepress/components/Layout/composables/useSidebarLayout.ts +25 -8
  30. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +8 -3
  31. package/vitepress/components/Layout/sidebarCollapsedState.test.ts +183 -0
  32. package/vitepress/components/Layout/sidebarCollapsedState.ts +241 -0
  33. package/vitepress/components/Layout/sidebarDragLogic.test.ts +1146 -0
  34. package/vitepress/components/Layout/sidebarDragLogic.ts +1307 -0
  35. package/vitepress/components/Layout/sidebarHitTest.test.ts +572 -0
  36. package/vitepress/components/Layout/sidebarHitTest.ts +801 -0
  37. package/vitepress/components/Layout/sidebarItemRenderLogic.test.ts +53 -0
  38. package/vitepress/components/Layout/sidebarItemRenderLogic.ts +50 -0
  39. package/vitepress/components/MarkMap/MarkMap.vue +67 -10
  40. package/vitepress/components/Settings/Settings.vue +78 -74
  41. package/vitepress/components/SidebarCard/FolderTreeItems.vue +268 -0
  42. package/vitepress/components/SidebarCard/SidebarCard.vue +125 -274
  43. package/vitepress/components/constants.ts +18 -6
  44. package/vitepress/components/sidebar.data.ts +13 -11
  45. package/vitepress/components/sidebarTreeHelpers.test.ts +142 -0
  46. package/vitepress/components/sidebarTreeHelpers.ts +168 -0
  47. package/vitepress/components/utils/vscodePaths.test.ts +41 -0
  48. package/vitepress/components/utils/vscodePaths.ts +52 -0
  49. package/vitepress/config/index.ts +2 -0
  50. package/vitepress/plugins/localSearchIndexBuilder.ts +209 -0
  51. package/vitepress/plugins/localSearchReindex.test.ts +46 -0
  52. package/vitepress/plugins/localSearchReindexLogic.ts +50 -0
  53. package/vitepress/plugins/localSearchReindexPatch.test.ts +30 -0
  54. package/vitepress/plugins/localSearchReindexPatch.ts +37 -0
  55. package/vitepress/plugins/localSearchReindexPlugin.ts +318 -0
  56. package/vitepress/plugins/sidebarStructurePlugin.ts +209 -63
  57. package/vitepress/theme/index.ts +8 -0
  58. package/vitepress/theme/styles/base.scss +0 -8
  59. package/vitepress/theme/styles/components/markmap.scss +23 -2
  60. package/vitepress/theme/styles/components/swiper.scss +1 -1
  61. package/vitepress/theme/styles/doc-layout.scss +212 -0
  62. package/vitepress/theme/styles/index.scss +3 -0
  63. package/vitepress/theme/styles/layout.scss +60 -10
  64. package/dist/chunk-4KG6RXFS.js +0 -4637
  65. package/dist/chunk-AGSL3VZE.js +0 -193
  66. package/dist/cli/index.js +0 -683
  67. package/dist/index.js +0 -8
  68. package/dist/vitepress/config/index.js +0 -1178
  69. package/vitepress/assets/icons/icon__mindmap.svg +0 -1
  70. package/vitepress/assets/icons/icon__setting.svg +0 -1
  71. package/vitepress/components/SidebarCard/MindMapView.vue +0 -488
@@ -4,29 +4,80 @@ vitepress/components/Layout/CustomSidebar.vue
4
4
 
5
5
  <template>
6
6
  <div class="custom-sidebar-wrapper">
7
- <nav class="nav" ref="navRef">
7
+ <nav class="nav sidebar-nav" ref="navRef">
8
+ <a
9
+ :href="readmeLink"
10
+ class="nav-item readme-nav-item"
11
+ :class="{ active: isReadmeActive }"
12
+ >
13
+ README
14
+ </a>
15
+
8
16
  <!-- 使用递归组件渲染侧边栏,支持任意层级嵌套 -->
9
17
  <SidebarItems
10
- :items="sidebarGroups"
18
+ :items="displaySidebarGroups"
11
19
  :depth="0"
12
20
  :max-depth="maxDepth"
13
21
  :show-note-id="showNoteId"
14
22
  :base="base"
15
23
  :current-path="route.path"
16
24
  :is-dev="isDev"
17
- :sidebar-density="sidebarDensity"
18
25
  :done-prefix="donePrefix"
19
26
  :undone-prefix="undonePrefix"
20
- @rename-group="renameGroup"
21
- @delete-group="deleteGroup"
27
+ :drag-context="sidebarDrag"
22
28
  @create-note-in-group="createNoteInGroup"
23
29
  @create-notes-in-group="createNotesInGroup"
30
+ @create-folder-in-group="createFolderInGroup"
31
+ @rename-folder="renameFolder"
32
+ @delete-entry="deleteEntry"
24
33
  @rename-note="renameNote"
25
34
  @delete-note="deleteNote"
26
35
  @create-note-around="createNoteAround"
27
36
  @open-note-about="openNoteAbout"
28
- @reorder-sidebar="reorderSidebar"
29
37
  />
38
+
39
+ <div
40
+ v-if="dropSlot.visible"
41
+ class="sidebar-drop-slot"
42
+ :style="{
43
+ top: `${dropSlot.top}px`,
44
+ left: `${dropSlot.left}px`,
45
+ width: `${dropSlot.width}px`,
46
+ height: `${dropSlot.height}px`,
47
+ }"
48
+ />
49
+
50
+ <div
51
+ v-if="dropIndicator.visible"
52
+ class="sidebar-drop-layer"
53
+ :style="{ top: `${dropIndicator.top}px` }"
54
+ >
55
+ <template v-if="dropIndicator.mode === 'tail-rail'">
56
+ <div
57
+ v-for="depth in dropIndicator.depthLevels"
58
+ :key="depth"
59
+ class="sidebar-drop-rail-segment"
60
+ :class="{ 'is-active': depth === dropIndicator.activeDepth }"
61
+ :style="{
62
+ left: `${getDropRailLeft(depth)}px`,
63
+ width: `${getDropRailWidth(depth)}px`,
64
+ }"
65
+ />
66
+ </template>
67
+ <div
68
+ v-else
69
+ class="sidebar-drop-indicator"
70
+ :class="{ 'is-inside': dropIndicator.mode === 'inside' }"
71
+ :style="{
72
+ left: `${dropIndicator.left}px`,
73
+ width: `${dropIndicator.width}px`,
74
+ height:
75
+ dropIndicator.mode === 'inside'
76
+ ? `${getDropInsideHeight()}px`
77
+ : undefined,
78
+ }"
79
+ />
80
+ </div>
30
81
  </nav>
31
82
  </div>
32
83
  </template>
@@ -35,13 +86,33 @@ vitepress/components/Layout/CustomSidebar.vue
35
86
  import { useRoute, useData } from 'vitepress'
36
87
  import { ref, onMounted, watch, computed } from 'vue'
37
88
 
89
+ import {
90
+ useSidebarDrag,
91
+ getDropRailLeft as calcDropRailLeft,
92
+ type DragReorderMeta,
93
+ type YuqueReorderPayload,
94
+ } from './composables/useSidebarDrag'
95
+ import {
96
+ applySidebarCollapsedState,
97
+ saveSidebarCollapsedState,
98
+ loadSidebarCollapsedState,
99
+ mergeCollapseStore,
100
+ saveCollapseEntries,
101
+ buildPostDropCollapsePatch,
102
+ shouldSuppressActiveItemScroll,
103
+ suppressActiveItemScroll,
104
+ getCollapseKeyForTreeItem,
105
+ } from './sidebarCollapsedState'
106
+ import { SIDEBAR_ROW_HEIGHT } from './sidebarDragLogic'
38
107
  import SidebarItems from './SidebarItems.vue'
108
+ import { enrichSidebarNodeIds, computeSidebarNodeId } from '../../../utils/tocNodeId'
39
109
  import {
40
110
  SIDEBAR_SHOW_NOTE_ID_KEY,
41
111
  SIDEBAR_MAX_DEPTH_KEY,
42
- SIDEBAR_DENSITY_KEY,
43
112
  SIDEBAR_DONE_PREFIX_KEY,
44
113
  SIDEBAR_UNDONE_PREFIX_KEY,
114
+ SIDEBAR_COLLAPSED_STATE_KEY,
115
+ SIDEBAR_SUPPRESS_ACTIVE_SCROLL_KEY,
45
116
  } from '../constants'
46
117
  // @ts-expect-error - VitePress Data Loader
47
118
  import { data as sidebarConfig } from '../sidebar.data'
@@ -54,6 +125,9 @@ interface SidebarItem {
54
125
  link?: string
55
126
  items?: SidebarItem[]
56
127
  collapsed?: boolean
128
+ tocLineIndex?: number
129
+ folderPath?: string[]
130
+ nodeId?: string
57
131
  }
58
132
 
59
133
  const route = useRoute()
@@ -62,12 +136,32 @@ const sidebarGroups = ref<SidebarItem[]>([])
62
136
  const navRef = ref<HTMLElement | null>(null)
63
137
  const currentFocusIndex = ref(0)
64
138
 
139
+ const base = computed(() => site.value.base || '/')
140
+ const readmeLink = computed(() => `${base.value}README`)
141
+
142
+ function normalizeRoutePath(path: string): string {
143
+ const normalizedPath = path.split(/[?#]/)[0].replace(/\.html$/, '')
144
+
145
+ return normalizedPath.length > 1
146
+ ? normalizedPath.replace(/\/$/, '')
147
+ : normalizedPath
148
+ }
149
+
150
+ const isReadmeActive = computed(() => {
151
+ const currentPath = normalizeRoutePath(route.path)
152
+ const readmePath = normalizeRoutePath(readmeLink.value)
153
+
154
+ return currentPath === readmePath || currentPath === '/README'
155
+ })
156
+
65
157
  const emit = defineEmits<{
66
158
  'open-note-about': [noteIndex: string]
67
159
  }>()
68
160
 
69
- interface GroupActionPayload {
70
- groupPath: string[]
161
+ interface ParentNoteActionPayload {
162
+ parentNoteIndex?: string
163
+ parentFolderPath?: string[]
164
+ parentTocLineIndex?: number
71
165
  text: string
72
166
  }
73
167
 
@@ -83,36 +177,47 @@ interface SidebarActionResult {
83
177
  message?: string
84
178
  redirectUrl?: string
85
179
  newUrl?: string
180
+ deletedNoteIndexes?: string[]
86
181
  }
87
182
 
88
183
  interface ReorderPayload {
89
- dragType: 'group' | 'note'
90
- groupPath?: string[]
184
+ dragType: 'note' | 'folder'
185
+ dragTocLineIndex?: number
91
186
  noteIndex?: string
92
187
  targetType: 'group' | 'note'
93
- targetGroupPath?: string[]
94
188
  targetNoteIndex?: string
95
- placement?: 'before' | 'after'
189
+ targetFolderPath?: string[]
190
+ targetTocLineIndex?: number
191
+ placement?: 'before' | 'after' | 'inside'
192
+ }
193
+
194
+ interface FolderActionPayload {
195
+ tocLineIndex: number
196
+ text: string
96
197
  }
97
198
 
98
- type SidebarDensity = 'compact' | 'default' | 'loose'
199
+ interface EntryActionPayload {
200
+ tocLineIndex: number
201
+ text: string
202
+ noteIndex?: string
203
+ }
99
204
 
100
- const DEFAULT_SIDEBAR_DENSITY: SidebarDensity = 'default'
101
205
  const DEFAULT_DONE_PREFIX = '✅'
102
206
  const DEFAULT_UNDONE_PREFIX = '⏰'
103
207
 
104
- // 最大解析层级(默认 3 层)
208
+ // 最大解析层级(0 = 不限制)
105
209
  const maxDepth = computed(() => {
106
210
  if (typeof window === 'undefined') {
107
- return tnotesConfig.sidebarMaxDepth ?? 3
211
+ return tnotesConfig.sidebarMaxDepth ?? 0
108
212
  }
109
213
 
110
214
  const savedMaxDepth = localStorage.getItem(SIDEBAR_MAX_DEPTH_KEY)
111
215
  if (savedMaxDepth !== null) {
112
- return parseInt(savedMaxDepth, 10)
216
+ const parsed = parseInt(savedMaxDepth, 10)
217
+ return parsed > 0 ? parsed : 0
113
218
  }
114
219
 
115
- return tnotesConfig.sidebarMaxDepth ?? 3
220
+ return tnotesConfig.sidebarMaxDepth ?? 0
116
221
  })
117
222
 
118
223
  // 获取配置:是否显示笔记 ID
@@ -129,12 +234,6 @@ const showNoteId = computed(() => {
129
234
  return tnotesConfig.sidebarShowNoteId ?? false
130
235
  })
131
236
 
132
- const sidebarDensity = computed<SidebarDensity>(() => {
133
- if (typeof window === 'undefined') return DEFAULT_SIDEBAR_DENSITY
134
-
135
- return normalizeSidebarDensity(localStorage.getItem(SIDEBAR_DENSITY_KEY))
136
- })
137
-
138
237
  const donePrefix = computed(() => {
139
238
  if (typeof window === 'undefined') return DEFAULT_DONE_PREFIX
140
239
 
@@ -149,10 +248,8 @@ const undonePrefix = computed(() => {
149
248
  return savedPrefix !== null ? savedPrefix : DEFAULT_UNDONE_PREFIX
150
249
  })
151
250
 
152
- // 获取 base 路径
153
- const base = computed(() => site.value.base || '/')
154
-
155
251
  const isDev = computed(() => {
252
+ if (import.meta.env.DEV) return true
156
253
  if (typeof window === 'undefined') return false
157
254
  return (
158
255
  window.location.hostname === 'localhost' ||
@@ -160,6 +257,110 @@ const isDev = computed(() => {
160
257
  )
161
258
  })
162
259
 
260
+ function getDropInsideHeight(): number {
261
+ const nodeId = sidebarDrag.dropIndicator.value.targetNodeId
262
+ if (!nodeId || !navRef.value) return SIDEBAR_ROW_HEIGHT
263
+
264
+ const row = navRef.value.querySelector<HTMLElement>(
265
+ `.sidebar-row[data-node-id="${nodeId}"]`,
266
+ )
267
+ return row?.getBoundingClientRect().height ?? SIDEBAR_ROW_HEIGHT
268
+ }
269
+
270
+ function findSidebarItemByNodeId(
271
+ items: SidebarItem[],
272
+ nodeId: string,
273
+ ): SidebarItem | null {
274
+ for (const item of items) {
275
+ const id = item.nodeId ?? computeSidebarNodeId(item)
276
+ if (id === nodeId) return item
277
+ if (item.items?.length) {
278
+ const found = findSidebarItemByNodeId(item.items, nodeId)
279
+ if (found) return found
280
+ }
281
+ }
282
+ return null
283
+ }
284
+
285
+ function resolveTargetCollapseKey(
286
+ targetNodeId: string | null | undefined,
287
+ ): string | null {
288
+ if (!targetNodeId) return null
289
+ const item = findSidebarItemByNodeId(sidebarGroups.value, targetNodeId)
290
+ return item ? resolveCollapseKey(item) : null
291
+ }
292
+
293
+ function applyPostDropCollapsePatch(patch: Record<string, boolean>) {
294
+ if (!Object.keys(patch).length) return
295
+
296
+ const merged = mergeCollapseStore(
297
+ loadSidebarCollapsedState(SIDEBAR_COLLAPSED_STATE_KEY),
298
+ patch,
299
+ )
300
+ saveCollapseEntries(SIDEBAR_COLLAPSED_STATE_KEY, merged)
301
+ }
302
+
303
+ function reorderSidebarOptimistic(
304
+ payload: YuqueReorderPayload,
305
+ meta: DragReorderMeta,
306
+ ) {
307
+ if (!payload.node_uuid || !payload.action) return
308
+ const isRootPrepend =
309
+ payload.action === 'prependChild' && !payload.target_uuid
310
+ if (!isRootPrepend && !payload.target_uuid) return
311
+
312
+ suppressActiveItemScroll(SIDEBAR_SUPPRESS_ACTIVE_SCROLL_KEY)
313
+ persistSidebarCollapsedState()
314
+ applyPostDropCollapsePatch(
315
+ buildPostDropCollapsePatch(payload, {
316
+ ...meta,
317
+ targetCollapseKey: resolveTargetCollapseKey(
318
+ meta.targetNodeId ?? payload.target_uuid,
319
+ ),
320
+ }),
321
+ )
322
+
323
+ void runSidebarAction(async () => {
324
+ await requestSidebarAction('/__tnotes_sidebar_reorder', payload)
325
+ })
326
+ }
327
+
328
+ function resolveCollapseKey(item: SidebarItem): string | null {
329
+ return getCollapseKeyForTreeItem(item)
330
+ }
331
+
332
+ const sidebarDrag = useSidebarDrag({
333
+ items: sidebarGroups,
334
+ navRef,
335
+ maxDepth,
336
+ isDev,
337
+ resolveCollapseKey,
338
+ onReorder: reorderSidebarOptimistic,
339
+ })
340
+
341
+ const displaySidebarGroups = computed(
342
+ () => sidebarDrag.previewItems.value ?? sidebarGroups.value,
343
+ )
344
+
345
+ const dropIndicator = computed(() => sidebarDrag.dropIndicator.value)
346
+ const dropSlot = computed(() => sidebarDrag.dropSlot.value)
347
+
348
+ function getDropRailLeft(depth: number): number {
349
+ const indentSize = dropIndicator.value.indentSize
350
+ return calcDropRailLeft(depth, indentSize)
351
+ }
352
+
353
+ function getDropRailWidth(depth: number): number {
354
+ const nav = navRef.value
355
+ if (!nav) return 24
356
+
357
+ if (depth === dropIndicator.value.activeDepth) {
358
+ return Math.max(nav.clientWidth - getDropRailLeft(depth) - 8, 24)
359
+ }
360
+
361
+ return 12
362
+ }
363
+
163
364
  // 加载 sidebar 数据
164
365
  function loadSidebar() {
165
366
  if (sidebarConfig && sidebarConfig['/notes/']) {
@@ -167,13 +368,24 @@ function loadSidebar() {
167
368
  }
168
369
  }
169
370
 
170
- // 递归处理侧边栏项,添加 collapsed 状态
371
+ function persistSidebarCollapsedState() {
372
+ saveSidebarCollapsedState(SIDEBAR_COLLAPSED_STATE_KEY, sidebarGroups.value)
373
+ }
374
+
375
+ // 递归处理侧边栏项,合并 localStorage 中的展开/折叠状态
171
376
  function processItems(items: any[]): SidebarItem[] {
172
- return items.map((item) => ({
377
+ const normalized = items.map((item) => ({
173
378
  ...item,
174
379
  collapsed: item.collapsed ?? true,
175
- items: item.items ? processItems(item.items) : undefined,
380
+ items: item.items as SidebarItem[] | undefined,
176
381
  }))
382
+
383
+ return enrichSidebarNodeIds(
384
+ applySidebarCollapsedState(
385
+ normalized,
386
+ loadSidebarCollapsedState(SIDEBAR_COLLAPSED_STATE_KEY),
387
+ ) as SidebarItem[],
388
+ )
177
389
  }
178
390
 
179
391
  function stripHeadingNumber(text: string): string {
@@ -187,19 +399,17 @@ function stripNoteTitlePrefix(text: string): string {
187
399
  .trim()
188
400
  }
189
401
 
190
- function normalizeSidebarDensity(value: string | null): SidebarDensity {
191
- if (value === 'compact' || value === 'default' || value === 'loose') {
192
- return value
193
- }
194
-
195
- return DEFAULT_SIDEBAR_DENSITY
196
- }
197
-
198
402
  function isCurrentNote(noteIndex: string): boolean {
199
403
  const decodedRoutePath = decodeURIComponent(route.path)
200
404
  return decodedRoutePath.includes(`/notes/${noteIndex}.`)
201
405
  }
202
406
 
407
+ function getCurrentNoteIndex(): string | null {
408
+ const decodedRoutePath = decodeURIComponent(route.path)
409
+ const match = decodedRoutePath.match(/\/notes\/(\d{4})\./)
410
+ return match ? match[1] : null
411
+ }
412
+
203
413
  function resolveUrl(path: string): string {
204
414
  const normalizedBase = base.value.endsWith('/') ? base.value : `${base.value}/`
205
415
  return normalizedBase + path.replace(/^\/+/, '')
@@ -208,6 +418,7 @@ function resolveUrl(path: string): string {
208
418
  async function requestSidebarAction(
209
419
  path: string,
210
420
  body: unknown,
421
+ options?: { skipReload?: boolean },
211
422
  ): Promise<SidebarActionResult> {
212
423
  const response = await fetch(path, {
213
424
  method: 'POST',
@@ -225,9 +436,11 @@ async function requestSidebarAction(
225
436
  throw new Error(result?.message || response.statusText || '操作失败')
226
437
  }
227
438
 
228
- setTimeout(() => {
229
- loadSidebar()
230
- }, 150)
439
+ if (!options?.skipReload) {
440
+ setTimeout(() => {
441
+ loadSidebar()
442
+ }, 150)
443
+ }
231
444
 
232
445
  return result
233
446
  }
@@ -242,45 +455,18 @@ async function runSidebarAction(action: () => Promise<void>) {
242
455
  }
243
456
  }
244
457
 
245
- function renameGroup(payload: GroupActionPayload) {
246
- void runSidebarAction(async () => {
247
- const newTitle = window.prompt('请输入新的目录名称', stripHeadingNumber(payload.text))
248
- if (newTitle === null) return
249
-
250
- await requestSidebarAction('/__tnotes_sidebar_rename_group', {
251
- groupPath: payload.groupPath,
252
- newTitle,
253
- })
254
- })
255
- }
256
-
257
- function deleteGroup(payload: GroupActionPayload) {
258
- void runSidebarAction(async () => {
259
- const confirmed = window.confirm(
260
- `确定删除目录「${payload.groupPath.join(' / ')}」及其下所有笔记吗?`,
261
- )
262
- if (!confirmed) return
263
-
264
- const result = await requestSidebarAction('/__tnotes_sidebar_delete_group', {
265
- groupPath: payload.groupPath,
266
- })
267
-
268
- if (result.redirectUrl) {
269
- window.location.replace(resolveUrl(result.redirectUrl))
270
- }
271
- })
272
- }
273
-
274
- function createNoteInGroup(payload: GroupActionPayload) {
458
+ function createNoteInGroup(payload: ParentNoteActionPayload) {
275
459
  void runSidebarAction(async () => {
276
460
  await requestSidebarAction('/__tnotes_sidebar_create_note', {
277
- groupPath: payload.groupPath,
461
+ parentNoteIndex: payload.parentNoteIndex,
462
+ parentFolderPath: payload.parentFolderPath,
463
+ parentTocLineIndex: payload.parentTocLineIndex,
278
464
  count: 1,
279
465
  })
280
466
  })
281
467
  }
282
468
 
283
- function createNotesInGroup(payload: GroupActionPayload) {
469
+ function createNotesInGroup(payload: ParentNoteActionPayload) {
284
470
  void runSidebarAction(async () => {
285
471
  const input = window.prompt('请输入新增笔记数量(1-100)', '1')
286
472
  if (input === null) return
@@ -291,12 +477,35 @@ function createNotesInGroup(payload: GroupActionPayload) {
291
477
  }
292
478
 
293
479
  await requestSidebarAction('/__tnotes_sidebar_create_notes', {
294
- groupPath: payload.groupPath,
480
+ parentNoteIndex: payload.parentNoteIndex,
481
+ parentFolderPath: payload.parentFolderPath,
482
+ parentTocLineIndex: payload.parentTocLineIndex,
295
483
  count,
296
484
  })
297
485
  })
298
486
  }
299
487
 
488
+ function createFolderInGroup(payload: ParentNoteActionPayload) {
489
+ void runSidebarAction(async () => {
490
+ if (payload.parentTocLineIndex === undefined) {
491
+ throw new Error('无法定位父节点(缺少 tocLineIndex,请刷新侧边栏后重试)')
492
+ }
493
+
494
+ const title = window.prompt('请输入子目录名称')
495
+ if (title === null) return
496
+
497
+ const trimmed = title.trim()
498
+ if (!trimmed) {
499
+ throw new Error('目录标题不能为空')
500
+ }
501
+
502
+ await requestSidebarAction('/__tnotes_sidebar_create_folder', {
503
+ parentTocLineIndex: payload.parentTocLineIndex,
504
+ title: trimmed,
505
+ })
506
+ })
507
+ }
508
+
300
509
  function renameNote(payload: NoteActionPayload) {
301
510
  void runSidebarAction(async () => {
302
511
  if (!payload.noteIndex) return
@@ -318,6 +527,48 @@ function renameNote(payload: NoteActionPayload) {
318
527
  })
319
528
  }
320
529
 
530
+ function renameFolder(payload: FolderActionPayload) {
531
+ void runSidebarAction(async () => {
532
+ if (payload.tocLineIndex < 0) return
533
+
534
+ const newTitle = window.prompt(
535
+ '请输入新的目录名称',
536
+ stripHeadingNumber(payload.text),
537
+ )
538
+ if (newTitle === null) return
539
+
540
+ const trimmed = newTitle.trim()
541
+ if (!trimmed) {
542
+ throw new Error('目录标题不能为空')
543
+ }
544
+
545
+ await requestSidebarAction('/__tnotes_sidebar_rename_folder', {
546
+ tocLineIndex: payload.tocLineIndex,
547
+ newTitle: trimmed,
548
+ })
549
+ })
550
+ }
551
+
552
+ function deleteEntry(payload: EntryActionPayload) {
553
+ void runSidebarAction(async () => {
554
+ if (payload.tocLineIndex < 0) return
555
+
556
+ const confirmed = window.confirm(
557
+ `确定删除「${stripHeadingNumber(payload.text)}」及其所有子项吗?子树内的笔记将从磁盘删除。`,
558
+ )
559
+ if (!confirmed) return
560
+
561
+ const result = await requestSidebarAction('/__tnotes_sidebar_delete_entry', {
562
+ tocLineIndex: payload.tocLineIndex,
563
+ currentNoteIndex: getCurrentNoteIndex() ?? '',
564
+ })
565
+
566
+ if (result.redirectUrl) {
567
+ window.location.replace(resolveUrl(result.redirectUrl))
568
+ }
569
+ })
570
+ }
571
+
321
572
  function deleteNote(payload: NoteActionPayload) {
322
573
  void runSidebarAction(async () => {
323
574
  if (!payload.noteIndex) return
@@ -354,12 +605,6 @@ function openNoteAbout(payload: NoteActionPayload) {
354
605
  emit('open-note-about', payload.noteIndex)
355
606
  }
356
607
 
357
- function reorderSidebar(payload: ReorderPayload) {
358
- void runSidebarAction(async () => {
359
- await requestSidebarAction('/__tnotes_sidebar_reorder', payload)
360
- })
361
- }
362
-
363
608
  // 判断项是否有子项
364
609
  function hasChildren(item: SidebarItem): boolean {
365
610
  return !!(item.items && item.items.length > 0)
@@ -457,7 +702,7 @@ function expandParentGroup(element: HTMLElement) {
457
702
  currentElement.closest<HTMLElement>('.group')
458
703
  if (!groupElement) break
459
704
 
460
- const groupTitleText = groupElement.querySelector('.group-title-text')
705
+ const groupTitleText = groupElement.querySelector('.parent-note-link, .group-title-text')
461
706
  if (groupTitleText) {
462
707
  const groupText = groupTitleText.textContent?.trim()
463
708
  if (groupText) {
@@ -611,6 +856,9 @@ function focusCurrentNote() {
611
856
 
612
857
  // 展开当前激活笔记的所有父级分组
613
858
  function expandActiveItemParents() {
859
+ if (shouldSuppressActiveItemScroll(SIDEBAR_SUPPRESS_ACTIVE_SCROLL_KEY)) {
860
+ return
861
+ }
614
862
  expandActiveItemParentsRecursive(sidebarGroups.value)
615
863
  }
616
864
 
@@ -620,7 +868,6 @@ function expandActiveItemParentsRecursive(items: SidebarItem[]): boolean {
620
868
 
621
869
  for (const item of items) {
622
870
  if (item.link) {
623
- // 检查当前项是否激活
624
871
  const fullLink = getFullLink(item.link)
625
872
  const decodedRoutePath = decodeURIComponent(route.path)
626
873
  const decodedFullLink = decodeURIComponent(fullLink)
@@ -631,7 +878,9 @@ function expandActiveItemParentsRecursive(items: SidebarItem[]): boolean {
631
878
  if (itemActive) {
632
879
  hasActive = true
633
880
  }
634
- } else if (item.items) {
881
+ }
882
+
883
+ if (item.items?.length) {
635
884
  const childHasActive = expandActiveItemParentsRecursive(item.items)
636
885
  if (childHasActive) {
637
886
  item.collapsed = false
@@ -651,18 +900,24 @@ function getFullLink(link: string) {
651
900
 
652
901
  // 滚动到当前激活的笔记
653
902
  function scrollToActiveItem() {
903
+ if (shouldSuppressActiveItemScroll(SIDEBAR_SUPPRESS_ACTIVE_SCROLL_KEY)) {
904
+ return
905
+ }
906
+
654
907
  // 等待 DOM 更新
655
908
  setTimeout(() => {
656
- const positions = getCurrentNotePositions()
657
- if (positions.length > 0) {
658
- // 展开所有包含当前笔记的分组
659
- expandActiveItemParents()
660
-
661
- // 滚动到第一个位置
662
- setTimeout(() => {
663
- scrollToElement(positions[0])
664
- }, 100)
665
- }
909
+ expandActiveItemParents()
910
+
911
+ setTimeout(() => {
912
+ const positions = getCurrentNotePositions()
913
+
914
+ if (positions.length > 0) {
915
+ expandParentGroup(positions[0])
916
+ setTimeout(() => {
917
+ scrollToElement(positions[0])
918
+ }, 100)
919
+ }
920
+ }, 100)
666
921
  }, 300)
667
922
  }
668
923
 
@@ -677,29 +932,19 @@ defineExpose({
677
932
 
678
933
  onMounted(() => {
679
934
  loadSidebar()
680
-
681
- // 调试:打印配置信息
682
- // console.log('🔧 [CustomSidebar] showNoteId:', showNoteId.value)
683
- // if (typeof window !== 'undefined') {
684
- // console.log(
685
- // '🔧 [CustomSidebar] localStorage value:',
686
- // localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
687
- // )
688
- // }
689
- // console.log(
690
- // '🔧 [CustomSidebar] tnotesConfig value:',
691
- // tnotesConfig.sidebarShowNoteId
692
- // )
693
-
694
- // 自动滚动到当前激活的笔记
695
935
  scrollToActiveItem()
696
936
  })
697
937
 
938
+ watch(
939
+ sidebarGroups,
940
+ () => persistSidebarCollapsedState(),
941
+ { deep: true, flush: 'post' },
942
+ )
943
+
698
944
  // 监听 sidebarConfig 的变化(HMR 会更新这个导入的数据)
699
945
  watch(
700
946
  () => sidebarConfig,
701
947
  () => {
702
- // console.log('🔄 [CustomSidebar] Sidebar config changed, reloading...')
703
948
  loadSidebar()
704
949
  },
705
950
  { deep: true }
@@ -734,16 +979,101 @@ watch(
734
979
  <style scoped>
735
980
  /* 自定义 sidebar 容器,适配 VitePress 的 sidebar-nav-before 插槽 */
736
981
  .custom-sidebar-wrapper {
737
- /* 不需要设置 position 和尺寸,因为它在 VitePress 的 sidebar 容器内 */
982
+ width: 100%;
983
+ min-width: 0;
738
984
  }
739
985
 
740
986
  .nav {
987
+ position: relative;
741
988
  font-size: 14px;
742
989
  line-height: 2;
743
990
  }
744
991
 
992
+ .sidebar-nav {
993
+ position: relative;
994
+ display: flex;
995
+ flex-direction: column;
996
+ flex-wrap: nowrap;
997
+ width: 100%;
998
+ min-width: 0;
999
+ }
1000
+
1001
+ .sidebar-drop-slot {
1002
+ position: absolute;
1003
+ z-index: 45;
1004
+ box-sizing: border-box;
1005
+ background: transparent;
1006
+ border: 1px dashed var(--vp-c-brand-1);
1007
+ border-radius: 6px;
1008
+ opacity: 0.65;
1009
+ pointer-events: none;
1010
+ }
1011
+
1012
+ .sidebar-drop-layer {
1013
+ position: absolute;
1014
+ right: 0;
1015
+ left: 0;
1016
+ z-index: 50;
1017
+ height: 0;
1018
+ pointer-events: none;
1019
+ }
1020
+
1021
+ .sidebar-drop-rail-segment {
1022
+ position: absolute;
1023
+ top: -2px;
1024
+ height: 3px;
1025
+ background: var(--vp-c-text-2);
1026
+ border-radius: 999px;
1027
+ opacity: 0.65;
1028
+ }
1029
+
1030
+ .sidebar-drop-rail-segment.is-active {
1031
+ top: -2px;
1032
+ height: 4px;
1033
+ background: var(--vp-c-brand-1);
1034
+ opacity: 1;
1035
+ box-shadow:
1036
+ 0 0 0 1px var(--vp-c-brand-soft),
1037
+ 0 0 10px color-mix(in srgb, var(--vp-c-brand-1) 70%, transparent);
1038
+ }
1039
+
1040
+ .sidebar-drop-rail-segment.is-active::before {
1041
+ position: absolute;
1042
+ top: -4px;
1043
+ left: -1px;
1044
+ width: 0;
1045
+ height: 0;
1046
+ border-top: 4px solid transparent;
1047
+ border-bottom: 4px solid transparent;
1048
+ border-left: 6px solid var(--vp-c-brand-1);
1049
+ content: '';
1050
+ }
1051
+
1052
+ .sidebar-drop-indicator {
1053
+ position: absolute;
1054
+ top: -1px;
1055
+ z-index: 50;
1056
+ height: 3px;
1057
+ background: var(--vp-c-brand-1);
1058
+ border-radius: 999px;
1059
+ box-shadow:
1060
+ 0 0 0 1px var(--vp-c-brand-soft),
1061
+ 0 0 8px color-mix(in srgb, var(--vp-c-brand-1) 65%, transparent);
1062
+ pointer-events: none;
1063
+ }
1064
+
1065
+ .sidebar-drop-indicator.is-inside {
1066
+ height: auto;
1067
+ min-height: 28px;
1068
+ background: var(--vp-c-brand-soft);
1069
+ border: 1px solid var(--vp-c-brand-1);
1070
+ border-radius: 6px;
1071
+ }
1072
+
745
1073
  .group {
746
1074
  margin-bottom: 16px;
1075
+ width: 100%;
1076
+ min-width: 0;
747
1077
  }
748
1078
 
749
1079
  .group-title {
@@ -777,12 +1107,15 @@ watch(
777
1107
 
778
1108
  .nav-item {
779
1109
  display: block;
780
- padding: 4px;
1110
+ box-sizing: border-box;
1111
+ width: 100%;
1112
+ min-height: 36px;
1113
+ padding: 6px 8px;
781
1114
  color: var(--vp-c-text-2);
782
1115
  text-decoration: none;
783
- border-radius: 4px;
1116
+ border-radius: 8px;
784
1117
  font-size: 14px;
785
- line-height: 24px;
1118
+ line-height: 22px;
786
1119
  transition: all 0.25s;
787
1120
  }
788
1121
 
@@ -796,6 +1129,12 @@ watch(
796
1129
  font-weight: 600;
797
1130
  }
798
1131
 
1132
+ .readme-nav-item {
1133
+ display: block;
1134
+ margin-bottom: 8px;
1135
+ font-weight: 600;
1136
+ }
1137
+
799
1138
  /* 聚焦高亮动画 */
800
1139
  .nav-item.focus-highlight {
801
1140
  animation: focusPulse 1s ease-in-out;
@@ -812,10 +1151,43 @@ watch(
812
1151
  }
813
1152
  </style>
814
1153
 
815
- <!-- 全局样式:隐藏 VitePress 默认的 sidebar nav -->
1154
+ <!-- 全局样式:侧边栏拖拽与默认导航隐藏 -->
816
1155
  <style>
817
- /* 隐藏 VitePress 默认的 sidebar 导航内容(保留容器) */
818
- .VPSidebarNav {
1156
+ /* 隐藏 VitePress 默认 sidebar 分组(自定义目录在 sidebar-nav-after 插槽) */
1157
+ #VPSidebarNav > .group {
819
1158
  display: none !important;
820
1159
  }
1160
+
1161
+ body.sidebar-drag-active {
1162
+ cursor: grabbing;
1163
+ user-select: none;
1164
+ }
1165
+
1166
+ body.sidebar-drag-active .sidebar-row {
1167
+ cursor: grabbing;
1168
+ }
1169
+
1170
+ .sidebar-drag-ghost {
1171
+ position: fixed;
1172
+ z-index: 9999;
1173
+ display: flex;
1174
+ align-items: center;
1175
+ min-height: 28px;
1176
+ padding: 4px 8px;
1177
+ opacity: 0.92;
1178
+ background: var(--vp-c-bg-elv);
1179
+ border: 1px solid var(--vp-c-brand-1);
1180
+ border-radius: 6px;
1181
+ box-shadow: var(--vp-shadow-3);
1182
+ pointer-events: none;
1183
+ }
1184
+
1185
+ .sidebar-drag-ghost .ghost-title-text {
1186
+ overflow: hidden;
1187
+ font-size: 14px;
1188
+ font-weight: 500;
1189
+ color: var(--vp-c-text-1);
1190
+ text-overflow: ellipsis;
1191
+ white-space: nowrap;
1192
+ }
821
1193
  </style>