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,'&quot;')
					.replace(/#/g,'&#35;')
					.replace(/;/g,'&#59;')
					.replace(/\[/g,'&#91;')
					.replace(/\]/g,'&#93;')
					.replace(/\(/g,'&#40;')
					.replace(/\)/g,'&#41;');
		}

		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>

效果图:
111

<!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,'&quot;')
					.replace(/#/g,'&#35;')
					.replace(/;/g,'&#59;');
		}

		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>

效果图:
222

posted @ 2026-07-28 16:19  onestopweb  阅读(21)  评论(0)    收藏  举报