文件中需要下载的组件:
npm install reactflow (我的版本是npm install [email protected]) 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; }
标签:node,const,flow,react,2.0,data,id From: https://www.cnblogs.com/qing1224/p/18321410