@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.
- package/README.md +84 -84
- package/dist/{chunk-LZQDLU3S.js → chunk-NI5B4S3T.js} +12 -2
- package/dist/cli/index.js +1 -1
- package/dist/vitepress/config/index.js +60 -17
- package/package.json +1 -1
- package/types/config.ts +61 -61
- package/types/index.ts +11 -11
- package/types/note.ts +33 -33
- package/vitepress/assets/icons/icon__check.svg +3 -3
- package/vitepress/assets/icons/icon__clipboard.svg +7 -7
- package/vitepress/assets/icons/icon__close.svg +1 -1
- package/vitepress/assets/icons/icon__collapse.svg +1 -1
- package/vitepress/assets/icons/icon__confirm.svg +1 -1
- package/vitepress/assets/icons/icon__copy.svg +4 -4
- package/vitepress/assets/icons/icon__focus.svg +1 -1
- package/vitepress/assets/icons/icon__fold.svg +3 -3
- package/vitepress/assets/icons/icon__folder.svg +1 -1
- package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
- package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
- package/vitepress/assets/icons/icon__github.svg +3 -3
- package/vitepress/assets/icons/icon__mindmap.svg +1 -1
- package/vitepress/assets/icons/icon__next.svg +1 -1
- package/vitepress/assets/icons/icon__number_gray.svg +1 -1
- package/vitepress/assets/icons/icon__number_purple.svg +1 -1
- package/vitepress/assets/icons/icon__prev.svg +1 -1
- package/vitepress/assets/icons/icon__restore.svg +1 -1
- package/vitepress/assets/icons/icon__rotate.svg +4 -4
- package/vitepress/assets/icons/icon__search.svg +1 -1
- package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
- package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
- package/vitepress/assets/icons/icon__totop.svg +5 -5
- package/vitepress/assets/icons/icon__vscode.svg +5 -5
- package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
- package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
- package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
- package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
- package/vitepress/assets/icons/index.ts +38 -38
- package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
- package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -373
- package/vitepress/components/CodeBlockFullscreen/index.ts +115 -115
- package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
- package/vitepress/components/Discussions/Discussions.module.scss +32 -32
- package/vitepress/components/Discussions/Discussions.vue +211 -211
- package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
- package/vitepress/components/EnWordList/EnWordList.vue +543 -543
- package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
- package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
- package/vitepress/components/Footprints/Footprints.module.scss +93 -93
- package/vitepress/components/Footprints/Footprints.vue +377 -377
- package/vitepress/components/Layout/AboutModal.module.scss +233 -233
- package/vitepress/components/Layout/AboutModal.vue +105 -105
- package/vitepress/components/Layout/AboutPanel.vue +266 -266
- package/vitepress/components/Layout/ContentCollapse.vue +603 -603
- package/vitepress/components/Layout/CustomSidebar.vue +605 -605
- package/vitepress/components/Layout/DocBeforeControls.vue +139 -139
- package/vitepress/components/Layout/DocFooter.vue +225 -225
- package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
- package/vitepress/components/Layout/ImagePreview.vue +281 -281
- package/vitepress/components/Layout/Layout.module.scss +661 -661
- package/vitepress/components/Layout/Layout.vue +542 -542
- package/vitepress/components/Layout/NoteStatus.vue +140 -140
- package/vitepress/components/Layout/SidebarItems.vue +263 -263
- package/vitepress/components/Layout/SidebarNavBefore.vue +92 -92
- package/vitepress/components/Layout/Swiper.vue +167 -167
- package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
- package/vitepress/components/Layout/ToggleFullContent.vue +34 -34
- package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
- package/vitepress/components/Layout/ToggleSidebar.vue +35 -35
- package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
- package/vitepress/components/Layout/composables/useNoteConfig.ts +121 -121
- package/vitepress/components/Layout/composables/useNoteSave.ts +173 -173
- package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
- package/vitepress/components/Layout/composables/useRedirect.ts +110 -110
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
- package/vitepress/components/Layout/homeReadme.data.ts +124 -124
- package/vitepress/components/LoadingPage/LoadingPage.vue +192 -192
- package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
- package/vitepress/components/MarkMap/MarkMap.vue +404 -404
- package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
- package/vitepress/components/Mermaid/Mermaid.vue +364 -364
- package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
- package/vitepress/components/NotesTable/NotesTable.vue +98 -98
- package/vitepress/components/NotesTable/README.md +67 -67
- package/vitepress/components/Settings/Settings.module.scss +433 -433
- package/vitepress/components/Settings/Settings.vue +306 -306
- package/vitepress/components/SidebarCard/MindMapView.vue +483 -483
- package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
- package/vitepress/components/SidebarCard/SidebarCard.vue +948 -948
- package/vitepress/components/Tooltip/Tooltip.vue +70 -70
- package/vitepress/components/constants.ts +91 -91
- package/vitepress/components/notesConfig.data.ts +73 -73
- package/vitepress/components/sidebar.data.ts +59 -59
- package/vitepress/components/tnotes-config.data.ts +21 -21
- package/vitepress/components/utils.ts +26 -26
- package/vitepress/config/index.ts +141 -141
- package/vitepress/configs/constants.ts +26 -26
- package/vitepress/configs/head.config.ts +25 -25
- package/vitepress/configs/index.ts +9 -9
- package/vitepress/configs/markdown-it.d.ts +23 -23
- package/vitepress/configs/markdown.config.ts +366 -366
- package/vitepress/configs/theme.config.ts +108 -108
- package/vitepress/plugins/buildProgressPlugin.ts +466 -390
- package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
- package/vitepress/plugins/renameNotePlugin.ts +60 -60
- package/vitepress/plugins/updateConfigPlugin.ts +63 -63
- package/vitepress/theme/index.ts +95 -95
- package/vitepress/theme/styles/base.scss +50 -50
- package/vitepress/theme/styles/components/404.scss +31 -31
- package/vitepress/theme/styles/components/collapse.scss +175 -175
- package/vitepress/theme/styles/components/markmap.scss +101 -101
- package/vitepress/theme/styles/components/swiper.scss +255 -255
- package/vitepress/theme/styles/index.scss +25 -25
- package/vitepress/theme/styles/layout.scss +62 -62
- package/vitepress/theme/styles/utilities.scss +39 -39
- package/vitepress/theme/styles/vitepress-override.scss +25 -25
|
@@ -1,404 +1,404 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
3
|
-
import { Transformer } from 'markmap-lib'
|
|
4
|
-
import { Markmap, IMarkmapOptions } from 'markmap-view'
|
|
5
|
-
import { Toolbar } from 'markmap-toolbar'
|
|
6
|
-
import 'markmap-toolbar/dist/style.css'
|
|
7
|
-
import { MARKMAP_THEME_KEY, MARKMAP_EXPAND_LEVEL_KEY } from '../constants'
|
|
8
|
-
import { icon__fullscreen, icon__fullscreen_exit, icon__confirm } from '../../assets/icons'
|
|
9
|
-
|
|
10
|
-
// doc: https://github.com/markmap/markmap/blob/205367a24603dc187f67da1658940c6cade20dce/packages/markmap-view/src/constants.ts#L15
|
|
11
|
-
import { scaleOrdinal, schemePastel2, schemeSet3, schemeTableau10 } from 'd3'
|
|
12
|
-
|
|
13
|
-
const props = defineProps({
|
|
14
|
-
content: { type: String, default: '' },
|
|
15
|
-
duration: { type: Number, default: 100 },
|
|
16
|
-
spacingVertical: { type: Number, default: 10 },
|
|
17
|
-
spacingHorizontal: { type: Number, default: 20 },
|
|
18
|
-
nodeMinHeight: { type: Number, default: 24 },
|
|
19
|
-
initialExpandLevel: { type: Number, default: 5 },
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const svgRef = ref<SVGSVGElement | null>(null)
|
|
23
|
-
const containerRef = ref<HTMLDivElement | null>(null)
|
|
24
|
-
|
|
25
|
-
let markmapInstance: Markmap | null = null
|
|
26
|
-
let observer: MutationObserver | null = null
|
|
27
|
-
let toolbarEl: HTMLElement | null = null
|
|
28
|
-
|
|
29
|
-
// 从 localStorage 读取配置,如果没有则使用 props 或默认值
|
|
30
|
-
const getInitialExpandLevel = () => {
|
|
31
|
-
if (typeof window !== 'undefined') {
|
|
32
|
-
const saved = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY)
|
|
33
|
-
if (saved) return parseInt(saved)
|
|
34
|
-
}
|
|
35
|
-
return props.initialExpandLevel
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const getThemeColorFn = () => {
|
|
39
|
-
if (typeof window !== 'undefined') {
|
|
40
|
-
const theme = localStorage.getItem(MARKMAP_THEME_KEY) || 'default'
|
|
41
|
-
switch (theme) {
|
|
42
|
-
case 'colorful':
|
|
43
|
-
return scaleOrdinal(schemeTableau10)
|
|
44
|
-
case 'dark':
|
|
45
|
-
return scaleOrdinal(schemeSet3)
|
|
46
|
-
default:
|
|
47
|
-
return scaleOrdinal(schemePastel2)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return scaleOrdinal(schemePastel2)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 可由配置/props 初始化,也可以通过工具栏改动
|
|
54
|
-
const expandLevel = ref(getInitialExpandLevel())
|
|
55
|
-
const transformer = new Transformer()
|
|
56
|
-
const isFullscreen = ref(false)
|
|
57
|
-
|
|
58
|
-
// 保存工具栏中层级输入框的引用,方便同步显示与绑定事件
|
|
59
|
-
let toolbarLevelInput: HTMLInputElement | null = null
|
|
60
|
-
|
|
61
|
-
function renderMarkmap(content: string, level = expandLevel.value) {
|
|
62
|
-
if (!svgRef.value) return
|
|
63
|
-
|
|
64
|
-
nextTick().then(() => {
|
|
65
|
-
if (markmapInstance) {
|
|
66
|
-
try {
|
|
67
|
-
markmapInstance.destroy()
|
|
68
|
-
} catch {}
|
|
69
|
-
markmapInstance = null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!content.trim()) {
|
|
73
|
-
svgRef.value!.innerHTML = '<text x="20" y="30" fill="#999">空内容</text>'
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const { root } = transformer.transform(content)
|
|
79
|
-
const colorFn = getThemeColorFn()
|
|
80
|
-
const options: Partial<IMarkmapOptions> = {
|
|
81
|
-
// autoFit 会自动调整 scale 和 position 来适配当前容器大小,在阅读大量节点内容的时候,会放大某块区域阅读,每次展开节点或者收起节点,都会自动触发 autoFit,导致阅读体验不佳,因此不启用。
|
|
82
|
-
// autoFit: true,
|
|
83
|
-
initialExpandLevel: level,
|
|
84
|
-
duration: props.duration,
|
|
85
|
-
nodeMinHeight: props.nodeMinHeight,
|
|
86
|
-
spacingVertical: props.spacingVertical,
|
|
87
|
-
spacingHorizontal: props.spacingHorizontal,
|
|
88
|
-
maxInitialScale: 2,
|
|
89
|
-
maxWidth: 400,
|
|
90
|
-
// 使用配置的主题颜色
|
|
91
|
-
color: (node): string => colorFn(`${node.state?.path || ''}`),
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
markmapInstance = Markmap.create(svgRef.value!, options, root)
|
|
95
|
-
|
|
96
|
-
setTimeout(() => {
|
|
97
|
-
try {
|
|
98
|
-
markmapInstance?.fit() // 确保居中
|
|
99
|
-
} catch (e) {
|
|
100
|
-
console.warn('fit failed', e)
|
|
101
|
-
}
|
|
102
|
-
}, 0)
|
|
103
|
-
|
|
104
|
-
initToolbar()
|
|
105
|
-
setupObserver()
|
|
106
|
-
} catch (error: any) {
|
|
107
|
-
console.error('Markmap render error:', error)
|
|
108
|
-
svgRef.value!.innerHTML = `<text x="20" y="30" fill="red">Markmap 错误: ${error.message}</text>`
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function initToolbar() {
|
|
114
|
-
if (!markmapInstance || !containerRef.value) return
|
|
115
|
-
|
|
116
|
-
// 移除现有的工具栏
|
|
117
|
-
if (toolbarEl) {
|
|
118
|
-
toolbarEl.remove()
|
|
119
|
-
toolbarEl = null
|
|
120
|
-
toolbarLevelInput = null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 创建新工具栏
|
|
124
|
-
const { el } = Toolbar.create(markmapInstance)
|
|
125
|
-
toolbarEl = el
|
|
126
|
-
toolbarEl.style.position = 'absolute'
|
|
127
|
-
toolbarEl.style.top = '1rem'
|
|
128
|
-
toolbarEl.style.right = '.5rem'
|
|
129
|
-
toolbarEl.style.scale = '.8'
|
|
130
|
-
const brand = toolbarEl.querySelector('.mm-toolbar-brand')
|
|
131
|
-
if (brand) toolbarEl.removeChild(brand)
|
|
132
|
-
containerRef.value.appendChild(toolbarEl)
|
|
133
|
-
|
|
134
|
-
// 添加自定义全屏按钮
|
|
135
|
-
addFullscreenButton(toolbarEl)
|
|
136
|
-
|
|
137
|
-
// 在工具栏中添加更新按钮 + 层级输入(并支持 Enter 键触发)
|
|
138
|
-
addUpdateButton(toolbarEl)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function addFullscreenButton(toolbar: HTMLElement) {
|
|
142
|
-
const fullscreenBtn = document.createElement('div')
|
|
143
|
-
fullscreenBtn.className = 'mm-toolbar-item'
|
|
144
|
-
fullscreenBtn.title = isFullscreen.value ? '退出全屏' : '全屏'
|
|
145
|
-
fullscreenBtn.innerHTML = isFullscreen.value
|
|
146
|
-
? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
|
|
147
|
-
: `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
|
|
148
|
-
fullscreenBtn.addEventListener('click', toggleFullscreen)
|
|
149
|
-
toolbar.appendChild(fullscreenBtn)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function addUpdateButton(toolbar: HTMLElement) {
|
|
153
|
-
// 创建更新按钮容器
|
|
154
|
-
const updateContainer = document.createElement('div')
|
|
155
|
-
updateContainer.style.display = 'flex'
|
|
156
|
-
updateContainer.style.alignItems = 'center'
|
|
157
|
-
updateContainer.style.gap = '8px'
|
|
158
|
-
updateContainer.style.marginRight = '5px'
|
|
159
|
-
|
|
160
|
-
// 创建输入框(确保具有 id/name,且样式能在工具栏中清晰可见)
|
|
161
|
-
const levelInput = document.createElement('input')
|
|
162
|
-
levelInput.type = 'number'
|
|
163
|
-
levelInput.id = 'markmap-expand-level'
|
|
164
|
-
levelInput.name = 'markmap-expand-level'
|
|
165
|
-
levelInput.min = '1'
|
|
166
|
-
levelInput.max = '100'
|
|
167
|
-
levelInput.value = expandLevel.value.toString()
|
|
168
|
-
levelInput.style.width = '2.4rem'
|
|
169
|
-
levelInput.style.height = '1.6rem'
|
|
170
|
-
levelInput.style.padding = '2px 6px'
|
|
171
|
-
levelInput.style.fontSize = '12px'
|
|
172
|
-
levelInput.style.lineHeight = '1.2'
|
|
173
|
-
levelInput.style.textAlign = 'center'
|
|
174
|
-
levelInput.style.boxSizing = 'border-box'
|
|
175
|
-
levelInput.style.borderBottom = '.5px solid var(--vp-c-tip-1)'
|
|
176
|
-
// levelInput.style.borderRadius = '4px'
|
|
177
|
-
// levelInput.style.background = 'var(--vp-c-bg)'
|
|
178
|
-
levelInput.style.color = 'var(--vp-c-tip-1)'
|
|
179
|
-
levelInput.title = '展开层级'
|
|
180
|
-
levelInput.setAttribute('aria-label', 'markmap-expand-level')
|
|
181
|
-
|
|
182
|
-
// 保存引用,供外部(props 改变)同步输入值
|
|
183
|
-
toolbarLevelInput = levelInput
|
|
184
|
-
|
|
185
|
-
// 实时限制输入范围(1-100)
|
|
186
|
-
levelInput.addEventListener('input', (e) => {
|
|
187
|
-
const input = e.target as HTMLInputElement
|
|
188
|
-
let value = parseInt(input.value)
|
|
189
|
-
|
|
190
|
-
// 如果输入为空或非数字,不做处理
|
|
191
|
-
if (input.value === '' || isNaN(value)) return
|
|
192
|
-
|
|
193
|
-
// 限制范围:1-100
|
|
194
|
-
if (value < 1) {
|
|
195
|
-
input.value = '1'
|
|
196
|
-
value = 1
|
|
197
|
-
} else if (value > 100) {
|
|
198
|
-
input.value = '100'
|
|
199
|
-
value = 100
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
// 当输入框值变化时仅更新内部 expandLevel(不自动渲染)
|
|
204
|
-
levelInput.addEventListener('change', (e) => {
|
|
205
|
-
const value = parseInt((e.target as HTMLInputElement).value)
|
|
206
|
-
if (!isNaN(value) && value >= 1 && value <= 100) {
|
|
207
|
-
expandLevel.value = value
|
|
208
|
-
} else {
|
|
209
|
-
// 回退到之前的值(避免非法输入)
|
|
210
|
-
levelInput.value = expandLevel.value.toString()
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
// 按键监听:按 Enter 时触发更新(相当于点击确认按钮)
|
|
215
|
-
levelInput.addEventListener('keydown', (e) => {
|
|
216
|
-
if (e.key === 'Enter') {
|
|
217
|
-
// 同步输入框值到 expandLevel(防止未触发 change)
|
|
218
|
-
const val = parseInt((e.target as HTMLInputElement).value)
|
|
219
|
-
if (!isNaN(val) && val >= 1 && val <= 100) {
|
|
220
|
-
expandLevel.value = val
|
|
221
|
-
} else {
|
|
222
|
-
// 如果超出范围,重置为有效值
|
|
223
|
-
levelInput.value = expandLevel.value.toString()
|
|
224
|
-
}
|
|
225
|
-
onUpdateClick()
|
|
226
|
-
}
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
// 创建按钮
|
|
230
|
-
const updateBtn = document.createElement('button')
|
|
231
|
-
updateBtn.type = 'button'
|
|
232
|
-
updateBtn.title = '确定层级并更新'
|
|
233
|
-
updateBtn.innerHTML = `<img src="${icon__confirm}" alt="确定" style="width:16px;height:16px;display:block" />`
|
|
234
|
-
updateBtn.addEventListener('click', onUpdateClick)
|
|
235
|
-
|
|
236
|
-
updateContainer.appendChild(levelInput)
|
|
237
|
-
updateContainer.appendChild(updateBtn)
|
|
238
|
-
|
|
239
|
-
// 将更新按钮插入到工具栏开头
|
|
240
|
-
toolbar.insertBefore(updateContainer, toolbar.firstChild)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 切换全屏(保持原来的实现)
|
|
244
|
-
function toggleFullscreen() {
|
|
245
|
-
if (!containerRef.value) return
|
|
246
|
-
|
|
247
|
-
if (!isFullscreen.value) {
|
|
248
|
-
// 进入全屏
|
|
249
|
-
if (containerRef.value.requestFullscreen) {
|
|
250
|
-
containerRef.value.requestFullscreen().catch((err) => {
|
|
251
|
-
console.error('全屏请求失败:', err)
|
|
252
|
-
})
|
|
253
|
-
} else if ((containerRef.value as any).webkitRequestFullscreen) {
|
|
254
|
-
;(containerRef.value as any).webkitRequestFullscreen()
|
|
255
|
-
} else if ((containerRef.value as any).msRequestFullscreen) {
|
|
256
|
-
;(containerRef.value as any).msRequestFullscreen()
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
// 退出全屏
|
|
260
|
-
if (document.exitFullscreen) {
|
|
261
|
-
document.exitFullscreen()
|
|
262
|
-
} else if ((document as any).webkitExitFullscreen) {
|
|
263
|
-
;(document as any).webkitExitFullscreen()
|
|
264
|
-
} else if ((document as any).msExitFullscreen) {
|
|
265
|
-
;(document as any).msExitFullscreen()
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// 监听全屏状态变化
|
|
271
|
-
function handleFullscreenChange() {
|
|
272
|
-
isFullscreen.value = !!(
|
|
273
|
-
document.fullscreenElement ||
|
|
274
|
-
(document as any).webkitFullscreenElement ||
|
|
275
|
-
(document as any).msFullscreenElement
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
// 更新工具栏中的全屏按钮(若存在)
|
|
279
|
-
if (toolbarEl) {
|
|
280
|
-
const fullscreenBtn = toolbarEl.querySelector(
|
|
281
|
-
'.mm-toolbar-item:last-child'
|
|
282
|
-
) as HTMLButtonElement
|
|
283
|
-
if (fullscreenBtn) {
|
|
284
|
-
fullscreenBtn.title = isFullscreen.value
|
|
285
|
-
? '退出全屏(Exit Fullscreen)'
|
|
286
|
-
: '全屏(Fullscreen)'
|
|
287
|
-
fullscreenBtn.innerHTML = isFullscreen.value
|
|
288
|
-
? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
|
|
289
|
-
: `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// 全屏模式下调整SVG高度
|
|
294
|
-
if (svgRef.value) {
|
|
295
|
-
if (isFullscreen.value) {
|
|
296
|
-
svgRef.value.style.height = 'calc(100vh - 100px)'
|
|
297
|
-
} else {
|
|
298
|
-
svgRef.value.style.height = '400px'
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// 确保居中 - 无论进入还是退出全屏都执行居中
|
|
302
|
-
setTimeout(() => {
|
|
303
|
-
if (markmapInstance) {
|
|
304
|
-
try {
|
|
305
|
-
markmapInstance.fit()
|
|
306
|
-
} catch (e) {
|
|
307
|
-
console.warn('居中失败', e)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}, 300)
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function setupObserver() {
|
|
315
|
-
if (!svgRef.value) return
|
|
316
|
-
if (observer !== null) {
|
|
317
|
-
observer.disconnect()
|
|
318
|
-
}
|
|
319
|
-
observer = new MutationObserver(() => {
|
|
320
|
-
// DOM 变动后处理(保留扩展点)
|
|
321
|
-
})
|
|
322
|
-
observer.observe(svgRef.value, {
|
|
323
|
-
childList: true,
|
|
324
|
-
subtree: true,
|
|
325
|
-
attributes: true,
|
|
326
|
-
})
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 只监听内容变化,expandLevel 改动不自动渲染(点击/Enter 确认渲染)
|
|
330
|
-
watch(
|
|
331
|
-
() => props.content,
|
|
332
|
-
(newVal) => {
|
|
333
|
-
renderMarkmap(decodeURIComponent(newVal || ''))
|
|
334
|
-
}
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
// 新增:当外部传入的 initialExpandLevel 改变时,组件应同步并重新渲染
|
|
338
|
-
watch(
|
|
339
|
-
() => props.initialExpandLevel,
|
|
340
|
-
(newVal) => {
|
|
341
|
-
if (typeof newVal === 'number' && !isNaN(newVal)) {
|
|
342
|
-
expandLevel.value = newVal
|
|
343
|
-
// 更新输入框显示(如果已创建)
|
|
344
|
-
if (toolbarLevelInput) toolbarLevelInput.value = newVal.toString()
|
|
345
|
-
// 重新渲染使用新的层级(保持对外部 prop 改动的即时响应)
|
|
346
|
-
renderMarkmap(decodeURIComponent(props.content || ''), newVal)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
// 同步:当内部 expandLevel 变更时(例如通过输入框 change 事件),更新工具栏输入显示
|
|
352
|
-
watch(expandLevel, (v) => {
|
|
353
|
-
if (toolbarLevelInput) {
|
|
354
|
-
const asStr = (v || 0).toString()
|
|
355
|
-
if (toolbarLevelInput.value !== asStr) {
|
|
356
|
-
toolbarLevelInput.value = asStr
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
// 点击更新按钮才用当前 expandLevel 渲染
|
|
362
|
-
function onUpdateClick() {
|
|
363
|
-
renderMarkmap(decodeURIComponent(props.content || ''), expandLevel.value)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
onMounted(() => {
|
|
367
|
-
renderMarkmap(decodeURIComponent(props.content || ''))
|
|
368
|
-
|
|
369
|
-
// 添加全屏事件监听
|
|
370
|
-
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
371
|
-
document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
|
|
372
|
-
document.addEventListener('MSFullscreenChange', handleFullscreenChange)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
onBeforeUnmount(() => {
|
|
376
|
-
if (markmapInstance) {
|
|
377
|
-
try {
|
|
378
|
-
markmapInstance.destroy()
|
|
379
|
-
} catch {}
|
|
380
|
-
markmapInstance = null
|
|
381
|
-
}
|
|
382
|
-
if (observer !== null) {
|
|
383
|
-
observer.disconnect()
|
|
384
|
-
observer = null
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// 移除全屏事件监听
|
|
388
|
-
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
389
|
-
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
|
|
390
|
-
document.removeEventListener('MSFullscreenChange', handleFullscreenChange)
|
|
391
|
-
})
|
|
392
|
-
</script>
|
|
393
|
-
|
|
394
|
-
<template>
|
|
395
|
-
<div
|
|
396
|
-
:class="$style.markmapContainer"
|
|
397
|
-
ref="containerRef"
|
|
398
|
-
style="position: relative"
|
|
399
|
-
>
|
|
400
|
-
<svg ref="svgRef" style="width: 100%; height: 400px"></svg>
|
|
401
|
-
</div>
|
|
402
|
-
</template>
|
|
403
|
-
|
|
404
|
-
<style module src="./MarkMap.module.scss"></style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
3
|
+
import { Transformer } from 'markmap-lib'
|
|
4
|
+
import { Markmap, IMarkmapOptions } from 'markmap-view'
|
|
5
|
+
import { Toolbar } from 'markmap-toolbar'
|
|
6
|
+
import 'markmap-toolbar/dist/style.css'
|
|
7
|
+
import { MARKMAP_THEME_KEY, MARKMAP_EXPAND_LEVEL_KEY } from '../constants'
|
|
8
|
+
import { icon__fullscreen, icon__fullscreen_exit, icon__confirm } from '../../assets/icons'
|
|
9
|
+
|
|
10
|
+
// doc: https://github.com/markmap/markmap/blob/205367a24603dc187f67da1658940c6cade20dce/packages/markmap-view/src/constants.ts#L15
|
|
11
|
+
import { scaleOrdinal, schemePastel2, schemeSet3, schemeTableau10 } from 'd3'
|
|
12
|
+
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
content: { type: String, default: '' },
|
|
15
|
+
duration: { type: Number, default: 100 },
|
|
16
|
+
spacingVertical: { type: Number, default: 10 },
|
|
17
|
+
spacingHorizontal: { type: Number, default: 20 },
|
|
18
|
+
nodeMinHeight: { type: Number, default: 24 },
|
|
19
|
+
initialExpandLevel: { type: Number, default: 5 },
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const svgRef = ref<SVGSVGElement | null>(null)
|
|
23
|
+
const containerRef = ref<HTMLDivElement | null>(null)
|
|
24
|
+
|
|
25
|
+
let markmapInstance: Markmap | null = null
|
|
26
|
+
let observer: MutationObserver | null = null
|
|
27
|
+
let toolbarEl: HTMLElement | null = null
|
|
28
|
+
|
|
29
|
+
// 从 localStorage 读取配置,如果没有则使用 props 或默认值
|
|
30
|
+
const getInitialExpandLevel = () => {
|
|
31
|
+
if (typeof window !== 'undefined') {
|
|
32
|
+
const saved = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY)
|
|
33
|
+
if (saved) return parseInt(saved)
|
|
34
|
+
}
|
|
35
|
+
return props.initialExpandLevel
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const getThemeColorFn = () => {
|
|
39
|
+
if (typeof window !== 'undefined') {
|
|
40
|
+
const theme = localStorage.getItem(MARKMAP_THEME_KEY) || 'default'
|
|
41
|
+
switch (theme) {
|
|
42
|
+
case 'colorful':
|
|
43
|
+
return scaleOrdinal(schemeTableau10)
|
|
44
|
+
case 'dark':
|
|
45
|
+
return scaleOrdinal(schemeSet3)
|
|
46
|
+
default:
|
|
47
|
+
return scaleOrdinal(schemePastel2)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return scaleOrdinal(schemePastel2)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 可由配置/props 初始化,也可以通过工具栏改动
|
|
54
|
+
const expandLevel = ref(getInitialExpandLevel())
|
|
55
|
+
const transformer = new Transformer()
|
|
56
|
+
const isFullscreen = ref(false)
|
|
57
|
+
|
|
58
|
+
// 保存工具栏中层级输入框的引用,方便同步显示与绑定事件
|
|
59
|
+
let toolbarLevelInput: HTMLInputElement | null = null
|
|
60
|
+
|
|
61
|
+
function renderMarkmap(content: string, level = expandLevel.value) {
|
|
62
|
+
if (!svgRef.value) return
|
|
63
|
+
|
|
64
|
+
nextTick().then(() => {
|
|
65
|
+
if (markmapInstance) {
|
|
66
|
+
try {
|
|
67
|
+
markmapInstance.destroy()
|
|
68
|
+
} catch {}
|
|
69
|
+
markmapInstance = null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!content.trim()) {
|
|
73
|
+
svgRef.value!.innerHTML = '<text x="20" y="30" fill="#999">空内容</text>'
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const { root } = transformer.transform(content)
|
|
79
|
+
const colorFn = getThemeColorFn()
|
|
80
|
+
const options: Partial<IMarkmapOptions> = {
|
|
81
|
+
// autoFit 会自动调整 scale 和 position 来适配当前容器大小,在阅读大量节点内容的时候,会放大某块区域阅读,每次展开节点或者收起节点,都会自动触发 autoFit,导致阅读体验不佳,因此不启用。
|
|
82
|
+
// autoFit: true,
|
|
83
|
+
initialExpandLevel: level,
|
|
84
|
+
duration: props.duration,
|
|
85
|
+
nodeMinHeight: props.nodeMinHeight,
|
|
86
|
+
spacingVertical: props.spacingVertical,
|
|
87
|
+
spacingHorizontal: props.spacingHorizontal,
|
|
88
|
+
maxInitialScale: 2,
|
|
89
|
+
maxWidth: 400,
|
|
90
|
+
// 使用配置的主题颜色
|
|
91
|
+
color: (node): string => colorFn(`${node.state?.path || ''}`),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
markmapInstance = Markmap.create(svgRef.value!, options, root)
|
|
95
|
+
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
try {
|
|
98
|
+
markmapInstance?.fit() // 确保居中
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.warn('fit failed', e)
|
|
101
|
+
}
|
|
102
|
+
}, 0)
|
|
103
|
+
|
|
104
|
+
initToolbar()
|
|
105
|
+
setupObserver()
|
|
106
|
+
} catch (error: any) {
|
|
107
|
+
console.error('Markmap render error:', error)
|
|
108
|
+
svgRef.value!.innerHTML = `<text x="20" y="30" fill="red">Markmap 错误: ${error.message}</text>`
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function initToolbar() {
|
|
114
|
+
if (!markmapInstance || !containerRef.value) return
|
|
115
|
+
|
|
116
|
+
// 移除现有的工具栏
|
|
117
|
+
if (toolbarEl) {
|
|
118
|
+
toolbarEl.remove()
|
|
119
|
+
toolbarEl = null
|
|
120
|
+
toolbarLevelInput = null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 创建新工具栏
|
|
124
|
+
const { el } = Toolbar.create(markmapInstance)
|
|
125
|
+
toolbarEl = el
|
|
126
|
+
toolbarEl.style.position = 'absolute'
|
|
127
|
+
toolbarEl.style.top = '1rem'
|
|
128
|
+
toolbarEl.style.right = '.5rem'
|
|
129
|
+
toolbarEl.style.scale = '.8'
|
|
130
|
+
const brand = toolbarEl.querySelector('.mm-toolbar-brand')
|
|
131
|
+
if (brand) toolbarEl.removeChild(brand)
|
|
132
|
+
containerRef.value.appendChild(toolbarEl)
|
|
133
|
+
|
|
134
|
+
// 添加自定义全屏按钮
|
|
135
|
+
addFullscreenButton(toolbarEl)
|
|
136
|
+
|
|
137
|
+
// 在工具栏中添加更新按钮 + 层级输入(并支持 Enter 键触发)
|
|
138
|
+
addUpdateButton(toolbarEl)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function addFullscreenButton(toolbar: HTMLElement) {
|
|
142
|
+
const fullscreenBtn = document.createElement('div')
|
|
143
|
+
fullscreenBtn.className = 'mm-toolbar-item'
|
|
144
|
+
fullscreenBtn.title = isFullscreen.value ? '退出全屏' : '全屏'
|
|
145
|
+
fullscreenBtn.innerHTML = isFullscreen.value
|
|
146
|
+
? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
|
|
147
|
+
: `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
|
|
148
|
+
fullscreenBtn.addEventListener('click', toggleFullscreen)
|
|
149
|
+
toolbar.appendChild(fullscreenBtn)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function addUpdateButton(toolbar: HTMLElement) {
|
|
153
|
+
// 创建更新按钮容器
|
|
154
|
+
const updateContainer = document.createElement('div')
|
|
155
|
+
updateContainer.style.display = 'flex'
|
|
156
|
+
updateContainer.style.alignItems = 'center'
|
|
157
|
+
updateContainer.style.gap = '8px'
|
|
158
|
+
updateContainer.style.marginRight = '5px'
|
|
159
|
+
|
|
160
|
+
// 创建输入框(确保具有 id/name,且样式能在工具栏中清晰可见)
|
|
161
|
+
const levelInput = document.createElement('input')
|
|
162
|
+
levelInput.type = 'number'
|
|
163
|
+
levelInput.id = 'markmap-expand-level'
|
|
164
|
+
levelInput.name = 'markmap-expand-level'
|
|
165
|
+
levelInput.min = '1'
|
|
166
|
+
levelInput.max = '100'
|
|
167
|
+
levelInput.value = expandLevel.value.toString()
|
|
168
|
+
levelInput.style.width = '2.4rem'
|
|
169
|
+
levelInput.style.height = '1.6rem'
|
|
170
|
+
levelInput.style.padding = '2px 6px'
|
|
171
|
+
levelInput.style.fontSize = '12px'
|
|
172
|
+
levelInput.style.lineHeight = '1.2'
|
|
173
|
+
levelInput.style.textAlign = 'center'
|
|
174
|
+
levelInput.style.boxSizing = 'border-box'
|
|
175
|
+
levelInput.style.borderBottom = '.5px solid var(--vp-c-tip-1)'
|
|
176
|
+
// levelInput.style.borderRadius = '4px'
|
|
177
|
+
// levelInput.style.background = 'var(--vp-c-bg)'
|
|
178
|
+
levelInput.style.color = 'var(--vp-c-tip-1)'
|
|
179
|
+
levelInput.title = '展开层级'
|
|
180
|
+
levelInput.setAttribute('aria-label', 'markmap-expand-level')
|
|
181
|
+
|
|
182
|
+
// 保存引用,供外部(props 改变)同步输入值
|
|
183
|
+
toolbarLevelInput = levelInput
|
|
184
|
+
|
|
185
|
+
// 实时限制输入范围(1-100)
|
|
186
|
+
levelInput.addEventListener('input', (e) => {
|
|
187
|
+
const input = e.target as HTMLInputElement
|
|
188
|
+
let value = parseInt(input.value)
|
|
189
|
+
|
|
190
|
+
// 如果输入为空或非数字,不做处理
|
|
191
|
+
if (input.value === '' || isNaN(value)) return
|
|
192
|
+
|
|
193
|
+
// 限制范围:1-100
|
|
194
|
+
if (value < 1) {
|
|
195
|
+
input.value = '1'
|
|
196
|
+
value = 1
|
|
197
|
+
} else if (value > 100) {
|
|
198
|
+
input.value = '100'
|
|
199
|
+
value = 100
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// 当输入框值变化时仅更新内部 expandLevel(不自动渲染)
|
|
204
|
+
levelInput.addEventListener('change', (e) => {
|
|
205
|
+
const value = parseInt((e.target as HTMLInputElement).value)
|
|
206
|
+
if (!isNaN(value) && value >= 1 && value <= 100) {
|
|
207
|
+
expandLevel.value = value
|
|
208
|
+
} else {
|
|
209
|
+
// 回退到之前的值(避免非法输入)
|
|
210
|
+
levelInput.value = expandLevel.value.toString()
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// 按键监听:按 Enter 时触发更新(相当于点击确认按钮)
|
|
215
|
+
levelInput.addEventListener('keydown', (e) => {
|
|
216
|
+
if (e.key === 'Enter') {
|
|
217
|
+
// 同步输入框值到 expandLevel(防止未触发 change)
|
|
218
|
+
const val = parseInt((e.target as HTMLInputElement).value)
|
|
219
|
+
if (!isNaN(val) && val >= 1 && val <= 100) {
|
|
220
|
+
expandLevel.value = val
|
|
221
|
+
} else {
|
|
222
|
+
// 如果超出范围,重置为有效值
|
|
223
|
+
levelInput.value = expandLevel.value.toString()
|
|
224
|
+
}
|
|
225
|
+
onUpdateClick()
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// 创建按钮
|
|
230
|
+
const updateBtn = document.createElement('button')
|
|
231
|
+
updateBtn.type = 'button'
|
|
232
|
+
updateBtn.title = '确定层级并更新'
|
|
233
|
+
updateBtn.innerHTML = `<img src="${icon__confirm}" alt="确定" style="width:16px;height:16px;display:block" />`
|
|
234
|
+
updateBtn.addEventListener('click', onUpdateClick)
|
|
235
|
+
|
|
236
|
+
updateContainer.appendChild(levelInput)
|
|
237
|
+
updateContainer.appendChild(updateBtn)
|
|
238
|
+
|
|
239
|
+
// 将更新按钮插入到工具栏开头
|
|
240
|
+
toolbar.insertBefore(updateContainer, toolbar.firstChild)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 切换全屏(保持原来的实现)
|
|
244
|
+
function toggleFullscreen() {
|
|
245
|
+
if (!containerRef.value) return
|
|
246
|
+
|
|
247
|
+
if (!isFullscreen.value) {
|
|
248
|
+
// 进入全屏
|
|
249
|
+
if (containerRef.value.requestFullscreen) {
|
|
250
|
+
containerRef.value.requestFullscreen().catch((err) => {
|
|
251
|
+
console.error('全屏请求失败:', err)
|
|
252
|
+
})
|
|
253
|
+
} else if ((containerRef.value as any).webkitRequestFullscreen) {
|
|
254
|
+
;(containerRef.value as any).webkitRequestFullscreen()
|
|
255
|
+
} else if ((containerRef.value as any).msRequestFullscreen) {
|
|
256
|
+
;(containerRef.value as any).msRequestFullscreen()
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// 退出全屏
|
|
260
|
+
if (document.exitFullscreen) {
|
|
261
|
+
document.exitFullscreen()
|
|
262
|
+
} else if ((document as any).webkitExitFullscreen) {
|
|
263
|
+
;(document as any).webkitExitFullscreen()
|
|
264
|
+
} else if ((document as any).msExitFullscreen) {
|
|
265
|
+
;(document as any).msExitFullscreen()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 监听全屏状态变化
|
|
271
|
+
function handleFullscreenChange() {
|
|
272
|
+
isFullscreen.value = !!(
|
|
273
|
+
document.fullscreenElement ||
|
|
274
|
+
(document as any).webkitFullscreenElement ||
|
|
275
|
+
(document as any).msFullscreenElement
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
// 更新工具栏中的全屏按钮(若存在)
|
|
279
|
+
if (toolbarEl) {
|
|
280
|
+
const fullscreenBtn = toolbarEl.querySelector(
|
|
281
|
+
'.mm-toolbar-item:last-child'
|
|
282
|
+
) as HTMLButtonElement
|
|
283
|
+
if (fullscreenBtn) {
|
|
284
|
+
fullscreenBtn.title = isFullscreen.value
|
|
285
|
+
? '退出全屏(Exit Fullscreen)'
|
|
286
|
+
: '全屏(Fullscreen)'
|
|
287
|
+
fullscreenBtn.innerHTML = isFullscreen.value
|
|
288
|
+
? `<img src="${icon__fullscreen_exit}" alt="退出全屏" style="width:18px;height:18px;display:block" />`
|
|
289
|
+
: `<img src="${icon__fullscreen}" alt="全屏" style="width:18px;height:18px;display:block" />`
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 全屏模式下调整SVG高度
|
|
294
|
+
if (svgRef.value) {
|
|
295
|
+
if (isFullscreen.value) {
|
|
296
|
+
svgRef.value.style.height = 'calc(100vh - 100px)'
|
|
297
|
+
} else {
|
|
298
|
+
svgRef.value.style.height = '400px'
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 确保居中 - 无论进入还是退出全屏都执行居中
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
if (markmapInstance) {
|
|
304
|
+
try {
|
|
305
|
+
markmapInstance.fit()
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.warn('居中失败', e)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}, 300)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function setupObserver() {
|
|
315
|
+
if (!svgRef.value) return
|
|
316
|
+
if (observer !== null) {
|
|
317
|
+
observer.disconnect()
|
|
318
|
+
}
|
|
319
|
+
observer = new MutationObserver(() => {
|
|
320
|
+
// DOM 变动后处理(保留扩展点)
|
|
321
|
+
})
|
|
322
|
+
observer.observe(svgRef.value, {
|
|
323
|
+
childList: true,
|
|
324
|
+
subtree: true,
|
|
325
|
+
attributes: true,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 只监听内容变化,expandLevel 改动不自动渲染(点击/Enter 确认渲染)
|
|
330
|
+
watch(
|
|
331
|
+
() => props.content,
|
|
332
|
+
(newVal) => {
|
|
333
|
+
renderMarkmap(decodeURIComponent(newVal || ''))
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
// 新增:当外部传入的 initialExpandLevel 改变时,组件应同步并重新渲染
|
|
338
|
+
watch(
|
|
339
|
+
() => props.initialExpandLevel,
|
|
340
|
+
(newVal) => {
|
|
341
|
+
if (typeof newVal === 'number' && !isNaN(newVal)) {
|
|
342
|
+
expandLevel.value = newVal
|
|
343
|
+
// 更新输入框显示(如果已创建)
|
|
344
|
+
if (toolbarLevelInput) toolbarLevelInput.value = newVal.toString()
|
|
345
|
+
// 重新渲染使用新的层级(保持对外部 prop 改动的即时响应)
|
|
346
|
+
renderMarkmap(decodeURIComponent(props.content || ''), newVal)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
// 同步:当内部 expandLevel 变更时(例如通过输入框 change 事件),更新工具栏输入显示
|
|
352
|
+
watch(expandLevel, (v) => {
|
|
353
|
+
if (toolbarLevelInput) {
|
|
354
|
+
const asStr = (v || 0).toString()
|
|
355
|
+
if (toolbarLevelInput.value !== asStr) {
|
|
356
|
+
toolbarLevelInput.value = asStr
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// 点击更新按钮才用当前 expandLevel 渲染
|
|
362
|
+
function onUpdateClick() {
|
|
363
|
+
renderMarkmap(decodeURIComponent(props.content || ''), expandLevel.value)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
onMounted(() => {
|
|
367
|
+
renderMarkmap(decodeURIComponent(props.content || ''))
|
|
368
|
+
|
|
369
|
+
// 添加全屏事件监听
|
|
370
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
371
|
+
document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
|
|
372
|
+
document.addEventListener('MSFullscreenChange', handleFullscreenChange)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
onBeforeUnmount(() => {
|
|
376
|
+
if (markmapInstance) {
|
|
377
|
+
try {
|
|
378
|
+
markmapInstance.destroy()
|
|
379
|
+
} catch {}
|
|
380
|
+
markmapInstance = null
|
|
381
|
+
}
|
|
382
|
+
if (observer !== null) {
|
|
383
|
+
observer.disconnect()
|
|
384
|
+
observer = null
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 移除全屏事件监听
|
|
388
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
389
|
+
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
|
|
390
|
+
document.removeEventListener('MSFullscreenChange', handleFullscreenChange)
|
|
391
|
+
})
|
|
392
|
+
</script>
|
|
393
|
+
|
|
394
|
+
<template>
|
|
395
|
+
<div
|
|
396
|
+
:class="$style.markmapContainer"
|
|
397
|
+
ref="containerRef"
|
|
398
|
+
style="position: relative"
|
|
399
|
+
>
|
|
400
|
+
<svg ref="svgRef" style="width: 100%; height: 400px"></svg>
|
|
401
|
+
</div>
|
|
402
|
+
</template>
|
|
403
|
+
|
|
404
|
+
<style module src="./MarkMap.module.scss"></style>
|