@tnotesjs/core 0.1.15 → 0.1.16

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 (115) hide show
  1. package/README.md +87 -87
  2. package/dist/{chunk-FVKQPBPP.js → chunk-4MO7F7Q4.js} +2005 -1769
  3. package/dist/cli/index.js +1 -1
  4. package/dist/vitepress/config/index.js +212 -4
  5. package/package.json +1 -1
  6. package/types/config.ts +61 -61
  7. package/types/index.ts +11 -11
  8. package/types/note.ts +33 -33
  9. package/vitepress/assets/icons/icon__check.svg +3 -3
  10. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  11. package/vitepress/assets/icons/icon__close.svg +1 -1
  12. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  13. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  14. package/vitepress/assets/icons/icon__copy.svg +4 -4
  15. package/vitepress/assets/icons/icon__focus.svg +1 -1
  16. package/vitepress/assets/icons/icon__fold.svg +3 -3
  17. package/vitepress/assets/icons/icon__folder.svg +1 -1
  18. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  19. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  20. package/vitepress/assets/icons/icon__github.svg +3 -3
  21. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  22. package/vitepress/assets/icons/icon__next.svg +1 -1
  23. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  24. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  25. package/vitepress/assets/icons/icon__prev.svg +1 -1
  26. package/vitepress/assets/icons/icon__restore.svg +1 -1
  27. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  28. package/vitepress/assets/icons/icon__search.svg +1 -1
  29. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  30. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  31. package/vitepress/assets/icons/icon__totop.svg +5 -5
  32. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  33. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  34. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  35. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  37. package/vitepress/assets/icons/index.ts +39 -39
  38. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  39. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -373
  40. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -115
  41. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
  42. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  43. package/vitepress/components/Discussions/Discussions.vue +211 -211
  44. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  45. package/vitepress/components/EnWordList/EnWordList.vue +543 -543
  46. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  47. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  48. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  49. package/vitepress/components/Footprints/Footprints.vue +377 -377
  50. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  51. package/vitepress/components/Layout/AboutModal.vue +105 -105
  52. package/vitepress/components/Layout/AboutPanel.vue +270 -270
  53. package/vitepress/components/Layout/ContentCollapse.vue +603 -603
  54. package/vitepress/components/Layout/CustomSidebar.vue +878 -608
  55. package/vitepress/components/Layout/DocBeforeControls.vue +149 -149
  56. package/vitepress/components/Layout/DocFooter.vue +233 -233
  57. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  58. package/vitepress/components/Layout/ImagePreview.vue +281 -281
  59. package/vitepress/components/Layout/Layout.module.scss +661 -661
  60. package/vitepress/components/Layout/Layout.vue +618 -559
  61. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  62. package/vitepress/components/Layout/SidebarItems.vue +1083 -268
  63. package/vitepress/components/Layout/SidebarNavBefore.vue +171 -108
  64. package/vitepress/components/Layout/Swiper.vue +167 -167
  65. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  66. package/vitepress/components/Layout/ToggleFullContent.vue +35 -35
  67. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  68. package/vitepress/components/Layout/ToggleSidebar.vue +36 -36
  69. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  70. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  71. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  72. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  73. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  74. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  75. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  76. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  77. package/vitepress/components/MarkMap/MarkMap.vue +405 -405
  78. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  79. package/vitepress/components/Mermaid/Mermaid.vue +365 -365
  80. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  81. package/vitepress/components/NotesTable/NotesTable.vue +98 -98
  82. package/vitepress/components/NotesTable/README.md +67 -67
  83. package/vitepress/components/Settings/Settings.module.scss +375 -433
  84. package/vitepress/components/Settings/Settings.vue +376 -306
  85. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  86. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  87. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -946
  88. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  89. package/vitepress/components/constants.ts +109 -91
  90. package/vitepress/components/notesConfig.data.ts +73 -73
  91. package/vitepress/components/sidebar.data.ts +59 -59
  92. package/vitepress/components/tnotes-config.data.ts +21 -21
  93. package/vitepress/components/utils.ts +26 -26
  94. package/vitepress/config/index.ts +169 -167
  95. package/vitepress/configs/constants.ts +26 -26
  96. package/vitepress/configs/head.config.ts +25 -25
  97. package/vitepress/configs/index.ts +9 -9
  98. package/vitepress/configs/markdown-it.d.ts +23 -23
  99. package/vitepress/configs/markdown.config.ts +366 -366
  100. package/vitepress/configs/theme.config.ts +108 -108
  101. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  102. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  103. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  104. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -0
  105. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  106. package/vitepress/theme/index.ts +137 -137
  107. package/vitepress/theme/styles/base.scss +50 -50
  108. package/vitepress/theme/styles/components/404.scss +31 -31
  109. package/vitepress/theme/styles/components/collapse.scss +175 -175
  110. package/vitepress/theme/styles/components/markmap.scss +101 -101
  111. package/vitepress/theme/styles/components/swiper.scss +255 -255
  112. package/vitepress/theme/styles/index.scss +25 -25
  113. package/vitepress/theme/styles/layout.scss +70 -62
  114. package/vitepress/theme/styles/utilities.scss +39 -39
  115. 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>