diff --git a/.dumi/theme/slots/Header/Navigation.tsx b/.dumi/theme/slots/Header/Navigation.tsx
index 853da4a90..a401265f2 100644
--- a/.dumi/theme/slots/Header/Navigation.tsx
+++ b/.dumi/theme/slots/Header/Navigation.tsx
@@ -1,7 +1,7 @@
-import React from 'react';
+import { CodeOutlined, DownOutlined } from '@ant-design/icons';
import { Menu } from 'antd';
import { Link } from 'dumi';
-import { CodeOutlined, DownOutlined, MobileOutlined, SwapOutlined, ToolOutlined } from '@ant-design/icons';
+import React from 'react';
const Navigation: React.FC = () => {
const items: any = [
@@ -13,9 +13,13 @@ const Navigation: React.FC = () => {
label: TableRender,
key: 'table-render',
},
+ // {
+ // label: XFlow,
+ // key: 'xflow',
+ // },
{
label: FRMobile,
- key: 'form-render-mobile'
+ key: 'form-render-mobile',
},
// {
// label: DataView,
@@ -23,7 +27,7 @@ const Navigation: React.FC = () => {
// },
{
label: Playground,
- key: 'playground'
+ key: 'playground',
},
{
label: SchemaBuilder,
@@ -33,17 +37,23 @@ const Navigation: React.FC = () => {
label: (
更多
-
+
),
children: [
{
- label: ChartRender,
+ label: (
+
+ ChartRender
+
+ ),
key: 'chart-render',
icon: ,
- }
- ]
- }
+ },
+ ],
+ },
];
return ;
diff --git a/.dumirc.ts b/.dumirc.ts
index b89b92635..84bdc3999 100644
--- a/.dumirc.ts
+++ b/.dumirc.ts
@@ -76,6 +76,7 @@ export default defineConfig({
'form-render-mobile': path.resolve(__dirname, 'packages/form-render-mobile/src'),
'@xrenders/schema-builder': path.resolve(__dirname, 'tools/schema-builder/src'),
'@xrenders/data-render': path.resolve(__dirname, 'packages/data-render/src'),
+ '@xrenders/xflow': path.resolve(__dirname, 'packages/x-flow/src'),
},
codeSplitting: { jsStrategy: 'granularChunks' },
//...(process.env.NODE_ENV === 'development' ? {} : { ssr: {} }),
diff --git a/.npmrc b/.npmrc
index 214c29d13..5e4086a7f 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1 +1 @@
-registry=https://registry.npmjs.org/
+registry=https://registry.npmmirror.com/
diff --git a/.turbo/daemon/58af6c43ce0c4d29-turbo.log.2024-12-04 b/.turbo/daemon/58af6c43ce0c4d29-turbo.log.2024-12-04
new file mode 100644
index 000000000..437d22305
--- /dev/null
+++ b/.turbo/daemon/58af6c43ce0c4d29-turbo.log.2024-12-04
@@ -0,0 +1,8 @@
+2024-12-04T00:52:39.144082Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/package.json")}
+2024-12-04T00:52:39.144107Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }}))
+2024-12-04T00:54:43.043002Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("docs/data-render/index.md")}
+2024-12-04T00:54:43.043046Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Root, path: AnchoredSystemPathBuf("") }}))
+2024-12-04T00:55:35.341519Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("docs/xflow/index.md")}
+2024-12-04T00:55:35.341535Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Root, path: AnchoredSystemPathBuf("") }}))
+2024-12-04T00:56:31.240344Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf(".dumi/theme/slots/Header/Navigation.tsx")}
+2024-12-04T00:56:31.240359Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Root, path: AnchoredSystemPathBuf("") }}))
diff --git a/.turbo/daemon/9af73f40ad414c73-turbo.log.2024-12-03 b/.turbo/daemon/9af73f40ad414c73-turbo.log.2024-12-03
new file mode 100644
index 000000000..a2ece7970
--- /dev/null
+++ b/.turbo/daemon/9af73f40ad414c73-turbo.log.2024-12-03
@@ -0,0 +1,10 @@
+2024-12-03T17:26:27.588772Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/types.ts")}
+2024-12-03T17:26:27.588794Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }}))
+2024-12-03T17:27:03.185223Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/types.ts")}
+2024-12-03T17:27:03.185237Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }}))
+2024-12-03T17:27:46.488909Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/withProvider.tsx")}
+2024-12-03T17:27:46.488931Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }}))
+2024-12-03T17:27:51.584147Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/types.ts")}
+2024-12-03T17:27:51.584179Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }}))
+2024-12-03T17:29:05.583324Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/withProvider.tsx")}
+2024-12-03T17:29:05.583342Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }}))
diff --git a/docs/form-render/advanced-widget.md b/docs/form-render/advanced-widget.md
index 8d9e2fa89..e78bce8c3 100644
--- a/docs/form-render/advanced-widget.md
+++ b/docs/form-render/advanced-widget.md
@@ -1,5 +1,5 @@
---
-order: 4
+order: 2
toc: content
mobile: false
group:
diff --git a/docs/xflow/FlowProvider.md b/docs/xflow/FlowProvider.md
new file mode 100644
index 000000000..086b200ce
--- /dev/null
+++ b/docs/xflow/FlowProvider.md
@@ -0,0 +1,12 @@
+---
+order: 3
+title: '多实例画布'
+mobile: false
+group:
+ title: 最佳展示
+ order: 4
+---
+
+# 基础交互
+
+
diff --git a/docs/xflow/api.md b/docs/xflow/api.md
new file mode 100644
index 000000000..d13cbdc56
--- /dev/null
+++ b/docs/xflow/api.md
@@ -0,0 +1,49 @@
+---
+order: 1
+toc: content
+title: API
+---
+# API
+
+## XFlow
+
+| 属性 | 描述 | 类型 | 默认值 |
+| ------------- | ------------------------------------ | ----------------------------------------------------------- | ------ |
+| initialValues | 初始的节点和边数据 | `{nodes:any[],edges:any[]}` | - | - |
+| layout | 节点布局的方向 | `LR \| TB` | LR | - |
+| widgets | 自定义组件 | `Record` | - | - |
+| settings | 节点配置,定义页面中可拖动的节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem) )[ ] | |
+| nodeSelector | 节点选择器配置,可控制节点的可搜索性 | `TNodeSelector` | |
+
+### TNodeGroup
+
+节点分组配置
+
+| 属性 | 描述 | 类型 | 默认值 |
+| ----- | ------------ | ------------- | ------ |
+| title | 分组名称 | `string` | |
+| type | 分组类型 | `_group` | _group |
+| items | 节点配置信息 | `TNodeItem[]` | |
+
+## TNodeItem
+
+单个节点配置
+
+| 属性 | 描述 | 类型 | 默认值 |
+| ------------------ | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
+| title | 节点名称 | `string` | |
+| type | 节点类型 | `string` | |
+| hidden | 是否在配置面板中显示节点 | `boolean` | false |
+| targetHandleHidden | 是否隐藏左侧输入连接头 | `boolean` | false |
+| sourceHandleHidden | 是否隐藏右侧输出连接头 | `boolean` | false |
+| icon | 节点的图标配置 | `{type:string;bgColor:string}` | |
+| settingSchema | 节点的业务配置信息,详见[form-render 文档](/form-render/api-schema)。同时设置`settingSchema`和`settingWidget`只生效`settingWidget` | SchemaBase | |
+| settingWidget | 自定义节点的业务配置组件,在弹窗中展示。同时设置`settingSchema`和`settingWidget`只生效`settingWidget`。定义之后需要在`widgets`中引入自定义组件。 | `string` | |
+| nodeWidget | 自定义节点的业务配置信息展示组件,在节点内部展示业务配置信息。定义之后需要在`widgets`中引入自定义组件。 | `string` | |
+
+## TNodeSelector
+
+| 属性 | 描述 | 类型 | 默认值 |
+| ---------- | ---------------- | ----------------------------------------------------------- | ------ |
+| showSearch | 节点是否可被搜索 | `boolean` | false |
+| items | 节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem) )[ ] | |
diff --git a/docs/xflow/custom-node-setting.md b/docs/xflow/custom-node-setting.md
new file mode 100644
index 000000000..0bb976f60
--- /dev/null
+++ b/docs/xflow/custom-node-setting.md
@@ -0,0 +1,75 @@
+---
+order: 2
+toc: content
+mobile: false
+group:
+ title: 最佳展示
+ order: 1
+---
+
+# 节点配置面板
+节点配置面板支持以下两种渲染方式自定义:
+
+- **Schema 方式**: 适用于节点配置较为简单的场景。通过 FormRender 配置 schema 来实现快速渲染。
+- **Widget 方式**: 针对复杂的配置需求,schema 无法满足时,可以通过自定义组件进行灵活渲染。
+
+
+## Schema
+通过配置节点的 settingSchema 属性,实现节点数据配置项的自定义渲染。
+
+```jsx
+import { Input } from 'antd';
+import React from 'react';
+import XFlow from '@xrenders/xflow';
+import settings from './schema/custom-settings.ts';
+
+
+const customWidget=({value,onChange})=>{
+ return
+}
+export default () => {
+ const nodes = [
+ {
+ id: '1',
+ type: 'Start',
+ data: {
+ inputVal:'我是自定义组件'
+ },
+ position: {
+ x: 40,
+ y: 240,
+ }
+ },
+ {
+ id: '2',
+ type: 'End',
+ data: {},
+ position: {
+ x: 500,
+ y: 240,
+ }
+ }
+ ];
+
+ const edges = [
+ { source: '1', target: '2', id: '234123' }
+ ]
+
+ return (
+
+
+
+ );
+}
+
+
+```
+
+## Widget
+通过配置节点的 `settingWidget` 属性,实现节点数据配置项的自定义渲染。
+
+
diff --git a/docs/xflow/custom-node-view.md b/docs/xflow/custom-node-view.md
new file mode 100644
index 000000000..787884b37
--- /dev/null
+++ b/docs/xflow/custom-node-view.md
@@ -0,0 +1,15 @@
+---
+order: 2
+toc: content
+mobile: false
+group:
+ title: 最佳展示
+ order: 1
+---
+
+# 节点内容展示
+当需要额外展示节点内容时,可以通过 `nodeWidget` 进行灵活渲染。
+
+使用`nodeWidget`自定义节点的业务配置信息展示组件,在节点内部展示业务配置信息
+
+
diff --git a/docs/xflow/data/basic.ts b/docs/xflow/data/basic.ts
new file mode 100644
index 000000000..e4e869ffc
--- /dev/null
+++ b/docs/xflow/data/basic.ts
@@ -0,0 +1,51 @@
+export default {
+ "creator": "清风徐来",
+ "relevanceCode": "421421",
+ "desc": "浙江省杭州市工专路",
+ "create-time": "2019-10-10",
+ "effective-date": "2019-10-10 ~ 2020-10-31",
+ "safety": {
+ "name": "Test demo 001",
+ "app": "中后台详情页面",
+ "mode": "代码包",
+ "yum": "592342323904823489",
+ "fore": "23"
+ },
+ "operLog": [{
+ "type": "创建测试",
+ "creator": "清风徐来",
+ "time": "2019-10-30 12:23:45",
+ "result": "1",
+ "desc": "这是备注"
+ }, {
+ "type": "创建测试",
+ "creator": "清风徐来",
+ "time": "2019-10-30 12:23:45",
+ "result": "1",
+ "desc": "这是备注"
+ }, {
+ "type": "创建测试",
+ "creator": "清风徐来",
+ "time": "2019-10-30 12:23:45",
+ "result": "1",
+ "desc": "这是备注"
+ }, {
+ "type": "创建测试",
+ "creator": "清风徐来",
+ "time": "2019-10-30 12:23:45",
+ "result": "1",
+ "desc": "这是备注"
+ }, {
+ "type": "创建测试",
+ "creator": "清风徐来",
+ "time": "2019-10-30 12:23:45",
+ "result": "1",
+ "desc": "这是备注"
+ }, {
+ "type": "创建测试",
+ "creator": "清风徐来",
+ "time": "2019-10-30 12:23:45",
+ "result": "1",
+ "desc": "这是备注"
+ }]
+}
\ No newline at end of file
diff --git a/docs/xflow/demo/basic/index.tsx b/docs/xflow/demo/basic/index.tsx
new file mode 100644
index 000000000..6ae0ae689
--- /dev/null
+++ b/docs/xflow/demo/basic/index.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import XFlow from '@xrenders/xflow';
+import settings from './setting';
+
+export default () => {
+ const nodes = [
+ {
+ id: '1',
+ type: 'Start',
+ data: {},
+ position: {
+ x: 40,
+ y: 240,
+ }
+ },
+ {
+ id: '2',
+ type: 'End',
+ data: {},
+ position: {
+ x: 500,
+ y: 240,
+ }
+ }
+ ];
+
+ const edges = [
+ { source: '1', target: '2', id: '234123' }
+ ]
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/xflow/demo/basic/setting.tsx b/docs/xflow/demo/basic/setting.tsx
new file mode 100644
index 000000000..937413778
--- /dev/null
+++ b/docs/xflow/demo/basic/setting.tsx
@@ -0,0 +1,299 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ radio1: {
+ title: '点击单选',
+ type: 'string',
+ widget: 'radio',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ textarea1: {
+ title: '长文本',
+ type: 'string',
+ widget: 'textArea'
+ },
+ date1: {
+ title: '日期选择',
+ type: 'string',
+ widget: 'datePicker'
+ },
+ dateRange1: {
+ title: '日期范围',
+ type: 'range',
+ widget: 'dateRange'
+ },
+ time1: {
+ title: '时间选择',
+ type: 'string',
+ widget: 'timePicker'
+ },
+ timeRange1: {
+ title: '时间范围',
+ type: 'range',
+ widget: 'timeRange'
+ },
+ },
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ }
+ }
+ },
+ ],
+ },
+];
diff --git a/docs/xflow/demo/custom-flow/customWidget.tsx b/docs/xflow/demo/custom-flow/customWidget.tsx
new file mode 100644
index 000000000..a5fe73558
--- /dev/null
+++ b/docs/xflow/demo/custom-flow/customWidget.tsx
@@ -0,0 +1,13 @@
+import { Input } from 'antd';
+
+const customWidget = ({ value, onChange }) => {
+
+ return (
+ onChange({ inputVal: e.target.value })}
+ />
+ );
+};
+
+export default customWidget;
diff --git a/docs/xflow/demo/custom-flow/index.tsx b/docs/xflow/demo/custom-flow/index.tsx
new file mode 100644
index 000000000..e3c50cf59
--- /dev/null
+++ b/docs/xflow/demo/custom-flow/index.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import XFlow from '@xrenders/xflow';
+import settings from './setting';
+import customWidget from './customWidget';
+
+export default () => {
+ const nodes = [
+ {
+ id: '1',
+ type: 'Start',
+ data: {
+ inputVal:'我是自定义组件'
+ },
+ position: {
+ x: 40,
+ y: 240,
+ }
+ },
+ {
+ id: '2',
+ type: 'End',
+ data: {},
+ position: {
+ x: 500,
+ y: 240,
+ }
+ }
+ ];
+
+ const edges = [
+ { source: '1', target: '2', id: '234123' }
+ ]
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/xflow/demo/custom-flow/setting.tsx b/docs/xflow/demo/custom-flow/setting.tsx
new file mode 100644
index 000000000..282e3dbc7
--- /dev/null
+++ b/docs/xflow/demo/custom-flow/setting.tsx
@@ -0,0 +1,141 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingWidget:"customWidget"
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+];
diff --git a/docs/xflow/demo/flow-provider/edges.ts b/docs/xflow/demo/flow-provider/edges.ts
new file mode 100644
index 000000000..d3f67bd08
--- /dev/null
+++ b/docs/xflow/demo/flow-provider/edges.ts
@@ -0,0 +1 @@
+export const edges = [{ source: '1', target: '2', id: '234123' }];
diff --git a/docs/xflow/demo/flow-provider/index.tsx b/docs/xflow/demo/flow-provider/index.tsx
new file mode 100644
index 000000000..39287d968
--- /dev/null
+++ b/docs/xflow/demo/flow-provider/index.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+
+import XFlow, { FlowProvider, useNodes } from '@xrenders/xflow';
+import { edges as initialEdges } from './edges';
+import { nodes as initialNodes } from './nodes';
+import settings from './setting';
+
+const App = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+function Sidebar() {
+ // This hook will only work if the component it's used in is a child of a
+ // .
+ const nodes = useNodes();
+
+ return (
+
+ );
+}
+
+export default () => {
+ return (
+
+
+
+ );
+};
diff --git a/docs/xflow/demo/flow-provider/nodes.ts b/docs/xflow/demo/flow-provider/nodes.ts
new file mode 100644
index 000000000..79e981129
--- /dev/null
+++ b/docs/xflow/demo/flow-provider/nodes.ts
@@ -0,0 +1,20 @@
+export const nodes = [
+ {
+ id: '1',
+ type: 'Start',
+ data: {},
+ position: {
+ x: 40,
+ y: 240,
+ },
+ },
+ {
+ id: '2',
+ type: 'End',
+ data: {},
+ position: {
+ x: 500,
+ y: 240,
+ },
+ },
+];
diff --git a/docs/xflow/demo/flow-provider/setting.tsx b/docs/xflow/demo/flow-provider/setting.tsx
new file mode 100644
index 000000000..72f080f69
--- /dev/null
+++ b/docs/xflow/demo/flow-provider/setting.tsx
@@ -0,0 +1,199 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ radio1: {
+ title: '点击单选',
+ type: 'string',
+ widget: 'radio',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ textarea1: {
+ title: '长文本',
+ type: 'string',
+ widget: 'textArea'
+ },
+ date1: {
+ title: '日期选择',
+ type: 'string',
+ widget: 'datePicker'
+ },
+ dateRange1: {
+ title: '日期范围',
+ type: 'range',
+ widget: 'dateRange'
+ },
+ time1: {
+ title: '时间选择',
+ type: 'string',
+ widget: 'timePicker'
+ },
+ timeRange1: {
+ title: '时间范围',
+ type: 'range',
+ widget: 'timeRange'
+ },
+ },
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+];
diff --git a/docs/xflow/demo/layout/LR/index.tsx b/docs/xflow/demo/layout/LR/index.tsx
new file mode 100644
index 000000000..c95655c45
--- /dev/null
+++ b/docs/xflow/demo/layout/LR/index.tsx
@@ -0,0 +1,65 @@
+import XFlow from '@xrenders/xflow';
+import settings from './setting';
+import React from 'react';
+
+export default () => {
+ const nodes = [
+ {
+ type: 'Start',
+ id: '1',
+ position: { x: -35, y: 268 },
+ },
+ {
+ type: 'Switch',
+ id: '2',
+ position: { x: 277.5, y: 268 },
+ },
+ {
+ type: 'Code',
+ id: '3',
+ position: { x: 675, y: 123.75 },
+ },
+ {
+ type: 'tool',
+ id: '4',
+ position: { x: 686.25, y: 495 },
+ },
+ {
+ type: 'End',
+ id: '5',
+ position: { x: 1176.2499999999998, y: 281.25 },
+ },
+ ];
+ const edges = [
+ { source: '1', target: '2', id: 'e1-2' },
+ {
+ source: '2',
+ target: '3',
+ id: 'e2-3',
+ },
+ { source: '2', target: '4', id: 'e2-4' },
+ {
+ source: '3',
+ target: '5',
+ id: 'e3-5',
+ },
+ {
+ source: '4',
+ target: '5',
+ id: 'e4-5',
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
diff --git a/docs/xflow/demo/layout/LR/setting.tsx b/docs/xflow/demo/layout/LR/setting.tsx
new file mode 100644
index 000000000..72f080f69
--- /dev/null
+++ b/docs/xflow/demo/layout/LR/setting.tsx
@@ -0,0 +1,199 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ radio1: {
+ title: '点击单选',
+ type: 'string',
+ widget: 'radio',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ textarea1: {
+ title: '长文本',
+ type: 'string',
+ widget: 'textArea'
+ },
+ date1: {
+ title: '日期选择',
+ type: 'string',
+ widget: 'datePicker'
+ },
+ dateRange1: {
+ title: '日期范围',
+ type: 'range',
+ widget: 'dateRange'
+ },
+ time1: {
+ title: '时间选择',
+ type: 'string',
+ widget: 'timePicker'
+ },
+ timeRange1: {
+ title: '时间范围',
+ type: 'range',
+ widget: 'timeRange'
+ },
+ },
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+];
diff --git a/docs/xflow/demo/layout/TB/index.tsx b/docs/xflow/demo/layout/TB/index.tsx
new file mode 100644
index 000000000..9b717eb3b
--- /dev/null
+++ b/docs/xflow/demo/layout/TB/index.tsx
@@ -0,0 +1,73 @@
+import XFlow from '@xrenders/xflow';
+import settings from './setting';
+import React from 'react';
+
+export default () => {
+ const nodes = [
+ {
+ type: 'Start',
+ id: '1',
+ position: { x: 327.5, y: -14.5 },
+ },
+ {
+ type: 'Switch',
+ id: '2',
+ position: { x: 328.75, y: 108 },
+ },
+ {
+ type: 'Code',
+ id: '3',
+ position: { x: 638.75, y: 247.5 },
+ },
+ {
+ type: 'tool',
+ id: '4',
+ position: { x: 75.00000000000003, y: 261.25 },
+ },
+ {
+ type: 'End',
+ id: '5',
+ position: { x: 360, y: 501.25 },
+ },
+ ];
+ const edges = [
+ {
+ source: '1',
+ target: '2',
+ id: 'e1-2',
+ },
+ {
+ source: '2',
+ target: '3',
+ id: 'e2-3',
+ },
+ {
+ source: '2',
+ target: '4',
+ id: 'e2-4',
+ },
+ {
+ source: '3',
+ target: '5',
+ id: 'e3-5',
+ },
+ {
+ source: '4',
+ target: '5',
+ id: 'e4-5',
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
diff --git a/docs/xflow/demo/layout/TB/setting.tsx b/docs/xflow/demo/layout/TB/setting.tsx
new file mode 100644
index 000000000..72f080f69
--- /dev/null
+++ b/docs/xflow/demo/layout/TB/setting.tsx
@@ -0,0 +1,199 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ radio1: {
+ title: '点击单选',
+ type: 'string',
+ widget: 'radio',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ textarea1: {
+ title: '长文本',
+ type: 'string',
+ widget: 'textArea'
+ },
+ date1: {
+ title: '日期选择',
+ type: 'string',
+ widget: 'datePicker'
+ },
+ dateRange1: {
+ title: '日期范围',
+ type: 'range',
+ widget: 'dateRange'
+ },
+ time1: {
+ title: '时间选择',
+ type: 'string',
+ widget: 'timePicker'
+ },
+ timeRange1: {
+ title: '时间范围',
+ type: 'range',
+ widget: 'timeRange'
+ },
+ },
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+];
diff --git a/docs/xflow/demo/nodeSetting/index.tsx b/docs/xflow/demo/nodeSetting/index.tsx
new file mode 100644
index 000000000..5deb83592
--- /dev/null
+++ b/docs/xflow/demo/nodeSetting/index.tsx
@@ -0,0 +1,219 @@
+import XFlow from '@xrenders/xflow';
+// import settings from './setting';
+import React from 'react';
+
+const settings = [
+ {
+ title: '开始', // 节点名称
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: { // 图标描述
+ type: 'icon-start', // icon-font
+ bgColor: '#17B26A', // 图标背景颜色
+ },
+ settingSchema: { // 自定义节点配置
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ radio1: {
+ title: '点击单选',
+ type: 'string',
+ widget: 'radio',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ textarea1: {
+ title: '长文本',
+ type: 'string',
+ widget: 'textArea'
+ },
+ date1: {
+ title: '日期选择',
+ type: 'string',
+ widget: 'datePicker'
+ },
+ dateRange1: {
+ title: '日期范围',
+ type: 'range',
+ widget: 'dateRange'
+ },
+ time1: {
+ title: '时间选择',
+ type: 'string',
+ widget: 'timePicker'
+ },
+ timeRange1: {
+ title: '时间范围',
+ type: 'range',
+ widget: 'timeRange'
+ },
+ },
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group', // 节点分组
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+]
+export default () => {
+ const nodes = [
+ {
+ type: 'Start',
+ id: '1',
+ position: { x: -35, y: 268 },
+ },
+ {
+ type: 'Switch',
+ id: '2',
+ position: { x: 277.5, y: 268 },
+ },
+ {
+ type: 'Code',
+ id: '3',
+ position: { x: 675, y: 123.75 },
+ },
+ {
+ type: 'tool',
+ id: '4',
+ position: { x: 686.25, y: 495 },
+ },
+ {
+ type: 'End',
+ id: '5',
+ position: { x: 1176.2499999999998, y: 281.25 },
+ },
+ ];
+ const edges = [
+ { source: '1', target: '2', id: 'e1-2' },
+ {
+ source: '2',
+ target: '3',
+ id: 'e2-3',
+ },
+ { source: '2', target: '4', id: 'e2-4' },
+ {
+ source: '3',
+ target: '5',
+ id: 'e3-5',
+ },
+ {
+ source: '4',
+ target: '5',
+ id: 'e4-5',
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
diff --git a/docs/xflow/demo/nodeSetting/setting.tsx b/docs/xflow/demo/nodeSetting/setting.tsx
new file mode 100644
index 000000000..72f080f69
--- /dev/null
+++ b/docs/xflow/demo/nodeSetting/setting.tsx
@@ -0,0 +1,199 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ radio1: {
+ title: '点击单选',
+ type: 'string',
+ widget: 'radio',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ textarea1: {
+ title: '长文本',
+ type: 'string',
+ widget: 'textArea'
+ },
+ date1: {
+ title: '日期选择',
+ type: 'string',
+ widget: 'datePicker'
+ },
+ dateRange1: {
+ title: '日期范围',
+ type: 'range',
+ widget: 'dateRange'
+ },
+ time1: {
+ title: '时间选择',
+ type: 'string',
+ widget: 'timePicker'
+ },
+ timeRange1: {
+ title: '时间范围',
+ type: 'range',
+ widget: 'timeRange'
+ },
+ },
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+];
diff --git a/docs/xflow/demo/nodeWidget/index.tsx b/docs/xflow/demo/nodeWidget/index.tsx
new file mode 100644
index 000000000..5acd4e865
--- /dev/null
+++ b/docs/xflow/demo/nodeWidget/index.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import XFlow from '@xrenders/xflow';
+import settings from './setting';
+import { Space, Tag, Typography } from 'antd';
+
+
+const customNodeWidget = ({ data }) => {
+ return
+
变量一:{data?.input}
+
变量二:{data?.select}
+
+}
+
+
+const endNodeWidget = ({ data }) => {
+ return
+ {data?.input}
+
+}
+
+
+const LLMNodeWidget = ({ data }) => {
+ const labels = Object.keys(data) || [];
+ return
+ {labels?.map(item => {data[item]})}
+
+}
+
+export default () => {
+ const nodes = [
+ { id: '1', type: 'Start', data: { input: '开始节点', select: "b" }, position: { x: 40, y: 240 } },
+ { id: '2', type: 'LLM', data: { input1: 'input1' }, position: { x: 400, y: 240 } },
+ { id: '3', type: 'End', data: { input: '通过nodeWidget自定义配置展示' }, position: { x: 800, y: 240 } }
+ ];
+
+ const edges = [
+ { source: '1', target: '2', id: '234123' },
+ { source: '2', target: '3', id: '56891' }
+ ]
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/xflow/demo/nodeWidget/setting.tsx b/docs/xflow/demo/nodeWidget/setting.tsx
new file mode 100644
index 000000000..26d8c6aca
--- /dev/null
+++ b/docs/xflow/demo/nodeWidget/setting.tsx
@@ -0,0 +1,90 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ },
+ nodeWidget:'customNodeWidget'
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '结束原因',
+ type: 'string',
+ widget: 'textArea',
+ },
+ }
+ },
+ nodeWidget: 'endNodeWidget',
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ settingSchema: {
+ type: 'object',
+ displayType: 'row',
+ labelCol: 6,
+ fieldCol:18,
+ properties: {
+ input1: {
+ title: 'Field A',
+ type: 'string',
+ },
+ input2: {
+ title: 'Field B',
+ type: 'string'
+ },
+ input3: {
+ title: 'Field C',
+ type: 'string'
+ },
+ input4: {
+ title: 'Field D',
+ type: 'string'
+ }
+ }
+ },
+ nodeWidget: 'LLMNodeWidget',
+ },
+];
diff --git a/docs/xflow/demo/switchNode/index.tsx b/docs/xflow/demo/switchNode/index.tsx
new file mode 100644
index 000000000..5a9c47fdd
--- /dev/null
+++ b/docs/xflow/demo/switchNode/index.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import XFlow from '@xrenders/xflow';
+import settings from './setting';
+
+export default () => {
+ const nodes = [
+ { id: '1', type: 'Switch', data: { input: '开始节点', select: "b" }, position: { x: 40, y: 240 } },
+ ];
+
+ const edges = []
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/xflow/demo/switchNode/setting.tsx b/docs/xflow/demo/switchNode/setting.tsx
new file mode 100644
index 000000000..1c790f4ef
--- /dev/null
+++ b/docs/xflow/demo/switchNode/setting.tsx
@@ -0,0 +1,29 @@
+export default [
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+];
diff --git a/docs/xflow/index.md b/docs/xflow/index.md
new file mode 100644
index 000000000..b11d9abd8
--- /dev/null
+++ b/docs/xflow/index.md
@@ -0,0 +1,81 @@
+---
+order: 0
+title: 开始使用
+mobile: false
+---
+
+
+

+
XFlow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+画布流程编排解决方案
+
+## 安装
+```shell
+npm i @xrenders/xflow --save
+```
+
+## 使用方式
+
+**函数组件**
+
+```jsx
+/**
+ * transform: true
+ * defaultShowCode: true
+ */
+import React from 'react';
+import XFlow from '@xrenders/xflow';
+import settings from './schema/settings';
+
+export default () => {
+ const nodes = [
+ {
+ id: '1', // 节点 ID
+ type: 'Start', // 节点类型
+ data: {}, // 节点配置数据
+ position: { // 节点画布坐标位置
+ x: 40,
+ y: 240,
+ }
+ },
+ {
+ id: '2',
+ type: 'End', // 节点类型
+ data: {}, // 节点配置数据
+ position: { // 节点画布坐标位置
+ x: 500,
+ y: 240,
+ }
+ }
+ ];
+
+ const edges = [
+ { source: '1', target: '2', id: '234123' }
+ ]
+
+ return (
+
+
+
+ );
+}
+```
diff --git a/docs/xflow/layout.md b/docs/xflow/layout.md
new file mode 100644
index 000000000..937ef17e7
--- /dev/null
+++ b/docs/xflow/layout.md
@@ -0,0 +1,19 @@
+---
+order: 5
+title: '画布布局'
+mobile: false
+group:
+ title: 最佳展示
+ order: 1
+---
+# 布局方向
+
+## LR布局
+从左到右布局
+
+
+
+## TB布局
+从上到下布局
+
+
diff --git a/docs/xflow/nodeBuildIn.md b/docs/xflow/nodeBuildIn.md
new file mode 100644
index 000000000..0df7c0a19
--- /dev/null
+++ b/docs/xflow/nodeBuildIn.md
@@ -0,0 +1,13 @@
+---
+order: 4
+title: '内置节点'
+mobile: false
+group:
+ title: 最佳展示
+ order: 2
+---
+# 内置节点
+
+## 条件内置节点
+SwitchNode
+
diff --git a/docs/xflow/nodeSetting.md b/docs/xflow/nodeSetting.md
new file mode 100644
index 000000000..ac9e72810
--- /dev/null
+++ b/docs/xflow/nodeSetting.md
@@ -0,0 +1,13 @@
+---
+order: 1
+title: '节点菜单配置'
+mobile: false
+group:
+ title: 最佳展示
+ order: 1
+---
+# 节点配置
+
+在`settings`属性中你可以自定义每个节点的名称、图标、描述以及配置方案。如果是分组节点,type只能为`_group`。
+
+
diff --git a/docs/xflow/schema/basic.ts b/docs/xflow/schema/basic.ts
new file mode 100644
index 000000000..fd9be2c96
--- /dev/null
+++ b/docs/xflow/schema/basic.ts
@@ -0,0 +1,165 @@
+export default [
+ {
+ "widget": "FPanel",
+ "style": {
+ "paddingTop": "20px",
+ "paddingLeft": "20px",
+ "paddingBottom": "20px",
+ "paddingRight": "20px",
+ "backgroundColor": "#ffffff",
+ "marginBottom": "12px"
+ },
+ "children": [
+ {
+ "widget": "FTitle",
+ "data": "基础信息"
+ },
+ {
+ "widget": "FDescriptions",
+ "column": 3,
+ "items": [
+ {
+ "label": "创建人",
+ "dataKey": "creator"
+ },
+ {
+ "label": "关联单据",
+ "dataKey": "relevanceCode"
+ },
+ {
+ "label": "单据备注",
+ "dataKey": "desc"
+ },
+ {
+ "label": "创建时间",
+ "dataKey": "create-time"
+ },
+ {
+ "label": "生效日期",
+ "dataKey": "effective-date"
+ },
+ {
+ "label": "描述项",
+ "showLevel": 1
+ }
+ ],
+ "style": {
+ "backgroundColor": "#ffffff",
+ "paddingTop": "0px",
+ "paddingLeft": "0px",
+ "paddingRight": "0px",
+ "paddingBottom": "0px"
+ },
+ "itemShowLevel": 1,
+ "getCompProps": "xxxx"
+ }
+ ]
+ },
+ {
+ "widget": "FTabs",
+ "items": [
+ {
+ "label": "负载均衡(SLB)",
+ "children": [
+ {
+ "widget": "FPanel",
+ "style": {
+ "paddingTop": "20px",
+ "paddingLeft": "20px",
+ "paddingBottom": "20px",
+ "paddingRight": "20px",
+ "backgroundColor": "#ffffff",
+ "marginBottom": "12px"
+ },
+ "children": [
+ {
+ "widget": "FTitle",
+ "data": "安全信息"
+ },
+ {
+ "widget": "FDescriptions",
+ "column": 2,
+ "items": [
+ {
+ "label": "安全构建名称",
+ "dataKey": "name"
+ },
+ {
+ "label": "所属应用",
+ "dataKey": "app"
+ },
+ {
+ "label": "构建模式",
+ "dataKey": "mode"
+ },
+ {
+ "label": "公网域名",
+ "dataKey": "yum"
+ },
+ {
+ "label": "保留计算实例",
+ "dataKey": "fore"
+ }
+ ],
+ "dataKey": "safety"
+ }
+ ]
+ },
+ {
+ "widget": "FPanel",
+ "style": {
+ "paddingTop": "20px",
+ "paddingLeft": "20px",
+ "paddingBottom": "20px",
+ "paddingRight": "20px",
+ "backgroundColor": "#ffffff",
+ "marginBottom": "12px"
+ },
+ "children": [
+ {
+ "widget": "FTitle",
+ "data": "操作日志"
+ },
+ {
+ "widget": "FTable",
+ "pagination": {
+ "pageSize": "3"
+ },
+ "style": {
+ "backgroundColor": "#ffffff"
+ },
+ "dataKey": "operLog",
+ "column": {
+ "type": {
+ "title": "操作类型",
+ "dataKey": "type"
+ },
+ "creator": {
+ "title": "操作人",
+ "dataKey": "creator"
+ },
+ "time": {
+ "title": "操作时间",
+ "dataKey": "time"
+ },
+ "result": {
+ "title": "执行结果",
+ "dataKey": "result"
+ },
+ "desc": {
+ "title": "备注",
+ "dataKey": "desc"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "label": "云服务器(ECS)",
+ "children": []
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/docs/xflow/schema/custom-settings.ts b/docs/xflow/schema/custom-settings.ts
new file mode 100644
index 000000000..58e00b49c
--- /dev/null
+++ b/docs/xflow/schema/custom-settings.ts
@@ -0,0 +1,150 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'customWidget',
+ },
+ }
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+];
diff --git a/docs/xflow/schema/settings.ts b/docs/xflow/schema/settings.ts
new file mode 100644
index 000000000..72f080f69
--- /dev/null
+++ b/docs/xflow/schema/settings.ts
@@ -0,0 +1,199 @@
+export default [
+ {
+ title: '开始',
+ type: 'Start',
+ hidden: true,
+ targetHandleHidden: true,
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ settingSchema: {
+ type: 'object',
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ radio1: {
+ title: '点击单选',
+ type: 'string',
+ widget: 'radio',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ textarea1: {
+ title: '长文本',
+ type: 'string',
+ widget: 'textArea'
+ },
+ date1: {
+ title: '日期选择',
+ type: 'string',
+ widget: 'datePicker'
+ },
+ dateRange1: {
+ title: '日期范围',
+ type: 'range',
+ widget: 'dateRange'
+ },
+ time1: {
+ title: '时间选择',
+ type: 'string',
+ widget: 'timePicker'
+ },
+ timeRange1: {
+ title: '时间范围',
+ type: 'range',
+ widget: 'timeRange'
+ },
+ },
+ },
+ },
+ {
+ title: '结束',
+ type: 'End',
+ hidden: true,
+ sourceHandleHidden: true,
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ settingSchema: {
+ type: "object",
+ properties: {
+ input: {
+ title: '变量一',
+ type: 'string',
+ widget: 'input',
+ },
+ select: {
+ title: '变量二',
+ type: 'string',
+ widget: 'select',
+ props: {
+ options: [
+ { label: 'a', value: 'a' },
+ { label: 'b', value: 'b' },
+ { label: 'c', value: 'c' },
+ ],
+ },
+ },
+ }
+ }
+ },
+ {
+ title: 'LLM',
+ type: 'LLM',
+ description: '调用大语言模型回答问题或者对自然语言进行处理',
+ icon: {
+ type: 'icon-model',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Prompt',
+ type: 'Prompt',
+ description: '通过精心设计提示词,提升大语言模型回答效果',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#17B26A',
+ },
+ },
+ {
+ title: '知识库',
+ type: 'knowledge',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#6172F3',
+ },
+ },
+ {
+ title: 'Switch',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-switch',
+ bgColor: '#06AED4',
+ },
+ },
+ {
+ title: 'HSF',
+ type: 'hsf',
+ description: '允许通过 HSF 协议发送服务器请求',
+ icon: {
+ type: 'icon-hsf',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: 'Http',
+ type: 'http',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#875BF7',
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '代码执行',
+ type: 'Code',
+ description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: '#2E90FA',
+ },
+ },
+ {
+ title: '工具',
+ type: 'tool',
+ description: '允许使用工具能力',
+ icon: {
+ type: 'icon-gongju',
+ bgColor: '#2E90FA',
+ },
+ },
+ ],
+ },
+];
diff --git a/packages/x-flow/.fatherrc.js b/packages/x-flow/.fatherrc.js
new file mode 100644
index 000000000..4d442af1f
--- /dev/null
+++ b/packages/x-flow/.fatherrc.js
@@ -0,0 +1,35 @@
+import copy from 'rollup-plugin-copy';
+
+export default {
+ cjs: 'babel',
+ esm: {
+ type: 'babel',
+ importLibToEs: true,
+ },
+ lessInBabelMode: true,
+ extraRollupPlugins: [
+ copy({
+ targets: [{ src: 'src/index.d.ts', dest: 'dist/' }],
+ }),
+ ],
+ extraBabelPlugins: [
+ [
+ 'import',
+ {
+ libraryName: 'antd',
+ libraryDirectory: 'es',
+ style: true,
+ },
+ 'antd',
+ ],
+ [
+ 'import',
+ {
+ libraryName: '@ant-design/icons',
+ libraryDirectory: 'lib/icons',
+ camel2DashComponentName: false,
+ },
+ '@ant-design/icons',
+ ],
+ ],
+};
diff --git a/packages/x-flow/CHANGELOG.md b/packages/x-flow/CHANGELOG.md
new file mode 100644
index 000000000..a8635cefa
--- /dev/null
+++ b/packages/x-flow/CHANGELOG.md
@@ -0,0 +1,2 @@
+# 更新日志
+
diff --git a/packages/x-flow/CONTRIBUTING.md b/packages/x-flow/CONTRIBUTING.md
new file mode 100644
index 000000000..b44c74a66
--- /dev/null
+++ b/packages/x-flow/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# 如何贡献代码
+
+欢迎给 FormRender 提优化建议,或者修复已有 Bug,共促其发展
+
+## Branch 管理
+
+```
+master
+ ↑
+dev <--- Develop/PR
+```
+
+- `dev` 分支
+ - 所有的开发均在 dev 分支进行
+ - 提 PR 时候请提交到 dev 分支
+- `master` 分支
+ - `master` 是稳定不改的分支,不会在上面进行代码开发
+ - 在 dev 分支 publish 后会 merge 到 master,同时打对应 tag
+
+## Commit 格式
+
+```
+[{action}] {description}
+```
+
+- `{action}`
+ - `+` 新增功能
+ - `!` 更新或者修复 bug
+ - `-` 移除功能
+- `{description}`
+ - 尽可能详细的描述就好
+
+for example:
+
+- [+] 列表选项新增拖拽功能
+- [!] 修复输入框长按闪烁的问题
+
+## 更多
+
+- 很推荐在提交 PR 前,先在钉钉群里进行讨论,已防止此功能已经有同学在开发了
+- 但是如果是想修复文档和明显代码错误,直接提交 PR 就好
+
+
diff --git a/packages/x-flow/LICENSE b/packages/x-flow/LICENSE
new file mode 100644
index 000000000..a68fd737b
--- /dev/null
+++ b/packages/x-flow/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-present XRender Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/x-flow/README.md b/packages/x-flow/README.md
new file mode 100644
index 000000000..c8988944a
--- /dev/null
+++ b/packages/x-flow/README.md
@@ -0,0 +1,126 @@
+
+

+
FormRender
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> 一站式中后台**表单解决方案**
+
+## 官网
+
+
+
+FormRender 是中后台开箱即用的表单解决方案,通过 JsonSchema 协议动态渲染表单。为了能切实承接日益复杂的表单场景需求,2.0 我们进行了底层重构。我们的目标是以强大的扩展能力对表单场景 100% 的覆盖支持,同时保持开发者能快速上手,并以表单编辑器、插件、自定义组件等一系列周边产品带来极致的开发体验。在开发 1.0 的道路上,我们做了一系列的取舍,详见[v2 升级方案](https://xrender.fun/form-render/migrate)
+
+## 安装
+
+FormRender 依赖 ant design,单独使用不要忘记同时安装 `antd`
+
+```shell
+npm i form-render --save
+```
+
+## 使用
+
+**最简使用 demo:**
+
+```jsx
+import React from 'react';
+import { Button } from 'antd';
+import FormRender, { connectForm } from 'form-render';
+
+const schema = {
+ type: 'object',
+ properties: {
+ input1: {
+ title: '简单输入框',
+ type: 'string',
+ required: true,
+ },
+ select1: {
+ title: '单选',
+ type: 'string',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ },
+ },
+};
+
+class Demo extends React.Component {
+ render() {
+ const { form } = this.props;
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default connectForm(Demo);
+```
+
+**对于函数组件,FormRender 提供了 `useForm` hooks, 书写更为灵活**
+
+```jsx
+import React from 'react';
+import { Button } from 'antd';
+import FormRender, { useForm } from 'form-render';
+
+const schema = {
+ type: 'object',
+ properties: {
+ input1: {
+ title: '简单输入框',
+ type: 'string',
+ required: true,
+ },
+ select1: {
+ title: '单选',
+ type: 'string',
+ props: {
+ options: [
+ { label: '早', value: 'a' },
+ { label: '中', value: 'b' },
+ { label: '晚', value: 'c' }
+ ]
+ }
+ }
+ }
+};
+
+const Demo = () => {
+ const form = useForm();
+ return (
+
+
+
+
+ );
+};
+
+export default Demo;
+```
\ No newline at end of file
diff --git a/packages/x-flow/package.json b/packages/x-flow/package.json
new file mode 100644
index 000000000..77aaf6a0d
--- /dev/null
+++ b/packages/x-flow/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@xrenders/xflow",
+ "version": "1.0.0-beta.2",
+ "description": "一款功能强大、易用灵活的流程编辑器框架,帮助你轻松构建复杂的工作流和流程产品",
+ "keywords": [
+ "xflow"
+ ],
+ "homepage": "https://xrender.fun/xflow",
+ "bugs": {
+ "url": "https://github.com/alibaba/x-render/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:alibaba/form-render.git"
+ },
+ "license": "MIT",
+ "contributors": [
+ {
+ "name": "lhbxs",
+ "email": "596850703@qq.com"
+ }
+ ],
+ "main": "lib/index.js",
+ "module": "es/index.js",
+ "files": [
+ "es",
+ "lib",
+ "package.json"
+ ],
+ "scripts": {
+ "beta": "npm publish --tag beta",
+ "build": "father-build",
+ "next": "npm publish --tag next",
+ "prepare": "npm run build",
+ "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
+ "postpublish": "git push --tags",
+ "test:ui": "vitest --ui"
+ },
+ "lint-staged": {
+ "*.{js,jsx,less,md,json}": [
+ "prettier --write"
+ ],
+ "*.ts?(x)": [
+ "prettier --parser=typescript --write"
+ ]
+ },
+ "dependencies": {
+ "@ant-design/icons": "^4.0.2",
+ "classnames": "^2.3.1",
+ "lodash-es": "^4.17.21",
+ "dayjs": "^1.11.7",
+ "ahooks": "^3.7.5",
+ "zustand": "^4.5.4",
+ "@xyflow/react": "^12.3.2",
+ "@remixicon/react": "^4.2.0",
+ "@dagrejs/dagre": "^1.1.3",
+ "zundo": "^2.1.0",
+ "use-context-selector": "^1.4.1",
+ "form-render": "^2.3.4"
+ },
+ "devDependencies": {
+ "deep-equal": "^2.0.3",
+ "rollup-plugin-copy": "^3.4.0"
+ },
+ "peerDependencies": {
+ "antd": "4.x || 5.x",
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ },
+ "gitHooks": {
+ "pre-commit": "lint-staged"
+ },
+ "sideEffect": false,
+ "publishConfig": {
+ "registry": "https://registry.npmjs.org/",
+ "access": "public"
+ }
+}
diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx
new file mode 100644
index 000000000..9f8e5e590
--- /dev/null
+++ b/packages/x-flow/src/XFlow.tsx
@@ -0,0 +1,290 @@
+import {
+ Background,
+ BackgroundVariant,
+ MarkerType,
+ ReactFlow,
+ useReactFlow,
+} from '@xyflow/react';
+import '@xyflow/react/dist/style.css';
+import { useEventListener, useMemoizedFn } from 'ahooks';
+import produce, { setAutoFreeze } from 'immer';
+import { debounce } from 'lodash';
+import type { FC } from 'react';
+import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
+import CandidateNode from './components/CandidateNode';
+import CustomEdge from './components/CustomEdge';
+import PanelContainer from './components/PanelContainer';
+import { useEventEmitterContextContext } from './models/event-emitter';
+
+import CustomNodeComponent from './components/CustomNode';
+import { useStore, useStoreApi } from './hooks/useStore';
+import { useTemporalStore } from './hooks/useTemporalStore';
+
+import Operator from './operator';
+import FlowProps from './types';
+import { transformNodes, uuid } from './utils';
+import autoLayoutNodes from './utils/autoLayoutNodes';
+
+import { shallow } from 'zustand/shallow';
+import NodeEditor from './components/NodeEditor';
+import { useFlow } from './hooks/useFlow';
+import './index.less';
+
+const CustomNode = memo(CustomNodeComponent);
+const edgeTypes = { buttonedge: memo(CustomEdge) };
+
+/***
+ *
+ * XFlow 入口
+ *
+ */
+const XFlow: FC = memo(props => {
+ const { initialValues, settings } = props;
+ const workflowContainerRef = useRef(null);
+ const store = useStoreApi();
+ const { zoomTo } = useReactFlow();
+ const {
+ layout,
+ nodes,
+ edges,
+ panOnDrag,
+ onNodesChange,
+ onEdgesChange,
+ onConnect,
+ setLayout,
+ setCandidateNode,
+ setMousePosition,
+ } = useStore(
+ state => ({
+ nodes: state.nodes,
+ edges: state.edges,
+ layout: state.layout,
+ panOnDrag: state.panOnDrag,
+ setLayout: state.setLayout,
+ setMousePosition: state.setMousePosition,
+ setCandidateNode: state.setCandidateNode,
+ onNodesChange: state.onNodesChange,
+ onEdgesChange: state.onEdgesChange,
+ onConnect: state.onConnect,
+ }),
+ shallow
+ );
+ const { setNodes, setEdges } = useFlow();
+ const [activeNode, setActiveNode] = useState(null);
+ const temporalStore = useTemporalStore();
+ useEffect(() => {
+ zoomTo(0.8);
+ setAutoFreeze(false);
+ return () => {
+ setAutoFreeze(true);
+ };
+ }, []);
+
+ useEffect(() => {
+ setLayout(props.layout);
+ // TODO: 默认关闭时间机器,可以向 zundo 贡献一个配置
+ temporalStore.pause();
+ setNodes(transformNodes(initialValues?.nodes));
+ setEdges(initialValues?.edges);
+ }, []);
+
+ useEventListener('keydown', e => {
+ if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey))
+ e.preventDefault();
+ if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey))
+ e.preventDefault();
+ if ((e.key === 'y' || e.key === 'Y') && (e.ctrlKey || e.metaKey))
+ e.preventDefault();
+ if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey))
+ e.preventDefault();
+ });
+
+ useEventListener('mousemove', e => {
+ const containerClientRect =
+ workflowContainerRef.current?.getBoundingClientRect();
+ if (containerClientRect) {
+ setMousePosition({
+ pageX: e.clientX,
+ pageY: e.clientY,
+ elementX: e.clientX - containerClientRect.left,
+ elementY: e.clientY - containerClientRect.top,
+ });
+ }
+ });
+
+ const { eventEmitter } = useEventEmitterContextContext();
+ eventEmitter?.useSubscription((v: any) => {
+ // 整理节点
+ if (v.type === 'auto-layout-nodes') {
+ const newNodes: any = autoLayoutNodes(store.getState().nodes, edges);
+ setNodes(newNodes);
+ }
+
+ if (v.type === 'deleteNode') {
+ setActiveNode(null);
+ }
+ });
+
+ // 新增节点
+ const handleAddNode = (data: any) => {
+ const newNode = {
+ id: uuid(),
+ type: 'custom',
+ data,
+ position: {
+ x: 0,
+ y: 0,
+ },
+ };
+ setCandidateNode(newNode);
+ };
+
+ // 插入节点
+ // const handleInsertNode = () => {
+ // const newNode = {
+ // id: uuid(),
+ // data: { label: 'new node' },
+ // position: {
+ // x: 0,
+ // y: 0,
+ // },
+ // };
+ // // record(() => {
+ // addNodes(newNode);
+ // addEdges({
+ // id: uuid(),
+ // source: '2',
+ // target: newNode.id,
+ // });
+ // const targetEdge = edges.find(edge => edge.source === '2');
+ // updateEdge(targetEdge?.id as string, {
+ // source: newNode.id,
+ // });
+ // // });
+ // };
+
+ // edge 移入/移出效果
+ const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => {
+ const newEdges = produce(edges, draft => {
+ const currEdge: any = draft.find(e => e.id === edge.id);
+ currEdge.style = {
+ ...edge.style,
+ stroke: color,
+ };
+ currEdge.markerEnd = {
+ ...edge?.markerEnd,
+ color,
+ };
+ });
+ setEdges(newEdges);
+ });
+
+ const handleNodeValueChange = debounce((data: any) => {
+ for (let node of nodes) {
+ if (node.id === data.id) {
+ node.data = {
+ ...node?.data,
+ ...data?.values,
+ };
+ break;
+ }
+ }
+ setNodes([...nodes]);
+ }, 200);
+
+ const nodeTypes = useMemo(() => {
+ return {
+ custom: (props: any) => {
+ const { data, id, ...rest } = props;
+ const { _nodeType, ...restData } = data || {};
+
+ return (
+ {
+ setActiveNode({ id, _nodeType, values: { ...restData } });
+ }}
+ />
+ );
+ },
+ };
+ }, [layout]);
+
+ // const edgeTypes = { buttonedge: (edgeProps: any) => };
+ // const { icon, description } =
+ // settings.find(
+ // item => item.type?.toLowerCase() === activeNode?.node?.toLowerCase()
+ // ) || {};
+
+ const NodeEditorWrap = useMemo(() => {
+ return (
+
+ );
+ }, [activeNode?.id]);
+
+ return (
+
+
{
+ onNodesChange(changes);
+ }}
+ onEdgesChange={changes => {
+ onEdgesChange(changes);
+ }}
+ onEdgeMouseEnter={(_, edge: any) => {
+ getUpdateEdgeConfig(edge, '#2970ff');
+ }}
+ onEdgeMouseLeave={(_, edge) => {
+ getUpdateEdgeConfig(edge, '#c9c9c9');
+ }}
+ >
+
+
+
+ {activeNode && (
+ setActiveNode(null)}
+ node={activeNode}
+ data={activeNode?.values}
+ // disabled
+ >
+ {NodeEditorWrap}
+
+ )}
+
+
+ );
+});
+
+export default XFlow;
diff --git a/packages/x-flow/src/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx
new file mode 100644
index 000000000..4e6bd6269
--- /dev/null
+++ b/packages/x-flow/src/components/CandidateNode/index.tsx
@@ -0,0 +1,72 @@
+import { useReactFlow, useViewport } from '@xyflow/react';
+import { useEventListener } from 'ahooks';
+import React, { memo } from 'react';
+import { shallow } from 'zustand/shallow';
+import { useStore } from '../../hooks/useStore';
+import CustomNode from '../CustomNode';
+
+const CandidateNode = () => {
+ const { zoom } = useViewport();
+ const reactflow = useReactFlow();
+ const { candidateNode, mousePosition, setCandidateNode, addNodes } = useStore(
+ (state: any) => ({
+ nodes: state.nodes,
+ edges: state.edges,
+ addNodes: state.addNodes,
+ candidateNode: state.candidateNode,
+ mousePosition: state.mousePosition,
+ setCandidateNode: state.setCandidateNode,
+ onNodesChange: state.onNodesChange,
+ onEdgesChange: state.onEdgesChange,
+ }),
+ shallow
+ );
+
+ useEventListener('click', ev => {
+ if (!candidateNode) {
+ return;
+ }
+ ev.preventDefault();
+ const { screenToFlowPosition } = reactflow;
+ const { x, y } = screenToFlowPosition({
+ x: mousePosition.pageX,
+ y: mousePosition.pageY,
+ });
+
+ const newNodes = {
+ ...candidateNode,
+ data: {
+ ...candidateNode.data,
+ _isCandidate: false,
+ },
+ position: { x, y },
+ };
+ // @ts-ignore
+ addNodes(newNodes);
+ setCandidateNode(null);
+ });
+
+ if (!candidateNode) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default memo(CandidateNode);
diff --git a/packages/x-flow/src/components/CustomEdge/index.less b/packages/x-flow/src/components/CustomEdge/index.less
new file mode 100644
index 000000000..9b41ecff7
--- /dev/null
+++ b/packages/x-flow/src/components/CustomEdge/index.less
@@ -0,0 +1,24 @@
+
+.custom-edge-line {
+ position: absolute;
+ z-index: 1000;
+ pointer-events: all;
+
+ .line-content {
+ width: 60px;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ }
+
+ .line-icon-box {
+ width: 16px;
+ height: 16px;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #296dff;
+ visibility: visible;
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx
new file mode 100644
index 000000000..975595e52
--- /dev/null
+++ b/packages/x-flow/src/components/CustomEdge/index.tsx
@@ -0,0 +1,138 @@
+import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
+import {
+ BezierEdge,
+ EdgeLabelRenderer,
+ getBezierPath,
+ useReactFlow,
+} from '@xyflow/react';
+import produce from 'immer';
+import React, { memo, useState } from 'react';
+import { shallow } from 'zustand/shallow';
+import { useStore } from '../../hooks/useStore';
+import { uuid } from '../../utils';
+import NodeSelectPopover from '../NodesPopover';
+import './index.less';
+
+export default memo((edge: any) => {
+ const { id, selected, sourceX, sourceY, targetX, targetY, source, target } =
+ edge;
+
+ const reactflow = useReactFlow();
+ const [isHovered, setIsHovered] = useState(false);
+ const [edgePath, labelX, labelY] = getBezierPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ });
+
+ const {
+ nodes,
+ edges,
+ setNodes,
+ setEdges,
+ mousePosition,
+ onEdgesChange,
+ layout,
+ } = useStore(
+ (state: any) => ({
+ layout: state.layout,
+ nodes: state.nodes,
+ edges: state.edges,
+ mousePosition: state.mousePosition,
+ setNodes: state.setNodes,
+ setEdges: state.setEdges,
+ onEdgesChange: state.onEdgesChange,
+ }),
+ shallow
+ );
+
+ const handleAddNode = (data: any) => {
+ const { screenToFlowPosition } = reactflow;
+ const { x, y } = screenToFlowPosition({
+ x: mousePosition.pageX,
+ y: mousePosition.pageY,
+ });
+
+ const targetId = uuid();
+ const newNodes = produce(nodes, (draft: any) => {
+ draft.push({
+ id: targetId,
+ type: 'custom',
+ data,
+ position: { x, y },
+ });
+ });
+
+ const newEdges = produce(edges, (draft: any) => {
+ draft.push(
+ ...[
+ {
+ id: uuid(),
+ source,
+ target: targetId,
+ },
+ {
+ id: uuid(),
+ source: targetId,
+ target,
+ },
+ ]
+ );
+ });
+
+ setNodes(newNodes);
+ setEdges(newEdges);
+ onEdgesChange([{ id, type: 'remove' }]);
+ };
+
+ let edgeExtra: any = {
+ sourceX: edge.sourceX - 15,
+ targetX: edge.targetX + 15,
+ };
+ if (layout === 'TB') {
+ edgeExtra = {
+ sourceY: edge.sourceY - 15,
+ targetY: edge.targetY + 13,
+ };
+ }
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
onEdgesChange([{ id, type: 'remove' }])}
+ >
+
+
+
+
+
+
+
+
+ )
+ }
+ />
+
+ );
+});
diff --git a/packages/x-flow/src/components/CustomHtml/index.tsx b/packages/x-flow/src/components/CustomHtml/index.tsx
new file mode 100644
index 000000000..64eda1582
--- /dev/null
+++ b/packages/x-flow/src/components/CustomHtml/index.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { Tooltip, Typography } from 'antd';
+import _ from 'lodash';
+import IconView from '../IconView';
+
+const { Text } = Typography;
+
+export default (props: any) => {
+ const { value, config, showEllipsis, width } = props;
+ const item = config?.[value];
+
+ if (showEllipsis && width) {
+ return (
+
+ {value}
+
+ );
+ }
+
+ if (config?.requiredByValue && props.addons.schemaPath.includes('[]')) {
+ // 根据当前行的数据判断是否必填
+ const values = props.addons.getValues();
+ const dataPath = props.addons.dataPath;
+ const record = _.get(values, dataPath.slice(0, dataPath.lastIndexOf('.')));
+ const pathName = dataPath.slice(dataPath.lastIndexOf('.') + 1);
+ // console.log('🚀 ~ record:', record, pathName);
+ // 获取当前行的数据
+
+ return (
+
+ {record?.required && config?.[pathName] === 'required' ? (
+ *
+ ) : (
+ ''
+ )}
+ {value}
+
+ );
+ }
+
+ if (!item) {
+ return value;
+ }
+ const { required, tooltip } = item;
+
+ return (
+
+ {required && (
+ *
+ )}
+ {item?.label || value}
+ {!!tooltip && (
+
+
+
+ )}
+
+ );
+};
diff --git a/packages/x-flow/src/components/CustomNode/index.less b/packages/x-flow/src/components/CustomNode/index.less
new file mode 100644
index 000000000..5c060dc6b
--- /dev/null
+++ b/packages/x-flow/src/components/CustomNode/index.less
@@ -0,0 +1,78 @@
+.xflow-node-container {
+ border: 2px solid #fff;
+ border-radius: 14px;
+
+ .react-flow__edge-path,
+ .react-flow__connection-path {
+ stroke: #d0d5dc;
+ stroke-width: 2px;
+ }
+
+ .react-flow__handle {
+ width: 30px;
+ height: 30px;
+ background: transparent;
+ border-radius: 0;
+ border: none;
+ }
+
+ .react-flow__handle::after {
+ content: '';
+ --tw-bg-opacity: 1;
+ background-color: #2970ff;
+ width: 2px;
+ height: 10px;
+ display: block;
+ margin: 11px 0 8px 15px;
+ }
+
+ .react-flow__handle:hover {
+ .xflow-node-add-box {
+ scale: 130%;
+ }
+ }
+
+ .xflow-node-add-box {
+ position: absolute;
+ top: 8px;
+ right: 9px;
+ width: 16px;
+ height: 16px;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #2970ff;
+ pointer-events: none;
+ transition: all 0.3s;
+ }
+
+ .xflow-node-switch-title{
+ position: absolute;
+ left: -45px;
+ top: 6px;
+ font-weight: 600;
+ text-align: right;
+ width: 35px;
+ }
+}
+
+.xflow-node-container-tb {
+ .react-flow__handle::after {
+ content: '';
+ --tw-bg-opacity: 1;
+ background-color: #2970ff;
+ width: 8px;
+ height: 2px;
+ display: block;
+ margin: 15px 0 0 12px;
+ }
+}
+
+.xflow-node-container-selected {
+ border: 2px solid #296dff;
+
+ .react-flow__handle::after {
+ display: none;
+ }
+}
diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx
new file mode 100644
index 000000000..bbff9b7fb
--- /dev/null
+++ b/packages/x-flow/src/components/CustomNode/index.tsx
@@ -0,0 +1,143 @@
+import { Handle, Position, useReactFlow } from '@xyflow/react';
+import classNames from 'classnames';
+import produce from 'immer';
+import React, { memo, useContext, useState } from 'react';
+import { shallow } from 'zustand/shallow';
+import { useStore } from '../../hooks/useStore';
+import { ConfigContext } from '../../models/context';
+import { capitalize, uuid } from '../../utils';
+import './index.less';
+import SourceHandle from './sourceHandle';
+
+const generateRandomArray = (x: number, type?: string) => {
+ const randomArray = [];
+ for (let i = 0; i < x; i++) {
+ const id = `id_${i}`;
+ if (type === 'Switch') {
+ if (i === 0) {
+ randomArray.push({ id, switchTitle: 'IF' });
+ } else if (i === x - 1) {
+ randomArray.push({ id, switchTitle: 'ELSE' });
+ } else {
+ randomArray.push({ id, switchTitle: 'ELIF' });
+ }
+ } else {
+ randomArray.push({ id });
+ }
+ }
+ return randomArray;
+};
+
+export default memo((props: any) => {
+ const { id, type, data, layout, isConnectable, selected, onClick } = props;
+ const { widgets, settingMap } = useContext(ConfigContext);
+ const NodeWidget =
+ widgets[`${capitalize(type)}Node`] || widgets['CommonNode'];
+ const [isHovered, setIsHovered] = useState(false);
+ const reactflow = useReactFlow();
+ const { edges, nodes, setNodes, setEdges, mousePosition } = useStore(
+ (state: any) => ({
+ nodes: state.nodes,
+ edges: state.edges,
+ mousePosition: state.mousePosition,
+ setNodes: state.setNodes,
+ setEdges: state.setEdges,
+ onEdgesChange: state.onEdgesChange,
+ }),
+ shallow
+ );
+ // data中的switchData的长度,即:if和if else 的数量,条件数量
+ const switchDataLength =
+ data?.switchData?.length >= 1 ? Number(data?.switchData?.length + 1) : 2;
+ const nodeHeight = 45; // 每次增长的节点高度
+ // 1.switch节点 if,else数量
+ const sourceHandleNum =
+ type === 'Switch'
+ ? generateRandomArray(switchDataLength, 'Switch')
+ : generateRandomArray(1);
+ const nodeMinHeight =
+ type === 'Switch' ? Number(switchDataLength * nodeHeight) : undefined;
+
+ // 增加节点并进行联系
+ const handleAddNode = (data: any) => {
+ const { screenToFlowPosition } = reactflow;
+ const { x, y } = screenToFlowPosition({
+ x: mousePosition.pageX + 100,
+ y: mousePosition.pageY + 100,
+ });
+ const targetId = uuid();
+
+ const newNodes = produce(nodes, (draft: any) => {
+ draft.push({
+ id: targetId,
+ type: 'custom',
+ data,
+ position: { x, y },
+ });
+ });
+ const newEdges = produce(edges, (draft: any) => {
+ draft.push({
+ id: uuid(),
+ source: id,
+ target: targetId,
+ });
+ });
+ setNodes(newNodes);
+ setEdges(newEdges);
+ };
+
+ let targetPosition = Position.Left;
+ let sourcePosition = Position.Right;
+ if (layout === 'TB') {
+ targetPosition = Position.Top;
+ sourcePosition = Position.Bottom;
+ }
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+ {!settingMap?.[type]?.targetHandleHidden && (
+
+ )}
+ onClick(data)}
+ nodeMinHeight={nodeMinHeight}
+ />
+ {!settingMap?.[type]?.sourceHandleHidden && (
+ <>
+ {(sourceHandleNum || [])?.map((item, key) => (
+
+ ))}
+ >
+ )}
+
+ );
+});
diff --git a/packages/x-flow/src/components/CustomNode/sourceHandle.tsx b/packages/x-flow/src/components/CustomNode/sourceHandle.tsx
new file mode 100644
index 000000000..9202b000b
--- /dev/null
+++ b/packages/x-flow/src/components/CustomNode/sourceHandle.tsx
@@ -0,0 +1,66 @@
+import { PlusOutlined } from '@ant-design/icons';
+import { Handle } from '@xyflow/react';
+import { Tooltip } from 'antd';
+import React, { memo, useRef, useState } from 'react';
+import NodeSelectPopover from '../NodesPopover';
+
+export default memo((props: any) => {
+ const {
+ position,
+ isConnectable,
+ selected,
+ isHovered,
+ handleAddNode,
+ id,
+ switchTitle,
+ ...rest
+ } = props;
+ const [isShowTooltip, setIsShowTooltip] = useState(false);
+ const [openNodeSelectPopover, setOpenNodeSelectPopover] = useState(false);
+ const popoverRef = useRef(null);
+
+ return (
+ setIsShowTooltip(true)}
+ onMouseLeave={() => setIsShowTooltip(false)}
+ onClick={e => {
+ e.stopPropagation();
+ popoverRef?.current?.changeOpen(true);
+ setIsShowTooltip(false);
+ setOpenNodeSelectPopover(true);
+ }}
+ id={id}
+ {...rest}
+ >
+ {(selected || isHovered || openNodeSelectPopover) && (
+ <>
+ {switchTitle && {switchTitle}
}
+
+
setOpenNodeSelectPopover(val)}
+ >
+
+
+
+
+
+ >
+ )}
+
+ );
+});
diff --git a/packages/x-flow/src/components/FAutoComplete/index.tsx b/packages/x-flow/src/components/FAutoComplete/index.tsx
new file mode 100644
index 000000000..5f1e86a0b
--- /dev/null
+++ b/packages/x-flow/src/components/FAutoComplete/index.tsx
@@ -0,0 +1,89 @@
+import React, { useEffect, useState } from 'react';
+import { AutoComplete, InputNumber } from 'antd';
+import _ from 'lodash';
+
+const FAutoComplete: React.FC = (props) => {
+ const {
+ value,
+ onChange,
+ placeholder,
+ optionList,
+ width = '100%',
+ disabled,
+ } = props;
+ const [options, setOptions] = useState<{ value: string }[]>(optionList);
+
+ useEffect(() => {
+ setOptions(optionList);
+ }, [optionList]);
+ const handleSearch = async (value: string) => {
+ if (!props.request) {
+ return;
+ }
+ const res = await props.request(value);
+ setOptions(res);
+ };
+
+ let customDisabled = false;
+ const dependValues = props.addons.dependValues;
+ if (dependValues) {
+ // 知识库组件的特殊处理
+ if (dependValues.length > 1) {
+ customDisabled = dependValues[1] === 'vector_weight' && dependValues[0];
+ }
+ if (dependValues[1] === 'vector_weight') {
+ const onNumberChange = (val: any) => {
+ let newValue: string | number = val;
+ if (val === null || val === undefined) {
+ newValue = '';
+ }
+ onChange(newValue);
+ };
+ return (
+
+ );
+ }
+ // if (
+ // ['search_attachment', 'with_associated_documents'].includes(
+ // dependValues[1],
+ // )
+ // ) {
+ // return (
+ // onChange(e.target.checked)}
+ // style={{ width }}
+ // disabled={disabled}
+ // />
+ // );
+ // }
+ }
+ const values = props.addons.getValues();
+ const dataPath = props.addons.dataPath;
+ const record = _.get(values, dataPath.slice(0, dataPath.lastIndexOf('.')));
+ const pathName = dataPath.slice(dataPath.lastIndexOf('.') + 1);
+
+ return (
+ {
+ onChange(val);
+ }}
+ disabled={disabled}
+ placeholder={placeholder}
+ />
+ );
+};
+export default FAutoComplete;
diff --git a/packages/x-flow/src/components/FlowProvider/index.tsx b/packages/x-flow/src/components/FlowProvider/index.tsx
new file mode 100644
index 000000000..ed34fcf5c
--- /dev/null
+++ b/packages/x-flow/src/components/FlowProvider/index.tsx
@@ -0,0 +1,44 @@
+import StoreContext, { Provider } from '../../models/context';
+import { createStore } from '../../models/store';
+
+import type { ReactNode } from 'react';
+import React, { memo, useContext, useState } from 'react';
+
+export const FlowProvider = memo<{
+ initialNodes: any[];
+ initialEdges: any[];
+ children: ReactNode;
+}>(({ initialNodes: nodes = [], initialEdges: edges = [], children }) => {
+ const [store] = useState(() =>
+ createStore({
+ nodes,
+ edges,
+ })
+ );
+
+ return {children};
+});
+
+export const FlowProviderWrapper = ({
+ children,
+ nodes,
+ edges,
+}: {
+ children: React.ReactNode;
+ nodes: any[];
+ edges: any[];
+}) => {
+ const isWrapped = useContext(StoreContext);
+
+ if (isWrapped) {
+ // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
+ // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
+ return <>{children}>;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/x-flow/src/components/IconView/index.tsx b/packages/x-flow/src/components/IconView/index.tsx
new file mode 100644
index 000000000..eb681076f
--- /dev/null
+++ b/packages/x-flow/src/components/IconView/index.tsx
@@ -0,0 +1,13 @@
+import { createFromIconfontCN } from '@ant-design/icons';
+
+/**
+ * icon 图标库
+ * 图标库资源变动需要更新 scriptUrl 资源路径
+ * https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=4201076
+ */
+
+const Icon = createFromIconfontCN({
+ scriptUrl: '//at.alicdn.com/t/a/font_4201076_frx3c9x07if.js',
+});
+
+export default Icon;
diff --git a/packages/x-flow/src/components/NodeContainer/index.less b/packages/x-flow/src/components/NodeContainer/index.less
new file mode 100644
index 000000000..c268b45a4
--- /dev/null
+++ b/packages/x-flow/src/components/NodeContainer/index.less
@@ -0,0 +1,38 @@
+.custom-node-container {
+ width: 240px;
+ padding: 13px 12px 5px 12px;
+ background: #fff;
+ border-radius: 12px;
+
+ .node-title {
+ display: flex;
+ height: 24px;
+ margin-bottom: 8px;
+ align-items: center;
+ font-size: 13px;
+ color: #1D2939;
+ span {
+ font-weight: bold;
+ }
+ }
+
+ .node-desc {
+ color: #676f83;
+ font-size: 12px;
+ padding-top: 4px;
+ margin-top: 10px;
+ }
+
+ .icon-box {
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .node-widget{
+ width: 100%;
+ }
+}
diff --git a/packages/x-flow/src/components/NodeContainer/index.tsx b/packages/x-flow/src/components/NodeContainer/index.tsx
new file mode 100644
index 000000000..5340bc2c1
--- /dev/null
+++ b/packages/x-flow/src/components/NodeContainer/index.tsx
@@ -0,0 +1,52 @@
+import { Typography } from 'antd';
+import classNames from 'classnames';
+import React, { memo } from 'react';
+import IconView from '../IconView';
+import './index.less';
+
+const { Text, Paragraph } = Typography;
+
+export default memo((props: any) => {
+ const { className, onClick, children, icon, title, desc, hideDesc, NodeWidget, nodeMinHeight } = props;
+
+ return (
+
+
+
+
+
+ {/* {title} */}
+
+ {title}
+
+
+
{children}
+ {/* {!hideDesc && !!desc &&
{desc}
} */}
+ {
+ NodeWidget &&
+ {NodeWidget}
+
+ }
+ {!hideDesc && !!desc && (
+
+ {desc}
+
+ )}
+
+ );
+});
diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx
new file mode 100644
index 000000000..91a75d60a
--- /dev/null
+++ b/packages/x-flow/src/components/NodeEditor/index.tsx
@@ -0,0 +1,107 @@
+import FormRender, { useForm } from 'form-render';
+import produce from 'immer';
+import { debounce } from 'lodash';
+import React, { FC, useContext, useEffect, useState } from 'react';
+import { shallow } from 'zustand/shallow';
+import { useStore } from '../../hooks/useStore';
+import { ConfigContext } from '../../models/context';
+
+interface INodeEditorProps {
+ data: any;
+ onChange: (data: any) => void;
+ nodeType: string;
+ id: string;
+}
+
+const NodeEditor: FC = (props: any) => {
+ const { data, onChange, nodeType, id } = props;
+ const form = useForm();
+ // // 1.获取节点配置信息
+ const { settingMap, widgets } = useContext(ConfigContext);
+ const nodeSetting = settingMap[nodeType] || {};
+ const [customVal, setCustomVal] = useState(data);
+ const CustomSettingWidget = widgets[`${nodeType}NodeSettingWidget`]; // 内置setting组件
+ const NodeWidget = widgets[nodeSetting?.settingWidget]; // 自定义面板配置组件
+
+ const { nodes, setNodes } = useStore(
+ (state: any) => ({
+ nodes: state.nodes,
+ setNodes: state.setNodes,
+ }),
+ shallow
+ );
+
+ useEffect(() => {
+ if (nodeSetting?.settingSchema) {
+ // 自定义Schema
+ form.resetFields();
+ form.setValues(data || {});
+ } else if (nodeSetting?.settingWidget) {
+ // 自定义组件
+ setCustomVal(data);
+ } else {
+ //可能为内置schema或者是没有
+ }
+ }, [JSON.stringify(data), id]);
+
+ const handleNodeValueChange = debounce((data: any) => {
+ const newNodes = produce(nodes, draft => {
+ let node = null;
+ // 反向查询ID,因为有多个ID相同的元素
+ for (let i = draft?.length - 1; i >= 0; i--) {
+ if (draft[i].id === id) {
+ node = draft[i];
+ break;
+ }
+ }
+ if (node) {
+ // 更新节点的 data
+ node.data = { ...node.data, ...data };
+ }
+ });
+ setNodes(newNodes);
+ }, 100);
+
+ const watch = {
+ '#': (allValues: any) => {
+ handleNodeValueChange({ ...allValues });
+ // onChange({ id, values: { ...allValues } });
+ },
+ };
+
+ if (nodeSetting?.settingWidget && NodeWidget) {
+ return (
+ {
+ setCustomVal(values);
+ handleNodeValueChange({ ...values });
+ }}
+ />
+ );
+ } else if (nodeSetting?.settingSchema) {
+ return (
+
+ );
+ } else if (CustomSettingWidget) {
+ // 内置节点
+ return (
+ {
+ handleNodeValueChange({ ...val });
+ }}
+ value={data}
+ />
+ );
+ } else {
+ return null;
+ }
+};
+
+export default NodeEditor;
diff --git a/packages/x-flow/src/components/NodesMenu/index.less b/packages/x-flow/src/components/NodesMenu/index.less
new file mode 100644
index 000000000..23131e0c0
--- /dev/null
+++ b/packages/x-flow/src/components/NodesMenu/index.less
@@ -0,0 +1,59 @@
+.xflow-node-menu{
+ min-height: 340px;
+ min-width: 150px;
+
+ .menu-group-title {
+ padding: 0 10px;
+ color: #667085;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 24px;
+ }
+
+ .menu-item {
+ height: 32px;
+ color: #101828;
+ padding: 0 10px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ .menu-item:hover {
+ background-color: #f9fafb;
+ border-radius: 8px;
+ }
+
+ .icon-box {
+ width: 20px;
+ height: 20px;
+ border-radius: 6px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+.xflow-node-menu-tooltip {
+ width: 200px;
+
+ .title {
+ color: #101828;
+ margin-top: 3px;
+ }
+
+ .description {
+ font-size: 12px;
+ font-weight: normal;
+ color: #101828;
+ }
+
+ .icon-box-max {
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/components/NodesMenu/index.tsx b/packages/x-flow/src/components/NodesMenu/index.tsx
new file mode 100644
index 000000000..d0281eff8
--- /dev/null
+++ b/packages/x-flow/src/components/NodesMenu/index.tsx
@@ -0,0 +1,138 @@
+
+import React, { forwardRef, Ref } from 'react';
+import { Popover, Input } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+import { useSet } from '../../utils/hooks';
+import IconView from '../IconView';
+import { TNodeMenu } from '../../types';
+import './index.less';
+
+// 检索节点
+const searchNodeList = (query: string, list: any[]) => {
+ if (!query) {
+ return list;
+ }
+ const searchTerm = query.toLowerCase();
+
+ function searchList(nodes: any, preResult = []) {
+ if (nodes.length === 0) {
+ return preResult;
+ }
+
+ const [currentNode, ...restNodes] = nodes;
+ let result: any = [...preResult];
+
+ if (currentNode.title.toLowerCase().includes(searchTerm)) {
+ result.push(currentNode);
+ } else if (currentNode?.type === '_group' && currentNode.items) {
+ const matchingItems = searchList(currentNode.items);
+ if (matchingItems.length > 0) {
+ result.push({ ...currentNode, items: matchingItems });
+ }
+ }
+ return searchList(restNodes, result);
+ }
+ return searchList(list);
+};
+
+// 悬浮菜单项详细描述
+const MenuTooltip = ({ icon, title, description }: any) => {
+ return (
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+ )
+};
+
+// 节点菜单项
+const MenuItem = (props: any) => {
+ const { title, type, icon, onClick } = props;
+ return (
+ }
+ placement='right'
+ arrow={false}
+ >
+
+
+
+
+ {title}
+
+
+ );
+};
+
+// 过滤 hidden 节点
+const filterHiddenMenu = (list: any) => {
+ return (list || []).filter((item: any) => !item.hidden)
+}
+
+/**
+ *
+ * 节点菜单List
+ *
+ */
+const NodesMenu = (props: TNodeMenu, ref: Ref) => {
+ const { items, showSearch, onClick } = props;
+
+ const [state, setState] = useSet({
+ menuList: [...items]
+ });
+ const { menuList } = state;
+
+ const handleItemClick = (type: string) => (ev: React.MouseEvent) => {
+ ev.stopPropagation();
+ onClick({ type });
+ }
+
+ const handleSearch = (ev: any) => {
+ setState({ menuList: searchNodeList(ev.target.value, items)})
+ };
+
+ return (
+
+ {!!showSearch && (
+
+ }
+ style={{ width: '100%' }}
+ />
+
+ )}
+
+ {filterHiddenMenu(menuList).map((item: any, index: number) => item.type === '_group' ? (
+
+
{item.title}
+ {filterHiddenMenu(item.items).map((data: any, index: number) => (
+
+ ))}
+
+ ) : (
+
+
+
+ ))}
+
+
+ );
+};
+
+export default forwardRef(NodesMenu);
diff --git a/packages/x-flow/src/components/NodesPopover/index.tsx b/packages/x-flow/src/components/NodesPopover/index.tsx
new file mode 100644
index 000000000..726ce81f3
--- /dev/null
+++ b/packages/x-flow/src/components/NodesPopover/index.tsx
@@ -0,0 +1,71 @@
+import { useClickAway } from 'ahooks';
+import { Popover } from 'antd';
+import React, {
+ forwardRef,
+ useCallback,
+ useContext,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from 'react';
+import { ConfigContext } from '../../models/context';
+import NodesMenu from '../NodesMenu';
+
+export default forwardRef((props: any, popoverRef) => {
+ const { addNode, children, onNodeSelectPopoverChange } = props;
+
+ const ref = useRef(null);
+ const closeRef: any = useRef(null);
+ const [open, setOpen] = useState(false);
+
+ const { settings, nodeSelector } = useContext(ConfigContext);
+ const { showSearch, popoverProps = { placement: 'top' } } =
+ nodeSelector || {};
+
+ useImperativeHandle(popoverRef, () => ({
+ changeOpen: val => {
+ setOpen(val);
+ },
+ }));
+
+ useClickAway(() => {
+ if (closeRef.current) {
+ setOpen(false);
+ onNodeSelectPopoverChange && onNodeSelectPopoverChange(false);
+ closeRef.current = false;
+ }
+ }, ref);
+
+ const handCreateNode = useCallback(({ type }) => {
+ addNode({ _nodeType: type });
+ setOpen(false);
+ onNodeSelectPopoverChange && onNodeSelectPopoverChange(false);
+ }, []);
+
+ return (
+ {
+ setTimeout(() => {
+ closeRef.current = true;
+ setOpen(true);
+ }, 50);
+ }}
+ content={
+
+ }
+ >
+ {children}
+
+ );
+});
diff --git a/packages/x-flow/src/components/PanelContainer/index.less b/packages/x-flow/src/components/PanelContainer/index.less
new file mode 100644
index 000000000..7679e2fa9
--- /dev/null
+++ b/packages/x-flow/src/components/PanelContainer/index.less
@@ -0,0 +1,125 @@
+.custom-node-panel {
+ .ant-drawer-content-wrapper {
+ top: 54px;
+ bottom: 14px;
+ right: 12px;
+ border-radius: 20px;
+ }
+
+ .ant-drawer-close {
+ display: none;
+ }
+
+ .ant-drawer-header {
+ padding: 24px 16px 0 16px;
+ }
+
+ .ant-drawer-content {
+ border-radius: 20px;
+ }
+
+ .ant-drawer-body {
+ padding: 12px 16px;
+ }
+
+ .title-box {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .ant-input-outlined {
+ border-color: #fff;
+ font-weight: 600;
+ }
+ }
+
+ .icon-box {
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 5px;
+ }
+
+ .title-actions {
+ display: flex;
+ align-items: center;
+ margin-left: 24px;
+ }
+
+ .desc-box {
+ font-size: 12px;
+ line-height: 32px;
+ font-weight: normal;
+ .ant-input-outlined {
+ border-color: #fff;
+ }
+ textarea {
+ margin: 12px 0;
+ }
+ }
+
+ .ant-input-outlined:focus-within {
+ border-color: #3b82f6 !important;
+ }
+
+ .fr-table-cell-content {
+ .ant-col {
+ padding: 0 !important;
+ }
+ }
+
+ .ant-collapse-content-box {
+ padding: 0 !important;
+ }
+
+ .item-collapse {
+ border: none;
+ background-color: #fff;
+
+ .ant-collapse-header {
+ background: none;
+ padding: 6px 0;
+ }
+
+ .ant-collapse-item {
+ border: none !important;
+ padding: 0;
+ }
+
+ .ant-collapse-content {
+ border: none;
+ }
+ }
+
+ .ant-collapse-header-text {
+ color: #354052;
+ font-weight: 600;
+ }
+
+ .ant-table-thead >tr>th {
+ font-size: 12px;
+ font-weight: normal;
+ }
+
+ .ant-table-tbody>tr>td {
+ padding: 4px !important;
+ }
+
+ input, select, textarea, .ant-select-selector {
+ font-size: 12px !important;
+ min-height: 30px;
+ }
+
+ input, select, .ant-select-selector {
+ height: 30px !important;
+ line-height: 30px !important;
+ }
+
+ .fr-table-list {
+ .ant-btn {
+ font-size: 12px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/components/PanelContainer/index.tsx b/packages/x-flow/src/components/PanelContainer/index.tsx
new file mode 100644
index 000000000..bc9778c59
--- /dev/null
+++ b/packages/x-flow/src/components/PanelContainer/index.tsx
@@ -0,0 +1,152 @@
+import { Divider, Drawer, Input, Space } from 'antd';
+import produce from 'immer';
+import { debounce } from 'lodash';
+import React, { FC, useContext, useEffect, useState } from 'react';
+import { shallow } from 'zustand/shallow';
+import { useStore } from '../../hooks/useStore';
+import { ConfigContext } from '../../models/context';
+import IconView from '../IconView';
+import './index.less';
+
+interface IPanelProps {
+ disabled?: boolean; // 是否禁用 ---to do:确认一下取的地方
+ nodeType: string;
+ onClose: () => void;
+ node?: { id: string; _isCandidate: boolean; _nodeType: string };
+ description?: string; // 业务描述---to do :确认一下取的地方---从data里面取?
+ children?: any;
+ id: string;
+ data: any; // data值
+}
+
+const getDescription = (nodeType: string, description: string) => {
+ if (nodeType === 'Input') {
+ return '工作流的起始节点,用于设定启动工作流入参信息';
+ }
+ if (nodeType === 'Output') {
+ return '工作流的最终节点,用于返回工作流运行后的出参信息';
+ }
+ return description || '';
+};
+
+const Panel: FC = (props: any) => {
+ // disabled属性取的地方可能不对------to do
+ const { onClose, children, nodeType, disabled, node, description, id, data } =
+ props;
+ // 1.获取节点配置信息
+ const { settingMap } = useContext(ConfigContext);
+ const nodeSetting = settingMap[nodeType] || {};
+ const { nodes, setNodes } = useStore(
+ (state: any) => ({
+ nodes: state.nodes,
+ setNodes: state.setNodes,
+ }),
+ shallow
+ );
+
+ const isDisabled = ['Input', 'Output'].includes(nodeType) || disabled;
+ const [descVal, setDescVal] = useState(data?.desc);
+ const [titleVal, setTitleVal] = useState(data?.title || nodeSetting?.title);
+
+ // const description = getDescription(nodeType, props.description);
+
+ const handleNodeValueChange = debounce((data: any) => {
+ const newNodes = produce(nodes, draft => {
+ let node = null;
+ // 反向查询ID,因为有多个ID相同的元素
+ for (let i = draft?.length - 1; i >= 0; i--) {
+ if (draft[i].id === id) {
+ node = draft[i];
+ break;
+ }
+ }
+ if (node) {
+ // 更新节点的 data
+ node.data = { ...node.data, ...data };
+ }
+ });
+ setNodes(newNodes);
+ }, 100);
+
+ useEffect(() => {
+ setDescVal(data?.desc);
+ setTitleVal(data?.title || nodeSetting?.title);
+ }, [JSON.stringify(data), id]);
+
+ return (
+
+
+
+
+
+
+ {isDisabled ? (
+ {nodeSetting?.title}
+ ) : (
+ {
+ setTitleVal(e.target.value);
+ handleNodeValueChange({ title: e.target.value });
+ }}
+ />
+ )}
+
+
+
+ {!isDisabled && (
+ <>
+
+
+ >
+ )}
+ {/* */}
+
+
+
+
+
+ {isDisabled ? (
+ description
+ ) : (
+ {
+ setDescVal(e.target.value);
+ handleNodeValueChange({ desc: e.target.value });
+ }}
+ />
+ )}
+
+ >
+ }
+ >
+ {children}
+
+ );
+};
+
+export default React.memo(Panel);
diff --git a/packages/x-flow/src/hooks/useEdges.ts b/packages/x-flow/src/hooks/useEdges.ts
new file mode 100644
index 000000000..f9c5a3eba
--- /dev/null
+++ b/packages/x-flow/src/hooks/useEdges.ts
@@ -0,0 +1,19 @@
+import type { Edge } from "@xyflow/react";
+import { shallow } from 'zustand/shallow';
+
+import { useStore } from './useStore';
+import type { FlowState } from '../models/store';
+
+const nodesSelector = (state: FlowState) => state.edges;
+
+/**
+ * Hook for getting the current edges from the store.
+ *
+ * @public
+ * @returns An array of edges
+ */
+export function useEdges(): Edge[] {
+ const nodes = useStore(nodesSelector, shallow) as EdgeType[];
+
+ return nodes;
+}
diff --git a/packages/x-flow/src/hooks/useFlow.ts b/packages/x-flow/src/hooks/useFlow.ts
new file mode 100644
index 000000000..baf2c8b49
--- /dev/null
+++ b/packages/x-flow/src/hooks/useFlow.ts
@@ -0,0 +1,40 @@
+import { useMemo } from 'react';
+import { useMemoizedFn } from 'ahooks';
+import { useStoreApi } from './useStore';
+import { useTemporalStore } from './useTemporalStore';
+import { FlowNode } from '../models/store';
+import { Edge } from '@xyflow/react';
+
+export const useFlow = () => {
+ const storeApi = useStoreApi();
+ const instance = storeApi.getState();
+ const temporalStore = useTemporalStore();
+
+ const getNodes = useMemoizedFn(() => storeApi.getState().nodes);
+ const getEdges = useMemoizedFn(() => storeApi.getState().edges);
+ const setNodes = useMemoizedFn((nodes: FlowNode[]) => {
+ temporalStore.record(() => {
+ storeApi.getState().setNodes(nodes);
+ })
+ })
+ const addNodes = useMemoizedFn((nodes: FlowNode[]) => {
+ temporalStore.record(() => {
+ storeApi.getState().addNodes(nodes);
+ })
+ })
+ const setEdges = useMemoizedFn((edges: Edge[]) => {
+ storeApi.getState().setEdges(edges);
+ })
+ const addEdges = useMemoizedFn((edges: Edge[]) => {
+ storeApi.getState().addEdges(edges);
+ })
+
+ return useMemo(() => ({
+ setNodes,
+ addNodes,
+ setEdges,
+ addEdges,
+ getNodes,
+ getEdges,
+ }), [instance]);
+};
diff --git a/packages/x-flow/src/hooks/useNodes.ts b/packages/x-flow/src/hooks/useNodes.ts
new file mode 100644
index 000000000..da54e91b4
--- /dev/null
+++ b/packages/x-flow/src/hooks/useNodes.ts
@@ -0,0 +1,18 @@
+import { shallow } from 'zustand/shallow';
+
+import { useStore } from '../hooks/useStore';
+import type { FlowNode, FlowState } from '../models/store';
+
+const nodesSelector = (state: FlowState) => state.nodes;
+
+/**
+ * Hook for getting the current nodes from the store.
+ *
+ * @public
+ * @returns An array of nodes
+ */
+export function useNodes(): NodeType[] {
+ const nodes = useStore(nodesSelector, shallow) as NodeType[];
+
+ return nodes;
+}
diff --git a/packages/x-flow/src/hooks/useStore.ts b/packages/x-flow/src/hooks/useStore.ts
new file mode 100644
index 000000000..7dc21dbb7
--- /dev/null
+++ b/packages/x-flow/src/hooks/useStore.ts
@@ -0,0 +1,45 @@
+import { useContext, useMemo } from 'react';
+import StoreContext from '../models/context';
+import { FlowNode, FlowState } from '../models/store';
+
+import { Edge } from '@xyflow/react';
+import { useStoreWithEqualityFn } from 'zustand/traditional';
+
+const useStore = (
+ selector: (state: FlowState) => T,
+ equalityFn?: (a: T, b: T) => boolean
+) => {
+ const store = useContext(StoreContext);
+
+ if (store === null) {
+ throw new Error(
+ '[XFlow]: Seems like you have not used zustand provider as an ancestor.'
+ );
+ }
+
+ return useStoreWithEqualityFn(store, selector, equalityFn);
+};
+
+const useStoreApi = <
+ NodeType extends FlowNode = FlowNode,
+ EdgeType extends Edge = Edge
+>() => {
+ const store = useContext(StoreContext);
+
+ if (store === null) {
+ throw new Error(
+ '[XFlow]: Seems like you have not used zustand provider as an ancestor.'
+ );
+ }
+ return useMemo(
+ () => ({
+ getState: store.getState,
+ setState: store.setState,
+ subscribe: store.subscribe,
+ temporal: store.temporal,
+ }),
+ [store]
+ );
+};
+
+export { useStore, useStoreApi };
diff --git a/packages/x-flow/src/hooks/useTemporalStore.ts b/packages/x-flow/src/hooks/useTemporalStore.ts
new file mode 100644
index 000000000..baa26653e
--- /dev/null
+++ b/packages/x-flow/src/hooks/useTemporalStore.ts
@@ -0,0 +1,22 @@
+import StoreContext from '../models/context';
+import { useContext } from 'react';
+
+export const useTemporalStore = () => {
+ const store = useContext(StoreContext);
+
+ if (store === null) {
+ throw new Error(
+ '[XFlow]: Seems like you have not used zustand provider as an ancestor.'
+ );
+ }
+
+ return {
+ ...store.temporal.getState(),
+ record: (callback: () => void) => {
+ const temporalStore = store.temporal.getState();
+ temporalStore.resume();
+ callback();
+ temporalStore.pause();
+ },
+ };
+};
diff --git a/packages/x-flow/src/index.less b/packages/x-flow/src/index.less
new file mode 100644
index 000000000..540913a0b
--- /dev/null
+++ b/packages/x-flow/src/index.less
@@ -0,0 +1,10 @@
+#xflow-container {
+ height: 100%;
+ width: 100%;
+ background: #F0F2F7;
+ position: relative;
+
+ .react-flow__attribution {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/index.ts b/packages/x-flow/src/index.ts
new file mode 100644
index 000000000..85dc9d99a
--- /dev/null
+++ b/packages/x-flow/src/index.ts
@@ -0,0 +1,15 @@
+import XFlow from './XFlow';
+import withProvider from './withProvider';
+
+import * as nodes from './nodes';
+
+export type {
+default as FR,
+} from './types';
+
+export { FlowProvider } from './components/FlowProvider';
+export { useFlow } from './hooks/useFlow';
+export { useNodes } from './hooks/useNodes';
+export { useEdges } from './hooks/useEdges';
+
+export default withProvider(XFlow, nodes);
diff --git a/packages/x-flow/src/models/context.ts b/packages/x-flow/src/models/context.ts
new file mode 100644
index 000000000..2930f6619
--- /dev/null
+++ b/packages/x-flow/src/models/context.ts
@@ -0,0 +1,8 @@
+import { createContext } from 'react';
+import { FlowStore } from './store';
+
+export const ConfigContext = createContext(null);
+
+const StoreContext = createContext(null);
+export const Provider = StoreContext.Provider;
+export default StoreContext;
diff --git a/packages/x-flow/src/models/event-emitter.tsx b/packages/x-flow/src/models/event-emitter.tsx
new file mode 100644
index 000000000..a62cf68cf
--- /dev/null
+++ b/packages/x-flow/src/models/event-emitter.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { useEventEmitter } from 'ahooks';
+import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
+import { createContext, useContext } from 'use-context-selector';
+
+const EventEmitterContext = createContext<{ eventEmitter: EventEmitter | null }>({
+ eventEmitter: null,
+})
+
+export const useEventEmitterContextContext = () => useContext(EventEmitterContext)
+
+type EventEmitterContextProviderProps = {
+ children: React.ReactNode
+}
+
+export const EventEmitterContextProvider = ({
+ children,
+}: EventEmitterContextProviderProps) => {
+ const eventEmitter = useEventEmitter()
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default EventEmitterContext
diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts
new file mode 100644
index 000000000..2f4833308
--- /dev/null
+++ b/packages/x-flow/src/models/store.ts
@@ -0,0 +1,131 @@
+import {
+ addEdge,
+ applyEdgeChanges,
+ applyNodeChanges,
+ Edge,
+ Node,
+ OnConnect,
+ OnEdgesChange,
+ OnNodesChange,
+} from '@xyflow/react';
+import isDeepEqual from 'fast-deep-equal';
+import { temporal } from 'zundo';
+import { createWithEqualityFn } from 'zustand/traditional';
+
+export type FlowProps = {
+ nodes?: Node[];
+ edges?: Edge[];
+ panOnDrag?: boolean;
+ layout?: 'LR' | 'TB';
+};
+
+export type FlowStore = ReturnType;
+
+export type FlowNode = Node;
+
+export type FlowState = {
+ layout?: 'LR' | 'TB';
+ nodes?: FlowNode[];
+ edges?: Edge[];
+ panOnDrag?: boolean;
+ candidateNode: any;
+ mousePosition: any;
+ onNodesChange: OnNodesChange;
+ onEdgesChange: OnEdgesChange;
+ onConnect: OnConnect;
+ setNodes: (nodes: FlowNode[]) => void;
+ setEdges: (edges: Edge[]) => void;
+ addNodes: (nodes: FlowNode[]) => void;
+ addEdges: (edges: Edge[]) => void;
+ setLayout: (layout: 'LR' | 'TB') => void;
+ setCandidateNode: (candidateNode: any) => void;
+ setMousePosition: (mousePosition: any) => void;
+};
+
+const createStore = (initProps?: Partial) => {
+ const DEFAULT_PROPS: FlowProps = {
+ layout: 'LR',
+ panOnDrag: true,
+ nodes: [],
+ edges: []
+ };
+
+ return createWithEqualityFn()(
+ temporal(
+ (set, get) => ({
+ ...DEFAULT_PROPS,
+ ...initProps,
+ candidateNode: null,
+ // nodeMenus: [],
+ mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 },
+ onNodesChange: changes => {
+ set({
+ nodes: applyNodeChanges(changes, get().nodes),
+ });
+ },
+ onEdgesChange: changes => {
+ set({
+ edges: applyEdgeChanges(changes, get().edges),
+ });
+ },
+ onConnect: connection => {
+ set({
+ edges: addEdge(connection, get().edges),
+ });
+ },
+ getNodes: () => {
+ return get().nodes;
+ },
+ setNodes: nodes => {
+ set({ nodes });
+ },
+ setEdges: edges => {
+ set({ edges });
+ },
+ getEdges: () => {
+ return get().nodes;
+ },
+ addNodes: payload => {
+ const newNodes = get().nodes.concat(payload);
+ set({ nodes: newNodes });
+ },
+ addEdges: payload => {
+ set({ edges: get().edges.concat(payload) });
+ },
+ // setNodeMenus: (nodeMenus: any) => {
+ // set({ nodeMenus });
+ // },
+ setCandidateNode: candidateNode => {
+ set({ candidateNode });
+ },
+ setMousePosition: (mousePosition: any) => {
+ set({ mousePosition });
+ },
+ setLayout: (layout: 'LR' | 'TB') => {
+ if (!layout) {
+ return;
+ }
+ set({ layout });
+ },
+ }),
+ {
+ // nodes 和 edges 是引用类型,所以使用深比较
+ equality: isDeepEqual,
+ // 偏函数
+ partialize: state => {
+ const { nodes, edges } = state;
+ return {
+ edges,
+ nodes,
+ };
+ },
+ onSave(pastState, currentState) {
+ console.log('onSave', pastState, currentState);
+ },
+ }
+ ),
+ Object.is
+ );
+};
+
+export { createStore };
diff --git a/packages/x-flow/src/nodes/index.less b/packages/x-flow/src/nodes/index.less
new file mode 100644
index 000000000..b4048db05
--- /dev/null
+++ b/packages/x-flow/src/nodes/index.less
@@ -0,0 +1,41 @@
+.node-container {
+ border: 2px solid #fff;
+ border-radius: 14px;
+
+ .react-flow__edge-path,
+ .react-flow__connection-path {
+ stroke: #d0d5dc;
+ stroke-width: 2px;
+ }
+}
+
+.node-container-selected {
+ border: 2px solid #296dff;
+
+ .react-flow__handle::after {
+ display: none;
+ }
+}
+
+.react-flow__handle {
+ width: 32px;
+ height: 32px;
+ background: transparent;
+ border-radius: 0;
+ border: none;
+
+ :hover {
+ border: 2px solid #00a952;
+ transform: scale(1.25);
+ }
+}
+
+.react-flow__handle::after {
+ content: '';
+ --tw-bg-opacity: 1;
+ background-color: #2970ff;
+ width: 8px;
+ height: 2px;
+ display: block;
+ margin: 15px 0 0 12px;
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/nodes/index.tsx b/packages/x-flow/src/nodes/index.tsx
new file mode 100644
index 000000000..18c684726
--- /dev/null
+++ b/packages/x-flow/src/nodes/index.tsx
@@ -0,0 +1,5 @@
+export { default as StartNode } from './node-start';
+export { default as EndNode } from './node-end';
+export { default as CommonNode } from './node-common';
+export { default as SwitchNode } from './node-switch';
+export { default as SwitchNodeSettingWidget } from './node-switch/setting';
diff --git a/packages/x-flow/src/nodes/node-common/index.less b/packages/x-flow/src/nodes/node-common/index.less
new file mode 100644
index 000000000..0377a0f36
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-common/index.less
@@ -0,0 +1,16 @@
+.custom-node-start {
+ width: 240px;
+ padding: 0 12px;
+ background: #fff;
+ border-radius: 12px;
+
+ .title {
+ display: flex;
+ height: 50px;
+ align-items: center;
+ span {
+ font-weight: bold;
+ margin-left: 8px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/nodes/node-common/index.tsx b/packages/x-flow/src/nodes/node-common/index.tsx
new file mode 100644
index 000000000..fa863ae3d
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-common/index.tsx
@@ -0,0 +1,26 @@
+import React, { memo, useContext } from 'react';
+import NodeContainer from '../../components/NodeContainer';
+import { ConfigContext } from '../../models/context';
+
+export default memo((props: any) => {
+ const { type, onClick, data } = props;
+ const { settingMap, widgets } = useContext(ConfigContext);
+ const nodeSetting = settingMap[type] || {};
+ const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined;
+
+ return (
+ : undefined}
+ />
+ );
+});
diff --git a/packages/x-flow/src/nodes/node-end/index.less b/packages/x-flow/src/nodes/node-end/index.less
new file mode 100644
index 000000000..0377a0f36
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-end/index.less
@@ -0,0 +1,16 @@
+.custom-node-start {
+ width: 240px;
+ padding: 0 12px;
+ background: #fff;
+ border-radius: 12px;
+
+ .title {
+ display: flex;
+ height: 50px;
+ align-items: center;
+ span {
+ font-weight: bold;
+ margin-left: 8px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/nodes/node-end/index.tsx b/packages/x-flow/src/nodes/node-end/index.tsx
new file mode 100644
index 000000000..890357a49
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-end/index.tsx
@@ -0,0 +1,26 @@
+import React, { memo, useContext } from 'react';
+import NodeContainer from '../../components/NodeContainer';
+import { ConfigContext } from '../../models/context';
+
+export default memo((props: any) => {
+ const { onClick, type, data } = props;
+ const { settingMap,widgets } = useContext(ConfigContext);
+ const nodeSetting = settingMap[type] || {};
+ const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined;
+
+ return (
+ : undefined}
+ />
+ );
+});
diff --git a/packages/x-flow/src/nodes/node-start/index.less b/packages/x-flow/src/nodes/node-start/index.less
new file mode 100644
index 000000000..0377a0f36
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-start/index.less
@@ -0,0 +1,16 @@
+.custom-node-start {
+ width: 240px;
+ padding: 0 12px;
+ background: #fff;
+ border-radius: 12px;
+
+ .title {
+ display: flex;
+ height: 50px;
+ align-items: center;
+ span {
+ font-weight: bold;
+ margin-left: 8px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/nodes/node-start/index.tsx b/packages/x-flow/src/nodes/node-start/index.tsx
new file mode 100644
index 000000000..923cadfd9
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-start/index.tsx
@@ -0,0 +1,26 @@
+import React, { memo, useContext } from 'react';
+import NodeContainer from '../../components/NodeContainer';
+import { ConfigContext } from '../../models/context';
+
+export default memo((props: any) => {
+ const { onClick, type, data } = props;
+ const { settingMap, widgets } = useContext(ConfigContext);
+ const nodeSetting = settingMap[type] || {};
+ const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined;
+
+ return (
+ : undefined}
+ />
+ );
+});
diff --git a/packages/x-flow/src/nodes/node-switch/SwitchBuildInNodeWidget.tsx b/packages/x-flow/src/nodes/node-switch/SwitchBuildInNodeWidget.tsx
new file mode 100644
index 000000000..e6efb73fa
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-switch/SwitchBuildInNodeWidget.tsx
@@ -0,0 +1,11 @@
+import { Space,Typography } from 'antd';
+import React, { memo } from 'react';
+
+export default memo((props: any) => {
+ const { data } = props;
+ return
+ {(data?.switchData || [])?.map(item =>
+ {item?.value}
+ )}
+
+});
diff --git a/packages/x-flow/src/nodes/node-switch/index.less b/packages/x-flow/src/nodes/node-switch/index.less
new file mode 100644
index 000000000..5a2c37ce3
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-switch/index.less
@@ -0,0 +1,22 @@
+.custom-node-start {
+ width: 240px;
+ padding: 0 12px;
+ background: #fff;
+ border-radius: 12px;
+
+ .title {
+ display: flex;
+ height: 50px;
+ align-items: center;
+ span {
+ font-weight: bold;
+ margin-left: 8px;
+ }
+ }
+}
+
+.custom-node-switch-setting{
+ .fr-list-simple,.ant-form-item,.fr-panel,.fr-inline-container{
+ width: 100%;
+ }
+}
diff --git a/packages/x-flow/src/nodes/node-switch/index.tsx b/packages/x-flow/src/nodes/node-switch/index.tsx
new file mode 100644
index 000000000..53824f8de
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-switch/index.tsx
@@ -0,0 +1,33 @@
+import React, { memo, useContext } from 'react';
+import NodeContainer from '../../components/NodeContainer';
+import { ConfigContext } from '../../models/context';
+import SwitchBuildInNodeWidget from './SwitchBuildInNodeWidget';
+
+export default memo((props: any) => {
+ const {
+ onClick,
+ type,
+ data,
+ nodeMinHeight
+ } = props;
+ const { settingMap, widgets } = useContext(ConfigContext);
+ const nodeSetting = settingMap[type] || {};
+ const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined;
+
+ return (
+ : }
+ nodeMinHeight={nodeMinHeight}
+ />
+ );
+});
diff --git a/packages/x-flow/src/nodes/node-switch/setting/index.tsx b/packages/x-flow/src/nodes/node-switch/setting/index.tsx
new file mode 100644
index 000000000..c4e3f9e01
--- /dev/null
+++ b/packages/x-flow/src/nodes/node-switch/setting/index.tsx
@@ -0,0 +1,59 @@
+import FormRender, { Schema, useForm } from 'form-render';
+import React, { memo } from 'react';
+import '../index.less'
+
+interface INodeSwitchSettingPorps {
+ onChange: (val: any) => void;
+ value: any;
+}
+
+const schema: Schema = {
+ type: 'object',
+ span: 24,
+ displayType: "row",
+ properties: {
+ switchData: {
+ type: 'array',
+ widget: 'simpleList',
+ display:"block",
+ props: {
+ hideCopy: true,
+ hideMove: true,
+ hideDelete: true,
+ },
+ items: {
+ type: 'object',
+ properties: {
+ value: {
+ title: '条件',
+ type: 'string',
+ },
+ },
+ },
+ },
+ },
+};
+
+export default memo((props: INodeSwitchSettingPorps) => {
+ const form = useForm();
+ const { onChange, value } = props;
+
+ const watch = {
+ '#': (allValues: any) => {
+ onChange({ ...allValues });
+ },
+ };
+
+ return (
+ {
+ form.setValues(value || {});
+ }}
+ className='custom-node-switch-setting'
+ />
+ );
+});
diff --git a/packages/x-flow/src/operator/Control/index.less b/packages/x-flow/src/operator/Control/index.less
new file mode 100644
index 000000000..bf99b50e9
--- /dev/null
+++ b/packages/x-flow/src/operator/Control/index.less
@@ -0,0 +1,48 @@
+.fai-reactflow-control {
+ display: flex;
+ align-items: center;
+ padding: 2px 1px;
+ border-radius: 8px;
+ border: 0.5px solid #f3f4f6;
+ background-color: #fff;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ color: #667085;
+ margin-left: 10px;
+}
+
+.control-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+
+ &.inactive:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+ color: #374151;
+ }
+
+ &.active {
+ background-color: #ecfdf5;
+ color: #10b981;
+ }
+
+ &.disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+}
+
+.icon {
+ width: 1rem;
+ height: 1rem;
+}
+
+.separator {
+ margin: 0 0.75rem;
+ width: 1px;
+ height: 0.875rem;
+ background-color: #e5e7eb;
+}
diff --git a/packages/x-flow/src/operator/Control/index.tsx b/packages/x-flow/src/operator/Control/index.tsx
new file mode 100644
index 000000000..cb8d9da65
--- /dev/null
+++ b/packages/x-flow/src/operator/Control/index.tsx
@@ -0,0 +1,79 @@
+import type { MouseEvent } from 'react';
+import React, { memo } from 'react';
+import {
+ RiCursorLine,
+ RiFunctionAddLine,
+ RiHand,
+ RiStickyNoteAddLine,
+} from '@remixicon/react';
+import { Tooltip, Button } from 'antd';
+import IconView from '../../components/IconView';
+import { useEventEmitterContextContext } from '../../models/event-emitter';
+import NodeSelectPopover from '../../components/NodesPopover';
+import { useStoreApi, useStore } from '../../hooks/useStore';
+
+import './index.less';
+
+const Control = (props: any) => {
+ const { addNode } = props;
+
+ const addNote = (e: MouseEvent) => {
+ e.stopPropagation();
+ };
+ const storeApi = useStoreApi();
+ const panOnDrag = useStore(s => s.panOnDrag);
+
+ const { eventEmitter } = useEventEmitterContextContext()
+
+ const handleInteractionModeChange = (panOnDrag) => {
+ storeApi.setState({ panOnDrag })
+ }
+
+ return (
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+ panOnDrag &&handleInteractionModeChange(false)}>
+
+
+
+
+ !panOnDrag && handleInteractionModeChange(true)}>
+
+
+
+
+
+ }
+ onClick={() => {
+ eventEmitter?.emit({ type: 'auto-layout-nodes' } as any)
+ }}
+ />
+
+
+ )
+};
+
+export default memo(Control);
diff --git a/packages/x-flow/src/operator/UndoRedo/index.less b/packages/x-flow/src/operator/UndoRedo/index.less
new file mode 100644
index 000000000..5994dc756
--- /dev/null
+++ b/packages/x-flow/src/operator/UndoRedo/index.less
@@ -0,0 +1,11 @@
+.fai-reactflow-undoredo {
+ display: flex;
+ align-items: center;
+ padding: 2px 1px;
+ border-radius: 8px;
+ border: 0.5px solid #f3f4f6;
+ background-color: #ffffff;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ color: #667085;
+ margin-left: 10px;
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/operator/UndoRedo/index.tsx b/packages/x-flow/src/operator/UndoRedo/index.tsx
new file mode 100644
index 000000000..2404b01b4
--- /dev/null
+++ b/packages/x-flow/src/operator/UndoRedo/index.tsx
@@ -0,0 +1,34 @@
+import React, { memo } from 'react';
+import { Button, Tooltip } from 'antd';
+import IconView from '../../components/IconView';
+import './index.less';
+
+export type UndoRedoProps = {
+ handleUndo: () => void;
+ handleRedo: () => void;
+ pastStates: any[];
+ futureStates: any[];
+};
+
+export default memo(({ handleUndo, handleRedo, pastStates, futureStates }: UndoRedoProps) => {
+ return (
+
+
+ }
+ onClick={handleUndo}
+ disabled={!pastStates?.length}
+ />
+
+
+ }
+ onClick={handleRedo}
+ disabled={!futureStates?.length}
+ />
+
+
+ );
+})
diff --git a/packages/x-flow/src/operator/ZoomInOut/index.less b/packages/x-flow/src/operator/ZoomInOut/index.less
new file mode 100644
index 000000000..98b4b5a6c
--- /dev/null
+++ b/packages/x-flow/src/operator/ZoomInOut/index.less
@@ -0,0 +1,35 @@
+.fai-reactflow-zoominout {
+ display: flex;
+ align-items: center;
+ padding: 2px 1px;
+ border-radius: 8px;
+ border: 0.5px solid #f3f4f6;
+ background-color: #ffffff;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ color: #667085;
+}
+
+.fai-reactflow-zoom-select {
+
+ .parting-line {
+ height: 1px;
+ background-color: #F3F4F6;
+ }
+
+ .zoom-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+ height: 2rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ color: #374151;
+
+ &:hover {
+ background-color: #F9FAFB;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/operator/ZoomInOut/index.tsx b/packages/x-flow/src/operator/ZoomInOut/index.tsx
new file mode 100644
index 000000000..4c56c2d54
--- /dev/null
+++ b/packages/x-flow/src/operator/ZoomInOut/index.tsx
@@ -0,0 +1,154 @@
+import type { FC } from 'react';
+import React, { Fragment, memo } from 'react';
+import { Button, Popover, Tooltip } from 'antd';
+import { useReactFlow, useViewport } from '@xyflow/react';
+import { getKeyboardKeyNameBySystem } from '../../utils';
+import ShortcutsName from './shortcuts-name';
+import IconView from '../../components/IconView';
+import './index.less';
+
+enum ZoomType {
+ zoomIn = 'zoomIn',
+ zoomOut = 'zoomOut',
+ zoomToFit = 'zoomToFit',
+ zoomTo25 = 'zoomTo25',
+ zoomTo50 = 'zoomTo50',
+ zoomTo75 = 'zoomTo75',
+ zoomTo100 = 'zoomTo100',
+ zoomTo200 = 'zoomTo200'
+};
+
+const ZOOM_IN_OUT_OPTIONS = [
+ [
+ {
+ key: ZoomType.zoomTo200,
+ text: '200%',
+ },
+ {
+ key: ZoomType.zoomTo100,
+ text: '100%',
+ },
+ {
+ key: ZoomType.zoomTo75,
+ text: '75%',
+ },
+ {
+ key: ZoomType.zoomTo50,
+ text: '50%',
+ },
+ {
+ key: ZoomType.zoomTo25,
+ text: '25%',
+ },
+ ],
+ [
+ {
+ key: ZoomType.zoomToFit,
+ text: '自适应视图',
+ },
+ ]
+];
+
+const ZoomSelect = ({ handleZoom }: any) => {
+ return (
+
+ {ZOOM_IN_OUT_OPTIONS.map((options, i) => (
+
+ {i !== 0 && }
+
+ {
+ options.map(option => (
+
handleZoom(option.key)}
+ >
+ {option.text}
+ {
+ option.key === ZoomType.zoomToFit && (
+
+ )
+ }
+ {
+ option.key === ZoomType.zoomTo50 && (
+
+ )
+ }
+ {
+ option.key === ZoomType.zoomTo100 && (
+
+ )
+ }
+
+ ))
+ }
+
+
+ ))}
+
+ )
+};
+
+
+const ZoomInOut: FC = () => {
+ const {
+ zoomIn,
+ zoomOut,
+ zoomTo,
+ fitView,
+ } = useReactFlow();
+
+ const { zoom } = useViewport();
+
+ const handleZoom = (type: string) => {
+ if (type === ZoomType.zoomToFit)
+ fitView()
+
+ if (type === ZoomType.zoomTo25)
+ zoomTo(0.25)
+
+ if (type === ZoomType.zoomTo50)
+ zoomTo(0.5)
+
+ if (type === ZoomType.zoomTo75)
+ zoomTo(0.75)
+
+ if (type === ZoomType.zoomTo100)
+ zoomTo(1)
+
+ if (type === ZoomType.zoomTo200)
+ zoomTo(2)
+ };
+
+ return (
+
+
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ zoomOut();
+ }}
+ />
+
+
+
} zIndex={1000} trigger='click'>
+ {parseFloat(`${zoom * 100}`).toFixed(0)}%
+
+
+
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ zoomIn();
+ }}
+ />
+
+
+ )
+};
+
+export default memo(ZoomInOut);
diff --git a/packages/x-flow/src/operator/ZoomInOut/shortcuts-name.tsx b/packages/x-flow/src/operator/ZoomInOut/shortcuts-name.tsx
new file mode 100644
index 000000000..303e43359
--- /dev/null
+++ b/packages/x-flow/src/operator/ZoomInOut/shortcuts-name.tsx
@@ -0,0 +1,32 @@
+import React, { memo } from 'react'
+import { getKeyboardKeyNameBySystem } from '../../utils'
+import cn from 'classnames'
+
+type ShortcutsNameProps = {
+ keys: string[]
+ className?: string
+}
+const ShortcutsName = ({
+ keys,
+ className,
+}: ShortcutsNameProps) => {
+ return (
+
+ {
+ keys.map(key => (
+
+ {getKeyboardKeyNameBySystem(key)}
+
+ ))
+ }
+
+ )
+}
+
+export default memo(ShortcutsName)
diff --git a/packages/x-flow/src/operator/index.less b/packages/x-flow/src/operator/index.less
new file mode 100644
index 000000000..d2cc387c0
--- /dev/null
+++ b/packages/x-flow/src/operator/index.less
@@ -0,0 +1,26 @@
+.fai-reactflow-operator {
+ position: absolute;
+ left: 4px;
+ bottom: 14px;
+ height: 50px;
+
+ .mini-map {
+ position: absolute;
+ left: 12px;
+ bottom: 45px;
+ margin: 0;
+ border: 0.5px solid rgba(0, 0, 0, 0.08);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ z-index: 9;
+ }
+
+ .operator-section {
+ display: flex;
+ align-items: center;
+ position: absolute;
+ left: 10px;
+ bottom: 4px;
+ z-index: 9;
+ }
+}
diff --git a/packages/x-flow/src/operator/index.tsx b/packages/x-flow/src/operator/index.tsx
new file mode 100644
index 000000000..e30cf1b02
--- /dev/null
+++ b/packages/x-flow/src/operator/index.tsx
@@ -0,0 +1,27 @@
+import React, { memo } from 'react';
+// import UndoRedo from '../header/undo-redo'
+import ZoomInOut from './ZoomInOut';
+import UndoRedo from './UndoRedo';
+import Control from './Control';
+
+import './index.less';
+import { useTemporalStore } from '../hooks/useTemporalStore';
+
+export type OperatorProps = {
+ addNode: any;
+}
+
+const Operator = ({ addNode }: OperatorProps) => {
+ const { undo, redo, pastStates, futureStates } = useTemporalStore();
+ return (
+
+
+
+ undo()} handleRedo={() => redo()} pastStates={pastStates} futureStates={futureStates} />
+
+
+
+ );
+}
+
+export default memo(Operator)
diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts
new file mode 100644
index 000000000..c23c315bc
--- /dev/null
+++ b/packages/x-flow/src/types.ts
@@ -0,0 +1,46 @@
+import { Schema } from 'form-render';
+import React from 'react';
+export interface TNodeItem {
+ title: string; // 节点 title
+ type: string; // 节点类型 _group 比较te
+ description?: string; // 节点描述
+ hidden?: boolean; // 是否可见
+ icon: {
+ type: string;
+ bgColor: string;
+ };
+ settingSchema?: Schema; // 节点的配置schema(弹窗) string为自定义组件
+ settingWidget?: string; // 自定义组件
+ hideDesc?: boolean;// 隐藏业务描述
+}
+
+export interface TNodeGroup {
+ title: string; // 节点 title
+ type: '_group';
+ items: TNodeItem[];
+}
+
+export interface TNodeMenu {
+ ref: React.RefObject; // 可选的 ref 属性
+ showSearch: boolean; // 配置是否可搜索
+ items: (TNodeGroup | TNodeItem)[];
+ onClick: ({}: { type: string }) => void;
+}
+
+export interface TNodeSelector {
+ showSearch: boolean; // 配置是否可搜索
+ items: (TNodeGroup | TNodeItem)[];
+}
+
+export interface FlowProps {
+ initialValues: {
+ nodes: any[];
+ edges: any;
+ };
+ layout: 'LR' | 'TB';
+ widgets: any; // 自定义组件
+ settings: (TNodeGroup | TNodeItem)[]; // 节点配置
+ nodeSelector: TNodeSelector;
+}
+
+export default FlowProps;
diff --git a/packages/x-flow/src/utils/autoLayoutNodes.ts b/packages/x-flow/src/utils/autoLayoutNodes.ts
new file mode 100644
index 000000000..ad2eab0f3
--- /dev/null
+++ b/packages/x-flow/src/utils/autoLayoutNodes.ts
@@ -0,0 +1,68 @@
+import produce from 'immer';
+import dagre from '@dagrejs/dagre';
+import { cloneDeep } from 'lodash-es'
+
+export const CUSTOM_NODE = 'custom';
+export const CUSTOM_EDGE = 'custom';
+
+export const getLayoutByDagre = (originNodes: any[], originEdges: any[]) => {
+ const dagreGraph = new dagre.graphlib.Graph()
+ dagreGraph.setDefaultEdgeLabel(() => ({}))
+
+ const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
+ const edges = cloneDeep(originEdges).filter(edge => !edge.data?.isInIteration)
+ dagreGraph.setGraph({
+ ranker: 'network-simplex', // 节点分层算法,可选:'tight-tree' 'longest-path' 'network-simplex'
+ rankdir: 'TB', // 图的延展方向,可选: 'TB' | 'BT' | 'LR' | 'RL'
+ nodesep: 150, // 同层各个节点之间的间距
+ ranksep: 150, // 图的各个层次之间的间距
+ // align: '', // 节点对齐方式,可选:'UL' | 'UR' | 'DL' | 'DR' | undefined
+ });
+
+ nodes.forEach((node) => {
+ dagreGraph.setNode(node.id, {
+ width: node.width || 204,
+ height: node.height || 45,
+ });
+ });
+
+ edges.forEach((edge) => {
+ dagreGraph.setEdge(edge.source, edge.target)
+ });
+
+ dagre.layout(dagreGraph);
+ return dagreGraph;
+}
+
+export default (nodes: any, edges: any) => {
+ const layout = getLayoutByDagre(nodes, edges);
+ const rankMap: any = {} as Record;
+
+ nodes.forEach((node: any) => {
+ if (!node.parentId && node.type === CUSTOM_NODE) {
+ const rank: any = layout.node(node.id).rank!
+
+ if (!rankMap[rank]) {
+ rankMap[rank] = node
+ }
+ else {
+ if (rankMap[rank].position.y > node.position.y)
+ rankMap[rank] = node
+ }
+ }
+ });
+
+ const newNodes = produce(nodes, (draft: any) => {
+ draft.forEach((node: any) => {
+ if (!node.parentId && node.type === CUSTOM_NODE) {
+ const nodeWithPosition = layout.node(node.id)
+ node.position = {
+ x: nodeWithPosition.x - (node.width || 204) / 2,
+ y: nodeWithPosition.y - (node.height || 45) / 2 + (rankMap[nodeWithPosition.rank!].height || 45) / 2,
+ }
+ }
+ })
+ });
+
+ return newNodes;
+}
diff --git a/packages/x-flow/src/utils/createIconFont.ts b/packages/x-flow/src/utils/createIconFont.ts
new file mode 100644
index 000000000..f11184a35
--- /dev/null
+++ b/packages/x-flow/src/utils/createIconFont.ts
@@ -0,0 +1,7 @@
+import { createFromIconfontCN } from '@ant-design/icons';
+
+export default (url?: string) => {
+ return createFromIconfontCN({
+ scriptUrl: url || '//at.alicdn.com/t/a/font_2750617_sax751jyfjl.js',
+ });
+};
\ No newline at end of file
diff --git a/packages/x-flow/src/utils/hooks.ts b/packages/x-flow/src/utils/hooks.ts
new file mode 100644
index 000000000..0c0f8baaf
--- /dev/null
+++ b/packages/x-flow/src/utils/hooks.ts
@@ -0,0 +1,43 @@
+import { useReducer, useRef, useEffect, useState } from 'react';
+import { message } from 'antd';
+
+export function usePrevious(value: any) {
+ const ref = useRef();
+ useEffect(() => {
+ ref.current = value;
+ }, [value]);
+ return ref.current;
+}
+
+// 类似于class component的setState
+export const useSet = (initState: any) => {
+ return useReducer((state: any, newState: any) => {
+ return { ...state, ...newState };
+ }, initState);
+};
+
+/**
+ * 业务中为了防止提交按钮会被连续点击 我们会手动的设置setLoading true 和setLoading false 非常麻烦
+ * 所以写了这个hook 省事
+ * @returns
+ */
+type TCallback = () => Promise;
+type TRequestLoading = () => [boolean, (callback: TCallback) => void];
+
+export const useLoadingRequest: TRequestLoading = () => {
+ const [loading, setLoading] = useState(false);
+
+ const handleRequest = async (callback: TCallback) => {
+ try {
+ setLoading(true);
+ await callback();
+ } catch (error: any) {
+ message.error(error);
+ throw error;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return [loading, handleRequest];
+};
diff --git a/packages/x-flow/src/utils/index.ts b/packages/x-flow/src/utils/index.ts
new file mode 100644
index 000000000..fa164c798
--- /dev/null
+++ b/packages/x-flow/src/utils/index.ts
@@ -0,0 +1,170 @@
+import { customAlphabet } from 'nanoid';
+export const uuid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16);
+
+
+
+import { isMatch, some, set, get, cloneDeep, has as _has, merge, mergeWith, isUndefined, omitBy } from 'lodash-es';
+
+export const _set = set;
+export const _get = get;
+export const _cloneDeep = cloneDeep;
+// export const _has = has;
+export { _has };
+export const _merge = merge;
+export const _mergeWith = mergeWith;
+export const _isUndefined = isUndefined;
+export const _omitBy = omitBy;
+export const _some = some;
+export const _isMatch = isMatch;
+
+export const isObject = (data: any) => {
+ const str = Object.prototype.toString.call(data);
+ return str.indexOf('Object') > -1;
+}
+
+export const isArray = (data: any) => {
+ const str = Object.prototype.toString.call(data);
+ return str.indexOf('Array') > -1;
+}
+
+export const isFunction = (data: any) => typeof data === 'function';
+
+export function isUrl(string: string) {
+ const protocolRE = /^(?:\w+:)?\/\/(\S+)$/;
+ // const domainRE = /^[^\s\.]+\.\S{2,}$/;
+ if (typeof string !== 'string') return false;
+ return protocolRE.test(string);
+}
+
+export const isNumber = (str: string | number) => !isNaN(Number(str))
+
+export const getArray = (arr, defaultValue = []) => {
+ if (Array.isArray(arr)) return arr;
+ return defaultValue;
+};
+
+export function getFormat(format) {
+ let dateFormat;
+ switch (format) {
+ case 'date':
+ dateFormat = 'YYYY-MM-DD';
+ break;
+ case 'time':
+ dateFormat = 'HH:mm:ss';
+ break;
+ case 'dateTime':
+ dateFormat = 'YYYY-MM-DD HH:mm:ss';
+ break;
+ case 'week':
+ dateFormat = 'YYYY-w';
+ break;
+ case 'year':
+ dateFormat = 'YYYY';
+ break;
+ case 'quarter':
+ dateFormat = 'YYYY-Q';
+ break;
+ case 'month':
+ dateFormat = 'YYYY-MM';
+ break;
+ default:
+ // dateTime
+ if (typeof format === 'string') {
+ dateFormat = format;
+ } else {
+ dateFormat = 'YYYY-MM-DD';
+ }
+ }
+ return dateFormat;
+}
+
+// TODO: to support case that item is not an object
+export function isObjType(schema: any) {
+ //return schema?.type === 'object' && schema.properties && !schema.widget;
+ return schema?.type === 'object' && schema?.properties && schema?.widgetType !== 'field';
+};
+
+export function isListType(schema: any) {
+ return schema?.type === 'array' && isObjType(schema?.items) && schema?.enum === undefined;
+};
+
+export function isCheckBoxType(schema: any, readOnly: boolean) {
+ if (readOnly) return false;
+ if (schema.widget === 'checkbox') return true;
+ if (schema && schema.type === 'boolean') {
+ if (schema.enum) return false;
+ if (schema.widget === undefined) return true;
+ return false;
+ }
+}
+
+export const translation = (configCtx: any) => (key: string) => {
+ const locale = configCtx?.locale.FormRender;
+ return locale[key];
+};
+
+export const hasFuncProperty = (obj: any) => {
+ return _some(obj, (value) => {
+ if (isFunction(value)) {
+ return true;
+ }
+ if (isObject(value)) {
+ return hasFuncProperty(value);
+ }
+ return false;
+ });
+};
+
+/**
+ * 安全地获取对象的值,如果值为 null 或 undefined,则返回 defaultValue。
+ *
+ * @param {Object} object - 要获取值的对象。
+ * @param {string|Array} path - 要获取的路径,可以是字符串或数组。
+ * @param {*} [defaultValue] - 如果值为 null 或 undefined,则返回 defaultValue。
+ * @returns {*} - 返回获取的值,或者默认值。
+ */
+export const safeGet = (object: any, path: string, defaultValue: any) => {
+ return get(object, path, defaultValue) ?? defaultValue;
+};
+
+
+
+
+
+export const isMac = () => {
+ return navigator.userAgent.toUpperCase().includes('MAC')
+}
+
+const specialKeysNameMap: Record = {
+ ctrl: '⌘',
+ alt: '⌥',
+}
+
+export const getKeyboardKeyNameBySystem = (key: string) => {
+ if (isMac())
+ return specialKeysNameMap[key] || key
+
+ return key
+}
+
+
+export const capitalize = (string: string) => {
+ if (typeof string !== 'string' || string.length === 0) {
+ return string;
+ }
+ return `${string.charAt(0).toUpperCase()}${string.slice(1)}`;
+}
+
+export const transformNodes = (nodes: any[]) => {
+ return nodes?.map(item => {
+ const { type, data, ...rest } = item;
+ return {
+ type: 'custom',
+ data: {
+ ...data,
+ _nodeType: type,
+ },
+ ...rest
+ }
+ })
+}
\ No newline at end of file
diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx
new file mode 100644
index 000000000..2f5b4a9c2
--- /dev/null
+++ b/packages/x-flow/src/withProvider.tsx
@@ -0,0 +1,75 @@
+import { ReactFlowProvider } from '@xyflow/react';
+import { ConfigProvider } from 'antd';
+import React, { useMemo } from 'react';
+import { FlowProviderWrapper } from './components/FlowProvider';
+import { ConfigContext } from './models/context';
+import { TNodeGroup, TNodeItem } from './types';
+interface ProviderProps {
+ configProvider?: any;
+ widgets?: any;
+ methods?: any;
+ nodeSelector?: any;
+ settings?: (TNodeGroup | TNodeItem)[];
+ [key: string]: any;
+}
+
+export default function withProvider(
+ Element: any,
+ defaultWidgets?: any
+): React.ComponentType {
+ return (props: ProviderProps) => {
+ const {
+ configProvider,
+ widgets,
+ methods,
+ nodeSelector,
+ settings,
+ initialValues,
+ ...restProps
+ } = props;
+
+ const settingMap = useMemo(() => {
+ const obj: Record = {};
+ settings?.forEach((node: any) => {
+ if (node.type !== '_group') {
+ obj[node.type] = node;
+ } else {
+ node.items.forEach((item: any) => {
+ obj[item.type] = item;
+ });
+ }
+ });
+ return obj;
+ }, [settings]);
+
+ const configContext = {
+ methods,
+ nodeSelector,
+ settings,
+ settingMap,
+ widgets: {
+ ...defaultWidgets,
+ ...widgets,
+ },
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ };
+}
diff --git a/packages/x-flow/tsconfig.json b/packages/x-flow/tsconfig.json
new file mode 100644
index 000000000..3e09e8c01
--- /dev/null
+++ b/packages/x-flow/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "compilerOptions": {
+ "target": "ES2015",
+ "module": "ES2015",
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "jsx": "react",
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "baseUrl": "./",
+ "skipLibCheck": true,
+ // "strict": true,
+ "declaration": true,
+ "paths": {
+ "@/*": [
+ "src/*"
+ ],
+ "@@/*": [
+ "src/.umi/*"
+ ],
+ "form-render": [
+ "packages/form-render/src/*"
+ ],
+ },
+ "allowJs": true,
+ "allowSyntheticDefaultImports": true,
+ "noImplicitAny": false,
+ "resolveJsonModule": true
+ },
+ "exclude": [
+ "node_modules",
+ "lib",
+ "es",
+ "dist",
+ "typings",
+ "**/__test__",
+ "test",
+ "tests",
+ "docs",
+ "**/*.js"
+ ]
+}