react-flow 流程圖2.0

Qing`ing發表於2024-07-24
檔案中需要下載的元件:

   npm install reactflow     (我的版本是npm install reactflow@11.11.4)
   npm install react-markdown (下面流程圖中用到了 markdown) 版本7.1.0
    npm i antd   (版本 5.18.3)
    npm i axios   (版本1.7.2)
//marjdown 中用到的樣式字型等
   npm i rehype-highlight
       npm i remark-gfm
       npm i rehype-raw
       npm i rehype-katex
       npm i remark-math
reactflow 官網
  https://reactflow.dev/

內建元件

  • <Background/>外掛實現了一些基本的可定製背景模式。
  • <MiniMap/>外掛在螢幕角落顯示圖形的小版本。
  • <Controls/>外掛新增控制元件以縮放、居中和鎖定視口。
  • <Panel/>外掛可以輕鬆地將內容定位在視口頂部。
  • <NodeToolbar/>外掛允許您渲染附加到節點的工具欄。
  • <NodeResizer/>外掛可以很容易地為節點新增調整大小的功能。

需求:

  例1:最初獲取到 第一個父節點 和父節點下面的選擇下拉資料,根據點選的每個父節點的下拉資訊後端返回子節點 。前端處理把子節點新增到整個資料中然後展示到頁面上。

  小要求:每個節點中的內容用到了 markdown 的形式展示 且 每個父節點 點選獲取資料時有載入效果。

例2:如果你的需求是隻獲取一次全部的資料 展示出流程圖這種會比較簡單。

⭐️ 例1中複雜處理的地方:

  當父節點有子節點 子節點還有子節點的情況 再次點選父節點需要有刪除節點的操作 。 需要刪除子節點下面的子節點。而且每次點選父節點 需要刪除上次獲取到的子節點 然後再次加入新的子節點。

整體效果圖:  (官網的:https://reactflow.dev/examples/styling/turbo-flow)

首先在 useEffect中 根據後端獲取得到第一個節點 和第一個節點的下拉資料

點選每個節點的下拉資料後 下拉框關閉 並且展示載入效果 再次獲取到子節點展示到頁面

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 檔案(流程圖檔案)  
  1 /* eslint-disable */
  2 import React, { useEffect, useCallback } from "react";
  3 import ReactFlow, {
  4   useNodesState,
  5   useEdgesState,
  6   Controls,
  7   MiniMap,
  8   getIncomers,
  9   getOutgoers,
 10   getConnectedEdges,
 11 } from "reactflow";
 12 import {
 13   nodes as initialNodes,
 14   edges as initialEdges,
 15 } from "./initial-elements";
 16 import CustomNode from "./ResizableNode";//自定義節點樣式
 17 import TurboEdge from "./TurboEdge";//自定義連線線
 18  19 import axios from "axios";
 20 import "reactflow/dist/style.css";
 21 import "reactflow/dist/base.css";
 22 
 23 const nodeTypes = {
 24   custom: CustomNode, //注意:用到自定義節點的話必須每個資料的 type:custom ,如果新增其他自定義節點如 custom2:引入檔案 資料的type 就是 custom2
 25 };
 26 const edgeTypes = {
 27   custom: TurboEdge, //注意:用到自定義的連線每個資料的 type:custom  如上一樣
 28 };
 29 const defaultEdgeOptions = {
 30   type: "custom",
 31   markerEnd: "edge-circle",
 32 };
 33 
 34 const OverviewFlow = () => {
 35   const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
 36   const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
 37 
 38   //初始獲取資料  如果需求是例2 只要下面就可以 就不需要onNodeClick函式中的程式碼
 39   useEffect(() => {
 40     axios({
 41       url: `/getConclusion`,
 42       method: "GET",
 43     }).then((res1) => {
 44       if (res1.data) {
 45         setNodes(res1.data.nodes);
 46         setEdges(res1.data.edges);
 47       }
 48     });
 49   }, []);
 50 
 51   if (!nodes?.length) {
 52     return null;
 53   }
 54 
 55   //頁面中點選刪除可以刪除每條線
 56   const onNodesDelete = useCallback(
 57     (deleted) => {
 58       setEdges(
 59         deleted.reduce((acc, node) => {
 60           const incomers = getIncomers(node, nodes, edges);
 61           const outgoers = getOutgoers(node, nodes, edges);
 62           const connectedEdges = getConnectedEdges([node], edges);
 63 
 64           const remainingEdges = acc.filter(
 65             (edge) => !connectedEdges.includes(edge)
 66           );
 67 
 68           const createdEdges = incomers.flatMap(({ id: source }) =>
 69             outgoers.map(({ id: target }) => ({
 70               id: `${source}->${target}`,
 71               source,
 72               target,
 73             }))
 74           );
 75 
 76           return [...remainingEdges, ...createdEdges];
 77         }, edges)
 78       );
 79     },
 80     [nodes, edges]
 81   );
 82 
 83   //處理資料的方法
 84   const findarr = (a, b) => {
 85     let arr = a.filter((item) => b.some((v) => v.source === item.source));
 86     return arr;
 87   };
 88 
 89   const findtarget = (a, b) => {
 90     if (a && b) {
 91       return a.filter((item) => !b.some((v) => v.target === item.id));
 92     } else {
 93       return [];
 94     }
 95   };
 96   const findsource = (a, b) => {
 97     if (a && b) {
 98       return a.filter((item) => !b.some((v) => v.source === item.source));
 99     } else {
100       return [];
101     }
102   };
103   const findnodes = (a, b) => {
104     if (a && b) {
105       return a.filter((item) => !b.some((v) => v.target === item.id));
106     } else {
107       return [];
108     }
109   };
110   const findedges = (a, b) => {
111     if (a && b) {
112       return a.filter((item) => !b.some((v) => v.id === item.id));
113     } else {
114       return [];
115     }
116   };
117   const debounce = (fn, delay) => {
118     let timer;
119     return (...args) => {
120       if (timer) {
121         clearTimeout(timer);
122       }
123       timer = setTimeout(() => {
124         fn(...args);
125       }, delay);
126     };
127   };
128 
129   const _ResizeObserver = window.ResizeObserver;
130   window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
131     /**
132      * @constructor
133      * @param {Function} callback - 回撥函式,將在每次滾動時被呼叫。該函式接受一個引數:event(包含滾動事件的資訊)。
134      * 該函式應該返回一個布林值,表示是否應該繼續觸發事件。如果返回false,則不會再次觸發事件。
135      * 該函式可以選擇性地使用preventDefault()來防止預設行為。
136      * @description 建構函式,建立一個DebouncedScrollEvent物件例項。
137      */
138     constructor(callback) {
139       callback = debounce(callback, 10);
140       super(callback);
141     }
142   };
143 
144   const findAllData = (node, edge) => {
145     if (findarr(edges, edge).length !== 0) {
146       if (edge) {
147         //如果要新增的 edge 的起點source 比總和的 edges 的最後一個的起點短 刪除edges裡面長的所有source
148         //刪除 nodes 中 id 和要刪除的長的source一樣的 id
149         if (edge[0].source.length < edges[edges.length - 1].source.length) {
150           let findeag = edges.filter((item) => {
151             return item.source.length > edge[0].source.length;
152           });
153           let findegds = findsource(edges, findeag);
154           let findnode = findnodes(nodes, findeag);
155           setEdges(findegds);
156           setNodes(findnode);
157         } else {
158           //刪掉之前的線 和 nodes中id 和刪除線的target相同的
159           const listall = findtarget(nodes, findarr(edges, edge)).concat(node);
160           listall.forEach((item) => {
161             item.data.loading = false;
162           });
163           //刪除兩個 id 重複的後合併edge
164           setEdges(findedges(edges, edge).concat(edge));
165           setNodes(listall);
166         }
167       }
168     } else {
169       const listall = nodes.concat(node);
170       listall.forEach((item) => {
171         item.data.loading = false;
172       });
173       setEdges(edges.concat(edge));
174       setNodes(listall);
175     }
176     Array.from(document.getElementsByClassName("ant-spin-dot-holder")).forEach(
177       (item) => {
178         item.style.display = "none";
179       }
180     );
181     Array.from(document.getElementsByClassName("loadingTitle")).forEach(
182       (item) => {
183         item.style.display = "none";
184       }
185     );
186   };
187 
188   // 點選節點,將節點初始配置傳入nodes
189   const onNodeClick = (e, node) => {
190     if (node.flags) {
191       if (node.data.select) {
192         nodes.forEach((item) => {
193           if (item.id === node.id) {
194             item.data.loading = true;
195           }
196         });
197         setNodes(nodes);
198         const obj = node;
199         obj.level = node.data.level;
200         obj.select = node.data.select;
201         delete node.data["select"];
202         delete node.data["weekly_template"];
203         204         axios({
205           url: `/getNodes`,
206           method: "post",
207           data: obj,
208         })
209           .then((res) => {
210             if (res.data) {
211               findAllData(res.data.nodes, res.data.edges);
212             }
213           })
214           .finally(() => {});
215       }
216     }
217   };
218 
219   return (
220     <ReactFlow
221       nodes={nodes}
222       edges={edges}
223       nodeTypes={nodeTypes}
224       edgeTypes={edgeTypes}
225       onNodeClick={onNodeClick}
226       onNodesChange={onNodesChange}
227       onNodesDelete={onNodesDelete}
228       onEdgesChange={onEdgesChange}
229       defaultEdgeOptions={defaultEdgeOptions} 
230     >
231       <Controls />
232       <MiniMap />
233       <svg>
234         <defs>
235           <linearGradient id="edge-gradient">
236             <stop offset="0%" stopColor="#ae53ba" />
237             <stop offset="100%" stopColor="#2a8af6" />
238           </linearGradient>
239 
240           <marker
241             id="edge-circle"
242             viewBox="-5 -5 10 10"
243             refX="0"
244             refY="0"
245             markerUnits="strokeWidth"
246             markerWidth="10"
247             markerHeight="10"
248             orient="auto"
249           >
250             <circle stroke="#2a8af6" strokeOpacity="0.75" r="2" cx="0" cy="0" />
251           </marker>
252         </defs>
253       </svg> 
254     </ReactFlow>
255   );
256 };
257  
258 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 檔案 自定義線
 1 import React from 'react';
 2 import {  getBezierPath } from 'reactflow';
 3 
 4 export default function CustomEdge({
 5   id,
 6   sourceX,
 7   sourceY,
 8   targetX,
 9   targetY,
10   sourcePosition,
11   targetPosition,
12   style = {},
13   markerEnd,
14 }) {
15   const xEqual = sourceX === targetX;
16   const yEqual = sourceY === targetY;
17 
18   const [edgePath] = getBezierPath({
19     // we need this little hack in order to display the gradient for a straight line
20     sourceX: xEqual ? sourceX + 0.0001 : sourceX,
21     sourceY: yEqual ? sourceY + 0.0001 : sourceY,
22     sourcePosition,
23     targetX,
24     targetY,
25     targetPosition,
26   });
27 
28   return (
29     <>
30       <path
31         id={id}
32         style={style}
33         className="react-flow__edge-path"
34         d={edgePath}
35         markerEnd={markerEnd}
36       />
37     </>
38   );
39 }

overflow.css

  1 /* eslint-disable */
  2 .react-flow__node {
  3     display: flex;
  4     width: 220px;
  5     height: auto;
  6     border-radius: var(--node-border-radius);
  7     box-shadow: var(--node-box-shadow);
  8     letter-spacing: -.2px;
  9     font-weight: 500;
 10     font-family: 'Fira Mono', Monospace;
 11 }
 12 
 13 .ResicabelNode {
 14     position: relative;
 15     display: flex;
 16     overflow: hidden;
 17     flex-grow: 1;
 18     padding: 2px;
 19     width: 100%;
 20     height: 100%;
 21     border-radius: var(--node-border-radius);
 22 }
 23 
 24 .box {
 25     display: 'flex';
 26     align-items: 'center';
 27     justify-content: 'center';
 28 }
 29 
 30 .gradient::before {
 31     position: absolute;
 32     top: 50%;
 33     left: 50%;
 34     padding-bottom: calc(100% * 1.41421356237);
 35     width: calc(100% * 1.41421356237);
 36     border-radius: 100%;
 37     background: conic-gradient(from -160deg at 50% 50%, #e92a67 0deg, #a853ba 120deg, #2a8af6 240deg, #e92a67 360deg);
 38     content: '';
 39     transform: translate(-50%, -50%);
 40 }
 41 
 42 .wrapper.gradient::before {
 43     z-index: -1;
 44     background: conic-gradient(from -160deg at 50% 50%, #e92a67 0deg, #a853ba 120deg, #2a8af6 240deg, rgba(42, 138, 246, 0) 360deg);
 45     content: '';
 46     transform: translate(-50%, -50%) rotate(0deg);
 47     animation: spinner 4s linear infinite;
 48 }
 49 
 50 @keyframes spinner {
 51     100% {
 52         transform: translate(-50%, -50%) rotate(-360deg);
 53     }
 54 }
 55 
 56 .inner {
 57     position: relative;
 58     display: flex;
 59     flex-direction: column;
 60     flex-grow: 1;
 61     justify-content: center;
 62     padding: 0px 9px;
 63     border-radius: var(--node-border-radius);
 64     background: var(--bg-color);
 65 }
 66 
 67 .cloud {
 68     position: absolute;
 69     top: 0;
 70     right: 0;
 71     z-index: 1;
 72     display: flex;
 73     overflow: hidden;
 74     padding: 2px;
 75     width: 30px;
 76     height: 30px;
 77     border-radius: 100%;
 78     box-shadow: var(--node-box-shadow);
 79     transform: translate(50%, -50%);
 80     transform-origin: center center;
 81 }
 82 
 83 .nodelabel {
 84     margin-bottom: 2px;
 85     /* width:120px; */
 86     font-size: 12px;
 87     line-height: 1;
 88 }
 89 
 90 .subline {
 91     margin-top: -6px;
 92     color: #777;
 93     font-size: 10px;
 94 }
 95 
 96 .circle {
 97     width: 2em;
 98     height: 2em;
 99     border-radius: 50%;
100     background: #ebf2fd;
101     box-shadow: .1em .125em 0 0 rgb(15 28 63 / 13%);
102     text-align: center;
103     font-weight: bold;
104     line-height: 2em;
105 }
106 
107 .popcon_btn {
108     margin: 10px 0;
109 }
110 
111 .ant-popconfirm-message-icon {
112     display: none;
113 }
114 
115 .ant-popover-buttons {
116     display: none;
117 }
118 
119 .ant-popover-inner-content {
120     padding: 7px 10px;
121 }
122 
123 .ant-popconfirm-buttons {
124     display: none;
125 }
126 
127 .react-flow {
128     background-color: var(--bg-color);
129     color: var(--text-color);
130     --bg-color: rgb(17, 17, 17);
131     --text-color: rgb(243, 244, 246);
132     --node-border-radius: 10px;
133     --node-box-shadow:
134         10px 0 15px rgba(42, 138, 246, .3),
135         -10px 0 15px rgba(233, 42, 103, .3);
136 }
137 
138 .react-flow__node-turbo {
139     display: flex;
140     min-width: 150px;
141     height: 70px;
142     border-radius: var(--node-border-radius);
143     box-shadow: var(--node-box-shadow);
144     letter-spacing: -.2px;
145     font-weight: 500;
146     font-family: 'Fira Mono', Monospace;
147 }
148 
149 .react-flow__node-turbo .wrapper {
150     position: relative;
151     display: flex;
152     overflow: hidden;
153     flex-grow: 1;
154     padding: 2px;
155     border-radius: var(--node-border-radius);
156 }
157 
158 /* .react-flow__node-turbo.selected .wrapper.gradient::before {
159     z-index: -1;
160     background:
161     conic-gradient(from -160deg at 50% 50%, #e92a67 0deg, #a853ba 120deg, #2a8af6 240deg, rgba(42, 138, 246, 0)360deg);
162     content: '';
163     transform: translate(-50%, -50%) rotate(0deg);
164     animation: spinner 4s linear infinite;
165 } */
166 
167 @keyframes spinner {
168     100% {
169         transform: translate(-50%, -50%) rotate(-360deg);
170     }
171 }
172 
173 .react-flow__node-turbo .inner {
174     position: relative;
175     display: flex;
176     flex-direction: column;
177     flex-grow: 1;
178     justify-content: center;
179     padding: 16px 20px;
180     border-radius: var(--node-border-radius);
181     background: var(--bg-color);
182 }
183 
184 .react-flow__node-turbo .icon {
185     margin-right: 8px;
186 }
187 
188 .react-flow__node-turbo .title {
189     margin-bottom: 2px;
190     font-size: 16px;
191     line-height: 1;
192 }
193 
194 .react-flow__node-turbo .subline {
195     color: #777;
196     font-size: 12px;
197 }
198 
199 .react-flow__node-turbo .cloud {
200     position: absolute;
201     top: 0;
202     right: 0;
203     z-index: 1;
204     display: flex;
205     overflow: hidden;
206     padding: 2px;
207     width: 30px;
208     height: 30px;
209     border-radius: 100%;
210     box-shadow: var(--node-box-shadow);
211     transform: translate(50%, -50%);
212     transform-origin: center center;
213 }
214 
215 .react-flow__node-turbo .cloud div {
216     position: relative;
217     display: flex;
218     align-items: center;
219     flex-grow: 1;
220     justify-content: center;
221     border-radius: 100%;
222     background-color: var(--bg-color);
223 }
224 
225 .react-flow__handle {
226     opacity: 0;
227 }
228 
229 .react-flow__handle.source {
230     right: -10px;
231 }
232 
233 .react-flow__handle.target {
234     left: -10px;
235 }
236 
237 .react-flow__node:focus {
238     outline: none;
239 }
240 
241 .react-flow__edge .react-flow__edge-path {
242     stroke: url(#edge-gradient);
243     stroke-width: 2;
244     stroke-opacity: .75;
245 }
246 
247 .react-flow__controls button {
248     border: 1px solid #95679e;
249     border-bottom: none;
250     background-color: var(--bg-color);
251     color: var(--text-color);
252 }
253 
254 .react-flow__controls button:hover {
255     background-color: rgb(37, 37, 37);
256 }
257 
258 .react-flow__controls button:first-child {
259     border-radius: 5px 5px 0 0;
260 }
261 
262 .react-flow__controls button:last-child {
263     border-bottom: 1px solid #95679e;
264     border-radius: 0 0 5px 5px;
265 }
266 
267 .react-flow__controls button path {
268     fill: var(--text-color);
269 }
270 
271 .react-flow__attribution {
272     background: rgba(200, 200, 200, .2);
273     display: none;
274 }
275 
276 .react-flow__attribution a {
277     color: #95679e;
278 }
279 
280 .loadingbox {
281     display: flex;
282     justify-content: space-evenly;
283     align-items: center;
284 }
285 /* eslint-enable */

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;
}

有問題歡迎到評論區探討 一起學習^_^

相關文章