益智教育网

HTML 思维导图工具,2025年哪个最好用?

纯 HTML + CSS (静态、简单)

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

HTML 思维导图工具,2025年哪个最好用?-图1

原理

  • 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 动态生成节点和连接线,并处理用户交互(如点击添加节点、拖拽等)。

原理

  1. HTML: 创建一个空的容器 <div id="mindmap-container"></div>,所有的节点和连接线都将由 JS 动态创建并插入这个容器中。
  2. CSS: 定义节点的通用样式(颜色、大小、形状等)。
  3. 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 库

如果你不想从零开始造轮子,或者需要更强大的功能(如缩放、平移、快捷键、数据绑定等),使用现成的库是最佳选择。

推荐库

  1. Mind-elixir:

    • 特点: 纯 JavaScript 实现,不依赖其他库,轻量级,易于上手,支持动态编辑、拖拽、缩放、导出图片等。
    • 适合: 快速集成,功能需求中等的场景。
  2. jsMind:

    • 特点: 老牌思维导图库,功能稳定,支持多种布局(树状、逻辑、组织结构图等),可定制性高。
    • 适合: 对布局有特定要求,需要稳定性的项目。
  3. 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)开始,亲手实现一个简单的版本,这个过程会让你对前端开发有更深刻的理解,当你需要更复杂的功能时,再转向方案三,使用成熟的库来加速开发。

分享:
扫描分享到社交APP
上一篇
下一篇