自定义节点开发
RuleGo-Editor 基于 LogicFlow (opens new window) 实现节点可视化编辑。本文档介绍如何开发自定义节点,包括节点类型说明、开发步骤、代码示例和表单配置。
# 节点类型说明
RuleGo-Editor 内置了以下节点类型,所有节点均位于 src/components/lf-node/nodes/ 目录下。
# BaseNode
基础节点类,其他节点继承自它。定义在 BaseNode.js 中。
主要特性:
- 默认尺寸:宽度 120,高度 30
- 支持左右锚点(左侧输入,右侧输出)
- 支持状态高亮(processing/success/error)
- 支持图标显示
- 内置连线校验规则
核心属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| width | number | 120 | 节点宽度 |
| height | number | 30 | 节点高度 |
| radius | number | 5 | 圆角半径 |
| iconPosition | string | '' | 图标位置,'left' 或 'right' |
| defaultFill | string | '#a6bbcf' | 默认填充颜色 |
状态高亮:
节点支持三种状态高亮,通过设置 properties.status 属性实现:
// 设置节点状态
lf.getNodeModelById(nodeId).setProperties({
status: 'processing' // 或 'success'、'error'
})
2
3
4
| 状态 | 边框颜色 | 说明 |
|---|---|---|
| processing | #409EFF (蓝色) | 处理中 |
| success | #67c23a (绿色) | 成功 |
| error | #f56c6c (红色) | 错误 |
# SimpleNode
简单节点,继承自 BaseNode。适用于大多数普通组件。
// 文件位置:src/components/lf-node/nodes/SimpleNode.js
export default {
type: 'simple-node',
model: SimpleNodeModel,
view: SimpleNode
}
2
3
4
5
6
特性:
- 继承 BaseNode 的所有特性
- 支持左右锚点
- 图标默认使用
/images/fetch.svg
# EndpointNode
输入端节点,继承自 BaseNode。用于规则链的起始输入。
// 文件位置:src/components/lf-node/nodes/EndpointNode.js
export default {
type: 'endpoint-node',
model: EndpointNodeModel,
view: EndpointNode
}
2
3
4
5
6
特性:
- 只有右锚点(只能输出,不能输入)
- 图标默认使用
/images/start.svg - 用于规则链的起始节点
# StartNode / EndNode
开始和结束节点,分别用于规则链的起点和终点。
// StartNode - 文件位置:src/components/lf-node/nodes/StartNode.js
export default {
type: 'start-node',
model: StartNodeModel,
view: StartNode
}
// EndNode - 文件位置:src/components/lf-node/nodes/EndNode.js
export default {
type: 'end-node',
model: EndNodeModel,
view: EndNode
}
2
3
4
5
6
7
8
9
10
11
12
13
StartNode 特性:
- 只有右锚点
- 默认填充颜色:rgb(166, 187, 207)
- 点击图标触发
rulego-editor:start事件
EndNode 特性:
- 只有左锚点
- 默认填充颜色:rgb(166, 187, 207)
# GroupNode
分组节点,继承自 LogicFlow 的 dynamicGroup。支持折叠/展开功能。
// 文件位置:src/components/lf-node/nodes/GroupNode.js
export default {
type: 'group-node',
model: GroupNodeModel,
view: GroupNode
}
2
3
4
5
6
特性:
- 默认尺寸:宽度 300,高度 200
- 支持折叠/展开(折叠后尺寸:120x30)
- 支持调整大小
- 支持拖入其他节点(endpoint-node 和 start-node 除外)
- 动态锚点位置(根据折叠状态自动调整)
配置选项:
| 属性 | 默认值 | 说明 |
|---|---|---|
| resizable | true | 是否可调整大小 |
| collapsible | true | 是否可折叠 |
| isRestrict | true | 是否限制节点在组内 |
| autoResize | true | 是否自动调整大小 |
| foldable | true | 是否可折叠 |
| autoToFront | true | 选中时是否自动置顶 |
# ChainNode
子规则链节点,继承自 BaseNode。用于引用其他规则链。
// 文件位置:src/components/lf-node/nodes/ChainNode.js
export default {
type: 'chain-node',
model: ChainNodeModel,
view: ChainNode
}
2
3
4
5
6
特性:
- 只有左锚点(只能输入)
- 用于调用子规则链
# CommentNode
注释节点,继承自 BaseNode。用于在画布上添加注释。
// 文件位置:src/components/lf-node/nodes/CommentNode.js
export default {
type: 'comment-node',
model: CommentNodeModel,
view: CommentNode
}
2
3
4
5
6
特性:
- 没有锚点(不能连线)
- 文本以斜体显示
- 宽度根据文本长度自动调整
# AgentNode
智能体节点,继承自 RectNode。用于 AI 智能体编排。
// 文件位置:src/components/lf-node/nodes/AgentNode.js
export default {
type: 'agent-node',
model: AgentNodeModel,
view: AgentNode
}
2
3
4
5
6
特性:
- 默认尺寸:宽度 160,高度 80
- 支持标准左右锚点
- 底部四个特殊锚点:大模型(llm)、工具(tool)、记忆(memory)、知识库(knowledge)
- 锚点使用不同颜色区分
# AgentOrchestrationNode
智能体编排节点,继承自 RectNode。用于可视化的多智能体编排。
// 文件位置:src/components/lf-node/nodes/AgentOrchestrationNode.js
export default {
type: 'agent-orchestration-node',
model: AgentOrchestrationNodeModel,
view: AgentOrchestrationNodeView
}
2
3
4
5
6
特性:
- 默认尺寸:宽度 180,高度 120
- 圆角 8px,蓝色主题(填充
#E8F4FD,边框#1890FF) - 只能作为连接的起点(无输入锚点)
- 支持右侧和底部锚点连出
# FlowLink
连线类型,使用贝塞尔曲线。定义在 FlowLink.js 中。
// 文件位置:src/components/lf-node/FlowLink.js
export default {
type: 'flow-link',
view: FlowLink,
model: FlowLinkModel
}
2
3
4
5
6
特性:
- 使用贝塞尔曲线
- 支持调试高亮(debugHighlight)
- 选中/悬停时边框变色
- 文本自动换行
# 自定义节点开发步骤
# 步骤 1:创建节点文件
在 src/components/lf-node/nodes/ 目录下创建新的节点文件,例如 MyCustomNode.js。
// src/components/lf-node/nodes/MyCustomNode.js
import { h } from '@logicflow/core'
import BaseNode from './BaseNode'
import { resolveIconPath } from '../../utils/common'
// 节点视图类
class MyCustomNodeView extends BaseNode.view {
/**
* 重写图标渲染方法
*/
getIcon() {
const { width, height, icon } = this.props.model
const _icon = resolveIconPath(icon || '/images/my-custom.svg')
return h('image', {
width: 30,
height: 30,
x: -width / 2,
y: -height / 2,
href: _icon
})
}
}
// 节点模型类
class MyCustomNodeModel extends BaseNode.model {
/**
* 初始化节点数据
*/
initNodeData(data) {
super.initNodeData(data)
// 设置默认尺寸
this.width = 120
this.height = 30
// 设置默认填充颜色
this.defaultFill = '#E6E0F8'
}
/**
* 重写节点样式
*/
getNodeStyle() {
const style = super.getNodeStyle()
const dataStyle = this.properties.view || {}
// 自定义样式
style.fill = dataStyle.background || this.defaultFill
style.stroke = dataStyle.borderColor || '#999'
return style
}
/**
* 重写锚点定义
*/
getDefaultAnchor() {
const { x, y, id, width, height } = this
const anchors = [
{
x: x + width / 2,
y: y,
id: `${id}_right`,
type: 'right'
},
{
x: x - width / 2,
y: y,
id: `${id}_left`,
type: 'left'
}
]
return anchors
}
/**
* 获取节点数据(用于保存)
*/
getData() {
const data = super.getData()
data.properties.ui = 'rulego-editor'
return data
}
}
// 导出节点定义
export default {
type: 'my-custom-node',
model: MyCustomNodeModel,
view: MyCustomNodeView
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# 步骤 2:注册节点
在 src/components/lf-node/index.js 中注册新节点。
// src/components/lf-node/index.js
import MyCustomNode from './nodes/MyCustomNode'
class NodeRedExtension {
constructor({ lf }) {
// ... 其他节点注册
// 注册自定义节点
lf.register(MyCustomNode)
// ... 其他代码
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 步骤 3:配置国际化
在 src/components/i18n/zh/index.js 中添加节点的国际化配置。
// src/components/i18n/zh/index.js
export const locales = {
category: {
// ... 其他分类
// 添加自定义分类(可选)
custom: {
label: '自定义',
background: '#C6EFCB',
nodeType: 'simple-node'
}
},
component: {
nodes: {
// ... 其他节点
// 添加自定义节点配置
'my-custom': {
label: '我的自定义组件',
icon: '/images/my-custom.svg',
desc: '这是一个自定义组件的描述',
// 字段配置
fieldName: {
label: '字段名称',
desc: '字段的详细说明',
rules: [
{ required: true, message: '该项是必须的' }
]
},
// 可选:指定节点类型
nodeType: 'my-custom-node'
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 步骤 4:添加组件到列表
在 src/components/utils/default.js 中添加组件定义,使其出现在左侧工具栏。
// src/components/utils/default.js
export const defaultComponents = {
endpoints: [
// ... 其他端点
],
nodes: [
// ... 其他节点
// 添加自定义组件
{
type: 'my-custom',
category: 'custom',
label: '我的自定义组件',
desc: '这是一个自定义组件',
icon: '/images/my-custom.svg',
fields: [
{
name: 'fieldName',
type: 'string',
defaultValue: '',
label: '字段名称',
desc: '字段的详细说明',
rules: [
{ required: true, message: '该项是必须的' }
]
}
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 步骤 5:添加图标
在 public/images/ 目录下添加节点图标文件(SVG 格式推荐)。
# 表单字段类型
RuleGo-Editor 支持多种表单字段类型,用于配置节点属性。
# string
文本输入框。
{
name: 'fieldName',
type: 'string',
label: '字段名称',
desc: '字段说明',
defaultValue: '',
rules: [
{ required: true, message: '该项是必须的' }
]
}
2
3
4
5
6
7
8
9
10
# int / float
数字输入框。
{
name: 'port',
type: 'int',
label: '端口号',
desc: '服务端口',
defaultValue: 8080
}
2
3
4
5
6
7
# boolean
布尔值,使用开关(Switch)组件。
{
name: 'enableLog',
type: 'boolean',
label: '启用日志',
desc: '是否启用日志记录',
defaultValue: false
}
2
3
4
5
6
7
# select
下拉选择框。
{
name: 'method',
type: 'string',
label: '请求方法',
component: {
type: 'select',
filterable: true,
allowCreate: false,
multiple: false,
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' }
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
select 组件属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| filterable | boolean | 是否可搜索 |
| allowCreate | boolean | 是否允许创建新选项 |
| multiple | boolean | 是否多选 |
| options | array | 选项列表,格式:[{label, value}] |
| loadData | function | 动态加载选项的函数 |
# textarea
多行文本输入框。
{
name: 'sql',
type: 'string',
label: 'SQL语句',
component: {
type: 'textarea',
rows: 4
}
}
2
3
4
5
6
7
8
9
# codeEditor
代码编辑器,支持语法高亮。
{
name: 'jsScript',
type: 'string',
label: 'JavaScript 脚本',
component: {
type: 'codeEditor'
}
}
2
3
4
5
6
7
8
# table
表格编辑器,用于编辑数组类型数据。
{
name: 'items',
type: 'string',
label: '缓存项',
component: {
type: 'table',
options: [
{
name: 'key',
label: '键',
type: 'string',
rules: [
{ required: true, message: '该项是必须的' }
]
},
{
name: 'value',
label: '值',
type: 'string'
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# struct
结构体编辑器,支持嵌套字段。
{
name: 'config',
type: 'string',
label: '配置',
fields: [
{
name: 'host',
type: 'string',
label: '主机地址'
},
{
name: 'port',
type: 'int',
label: '端口'
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# map
键值对编辑器。
{
name: 'headers',
type: 'string',
label: '请求头',
component: {
type: 'map'
}
}
2
3
4
5
6
7
8
# expr
表达式编辑器。
{
name: 'condition',
type: 'string',
label: '过滤表达式',
desc: '例如: msg.temperature > 50',
component: {
type: 'expr'
}
}
2
3
4
5
6
7
8
9
# slider
滑块组件。
{
name: 'timeout',
type: 'int',
label: '超时时间',
component: {
type: 'slider',
min: 0,
max: 100,
step: 1
}
}
2
3
4
5
6
7
8
9
10
11
# input-number
数字输入框,支持步进。
{
name: 'count',
type: 'int',
label: '数量',
component: {
type: 'input-number',
min: 0,
max: 100,
step: 1
}
}
2
3
4
5
6
7
8
9
10
11
# datePicker
日期选择器。
{
name: 'startDate',
type: 'string',
label: '开始日期',
component: {
type: 'datePicker'
}
}
2
3
4
5
6
7
8
# timePicker
时间选择器。
{
name: 'startTime',
type: 'string',
label: '开始时间',
component: {
type: 'timePicker'
}
}
2
3
4
5
6
7
8
# ruleChainSelector
规则链选择器,用于选择子规则链。
{
name: 'targetId',
type: 'string',
label: '规则链',
component: {
type: 'ruleChainSelector'
}
}
2
3
4
5
6
7
8
# 高级用法
# 自定义锚点形状
重写 getAnchorShape 方法来自定义锚点形状。
class MyNode extends BaseNode.view {
getAnchorShape(anchorData) {
const { x, y, type } = anchorData
// 根据锚点类型返回不同形状
if (type === 'right') {
return h('circle', {
cx: x,
cy: y,
r: 5,
fill: '#1890ff',
stroke: '#fff',
strokeWidth: 2
})
}
// 默认使用矩形
return h('rect', {
x: x - 5,
y: y - 5,
width: 10,
height: 10,
className: 'custom-anchor'
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 自定义节点形状
重写 getShape 方法来自定义节点外观。
class MyNode extends BaseNode.view {
getShape() {
const { x, y, width, height, radius } = this.props.model
const style = this.props.model.getNodeStyle()
return h('g', { className: 'my-custom-node' }, [
// 主体矩形
h('rect', {
...style,
x: x - width / 2,
y: y - height / 2,
width,
height,
rx: radius,
ry: radius
}),
// 图标区域
h('rect', {
x: x - width / 2,
y: y - height / 2,
width: 30,
height: 30,
fill: '#000',
fillOpacity: 0.05
}),
// 图标
this.getIcon(),
// 分隔线
h('line', {
x1: x - width / 2 + 30,
y1: y - height / 2,
x2: x - width / 2 + 30,
y2: y + height / 2,
stroke: '#000',
strokeOpacity: 0.1
})
])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 添加连线校验规则
在 initNodeData 中添加自定义连线校验规则。
class MyNodeModel extends BaseNode.model {
initNodeData(data) {
super.initNodeData(data)
// 连出校验规则
const sourceRule = {
message: '只允许连接到特定类型的节点',
validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
// 只允许连接到 filter 类型的节点
return targetNode.type === 'filter-node'
}
}
this.sourceRules.push(sourceRule)
// 连入校验规则
const targetRule = {
message: '只允许从左边锚点连入',
validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
return targetAnchor.type === 'left'
}
}
this.targetRules.push(targetRule)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 动态调整节点尺寸
根据文本内容动态调整节点宽度。
class MyNodeModel extends BaseNode.model {
setAttributes() {
if (this.text.value) {
// 根据文本长度计算宽度
let width = 30 + getBytesLength(this.text.value) * 9
// 向上取整到 20 的倍数
width = Math.ceil(width / 20) * 20
// 设置最小宽度
this.width = Math.max(width, 120)
// 截断过长的文本
const cutNum = Math.ceil((width - this.width) / 20)
if (cutNum > 0) {
const endIndex = this.text.value.length - cutNum
if (endIndex > 0) {
this.text.value = this.text.value.substring(0, endIndex) + '...'
}
}
}
}
updateText(val) {
super.updateText(val)
this.setAttributes()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 完整示例
以下是一个完整的自定义节点示例,实现一个带状态指示器的过滤器节点。
// src/components/lf-node/nodes/FilterNode.js
import { h } from '@logicflow/core'
import BaseNode from './BaseNode'
import { resolveIconPath } from '../../utils/common'
class FilterNodeView extends BaseNode.view {
getIcon() {
const { width, height } = this.props.model
return h('image', {
width: 30,
height: 30,
x: -width / 2,
y: -height / 2,
href: resolveIconPath('/images/filter.svg')
})
}
getShape() {
const { x, y, width, height, radius } = this.props.model
const style = this.props.model.getNodeStyle()
const status = this.props.model.properties.status
return h('g', { className: 'lf-filter-node' }, [
// 主体矩形
h('rect', {
...style,
x: x - width / 2,
y: y - height / 2,
width,
height,
rx: radius,
ry: radius,
className: status === 'processing' ? 'lf-node-processing' :
status === 'success' ? 'lf-node-success' :
status === 'error' ? 'lf-node-error' : ''
}),
// 图标和分隔线容器
h('g', {
style: 'pointer-events: none;',
transform: `translate(${x}, ${y})`
}, [
// 图标背景
h('rect', {
x: -width / 2,
y: -height / 2,
width: 30,
height: 30,
fill: '#000',
fillOpacity: 0.05,
stroke: 'none'
}),
// 图标
this.getIcon(),
// 分隔线
h('path', {
d: `M ${30 - width / 2} ${1 - height / 2} l 0 28`,
stroke: '#000',
strokeOpacity: 0.1,
strokeWidth: 1
})
])
])
}
}
class FilterNodeModel extends BaseNode.model {
initNodeData(data) {
super.initNodeData(data)
this.width = 120
this.height = 30
this.defaultFill = '#f1e861'
}
getNodeStyle() {
const style = super.getNodeStyle()
const dataStyle = this.properties.view || {}
style.fill = dataStyle.background || this.defaultFill
this.icon = resolveIconPath(dataStyle.icon || '/images/filter.svg')
return style
}
getData() {
const data = super.getData()
data.properties.ui = 'rulego-editor'
return data
}
}
export default {
type: 'filter-node',
model: FilterNodeModel,
view: FilterNodeView
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
对应的国际化配置:
// src/components/i18n/zh/index.js
{
category: {
filter: {
label: '过滤器',
background: '#f1e861',
nodeType: 'filter-node'
}
},
component: {
nodes: {
'my-filter': {
label: '自定义过滤器',
icon: '/images/filter.svg',
desc: '自定义过滤器组件',
expression: {
label: '过滤表达式',
desc: '例如: msg.temperature > 50',
rules: [
{ required: true, message: '过滤表达式是必须的' }
],
component: {
type: 'expr'
}
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29