安装: 
    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;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐