@tnotesjs/core 0.2.1 → 0.3.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 (86) hide show
  1. package/README.md +0 -1
  2. package/config/ConfigManager.ts +137 -0
  3. package/config/constants.ts +121 -0
  4. package/config/defaultConfig.ts +33 -4
  5. package/config/index.ts +31 -0
  6. package/config/templates.ts +49 -0
  7. package/core/GitManager.ts +513 -0
  8. package/core/NoteIndexCache.ts +200 -0
  9. package/core/NoteManager.ts +395 -0
  10. package/core/ProcessManager.ts +180 -0
  11. package/core/ReadmeGenerator.ts +215 -0
  12. package/core/TocGenerator.ts +212 -0
  13. package/core/index.ts +11 -0
  14. package/dist/{chunk-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
  15. package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
  16. package/dist/cli/index.js +26 -78
  17. package/dist/index.js +1 -1
  18. package/dist/vitepress/config/index.js +2 -2
  19. package/package.json +5 -2
  20. package/services/file-watcher/configChangeHandler.ts +64 -0
  21. package/services/file-watcher/eventScheduler.ts +179 -0
  22. package/services/file-watcher/folderChangeHandler.ts +325 -0
  23. package/services/file-watcher/fsWatcherAdapter.ts +128 -0
  24. package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
  25. package/services/file-watcher/index.ts +7 -0
  26. package/services/file-watcher/internal.ts +79 -0
  27. package/services/file-watcher/readmeChangeHandler.ts +28 -0
  28. package/services/file-watcher/renameDetector.ts +120 -0
  29. package/services/file-watcher/service.ts +352 -0
  30. package/services/file-watcher/watchState.ts +194 -0
  31. package/services/git/index.ts +7 -0
  32. package/services/git/service.ts +114 -0
  33. package/services/index.ts +14 -0
  34. package/services/init-sub-repo/index.ts +2 -0
  35. package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
  36. package/services/init-sub-repo/initSubRepoLogic.ts +303 -0
  37. package/services/init-sub-repo/service.ts +101 -0
  38. package/services/note/index.ts +7 -0
  39. package/services/note/service.ts +361 -0
  40. package/services/readme/index.ts +7 -0
  41. package/services/readme/service.ts +761 -0
  42. package/services/toc/index.ts +5 -0
  43. package/services/toc/moveTocInside.test.ts +73 -0
  44. package/services/toc/service.ts +759 -0
  45. package/services/vitepress/index.ts +7 -0
  46. package/services/vitepress/service.ts +339 -0
  47. package/types/config.ts +0 -3
  48. package/types/note.ts +0 -2
  49. package/utils/errorHandler.ts +174 -0
  50. package/utils/file.ts +17 -0
  51. package/utils/genHierarchicalSidebar.ts +69 -0
  52. package/utils/generateAnchor.ts +24 -0
  53. package/utils/getChangedIds.ts +35 -0
  54. package/utils/index.ts +71 -0
  55. package/utils/logger.ts +231 -0
  56. package/utils/markdown.ts +75 -0
  57. package/utils/migrateReadmeToToc.test.ts +111 -0
  58. package/utils/migrateReadmeToToc.ts +135 -0
  59. package/utils/parseArgs.ts +90 -0
  60. package/utils/parseReadmeCompletedNotes.test.ts +90 -0
  61. package/utils/parseReadmeCompletedNotes.ts +108 -0
  62. package/utils/portUtils.ts +113 -0
  63. package/utils/readmeHelpers.ts +190 -0
  64. package/utils/runCommand.ts +29 -0
  65. package/utils/tocHelpers.test.ts +278 -0
  66. package/utils/tocHelpers.ts +855 -0
  67. package/utils/tocNodeId.test.ts +60 -0
  68. package/utils/tocNodeId.ts +97 -0
  69. package/utils/validators.ts +102 -0
  70. package/vitepress/assets/icons/icon__cursor.svg +4 -0
  71. package/vitepress/assets/icons/index.ts +1 -0
  72. package/vitepress/components/Layout/AboutPanel.vue +0 -24
  73. package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
  74. package/vitepress/components/Layout/DocFooter.vue +3 -3
  75. package/vitepress/components/Layout/Layout.vue +0 -30
  76. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
  77. package/vitepress/components/Layout/homeReadme.data.ts +0 -48
  78. package/vitepress/components/MarkMap/MarkMap.vue +6 -13
  79. package/vitepress/components/Settings/Settings.vue +76 -73
  80. package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
  81. package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
  82. package/vitepress/components/composables/useLocalIde.ts +83 -0
  83. package/vitepress/components/constants.ts +5 -6
  84. package/vitepress/components/utils/vscodePaths.test.ts +22 -0
  85. package/vitepress/components/utils/vscodePaths.ts +24 -2
  86. package/vitepress/theme/styles/doc-layout.scss +4 -4
@@ -0,0 +1,60 @@
1
+ /**
2
+ * utils/tocNodeId.test.ts
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import {
8
+ computeSidebarNodeId,
9
+ enrichSidebarNodeIds,
10
+ nodeIdForNote,
11
+ parseNodeId,
12
+ } from './tocNodeId'
13
+
14
+ import type { SidebarNodeLike } from './tocNodeId'
15
+
16
+ describe('tocNodeId', () => {
17
+ it('computes note nodeId', () => {
18
+ expect(
19
+ computeSidebarNodeId({
20
+ text: '✅ 0019. Author',
21
+ link: '/notes/0019. Author/README',
22
+ tocLineIndex: 0,
23
+ }),
24
+ ).toBe(nodeIdForNote('0019'))
25
+ })
26
+
27
+ it('computes folder nodeId from tocLineIndex', () => {
28
+ expect(
29
+ computeSidebarNodeId({
30
+ text: 'build',
31
+ folderPath: ['build'],
32
+ tocLineIndex: 12,
33
+ }),
34
+ ).toBe('line:12')
35
+ })
36
+
37
+ it('enriches tree recursively', () => {
38
+ const tree = enrichSidebarNodeIds<SidebarNodeLike>([
39
+ {
40
+ text: 'Parent',
41
+ link: '/notes/0019. Author/README',
42
+ tocLineIndex: 0,
43
+ items: [{ text: 'Child', link: '/notes/0039. new/README', tocLineIndex: 1 }],
44
+ },
45
+ ])
46
+ expect(tree[0].nodeId).toBe('note:0019')
47
+ expect(tree[0].items?.[0].nodeId).toBe('note:0039')
48
+ })
49
+
50
+ it('parses nodeId kinds', () => {
51
+ expect(parseNodeId('note:0019')).toEqual({
52
+ kind: 'note',
53
+ noteIndex: '0019',
54
+ })
55
+ expect(parseNodeId('line:34')).toEqual({
56
+ kind: 'line',
57
+ tocLineIndex: 34,
58
+ })
59
+ })
60
+ })
@@ -0,0 +1,97 @@
1
+ /**
2
+ * utils/tocNodeId.ts
3
+ *
4
+ * 语雀式拖拽 nodeId:dev 确定性 id,不写 TOC.md。
5
+ */
6
+
7
+ export type TocMoveAction = 'moveAfter' | 'prependChild'
8
+
9
+ export interface SidebarNodeLike {
10
+ text: string
11
+ link?: string
12
+ folderPath?: string[]
13
+ tocLineIndex?: number
14
+ nodeId?: string
15
+ items?: SidebarNodeLike[]
16
+ }
17
+
18
+ function extractNoteIndexFromLink(link?: string): string | null {
19
+ if (!link) return null
20
+ const match = link.match(/\/notes\/(\d{4})\./)
21
+ return match ? match[1] : null
22
+ }
23
+
24
+ export interface SidebarNodeLike {
25
+ text: string
26
+ link?: string
27
+ folderPath?: string[]
28
+ tocLineIndex?: number
29
+ nodeId?: string
30
+ items?: SidebarNodeLike[]
31
+ }
32
+
33
+ export function nodeIdForNote(noteIndex: string): string {
34
+ return `note:${noteIndex}`
35
+ }
36
+
37
+ export function nodeIdForFolder(folderPath: string[]): string {
38
+ return `folder:${folderPath.join('/')}`
39
+ }
40
+
41
+ export function nodeIdForLine(tocLineIndex: number): string {
42
+ return `line:${tocLineIndex}`
43
+ }
44
+
45
+ export function computeSidebarNodeId(item: SidebarNodeLike): string {
46
+ if (item.nodeId) return item.nodeId
47
+
48
+ const noteIndex = extractNoteIndexFromLink(item.link)
49
+ if (noteIndex) return nodeIdForNote(noteIndex)
50
+
51
+ if (item.tocLineIndex !== undefined && !item.link) {
52
+ return nodeIdForLine(item.tocLineIndex)
53
+ }
54
+
55
+ if (item.folderPath?.length) {
56
+ return nodeIdForFolder(item.folderPath)
57
+ }
58
+
59
+ if (item.tocLineIndex !== undefined) {
60
+ return nodeIdForLine(item.tocLineIndex)
61
+ }
62
+
63
+ return `text:${item.text}`
64
+ }
65
+
66
+ export function enrichSidebarNodeIds<T extends SidebarNodeLike>(items: T[]): T[] {
67
+ return items.map((item) => {
68
+ const nodeId = computeSidebarNodeId(item)
69
+ const next = { ...item, nodeId } as T
70
+ if (item.items?.length) {
71
+ next.items = enrichSidebarNodeIds(item.items) as T['items']
72
+ }
73
+ return next
74
+ })
75
+ }
76
+
77
+ export function parseNodeId(nodeId: string): {
78
+ kind: 'note' | 'folder' | 'line' | 'text'
79
+ noteIndex?: string
80
+ folderPath?: string[]
81
+ tocLineIndex?: number
82
+ } {
83
+ if (nodeId.startsWith('note:')) {
84
+ return { kind: 'note', noteIndex: nodeId.slice(5) }
85
+ }
86
+ if (nodeId.startsWith('folder:')) {
87
+ const path = nodeId.slice(7)
88
+ return {
89
+ kind: 'folder',
90
+ folderPath: path ? path.split('/') : [],
91
+ }
92
+ }
93
+ if (nodeId.startsWith('line:')) {
94
+ return { kind: 'line', tocLineIndex: Number(nodeId.slice(5)) }
95
+ }
96
+ return { kind: 'text' }
97
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * utils/validators.ts
3
+ *
4
+ * 验证工具函数
5
+ */
6
+
7
+ /**
8
+ * Windows 和 macOS 都不允许的文件名字符
9
+ * Windows: < > : " / \ | ? *
10
+ * macOS: : (冒号会被转换为斜杠)
11
+ *
12
+ * 为了兼容性,我们禁止以下字符:
13
+ * - < > : " / \ | ? *
14
+ * - 控制字符 (0x00-0x1F)
15
+ */
16
+ const INVALID_FILENAME_CHARS = /[<>:"/\\|?*\x00-\x1F]/
17
+
18
+ /**
19
+ * Windows 保留文件名
20
+ */
21
+ const WINDOWS_RESERVED_NAMES = new Set([
22
+ 'CON',
23
+ 'PRN',
24
+ 'AUX',
25
+ 'NUL',
26
+ 'COM1',
27
+ 'COM2',
28
+ 'COM3',
29
+ 'COM4',
30
+ 'COM5',
31
+ 'COM6',
32
+ 'COM7',
33
+ 'COM8',
34
+ 'COM9',
35
+ 'LPT1',
36
+ 'LPT2',
37
+ 'LPT3',
38
+ 'LPT4',
39
+ 'LPT5',
40
+ 'LPT6',
41
+ 'LPT7',
42
+ 'LPT8',
43
+ 'LPT9',
44
+ ])
45
+
46
+ /**
47
+ * 验证标题是否为合法的文件夹名称
48
+ * @param title - 笔记标题
49
+ * @returns 验证结果 { valid: boolean, error?: string }
50
+ */
51
+ export function validateNoteTitle(title: string): {
52
+ valid: boolean
53
+ error?: string
54
+ } {
55
+ // 检查是否为空
56
+ if (!title || title.trim().length === 0) {
57
+ return { valid: false, error: '标题不能为空' }
58
+ }
59
+
60
+ const trimmedTitle = title.trim()
61
+
62
+ // 检查长度 (Windows 路径限制 260 字符,留足够空间)
63
+ if (trimmedTitle.length > 200) {
64
+ return { valid: false, error: '标题过长(最多200个字符)' }
65
+ }
66
+
67
+ // 检查是否包含非法字符
68
+ if (INVALID_FILENAME_CHARS.test(trimmedTitle)) {
69
+ return {
70
+ valid: false,
71
+ error: '标题包含非法字符(不允许: < > : " / \\ | ? *)',
72
+ }
73
+ }
74
+
75
+ // 检查是否以点或空格开头/结尾 (Windows 限制)
76
+ if (/^[.\s]|[.\s]$/.test(trimmedTitle)) {
77
+ return {
78
+ valid: false,
79
+ error: '标题不能以点或空格开头/结尾',
80
+ }
81
+ }
82
+
83
+ // 检查 Windows 保留名称
84
+ const upperTitle = trimmedTitle.toUpperCase()
85
+ if (WINDOWS_RESERVED_NAMES.has(upperTitle)) {
86
+ return {
87
+ valid: false,
88
+ error: `"${trimmedTitle}" 是 Windows 系统保留名称`,
89
+ }
90
+ }
91
+
92
+ // 检查是否为 Windows 保留名称加扩展名 (例如 CON.txt)
93
+ const baseName = trimmedTitle.split('.')[0].toUpperCase()
94
+ if (WINDOWS_RESERVED_NAMES.has(baseName)) {
95
+ return {
96
+ valid: false,
97
+ error: `"${trimmedTitle}" 包含 Windows 系统保留名称`,
98
+ }
99
+ }
100
+
101
+ return { valid: true }
102
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 128 128">
2
+ <path d="M0 0h128v128H0z" fill="none" />
3
+ <path fill="#656cff" fill-rule="evenodd" d="M117.9 30.289L66.664.713a5.32 5.32 0 0 0-5.323 0L10.09 30.29a4.48 4.48 0 0 0-2.234 3.872v59.663c0 1.6.853 3.077 2.24 3.878l51.24 29.586a5.33 5.33 0 0 0 5.324 0l51.246-29.586a4.48 4.48 0 0 0 2.24-3.878V34.166a4.48 4.48 0 0 0-2.24-3.872zm-3.216 6.272l-49.47 85.681c-.337.576-1.217.341-1.217-.325V65.81a3.15 3.15 0 0 0-1.573-2.72l-48.59-28.055c-.571-.331-.336-1.216.33-1.216h98.94c1.409 0 2.284 1.525 1.58 2.741" />
4
+ </svg>
@@ -14,6 +14,7 @@ export { default as icon__close } from './icon__close.svg'
14
14
  export { default as icon__collapse } from './icon__collapse.svg'
15
15
  export { default as icon__confirm } from './icon__confirm.svg'
16
16
  export { default as icon__copy } from './icon__copy.svg'
17
+ export { default as icon__cursor } from './icon__cursor.svg'
17
18
  export { default as icon__focus } from './icon__focus.svg'
18
19
  export { default as icon__fold } from './icon__fold.svg'
19
20
  export { default as icon__folder } from './icon__folder.svg'
@@ -107,22 +107,6 @@ vitepress/components/Layout/AboutPanel.vue
107
107
  </div>
108
108
  </div>
109
109
 
110
- <!-- 首次提交时间 -->
111
- <div class="timeLine" title="首次提交时间">
112
- <div class="timeLabel"><strong>⌛️ 首次提交</strong></div>
113
- <div class="timeValue">
114
- {{ formatOptionalDate(modalCreatedAt) }}
115
- </div>
116
- </div>
117
-
118
- <!-- 最近提交时间 -->
119
- <div class="timeLine" title="最近提交时间">
120
- <div class="timeLabel"><strong>⌛️ 最近提交</strong></div>
121
- <div class="timeValue">
122
- {{ formatOptionalDate(modalUpdatedAt) }}
123
- </div>
124
- </div>
125
-
126
110
  <!-- GitHub -->
127
111
  <div
128
112
  class="timeLine"
@@ -196,8 +180,6 @@ vitepress/components/Layout/AboutPanel.vue
196
180
  <script setup lang="ts">
197
181
  import { computed } from 'vue'
198
182
 
199
- import { formatDate } from '../utils'
200
-
201
183
  const props = defineProps<{
202
184
  isHomeReadme: boolean
203
185
  currentNoteId: string | null
@@ -207,8 +189,6 @@ const props = defineProps<{
207
189
  editableNoteStatus: boolean
208
190
  editableDiscussionsEnabled: boolean
209
191
  titleError: string
210
- modalCreatedAt: number | undefined
211
- modalUpdatedAt: number | undefined
212
192
  modalGithubUrl: string
213
193
  modalGithubPageUrl: string
214
194
  completionPercentage: number | null
@@ -263,10 +243,6 @@ function onDescriptionInput() {
263
243
  function onConfigChange() {
264
244
  emit('configChange')
265
245
  }
266
-
267
- function formatOptionalDate(timestamp: number | undefined) {
268
- return timestamp === undefined ? '-' : formatDate(timestamp)
269
- }
270
246
  </script>
271
247
 
272
248
  <style scoped lang="scss">
@@ -14,10 +14,10 @@ vitepress/components/Layout/DocBeforeControls.vue
14
14
  <div class="vscodeBox pcOnly" v-show="vscodeNotesDir">
15
15
  <a
16
16
  :href="vscodeNotesDir"
17
- title="open in vscode"
17
+ :title="openCurrentTitle"
18
18
  target="_blank"
19
19
  >
20
- <img :src="icon__vscode" alt="open in vscode" />
20
+ <img :src="localIdeIcon" :alt="openCurrentTitle" />
21
21
  </a>
22
22
  </div>
23
23
  <div class="contentToggleBox pcOnly">
@@ -65,13 +65,7 @@ vitepress/components/Layout/DocBeforeControls.vue
65
65
  </button>
66
66
  </div>
67
67
  <!-- 单个图标,点击打开 modal,只在有笔记数据的页面显示 -->
68
- <div
69
- class="aboutBtn"
70
- v-show="
71
- (currentNoteId && created_at && updated_at) ||
72
- (isHomeReadme && homeReadmeCreatedAt && homeReadmeUpdatedAt)
73
- "
74
- >
68
+ <div class="aboutBtn" v-show="currentNoteId || isHomeReadme">
75
69
  <button
76
70
  class="aboutIconButton"
77
71
  @click="$emit('open-time-modal')"
@@ -93,24 +87,22 @@ import ToggleFullContent from './ToggleFullContent.vue'
93
87
  import ToggleSidebar from './ToggleSidebar.vue'
94
88
  import {
95
89
  icon__github,
96
- icon__vscode,
97
90
  icon__fold,
98
91
  icon__clipboard,
99
92
  } from '../../assets/icons'
93
+ import { useLocalIde } from '../composables/useLocalIde'
100
94
 
101
95
  const props = defineProps<{
102
96
  isFullContentMode: boolean
103
97
  vscodeNotesDir: string
104
98
  isHomeReadme: boolean
105
99
  currentNoteId: string | null
106
- created_at: string | undefined
107
- updated_at: string | undefined
108
- homeReadmeCreatedAt: string | undefined
109
- homeReadmeUpdatedAt: string | undefined
110
100
  timeModalOpen: boolean
111
101
  allCollapsed: boolean
112
102
  }>()
113
103
 
104
+ const { icon: localIdeIcon, openCurrentTitle } = useLocalIde()
105
+
114
106
  const showCopyToast = ref(false)
115
107
  let copyToastTimer: ReturnType<typeof setTimeout> | null = null
116
108
 
@@ -4,7 +4,7 @@ vitepress/components/Layout/DocFooter.vue
4
4
 
5
5
  <template>
6
6
  <nav class="custom-doc-footer" v-if="prev || next">
7
- <div class="container">
7
+ <div class="pager-container">
8
8
  <div class="prev">
9
9
  <a v-if="prev" :href="prev.link" class="pager-link prev-link">
10
10
  <span class="desc">上一篇</span>
@@ -147,7 +147,7 @@ const next = computed(() => {
147
147
  padding-top: 32px;
148
148
  }
149
149
 
150
- .container {
150
+ .pager-container {
151
151
  display: flex;
152
152
  gap: 16px;
153
153
  }
@@ -219,7 +219,7 @@ const next = computed(() => {
219
219
 
220
220
  /* 响应式调整 */
221
221
  @media (max-width: 768px) {
222
- .container {
222
+ .pager-container {
223
223
  flex-direction: column;
224
224
  gap: 12px;
225
225
  }
@@ -32,10 +32,6 @@ vitepress/components/Layout/Layout.vue
32
32
  :vscode-notes-dir="vscodeNotesDir"
33
33
  :is-home-readme="isHomeReadme"
34
34
  :current-note-id="currentNoteId"
35
- :created_at="created_at"
36
- :updated_at="updated_at"
37
- :home-readme-created-at="homeReadmeCreatedAt"
38
- :home-readme-updated-at="homeReadmeUpdatedAt"
39
35
  :time-modal-open="timeModalOpen"
40
36
  :all-collapsed="allCollapsed"
41
37
  @open-time-modal="openTimeModal"
@@ -65,8 +61,6 @@ vitepress/components/Layout/Layout.vue
65
61
  v-model:editable-note-status="editableNoteStatus"
66
62
  v-model:editable-discussions-enabled="editableDiscussionsEnabled"
67
63
  v-model:title-error="titleError"
68
- :modal-created-at="modalCreatedAt"
69
- :modal-updated-at="modalUpdatedAt"
70
64
  :modal-github-url="modalGithubUrl"
71
65
  :modal-github-page-url="modalGithubPageUrl"
72
66
  :completion-percentage="completionPercentage"
@@ -116,10 +110,6 @@ vitepress/components/Layout/Layout.vue
116
110
  </AboutModal>
117
111
  </template>
118
112
  <template #doc-footer-before>
119
- <!-- <div class="footer-time-info">
120
- <p title="首次提交时间">首次提交时间:{{ formatDate(created_at) }}</p>
121
- <p title="最近提交时间">最近提交时间:{{ formatDate(updated_at) }}</p>
122
- </div> -->
123
113
  </template>
124
114
  <template #doc-after>
125
115
  <!-- 自定义 DocFooter -->
@@ -335,8 +325,6 @@ function getEmptyNoteConfig() {
335
325
  const isDiscussionsVisible = computed(
336
326
  () => currentNoteConfig.value.enableDiscussions,
337
327
  );
338
- const updated_at = computed(() => currentNoteConfig.value.updated_at);
339
- const created_at = computed(() => currentNoteConfig.value.created_at);
340
328
 
341
329
  // 判断是否为首页 README.md
342
330
  const isHomeReadme = computed(() => vpData.page.value.filePath === "README.md");
@@ -349,10 +337,6 @@ const completionPercentage = computed(() => {
349
337
  return Math.round((doneNotesLen.value / totalNotesLen.value) * 100);
350
338
  });
351
339
 
352
- // 首页 README.md 的时间戳
353
- const homeReadmeCreatedAt = computed(() => readmeData?.created_at);
354
- const homeReadmeUpdatedAt = computed(() => readmeData?.updated_at);
355
-
356
340
  // #region - Composables
357
341
  // 404 重定向
358
342
  const { showNotFound, decodedCurrentPath, initRedirectCheck } =
@@ -509,20 +493,6 @@ const modalGithubPageUrl = computed(() => {
509
493
  return "";
510
494
  });
511
495
 
512
- // modal 中显示的创建时间
513
- const modalCreatedAt = computed(() => {
514
- return modalIsHomeReadme.value
515
- ? homeReadmeCreatedAt.value
516
- : modalNoteConfig.value.created_at;
517
- });
518
-
519
- // modal 中显示的更新时间
520
- const modalUpdatedAt = computed(() => {
521
- return modalIsHomeReadme.value
522
- ? homeReadmeUpdatedAt.value
523
- : modalNoteConfig.value.updated_at;
524
- });
525
-
526
496
  function openTimeModal() {
527
497
  sidebarAboutNoteId.value = null;
528
498
  timeModalOpen.value = true;
@@ -1,40 +1,49 @@
1
1
  /**
2
2
  * vitepress/components/Layout/composables/useVSCodeIntegration.ts
3
3
  *
4
- * VSCode 集成和 GitHub 链接拦截
4
+ * 本地 IDE 集成和 GitHub 链接拦截
5
5
  */
6
6
 
7
7
  import { useData } from 'vitepress'
8
- import { ref } from 'vue'
8
+ import { computed, ref, watch } from 'vue'
9
9
 
10
+ import { useLocalIde } from '../../composables/useLocalIde'
10
11
  import { NOTES_DIR_KEY } from '../../constants'
11
- import {
12
- resolveNoteReadmePath,
13
- toVscodeFileUrl,
14
- } from '../../utils/vscodePaths'
12
+ import { resolveNoteReadmePath, toIdeFileUrl } from '../../utils/vscodePaths'
15
13
 
16
14
  import type { Router } from 'vitepress'
17
15
  import type { Ref, ComputedRef } from 'vue'
18
16
 
19
-
20
17
  /**
21
- * VSCode 集成和 GitHub 链接拦截
18
+ * 本地 IDE 集成和 GitHub 链接拦截
22
19
  */
23
20
  export function useVSCodeIntegration() {
24
21
  const vpData = useData()
25
- const vscodeNotesDir = ref('')
22
+ const { ide } = useLocalIde()
23
+ const noteLocalPath = ref('')
24
+
25
+ const vscodeNotesDir = computed(() => {
26
+ if (!noteLocalPath.value) return ''
27
+ // 显式依赖 ide,确保切换 IDE 后 href 更新
28
+ return toIdeFileUrl(noteLocalPath.value, ide.value)
29
+ })
26
30
 
27
- // 更新 VSCode 笔记目录
31
+ // 更新当前页面对应的本地笔记路径
28
32
  const updateVscodeNoteDir = () => {
29
33
  if (typeof window !== 'undefined') {
30
34
  const notesDir = localStorage.getItem(NOTES_DIR_KEY)
31
35
  const notePath = notesDir
32
36
  ? resolveNoteReadmePath(notesDir, vpData.page.value.relativePath)
33
37
  : null
34
- vscodeNotesDir.value = notePath ? toVscodeFileUrl(notePath) : ''
38
+ noteLocalPath.value = notePath || ''
35
39
  }
36
40
  }
37
41
 
42
+ watch(ide, () => {
43
+ // IDE 切换后重新解析路径(无需刷新页面)
44
+ updateVscodeNoteDir()
45
+ })
46
+
38
47
  // 拦截 home README 中的笔记链接,将 GitHub 链接转换为站点内跳转
39
48
  const interceptHomeReadmeLinks = (
40
49
  isHomeReadme: Ref<boolean> | ComputedRef<boolean>,
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * vitepress/components/Layout/homeReadme.data.ts
3
3
  */
4
- import { execSync } from 'node:child_process'
5
4
  import fs from 'node:fs'
6
5
  import path from 'node:path'
7
6
 
@@ -12,8 +11,6 @@ interface ReadmeData {
12
11
  doneNotesID: string[]
13
12
  doneNotesLen: number
14
13
  totalNotesLen: number
15
- created_at?: number
16
- updated_at?: number
17
14
  }
18
15
 
19
16
  export default {
@@ -28,15 +25,10 @@ export default {
28
25
 
29
26
  watchedFiles.forEach((file) => {
30
27
  if (file.endsWith('README.md')) {
31
- // console.log('file:', file) // => file: README.md
32
-
33
28
  const fileContent = fs.readFileSync(file, 'utf-8')
34
29
  const doneNotesID = getDoneNotesID(fileContent)
35
30
  const doneNotesLen = doneNotesID.length
36
31
 
37
- // 获取 git 仓库的时间戳
38
- const timestamps = getGitTimestamps()
39
-
40
32
  // 计算总笔记数
41
33
  const notesDir = path.join(path.dirname(file), 'notes')
42
34
  const totalNotesLen = getTotalNotesCount(notesDir)
@@ -46,7 +38,6 @@ export default {
46
38
  doneNotesID,
47
39
  doneNotesLen,
48
40
  totalNotesLen,
49
- ...timestamps,
50
41
  }
51
42
  }
52
43
  })
@@ -74,45 +65,6 @@ function getTotalNotesCount(notesDir: string): number {
74
65
  }
75
66
  }
76
67
 
77
- /**
78
- * 获取 git 仓库的时间戳
79
- * @returns 包含 created_at 和 updated_at 的对象
80
- */
81
- function getGitTimestamps(): {
82
- created_at?: number
83
- updated_at?: number
84
- } {
85
- const now = Date.now()
86
- let created_at = now
87
- let updated_at = now
88
-
89
- try {
90
- // 仓库的首次提交时间(最早的提交)
91
- // 先获取所有提交,然后取第一行
92
- const createdStdout = execSync(`git log --reverse --format=%ct`, {
93
- encoding: 'utf-8',
94
- })
95
- const createdTs = createdStdout.toString().trim().split('\n')[0]
96
- if (createdTs) {
97
- created_at = parseInt(createdTs, 10) * 1000
98
- }
99
-
100
- // 仓库的最近一次提交时间
101
- const updatedStdout = execSync(`git log -1 --format=%ct`, {
102
- encoding: 'utf-8',
103
- })
104
- const updatedTs = updatedStdout.toString().trim()
105
- if (updatedTs) {
106
- updated_at = parseInt(updatedTs, 10) * 1000
107
- }
108
-
109
- return { created_at, updated_at }
110
- } catch (error) {
111
- console.error(`获取 git 仓库时间戳失败:`, error)
112
- return { created_at, updated_at }
113
- }
114
- }
115
-
116
68
  /**
117
69
  * 返回已完成的笔记的 ID 列表
118
70
  * @param fileContent 文件内容
@@ -12,17 +12,19 @@ import { onContentUpdated } from 'vitepress'
12
12
  import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
13
13
 
14
14
  import { icon__fullscreen, icon__fullscreen_exit, icon__confirm } from '../../assets/icons'
15
- import { MARKMAP_THEME_KEY, MARKMAP_EXPAND_LEVEL_KEY } from '../constants'
15
+ import { MARKMAP_THEME_KEY } from '../constants'
16
16
 
17
17
  // doc: https://github.com/markmap/markmap/blob/205367a24603dc187f67da1658940c6cade20dce/packages/markmap-view/src/constants.ts#L15
18
18
 
19
+ const DEFAULT_EXPAND_LEVEL = 3
20
+
19
21
  const props = defineProps({
20
22
  content: { type: String, default: '' },
21
23
  duration: { type: Number, default: 100 },
22
24
  spacingVertical: { type: Number, default: 10 },
23
25
  spacingHorizontal: { type: Number, default: 20 },
24
26
  nodeMinHeight: { type: Number, default: 24 },
25
- initialExpandLevel: { type: Number, default: 5 },
27
+ initialExpandLevel: { type: Number, default: DEFAULT_EXPAND_LEVEL },
26
28
  })
27
29
 
28
30
  const svgRef = ref<SVGSVGElement | null>(null)
@@ -36,15 +38,6 @@ let darkClassObserver: MutationObserver | null = null
36
38
 
37
39
  const isMarkmapDark = ref(false)
38
40
 
39
- // 从 localStorage 读取配置,如果没有则使用 props 或默认值
40
- const getInitialExpandLevel = () => {
41
- if (typeof window !== 'undefined') {
42
- const saved = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY)
43
- if (saved) return parseInt(saved)
44
- }
45
- return props.initialExpandLevel
46
- }
47
-
48
41
  const getThemeColorFn = () => {
49
42
  if (typeof window !== 'undefined') {
50
43
  if (document.documentElement.classList.contains('markmap-dark')) {
@@ -63,8 +56,8 @@ const getThemeColorFn = () => {
63
56
  return scaleOrdinal(schemePastel2)
64
57
  }
65
58
 
66
- // 可由配置/props 初始化,也可以通过工具栏改动
67
- const expandLevel = ref(getInitialExpandLevel())
59
+ // 可由 props 初始化,也可以通过工具栏改动
60
+ const expandLevel = ref(props.initialExpandLevel)
68
61
  const transformer = new Transformer()
69
62
  const isFullscreen = ref(false)
70
63