LogicFlow,画布流程设计方案
### 画布流程设计方案
#### 本次使用的是LogicFlow, 那LogicFlow 是什么?
LogicFlow 是一款流程图编辑框架,提供了一系列流程图交互、编辑所必需的功能和灵活的节点自定义、插件等拓展机制。LogicFlow 支持前端研发自定义开发各种逻辑编排场景,如流程图、ER 图、BPMN 流程等。在工作审批配置、机器人逻辑编排、无代码平台流程配置都有较好的应用。
### 创建一个实例
import LogicFlow from "@logicflow/core"; import "@logicflow/core/dist/style/index.css"; import { useEffect, useRef } from "react"; export default function App() { const refContainer = useRef(); useEffect(() => { const logicflow = new LogicFlow({ container: refContainer.current, grid: true, width: 1000, height: 500, }); logicflow.render(); }, []); return <div className="App" ref={refContainer}></div>; } ### LogicFlow 的图数据 const graphData = { nodes: [ { id: "node_id_1", type: "rect", x: 100, y: 100, text: { x: 100, y: 100, value: "节点1" }, properties: {}, }, { id: "node_id_2", type: "circle", x: 200, y: 300, text: { x: 300, y: 300, value: "节点2" }, properties: {}, }, ], edges: [ { id: "edge_id", type: "polyline", sourceNodeId: "node_id_1", targetNodeId: "node_id_2", text: { x: 139, y: 200, value: "连线" }, startPoint: { x: 100, y: 140 }, endPoint: { x: 200, y: 250 }, pointsList: [ { x: 100, y: 140 }, { x: 100, y: 200 }, { x: 200, y: 200 }, { x: 200, y: 250 }, ], properties: {}, }, ], }; ### 将图数据渲染到画布上 lf.render(graphData);
### 认识 LogicFlow 的基础节点
LogicFlow 是基于 svg 做的流程图编辑框架,所以我们的节点和连线都是 svg 基本形状,对 LogicFlow 节点样式的修改,也就是对 svg 基本形状的修改。LogicFlow 内部存在 7 种基础节点,分别为:
矩形:rect
圆形: circle
椭圆: ellipse
多边形: polygon
菱形: diamond
文本: text
HTML: html
### 基于继承的自定义节点
LogicFlow 的基础节点是比较简单的,但是在业务中对节点外观要求可能有各种情况。LogicFlow 提供了非常强大的自定义节点功能,可以支持开发者自定义各种节点。
注意LogicFlow 推荐在实际应用场景中,所有的节点都使用自定义节点,将节点的 type 定义为符合项目业务意义的名称。而不是使用圆形、矩形这种仅表示外观的节点。
LogicFlow 是基于继承来实现自定义节点、边。开发者可以继承 LogicFlow 内置的节点,然后利用面向对象的重写机制。重写节点样式相关的方法,来达到自定义节点样式的效果。
### 二次自定义
import { RectResize } from "@logicflow/extension";
class CustomNodeModel extends RectResize.model {}
class CustomNode extends RectResize.view {}
### 自定义一个业务节点
我们以定义一个如下图所示的用户任务节点为例,来实现一个基于内置矩形节点的自定义节点。
### 1. 定义节点并注册
// UserTaskNode.js import { RectNode, RectNodeModel } from "@logicflow/core"; class UserTaskModel extends RectNodeModel {} class UserTaskView extends RectNode {} export default { type: "UserTask", view: UserTaskView, model: UserTaskModel, }; // main.js import UserTask from "./UserTaskNode.js"; const lf = new LogicFlow({ container: document.querySelector("#container"), }); lf.register(UserTask); lf.render({ nodes: [ { type: "UserTask", x: 100, y: 100, }, ], });
从上面的代码,可以看到,在自定义一个节点的时候,我们需要定义节点的model和view。这是因为由于 LogicFlow 基于 MVVM 模式,所有自定义节点和连线的时候,我们需要自定义view和model。大多数情况下,需要通过重写定义model上获取样式相关的方法和重写view上的getShape来定义更复杂的节点外观。
### 2. 自定义节点 model
#### 自定义节点的样式属性
1. getNodeStyle:支持重写,自定义节点样式属性 class UserTaskModel extends RectNodeModel { getNodeStyle() { const style = super.getNodeStyle(); style.stroke = "blue"; style.strokeDasharray = "3 3"; return style; } } 2. getTextStyle:支持重写,自定义节点文本样式属性 3. getAnchorStyle:支持重写,自定义节点锚点样式属性 4. getAnchorLineStyle:支持重写,自定义节点锚点拖出连接线的样式属性 5. getOutlineStyle:支持重写,自定义节点轮廓框的样式属性 6. initNodeData: 支持重写,初始化节点数据,将传入的图数据(data)转换为节点属性, 所以需要调用super.initNodeData触发转换方法。 在super.initNodeData之前,对图数据进行处理。 在super.initNodeData之后,对节点属性进行初始化。 class UserTaskModel extends RectResize.model { initNodeData(data) { // 可以在super之前,强制设置节点文本位置不居中,而且在节点下面 if (!data.text || typeof data.text === "string") { data.text = { value: data.text || "", x: data.x, y: data.y + 40, }; } super.initNodeData(data); this.width = 100; this.height = 80; } } 提示initNodeData 和 setAttributes 都可以对 nodeModel 的属性进行赋值,但是两者的区别在于: initNodeData只在节点初始化的时候调用,用于初始化节点的属性。 setAttributes除了初始化调用外,还会在 properties 发生变化了调用。 7. setAttributes: 设置 model 形状属性,每次 properties 发生变化会触发 class UserTaskModel extends RectNodeModel { setAttributes() { const size = this.properties.scale || 1; this.width = 100 * size; this.height = 80 * size; } } 8. getData:(不支持重写此方法)获取被保存时返回的数据。LogicFlow 有固定节点数据格式。如果期望在保存数据上添加数据,请添加到 properties 上。 const nodeModel = lf.getNodeModelById("node_1"); const nodeData = nodeModel.getData(); 9. getProperties: (不支持重写此方法)获取节点属性 const nodeModel = lf.getNodeModelById("node_1"); const properties = nodeModel.getProperties(); 10. updateText: 修改节点文本内容 const nodeModel = lf.getNodeModelById("node_1"); nodeModel.updateText("hello world"); 11. setProperties: 设置节点 properties lf.on("node:click", ({ data }) => { lf.getNodeModelById(data.id).setProperties({ disabled: !data.properties.disabled, scale: 2, }); }); 12. deleteProperty: 删除节点的某个属性 lf.on("node:click", ({ data }) => { lf.getNodeModelById(data.id).deleteProperty("disabled"); lf.getNodeModelById(data.id).deleteProperty("scale"); });
#### 自定义节点的形状属性
class customRectModel extends RectNodeModel { initNodeData(data) { super.initNodeData(data); this.width = 200; this.height = 80; this.radius = 50; } }
#### 基于 properties 属性自定义节点样式
class UserTaskModel extends RectNodeModel { initNodeData(data) { super.initNodeData(data); this.width = 80; this.height = 60; this.radius = 5; } getNodeStyle() { const style = super.getNodeStyle(); const properties = this.properties; if (properties.statu === "pass") { style.stroke = "green"; } else if (properties.statu === "reject") { style.stroke = "red"; } else { style.stroke = "rgb(24, 125, 255)"; } return style; } }
### 3. 自定义节点 view
LogicFlow 在自定义节点的model时,可以定义节点的基础形状、样式等属性。但是当开发者需要一个更加复杂的节点时,可以使用 LogicFlow 提供的自定义节点view的方式。
class UserTaskView extends RectNode { private getLabelShape() { const { model } = this.props; const { x, y, width, height } = model; const style = model.getNodeStyle(); return h( "svg", { x: x - width / 2 + 5, y: y - height / 2 + 5, width: 25, height: 25, viewBox: "0 0 1274 1024" }, h("path", { fill: style.stroke, d: "M655.807326 287.35973m-223.989415 0a218.879 218.879 0 1 0 447.978829 0 218.879 218.879 0 1 0-447.978829 0ZM1039.955839 895.482975c-0.490184-212.177424-172.287821-384.030443-384.148513-384.030443-211.862739 0-383.660376 171.85302-384.15056 384.030443L1039.955839 895.482975z" }) ); } /** * 完全自定义节点外观方法 */ getShape() { const { model, graphModel } = this.props; const { x, y, width, height, radius } = model; const style = model.getNodeStyle(); return h("g", {}, [ h("rect", { ...style, x: x - width / 2, y: y - height / 2, rx: radius, ry: radius, width, height }), this.getLabelShape() ]); } }
### h 函数
h方法是 LogicFlow 对外暴露的渲染函数,其用法与react、vue的createElement一致。但是这里我们需要创建的是svg标签,所以需要有一定的 svg 基础知识。但是大多数情况下,我们不会涉及太复杂的知识,只是简单的矩形、圆形、多边形这种。
h(nodeName, attributes, [...children]) // <text x="100" y="100">文本内容</text> h('text', { x: 100, y: 100 }, ['文本内容']) /** * <g> * <rect x="100" y="100" stroke="#000000" strokeDasharray="3 3"></rect> * <text x="100" y="100">文本内容</text> * </g> */ <!-- 外部的 h('g', {}, [...]) 表示一个 SVG <g> 元素,用于对其内部的元素进行分组。 --> h('g',{}, [ h('rect', { x: 100, y: 100, stroke: "#000000", strokeDasharray="3 3"}), h('text', { x: 100, y: 100 }, ['文本内容']) ])
### getAnchorPoints
此方法作用就是定义锚点,锚点是用于连接线条的点,可以是多个,也可以是一个,返回的是一个数组,数组中的每一项都是一个对象,对象中包含 x 和 y 属性,分别表示锚点的 x 和 y 坐标。
### getShape
此方法作用就是定义最终渲染的图形, LogicFlow 内部会将其返回的内容插入到 svg DOM 上。
在 LogicFlow 所有的基础节点中,model里面的x,y都是统一表示中心点。但是getShape方法给我们提供直接生成 svg dom 的方式,在 svg 中, 对图形位置的控制则存在差异:
const { x, y, width, height, radius } = this.props.model; // svg dom <rect x="100" y="100" width="100" height="80"> h("rect", { ...style, x: x - width / 2, y: y - height / 2, rx: radius, // 注意这里是rx而不是radius ry: radius, width, height }),
自定义矩形的 view 时 radius 设置在model中,radius是矩形节点的形状属性。但是在自定义view时需要注意,svg 里面设置矩形的圆角并不是用radius,而是使用rx, ry。所以在自定义view的矩形时,需要将 model 中radius的赋值给rx和ry,否则圆角将不生效。
### props
LogicFlow 是基于preact开发的,我们自定义节点 view 的时候,可以通过this.props获取父组件传递过来的数据。this.props对象包含两个属性,分别为:
model: 表示自定义节点的 model
graphModel: 表示 logicflow 整个图的 model
### 自定义连接规则校验
在某些时候,我们可能需要控制边的连接方式,比如开始节点不能被其它节点连接、结束节点不能连接其他节点、用户节点后面必须是判断节点等,要想达到这种效果,我们需要为节点设置以下两个属性。
1. sourceRules - 当节点作为边的起始节点(source)时的校验规则
2. targetRules - 当节点作为边的目标节点(target)时的校验规则
以正方形(square)为例,在边时我们希望它的下一节点只能是圆形节点(circle),那么我们应该给square添加作为source节点的校验规则。
import { RectNode, RectNodeModel } from "@logicflow/core"; class SquareModel extends RectNodeModel { initNodeData(data) { super.initNodeData(data); const circleOnlyAsTarget = { message: "正方形节点下一个节点只能是圆形节点", validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { return targetNode.type === "circle"; }, }; this.sourceRules.push(circleOnlyAsTarget); } }
在上例中,我们为model的sourceRules属性添加了一条校验规则,校验规则是一个对象,我们需要为其提供messgage和validate属性。
message属性是当不满足校验规则时所抛出的错误信息,validate则是传入规则检验的回调函数。validate方法有四个参数,分别为边的起始节点(source)、目标节点(target)、起始锚点(sourceAnchor)、 目标锚点(targetAnchor),我们可以根据参数信息来决定是否通过校验,其返回值是一个布尔值。
3. moveRules: 限制节点移动, 在graphModel中支持添加全局移动规则,例如在移动 A 节点的时候,期望把 B 节点也一起移动了。
lf.graphModel.addNodeMoveRules((model, deltaX, deltaY) => { // 如果移动的是分组,那么分组的子节点也跟着移动。 if (model.isGroup && model.children) { lf.graphModel.moveNodes(model.children, deltaX, deltaY, true); } return true; });
### 自定义节点的锚点
对于各种基础类型节点,我们都内置了默认锚点。LogicFlow 支持通过重写获取锚点的方法来实现自定义节点的锚点。
import { RectNode, RectNodeModel } from "@logicflow/core"; class SquareModel extends RectNodeModel { initNodeData(data) { super.initNodeData(data); const rule = { message: "只允许从右边的锚点连出", validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { return sourceAnchor.name === "right"; }, }; this.sourceRules.push(rule); } getAnchorStyle(anchorInfo) { const style = super.getAnchorStyle(anchorInfo); if (anchorInfo.type === "left") { style.fill = "red"; style.hover.fill = "transparent"; style.hover.stroke = "transpanrent"; style.className = "lf-hide-default"; } else { style.fill = "green"; } return style; } getDefaultAnchor() { const { width, height, x, y, id } = this; return [ { x: x - width / 2, y, type: "left", edgeAddable: false, // 控制锚点是否可以从此锚点手动创建连线。默认为true。 id: `${id}_0`, }, { x: x + width / 2, y, type: "right", id: `${id}_1`, }, ]; } } class SqlNode extends RectNode { /** * 1.1.7版本后支持在view中重写锚点形状。 * 重写锚点新增 */ getAnchorShape(anchorData) { const { x, y, type } = anchorData; return h("rect", { x: x - 5, y: y - 5, width: 10, height: 10, className: `custom-anchor ${ type === "left" ? "incomming-anchor" : "outgoing-anchor" }` }); } }
### 自定义 HTML 节点
LogicFlow 内置了基础的 HTML 节点和其他基础节点不一样,我们可以利用 LogicFlow 的自定义机制,实现各种形态的 HTML 节点,而且 HTML 节点内部可以使用任意框架进行渲染。
class UmlModel extends HtmlNodeModel { setAttributes() { this.text.editable = false; // 禁止节点文本编辑 // 设置节点宽高和锚点 const width = 200; const height = 130; this.width = width; this.height = height; this.anchorsOffset = [ [width / 2, 0], [0, height / 2], [-width / 2, 0], [0, -height / 2], ]; } } class UmlNode extends HtmlNode { currentProperties: string; setHtml(rootEl: HTMLElement) { const { properties } = this.props.model; const el = document.createElement("div"); el.className = "uml-wrapper"; const html = ` <div> <div class="uml-head">Head</div> <div class="uml-body"> <div>+ ${properties.name}</div> <div>+ ${properties.body}</div> </div> <div class="uml-footer"> <div>+ setHead(Head $head)</div> <div>+ setBody(Body $body)</div> </div> </div> `; el.innerHTML = html; // 需要先把之前渲染的子节点清除掉。 rootEl.innerHTML = ""; rootEl.appendChild(el); } }
#### 使用 react 编写 html 节点
import { HtmlNodeModel, HtmlNode } from "@logicflow/core"; import React from "react"; import ReactDOM from "react-dom"; import "./uml.css"; function Hello(props) { return ( <> <h1 className="box-title">title</h1> <div className="box-content"> <p>{props.name}</p> <p>{props.body}</p> <p>content3</p> </div> </> ); } class BoxxModel extends HtmlNodeModel { setAttributes() { this.text.editable = false; const width = 200; const height = 116; this.width = width; this.height = height; this.anchorsOffset = [ [width / 2, 0], [0, height / 2], [-width / 2, 0], [0, -height / 2], ]; } } class BoxxNode extends HtmlNode { setHtml(rootEl: HTMLElement) { const { properties } = this.props.model; ReactDOM.render( <Hello name={properties.name} body={properties.body} />, rootEl ); } } const boxx = { type: "boxx", view: BoxxNode, model: BoxxModel, }; export default boxx; // page.jsx import box from './box.tsx'; export default function PageIndex() { useEffect(() => { const lf = new LogicFlow({ ...config, container: document.querySelector('#graph_html') as HTMLElement }); lf.register(box); lf.render({ nodes: [ { id: 11, type: 'boxx', x: 350, y: 100, properties: { name: 'turbo', body: 'hello' } }, ] }); lf.on('node:click', ({ data}) => { lf.setProperties(data.id, { name: 'turbo', body: Math.random() }) }); }, []); return ( <> <div id="graph_html" className="viewport" /> </> ) }
### 边 Edge
和节点一样,LogicFlow 也内置一些基础的边。LogicFlow 的内置边包括:
1. 直线(line)
2. 直角折线(polyline)
3. 贝塞尔曲线(bezier)
#### 基于 React 组件自定义边
使用以下方法可以基于 React 组件自定义边,你可以在边上添加任何你想要的 React 组件,甚至将原有的边通过样式隐藏,使用 React 重新绘制
import React from "react"; import ReactDOM from "react-dom"; import { BaseEdgeModel, LineEdge, h } from "@logicflow/core"; const DEFAULT_WIDTH = 48; const DEFAULT_HEIGHT = 32; class CustomEdgeModel extends BaseEdgeModel { getEdgeStyle() { const edgeStyle = super.getEdgeStyle(); //可以自己设置线的显示样式,甚至隐藏掉原本的线,自己用react绘制 edgeStyle.strokeDasharray = "4 4"; edgeStyle.stroke = "#DDDFE3"; return edgeStyle; } } const CustomLine: React.FC = () => { return <div className="custom-edge">aaa</div>; }; class CustomEdgeView extends LineEdge { getEdge() { const { model } = this.props; const { customWidth = DEFAULT_WIDTH, customHeight = DEFAULT_HEIGHT } = model.getProperties(); const id = model.id; const edgeStyle = model.getEdgeStyle(); const { startPoint, endPoint, arrowConfig } = model; const lineData = { x1: startPoint.x, y1: startPoint.y, x2: endPoint.x, y2: endPoint.y, }; const positionData = { x: (startPoint.x + endPoint.x - customWidth) / 2, y: (startPoint.y + endPoint.y - customHeight) / 2, width: customWidth, height: customHeight, }; const wrapperStyle = { width: customWidth, height: customHeight, }; setTimeout(() => { ReactDOM.render(<CustomLine />, document.querySelector("#" + id)); }, 0); return h("g", {}, [ h("line", { ...lineData, ...edgeStyle, ...arrowConfig }), h("foreignObject", { ...positionData }, [ h("div", { id, style: wrapperStyle, className: "lf-custom-edge-wrapper", }), ]), ]); } getAppend() { return h("g", {}, []); } } export default { type: "CustomEdge", view: CustomEdgeView, model: CustomEdgeModel, };
### 主题 Theme
LogicFlow 提供了设置主题的方法,便于用户统一设置其内部所有元素的样式。
设置方式有两种:
初始化LogicFlow时作为配置传入
初始化后,调用LogicFlow的 setTheme 方法
// 方法1:new LogicFlow时作为配置传入 const config = { domId: 'app', width: 1000, height: 800, style: { // 设置默认主题样式 rect: { // 矩形样式 ... }, circle: { // 圆形样式 ... }, nodeText: { // 节点文本样式 ... }, edgeText: { // 边文本样式 ... }, anchor: { // 锚点样式 ... } ... } } const lf = new LogicFlow(config); // 方法2: 调用LogicFlow的setTheme方法 lf.setTheme({ // 设置默认主题样式 rect: { // 矩形样式 ... }, circle: { // 圆形样式 ... }, nodeText: { // 节点文本样式 ... }, edgeText: { // 边文本样式 ... }, anchor: { // 锚点样式 ... } ... })
### 事件 Event
监听事件
<!-- lf实例上提供on方法支持监听事件。 --> lf.on("node:dnd-add", (data) => {}); <!-- LogicFlow 支持用逗号分割事件名。 --> lf.on("node:click,edge:click", (data) => {});
自定义事件
除了 lf 上支持的监听事件外,还可以使用eventCenter对象来监听和触发事件。eventCenter是一个graphModel上的一个属性。所以在自定义节点的时候,我们可以使用eventCenter触发自定义事件。
class ButtonNode extends HtmlNode { setHtml(rootEl) { const { properties } = this.props.model; const el = document.createElement("div"); el.className = "uml-wrapper"; const html = ` <div> <div class="uml-head">Head</div> <div class="uml-body"> <div><button onclick="setData()">+</button> ${properties.name}</div> <div>${properties.body}</div> </div> <div class="uml-footer"> <div>setHead(Head $head)</div> <div>setBody(Body $body)</div> </div> </div> `; el.innerHTML = html; rootEl.innerHTML = ""; rootEl.appendChild(el); window.setData = () => { const { graphModel, model } = this.props; graphModel.eventCenter.emit("custom:button-click", model); }; } }
### 拖拽创建节点 Dnd
拖拽需要结合图形面板来实现,步骤:创建面板 → 拖拽初始化 → 监听 drop 事件创建节点
lf.dnd.startDrag({
type,
text: `${type}节点`,
});
通过上面的代码可以看出,将节点通过div标签+css样式的方式绘制到面板中,并为其绑定onMouseDown事件,当拖拽图形时,会触发lf.dnd.startDrag函数,表示开始拖拽,并传入选中图形的配置,startDrag入参格式:
lf.dnd.startDrag(nodeConfig: NodeConfig):void type NodeConfig = { id?: string; // 不建议直接传id, logicflow id不允许重复 type: string; text?: TextConfig; properties?: Record<string, unknown>; };
拖拽结束鼠标松开时,将当前鼠标的位置转换为画布上的坐标,并以此为节点的中心点坐标x、y,合并拖拽节点传入的nodeConfig,监听到 drop 事件后会调用lf.addNode方法创建节点。
注意: 如果是用图片作为配置面板中添加节点的元素,需要将其设置为不可拖动的,解决办法参考下面
1. 如果是用图片作为触发元素,可以考虑设置dragable为false或者将其作为背景图。
2. 如果触发元素被a标签包裹,获取其它可以拖动的元素。这个时候可以设置user-select: none。
注意 如果遇到拖拽添加节点报错“不存在 id 为 xx 的节点”,需要在 mousedown 时触发dnd.startDrag。
1. 给图片添加point-events:none属性
2. 使用背景图来显示图标
3. 使用dndPanel拖拽组件来实现拖拽面板。http://logic-flow.org/guide/extension/component-dnd-panel.html#%E5%90%AF%E7%94%A8
### 对齐线 Snapline
对齐线能够在节点移动过程中,将移动节点的位置与画布中其他节点位置进行对比,辅助位置调整。位置对比有如下两个方面。
1. 节点中心位置
2. 节点的边框
对齐线使用
普通编辑模式下,默认开启对齐线,也可通过配置进行关闭。
在静默模式下,无法移动节点,所以关闭了对齐线功能,无法通过配置开启。
// 关闭对齐线功能 const lf = new LogicFlow({ snapline: false, });
对齐线样式设置
对齐线的样式包括颜色和宽度,可以通过设置主题的方式进行修改。
// 默认配置 { stroke: '#1E90FF', strokeWidth: 1, } // 修改对齐线样式 lf.setTheme({ snapline: { stroke: '#1E90FF', // 对齐线颜色 strokeWidth: 1, // 对齐线宽度 }, })
### 参考文献:
文档地址: https://site.logic-flow.cn/docs/#/zh/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理