@tnotesjs/core 0.1.22 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +87 -87
  2. package/package.json +1 -1
  3. package/types/config.ts +60 -60
  4. package/types/index.ts +11 -11
  5. package/types/note.ts +33 -33
  6. package/vitepress/assets/icons/icon__check.svg +3 -3
  7. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  8. package/vitepress/assets/icons/icon__close.svg +1 -1
  9. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  10. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  11. package/vitepress/assets/icons/icon__copy.svg +4 -4
  12. package/vitepress/assets/icons/icon__focus.svg +1 -1
  13. package/vitepress/assets/icons/icon__fold.svg +3 -3
  14. package/vitepress/assets/icons/icon__folder.svg +1 -1
  15. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  16. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  17. package/vitepress/assets/icons/icon__github.svg +3 -3
  18. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  19. package/vitepress/assets/icons/icon__next.svg +1 -1
  20. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  21. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  22. package/vitepress/assets/icons/icon__prev.svg +1 -1
  23. package/vitepress/assets/icons/icon__restore.svg +1 -1
  24. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  25. package/vitepress/assets/icons/icon__search.svg +1 -1
  26. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  27. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  28. package/vitepress/assets/icons/icon__totop.svg +5 -5
  29. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  30. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  31. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  32. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  33. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  34. package/vitepress/assets/icons/index.ts +39 -39
  35. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  36. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  37. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -209
  38. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  39. package/vitepress/components/Discussions/Discussions.vue +242 -242
  40. package/vitepress/components/EnWordList/EnWordList.vue +664 -664
  41. package/vitepress/components/EnWordList/RightClickMenu.vue +87 -87
  42. package/vitepress/components/Footprints/Footprints.vue +472 -472
  43. package/vitepress/components/Layout/AboutModal.vue +329 -329
  44. package/vitepress/components/Layout/AboutPanel.vue +485 -485
  45. package/vitepress/components/Layout/ContentCollapse.vue +591 -591
  46. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  47. package/vitepress/components/Layout/DocBeforeControls.vue +145 -145
  48. package/vitepress/components/Layout/DocFooter.vue +233 -233
  49. package/vitepress/components/Layout/ImagePreview.vue +485 -485
  50. package/vitepress/components/Layout/Layout.vue +744 -744
  51. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  52. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  53. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -170
  54. package/vitepress/components/Layout/SidebarResizeHandle.vue +311 -311
  55. package/vitepress/components/Layout/Swiper.vue +167 -167
  56. package/vitepress/components/Layout/ToggleFullContent.vue +45 -45
  57. package/vitepress/components/Layout/ToggleSidebar.vue +35 -35
  58. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  59. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  60. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  61. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  62. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  63. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  64. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  65. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  66. package/vitepress/components/MarkMap/MarkMap.vue +562 -562
  67. package/vitepress/components/Mermaid/Mermaid.vue +644 -644
  68. package/vitepress/components/NotesTable/NotesTable.vue +175 -175
  69. package/vitepress/components/NotesTable/README.md +67 -67
  70. package/vitepress/components/Settings/Settings.vue +711 -711
  71. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  72. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  73. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -945
  74. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  75. package/vitepress/components/constants.ts +109 -109
  76. package/vitepress/components/notesConfig.data.ts +73 -73
  77. package/vitepress/components/sidebar.data.ts +59 -59
  78. package/vitepress/components/tnotes-config.data.ts +21 -21
  79. package/vitepress/components/utils.ts +26 -26
  80. package/vitepress/config/index.ts +169 -169
  81. package/vitepress/configs/constants.ts +26 -26
  82. package/vitepress/configs/head.config.ts +25 -25
  83. package/vitepress/configs/index.ts +9 -9
  84. package/vitepress/configs/markdown-it.d.ts +23 -23
  85. package/vitepress/configs/markdown.config.ts +368 -368
  86. package/vitepress/configs/theme.config.ts +108 -108
  87. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  88. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  89. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  90. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  91. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  92. package/vitepress/theme/index.ts +137 -137
  93. package/vitepress/theme/styles/base.scss +66 -64
  94. package/vitepress/theme/styles/components/404.scss +31 -31
  95. package/vitepress/theme/styles/components/collapse.scss +172 -172
  96. package/vitepress/theme/styles/components/markmap.scss +101 -101
  97. package/vitepress/theme/styles/components/swiper.scss +255 -255
  98. package/vitepress/theme/styles/index.scss +25 -25
  99. package/vitepress/theme/styles/layout.scss +78 -78
  100. package/vitepress/theme/styles/utilities.scss +39 -39
  101. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,744 +1,744 @@
1
- <template>
2
- <Layout>
3
- <template v-if="showNotFound" #not-found>
4
- <div class="not-found-container">
5
- <h1>404</h1>
6
- <p>Page not found</p>
7
- <!-- <div class="debug-info">
8
- <p>Current path: {{ decodedCurrentPath }}</p>
9
- <p>Matched ID: {{ matchedId }}</p>
10
- <p>Redirect path: {{ redirectPath }}</p>
11
- </div> -->layout
12
- </div>
13
- </template>
14
- <template #doc-top>
15
- <!-- <pre>vscodesNoteDir: {{ vscodeNotesDir }}</pre> -->
16
- <!-- <pre>vpData.page.value: {{ vpData.page.value }}</pre>
17
- <pre>currentNoteConfig: {{ currentNoteConfig }}</pre> -->
18
- <!-- <button @click="copyRawFile" title="Copy raw file">raw</button> -->
19
- <!-- <pre>{{ tocData }}</pre> -->
20
- <ImagePreview />
21
- <Swiper />
22
- <ContentCollapse />
23
- </template>
24
- <!-- <template #doc-bottom>doc-bottom</template> -->
25
- <template #doc-before>
26
- <DocBeforeControls
27
- :is-full-content-mode="isFullContentMode"
28
- :vscode-notes-dir="vscodeNotesDir"
29
- :is-home-readme="isHomeReadme"
30
- :current-note-id="currentNoteId"
31
- :created_at="created_at"
32
- :updated_at="updated_at"
33
- :home-readme-created-at="homeReadmeCreatedAt"
34
- :home-readme-updated-at="homeReadmeUpdatedAt"
35
- :time-modal-open="timeModalOpen"
36
- :all-collapsed="allCollapsed"
37
- @open-time-modal="openTimeModal"
38
- @toggle-all-collapse="toggleAllCollapse"
39
- />
40
- <!-- 笔记状态标题 -->
41
- <NoteStatus
42
- :note-config="currentNoteConfig"
43
- :is-notes-page="isNotesPage"
44
- />
45
-
46
- <AboutModal
47
- v-model="timeModalOpen"
48
- @close="onTimeModalClose"
49
- :title="modalTitle"
50
- >
51
- <template #title>
52
- {{ modalTitle }}
53
- </template>
54
-
55
- <AboutPanel
56
- :is-home-readme="modalIsHomeReadme"
57
- :current-note-id="modalNoteId"
58
- :is-dev="isDev"
59
- v-model:editable-note-title="editableNoteTitle"
60
- v-model:editable-description="editableDescription"
61
- v-model:editable-note-status="editableNoteStatus"
62
- v-model:editable-discussions-enabled="editableDiscussionsEnabled"
63
- v-model:title-error="titleError"
64
- :modal-created-at="modalCreatedAt"
65
- :modal-updated-at="modalUpdatedAt"
66
- :modal-github-url="modalGithubUrl"
67
- :modal-github-page-url="modalGithubPageUrl"
68
- :completion-percentage="completionPercentage"
69
- :done-notes-len="doneNotesLen"
70
- :total-notes-len="totalNotesLen"
71
- @title-input="onTitleInput"
72
- @title-blur="onTitleBlur"
73
- @description-input="onDescriptionInput"
74
- @config-change="onConfigChange"
75
- />
76
-
77
- <!-- 操作按钮(仅开发环境且非首页显示) -->
78
- <template #footer v-if="isDev && !modalIsHomeReadme">
79
- <div class="actionBar">
80
- <button
81
- class="saveButton"
82
- :class="{ disabled: !hasConfigChanges }"
83
- @click="saveNoteConfig"
84
- :disabled="!hasConfigChanges || isSaving"
85
- type="button"
86
- >
87
- {{ saveButtonText }}
88
- </button>
89
- <button
90
- v-if="hasConfigChanges"
91
- @click="resetNoteConfig"
92
- class="resetButton"
93
- type="button"
94
- >
95
- 重置
96
- </button>
97
- </div>
98
-
99
- <!-- 保存进度提示 -->
100
- <Transition name="toast">
101
- <div v-if="isSaving && savingMessage" class="loadingToast">
102
- <div class="loadingSpinner"></div>
103
- <span>{{ savingMessage }}</span>
104
- </div>
105
- </Transition>
106
-
107
- <!-- 保存成功提示 -->
108
- <Transition name="toast">
109
- <div v-if="showSuccessToast" class="toast">✓ 保存成功</div>
110
- </Transition>
111
- </template>
112
- </AboutModal>
113
- </template>
114
- <template #doc-footer-before>
115
- <!-- <div class="footer-time-info">
116
- <p title="首次提交时间">首次提交时间:{{ formatDate(created_at) }}</p>
117
- <p title="最近提交时间">最近提交时间:{{ formatDate(updated_at) }}</p>
118
- </div> -->
119
- </template>
120
- <template #doc-after>
121
- <!-- 自定义 DocFooter -->
122
- <DocFooter />
123
- <!-- {{ REPO_NAME + '.' + currentNoteId }} -->
124
- <Discussions
125
- v-if="isDiscussionsVisible"
126
- :id="currentNoteConfig.id"
127
- :note-number="currentNoteId || ''"
128
- :note-title="currentNoteTitle"
129
- />
130
- </template>
131
- <!-- <template #doc-bottom>
132
- <Discussions id="TNotes.template.0003" />
133
- </template> -->
134
-
135
- <template #aside-top>
136
- <!-- aside-top -->
137
- <!-- {{ vpData.page.value.title }} -->
138
- </template>
139
- <!-- <template #aside-outline-before>
140
- <span
141
- @click="scrollToTop"
142
- style="cursor: pointer; height: 1em; width: 1em"
143
- title="回到顶部"
144
- >
145
- <img :src="icon__totop" alt="to top" />
146
- </span>
147
- </template> -->
148
-
149
- <!-- 使用 sidebar-nav-before 插槽插入控制按钮 -->
150
- <template #sidebar-nav-before>
151
- <SidebarNavBefore
152
- :is-expanded="allSidebarExpanded"
153
- :show-note-id="showNoteId"
154
- @toggle-expand="toggleSidebarSections"
155
- @toggle-note-id="toggleNoteId"
156
- @focus-current="focusCurrentNote"
157
- />
158
- </template>
159
-
160
- <!-- 使用 sidebar-nav-after 插槽插入自定义 Sidebar -->
161
- <template #sidebar-nav-after>
162
- <CustomSidebar
163
- ref="customSidebarRef"
164
- @open-note-about="openSidebarNoteAbout"
165
- />
166
- </template>
167
- <!-- <template #sidebar-nav-after>sidebar-nav-after</template> -->
168
-
169
- <!-- <template #aside-outline-after>aside-outline-after</template> -->
170
- <!-- <template #aside-bottom>aside-bottom</template> -->
171
- <!-- <template #aside-ads-before>aside-ads-before</template> -->
172
- <!-- <template #aside-ads-after>aside-ads-after</template> -->
173
- <!-- <template #layout-top>layout-top</template> -->
174
- <!-- <template #layout-bottom>layout-bottom</template> -->
175
- <!-- <template #nav-bar-title-before>nav-bar-title-before</template> -->
176
- <!-- <template #nav-bar-title-after>nav-bar-title-after</template> -->
177
- <!-- <template #nav-bar-content-before>nav-bar-content-before</template> -->
178
- <!-- 设置入口:固定在 VPNav 右侧(social-links / appearance 之后,extra 之前) -->
179
- <template #nav-bar-content-after>
180
- <NavBarSettingsTrigger />
181
- </template>
182
-
183
- <!-- !NOTE 不清楚下面的插槽所对应的位置 -->
184
- <!-- <template #nav-screen-content-before>nav-screen-content-before</template> -->
185
- <!-- <template #nav-screen-content-after>nav-screen-content-after</template> -->
186
- </Layout>
187
-
188
- <SidebarResizeHandle />
189
-
190
- <!-- 全局重命名遮罩:关于面板保存或文件系统重命名时由 useRenameOverlay 控制 -->
191
- <LoadingPage
192
- :visible="renameOverlayState.visible"
193
- :message="renameOverlayState.message"
194
- :tip="renameOverlayState.tip"
195
- />
196
-
197
- <!-- 全局设置 Dialog:由 useSettingsDialog 控制显隐 -->
198
- <SettingsDialog />
199
- </template>
200
-
201
- <script setup>
202
- import { useData, useRoute, useRouter } from "vitepress";
203
- import DefaultTheme from "vitepress/theme";
204
- import { computed, onMounted, ref, watch } from "vue";
205
-
206
- import { useCodeBlockFullscreen } from "../CodeBlockFullscreen";
207
- import { SIDEBAR_SHOW_NOTE_ID_KEY } from "../constants";
208
- import { data as allNotesConfig } from "../notesConfig.data.ts";
209
- import AboutModal from "./AboutModal.vue";
210
- import AboutPanel from "./AboutPanel.vue";
211
- import Discussions from "../Discussions/Discussions.vue";
212
- import LoadingPage from "../LoadingPage/LoadingPage.vue";
213
- import { useCollapseControl } from "./composables/useCollapseControl";
214
- import { useNoteConfig } from "./composables/useNoteConfig";
215
- import { useNoteSave } from "./composables/useNoteSave";
216
- import { useNoteValidation } from "./composables/useNoteValidation";
217
- import { useRedirect } from "./composables/useRedirect";
218
- import { useRenameOverlay } from "./composables/useRenameOverlay";
219
- import { useVSCodeIntegration } from "./composables/useVSCodeIntegration";
220
- import ContentCollapse from "./ContentCollapse.vue";
221
- import CustomSidebar from "./CustomSidebar.vue";
222
- import DocBeforeControls from "./DocBeforeControls.vue";
223
- import DocFooter from "./DocFooter.vue";
224
- import { data as readmeData } from "./homeReadme.data.ts";
225
- import ImagePreview from "./ImagePreview.vue";
226
- import NavBarSettingsTrigger from "./NavBarSettingsTrigger.vue";
227
- import NoteStatus from "./NoteStatus.vue";
228
- import SidebarNavBefore from "./SidebarNavBefore.vue";
229
- import SidebarResizeHandle from "./SidebarResizeHandle.vue";
230
- import Swiper from "./Swiper.vue";
231
- import SettingsDialog from "../Settings/SettingsDialog.vue";
232
-
233
- const { Layout } = DefaultTheme;
234
- const vpData = useData();
235
- const router = useRouter();
236
- const route = useRoute();
237
-
238
- // 启用代码块全屏功能
239
- useCodeBlockFullscreen();
240
-
241
- // 全局重命名遮罩状态(由 useRenameRedirect 控制 show/hide)
242
- const { state: renameOverlayState } = useRenameOverlay();
243
-
244
- // 自定义侧边栏引用
245
- const customSidebarRef = ref(null);
246
- const showNoteId = ref(false);
247
-
248
- // 计算是否有展开的一级章节
249
- const allSidebarExpanded = computed(() => {
250
- if (!customSidebarRef.value) return false;
251
- return customSidebarRef.value.hasAnyFirstLevelExpanded();
252
- });
253
-
254
- // 初始化笔记编号显示状态
255
- if (typeof window !== "undefined") {
256
- const savedShowNoteId = localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY);
257
- showNoteId.value = savedShowNoteId === "true";
258
- }
259
-
260
- // 切换侧边栏展开/折叠状态(智能切换)
261
- function toggleSidebarSections() {
262
- if (customSidebarRef.value) {
263
- customSidebarRef.value.toggleExpandCollapse();
264
- }
265
- }
266
-
267
- // 切换笔记编号显示状态
268
- function toggleNoteId() {
269
- showNoteId.value = !showNoteId.value;
270
- if (typeof window !== "undefined") {
271
- localStorage.setItem(SIDEBAR_SHOW_NOTE_ID_KEY, showNoteId.value.toString());
272
- // 刷新页面以应用变化
273
- window.location.reload();
274
- }
275
- }
276
-
277
- // 聚焦到当前笔记
278
- function focusCurrentNote() {
279
- if (customSidebarRef.value) {
280
- customSidebarRef.value.focusCurrentNote();
281
- }
282
- }
283
-
284
- // 提取当前笔记的 ID(前 4 个数字)
285
- const currentNoteId = computed(() => {
286
- const relativePath = vpData.page.value.relativePath;
287
- // relativePath 格式: notes/0001. 标题/README.md
288
- const match = relativePath.match(/notes\/(\d{4})/);
289
- const id = match ? match[1] : null;
290
-
291
- return id;
292
- });
293
-
294
- // 判断是否是笔记页面(notes 目录下)
295
- const isNotesPage = computed(() => {
296
- return vpData.page.value.relativePath.startsWith("notes/");
297
- });
298
-
299
- // 提取当前笔记的标题(从 relativePath)
300
- const currentNoteTitle = computed(() => {
301
- const relativePath = vpData.page.value.relativePath;
302
- // relativePath 格式: notes/0001. 标题/README.md
303
- const match = relativePath.match(/notes\/\d{4}\.\s+([^/]+)\//);
304
- const title = match ? match[1] : "";
305
-
306
- return title;
307
- });
308
-
309
- // 根据当前笔记 ID 获取配置数据
310
- const currentNoteConfig = computed(() => {
311
- return currentNoteId.value && allNotesConfig[currentNoteId.value]
312
- ? allNotesConfig[currentNoteId.value]
313
- : {
314
- bilibili: [],
315
- done: false,
316
- enableDiscussions: false,
317
- };
318
- });
319
-
320
- function getEmptyNoteConfig() {
321
- return {
322
- bilibili: [],
323
- done: false,
324
- enableDiscussions: false,
325
- };
326
- }
327
-
328
- const isDiscussionsVisible = computed(
329
- () => currentNoteConfig.value.enableDiscussions,
330
- );
331
- const updated_at = computed(() => currentNoteConfig.value.updated_at);
332
- const created_at = computed(() => currentNoteConfig.value.created_at);
333
-
334
- // 判断是否为首页 README.md
335
- const isHomeReadme = computed(() => vpData.page.value.filePath === "README.md");
336
- const doneNotesLen = computed(() => readmeData?.doneNotesLen || 0);
337
- const totalNotesLen = computed(() => readmeData?.totalNotesLen || 0);
338
-
339
- // 完成进度百分比
340
- const completionPercentage = computed(() => {
341
- if (!totalNotesLen.value || totalNotesLen.value === 0) return null;
342
- return Math.round((doneNotesLen.value / totalNotesLen.value) * 100);
343
- });
344
-
345
- // 首页 README.md 的时间戳
346
- const homeReadmeCreatedAt = computed(() => readmeData?.created_at);
347
- const homeReadmeUpdatedAt = computed(() => readmeData?.updated_at);
348
-
349
- // #region - Composables
350
- // 404 重定向
351
- const { showNotFound, decodedCurrentPath, initRedirectCheck } =
352
- useRedirect(allNotesConfig);
353
-
354
- // modal 控制
355
- const timeModalOpen = ref(false);
356
- const sidebarAboutNoteId = ref(null);
357
-
358
- const modalIsHomeReadme = computed(
359
- () => !sidebarAboutNoteId.value && isHomeReadme.value,
360
- );
361
-
362
- const modalNoteId = computed(() => sidebarAboutNoteId.value || currentNoteId.value);
363
-
364
- function safeDecode(value) {
365
- try {
366
- return decodeURIComponent(value);
367
- } catch {
368
- return value;
369
- }
370
- }
371
-
372
- function getNoteFolderSegment(noteId) {
373
- if (!noteId) return "";
374
-
375
- const source =
376
- noteId === currentNoteId.value
377
- ? vpData.page.value.relativePath
378
- : allNotesConfig[noteId]?.redirect || "";
379
- const match = source.match(/notes\/([^/]+)\/README/);
380
-
381
- return match ? match[1] : "";
382
- }
383
-
384
- function getNoteTitleById(noteId) {
385
- const folderSegment = getNoteFolderSegment(noteId);
386
- const match = folderSegment.match(/^\d{4}\.\s+(.+)$/);
387
-
388
- return match ? safeDecode(match[1]) : "";
389
- }
390
-
391
- const modalNoteTitle = computed(() => {
392
- if (!sidebarAboutNoteId.value) return currentNoteTitle.value;
393
- return getNoteTitleById(sidebarAboutNoteId.value);
394
- });
395
-
396
- const modalNoteConfig = computed(() => {
397
- return modalNoteId.value && allNotesConfig[modalNoteId.value]
398
- ? allNotesConfig[modalNoteId.value]
399
- : getEmptyNoteConfig();
400
- });
401
-
402
- // 笔记配置管理
403
- const {
404
- editableNoteStatus,
405
- editableDiscussionsEnabled,
406
- editableNoteTitle,
407
- editableDescription,
408
- titleError,
409
- hasConfigChanges,
410
- resetNoteConfig,
411
- updateOriginalValues,
412
- } = useNoteConfig(
413
- modalNoteId,
414
- modalNoteConfig,
415
- modalNoteTitle,
416
- timeModalOpen,
417
- );
418
-
419
- // 标题验证
420
- const { onTitleInput: validateTitleInput, onTitleBlur: validateTitleBlur } =
421
- useNoteValidation();
422
-
423
- // 保存逻辑
424
- const {
425
- isSaving,
426
- showSuccessToast,
427
- savingMessage,
428
- saveButtonText,
429
- saveNoteConfig,
430
- } = useNoteSave(
431
- modalNoteId,
432
- computed(() => {
433
- if (typeof window === "undefined") return false;
434
- return (
435
- window.location.hostname === "localhost" ||
436
- window.location.hostname === "127.0.0.1"
437
- );
438
- }),
439
- hasConfigChanges,
440
- titleError,
441
- editableNoteTitle,
442
- computed(() => modalNoteTitle.value),
443
- editableNoteStatus,
444
- computed(() => modalNoteConfig.value.done || false),
445
- editableDiscussionsEnabled,
446
- computed(() => modalNoteConfig.value.enableDiscussions || false),
447
- editableDescription,
448
- computed(() => modalNoteConfig.value.description || ""),
449
- allNotesConfig,
450
- updateOriginalValues,
451
- );
452
-
453
- // 折叠控制
454
- const { allCollapsed, toggleAllCollapse } = useCollapseControl();
455
-
456
- // VSCode 集成
457
- const { vscodeNotesDir, updateVscodeNoteDir, interceptHomeReadmeLinks } =
458
- useVSCodeIntegration();
459
-
460
- // 判断是否为开发环境
461
- const isDev = computed(() => {
462
- if (typeof window === "undefined") return false;
463
- return (
464
- window.location.hostname === "localhost" ||
465
- window.location.hostname === "127.0.0.1"
466
- );
467
- });
468
-
469
- // #endregion
470
-
471
- // modal 标题
472
- const modalTitle = computed(() => {
473
- return modalIsHomeReadme.value ? "关于这个知识库" : "关于这篇笔记";
474
- });
475
-
476
- // modal 中显示的 GitHub 链接
477
- const modalGithubUrl = computed(() => {
478
- if (modalIsHomeReadme.value) {
479
- const repoName = vpData.site.value.title.toLowerCase();
480
- return `https://github.com/tnotesjs/${repoName}`;
481
- }
482
-
483
- const folderSegment = getNoteFolderSegment(modalNoteId.value);
484
- if (!folderSegment) return "";
485
-
486
- const repoName = vpData.site.value.title.toLowerCase();
487
- return `https://github.com/tnotesjs/${repoName}/tree/main/notes/${encodeURIComponent(folderSegment)}`;
488
- });
489
-
490
- // modal 中显示的 GitHub Page 链接
491
- const modalGithubPageUrl = computed(() => {
492
- if (modalIsHomeReadme.value) {
493
- const repoName = vpData.site.value.title; // 保持原始大小写
494
- return `https://tnotesjs.github.io/${repoName}/`;
495
- }
496
- // 笔记页面的 GitHub Page 链接
497
- const folderSegment = getNoteFolderSegment(modalNoteId.value);
498
- if (modalNoteId.value && folderSegment) {
499
- const repoName = vpData.site.value.title; // 保持原始大小写
500
- return `https://tnotesjs.github.io/${repoName}/notes/${encodeURIComponent(folderSegment)}/README`;
501
- }
502
- return "";
503
- });
504
-
505
- // modal 中显示的创建时间
506
- const modalCreatedAt = computed(() => {
507
- return modalIsHomeReadme.value
508
- ? homeReadmeCreatedAt.value
509
- : modalNoteConfig.value.created_at;
510
- });
511
-
512
- // modal 中显示的更新时间
513
- const modalUpdatedAt = computed(() => {
514
- return modalIsHomeReadme.value
515
- ? homeReadmeUpdatedAt.value
516
- : modalNoteConfig.value.updated_at;
517
- });
518
-
519
- function openTimeModal() {
520
- sidebarAboutNoteId.value = null;
521
- timeModalOpen.value = true;
522
- }
523
-
524
- function onTimeModalClose() {
525
- timeModalOpen.value = false;
526
- sidebarAboutNoteId.value = null;
527
- }
528
-
529
- function openSidebarNoteAbout(noteIndex) {
530
- sidebarAboutNoteId.value = noteIndex;
531
- timeModalOpen.value = true;
532
- }
533
-
534
- // 配置变更时的回调
535
- function onConfigChange() {
536
- // 配置变更时不需要做额外操作,只需要触发 hasConfigChanges 计算
537
- }
538
-
539
- // 标题输入事件
540
- function onTitleInput() {
541
- validateTitleInput(editableNoteTitle, titleError);
542
- }
543
-
544
- // 标题失焦事件
545
- function onTitleBlur() {
546
- validateTitleBlur(editableNoteTitle, titleError);
547
- }
548
-
549
- // 简介输入事件
550
- function onDescriptionInput() {
551
- // 简介没有特殊验证,只需要触发变更检测
552
- }
553
-
554
- // #region - 全屏状态检测
555
- const isFullContentMode = ref(false);
556
-
557
- function checkFullContentMode() {
558
- if (typeof document === "undefined") return;
559
-
560
- const vpApp = document.querySelector(".VPContent");
561
- if (vpApp) {
562
- const sidebar = document.querySelector(".VPSidebar");
563
- if (sidebar) {
564
- const sidebarDisplay = window.getComputedStyle(sidebar).display;
565
- isFullContentMode.value = sidebarDisplay === "none";
566
- }
567
- }
568
- }
569
- // #endregion
570
-
571
- // 生命周期钩子
572
- onMounted(() => {
573
- updateVscodeNoteDir();
574
- interceptHomeReadmeLinks(isHomeReadme, router);
575
- initRedirectCheck();
576
-
577
- if (typeof window !== "undefined") {
578
- checkFullContentMode();
579
- window.addEventListener("resize", checkFullContentMode);
580
-
581
- // 监听 VitePress 的侧边栏切换事件
582
- const observer = new MutationObserver(checkFullContentMode);
583
- const vpLayout = document.querySelector(".Layout");
584
- if (vpLayout) {
585
- observer.observe(vpLayout, {
586
- attributes: true,
587
- childList: true,
588
- subtree: true,
589
- });
590
- }
591
- }
592
- });
593
-
594
- // 监听路由变化
595
- watch(
596
- () => vpData.page.value.relativePath,
597
- () => {
598
- updateVscodeNoteDir();
599
- interceptHomeReadmeLinks(isHomeReadme, router);
600
- },
601
- );
602
-
603
- watch(
604
- () => route.path,
605
- () => {
606
- setTimeout(checkFullContentMode, 100);
607
- },
608
- );
609
- </script>
610
-
611
- <style>
612
- @import "../CodeBlockFullscreen/styles.css";
613
-
614
- #VPSidebarNav > .group {
615
- display: none;
616
- }
617
- </style>
618
-
619
- <style scoped lang="scss">
620
- /* 操作栏样式 */
621
- .actionBar {
622
- display: flex;
623
- gap: 0.75rem;
624
- justify-content: flex-end;
625
- width: 100%;
626
- margin-top: 0.5rem;
627
- }
628
-
629
- /* 保存按钮样式 */
630
- .saveButton {
631
- padding: 0.625rem 1.5rem;
632
- background: linear-gradient(135deg, var(--vp-c-brand-1), var(--vp-c-brand-2));
633
- color: white;
634
- border: none;
635
- border-radius: 8px;
636
- font-size: 0.95rem;
637
- font-weight: 600;
638
- cursor: pointer;
639
- transition: all 0.2s ease;
640
- box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.3);
641
-
642
- &:hover:not(:disabled) {
643
- transform: translateY(-2px);
644
- box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.4);
645
- }
646
-
647
- &:active:not(:disabled) {
648
- transform: translateY(0);
649
- }
650
-
651
- &.disabled,
652
- &:disabled {
653
- opacity: 0.4;
654
- cursor: not-allowed;
655
- transform: none;
656
- box-shadow: none;
657
- }
658
- }
659
-
660
- /* 重置按钮样式 */
661
- .resetButton {
662
- padding: 0.625rem 1.5rem;
663
- background: transparent;
664
- color: var(--vp-c-text-2);
665
- border: 1px solid var(--vp-c-divider);
666
- border-radius: 8px;
667
- font-size: 0.95rem;
668
- font-weight: 500;
669
- cursor: pointer;
670
- transition: all 0.2s ease;
671
-
672
- &:hover {
673
- color: var(--vp-c-text-1);
674
- border-color: var(--vp-c-brand-1);
675
- background: var(--vp-c-bg-soft);
676
- }
677
-
678
- &:active {
679
- transform: scale(0.98);
680
- }
681
- }
682
-
683
- /* Toast 提示样式 */
684
- .toast {
685
- position: fixed;
686
- bottom: 2rem;
687
- right: 2rem;
688
- padding: 0.75rem 1.5rem;
689
- background: var(--vp-c-brand-1);
690
- color: white;
691
- border-radius: 8px;
692
- font-size: 0.9rem;
693
- font-weight: 500;
694
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
695
- z-index: 9999;
696
- }
697
-
698
- /* Loading Toast 样式 */
699
- .loadingToast {
700
- position: fixed;
701
- bottom: 2rem;
702
- right: 2rem;
703
- padding: 0.75rem 1.5rem;
704
- background: var(--vp-c-bg-soft);
705
- color: var(--vp-c-text-1);
706
- border: 1px solid var(--vp-c-divider);
707
- border-radius: 8px;
708
- font-size: 0.9rem;
709
- font-weight: 500;
710
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
711
- z-index: 9999;
712
- display: flex;
713
- align-items: center;
714
- gap: 0.75rem;
715
- }
716
-
717
- /* Loading 旋转动画 */
718
- .loadingSpinner {
719
- width: 16px;
720
- height: 16px;
721
- border: 2px solid var(--vp-c-divider);
722
- border-top-color: var(--vp-c-brand-1);
723
- border-radius: 50%;
724
- animation: spin 0.8s linear infinite;
725
- }
726
-
727
- @keyframes spin {
728
- to {
729
- transform: rotate(360deg);
730
- }
731
- }
732
-
733
- /* Toast 过渡动画 */
734
- :global(.toast-enter-active),
735
- :global(.toast-leave-active) {
736
- transition: all 0.3s ease;
737
- }
738
-
739
- :global(.toast-enter-from),
740
- :global(.toast-leave-to) {
741
- opacity: 0;
742
- transform: translateY(20px);
743
- }
744
- </style>
1
+ <template>
2
+ <Layout>
3
+ <template v-if="showNotFound" #not-found>
4
+ <div class="not-found-container">
5
+ <h1>404</h1>
6
+ <p>Page not found</p>
7
+ <!-- <div class="debug-info">
8
+ <p>Current path: {{ decodedCurrentPath }}</p>
9
+ <p>Matched ID: {{ matchedId }}</p>
10
+ <p>Redirect path: {{ redirectPath }}</p>
11
+ </div> -->layout
12
+ </div>
13
+ </template>
14
+ <template #doc-top>
15
+ <!-- <pre>vscodesNoteDir: {{ vscodeNotesDir }}</pre> -->
16
+ <!-- <pre>vpData.page.value: {{ vpData.page.value }}</pre>
17
+ <pre>currentNoteConfig: {{ currentNoteConfig }}</pre> -->
18
+ <!-- <button @click="copyRawFile" title="Copy raw file">raw</button> -->
19
+ <!-- <pre>{{ tocData }}</pre> -->
20
+ <ImagePreview />
21
+ <Swiper />
22
+ <ContentCollapse />
23
+ </template>
24
+ <!-- <template #doc-bottom>doc-bottom</template> -->
25
+ <template #doc-before>
26
+ <DocBeforeControls
27
+ :is-full-content-mode="isFullContentMode"
28
+ :vscode-notes-dir="vscodeNotesDir"
29
+ :is-home-readme="isHomeReadme"
30
+ :current-note-id="currentNoteId"
31
+ :created_at="created_at"
32
+ :updated_at="updated_at"
33
+ :home-readme-created-at="homeReadmeCreatedAt"
34
+ :home-readme-updated-at="homeReadmeUpdatedAt"
35
+ :time-modal-open="timeModalOpen"
36
+ :all-collapsed="allCollapsed"
37
+ @open-time-modal="openTimeModal"
38
+ @toggle-all-collapse="toggleAllCollapse"
39
+ />
40
+ <!-- 笔记状态标题 -->
41
+ <NoteStatus
42
+ :note-config="currentNoteConfig"
43
+ :is-notes-page="isNotesPage"
44
+ />
45
+
46
+ <AboutModal
47
+ v-model="timeModalOpen"
48
+ @close="onTimeModalClose"
49
+ :title="modalTitle"
50
+ >
51
+ <template #title>
52
+ {{ modalTitle }}
53
+ </template>
54
+
55
+ <AboutPanel
56
+ :is-home-readme="modalIsHomeReadme"
57
+ :current-note-id="modalNoteId"
58
+ :is-dev="isDev"
59
+ v-model:editable-note-title="editableNoteTitle"
60
+ v-model:editable-description="editableDescription"
61
+ v-model:editable-note-status="editableNoteStatus"
62
+ v-model:editable-discussions-enabled="editableDiscussionsEnabled"
63
+ v-model:title-error="titleError"
64
+ :modal-created-at="modalCreatedAt"
65
+ :modal-updated-at="modalUpdatedAt"
66
+ :modal-github-url="modalGithubUrl"
67
+ :modal-github-page-url="modalGithubPageUrl"
68
+ :completion-percentage="completionPercentage"
69
+ :done-notes-len="doneNotesLen"
70
+ :total-notes-len="totalNotesLen"
71
+ @title-input="onTitleInput"
72
+ @title-blur="onTitleBlur"
73
+ @description-input="onDescriptionInput"
74
+ @config-change="onConfigChange"
75
+ />
76
+
77
+ <!-- 操作按钮(仅开发环境且非首页显示) -->
78
+ <template #footer v-if="isDev && !modalIsHomeReadme">
79
+ <div class="actionBar">
80
+ <button
81
+ class="saveButton"
82
+ :class="{ disabled: !hasConfigChanges }"
83
+ @click="saveNoteConfig"
84
+ :disabled="!hasConfigChanges || isSaving"
85
+ type="button"
86
+ >
87
+ {{ saveButtonText }}
88
+ </button>
89
+ <button
90
+ v-if="hasConfigChanges"
91
+ @click="resetNoteConfig"
92
+ class="resetButton"
93
+ type="button"
94
+ >
95
+ 重置
96
+ </button>
97
+ </div>
98
+
99
+ <!-- 保存进度提示 -->
100
+ <Transition name="toast">
101
+ <div v-if="isSaving && savingMessage" class="loadingToast">
102
+ <div class="loadingSpinner"></div>
103
+ <span>{{ savingMessage }}</span>
104
+ </div>
105
+ </Transition>
106
+
107
+ <!-- 保存成功提示 -->
108
+ <Transition name="toast">
109
+ <div v-if="showSuccessToast" class="toast">✓ 保存成功</div>
110
+ </Transition>
111
+ </template>
112
+ </AboutModal>
113
+ </template>
114
+ <template #doc-footer-before>
115
+ <!-- <div class="footer-time-info">
116
+ <p title="首次提交时间">首次提交时间:{{ formatDate(created_at) }}</p>
117
+ <p title="最近提交时间">最近提交时间:{{ formatDate(updated_at) }}</p>
118
+ </div> -->
119
+ </template>
120
+ <template #doc-after>
121
+ <!-- 自定义 DocFooter -->
122
+ <DocFooter />
123
+ <!-- {{ REPO_NAME + '.' + currentNoteId }} -->
124
+ <Discussions
125
+ v-if="isDiscussionsVisible"
126
+ :id="currentNoteConfig.id"
127
+ :note-number="currentNoteId || ''"
128
+ :note-title="currentNoteTitle"
129
+ />
130
+ </template>
131
+ <!-- <template #doc-bottom>
132
+ <Discussions id="TNotes.template.0003" />
133
+ </template> -->
134
+
135
+ <template #aside-top>
136
+ <!-- aside-top -->
137
+ <!-- {{ vpData.page.value.title }} -->
138
+ </template>
139
+ <!-- <template #aside-outline-before>
140
+ <span
141
+ @click="scrollToTop"
142
+ style="cursor: pointer; height: 1em; width: 1em"
143
+ title="回到顶部"
144
+ >
145
+ <img :src="icon__totop" alt="to top" />
146
+ </span>
147
+ </template> -->
148
+
149
+ <!-- 使用 sidebar-nav-before 插槽插入控制按钮 -->
150
+ <template #sidebar-nav-before>
151
+ <SidebarNavBefore
152
+ :is-expanded="allSidebarExpanded"
153
+ :show-note-id="showNoteId"
154
+ @toggle-expand="toggleSidebarSections"
155
+ @toggle-note-id="toggleNoteId"
156
+ @focus-current="focusCurrentNote"
157
+ />
158
+ </template>
159
+
160
+ <!-- 使用 sidebar-nav-after 插槽插入自定义 Sidebar -->
161
+ <template #sidebar-nav-after>
162
+ <CustomSidebar
163
+ ref="customSidebarRef"
164
+ @open-note-about="openSidebarNoteAbout"
165
+ />
166
+ </template>
167
+ <!-- <template #sidebar-nav-after>sidebar-nav-after</template> -->
168
+
169
+ <!-- <template #aside-outline-after>aside-outline-after</template> -->
170
+ <!-- <template #aside-bottom>aside-bottom</template> -->
171
+ <!-- <template #aside-ads-before>aside-ads-before</template> -->
172
+ <!-- <template #aside-ads-after>aside-ads-after</template> -->
173
+ <!-- <template #layout-top>layout-top</template> -->
174
+ <!-- <template #layout-bottom>layout-bottom</template> -->
175
+ <!-- <template #nav-bar-title-before>nav-bar-title-before</template> -->
176
+ <!-- <template #nav-bar-title-after>nav-bar-title-after</template> -->
177
+ <!-- <template #nav-bar-content-before>nav-bar-content-before</template> -->
178
+ <!-- 设置入口:固定在 VPNav 右侧(social-links / appearance 之后,extra 之前) -->
179
+ <template #nav-bar-content-after>
180
+ <NavBarSettingsTrigger />
181
+ </template>
182
+
183
+ <!-- !NOTE 不清楚下面的插槽所对应的位置 -->
184
+ <!-- <template #nav-screen-content-before>nav-screen-content-before</template> -->
185
+ <!-- <template #nav-screen-content-after>nav-screen-content-after</template> -->
186
+ </Layout>
187
+
188
+ <SidebarResizeHandle />
189
+
190
+ <!-- 全局重命名遮罩:关于面板保存或文件系统重命名时由 useRenameOverlay 控制 -->
191
+ <LoadingPage
192
+ :visible="renameOverlayState.visible"
193
+ :message="renameOverlayState.message"
194
+ :tip="renameOverlayState.tip"
195
+ />
196
+
197
+ <!-- 全局设置 Dialog:由 useSettingsDialog 控制显隐 -->
198
+ <SettingsDialog />
199
+ </template>
200
+
201
+ <script setup>
202
+ import { useData, useRoute, useRouter } from "vitepress";
203
+ import DefaultTheme from "vitepress/theme";
204
+ import { computed, onMounted, ref, watch } from "vue";
205
+
206
+ import { useCodeBlockFullscreen } from "../CodeBlockFullscreen";
207
+ import { SIDEBAR_SHOW_NOTE_ID_KEY } from "../constants";
208
+ import { data as allNotesConfig } from "../notesConfig.data.ts";
209
+ import AboutModal from "./AboutModal.vue";
210
+ import AboutPanel from "./AboutPanel.vue";
211
+ import Discussions from "../Discussions/Discussions.vue";
212
+ import LoadingPage from "../LoadingPage/LoadingPage.vue";
213
+ import { useCollapseControl } from "./composables/useCollapseControl";
214
+ import { useNoteConfig } from "./composables/useNoteConfig";
215
+ import { useNoteSave } from "./composables/useNoteSave";
216
+ import { useNoteValidation } from "./composables/useNoteValidation";
217
+ import { useRedirect } from "./composables/useRedirect";
218
+ import { useRenameOverlay } from "./composables/useRenameOverlay";
219
+ import { useVSCodeIntegration } from "./composables/useVSCodeIntegration";
220
+ import ContentCollapse from "./ContentCollapse.vue";
221
+ import CustomSidebar from "./CustomSidebar.vue";
222
+ import DocBeforeControls from "./DocBeforeControls.vue";
223
+ import DocFooter from "./DocFooter.vue";
224
+ import { data as readmeData } from "./homeReadme.data.ts";
225
+ import ImagePreview from "./ImagePreview.vue";
226
+ import NavBarSettingsTrigger from "./NavBarSettingsTrigger.vue";
227
+ import NoteStatus from "./NoteStatus.vue";
228
+ import SidebarNavBefore from "./SidebarNavBefore.vue";
229
+ import SidebarResizeHandle from "./SidebarResizeHandle.vue";
230
+ import Swiper from "./Swiper.vue";
231
+ import SettingsDialog from "../Settings/SettingsDialog.vue";
232
+
233
+ const { Layout } = DefaultTheme;
234
+ const vpData = useData();
235
+ const router = useRouter();
236
+ const route = useRoute();
237
+
238
+ // 启用代码块全屏功能
239
+ useCodeBlockFullscreen();
240
+
241
+ // 全局重命名遮罩状态(由 useRenameRedirect 控制 show/hide)
242
+ const { state: renameOverlayState } = useRenameOverlay();
243
+
244
+ // 自定义侧边栏引用
245
+ const customSidebarRef = ref(null);
246
+ const showNoteId = ref(false);
247
+
248
+ // 计算是否有展开的一级章节
249
+ const allSidebarExpanded = computed(() => {
250
+ if (!customSidebarRef.value) return false;
251
+ return customSidebarRef.value.hasAnyFirstLevelExpanded();
252
+ });
253
+
254
+ // 初始化笔记编号显示状态
255
+ if (typeof window !== "undefined") {
256
+ const savedShowNoteId = localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY);
257
+ showNoteId.value = savedShowNoteId === "true";
258
+ }
259
+
260
+ // 切换侧边栏展开/折叠状态(智能切换)
261
+ function toggleSidebarSections() {
262
+ if (customSidebarRef.value) {
263
+ customSidebarRef.value.toggleExpandCollapse();
264
+ }
265
+ }
266
+
267
+ // 切换笔记编号显示状态
268
+ function toggleNoteId() {
269
+ showNoteId.value = !showNoteId.value;
270
+ if (typeof window !== "undefined") {
271
+ localStorage.setItem(SIDEBAR_SHOW_NOTE_ID_KEY, showNoteId.value.toString());
272
+ // 刷新页面以应用变化
273
+ window.location.reload();
274
+ }
275
+ }
276
+
277
+ // 聚焦到当前笔记
278
+ function focusCurrentNote() {
279
+ if (customSidebarRef.value) {
280
+ customSidebarRef.value.focusCurrentNote();
281
+ }
282
+ }
283
+
284
+ // 提取当前笔记的 ID(前 4 个数字)
285
+ const currentNoteId = computed(() => {
286
+ const relativePath = vpData.page.value.relativePath;
287
+ // relativePath 格式: notes/0001. 标题/README.md
288
+ const match = relativePath.match(/notes\/(\d{4})/);
289
+ const id = match ? match[1] : null;
290
+
291
+ return id;
292
+ });
293
+
294
+ // 判断是否是笔记页面(notes 目录下)
295
+ const isNotesPage = computed(() => {
296
+ return vpData.page.value.relativePath.startsWith("notes/");
297
+ });
298
+
299
+ // 提取当前笔记的标题(从 relativePath)
300
+ const currentNoteTitle = computed(() => {
301
+ const relativePath = vpData.page.value.relativePath;
302
+ // relativePath 格式: notes/0001. 标题/README.md
303
+ const match = relativePath.match(/notes\/\d{4}\.\s+([^/]+)\//);
304
+ const title = match ? match[1] : "";
305
+
306
+ return title;
307
+ });
308
+
309
+ // 根据当前笔记 ID 获取配置数据
310
+ const currentNoteConfig = computed(() => {
311
+ return currentNoteId.value && allNotesConfig[currentNoteId.value]
312
+ ? allNotesConfig[currentNoteId.value]
313
+ : {
314
+ bilibili: [],
315
+ done: false,
316
+ enableDiscussions: false,
317
+ };
318
+ });
319
+
320
+ function getEmptyNoteConfig() {
321
+ return {
322
+ bilibili: [],
323
+ done: false,
324
+ enableDiscussions: false,
325
+ };
326
+ }
327
+
328
+ const isDiscussionsVisible = computed(
329
+ () => currentNoteConfig.value.enableDiscussions,
330
+ );
331
+ const updated_at = computed(() => currentNoteConfig.value.updated_at);
332
+ const created_at = computed(() => currentNoteConfig.value.created_at);
333
+
334
+ // 判断是否为首页 README.md
335
+ const isHomeReadme = computed(() => vpData.page.value.filePath === "README.md");
336
+ const doneNotesLen = computed(() => readmeData?.doneNotesLen || 0);
337
+ const totalNotesLen = computed(() => readmeData?.totalNotesLen || 0);
338
+
339
+ // 完成进度百分比
340
+ const completionPercentage = computed(() => {
341
+ if (!totalNotesLen.value || totalNotesLen.value === 0) return null;
342
+ return Math.round((doneNotesLen.value / totalNotesLen.value) * 100);
343
+ });
344
+
345
+ // 首页 README.md 的时间戳
346
+ const homeReadmeCreatedAt = computed(() => readmeData?.created_at);
347
+ const homeReadmeUpdatedAt = computed(() => readmeData?.updated_at);
348
+
349
+ // #region - Composables
350
+ // 404 重定向
351
+ const { showNotFound, decodedCurrentPath, initRedirectCheck } =
352
+ useRedirect(allNotesConfig);
353
+
354
+ // modal 控制
355
+ const timeModalOpen = ref(false);
356
+ const sidebarAboutNoteId = ref(null);
357
+
358
+ const modalIsHomeReadme = computed(
359
+ () => !sidebarAboutNoteId.value && isHomeReadme.value,
360
+ );
361
+
362
+ const modalNoteId = computed(() => sidebarAboutNoteId.value || currentNoteId.value);
363
+
364
+ function safeDecode(value) {
365
+ try {
366
+ return decodeURIComponent(value);
367
+ } catch {
368
+ return value;
369
+ }
370
+ }
371
+
372
+ function getNoteFolderSegment(noteId) {
373
+ if (!noteId) return "";
374
+
375
+ const source =
376
+ noteId === currentNoteId.value
377
+ ? vpData.page.value.relativePath
378
+ : allNotesConfig[noteId]?.redirect || "";
379
+ const match = source.match(/notes\/([^/]+)\/README/);
380
+
381
+ return match ? match[1] : "";
382
+ }
383
+
384
+ function getNoteTitleById(noteId) {
385
+ const folderSegment = getNoteFolderSegment(noteId);
386
+ const match = folderSegment.match(/^\d{4}\.\s+(.+)$/);
387
+
388
+ return match ? safeDecode(match[1]) : "";
389
+ }
390
+
391
+ const modalNoteTitle = computed(() => {
392
+ if (!sidebarAboutNoteId.value) return currentNoteTitle.value;
393
+ return getNoteTitleById(sidebarAboutNoteId.value);
394
+ });
395
+
396
+ const modalNoteConfig = computed(() => {
397
+ return modalNoteId.value && allNotesConfig[modalNoteId.value]
398
+ ? allNotesConfig[modalNoteId.value]
399
+ : getEmptyNoteConfig();
400
+ });
401
+
402
+ // 笔记配置管理
403
+ const {
404
+ editableNoteStatus,
405
+ editableDiscussionsEnabled,
406
+ editableNoteTitle,
407
+ editableDescription,
408
+ titleError,
409
+ hasConfigChanges,
410
+ resetNoteConfig,
411
+ updateOriginalValues,
412
+ } = useNoteConfig(
413
+ modalNoteId,
414
+ modalNoteConfig,
415
+ modalNoteTitle,
416
+ timeModalOpen,
417
+ );
418
+
419
+ // 标题验证
420
+ const { onTitleInput: validateTitleInput, onTitleBlur: validateTitleBlur } =
421
+ useNoteValidation();
422
+
423
+ // 保存逻辑
424
+ const {
425
+ isSaving,
426
+ showSuccessToast,
427
+ savingMessage,
428
+ saveButtonText,
429
+ saveNoteConfig,
430
+ } = useNoteSave(
431
+ modalNoteId,
432
+ computed(() => {
433
+ if (typeof window === "undefined") return false;
434
+ return (
435
+ window.location.hostname === "localhost" ||
436
+ window.location.hostname === "127.0.0.1"
437
+ );
438
+ }),
439
+ hasConfigChanges,
440
+ titleError,
441
+ editableNoteTitle,
442
+ computed(() => modalNoteTitle.value),
443
+ editableNoteStatus,
444
+ computed(() => modalNoteConfig.value.done || false),
445
+ editableDiscussionsEnabled,
446
+ computed(() => modalNoteConfig.value.enableDiscussions || false),
447
+ editableDescription,
448
+ computed(() => modalNoteConfig.value.description || ""),
449
+ allNotesConfig,
450
+ updateOriginalValues,
451
+ );
452
+
453
+ // 折叠控制
454
+ const { allCollapsed, toggleAllCollapse } = useCollapseControl();
455
+
456
+ // VSCode 集成
457
+ const { vscodeNotesDir, updateVscodeNoteDir, interceptHomeReadmeLinks } =
458
+ useVSCodeIntegration();
459
+
460
+ // 判断是否为开发环境
461
+ const isDev = computed(() => {
462
+ if (typeof window === "undefined") return false;
463
+ return (
464
+ window.location.hostname === "localhost" ||
465
+ window.location.hostname === "127.0.0.1"
466
+ );
467
+ });
468
+
469
+ // #endregion
470
+
471
+ // modal 标题
472
+ const modalTitle = computed(() => {
473
+ return modalIsHomeReadme.value ? "关于这个知识库" : "关于这篇笔记";
474
+ });
475
+
476
+ // modal 中显示的 GitHub 链接
477
+ const modalGithubUrl = computed(() => {
478
+ if (modalIsHomeReadme.value) {
479
+ const repoName = vpData.site.value.title.toLowerCase();
480
+ return `https://github.com/tnotesjs/${repoName}`;
481
+ }
482
+
483
+ const folderSegment = getNoteFolderSegment(modalNoteId.value);
484
+ if (!folderSegment) return "";
485
+
486
+ const repoName = vpData.site.value.title.toLowerCase();
487
+ return `https://github.com/tnotesjs/${repoName}/tree/main/notes/${encodeURIComponent(folderSegment)}`;
488
+ });
489
+
490
+ // modal 中显示的 GitHub Page 链接
491
+ const modalGithubPageUrl = computed(() => {
492
+ if (modalIsHomeReadme.value) {
493
+ const repoName = vpData.site.value.title; // 保持原始大小写
494
+ return `https://tnotesjs.github.io/${repoName}/`;
495
+ }
496
+ // 笔记页面的 GitHub Page 链接
497
+ const folderSegment = getNoteFolderSegment(modalNoteId.value);
498
+ if (modalNoteId.value && folderSegment) {
499
+ const repoName = vpData.site.value.title; // 保持原始大小写
500
+ return `https://tnotesjs.github.io/${repoName}/notes/${encodeURIComponent(folderSegment)}/README`;
501
+ }
502
+ return "";
503
+ });
504
+
505
+ // modal 中显示的创建时间
506
+ const modalCreatedAt = computed(() => {
507
+ return modalIsHomeReadme.value
508
+ ? homeReadmeCreatedAt.value
509
+ : modalNoteConfig.value.created_at;
510
+ });
511
+
512
+ // modal 中显示的更新时间
513
+ const modalUpdatedAt = computed(() => {
514
+ return modalIsHomeReadme.value
515
+ ? homeReadmeUpdatedAt.value
516
+ : modalNoteConfig.value.updated_at;
517
+ });
518
+
519
+ function openTimeModal() {
520
+ sidebarAboutNoteId.value = null;
521
+ timeModalOpen.value = true;
522
+ }
523
+
524
+ function onTimeModalClose() {
525
+ timeModalOpen.value = false;
526
+ sidebarAboutNoteId.value = null;
527
+ }
528
+
529
+ function openSidebarNoteAbout(noteIndex) {
530
+ sidebarAboutNoteId.value = noteIndex;
531
+ timeModalOpen.value = true;
532
+ }
533
+
534
+ // 配置变更时的回调
535
+ function onConfigChange() {
536
+ // 配置变更时不需要做额外操作,只需要触发 hasConfigChanges 计算
537
+ }
538
+
539
+ // 标题输入事件
540
+ function onTitleInput() {
541
+ validateTitleInput(editableNoteTitle, titleError);
542
+ }
543
+
544
+ // 标题失焦事件
545
+ function onTitleBlur() {
546
+ validateTitleBlur(editableNoteTitle, titleError);
547
+ }
548
+
549
+ // 简介输入事件
550
+ function onDescriptionInput() {
551
+ // 简介没有特殊验证,只需要触发变更检测
552
+ }
553
+
554
+ // #region - 全屏状态检测
555
+ const isFullContentMode = ref(false);
556
+
557
+ function checkFullContentMode() {
558
+ if (typeof document === "undefined") return;
559
+
560
+ const vpApp = document.querySelector(".VPContent");
561
+ if (vpApp) {
562
+ const sidebar = document.querySelector(".VPSidebar");
563
+ if (sidebar) {
564
+ const sidebarDisplay = window.getComputedStyle(sidebar).display;
565
+ isFullContentMode.value = sidebarDisplay === "none";
566
+ }
567
+ }
568
+ }
569
+ // #endregion
570
+
571
+ // 生命周期钩子
572
+ onMounted(() => {
573
+ updateVscodeNoteDir();
574
+ interceptHomeReadmeLinks(isHomeReadme, router);
575
+ initRedirectCheck();
576
+
577
+ if (typeof window !== "undefined") {
578
+ checkFullContentMode();
579
+ window.addEventListener("resize", checkFullContentMode);
580
+
581
+ // 监听 VitePress 的侧边栏切换事件
582
+ const observer = new MutationObserver(checkFullContentMode);
583
+ const vpLayout = document.querySelector(".Layout");
584
+ if (vpLayout) {
585
+ observer.observe(vpLayout, {
586
+ attributes: true,
587
+ childList: true,
588
+ subtree: true,
589
+ });
590
+ }
591
+ }
592
+ });
593
+
594
+ // 监听路由变化
595
+ watch(
596
+ () => vpData.page.value.relativePath,
597
+ () => {
598
+ updateVscodeNoteDir();
599
+ interceptHomeReadmeLinks(isHomeReadme, router);
600
+ },
601
+ );
602
+
603
+ watch(
604
+ () => route.path,
605
+ () => {
606
+ setTimeout(checkFullContentMode, 100);
607
+ },
608
+ );
609
+ </script>
610
+
611
+ <style>
612
+ @import "../CodeBlockFullscreen/styles.css";
613
+
614
+ #VPSidebarNav > .group {
615
+ display: none;
616
+ }
617
+ </style>
618
+
619
+ <style scoped lang="scss">
620
+ /* 操作栏样式 */
621
+ .actionBar {
622
+ display: flex;
623
+ gap: 0.75rem;
624
+ justify-content: flex-end;
625
+ width: 100%;
626
+ margin-top: 0.5rem;
627
+ }
628
+
629
+ /* 保存按钮样式 */
630
+ .saveButton {
631
+ padding: 0.625rem 1.5rem;
632
+ background: linear-gradient(135deg, var(--vp-c-brand-1), var(--vp-c-brand-2));
633
+ color: white;
634
+ border: none;
635
+ border-radius: 8px;
636
+ font-size: 0.95rem;
637
+ font-weight: 600;
638
+ cursor: pointer;
639
+ transition: all 0.2s ease;
640
+ box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.3);
641
+
642
+ &:hover:not(:disabled) {
643
+ transform: translateY(-2px);
644
+ box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.4);
645
+ }
646
+
647
+ &:active:not(:disabled) {
648
+ transform: translateY(0);
649
+ }
650
+
651
+ &.disabled,
652
+ &:disabled {
653
+ opacity: 0.4;
654
+ cursor: not-allowed;
655
+ transform: none;
656
+ box-shadow: none;
657
+ }
658
+ }
659
+
660
+ /* 重置按钮样式 */
661
+ .resetButton {
662
+ padding: 0.625rem 1.5rem;
663
+ background: transparent;
664
+ color: var(--vp-c-text-2);
665
+ border: 1px solid var(--vp-c-divider);
666
+ border-radius: 8px;
667
+ font-size: 0.95rem;
668
+ font-weight: 500;
669
+ cursor: pointer;
670
+ transition: all 0.2s ease;
671
+
672
+ &:hover {
673
+ color: var(--vp-c-text-1);
674
+ border-color: var(--vp-c-brand-1);
675
+ background: var(--vp-c-bg-soft);
676
+ }
677
+
678
+ &:active {
679
+ transform: scale(0.98);
680
+ }
681
+ }
682
+
683
+ /* Toast 提示样式 */
684
+ .toast {
685
+ position: fixed;
686
+ bottom: 2rem;
687
+ right: 2rem;
688
+ padding: 0.75rem 1.5rem;
689
+ background: var(--vp-c-brand-1);
690
+ color: white;
691
+ border-radius: 8px;
692
+ font-size: 0.9rem;
693
+ font-weight: 500;
694
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
695
+ z-index: 9999;
696
+ }
697
+
698
+ /* Loading Toast 样式 */
699
+ .loadingToast {
700
+ position: fixed;
701
+ bottom: 2rem;
702
+ right: 2rem;
703
+ padding: 0.75rem 1.5rem;
704
+ background: var(--vp-c-bg-soft);
705
+ color: var(--vp-c-text-1);
706
+ border: 1px solid var(--vp-c-divider);
707
+ border-radius: 8px;
708
+ font-size: 0.9rem;
709
+ font-weight: 500;
710
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
711
+ z-index: 9999;
712
+ display: flex;
713
+ align-items: center;
714
+ gap: 0.75rem;
715
+ }
716
+
717
+ /* Loading 旋转动画 */
718
+ .loadingSpinner {
719
+ width: 16px;
720
+ height: 16px;
721
+ border: 2px solid var(--vp-c-divider);
722
+ border-top-color: var(--vp-c-brand-1);
723
+ border-radius: 50%;
724
+ animation: spin 0.8s linear infinite;
725
+ }
726
+
727
+ @keyframes spin {
728
+ to {
729
+ transform: rotate(360deg);
730
+ }
731
+ }
732
+
733
+ /* Toast 过渡动画 */
734
+ :global(.toast-enter-active),
735
+ :global(.toast-leave-active) {
736
+ transition: all 0.3s ease;
737
+ }
738
+
739
+ :global(.toast-enter-from),
740
+ :global(.toast-leave-to) {
741
+ opacity: 0;
742
+ transform: translateY(20px);
743
+ }
744
+ </style>