HTML历理 树状思维导图2
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>ICbA - 添加脑图</title>
<link rel="stylesheet" href="/_/_js/layui/css/layui.css">
<style>
html,body{height:100%;margin:0;overflow:hidden;font-family:"Microsoft YaHei",system-ui,-apple-system}
.wrap-box{display:flex;height:calc(100vh - 20px);gap:10px}
.left-box{width:420px;min-width:280px;max-width:1000px;display:flex;flex-direction:column}
.layui-card-header { white-space: nowrap !important; overflow: hidden; text-overflow: ellipsis;}
.resize-bar{width:6px;background:#e2e2e2;cursor:ew-resize;border-radius:3px;user-select:none;transition:background .2s;position:relative;}
.resize-bar::before{content:"";position:absolute;left:-4px;right:-4px;top:0;bottom:0;}
.resize-bar:hover,.resize-bar.active{background:#1E9FFF}
.right-box{flex:1;min-width:340px;display:flex;flex-direction:column}
.mermaid svg .node{cursor:grab!important;transition:opacity 0.15s}
.mermaid svg .node:active{cursor:grabbing!important}
.mermaid svg .node rect{rx:6px;ry:6px}
#chart::-webkit-scrollbar,
.layui-textarea::-webkit-scrollbar{width:6px;height:6px}
#chart::-webkit-scrollbar-thumb,
.layui-textarea::-webkit-scrollbar-thumb{background:#cccccc;border-radius:3px}
#chart::-webkit-scrollbar-track,
.layui-textarea::-webkit-scrollbar-track{background:#f6f6f6}
#chart{overscroll-behavior: contain;}
.loading-tip{color:#666;text-align:center;padding:40px;font-size:14px}
.error-tip{color:#d94444;padding:16px;line-height:1.7}
</style>
</head>
<body>
<div class="layui-fluid" style="padding:10px;height:100vh;box-sizing:border-box;">
<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;flex-wrap:wrap;">
<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>
<button class="layui-btn layui-btn-sm layui-btn-warm" id="btnExportSvg">导出</button>
</div>
<textarea id="txt" class="layui-textarea" style="flex:1;resize:none;font-size:14px;line-height:1.6;" placeholder="一级标题1
二级标题11
三级内容111
三级内容112
二级标题12
三级内容121
三级内容122">一级标题1
二级标题11
三级内容111
三级内容112
二级标题12
三级内容121
三级内容122</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">
树形预览图
<span style="color:#888;font-size:12px;margin-left:10px;">(节点可拖拽调整位置;单击打开链接,拖动不会触发跳转)</span>
</div>
<div class="layui-card-body" id="chart" style="height:calc(100% - 55px);overflow:auto;">
<div class="loading-tip">正在初始化渲染引擎...</div>
</div>
</div>
</div>
</div>
</div>
<script src="/_/_js/layui/layui.js"></script>
<script src="/_/_js/mermaid/mermaid.min.js"></script>
<script>
layui.use(['jquery','layer'], function(){
const $ = layui.jquery;
const layer = layui.layer;
const CONFIG = Object.freeze({
DEBOUNCE: 320,
RESIZE_DEBOUNCE: 400,
MAX_TEXT: 40000,
DRAG_THRESHOLD: 6
});
let direction = "LR";
let renderTimer = null;
let resizeTimer = null;
let lastMmdCode = "";
mermaid.initialize({
startOnLoad: false,
maxTextSize: 60000,
theme: "neutral",
securityLevel: 'loose',
flowchart: {
useMaxWidth: false,
rankSpacing: 60,
nodeSpacing: 35
}
});
// 左右面板拖拽缩放
(function initResize(){
const $left = $('.left-box');
const $bar = $('#dragBar');
let dragging = false;
const savedWidth = localStorage.getItem('treeEditorLeftWidth');
if(savedWidth) $left.width(+savedWidth);
$bar.on('mousedown', e => {
dragging = true;
$bar.addClass('active');
e.preventDefault();
});
$(document)
.on('mousemove.resizepanel', e => {
if(!dragging) return;
const w = Math.max(280, Math.min(1000, e.clientX - $left.offset().left));
$left.width(w);
})
.on('mouseup.resizepanel', () => {
if(dragging){
$bar.removeClass('active');
dragging = false;
localStorage.setItem('treeEditorLeftWidth', $left.width());
}
});
})();
// Textarea Tab缩进处理
$('#txt').on('keydown', e => {
const ta = e.target;
const start = ta.selectionStart;
const end = ta.selectionEnd;
const value = ta.value;
if(e.key === 'Tab' && !e.shiftKey){
e.preventDefault();
if(start !== end){
const selected = value.slice(start, end);
const newSel = selected.replace(/^/gm,'\t');
ta.value = value.slice(0,start)+newSel+value.slice(end);
ta.selectionStart = start;
ta.selectionEnd = start + newSel.length;
}else{
ta.value = value.slice(0, start) + '\t' + value.slice(end);
ta.selectionStart = ta.selectionEnd = start + 1;
}
ta.dispatchEvent(new Event('input', {bubbles:true}));
return;
}
if(e.key === 'Tab' && e.shiftKey){
e.preventDefault();
if(start !== end){
const selected = value.slice(start, end);
const newSel = selected.replace(/^\t/gm,'');
ta.value = value.slice(0,start)+newSel+value.slice(end);
ta.selectionStart = start;
ta.selectionEnd = start + newSel.length;
}else{
if(start > 0 && value[start-1] === '\t'){
ta.value = value.slice(0, start-1) + value.slice(start);
ta.selectionStart = ta.selectionEnd = start - 1;
}
}
ta.dispatchEvent(new Event('input', {bubbles:true}));
}
});
// 获取元素translate位置
const getTranslate = el => {
const transform = getComputedStyle(el).transform;
if(transform === 'none') return {x:0,y:0};
const m = new WebKitCSSMatrix(transform);
return {x: m.m41, y: m.m42};
};
// mermaid标签转义
const escapeStr = str => str
.replace(/&/g,'&')
.replace(/"/g,'"')
.replace(/#/g,'#')
.replace(/;/g,'#59;')
.replace(/\[/g,'#91;')
.replace(/\]/g,'#93;')
.replace(/\(/g,'#40;')
.replace(/\)/g,'#41;')
.replace(/\|/g,'#124;')
.replace(/{/g,'#123;')
.replace(/}/g,'#125;');
let dragEventBind = false;
const unbindNodeDrag = () => {
$(document).off('mousemove.dragnode mouseup.dragnode');
dragEventBind = false;
};
// 节点拖拽绑定
const bindNodeDrag = () => {
unbindNodeDrag();
const $svg = $('#chart svg');
if(!$svg.length) return;
let dragTarget = null;
let isDragging = false;
let startX, startY, originX, originY;
$svg.find('.node').off('mousedown').on('mousedown', e => {
dragTarget = e.currentTarget;
isDragging = false;
const pos = getTranslate(dragTarget);
originX = pos.x;
originY = pos.y;
startX = e.clientX;
startY = e.clientY;
e.preventDefault();
e.stopPropagation();
});
$(document)
.on('mousemove.dragnode', e => {
if(!dragTarget) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if(Math.abs(dx) > CONFIG.DRAG_THRESHOLD || Math.abs(dy) > CONFIG.DRAG_THRESHOLD){
isDragging = true;
dragTarget.style.transform = `translate(${originX+dx}px,${originY+dy}px)`;
}
})
.on('mouseup.dragnode', () => {
if(isDragging && dragTarget){
$(dragTarget).one('click', ev => ev.preventDefault());
}
dragTarget = null;
isDragging = false;
});
dragEventBind = true;
};
// 渲染主逻辑
async function renderChart(){
try{
const raw = $('#txt').val();
if(raw.length > CONFIG.MAX_TEXT){
$('#chart').html('<div class="error-tip">文本超出最大限制,请精简内容!</div>');
unbindNodeDrag();
return;
}
if(!raw.trim()){
$('#chart').html('<div style="padding:40px;color:#999;text-align:center;">请在左侧输入树形文本</div>');
unbindNodeDrag();
lastMmdCode = "";
return;
}
const lines = [];
raw.split('\n').forEach(line => {
const contentTrim = line.trim();
if(!contentTrim) return;
const prefix = line.match(/^([\t ]*)/)[1];
const level = prefix.length;
lines.push({level, text: contentTrim});
});
if(!lines.length){
$('#chart').html('<div style="padding:40px;color:#999;text-align:center;">有效内容为空</div>');
unbindNodeDrag();
lastMmdCode = "";
return;
}
let code = `graph ${direction}
classDef linkNode fill:#fff8e8,stroke:#ff9800,stroke-width:1.5px,color:#222
`;
const stack = [];
lines.forEach((item, idx) => {
const nid = `node_${idx}`;
let label, linkUrl = null;
const sepIndex = item.text.indexOf('||');
if(sepIndex > 0){
label = escapeStr(item.text.slice(0, sepIndex));
linkUrl = item.text.slice(sepIndex+2).trim() || null;
// 禁止javascript伪协议
if(linkUrl && /^javascript:/i.test(linkUrl)) linkUrl = null;
}else{
label = escapeStr(item.text);
}
code += ` ${nid}["${label}"]\n`;
if(linkUrl){
code += ` click ${nid} "${linkUrl}" _blank\n class ${nid} linkNode\n`;
}
while(stack.length && stack[stack.length-1].level >= item.level) stack.pop();
if(stack.length){
const parentId = stack[stack.length-1].nid;
code += ` ${parentId} --> ${nid}\n`;
}
stack.push({level: item.level, nid});
});
// 代码无变化跳过渲染
if(lastMmdCode === code) return;
lastMmdCode = code;
$('#chart').html('<div class="loading-tip">渲染中...</div>');
const uniqueId = 'mermaid_' + Date.now() + '_' + Math.random().toString(36).slice(2);
const {svg} = await mermaid.render(uniqueId, code);
$('#chart').html(svg);
setTimeout(bindNodeDrag, 80);
}catch(err){
console.error('渲染失败', err);
unbindNodeDrag();
lastMmdCode = "";
$('#chart').html(`<div class="error-tip">渲染出错:${err.message}<br>请检查文本缩进与语法</div>`);
}
}
// 输入防抖
$('#txt').on('input', () => {
clearTimeout(renderTimer);
renderTimer = setTimeout(renderChart, CONFIG.DEBOUNCE);
});
// 按钮事件
$('#btnDemo').click(() => {
$('#txt').val(`一级标题1
二级标题11
三级内容111
三级内容112
二级标题12
三级内容121
三级内容122
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
使用示例
普通文本
外部链接||https://www.doubao.com/chat/
内部链接||/R/R11/in.se.php?f=MTU%3D&u=Mg%3D%3D&w=0
图片文件||/_/picf/Z/Z91/26071523370986.png
音频文件||/_/picf/Z/Z91/26071517454150.mp3
视频文件||/_/picf/Z/Z91/26071523373274.mp4
文档文件||/Z/Ze3/file/0006/博弈论.pdf
网页文件||/Z/Ze2/html/0002/index.php
备注内容||###这是备注`);
renderChart();
});
$('#btnClear').click(() => {
$('#txt').val('');
renderChart();
});
$('#btnToggleDir').click(() => {
direction = direction === 'LR' ? 'TD' : 'LR';
renderChart();
});
$('#btnExportSvg').click(()=>{
const svgDom = $('#chart svg')[0];
if(!svgDom) return layer.msg('暂无预览图形');
const serializer = new XMLSerializer();
let source = serializer.serializeToString(svgDom);
source = '<' + '?xml version="1.0" standalone="no"?>\r\n' + source;
const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source);
const a = document.createElement('a');
a.href = url;
a.download = 'ICbA树形图_'+new Date().getTime()+'.svg';
document.body.appendChild(a);
a.click();
a.remove();
});
$(window).on('resize', ()=>{
clearTimeout(resizeTimer);
resizeTimer = setTimeout(renderChart, CONFIG.RESIZE_DEBOUNCE);
})
renderChart();
});
</script>
</body>
</html>
效果图:


浙公网安备 33010602011771号