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();
  layer.draw();
};
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);

layer.draw();