@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,644 +1,644 @@
1
- <template>
2
- <div
3
- ref="mermaidRef"
4
- class="mermaidWrapper"
5
- @mouseenter="showToolbar = true"
6
- @mouseleave="showToolbar = false"
7
- >
8
- <!-- 工具栏 -->
9
- <div
10
- v-if="!loading && !error"
11
- class="toolbar"
12
- :class="{ visible: showToolbar }"
13
- >
14
- <button
15
- @click="zoomIn"
16
- class="toolbarBtn iconBtn"
17
- title="放大 (Ctrl + +)"
18
- >
19
- <img :src="icon__zoom_in" alt="放大" class="btnIcon" />
20
- </button>
21
- <button
22
- @click="zoomOut"
23
- class="toolbarBtn iconBtn"
24
- title="缩小 (Ctrl + -)"
25
- >
26
- <img :src="icon__zoom_out" alt="缩小" class="btnIcon" />
27
- </button>
28
- <button
29
- @click="resetZoom"
30
- class="toolbarBtn iconBtn"
31
- title="重置缩放 (Ctrl + 0)"
32
- >
33
- <img :src="icon__zoom_reset" alt="重置" class="btnIcon" />
34
- </button>
35
- <button
36
- @click="fitToScreen"
37
- class="toolbarBtn iconBtn"
38
- title="适应屏幕"
39
- >
40
- <img :src="icon__zoom_fit" alt="适应屏幕" class="btnIcon" />
41
- </button>
42
- <button
43
- @click="toggleFullscreen"
44
- class="toolbarBtn iconBtn"
45
- title="全屏 (F11)"
46
- >
47
- <img
48
- v-if="isFullscreen"
49
- :src="icon__fullscreen_exit"
50
- alt="退出全屏"
51
- class="btnIcon"
52
- />
53
- <img
54
- v-else
55
- :src="icon__fullscreen"
56
- alt="全屏"
57
- class="btnIcon"
58
- />
59
- </button>
60
- <span class="zoomLevel">{{ Math.round(scale * 100) }}%</span>
61
- </div>
62
-
63
- <!-- 图表容器 -->
64
- <div
65
- class="mermaidContainer"
66
- :class="{ fullscreen: isFullscreen }"
67
- @click="handleContainerClick"
68
- >
69
- <div v-if="loading" class="mermaidLoading">
70
- <div class="spinner"></div>
71
- <p>加载图表中...</p>
72
- </div>
73
- <div v-else-if="error" class="mermaidError">
74
- <span class="errorIcon">⚠️</span>
75
- <p>{{ error }}</p>
76
- </div>
77
- <div
78
- v-show="!loading && !error"
79
- ref="diagramRef"
80
- :class="['mermaidDiagram', props.class]"
81
- :style="{
82
- transform: `scale(${scale})`,
83
- transformOrigin: 'center center',
84
- }"
85
- @wheel.prevent="handleWheel"
86
- ></div>
87
- </div>
88
- </div>
89
- </template>
90
-
91
- <script setup lang="ts">
92
- import mermaid from 'mermaid'
93
- import { nextTick, onMounted, onBeforeUnmount, ref, watch } from 'vue'
94
-
95
- import {
96
- icon__zoom_in,
97
- icon__zoom_out,
98
- icon__zoom_reset,
99
- icon__zoom_fit,
100
- icon__fullscreen,
101
- icon__fullscreen_exit,
102
- } from '../../assets/icons'
103
-
104
- const props = defineProps({
105
- graph: {
106
- type: String,
107
- required: true,
108
- },
109
- id: {
110
- type: String,
111
- required: true,
112
- },
113
- class: {
114
- type: String,
115
- default: 'mermaid',
116
- },
117
- })
118
-
119
- const mermaidRef = ref<HTMLElement | null>(null)
120
- const diagramRef = ref<HTMLElement | null>(null)
121
- const loading = ref(true)
122
- const error = ref<string | null>(null)
123
- const scale = ref(1)
124
- const isFullscreen = ref(false)
125
- const showToolbar = ref(false)
126
-
127
- // ===================================
128
- // #region 工具栏显示控制
129
- // ===================================
130
- let hideTimer: ReturnType<typeof setTimeout> | null = null
131
-
132
- function handleContainerClick() {
133
- // 移动端点击切换工具栏显示
134
- if (window.innerWidth <= 768) {
135
- showToolbar.value = !showToolbar.value
136
-
137
- // 3秒后自动隐藏
138
- if (showToolbar.value) {
139
- if (hideTimer) clearTimeout(hideTimer)
140
- hideTimer = setTimeout(() => {
141
- showToolbar.value = false
142
- }, 3000)
143
- }
144
- }
145
- }
146
- // #endregion
147
-
148
- // ===================================
149
- // #region 缩放控制
150
- // ===================================
151
- const MIN_SCALE = 0.1
152
- const MAX_SCALE = 5
153
- const SCALE_STEP = 0.1
154
-
155
- function zoomIn() {
156
- scale.value = Math.min(scale.value + SCALE_STEP, MAX_SCALE)
157
- }
158
-
159
- function zoomOut() {
160
- scale.value = Math.max(scale.value - SCALE_STEP, MIN_SCALE)
161
- }
162
-
163
- function resetZoom() {
164
- scale.value = 1
165
- }
166
-
167
- function fitToScreen() {
168
- if (!diagramRef.value || !mermaidRef.value) return
169
-
170
- const svg = diagramRef.value.querySelector('svg')
171
- if (!svg) return
172
-
173
- const containerRect = mermaidRef.value.getBoundingClientRect()
174
- const svgRect = svg.getBoundingClientRect()
175
-
176
- const scaleX = (containerRect.width * 0.9) / svgRect.width
177
- const scaleY = (containerRect.height * 0.9) / svgRect.height
178
-
179
- scale.value = Math.min(scaleX, scaleY, 1)
180
- }
181
-
182
- function handleWheel(e: WheelEvent) {
183
- if (e.ctrlKey || e.metaKey) {
184
- e.preventDefault()
185
- const delta = e.deltaY > 0 ? -SCALE_STEP : SCALE_STEP
186
- scale.value = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale.value + delta))
187
- }
188
- }
189
- // #endregion
190
-
191
- // ===================================
192
- // #region 全屏控制
193
- // ===================================
194
- function toggleFullscreen() {
195
- if (!mermaidRef.value) return
196
-
197
- if (!isFullscreen.value) {
198
- if (mermaidRef.value.requestFullscreen) {
199
- mermaidRef.value.requestFullscreen()
200
- }
201
- } else {
202
- if (document.exitFullscreen) {
203
- document.exitFullscreen()
204
- }
205
- }
206
- }
207
-
208
- function handleFullscreenChange() {
209
- isFullscreen.value = !!document.fullscreenElement
210
- }
211
- // #endregion
212
-
213
- // ===================================
214
- // #region 键盘快捷键
215
- // ===================================
216
- function handleKeydown(e: KeyboardEvent) {
217
- if (e.ctrlKey || e.metaKey) {
218
- if (e.key === '=' || e.key === '+') {
219
- e.preventDefault()
220
- zoomIn()
221
- } else if (e.key === '-' || e.key === '_') {
222
- e.preventDefault()
223
- zoomOut()
224
- } else if (e.key === '0') {
225
- e.preventDefault()
226
- resetZoom()
227
- }
228
- } else if (e.key === 'F11') {
229
- e.preventDefault()
230
- toggleFullscreen()
231
- } else if (e.key === 'Escape' && isFullscreen.value) {
232
- toggleFullscreen()
233
- }
234
- }
235
- // #endregion
236
-
237
- // ===================================
238
- // #region Mermaid 渲染
239
- // ===================================
240
- // 获取当前主题
241
- const getCurrentTheme = () => {
242
- return document.documentElement.classList.contains('dark')
243
- ? 'dark'
244
- : 'default'
245
- }
246
-
247
- // 初始化 Mermaid
248
- const initMermaid = async () => {
249
- try {
250
- mermaid.initialize({
251
- startOnLoad: false,
252
- theme: getCurrentTheme(),
253
- securityLevel: 'loose',
254
- fontFamily: 'inherit',
255
- })
256
- } catch (err) {
257
- console.error('Mermaid initialization error:', err)
258
- }
259
- }
260
-
261
- // 渲染图表
262
- const renderDiagram = async () => {
263
- // 等待下一个 DOM 更新周期
264
- await nextTick()
265
-
266
- if (!diagramRef.value) {
267
- // 再等待一小段时间确保 DOM 完全渲染
268
- await new Promise((resolve) => setTimeout(resolve, 0))
269
- if (!diagramRef.value) {
270
- // console.warn('diagramRef is still null')
271
- return
272
- }
273
- }
274
-
275
- try {
276
- loading.value = true
277
- error.value = null
278
-
279
- // 每次渲染前都重新初始化主题
280
- mermaid.initialize({
281
- startOnLoad: false,
282
- theme: getCurrentTheme(),
283
- securityLevel: 'loose',
284
- fontFamily: 'inherit',
285
- })
286
-
287
- const { svg, bindFunctions } = await mermaid.render(
288
- props.id,
289
- decodeURIComponent(props.graph)
290
- )
291
-
292
- diagramRef.value.innerHTML = svg
293
-
294
- // 绑定交互函数(如果有)
295
- if (bindFunctions) {
296
- bindFunctions(diagramRef.value)
297
- }
298
- } catch (err) {
299
- const message = err instanceof Error ? err.message : String(err)
300
- error.value = `Failed to render diagram: ${message}`
301
- console.error('Mermaid render error:', err)
302
- } finally {
303
- loading.value = false
304
- }
305
- }
306
-
307
- // 监听主题变化
308
- const observeTheme = () => {
309
- const observer = new MutationObserver((mutations) => {
310
- mutations.forEach((mutation) => {
311
- if (
312
- mutation.type === 'attributes' &&
313
- mutation.attributeName === 'class'
314
- ) {
315
- // 检查是否是主题变化
316
- const isDark = document.documentElement.classList.contains('dark')
317
- const wasDark = mutation.oldValue?.includes('dark')
318
-
319
- // 只有当主题实际发生变化时才重新渲染
320
- if ((isDark && !wasDark) || (!isDark && wasDark)) {
321
- renderDiagram()
322
- }
323
- }
324
- })
325
- })
326
-
327
- observer.observe(document.documentElement, {
328
- attributes: true,
329
- attributeFilter: ['class'],
330
- attributeOldValue: true,
331
- })
332
-
333
- return observer
334
- }
335
-
336
- onMounted(async () => {
337
- await initMermaid()
338
- await renderDiagram()
339
-
340
- // 观察主题变化
341
- const themeObserver = observeTheme()
342
-
343
- // 监听键盘事件
344
- document.addEventListener('keydown', handleKeydown)
345
-
346
- // 监听全屏变化
347
- document.addEventListener('fullscreenchange', handleFullscreenChange)
348
-
349
- // 清理函数
350
- onBeforeUnmount(() => {
351
- themeObserver.disconnect()
352
- document.removeEventListener('keydown', handleKeydown)
353
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
354
- })
355
- })
356
-
357
- // 当图表内容变化时重新渲染并重置缩放
358
- watch(
359
- () => props.graph,
360
- () => {
361
- renderDiagram()
362
- resetZoom()
363
- }
364
- )
365
- // #endregion
366
- </script>
367
-
368
- <style scoped lang="scss">
369
- /* ===================================== */
370
- /* #region 容器布局 */
371
- /* ===================================== */
372
- .mermaidWrapper {
373
- position: relative;
374
- margin: 1rem 0;
375
- border: 1px solid var(--vp-c-divider);
376
- border-radius: 8px;
377
- background: var(--vp-c-bg-soft);
378
- overflow: hidden;
379
- }
380
- /* ===================================== */
381
- /* #endregion 容器布局 */
382
- /* ===================================== */
383
-
384
- /* ===================================== */
385
- /* #region 工具栏 */
386
- /* ===================================== */
387
- .toolbar {
388
- position: absolute;
389
- top: 0;
390
- left: 0;
391
- right: 0;
392
- display: flex;
393
- align-items: center;
394
- gap: 2px;
395
- padding: 6px 8px;
396
- background: var(--vp-c-bg);
397
- border-bottom: 1px solid var(--vp-c-divider);
398
- flex-wrap: wrap;
399
- transform: translateY(-100%);
400
- transition: transform 0.3s ease, opacity 0.3s ease;
401
- opacity: 0;
402
- z-index: 10;
403
- pointer-events: none;
404
-
405
- &.visible {
406
- transform: translateY(0);
407
- opacity: 1;
408
- pointer-events: auto;
409
- }
410
- }
411
-
412
- .toolbarBtn {
413
- display: inline-flex;
414
- align-items: center;
415
- justify-content: center;
416
- width: 28px;
417
- height: 28px;
418
- padding: 0;
419
- font-size: 12px;
420
- background: transparent;
421
- border: none;
422
- border-radius: 4px;
423
- cursor: pointer;
424
- transition: all 0.2s ease;
425
- opacity: 0.6;
426
-
427
- &:hover {
428
- background: var(--vp-c-brand-soft);
429
- opacity: 1;
430
- }
431
-
432
- &:active {
433
- transform: scale(0.95);
434
- }
435
- }
436
-
437
- .iconBtn {
438
- padding: 4px;
439
-
440
- .btnIcon {
441
- width: 16px;
442
- height: 16px;
443
- display: block;
444
- pointer-events: none;
445
- }
446
- }
447
-
448
- .zoomLevel {
449
- margin-left: auto;
450
- padding: 0 6px;
451
- font-size: 11px;
452
- font-weight: 500;
453
- color: var(--vp-c-text-3);
454
- user-select: none;
455
- }
456
- /* ===================================== */
457
- /* #endregion 工具栏 */
458
- /* ===================================== */
459
-
460
- /* ===================================== */
461
- /* #region 图表容器 */
462
- /* ===================================== */
463
- .mermaidContainer {
464
- position: relative;
465
- min-height: 200px;
466
- max-height: 600px;
467
- overflow: auto;
468
- display: flex;
469
- align-items: center;
470
- justify-content: center;
471
- padding: 20px;
472
- background: var(--vp-c-bg);
473
-
474
- &.fullscreen {
475
- position: fixed;
476
- top: 0;
477
- left: 0;
478
- right: 0;
479
- bottom: 0;
480
- max-height: none;
481
- z-index: 9999;
482
- background: var(--vp-c-bg);
483
- padding: 40px;
484
- }
485
-
486
- /* 自定义滚动条 */
487
- &::-webkit-scrollbar {
488
- width: 8px;
489
- height: 8px;
490
- }
491
-
492
- &::-webkit-scrollbar-track {
493
- background: var(--vp-c-bg-soft);
494
- }
495
-
496
- &::-webkit-scrollbar-thumb {
497
- background: var(--vp-c-divider);
498
- border-radius: 4px;
499
-
500
- &:hover {
501
- background: var(--vp-c-text-3);
502
- }
503
- }
504
- }
505
- /* ===================================== */
506
- /* #endregion 图表容器 */
507
- /* ===================================== */
508
-
509
- /* ===================================== */
510
- /* #region 加载和错误状态 */
511
- /* ===================================== */
512
- .mermaidLoading {
513
- display: flex;
514
- flex-direction: column;
515
- align-items: center;
516
- gap: 12px;
517
- padding: 2rem;
518
- color: var(--vp-c-text-2);
519
-
520
- p {
521
- margin: 0;
522
- font-size: 14px;
523
- }
524
- }
525
-
526
- .spinner {
527
- width: 40px;
528
- height: 40px;
529
- border: 3px solid var(--vp-c-divider);
530
- border-top-color: var(--vp-c-brand-1);
531
- border-radius: 50%;
532
- animation: spin 0.8s linear infinite;
533
- }
534
-
535
- @keyframes spin {
536
- to {
537
- transform: rotate(360deg);
538
- }
539
- }
540
-
541
- .mermaidError {
542
- display: flex;
543
- flex-direction: column;
544
- align-items: center;
545
- gap: 8px;
546
- padding: 2rem;
547
- color: var(--vp-c-danger-1);
548
-
549
- p {
550
- margin: 0;
551
- font-size: 14px;
552
- text-align: center;
553
- }
554
- }
555
-
556
- .errorIcon {
557
- font-size: 32px;
558
- }
559
- /* ===================================== */
560
- /* #endregion 加载和错误状态 */
561
- /* ===================================== */
562
-
563
- /* ===================================== */
564
- /* #region 图表样式 */
565
- /* ===================================== */
566
- .mermaidDiagram {
567
- width: 100%;
568
- display: flex;
569
- align-items: center;
570
- justify-content: center;
571
- transition: transform 0.2s ease;
572
- cursor: grab;
573
-
574
- &:active {
575
- cursor: grabbing;
576
- }
577
-
578
- :global(svg) {
579
- max-width: 100%;
580
- height: auto;
581
- }
582
- }
583
- /* ===================================== */
584
- /* #endregion 图表样式 */
585
- /* ===================================== */
586
-
587
- /* ===================================== */
588
- /* #region 暗色主题优化 */
589
- /* ===================================== */
590
- :global(html.dark) .mermaidWrapper {
591
- background: var(--vp-code-block-bg);
592
- border-color: var(--vp-c-divider);
593
- }
594
-
595
- :global(html.dark) .toolbar {
596
- background: var(--vp-code-block-bg);
597
- }
598
-
599
- :global(html.dark) .mermaidContainer {
600
- background: var(--vp-code-block-bg);
601
- }
602
- /* ===================================== */
603
- /* #endregion 暗色主题优化 */
604
- /* ===================================== */
605
-
606
- /* ===================================== */
607
- /* #region 响应式设计 */
608
- /* ===================================== */
609
- @media (max-width: 768px) {
610
- .toolbar {
611
- padding: 4px 6px;
612
- }
613
-
614
- .toolbarBtn {
615
- width: 24px;
616
- height: 24px;
617
- }
618
-
619
- .iconBtn {
620
- padding: 3px;
621
-
622
- .btnIcon {
623
- width: 14px;
624
- height: 14px;
625
- }
626
- }
627
-
628
- .zoomLevel {
629
- font-size: 10px;
630
- }
631
-
632
- .mermaidContainer {
633
- padding: 12px;
634
- max-height: 400px;
635
-
636
- &.fullscreen {
637
- padding: 20px;
638
- }
639
- }
640
- }
641
- /* ===================================== */
642
- /* #endregion 响应式设计 */
643
- /* ===================================== */
644
- </style>
1
+ <template>
2
+ <div
3
+ ref="mermaidRef"
4
+ class="mermaidWrapper"
5
+ @mouseenter="showToolbar = true"
6
+ @mouseleave="showToolbar = false"
7
+ >
8
+ <!-- 工具栏 -->
9
+ <div
10
+ v-if="!loading && !error"
11
+ class="toolbar"
12
+ :class="{ visible: showToolbar }"
13
+ >
14
+ <button
15
+ @click="zoomIn"
16
+ class="toolbarBtn iconBtn"
17
+ title="放大 (Ctrl + +)"
18
+ >
19
+ <img :src="icon__zoom_in" alt="放大" class="btnIcon" />
20
+ </button>
21
+ <button
22
+ @click="zoomOut"
23
+ class="toolbarBtn iconBtn"
24
+ title="缩小 (Ctrl + -)"
25
+ >
26
+ <img :src="icon__zoom_out" alt="缩小" class="btnIcon" />
27
+ </button>
28
+ <button
29
+ @click="resetZoom"
30
+ class="toolbarBtn iconBtn"
31
+ title="重置缩放 (Ctrl + 0)"
32
+ >
33
+ <img :src="icon__zoom_reset" alt="重置" class="btnIcon" />
34
+ </button>
35
+ <button
36
+ @click="fitToScreen"
37
+ class="toolbarBtn iconBtn"
38
+ title="适应屏幕"
39
+ >
40
+ <img :src="icon__zoom_fit" alt="适应屏幕" class="btnIcon" />
41
+ </button>
42
+ <button
43
+ @click="toggleFullscreen"
44
+ class="toolbarBtn iconBtn"
45
+ title="全屏 (F11)"
46
+ >
47
+ <img
48
+ v-if="isFullscreen"
49
+ :src="icon__fullscreen_exit"
50
+ alt="退出全屏"
51
+ class="btnIcon"
52
+ />
53
+ <img
54
+ v-else
55
+ :src="icon__fullscreen"
56
+ alt="全屏"
57
+ class="btnIcon"
58
+ />
59
+ </button>
60
+ <span class="zoomLevel">{{ Math.round(scale * 100) }}%</span>
61
+ </div>
62
+
63
+ <!-- 图表容器 -->
64
+ <div
65
+ class="mermaidContainer"
66
+ :class="{ fullscreen: isFullscreen }"
67
+ @click="handleContainerClick"
68
+ >
69
+ <div v-if="loading" class="mermaidLoading">
70
+ <div class="spinner"></div>
71
+ <p>加载图表中...</p>
72
+ </div>
73
+ <div v-else-if="error" class="mermaidError">
74
+ <span class="errorIcon">⚠️</span>
75
+ <p>{{ error }}</p>
76
+ </div>
77
+ <div
78
+ v-show="!loading && !error"
79
+ ref="diagramRef"
80
+ :class="['mermaidDiagram', props.class]"
81
+ :style="{
82
+ transform: `scale(${scale})`,
83
+ transformOrigin: 'center center',
84
+ }"
85
+ @wheel.prevent="handleWheel"
86
+ ></div>
87
+ </div>
88
+ </div>
89
+ </template>
90
+
91
+ <script setup lang="ts">
92
+ import mermaid from 'mermaid'
93
+ import { nextTick, onMounted, onBeforeUnmount, ref, watch } from 'vue'
94
+
95
+ import {
96
+ icon__zoom_in,
97
+ icon__zoom_out,
98
+ icon__zoom_reset,
99
+ icon__zoom_fit,
100
+ icon__fullscreen,
101
+ icon__fullscreen_exit,
102
+ } from '../../assets/icons'
103
+
104
+ const props = defineProps({
105
+ graph: {
106
+ type: String,
107
+ required: true,
108
+ },
109
+ id: {
110
+ type: String,
111
+ required: true,
112
+ },
113
+ class: {
114
+ type: String,
115
+ default: 'mermaid',
116
+ },
117
+ })
118
+
119
+ const mermaidRef = ref<HTMLElement | null>(null)
120
+ const diagramRef = ref<HTMLElement | null>(null)
121
+ const loading = ref(true)
122
+ const error = ref<string | null>(null)
123
+ const scale = ref(1)
124
+ const isFullscreen = ref(false)
125
+ const showToolbar = ref(false)
126
+
127
+ // ===================================
128
+ // #region 工具栏显示控制
129
+ // ===================================
130
+ let hideTimer: ReturnType<typeof setTimeout> | null = null
131
+
132
+ function handleContainerClick() {
133
+ // 移动端点击切换工具栏显示
134
+ if (window.innerWidth <= 768) {
135
+ showToolbar.value = !showToolbar.value
136
+
137
+ // 3秒后自动隐藏
138
+ if (showToolbar.value) {
139
+ if (hideTimer) clearTimeout(hideTimer)
140
+ hideTimer = setTimeout(() => {
141
+ showToolbar.value = false
142
+ }, 3000)
143
+ }
144
+ }
145
+ }
146
+ // #endregion
147
+
148
+ // ===================================
149
+ // #region 缩放控制
150
+ // ===================================
151
+ const MIN_SCALE = 0.1
152
+ const MAX_SCALE = 5
153
+ const SCALE_STEP = 0.1
154
+
155
+ function zoomIn() {
156
+ scale.value = Math.min(scale.value + SCALE_STEP, MAX_SCALE)
157
+ }
158
+
159
+ function zoomOut() {
160
+ scale.value = Math.max(scale.value - SCALE_STEP, MIN_SCALE)
161
+ }
162
+
163
+ function resetZoom() {
164
+ scale.value = 1
165
+ }
166
+
167
+ function fitToScreen() {
168
+ if (!diagramRef.value || !mermaidRef.value) return
169
+
170
+ const svg = diagramRef.value.querySelector('svg')
171
+ if (!svg) return
172
+
173
+ const containerRect = mermaidRef.value.getBoundingClientRect()
174
+ const svgRect = svg.getBoundingClientRect()
175
+
176
+ const scaleX = (containerRect.width * 0.9) / svgRect.width
177
+ const scaleY = (containerRect.height * 0.9) / svgRect.height
178
+
179
+ scale.value = Math.min(scaleX, scaleY, 1)
180
+ }
181
+
182
+ function handleWheel(e: WheelEvent) {
183
+ if (e.ctrlKey || e.metaKey) {
184
+ e.preventDefault()
185
+ const delta = e.deltaY > 0 ? -SCALE_STEP : SCALE_STEP
186
+ scale.value = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale.value + delta))
187
+ }
188
+ }
189
+ // #endregion
190
+
191
+ // ===================================
192
+ // #region 全屏控制
193
+ // ===================================
194
+ function toggleFullscreen() {
195
+ if (!mermaidRef.value) return
196
+
197
+ if (!isFullscreen.value) {
198
+ if (mermaidRef.value.requestFullscreen) {
199
+ mermaidRef.value.requestFullscreen()
200
+ }
201
+ } else {
202
+ if (document.exitFullscreen) {
203
+ document.exitFullscreen()
204
+ }
205
+ }
206
+ }
207
+
208
+ function handleFullscreenChange() {
209
+ isFullscreen.value = !!document.fullscreenElement
210
+ }
211
+ // #endregion
212
+
213
+ // ===================================
214
+ // #region 键盘快捷键
215
+ // ===================================
216
+ function handleKeydown(e: KeyboardEvent) {
217
+ if (e.ctrlKey || e.metaKey) {
218
+ if (e.key === '=' || e.key === '+') {
219
+ e.preventDefault()
220
+ zoomIn()
221
+ } else if (e.key === '-' || e.key === '_') {
222
+ e.preventDefault()
223
+ zoomOut()
224
+ } else if (e.key === '0') {
225
+ e.preventDefault()
226
+ resetZoom()
227
+ }
228
+ } else if (e.key === 'F11') {
229
+ e.preventDefault()
230
+ toggleFullscreen()
231
+ } else if (e.key === 'Escape' && isFullscreen.value) {
232
+ toggleFullscreen()
233
+ }
234
+ }
235
+ // #endregion
236
+
237
+ // ===================================
238
+ // #region Mermaid 渲染
239
+ // ===================================
240
+ // 获取当前主题
241
+ const getCurrentTheme = () => {
242
+ return document.documentElement.classList.contains('dark')
243
+ ? 'dark'
244
+ : 'default'
245
+ }
246
+
247
+ // 初始化 Mermaid
248
+ const initMermaid = async () => {
249
+ try {
250
+ mermaid.initialize({
251
+ startOnLoad: false,
252
+ theme: getCurrentTheme(),
253
+ securityLevel: 'loose',
254
+ fontFamily: 'inherit',
255
+ })
256
+ } catch (err) {
257
+ console.error('Mermaid initialization error:', err)
258
+ }
259
+ }
260
+
261
+ // 渲染图表
262
+ const renderDiagram = async () => {
263
+ // 等待下一个 DOM 更新周期
264
+ await nextTick()
265
+
266
+ if (!diagramRef.value) {
267
+ // 再等待一小段时间确保 DOM 完全渲染
268
+ await new Promise((resolve) => setTimeout(resolve, 0))
269
+ if (!diagramRef.value) {
270
+ // console.warn('diagramRef is still null')
271
+ return
272
+ }
273
+ }
274
+
275
+ try {
276
+ loading.value = true
277
+ error.value = null
278
+
279
+ // 每次渲染前都重新初始化主题
280
+ mermaid.initialize({
281
+ startOnLoad: false,
282
+ theme: getCurrentTheme(),
283
+ securityLevel: 'loose',
284
+ fontFamily: 'inherit',
285
+ })
286
+
287
+ const { svg, bindFunctions } = await mermaid.render(
288
+ props.id,
289
+ decodeURIComponent(props.graph)
290
+ )
291
+
292
+ diagramRef.value.innerHTML = svg
293
+
294
+ // 绑定交互函数(如果有)
295
+ if (bindFunctions) {
296
+ bindFunctions(diagramRef.value)
297
+ }
298
+ } catch (err) {
299
+ const message = err instanceof Error ? err.message : String(err)
300
+ error.value = `Failed to render diagram: ${message}`
301
+ console.error('Mermaid render error:', err)
302
+ } finally {
303
+ loading.value = false
304
+ }
305
+ }
306
+
307
+ // 监听主题变化
308
+ const observeTheme = () => {
309
+ const observer = new MutationObserver((mutations) => {
310
+ mutations.forEach((mutation) => {
311
+ if (
312
+ mutation.type === 'attributes' &&
313
+ mutation.attributeName === 'class'
314
+ ) {
315
+ // 检查是否是主题变化
316
+ const isDark = document.documentElement.classList.contains('dark')
317
+ const wasDark = mutation.oldValue?.includes('dark')
318
+
319
+ // 只有当主题实际发生变化时才重新渲染
320
+ if ((isDark && !wasDark) || (!isDark && wasDark)) {
321
+ renderDiagram()
322
+ }
323
+ }
324
+ })
325
+ })
326
+
327
+ observer.observe(document.documentElement, {
328
+ attributes: true,
329
+ attributeFilter: ['class'],
330
+ attributeOldValue: true,
331
+ })
332
+
333
+ return observer
334
+ }
335
+
336
+ onMounted(async () => {
337
+ await initMermaid()
338
+ await renderDiagram()
339
+
340
+ // 观察主题变化
341
+ const themeObserver = observeTheme()
342
+
343
+ // 监听键盘事件
344
+ document.addEventListener('keydown', handleKeydown)
345
+
346
+ // 监听全屏变化
347
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
348
+
349
+ // 清理函数
350
+ onBeforeUnmount(() => {
351
+ themeObserver.disconnect()
352
+ document.removeEventListener('keydown', handleKeydown)
353
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
354
+ })
355
+ })
356
+
357
+ // 当图表内容变化时重新渲染并重置缩放
358
+ watch(
359
+ () => props.graph,
360
+ () => {
361
+ renderDiagram()
362
+ resetZoom()
363
+ }
364
+ )
365
+ // #endregion
366
+ </script>
367
+
368
+ <style scoped lang="scss">
369
+ /* ===================================== */
370
+ /* #region 容器布局 */
371
+ /* ===================================== */
372
+ .mermaidWrapper {
373
+ position: relative;
374
+ margin: 1rem 0;
375
+ border: 1px solid var(--vp-c-divider);
376
+ border-radius: 8px;
377
+ background: var(--vp-c-bg-soft);
378
+ overflow: hidden;
379
+ }
380
+ /* ===================================== */
381
+ /* #endregion 容器布局 */
382
+ /* ===================================== */
383
+
384
+ /* ===================================== */
385
+ /* #region 工具栏 */
386
+ /* ===================================== */
387
+ .toolbar {
388
+ position: absolute;
389
+ top: 0;
390
+ left: 0;
391
+ right: 0;
392
+ display: flex;
393
+ align-items: center;
394
+ gap: 2px;
395
+ padding: 6px 8px;
396
+ background: var(--vp-c-bg);
397
+ border-bottom: 1px solid var(--vp-c-divider);
398
+ flex-wrap: wrap;
399
+ transform: translateY(-100%);
400
+ transition: transform 0.3s ease, opacity 0.3s ease;
401
+ opacity: 0;
402
+ z-index: 10;
403
+ pointer-events: none;
404
+
405
+ &.visible {
406
+ transform: translateY(0);
407
+ opacity: 1;
408
+ pointer-events: auto;
409
+ }
410
+ }
411
+
412
+ .toolbarBtn {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ width: 28px;
417
+ height: 28px;
418
+ padding: 0;
419
+ font-size: 12px;
420
+ background: transparent;
421
+ border: none;
422
+ border-radius: 4px;
423
+ cursor: pointer;
424
+ transition: all 0.2s ease;
425
+ opacity: 0.6;
426
+
427
+ &:hover {
428
+ background: var(--vp-c-brand-soft);
429
+ opacity: 1;
430
+ }
431
+
432
+ &:active {
433
+ transform: scale(0.95);
434
+ }
435
+ }
436
+
437
+ .iconBtn {
438
+ padding: 4px;
439
+
440
+ .btnIcon {
441
+ width: 16px;
442
+ height: 16px;
443
+ display: block;
444
+ pointer-events: none;
445
+ }
446
+ }
447
+
448
+ .zoomLevel {
449
+ margin-left: auto;
450
+ padding: 0 6px;
451
+ font-size: 11px;
452
+ font-weight: 500;
453
+ color: var(--vp-c-text-3);
454
+ user-select: none;
455
+ }
456
+ /* ===================================== */
457
+ /* #endregion 工具栏 */
458
+ /* ===================================== */
459
+
460
+ /* ===================================== */
461
+ /* #region 图表容器 */
462
+ /* ===================================== */
463
+ .mermaidContainer {
464
+ position: relative;
465
+ min-height: 200px;
466
+ max-height: 600px;
467
+ overflow: auto;
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ padding: 20px;
472
+ background: var(--vp-c-bg);
473
+
474
+ &.fullscreen {
475
+ position: fixed;
476
+ top: 0;
477
+ left: 0;
478
+ right: 0;
479
+ bottom: 0;
480
+ max-height: none;
481
+ z-index: 9999;
482
+ background: var(--vp-c-bg);
483
+ padding: 40px;
484
+ }
485
+
486
+ /* 自定义滚动条 */
487
+ &::-webkit-scrollbar {
488
+ width: 8px;
489
+ height: 8px;
490
+ }
491
+
492
+ &::-webkit-scrollbar-track {
493
+ background: var(--vp-c-bg-soft);
494
+ }
495
+
496
+ &::-webkit-scrollbar-thumb {
497
+ background: var(--vp-c-divider);
498
+ border-radius: 4px;
499
+
500
+ &:hover {
501
+ background: var(--vp-c-text-3);
502
+ }
503
+ }
504
+ }
505
+ /* ===================================== */
506
+ /* #endregion 图表容器 */
507
+ /* ===================================== */
508
+
509
+ /* ===================================== */
510
+ /* #region 加载和错误状态 */
511
+ /* ===================================== */
512
+ .mermaidLoading {
513
+ display: flex;
514
+ flex-direction: column;
515
+ align-items: center;
516
+ gap: 12px;
517
+ padding: 2rem;
518
+ color: var(--vp-c-text-2);
519
+
520
+ p {
521
+ margin: 0;
522
+ font-size: 14px;
523
+ }
524
+ }
525
+
526
+ .spinner {
527
+ width: 40px;
528
+ height: 40px;
529
+ border: 3px solid var(--vp-c-divider);
530
+ border-top-color: var(--vp-c-brand-1);
531
+ border-radius: 50%;
532
+ animation: spin 0.8s linear infinite;
533
+ }
534
+
535
+ @keyframes spin {
536
+ to {
537
+ transform: rotate(360deg);
538
+ }
539
+ }
540
+
541
+ .mermaidError {
542
+ display: flex;
543
+ flex-direction: column;
544
+ align-items: center;
545
+ gap: 8px;
546
+ padding: 2rem;
547
+ color: var(--vp-c-danger-1);
548
+
549
+ p {
550
+ margin: 0;
551
+ font-size: 14px;
552
+ text-align: center;
553
+ }
554
+ }
555
+
556
+ .errorIcon {
557
+ font-size: 32px;
558
+ }
559
+ /* ===================================== */
560
+ /* #endregion 加载和错误状态 */
561
+ /* ===================================== */
562
+
563
+ /* ===================================== */
564
+ /* #region 图表样式 */
565
+ /* ===================================== */
566
+ .mermaidDiagram {
567
+ width: 100%;
568
+ display: flex;
569
+ align-items: center;
570
+ justify-content: center;
571
+ transition: transform 0.2s ease;
572
+ cursor: grab;
573
+
574
+ &:active {
575
+ cursor: grabbing;
576
+ }
577
+
578
+ :global(svg) {
579
+ max-width: 100%;
580
+ height: auto;
581
+ }
582
+ }
583
+ /* ===================================== */
584
+ /* #endregion 图表样式 */
585
+ /* ===================================== */
586
+
587
+ /* ===================================== */
588
+ /* #region 暗色主题优化 */
589
+ /* ===================================== */
590
+ :global(html.dark) .mermaidWrapper {
591
+ background: var(--vp-code-block-bg);
592
+ border-color: var(--vp-c-divider);
593
+ }
594
+
595
+ :global(html.dark) .toolbar {
596
+ background: var(--vp-code-block-bg);
597
+ }
598
+
599
+ :global(html.dark) .mermaidContainer {
600
+ background: var(--vp-code-block-bg);
601
+ }
602
+ /* ===================================== */
603
+ /* #endregion 暗色主题优化 */
604
+ /* ===================================== */
605
+
606
+ /* ===================================== */
607
+ /* #region 响应式设计 */
608
+ /* ===================================== */
609
+ @media (max-width: 768px) {
610
+ .toolbar {
611
+ padding: 4px 6px;
612
+ }
613
+
614
+ .toolbarBtn {
615
+ width: 24px;
616
+ height: 24px;
617
+ }
618
+
619
+ .iconBtn {
620
+ padding: 3px;
621
+
622
+ .btnIcon {
623
+ width: 14px;
624
+ height: 14px;
625
+ }
626
+ }
627
+
628
+ .zoomLevel {
629
+ font-size: 10px;
630
+ }
631
+
632
+ .mermaidContainer {
633
+ padding: 12px;
634
+ max-height: 400px;
635
+
636
+ &.fullscreen {
637
+ padding: 20px;
638
+ }
639
+ }
640
+ }
641
+ /* ===================================== */
642
+ /* #endregion 响应式设计 */
643
+ /* ===================================== */
644
+ </style>