Skip to main content

React 流程图 — 使用 JavaScript 在 Canvas 上构建交互式流程图和图表

直接在 HTML5 Canvas 上构建交互式流程图和连接图。拖动节点,观察连接箭头实时跟随移动——这是一种常见模式,适用于工作流构建器、思维导图、组织结构图和可视化编程工具。适用于原生 JavaScript、React(通过 react-konva)和 Vue。

说明: 拖动任意节点以重新定位它。连接会自动更新。点击“添加节点”可添加更多。

import Konva from 'konva';

var width = window.innerWidth;
var height = window.innerHeight;

var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});

var layer = new Konva.Layer();
stage.add(layer);

var container = document.getElementById('container');

var controls = document.createElement('div');
controls.style.cssText = 'margin-bottom: 8px; display: flex; gap: 8px; align-items: center; font: 13px Arial, sans-serif;';

var addBtn = document.createElement('button');
addBtn.textContent = '+ 添加节点';
addBtn.style.cssText = 'padding: 6px 14px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600;';
addBtn.onclick = function() {
var x = 100 + Math.random() * (width - 200);
var y = 80 + Math.random() * (height - 160);
var node = makeNode('新节点', '#2196F3', x, y);
node.moveToTop();
};
controls.appendChild(addBtn);

var hint = document.createElement('span');
hint.textContent = '拖动节点以重新定位。箭头会自动跟随。';
hint.style.cssText = 'color: #888; font-size: 12px;';
controls.appendChild(hint);

container.parentNode.insertBefore(controls, container);

var nodeList = [];
var arrowList = [];

function makeNode(label, color, x, y) {
var group = new Konva.Group({ x: x, y: y, draggable: true });

var rect = new Konva.Rect({
width: 120, height: 50,
fill: color, cornerRadius: 8,
shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 6, shadowOffsetY: 2,
offsetX: 60, offsetY: 25,
});

var text = new Konva.Text({
text: label, fontSize: 14, fontFamily: 'Arial',
fill: '#fff', width: 120, height: 50,
align: 'center', verticalAlign: 'middle',
offsetX: 60, offsetY: 25,
});

group.add(rect);
group.add(text);
layer.add(group);

var entry = { group: group };
nodeList.push(entry);

group.on('dragmove', updateArrows);

return group;
}

function connect(from, to) {
var arrow = new Konva.Arrow({
points: [from.x(), from.y(), to.x(), to.y()],
pointerLength: 10, pointerWidth: 8,
fill: '#555', stroke: '#555', strokeWidth: 2,
listening: false,
});
layer.add(arrow);
arrow.moveToBottom();
arrowList.push({ shape: arrow, from: from, to: to });
}

function updateArrows() {
for (var i = 0; i < arrowList.length; i++) {
var a = arrowList[i];
a.shape.points([a.from.x(), a.from.y(), a.to.x(), a.to.y()]);
}
layer.batchDraw();
}

var start = makeNode('开始', '#4CAF50', 100, 120);
var procA = makeNode('处理 A', '#2196F3', 300, 80);
var decide = makeNode('决策', '#FF9800', 500, 120);
var procB = makeNode('处理 B', '#2196F3', 300, 260);
var end = makeNode('结束', '#f44336', 500, 260);

connect(start, procA);
connect(procA, decide);
connect(decide, procB);
connect(decide, end);
connect(procB, end);

设计图数据

上面的示例将每个节点集中存放,并让每条连接只保存两个节点 ID。这种规范化的重要性超乎想象:永远不要将节点的位置复制到其边中,否则每次移动都会留下过时的重复数据。绘制时读取当前节点位置即可。

对于小型图,在每次拖动移动事件中保存位置并重新计算箭头,已经足够清晰且快速。对于大型图,在手势过程中直接移动被拖动的节点,并在拖动结束时提交一次文档更改——每个操作记录一个历史条目,而不是每帧记录一个。

此示例连接了两个矩形的水平边,这对于从左到右的流程来说清晰易读。真正的编辑器需要显式的端口:在边上存储端口 ID,并从拥有该端口的节点推导其绝对位置。待位置稳定后再计算正交连接器的路径——每帧重新计算路径的开销很高。

如果添加平移和缩放功能,请在任何命中或放置测试之前,通过舞台变换的逆变换转换指针位置,并使图坐标独立于相机。请参阅相对于指针进行缩放

对于大型图,仅绘制可见节点,使用空间索引处理视口和选择查询,将连接器放在节点形状下方,并对永远不会接收指针输入的连接器设置 listening: false。请参阅性能提示

端口、边选择、循环规则、自动布局、历史记录和持久化都属于图规则。在构建界面之前,先在数据模型中定义每一项。