@tnotesjs/core 0.1.19 → 0.1.21

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