@tnotesjs/core 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +87 -87
  2. package/package.json +1 -1
  3. package/types/config.ts +60 -60
  4. package/types/index.ts +11 -11
  5. package/types/note.ts +33 -33
  6. package/vitepress/assets/icons/icon__check.svg +3 -3
  7. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  8. package/vitepress/assets/icons/icon__close.svg +1 -1
  9. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  10. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  11. package/vitepress/assets/icons/icon__copy.svg +4 -4
  12. package/vitepress/assets/icons/icon__focus.svg +1 -1
  13. package/vitepress/assets/icons/icon__fold.svg +3 -3
  14. package/vitepress/assets/icons/icon__folder.svg +1 -1
  15. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  16. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  17. package/vitepress/assets/icons/icon__github.svg +3 -3
  18. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  19. package/vitepress/assets/icons/icon__next.svg +1 -1
  20. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  21. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  22. package/vitepress/assets/icons/icon__prev.svg +1 -1
  23. package/vitepress/assets/icons/icon__restore.svg +1 -1
  24. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  25. package/vitepress/assets/icons/icon__search.svg +1 -1
  26. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  27. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  28. package/vitepress/assets/icons/icon__totop.svg +5 -5
  29. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  30. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  31. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  32. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  33. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  34. package/vitepress/assets/icons/index.ts +39 -39
  35. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  36. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  37. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -209
  38. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  39. package/vitepress/components/Discussions/Discussions.vue +242 -242
  40. package/vitepress/components/EnWordList/EnWordList.vue +664 -664
  41. package/vitepress/components/EnWordList/RightClickMenu.vue +87 -87
  42. package/vitepress/components/Footprints/Footprints.vue +472 -472
  43. package/vitepress/components/Layout/AboutModal.vue +329 -329
  44. package/vitepress/components/Layout/AboutPanel.vue +485 -485
  45. package/vitepress/components/Layout/ContentCollapse.vue +591 -591
  46. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  47. package/vitepress/components/Layout/DocBeforeControls.vue +145 -145
  48. package/vitepress/components/Layout/DocFooter.vue +233 -233
  49. package/vitepress/components/Layout/ImagePreview.vue +485 -485
  50. package/vitepress/components/Layout/Layout.vue +744 -744
  51. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  52. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  53. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -170
  54. package/vitepress/components/Layout/SidebarResizeHandle.vue +368 -311
  55. package/vitepress/components/Layout/Swiper.vue +167 -167
  56. package/vitepress/components/Layout/ToggleFullContent.vue +45 -45
  57. package/vitepress/components/Layout/ToggleSidebar.vue +35 -35
  58. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  59. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  60. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  61. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  62. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  63. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  64. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  65. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  66. package/vitepress/components/MarkMap/MarkMap.vue +562 -562
  67. package/vitepress/components/Mermaid/Mermaid.vue +644 -644
  68. package/vitepress/components/NotesTable/NotesTable.vue +175 -175
  69. package/vitepress/components/NotesTable/README.md +67 -67
  70. package/vitepress/components/Settings/Settings.vue +711 -711
  71. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  72. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  73. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -945
  74. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  75. package/vitepress/components/constants.ts +109 -109
  76. package/vitepress/components/notesConfig.data.ts +73 -73
  77. package/vitepress/components/sidebar.data.ts +59 -59
  78. package/vitepress/components/tnotes-config.data.ts +21 -21
  79. package/vitepress/components/utils.ts +26 -26
  80. package/vitepress/config/index.ts +169 -169
  81. package/vitepress/configs/constants.ts +26 -26
  82. package/vitepress/configs/head.config.ts +25 -25
  83. package/vitepress/configs/index.ts +9 -9
  84. package/vitepress/configs/markdown-it.d.ts +23 -23
  85. package/vitepress/configs/markdown.config.ts +368 -368
  86. package/vitepress/configs/theme.config.ts +108 -108
  87. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  88. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  89. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  90. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  91. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  92. package/vitepress/theme/index.ts +137 -137
  93. package/vitepress/theme/styles/base.scss +66 -64
  94. package/vitepress/theme/styles/components/404.scss +31 -31
  95. package/vitepress/theme/styles/components/collapse.scss +172 -172
  96. package/vitepress/theme/styles/components/markmap.scss +101 -101
  97. package/vitepress/theme/styles/components/swiper.scss +255 -255
  98. package/vitepress/theme/styles/index.scss +25 -25
  99. package/vitepress/theme/styles/layout.scss +79 -78
  100. package/vitepress/theme/styles/utilities.scss +39 -39
  101. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,945 +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 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>
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>