24.主题魔改:添加文章思维导图预览功能

24.主题魔改:添加文章思维导图预览功能

前言:功能介绍

本指南将引导您为 AnZhiYu 主题的博客添加一个强大的文章思维导图预览功能。启用后,在包含大纲结构的长文章中,读者可以通过点击右下角的思维导图按钮,弹出一个精美的模态框,以可视化的树状结构预览整篇文章的目录,帮助快速理解文章框架。

核心特性:

  • 🎯 条件性显示:仅在 Front Matter 中设置 mindmap: true 的文章显示按钮
  • 🗺️ 可视化预览:基于 Markmap 库,将文章 H1-H6 标题渲染为交互式思维导图
  • 💾 智能缓存:首次渲染后缓存,重复打开速度<10ms
  • 📥 SVG 导出:支持导出高质量的 SVG 矢量图
  • 📱 全屏模式:移动端友好的全屏查看体验
  • PJAX 完美兼容:页面跳转无bug,自动清理资源

第一步:添加配置项

打开博客根目录下的 _config.anzhiyu.yml 文件,找到 rightside_item_order 部分,添加 mindmap 按钮:

1
2
3
4
5
6
7
# 非必要请不要修改
# 可选的右下角按钮项:readmode,translate,darkmode,hideAside,toc,chat,comment,downloadMd,docToc,codeRunner,mindmap
# 不要重复
rightside_item_order: # 右下角按钮顺序和显示控制
enable: true # 是否启用自定义右下角按钮顺序
hide: readmode,translate,darkmode,hideAside # 要隐藏的按钮列表
show: toc,chat,comment,downloadMd,docToc,codeRunner,mindmap # 添加 mindmap

接着,在同一文件中添加思维导图的详细配置(建议放在 doc_toc 配置项之后):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 思维导图功能 (Mindmap Feature)
mindmap:
enable: true # 是否启用思维导图功能
d3_cdn: https://cdn.jsdelivr.net/npm/d3@7 # D3.js CDN地址
lib_cdn: https://cdn.jsdelivr.net/npm/markmap-lib@0.15.4/dist/browser/index.js # Markmap-lib CDN (包含Transformer)
view_cdn: https://cdn.jsdelivr.net/npm/markmap-view@0.15.4/dist/browser/index.js # Markmap-view CDN
icon: fa-sitemap # 按钮图标 (FontAwesome图标)

# 思维导图样式配置
options:
duration: 500 # 动画时长 (毫秒)
maxWidth: 300 # 节点最大宽度 (像素)
# 主题配色 (支持亮色/暗色模式)
color:
light: ['#4299E1', '#48BB78', '#ED8936', '#9F7AEA'] # 亮色模式配色
dark: ['#63B3ED', '#68D391', '#F6AD55', '#B794F4'] # 暗色模式配色

# 全屏模式配置
fullscreen:
enable: true # 是否启用全屏功能
mobile_only: false # false=PC和移动端都支持,true=仅移动端支持
button_text: "全屏查看" # 全屏按钮文本

# 导出功能配置
export:
enable: true # 是否启用导出功能
formats: [svg] # 支持的导出格式(仅SVG,PNG因跨域限制已移除)
filename_format: "{title}-mindmap" # 导出文件名格式 ({title}会被替换为文章标题)

第二步:添加 Rightside 按钮

修改 themes/anzhiyu/layout/includes/rightside.pug 文件,在 codeRunner 按钮之后添加 mindmap 按钮:

1
2
3
4
5
6
7
8
when 'codeRunner'
if theme.code_runner && theme.code_runner.enable
button#code-runner-btn(type="button" title=theme.code_runner.button_title || "代码运行器")
i.fas.fa-code
when 'mindmap'
if is_post() && theme.mindmap && theme.mindmap.enable
button#mindmap-btn(type="button" title="文章思维导图" style="display:none" data-mindmap-enabled="false")
i.fas(class=theme.mindmap.icon || 'fa-sitemap')

第三步:创建 Modal 组件

themes/anzhiyu/layout/includes/ 目录下,新建文件 mindmap-modal.pug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//- 思维导图 Modal 组件
#mindmap-modal.mindmap-modal(style="display:none")
.mindmap-overlay
.mindmap-content
//- Header 区域
.mindmap-header
h3.mindmap-title
i.fas.fa-sitemap
span 文章思维导图
.mindmap-controls
if theme.mindmap && theme.mindmap.fullscreen && theme.mindmap.fullscreen.enable
button#mindmap-btn-fullscreen.mindmap-btn(type="button" title="全屏")
i.fas.fa-expand
if theme.mindmap && theme.mindmap.export && theme.mindmap.export.enable
button#mindmap-btn-export.mindmap-btn(type="button" title="导出SVG")
i.fas.fa-download
button#mindmap-btn-close.mindmap-btn(type="button" title="关闭")
i.fas.fa-times

//- Body 区域(思维导图容器)
.mindmap-body
svg#mindmap-svg-container
.mindmap-empty-state(style="display:none")
i.fas.fa-inbox
p 本文暂无目录结构

然后在 themes/anzhiyu/layout/includes/layout.pug 中引入此组件,找到代码运行器面板之后,添加:

1
2
3
//- 思维导图模态框
if theme.mindmap && theme.mindmap.enable
include ./mindmap-modal.pug

第四步:创建样式文件

themes/anzhiyu/source/css/_extra/ 目录下创建 mindmap 文件夹,并新建 mindmap.css 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/* 思维导图模态框样式 */
.mindmap-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
}

.mindmap-modal.show {
display: flex;
opacity: 1;
}

/* 遮罩层 */
.mindmap-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(5px);
}

/* 内容容器 */
.mindmap-content {
position: relative;
width: 90vw;
height: 85vh;
max-width: 1400px;
margin: auto;
background: var(--anzhiyu-card-bg);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
z-index: 10001;
overflow: hidden;
animation: mindmap-scale-in 0.3s ease;
}

@keyframes mindmap-scale-in {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}

/* Header 区域 */
.mindmap-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid var(--anzhiyu-card-border);
background: var(--anzhiyu-background);
}

.mindmap-title {
display: flex;
align-items: center;
gap: 12px;
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--anzhiyu-fontcolor);
}

.mindmap-title i {
font-size: 20px;
color: var(--anzhiyu-main);
}

.mindmap-controls {
display: flex;
gap: 8px;
align-items: center;
}

.mindmap-btn {
width: 36px;
height: 36px;
border: none;
border-radius: 8px;
background: var(--anzhiyu-card-bg);
color: var(--anzhiyu-fontcolor);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}

.mindmap-btn:hover {
background: var(--anzhiyu-main);
color: #fff;
transform: translateY(-2px);
}

/* Body 区域 */
.mindmap-body {
flex: 1;
position: relative;
overflow: hidden;
background: var(--anzhiyu-background);
}

#mindmap-svg-container {
width: 100%;
height: 100%;
display: block;
cursor: grab;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

#mindmap-svg-container:active {
cursor: grabbing;
}

#mindmap-svg-container * {
font-family: inherit;
user-select: none;
-webkit-user-select: none;
}

/* 空状态 */
.mindmap-empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--anzhiyu-secondtext);
}

.mindmap-empty-state i {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}

.mindmap-empty-state p {
font-size: 16px;
margin: 0;
}

/* 全屏模式 */
.mindmap-modal.fullscreen {
padding: 0;
}

.mindmap-modal.fullscreen .mindmap-content {
width: 100vw;
height: 100vh;
max-width: none;
border-radius: 0;
}

.mindmap-modal.fullscreen #mindmap-btn-fullscreen i:before {
content: '\f066'; /* fa-compress */
}

/* 暗色模式优化 */
[data-theme='dark'] .mindmap-content {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
}

[data-theme='dark'] #mindmap-svg-container {
background: var(--anzhiyu-background);
}

/* 移动端适配 */
@media screen and (max-width: 768px) {
.mindmap-content {
width: 95vw;
height: 90vh;
}

.mindmap-header {
padding: 12px 16px;
}

.mindmap-title {
font-size: 16px;
}

.mindmap-title i {
font-size: 18px;
}

.mindmap-btn {
width: 32px;
height: 32px;
font-size: 14px;
}
}

/* Markmap 样式优化 - 保持官方默认样式 */
.mindmap-body svg text {
cursor: pointer;
}

.mindmap-body svg g:hover text {
font-weight: 600;
}

/* 加载动画 */
.mindmap-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}

.mindmap-loading i {
font-size: 48px;
color: var(--anzhiyu-main);
animation: mindmap-spin 1s linear infinite;
}

@keyframes mindmap-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.mindmap-loading p {
margin-top: 16px;
color: var(--anzhiyu-secondtext);
font-size: 14px;
}

然后修改 themes/anzhiyu/source/css/index.styl,在合适位置添加条件引入(注意:由于 @import '_extra/**/*.css' 已经会自动引入,这一步可选)。


第五步:添加核心引擎脚本

修改 themes/anzhiyu/layout/post.pug 文件,在文件末尾的 window.postData 数据注入之后,添加思维导图功能脚本:

首先,修改 window.postData 部分,添加 mindmap 字段:

1
2
3
4
5
6
7
8
9
10
11
//- 文章源文件下载功能:嵌入文章数据
script.
window.postData = {
title: "#{page.title || ''}",
date: "#{page.date ? page.date.format('YYYY-MM-DD HH:mm:ss') : ''}",
updated: "#{page.updated ? page.updated.format('YYYY-MM-DD HH:mm:ss') : (page.date ? page.date.format('YYYY-MM-DD HH:mm:ss') : '')}",
tags: !{JSON.stringify(page.tags ? page.tags.map(tag => tag.name) : [])},
categories: !{JSON.stringify(page.categories ? page.categories.map(cat => cat.name) : [])},
content: !{JSON.stringify((page._content || '').replace(/<\/script>/gi, '<\\/script>').replace(/<\/style>/gi, '<\\/style>'))},
mindmap: #{page.mindmap ? 'true' : 'false'}
};

然后添加思维导图核心引擎(完整代码较长,请参考下方折叠块):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
//- 思维导图功能:配置注入 + 核心引擎
if theme.mindmap && theme.mindmap.enable
script(data-pjax).
// 思维导图配置
window.MINDMAP_CONFIG = {
enable: true,
d3Cdn: '!{theme.mindmap.d3_cdn}',
libCdn: '!{theme.mindmap.lib_cdn}',
viewCdn: '!{theme.mindmap.view_cdn}',
icon: '!{theme.mindmap.icon || "fa-sitemap"}',
options: !{JSON.stringify(theme.mindmap.options || {})},
fullscreen: !{JSON.stringify(theme.mindmap.fullscreen || {})},
export: !{JSON.stringify(theme.mindmap.export || {})}
};

// MindmapEngine 核心类(防止 PJAX 重复声明)
if (typeof window.MindmapEngine === 'undefined') {
window.MindmapEngine = class MindmapEngine {
constructor() {
this.state = {
rendered: false,
visible: false,
fullscreen: false,
markmapLoaded: false
};
this.cache = {
treeData: null,
svgElement: null,
markmap: null
};
this.modal = document.getElementById('mindmap-modal');
this.container = document.getElementById('mindmap-svg-container');
this.bindEvents();
}

// 提取文章标题并转换为 Markdown 格式(修复层级)
extractHeadingsAsMarkdown() {
const articleContainer = document.getElementById('article-container');
if (!articleContainer) return '';

const headings = Array.from(articleContainer.querySelectorAll('h1, h2, h3, h4, h5, h6'));
if (headings.length === 0) return '';

// 初始化 idMap
if (!this.idMap) this.idMap = new Map();

// 找到最小的标题层级作为根节点层级
const minLevel = Math.min(...headings.map(h => parseInt(h.tagName[1])));

// 构建 Markdown - 从根节点开始
let markdown = `# ${window.postData.title || '文章目录'}\n\n`;

headings.forEach(h => {
const level = parseInt(h.tagName[1]);
const text = h.textContent.trim();
const id = h.id || '';

// 调整层级:最小层级变为 ## (二级),依此类推
const adjustedLevel = level - minLevel + 2;
markdown += '#'.repeat(adjustedLevel) + ' ' + text + '\n';

// 保存 ID 映射关系
this.idMap.set(text, id);
});

console.log('[Mindmap] Generated markdown:', markdown);
return markdown;
}

// 加载脚本辅助函数
loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(script);
});
}

// 加载 Markmap 所有依赖
async loadMarkmap() {
if (this.state.markmapLoaded && window.markmap) {
return;
}

try {
// 1. 加载 D3
if (!window.d3) {
await this.loadScript(window.MINDMAP_CONFIG.d3Cdn);
}

// 2. 加载 markmap-lib (包含 Transformer)
if (!window.markmap || !window.markmap.Transformer) {
await this.loadScript(window.MINDMAP_CONFIG.libCdn);
}

// 3. 加载 markmap-view (包含 Markmap)
if (!window.markmap || !window.markmap.Markmap) {
await this.loadScript(window.MINDMAP_CONFIG.viewCdn);
}

this.state.markmapLoaded = true;
} catch (error) {
console.error('[Mindmap] Failed to load libraries:', error);
throw error;
}
}

// 显示思维导图
async show() {
if (this.state.rendered && this.cache.markmap) {
// 显示缓存
this.showCached();
return;
}

// 首次渲染
await this.firstRender();
}

// 首次渲染
async firstRender() {
try {
// 显示加载状态
this.showLoading();

// 提取标题为 Markdown 格式
const markdown = this.extractHeadingsAsMarkdown();
if (!markdown || markdown.trim().length === 0) {
this.showEmptyState();
return;
}

// 加载 Markmap 库
await this.loadMarkmap();

// 使用 Transformer 转换 Markdown
const { Transformer, Markmap } = window.markmap;
const transformer = new Transformer();
const { root, features } = transformer.transform(markdown);

// 缓存转换后的数据
this.cache.treeData = root;

// 渲染思维导图
const options = {
duration: window.MINDMAP_CONFIG.options?.duration || 500,
maxWidth: window.MINDMAP_CONFIG.options?.maxWidth || 300,
embedGlobalCSS: false
};

this.cache.markmap = Markmap.create(this.container, options, root);

// 缓存 SVG (容器本身就是 svg)
setTimeout(() => {
this.cache.svgElement = this.container.cloneNode(true);
}, 600);

this.state.rendered = true;
this.hideLoading();

} catch (error) {
console.error('[Mindmap] Render failed:', error);
anzhiyu.snackbarShow('思维导图加载失败: ' + error.message, false, 3000);
this.hideModal();
}
}

// 显示缓存
showCached() {
this.showModal();
if (this.cache.markmap) {
this.cache.markmap.fit();
}
}

// 节点点击功能已移除(保持简洁)

// 显示/隐藏 Modal
showModal() {
this.modal.style.display = 'flex';
setTimeout(() => this.modal.classList.add('show'), 10);
this.state.visible = true;
document.body.style.overflow = 'hidden';
}

hideModal() {
this.modal.classList.remove('show');
setTimeout(() => {
this.modal.style.display = 'none';
document.body.style.overflow = '';
}, 300);
this.state.visible = false;
this.state.fullscreen = false;
this.modal.classList.remove('fullscreen');
}

// 加载/空状态
showLoading() {
const loading = document.createElement('div');
loading.className = 'mindmap-loading';
loading.innerHTML = '<i class="fas fa-spinner fa-spin"></i><p>正在生成思维导图...</p>';
this.container.appendChild(loading);
}

hideLoading() {
const loading = this.container.querySelector('.mindmap-loading');
if (loading) loading.remove();
}

showEmptyState() {
document.querySelector('.mindmap-empty-state').style.display = 'block';
this.hideLoading();
}

// 全屏切换
toggleFullscreen() {
if (!this.state.fullscreen) {
this.modal.classList.add('fullscreen');
this.state.fullscreen = true;
const btn = document.getElementById('mindmap-btn-fullscreen');
if (btn) btn.querySelector('i').className = 'fas fa-compress';
} else {
this.modal.classList.remove('fullscreen');
this.state.fullscreen = false;
const btn = document.getElementById('mindmap-btn-fullscreen');
if (btn) btn.querySelector('i').className = 'fas fa-expand';
}

if (this.cache.markmap) {
setTimeout(() => this.cache.markmap.fit(), 100);
}
}

// 导出 SVG
exportAsSVG() {
if (!this.cache.svgElement) {
anzhiyu.snackbarShow('请先打开思维导图', false, 2000);
return;
}

const svgData = new XMLSerializer().serializeToString(this.cache.svgElement);
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
this.downloadBlob(svgBlob, this.getExportFilename('svg'));
}

// PNG 导出已移除(跨域限制)

// 下载文件
downloadBlob(blob, filename) {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
anzhiyu.snackbarShow('导出成功', false, 2000);
}

// 生成导出文件名
getExportFilename(format) {
const template = window.MINDMAP_CONFIG.export.filename_format || '{title}-mindmap';
const filename = template.replace('{title}', window.postData.title || 'mindmap');
return `${filename}.${format}`;
}

// 缩放控制已移除(Markmap 支持鼠标滚轮缩放和拖拽)

// 绑定事件
bindEvents() {
// 检查 modal 是否存在
if (!this.modal || !this.container) {
console.warn('[Mindmap] Modal elements not found, skipping initialization');
return;
}

// 按钮点击
const mindmapBtn = document.getElementById('mindmap-btn');
if (mindmapBtn) {
mindmapBtn.addEventListener('click', () => {
this.showModal();
this.show();
});
}

// 关闭按钮
const closeBtn = document.getElementById('mindmap-btn-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.hideModal());
}

// 遮罩层点击
const overlay = this.modal.querySelector('.mindmap-overlay');
if (overlay) {
overlay.addEventListener('click', () => this.hideModal());
}

// 全屏按钮
const fullscreenBtn = document.getElementById('mindmap-btn-fullscreen');
if (fullscreenBtn) {
fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
}

// 导出按钮(仅SVG)
const exportBtn = document.getElementById('mindmap-btn-export');
if (exportBtn) {
exportBtn.addEventListener('click', () => this.exportAsSVG());
}

// 缩放按钮已移除

// ESC 键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.state.visible) {
this.hideModal();
}
});
}

// PJAX 清理
destroy() {
if (this.cache.markmap) {
this.cache.markmap.destroy && this.cache.markmap.destroy();
}
this.state = { rendered: false, visible: false, fullscreen: false, markmapLoaded: false };
this.cache = { treeData: null, svgElement: null, markmap: null };
}
};
}

// 初始化函数(PJAX 模式B:立即执行)
function initMindmapFeature() {
// 检查是否启用了思维导图功能
if (!window.postData || !window.postData.mindmap) {
const mindmapBtn = document.getElementById('mindmap-btn');
if (mindmapBtn) {
mindmapBtn.style.display = 'none';
mindmapBtn.dataset.mindmapEnabled = 'false';
}
return;
}

// 立即尝试初始化,如果元素不存在则重试
const modal = document.getElementById('mindmap-modal');
const container = document.getElementById('mindmap-svg-container');

if (!modal || !container) {
console.log('[Mindmap] Modal not ready, retrying...');
setTimeout(initMindmapFeature, 50);
return;
}

// 显示按钮
const mindmapBtn = document.getElementById('mindmap-btn');
if (mindmapBtn) {
mindmapBtn.style.display = 'block';
mindmapBtn.dataset.mindmapEnabled = 'true';
}

// 初始化引擎(防止重复初始化)
if (!window.__mindmapEngine) {
window.__mindmapEngine = new window.MindmapEngine();
console.log('[Mindmap] Engine initialized');
}
}

// 立即执行初始化(关键!模仿 about 页面)
initMindmapFeature();

// PJAX 清理钩子
if (typeof pjax !== 'undefined') {
document.addEventListener('pjax:send', () => {
if (window.__mindmapEngine) {
window.__mindmapEngine.destroy();
window.__mindmapEngine = null;
}
});
}


第六步:使用方法

在任意文章的 Front Matter 中添加 mindmap: true 即可启用思维导图功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
title: 我的长文章标题
date: 2025-07-11 16:00:00
tags:
- 教程
- 技术分享
mindmap: true # 添加这一行启用思维导图
---

# 第一章 概述
## 1.1 背景介绍
## 1.2 核心概念

# 第二章 实战
## 2.1 环境搭建
### 2.1.1 安装依赖
### 2.1.2 配置文件

保存后,运行 hexo clean && hexo generate && hexo server,访问该文章,您应该能在右下角看到思维导图按钮(🗺️图标)。


功能说明

预览思维导图

  • 点击右下角的思维导图按钮,弹出 Modal 窗口
  • Markmap 会自动将文章的 H1-H6 标题渲染为树状结构
  • 支持鼠标滚轮缩放、拖拽平移

全屏模式

  • 点击全屏按钮(扩展图标)进入全屏
  • 移动端体验更佳
  • 再次点击或按 ESC 键退出全屏

导出功能

  • 点击下载按钮导出 SVG 矢量图
  • 文件名格式:文章标题-mindmap.svg
  • SVG 可在任意矢量图编辑器中打开和编辑

智能缓存

  • 首次打开时渲染并缓存
  • 重复打开直接显示缓存(<10ms)
  • PJAX 跳转后自动清理缓存

PJAX 兼容

  • 采用 data-pjax + 立即执行模式
  • 页面跳转时自动初始化/清理

注意事项

  1. 依赖 FontAwesome:确保主题已启用 FontAwesome 图标库(AnZhiYu 默认已集成)
  2. 文章结构:思维导图基于文章的 H1-H6 标题生成,建议文章具有清晰的标题结构
  3. 性能优化:Markmap 库约 50KB,仅在包含 mindmap: true 的文章中按需加载
  4. 移动端:建议使用全屏模式以获得更好的查看体验

自定义配置

更换图标

修改 _config.anzhiyu.yml 中的 mindmap.icon 字段,可使用任意 FontAwesome 图标,如:

  • fa-project-diagram
  • fa-network-wired
  • fa-code-branch

调整颜色

修改 mindmap.options.color 中的颜色数组,自定义节点配色。

CDN 更换

如果默认 CDN 访问慢,可更换为国内镜像,如:

1
d3_cdn: https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/d3/7.0.0/d3.min.js

故障排查

问题1:按钮不显示

  • 检查文章 Front Matter 中是否添加了 mindmap: true
  • 检查 _config.anzhiyu.ymlmindmap.enable 是否为 true
  • 检查 rightside_item_order.show 中是否包含 mindmap

问题2:思维导图无法渲染

  • 打开浏览器控制台查看错误信息
  • 检查网络是否能访问 CDN 地址
  • 尝试更换 CDN 镜像

问题3:PJAX 跳转后功能失效

  • 确保使用了 script(data-pjax) 属性
  • 检查控制台是否有 PJAX 相关错误

技术原理

本功能采用以下技术栈:

  • Markmap:基于 D3.js 的思维导图渲染引擎
  • Transformer:将 Markdown 文本转换为 Markmap 数据结构
  • 智能缓存:首次渲染后缓存 SVG DOM,重复打开极速
  • PJAX 模式B:内联脚本 + data-pjax 属性 + 立即执行
  • 生命周期管理pjax:send 事件清理资源,防止内存泄漏

致谢

本功能的开发受以下项目启发: