檔案中需要下載的元件:
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; }
有問題歡迎到評論區探討 一起學習^_^