跳到主要内容

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

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

操作说明: 拖动任意节点以调整其位置。连接会自动更新。单击“Add Node”添加更多节点。

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 = '+ Add Node';
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('New Node', '#2196F3', x, y);
  node.moveToTop();
  layer.draw();
};
controls.appendChild(addBtn);

var hint = document.createElement('span');
hint.textContent = 'Drag nodes to reposition. Arrows follow automatically.';
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('Start', '#4CAF50', 100, 120);
var procA = makeNode('Process A', '#2196F3', 300, 80);
var decide = makeNode('Decision', '#FF9800', 500, 120);
var procB = makeNode('Process B', '#2196F3', 300, 260);
var end = makeNode('End', '#f44336', 500, 260);

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

layer.draw();

设计图数据

上面的示例把每个节点保存在一处,并让每条连接只存储两个节点 ID。这种归一化比看上去 更重要:绝不要把节点位置复制到边的数据中,否则每次移动都会留下过时的副本。应该在绘制 时读取当前节点位置。

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

本示例连接两个矩形的水平边,这对从左到右的流程图可读性很好。真实的编辑器需要显式的 端口:把端口 ID 存储在边上,并从拥有它的节点计算其绝对位置。在位置稳定后再计算 正交连接线的路由 — 每帧重新计算路由开销很大。

如果你添加平移和缩放,在任何命中或放置计算之前,用舞台变换的逆矩阵转换指针位置,并 让图坐标独立于相机 state。参见相对于指针缩放

对于大图,只绘制可见节点,用空间索引处理视口和选择查询,把连接线保持在节点图形下方, 并对不接收指针输入的连接线设置 listening: false。参见 性能建议

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