@tnotesjs/core 0.6.1 → 0.8.0

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 (92) hide show
  1. package/commands/BaseCommand.ts +58 -0
  2. package/commands/build/BuildCommand.ts +25 -0
  3. package/commands/build/PreviewCommand.ts +29 -0
  4. package/commands/build/index.ts +8 -0
  5. package/commands/dev/DevCommand.ts +75 -0
  6. package/commands/dev/index.ts +7 -0
  7. package/commands/git/PullCommand.ts +25 -0
  8. package/commands/git/PushCommand.ts +64 -0
  9. package/commands/git/index.ts +8 -0
  10. package/commands/index.ts +11 -0
  11. package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
  12. package/commands/init-sub-repo/index.ts +1 -0
  13. package/commands/misc/HelpCommand.ts +104 -0
  14. package/commands/misc/index.ts +7 -0
  15. package/commands/models.ts +87 -0
  16. package/commands/note/CreateNoteCommand.ts +160 -0
  17. package/commands/note/RenameNoteCommand.ts +147 -0
  18. package/commands/note/UpdateNoteConfigCommand.ts +78 -0
  19. package/commands/note/index.ts +9 -0
  20. package/commands/registry.ts +47 -0
  21. package/commands/update/UpdateCommand.ts +219 -0
  22. package/commands/update/index.ts +7 -0
  23. package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
  24. package/commands/update-completed-count/index.ts +5 -0
  25. package/config/templates.ts +0 -5
  26. package/dist/{chunk-5F5RJ2HV.cjs → chunk-7FEANCT6.cjs} +16 -7
  27. package/dist/chunk-AGVER5MX.cjs +1746 -0
  28. package/dist/{chunk-TJ4527KA.cjs → chunk-BL22KD4V.cjs} +324 -950
  29. package/dist/{chunk-56PKXCOX.js → chunk-GWG75A5M.js} +13 -4
  30. package/dist/{chunk-DUTS75WQ.js → chunk-XNO3PCEH.js} +374 -1000
  31. package/dist/chunk-ZEQS64PE.js +1746 -0
  32. package/dist/cli/index.cjs +76 -155
  33. package/dist/cli/index.js +10 -89
  34. package/dist/markdown/index.cjs +8 -12
  35. package/dist/markdown/index.d.cts +1 -1
  36. package/dist/markdown/index.d.ts +1 -1
  37. package/dist/markdown/index.js +7 -11
  38. package/dist/{types-CwBWwNVv.d.ts → types-C_d0fFeD.d.ts} +8 -1
  39. package/dist/{types-CpsEy8lA.d.cts → types-MRBGgJTX.d.cts} +8 -1
  40. package/dist/vitepress/config/index.cjs +402 -178
  41. package/dist/vitepress/config/index.js +392 -168
  42. package/dist/workspace/index.cjs +4 -1202
  43. package/dist/workspace/index.d.cts +2 -2
  44. package/dist/workspace/index.d.ts +2 -2
  45. package/dist/workspace/index.js +4 -1202
  46. package/markdown/components.ts +86 -0
  47. package/markdown/index.ts +17 -0
  48. package/markdown/noteFormatter.test.ts +44 -0
  49. package/markdown/noteFormatter.ts +237 -0
  50. package/package.json +7 -3
  51. package/services/file-watcher/folderChangeHandler.ts +19 -15
  52. package/services/file-watcher/globalUpdateCoordinator.ts +21 -27
  53. package/services/file-watcher/service.ts +1 -6
  54. package/services/index.ts +0 -1
  55. package/services/note/service.ts +4 -6
  56. package/services/readme/service.ts +11 -18
  57. package/services/reconcileToc.ts +20 -0
  58. package/utils/index.ts +0 -9
  59. package/{services/toc → utils}/moveTocInside.test.ts +1 -1
  60. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
  61. package/vitepress/components/EnWordList/EnWordList.vue +16 -662
  62. package/vitepress/components/Footprints/Footprints.vue +15 -537
  63. package/vitepress/components/Mermaid/Mermaid.vue +13 -588
  64. package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
  65. package/vitepress/components/MindmapPreview/markdown.ts +1 -1
  66. package/vitepress/components/NotesTable/NotesTable.vue +11 -130
  67. package/vitepress/components/constants.ts +2 -2
  68. package/vitepress/components/sidebar.data.ts +8 -5
  69. package/vitepress/configs/markdown.config.ts +170 -26
  70. package/vitepress/plugins/sidebarStructurePlugin.ts +242 -145
  71. package/vitepress/theme/index.ts +9 -13
  72. package/vitepress/theme/styles/base.scss +15 -0
  73. package/workspace/atomic.ts +113 -0
  74. package/workspace/errors.ts +27 -0
  75. package/workspace/index.ts +40 -0
  76. package/workspace/mutationQueue.ts +28 -0
  77. package/workspace/paths.ts +64 -0
  78. package/workspace/reconcile.test.ts +300 -0
  79. package/workspace/reconcile.ts +95 -0
  80. package/workspace/scanner.ts +292 -0
  81. package/workspace/types.ts +224 -0
  82. package/workspace/workspace.test.ts +333 -0
  83. package/workspace/workspace.ts +1020 -0
  84. package/dist/chunk-3R7QSABB.cjs +0 -502
  85. package/dist/chunk-CZXRQPFJ.js +0 -11
  86. package/dist/chunk-HXED7KVT.cjs +0 -11
  87. package/dist/chunk-U6IBLREL.js +0 -502
  88. package/services/toc/index.ts +0 -5
  89. package/services/toc/service.ts +0 -759
  90. package/utils/migrateReadmeToToc.test.ts +0 -111
  91. package/utils/migrateReadmeToToc.ts +0 -135
  92. package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
@@ -1,442 +1,20 @@
1
+ <!-- Legacy path: re-export shared Mindmap as MindmapPreview for old fence output. -->
1
2
  <script setup lang="ts">
2
- import { CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
3
- import { onContentUpdated, useData } from 'vitepress'
4
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
3
+ import { Mindmap } from '@tnotesjs/ui'
5
4
 
6
- import { applyInitialExpandLevel } from './expandLevel'
7
- import { normalizeMindmapMarkdown } from './markdown'
8
- import MindmapOutlineNode from './MindmapOutlineNode.vue'
9
- import MindmapViewIcon from './MindmapViewIcon.vue'
10
- import { gateMindmapWheel } from './wheelInteraction'
11
- import { icon__fullscreen, icon__fullscreen_exit, icon__zoom_fit } from '../../assets/icons'
12
-
13
- type PreviewView = 'mindmap' | 'outline' | 'source'
14
-
15
- const props = withDefaults(defineProps<{
5
+ defineProps<{
16
6
  content?: string
7
+ source?: string
17
8
  initialExpandLevel?: number
18
- }>(), {
19
- content: '',
20
- initialExpandLevel: 3,
21
- })
22
-
23
- const { isDark } = useData()
24
- const activeView = ref<PreviewView>('mindmap')
25
- const previewRoot = ref<HTMLElement | null>(null)
26
- const canvasHost = ref<HTMLElement | null>(null)
27
- const session = shallowRef<MindmapSession | null>(null)
28
- const renderVersion = ref(0)
29
- const isFullscreen = ref(false)
30
- const isCanvasActive = ref(false)
31
- let viewer: CanvasViewer | null = null
32
- let mounted = false
33
-
34
- const viewOptions = [
35
- { value: 'mindmap', label: '脑图' },
36
- { value: 'outline', label: '大纲' },
37
- { value: 'source', label: '源码' },
38
- ] as const
39
-
40
- function decodeContent(value: string): string {
41
- try {
42
- return decodeURIComponent(value)
43
- } catch {
44
- return value
45
- }
46
- }
47
-
48
- const normalizedContent = computed(() => normalizeMindmapMarkdown(decodeContent(props.content)))
49
-
50
- function createViewer(): void {
51
- if (!mounted || !canvasHost.value || !session.value || viewer) return
52
- viewer = new CanvasViewer(canvasHost.value, session.value, {
53
- theme: isDark.value ? 'dark' : 'light',
54
- })
55
- }
56
-
57
- function rebuildSession(): void {
58
- viewer?.destroy()
59
- viewer = null
60
- const next = new MindmapSession({
61
- markdown: normalizedContent.value,
62
- fileName: 'mindmap-preview.tn-mindmap.md',
63
- })
64
- applyInitialExpandLevel(next, props.initialExpandLevel)
65
- const invalidate = () => { renderVersion.value += 1 }
66
- next.on('collapseChange', invalidate)
67
- next.on('focusChange', invalidate)
68
- next.on('change', invalidate)
69
- session.value = next
70
- renderVersion.value += 1
71
- void nextTick(createViewer)
72
- }
73
-
74
- function setView(view: PreviewView): void {
75
- activeView.value = view
76
- if (view !== 'mindmap') isCanvasActive.value = false
77
- if (view === 'mindmap') void nextTick(() => viewer?.zoomToFit())
78
- }
79
-
80
- function toggleNode(id: string): void {
81
- session.value?.toggleCollapse(id)
82
- }
83
-
84
- function exitFocusTo(index: number): void {
85
- session.value?.exitFocusTo(index)
86
- }
87
-
88
- async function toggleFullscreen(): Promise<void> {
89
- const root = previewRoot.value
90
- if (!root) return
91
-
92
- try {
93
- if (document.fullscreenElement === root) await document.exitFullscreen()
94
- else await root.requestFullscreen()
95
- } catch {
96
- // Fullscreen can be rejected by the browser or an embedded document policy.
97
- }
98
- }
99
-
100
- function handleFullscreenChange(): void {
101
- isFullscreen.value = document.fullscreenElement === previewRoot.value
102
- if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
103
- }
104
-
105
- function activateCanvas(): void {
106
- isCanvasActive.value = true
107
- }
108
-
109
- function handleCanvasWheelCapture(event: WheelEvent): void {
110
- gateMindmapWheel(event, isCanvasActive.value)
111
- }
112
-
113
- function handleDocumentPointerDown(event: PointerEvent): void {
114
- if (event.target instanceof Node && !previewRoot.value?.contains(event.target)) {
115
- isCanvasActive.value = false
116
- }
117
- }
118
-
119
- function handleDocumentKeydown(event: KeyboardEvent): void {
120
- if (event.key !== 'Escape' || !isCanvasActive.value) return
121
- isCanvasActive.value = false
122
- canvasHost.value?.blur()
123
- }
124
-
125
- watch([normalizedContent, () => props.initialExpandLevel], rebuildSession, { immediate: true })
126
- watch(isDark, (dark) => viewer?.setTheme(dark ? 'dark' : 'light'))
127
-
128
- onMounted(() => {
129
- mounted = true
130
- document.addEventListener('fullscreenchange', handleFullscreenChange)
131
- document.addEventListener('pointerdown', handleDocumentPointerDown, true)
132
- document.addEventListener('keydown', handleDocumentKeydown, true)
133
- createViewer()
134
- })
135
-
136
- onContentUpdated(() => {
137
- if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
138
- })
139
-
140
- onBeforeUnmount(() => {
141
- mounted = false
142
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
143
- document.removeEventListener('pointerdown', handleDocumentPointerDown, true)
144
- document.removeEventListener('keydown', handleDocumentKeydown, true)
145
- viewer?.destroy()
146
- viewer = null
147
- })
9
+ isDark?: boolean
10
+ }>()
148
11
  </script>
149
12
 
150
13
  <template>
151
- <section
152
- ref="previewRoot"
153
- class="mindmap-preview"
154
- :class="{ 'is-dark': isDark, 'is-fullscreen': isFullscreen }"
155
- :data-version="renderVersion"
156
- >
157
- <div class="mindmap-preview-actions">
158
- <nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
159
- <button
160
- v-for="item in viewOptions"
161
- :key="item.value"
162
- type="button"
163
- class="mindmap-preview-action"
164
- :class="{ 'is-active': activeView === item.value }"
165
- :aria-label="item.label"
166
- :aria-pressed="activeView === item.value"
167
- :title="item.label"
168
- @click="setView(item.value)"
169
- >
170
- <MindmapViewIcon :view="item.value" />
171
- </button>
172
- </nav>
173
- <span class="mindmap-preview-action-divider" aria-hidden="true" />
174
- <button
175
- v-if="activeView === 'mindmap'"
176
- type="button"
177
- class="mindmap-preview-action"
178
- aria-label="适应视口"
179
- title="适应视口"
180
- @click="viewer?.zoomToFit()"
181
- >
182
- <img :src="icon__zoom_fit" alt="" />
183
- </button>
184
- <button
185
- type="button"
186
- class="mindmap-preview-action"
187
- :aria-label="isFullscreen ? '退出全屏' : '全屏查看'"
188
- :title="isFullscreen ? '退出全屏' : '全屏查看'"
189
- @click="toggleFullscreen"
190
- >
191
- <img :src="isFullscreen ? icon__fullscreen_exit : icon__fullscreen" alt="" />
192
- </button>
193
- </div>
194
-
195
- <nav v-if="session && session.focusPath.length > 0" class="mindmap-focus-path" aria-label="当前主题路径">
196
- <button type="button" @click="exitFocusTo(0)">全部</button>
197
- <template v-for="(node, index) in session.focusPath" :key="node.id">
198
- <span aria-hidden="true">/</span>
199
- <button type="button" @click="exitFocusTo(index + 1)">{{ node.content.text }}</button>
200
- </template>
201
- </nav>
202
-
203
- <div
204
- v-show="activeView === 'mindmap'"
205
- ref="canvasHost"
206
- class="mindmap-canvas-host"
207
- :class="{ 'is-interaction-active': isCanvasActive }"
208
- @pointerdown.capture="activateCanvas"
209
- @wheel.capture="handleCanvasWheelCapture"
210
- />
211
-
212
- <div v-if="activeView === 'outline' && session" class="mindmap-outline" :data-version="renderVersion">
213
- <ul class="mindmap-outline-root">
214
- <MindmapOutlineNode
215
- :node="session.focusRootNode"
216
- :version="renderVersion"
217
- root
218
- @toggle="toggleNode"
219
- />
220
- </ul>
221
- </div>
222
-
223
- <pre v-if="activeView === 'source'" class="mindmap-source"><code>{{ normalizedContent }}</code></pre>
224
- </section>
14
+ <Mindmap
15
+ :content="content"
16
+ :source="source"
17
+ :initial-expand-level="initialExpandLevel"
18
+ :is-dark="isDark"
19
+ />
225
20
  </template>
226
-
227
- <style scoped lang="scss">
228
- .mindmap-preview {
229
- --mindmap-panel: var(--vp-c-bg-soft);
230
- --mindmap-border: var(--vp-c-divider);
231
- position: relative;
232
- margin: 1.5rem 0;
233
- overflow: hidden;
234
- border: 1px solid var(--mindmap-border);
235
- border-radius: 10px;
236
- background: var(--vp-c-bg);
237
- }
238
-
239
- .mindmap-preview-actions {
240
- position: absolute;
241
- top: 8px;
242
- right: 8px;
243
- z-index: 20;
244
- display: flex;
245
- align-items: center;
246
- gap: 4px;
247
- padding: 2px;
248
- border: .1px solid var(--vp-c-divider);
249
- border-radius: 7px;
250
- background-color: color-mix(in srgb, var(--vp-code-block-bg) 92%, transparent);
251
- box-shadow: var(--vp-shadow-1);
252
- opacity: 0;
253
- pointer-events: none;
254
- transition: opacity .2s;
255
- }
256
-
257
- .mindmap-preview-tabs {
258
- display: flex;
259
- gap: 4px;
260
- }
261
-
262
- .mindmap-preview:hover > .mindmap-preview-actions,
263
- .mindmap-preview-actions:focus-within {
264
- opacity: 1;
265
- pointer-events: auto;
266
- }
267
-
268
- .mindmap-preview-action {
269
- display: inline-flex;
270
- align-items: center;
271
- justify-content: center;
272
- width: 32px;
273
- height: 32px;
274
- padding: 0;
275
- border: 0;
276
- border-radius: 6px;
277
- background: transparent;
278
- color: var(--vp-c-brand-1);
279
- cursor: pointer;
280
- transition: background-color .2s, transform .2s;
281
-
282
- svg,
283
- img {
284
- width: 18px;
285
- height: 18px;
286
- }
287
-
288
- &:hover,
289
- &.is-active {
290
- background-color: var(--vp-c-default-soft);
291
- }
292
-
293
- &.is-active {
294
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 35%, transparent);
295
- }
296
-
297
- &:hover { transform: scale(1.05); }
298
- &:active { transform: scale(.95); }
299
- }
300
-
301
- .mindmap-preview-action-divider {
302
- width: 1px;
303
- height: 20px;
304
- margin: 0 1px;
305
- background: var(--vp-c-divider);
306
- }
307
-
308
- .mindmap-focus-path button {
309
- color: var(--vp-c-text-2);
310
- font-size: 12px;
311
-
312
- &:hover { color: var(--vp-c-brand-1); }
313
- }
314
-
315
- .mindmap-focus-path {
316
- display: flex;
317
- gap: 6px;
318
- align-items: center;
319
- padding: 6px 12px;
320
- overflow-x: auto;
321
- border-bottom: 1px solid var(--mindmap-border);
322
- white-space: nowrap;
323
- color: var(--vp-c-text-3);
324
- }
325
-
326
- .mindmap-canvas-host {
327
- position: relative;
328
- width: 100%;
329
- height: 440px;
330
- overflow: hidden;
331
- background: var(--vp-c-bg);
332
- touch-action: none;
333
- user-select: none;
334
-
335
- &.is-interaction-active {
336
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 38%, transparent);
337
- }
338
- }
339
-
340
- .mindmap-canvas-host:deep(.mm-canvas),
341
- .mindmap-canvas-host:deep(.mm-overlay) {
342
- position: absolute;
343
- inset: 0;
344
- width: 100%;
345
- height: 100%;
346
- }
347
-
348
- .mindmap-canvas-host:deep(.mm-overlay) { pointer-events: none; }
349
- .mindmap-canvas-host:deep(.mm-editor) { outline: none; }
350
-
351
- .mindmap-outline {
352
- max-height: 560px;
353
- padding: 18px 22px 22px;
354
- overflow: auto;
355
- }
356
-
357
- .mindmap-outline-root,
358
- .mindmap-outline :deep(ul) {
359
- margin: 0;
360
- padding: 0;
361
- list-style: none;
362
- }
363
-
364
- .mindmap-outline :deep(.mindmap-outline-children) {
365
- margin-left: 10px;
366
- padding-left: 19px;
367
- border-left: 1px solid var(--vp-c-divider);
368
- }
369
-
370
- .mindmap-outline :deep(.mindmap-outline-row) {
371
- display: flex;
372
- align-items: flex-start;
373
- gap: 7px;
374
- min-height: 30px;
375
- padding: 3px 0;
376
- color: var(--vp-c-text-1);
377
- line-height: 24px;
378
- }
379
-
380
- .mindmap-outline :deep(.mindmap-outline-toggle),
381
- .mindmap-outline :deep(.mindmap-outline-leaf) {
382
- flex: 0 0 18px;
383
- width: 18px;
384
- color: var(--vp-c-text-3);
385
- text-align: center;
386
- }
387
-
388
- .mindmap-outline :deep(.mindmap-outline-toggle:hover) { color: var(--vp-c-brand-1); }
389
- .mindmap-outline :deep(.mindmap-outline-checkbox) { margin-top: 5px; }
390
- .mindmap-outline :deep(.mindmap-outline-label) { min-width: 0; overflow-wrap: anywhere; }
391
- .mindmap-outline :deep(.mindmap-outline-node.is-root > .mindmap-outline-row) { font-size: 18px; font-weight: 700; }
392
- .mindmap-outline :deep(.mindmap-outline-node.is-done > .mindmap-outline-row .mindmap-outline-label) { opacity: .58; text-decoration: line-through; }
393
- .mindmap-outline :deep(.mindmap-outline-image) { display: block; max-width: min(100%, 560px); max-height: 360px; margin: 5px 0 12px 25px; border-radius: 6px; }
394
- .mindmap-outline :deep(.is-bold) { font-weight: 700; }
395
- .mindmap-outline :deep(.is-italic) { font-style: italic; }
396
- .mindmap-outline :deep(.is-underline) { text-decoration: underline; }
397
- .mindmap-outline :deep(.is-strike) { text-decoration: line-through; }
398
- .mindmap-outline :deep(.is-highlight) { padding: 0 2px; border-radius: 2px; background: #ffe56b; color: #252525; }
399
- .mindmap-outline :deep(.is-code) { padding: 1px 5px; border-radius: 4px; background: var(--vp-c-bg-soft); color: var(--vp-c-danger-1); font-family: var(--vp-font-family-mono); }
400
- .mindmap-outline :deep(.is-link) { color: var(--vp-c-brand-1); text-decoration: underline; text-underline-offset: 3px; }
401
-
402
- .mindmap-source {
403
- max-height: 560px;
404
- margin: 0;
405
- padding: 18px 22px;
406
- overflow: auto;
407
- border-radius: 0;
408
- background: var(--vp-code-block-bg);
409
- color: var(--vp-code-block-color);
410
- font-size: 13px;
411
- line-height: 1.65;
412
- white-space: pre;
413
- }
414
-
415
- .mindmap-preview:fullscreen {
416
- display: flex;
417
- width: 100%;
418
- height: 100%;
419
- margin: 0;
420
- border: 0;
421
- border-radius: 0;
422
- background: var(--vp-c-bg);
423
- flex-direction: column;
424
-
425
- .mindmap-canvas-host,
426
- .mindmap-outline,
427
- .mindmap-source {
428
- flex: 1 1 auto;
429
- max-height: none;
430
- min-height: 0;
431
- }
432
-
433
- .mindmap-canvas-host { height: auto; }
434
- }
435
-
436
- @media (max-width: 768px) {
437
- .mindmap-canvas-host { height: 360px; }
438
- .mindmap-preview-actions { top: 6px; right: 6px; }
439
- .mindmap-preview-action { width: 28px; height: 28px; }
440
- .mindmap-outline { padding-inline: 12px; }
441
- }
442
- </style>
@@ -33,7 +33,7 @@ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null
33
33
  return options
34
34
  }
35
35
 
36
- /** Parse `<<< file.md [title]`; the title is optional and paths may be quoted. */
36
+ /** @deprecated Mindmap fences no longer resolve `<<<` includes; kept for tests / migration docs. */
37
37
  export function parseMindmapReference(line: string): MindmapReference | null {
38
38
  const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
39
39
  if (!match) return null
@@ -1,42 +1,14 @@
1
- <!--
2
- vitepress/components/NotesTable/NotesTable.vue
1
+ <!--
2
+ VitePress adapter: resolves note ids via notesConfig + useData,
3
+ then renders host-neutral @tnotesjs/ui NotesTable.
3
4
  -->
4
-
5
5
  <template>
6
- <div v-if="errorMessage" class="error">
7
- {{ errorMessage }}
8
- </div>
9
-
10
- <div v-else-if="notFoundIds.length > 0" class="warning">
11
- 以下笔记 ID 未找到配置: {{ notFoundIds.join(', ') }}
12
- </div>
13
-
14
- <table v-if="tableData.length > 0" class="notesTable">
15
- <thead>
16
- <tr>
17
- <th>笔记</th>
18
- <th>简介</th>
19
- </tr>
20
- </thead>
21
- <tbody>
22
- <tr v-for="note in tableData" :key="note.id">
23
- <td>
24
- <a :href="note.url" class="noteLink">
25
- <span class="noteId">{{ note.id }}.</span>
26
- <span>{{ note.title }}</span>
27
- </a>
28
- </td>
29
- <td>
30
- <span class="description" :class="{ empty: !note.description }">
31
- {{ note.description || '暂无简介' }}
32
- </span>
33
- </td>
34
- </tr>
35
- </tbody>
36
- </table>
6
+ <NotesTable :notes="tableData" :missing-ids="notFoundIds" :error="errorMessage" />
37
7
  </template>
38
8
 
39
9
  <script setup lang="ts">
10
+ import { NotesTable } from '@tnotesjs/ui'
11
+ import type { NotesTableRow } from '@tnotesjs/ui'
40
12
  import { useData } from 'vitepress'
41
13
  import { computed } from 'vue'
42
14
 
@@ -50,7 +22,6 @@ interface Props {
50
22
  const props = defineProps<Props>()
51
23
  const vpData = useData()
52
24
 
53
- // 错误消息
54
25
  const errorMessage = computed(() => {
55
26
  if (!props.ids || !Array.isArray(props.ids)) {
56
27
  return '错误: ids 属性必须是一个数组'
@@ -61,119 +32,29 @@ const errorMessage = computed(() => {
61
32
  return null
62
33
  })
63
34
 
64
- // 未找到的笔记 ID
65
35
  const notFoundIds = computed(() => {
66
36
  if (errorMessage.value) return []
67
-
68
37
  return props.ids.filter((id) => !allNotesConfig[id])
69
38
  })
70
39
 
71
- // 表格数据
72
- const tableData = computed(() => {
40
+ const tableData = computed((): NotesTableRow[] => {
73
41
  if (errorMessage.value) return []
74
-
42
+ const base = vpData.site.value.base || '/'
75
43
  return props.ids
76
- .filter((id) => allNotesConfig[id]) // 只保留存在的笔记
44
+ .filter((id) => allNotesConfig[id])
77
45
  .map((id) => {
78
46
  const config = allNotesConfig[id]
79
- const base = vpData.site.value.base || '/'
80
-
81
- // 从 redirect 路径中提取笔记标题
82
- // redirect 格式: notes/0001. 标题/README
83
47
  let title = id
84
48
  if (config.redirect) {
85
49
  const match = config.redirect.match(/notes\/\d{4}\.\s*([^/]+)\/README/)
86
- if (match) {
87
- title = match[1]
88
- }
50
+ if (match) title = match[1]
89
51
  }
90
-
91
52
  return {
92
53
  id,
93
54
  title,
94
55
  description: config.description || '',
95
- url: config.redirect ? `${base}${config.redirect}` : '#',
56
+ url: config.redirect ? `${base}${config.redirect}` : '#'
96
57
  }
97
58
  })
98
59
  })
99
60
  </script>
100
-
101
- <style scoped lang="scss">
102
- .notesTable {
103
- width: 100%;
104
- border-collapse: collapse;
105
- margin: 1.5rem 0;
106
- font-size: 0.95rem;
107
-
108
- th,
109
- td {
110
- padding: 0.75rem 1rem;
111
- text-align: left;
112
- border: 1px solid var(--vp-c-divider);
113
- }
114
-
115
- th {
116
- background-color: var(--vp-c-bg-soft);
117
- font-weight: 600;
118
- color: var(--vp-c-text-1);
119
- }
120
-
121
- tbody tr {
122
- transition: background-color 0.2s;
123
-
124
- &:hover {
125
- background-color: var(--vp-c-bg-soft);
126
- }
127
- }
128
-
129
- td {
130
- color: var(--vp-c-text-2);
131
- }
132
-
133
- .noteLink {
134
- color: var(--vp-c-brand-1);
135
- text-decoration: none;
136
- font-weight: 500;
137
- transition: color 0.2s;
138
-
139
- &:hover {
140
- color: var(--vp-c-brand-2);
141
- text-decoration: underline;
142
- }
143
- }
144
-
145
- .noteId {
146
- font-family: var(--vp-font-family-mono);
147
- font-size: 0.9em;
148
- color: var(--vp-c-text-3);
149
- margin-right: 0.5rem;
150
- }
151
-
152
- .description {
153
- line-height: 1.6;
154
-
155
- &.empty {
156
- color: var(--vp-c-text-3);
157
- font-style: italic;
158
- }
159
- }
160
- }
161
-
162
- .error {
163
- padding: 1rem;
164
- margin: 1rem 0;
165
- border-left: 4px solid var(--vp-c-danger-1);
166
- background-color: var(--vp-c-danger-soft);
167
- color: var(--vp-c-danger-1);
168
- border-radius: 4px;
169
- }
170
-
171
- .warning {
172
- padding: 1rem;
173
- margin: 1rem 0;
174
- border-left: 4px solid var(--vp-c-warning-1);
175
- background-color: var(--vp-c-warning-soft);
176
- color: var(--vp-c-warning-1);
177
- border-radius: 4px;
178
- }
179
- </style>
@@ -108,11 +108,11 @@ export const TOC_MD: string = TOC + '.md'
108
108
  * https://github.com/tnotesjs/en-words/blob/main/{word}.md
109
109
  */
110
110
  export const EN_WORDS_REPO_BASE_URL: string =
111
- 'https://github.com/tnotesjs/TNotes.en-words/blob/main/'
111
+ 'https://github.com/tnotesjs/en-words/blob/main/'
112
112
 
113
113
  /**
114
114
  * 英语单词仓库 raw 地址
115
115
  * https://raw.githubusercontent.com/tnotesjs/TNotes.en-words/refs/heads/main/{word}.md
116
116
  */
117
117
  export const EN_WORDS_REPO_BASE_RAW_URL: string =
118
- 'https://raw.githubusercontent.com/tnotesjs/TNotes.en-words/refs/heads/main/'
118
+ 'https://raw.githubusercontent.com/tnotesjs/en-words/refs/heads/main/'
@@ -5,15 +5,19 @@
5
5
  import fs from 'node:fs'
6
6
  import path from 'node:path'
7
7
 
8
- import { TocService } from '../../services/toc/service'
8
+ import { ROOT_DIR_PATH } from '../../config/constants'
9
+ import { createWorkspace } from '../../workspace'
9
10
 
10
- const rootPath = process.cwd()
11
+ const rootPath = ROOT_DIR_PATH
11
12
  const sidebarFilePath = path.resolve(rootPath, 'sidebar.json')
12
13
  const tocFilePath = path.resolve(rootPath, 'TOC.md')
13
14
 
15
+ const workspace = createWorkspace({ rootPath })
16
+
14
17
  /**
15
18
  * VitePress Data Loader for Sidebar
16
- * 以 TOC.md 为唯一数据源,加载前重建 sidebar.json,并监听 TOC/sidebar 变化做 HMR
19
+ * 以 TOC.md 为唯一数据源,加载前用 Workspace 对齐(files→TOC)重建 sidebar.json
20
+ * 并监听 TOC/sidebar 变化做 HMR
17
21
  */
18
22
 
19
23
  interface SidebarItem {
@@ -37,8 +41,7 @@ export default {
37
41
 
38
42
  async load(): Promise<SidebarConfig> {
39
43
  try {
40
- const tocService = TocService.getInstance()
41
- await tocService.regenerateSidebar()
44
+ await workspace.toc.reconcileFromFiles()
42
45
 
43
46
  const fileContent = fs.readFileSync(sidebarFilePath, 'utf-8')
44
47
  const sidebarArray = JSON.parse(fileContent) as SidebarGroup[]