@tnotesjs/core 0.1.19 → 0.1.20

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 (114) hide show
  1. package/README.md +87 -87
  2. package/package.json +1 -1
  3. package/types/config.ts +61 -61
  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 +213 -213
  38. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  39. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  40. package/vitepress/components/Discussions/Discussions.vue +211 -211
  41. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  42. package/vitepress/components/EnWordList/EnWordList.vue +543 -543
  43. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  44. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  45. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  46. package/vitepress/components/Footprints/Footprints.vue +377 -377
  47. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  48. package/vitepress/components/Layout/AboutModal.vue +105 -105
  49. package/vitepress/components/Layout/AboutPanel.vue +270 -270
  50. package/vitepress/components/Layout/ContentCollapse.vue +603 -603
  51. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  52. package/vitepress/components/Layout/DocBeforeControls.vue +149 -149
  53. package/vitepress/components/Layout/DocFooter.vue +233 -233
  54. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  55. package/vitepress/components/Layout/ImagePreview.vue +281 -281
  56. package/vitepress/components/Layout/Layout.module.scss +661 -661
  57. package/vitepress/components/Layout/Layout.vue +621 -621
  58. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  59. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  60. package/vitepress/components/Layout/SidebarNavBefore.vue +171 -171
  61. package/vitepress/components/Layout/SidebarResizeHandle.vue +319 -319
  62. package/vitepress/components/Layout/Swiper.vue +167 -167
  63. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  64. package/vitepress/components/Layout/ToggleFullContent.vue +35 -35
  65. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  66. package/vitepress/components/Layout/ToggleSidebar.vue +26 -26
  67. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  68. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  69. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  70. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  71. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  72. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  73. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  74. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  75. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  76. package/vitepress/components/MarkMap/MarkMap.vue +405 -405
  77. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  78. package/vitepress/components/Mermaid/Mermaid.vue +365 -365
  79. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  80. package/vitepress/components/NotesTable/NotesTable.vue +98 -98
  81. package/vitepress/components/NotesTable/README.md +67 -67
  82. package/vitepress/components/Settings/Settings.module.scss +375 -375
  83. package/vitepress/components/Settings/Settings.vue +334 -334
  84. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  85. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  86. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -946
  87. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  88. package/vitepress/components/constants.ts +109 -109
  89. package/vitepress/components/notesConfig.data.ts +73 -73
  90. package/vitepress/components/sidebar.data.ts +59 -59
  91. package/vitepress/components/tnotes-config.data.ts +21 -21
  92. package/vitepress/components/utils.ts +26 -26
  93. package/vitepress/config/index.ts +169 -169
  94. package/vitepress/configs/constants.ts +26 -26
  95. package/vitepress/configs/head.config.ts +25 -25
  96. package/vitepress/configs/index.ts +9 -9
  97. package/vitepress/configs/markdown-it.d.ts +23 -23
  98. package/vitepress/configs/markdown.config.ts +366 -366
  99. package/vitepress/configs/theme.config.ts +108 -108
  100. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  101. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  102. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  103. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  104. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  105. package/vitepress/theme/index.ts +137 -137
  106. package/vitepress/theme/styles/base.scss +64 -59
  107. package/vitepress/theme/styles/components/404.scss +31 -31
  108. package/vitepress/theme/styles/components/collapse.scss +172 -172
  109. package/vitepress/theme/styles/components/markmap.scss +101 -101
  110. package/vitepress/theme/styles/components/swiper.scss +255 -255
  111. package/vitepress/theme/styles/index.scss +25 -25
  112. package/vitepress/theme/styles/layout.scss +78 -78
  113. package/vitepress/theme/styles/utilities.scss +39 -39
  114. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,484 +1,484 @@
1
- <script setup>
2
- import { scaleOrdinal, schemePastel2, schemeSet3, schemeTableau10 } from 'd3'
3
- import { Transformer } from 'markmap-lib'
4
- import { Toolbar } from 'markmap-toolbar'
5
- import 'markmap-toolbar/dist/style.css'
6
- import { Markmap } from 'markmap-view'
7
- import { withBase, useData } from 'vitepress'
8
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
9
-
10
- import {
11
- icon__fullscreen,
12
- icon__fullscreen_exit,
13
- icon__confirm,
14
- } from '../../assets/icons'
15
- import {
16
- MARKMAP_THEME_KEY,
17
- MARKMAP_EXPAND_LEVEL_KEY,
18
- REPO_NAME,
19
- } from '../constants'
20
-
21
- const props = defineProps({
22
- sidebarData: {
23
- type: Array,
24
- required: true,
25
- },
26
- })
27
-
28
- const svgRef = ref(null)
29
- const containerRef = ref(null)
30
- const expandLevel = ref(2)
31
- const isFullscreen = ref(false)
32
- const markmapTheme = ref('default')
33
-
34
- const containerBg = computed(() =>
35
- markmapTheme.value === 'dark' ? '#1d1d1d' : '#ffffff',
36
- )
37
-
38
- let markmapInstance = null
39
- let toolbarEl = null
40
- let toolbarLevelInput = null
41
- let darkClassObserver = null
42
- const transformer = new Transformer()
43
- const { isDark } = useData()
44
-
45
- // 同步暗色状态到 markmap-dark 类(markmap 内部样式依赖此类)
46
- function syncMarkmapDark(dark) {
47
- if (dark) {
48
- document.documentElement.classList.add('markmap-dark')
49
- } else {
50
- document.documentElement.classList.remove('markmap-dark')
51
- }
52
- }
53
-
54
- watch(isDark, (val) => syncMarkmapDark(val))
55
- watch(markmapTheme, (val) => syncMarkmapDark(val === 'dark'))
56
-
57
- // 读取 localStorage 配置
58
- function loadSettings() {
59
- if (typeof window === 'undefined') return
60
- const savedTheme = localStorage.getItem(MARKMAP_THEME_KEY)
61
- if (savedTheme && ['default', 'colorful', 'dark'].includes(savedTheme)) {
62
- markmapTheme.value = savedTheme
63
- }
64
- const savedLevel = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY)
65
- if (savedLevel) {
66
- const level = parseInt(savedLevel)
67
- if (!isNaN(level) && level >= 1 && level <= 100) {
68
- expandLevel.value = level
69
- }
70
- }
71
- }
72
-
73
- const getThemeColorFn = () => {
74
- const theme = markmapTheme.value
75
- switch (theme) {
76
- case 'colorful':
77
- return scaleOrdinal(schemeTableau10)
78
- case 'dark':
79
- return scaleOrdinal(schemeSet3)
80
- default:
81
- return scaleOrdinal(schemePastel2)
82
- }
83
- }
84
-
85
- // 将 sidebar 数据转换为 markdown
86
- function sidebarToMarkdown(items, level = 1, isRoot = true) {
87
- if (level === 1 && isRoot) {
88
- const rootName = REPO_NAME.replace(/^TNotes\./, '')
89
- const rootMarkdown = `# ${rootName}\n\n`
90
- const childrenMarkdown = sidebarToMarkdown(items, 1, false)
91
- return rootMarkdown + childrenMarkdown
92
- }
93
-
94
- return items
95
- .map((item) => {
96
- const indent = ' '.repeat(level - 1)
97
- let text
98
- if (item.link) {
99
- const encodedLink = withBase(item.link).replace(/ /g, '%20')
100
- text = `[${item.text}](${encodedLink})`
101
- } else if (level === 1) {
102
- text = `**${item.text}**`
103
- } else {
104
- text = item.text
105
- }
106
-
107
- let markdown = `${indent}- ${text}\n`
108
- if (item.items && item.items.length > 0) {
109
- markdown += sidebarToMarkdown(item.items, level + 1, false)
110
- }
111
- return markdown
112
- })
113
- .join('')
114
- }
115
-
116
- function renderMindmap() {
117
- if (!props.sidebarData || !svgRef.value) return
118
-
119
- nextTick().then(() => {
120
- if (markmapInstance) {
121
- try {
122
- markmapInstance.destroy()
123
- } catch {}
124
- markmapInstance = null
125
- }
126
-
127
- if (!svgRef.value) return
128
-
129
- const markdown = sidebarToMarkdown(props.sidebarData)
130
- if (!markdown.trim()) {
131
- svgRef.value.innerHTML = '<text x="20" y="30" fill="#999">空内容</text>'
132
- return
133
- }
134
-
135
- try {
136
- const { root } = transformer.transform(markdown)
137
- const colorFn = getThemeColorFn()
138
- const options = {
139
- initialExpandLevel: expandLevel.value,
140
- duration: 100,
141
- nodeMinHeight: 24,
142
- spacingVertical: 10,
143
- spacingHorizontal: 20,
144
- maxWidth: 400,
145
- maxInitialScale: 2,
146
- color: (node) => colorFn(`${node.state?.path || ''}`),
147
- }
148
-
149
- markmapInstance = Markmap.create(svgRef.value, options, root)
150
-
151
- // 为链接添加 target="_blank"
152
- setTimeout(() => {
153
- if (svgRef.value) {
154
- svgRef.value.querySelectorAll('a').forEach((link) => {
155
- link.setAttribute('target', '_blank')
156
- })
157
- }
158
- }, 100)
159
-
160
- setTimeout(() => {
161
- try {
162
- markmapInstance?.fit()
163
- } catch {}
164
- }, 0)
165
-
166
- initToolbar()
167
- } catch (error) {
168
- console.error('Markmap render error:', error)
169
- if (svgRef.value) {
170
- svgRef.value.innerHTML =
171
- '<text x="20" y="30" fill="red">思维导图渲染错误</text>'
172
- }
173
- }
174
- })
175
- }
176
-
177
- function initToolbar() {
178
- if (!markmapInstance || !containerRef.value) return
179
-
180
- if (toolbarEl) {
181
- toolbarEl.remove()
182
- toolbarEl = null
183
- toolbarLevelInput = null
184
- }
185
-
186
- const { el } = Toolbar.create(markmapInstance)
187
- toolbarEl = el
188
- toolbarEl.style.position = 'absolute'
189
- toolbarEl.style.top = '1rem'
190
- toolbarEl.style.right = '0.5rem'
191
- toolbarEl.style.scale = '0.8'
192
- toolbarEl.style.zIndex = '10'
193
-
194
- const brand = toolbarEl.querySelector('.mm-toolbar-brand')
195
- if (brand) toolbarEl.removeChild(brand)
196
-
197
- addLevelControl(toolbarEl)
198
- addFullscreenButton(toolbarEl)
199
-
200
- containerRef.value.appendChild(toolbarEl)
201
- }
202
-
203
- function addLevelControl(toolbar) {
204
- const container = document.createElement('div')
205
- container.style.display = 'flex'
206
- container.style.alignItems = 'center'
207
- container.style.gap = '8px'
208
- container.style.marginRight = '5px'
209
-
210
- const levelInput = document.createElement('input')
211
- levelInput.type = 'number'
212
- levelInput.min = '1'
213
- levelInput.max = '100'
214
- levelInput.value = expandLevel.value.toString()
215
- levelInput.style.width = '2.4rem'
216
- levelInput.style.height = '1.6rem'
217
- levelInput.style.padding = '2px 6px'
218
- levelInput.style.fontSize = '12px'
219
- levelInput.style.lineHeight = '1.2'
220
- levelInput.style.textAlign = 'center'
221
- levelInput.style.boxSizing = 'border-box'
222
- levelInput.style.borderBottom = '.5px solid var(--vp-c-tip-1)'
223
- levelInput.style.color = 'var(--vp-c-tip-1)'
224
- levelInput.title = '展开层级'
225
- levelInput.setAttribute('aria-label', 'markmap-expand-level')
226
-
227
- toolbarLevelInput = levelInput
228
-
229
- levelInput.addEventListener('input', (e) => {
230
- const input = e.target
231
- let value = parseInt(input.value)
232
- if (input.value === '' || isNaN(value)) return
233
- if (value < 1) input.value = '1'
234
- else if (value > 100) input.value = '100'
235
- })
236
-
237
- levelInput.addEventListener('change', (e) => {
238
- const value = parseInt(e.target.value)
239
- if (!isNaN(value) && value >= 1 && value <= 100) {
240
- expandLevel.value = value
241
- } else {
242
- levelInput.value = expandLevel.value.toString()
243
- }
244
- })
245
-
246
- levelInput.addEventListener('keydown', (e) => {
247
- if (e.key === 'Enter') {
248
- const val = parseInt(e.target.value)
249
- if (!isNaN(val) && val >= 1 && val <= 100) {
250
- expandLevel.value = val
251
- } else {
252
- levelInput.value = expandLevel.value.toString()
253
- }
254
- onUpdateClick()
255
- }
256
- })
257
-
258
- const updateBtn = document.createElement('button')
259
- updateBtn.type = 'button'
260
- updateBtn.className = 'mm-toolbar-item'
261
- updateBtn.title = '确定层级并更新'
262
- updateBtn.innerHTML = `<img src="${icon__confirm}" alt="确定" style="width:16px;height:16px;display:block" />`
263
- updateBtn.addEventListener('click', onUpdateClick)
264
-
265
- container.appendChild(levelInput)
266
- container.appendChild(updateBtn)
267
- toolbar.insertBefore(container, toolbar.firstChild)
268
- }
269
-
270
- function addFullscreenButton(toolbar) {
271
- const fullscreenBtn = document.createElement('div')
272
- fullscreenBtn.className = 'mm-toolbar-item'
273
- fullscreenBtn.title = isFullscreen.value ? '退出全屏' : '全屏'
274
- fullscreenBtn.innerHTML = isFullscreen.value
275
- ? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
276
- : `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
277
- fullscreenBtn.addEventListener('click', toggleFullscreen)
278
- toolbar.appendChild(fullscreenBtn)
279
- }
280
-
281
- function onUpdateClick() {
282
- localStorage.setItem(MARKMAP_EXPAND_LEVEL_KEY, expandLevel.value.toString())
283
- renderMindmap()
284
- }
285
-
286
- function toggleFullscreen() {
287
- if (!containerRef.value) return
288
-
289
- if (!isFullscreen.value) {
290
- if (containerRef.value.requestFullscreen) {
291
- containerRef.value.requestFullscreen().catch((err) => {
292
- console.error('全屏请求失败:', err)
293
- })
294
- } else if (containerRef.value.webkitRequestFullscreen) {
295
- containerRef.value.webkitRequestFullscreen()
296
- }
297
- } else {
298
- if (document.exitFullscreen) {
299
- document.exitFullscreen()
300
- } else if (document.webkitExitFullscreen) {
301
- document.webkitExitFullscreen()
302
- }
303
- }
304
- }
305
-
306
- function handleFullscreenChange() {
307
- isFullscreen.value = !!(
308
- document.fullscreenElement || document.webkitFullscreenElement
309
- )
310
-
311
- if (toolbarEl) {
312
- const fullscreenBtn = Array.from(toolbarEl.children).find((child) =>
313
- child.querySelector('img[alt="全屏"], img[alt="退出全屏"]'),
314
- )
315
- if (fullscreenBtn) {
316
- fullscreenBtn.title = isFullscreen.value ? '退出全屏' : '全屏'
317
- fullscreenBtn.innerHTML = isFullscreen.value
318
- ? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
319
- : `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
320
- }
321
- }
322
-
323
- if (svgRef.value) {
324
- if (isFullscreen.value) {
325
- svgRef.value.style.height = 'calc(100vh - 100px)'
326
- } else {
327
- svgRef.value.style.height = ''
328
- }
329
-
330
- setTimeout(() => {
331
- try {
332
- markmapInstance?.fit()
333
- } catch {}
334
- }, 300)
335
- }
336
- }
337
-
338
- watch(expandLevel, (v) => {
339
- if (toolbarLevelInput) {
340
- const asStr = (v || 0).toString()
341
- if (toolbarLevelInput.value !== asStr) {
342
- toolbarLevelInput.value = asStr
343
- }
344
- }
345
- })
346
-
347
- watch(
348
- () => props.sidebarData,
349
- () => renderMindmap(),
350
- { deep: true },
351
- )
352
-
353
- onMounted(() => {
354
- loadSettings()
355
- syncMarkmapDark(markmapTheme.value === 'dark' || isDark.value)
356
- document.addEventListener('fullscreenchange', handleFullscreenChange)
357
- document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
358
-
359
- // 监听 markmap 工具栏切换暗色主题(它直接 toggle markmap-dark 类,不通知 Vue)
360
- darkClassObserver = new MutationObserver(() => {
361
- const hasDark = document.documentElement.classList.contains('markmap-dark')
362
- markmapTheme.value = hasDark ? 'dark' : 'default'
363
- })
364
- darkClassObserver.observe(document.documentElement, {
365
- attributes: true,
366
- attributeFilter: ['class'],
367
- })
368
-
369
- renderMindmap()
370
- })
371
-
372
- onBeforeUnmount(() => {
373
- if (markmapInstance) {
374
- try {
375
- markmapInstance.destroy()
376
- } catch {}
377
- markmapInstance = null
378
- }
379
- if (toolbarEl) {
380
- toolbarEl.remove()
381
- toolbarEl = null
382
- toolbarLevelInput = null
383
- }
384
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
385
- document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
386
- if (darkClassObserver) {
387
- darkClassObserver.disconnect()
388
- darkClassObserver = null
389
- }
390
- })
391
- </script>
392
-
393
- <template>
394
- <div class="mindmap-view">
395
- <div
396
- ref="containerRef"
397
- class="mindmap-container"
398
- :style="{ backgroundColor: containerBg }"
399
- >
400
- <svg ref="svgRef"></svg>
401
- </div>
402
- </div>
403
- </template>
404
-
405
- <style scoped lang="scss">
406
- .mindmap-view {
407
- width: 100%;
408
- padding: 1rem 0;
409
- display: flex;
410
- flex-direction: column;
411
- }
412
-
413
- .mindmap-container {
414
- width: 100%;
415
- height: 500px;
416
- position: relative;
417
- overflow: hidden;
418
- border-radius: 8px;
419
- padding: 1rem;
420
- transition: all 0.3s ease;
421
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
422
- box-sizing: border-box;
423
-
424
- &:hover {
425
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
426
- }
427
-
428
- svg {
429
- width: 100%;
430
- height: 100%;
431
- display: block;
432
- transition: height 0.3s ease;
433
- }
434
-
435
- /* 鼠标悬停显示 toolbar */
436
- &:hover :deep(.mm-toolbar) {
437
- opacity: 1;
438
- pointer-events: auto;
439
- }
440
- }
441
-
442
- /* 暗色主题 */
443
- :global(.dark) .mindmap-container,
444
- :global(.markmap-dark) .mindmap-container {
445
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
446
-
447
- &:hover {
448
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
449
- }
450
- }
451
-
452
- /* 全屏样式 */
453
- .mindmap-container:fullscreen,
454
- .mindmap-container:-webkit-full-screen {
455
- width: 100%;
456
- height: 100%;
457
- padding: 20px;
458
- background: white;
459
- display: flex;
460
- flex-direction: column;
461
- justify-content: center;
462
- box-shadow: none;
463
- }
464
-
465
- :global(.dark) .mindmap-container:fullscreen,
466
- :global(.dark) .mindmap-container:-webkit-full-screen,
467
- :global(.markmap-dark) .mindmap-container:fullscreen,
468
- :global(.markmap-dark) .mindmap-container:-webkit-full-screen {
469
- background: #1d1d1d;
470
- }
471
-
472
- @media (max-width: 768px) {
473
- .mindmap-container {
474
- height: 400px;
475
- padding: 0.75rem;
476
- }
477
- }
478
-
479
- @media (max-width: 480px) {
480
- .mindmap-container {
481
- padding: 0.5rem;
482
- }
483
- }
484
- </style>
1
+ <script setup>
2
+ import { scaleOrdinal, schemePastel2, schemeSet3, schemeTableau10 } from 'd3'
3
+ import { Transformer } from 'markmap-lib'
4
+ import { Toolbar } from 'markmap-toolbar'
5
+ import 'markmap-toolbar/dist/style.css'
6
+ import { Markmap } from 'markmap-view'
7
+ import { withBase, useData } from 'vitepress'
8
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
9
+
10
+ import {
11
+ icon__fullscreen,
12
+ icon__fullscreen_exit,
13
+ icon__confirm,
14
+ } from '../../assets/icons'
15
+ import {
16
+ MARKMAP_THEME_KEY,
17
+ MARKMAP_EXPAND_LEVEL_KEY,
18
+ REPO_NAME,
19
+ } from '../constants'
20
+
21
+ const props = defineProps({
22
+ sidebarData: {
23
+ type: Array,
24
+ required: true,
25
+ },
26
+ })
27
+
28
+ const svgRef = ref(null)
29
+ const containerRef = ref(null)
30
+ const expandLevel = ref(2)
31
+ const isFullscreen = ref(false)
32
+ const markmapTheme = ref('default')
33
+
34
+ const containerBg = computed(() =>
35
+ markmapTheme.value === 'dark' ? '#1d1d1d' : '#ffffff',
36
+ )
37
+
38
+ let markmapInstance = null
39
+ let toolbarEl = null
40
+ let toolbarLevelInput = null
41
+ let darkClassObserver = null
42
+ const transformer = new Transformer()
43
+ const { isDark } = useData()
44
+
45
+ // 同步暗色状态到 markmap-dark 类(markmap 内部样式依赖此类)
46
+ function syncMarkmapDark(dark) {
47
+ if (dark) {
48
+ document.documentElement.classList.add('markmap-dark')
49
+ } else {
50
+ document.documentElement.classList.remove('markmap-dark')
51
+ }
52
+ }
53
+
54
+ watch(isDark, (val) => syncMarkmapDark(val))
55
+ watch(markmapTheme, (val) => syncMarkmapDark(val === 'dark'))
56
+
57
+ // 读取 localStorage 配置
58
+ function loadSettings() {
59
+ if (typeof window === 'undefined') return
60
+ const savedTheme = localStorage.getItem(MARKMAP_THEME_KEY)
61
+ if (savedTheme && ['default', 'colorful', 'dark'].includes(savedTheme)) {
62
+ markmapTheme.value = savedTheme
63
+ }
64
+ const savedLevel = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY)
65
+ if (savedLevel) {
66
+ const level = parseInt(savedLevel)
67
+ if (!isNaN(level) && level >= 1 && level <= 100) {
68
+ expandLevel.value = level
69
+ }
70
+ }
71
+ }
72
+
73
+ const getThemeColorFn = () => {
74
+ const theme = markmapTheme.value
75
+ switch (theme) {
76
+ case 'colorful':
77
+ return scaleOrdinal(schemeTableau10)
78
+ case 'dark':
79
+ return scaleOrdinal(schemeSet3)
80
+ default:
81
+ return scaleOrdinal(schemePastel2)
82
+ }
83
+ }
84
+
85
+ // 将 sidebar 数据转换为 markdown
86
+ function sidebarToMarkdown(items, level = 1, isRoot = true) {
87
+ if (level === 1 && isRoot) {
88
+ const rootName = REPO_NAME.replace(/^TNotes\./, '')
89
+ const rootMarkdown = `# ${rootName}\n\n`
90
+ const childrenMarkdown = sidebarToMarkdown(items, 1, false)
91
+ return rootMarkdown + childrenMarkdown
92
+ }
93
+
94
+ return items
95
+ .map((item) => {
96
+ const indent = ' '.repeat(level - 1)
97
+ let text
98
+ if (item.link) {
99
+ const encodedLink = withBase(item.link).replace(/ /g, '%20')
100
+ text = `[${item.text}](${encodedLink})`
101
+ } else if (level === 1) {
102
+ text = `**${item.text}**`
103
+ } else {
104
+ text = item.text
105
+ }
106
+
107
+ let markdown = `${indent}- ${text}\n`
108
+ if (item.items && item.items.length > 0) {
109
+ markdown += sidebarToMarkdown(item.items, level + 1, false)
110
+ }
111
+ return markdown
112
+ })
113
+ .join('')
114
+ }
115
+
116
+ function renderMindmap() {
117
+ if (!props.sidebarData || !svgRef.value) return
118
+
119
+ nextTick().then(() => {
120
+ if (markmapInstance) {
121
+ try {
122
+ markmapInstance.destroy()
123
+ } catch {}
124
+ markmapInstance = null
125
+ }
126
+
127
+ if (!svgRef.value) return
128
+
129
+ const markdown = sidebarToMarkdown(props.sidebarData)
130
+ if (!markdown.trim()) {
131
+ svgRef.value.innerHTML = '<text x="20" y="30" fill="#999">空内容</text>'
132
+ return
133
+ }
134
+
135
+ try {
136
+ const { root } = transformer.transform(markdown)
137
+ const colorFn = getThemeColorFn()
138
+ const options = {
139
+ initialExpandLevel: expandLevel.value,
140
+ duration: 100,
141
+ nodeMinHeight: 24,
142
+ spacingVertical: 10,
143
+ spacingHorizontal: 20,
144
+ maxWidth: 400,
145
+ maxInitialScale: 2,
146
+ color: (node) => colorFn(`${node.state?.path || ''}`),
147
+ }
148
+
149
+ markmapInstance = Markmap.create(svgRef.value, options, root)
150
+
151
+ // 为链接添加 target="_blank"
152
+ setTimeout(() => {
153
+ if (svgRef.value) {
154
+ svgRef.value.querySelectorAll('a').forEach((link) => {
155
+ link.setAttribute('target', '_blank')
156
+ })
157
+ }
158
+ }, 100)
159
+
160
+ setTimeout(() => {
161
+ try {
162
+ markmapInstance?.fit()
163
+ } catch {}
164
+ }, 0)
165
+
166
+ initToolbar()
167
+ } catch (error) {
168
+ console.error('Markmap render error:', error)
169
+ if (svgRef.value) {
170
+ svgRef.value.innerHTML =
171
+ '<text x="20" y="30" fill="red">思维导图渲染错误</text>'
172
+ }
173
+ }
174
+ })
175
+ }
176
+
177
+ function initToolbar() {
178
+ if (!markmapInstance || !containerRef.value) return
179
+
180
+ if (toolbarEl) {
181
+ toolbarEl.remove()
182
+ toolbarEl = null
183
+ toolbarLevelInput = null
184
+ }
185
+
186
+ const { el } = Toolbar.create(markmapInstance)
187
+ toolbarEl = el
188
+ toolbarEl.style.position = 'absolute'
189
+ toolbarEl.style.top = '1rem'
190
+ toolbarEl.style.right = '0.5rem'
191
+ toolbarEl.style.scale = '0.8'
192
+ toolbarEl.style.zIndex = '10'
193
+
194
+ const brand = toolbarEl.querySelector('.mm-toolbar-brand')
195
+ if (brand) toolbarEl.removeChild(brand)
196
+
197
+ addLevelControl(toolbarEl)
198
+ addFullscreenButton(toolbarEl)
199
+
200
+ containerRef.value.appendChild(toolbarEl)
201
+ }
202
+
203
+ function addLevelControl(toolbar) {
204
+ const container = document.createElement('div')
205
+ container.style.display = 'flex'
206
+ container.style.alignItems = 'center'
207
+ container.style.gap = '8px'
208
+ container.style.marginRight = '5px'
209
+
210
+ const levelInput = document.createElement('input')
211
+ levelInput.type = 'number'
212
+ levelInput.min = '1'
213
+ levelInput.max = '100'
214
+ levelInput.value = expandLevel.value.toString()
215
+ levelInput.style.width = '2.4rem'
216
+ levelInput.style.height = '1.6rem'
217
+ levelInput.style.padding = '2px 6px'
218
+ levelInput.style.fontSize = '12px'
219
+ levelInput.style.lineHeight = '1.2'
220
+ levelInput.style.textAlign = 'center'
221
+ levelInput.style.boxSizing = 'border-box'
222
+ levelInput.style.borderBottom = '.5px solid var(--vp-c-tip-1)'
223
+ levelInput.style.color = 'var(--vp-c-tip-1)'
224
+ levelInput.title = '展开层级'
225
+ levelInput.setAttribute('aria-label', 'markmap-expand-level')
226
+
227
+ toolbarLevelInput = levelInput
228
+
229
+ levelInput.addEventListener('input', (e) => {
230
+ const input = e.target
231
+ let value = parseInt(input.value)
232
+ if (input.value === '' || isNaN(value)) return
233
+ if (value < 1) input.value = '1'
234
+ else if (value > 100) input.value = '100'
235
+ })
236
+
237
+ levelInput.addEventListener('change', (e) => {
238
+ const value = parseInt(e.target.value)
239
+ if (!isNaN(value) && value >= 1 && value <= 100) {
240
+ expandLevel.value = value
241
+ } else {
242
+ levelInput.value = expandLevel.value.toString()
243
+ }
244
+ })
245
+
246
+ levelInput.addEventListener('keydown', (e) => {
247
+ if (e.key === 'Enter') {
248
+ const val = parseInt(e.target.value)
249
+ if (!isNaN(val) && val >= 1 && val <= 100) {
250
+ expandLevel.value = val
251
+ } else {
252
+ levelInput.value = expandLevel.value.toString()
253
+ }
254
+ onUpdateClick()
255
+ }
256
+ })
257
+
258
+ const updateBtn = document.createElement('button')
259
+ updateBtn.type = 'button'
260
+ updateBtn.className = 'mm-toolbar-item'
261
+ updateBtn.title = '确定层级并更新'
262
+ updateBtn.innerHTML = `<img src="${icon__confirm}" alt="确定" style="width:16px;height:16px;display:block" />`
263
+ updateBtn.addEventListener('click', onUpdateClick)
264
+
265
+ container.appendChild(levelInput)
266
+ container.appendChild(updateBtn)
267
+ toolbar.insertBefore(container, toolbar.firstChild)
268
+ }
269
+
270
+ function addFullscreenButton(toolbar) {
271
+ const fullscreenBtn = document.createElement('div')
272
+ fullscreenBtn.className = 'mm-toolbar-item'
273
+ fullscreenBtn.title = isFullscreen.value ? '退出全屏' : '全屏'
274
+ fullscreenBtn.innerHTML = isFullscreen.value
275
+ ? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
276
+ : `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
277
+ fullscreenBtn.addEventListener('click', toggleFullscreen)
278
+ toolbar.appendChild(fullscreenBtn)
279
+ }
280
+
281
+ function onUpdateClick() {
282
+ localStorage.setItem(MARKMAP_EXPAND_LEVEL_KEY, expandLevel.value.toString())
283
+ renderMindmap()
284
+ }
285
+
286
+ function toggleFullscreen() {
287
+ if (!containerRef.value) return
288
+
289
+ if (!isFullscreen.value) {
290
+ if (containerRef.value.requestFullscreen) {
291
+ containerRef.value.requestFullscreen().catch((err) => {
292
+ console.error('全屏请求失败:', err)
293
+ })
294
+ } else if (containerRef.value.webkitRequestFullscreen) {
295
+ containerRef.value.webkitRequestFullscreen()
296
+ }
297
+ } else {
298
+ if (document.exitFullscreen) {
299
+ document.exitFullscreen()
300
+ } else if (document.webkitExitFullscreen) {
301
+ document.webkitExitFullscreen()
302
+ }
303
+ }
304
+ }
305
+
306
+ function handleFullscreenChange() {
307
+ isFullscreen.value = !!(
308
+ document.fullscreenElement || document.webkitFullscreenElement
309
+ )
310
+
311
+ if (toolbarEl) {
312
+ const fullscreenBtn = Array.from(toolbarEl.children).find((child) =>
313
+ child.querySelector('img[alt="全屏"], img[alt="退出全屏"]'),
314
+ )
315
+ if (fullscreenBtn) {
316
+ fullscreenBtn.title = isFullscreen.value ? '退出全屏' : '全屏'
317
+ fullscreenBtn.innerHTML = isFullscreen.value
318
+ ? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
319
+ : `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
320
+ }
321
+ }
322
+
323
+ if (svgRef.value) {
324
+ if (isFullscreen.value) {
325
+ svgRef.value.style.height = 'calc(100vh - 100px)'
326
+ } else {
327
+ svgRef.value.style.height = ''
328
+ }
329
+
330
+ setTimeout(() => {
331
+ try {
332
+ markmapInstance?.fit()
333
+ } catch {}
334
+ }, 300)
335
+ }
336
+ }
337
+
338
+ watch(expandLevel, (v) => {
339
+ if (toolbarLevelInput) {
340
+ const asStr = (v || 0).toString()
341
+ if (toolbarLevelInput.value !== asStr) {
342
+ toolbarLevelInput.value = asStr
343
+ }
344
+ }
345
+ })
346
+
347
+ watch(
348
+ () => props.sidebarData,
349
+ () => renderMindmap(),
350
+ { deep: true },
351
+ )
352
+
353
+ onMounted(() => {
354
+ loadSettings()
355
+ syncMarkmapDark(markmapTheme.value === 'dark' || isDark.value)
356
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
357
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
358
+
359
+ // 监听 markmap 工具栏切换暗色主题(它直接 toggle markmap-dark 类,不通知 Vue)
360
+ darkClassObserver = new MutationObserver(() => {
361
+ const hasDark = document.documentElement.classList.contains('markmap-dark')
362
+ markmapTheme.value = hasDark ? 'dark' : 'default'
363
+ })
364
+ darkClassObserver.observe(document.documentElement, {
365
+ attributes: true,
366
+ attributeFilter: ['class'],
367
+ })
368
+
369
+ renderMindmap()
370
+ })
371
+
372
+ onBeforeUnmount(() => {
373
+ if (markmapInstance) {
374
+ try {
375
+ markmapInstance.destroy()
376
+ } catch {}
377
+ markmapInstance = null
378
+ }
379
+ if (toolbarEl) {
380
+ toolbarEl.remove()
381
+ toolbarEl = null
382
+ toolbarLevelInput = null
383
+ }
384
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
385
+ document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
386
+ if (darkClassObserver) {
387
+ darkClassObserver.disconnect()
388
+ darkClassObserver = null
389
+ }
390
+ })
391
+ </script>
392
+
393
+ <template>
394
+ <div class="mindmap-view">
395
+ <div
396
+ ref="containerRef"
397
+ class="mindmap-container"
398
+ :style="{ backgroundColor: containerBg }"
399
+ >
400
+ <svg ref="svgRef"></svg>
401
+ </div>
402
+ </div>
403
+ </template>
404
+
405
+ <style scoped lang="scss">
406
+ .mindmap-view {
407
+ width: 100%;
408
+ padding: 1rem 0;
409
+ display: flex;
410
+ flex-direction: column;
411
+ }
412
+
413
+ .mindmap-container {
414
+ width: 100%;
415
+ height: 500px;
416
+ position: relative;
417
+ overflow: hidden;
418
+ border-radius: 8px;
419
+ padding: 1rem;
420
+ transition: all 0.3s ease;
421
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
422
+ box-sizing: border-box;
423
+
424
+ &:hover {
425
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
426
+ }
427
+
428
+ svg {
429
+ width: 100%;
430
+ height: 100%;
431
+ display: block;
432
+ transition: height 0.3s ease;
433
+ }
434
+
435
+ /* 鼠标悬停显示 toolbar */
436
+ &:hover :deep(.mm-toolbar) {
437
+ opacity: 1;
438
+ pointer-events: auto;
439
+ }
440
+ }
441
+
442
+ /* 暗色主题 */
443
+ :global(.dark) .mindmap-container,
444
+ :global(.markmap-dark) .mindmap-container {
445
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
446
+
447
+ &:hover {
448
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
449
+ }
450
+ }
451
+
452
+ /* 全屏样式 */
453
+ .mindmap-container:fullscreen,
454
+ .mindmap-container:-webkit-full-screen {
455
+ width: 100%;
456
+ height: 100%;
457
+ padding: 20px;
458
+ background: white;
459
+ display: flex;
460
+ flex-direction: column;
461
+ justify-content: center;
462
+ box-shadow: none;
463
+ }
464
+
465
+ :global(.dark) .mindmap-container:fullscreen,
466
+ :global(.dark) .mindmap-container:-webkit-full-screen,
467
+ :global(.markmap-dark) .mindmap-container:fullscreen,
468
+ :global(.markmap-dark) .mindmap-container:-webkit-full-screen {
469
+ background: #1d1d1d;
470
+ }
471
+
472
+ @media (max-width: 768px) {
473
+ .mindmap-container {
474
+ height: 400px;
475
+ padding: 0.75rem;
476
+ }
477
+ }
478
+
479
+ @media (max-width: 480px) {
480
+ .mindmap-container {
481
+ padding: 0.5rem;
482
+ }
483
+ }
484
+ </style>