logicflow 基础操作
一、 register(): 注册图标方法
1、节点model和view
model: 数据层,包含节点各种样式(边框、颜色)、形状(宽高、顶点位置)、业务属性等。
view: 视图层,控制节点的最终渲染效果,通过改变model就可以满足自定义节点,同时可以在view 上定制更加复杂的svg元素。
LogicFlow基于MVVM模式实现的,在自定义一个节点的时候,我们可以重新定义节点的model和view ,通过重写定义在model上获取样式相关的方法和重写view上的getShape来定义复杂的节点外观。
这是一个基于继承内置节点并重写model的自定义节点例子👇,节点自定义采用了不同的方式实现😊。
1.1 自定义注册图标的源码如下:
/**
* 注册元素(节点 or 边)
* @param config 注册元素的配置项
* @private
*/
private registerElement(config: RegisterConfig) {
let ViewComp = config.view
if (config.isObserverView !== false && !ViewComp.isObserved) {
ViewComp.isObserved = true
ViewComp = observer(ViewComp)
}
this.setView(config.type, ViewComp)
this.graphModel.setModel(config.type, config.model)
}
注册图形:根据源码注册的自定义图形需要有三个参数:
params:{type,view,model}
const registerElements = (lf: LogicFlow) => {
const elements = [
combine,
square,
circleStart
]
map(elements, (customElement) => {
lf.register(customElement)
})
}
说明:每个扩展的节点形状格式
例如square:
import { BaseNodeModel, h, RectNode, RectNodeModel } from '@logicflow/core'
export class SquareModel extends RectNodeModel {
setAttributes() {
const size = 80
const circleOnlyAsTarget = {
message: '正方形节点下一个节点只能是圆形节点',
validate: (source?: BaseNodeModel, target?: BaseNodeModel | any) => {
return target?.type === 'circle' // 确认上面 target 类型定义
}
}
this.width = size
this.height = size
this.anchorsOffset = [
[size / 2, 0],
[-size / 2, 0]
]
this.sourceRules.push(circleOnlyAsTarget)
}
}
export class SquareView extends RectNode {
getTextStyle() {
const { model } = this.props
const style = model.getTextStyle()
const {
model: { properties = {} }
} = this.props
if (properties.isUsed) {
style.color = 'red'
}
return style
}
// getShape 的返回值是一个通过 h 方法创建的 svg 元素
getShape() {
const { x, y, width, height } = this.props.model
const { fill, stroke, strokeWidth } = this.props.model.getNodeStyle()
const attrs = {
x: x - width / 2,
y: y - height / 2,
width,
height,
stroke,
fill,
strokeWidth
}
// 使用 h 方法创建一个矩形
return h('g', {}, [
h('rect', { ...attrs }),
h(
'svg',
{
x: x - width / 2 + 5,
y: y - height / 2 + 5,
width: 25,
height: 25,
viewBox: '0 0 1274 1024'
},
h('path', {
fill: 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'
})
)
])
}
}
export default {
type: 'square',
view: SquareView,
model: SquareModel
}
例如combine:
import { h, BaseNode, BaseNodeModel } from '@logicflow/core'
export class CombineNode extends BaseNode {
getShape() {
const { x, y } = this.props.model
const { fill } = this.props.model.getNodeStyle()
return h(
'g',
{
transform: `matrix(1 0 0 1 ${x - 25} ${y - 25})`
},
h('path', {
d: 'm 0,6.65 l 0,36.885245901639344 c 1.639344262295082,8.196721311475411 47.540983606557376,8.196721311475411 49.18032786885246,0 l 0,-36.885245901639344 c -1.639344262295082,-8.196721311475411 -47.540983606557376,-8.196721311475411 -49.18032786885246,0c 1.639344262295082,8.196721311475411 47.540983606557376,8.196721311475411 49.18032786885246,0 m -49.18032786885246,5.737704918032787c 1.639344262295082,8.196721311475411 47.540983606557376,8.196721311475411 49.18032786885246,0m -49.18032786885246,5.737704918032787c 1.639344262295082,8.196721311475411 47.540983606557376,8.196721311475411 49.18032786885246,0',
fill: fill,
strokeWidth: 2,
stroke: 'red',
fillOpacity: 0.95
})
)
}
}
export class CombineModel extends BaseNodeModel {
setAttributes() {
this.width = 50
this.height = 60
this.fill = 'orange'
this.anchorsOffset = [
[0, -this.height / 2],
[this.width / 2, 0],
[0, this.height / 2],
[-this.width / 2, 0]
]
}
}
export default {
type: 'combine',
view: CombineNode,
model: CombineModel
}
例如circleStart:
import { h, CircleNode, CircleNodeModel } from '@logicflow/core'
export class StartNode extends CircleNode {
getLabelShape() {
const { model } = this.props
const {
x,
y
} = model
return h(
'text',
{
fill: '#000000',
fontSize: 12,
x: x - 12,
y: y + 4,
width: 50,
height: 25
},
'Start'
)
}
getShape() {
const { model } = this.props
const {
x,
y,
r,
} = model
const {
fill,
stroke,
strokeWidth } = model.getNodeStyle()
return h(
'g',
{
},
[
h(
'circle',
{
cx: x,
cy: y,
r,
fill,
stroke,
strokeWidth
}
),
this.getLabelShape()
]
)
}
}
export class StartModel extends CircleNodeModel {
// 自定义节点形状属性
initNodeData(data) {
data.text = {
value: (data.text && data.text.value) || '',
x: data.x,
y: data.y + 35,
dragable: false,
editable: true
}
super.initNodeData(data)
this.r = 20
}
// 自定义节点样式属性
getNodeStyle() {
const style = super.getNodeStyle()
return style
}
// 自定义锚点样式
getAnchorStyle() {
const style = super.getAnchorStyle();
style.hover.r = 8;
style.hover.fill = "rgb(24, 125, 255)";
style.hover.stroke = "rgb(24, 125, 255)";
return style;
}
// 自定义节点outline
getOutlineStyle() {
const style = super.getOutlineStyle();
style.stroke = '#88f'
return style
}
getConnectedTargetRules() {
const rules = super.getConnectedTargetRules()
const notAsTarget = {
message: '起始节点不能作为连线的终点',
validate: () => false
}
rules.push(notAsTarget)
return rules
}
}
export default {
type: "circleStart",
view: StartNode,
model: StartModel
}
1.2 默认注册的图标有10个,7个node,3个edge
源码如下:
private defaultRegister() {
// LogicFlow default Nodes and Edges
const defaultElements: RegisterConfig[] = [
// Node
{
type: 'rect',
view: _View.RectNode,
model: _Model.RectNodeModel,
},
{
type: 'circle',
view: _View.CircleNode,
model: _Model.CircleNodeModel,
},
{
type: 'polygon',
view: _View.PolygonNode,
model: _Model.PolygonNodeModel,
},
{
type: 'text',
view: _View.TextNode,
model: _Model.TextNodeModel,
},
{
type: 'ellipse',
view: _View.EllipseNode,
model: _Model.EllipseNodeModel,
},
{
type: 'diamond',
view: _View.DiamondNode,
model: _Model.DiamondNodeModel,
},
{
type: 'html',
view: _View.HtmlNode,
model: _Model.HtmlNodeModel,
},
// Edge
{
type: 'line',
view: _View.LineEdge,
model: _Model.LineEdgeModel,
},
{
type: 'polyline',
view: _View.PolylineEdge,
model: _Model.PolylineEdgeModel,
},
{
type: 'bezier',
view: _View.BezierEdge,
model: _Model.BezierEdgeModel,
},
]
this.batchRegister(defaultElements)
}
如图所示:
二、图标的事件注册
1、on:注册事件;emit:触发事件;off:注销事件
const lf = new LogicFlow({
...this.config,
container: this.$refs.container,
})
//注册事件
this.lf.on('node:click', ({data}) => {
console.log('node:click', data)
})
//触发事件
this.lf.emit('node:click', (data) => {
console.log(data)
})
this.lf.off('node:click')
三、图标的渲染
1、render(data)
data的数据类型:{Nodes :[],Edges :[]}
Nodes (节点):
每个节点表示图中的一个元素,比如开始节点、结束节点、用户自定义节点等。
-
id: 每个节点的唯一标识符,用于区分不同的节点。
-
type: 节点的类型。不同类型的节点具有不同的图形和功能。
-
x: 节点在画布上的 x 坐标。
-
y: 节点在画布上的 y 坐标。
-
text
: 节点显示的文本信息,包括以下字段:
- x: 文本相对于节点的 x 坐标。
- y: 文本相对于节点的 y 坐标。
- value: 显示的文本内容,比如节点名称或描述。
-
properties: 节点的自定义属性,通常用于保存特定业务逻辑的数据。在这里为空
{}。 -
baseType: 节点的基本类型,通常为
node,表明这是一个普通的节点。
Edges (连线)
连线用于连接节点,定义节点之间的流向。
- id: 每条连线的唯一标识符。
- type: 连线的类型,
polyline表示折线。 - sourceNodeId: 连线的起始节点 ID。
- targetNodeId: 连线的目标节点 ID。
- startPoint: 连线的起始点坐标(x, y)。
- endPoint: 连线的结束点坐标(x, y)。
- properties: 连线的自定义属性,也通常为空
{}。 - pointsList: 连线的中间点列表,用于定义连线的路径,由一系列坐标点(x, y)构成。
- text (可选): 连线中显示的文本,通常用于决策节点的分支连线。
源代码:
export interface GraphConfigData {
nodes?: NodeConfig[]
edges?: EdgeConfig[]
}
// 表示边缘连接线
export interface EdgeConfig<P extends PropertiesType = PropertiesType> {
id?: string
type?: string // 连接节点的线类型
sourceNodeId: string // 连接开始节点的id
sourceAnchorId?: string
targetNodeId: string // 连接结束节点的id
targetAnchorId?: string
startPoint?: Point // 线的开始的位置 {x,y}
endPoint?: Point // 线的结束的位置 {x,y}
text?: TextConfig | string // 线的内容 {"x": 850,"y": 400,"value": "N"}
pointsList?: Point[]
zIndex?: number
properties?: P // 线的自定义属性
}
// 节点的属性
export interface NodeConfig<P extends PropertiesType = PropertiesType> {
id?: string
type: string //节点类型名字
x: number // x轴的位置
y: number // y轴的位置
text?: TextConfig | string // 节点内的文字内容
zIndex?: number
properties?: P // 节点的自定义属性
virtual?: boolean // 是否虚拟节点
rotate?: number
rotatable?: boolean // 节点是否可旋转
resizable?: boolean // 节点是否可缩放
[key: string]: any
}
* @param graphData 图数据
*/
render(graphData: GraphConfigData) {
let graphRawData = cloneDeep(graphData)
if (this.adapterIn) {
graphRawData = this.adapterIn(graphRawData)
}
this.renderRawData(graphRawData)
}
例子数据:
{
"nodes": [
{
"id": "742356ea-762b-4899-b96a-bd567e3c4361",
"type": "start",
"x": 220,
"y": 170,
"text": {
"x": 350,
"y": 190,
"value": "sdfasf"
},
"properties": {},
"baseType": "node"
},
{
"id": "dacda6b6-48d3-4dff-911d-287704eb23d8",
"type": "rect",
"x": 350,
"y": 170,
"properties": {},
"baseType": "node",
"text": {
"x": 350,
"y": 170,
"value": "基础节点"
}
},
{
"id": "49106603-2b88-4b2c-b1e8-723c1f2210bd",
"type": "user",
"x": 530,
"y": 170,
"properties": {},
"baseType": "node",
"text": {
"x": 530,
"y": 220,
"value": "自定义节点"
}
},
{
"id": "647fa2bc-98ee-40cf-99c5-4756c0bc130d",
"type": "push",
"x": 690,
"y": 170,
"properties": {},
"baseType": "node",
"text": {
"x": 690,
"y": 220,
"value": "自定义节点 可添加下一个节点/节点组"
}
},
{
"id": "37e7bac3-8804-4237-abe9-7b6065c207e9",
"type": "download",
"x": 690,
"y": 320,
"properties": {},
"baseType": "node"
},
{
"id": "6bb4396f-54c9-4b1c-b34c-87ef004f2e29",
"type": "user",
"x": 840,
"y": 320,
"properties": {},
"baseType": "node"
},
{
"id": "abf76937-63b8-493c-a978-a4a58bc4f6b8",
"type": "push",
"x": 840,
"y": 470,
"properties": {},
"baseType": "node"
},
{
"id": "b119f24f-2669-4a90-a837-afd853b2ffcc",
"type": "end",
"x": 990,
"y": 320,
"properties": {},
"baseType": "node"
},
{
"id": "60326ad9-cae2-4a85-ae98-d340fb7bd67f",
"type": "end",
"x": 990,
"y": 470,
"properties": {},
"baseType": "node"
}
],
"edges": [
{
"id": "00f55245-513e-43a2-9cb0-adb61b01adc8",
"type": "polyline",
"sourceNodeId": "742356ea-762b-4899-b96a-bd567e3c4361",
"targetNodeId": "dacda6b6-48d3-4dff-911d-287704eb23d8",
"startPoint": {
"x": 240,
"y": 170
},
"endPoint": {
"x": 300,
"y": 170
},
"properties": {},
"pointsList": [
{
"x": 240,
"y": 170
},
{
"x": 300,
"y": 170
}
]
},
{
"id": "bbf9754f-603e-48e4-85fe-84ed44459a6a",
"type": "polyline",
"sourceNodeId": "dacda6b6-48d3-4dff-911d-287704eb23d8",
"targetNodeId": "49106603-2b88-4b2c-b1e8-723c1f2210bd",
"startPoint": {
"x": 400,
"y": 170
},
"endPoint": {
"x": 495,
"y": 170
},
"properties": {},
"pointsList": [
{
"x": 400,
"y": 170
},
{
"x": 495,
"y": 170
}
]
},
{
"id": "12bb443b-4070-4a08-ad4d-2755ee856f0d",
"type": "polyline",
"sourceNodeId": "49106603-2b88-4b2c-b1e8-723c1f2210bd",
"targetNodeId": "647fa2bc-98ee-40cf-99c5-4756c0bc130d",
"startPoint": {
"x": 565,
"y": 170
},
"endPoint": {
"x": 655,
"y": 170
},
"properties": {},
"pointsList": [
{
"x": 565,
"y": 170
},
{
"x": 655,
"y": 170
}
]
},
{
"id": "33fa3c09-9c29-4cb7-8373-67d537b8b623",
"type": "polyline",
"sourceNodeId": "647fa2bc-98ee-40cf-99c5-4756c0bc130d",
"targetNodeId": "37e7bac3-8804-4237-abe9-7b6065c207e9",
"startPoint": {
"x": 690,
"y": 205
},
"endPoint": {
"x": 690,
"y": 295
},
"properties": {},
"pointsList": [
{
"x": 690,
"y": 205
},
{
"x": 690,
"y": 295
}
]
},
{
"id": "2b5a5e89-005e-4fda-9a44-dc795050534f",
"type": "polyline",
"sourceNodeId": "37e7bac3-8804-4237-abe9-7b6065c207e9",
"targetNodeId": "6bb4396f-54c9-4b1c-b34c-87ef004f2e29",
"startPoint": {
"x": 715,
"y": 320
},
"endPoint": {
"x": 805,
"y": 320
},
"properties": {},
"pointsList": [
{
"x": 715,
"y": 320
},
{
"x": 805,
"y": 320
}
]
},
{
"id": "62b54f8a-bcfd-494b-9144-5aeb09ca77a1",
"type": "polyline",
"sourceNodeId": "6bb4396f-54c9-4b1c-b34c-87ef004f2e29",
"targetNodeId": "b119f24f-2669-4a90-a837-afd853b2ffcc",
"startPoint": {
"x": 875,
"y": 320
},
"endPoint": {
"x": 970,
"y": 320
},
"properties": {},
"text": {
"x": 920,
"y": 310,
"value": "Y"
},
"pointsList": [
{
"x": 875,
"y": 320
},
{
"x": 970,
"y": 320
}
]
},
{
"id": "ba816d4a-5785-4911-9f78-03933f1463a1",
"type": "polyline",
"sourceNodeId": "6bb4396f-54c9-4b1c-b34c-87ef004f2e29",
"targetNodeId": "abf76937-63b8-493c-a978-a4a58bc4f6b8",
"startPoint": {
"x": 840,
"y": 355
},
"endPoint": {
"x": 840,
"y": 435
},
"properties": {},
"text": {
"x": 850,
"y": 400,
"value": "N"
},
"pointsList": [
{
"x": 840,
"y": 355
},
{
"x": 840,
"y": 435
}
]
},
{
"id": "2b3007ed-7a13-4db7-a1ea-6691d7564c34",
"type": "polyline",
"sourceNodeId": "abf76937-63b8-493c-a978-a4a58bc4f6b8",
"targetNodeId": "60326ad9-cae2-4a85-ae98-d340fb7bd67f",
"startPoint": {
"x": 875,
"y": 470
},
"endPoint": {
"x": 970,
"y": 470
},
"properties": {},
"pointsList": [
{
"x": 875,
"y": 470
},
{
"x": 970,
"y": 470
}
]
}
]
}
四、设置主题 :
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: {...}, // 锚点样式
...
})
源代码的类型:只列出大概的
export interface Theme {
baseNode: CommonTheme // 所有节点的通用主题设置
baseEdge: EdgeTheme // 所有边的通用主题设置
/**
* 基础图形节点相关主题
*/
rect: RectTheme // 矩形样式
circle: CircleTheme // 圆形样式
diamond: PolygonTheme // 菱形样式
ellipse: EllipseTheme // 椭圆样式
polygon: PolygonTheme // 多边形样式
/**
* 基础图形线相关主题
*/
line: EdgeTheme // 直线样式
polyline: EdgePolylineTheme // 折现样式
bezier: EdgeBezierTheme // 贝塞尔曲线样式
anchorLine: AnchorLineTheme // 从锚点拉出的边的样式
/**
* 文本内容相关主题
*/
text: TextNodeTheme // 文本节点样式
nodeText: NodeTextTheme // 节点文本样式
edgeText: EdgeTextTheme // 边文本样式
inputText?: CommonTheme
/**
* 其他元素相关主题
*/
anchor: AnchorTheme // 锚点样式
arrow: ArrowTheme // 边上箭头的样式
snapline: EdgeTheme // 对齐线样式
rotateControl: CommonTheme // 节点旋转控制点样式
resizeControl: CommonTheme // 节点旋转控制点样式
resizeOutline: CommonTheme // 节点调整大小时的外框样式
/**
* REMIND: 当开启了跳转边的起点和终点(adjustEdgeStartAndEnd:true)后
* 边的两端会出现调整按钮
* 边连段的调整点样式
*/
edgeAdjust: CircleTheme
outline: OutlineTheme // 节点选择状态下外侧的选框样式
edgeAnimation: EdgeAnimation // 边动画样式
}
五、网格
网格是指渲染/移动节点的最小单位。网格最主要的作用是在移动节点的时候,保证每个节点中心点的位置都是在网格上。这样更有利于节点直接的对齐。一般来说,网格的间隔越大,在编辑流程图的时候,节点就更好对齐;网格的间隔越小,拖动节点的感觉就更加流畅。
网格默认关闭,渲染/移动最小单位为 1px,若开启网格,则网格默认大小为 20px,渲染节点时表示以 20 为最小单位对齐到网络,移动节点时表示每次移动最小距离为 20px。
注意
在设置节点坐标时会按照网格的大小来对坐标进行转换,如设置中心点位置{ x: 124, y: 138 } 的节点渲染到画布后的实际位置为 { x: 120, y: 140 }。所以使用 LogicFlow 替换项目中旧的流程设计器时,需要对历史数据的坐标进行处理。 在实际开发中,如果期望节点既可以中心对齐,也可以按照两边对齐。那么自定义节点的宽高需要是 grid 的偶数倍。也就是假设 grid 为 20,那么所有的节点宽度最好是 20、40、80、120 这种偶数倍的宽度。
开启网格
在创建画布的时候通过配置 grid 来设置网格属性
开启网格并应用默认属性:
const lf1 = new LogicFlow({ grid: true,})
// 等同于默认属性如下const lf2 = new LogicFlow({ grid: { size: 20, visible: true, type: 'dot', config: { color: '#ababab', thickness: 1, }, },})
设置网格属性
支持设置网格大小、类型、网格线颜色和宽度等属性。
export type GridOptions = {
size?: number // 设置网格大小
visible?: boolean, // 设置是否可见,若设置为false则不显示网格线但是仍然保留size栅格的效果
type?: 'dot' | 'mesh', // 设置网格类型,目前支持 dot 点状和 mesh 线状两种
config?: {
color: string, // 设置网格的颜色
thickness?: number, // 设置网格线的宽度
}
};
示例
六、背景 Background
提供可以修改画布背景的方法,包括背景颜色或背景图片,背景层位于画布的最底层。
创建画布时,通过 background 选项来设置画布的背景层样式,支持透传任何样式属性到背景层。默认值为 false 表示没有背景。
const lf = new LogicFlow({ background: false | BackgroundConfig,})
type BackgroundConfig = { backgroundImage?: string, backgroundColor?: string, backgroundRepeat?: string, backgroundPosition?: string, backgroundSize?: string, backgroundOpacity?: number, filter?: string, // 滤镜 [key: any]: any,};
const lf = new LogicFlow({
// ...
background: {
backgroundImage: "url(../asserts/img/grid.svg)",
backgroundRepeat: "repeat",
},
});
示例
七、节点的验证
在某些时候,我们可能需要控制边的连接方式,比如开始节点不能被其它节点连接、结束节点不能连接其他节点、用户节点后面必须是判断节点等,要想达到这种效果,我们需要为节点设置以下两个属性。
sourceRules- 当节点作为边的起始节点(source)时的校验规则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) }}
写法二:getConnectedSourceRules()
import LogicFlow, { PolygonNodeModel, PolygonNode } from '@logicflow/core';
class CustomHexagonModel extends PolygonNodeModel {
getConnectedSourceRules() {
const rules = super.getConnectedSourceRules();
const geteWayOnlyAsTarget = {
message: '下一个节点只能是circle',
validate: (
source: any,
target: any,
sourceAnchor: any,
targetAnchor: any,
) => {
console.log('sourceAnchor, targetAnchor', sourceAnchor, targetAnchor);
return target.type === 'circle';
},
};
rules.push(geteWayOnlyAsTarget);
return rules;
}
}
export default {
type: 'hexagonNode',
model: CustomHexagonModel,
view: PolygonNode,
};