react-flow 动态流程图
npm install reactflow(我的版本是npm install reactflow@11.11.4)
·
安装: npm install reactflow (我的版本是npm install reactflow@11.11.4)
reactflow 官网 https://reactflow.dev/
内置组件
- <Background/>插件实现了一些基本的可定制背景模式。
- <MiniMap/>插件在屏幕角落显示图形的小版本。
- <Controls/>插件添加控件以缩放、居中和锁定视口。
- <Panel/>插件可以轻松地将内容定位在视口顶部。
- <NodeToolbar/>插件允许您渲染附加到节点的工具栏。
- <NodeResizer/>插件可以很容易地为节点添加调整大小的功能。
整体效果图: (官网的:https://reactflow.dev/examples/styling/turbo-flow)
代码实现效果
index.js 文件 (包裹流程图的文件)
import React from 'react';
import OverviewFlow from './overcierFlow';
import './index.css'
import './overflow.css'
class MindFlow extends React.Component {
render() {
return (
<div
className='box'
style={{ height: '100vh', width: '100%' }}>
<OverviewFlow>
</OverviewFlow>
</div>
);
}
}
export default MindFlow;
overcierFlow.js 文件(流程图文件)
import React, { useEffect, useCallback } from "react";
import ReactFlow, {
useNodesState,
useEdgesState,
Controls,
MiniMap,
getIncomers,
getOutgoers,
getConnectedEdges,
} from "reactflow";
import {
nodes as initialNodes,
edges as initialEdges,
} from "./initial-elements";
import CustomNode from "./ResizableNode";//自定义节点样式
import TurboEdge from "./TurboEdge";//自定义连接线
import axios from "axios";
import "reactflow/dist/style.css";
import "reactflow/dist/base.css";
const nodeTypes = {
custom: CustomNode,
//注意:用到自定义节点的话必须每个数据的 type:custom ,如果添加其他自定义节点如 custom2:引入文件 数据的type 就是 custom2
};
const edgeTypes = {
custom: TurboEdge, //注意:用到自定义的连线每个数据的 type:custom 如上一样
};
const defaultEdgeOptions = {
type: "custom",
markerEnd: "edge-circle",
};
const OverviewFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
//初始获取数据
useEffect(() => {
axios({
url: `/getConclusion`,
method: "GET",
}).then((res1) => {
if (res1.data) {
setNodes(res1.data.nodes);
setEdges(res1.data.edges);
}
});
}, []);
if (!nodes?.length) {
return null;
}
//页面中点击删除可以删除每条线
const onNodesDelete = useCallback(
(deleted) => {
setEdges(
deleted.reduce((acc, node) => {
const incomers = getIncomers(node, nodes, edges);
const outgoers = getOutgoers(node, nodes, edges);
const connectedEdges = getConnectedEdges([node], edges);
const remainingEdges = acc.filter(
(edge) => !connectedEdges.includes(edge)
);
const createdEdges = incomers.flatMap(({ id: source }) =>
outgoers.map(({ id: target }) => ({
id: `${source}->${target}`,
source,
target,
}))
);
return [...remainingEdges, ...createdEdges];
}, edges)
);
},
[nodes, edges]
);
//处理数据的方法
const findarr = (a, b) => {
let arr = a.filter((item) => b.some((v) => v.source === item.source));
return arr;
};
const findtarget = (a, b) => {
if (a && b) {
return a.filter((item) => !b.some((v) => v.target === item.id));
} else {
return [];
}
};
const findsource = (a, b) => {
if (a && b) {
return a.filter((item) => !b.some((v) => v.source === item.source));
} else {
return [];
}
};
const findnodes = (a, b) => {
if (a && b) {
return a.filter((item) => !b.some((v) => v.target === item.id));
} else {
return [];
}
};
const findedges = (a, b) => {
if (a && b) {
return a.filter((item) => !b.some((v) => v.id === item.id));
} else {
return [];
}
};
const debounce = (fn, delay) => {
let timer;
return (...args) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args);
}, delay);
};
};
const _ResizeObserver = window.ResizeObserver;
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
/**
* @constructor
* @param {Function} callback - 回调函数,将在每次滚动时被调用。该函数接受一个参数:event(包含滚动事件的信息)。
* 该函数应该返回一个布尔值,表示是否应该继续触发事件。如果返回false,则不会再次触发事件。
* 该函数可以选择性地使用preventDefault()来防止默认行为。
* @description 构造函数,创建一个DebouncedScrollEvent对象实例。
*/
constructor(callback) {
callback = debounce(callback, 10);
super(callback);
}
};
const findAllData = (node, edge) => {
if (findarr(edges, edge).length !== 0) {
if (edge) {
//如果要添加的 edge 的起点source 比总和的 edges 的最后一个的起点短 删除edges里面长的所有source
//删除 nodes 中 id 和要删除的长的source一样的 id
if (edge[0].source.length < edges[edges.length - 1].source.length) {
let findeag = edges.filter((item) => {
return item.source.length > edge[0].source.length;
});
let findegds = findsource(edges, findeag);
let findnode = findnodes(nodes, findeag);
setEdges(findegds);
setNodes(findnode);
} else {
//删掉之前的线 和 nodes中id 和删除线的target相同的
const listall = findtarget(nodes, findarr(edges, edge)).concat(node);
listall.forEach((item) => {
item.data.loading = false;
});
//删除两个 id 重复的后合并edge
setEdges(findedges(edges, edge).concat(edge));
setNodes(listall);
}
}
} else {
const listall = nodes.concat(node);
listall.forEach((item) => {
item.data.loading = false;
});
setEdges(edges.concat(edge));
setNodes(listall);
}
Array.from(document.getElementsByClassName("ant-spin-dot-holder")).forEach(
(item) => {
item.style.display = "none";
}
);
Array.from(document.getElementsByClassName("loadingTitle")).forEach(
(item) => {
item.style.display = "none";
}
);
};
// 点击节点,将节点初始配置传入nodes
const onNodeClick = (e, node) => {
if (node.flags) {
if (node.data.select) {
nodes.forEach((item) => {
if (item.id === node.id) {
item.data.loading = true;
}
});
setNodes(nodes);
const obj = node;
obj.level = node.data.level;
obj.select = node.data.select;
delete node.data["select"];
delete node.data["weekly_template"];
axios({
url: `/getNodes`,
method: "post",
data: obj,
})
.then((res) => {
if (res.data) {
findAllData(res.data.nodes, res.data.edges);
}
})
.finally(() => {});
}
}
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodeClick={onNodeClick}
onNodesChange={onNodesChange}
onNodesDelete={onNodesDelete}
onEdgesChange={onEdgesChange}
defaultEdgeOptions={defaultEdgeOptions}
>
<Controls />
<MiniMap />
<svg>
<defs>
<linearGradient id="edge-gradient">
<stop offset="0%" stopColor="#ae53ba" />
<stop offset="100%" stopColor="#2a8af6" />
</linearGradient>
<marker
id="edge-circle"
viewBox="-5 -5 10 10"
refX="0"
refY="0"
markerUnits="strokeWidth"
markerWidth="10"
markerHeight="10"
orient="auto"
>
<circle stroke="#2a8af6" strokeOpacity="0.75" r="2" cx="0" cy="0" />
</marker>
</defs>
</svg>
</ReactFlow>
);
};
export default OverviewFlow;
initial-elements.js文件 流程图的数据
export const nodes = [
{
id: "root",
type: "custom", //type:custom 就是和上面文件的自定义对上了
data: {
label: '',
loading: true,
},
position: { x: 15, y: 10 },//-113px, -130.5
flags: true,
},
id: "hangye",
type: "custom",
data: { label: "行业",show:true },
position: { x: 0, y: -55 },
flags:true,
style: {
borderRadius: '5px',
width:100,
height:50
}
},
{
id: "chanpinxian",
type: "custom",
data: { label: "产品线",show:true },
position: { x: 0, y: -60 },
flags:true,
style: {
borderRadius: '5px',
width:100,
height:50
}
},
{
id: "duan",
type: "custom",
data: { label: "端" ,show:true},
position: { x: 0, y: -80 ,},
flags:true,
style: {
borderRadius: '5px',
width:100,
height:50
}
},
{
id: "horizontal-2",
type:'custom',
data: { label: "端内" },
position: { x: 300, y: -50 },
flags:true
},
{
id: "horizontal-3",
type:'custom',
// sourcePosition: "right",
// targetPosition: "left",
data: { label: "端外" },
position: { x: 300, y: 0 }
},
{
id: "horizontal-0",
type:'custom',
// sourcePosition: "right",
// targetPosition: "left",
data: { label: "PC" },
position: { x: 300, y: 50 }
},
{
id: "hzhuong",
type:'custom',
data: { label: "主动" },
position: { x: 400, y: -100 }
},
{
id: "jifa-3",
type:'custom',
// sourcePosition: "right",
// targetPosition: "left",
data: { label: "激发" },
position: { x: 400, y: -50 }
},
{
id: "diaodong-0",
type:'custom',
// sourcePosition: "right",
// targetPosition: "left",
data: { label: "调动运营" },
position: { x: 400, y: 0 }
},
];
export const edges = [
{
id: "horizontal-e1-2",
source: "root",
type: "smoothstep",
target: "horizontal-2",
label: '中间字段'
// animated: true
},
{
id: "horizontal-e1-0",
source: "root",
type: "smoothstep",
target: "horizontal-0"
},
{
id: "horizontal-e1-3",
source: "root",
type: "smoothstep",
target: "horizontal-3",
// animated: true
},
{
id: "duannei-1",
source: "horizontal-2",
type: "smoothstep",
target: "hzhuong",
// animated: true
},
{
id: "duannei-2",
source: "horizontal-2",
type: "smoothstep",
target: "jifa-3",
// animated: true
},
{
id: "duannei-3",
source: "horizontal-2",
type: "smoothstep",
target: "diaodong-0",
// animated: true
},
];
ResizableNode.js自定义节点文件
import React, { memo, useState, useEffect, useRef } from "react";
import { SearchOutlined } from "@ant-design/icons";
import { Popconfirm, Button } from "antd";
import { Handle } from "reactflow";
import { Spin } from "antd";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import "katex/dist/katex.min.css";
import "highlight.js/styles/github.css";
import "./index.css";
import "./overflow.css";
export default memo(({ data, id, isConnectable }) => {
const [open, setOpen] = useState(false);
const [loadTitle, setLoadTitle] = useState(null);
const tooltipContainerRef = useRef(null);
const getPopupContainer = (triggerNode) => {
// 这里返回你希望 Tooltip 弹出层挂载到的 DOM 元素 f
return tooltipContainerRef.current;
};
useEffect(() => {
setOpen(false);
}, []);
const visivleChange = (visible) => {
setOpen(visible); //这里使用的HOOKs
data.select = "";
};
const changeDrawer = (obj, key, label) => {
setLoadTitle(label);
data.select = obj;
data.select.key = key;
};
const resicaBox = () => {
return (
<div
className="ResicabelNode gradient"
ref={tooltipContainerRef}
id="root"
>
<div className="inner">
<Handle
type="target"
position={data.show ? "top" : "left"}
className="my_handle"
onConnect={(params) => console.log("handle onConnect", params)}
isConnectable={isConnectable}
/>
<div className="nodeContent" style={data.style}>
<div className="nodelabel">
<ReactMarkdown
components={{
// Map `h1` (`# heading`) to use `h2`s.
h1: "h2",
// Rewrite `em`s (`*like so*`) to `i` with a red foreground color.
em: ({ node, ...props }) => (
<i style={{ color: "red" }} {...props} />
),
}}
children={data.label}
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
/>
{/* {data.desc && <div className="subline">{data.desc}</div>} */}
{data.desc && (
<ReactMarkdown
components={{
// Map `h1` (`# heading`) to use `h2`s.
h1: "h2",
// Rewrite `em`s (`*like so*`) to `i` with a red foreground color.
em: ({ node, ...props }) => (
<i style={{ color: "red" }} {...props} />
),
}}
children={data.desc}
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
/>
)}
{data.loading ? (
<div className="loadingbox">
<p className="loadingTitle">
{loadTitle && loadTitle + "加载中"}
</p>
<Spin></Spin>
</div>
) : (
""
)}
</div>
</div>
<Handle
type="source"
position={data.show ? "bottom" : "right"}
id="a"
className="my_handle"
isConnectable={isConnectable}
/>
</div>
</div>
);
};
return (
<>
{data.list ? (
<Popconfirm
title={
<div className="popcon">
{data.list.map((item) => {
return (
<div key={item.key}>
<p>{item.label}:</p>
{item.children &&
item.children.map((nitem) => {
return (
<div
className="popcon_btn"
key={nitem.id}
onClick={() => {
changeDrawer(nitem, item.key, item.label);
}}
>
<Button
onClick={() => {
setOpen(false);
}}
type="primary"
>
{nitem.title}
</Button>
</div>
);
})}
</div>
);
})}
</div>
}
cancelText=""
okText=""
open={open}
onOpenChange={visivleChange}
>
{resicaBox()}
</Popconfirm>
) : (
resicaBox()
)}
</>
);
});
TurboEdge.js 文件 自定义线
import React from 'react';
import { getBezierPath } from 'reactflow';
export default function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}) {
const xEqual = sourceX === targetX;
const yEqual = sourceY === targetY;
const [edgePath] = getBezierPath({
// we need this little hack in order to display the gradient for a straight line
sourceX: xEqual ? sourceX + 0.0001 : sourceX,
sourceY: yEqual ? sourceY + 0.0001 : sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<path
id={id}
style={style}
className="react-flow__edge-path"
d={edgePath}
markerEnd={markerEnd}
/>
</>
);
}
overflow.css
.react-flow__node {
display: flex;
width: 220px;
height: auto;
border-radius: var(--node-border-radius);
box-shadow: var(--node-box-shadow);
letter-spacing: -.2px;
font-weight: 500;
font-family: 'Fira Mono', Monospace;
}
.ResicabelNode {
position: relative;
display: flex;
overflow: hidden;
flex-grow: 1;
padding: 2px;
width: 100%;
height: 100%;
border-radius: var(--node-border-radius);
}
.box {
display: 'flex';
align-items: 'center';
justify-content: 'center';
}
.gradient::before {
position: absolute;
top: 50%;
left: 50%;
padding-bottom: calc(100% * 1.41421356237);
width: calc(100% * 1.41421356237);
border-radius: 100%;
background: conic-gradient(from -160deg at 50% 50%, #e92a67 0deg, #a853ba 120deg, #2a8af6 240deg, #e92a67 360deg);
content: '';
transform: translate(-50%, -50%);
}
.wrapper.gradient::before {
z-index: -1;
background: conic-gradient(from -160deg at 50% 50%, #e92a67 0deg, #a853ba 120deg, #2a8af6 240deg, rgba(42, 138, 246, 0) 360deg);
content: '';
transform: translate(-50%, -50%) rotate(0deg);
animation: spinner 4s linear infinite;
}
@keyframes spinner {
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
.inner {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
padding: 0px 9px;
border-radius: var(--node-border-radius);
background: var(--bg-color);
}
.cloud {
position: absolute;
top: 0;
right: 0;
z-index: 1;
display: flex;
overflow: hidden;
padding: 2px;
width: 30px;
height: 30px;
border-radius: 100%;
box-shadow: var(--node-box-shadow);
transform: translate(50%, -50%);
transform-origin: center center;
}
.nodelabel {
margin-bottom: 2px;
/* width:120px; */
font-size: 12px;
line-height: 1;
}
.subline {
margin-top: -6px;
color: #777;
font-size: 10px;
}
.circle {
width: 2em;
height: 2em;
border-radius: 50%;
background: #ebf2fd;
box-shadow: .1em .125em 0 0 rgb(15 28 63 / 13%);
text-align: center;
font-weight: bold;
line-height: 2em;
}
.popcon_btn {
margin: 10px 0;
}
.ant-popconfirm-message-icon {
display: none;
}
.ant-popover-buttons {
display: none;
}
.ant-popover-inner-content {
padding: 7px 10px;
}
.ant-popconfirm-buttons {
display: none;
}
.react-flow {
background-color: var(--bg-color);
color: var(--text-color);
--bg-color: rgb(17, 17, 17);
--text-color: rgb(243, 244, 246);
--node-border-radius: 10px;
--node-box-shadow:
10px 0 15px rgba(42, 138, 246, .3),
-10px 0 15px rgba(233, 42, 103, .3);
}
.react-flow__node-turbo {
display: flex;
min-width: 150px;
height: 70px;
border-radius: var(--node-border-radius);
box-shadow: var(--node-box-shadow);
letter-spacing: -.2px;
font-weight: 500;
font-family: 'Fira Mono', Monospace;
}
.react-flow__node-turbo .wrapper {
position: relative;
display: flex;
overflow: hidden;
flex-grow: 1;
padding: 2px;
border-radius: var(--node-border-radius);
}
@keyframes spinner {
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
.react-flow__node-turbo .inner {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
padding: 16px 20px;
border-radius: var(--node-border-radius);
background: var(--bg-color);
}
.react-flow__node-turbo .icon {
margin-right: 8px;
}
.react-flow__node-turbo .title {
margin-bottom: 2px;
font-size: 16px;
line-height: 1;
}
.react-flow__node-turbo .subline {
color: #777;
font-size: 12px;
}
.react-flow__node-turbo .cloud {
position: absolute;
top: 0;
right: 0;
z-index: 1;
display: flex;
overflow: hidden;
padding: 2px;
width: 30px;
height: 30px;
border-radius: 100%;
box-shadow: var(--node-box-shadow);
transform: translate(50%, -50%);
transform-origin: center center;
}
.react-flow__node-turbo .cloud div {
position: relative;
display: flex;
align-items: center;
flex-grow: 1;
justify-content: center;
border-radius: 100%;
background-color: var(--bg-color);
}
.react-flow__handle {
opacity: 0;
}
.react-flow__handle.source {
right: -10px;
}
.react-flow__handle.target {
left: -10px;
}
.react-flow__node:focus {
outline: none;
}
.react-flow__edge .react-flow__edge-path {
stroke: url(#edge-gradient);
stroke-width: 2;
stroke-opacity: .75;
}
.react-flow__controls button {
border: 1px solid #95679e;
border-bottom: none;
background-color: var(--bg-color);
color: var(--text-color);
}
.react-flow__controls button:hover {
background-color: rgb(37, 37, 37);
}
.react-flow__controls button:first-child {
border-radius: 5px 5px 0 0;
}
.react-flow__controls button:last-child {
border-bottom: 1px solid #95679e;
border-radius: 0 0 5px 5px;
}
.react-flow__controls button path {
fill: var(--text-color);
}
.react-flow__attribution {
background: rgba(200, 200, 200, .2);
display: none;
}
.react-flow__attribution a {
color: #95679e;
}
.loadingbox {
display: flex;
justify-content: space-evenly;
align-items: center;
}
index.css
.react-flow__container {
top: -30px !important;
}
.anticon .anticon-exclamation-circle {
display: none;
}
.ant-popover-inner-content .ant-popover-buttons {
display: none;
}
.ant-popover-message-title {
display: none;
}
.a-Page-body .homeHeader {
overflow: auto !important;
}
.react-flow__attribution.left {
display: none;
}
.react-flow {
overflow: auto;
display: block;
}
.react-flow__edge-textbg {
fill: #e2e6f3 !important;
}
更多推荐
所有评论(0)