HTML历理 树状思维导图
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Tab缩进文本生成横向树状图(Layui版)</title>
<link rel="stylesheet" href="/_/_js/layui/css/layui.css">
<style>
html,body{height:100%;margin:0;}
/* 左右拖拽分割 */
.wrap-box{display:flex;height:calc(100vh - 20px);gap:10px;}
.left-box{width:420px;min-width:280px;max-width:650px;display:flex;flex-direction:column;}
.resize-bar{width:6px;background:#e2e2e2;cursor:ew-resize;border-radius:3px;user-select:none;}
.resize-bar:hover{background:#1E9FFF;}
.right-box{flex:1;min-width:300px;display:flex;flex-direction:column;}
</style>
</head>
<body>
<div class="layui-fluid" style="padding:10px;height:100vh;">
<div class="wrap-box">
<!-- 左侧文本区域 -->
<div class="left-box">
<div class="layui-card" style="flex:1;display:flex;flex-direction:column;height:100%;">
<div class="layui-card-header">
<span class="layui-text">格式:名称||url 添加链接,Tab/空格缩进分级</span>
</div>
<div class="layui-card-body" style="flex:1;display:flex;flex-direction:column;padding:10px;">
<div class="layui-btn-group" style="margin-bottom:8px;">
<button class="layui-btn layui-btn-sm" id="btnDemo">载入示例</button>
<button class="layui-btn layui-btn-sm layui-btn-primary" id="btnClear">清空</button>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="btnToggleDir">切换横向/纵向</button>
</div>
<textarea id="txt" class="layui-textarea" style="flex:1;resize:none;font-size:14px;line-height:1.6;">项目总规划
前端模块||https://www.baidu.com
页面开发
组件封装
后端模块
接口编写||https://github.com
数据库设计
测试工作
功能测试
性能测试
</textarea>
</div>
</div>
</div>
<!-- 拖拽分割条 -->
<div class="resize-bar" id="dragBar"></div>
<!-- 右侧图表 -->
<div class="right-box">
<div class="layui-card" style="height:100%;">
<div class="layui-card-header">树形预览图</div>
<div class="layui-card-body" id="chart" style="height:calc(100% - 55px);overflow:auto;">
加载中...
</div>
</div>
</div>
</div>
</div>
<script src="/_/_js/layui/layui.js"></script>
<script src="/_/_js/mermaid/mermaid.min.js"></script>
<script>
layui.use(['jquery'], function(){
var $ = layui.jquery;
mermaid.initialize({
startOnLoad: false,
maxTextSize: 50000,
theme: "neutral",
securityLevel: 'loose' // 保证click链接正常跳转
});
const TAB_WIDTH = 4;
let direction = "LR"; // LR横向 TD纵向
let renderTimer = null;
// 拖拽分割实现
(function dragPanel(){
const $left = $('.left-box');
const $bar = $('#dragBar');
let dragging = false;
$bar.mousedown(function(e){
dragging = true;
e.preventDefault();
});
$(document).mousemove(function(e){
if(!dragging) return;
let w = e.clientX - $left.offset().left;
w = Math.max(280, Math.min(650, w));
$left.width(w);
});
$(document).mouseup(function(){ dragging = false; });
})();
// 增强mermaid转义,修复括号、引号等特殊字符导致渲染崩溃
function escapeMermaidStr(str){
return str.replace(/"/g,'"')
.replace(/#/g,'#')
.replace(/;/g,';')
.replace(/\[/g,'[')
.replace(/\]/g,']')
.replace(/\(/g,'(')
.replace(/\)/g,')');
}
async function renderChart(){
try{
const rawText = $('#txt').val();
const rawLines = rawText.split('\n');
const lines = [];
for(let line of rawLines){
const trimStr = line.trim();
if(!trimStr) continue;
const spaceMatch = line.match(/^(\s*)/)[1];
const spaceNorm = spaceMatch.replace(/\t/g,' '.repeat(TAB_WIDTH));
const level = spaceNorm.length;
lines.push({level, text: trimStr})
}
if(lines.length === 0){
$('#chart').html('<div style="padding:20px;color:#999">请输入树形文本</div>');
return;
}
let graphCode = `graph ${direction}\n`;
graphCode += ` classDef linkNode fill:#fff8e8,stroke:#ff9800,color:#000\n`;
const idList = [];
let idIndex = 0;
const stack = [];
for(const item of lines){
const nodeId = `n${idIndex}`;
idList.push({...item, id: nodeId});
let label, link;
if(item.text.includes('||')){
const arr = item.text.split('||');
label = escapeMermaidStr(arr[0]);
link = arr[1].trim();
}else{
label = escapeMermaidStr(item.text);
link = null;
}
graphCode += ` ${nodeId}["${label}"]\n`;
if(link){
graphCode += ` click ${nodeId} "${link}" _blank\n`;
graphCode += ` class ${nodeId} linkNode\n`;
}
while(stack.length > 0 && stack[stack.length-1].level >= item.level){
stack.pop();
}
if(stack.length > 0){
const parentIdx = stack[stack.length-1].idx;
const parentId = idList[parentIdx].id;
graphCode += ` ${parentId} --> ${nodeId}\n`;
}
stack.push({level: item.level, idx: idIndex});
idIndex++;
}
const renderId = 'mermaid_graph_' + Date.now();
const { svg } = await mermaid.render(renderId, graphCode);
$('#chart').html(svg);
}catch(err){
console.error(err);
$('#chart').html(`<div style="color:#c33;padding:16px">渲染出错:${err.message}</div>`);
}
}
// 防抖输入
$('#txt').on('input', function(){
clearTimeout(renderTimer);
renderTimer = setTimeout(renderChart, 350);
});
// 按钮事件
$('#btnDemo').click(function(){
$('#txt').val(`一级标题1||https://www.doubao.com/chat/
二级标题11||/R/R11/in.se.php?f=MTU%3D&u=Mg%3D%3D&w=0
三级内容111||/_/picf/Z/Z91/26071517454150.mp3
三级内容112||/_/picf/Z/Z91/26071523373274.mp4
三级内容113||/_/picf/Z/Z91/26071523370986.png
二级标题12
三级内容121||/Z/Ze3/file/0006/博弈论.pdf
三级内容122||/Z/Ze2/html/0002/index.php
二级标题13
三级内容131
三级内容132`);
renderChart();
});
$('#btnClear').click(function(){
$('#txt').val('');
renderChart();
});
$('#btnToggleDir').click(function(){
direction = direction === "LR" ? "TD" : "LR";
renderChart();
});
renderChart();
});
</script>
</body>
</html>
效果图:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Layui - 单按钮切换横/纵向树状图(mermaid)</title>
<link rel="stylesheet" href="/_/_js/layui/css/layui.css">
</head>
<body>
<div class="layui-card">
<div class="layui-card-header">树状结构图</div>
<div class="layui-card-body">
<div class="control-bar">
<button class="layui-btn layui-btn-sm" id="toggleDir">切换为纵向布局</button>
</div>
<!-- 第一棵树 -->
<div class="tree-item">
<div class="tree-title">项目总规划</div>
<pre class="tree-source">项目总规划
前端模块||https://www.baidu.com
页面开发
组件封装
后端模块
接口编写||https://github.com
数据库设计
测试工作
功能测试
性能测试</pre>
<div class="tree-view"></div>
</div>
<!-- 第二棵树 -->
<div class="tree-item">
<div class="tree-title">商城系统架构</div>
<pre class="tree-source">商城系统
用户中心
登录注册
会员管理
商品模块||https://example.com/goods
商品上架
库存管理
订单模块
下单流程
订单售后</pre>
<div class="tree-view"></div>
</div>
<!-- 第三棵树 -->
<div class="tree-item">
<div class="tree-title">演示框架</div>
<div class="tree-source">一级标题1||https://www.doubao.com/chat/
二级标题11||/R/R11/in.se.php?f=MTU%3D&u=Mg%3D%3D&w=0
三级内容111||/_/picf/Z/Z91/26071517454150.mp3
三级内容112||/_/picf/Z/Z91/26071523373274.mp4
三级内容113||/_/picf/Z/Z91/26071523370986.png
二级标题12
三级内容121||/Z/Ze3/file/0006/博弈论.pdf
三级内容122||/Z/Ze2/html/0002/index.php
二级标题13
三级内容131
三级内容132</div>
<div class="tree-view"></div>
</div>
</div>
</div>
<script src="/_/_js/layui/layui.js"></script>
<script src="/_/_js/mermaid/mermaid.min.js"></script>
<script>
// 动态注入样式
(function(){
const style = document.createElement('style');
style.textContent = `
.tree-item{margin-bottom:48px}
.tree-title{margin-bottom:10px;font-size:16px;font-weight:bold;color:#222}
.tree-source{display:none;white-space:pre}
.tree-view{margin-top:8px;overflow:auto;border:1px solid #eee;padding:10px;min-height:120px}
.tree-view svg{max-width:100%}
.control-bar{margin-bottom:20px}
`;
document.head.appendChild(style);
})();
layui.use(['jquery'], function(){
var $ = layui.jquery;
const TAB_WIDTH = 4;
// 默认横向 LR
let graphDir = "LR";
const $toggleBtn = $("#toggleDir");
mermaid.initialize({
startOnLoad: false,
maxTextSize: 50000,
theme: "neutral"
});
function escapeMermaidStr(str){
return str.replace(/"/g,'"')
.replace(/#/g,'#')
.replace(/;/g,';');
}
function parseTextToMermaid(text){
const rawLines = text.split('\n');
const lines = [];
for(let line of rawLines){
const trimStr = line.trim();
if(!trimStr) continue;
const spaceMatch = line.match(/^(\s*)/)[1];
const spaceNorm = spaceMatch.replace(/\t/g,' '.repeat(TAB_WIDTH));
const level = spaceNorm.length;
lines.push({
level,
rawText: trimStr
})
}
if(lines.length === 0) return "";
let graphCode = `graph ${graphDir}\n`;
graphCode += ` classDef linkNode fill:#fff8e8,stroke:#ff9800,color:#000\n`;
const idList = [];
let idIndex = 0;
const stack = [];
for(const item of lines){
const nodeId = `n${idIndex}`;
idList.push({ ...item, id: nodeId });
let label, linkUrl = null;
if(item.rawText.includes('||')){
const parts = item.rawText.split('||');
label = escapeMermaidStr(parts[0]);
linkUrl = parts[1].trim();
}else{
label = escapeMermaidStr(item.rawText);
}
graphCode += ` ${nodeId}["${label}"]\n`;
if(linkUrl){
graphCode += ` click ${nodeId} "${linkUrl}" _blank\n`;
graphCode += ` class ${nodeId} linkNode\n`;
}
while(stack.length > 0 && stack[stack.length-1].level >= item.level){
stack.pop();
}
if(stack.length > 0){
const parent = idList[stack[stack.length-1].idx];
graphCode += ` ${parent.id} --> ${nodeId}\n`;
}
stack.push({ level: item.level, idx: idIndex });
idIndex++;
}
return graphCode;
}
async function renderAllTree(){
const groups = document.querySelectorAll('.tree-item');
for(const g of groups){
const sourceEl = g.querySelector('.tree-source');
const viewEl = g.querySelector('.tree-view');
const mmdCode = parseTextToMermaid(sourceEl.textContent);
if(!mmdCode) continue;
const uniqueId = 'tree_' + Date.now() + '_' + Math.floor(Math.random()*9999);
const { svg } = await mermaid.render(uniqueId, mmdCode);
viewEl.innerHTML = svg;
}
}
// 单按钮切换逻辑
$toggleBtn.on("click",function(){
if(graphDir === "LR"){
graphDir = "TD";
$(this).text("切换为横向布局");
}else{
graphDir = "LR";
$(this).text("切换为纵向布局");
}
renderAllTree();
});
// 初始渲染
renderAllTree();
});
</script>
</body>
</html>
效果图:


浙公网安备 33010602011771号