@tnotesjs/core 0.0.2 → 0.0.3

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