@tnotesjs/core 0.1.21 → 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 (122) hide show
  1. package/README.md +87 -87
  2. package/dist/{chunk-4MO7F7Q4.js → chunk-BSTQOJVE.js} +1 -1
  3. package/dist/{chunk-SQVHJR2Z.js → chunk-O4DCXHOV.js} +0 -3
  4. package/dist/cli/index.js +2 -2
  5. package/dist/index.js +1 -1
  6. package/dist/vitepress/config/index.js +2 -2
  7. package/package.json +3 -4
  8. package/types/config.ts +60 -61
  9. package/types/index.ts +11 -11
  10. package/types/note.ts +33 -33
  11. package/vitepress/assets/icons/icon__check.svg +3 -3
  12. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  13. package/vitepress/assets/icons/icon__close.svg +1 -1
  14. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  15. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  16. package/vitepress/assets/icons/icon__copy.svg +4 -4
  17. package/vitepress/assets/icons/icon__focus.svg +1 -1
  18. package/vitepress/assets/icons/icon__fold.svg +3 -3
  19. package/vitepress/assets/icons/icon__folder.svg +1 -1
  20. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  21. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  22. package/vitepress/assets/icons/icon__github.svg +3 -3
  23. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  24. package/vitepress/assets/icons/icon__next.svg +1 -1
  25. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  26. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  27. package/vitepress/assets/icons/icon__prev.svg +1 -1
  28. package/vitepress/assets/icons/icon__restore.svg +1 -1
  29. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  30. package/vitepress/assets/icons/icon__search.svg +1 -1
  31. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  32. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  33. package/vitepress/assets/icons/icon__totop.svg +5 -5
  34. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  35. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  37. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  38. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  39. package/vitepress/assets/icons/index.ts +39 -39
  40. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  41. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  42. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -213
  43. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  44. package/vitepress/components/Discussions/Discussions.vue +244 -211
  45. package/vitepress/components/EnWordList/EnWordList.vue +663 -543
  46. package/vitepress/components/EnWordList/RightClickMenu.vue +89 -66
  47. package/vitepress/components/Footprints/Footprints.vue +472 -377
  48. package/vitepress/components/Layout/AboutModal.vue +331 -105
  49. package/vitepress/components/Layout/AboutPanel.vue +484 -270
  50. package/vitepress/components/Layout/ContentCollapse.vue +590 -603
  51. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  52. package/vitepress/components/Layout/DocBeforeControls.vue +351 -149
  53. package/vitepress/components/Layout/DocFooter.vue +233 -233
  54. package/vitepress/components/Layout/ImagePreview.vue +484 -281
  55. package/vitepress/components/Layout/Layout.vue +744 -621
  56. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +0 -2
  57. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  58. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  59. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -171
  60. package/vitepress/components/Layout/SidebarResizeHandle.vue +311 -319
  61. package/vitepress/components/Layout/Swiper.vue +167 -167
  62. package/vitepress/components/Layout/ToggleFullContent.vue +47 -35
  63. package/vitepress/components/Layout/ToggleSidebar.vue +37 -26
  64. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  65. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  66. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  67. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  68. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  69. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  70. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  71. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  72. package/vitepress/components/LoadingPage/LoadingPage.vue +1 -6
  73. package/vitepress/components/MarkMap/MarkMap.vue +564 -405
  74. package/vitepress/components/Mermaid/Mermaid.vue +643 -365
  75. package/vitepress/components/NotesTable/NotesTable.vue +174 -98
  76. package/vitepress/components/NotesTable/README.md +67 -67
  77. package/vitepress/components/Settings/Settings.vue +711 -334
  78. package/vitepress/components/Settings/SettingsDialog.vue +1 -11
  79. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  80. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  81. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -946
  82. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  83. package/vitepress/components/constants.ts +109 -109
  84. package/vitepress/components/notesConfig.data.ts +73 -73
  85. package/vitepress/components/sidebar.data.ts +59 -59
  86. package/vitepress/components/tnotes-config.data.ts +21 -21
  87. package/vitepress/components/utils.ts +26 -26
  88. package/vitepress/config/index.ts +169 -169
  89. package/vitepress/configs/constants.ts +26 -26
  90. package/vitepress/configs/head.config.ts +25 -25
  91. package/vitepress/configs/index.ts +9 -9
  92. package/vitepress/configs/markdown-it.d.ts +23 -23
  93. package/vitepress/configs/markdown.config.ts +368 -367
  94. package/vitepress/configs/theme.config.ts +108 -108
  95. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  96. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  97. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  98. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  99. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  100. package/vitepress/theme/index.ts +137 -137
  101. package/vitepress/theme/styles/base.scss +66 -64
  102. package/vitepress/theme/styles/components/404.scss +31 -31
  103. package/vitepress/theme/styles/components/collapse.scss +172 -172
  104. package/vitepress/theme/styles/components/markmap.scss +101 -101
  105. package/vitepress/theme/styles/components/swiper.scss +255 -255
  106. package/vitepress/theme/styles/index.scss +25 -25
  107. package/vitepress/theme/styles/layout.scss +78 -78
  108. package/vitepress/theme/styles/utilities.scss +39 -39
  109. package/vitepress/theme/styles/vitepress-override.scss +25 -25
  110. package/vitepress/components/Discussions/Discussions.module.scss +0 -32
  111. package/vitepress/components/EnWordList/EnWordList.module.scss +0 -124
  112. package/vitepress/components/EnWordList/RightClickMenu.module.scss +0 -22
  113. package/vitepress/components/Footprints/Footprints.module.scss +0 -93
  114. package/vitepress/components/Layout/AboutModal.module.scss +0 -233
  115. package/vitepress/components/Layout/ImagePreview.module.scss +0 -201
  116. package/vitepress/components/Layout/Layout.module.scss +0 -661
  117. package/vitepress/components/Layout/ToggleFullContent.module.scss +0 -11
  118. package/vitepress/components/Layout/ToggleSidebar.module.scss +0 -11
  119. package/vitepress/components/MarkMap/MarkMap.module.scss +0 -159
  120. package/vitepress/components/Mermaid/Mermaid.module.scss +0 -275
  121. package/vitepress/components/NotesTable/NotesTable.module.scss +0 -77
  122. package/vitepress/components/Settings/Settings.module.scss +0 -375
@@ -1,366 +1,644 @@
1
- <template>
2
- <div
3
- ref="mermaidRef"
4
- :class="[$style.mermaidWrapper, { [$style.showToolbar]: showToolbar }]"
5
- @mouseenter="showToolbar = true"
6
- @mouseleave="showToolbar = false"
7
- >
8
- <!-- 工具栏 -->
9
- <div
10
- v-if="!loading && !error"
11
- :class="[$style.toolbar, { [$style.visible]: showToolbar }]"
12
- >
13
- <button
14
- @click="zoomIn"
15
- :class="[$style.toolbarBtn, $style.iconBtn]"
16
- title="放大 (Ctrl + +)"
17
- >
18
- <img :src="icon__zoom_in" alt="放大" :class="$style.btnIcon" />
19
- </button>
20
- <button
21
- @click="zoomOut"
22
- :class="[$style.toolbarBtn, $style.iconBtn]"
23
- title="缩小 (Ctrl + -)"
24
- >
25
- <img :src="icon__zoom_out" alt="缩小" :class="$style.btnIcon" />
26
- </button>
27
- <button
28
- @click="resetZoom"
29
- :class="[$style.toolbarBtn, $style.iconBtn]"
30
- title="重置缩放 (Ctrl + 0)"
31
- >
32
- <img :src="icon__zoom_reset" alt="重置" :class="$style.btnIcon" />
33
- </button>
34
- <button
35
- @click="fitToScreen"
36
- :class="[$style.toolbarBtn, $style.iconBtn]"
37
- title="适应屏幕"
38
- >
39
- <img :src="icon__zoom_fit" alt="适应屏幕" :class="$style.btnIcon" />
40
- </button>
41
- <button
42
- @click="toggleFullscreen"
43
- :class="[$style.toolbarBtn, $style.iconBtn]"
44
- title="全屏 (F11)"
45
- >
46
- <img
47
- v-if="isFullscreen"
48
- :src="icon__fullscreen_exit"
49
- alt="退出全屏"
50
- :class="$style.btnIcon"
51
- />
52
- <img
53
- v-else
54
- :src="icon__fullscreen"
55
- alt="全屏"
56
- :class="$style.btnIcon"
57
- />
58
- </button>
59
- <span :class="$style.zoomLevel">{{ Math.round(scale * 100) }}%</span>
60
- </div>
61
-
62
- <!-- 图表容器 -->
63
- <div
64
- :class="[$style.mermaidContainer, { [$style.fullscreen]: isFullscreen }]"
65
- @click="handleContainerClick"
66
- >
67
- <div v-if="loading" :class="$style.mermaidLoading">
68
- <div :class="$style.spinner"></div>
69
- <p>加载图表中...</p>
70
- </div>
71
- <div v-else-if="error" :class="$style.mermaidError">
72
- <span :class="$style.errorIcon">⚠️</span>
73
- <p>{{ error }}</p>
74
- </div>
75
- <div
76
- v-show="!loading && !error"
77
- ref="diagramRef"
78
- :class="[$style.mermaidDiagram, props.class]"
79
- :style="{
80
- transform: `scale(${scale})`,
81
- transformOrigin: 'center center',
82
- }"
83
- @wheel.prevent="handleWheel"
84
- ></div>
85
- </div>
86
- </div>
87
- </template>
88
-
89
- <script setup lang="ts">
90
- import mermaid from 'mermaid'
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'
91
93
  import { nextTick, onMounted, onBeforeUnmount, ref, watch } from 'vue'
92
-
93
- import {
94
- icon__zoom_in,
95
- icon__zoom_out,
96
- icon__zoom_reset,
97
- icon__zoom_fit,
98
- icon__fullscreen,
99
- icon__fullscreen_exit,
100
- } from '../../assets/icons'
101
-
102
- const props = defineProps({
103
- graph: {
104
- type: String,
105
- required: true,
106
- },
107
- id: {
108
- type: String,
109
- required: true,
110
- },
111
- class: {
112
- type: String,
113
- default: 'mermaid',
114
- },
115
- })
116
-
117
- const mermaidRef = ref<HTMLElement | null>(null)
118
- const diagramRef = ref<HTMLElement | null>(null)
119
- const loading = ref(true)
120
- const error = ref<string | null>(null)
121
- const scale = ref(1)
122
- const isFullscreen = ref(false)
123
- const showToolbar = ref(false)
124
-
125
- // ===================================
126
- // #region 工具栏显示控制
127
- // ===================================
128
- let hideTimer: ReturnType<typeof setTimeout> | null = null
129
-
130
- function handleContainerClick() {
131
- // 移动端点击切换工具栏显示
132
- if (window.innerWidth <= 768) {
133
- showToolbar.value = !showToolbar.value
134
-
135
- // 3秒后自动隐藏
136
- if (showToolbar.value) {
137
- if (hideTimer) clearTimeout(hideTimer)
138
- hideTimer = setTimeout(() => {
139
- showToolbar.value = false
140
- }, 3000)
141
- }
142
- }
143
- }
144
- // #endregion
145
-
146
- // ===================================
147
- // #region 缩放控制
148
- // ===================================
149
- const MIN_SCALE = 0.1
150
- const MAX_SCALE = 5
151
- const SCALE_STEP = 0.1
152
-
153
- function zoomIn() {
154
- scale.value = Math.min(scale.value + SCALE_STEP, MAX_SCALE)
155
- }
156
-
157
- function zoomOut() {
158
- scale.value = Math.max(scale.value - SCALE_STEP, MIN_SCALE)
159
- }
160
-
161
- function resetZoom() {
162
- scale.value = 1
163
- }
164
-
165
- function fitToScreen() {
166
- if (!diagramRef.value || !mermaidRef.value) return
167
-
168
- const svg = diagramRef.value.querySelector('svg')
169
- if (!svg) return
170
-
171
- const containerRect = mermaidRef.value.getBoundingClientRect()
172
- const svgRect = svg.getBoundingClientRect()
173
-
174
- const scaleX = (containerRect.width * 0.9) / svgRect.width
175
- const scaleY = (containerRect.height * 0.9) / svgRect.height
176
-
177
- scale.value = Math.min(scaleX, scaleY, 1)
178
- }
179
-
180
- function handleWheel(e: WheelEvent) {
181
- if (e.ctrlKey || e.metaKey) {
182
- e.preventDefault()
183
- const delta = e.deltaY > 0 ? -SCALE_STEP : SCALE_STEP
184
- scale.value = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale.value + delta))
185
- }
186
- }
187
- // #endregion
188
-
189
- // ===================================
190
- // #region 全屏控制
191
- // ===================================
192
- function toggleFullscreen() {
193
- if (!mermaidRef.value) return
194
-
195
- if (!isFullscreen.value) {
196
- if (mermaidRef.value.requestFullscreen) {
197
- mermaidRef.value.requestFullscreen()
198
- }
199
- } else {
200
- if (document.exitFullscreen) {
201
- document.exitFullscreen()
202
- }
203
- }
204
- }
205
-
206
- function handleFullscreenChange() {
207
- isFullscreen.value = !!document.fullscreenElement
208
- }
209
- // #endregion
210
-
211
- // ===================================
212
- // #region 键盘快捷键
213
- // ===================================
214
- function handleKeydown(e: KeyboardEvent) {
215
- if (e.ctrlKey || e.metaKey) {
216
- if (e.key === '=' || e.key === '+') {
217
- e.preventDefault()
218
- zoomIn()
219
- } else if (e.key === '-' || e.key === '_') {
220
- e.preventDefault()
221
- zoomOut()
222
- } else if (e.key === '0') {
223
- e.preventDefault()
224
- resetZoom()
225
- }
226
- } else if (e.key === 'F11') {
227
- e.preventDefault()
228
- toggleFullscreen()
229
- } else if (e.key === 'Escape' && isFullscreen.value) {
230
- toggleFullscreen()
231
- }
232
- }
233
- // #endregion
234
-
235
- // ===================================
236
- // #region Mermaid 渲染
237
- // ===================================
238
- // 获取当前主题
239
- const getCurrentTheme = () => {
240
- return document.documentElement.classList.contains('dark')
241
- ? 'dark'
242
- : 'default'
243
- }
244
-
245
- // 初始化 Mermaid
246
- const initMermaid = async () => {
247
- try {
248
- mermaid.initialize({
249
- startOnLoad: false,
250
- theme: getCurrentTheme(),
251
- securityLevel: 'loose',
252
- fontFamily: 'inherit',
253
- })
254
- } catch (err) {
255
- console.error('Mermaid initialization error:', err)
256
- }
257
- }
258
-
259
- // 渲染图表
260
- const renderDiagram = async () => {
261
- // 等待下一个 DOM 更新周期
262
- await nextTick()
263
-
264
- if (!diagramRef.value) {
265
- // 再等待一小段时间确保 DOM 完全渲染
266
- await new Promise((resolve) => setTimeout(resolve, 0))
267
- if (!diagramRef.value) {
268
- // console.warn('diagramRef is still null')
269
- return
270
- }
271
- }
272
-
273
- try {
274
- loading.value = true
275
- error.value = null
276
-
277
- // 每次渲染前都重新初始化主题
278
- mermaid.initialize({
279
- startOnLoad: false,
280
- theme: getCurrentTheme(),
281
- securityLevel: 'loose',
282
- fontFamily: 'inherit',
283
- })
284
-
285
- const { svg, bindFunctions } = await mermaid.render(
286
- props.id,
287
- decodeURIComponent(props.graph)
288
- )
289
-
290
- diagramRef.value.innerHTML = svg
291
-
292
- // 绑定交互函数(如果有)
293
- if (bindFunctions) {
294
- bindFunctions(diagramRef.value)
295
- }
296
- } catch (err) {
297
- const message = err instanceof Error ? err.message : String(err)
298
- error.value = `Failed to render diagram: ${message}`
299
- console.error('Mermaid render error:', err)
300
- } finally {
301
- loading.value = false
302
- }
303
- }
304
-
305
- // 监听主题变化
306
- const observeTheme = () => {
307
- const observer = new MutationObserver((mutations) => {
308
- mutations.forEach((mutation) => {
309
- if (
310
- mutation.type === 'attributes' &&
311
- mutation.attributeName === 'class'
312
- ) {
313
- // 检查是否是主题变化
314
- const isDark = document.documentElement.classList.contains('dark')
315
- const wasDark = mutation.oldValue?.includes('dark')
316
-
317
- // 只有当主题实际发生变化时才重新渲染
318
- if ((isDark && !wasDark) || (!isDark && wasDark)) {
319
- renderDiagram()
320
- }
321
- }
322
- })
323
- })
324
-
325
- observer.observe(document.documentElement, {
326
- attributes: true,
327
- attributeFilter: ['class'],
328
- attributeOldValue: true,
329
- })
330
-
331
- return observer
332
- }
333
-
334
- onMounted(async () => {
335
- await initMermaid()
336
- await renderDiagram()
337
-
338
- // 观察主题变化
339
- const themeObserver = observeTheme()
340
-
341
- // 监听键盘事件
342
- document.addEventListener('keydown', handleKeydown)
343
-
344
- // 监听全屏变化
345
- document.addEventListener('fullscreenchange', handleFullscreenChange)
346
-
347
- // 清理函数
348
- onBeforeUnmount(() => {
349
- themeObserver.disconnect()
350
- document.removeEventListener('keydown', handleKeydown)
351
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
352
- })
353
- })
354
-
355
- // 当图表内容变化时重新渲染并重置缩放
356
- watch(
357
- () => props.graph,
358
- () => {
359
- renderDiagram()
360
- resetZoom()
361
- }
362
- )
363
- // #endregion
364
- </script>
365
-
366
- <style module src="./Mermaid.module.scss"></style>
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>