@tnotesjs/core 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +87 -87
  2. package/dist/{chunk-4MO7F7Q4.js → chunk-BSTQOJVE.js} +1 -1
  3. package/dist/{chunk-SQVHJR2Z.js → chunk-O4DCXHOV.js} +0 -3
  4. package/dist/cli/index.js +2 -2
  5. package/dist/index.js +1 -1
  6. package/dist/vitepress/config/index.js +2 -2
  7. package/package.json +3 -4
  8. package/types/config.ts +60 -61
  9. package/types/index.ts +11 -11
  10. package/types/note.ts +33 -33
  11. package/vitepress/assets/icons/icon__check.svg +3 -3
  12. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  13. package/vitepress/assets/icons/icon__close.svg +1 -1
  14. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  15. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  16. package/vitepress/assets/icons/icon__copy.svg +4 -4
  17. package/vitepress/assets/icons/icon__focus.svg +1 -1
  18. package/vitepress/assets/icons/icon__fold.svg +3 -3
  19. package/vitepress/assets/icons/icon__folder.svg +1 -1
  20. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  21. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  22. package/vitepress/assets/icons/icon__github.svg +3 -3
  23. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  24. package/vitepress/assets/icons/icon__next.svg +1 -1
  25. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  26. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  27. package/vitepress/assets/icons/icon__prev.svg +1 -1
  28. package/vitepress/assets/icons/icon__restore.svg +1 -1
  29. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  30. package/vitepress/assets/icons/icon__search.svg +1 -1
  31. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  32. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  33. package/vitepress/assets/icons/icon__totop.svg +5 -5
  34. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  35. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  37. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  38. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  39. package/vitepress/assets/icons/index.ts +39 -39
  40. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  41. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  42. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -213
  43. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  44. package/vitepress/components/Discussions/Discussions.vue +244 -211
  45. package/vitepress/components/EnWordList/EnWordList.vue +663 -543
  46. package/vitepress/components/EnWordList/RightClickMenu.vue +89 -66
  47. package/vitepress/components/Footprints/Footprints.vue +472 -377
  48. package/vitepress/components/Layout/AboutModal.vue +331 -105
  49. package/vitepress/components/Layout/AboutPanel.vue +484 -270
  50. package/vitepress/components/Layout/ContentCollapse.vue +590 -603
  51. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  52. package/vitepress/components/Layout/DocBeforeControls.vue +351 -149
  53. package/vitepress/components/Layout/DocFooter.vue +233 -233
  54. package/vitepress/components/Layout/ImagePreview.vue +484 -281
  55. package/vitepress/components/Layout/Layout.vue +744 -621
  56. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +0 -2
  57. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  58. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  59. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -171
  60. package/vitepress/components/Layout/SidebarResizeHandle.vue +311 -319
  61. package/vitepress/components/Layout/Swiper.vue +167 -167
  62. package/vitepress/components/Layout/ToggleFullContent.vue +47 -35
  63. package/vitepress/components/Layout/ToggleSidebar.vue +37 -26
  64. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  65. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  66. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  67. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  68. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  69. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  70. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  71. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  72. package/vitepress/components/LoadingPage/LoadingPage.vue +1 -6
  73. package/vitepress/components/MarkMap/MarkMap.vue +564 -405
  74. package/vitepress/components/Mermaid/Mermaid.vue +643 -365
  75. package/vitepress/components/NotesTable/NotesTable.vue +174 -98
  76. package/vitepress/components/NotesTable/README.md +67 -67
  77. package/vitepress/components/Settings/Settings.vue +711 -334
  78. package/vitepress/components/Settings/SettingsDialog.vue +1 -11
  79. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  80. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  81. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -946
  82. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  83. package/vitepress/components/constants.ts +109 -109
  84. package/vitepress/components/notesConfig.data.ts +73 -73
  85. package/vitepress/components/sidebar.data.ts +59 -59
  86. package/vitepress/components/tnotes-config.data.ts +21 -21
  87. package/vitepress/components/utils.ts +26 -26
  88. package/vitepress/config/index.ts +169 -169
  89. package/vitepress/configs/constants.ts +26 -26
  90. package/vitepress/configs/head.config.ts +25 -25
  91. package/vitepress/configs/index.ts +9 -9
  92. package/vitepress/configs/markdown-it.d.ts +23 -23
  93. package/vitepress/configs/markdown.config.ts +368 -367
  94. package/vitepress/configs/theme.config.ts +108 -108
  95. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  96. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  97. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  98. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  99. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  100. package/vitepress/theme/index.ts +137 -137
  101. package/vitepress/theme/styles/base.scss +66 -64
  102. package/vitepress/theme/styles/components/404.scss +31 -31
  103. package/vitepress/theme/styles/components/collapse.scss +172 -172
  104. package/vitepress/theme/styles/components/markmap.scss +101 -101
  105. package/vitepress/theme/styles/components/swiper.scss +255 -255
  106. package/vitepress/theme/styles/index.scss +25 -25
  107. package/vitepress/theme/styles/layout.scss +78 -78
  108. package/vitepress/theme/styles/utilities.scss +39 -39
  109. package/vitepress/theme/styles/vitepress-override.scss +25 -25
  110. package/vitepress/components/Discussions/Discussions.module.scss +0 -32
  111. package/vitepress/components/EnWordList/EnWordList.module.scss +0 -124
  112. package/vitepress/components/EnWordList/RightClickMenu.module.scss +0 -22
  113. package/vitepress/components/Footprints/Footprints.module.scss +0 -93
  114. package/vitepress/components/Layout/AboutModal.module.scss +0 -233
  115. package/vitepress/components/Layout/ImagePreview.module.scss +0 -201
  116. package/vitepress/components/Layout/Layout.module.scss +0 -661
  117. package/vitepress/components/Layout/ToggleFullContent.module.scss +0 -11
  118. package/vitepress/components/Layout/ToggleSidebar.module.scss +0 -11
  119. package/vitepress/components/MarkMap/MarkMap.module.scss +0 -159
  120. package/vitepress/components/Mermaid/Mermaid.module.scss +0 -275
  121. package/vitepress/components/NotesTable/NotesTable.module.scss +0 -77
  122. package/vitepress/components/Settings/Settings.module.scss +0 -375
@@ -1,946 +1,945 @@
1
- <script setup>
2
- import { useData } from 'vitepress'
3
- import { ref, computed, watch } from 'vue'
4
-
5
- import {
6
- icon__fold,
7
- icon__github,
8
- icon__vscode,
9
- icon__folder,
10
- icon__search,
11
- icon__mindmap,
12
- icon__number_gray,
13
- icon__number_purple,
14
- } from '../../assets/icons'
15
- import { NOTES_DIR_KEY, REPO_NAME, AUTHOR, ROOT_ITEM } from '../constants.ts'
16
- import { useSettingsDialog } from '../Settings/composables/useSettingsDialog'
17
- import { data as sidebarConfig } from '../sidebar.data'
18
- import { formatDate } from '../utils.ts'
19
- // @ts-expect-error - VitePress Data Loader
20
- import MindMapView from './MindMapView.vue'
21
- import NotesTrendChart from './NotesTrendChart.vue'
22
-
23
- // #region props
24
- const props = defineProps({
25
- pending: {
26
- type: Boolean,
27
- default: false,
28
- },
29
- done: {
30
- type: Boolean,
31
- default: true,
32
- },
33
- })
34
- // #endregion
35
-
36
- // #region data
37
- const viewMode = ref('folder')
38
- const expandedGroupsFolder = ref(new Set())
39
- const searchQuery = ref('')
40
- const debouncedQuery = ref('')
41
- const showAbout = ref(false)
42
- const showNumber = ref(false)
43
- let debounceTimer = null
44
- // #endregion
45
-
46
- // #region computed
47
- const searchResults = computed(() => {
48
- const query = debouncedQuery.value.trim().toLowerCase()
49
- if (!query) return []
50
- return articles.filter((article) => {
51
- const fullTitle = `${article.realNumber}. ${article.text}`.toLowerCase()
52
- return fullTitle.includes(query)
53
- })
54
- })
55
-
56
- const folderViewData = computed(() => {
57
- const result = {}
58
-
59
- // 按完整分组路径构建层级结构(平铺模式)
60
- articles.forEach((article) => {
61
- // 将所有层级铺出来,路径信息作为分组名
62
- // eg. 1. 第一层级/第一个第二层级
63
- // article 1
64
- // article 2
65
- // ……
66
- // 1. 第一层级/第二个第二层级
67
- // article 1
68
- // article 2
69
- // ……
70
- // 1. 第一层级/第三个第二层级
71
- // article 1
72
- // article 2
73
- // ……
74
- // const group = article.group
75
- // if (!result[group]) {
76
- // result[group] = {
77
- // name: group,
78
- // articles: [],
79
- // fullPath: group,
80
- // }
81
- // }
82
- // result[group].articles.push(article)
83
-
84
- // 只铺出第一层级
85
- const firstLevelCategory = article.firstLevelCategory
86
- if (!result[firstLevelCategory]) {
87
- result[firstLevelCategory] = {
88
- name: firstLevelCategory,
89
- articles: [],
90
- fullPath: firstLevelCategory,
91
- }
92
- }
93
- result[firstLevelCategory].articles.push(article)
94
- })
95
-
96
- return result
97
- })
98
- // #endregion
99
-
100
- const { open: openSettings } = useSettingsDialog()
101
- const { site } = useData()
102
- const baseUrl = site.value.base.replace(/\/$/, '')
103
-
104
- // 使用 VitePress Data Loader 读取的 sidebar 数据
105
- const sidebarData = computed(() => {
106
- return sidebarConfig && sidebarConfig['/notes/']
107
- ? sidebarConfig['/notes/']
108
- : []
109
- })
110
-
111
- const { articles, groups } = extractArticlesWithGroups(
112
- sidebarData.value,
113
- props.pending,
114
- props.done,
115
- )
116
-
117
- // 根据笔记数量动态计算防抖时间
118
- // ≤100: 无需防抖,过滤几乎无开销
119
- // ≤500: 100ms,轻量防抖避免频繁 DOM 更新
120
- // ≤2000: 150ms
121
- // 5000: 200ms,3k+ 笔记场景(如 LeetCode 知识库)
122
- // >5000: 300ms,接近万篇上限时适当增加
123
- const searchDebounceDelay = (() => {
124
- const count = articles.length
125
- if (count <= 100) return 0
126
- if (count <= 500) return 100
127
- if (count <= 2000) return 150
128
- if (count <= 5000) return 200
129
- return 300
130
- })()
131
-
132
- watch(searchQuery, (val) => {
133
- if (debounceTimer) clearTimeout(debounceTimer)
134
- if (searchDebounceDelay === 0) {
135
- debouncedQuery.value = val
136
- return
137
- }
138
- debounceTimer = setTimeout(() => {
139
- debouncedQuery.value = val
140
- }, searchDebounceDelay)
141
- })
142
-
143
- function extractArticlesWithGroups(sidebar, showPending, showDone) {
144
- const articles = []
145
- const groups = {}
146
-
147
- function traverse(items, groupPath = []) {
148
- for (let i = 0; i < items.length; i++) {
149
- const item = items[i]
150
-
151
- if (item.items && Array.isArray(item.items)) {
152
- const newGroupPath = [...groupPath, item.text]
153
- traverse(item.items, newGroupPath)
154
- } else if (item.text) {
155
- let shouldInclude = false
156
- let status = ''
157
- let cleanText = ''
158
-
159
- if (item.text.startsWith('✅') && showDone) {
160
- shouldInclude = true
161
- status = 'done'
162
- cleanText = item.text.replace('', '')
163
- } else if (item.text.startsWith('⏰') && showPending) {
164
- shouldInclude = true
165
- status = 'pending'
166
- cleanText = item.text.replace('⏰ ', '')
167
- }
168
-
169
- if (shouldInclude) {
170
- const group = groupPath.join(' / ')
171
- const firstLevelCategory = groupPath[0] || '未分类'
172
-
173
- const realNumber = extractRealNumberFromLink(item.link)
174
- const title = extractTitleFromText(cleanText)
175
- const fullLink = baseUrl ? baseUrl + item.link : item.link
176
-
177
- // 添加 relativePath 字段
178
- const article = {
179
- text: title,
180
- realNumber,
181
- link: fullLink,
182
- relativePath: item.link, // 存储相对路径
183
- group: group || '未分类',
184
- status,
185
- firstLevelCategory,
186
- }
187
-
188
- articles.push(article)
189
-
190
- if (!groups[firstLevelCategory]) {
191
- groups[firstLevelCategory] = []
192
- }
193
- groups[firstLevelCategory].push(article)
194
- }
195
- }
196
- }
197
- }
198
-
199
- traverse(sidebar)
200
- return { articles, groups }
201
- }
202
-
203
- // link 中提取真实编号
204
- function extractRealNumberFromLink(link) {
205
- if (!link) return '0000'
206
-
207
- const match = link.match(/\/notes\/(\d{4})/)
208
- return match ? match[1] : '0000'
209
- }
210
-
211
- // 从文本中提取标题(去除开头的编号部分)
212
- function extractTitleFromText(text) {
213
- if (!text) return ''
214
-
215
- return text.replace(/^\d{4}\.?\s/, '')
216
- }
217
-
218
- function handleCardClick(link) {
219
- // open in new tab
220
- if (link) {
221
- window.open(link, '_blank')
222
- }
223
-
224
- // open in current tab
225
- // if (link) {
226
- // window.location.href = link
227
- // }
228
- }
229
-
230
- // 在文件夹视图中切换分组展开状态
231
- function toggleGroupFolder(groupName) {
232
- if (expandedGroupsFolder.value.has(groupName)) {
233
- expandedGroupsFolder.value.delete(groupName)
234
- } else {
235
- expandedGroupsFolder.value.add(groupName)
236
- }
237
- }
238
-
239
- // 切换所有折叠状态
240
- function toggleAllFold() {
241
- const isAllExpanded =
242
- expandedGroupsFolder.value.size === Object.keys(folderViewData.value).length
243
-
244
- if (isAllExpanded) {
245
- expandedGroupsFolder.value.clear()
246
- } else {
247
- Object.keys(folderViewData.value).forEach((folderName) => {
248
- expandedGroupsFolder.value.add(folderViewData.value[folderName].fullPath)
249
- })
250
- }
251
- }
252
-
253
- // 打开 GitHub 仓库
254
- function openGithubRepo() {
255
- window.open(`https://github.com/${AUTHOR}/${REPO_NAME}`, '_blank')
256
- }
257
-
258
- // 打开 VS Code 知识库
259
- function openVSCodeRepo() {
260
- const notesDir = localStorage.getItem(NOTES_DIR_KEY)
261
-
262
- if (!notesDir) {
263
- const shouldRedirect = confirm(
264
- '请先配置本地知识库所在位置,点击确定打开设置面板',
265
- )
266
- if (shouldRedirect) {
267
- openSettings()
268
- }
269
- return
270
- }
271
-
272
- window.open('vscode://file/' + notesDir, '_blank')
273
- }
274
-
275
- // 打开 VS Code 中的笔记目录
276
- function openVSCodeArticle(article) {
277
- const notesDir = localStorage.getItem(NOTES_DIR_KEY)
278
-
279
- if (!notesDir) {
280
- const shouldRedirect = confirm(
281
- '请先配置本地知识库所在位置,点击确定打开设置面板',
282
- )
283
- if (shouldRedirect) {
284
- openSettings()
285
- }
286
- return
287
- }
288
-
289
- // 构建笔记目录路径
290
- const notePath = `${notesDir}/${article.relativePath.replace('/README', '')}`
291
- // console.log(notePath, encodeURI(notePath))
292
- window.open('vscode://file/' + encodeURI(notePath), '_blank')
293
- }
294
- </script>
295
-
296
- <template>
297
- <div class="sidebar-view-container">
298
- <!-- 控制栏区域 -->
299
- <div class="control-bar">
300
- <!-- 左侧视图切换按钮 -->
301
- <div class="view-toggle">
302
- <button
303
- :class="{ active: viewMode === 'folder' }"
304
- @click="viewMode = 'folder'"
305
- >
306
- <img :src="icon__folder" alt="文件夹视图" />
307
- </button>
308
- <button
309
- :class="{ active: viewMode === 'search' }"
310
- @click="viewMode = 'search'"
311
- >
312
- <img :src="icon__search" alt="搜索视图" />
313
- </button>
314
- <button
315
- :class="{ active: viewMode === 'mindmap' }"
316
- @click="viewMode = 'mindmap'"
317
- >
318
- <img :src="icon__mindmap" alt="思维导图" />
319
- </button>
320
- </div>
321
- <!-- 右侧控制按钮 -->
322
- <div class="actions">
323
- <!-- 编号显示切换按钮 -->
324
- <button
325
- class="number-toggle"
326
- v-show="viewMode === 'folder'"
327
- @click="showNumber = !showNumber"
328
- >
329
- <img
330
- :src="showNumber ? icon__number_purple : icon__number_gray"
331
- alt="切换编号显示"
332
- />
333
- </button>
334
- <!-- 折叠/展开按钮 -->
335
- <button
336
- class="fold-toggle"
337
- v-show="viewMode === 'folder'"
338
- @click="toggleAllFold"
339
- >
340
- <img :src="icon__fold" alt="折叠/展开" />
341
- </button>
342
-
343
- <!-- GitHub 按钮 -->
344
- <button class="github-link" @click="openGithubRepo">
345
- <img :src="icon__github" alt="GitHub仓库" />
346
- </button>
347
-
348
- <!-- VS Code 按钮 -->
349
- <button class="vscode-open" @click="openVSCodeRepo">
350
- <img :src="icon__vscode" alt="使用 VS Code 打开本地知识库" />
351
- </button>
352
-
353
- <!-- 关于按钮 -->
354
- <button class="about-toggle" @click="showAbout = !showAbout">!</button>
355
- </div>
356
- </div>
357
-
358
- <!-- 关于弹窗 -->
359
- <div class="about-overlay" v-if="showAbout" @click.self="showAbout = false">
360
- <div class="about-dialog">
361
- <div class="about-header">
362
- <span>关于知识库</span>
363
- <button class="about-close" @click="showAbout = false">✕</button>
364
- </div>
365
- <div class="about-body">
366
- <div class="about-item">
367
- <span class="about-label">GitHub</span>
368
- <a
369
- class="about-value about-link"
370
- :href="`https://github.com/${AUTHOR}/${REPO_NAME}`"
371
- target="_blank"
372
- >
373
- {{ AUTHOR }}/{{ REPO_NAME }}
374
- </a>
375
- </div>
376
- <div class="about-item">
377
- <span class="about-label">📅 创建时间</span>
378
- <span class="about-value">{{
379
- formatDate(ROOT_ITEM.created_at)
380
- }}</span>
381
- </div>
382
- <div class="about-item">
383
- <span class="about-label">♻️ 最近更新</span>
384
- <span class="about-value">{{
385
- formatDate(ROOT_ITEM.updated_at)
386
- }}</span>
387
- </div>
388
- </div>
389
- </div>
390
- </div>
391
- <!-- 搜索视图 -->
392
- <div class="search-view" v-if="viewMode === 'search'">
393
- <div class="search-box">
394
- <input
395
- v-model="searchQuery"
396
- type="text"
397
- placeholder="搜索笔记标题…"
398
- class="search-input"
399
- />
400
- </div>
401
- <div class="search-summary">找到 {{ searchResults.length }} 条结果</div>
402
- <div class="folder-tree" v-show="searchResults.length > 0">
403
- <div
404
- v-for="article in searchResults"
405
- :key="article.link"
406
- class="folder-article"
407
- >
408
- <div class="article-info" @click="handleCardClick(article.link)">
409
- <span class="article-status" :class="`status-${article.status}`">
410
- {{
411
- article.status === 'done'
412
- ? ''
413
- : article.status === 'pending'
414
- ? ''
415
- : '📄'
416
- }}
417
- </span>
418
- <span class="article-title">{{
419
- `${article.realNumber}. ${article.text}`
420
- }}</span>
421
- </div>
422
- <button
423
- class="vscode-article"
424
- @click.stop="openVSCodeArticle(article)"
425
- title="在 VS Code 中打开笔记目录"
426
- >
427
- <img :src="icon__vscode" alt="打开笔记目录" />
428
- </button>
429
- </div>
430
- </div>
431
- </div>
432
- <!-- 思维导图视图 -->
433
- <MindMapView v-if="viewMode === 'mindmap'" :sidebarData="sidebarData" />
434
- <!-- 文件夹视图 -->
435
- <div class="folder-view" v-if="viewMode === 'folder'">
436
- <NotesTrendChart
437
- v-if="ROOT_ITEM.completed_notes_count"
438
- :completedNotesCount="ROOT_ITEM.completed_notes_count"
439
- />
440
- <div class="folder-tree">
441
- <div
442
- v-for="(folder, folderName) in folderViewData"
443
- :key="folderName"
444
- class="folder-group"
445
- >
446
- <!-- 文件夹组 -->
447
- <div
448
- class="folder-header"
449
- @click="toggleGroupFolder(folder.fullPath)"
450
- >
451
- <span class="folder-icon">
452
- {{ expandedGroupsFolder.has(folder.fullPath) ? '📂' : '📁' }}
453
- </span>
454
- <span class="folder-name">{{ folderName }}</span>
455
- <span class="folder-count">{{ folder.articles.length }}</span>
456
- </div>
457
-
458
- <!-- 展开的文章 -->
459
- <div
460
- class="folder-content"
461
- v-if="expandedGroupsFolder.has(folder.fullPath)"
462
- >
463
- <!-- 当前文件夹中的文章 -->
464
- <div
465
- v-for="article in folder.articles"
466
- :key="article.link"
467
- class="folder-article"
468
- >
469
- <div class="article-info" @click="handleCardClick(article.link)">
470
- <span
471
- class="article-status"
472
- :class="`status-${article.status}`"
473
- >
474
- {{
475
- article.status === 'done'
476
- ? ''
477
- : article.status === 'pending'
478
- ? ''
479
- : '📄'
480
- }}
481
- </span>
482
- <!-- <span class="article-number">{{ article.realNumber }}</span> -->
483
- <span class="article-title">{{
484
- showNumber
485
- ? `${article.realNumber}. ${article.text}`
486
- : article.text
487
- }}</span>
488
- </div>
489
-
490
- <button
491
- class="vscode-article"
492
- @click.stop="openVSCodeArticle(article)"
493
- title="在 VS Code 中打开笔记目录"
494
- >
495
- <img :src="icon__vscode" alt="打开笔记目录" />
496
- </button>
497
- </div>
498
- </div>
499
- </div>
500
- </div>
501
- </div>
502
- </div>
503
- </template>
504
-
505
- <style scoped lang="scss">
506
- /* 关于弹窗样式 */
507
- .about-overlay {
508
- position: fixed;
509
- top: 0;
510
- left: 0;
511
- right: 0;
512
- bottom: 0;
513
- background-color: rgba(0, 0, 0, 0.5);
514
- display: flex;
515
- align-items: center;
516
- justify-content: center;
517
- z-index: 100;
518
- }
519
-
520
- .about-dialog {
521
- background-color: var(--vp-c-bg);
522
- border: 1px solid var(--vp-c-divider);
523
- border-radius: 12px;
524
- width: 90%;
525
- max-width: 400px;
526
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
527
-
528
- .about-header {
529
- display: flex;
530
- justify-content: space-between;
531
- align-items: center;
532
- padding: 0.75rem 1rem;
533
- border-bottom: 1px solid var(--vp-c-divider);
534
- font-weight: 600;
535
- font-size: 0.95rem;
536
- }
537
-
538
- .about-close {
539
- background: none;
540
- border: none;
541
- cursor: pointer;
542
- color: var(--vp-c-text-2);
543
- font-size: 1rem;
544
- padding: 0.25rem 0.5rem;
545
- border-radius: 4px;
546
- transition: background-color 0.2s;
547
-
548
- &:hover {
549
- background-color: var(--vp-c-bg-soft);
550
- color: var(--vp-c-text-1);
551
- }
552
- }
553
-
554
- .about-body {
555
- padding: 0.75rem 1rem;
556
- }
557
-
558
- .about-item {
559
- display: flex;
560
- justify-content: space-between;
561
- align-items: center;
562
- padding: 0.5rem 0;
563
-
564
- &:not(:last-child) {
565
- border-bottom: 1px solid var(--vp-c-divider);
566
- }
567
- }
568
-
569
- .about-label {
570
- color: var(--vp-c-text-2);
571
- font-size: 0.85rem;
572
- }
573
-
574
- .about-value {
575
- color: var(--vp-c-text-1);
576
- font-size: 0.85rem;
577
- }
578
-
579
- .about-link {
580
- color: var(--vp-c-brand-1);
581
- text-decoration: none;
582
-
583
- &:hover {
584
- text-decoration: underline;
585
- }
586
- }
587
- }
588
-
589
- .about-toggle {
590
- background: none;
591
- border: none;
592
- padding: 0.5rem;
593
- cursor: pointer;
594
- border-radius: 4px;
595
- transition: background-color 0.3s ease;
596
- color: #646cff;
597
- font-size: 0.85rem;
598
- line-height: 1;
599
-
600
- &:hover {
601
- background-color: var(--vp-c-bg-soft);
602
- }
603
- }
604
-
605
- /* 文件夹视图样式 */
606
- .folder-view {
607
- width: 100%;
608
- padding: 1rem 0;
609
-
610
- .folder-tree {
611
- // background-color: var(--vp-c-bg-soft);
612
- border-radius: 8px;
613
- padding: 1rem;
614
-
615
- .folder-group {
616
- margin-bottom: 1rem;
617
- border: 1px solid var(--vp-c-divider);
618
- border-radius: 8px;
619
- overflow: hidden;
620
-
621
- .folder-header {
622
- display: flex;
623
- align-items: center;
624
- padding: 0.75rem 1rem;
625
- // background-color: var(--vp-c-bg-soft);
626
- cursor: pointer;
627
- transition: background-color 0.3s;
628
-
629
- // &:hover {
630
- // background-color: var(--vp-c-bg-elv);
631
- // }
632
-
633
- .folder-icon {
634
- margin-right: 0.75rem;
635
- font-size: 1.1rem;
636
- }
637
-
638
- .folder-name {
639
- flex: 1;
640
- font-weight: 500;
641
- font-size: 0.95rem;
642
- }
643
-
644
- .folder-count {
645
- background-color: var(--vp-c-default-soft);
646
- color: var(--vp-c-text-2);
647
- font-size: 0.8rem;
648
- padding: 0.2rem 0.5rem;
649
- border-radius: 4px;
650
- }
651
- }
652
-
653
- .folder-content {
654
- padding: 0.5rem 1rem;
655
- background-color: var(--vp-c-bg);
656
-
657
- .folder-article {
658
- display: flex;
659
- align-items: center;
660
- justify-content: space-between;
661
- padding: 0.6rem 0.8rem;
662
- margin: 0.4rem 0;
663
- border-radius: 6px;
664
- transition: background-color 0.3s;
665
-
666
- &:hover {
667
- background-color: var(--vp-c-bg-soft);
668
- }
669
-
670
- .article-info {
671
- display: flex;
672
- align-items: center;
673
- flex: 1;
674
- cursor: pointer;
675
- overflow: hidden;
676
-
677
- .article-status {
678
- margin-right: 0.8rem;
679
- font-size: 0.9rem;
680
- width: 1.2rem;
681
- text-align: center;
682
- flex-shrink: 0;
683
-
684
- &.status-done {
685
- color: var(--vp-c-green-1);
686
- }
687
-
688
- &.status-pending {
689
- color: var(--vp-c-yellow-1);
690
- }
691
- }
692
-
693
- .article-number {
694
- background-color: var(--vp-c-default-soft);
695
- color: var(--vp-c-text-2);
696
- font-size: 0.75rem;
697
- padding: 0.2rem 0.5rem;
698
- border-radius: 4px;
699
- font-family: var(--vp-font-family-mono);
700
- flex-shrink: 0;
701
- margin-right: 0.8rem;
702
- }
703
-
704
- .article-title {
705
- font-size: 0.9rem;
706
- overflow: hidden;
707
- text-overflow: ellipsis;
708
- white-space: nowrap;
709
- }
710
- }
711
-
712
- .vscode-article {
713
- background: none;
714
- border: none;
715
- padding: 0.3rem;
716
- cursor: pointer;
717
- border-radius: 4px;
718
- flex-shrink: 0;
719
- opacity: 0.5;
720
- transition: opacity 0.3s ease;
721
-
722
- &:hover {
723
- opacity: 1;
724
- background-color: var(--vp-c-bg-soft-down);
725
- }
726
-
727
- img {
728
- display: block;
729
- height: 1rem;
730
- width: 1rem;
731
- }
732
- }
733
- }
734
- }
735
- }
736
- }
737
-
738
- /* 响应式调整 */
739
- @media (max-width: 768px) {
740
- .folder-content {
741
- padding-left: 1rem !important;
742
- padding-right: 1rem !important;
743
- }
744
- }
745
- }
746
-
747
- /* 搜索视图 */
748
- .search-view {
749
- .search-box {
750
- margin-bottom: 0.75rem;
751
-
752
- .search-input {
753
- width: 100%;
754
- padding: 0.5rem 0.75rem;
755
- border: 1px solid var(--vp-c-divider);
756
- border-radius: 6px;
757
- background-color: var(--vp-c-bg);
758
- color: var(--vp-c-text-1);
759
- font-size: 0.85rem;
760
- outline: none;
761
- transition: border-color 0.3s;
762
- box-sizing: border-box;
763
-
764
- &::placeholder {
765
- color: var(--vp-c-text-3);
766
- }
767
-
768
- &:focus {
769
- border-color: var(--vp-c-brand-1);
770
- }
771
- }
772
- }
773
-
774
- width: 100%;
775
- padding: 1rem 0;
776
-
777
- .search-summary {
778
- font-size: 0.85rem;
779
- color: var(--vp-c-text-2);
780
- margin-bottom: 0.75rem;
781
- }
782
-
783
- .folder-tree {
784
- border-radius: 8px;
785
- padding: 0.5rem 1rem;
786
- border: 1px solid var(--vp-c-divider);
787
- }
788
-
789
- .folder-article {
790
- display: flex;
791
- align-items: center;
792
- justify-content: space-between;
793
- padding: 0.6rem 0.8rem;
794
- margin: 0.4rem 0;
795
- border-radius: 6px;
796
- transition: background-color 0.3s;
797
-
798
- &:hover {
799
- background-color: var(--vp-c-bg-soft);
800
- }
801
-
802
- .article-info {
803
- display: flex;
804
- align-items: center;
805
- flex: 1;
806
- cursor: pointer;
807
- overflow: hidden;
808
-
809
- .article-status {
810
- margin-right: 0.8rem;
811
- font-size: 0.9rem;
812
- width: 1.2rem;
813
- text-align: center;
814
- flex-shrink: 0;
815
-
816
- &.status-done {
817
- color: var(--vp-c-green-1);
818
- }
819
-
820
- &.status-pending {
821
- color: var(--vp-c-yellow-1);
822
- }
823
- }
824
-
825
- .article-title {
826
- font-size: 0.9rem;
827
- overflow: hidden;
828
- text-overflow: ellipsis;
829
- white-space: nowrap;
830
- }
831
- }
832
-
833
- .vscode-article {
834
- background: none;
835
- border: none;
836
- padding: 0.3rem;
837
- cursor: pointer;
838
- border-radius: 4px;
839
- flex-shrink: 0;
840
- opacity: 0.5;
841
- transition: opacity 0.3s ease;
842
-
843
- &:hover {
844
- opacity: 1;
845
- background-color: var(--vp-c-bg-soft-down);
846
- }
847
-
848
- img {
849
- display: block;
850
- height: 1rem;
851
- width: 1rem;
852
- }
853
- }
854
- }
855
- }
856
-
857
- /* 控制栏样式 */
858
- .control-bar {
859
- display: flex;
860
- justify-content: space-between;
861
- align-items: center;
862
- margin-bottom: 1.5rem;
863
- padding-bottom: 0.5rem;
864
- border-bottom: 1px solid var(--vp-c-divider);
865
- }
866
-
867
- .view-toggle {
868
- display: flex;
869
-
870
- button {
871
- background: none;
872
- border: none;
873
- padding: 0.5rem;
874
- cursor: pointer;
875
- position: relative;
876
- border-radius: 4px;
877
- transition: background-color 0.3s ease;
878
- font-size: 0.85rem;
879
- line-height: 1;
880
-
881
- &.active {
882
- background-color: var(--vp-c-bg-soft);
883
-
884
- &::after {
885
- content: '';
886
- position: absolute;
887
- bottom: -0.5rem;
888
- left: 0;
889
- right: 0;
890
- height: 2px;
891
- background-color: var(--vp-c-brand);
892
- }
893
- }
894
-
895
- &:hover:not(.active) {
896
- background-color: var(--vp-c-bg-soft);
897
- }
898
-
899
- img {
900
- display: block;
901
- height: 1rem;
902
- width: 1rem;
903
- opacity: 0.7;
904
- transition: opacity 0.3s ease;
905
-
906
- &:hover {
907
- opacity: 1;
908
- }
909
- }
910
- }
911
- }
912
-
913
- .actions {
914
- display: flex;
915
- gap: 0.5rem;
916
- }
917
-
918
- /* 折叠/展开按钮和GitHub按钮样式 */
919
- .fold-toggle,
920
- .github-link,
921
- .vscode-open,
922
- .number-toggle {
923
- background: none;
924
- border: none;
925
- padding: 0.5rem;
926
- cursor: pointer;
927
- border-radius: 4px;
928
- transition: background-color 0.3s ease;
929
-
930
- &:hover {
931
- background-color: var(--vp-c-bg-soft);
932
- }
933
-
934
- img {
935
- display: block;
936
- height: 1rem;
937
- width: 1rem;
938
- opacity: 0.7;
939
- transition: opacity 0.3s ease;
940
-
941
- &:hover {
942
- opacity: 1;
943
- }
944
- }
945
- }
946
- </style>
1
+ <script setup>
2
+ import { useData } from 'vitepress'
3
+ import { ref, computed, watch } from 'vue'
4
+
5
+ import {
6
+ icon__fold,
7
+ icon__github,
8
+ icon__vscode,
9
+ icon__folder,
10
+ icon__search,
11
+ icon__mindmap,
12
+ icon__number_gray,
13
+ icon__number_purple,
14
+ } from '../../assets/icons'
15
+ import { NOTES_DIR_KEY, REPO_NAME, AUTHOR, ROOT_ITEM } from '../constants.ts'
16
+ import MindMapView from './MindMapView.vue'
17
+ import NotesTrendChart from './NotesTrendChart.vue'
18
+ import { useSettingsDialog } from '../Settings/composables/useSettingsDialog'
19
+ import { data as sidebarConfig } from '../sidebar.data'
20
+ import { formatDate } from '../utils.ts'
21
+
22
+ // #region props
23
+ const props = defineProps({
24
+ pending: {
25
+ type: Boolean,
26
+ default: false,
27
+ },
28
+ done: {
29
+ type: Boolean,
30
+ default: true,
31
+ },
32
+ })
33
+ // #endregion
34
+
35
+ // #region data
36
+ const viewMode = ref('folder')
37
+ const expandedGroupsFolder = ref(new Set())
38
+ const searchQuery = ref('')
39
+ const debouncedQuery = ref('')
40
+ const showAbout = ref(false)
41
+ const showNumber = ref(false)
42
+ let debounceTimer = null
43
+ // #endregion
44
+
45
+ // #region computed
46
+ const searchResults = computed(() => {
47
+ const query = debouncedQuery.value.trim().toLowerCase()
48
+ if (!query) return []
49
+ return articles.filter((article) => {
50
+ const fullTitle = `${article.realNumber}. ${article.text}`.toLowerCase()
51
+ return fullTitle.includes(query)
52
+ })
53
+ })
54
+
55
+ const folderViewData = computed(() => {
56
+ const result = {}
57
+
58
+ // 按完整分组路径构建层级结构(平铺模式)
59
+ articles.forEach((article) => {
60
+ // 将所有层级铺出来,路径信息作为分组名
61
+ // eg. 1. 第一层级/第一个第二层级
62
+ // article 1
63
+ // article 2
64
+ // ……
65
+ // 1. 第一层级/第二个第二层级
66
+ // article 1
67
+ // article 2
68
+ // ……
69
+ // 1. 第一层级/第三个第二层级
70
+ // article 1
71
+ // article 2
72
+ // ……
73
+ // const group = article.group
74
+ // if (!result[group]) {
75
+ // result[group] = {
76
+ // name: group,
77
+ // articles: [],
78
+ // fullPath: group,
79
+ // }
80
+ // }
81
+ // result[group].articles.push(article)
82
+
83
+ // 只铺出第一层级
84
+ const firstLevelCategory = article.firstLevelCategory
85
+ if (!result[firstLevelCategory]) {
86
+ result[firstLevelCategory] = {
87
+ name: firstLevelCategory,
88
+ articles: [],
89
+ fullPath: firstLevelCategory,
90
+ }
91
+ }
92
+ result[firstLevelCategory].articles.push(article)
93
+ })
94
+
95
+ return result
96
+ })
97
+ // #endregion
98
+
99
+ const { open: openSettings } = useSettingsDialog()
100
+ const { site } = useData()
101
+ const baseUrl = site.value.base.replace(/\/$/, '')
102
+
103
+ // 使用 VitePress Data Loader 读取的 sidebar 数据
104
+ const sidebarData = computed(() => {
105
+ return sidebarConfig && sidebarConfig['/notes/']
106
+ ? sidebarConfig['/notes/']
107
+ : []
108
+ })
109
+
110
+ const { articles, groups } = extractArticlesWithGroups(
111
+ sidebarData.value,
112
+ props.pending,
113
+ props.done,
114
+ )
115
+
116
+ // 根据笔记数量动态计算防抖时间
117
+ // ≤100: 无需防抖,过滤几乎无开销
118
+ // ≤500: 100ms,轻量防抖避免频繁 DOM 更新
119
+ // ≤2000: 150ms
120
+ // ≤5000: 200ms,3k+ 笔记场景(如 LeetCode 知识库)
121
+ // >5000: 300ms,接近万篇上限时适当增加
122
+ const searchDebounceDelay = (() => {
123
+ const count = articles.length
124
+ if (count <= 100) return 0
125
+ if (count <= 500) return 100
126
+ if (count <= 2000) return 150
127
+ if (count <= 5000) return 200
128
+ return 300
129
+ })()
130
+
131
+ watch(searchQuery, (val) => {
132
+ if (debounceTimer) clearTimeout(debounceTimer)
133
+ if (searchDebounceDelay === 0) {
134
+ debouncedQuery.value = val
135
+ return
136
+ }
137
+ debounceTimer = setTimeout(() => {
138
+ debouncedQuery.value = val
139
+ }, searchDebounceDelay)
140
+ })
141
+
142
+ function extractArticlesWithGroups(sidebar, showPending, showDone) {
143
+ const articles = []
144
+ const groups = {}
145
+
146
+ function traverse(items, groupPath = []) {
147
+ for (let i = 0; i < items.length; i++) {
148
+ const item = items[i]
149
+
150
+ if (item.items && Array.isArray(item.items)) {
151
+ const newGroupPath = [...groupPath, item.text]
152
+ traverse(item.items, newGroupPath)
153
+ } else if (item.text) {
154
+ let shouldInclude = false
155
+ let status = ''
156
+ let cleanText = ''
157
+
158
+ if (item.text.startsWith('✅') && showDone) {
159
+ shouldInclude = true
160
+ status = 'done'
161
+ cleanText = item.text.replace('', '')
162
+ } else if (item.text.startsWith('') && showPending) {
163
+ shouldInclude = true
164
+ status = 'pending'
165
+ cleanText = item.text.replace('', '')
166
+ }
167
+
168
+ if (shouldInclude) {
169
+ const group = groupPath.join(' / ')
170
+ const firstLevelCategory = groupPath[0] || '未分类'
171
+
172
+ const realNumber = extractRealNumberFromLink(item.link)
173
+ const title = extractTitleFromText(cleanText)
174
+ const fullLink = baseUrl ? baseUrl + item.link : item.link
175
+
176
+ // 添加 relativePath 字段
177
+ const article = {
178
+ text: title,
179
+ realNumber,
180
+ link: fullLink,
181
+ relativePath: item.link, // 存储相对路径
182
+ group: group || '未分类',
183
+ status,
184
+ firstLevelCategory,
185
+ }
186
+
187
+ articles.push(article)
188
+
189
+ if (!groups[firstLevelCategory]) {
190
+ groups[firstLevelCategory] = []
191
+ }
192
+ groups[firstLevelCategory].push(article)
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ traverse(sidebar)
199
+ return { articles, groups }
200
+ }
201
+
202
+ // 从 link 中提取真实编号
203
+ function extractRealNumberFromLink(link) {
204
+ if (!link) return '0000'
205
+
206
+ const match = link.match(/\/notes\/(\d{4})/)
207
+ return match ? match[1] : '0000'
208
+ }
209
+
210
+ // 从文本中提取标题(去除开头的编号部分)
211
+ function extractTitleFromText(text) {
212
+ if (!text) return ''
213
+
214
+ return text.replace(/^\d{4}\.?\s/, '')
215
+ }
216
+
217
+ function handleCardClick(link) {
218
+ // open in new tab
219
+ if (link) {
220
+ window.open(link, '_blank')
221
+ }
222
+
223
+ // open in current tab
224
+ // if (link) {
225
+ // window.location.href = link
226
+ // }
227
+ }
228
+
229
+ // 在文件夹视图中切换分组展开状态
230
+ function toggleGroupFolder(groupName) {
231
+ if (expandedGroupsFolder.value.has(groupName)) {
232
+ expandedGroupsFolder.value.delete(groupName)
233
+ } else {
234
+ expandedGroupsFolder.value.add(groupName)
235
+ }
236
+ }
237
+
238
+ // 切换所有折叠状态
239
+ function toggleAllFold() {
240
+ const isAllExpanded =
241
+ expandedGroupsFolder.value.size === Object.keys(folderViewData.value).length
242
+
243
+ if (isAllExpanded) {
244
+ expandedGroupsFolder.value.clear()
245
+ } else {
246
+ Object.keys(folderViewData.value).forEach((folderName) => {
247
+ expandedGroupsFolder.value.add(folderViewData.value[folderName].fullPath)
248
+ })
249
+ }
250
+ }
251
+
252
+ // 打开 GitHub 仓库
253
+ function openGithubRepo() {
254
+ window.open(`https://github.com/${AUTHOR}/${REPO_NAME}`, '_blank')
255
+ }
256
+
257
+ // 打开 VS Code 知识库
258
+ function openVSCodeRepo() {
259
+ const notesDir = localStorage.getItem(NOTES_DIR_KEY)
260
+
261
+ if (!notesDir) {
262
+ const shouldRedirect = confirm(
263
+ '请先配置本地知识库所在位置,点击确定打开设置面板',
264
+ )
265
+ if (shouldRedirect) {
266
+ openSettings()
267
+ }
268
+ return
269
+ }
270
+
271
+ window.open('vscode://file/' + notesDir, '_blank')
272
+ }
273
+
274
+ // 打开 VS Code 中的笔记目录
275
+ function openVSCodeArticle(article) {
276
+ const notesDir = localStorage.getItem(NOTES_DIR_KEY)
277
+
278
+ if (!notesDir) {
279
+ const shouldRedirect = confirm(
280
+ '请先配置本地知识库所在位置,点击确定打开设置面板',
281
+ )
282
+ if (shouldRedirect) {
283
+ openSettings()
284
+ }
285
+ return
286
+ }
287
+
288
+ // 构建笔记目录路径
289
+ const notePath = `${notesDir}/${article.relativePath.replace('/README', '')}`
290
+ // console.log(notePath, encodeURI(notePath))
291
+ window.open('vscode://file/' + encodeURI(notePath), '_blank')
292
+ }
293
+ </script>
294
+
295
+ <template>
296
+ <div>
297
+ <!-- 控制栏区域 -->
298
+ <div class="control-bar">
299
+ <!-- 左侧视图切换按钮 -->
300
+ <div class="view-toggle">
301
+ <button
302
+ :class="{ active: viewMode === 'folder' }"
303
+ @click="viewMode = 'folder'"
304
+ >
305
+ <img :src="icon__folder" alt="文件夹视图" />
306
+ </button>
307
+ <button
308
+ :class="{ active: viewMode === 'search' }"
309
+ @click="viewMode = 'search'"
310
+ >
311
+ <img :src="icon__search" alt="搜索视图" />
312
+ </button>
313
+ <button
314
+ :class="{ active: viewMode === 'mindmap' }"
315
+ @click="viewMode = 'mindmap'"
316
+ >
317
+ <img :src="icon__mindmap" alt="思维导图" />
318
+ </button>
319
+ </div>
320
+ <!-- 右侧控制按钮 -->
321
+ <div class="actions">
322
+ <!-- 编号显示切换按钮 -->
323
+ <button
324
+ class="number-toggle"
325
+ v-show="viewMode === 'folder'"
326
+ @click="showNumber = !showNumber"
327
+ >
328
+ <img
329
+ :src="showNumber ? icon__number_purple : icon__number_gray"
330
+ alt="切换编号显示"
331
+ />
332
+ </button>
333
+ <!-- 折叠/展开按钮 -->
334
+ <button
335
+ class="fold-toggle"
336
+ v-show="viewMode === 'folder'"
337
+ @click="toggleAllFold"
338
+ >
339
+ <img :src="icon__fold" alt="折叠/展开" />
340
+ </button>
341
+
342
+ <!-- GitHub 按钮 -->
343
+ <button class="github-link" @click="openGithubRepo">
344
+ <img :src="icon__github" alt="GitHub仓库" />
345
+ </button>
346
+
347
+ <!-- VS Code 按钮 -->
348
+ <button class="vscode-open" @click="openVSCodeRepo">
349
+ <img :src="icon__vscode" alt="使用 VS Code 打开本地知识库" />
350
+ </button>
351
+
352
+ <!-- 关于按钮 -->
353
+ <button class="about-toggle" @click="showAbout = !showAbout">!</button>
354
+ </div>
355
+ </div>
356
+
357
+ <!-- 关于弹窗 -->
358
+ <div class="about-overlay" v-if="showAbout" @click.self="showAbout = false">
359
+ <div class="about-dialog">
360
+ <div class="about-header">
361
+ <span>关于知识库</span>
362
+ <button class="about-close" @click="showAbout = false">✕</button>
363
+ </div>
364
+ <div class="about-body">
365
+ <div class="about-item">
366
+ <span class="about-label">GitHub</span>
367
+ <a
368
+ class="about-value about-link"
369
+ :href="`https://github.com/${AUTHOR}/${REPO_NAME}`"
370
+ target="_blank"
371
+ >
372
+ {{ AUTHOR }}/{{ REPO_NAME }}
373
+ </a>
374
+ </div>
375
+ <div class="about-item">
376
+ <span class="about-label">📅 创建时间</span>
377
+ <span class="about-value">{{
378
+ formatDate(ROOT_ITEM.created_at)
379
+ }}</span>
380
+ </div>
381
+ <div class="about-item">
382
+ <span class="about-label">♻️ 最近更新</span>
383
+ <span class="about-value">{{
384
+ formatDate(ROOT_ITEM.updated_at)
385
+ }}</span>
386
+ </div>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ <!-- 搜索视图 -->
391
+ <div class="search-view" v-if="viewMode === 'search'">
392
+ <div class="search-box">
393
+ <input
394
+ v-model="searchQuery"
395
+ type="text"
396
+ placeholder="搜索笔记标题…"
397
+ class="search-input"
398
+ />
399
+ </div>
400
+ <div class="search-summary">找到 {{ searchResults.length }} 条结果</div>
401
+ <div class="folder-tree" v-show="searchResults.length > 0">
402
+ <div
403
+ v-for="article in searchResults"
404
+ :key="article.link"
405
+ class="folder-article"
406
+ >
407
+ <div class="article-info" @click="handleCardClick(article.link)">
408
+ <span class="article-status" :class="`status-${article.status}`">
409
+ {{
410
+ article.status === 'done'
411
+ ? ''
412
+ : article.status === 'pending'
413
+ ? ''
414
+ : '📄'
415
+ }}
416
+ </span>
417
+ <span class="article-title">{{
418
+ `${article.realNumber}. ${article.text}`
419
+ }}</span>
420
+ </div>
421
+ <button
422
+ class="vscode-article"
423
+ @click.stop="openVSCodeArticle(article)"
424
+ title="在 VS Code 中打开笔记目录"
425
+ >
426
+ <img :src="icon__vscode" alt="打开笔记目录" />
427
+ </button>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ <!-- 思维导图视图 -->
432
+ <MindMapView v-if="viewMode === 'mindmap'" :sidebarData="sidebarData" />
433
+ <!-- 文件夹视图 -->
434
+ <div class="folder-view" v-if="viewMode === 'folder'">
435
+ <NotesTrendChart
436
+ v-if="ROOT_ITEM.completed_notes_count"
437
+ :completedNotesCount="ROOT_ITEM.completed_notes_count"
438
+ />
439
+ <div class="folder-tree">
440
+ <div
441
+ v-for="(folder, folderName) in folderViewData"
442
+ :key="folderName"
443
+ class="folder-group"
444
+ >
445
+ <!-- 文件夹组 -->
446
+ <div
447
+ class="folder-header"
448
+ @click="toggleGroupFolder(folder.fullPath)"
449
+ >
450
+ <span class="folder-icon">
451
+ {{ expandedGroupsFolder.has(folder.fullPath) ? '📂' : '📁' }}
452
+ </span>
453
+ <span class="folder-name">{{ folderName }}</span>
454
+ <span class="folder-count">{{ folder.articles.length }}</span>
455
+ </div>
456
+
457
+ <!-- 展开的文章 -->
458
+ <div
459
+ class="folder-content"
460
+ v-if="expandedGroupsFolder.has(folder.fullPath)"
461
+ >
462
+ <!-- 当前文件夹中的文章 -->
463
+ <div
464
+ v-for="article in folder.articles"
465
+ :key="article.link"
466
+ class="folder-article"
467
+ >
468
+ <div class="article-info" @click="handleCardClick(article.link)">
469
+ <span
470
+ class="article-status"
471
+ :class="`status-${article.status}`"
472
+ >
473
+ {{
474
+ article.status === 'done'
475
+ ? ''
476
+ : article.status === 'pending'
477
+ ? ''
478
+ : '📄'
479
+ }}
480
+ </span>
481
+ <!-- <span class="article-number">{{ article.realNumber }}</span> -->
482
+ <span class="article-title">{{
483
+ showNumber
484
+ ? `${article.realNumber}. ${article.text}`
485
+ : article.text
486
+ }}</span>
487
+ </div>
488
+
489
+ <button
490
+ class="vscode-article"
491
+ @click.stop="openVSCodeArticle(article)"
492
+ title="在 VS Code 中打开笔记目录"
493
+ >
494
+ <img :src="icon__vscode" alt="打开笔记目录" />
495
+ </button>
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ </div>
502
+ </template>
503
+
504
+ <style scoped lang="scss">
505
+ /* 关于弹窗样式 */
506
+ .about-overlay {
507
+ position: fixed;
508
+ top: 0;
509
+ left: 0;
510
+ right: 0;
511
+ bottom: 0;
512
+ background-color: rgba(0, 0, 0, 0.5);
513
+ display: flex;
514
+ align-items: center;
515
+ justify-content: center;
516
+ z-index: 100;
517
+ }
518
+
519
+ .about-dialog {
520
+ background-color: var(--vp-c-bg);
521
+ border: 1px solid var(--vp-c-divider);
522
+ border-radius: 12px;
523
+ width: 90%;
524
+ max-width: 400px;
525
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
526
+
527
+ .about-header {
528
+ display: flex;
529
+ justify-content: space-between;
530
+ align-items: center;
531
+ padding: 0.75rem 1rem;
532
+ border-bottom: 1px solid var(--vp-c-divider);
533
+ font-weight: 600;
534
+ font-size: 0.95rem;
535
+ }
536
+
537
+ .about-close {
538
+ background: none;
539
+ border: none;
540
+ cursor: pointer;
541
+ color: var(--vp-c-text-2);
542
+ font-size: 1rem;
543
+ padding: 0.25rem 0.5rem;
544
+ border-radius: 4px;
545
+ transition: background-color 0.2s;
546
+
547
+ &:hover {
548
+ background-color: var(--vp-c-bg-soft);
549
+ color: var(--vp-c-text-1);
550
+ }
551
+ }
552
+
553
+ .about-body {
554
+ padding: 0.75rem 1rem;
555
+ }
556
+
557
+ .about-item {
558
+ display: flex;
559
+ justify-content: space-between;
560
+ align-items: center;
561
+ padding: 0.5rem 0;
562
+
563
+ &:not(:last-child) {
564
+ border-bottom: 1px solid var(--vp-c-divider);
565
+ }
566
+ }
567
+
568
+ .about-label {
569
+ color: var(--vp-c-text-2);
570
+ font-size: 0.85rem;
571
+ }
572
+
573
+ .about-value {
574
+ color: var(--vp-c-text-1);
575
+ font-size: 0.85rem;
576
+ }
577
+
578
+ .about-link {
579
+ color: var(--vp-c-brand-1);
580
+ text-decoration: none;
581
+
582
+ &:hover {
583
+ text-decoration: underline;
584
+ }
585
+ }
586
+ }
587
+
588
+ .about-toggle {
589
+ background: none;
590
+ border: none;
591
+ padding: 0.5rem;
592
+ cursor: pointer;
593
+ border-radius: 4px;
594
+ transition: background-color 0.3s ease;
595
+ color: #646cff;
596
+ font-size: 0.85rem;
597
+ line-height: 1;
598
+
599
+ &:hover {
600
+ background-color: var(--vp-c-bg-soft);
601
+ }
602
+ }
603
+
604
+ /* 文件夹视图样式 */
605
+ .folder-view {
606
+ width: 100%;
607
+ padding: 1rem 0;
608
+
609
+ .folder-tree {
610
+ // background-color: var(--vp-c-bg-soft);
611
+ border-radius: 8px;
612
+ padding: 1rem;
613
+
614
+ .folder-group {
615
+ margin-bottom: 1rem;
616
+ border: 1px solid var(--vp-c-divider);
617
+ border-radius: 8px;
618
+ overflow: hidden;
619
+
620
+ .folder-header {
621
+ display: flex;
622
+ align-items: center;
623
+ padding: 0.75rem 1rem;
624
+ // background-color: var(--vp-c-bg-soft);
625
+ cursor: pointer;
626
+ transition: background-color 0.3s;
627
+
628
+ // &:hover {
629
+ // background-color: var(--vp-c-bg-elv);
630
+ // }
631
+
632
+ .folder-icon {
633
+ margin-right: 0.75rem;
634
+ font-size: 1.1rem;
635
+ }
636
+
637
+ .folder-name {
638
+ flex: 1;
639
+ font-weight: 500;
640
+ font-size: 0.95rem;
641
+ }
642
+
643
+ .folder-count {
644
+ background-color: var(--vp-c-default-soft);
645
+ color: var(--vp-c-text-2);
646
+ font-size: 0.8rem;
647
+ padding: 0.2rem 0.5rem;
648
+ border-radius: 4px;
649
+ }
650
+ }
651
+
652
+ .folder-content {
653
+ padding: 0.5rem 1rem;
654
+ background-color: var(--vp-c-bg);
655
+
656
+ .folder-article {
657
+ display: flex;
658
+ align-items: center;
659
+ justify-content: space-between;
660
+ padding: 0.6rem 0.8rem;
661
+ margin: 0.4rem 0;
662
+ border-radius: 6px;
663
+ transition: background-color 0.3s;
664
+
665
+ &:hover {
666
+ background-color: var(--vp-c-bg-soft);
667
+ }
668
+
669
+ .article-info {
670
+ display: flex;
671
+ align-items: center;
672
+ flex: 1;
673
+ cursor: pointer;
674
+ overflow: hidden;
675
+
676
+ .article-status {
677
+ margin-right: 0.8rem;
678
+ font-size: 0.9rem;
679
+ width: 1.2rem;
680
+ text-align: center;
681
+ flex-shrink: 0;
682
+
683
+ &.status-done {
684
+ color: var(--vp-c-green-1);
685
+ }
686
+
687
+ &.status-pending {
688
+ color: var(--vp-c-yellow-1);
689
+ }
690
+ }
691
+
692
+ .article-number {
693
+ background-color: var(--vp-c-default-soft);
694
+ color: var(--vp-c-text-2);
695
+ font-size: 0.75rem;
696
+ padding: 0.2rem 0.5rem;
697
+ border-radius: 4px;
698
+ font-family: var(--vp-font-family-mono);
699
+ flex-shrink: 0;
700
+ margin-right: 0.8rem;
701
+ }
702
+
703
+ .article-title {
704
+ font-size: 0.9rem;
705
+ overflow: hidden;
706
+ text-overflow: ellipsis;
707
+ white-space: nowrap;
708
+ }
709
+ }
710
+
711
+ .vscode-article {
712
+ background: none;
713
+ border: none;
714
+ padding: 0.3rem;
715
+ cursor: pointer;
716
+ border-radius: 4px;
717
+ flex-shrink: 0;
718
+ opacity: 0.5;
719
+ transition: opacity 0.3s ease;
720
+
721
+ &:hover {
722
+ opacity: 1;
723
+ background-color: var(--vp-c-bg-soft-down);
724
+ }
725
+
726
+ img {
727
+ display: block;
728
+ height: 1rem;
729
+ width: 1rem;
730
+ }
731
+ }
732
+ }
733
+ }
734
+ }
735
+ }
736
+
737
+ /* 响应式调整 */
738
+ @media (max-width: 768px) {
739
+ .folder-content {
740
+ padding-left: 1rem !important;
741
+ padding-right: 1rem !important;
742
+ }
743
+ }
744
+ }
745
+
746
+ /* 搜索视图 */
747
+ .search-view {
748
+ .search-box {
749
+ margin-bottom: 0.75rem;
750
+
751
+ .search-input {
752
+ width: 100%;
753
+ padding: 0.5rem 0.75rem;
754
+ border: 1px solid var(--vp-c-divider);
755
+ border-radius: 6px;
756
+ background-color: var(--vp-c-bg);
757
+ color: var(--vp-c-text-1);
758
+ font-size: 0.85rem;
759
+ outline: none;
760
+ transition: border-color 0.3s;
761
+ box-sizing: border-box;
762
+
763
+ &::placeholder {
764
+ color: var(--vp-c-text-3);
765
+ }
766
+
767
+ &:focus {
768
+ border-color: var(--vp-c-brand-1);
769
+ }
770
+ }
771
+ }
772
+
773
+ width: 100%;
774
+ padding: 1rem 0;
775
+
776
+ .search-summary {
777
+ font-size: 0.85rem;
778
+ color: var(--vp-c-text-2);
779
+ margin-bottom: 0.75rem;
780
+ }
781
+
782
+ .folder-tree {
783
+ border-radius: 8px;
784
+ padding: 0.5rem 1rem;
785
+ border: 1px solid var(--vp-c-divider);
786
+ }
787
+
788
+ .folder-article {
789
+ display: flex;
790
+ align-items: center;
791
+ justify-content: space-between;
792
+ padding: 0.6rem 0.8rem;
793
+ margin: 0.4rem 0;
794
+ border-radius: 6px;
795
+ transition: background-color 0.3s;
796
+
797
+ &:hover {
798
+ background-color: var(--vp-c-bg-soft);
799
+ }
800
+
801
+ .article-info {
802
+ display: flex;
803
+ align-items: center;
804
+ flex: 1;
805
+ cursor: pointer;
806
+ overflow: hidden;
807
+
808
+ .article-status {
809
+ margin-right: 0.8rem;
810
+ font-size: 0.9rem;
811
+ width: 1.2rem;
812
+ text-align: center;
813
+ flex-shrink: 0;
814
+
815
+ &.status-done {
816
+ color: var(--vp-c-green-1);
817
+ }
818
+
819
+ &.status-pending {
820
+ color: var(--vp-c-yellow-1);
821
+ }
822
+ }
823
+
824
+ .article-title {
825
+ font-size: 0.9rem;
826
+ overflow: hidden;
827
+ text-overflow: ellipsis;
828
+ white-space: nowrap;
829
+ }
830
+ }
831
+
832
+ .vscode-article {
833
+ background: none;
834
+ border: none;
835
+ padding: 0.3rem;
836
+ cursor: pointer;
837
+ border-radius: 4px;
838
+ flex-shrink: 0;
839
+ opacity: 0.5;
840
+ transition: opacity 0.3s ease;
841
+
842
+ &:hover {
843
+ opacity: 1;
844
+ background-color: var(--vp-c-bg-soft-down);
845
+ }
846
+
847
+ img {
848
+ display: block;
849
+ height: 1rem;
850
+ width: 1rem;
851
+ }
852
+ }
853
+ }
854
+ }
855
+
856
+ /* 控制栏样式 */
857
+ .control-bar {
858
+ display: flex;
859
+ justify-content: space-between;
860
+ align-items: center;
861
+ margin-bottom: 1.5rem;
862
+ padding-bottom: 0.5rem;
863
+ border-bottom: 1px solid var(--vp-c-divider);
864
+ }
865
+
866
+ .view-toggle {
867
+ display: flex;
868
+
869
+ button {
870
+ background: none;
871
+ border: none;
872
+ padding: 0.5rem;
873
+ cursor: pointer;
874
+ position: relative;
875
+ border-radius: 4px;
876
+ transition: background-color 0.3s ease;
877
+ font-size: 0.85rem;
878
+ line-height: 1;
879
+
880
+ &.active {
881
+ background-color: var(--vp-c-bg-soft);
882
+
883
+ &::after {
884
+ content: '';
885
+ position: absolute;
886
+ bottom: -0.5rem;
887
+ left: 0;
888
+ right: 0;
889
+ height: 2px;
890
+ background-color: var(--vp-c-brand);
891
+ }
892
+ }
893
+
894
+ &:hover:not(.active) {
895
+ background-color: var(--vp-c-bg-soft);
896
+ }
897
+
898
+ img {
899
+ display: block;
900
+ height: 1rem;
901
+ width: 1rem;
902
+ opacity: 0.7;
903
+ transition: opacity 0.3s ease;
904
+
905
+ &:hover {
906
+ opacity: 1;
907
+ }
908
+ }
909
+ }
910
+ }
911
+
912
+ .actions {
913
+ display: flex;
914
+ gap: 0.5rem;
915
+ }
916
+
917
+ /* 折叠/展开按钮和GitHub按钮样式 */
918
+ .fold-toggle,
919
+ .github-link,
920
+ .vscode-open,
921
+ .number-toggle {
922
+ background: none;
923
+ border: none;
924
+ padding: 0.5rem;
925
+ cursor: pointer;
926
+ border-radius: 4px;
927
+ transition: background-color 0.3s ease;
928
+
929
+ &:hover {
930
+ background-color: var(--vp-c-bg-soft);
931
+ }
932
+
933
+ img {
934
+ display: block;
935
+ height: 1rem;
936
+ width: 1rem;
937
+ opacity: 0.7;
938
+ transition: opacity 0.3s ease;
939
+
940
+ &:hover {
941
+ opacity: 1;
942
+ }
943
+ }
944
+ }
945
+ </style>