157 lines
4.8 KiB
HTML
157 lines
4.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>DAG Editor</title>
|
|
<style>
|
|
body { font-family: Arial; margin: 0; overflow: hidden; }
|
|
#toolbar { padding: 10px; background: #eee; border-bottom: 1px solid #ccc; }
|
|
#canvas { position: relative; width: 100%; height: calc(100vh - 40px); background: #fafafa; }
|
|
.node {
|
|
position: absolute; padding: 10px 15px;
|
|
background: #fff; border: 2px solid #333; border-radius: 6px;
|
|
cursor: move; user-select: none; text-align: center;
|
|
}
|
|
.start { background: #d1ffd1; border-color: green; }
|
|
.end { background: #ffd1d1; border-color: red; }
|
|
.selected { border-color: blue; }
|
|
svg { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
line, path { stroke: #333; stroke-width: 2; marker-end: url(#arrow); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="toolbar">
|
|
<button onclick="addNode('normal')">添加节点</button>
|
|
<button onclick="addNode('start')">添加开始节点</button>
|
|
<button onclick="addNode('end')">添加结束节点</button>
|
|
<button onclick="deleteSelected()">删除选中</button>
|
|
<button onclick="exportData()">导出流程</button>
|
|
</div>
|
|
<div id="canvas">
|
|
<svg id="edges"></svg>
|
|
</div>
|
|
|
|
<script>
|
|
let canvas = document.getElementById('canvas');
|
|
let edgesSVG = document.getElementById('edges');
|
|
let nodes = [];
|
|
let edges = [];
|
|
let selectedNode = null;
|
|
let linkingStart = null;
|
|
|
|
// 添加箭头标记
|
|
let defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
let marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
|
|
marker.setAttribute("id", "arrow");
|
|
marker.setAttribute("markerWidth", "10");
|
|
marker.setAttribute("markerHeight", "10");
|
|
marker.setAttribute("refX", "10");
|
|
marker.setAttribute("refY", "3");
|
|
marker.setAttribute("orient", "auto");
|
|
let arrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
arrowPath.setAttribute("d", "M0,0 L10,3 L0,6 Z");
|
|
arrowPath.setAttribute("fill", "#333");
|
|
marker.appendChild(arrowPath);
|
|
defs.appendChild(marker);
|
|
edgesSVG.appendChild(defs);
|
|
|
|
function addNode(type) {
|
|
let div = document.createElement('div');
|
|
div.className = 'node ' + (type === 'start' ? 'start' : type === 'end' ? 'end' : '');
|
|
div.style.left = "100px";
|
|
div.style.top = "100px";
|
|
div.contentEditable = true;
|
|
div.innerText = type === 'start' ? "开始" : type === 'end' ? "结束" : "节点";
|
|
|
|
div.onclick = (e) => {
|
|
e.stopPropagation();
|
|
if (linkingStart && linkingStart !== div) {
|
|
addEdge(linkingStart, div);
|
|
linkingStart.classList.remove("selected");
|
|
linkingStart = null;
|
|
} else {
|
|
if (selectedNode) selectedNode.classList.remove("selected");
|
|
div.classList.add("selected");
|
|
selectedNode = div;
|
|
linkingStart = div; // 准备连线
|
|
}
|
|
};
|
|
|
|
// 拖拽
|
|
div.onmousedown = (e) => {
|
|
let offsetX = e.offsetX, offsetY = e.offsetY;
|
|
function move(ev) {
|
|
div.style.left = (ev.pageX - offsetX) + "px";
|
|
div.style.top = (ev.pageY - offsetY) + "px";
|
|
updateEdges();
|
|
}
|
|
function up() {
|
|
document.removeEventListener('mousemove', move);
|
|
document.removeEventListener('mouseup', up);
|
|
}
|
|
document.addEventListener('mousemove', move);
|
|
document.addEventListener('mouseup', up);
|
|
};
|
|
|
|
canvas.appendChild(div);
|
|
nodes.push(div);
|
|
}
|
|
|
|
function addEdge(from, to) {
|
|
let edge = {from, to};
|
|
edges.push(edge);
|
|
updateEdges();
|
|
}
|
|
|
|
function updateEdges() {
|
|
edgesSVG.innerHTML = "<defs>"+defs.innerHTML+"</defs>";
|
|
edges.forEach(e => {
|
|
let x1 = e.from.offsetLeft + e.from.offsetWidth/2;
|
|
let y1 = e.from.offsetTop + e.from.offsetHeight/2;
|
|
let x2 = e.to.offsetLeft + e.to.offsetWidth/2;
|
|
let y2 = e.to.offsetTop + e.to.offsetHeight/2;
|
|
|
|
let path = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
path.setAttribute("x1", x1);
|
|
path.setAttribute("y1", y1);
|
|
path.setAttribute("x2", x2);
|
|
path.setAttribute("y2", y2);
|
|
path.setAttribute("stroke", "#333");
|
|
path.setAttribute("stroke-width", "2");
|
|
path.setAttribute("marker-end", "url(#arrow)");
|
|
path.onclick = (ev) => {
|
|
ev.stopPropagation();
|
|
edges = edges.filter(ed => ed !== e);
|
|
updateEdges();
|
|
}
|
|
edgesSVG.appendChild(path);
|
|
});
|
|
}
|
|
|
|
function deleteSelected() {
|
|
if (selectedNode) {
|
|
edges = edges.filter(e => e.from !== selectedNode && e.to !== selectedNode);
|
|
canvas.removeChild(selectedNode);
|
|
nodes = nodes.filter(n => n !== selectedNode);
|
|
selectedNode = null;
|
|
updateEdges();
|
|
}
|
|
}
|
|
|
|
function exportData() {
|
|
let data = {
|
|
nodes: nodes.map(n => ({
|
|
id: n.innerText,
|
|
type: n.classList.contains('start') ? 'start' : n.classList.contains('end') ? 'end' : 'normal',
|
|
x: n.offsetLeft, y: n.offsetTop
|
|
})),
|
|
edges: edges.map(e => ({ from: e.from.innerText, to: e.to.innerText }))
|
|
};
|
|
console.log(JSON.stringify(data, null, 2));
|
|
alert("导出数据已打印到控制台");
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|