效果:
代码:
import dagre from "dagre";
/** 传入的节点类型 */
export type NodeType = {
id: React.Key;
/** 节点宽度 */
width: number;
/** 节点高度 */
height: number;
nextIdList: React.Key[];
};
// 计算时附加的节点类型
export type NodeTypeWithCalc = {
prevIdList: React.Key[];
};
/** 渲染时附加的节点类型 */
export type NodeTypeWithRender = NodeType &
NodeTypeWithCalc & {
level: number;
x: number;
y: number;
};
/** 处理数据计算层级 */
export const calcDataLevel = (nodeList: (NodeType & NodeTypeWithCalc)[]) => {
// 默认层级为0
const defaultLevel = 0;
// 深拷贝节点列表
let newNodes: ((NodeType & NodeTypeWithCalc) & { level: number })[] = [
...nodeList,
].map((item) => ({
...item,
level: defaultLevel,
}));
// 起始节点
const startNode = newNodes?.find((item) => item.prevIdList.length === 0);
// 递归计算节点的层级
const calcLevel = (node: NodeType & { level: number }) => {
node.nextIdList.forEach((nextId) => {
const nextNode = newNodes.find((item) => item.id === nextId);
if (nextNode?.level === defaultLevel) {
nextNode.level = node.level + 1;
calcLevel(nextNode);
}
});
};
if (startNode) {
startNode.level = 1;
calcLevel(startNode);
}
return newNodes;
};
/** 处理数据计算坐标Y */
export const calcDataPositionY = (pa: {
nodeList: ((NodeType & NodeTypeWithCalc) & { level: number })[];
nodeVerticalSpace: number;
}) => {
const { nodeList, nodeVerticalSpace } = pa;
// 深拷贝节点列表
const newNodes: ((NodeType & NodeTypeWithCalc) & {
level: number;
y: number;
})[] = [...nodeList].map((item) => ({
...item,
y: 0,
}));
// 每层高度的最高值字典
const levelHeightMap: Record<number, number> = {};
// 计算每层的最高值
newNodes.forEach((item) => {
if (!levelHeightMap[item.level]) {
levelHeightMap[item.level] = item.height;
} else {
levelHeightMap[item.level] = Math.max(
levelHeightMap[item.level],
item.height
);
}
});
// 根据层级和前置层最高的节点高度计算Y坐标
newNodes.forEach((item) => {
const prevNodes = newNodes.filter((node) =>
item.prevIdList.includes(node.id)
);
if (prevNodes.length === 0) {
item.y = 0;
} else {
const prevNode = prevNodes[0];
const prevLevelHeight = levelHeightMap[prevNode.level];
item.y = prevNode.y + prevLevelHeight + nodeVerticalSpace;
}
});
return newNodes as NodeTypeWithRender[];
};
/** 处理数据计算坐标X */
export const calcDataPositionX = (pa: {
nodeList: (NodeType & { level: number })[];
nodeHorizontalSpace: number;
}) => {
const { nodeList, nodeHorizontalSpace } = pa;
// 深拷贝节点列表
const newNodes: (NodeType & { level: number; x: number })[] = [
...nodeList,
].map((item) => ({
...item,
x: 0,
}));
const g = new dagre.graphlib.Graph();
// 设置图的默认值,算法我也不太懂,但是network-simplex会导致节点位置变化,所以这里设置为longest-path
g.setGraph({
rankDir: "BT",
nodesep: nodeHorizontalSpace,
ranker: "longest-path",
});
g.setDefaultEdgeLabel(function () {
return {};
});
newNodes.forEach((node) => {
g.setNode(node.id.toString(), {
width: node.width,
height: node.height,
});
});
newNodes.forEach((node) => {
node.nextIdList.forEach((nextId) => {
g.setEdge(node.id.toString(), nextId.toString());
});
});
dagre.layout(g);
newNodes.forEach((node) => {
const nodePosition = g.node(node.id.toString());
if (nodePosition) node.x = nodePosition.x;
});
return newNodes as NodeTypeWithRender[];
};
/** 根据节点的nextIdList,得到prevIdList */
export const setNodePrevIdList = (data: NodeType[]) => {
const newNodes: (NodeType & NodeTypeWithCalc)[] = [...data].map((item) => ({
...item,
prevIdList: [],
}));
newNodes.forEach((item) => {
item.nextIdList.forEach((nextId) => {
const nextNode = newNodes.find((node) => node.id === nextId);
if (nextNode) {
nextNode.prevIdList.push(item.id);
}
});
});
return newNodes;
};
引用:
import {
NodeTypeWithRender,
calcDataLevel,
calcDataPositionY,
calcDataPositionX,
type NodeType,
setNodePrevIdList,
} from "./const";
import { useEffect, useMemo } from "react";
import { jsPlumb as jsplumbUI, type jsPlumbInstance } from "jsplumb";
import { jsplumbSetting, createConnections } from "./js_plumb_mixins";
import "./index.less";
type PropsType = {
nodeList: NodeType[];
/** 节点间的垂直间距 */
nodeVerticalSpace?: number;
/** 节点间的水平间距 */
nodeHorizontalSpace?: number;
renderNode?: (node: NodeTypeWithRender) => React.ReactNode;
};
// 要拉到外面,不然每次渲染都会重新创建一个实例(出现删除不了线)
const jsPlumb: jsPlumbInstance = jsplumbUI.getInstance();
/** 流程图组件,只适用树形控件(只能有一个起始点)*/
const FlowChart = (props: PropsType) => {
// 用于渲染的节点列表
const showNodeList = useMemo(() => {
return calcDataPositionX({
nodeList: calcDataPositionY({
nodeList: calcDataLevel(setNodePrevIdList(props.nodeList)),
nodeVerticalSpace: props.nodeVerticalSpace || 80,
}),
nodeHorizontalSpace: props.nodeHorizontalSpace || 20,
});
}, [props.nodeList]);
// 初始化jsPlumb
const initJsPlumb = () => {
jsPlumb?.ready(function () {
// 导入默认配置
jsPlumb.importDefaults(jsplumbSetting);
// 会使整个jsPlumb立即重绘。
jsPlumb.setSuspendDrawing(false, true);
// 绑定鼠标移入连线事件
jsPlumb?.bind("mouseover", (conn: any) => {
// 点击连线,不是点击连线上的path
conn.setPaintStyle({ stroke: "#7aff00", strokeWidth: 2 });
});
// 绑定鼠标移出连线事件
jsPlumb?.bind("mouseout", (conn: any) => {
// 点击连线,不是点击连线上的path
conn.setPaintStyle({ stroke: "#3a9ffc", strokeWidth: 2 });
});
});
};
useEffect(() => {
initJsPlumb();
return () => {
if (!jsPlumb) return;
jsPlumb.deleteEveryEndpoint();
jsPlumb.deleteEveryConnection();
jsPlumb.unbind();
};
}, []);
useEffect(() => {
setTimeout(() => {
// 创建连线
createConnections({
dataArr: showNodeList.map((item) => ({
...item,
nodeId: item.id.toString(),
bottom: item.nextIdList?.map((id) => id.toString()) || [],
top: item.prevIdList?.map((id) => id.toString()) || [],
left: [],
right: [],
})),
jsPlumb,
});
}, 10);
}, [showNodeList]);
return (
<div className="flow-chart-wrap">
{showNodeList.map(
(item) =>
props.renderNode?.(item) || (
<div
id={item.id.toString()}
key={item.id}
className="node"
style={{
left: item.x,
top: item.y,
width: item.width,
height: item.height,
}}
>
{item.id}
</div>
)
)}
</div>
);
};
export default FlowChart;