166 lines
4.2 KiB
HTML
166 lines
4.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>DAG 可视化编辑器</title>
|
|
<style>
|
|
body { font-family: Arial; }
|
|
#canvas { border: 1px solid #ccc; width: 100%; height: 600px; position: relative; }
|
|
.node {
|
|
position: absolute;
|
|
padding: 10px 20px;
|
|
border: 2px solid #333;
|
|
border-radius: 8px;
|
|
background: #f9f9f9;
|
|
cursor: move;
|
|
user-select: none;
|
|
}
|
|
svg {
|
|
position: absolute;
|
|
top: 0; left: 0; width: 100%; height: 100%;
|
|
pointer-events: none;
|
|
}
|
|
line, path {
|
|
stroke: #333;
|
|
stroke-width: 2;
|
|
fill: none;
|
|
marker-end: url(#arrow);
|
|
pointer-events: auto;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h3>DAG 可视化编辑器</h3>
|
|
<div id="canvas">
|
|
<svg id="edges"></svg>
|
|
</div>
|
|
|
|
<script>
|
|
let nodes = [];
|
|
let edges = [];
|
|
let selectedNode = null;
|
|
const canvas = document.getElementById("canvas");
|
|
const svg = document.getElementById("edges");
|
|
|
|
function createNode(id, text, x, y) {
|
|
const div = document.createElement("div");
|
|
div.className = "node";
|
|
div.innerText = text;
|
|
div.contentEditable = true;
|
|
div.style.left = x + "px";
|
|
div.style.top = y + "px";
|
|
div.dataset.id = id;
|
|
div.onmousedown = (e) => startDrag(e, div);
|
|
div.onclick = (e) => selectNode(e, div);
|
|
canvas.appendChild(div);
|
|
nodes.push({id, x, y, el: div});
|
|
}
|
|
|
|
function drawEdges() {
|
|
svg.innerHTML = `
|
|
<defs>
|
|
<marker id="arrow" markerWidth="10" markerHeight="10" refX="10" refY="3" orient="auto">
|
|
<path d="M0,0 L0,6 L9,3 z" fill="#333" />
|
|
</marker>
|
|
</defs>`;
|
|
edges.forEach(edge => {
|
|
const from = nodes.find(n => n.id === edge.from);
|
|
const to = nodes.find(n => n.id === edge.to);
|
|
if (from && to) {
|
|
const x1 = from.x + from.el.offsetWidth/2;
|
|
const y1 = from.y + from.el.offsetHeight/2;
|
|
const x2 = to.x + to.el.offsetWidth/2;
|
|
const y2 = to.y + to.el.offsetHeight/2;
|
|
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
line.setAttribute("x1", x1);
|
|
line.setAttribute("y1", y1);
|
|
line.setAttribute("x2", x2);
|
|
line.setAttribute("y2", y2);
|
|
line.setAttribute("stroke", "black");
|
|
line.setAttribute("marker-end", "url(#arrow)");
|
|
svg.appendChild(line);
|
|
}
|
|
});
|
|
}
|
|
|
|
function selectNode(e, div) {
|
|
if (!selectedNode) {
|
|
selectedNode = div.dataset.id;
|
|
} else {
|
|
if (selectedNode !== div.dataset.id) {
|
|
edges.push({from: selectedNode, to: div.dataset.id});
|
|
drawEdges();
|
|
}
|
|
selectedNode = null;
|
|
}
|
|
e.stopPropagation();
|
|
}
|
|
|
|
let dragNode = null, offsetX = 0, offsetY = 0;
|
|
function startDrag(e, div) {
|
|
dragNode = div;
|
|
offsetX = e.offsetX;
|
|
offsetY = e.offsetY;
|
|
document.onmousemove = drag;
|
|
document.onmouseup = stopDrag;
|
|
}
|
|
function drag(e) {
|
|
if (dragNode) {
|
|
dragNode.style.left = (e.pageX - canvas.offsetLeft - offsetX) + "px";
|
|
dragNode.style.top = (e.pageY - canvas.offsetTop - offsetY) + "px";
|
|
let n = nodes.find(n => n.id === dragNode.dataset.id);
|
|
n.x = e.pageX - canvas.offsetLeft - offsetX;
|
|
n.y = e.pageY - canvas.offsetTop - offsetY;
|
|
drawEdges();
|
|
}
|
|
}
|
|
function stopDrag() {
|
|
dragNode = null;
|
|
document.onmousemove = null;
|
|
document.onmouseup = null;
|
|
}
|
|
|
|
// 简单自动布局:拓扑排序分层排布
|
|
function autoLayout() {
|
|
let layerMap = {};
|
|
let visited = new Set();
|
|
|
|
function dfs(nodeId, depth) {
|
|
if (layerMap[nodeId] === undefined || depth > layerMap[nodeId]) {
|
|
layerMap[nodeId] = depth;
|
|
}
|
|
edges.filter(e => e.from === nodeId).forEach(e => dfs(e.to, depth+1));
|
|
}
|
|
nodes.forEach(n => dfs(n.id, 0));
|
|
|
|
let maxLayer = Math.max(...Object.values(layerMap));
|
|
let layerGroups = Array.from({length: maxLayer+1}, () => []);
|
|
for (let n of nodes) {
|
|
let layer = layerMap[n.id] || 0;
|
|
layerGroups[layer].push(n);
|
|
}
|
|
layerGroups.forEach((group, depth) => {
|
|
group.forEach((n, i) => {
|
|
n.x = 150 * i + 50;
|
|
n.y = depth * 100 + 50;
|
|
n.el.style.left = n.x + "px";
|
|
n.el.style.top = n.y + "px";
|
|
});
|
|
});
|
|
drawEdges();
|
|
}
|
|
|
|
// 测试数据
|
|
createNode("1", "开始", 50, 50);
|
|
createNode("2", "处理", 250, 200);
|
|
createNode("3", "结束", 450, 350);
|
|
edges.push({from: "1", to: "2"});
|
|
edges.push({from: "2", to: "3"});
|
|
drawEdges();
|
|
autoLayout();
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|