纯 HTML + CSS (静态、简单)
这种方法最简单,不需要任何 JavaScript,适合创建结构固定、不需要交互的思维导图,主要利用 CSS 的 position 属性来定位节点。

原理
- HTML: 使用
<ul>和<li>标签来构建嵌套的列表结构,这天然符合思维导图的层级关系。 - CSS: 使用
position: absolute;将<li>元素精确定位到画布的任意位置,然后用border或:before/:after伪元素来绘制连接线。
示例代码
HTML (index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">纯CSS思维导图</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>我的中心主题</h1>
<div class="mindmap">
<ul>
<li>
<a href="#">分支 1</a>
<ul>
<li><a href="#">子分支 1.1</a></li>
<li><a href="#">子分支 1.2</a></li>
</ul>
</li>
<li>
<a href="#">分支 2</a>
<ul>
<li><a href="#">子分支 2.1</a></li>
<li><a href="#">子分支 2.2</a></li>
</ul>
</li>
<li>
<a href="#">分支 3</a>
<ul>
<li><a href="#">子分支 3.1</a></li>
<li><a href="#">子分支 3.2</a></li>
</ul>
</li>
</ul>
</div>
</body>
</html>
CSS (style.css)
body {
font-family: 'Arial', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f4f4f4;
}
.mindmap {
position: relative; /* 这是定位的基准 */
padding: 40px;
}
/* 隐藏默认列表样式 */
.mindmap ul, .mindmap li {
list-style: none;
padding: 0;
margin: 0;
}
/* 节点样式 */
.mindmap > ul > li {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.mindmap li a {
background: #fff;
padding: 10px 15px;
border-radius: 5px;
text-decoration: none;
color: #333;
border: 2px solid #5c6ac4;
display: block;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.mindmap li a:hover {
background-color: #5c6ac4;
color: white;
}
/* 连接线样式 - 使用伪元素 */
.mindmap ul {
position: relative;
}
.mindmap li > ul::before {
content: '';
position: absolute;
top: 50%;
left: 100%;
width: 100px; /* 连接线长度 */
height: 2px;
background: #5c6ac4;
transform: translateY(-50%);
}
/* 子节点的定位 */
.mindmap li > ul > li {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 100px; /* 连接线长度 */
}
/* 递归应用连接线,为多层子节点画线 */
.mindmap li > ul > li > ul::before {
content: '';
position: absolute;
top: 50%;
left: 100%;
width: 80px; /* 子分支连接线可以稍短 */
height: 2px;
background: #5c6ac4;
transform: translateY(-50%);
}
.mindmap li > ul > li > ul > li {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 80px; /* 子分支连接线长度 */
}
优点:
- 代码简单,易于理解。
- 无需 JavaScript。
缺点:
- 布局不灵活:节点的位置需要手动计算和调整,非常耗时。
- 无法动态添加一旦写死,就无法通过交互来改变。
- 样式复杂:多级连接线的样式会变得越来越复杂。
HTML + CSS + JavaScript (动态、推荐)
这是最常用、最灵活的方法,我们利用 JavaScript 动态生成节点和连接线,并处理用户交互(如点击添加节点、拖拽等)。
原理
- HTML: 创建一个空的容器
<div id="mindmap-container"></div>,所有的节点和连接线都将由 JS 动态创建并插入这个容器中。 - CSS: 定义节点的通用样式(颜色、大小、形状等)。
- JavaScript:
- 数据结构: 使用一个 JavaScript 对象或数组来存储思维导图的数据。
- 渲染函数: 编写一个函数(如
renderNode),它接收一个节点数据,创建对应的 DOM 元素(节点和连接线),并递归地处理其子节点。 - 事件处理: 为节点添加点击事件,用于添加子节点、删除节点或编辑文本。
- 布局算法: 这是核心,我们需要一个算法来计算每个节点的坐标(
left,top),常见的算法有:- 树状布局: 根节点在中心,子节点围绕根节点均匀分布。
- 径向布局: 根节点在中心,所有节点围绕根节点呈圆形或扇形分布。
- 力导向布局: 节点之间像有弹簧一样,会互相吸引或排斥,最终达到一个稳定的平衡状态(较复杂,通常需要 D3.js 这样的库)。
示例代码 (树状布局)
HTML (index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">JS动态思维导图</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>动态思维导图</h1>
<div id="mindmap-container"></div>
<script src="script.js"></script>
</body>
</html>
CSS (style.css)
body {
font-family: 'Arial', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f0f0f0;
margin: 0;
overflow: hidden; /* 防止滚动条 */
}
#mindmap-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: auto; /* 允许容器滚动,以便查看大图 */
}
.node {
position: absolute;
background: #fff;
border: 2px solid #5c6ac4;
border-radius: 5px;
padding: 10px 15px;
cursor: pointer;
user-select: none; /* 防止拖动时选中文本 */
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
text-align: center;
}
.node:hover {
background-color: #e8eaf6;
transform: scale(1.05);
}
.node.selected {
background-color: #c5cae9;
border-color: #3949ab;
}
.node-text {
font-size: 14px;
color: #333;
}
.node-controls {
position: absolute;
top: -25px;
right: -25px;
display: none;
}
.node:hover .node-controls {
display: block;
}
.add-btn, .del-btn {
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
cursor: pointer;
margin-left: 5px;
font-size: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.add-btn {
background-color: #4caf50;
color: white;
}
.del-btn {
background-color: #f44336;
color: white;
}
.link {
position: absolute;
background-color: #5c6ac4;
height: 2px;
transform-origin: left center;
pointer-events: none; /* 让鼠标事件穿透到节点上 */
z-index: -1; /* 确保连接线在节点下方 */
}
JavaScript (script.js)
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('mindmap-container');
// 1. 定义数据
const mindmapData = {
id: 'root',
text: '中心主题',
children: [
{
id: 'child1',
text: '分支 1',
children: [
{ id: 'child1-1', text: '子分支 1.1', children: [] },
{ id: 'child1-2', text: '子分支 1.2', children: [] }
]
},
{
id: 'child2',
text: '分支 2',
children: [
{ id: 'child2-1', text: '子分支 2.1', children: [] }
]
},
{
id: 'child3',
text: '分支 3',
children: []
}
]
};
// 2. 全局变量
let nodes = new Map(); // 存储所有节点元素
let links = new Map(); // 存储所有连接线元素
let selectedNode = null;
const nodeWidth = 100;
const nodeHeight = 40;
const levelHeight = 150; // 层与层之间的垂直间距
const siblingSpacing = 120; // 兄弟节点之间的水平间距
// 3. 递归渲染节点
function renderNode(data, parentElement, level = 0, index = 0, siblingCount = 1) {
const nodeEl = document.createElement('div');
nodeEl.className = 'node';
nodeEl.id = data.id;
nodeEl.style.left = `${calculateX(level, index, siblingCount)}px`;
nodeEl.style.top = `${level * levelHeight}px`;
nodeEl.dataset.level = level;
nodeEl.dataset.index = index;
const textEl = document.createElement('div');
textEl.className = 'node-text';
textEl.contentEditable = false; // 初始不可编辑
textEl.textContent = data.text;
nodeEl.appendChild(textEl);
const controlsEl = document.createElement('div');
controlsEl.className = 'node-controls';
const addBtn = document.createElement('button');
addBtn.className = 'add-btn';
addBtn.innerHTML = '+';
addBtn.title = '添加子节点';
addBtn.addEventListener('click', (e) => {
e.stopPropagation();
addChild(data);
});
const delBtn = document.createElement('button');
delBtn.className = 'del-btn';
delBtn.innerHTML = '-';
delBtn.title = '删除节点';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteNode(data);
});
controlsEl.appendChild(addBtn);
controlsEl.appendChild(delBtn);
nodeEl.appendChild(controlsEl);
// 节点点击事件
nodeEl.addEventListener('click', () => selectNode(nodeEl, data));
container.appendChild(nodeEl);
nodes.set(data.id, nodeEl);
// 如果有父节点,则创建连接线
if (parentElement) {
createLink(parentElement, nodeEl);
}
// 递归渲染子节点
if (data.children && data.children.length > 0) {
data.children.forEach((child, i) => {
renderNode(child, nodeEl, level + 1, i, data.children.length);
});
}
}
// 4. 计算节点 X 坐标 (简单的居中算法)
function calculateX(level, index, siblingCount) {
const containerWidth = container.clientWidth;
const totalWidth = siblingCount * siblingSpacing;
const startX = (containerWidth - totalWidth) / 2;
return startX + index * siblingSpacing;
}
// 5. 创建连接线
function createLink(parentEl, childEl) {
const link = document.createElement('div');
link.className = 'link';
const parentRect = parentEl.getBoundingClientRect();
const childRect = childEl.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const x1 = parentRect.left - containerRect.left + parentRect.width / 2;
const y1 = parentRect.top - containerRect.top + parentRect.height;
const x2 = childRect.left - containerRect.left + childRect.width / 2;
const y2 = childRect.top - containerRect.top;
const length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
link.style.width = `${length}px`;
link.style.left = `${x1}px`;
link.style.top = `${y1}px`;
link.style.transform = `rotate(${angle}deg)`;
container.appendChild(link);
links.set(`${parentEl.id}-${childEl.id}`, link);
}
// 6. 选择节点
function selectNode(nodeEl, data) {
// 清除之前选中的节点
if (selectedNode) {
selectedNode.classList.remove('selected');
}
selectedNode = nodeEl;
nodeEl.classList.add('selected');
}
// 7. 添加子节点
function addChild(parentData) {
const newId = `node-${Date.now()}`;
const newNode = {
id: newId,
text: '新节点',
children: []
};
parentData.children.push(newNode);
// 重新渲染整个子树
const parentEl = nodes.get(parentData.id);
// 先删除旧的子节点和连接线
while (parentEl.firstChild) {
parentEl.removeChild(parentEl.firstChild);
}
links.forEach((link, key) => {
if (key.startsWith(parentData.id + '-')) {
link.remove();
links.delete(key);
}
});
// 重新渲染
parentData.children.forEach((child, i) => {
renderNode(child, parentEl, parseInt(parentEl.dataset.level) + 1, i, parentData.children.length);
});
// 自动选中新节点
const newEl = nodes.get(newId);
selectNode(newEl, newNode);
}
// 8. 删除节点
function deleteNode(nodeData) {
if (nodeData.id === 'root') {
alert('不能删除根节点!');
return;
}
// 找到父节点
let parentData = null;
function findParent(data, targetId) {
if (data.children) {
for (let child of data.children) {
if (child.id === targetId) {
return data;
}
const found = findParent(child, targetId);
if (found) return found;
}
}
return null;
}
parentData = findParent(mindmapData, nodeData.id);
if (parentData) {
// 从数据中删除
parentData.children = parentData.children.filter(child => child.id !== nodeData.id);
// 从 DOM 中删除
const nodeEl = nodes.get(nodeData.id);
nodeEl.remove();
nodes.delete(nodeData.id);
// 删除相关连接线
links.forEach((link, key) => {
if (key.startsWith(`${nodeData.id}-`) || key.endsWith(`-${nodeData.id}`)) {
link.remove();
links.delete(key);
}
});
}
}
// 9. 初始渲染
renderNode(mindmapData, null);
// 10. 实现拖拽功能 (进阶)
let isDragging = false;
let currentDragNode = null;
let offset = { x: 0, y: 0 };
container.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('node')) {
isDragging = true;
currentDragNode = e.target;
const rect = currentDragNode.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
offset.x = e.clientX - rect.left + containerRect.left;
offset.y = e.clientY - rect.top + containerRect.top;
currentDragNode.style.cursor = 'grabbing';
}
});
document.addEventListener('mousemove', (e) => {
if (isDragging && currentDragNode) {
const x = e.clientX - offset.x;
const y = e.clientY - offset.y;
currentDragNode.style.left = `${x}px`;
currentDragNode.style.top = `${y}px`;
}
});
document.addEventListener('mouseup', () => {
if (isDragging && currentDragNode) {
isDragging = false;
currentDragNode.style.cursor = 'pointer';
currentDragNode = null;
// 拖拽结束后,更新所有连接线的位置
updateAllLinks();
}
});
function updateAllLinks() {
links.forEach((link, key) => {
const [parentId, childId] = key.split('-');
const parentEl = nodes.get(parentId);
const childEl = nodes.get(childId);
if (parentEl && childEl) {
createLink(parentEl, childEl);
}
});
}
});
优点:
- 高度灵活:可以动态添加、删除、修改节点。
- 交互性强:可以实现拖拽、编辑、高亮等复杂交互。
- 可扩展性好:可以轻松添加新功能,如导出为图片、保存数据到服务器等。
缺点:
- 代码量较大:需要编写较多的 JavaScript 逻辑。
- 布局算法复杂:一个好的布局算法是实现美观思维导图的关键,也是难点。
使用现成的 JavaScript 库
如果你不想从零开始造轮子,或者需要更强大的功能(如缩放、平移、快捷键、数据绑定等),使用现成的库是最佳选择。
推荐库
-
Mind-elixir:
- 特点: 纯 JavaScript 实现,不依赖其他库,轻量级,易于上手,支持动态编辑、拖拽、缩放、导出图片等。
- 适合: 快速集成,功能需求中等的场景。
-
jsMind:
- 特点: 老牌思维导图库,功能稳定,支持多种布局(树状、逻辑、组织结构图等),可定制性高。
- 适合: 对布局有特定要求,需要稳定性的项目。
-
D3.js:
- 特点: 不是专门的思维导图库,而是一个强大的数据可视化库,你可以用它来创建任何你想要的图表,包括极其复杂和美观的思维导图。
- 适合: 对视觉效果和交互有极致追求,且愿意投入时间学习的开发者。
使用 Mind-elixir 示例
HTML (index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">Mind-elixir 示例</title>
<!-- 引入 Mind-elixir 的 CSS -->
<link rel="stylesheet" href="https://unpkg.com/mind-elixir@1/dist/mind-elixir.min.css">
<style>
html, body, #mind-elixir {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<!-- 创建一个容器 -->
<div id="mind-elixir"></div>
<!-- 引入 Mind-elixir 的 JS -->
<script src="https://unpkg.com/mind-elixir@1/dist/mind-elixir.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const options = {
el: '#mind-elixir',
direction: 'horizontal', // 布局方向: horizontal | vertical
draggable: true, // 允许拖拽节点
editable: true, // 允许编辑节点文本
theme: {
// 主题配置
'background': '#f5f5f5',
'main': '#5c6ac4',
'line': '#5c6ac4',
'secondary': '#ff9800',
'tertiary': '#4caf50'
}
};
const data = {
meta: {
name: "Mind-elixir",
author: "efficiency",
version: "1.0"
},
format: 'node_tree',
data: {
id: 'root',
topic: '中心主题',
children: [
{
id: 'child1',
topic: '分支 1',
children: [
{ id: 'child1-1', topic: '子分支 1.1' },
{ id: 'child1-2', topic: '子分支 1.2' }
]
},
{
id: 'child2',
topic: '分支 2',
children: [
{ id: 'child2-1', topic: '子分支 2.1' }
]
},
{ id: 'child3', topic: '分支 3' }
]
}
};
const mind = new MindElixir(options);
mind.init(data);
});
</script>
</body>
</html>
优点:
- 功能强大:开箱即用,包含大量高级功能。
- 节省时间:无需关心底层实现,专注于业务逻辑。
- 稳定可靠:经过大量项目验证,bug 较少。
缺点:
- 灵活性受限:受限于库的设计,难以实现库本身不支持的定制化功能。
- 需要学习库的 API:虽然比自己写简单,但仍需阅读文档来正确使用。
总结与选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯 HTML+CSS | 简单、无 JS 依赖 | 布局死板、无法交互 | 静态展示、快速原型、学习 CSS 布局 |
| HTML+CSS+JS | 灵活、可交互、可定制 | 代码量大、布局算法复杂 | 需要自定义功能的个人项目、学习前端综合技能 |
| JS 库 | 功能强大、开发高效 | 灵活性受限、需学习 API | 商业项目、对功能和时间有要求的场景、不想重复造轮子 |
对于初学者,强烈建议从方案二(HTML+CSS+JS)开始,亲手实现一个简单的版本,这个过程会让你对前端开发有更深刻的理解,当你需要更复杂的功能时,再转向方案三,使用成熟的库来加速开发。
