Merge branch 'refs/heads/master' into production

production
钟良源 7 months ago
commit dfe80b1ee8

@ -1,34 +1,150 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# Flow Platform React
## Getting Started
这是一个基于 React 和 Next.js 构建的可视化流程编辑平台。该平台提供了丰富的组件和工具,用于创建、编辑和管理复杂的业务流程。
First, run the development server:
## 项目概述
Flow Platform React 是一个功能强大的流程编辑器,支持拖拽式操作、组件连接、循环结构、条件判断等多种功能。平台内置了多种组件类型,包括基础组件、复合组件、系统组件等,可以满足各种业务场景的需求。
## 技术栈
- [Next.js](https://nextjs.org/) - React 框架
- [React](https://reactjs.org/) - JavaScript UI 库
- [TypeScript](https://www.typescriptlang.org/) - JavaScript 的超集,提供类型安全
- [@xyflow/react](https://reactflow.dev/) - 可视化流程图库
- [@arco-design/web-react](https://arco.design/react) - React UI 组件库
- [Redux Toolkit](https://redux-toolkit.js.org/) - 状态管理
## 功能特性
- 🎨 可视化流程编辑器,支持拖拽和连接节点
- 🔗 多种节点类型:开始/结束节点、基础节点、循环节点、条件节点等
- ⚙️ 组件库管理,支持自定义组件
- 🔄 流程执行和状态跟踪
- 📊 实时数据监控和可视化
- 🎯 精确的对齐辅助线
- 📚 丰富的组件市场
- 🧩 复合组件支持
## 快速开始
### 环境要求
- Node.js >= 16.20.0
- pnpm
### 安装依赖
```bash
npm run dev
# or
yarn dev
pnpm install
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
### 开发模式
```bash
pnpm run dev
```
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
打开浏览器访问 [http://localhost:4121](http://localhost:4121) 查看应用。
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
### 构建生产版本
```bash
# 构建服务端渲染
pnpm run build
# 构建服务端渲染同时输出dist文件供静态部署
pnpm run export
```
### 启动生产服务器
```bash
pnpm run start
```
## 项目结构
```
src/
├── api/ # API 接口定义
├── components/ # 公共组件
├── hooks/ # 自定义 React Hooks
├── pages/ # 页面组件
├── routes/ # 路由配置
├── store/ # Redux 状态管理
├── utils/ # 工具函数
└── ...
```
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## 核心模块
## Learn More
### 流程编辑器
To learn more about Next.js, take a look at the following resources:
流程编辑器是本项目的核心功能,提供了以下特性:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- 节点拖拽和自由布局
- 连接线绘制和管理
- 循环结构支持LOOP_START/LOOP_END
- 条件分支支持
- 实时对齐辅助线
- 撤销/重做功能
- 流程执行状态跟踪
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
### 组件库
## Deploy on Vercel
项目提供了丰富的组件库:
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
- 基础组件库:包含常用的业务组件
- 复合组件库:由多个基础组件组合而成的复杂组件
- 组件市场:可共享和复用的组件
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
### 状态管理
使用 Redux Toolkit 进行全局状态管理,主要包括:
- 用户信息
- 流程数据
- 组件库数据
- 画布状态
- 执行状态
## 开发指南
### 添加新节点类型
1. 在 `src/components/FlowEditor/node/` 目录下创建新的节点组件
2. 在 `src/utils/flowCommon.ts` 中的 `getNodeComponent` 函数中注册新节点类型
3. 在 `src/components/FlowEditor/node/types/defaultType.ts` 中定义节点类型
### 添加新组件
1. 在相应的组件目录中创建新组件
2. 在路由和菜单中注册新组件
3. 添加必要的状态管理和 API 接口
## 部署
项目使用静态导出模式,可以部署到任何支持静态文件托管的服务上:
```bash
pnpm run export
```
构建后的文件将位于 `dist/` 目录中。
## 环境配置
项目使用环境变量进行配置,创建 `.env` 文件:
```env
NEXT_PUBLIC_DEV_SERVER_HOST=http://localhost:8080
```
## 代码规范
项目使用 ESLint 和 Prettier 进行代码规范检查:
```bash
pnpm run eslint
pnpm run stylelint
```

@ -6,7 +6,8 @@ const withTM = require('next-transpile-modules')([
'@arco-design/web-react',
'@arco-themes/react-arco-pro',
'@xyflow/react',
'@xyflow/system'
'@xyflow/system',
'@uiw/react-codemirror'
]);
const setting = require('./src/settings.json');
@ -38,7 +39,7 @@ module.exports = withLess(
config.resolve.alias['@'] = path.resolve(__dirname, './src');
// 解决 react/jsx-runtime 找不到的问题
config.resolve.alias['react/jsx-runtime'] = path.resolve(__dirname, './node_modules/react/jsx-runtime.js');
config.resolve.alias['react/jsx-dev-runtime'] = path.resolve(__dirname, './node_modules/react/jsx-dev-runtime.js');
config.resolve.alias['react/jsx-dev-runtime'] = path.resolve(__dirname, './node_modules/react/jx-dev-runtime.js');
return config;
},

@ -1,6 +1,9 @@
{
"name": "next-app-ts",
"private": true,
"engines": {
"node": ">=16.20.0"
},
"scripts": {
"dev": "next dev -H 0.0.0.0 -p 4121",
"build": "next build",
@ -15,9 +18,14 @@
"@arco-design/color": "^0.4.0",
"@arco-design/web-react": "^2.32.2",
"@arco-themes/react-arco-pro": "^0.0.7",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@loadable/component": "^5.13.2",
"@reduxjs/toolkit": "^2.9.0",
"@turf/turf": "^6.5.0",
"@uiw/codemirror-theme-github": "^4.25.2",
"@uiw/react-codemirror": "^4.21.25",
"@xyflow/react": "^12.8.2",
"@xyflow/system": "^0.0.68",
"axios": "^0.24.0",

@ -20,6 +20,15 @@ importers:
'@arco-themes/react-arco-pro':
specifier: ^0.0.7
version: 0.0.7(@arco-design/web-react@2.66.5(@types/react@17.0.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))
'@codemirror/lang-java':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-json':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-python':
specifier: ^6.2.1
version: 6.2.1
'@loadable/component':
specifier: ^5.13.2
version: 5.16.7(react@17.0.2)
@ -29,6 +38,12 @@ importers:
'@turf/turf':
specifier: ^6.5.0
version: 6.5.0
'@uiw/codemirror-theme-github':
specifier: ^4.25.2
version: 4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
'@uiw/react-codemirror':
specifier: ^4.21.25
version: 4.21.25(@babel/runtime@7.28.3)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.6)(codemirror@6.0.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
'@xyflow/react':
specifier: ^12.8.2
version: 12.8.4(@types/react@17.0.2)(immer@10.1.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
@ -816,6 +831,39 @@ packages:
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'}
'@codemirror/autocomplete@6.19.0':
resolution: {integrity: sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==}
'@codemirror/commands@6.9.0':
resolution: {integrity: sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==}
'@codemirror/lang-java@6.0.2':
resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==}
'@codemirror/lang-json@6.0.2':
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
'@codemirror/lang-python@6.2.1':
resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==}
'@codemirror/language@6.11.3':
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
'@codemirror/lint@6.9.0':
resolution: {integrity: sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A==}
'@codemirror/search@6.5.11':
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
'@codemirror/state@6.5.2':
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
'@codemirror/theme-one-dark@6.1.3':
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
'@codemirror/view@6.38.6':
resolution: {integrity: sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==}
'@csstools/selector-specificity@2.2.0':
resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==}
engines: {node: ^14 || ^16 || >=18}
@ -878,6 +926,24 @@ packages:
'@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
'@lezer/common@1.3.0':
resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==}
'@lezer/highlight@1.2.2':
resolution: {integrity: sha512-z8TQwaBXXQIvG6i2g3e9cgMwUUXu9Ib7jo2qRRggdhwKpM56Dw3PM3wmexn+EGaaOZ7az0K7sjc3/gcGW7sz7A==}
'@lezer/java@1.1.3':
resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==}
'@lezer/json@1.0.3':
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
'@lezer/lr@1.4.2':
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
'@lezer/python@1.1.18':
resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==}
'@ljharb/resumer@0.0.1':
resolution: {integrity: sha512-skQiAOrCfO7vRTq53cxznMpks7wS1va95UCidALlOVWqvBAzwPVErwizDwoMqNVMEn1mDq0utxZd02eIrvF1lw==}
engines: {node: '>= 0.4'}
@ -892,6 +958,9 @@ packages:
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@napi-rs/triples@1.0.3':
resolution: {integrity: sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA==}
@ -1524,6 +1593,38 @@ packages:
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@uiw/codemirror-extensions-basic-setup@4.21.25':
resolution: {integrity: sha512-eeUKlmEE8aSoSgelS8OR2elcPGntpRo669XinAqPCLa0eKorT2B0d3ts+AE+njAeGk744tiyAEbHb2n+6OQmJw==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
'@codemirror/commands': '>=6.0.0'
'@codemirror/language': '>=6.0.0'
'@codemirror/lint': '>=6.0.0'
'@codemirror/search': '>=6.0.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
'@uiw/codemirror-theme-github@4.25.2':
resolution: {integrity: sha512-9g3ujmYCNU2VQCp0+XzI1NS5hSZGgXRtH+5yWli5faiPvHGYZUVke+5Pnzdn/1tkgW6NpTQ7U/JHsyQkgbnZ/w==}
'@uiw/codemirror-themes@4.25.2':
resolution: {integrity: sha512-WFYxW3OlCkMomXQBlQdGj1JZ011UNCa7xYdmgYqywVc4E8f5VgIzRwCZSBNVjpWGGDBOjc+Z6F65l7gttP16pg==}
peerDependencies:
'@codemirror/language': '>=6.0.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
'@uiw/react-codemirror@4.21.25':
resolution: {integrity: sha512-mBrCoiffQ+hbTqV1JoixFEcH7BHXkS3PjTyNH7dE8Gzf3GSBRazhtSM5HrAFIiQ5FIRGFs8Gznc4UAdhtevMmw==}
peerDependencies:
'@babel/runtime': '>=7.11.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/theme-one-dark': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
codemirror: '>=6.0.0'
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@ -1932,6 +2033,9 @@ packages:
resolution: {integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==}
engines: {node: '>= 4.0'}
codemirror@6.0.2:
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@ -2020,6 +2124,9 @@ packages:
create-hmac@1.1.7:
resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cron-parser@5.3.1:
resolution: {integrity: sha512-Mu5Jk1b4cUfY8u34+thI9TZxvQiuhaMBS2Ag84rOSoHlU33xtIPkXwr6lWuw3XPmxSxq317B+hl0o4J+LdhwNg==}
engines: {node: '>=18'}
@ -4159,6 +4266,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
style-search@0.1.0:
resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==}
@ -4479,6 +4589,9 @@ packages:
vm-browserify@1.1.2:
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
@ -5535,6 +5648,77 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@codemirror/autocomplete@6.19.0':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
'@lezer/common': 1.3.0
'@codemirror/commands@6.9.0':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
'@lezer/common': 1.3.0
'@codemirror/lang-java@6.0.2':
dependencies:
'@codemirror/language': 6.11.3
'@lezer/java': 1.1.3
'@codemirror/lang-json@6.0.2':
dependencies:
'@codemirror/language': 6.11.3
'@lezer/json': 1.0.3
'@codemirror/lang-python@6.2.1':
dependencies:
'@codemirror/autocomplete': 6.19.0
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@lezer/common': 1.3.0
'@lezer/python': 1.1.18
'@codemirror/language@6.11.3':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
'@lezer/common': 1.3.0
'@lezer/highlight': 1.2.2
'@lezer/lr': 1.4.2
style-mod: 4.1.3
'@codemirror/lint@6.9.0':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
crelt: 1.0.6
'@codemirror/search@6.5.11':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
crelt: 1.0.6
'@codemirror/state@6.5.2':
dependencies:
'@marijn/find-cluster-break': 1.0.2
'@codemirror/theme-one-dark@6.1.3':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
'@lezer/highlight': 1.2.2
'@codemirror/view@6.38.6':
dependencies:
'@codemirror/state': 6.5.2
crelt: 1.0.6
style-mod: 4.1.3
w3c-keyname: 2.2.8
'@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2)':
dependencies:
postcss-selector-parser: 6.1.2
@ -5606,6 +5790,34 @@ snapshots:
'@juggle/resize-observer@3.4.0': {}
'@lezer/common@1.3.0': {}
'@lezer/highlight@1.2.2':
dependencies:
'@lezer/common': 1.3.0
'@lezer/java@1.1.3':
dependencies:
'@lezer/common': 1.3.0
'@lezer/highlight': 1.2.2
'@lezer/lr': 1.4.2
'@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.3.0
'@lezer/highlight': 1.2.2
'@lezer/lr': 1.4.2
'@lezer/lr@1.4.2':
dependencies:
'@lezer/common': 1.3.0
'@lezer/python@1.1.18':
dependencies:
'@lezer/common': 1.3.0
'@lezer/highlight': 1.2.2
'@lezer/lr': 1.4.2
'@ljharb/resumer@0.0.1':
dependencies:
'@ljharb/through': 2.3.14
@ -5621,6 +5833,8 @@ snapshots:
react: 17.0.2
react-is: 16.13.1
'@marijn/find-cluster-break@1.0.2': {}
'@napi-rs/triples@1.0.3': {}
'@next/env@12.0.4': {}
@ -6755,6 +6969,47 @@ snapshots:
'@typescript-eslint/types': 5.62.0
eslint-visitor-keys: 3.4.3
'@uiw/codemirror-extensions-basic-setup@4.21.25(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)':
dependencies:
'@codemirror/autocomplete': 6.19.0
'@codemirror/commands': 6.9.0
'@codemirror/language': 6.11.3
'@codemirror/lint': 6.9.0
'@codemirror/search': 6.5.11
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
'@uiw/codemirror-theme-github@4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)':
dependencies:
'@uiw/codemirror-themes': 4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
transitivePeerDependencies:
- '@codemirror/language'
- '@codemirror/state'
- '@codemirror/view'
'@uiw/codemirror-themes@4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
'@uiw/react-codemirror@4.21.25(@babel/runtime@7.28.3)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.6)(codemirror@6.0.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)':
dependencies:
'@babel/runtime': 7.28.3
'@codemirror/commands': 6.9.0
'@codemirror/state': 6.5.2
'@codemirror/theme-one-dark': 6.1.3
'@codemirror/view': 6.38.6
'@uiw/codemirror-extensions-basic-setup': 4.21.25(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
codemirror: 6.0.2
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
transitivePeerDependencies:
- '@codemirror/autocomplete'
- '@codemirror/language'
- '@codemirror/lint'
- '@codemirror/search'
'@webassemblyjs/ast@1.14.1':
dependencies:
'@webassemblyjs/helper-numbers': 1.13.2
@ -7290,6 +7545,16 @@ snapshots:
chalk: 2.4.2
q: 1.5.1
codemirror@6.0.2:
dependencies:
'@codemirror/autocomplete': 6.19.0
'@codemirror/commands': 6.9.0
'@codemirror/language': 6.11.3
'@codemirror/lint': 6.9.0
'@codemirror/search': 6.5.11
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.6
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@ -7396,6 +7661,8 @@ snapshots:
safe-buffer: 5.2.1
sha.js: 2.4.12
crelt@1.0.6: {}
cron-parser@5.3.1:
dependencies:
luxon: 3.7.1
@ -9768,6 +10035,8 @@ snapshots:
strip-json-comments@3.1.1: {}
style-mod@4.1.3: {}
style-search@0.1.0: {}
styled-jsx@5.0.0-beta.3(@babel/core@7.28.3)(react@17.0.2):
@ -10134,6 +10403,8 @@ snapshots:
vm-browserify@1.1.2: {}
w3c-keyname@2.2.8: {}
warning@4.0.3:
dependencies:
loose-envify: 1.4.0

@ -0,0 +1,20 @@
import axios from 'axios';
import { apiResData } from '@/api/interface/index';
// 公共路径
const urlPrefix = '/api/v1/bpms-workbench';
// 获取应用事件
export function getAppEventData(id: any) {
return axios.get<apiResData>(`${urlPrefix}/appEvent/${id}`);
}
// 获取工程下的所有应用事件
export function getAppEventList(id: any) {
return axios.get<apiResData>(`${urlPrefix}/appEvent/${id}/list`);
}
// 更新事件
export function updateAppEvent(id: any, data: any) {
return axios.post<apiResData>(`${urlPrefix}/appEvent/${id}/update`, data);
}

@ -9,11 +9,27 @@ export function getAppInfo(data: string) {
return axios.get(`${urlPrefix}/appRes/${data}`);
}
// 获取应用资源
export function getAppInfoNew(data: string) {
return axios.get(`${urlPrefix}/appRes/${data}/new`);
}
// 更新主流程
export function setMainFlow(data: FlowDefinition, appId: string) {
return axios.post(`${urlPrefix}/appRes/${appId}/updateMain`, data);
}
// 更新主流程-新数据结构
export function setMainFlowNew(data: FlowDefinition, appId: string) {
return axios.post(`${urlPrefix}/appRes/${appId}/updateMainNew`, data);
}
// 引用公开组件到应用组件内
export function refPublish(data) {
return axios.put(`${urlPrefix}/appRes/refPublish`, data);
}
// 新增子流程
export function addSub(appId: string, data?: FlowDefinition[]) {
return axios.post(

@ -39,3 +39,8 @@ export function queryEventItemBySceneId(sceneId: string) {
return axios.get(`${urlPrefix}/event/${sceneId}/get`);
}
// 事件管理-获取工程下可用的topic
export function getTopicList(id: string) {
return axios.get(`${urlPrefix}/event/${id}/topic`);
}

@ -0,0 +1,37 @@
import React from 'react';
import styles from './node/style/baseOther.module.less';
// 定义节点状态类型
export type NodeStatus = 'waiting' | 'running' | 'success' | 'failed';
// 节点状态指示器组件
const NodeStatusIndicator: React.FC<{ status: NodeStatus, isVisible: boolean }> = ({ status, isVisible }) => {
// 如果不可见,不渲染任何内容
if (!isVisible) {
return null;
}
// 根据状态返回相应的指示器样式
const getStatusIndicator = () => {
switch (status) {
case 'waiting':
return <div className={styles['status-waiting']} />;
case 'running':
return <div className={styles['status-running']} />;
case 'success':
return <div className={styles['status-success']} />;
case 'failed':
return <div className={styles['status-failed']} />;
default:
return null;
}
};
return (
<div className={styles['node-status-indicator']}>
{getStatusIndicator()}
</div>
);
};
export default NodeStatusIndicator;

@ -0,0 +1,43 @@
import React, { useMemo } from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContentApp';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const AppNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '应用节点';
// 生成随机背景色使用useMemo确保颜色只在节点首次创建时生成一次
const backgroundColor = useMemo(() => {
const colors = ['#e59428', '#4a90e2', '#7b68ee', '#50c878', '#ff6347', '#9370db', '#00bfff', '#ff8c00'];
return colors[Math.floor(Math.random() * colors.length)];
}, []);
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor }}>
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
<NodeContent data={data} />
</div>
);
};
export default AppNode;

@ -1,13 +1,11 @@
import React from 'react';
// import styles from '@/pages/flowEditor/node/style/base.module.less';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import { useStore } from '@xyflow/react';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const BasicNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const BasicNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '基础节点';
// 获取节点选中状态 - 适配React Flow v12 API
@ -15,13 +13,22 @@ const BasicNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#e59428' }}>
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
{/*<NodeContent data={{ ...data, type: 'basic' }} />*/}
<NodeContentOther data={{ ...data, type: 'basic' }} />
</div>
);

@ -0,0 +1,43 @@
import React from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import DynamicIcon from '@/components/DynamicIcon';
import NodeContentCode from '@/pages/flowEditor/components/nodeContentCode';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const setIcon = () => {
return <DynamicIcon type="IconCode" style={{ fontSize: '16px', marginRight: '5px' }} />;
};
const CodeNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '代码编辑器';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon()}
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
<NodeContentCode data={data} />
</div>
);
};
export default CodeNode;

@ -1,11 +1,10 @@
import React from 'react';
// import styles from '@/pages/flowEditor/node/style/base.module.less';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import { useStore } from '@xyflow/react';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const EndNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const title = data.title || '结束';
@ -15,13 +14,22 @@ const EndNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#c05144' }}>
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
{/*<NodeContent data={{ ...data, type: 'end' }} />*/}
<NodeContentOther data={{ ...data, type: 'end' }} />
</div>
);

@ -0,0 +1,43 @@
import React from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import DynamicIcon from '@/components/DynamicIcon';
import NodeContentImage from '@/pages/flowEditor/components/nodeContentImage';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const setIcon = () => {
return <DynamicIcon type="IconImage" style={{ fontSize: '16px', marginRight: '5px' }} />;
};
const ImageNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '图片展示';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon()}
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
<NodeContentImage data={data} />
</div>
);
};
export default ImageNode;

@ -3,26 +3,53 @@ import { NodeTypes } from '@xyflow/react';
import StartNode from './startNode/StartNode';
import EndNode from './endNode/EndNode';
import BasicNode from './basicNode/BasicNode';
import AppNode from './appNode/AppNode';
import CodeNode from './codeNode/CodeNode';
import ImageNode from './imageNode/ImageNode';
import RestNode from './restNode/RestNode';
import SwitchNode from './switchNode/SwitchNode';
import LoopNode from './loopNode/LoopNode';
// 定义所有可用的节点类型
export const nodeTypes: NodeTypes = {
start: StartNode,
end: EndNode,
BASIC: BasicNode
BASIC: BasicNode,
SUB: BasicNode,
APP: AppNode,
CODE: CodeNode,
IMAGE: ImageNode,
REST: RestNode,
SWITCH: SwitchNode,
LOOP: LoopNode
};
// 节点类型映射,用于创建节点时的类型查找
export const nodeTypeMap: Record<string, string> = {
'start': 'start',
'end': 'end',
'basic': 'BASIC'
'basic': 'BASIC',
'sub': 'SUB',
'app': 'APP',
'code': 'CODE',
'image': 'IMAGE',
'rest': 'REST',
'switch': 'SWITCH',
'loop': 'LOOP'
};
// 节点显示名称映射
export const nodeTypeNameMap: Record<string, string> = {
'start': '开始节点',
'end': '结束节点',
'basic': '基础节点'
'basic': '基础节点',
'sub': '复合节点',
'app': '应用节点',
'code': '代码节点',
'image': '图片节点',
'rest': 'REST节点',
'switch': '条件节点',
'loop': '循环节点'
};
// 注册新节点类型的函数

@ -1,16 +1,15 @@
import React from 'react';
import { useStore } from '@xyflow/react';
// import styles from '@/pages/flowEditor/node/style/base.module.less';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import DynamicIcon from '@/components/DynamicIcon';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const setIcon = (nodeType: string) => {
let type = 'IconApps';
switch (nodeType) {
case 'CONDITION':
case 'SWITCH':
type = 'IconBranch';
break;
case 'AND':
@ -67,13 +66,23 @@ const LocalNode = ({ data, id }: { data: any; id: string }) => {
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon(data.type)}
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
{/*<NodeContent data={data} />*/}
<NodeContentOther data={data} />
</div>
);

@ -0,0 +1,188 @@
import React, { useEffect, useState } from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import DynamicIcon from '@/components/DynamicIcon';
import { Handle, Position } from '@xyflow/react';
import NodeContentLoop from '@/pages/flowEditor/components/nodeContentLoop';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
// 循环节点组件,用于显示循环开始和循环结束节点
const LoopNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const [newData, setNewData] = useState<any[]>([]);
const title = data.title || '循环节点';
const isStartNode = data.type === 'LOOP_START';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
// 设置图标
const setIcon = () => {
if (isStartNode) {
return <DynamicIcon type="IconPlayArrow" style={{ fontSize: '16px', marginRight: '5px' }} />;
}
else {
return <DynamicIcon type="IconStop" style={{ fontSize: '16px', marginRight: '5px' }} />;
}
};
const getOperator = (expr: string) => {
let operator;
if (expr.includes('==')) {
operator = '==';
}
else if (expr.includes('>=')) {
operator = '>=';
}
else if (expr.includes('<=')) {
operator = '<=';
}
else if (expr.includes('<')) {
operator = '<';
}
else if (expr.includes('>')) {
operator = '>';
}
else {
operator = '!=';
}
return operator;
};
const reverseDataStructure = (processedData: any) => {
try {
const parsedCustomDef = JSON.parse(processedData.customDef);
if (!parsedCustomDef.conditions) {
return [];
}
return parsedCustomDef.conditions.map((condition: any, index: number) => {
// 解析表达式以获取左值、操作符和右值
let lftVal = '';
let operator = '';
let rgtVal = '';
const valueType = condition.valueType || '';
if (condition.expression) {
// 处理布尔值表达式
if (valueType.includes('boolean')) {
const splitStr = valueType.split('-')[1];
operator = getOperator(condition.expression);
const pattern = new RegExp(`\\$\\.(.+)(${operator})${splitStr}`);
const match = condition.expression.match(pattern);
if (match) {
lftVal = match[1];
operator = match[2];
}
}
// 处理其他类型的表达式
else {
// 简单的解析逻辑,可能需要根据实际表达式格式进行调整
const match = condition.expression.match(/\$\.([^=!<>]+)(==|!=|>=|<=|>|<)(.+)/);
if (match) {
lftVal = match[1];
operator = match[2];
rgtVal = match[3];
}
}
}
return {
key: index,
id: Date.now(),
apiOutId: condition.apiOutId || '',
lftVal,
operator,
valueType,
rgtVal
};
});
} catch (e) {
console.error('Error parsing customDef:', e);
return [];
}
};
useEffect(() => {
if (data) {
const reverseData = reverseDataStructure(data.component);
if (reverseData.length > 0) {
const list = reverseData.map(item => {
let expression = '';
if (item.valueType.includes('boolean')) {
const splitStr = item.valueType.split('-')[1];
expression = `$.${item.lftVal}${item.operator}${splitStr}`;
}
else expression = `$.${item.lftVal}${item.operator}${item.rgtVal}`;
return {
name: item.apiOutId,
id: item.apiOutId,
desc: '',
defaultValue: item.valueType,
dataType: expression
};
});
// 合并list数组与data.parameters.apiOuts数组
const apiOuts = data.parameters?.apiOuts || [];
const mergedApiOuts = [...apiOuts];
setNewData(mergedApiOuts);
}
else {
// 如果没有reverseData则直接使用原始apiOuts
setNewData(data.parameters?.apiOuts || []);
}
}
}, [data]);
// 创建包含额外apiOuts的新data对象
const modifiedData = {
...data,
parameters: {
...data.parameters,
apiOuts: newData
}
};
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon()}
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
{/* 顶部连接点,用于标识循环开始和结束节点是一组 */}
<Handle
type={id.includes('END') ? 'target' : 'source'}
position={Position.Top}
id={`${id}-group`}
style={{
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)',
top: '-4px'
}}
/>
<NodeContentLoop data={modifiedData} />
</div>
);
};
export default LoopNode;

@ -0,0 +1,43 @@
import React from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import DynamicIcon from '@/components/DynamicIcon';
import NodeContentREST from '@/pages/flowEditor/components/nodeContentREST';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const setIcon = () => {
return <DynamicIcon type="IconCloudDownload" style={{ fontSize: '16px', marginRight: '5px' }} />;
};
const RestNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || 'REST调用';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon()}
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
<NodeContentREST data={data} />
</div>
);
};
export default RestNode;

@ -1,11 +1,10 @@
import React from 'react';
// import styles from '@/pages/flowEditor/node/style/base.module.less';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import { useStore } from '@xyflow/react';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import NodeContentOther from '@/pages/flowEditor/components/nodeContentOther';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
const StartNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const title = data.title || '开始';
@ -15,13 +14,22 @@ const StartNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#29b971' }}>
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
{/*<NodeContent data={{ ...data, type: 'start' }} />*/}
<NodeContentOther data={{ ...data, type: 'start' }} />
</div>
);

@ -13,6 +13,7 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
color: #000000;
text-align: center;
position: relative;
}
.node-api-box,
@ -52,9 +53,12 @@
.node-inputs,
.node-outputs,
.node-inputs-api,
.node-outputs-api {
flex: 1;
}
.node-outputs-api {
.node-input-label {
font-size: 12px;
padding: 1px 0;
@ -65,7 +69,6 @@
.node-inputs {
margin-bottom: 5px;
margin-right: 30px;
}
.node-outputs,
@ -91,4 +94,63 @@
min-height: 20px;
text-align: center;
}
}
// 节点状态指示器样式
.node-status-indicator {
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
z-index: 10;
}
.status-waiting {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #cccccc;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}
.status-running {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #1890ff;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
animation: pulse 1.5s infinite;
}
.status-success {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #52c41a;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}
.status-failed {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #ff4d4f;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(24, 144, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
}
}

@ -0,0 +1,170 @@
import React, { useEffect, useState } from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import DynamicIcon from '@/components/DynamicIcon';
import { Handle, Position } from '@xyflow/react';
import NodeContentSwitch from '@/pages/flowEditor/components/nodeContentSwitch';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
// 循环节点组件,用于显示循环开始和循环结束节点
const SwitchNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const [newData, setNewData] = useState<any[]>([]);
const title = data.title || '条件选择';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
// 获取节点运行状态
const nodeStatus: NodeStatus = useFlowStore((state) =>
(state.nodeLookup.get(id)?.data?.status as NodeStatus) || 'waiting'
);
// 获取运行状态可见性
const isStatusVisible = useFlowStore((state) =>
!!state.nodeLookup.get(id)?.data?.isStatusVisible
);
// 设置图标
const setIcon = () => {
return <DynamicIcon type="IconBranch" style={{ fontSize: '16px', marginRight: '5px' }} />;
};
const getOperator = (expr: string) => {
let operator;
if (expr.includes('==')) {
operator = '==';
}
else if (expr.includes('>=')) {
operator = '>=';
}
else if (expr.includes('<=')) {
operator = '<=';
}
else if (expr.includes('<')) {
operator = '<';
}
else if (expr.includes('>')) {
operator = '>';
}
else {
operator = '!=';
}
return operator;
};
const reverseDataStructure = (processedData: any) => {
if (!processedData) {
return [];
}
try {
const parsedCustomDef = JSON.parse(processedData?.customDef);
if (!parsedCustomDef.conditions) {
return [];
}
return parsedCustomDef.conditions.map((condition: any, index: number) => {
// 解析表达式以获取左值、操作符和右值
let lftVal = '';
let operator = '';
let rgtVal = '';
const valueType = condition.valueType || '';
if (condition.expression) {
// 处理布尔值表达式
if (valueType.includes('boolean')) {
const splitStr = valueType.split('-')[1];
operator = getOperator(condition.expression);
const pattern = new RegExp(`\\$\\.(.+)(${operator})${splitStr}`);
const match = condition.expression.match(pattern);
if (match) {
lftVal = match[1];
operator = match[2];
}
}
// 处理其他类型的表达式
else {
// 简单的解析逻辑,可能需要根据实际表达式格式进行调整
const match = condition.expression.match(/\$\.([^=!<>]+)(==|!=|>=|<=|>|<)(.+)/);
if (match) {
lftVal = match[1];
operator = match[2];
rgtVal = match[3];
}
}
}
return {
key: index,
id: Date.now(),
apiOutId: condition.apiOutId || '',
lftVal,
operator,
valueType,
rgtVal
};
});
} catch (e) {
console.error('Error parsing customDef:', e);
return [];
}
};
useEffect(() => {
if (data) {
const reverseData = reverseDataStructure(data.component);
if (reverseData.length > 0) {
const list = reverseData.map(item => {
let expression = '';
if (item.valueType.includes('boolean')) {
const splitStr = item.valueType.split('-')[1];
expression = `$.${item.lftVal}${item.operator}${splitStr}`;
}
else expression = `$.${item.lftVal}${item.operator}${item.rgtVal}`;
return {
name: item.apiOutId,
id: item.apiOutId,
desc: '',
defaultValue: item.valueType,
dataType: expression
};
});
// 合并list数组与data.parameters.apiOuts数组
const apiOuts = data.parameters?.apiOuts || [];
const mergedApiOuts = [...apiOuts];
setNewData(mergedApiOuts);
}
else {
// 如果没有reverseData则直接使用原始apiOuts
setNewData(data.parameters?.apiOuts || []);
}
}
}, [data]);
// 创建包含额外apiOuts的新data对象
const modifiedData = {
...data,
parameters: {
...data.parameters,
apiOuts: newData
}
};
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{setIcon()}
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
<NodeContentSwitch data={modifiedData} />
</div>
);
};
export default SwitchNode;

@ -1,12 +1,98 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { NodeEditorProps } from './index';
import { Form, Input, Select } from '@arco-design/web-react';
import { Form, Input, Select, Typography } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from '@/components/FlowEditor/nodeEditors/components/ParamsTable';
import { queryInstance } from '@/api/components';
const BasicNodeEditor: React.FC<NodeEditorProps> = ({
node,
nodeData,
updateNodeData
}) => {
node,
nodeData,
updateNodeData
}) => {
const [currentCompInfo, setCurrentCompInfo] = useState(null);
const [options, setOptions] = useState([]);
const { projectComponentData, info, currentAppData } = useSelector(state => state.ideContainer);
const getCurrentProjectStoreData = () => {
const compData = projectComponentData[currentAppData.sceneId] || {};
const result: any[] = [];
// 处理projectCompDto中的数据
if (compData.projectCompDto) {
const { mineComp = [], pubComp = [], teamWorkComp = [] } = compData.projectCompDto;
// 添加mineComp数据
mineComp.forEach((item: any) => {
result.push({
...item,
type: 'mineComp'
});
});
// 添加pubComp数据
pubComp.forEach((item: any) => {
result.push({
...item,
type: 'pubComp'
});
});
// 添加teamWorkComp数据
teamWorkComp.forEach((item: any) => {
result.push({
...item,
type: 'teamWorkComp'
});
});
}
// 处理projectFlowDto中的数据
if (compData.projectFlowDto) {
const { mineFlow = [], pubFlow = [] } = compData.projectFlowDto;
// 添加mineFlow数据
mineFlow.forEach((item: any) => {
result.push({
...item,
type: 'mineFlow'
});
});
// 添加pubFlow数据
pubFlow.forEach((item: any) => {
result.push({
...item,
type: 'pubFlow'
});
});
}
return result;
};
const getCompInfo = () => {
const flatData = getCurrentProjectStoreData();
setCurrentCompInfo(flatData.find((item: any) => item.id === nodeData.compId));
};
const getCompInstance = async () => {
const res: any = await queryInstance(nodeData.compId);
if (res.code === 200) {
const newOptions = res.data.map((item: any) => {
return {
label: item.identifier,
value: item.identifier
};
});
setOptions(newOptions);
}
};
useEffect(() => {
getCompInfo();
getCompInstance();
}, []);
return (
<Form layout="vertical">
<Form.Item label="节点标题">
@ -21,17 +107,53 @@ const BasicNodeEditor: React.FC<NodeEditorProps> = ({
onChange={(value) => updateNodeData('description', value)}
/>
</Form.Item>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<div style={{ display: 'flex', marginTop: 10, marginBottom: 10 }}>
<span></span>
<div>{currentCompInfo?.type}</div>
</div>
<div style={{ display: 'flex', marginTop: 10, marginBottom: 10 }}>
<span></span>
<div>{currentCompInfo?.name}</div>
</div>
<div style={{ display: 'flex', marginTop: 10, marginBottom: 10 }}>
<span></span>
<div>{currentCompInfo?.description}</div>
</div>
<Form.Item label="节点类型">
<Select
value={nodeData.category || ''}
onChange={(value) => updateNodeData('category', value)}
options={[
{ label: '数据处理', value: 'data' },
{ label: '逻辑判断', value: 'logic' },
{ label: '外部接口', value: 'api' }
]}
value={nodeData?.component?.compIdentifier || ''}
onChange={(value) => {
console.log(value);
updateNodeData('component', {
...nodeData.component,
compIdentifier: value,
compInstanceIdentifier: value
});
}}
options={options}
/>
</Form.Item>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataIns || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataIns: data
});
}}
/>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataOuts || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataOuts: data
});
}}
/>
</Form>
);
};

@ -1,7 +1,7 @@
import React from 'react';
import { NodeEditorProps } from './index';
import { Form, Input } from '@arco-design/web-react';
import ConditionEditor from './components/ConditionEditor';
import SwitchEditor from './components/SwitchEditor';
import AndEditor from './components/AndEditor';
import OrEditor from './components/OrEditor';
import WaitEditor from './components/WaitEditor';
@ -27,15 +27,16 @@ const LocalNodeEditor: React.FC<NodeEditorProps> = ({
const localNodeType = nodeData.type || '';
switch (localNodeType) {
case 'CONDITION': // 条件选择
return <ConditionEditor nodeData={nodeData} updateNodeData={updateNodeData} />;
case 'SWITCH': // 条件选择
return <SwitchEditor nodeData={nodeData} updateNodeData={updateNodeData} />;
case 'AND': // 与门
return <AndEditor nodeData={nodeData} updateNodeData={updateNodeData} />;
case 'OR': // 或门
return <OrEditor nodeData={nodeData} updateNodeData={updateNodeData} />;
case 'WAIT': // 等待
return <WaitEditor nodeData={nodeData} updateNodeData={updateNodeData} />;
case 'LOOP': // 循环
case 'LOOP_START': // 循环
case 'LOOP_END': // 循环
return <LoopEditor nodeData={nodeData} updateNodeData={updateNodeData} />;
case 'CYCLE': // 周期
return <CycleEditor nodeData={nodeData} updateNodeData={updateNodeData} />;
@ -78,7 +79,7 @@ const LocalNodeEditor: React.FC<NodeEditorProps> = ({
};
return (
<Form layout="vertical">
<Form layout="vertical" style={{ minWidth: 200, maxWidth: 1100 }}>
{renderLocalNodeEditor()}
</Form>
);

@ -1,10 +1,46 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from './ParamsTable';
import CodeMirror from '@/components/FlowEditor/nodeEditors/components/CodeMirror';
const CodeEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
const [codeComponent, setCodeComponent] = useState(nodeData.component || {});
// 当组件加载时,主动触发 CodeMirror 的 onUpdateData 方法
useEffect(() => {
// 如果是新创建的节点,没有默认的 component 数据,则设置默认值
if (!nodeData.component || Object.keys(nodeData.component).length === 0) {
const defaultData = {
customDef: {
languageId: '63', // 默认 Java
sourceCode: '/**\n' +
'ExecClass类main 方法是固定的启动函数,参数个数、类型、返回类型不可更改\n' +
'当前版本gson-2.10.1.jar的文档地址\n' +
'https://www.javadoc.io/doc/com.google.code.gson/gson/2.10.1/index.html\n' +
'*/\n' +
'import com.google.gson.JsonObject;\n' +
'class ExecClass{ \n' +
' public JsonObject main(JsonObject args){\n' +
' return args;\n' +
' }\n' +
'}'
},
type: 'CODE'
};
// 更新节点数据
updateNodeData('component', defaultData);
setCodeComponent(defaultData);
}
else {
// 如果已有 component 数据,则使用现有数据
setCodeComponent(nodeData.component);
}
}, []);
return (
<>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
@ -17,6 +53,26 @@ const CodeEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) =>
});
}}
/>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataOuts || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataOuts: data
});
}}
/>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<CodeMirror
initialData={codeComponent}
onUpdateData={(data) => {
setCodeComponent(data);
updateNodeData('component', {
...data
});
}}
/>
</>
);
};

@ -0,0 +1,98 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Select } from '@arco-design/web-react';
import CodeMirror from '@uiw/react-codemirror';
import { java } from '@codemirror/lang-java';
import { python } from '@codemirror/lang-python';
import { githubLight } from '@uiw/codemirror-theme-github';
const Option = Select.Option;
interface TableDataItem {
key: number | string;
id: string;
dataType: string;
arrayType: string;
desc: string;
defaultValue: string;
[key: string]: any; // 允许其他自定义字段
}
interface CodeMirrorProps {
initialData: TableDataItem[],
onUpdateData: (data) => void,
}
const extensions = [java(), python(), githubLight];
const options = ['java', 'python'];
const defaultCode = {
'java': '/**\n' +
'ExecClass类main 方法是固定的启动函数,参数个数、类型、返回类型不可更改\n' +
'当前版本gson-2.10.1.jar的文档地址\n' +
'https://www.javadoc.io/doc/com.google.code.gson/gson/2.10.1/index.html\n' +
'*/\n' +
'import com.google.gson.JsonObject;\n' +
'class ExecClass{ \n' +
' public JsonObject main(JsonObject args){\n' +
' return args;\n' +
' }\n' +
'}',
'python': '# main函数是启动函数参数类型、个数、返回类型不可更改\n' +
'def main(a:dict)->dict:\n' +
' return {\'b\': a.get(\'a\')+[4,5]}'
};
const nameToCode = {
'java': '63',
'python': '70'
};
const CodeMirrorComp: React.FC<CodeMirrorProps> = ({
initialData,
onUpdateData
}) => {
const [value, setValue] = useState('console.log(\'hello world!\');');
const [language, setLanguage] = useState('java');
const onChange = useCallback((val, viewUpdate) => {
setValue(val);
const data = {
customDef: {
languageId: nameToCode[language],
sourceCode: val
}
};
console.log('data:', data);
onUpdateData(data);
}, []);
useEffect(() => {
setValue(defaultCode[language]);
}, []);
return (
<>
<Select
defaultValue={'java'}
placeholder="请选择语言"
style={{ width: 154 }}
onChange={(value) => {
setLanguage(value);
setValue(defaultCode[value]);
}}
>
{options.map((option, index) => (
<Option key={option} disabled={index === 3} value={option}>
{option}
</Option>
))}
</Select>
<CodeMirror
value={value}
height="300px"
extensions={extensions}
onChange={onChange} />
</>
);
};
export default CodeMirrorComp;

@ -1,24 +0,0 @@
import React from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from './ParamsTable';
const ConditionEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
return (
<>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataIns || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataIns: data
});
}}
/>
</>
);
};
export default ConditionEditor;

@ -0,0 +1,531 @@
import React, { useState, useEffect } from 'react';
import { Input, Select, Table, Button } from '@arco-design/web-react';
import { IconDelete } from '@arco-design/web-react/icon';
interface TableDataItem {
key: number | string;
id: string;
apiOutId: string,
lftVal: string,
operator: string,
valueType: string,
rgtVal: string
[key: string]: any; // 允许其他自定义字段
}
interface ConditionsTableProps {
initialData: any;
nodeData: any;
onUpdateData: (data: any) => void;
type?: string;
}
const dataTypeOptions = [
{ label: '字符串', value: 'string' },
{ label: '数字', value: 'number' },
{ label: '布尔值(真)', value: 'boolean-true' },
{ label: '布尔值(假)', value: 'boolean-false' },
{ label: '表达式', value: 'expression' }
];
const operationOptions = [
{ label: '==', value: '==' },
{ label: '!=', value: '!=' },
{ label: '>', value: '>' },
{ label: '<', value: '<' },
{ label: '>=', value: '>=' },
{ label: '<=', value: '<=' }
// { label: '包含', value: 'contains' },
// { label: '不包含', value: 'notContains' },
// { label: '匹配正则表达式', value: 'matchRegex' },
// { label: '不匹配正则表达式', value: 'notMatchRegex' },
// { label: '为空', value: 'isEmpty' },
// { label: '不为空', value: 'isNotEmpty' },
// { label: '为真', value: 'isTrue' },
// { label: '为假', value: 'isFalse' },
// { label: '为空或为假', value: 'isEmptyOrFalse' },
// { label: '不为空或为真', value: 'isNotEmptyOrTrue' }
];
const ConditionsTable: React.FC<ConditionsTableProps> = ({
initialData,
nodeData,
onUpdateData,
type = 'LOOP'
}) => {
const [data, setData] = useState<any[]>([]);
const [rowData, setRowData] = useState<any>({});
const [apiOutsList, setApiOutsList] = useState([]);
const [leftList, setLeftList] = useState([]);
const columns = [
{
title: '序号',
dataIndex: 'index',
render: (_: any, record: TableDataItem, i) => (
<span>{i + 1}</span>
)
},
{
title: '逻辑出口',
dataIndex: 'apiOutId',
render: (_: any, record: TableDataItem) => (
<Input
value={record.apiOutId}
onChange={(value) => {
// 仅更新本地状态,不立即触发保存
const newData = [...data];
const index = newData.findIndex((item) => record.key === item.key);
if (index >= 0) {
newData.splice(index, 1, { ...newData[index], apiOutId: value });
setData(newData);
}
}}
onBlur={() => {
// 失去焦点时才触发保存
const currentRow = data.find(item => item.key === record.key);
if (currentRow) {
handleSave(currentRow);
}
}}
onPressEnter={() => {
// 按回车键时也触发保存
const currentRow = data.find(item => item.key === record.key);
if (currentRow) {
handleSave(currentRow);
}
}}
placeholder="请输入逻辑出口"
/>
)
},
{
title: '左值',
dataIndex: 'lftVal',
render: (_: any, record: TableDataItem) => (
<Select
autoWidth={{ minWidth: 200, maxWidth: 500 }}
options={leftList}
value={record.lftVal}
onChange={(value) => {
// 仅更新本地状态,不立即触发保存
const newData = [...data];
const index = newData.findIndex((item) => record.key === item.key);
if (index >= 0) {
newData.splice(index, 1, { ...newData[index], lftVal: value });
setData(newData);
}
}}
onBlur={() => {
// 失去焦点时才触发保存
const currentRow = data.find(item => item.key === record.key);
if (currentRow) {
handleSave(currentRow);
}
}}
placeholder="请选择需要引用的出入参数"
/>
)
},
{
title: '运算/比较',
dataIndex: 'operator',
render: (_: any, record: TableDataItem) => (
<Select
autoWidth={{ minWidth: 200, maxWidth: 500 }}
options={operationOptions}
value={record.operator}
onChange={(value) => {
// 仅更新本地状态,不立即触发保存
const newData = [...data];
const index = newData.findIndex((item) => record.key === item.key);
if (index >= 0) {
newData.splice(index, 1, { ...newData[index], operator: value });
setData(newData);
}
}}
onBlur={() => {
// 失去焦点时才触发保存
const currentRow = data.find(item => item.key === record.key);
if (currentRow) {
handleSave(currentRow);
}
}}
placeholder="请选择运算/比较符"
/>
)
},
{
title: '右值',
dataIndex: 'valueType',
render: (_: any, record: TableDataItem) => (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Select
autoWidth={{ minWidth: 200, maxWidth: 500 }}
options={dataTypeOptions}
value={record.valueType}
onChange={(value) => {
// 仅更新本地状态,不立即触发保存
const newData = [...data];
const index = newData.findIndex((item) => record.key === item.key);
if (index >= 0) {
newData.splice(index, 1, {
...newData[index],
valueType: value,
// 如果类型不是string、number或expression则清空rgtVal
rgtVal: ['string', 'number', 'expression'].includes(value) ? newData[index].rgtVal : ''
});
setData(newData);
}
}}
onBlur={() => {
// 失去焦点时才触发保存
const currentRow = data.find(item => item.key === record.key);
if (currentRow) {
handleSave(currentRow);
}
}}
placeholder="请选择右值类型"
/>
{['string', 'number', 'expression'].includes(record.valueType) ? (
<Input
style={{ marginTop: 8 }}
autoWidth={{ minWidth: 200, maxWidth: 500 }}
value={record.rgtVal}
onChange={(value) => {
// 仅更新本地状态,不立即触发保存
const newData = [...data];
const index = newData.findIndex((item) => record.key === item.key);
if (index >= 0) {
newData.splice(index, 1, { ...newData[index], rgtVal: value });
setData(newData);
}
}}
onBlur={() => {
// 失去焦点时才触发保存
const currentRow = data.find(item => item.key === record.key);
if (currentRow) {
handleSave(currentRow);
}
}}
onPressEnter={() => {
// 按回车键时也触发保存
const currentRow = data.find(item => item.key === record.key);
if (currentRow) {
handleSave(currentRow);
}
}}
placeholder={'请输入'}
/>
) : (<span></span>)}
</div>
)
},
{
title: '操作',
dataIndex: 'op',
render: (_: any, record: TableDataItem) => (
<Button onClick={() => removeRow(record.key)} type="text" status="danger">
<IconDelete />
</Button>
)
}
];
const convertData = (originData) => {
const apiOutIds = apiOutsList;
const conditions = originData.map(item => {
let expression = '';
if (item.valueType.includes('boolean')) {
const splitStr = item.valueType.split('-')[1];
expression = `$.${item.lftVal}${item.operator}${splitStr}`;
}
else expression = `$.${item.lftVal}${item.operator}${item.rgtVal}`;
if (item.apiOutId && !apiOutIds.includes(item.apiOutId)) {
apiOutIds.push(item.apiOutId);
}
return {
apiOutId: item.apiOutId,
valueType: item.valueType,
expression: expression
};
});
const customDef = {
apiOutIds,
conditions
};
// 只需要动态添加开始节点的NodeId 循环开始的节点不允许编辑信息的,所以接口需要的信息会在节点实例的时候配置
if (type === 'LOOP') customDef['loopStartNodeId'] = nodeData.component.loopStartNodeId;
return {
type: nodeData.type,
customDef: JSON.stringify(customDef)
};
};
const getOperator = (expr: string) => {
let operator;
if (expr.includes('==')) {
operator = '==';
}
else if (expr.includes('>=')) {
operator = '>=';
}
else if (expr.includes('<=')) {
operator = '<=';
}
else if (expr.includes('<')) {
operator = '<';
}
else if (expr.includes('>')) {
operator = '>';
}
else {
operator = '!=';
}
return operator;
};
// 反转结构的函数,将处理后的数据转回原始格式
const reverseDataStructure = (processedData: any) => {
try {
const parsedCustomDef = JSON.parse(processedData.customDef);
if (!parsedCustomDef.conditions) {
return [];
}
return parsedCustomDef.conditions.map((condition: any, index: number) => {
// 解析表达式以获取左值、操作符和右值
let lftVal = '';
let operator = '';
let rgtVal = '';
const valueType = condition.valueType || '';
if (condition.expression) {
// 处理布尔值表达式
if (valueType.includes('boolean')) {
const splitStr = valueType.split('-')[1];
operator = getOperator(condition.expression);
const pattern = new RegExp(`\\$\\.(.+)(${operator})${splitStr}`);
const match = condition.expression.match(pattern);
if (match) {
lftVal = match[1];
operator = match[2];
}
}
// 处理其他类型的表达式
else {
// 简单的解析逻辑,可能需要根据实际表达式格式进行调整
const match = condition.expression.match(/\$\.([^=!<>]+)(==|!=|>=|<=|>|<)(.+)/);
if (match) {
lftVal = match[1];
operator = match[2];
rgtVal = match[3];
}
}
}
return {
key: index,
id: Date.now(),
apiOutId: condition.apiOutId || '',
lftVal,
operator,
valueType,
rgtVal
};
});
} catch (e) {
console.error('Error parsing customDef:', e);
return [];
}
};
// 提取apiIns和apiOuts中的name属性合并成一个一维数组
const extractApiNames = () => {
const apiInsNames = nodeData.parameters?.apiIns?.map((item: any) => item.name) || [];
const apiOutsNames = nodeData.parameters?.apiOuts?.map((item: any) => item.name) || [];
if (type === 'LOOP') return [...apiInsNames, ...apiOutsNames];
else return [...apiOutsNames];
};
// 提取dataIns中的属性并构造成options结构
const extractDataInsOptions = () => {
const dataInsOptions = nodeData.parameters?.dataIns?.map((item: any) => ({
value: item.id,
label: item.id
})) || [];
return [...dataInsOptions];
};
const updateOriginData = (data) => {
// 获取现有的apiOuts数组确保我们保留所有原始数据
const existingApiOuts = nodeData.parameters?.apiOuts || [];
// 创建一个Map来存储当前表格中的数据便于快速查找
const tableDataMap = new Map();
data.forEach(item => {
if (item.apiOutId) {
tableDataMap.set(item.apiOutId, item);
}
});
// 更新现有的数据或标记需要删除的数据
const updatedApiOuts = existingApiOuts.map(item => {
// 如果在表格数据中存在,则更新它
if (item.id && tableDataMap.has(item.id)) {
const tableItem = tableDataMap.get(item.id);
let expression = '';
if (tableItem.valueType.includes('boolean')) {
const splitStr = tableItem.valueType.split('-')[1];
expression = `$.${tableItem.lftVal}${tableItem.operator}${splitStr}`;
}
else {
expression = `$.${tableItem.lftVal}${tableItem.operator}${tableItem.rgtVal}`;
}
// 更新现有项,但保留原始属性
return {
...item,
name: tableItem.apiOutId,
id: tableItem.apiOutId,
desc: item.desc || '',
defaultValue: tableItem.valueType,
dataType: expression
};
}
// 如果不在表格数据中,保持原样(可能是其他地方引用的数据)
return item;
});
// 添加表格中有但原始数据中没有的新项
const existingIds = new Set(existingApiOuts.map(item => item.id));
const newItems = data
.filter(item => item.apiOutId && !existingIds.has(item.apiOutId))
.map(tableItem => {
let expression = '';
if (tableItem.valueType.includes('boolean')) {
const splitStr = tableItem.valueType.split('-')[1];
expression = `$.${tableItem.lftVal}${tableItem.operator}${splitStr}`;
}
else {
expression = `$.${tableItem.lftVal}${tableItem.operator}${tableItem.rgtVal}`;
}
return {
name: tableItem.apiOutId,
id: tableItem.apiOutId,
desc: '',
defaultValue: tableItem.valueType,
dataType: expression
};
});
// 合并更新后的数据和新增数据
return [...updatedApiOuts, ...newItems];
};
const handleSave = (row: TableDataItem) => {
const newData = [...data];
const index = newData.findIndex((item) => row.key === item.key);
if (index >= 0) {
newData.splice(index, 1, { ...newData[index], ...row });
}
else {
newData.push(row);
}
setData(newData);
// 重新构建数据结构
const newApiOuts = updateOriginData(newData);
const newComponentData = convertData(newData);
onUpdateData({
...nodeData,
parameters: {
...nodeData.parameters,
apiOuts: newApiOuts
},
component: newComponentData
});
};
const removeRow = (key: number | string) => {
const newData = data.filter((item) => item.key !== key);
setData(newData);
// 重新构建数据结构
const newApiOuts = updateOriginData(newData);
const newComponentData = convertData(newData);
onUpdateData({
...nodeData,
parameters: {
...nodeData.parameters,
apiOuts: newApiOuts
},
component: newComponentData
});
};
const addRow = () => {
const newKey = Date.now();
const newRow = {
key: newKey,
id: '',
apiOutId: '',
lftVal: '',
operator: '',
valueType: '',
rgtVal: ''
};
const newData = [...data, newRow];
setData(newData);
// 重新构建数据结构 添加新行时不更新
// const newApiOuts = updateOriginData(newData);
// const newComponentData = convertData(newData);
// onUpdateData({
// ...nodeData,
// parameters: {
// ...nodeData.parameters,
// apiOuts: newApiOuts
// },
// component: newComponentData
// });
};
// 监听nodeData.parameters.dataIns的变化更新leftList
useEffect(() => {
if (nodeData.parameters?.dataIns) {
setLeftList(extractDataInsOptions());
}
}, [nodeData.parameters?.dataIns]);
useEffect(() => {
try {
console.log('nodeData:', nodeData);
setApiOutsList(extractApiNames());
setLeftList(extractDataInsOptions());
setData(reverseDataStructure(initialData));
} catch (e) {
setApiOutsList([]);
setLeftList([]);
setData([]);
}
}, []);
return (
<>
<Table columns={columns} data={data} pagination={false} />
<Button
style={{ height: 45 }}
long
type="outline"
onClick={addRow}
>
+
</Button>
</>
);
};
export default ConditionsTable;

@ -1,24 +1,56 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { Form, Input, Typography } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import EventSelect from './EventSelect';
import { tempEventList } from '@/pages/flowEditor/test/exampleFlowData';
import { useDispatch, useSelector } from 'react-redux';
import { queryEventItemBySceneId } from '@/api/event';
import ParamsTable from '@/components/FlowEditor/nodeEditors/components/ParamsTable';
const EventListenEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
const [eventList, setEventList] = useState<any[]>(tempEventList);
const [eventList, setEventList] = useState<any[]>();
const { currentAppData } = useSelector(state => state.ideContainer);
const getEventList = async () => {
const res = await queryEventItemBySceneId(currentAppData.sceneId);
setEventList(res.data);
};
useEffect(() => {
getEventList();
}, []);
return (
<>
<Form layout="vertical">
<Form.Item label="节点标题">
<Input
value={nodeData.title || ''}
onChange={(value) => updateNodeData('title', value)}
/>
</Form.Item>
</Form>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<EventSelect
nodeData={nodeData}
eventList={eventList}
type="send"
type="listen"
onRefresh={getEventList}
onUpdateData={(data) => {
updateNodeData('component', {
...data
});
}}></EventSelect>
}} />
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataOuts || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataOuts: data
});
}}
/>
</>
);
};

@ -1,14 +1,19 @@
import React, { useEffect, useState } from 'react';
import { Select, Divider, Modal, Button, Form, Input } from '@arco-design/web-react';
import { Select, Divider, Modal, Button, Form, Input, Message } from '@arco-design/web-react';
import { IconPlus } from '@arco-design/web-react/icon';
import { useDispatch, useSelector } from 'react-redux';
import { addEventItem } from '@/api/event';
import { AddEventParams } from '@/api/interface';
const FormItem = Form.Item;
const TextArea = Input.TextArea;
const Option = Select.Option;
interface EventSelectProps {
nodeData: any;
eventList: any[];
type: 'send' | 'listen';
onRefresh: () => void;
onUpdateData: (data) => void;
}
@ -17,14 +22,38 @@ const typeMap = {
listen: 'EVENTLISTENE'
};
const EventSelect: React.FC<EventSelectProps> = ({ eventList, type, onUpdateData }) => {
const EventSelect: React.FC<EventSelectProps> = ({ nodeData, eventList, type, onRefresh, onUpdateData }) => {
const [options, setOptions] = useState<any[]>([]);
const [specialOptions, setSpecialOptions] = useState<any>({});
const [form] = Form.useForm();
const [showModal, setShowModal] = useState(false);
const [currentEvent, setCurrentEvent] = useState<any>(null);
const { currentAppData } = useSelector(state => state.ideContainer);
useEffect(() => {
setOptions(eventList);
}, [eventList]);
if (nodeData && eventList && eventList.length > 0) {
setSpecialOptions(eventList.find(item => item.topic.includes('**empty**')));
setOptions(eventList.filter(item => !item.topic.includes('**empty**')));
try {
const customDef = JSON.parse(nodeData.component?.customDef);
// 先判断topic是不是**empty**是就不设置currentevent
if (customDef.topic && customDef.topic.includes('**empty**')) {
setCurrentEvent(null);
}
else {
setCurrentEvent(eventList.find(item => customDef.eventId === item.eventId));
}
} catch (e) {
// 先判断topic是不是**empty**是就不设置currentevent
if (nodeData.component?.customDef?.topic && nodeData.component?.customDef?.topic.includes('**empty**')) {
setCurrentEvent(null);
}
else {
setCurrentEvent(eventList.find(item => nodeData.component?.customDef.eventId === item.eventId));
}
}
}
}, [nodeData, eventList]);
const addItem = () => {
setShowModal(true);
@ -32,9 +61,21 @@ const EventSelect: React.FC<EventSelectProps> = ({ eventList, type, onUpdateData
const saveForm = async () => {
try {
// TODO 需要对接事件新增的接口
await form.validate();
console.log('form:', form.getFields());
const formData = form.getFields();
const params = {
...formData,
sceneId: currentAppData.sceneId
};
const res: any = await addEventItem(params as AddEventParams);
if (res && res.code === 200) {
Message.success('添加成功');
onRefresh();
}
else {
Message.error(res.message);
}
setShowModal(false);
} catch (e) {
}
@ -44,7 +85,7 @@ const EventSelect: React.FC<EventSelectProps> = ({ eventList, type, onUpdateData
const data = {
type: typeMap[type],
customDef: {
eventId: e.id,
eventId: e.eventId,
name: e.name,
topic: e.topic
}
@ -55,6 +96,7 @@ const EventSelect: React.FC<EventSelectProps> = ({ eventList, type, onUpdateData
return (
<>
<Select
value={currentEvent}
placeholder="请选择事件"
onChange={(e) => handelSelect(e)}
dropdownRender={(menu) => (
@ -84,7 +126,7 @@ const EventSelect: React.FC<EventSelectProps> = ({ eventList, type, onUpdateData
dropdownMenuStyle={{ maxHeight: 300 }}
>
{options.map((option) => (
<Option key={option.id} value={option}>
<Option key={option.eventId} value={option}>
{option.name}
</Option>
))}
@ -129,6 +171,9 @@ const EventSelect: React.FC<EventSelectProps> = ({ eventList, type, onUpdateData
if (!value) {
return cb('请填写事件标识');
}
if (value.includes('**empty**')) {
return cb('非法事件标识,请重新输入');
}
return cb();
}
@ -140,6 +185,18 @@ const EventSelect: React.FC<EventSelectProps> = ({ eventList, type, onUpdateData
<FormItem
label="事件描述"
field="description"
required
rules={[
{
validator(value, cb) {
if (!value) {
return cb('请填写事件描述');
}
return cb();
}
}
]}
>
<TextArea placeholder="请输入事件描述" />
</FormItem>

@ -1,23 +1,56 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { Typography, Form, Input } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import { useDispatch, useSelector } from 'react-redux';
import EventSelect from '@/components/FlowEditor/nodeEditors/components/EventSelect';
import { tempEventList } from '@/pages/flowEditor/test/exampleFlowData';
import { queryEventItemBySceneId } from '@/api/event';
import ParamsTable from '@/components/FlowEditor/nodeEditors/components/ParamsTable';
const EventSendEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
const [eventList, setEventList] = useState<any[]>(tempEventList);
const [eventList, setEventList] = useState<any[]>();
const { currentAppData } = useSelector(state => state.ideContainer);
const getEventList = async () => {
const res = await queryEventItemBySceneId(currentAppData.sceneId);
setEventList(res.data);
};
useEffect(() => {
getEventList();
}, []);
return (
<>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<Form layout="vertical">
<Form.Item label="节点标题">
<Input
value={nodeData.title || ''}
onChange={(value) => updateNodeData('title', value)}
/>
</Form.Item>
</Form>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<EventSelect
nodeData={nodeData}
eventList={eventList}
type="send"
onRefresh={getEventList}
onUpdateData={(data) => {
updateNodeData('component', {
...data
});
}}></EventSelect>
}} />
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataIns || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataIns: data
});
}}
/>
</>
);
};

@ -0,0 +1,115 @@
import React, { useState, useEffect } from 'react';
import { Input, Select, Table, Button } from '@arco-design/web-react';
import { IconDelete } from '@arco-design/web-react/icon';
interface TableDataItem {
key: number | string;
id: string;
dataType: string;
arrayType: string;
desc: string;
defaultValue: string;
[key: string]: any; // 允许其他自定义字段
}
interface ParamsTableProps {
initialData: TableDataItem[] | any;
onChange: (data) => void;
}
const ParamsTable: React.FC<ParamsTableProps> = ({
initialData,
onChange
}) => {
const [data, setData] = useState([]);
useEffect(() => {
// 为现有数据添加key属性如果不存在
const dataWithKeys = initialData?.map((item, index) => ({
...item,
key: item.key ?? index
})) || [];
setData(dataWithKeys);
}, [initialData]);
const columns = [
{
title: 'key',
dataIndex: 'paramsKey',
render: (_: any, record: TableDataItem) => (
<Input
value={record.paramsKey}
onChange={(value) => handleSave({ ...record, paramsKey: value })}
/>
)
},
{
title: 'value',
dataIndex: 'paramsValue',
render: (_: any, record: TableDataItem) => (
<Input
value={record.paramsValue}
onChange={(value) => handleSave({ ...record, paramsValue: value })}
/>
)
},
{
title: '操作',
dataIndex: 'op',
render: (_: any, record: TableDataItem) => (
record.id !== 'maxTime' && <Button onClick={() => removeRow(record.key)} type="text" status="danger">
<IconDelete />
</Button>
)
}
];
const handleSave = (row: TableDataItem) => {
const newData = [...data];
const index = newData.findIndex((item) => row.key === item.key);
if (index >= 0) {
newData.splice(index, 1, { ...newData[index], ...row });
}
else {
newData.push(row);
}
setData(newData);
onChange(newData);
};
const removeRow = (key: number | string) => {
const newData = data.filter((item) => item.key !== key);
setData(newData);
onChange(newData);
};
const addRow = () => {
const newKey = Date.now();
const newRow = {
key: newKey,
paramsKey: '',
paramsValue: ''
};
const newData = [...data, newRow];
setData(newData);
onChange(newData);
};
return (
<>
<Table columns={columns} data={data} pagination={false} />
<Button
style={{ height: 45 }}
long
type="outline"
onClick={addRow}
>
+
</Button>
</>
);
};
export default ParamsTable;

@ -1,8 +1,9 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { Typography, Slider } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from './ParamsTable';
import ParamsTable from '@/components/FlowEditor/nodeEditors/components/ParamsTable';
import ConditionsTable from '@/components/FlowEditor/nodeEditors/components/ConditionsTable';
const LoopEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
return (
@ -17,6 +18,22 @@ const LoopEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) =>
});
}}
/>
<Typography.Title heading={5}>
<IconUnorderedList style={{ marginRight: 5, marginTop: 20 }} />
</Typography.Title>
<ConditionsTable
initialData={nodeData.component || {}}
nodeData={nodeData || null}
onUpdateData={(data) => {
updateNodeData('component', {
...data.component
});
updateNodeData('parameters', {
...nodeData.parameters,
apiOuts: data.parameters.apiOuts
});
}} />
</>
);
};

@ -13,15 +13,15 @@ interface TableDataItem {
[key: string]: any; // 允许其他自定义字段
}
interface EndNodeTableProps {
interface ParamsTableProps {
initialData: TableDataItem[];
onUpdateData: (data: TableDataItem[]) => void;
}
const EndNodeTable: React.FC<EndNodeTableProps> = ({
initialData,
onUpdateData
}) => {
const ParamsTable: React.FC<ParamsTableProps> = ({
initialData,
onUpdateData
}) => {
const [data, setData] = useState<TableDataItem[]>([]);
useEffect(() => {
@ -52,23 +52,31 @@ const EndNodeTable: React.FC<EndNodeTableProps> = ({
title: '标识',
dataIndex: 'id',
render: (_: any, record: TableDataItem) => (
<Input
value={record.id}
onChange={(value) => handleSave({ ...record, id: value })}
/>
record.id === 'maxTime' ? (
<span>{record.id}</span>
) : (
<Input
value={record.id}
onChange={(value) => handleSave({ ...record, id: value })}
/>
)
)
},
{
title: '数据类型',
dataIndex: 'dataType',
render: (_: any, record: TableDataItem) => (
<Select
autoWidth={{ minWidth: 200, maxWidth: 500 }}
options={dataTypeOptions}
value={record.dataType}
onChange={(value) => handleSave({ ...record, dataType: value })}
placeholder="请选择数据类型"
/>
record.id === 'maxTime' ? (
<span>{record.dataType === 'INTEGER' ? '整数' : record.dataType}</span>
) : (
<Select
autoWidth={{ minWidth: 200, maxWidth: 500 }}
options={dataTypeOptions}
value={record.dataType}
onChange={(value) => handleSave({ ...record, dataType: value })}
placeholder="请选择数据类型"
/>
)
)
},
{
@ -92,27 +100,41 @@ const EndNodeTable: React.FC<EndNodeTableProps> = ({
title: '描述',
dataIndex: 'desc',
render: (_: any, record: TableDataItem) => (
<Input
value={record.desc}
onChange={(value) => handleSave({ ...record, desc: value })}
/>
record.id === 'maxTime' ? (
<span>{record.desc}</span>
) : (
<Input
value={record.desc}
onChange={(value) => handleSave({ ...record, desc: value })}
/>
)
)
},
{
title: '默认值',
dataIndex: 'defaultValue',
render: (_: any, record: TableDataItem) => (
<Input
value={record.defaultValue}
onChange={(value) => handleSave({ ...record, defaultValue: value })}
/>
record.id === 'maxTime' ? (
<Input
type="number"
min="1"
step="1"
value={record.defaultValue}
onChange={(value) => handleSave({ ...record, defaultValue: value })}
/>
) : (
<Input
value={record.defaultValue}
onChange={(value) => handleSave({ ...record, defaultValue: value })}
/>
)
)
},
{
title: '操作',
dataIndex: 'op',
render: (_: any, record: TableDataItem) => (
<Button onClick={() => removeRow(record.key)} type="text" status="danger">
record.id !== 'maxTime' && <Button onClick={() => removeRow(record.key)} type="text" status="danger">
<IconDelete />
</Button>
)
@ -168,4 +190,4 @@ const EndNodeTable: React.FC<EndNodeTableProps> = ({
);
};
export default EndNodeTable;
export default ParamsTable;

@ -1,22 +1,269 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { Typography, Form, Grid, Select, Input, Button, Link, Message } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from './ParamsTable';
import KYTable from '@/components/FlowEditor/nodeEditors/components/KYTable';
import CodeMirror from '@uiw/react-codemirror';
import { json } from '@codemirror/lang-json';
const Row = Grid.Row;
const Col = Grid.Col;
const FormItem = Form.Item;
const Option = Select.Option;
const RestEditor: React.FC<NodeEditorProps & {
onValidationChange?: (isValid: boolean) => void
}> = ({ nodeData, updateNodeData, onValidationChange }) => {
const [formData, setFormData] = useState({
method: 'GET',
url: '',
inputType: 'JSON',
headers: [],
defaultInput: '',
dataIns: nodeData.parameters.dataIns || [],
component: {}
});
const [isShowHeaders, setIsShowHeaders] = useState(false);
const [isShowDataIns, setIsShowDataIns] = useState(false);
const [isValidUrl, setIsValidUrl] = useState(true); // 添加URL有效性状态
const [value, setValue] = useState('');
const methodList = ['GET', 'POST'];
const dataTypeList = [
{ label: 'JSON', value: 'JSON' },
{ label: '路径参数', value: '路径参数' }
];
// URL 校验函数
const validateUrl = useCallback((url: string) => {
if (!url) return true; // 空URL认为是有效的但会在提交时提示
// 基本的 URL 格式校验正则表达式
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
return urlPattern.test(url);
}, []);
// 通知父组件校验状态变化
useEffect(() => {
if (onValidationChange) {
onValidationChange(isValidUrl);
}
}, [isValidUrl, onValidationChange]);
// 初始化数据
useEffect(() => {
if (nodeData.component && nodeData.component.customDef) {
try {
const customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
setFormData(prev => ({
...prev,
method: customDef.method || 'GET',
url: customDef.url || '',
inputType: customDef.inputType || 'JSON',
headers: customDef.headers || [],
defaultInput: customDef.defaultInput || '',
dataIns: nodeData.parameters.dataIns || []
}));
// 检查初始URL有效性
const initialUrlValid = validateUrl(customDef.url || '');
setIsValidUrl(initialUrlValid);
setIsShowHeaders(!!(customDef.headers && customDef.headers.length > 0));
setIsShowDataIns(!!(nodeData.parameters.dataIns && nodeData.parameters.dataIns.length > 0));
} catch (e) {
console.error('解析组件数据失败:', e);
}
}
}, [nodeData, validateUrl]);
// 更新表单数据时同步到节点
const handleFormChange = (newFormData) => {
setFormData(newFormData);
// 更新 component 数据
const componentData = {
...nodeData.component,
customDef: JSON.stringify({
method: newFormData.method,
url: newFormData.url,
inputType: newFormData.inputType,
headers: newFormData.headers,
defaultInput: newFormData.defaultInput
}),
type: 'REST'
};
updateNodeData('component', componentData);
};
const handleChangeMethod = (method) => {
handleFormChange({ ...formData, method });
};
const handleUrlChange = (url) => {
// 实时校验URL
const isValid = validateUrl(url);
setIsValidUrl(isValid);
handleFormChange({ ...formData, url });
// 可选:在用户输入时实时校验 URL
if (url && !isValid) {
// 这里可以添加实时提示,但不阻止用户输入
console.warn('URL 格式可能不正确');
}
};
const formatJSON = () => {
try {
console.log('formData.defaultInput:', formData.defaultInput);
const formatted = JSON.stringify(JSON.parse(formData.defaultInput), null, 2);
handleFormChange({ ...formData, defaultInput: formatted });
} catch (e) {
// 如果不是有效的JSON不进行格式化
Message.error('无效的 JSON 格式');
}
};
// URL 校验并提示
const handleUrlBlur = () => {
if (formData.url && !validateUrl(formData.url)) {
Message.warning('请输入有效的 URL 地址');
}
};
const RestEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
return (
<>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataIns || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataIns: data
});
}}
/>
<Form layout="vertical">
<div style={{ padding: '0 16px' }}>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<Row gutter={10}>
<Col span={6}>
<FormItem label="方法:" required>
<Select
value={formData.method}
onChange={handleChangeMethod}
>
{methodList.map(method => (
<Option key={method} value={method}>
{method}
</Option>
))}
</Select>
</FormItem>
</Col>
<Col span={12}>
<FormItem label="URL地址:" required validateStatus={formData.url && !isValidUrl ? 'error' : 'success'}>
<Input
value={formData.url}
onChange={handleUrlChange}
onBlur={handleUrlBlur}
placeholder="请输入URL地址例如: https://api.example.com/endpoint"
/>
{formData.url && !isValidUrl && (
<div style={{ color: '#ff0000', fontSize: '12px', marginTop: '4px' }}>
URL
</div>
)}
</FormItem>
</Col>
<Col span={6}>
<FormItem label="参数类型:">
<Select
value={formData.inputType}
onChange={inputType => handleFormChange({ ...formData, inputType })}
>
{dataTypeList.map(item => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
</Col>
</Row>
</div>
<div style={{ padding: '0 16px' }}>
<Row gutter={24}>
<Col span={24}>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} />
{!isShowHeaders ? (
<Link onClick={() => setIsShowHeaders(true)} style={{ marginLeft: 8 }}>
</Link>
) : null}
</Typography.Title>
<FormItem>
{isShowHeaders ? (
<div>
{/* 这里需要实现KV编辑器暂时用简单输入框替代 */}
<KYTable
initialData={formData.headers}
onChange={(data) => {
handleFormChange({ ...formData, headers: data });
}} />
</div>
) : null}
</FormItem>
</Col>
<Col span={24}>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} />
<Link onClick={formatJSON} style={{ marginLeft: 8 }}>
</Link>
</Typography.Title>
<FormItem>
<CodeMirror
value={value}
height="300px"
extensions={[json()]}
onChange={data => {
setValue(data);
handleFormChange({ ...formData, defaultInput: data });
}} />
</FormItem>
</Col>
<Col span={24}>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} />
{!isShowDataIns ? (
<Link onClick={() => setIsShowDataIns(true)} style={{ marginLeft: 8 }}>
</Link>
) : null}
</Typography.Title>
<FormItem>
{isShowDataIns ? (
<div>
<ParamsTable
initialData={formData.dataIns}
onUpdateData={(dataIns) => {
handleFormChange({ ...formData, dataIns });
// 同时更新parameters.dataIns
updateNodeData('parameters', {
...nodeData.parameters,
dataIns
});
}}
/>
</div>
) : null}
</FormItem>
</Col>
</Row>
</div>
</Form>
</>
);
};

@ -0,0 +1,43 @@
import React from 'react';
import { NodeEditorProps } from '@/components/FlowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from './ParamsTable';
import ConditionsTable from '@/components/FlowEditor/nodeEditors/components/ConditionsTable';
const SwitchEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
return (
<>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataIns || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataIns: data
});
}}
/>
<Typography.Title heading={5}>
<IconUnorderedList style={{ marginRight: 5, marginTop: 20 }} />
</Typography.Title>
<ConditionsTable
initialData={nodeData.component || {}}
nodeData={nodeData || null}
onUpdateData={(data) => {
updateNodeData('component', {
...data.component
});
updateNodeData('parameters', {
...nodeData.parameters,
apiOuts: data.parameters.apiOuts
});
}}
type="switch"
/>
</>
);
};
export default SwitchEditor;

@ -10,6 +10,8 @@ export interface NodeEditorProps {
node?: Node;
nodeData: any;
updateNodeData: (key: string, value: any) => void;
[key: string]: any;
}
// 节点编辑器映射
@ -39,6 +41,8 @@ export const getNodeEditorByType = (nodeType: string, localNodeType?: string) =>
return nodeEditors[nodeType] || nodeEditors['basic'];
};
export * from './validators';
export default {
nodeEditors,
registerNodeEditor,

@ -0,0 +1,518 @@
import { Message } from '@arco-design/web-react';
import { Edge } from '@xyflow/react';
export interface ValidationResult {
isValid: boolean;
errors: string[];
}
/**
*
* @param nodeData
* @param nodeType
* @returns
*/
export const validateNodeData = (nodeData: any, nodeType: string): ValidationResult => {
const errors: string[] = [];
// 检查基本字段
if (!nodeData.title) {
errors.push('节点标题不能为空');
}
// 根据不同节点类型进行特定校验
switch (nodeType) {
case 'REST':
errors.push(...validateRestNode(nodeData));
break;
case 'CODE':
errors.push(...validateCodeNode(nodeData));
break;
case 'SWITCH':
errors.push(...validateSwitchNode(nodeData));
break;
case 'LOOP_START':
case 'LOOP_END':
errors.push(...validateLoopNode(nodeData, nodeType));
break;
// case 'EVENTSEND':
// case 'EVENTLISTENE':
// errors.push(...validateEventNode(nodeData));
// break;
case 'WAIT':
errors.push(...validateWaitNode(nodeData));
break;
case 'CYCLE':
errors.push(...validateCycleNode(nodeData));
break;
default:
// 对于其他节点类型,检查基本参数
errors.push(...validateBasicParams(nodeData));
break;
}
return {
isValid: errors.length === 0,
errors
};
};
/**
* REST
* @param nodeData
* @returns
*/
const validateRestNode = (nodeData: any): string[] => {
const errors: string[] = [];
if (!nodeData.component || !nodeData.component.customDef) {
errors.push('REST节点配置信息不完整');
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
errors.push('REST节点配置信息格式错误');
return errors;
}
if (!customDef.method) {
errors.push('请求方法不能为空');
}
if (!customDef.url) {
errors.push('URL地址不能为空');
}
else {
// 基本的 URL 格式校验
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
if (!urlPattern.test(customDef.url)) {
errors.push('URL地址格式不正确');
}
}
return errors;
};
/**
*
* @param nodeData
* @returns
*/
const validateCodeNode = (nodeData: any): string[] => {
const errors: string[] = [];
if (!nodeData.component || !nodeData.component.customDef) {
errors.push('代码节点配置信息不完整');
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
errors.push('代码节点配置信息格式错误');
return errors;
}
if (!customDef.sourceCode) {
errors.push('代码内容不能为空');
}
return errors;
};
/**
*
* @param nodeData
* @returns
*/
const validateSwitchNode = (nodeData: any): string[] => {
const errors: string[] = [];
// 检查条件表达式
if (!nodeData.component || !nodeData.component.customDef) {
errors.push('条件节点配置信息不完整');
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
errors.push('条件节点配置信息格式错误');
return errors;
}
// 检查是否有条件表达式
if (!customDef.conditions || customDef.conditions.length === 0) {
errors.push('请至少添加一个条件表达式');
}
// 检查每个条件表达式的完整性
if (customDef.conditions && customDef.conditions.length > 0) {
customDef.conditions.forEach((condition: any, index: number) => {
if (!condition.apiOutId) {
errors.push(`${index + 1}个条件表达式的逻辑出口不能为空`);
}
if (!condition.expression) {
errors.push(`${index + 1}个条件表达式的表达式不能为空`);
}
});
}
return errors;
};
/**
*
* @param nodeData
* @param nodeType
* @returns
*/
const validateLoopNode = (nodeData: any, nodeType: string): string[] => {
// LOOP_START 类型节点不需要校验
if (nodeType === 'LOOP_START') {
return [];
}
const errors: string[] = [];
// 检查循环条件(仅针对 LOOP_END 节点)
if (!nodeData.component || !nodeData.component.customDef) {
errors.push('循环节点配置信息不完整');
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
errors.push('循环节点配置信息格式错误');
return errors;
}
// // 检查循环条件
// if (!customDef.conditions || customDef.conditions.length === 0) {
// errors.push('请至少添加一个循环条件');
// }
return errors;
};
/**
*
* @param nodeData
* @returns
*/
const validateEventNode = (nodeData: any): string[] => {
const errors: string[] = [];
if (!nodeData.component || !nodeData.component.customDef) {
errors.push('事件节点配置信息不完整');
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
errors.push('事件节点配置信息格式错误');
return errors;
}
if (!customDef.eventId) {
errors.push('请选择事件');
}
return errors;
};
/**
*
* @param nodeData
* @returns
*/
const validateWaitNode = (nodeData: any): string[] => {
const errors: string[] = [];
if (!nodeData.component || !nodeData.component.customDef) {
errors.push('等待节点配置信息不完整');
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
errors.push('等待节点配置信息格式错误');
return errors;
}
// 检查等待时间是否设置
if (customDef.duration === undefined || customDef.duration === null) {
errors.push('请设置等待时间');
}
else if (typeof customDef.duration !== 'number' || customDef.duration < 0) {
errors.push('等待时间必须是非负数');
}
return errors;
};
/**
*
* @param nodeData
* @returns
*/
const validateCycleNode = (nodeData: any): string[] => {
const errors: string[] = [];
if (!nodeData.component || !nodeData.component.customDef) {
errors.push('周期节点配置信息不完整');
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
errors.push('周期节点配置信息格式错误');
return errors;
}
// 检查 Cron 表达式是否设置
if (!customDef.intervalSeconds) {
errors.push('请设置 Cron 表达式');
}
return errors;
};
/**
*
* @param nodeData
* @returns
*/
const validateBasicParams = (nodeData: any): string[] => {
const errors: string[] = [];
// 检查输入参数的完整性
if (nodeData.parameters?.dataIns) {
nodeData.parameters.dataIns.forEach((param: any, index: number) => {
if (!param.id) {
errors.push(`${index + 1}个输入参数的标识不能为空`);
}
if (!param.dataType) {
errors.push(`${index + 1}个输入参数的数据类型不能为空`);
}
});
}
// 检查输出参数的完整性
if (nodeData.parameters?.dataOuts) {
nodeData.parameters.dataOuts.forEach((param: any, index: number) => {
if (!param.id) {
errors.push(`${index + 1}个输出参数的标识不能为空`);
}
if (!param.dataType) {
errors.push(`${index + 1}个输出参数的数据类型不能为空`);
}
});
}
return errors;
};
/**
*
* @param nodes
* @returns
*/
export const validateAllNodes = (nodes: any[]): ValidationResult => {
const allErrors: string[] = [];
nodes.forEach((node, index) => {
const nodeType = node.data?.type || node.type;
const nodeName = node.data?.title || `节点${index + 1}`;
const result = validateNodeData(node.data, nodeType);
if (!result.isValid) {
result.errors.forEach(error => {
allErrors.push(`[${nodeName}] ${error}`);
});
}
});
return {
isValid: allErrors.length === 0,
errors: allErrors
};
};
/**
* 线
* @param edges 线
* @param nodes
* @returns
*/
export const validateAllEdges = (edges: Edge[], nodes: any[]): ValidationResult => {
const allErrors: string[] = [];
// 创建节点ID到节点标题的映射方便错误信息展示
const nodeTitleMap = new Map<string, string>();
const nodeMap = new Map<string, any>();
nodes.forEach(node => {
const nodeTitle = node.data?.title || node.id;
nodeTitleMap.set(node.id, nodeTitle);
nodeMap.set(node.id, node);
});
// 检查连接线的基本信息
edges.forEach((edge, index) => {
// 检查连接线是否有源节点和目标节点
if (!edge.source) {
allErrors.push(`${index + 1}条连接线缺少源节点`);
return;
}
if (!edge.target) {
allErrors.push(`${index + 1}条连接线缺少目标节点`);
return;
}
// 检查源节点和目标节点是否存在
if (edge.source && !nodeTitleMap.has(edge.source)) {
allErrors.push(`${index + 1}条连接线的源节点"${edge.source}"不存在`);
}
if (edge.target && !nodeTitleMap.has(edge.target)) {
allErrors.push(`${index + 1}条连接线的目标节点"${edge.target}"不存在`);
}
});
// 检查是否有未连接的节点(开始节点和结束节点除外)
const connectedNodeIds = new Set<string>();
edges.forEach(edge => {
if (edge.source) connectedNodeIds.add(edge.source);
if (edge.target) connectedNodeIds.add(edge.target);
});
nodes.forEach(node => {
// 如果节点没有被任何连接线连接
if (!connectedNodeIds.has(node.id)) {
const nodeType = node.data?.type || node.type;
// 开始节点和结束节点可以不连接
if (nodeType !== 'start' && nodeType !== 'end') {
const nodeName = node.data?.title || node.id;
allErrors.push(`节点"${nodeName}"未连接任何连线`);
}
}
});
// 检查节点的连接是否符合规则
const sourceCount = new Map<string, number>(); // 每个节点作为源节点的次数
const targetCount = new Map<string, number>(); // 每个节点作为目标节点的次数
edges.forEach(edge => {
if (edge.source) {
sourceCount.set(edge.source, (sourceCount.get(edge.source) || 0) + 1);
}
if (edge.target) {
targetCount.set(edge.target, (targetCount.get(edge.target) || 0) + 1);
}
});
// 检查每个节点的连接情况
nodes.forEach(node => {
const nodeId = node.id;
const nodeType = node.data?.type || node.type;
const nodeName = node.data?.title || node.id;
const sourceEdges = sourceCount.get(nodeId) || 0;
const targetEdges = targetCount.get(nodeId) || 0;
// 特定节点类型可能有特殊的连接规则
switch (nodeType) {
case 'SWITCH':
// 条件节点应该有至少一个输出连接
if (sourceEdges === 0) {
allErrors.push(`条件节点"${nodeName}"缺少输出连接`);
}
break;
case 'start':
// 开始节点应该有输出连接,但不需要输入连接
if (sourceEdges === 0) {
allErrors.push(`开始节点"${nodeName}"缺少输出连接`);
}
break;
case 'end':
// 结束节点应该有输入连接,但不需要输出连接
if (targetEdges === 0) {
allErrors.push(`结束节点"${nodeName}"缺少输入连接`);
}
break;
default:
// 其他节点应该至少有一个输入和一个输出连接
if (targetEdges === 0) {
allErrors.push(`节点"${nodeName}"缺少输入连接`);
}
if (sourceEdges === 0) {
allErrors.push(`节点"${nodeName}"缺少输出连接`);
}
break;
}
});
return {
isValid: allErrors.length === 0,
errors: allErrors
};
};
/**
*
* @param errors
*/
export const showValidationErrors = (errors: string[]) => {
if (errors.length > 0) {
// 创建错误信息内容
let content = '存在以下问题需要修正:\n';
content += errors.map((error, index) => `${index + 1}. ${error}`).join('\n');
// 发送错误信息到 logBar
const logEvent = new CustomEvent('logMessage', {
detail: {
type: 'validation',
message: content,
timestamp: new Date().toISOString()
}
});
document.dispatchEvent(logEvent);
Message.error({
content: content,
duration: 5000
});
}
};

File diff suppressed because it is too large Load Diff

@ -0,0 +1,80 @@
import { useState, useRef, useEffect } from 'react';
import { Node, Edge } from '@xyflow/react';
import { debounce } from 'lodash';
import { useSelector, useDispatch } from 'react-redux';
import { updateCanvasDataMap } from '@/store/ideContainer';
import { Dispatch } from 'redux';
export const useFlowEditorState = (initialData?: any) => {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const { canvasDataMap, nodeStatusMap, isRunning } = useSelector((state: any) => state.ideContainer);
const dispatch = useDispatch();
// 添加编辑弹窗相关状态
const [editingNode, setEditingNode] = useState<Node | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDelete, setIsDelete] = useState(false);
// 添加节点选择弹窗状态
const [edgeForNodeAdd, setEdgeForNodeAdd] = useState<Edge | null>(null);
const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null);
// 在组件顶部添加历史记录相关状态
const [historyInitialized, setHistoryInitialized] = useState(false);
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// 更新节点状态将从store获取的状态应用到节点上
useEffect(() => {
setNodes(prevNodes =>
prevNodes.map(node => ({
...node,
data: {
...node.data,
status: nodeStatusMap[node.id] || 'waiting',
isStatusVisible: isRunning // 只有在运行时才显示状态指示器
}
}))
);
}, [nodeStatusMap, isRunning]);
const updateCanvasDataMapDebounced = useRef(
debounce((dispatch: Dispatch<any>, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => {
dispatch(updateCanvasDataMap({
...canvasDataMap,
[id]: { nodes, edges }
}));
}, 500)
).current;
return {
// State values
nodes,
setNodes,
edges,
setEdges,
canvasDataMap,
editingNode,
setEditingNode,
isEditModalOpen,
setIsEditModalOpen,
isDelete,
setIsDelete,
edgeForNodeAdd,
setEdgeForNodeAdd,
positionForNodeAdd,
setPositionForNodeAdd,
isRunning,
historyInitialized,
setHistoryInitialized,
historyTimeoutRef,
updateCanvasDataMapDebounced,
// Redux
dispatch,
// Initial data
initialData: initialData
};
};

@ -0,0 +1,419 @@
import React, { useEffect, useMemo } from 'react';
import {
ReactFlow,
Background,
Panel,
SelectionMode,
ConnectionLineType,
Node,
Edge,
OnNodesChange,
OnEdgesChange,
OnConnect,
OnReconnect
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import CustomEdge from './components/customEdge';
import CustomConnectionLine from './components/customConnectionLine';
import NodeContextMenu from './components/nodeContextMenu';
import EdgeContextMenu from './components/edgeContextMenu';
import PaneContextMenu from './components/paneContextMenu';
import NodeEditModal from './components/nodeEditModal';
import AddNodeMenu from './components/addNodeMenu';
import ActionBar from './components/actionBar';
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
import { useHistory } from './components/historyContext';
import { NodeTypes } from '@xyflow/react';
const edgeTypes = {
custom: CustomEdge
};
interface FlowEditorMainProps {
nodes: Node[];
edges: Edge[];
nodeTypes: NodeTypes;
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
useDefault: boolean;
reactFlowInstance: any;
reactFlowWrapper: React.RefObject<HTMLDivElement>;
menu: any;
setMenu: React.Dispatch<React.SetStateAction<any>>;
editingNode: Node | null;
setEditingNode: React.Dispatch<React.SetStateAction<Node | null>>;
isEditModalOpen: boolean;
setIsEditModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
isDelete: boolean;
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
edgeForNodeAdd: Edge | null;
setEdgeForNodeAdd: React.Dispatch<React.SetStateAction<Edge | null>>;
positionForNodeAdd: { x: number, y: number } | null;
setPositionForNodeAdd: React.Dispatch<React.SetStateAction<{ x: number, y: number } | null>>;
isRunning: boolean;
initialData: any;
canvasDataMap: any;
// Callbacks
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
onConnect: OnConnect;
onReconnect: OnReconnect;
onDragOver: (event: React.DragEvent) => void;
onDrop: (event: React.DragEvent) => void;
onNodeDrag: (event: React.MouseEvent, node: Node) => void;
onNodeDragStop: () => void;
onNodeContextMenu: (event: React.MouseEvent, node: Node) => void;
onNodeDoubleClick: (event: React.MouseEvent, node: Node) => void;
onEdgeContextMenu: (event: React.MouseEvent, edge: Edge) => void;
onPaneContextMenu: (event: React.MouseEvent) => void;
onPaneClick: () => void;
closeEditModal: () => void;
saveNodeEdit: (updatedData: any) => void;
deleteNode: (node: Node) => void;
deleteEdge: (edge: Edge) => void;
editNode: (node: Node) => void;
editEdge: (edge: Edge) => void;
copyNode: (node: Node) => void;
addNodeOnEdge: (nodeType: string, node: any) => void;
addNodeOnPane: (nodeType: string, position: { x: number, y: number }, node?: any) => void;
handleAddNode: (nodeType: string, node: any) => void;
saveFlowDataToServer: () => void;
handleRun: (running: boolean) => void;
}
const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
const {
nodes,
edges,
nodeTypes,
setNodes,
setEdges,
useDefault,
reactFlowInstance,
reactFlowWrapper,
menu,
setMenu,
editingNode,
setEditingNode,
isEditModalOpen,
setIsEditModalOpen,
isDelete,
setIsDelete,
edgeForNodeAdd,
setEdgeForNodeAdd,
positionForNodeAdd,
setPositionForNodeAdd,
isRunning,
initialData,
canvasDataMap,
onNodesChange,
onEdgesChange,
onConnect,
onReconnect,
onDragOver,
onDrop,
onNodeDrag,
onNodeDragStop,
onNodeContextMenu,
onNodeDoubleClick,
onEdgeContextMenu,
onPaneContextMenu,
onPaneClick,
closeEditModal,
saveNodeEdit,
deleteNode,
deleteEdge,
editNode,
editEdge,
copyNode,
addNodeOnEdge,
addNodeOnPane,
handleAddNode,
saveFlowDataToServer,
handleRun
} = props;
const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines();
const { undo, redo, canUndo, canRedo } = useHistory();
const reactFlowId = useMemo(() => new Date().getTime().toString(), []);
// 监听键盘事件实现快捷键
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+Z 撤销
if (e.ctrlKey && e.key === 'z' && !e.shiftKey && canUndo) {
e.preventDefault();
undo();
}
// Ctrl+Shift+Z 重做
if (e.ctrlKey && e.shiftKey && e.key === 'Z' && canRedo) {
e.preventDefault();
redo();
}
// Ctrl+Y 重做
if (e.ctrlKey && e.key === 'y' && canRedo) {
e.preventDefault();
redo();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [undo, redo, canUndo, canRedo]);
// 监听节点和边的变化以拍摄快照
useEffect(() => {
// 获取 HistoryProvider 中的 takeSnapshot 方法
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...nodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, [nodes, edges]);
return (
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%', position: 'relative' }}
onContextMenu={(e) => e.preventDefault()}>
<ReactFlow
id={reactFlowId}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapToGrid={true}
snapGrid={[2, 2]}
onBeforeDelete={async ({ nodes }) => {
// 检查是否有开始或结束节点
const hasStartOrEndNode = nodes.some(node => node.type === 'start' || node.type === 'end');
if (hasStartOrEndNode) {
console.warn('开始和结束节点不允许删除');
return false; // 阻止删除操作
}
// 检查是否有循环节点这里只是检查实际删除逻辑在onNodesDelete中处理
const loopNodes = nodes.filter(node =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
// 允许删除操作继续进行
return true;
}}
onNodesDelete={(deleted) => {
console.log('deleted:', deleted);
// 检查是否有循环节点
const loopNodes = deleted.filter(node =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
if (loopNodes.length > 0) {
// 处理循环节点删除
let nodesToRemove = [...deleted];
// 为每个循环节点找到其配对节点
loopNodes.forEach(loopNode => {
const component = loopNode.data?.component as { customDef?: string } | undefined;
if (loopNode.data?.type === 'LOOP_START' && component?.customDef) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopEndNodeId;
// 添加关联的结束节点到删除列表
const relatedNode = nodes.find(n => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
} catch (e) {
console.error('解析循环开始节点数据失败:', e);
}
}
else if (loopNode.data?.type === 'LOOP_END' && component?.customDef) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopStartNodeId;
// 添加关联的开始节点到删除列表
const relatedNode = nodes.find(n => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
} catch (e) {
console.error('解析循环结束节点数据失败:', e);
}
}
});
// 去重
nodesToRemove = nodesToRemove.filter((node, index, self) =>
index === self.findIndex(n => n.id === node.id)
);
// 删除所有相关节点
setNodes((nds) => nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id)));
}
else {
// 普通节点删除
setNodes((nds) => nds.filter((n) => !deleted.find((d) => d.id === n.id)));
}
setIsEditModalOpen(false);
}}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onReconnect={onReconnect}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeDrag={onNodeDrag}
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineComponent={CustomConnectionLine}
onNodeDragStop={onNodeDragStop}
onNodeContextMenu={onNodeContextMenu}
onEdgeContextMenu={onEdgeContextMenu}
onNodeClick={onNodeDoubleClick}
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onEdgeMouseEnter={(_event, edge) => {
setEdges((eds) => eds.map(e => {
if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: true } };
}
return e;
}));
}}
onEdgeMouseLeave={(_event, edge) => {
setEdges((eds) => eds.map(e => {
if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: false } };
}
return e;
}));
}}
fitView
selectionKeyCode={['Meta', 'Control']}
selectionMode={SelectionMode.Partial}
panOnDrag={[0, 1, 2]} // 支持多点触控平移
zoomOnScroll={true}
zoomOnPinch={true}
panOnScrollSpeed={0.5}
>
<Background />
<Panel position="top-left">
<ActionBar
useDefault={useDefault}
onSave={saveFlowDataToServer}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
onRun={handleRun}
isRunning={isRunning}
></ActionBar>
</Panel>
<AlignmentGuides />
</ReactFlow>
{/*节点右键上下文*/}
{menu && menu.type === 'node' && (
<div
style={{
position: 'absolute',
top: menu.top,
left: menu.left,
zIndex: 1000
}}
>
<NodeContextMenu
node={nodes.find(n => n.id === menu.id)!}
onDelete={deleteNode}
onEdit={editNode}
onCopy={copyNode}
onCloseMenu={setMenu}
onCloseOpenModal={setIsEditModalOpen}
/>
</div>
)}
{/*边右键上下文*/}
{menu && menu.type === 'edge' && (
<div
style={{
position: 'absolute',
top: menu.top,
left: menu.left,
zIndex: 1000
}}
>
<EdgeContextMenu
edge={edges.find(e => e.id === menu.id)!}
onDelete={deleteEdge}
onEdit={editEdge}
onAddNode={(edge) => {
setEdgeForNodeAdd(edge);
setIsEditModalOpen(false);
setMenu(null); // 关闭上下文菜单
}}
/>
</div>
)}
{/*画布右键上下文*/}
{menu && menu.type === 'pane' && (
<div
style={{
position: 'absolute',
top: menu.top,
left: menu.left,
zIndex: 1000
}}
>
<PaneContextMenu
position={menu.position!}
onAddNode={(nodeType: string, position: { x: number, y: number }, node: any) => {
addNodeOnPane(nodeType, position, node);
setIsEditModalOpen(false);
setMenu(null); // 关闭上下文菜单
}}
/>
</div>
)}
{/*节点点击/节点编辑上下文*/}
<NodeEditModal
popupContainer={reactFlowWrapper}
node={editingNode}
isOpen={isEditModalOpen}
isDelete={isDelete}
onSave={saveNodeEdit}
onClose={closeEditModal}
/>
{/*统一的添加节点菜单*/}
{(edgeForNodeAdd || positionForNodeAdd) && (
<div
style={{
position: 'absolute',
top: edgeForNodeAdd ? (edgeForNodeAdd.data?.y as number || 0) : (positionForNodeAdd?.y || 0),
left: edgeForNodeAdd ? ((edgeForNodeAdd.data?.x as number || 0) + 20) : (positionForNodeAdd?.x || 0),
zIndex: 1000,
transform: 'none'
}}
>
<AddNodeMenu
onAddNode={(nodeType, node) => {
handleAddNode(nodeType, node);
// 关闭菜单
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}}
position={positionForNodeAdd || undefined}
edgeId={edgeForNodeAdd?.id}
/>
</div>
)}
</div>
);
};
export default FlowEditorMain;

@ -7,6 +7,7 @@ import { useSelector, useDispatch } from 'react-redux';
const ButtonGroup = Button.Group;
interface ActionBarProps {
useDefault: boolean;
onSave?: () => void;
onUndo?: () => void;
onRedo?: () => void;
@ -17,6 +18,7 @@ interface ActionBarProps {
}
const ActionBar: React.FC<ActionBarProps> = ({
useDefault,
onSave,
onUndo,
onRedo,
@ -39,49 +41,54 @@ const ActionBar: React.FC<ActionBarProps> = ({
return (
<div className="action-bar">
<Button onClick={onSave} type="primary" shape="round" icon={<IconSave />}></Button>
<ButtonGroup style={{ marginLeft: 8 }}>
<Button
type="outline"
shape="round"
icon={<IconPlayArrow />}
onClick={() => handleRun()}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
status={isRunning ? 'danger' : undefined}
>
{isRunning ? '停止' : '运行'}
</Button>
<Button
type="outline"
shape="round"
icon={<IconCodeSquare />}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
onClick={() => changeLogBarStatus()}
>
</Button>
</ButtonGroup>
<ButtonGroup style={{ marginLeft: 15 }}>
<Button
type="outline"
shape="round"
icon={<IconUndo />}
onClick={onUndo}
disabled={!canUndo}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
>
</Button>
<Button
type="outline"
shape="round"
icon={<IconRedo />}
onClick={onRedo}
disabled={!canRedo}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
>
</Button>
</ButtonGroup>
{useDefault && (
<>
<ButtonGroup style={{ marginLeft: 8 }}>
<Button
type="outline"
shape="round"
icon={<IconPlayArrow />}
onClick={() => handleRun()}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
status={isRunning ? 'danger' : undefined}
>
{isRunning ? '停止' : '运行'}
</Button>
<Button
type="outline"
shape="round"
icon={<IconCodeSquare />}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
onClick={() => changeLogBarStatus()}
>
</Button>
</ButtonGroup>
<ButtonGroup style={{ marginLeft: 15 }}>
<Button
type="outline"
shape="round"
icon={<IconUndo />}
onClick={onUndo}
disabled={!canUndo}
status="danger"
style={{ padding: '0 8px', backgroundColor: '#fff' }}
>
</Button>
<Button
type="outline"
shape="round"
icon={<IconRedo />}
onClick={onRedo}
disabled={!canRedo}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
>
</Button>
</ButtonGroup>
</>
)}
</div>
);
};

@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Menu, Tabs } from '@arco-design/web-react';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { Input, Menu, Tabs } from '@arco-design/web-react';
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
import { useSelector } from 'react-redux';
import { IconSearch } from '@arco-design/web-react/icon';
const TabPane = Tabs.TabPane;
@ -17,6 +18,7 @@ const AddNodeMenu: React.FC<AddNodeMenuProps> = ({
const { projectComponentData, info } = useSelector((state: any) => state.ideContainer);
const [groupedNodes, setGroupedNodes] = useState<Record<string, any[]>>({});
const [activeTab, setActiveTab] = useState('common');
const [searchValue, setSearchValue] = useState(''); // 添加搜索状态
// 按分组组织节点数据
const formattedNodes = useCallback(() => {
@ -53,34 +55,79 @@ const AddNodeMenu: React.FC<AddNodeMenuProps> = ({
}
};
});
// groupedNodes['composite'] = projectFlowList.map((v: any) => {
// return {
// ...v,
// nodeName: v.name,
// nodeType: 'BASE',
// nodeGroup: 'application',
// data: {
// parameters: {
// apiIns: v.def.apis,
// apiOuts: v.def.apiOut,
// dataIns: v.def.dataIns,
// dataOuts: v.def.dataOuts
// }
// }
// };
// });
console.log(projectCompList, projectFlowList, initialGroupedNodes);
initialGroupedNodes['composite'] = projectFlowList.map((v: any) => {
return {
...v,
nodeName: v?.main?.name || '复合组件',
nodeType: 'SUB',
nodeGroup: 'composite',
data: {
parameters: {
apiIns: [{ id: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ id: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: v.flowHousVO.dataIns,
dataOuts: v.flowHousVO.dataOuts
}
}
};
});
}
// 更新状态以触发重新渲染
setGroupedNodes(initialGroupedNodes);
}, [projectComponentData, info.id]);
// 根据搜索值过滤节点
const filteredGroupedNodes = useMemo(() => {
if (!searchValue) return groupedNodes;
const filteredNodes: Record<string, any[]> = {};
Object.keys(groupedNodes).forEach(group => {
const nodes = groupedNodes[group];
const filtered = nodes.filter(node => {
const nodeName = node.nodeName || node.name || '';
return nodeName.toLowerCase().includes(searchValue.toLowerCase());
});
if (filtered.length > 0) {
filteredNodes[group] = filtered;
}
});
return filteredNodes;
}, [groupedNodes, searchValue]);
// 获取第一个有数据的tab
const getFirstAvailableTab = useCallback(() => {
const groupKeys = Object.keys(filteredGroupedNodes);
if (groupKeys.length > 0) {
return groupKeys[0];
}
return 'common';
}, [filteredGroupedNodes]);
// 当搜索值改变时自动选中第一个有结果的tab
useEffect(() => {
if (searchValue) {
const firstTab = getFirstAvailableTab();
setActiveTab(firstTab);
} else {
// 如果搜索值为空恢复默认选中tab
setActiveTab('common');
}
}, [searchValue, getFirstAvailableTab]);
const handleAddNode = (nodeType: string, node: any) => {
onAddNode(nodeType, node);
};
// 处理搜索输入变化
const handleSearchChange = (value: string) => {
setSearchValue(value);
};
// 分组名称映射
const groupNames: Record<string, string> = {
'application': '基础组件',
@ -107,15 +154,22 @@ const AddNodeMenu: React.FC<AddNodeMenuProps> = ({
flexDirection: 'column'
}}
>
<Tabs defaultActiveTab="common" style={{ flex: '0 0 auto' }} onChange={handleTabChange}>
{Object.entries(groupedNodes).map(([group, nodes]) => (
<Input
size="large"
placeholder="搜索节点"
prefix={<IconSearch />}
style={{ marginRight: 16 }}
value={searchValue}
onChange={handleSearchChange}
/>
<Tabs activeTab={activeTab} style={{ flex: '0 0 auto' }} onChange={handleTabChange}>
{Object.entries(filteredGroupedNodes).map(([group, nodes]) => (
<TabPane key={group} title={groupNames[group] || group}>
{/* 只有在当前 tab 激活时才渲染内容 */}
{activeTab === group && (
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
<Menu
style={{
width: 200,
border: '1px solid #e4e7ed',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'

@ -1,10 +1,16 @@
import React from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useReactFlow } from '@xyflow/react';
import EdgeAddNodeButton from '@/pages/flowEditor/components/edgeAddNodeButton';
import { getTopicList } from '@/api/event';
import { useSelector } from 'react-redux';
import { Message } from '@arco-design/web-react';
type DataDisplayEdgeData = {
label?: string;
value?: any;
name?: string;
topic?: string;
eventId?: string;
};
const DataDisplayEdge: React.FC<EdgeProps> = ({
@ -20,6 +26,9 @@ const DataDisplayEdge: React.FC<EdgeProps> = ({
selected,
data
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
@ -29,6 +38,7 @@ const DataDisplayEdge: React.FC<EdgeProps> = ({
targetPosition,
borderRadius: 8 // 设置圆角半径
});
const { info, eventTopicList } = useSelector((state: any) => state.ideContainer);
// 从数据中获取悬停状态
const hovered = data?.hovered || false;
@ -58,8 +68,22 @@ const DataDisplayEdge: React.FC<EdgeProps> = ({
}));
};
const handleDataClick = (e) => {
console.log('click:', e);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleSelect = (option) => {
setSelectedValue(option.label);
setIsOpen(false);
};
return (
@ -86,35 +110,66 @@ const DataDisplayEdge: React.FC<EdgeProps> = ({
{/* 数据展示框 */}
{displayData && Object.keys(displayData).length > 0 && (
<div
onClick={(e) => handleDataClick(e)}
ref={dropdownRef}
style={{
background: '#ffffff',
border: '1px solid #ddd',
width: 150,
border: isOpen ? '1px solid #1890ff' : '1px solid #d9d9d9',
borderRadius: 4,
padding: 4,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
minWidth: 100,
marginBottom: 4,
fontSize: 12,
textAlign: 'center'
backgroundColor: '#fff',
position: 'relative'
}}
>
{displayData.label && (
<div style={{ fontWeight: 'bold', marginBottom: 2 }}>
{displayData.label}
</div>
)}
{displayData.value !== undefined && (
<div>
{typeof displayData.value === 'object'
? JSON.stringify(displayData.value)
: String(displayData.value)}
<div
onClick={() => setIsOpen(!isOpen)}
style={{
padding: '4px 11px',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<span>{selectedValue || displayData.name}</span>
<span>{isOpen ? '▲' : '▼'}</span>
</div>
{isOpen && (
<div
style={{
position: 'absolute',
top: '105%',
left: 0,
right: 0,
border: '1px solid #d9d9d9',
borderTop: 'none',
borderRadius: '0 0 4px 4px',
backgroundColor: '#fff',
zIndex: 1000,
maxHeight: 200,
overflowY: 'auto'
}}
>
{eventTopicList.map((option: { value: string; label: string }) => (
<div
key={option.value}
onClick={() => handleSelect(option)}
style={{
padding: '5px 12px',
cursor: 'pointer',
borderBottom: '1px solid #f0f0f0'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#fff'}
>
{option.label}
</div>
))}
</div>
)}
</div>
)}
{hovered && (
{hovered && Object.keys(displayData).length === 0 && (
<EdgeAddNodeButton
onClick={(e) => handleEdgeAddNode(e)}
/>

@ -39,9 +39,6 @@ const EdgeContextMenu: React.FC<EdgeContextMenuProps> = ({
<Menu.Item key="add-node" onClick={handleAddNode}>
</Menu.Item>
<Menu.Item key="edit" onClick={handleEdit}>
</Menu.Item>
<Menu.Item key="delete" onClick={handleDelete}>
</Menu.Item>

@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
import { debounce } from 'lodash';
import { Node, Edge } from '@xyflow/react';
interface HistoryContextType {
@ -147,11 +148,11 @@ const HistoryProvider: React.FC<HistoryProviderProps> = ({
// 监听 takeSnapshot 事件
useEffect(() => {
const handleTakeSnapshot = ((event: CustomEvent) => {
const handleTakeSnapshot = debounce((event: CustomEvent) => {
const { nodes, edges } = event.detail;
updateCurrentState(nodes, edges);
takeSnapshot();
}) as EventListener;
}, 100) as EventListener;
document.addEventListener('takeSnapshot', handleTakeSnapshot);
return () => {

@ -0,0 +1,417 @@
import React, { useMemo } from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue, isJSON } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
component?: any;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染特殊节点(开始/结束节点)的句柄
const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
const renderStartNodeHandles = () => {
if (!isStartNode) return null;
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`start-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || `start-output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{dataOuts.length > 0 && dataOuts.map((_, index) => (
<Handle
key={`output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + apiOuts.length * 20 + index * 20}px`
}}
/>
))}
</>
);
};
const renderEndNodeHandles = () => {
if (!isEndNode) return null;
return (
<>
{apiIns.map((_, index) => (
<Handle
key={`end-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || `end-input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{dataIns.length > 0 && dataIns.map((_, index) => (
<Handle
key={`input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + apiIns.length * 20 + index * 20}px`
}}
/>
))}
</>
);
};
return (
<>
{renderStartNodeHandles()}
{renderEndNodeHandles()}
</>
);
};
// 渲染普通节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiIns.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
// 检查是否为有效的API非empty
const isValidApi = (api: any) => {
return api && !api.topic?.includes('**empty**') && !api.name?.includes('**empty**');
};
// 检查是否为有效的数据非empty
const isValidData = (data: any) => {
return data && !data.topic?.includes('**empty**') && !data.name?.includes('**empty**');
};
// 为每组关联的数据定义颜色
const getGroupColor = (groupId: number) => {
const colors = [
'#1890ff', // 蓝色
'#52c41a', // 绿色
'#faad14', // 黄色
'#f5222d', // 红色
'#722ed1', // 紫色
'#13c2c2' // 青色
];
return colors[groupId % colors.length];
};
// 从customDef中提取事件分组信息
const useEventGroups = (component: any) => {
return useMemo(() => {
if (!component?.customDef) return {};
try {
const customDef = isJSON(component.customDef)
? JSON.parse(component.customDef)
: component.customDef;
const groups: Record<string, { event: any; color: string; index: number }> = {};
let groupIndex = 0;
// 处理eventListenes对应apiOuts
if (Array.isArray(customDef.eventListenes)) {
customDef.eventListenes
.filter(event => event && !event.topic?.includes('**empty**'))
.forEach(event => {
groups[`eventListenes-${event.eventId}`] = {
event,
color: getGroupColor(groupIndex++),
index: groupIndex - 1
};
});
}
// 处理eventSends对应apiIns
if (Array.isArray(customDef.eventSends)) {
customDef.eventSends
.filter(event => event && !event.topic?.includes('**empty**'))
.forEach(event => {
groups[`eventSends-${event.eventId}`] = {
event,
color: getGroupColor(groupIndex++),
index: groupIndex - 1
};
});
}
console.log('groups:', groups);
return groups;
} catch (e) {
console.error('解析customDef时出错:', e);
return {};
}
}, [component?.customDef]);
};
// 根据数据ID查找关联的事件分组
const findRelatedEventGroup = (dataItem: any, eventGroups: Record<string, any>, eventType: string) => {
if (!dataItem || !eventGroups) return null;
// 查找与数据项关联的事件
for (const key in eventGroups) {
const group = eventGroups[key];
const eventDataIns = group.event?.dataIns || [];
const eventDataOuts = group.event?.dataOuts || [];
// 检查数据项是否属于当前事件的数据输入或输出
if (eventType === 'eventListenes' && eventDataIns.some((input: any) => input.id === dataItem.id)) {
return group;
}
if (eventType === 'eventSends' && eventDataOuts.some((output: any) => output.id === dataItem.id)) {
return group;
}
}
return null;
};
// 根据topic查找API关联的事件分组
const findApiGroupByTopic = (apiItem: any, eventGroups: Record<string, any>) => {
if (!apiItem || !apiItem.topic || !eventGroups) return null;
// 查找与API项topic关联的事件
for (const key in eventGroups) {
const group = eventGroups[key];
if (group.event.topic === apiItem.topic) {
return group;
}
}
return null;
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
// 获取事件分组信息
const eventGroups = useEventGroups(data.component);
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
const isSpecialNode = isStartNode || isEndNode;
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => {
// 查找关联的事件分组
const group = findApiGroupByTopic(input, eventGroups);
return (
<div
key={input.id || `input-${index}`}
className={styles['node-input-label']}
style={{
color: group ? group.color : '#000'
}}
>
{isValidApi(input) ? input.name : ''}
</div>
);
})}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => {
// 查找关联的事件分组
const group = findApiGroupByTopic(output, eventGroups);
return (
<div
key={output.id || `output-${index}`}
className={styles['node-input-label']}
style={{
color: group ? group.color : '#000'
}}
>
{isValidApi(output) ? output.name : ''}
</div>
);
})}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div className={styles['node-split-line']}></div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => {
// 查找关联的事件分组
const group = findRelatedEventGroup(input, eventGroups, 'eventSends');
return (
<div
key={input.id || `input-${index}`}
className={styles['node-input-label']}
style={{
color: group ? group.color : '#000'
}}
>
{isValidData(input) ? (input.name || input.id || `输入${index + 1}`) : ''}
</div>
);
})}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => {
// 查找关联的事件分组
const group = findRelatedEventGroup(output, eventGroups, 'eventListenes');
return (
<div
key={output.id || `output-${index}`}
className={styles['node-input-label']}
style={{
color: group ? group.color : '#000'
}}
>
{isValidData(output) ? (output.name || output.id || `输出${index + 1}`) : ''}
</div>
);
})}
</div>
)}
</div>
</div>
</>
)}
{/*footer栏*/}
{/*{showFooter && (*/}
{/* <div className={styles['node-footer']}>*/}
{/* {formatFooter(footerData)}*/}
{/* </div>*/}
{/*)}*/}
{/* 根据节点类型渲染不同的句柄 */}
{isSpecialNode
? renderSpecialNodeHandles(isStartNode, isEndNode, dataIns, dataOuts, apiIns, apiOuts)
: renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -0,0 +1,185 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue, isJSON } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染普通节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiIns.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = data?.component?.customDef || false;
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.desc}
</div>
))}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.desc}
</div>
))}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div
className={styles['node-split-line']}
>
</div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.id || `输入${index + 1}`}
</div>
))}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{`${output.id} ${output.dataType}` || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{/* 根据节点类型渲染不同的句柄 */}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -0,0 +1,189 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { Image } from '@arco-design/web-react';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染普通节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiIns.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
const isSpecialNode = isStartNode || isEndNode;
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.desc}
</div>
))}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.desc}
</div>
))}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div className={styles['node-split-line']}></div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{`${input.desc} ${input.dataType}` || `输入${index + 1}`}
</div>
))}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.id || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{/*图片展示 TODO 需要对接接口*/}
{/*<div className={styles['node-image-box']}>*/}
{/* <Image*/}
{/* width={150}*/}
{/* src=""*/}
{/* alt="lamp"*/}
{/* />*/}
{/*</div>*/}
{/* 根据节点类型渲染不同的句柄 */}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -0,0 +1,178 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染LOOP节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${65 + (apiOuts.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${65 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{data.type !== 'LOOP_START' ? input.desc || input.id : ''}
</div>
))}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{data.type !== 'LOOP_START' ? output.desc || output.id || output.name : ''}
</div>
))}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div className={styles['node-split-line']}></div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.id || `输入${index + 1}`}
</div>
))}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.id || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -1,7 +1,7 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue } from '@/utils/common';
import { deserializeValue, isJSON } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
interface NodeContentData {
@ -180,7 +180,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
const formatFooter = (data: any) => {
try {
switch (data.type) {
switch (data?.type) {
case 'WAIT':
const { duration } = deserializeValue(data.customDef);
const hours = Math.floor(duration / 3600);
@ -192,10 +192,13 @@ const formatFooter = (data: any) => {
return cronstrue.toString(intervalSeconds, { locale: 'zh_CN' });
case 'EVENTSEND':
case 'EVENTLISTENE':
const { name } = data.customDef;
const { name, topic } = isJSON(data.customDef) ? JSON.parse(data.customDef) : data.customDef;
if (topic.includes('**empty**')) return '';
return `事件: ${name}`;
case 'BASIC':
return data.compIdentifier ? `当前实例:${data.compIdentifier}` : '';
default:
return '这个类型还没开发';
return '';
}
} catch (e) {
console.log(e);
@ -207,7 +210,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = data?.component?.customDef || false;
const showFooter = formatFooter(data.component) || false;
const footerData = (showFooter && data.component) || {};
// console.log(apiIns, apiOuts, dataIns, dataOuts);

@ -0,0 +1,182 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染普通节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiIns.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${70 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.desc}
</div>
))}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.desc}
</div>
))}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div
className={styles['node-split-line']}
>
</div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.id || `输入${index + 1}`}
</div>
))}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{`${output.id} ${output.dataType}` || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{/* 根据节点类型渲染不同的句柄 */}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -0,0 +1,176 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染普通节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${65 + (apiOuts.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${65 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.desc}
</div>
))}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.desc || output.id || output.name}
</div>
))}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div className={styles['node-split-line']}></div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.id || `输入${index + 1}`}
</div>
))}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.id || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -1,5 +1,5 @@
import React from 'react';
import { Menu, Dropdown } from '@arco-design/web-react';
import React, { useEffect } from 'react';
import { Menu } from '@arco-design/web-react';
import { Node } from '@xyflow/react';
interface NodeContextMenuProps {
@ -8,42 +8,79 @@ interface NodeContextMenuProps {
onDelete?: (node: Node) => void;
onCopy?: (node: Node) => void;
onEdit?: (node: Node) => void;
onCloseMenu?: (data: React.Dispatch<React.SetStateAction<any>>) => void;
onCloseOpenModal?: (boolean: boolean) => void;
}
const NodeContextMenu: React.FC<NodeContextMenuProps> = ({
node,
onDelete,
onCopy,
onEdit
onEdit,
onCloseMenu,
onCloseOpenModal
}) => {
const handleDelete = () => {
onDelete && onDelete(node);
onCloseOpenModal(false);
onCloseMenu(null);
};
const handleCopy = () => {
onCopy && onCopy(node);
onCloseOpenModal(false);
onCloseMenu(null);
};
const handleEdit = () => {
onEdit && onEdit(node);
onCloseOpenModal(true);
onCloseMenu(null);
};
// 根据节点类型和其他条件动态生成菜单项
const renderMenuItems = () => {
const menuItems = [];
// if (!useDefault) return;
// 对于非开始和结束节点,添加基本操作
if (!['start', 'end'].includes(node?.type)) {
if (!['AND', 'OR', 'JSON2STR', 'STR2JSON', 'IMAGE', 'RESULT', 'LOOP_START'].includes(node.data.type as string)) {
menuItems.push(
<Menu.Item key="edit" onClick={handleEdit}>
</Menu.Item>
);
}
menuItems.push(
<Menu.Item key="copy" onClick={handleCopy}>
</Menu.Item>
);
menuItems.push(
<Menu.Item key="delete" onClick={handleDelete}>
</Menu.Item>
);
}
// 可以根据节点类型添加特定的操作
if (node?.type === 'special') {
menuItems.push(
<Menu.Item key="special-action">
</Menu.Item>
);
}
return menuItems;
};
return (
<Menu>
<Menu.Item key="edit" onClick={handleEdit}>
</Menu.Item>
{(!['start', 'end'].includes(node.type)) && (
<>
<Menu.Item key="copy" onClick={handleCopy}>
</Menu.Item>
<Menu.Item key="delete" onClick={handleDelete}>
</Menu.Item>
</>
)}
{renderMenuItems()}
</Menu>
);
};

@ -98,6 +98,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({
mask={false}
maskClosable={false}
footer={null}
focusLock={false}
getPopupContainer={() => popupContainer?.current || document.body}
onOk={handleSave}
onCancel={isDelete ? handleClose : handleSave}

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Menu } from '@arco-design/web-react';
import AddNodeMenu from './addNodeMenu';
@ -14,9 +14,22 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
position
}) => {
// 包装onAddNode函数以适配AddNodeMenu组件的接口
const handleAddNode = (nodeType: string, node: any) => {
const handleAddNode = useCallback((nodeType: string, node: any) => {
onAddNode(nodeType, position, node);
};
}, [onAddNode, position]);
// 处理粘贴节点
const handlePasteNode = useCallback(() => {
// 创建自定义粘贴事件
const pasteEvent = new ClipboardEvent('paste', {
clipboardData: new DataTransfer()
});
pasteEvent.clipboardData?.setData('text', 'paste-node');
document.dispatchEvent(pasteEvent);
}, []);
// 检查是否有复制的节点数据
const hasCopiedNode = !!localStorage.getItem('copiedNode');
return (
<Menu
@ -26,6 +39,7 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
triggerProps={{ trigger: 'click' }}
>
<SubMenu
key="add-node"
@ -39,6 +53,11 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
handleAddNode(nodeType, node);
}} />
</SubMenu>
{hasCopiedNode && (
<Menu.Item key="paste" onClick={handlePasteNode}>
</Menu.Item>
)}
</Menu>
);
};

File diff suppressed because it is too large Load Diff

@ -46,10 +46,70 @@ const str2jsonParameters = {
dataIns: [],
dataOuts: []
};
const switchParameters = {
apiIns: [{
name: 'start',
desc: '',
dataType: '',
defaultValue: ''
}],
apiOuts: [{
name: 'default',
desc: '',
dataType: '',
defaultValue: ''
}],
dataIns: [],
dataOuts: []
};
const imageParameters = {
apiIns: [{
name: 'start',
desc: '',
dataType: '',
defaultValue: ''
}],
apiOuts: [{
name: 'done',
desc: '',
dataType: '',
defaultValue: ''
}],
dataIns: [{
name: 'in',
desc: 'url',
dataType: 'STRING',
defaultValue: '',
arrayType: ''
}],
dataOuts: []
};
const codeParameters = {
apiIns: [{
name: 'start',
desc: '',
dataType: '',
defaultValue: ''
}],
apiOuts: [{
name: 'done',
desc: '',
dataType: '',
defaultValue: ''
}],
dataIns: [],
dataOuts: [{
'arrayType': null,
'dataType': 'STRING',
'defaultValue': 'STRING',
'desc': '输出参数',
'id': 'arg'
}]
};
// 定义节点基本信息
// 定义节点基本信息 画布中添加的组件列表依赖这里
const nodeDefinitions = [
{ nodeName: '条件选择', nodeType: 'CONDITION', nodeGroup: 'common', icon: 'IconBranch' },
{ nodeName: '条件选择', nodeType: 'SWITCH', nodeGroup: 'common', icon: 'IconBranch' },
{ nodeName: '与门', nodeType: 'AND', nodeGroup: 'common', icon: 'IconShareAlt' },
{ nodeName: '或门', nodeType: 'OR', nodeGroup: 'common', icon: 'IconPause' },
{ nodeName: '等待', nodeType: 'WAIT', nodeGroup: 'common', icon: 'IconClockCircle' },
@ -57,9 +117,9 @@ const nodeDefinitions = [
{ nodeName: '周期', nodeType: 'CYCLE', nodeGroup: 'common', icon: 'IconSchedule' },
{ nodeName: '事件接收', nodeType: 'EVENTLISTENE', nodeGroup: 'common', icon: 'IconImport' },
{ nodeName: '事件发送', nodeType: 'EVENTSEND', nodeGroup: 'common', icon: 'IconExport' },
{ nodeName: 'JSON转字符串', nodeType: 'JSON2STR', nodeGroup: 'common', icon: 'IconCodeBlock' },
{ nodeName: '字符串转JSON', nodeType: 'STR2JSON', nodeGroup: 'common', icon: 'IconCodeSquare' },
{ nodeName: 'JSON封装', nodeType: 'JSONCONVERT', nodeGroup: 'common', icon: 'IconTranslate' },
// { nodeName: 'JSON转字符串', nodeType: 'JSON2STR', nodeGroup: 'common', icon: 'IconCodeBlock' },
// { nodeName: '字符串转JSON', nodeType: 'STR2JSON', nodeGroup: 'common', icon: 'IconCodeSquare' },
// { nodeName: 'JSON封装', nodeType: 'JSONCONVERT', nodeGroup: 'common', icon: 'IconTranslate' },
{ nodeName: '结果展示', nodeType: 'RESULT', nodeGroup: 'common', icon: 'IconInteraction' },
{ nodeName: '图片展示', nodeType: 'IMAGE', nodeGroup: 'common', icon: 'IconImage' },
{ nodeName: '代码编辑器', nodeType: 'CODE', nodeGroup: 'common', icon: 'IconCode' },
@ -77,6 +137,15 @@ export const localNodeData = nodeDefinitions.map(({ nodeName, nodeType, nodeGrou
else if (nodeType === 'STR2JSON') {
parameters = str2jsonParameters;
}
else if (nodeType === 'SWITCH') {
parameters = switchParameters;
}
else if (nodeType === 'IMAGE') {
parameters = imageParameters;
}
else if (nodeType === 'CODE') {
parameters = codeParameters;
}
return {
nodeName,

File diff suppressed because it is too large Load Diff

@ -0,0 +1,34 @@
/*
*
* */
import { convertAppFlowData } from '@/utils/convertAppFlowData';
import { convertFlowData } from '@/utils/convertFlowData';
import { Edge } from '@xyflow/react';
import { updateCanvasDataMap } from '@/store/ideContainer';
export const appFLowHandle = (initialData, useDefault, setNodes, setEdges, dispatch) => {
const {
nodes: convertedNodes,
edges: convertedEdges
} = convertAppFlowData(initialData);
console.log('nodes:', convertedNodes);
console.log('edges:', convertedEdges);
// 为所有边添加类型
const initialEdges: Edge[] = convertedEdges.map(edge => ({
...edge,
type: 'custom'
}));
setNodes(convertedNodes);
setEdges(initialEdges);
// if (initialData?.appId) {
// dispatch(updateCanvasDataMap({
// ...canvasDataMap,
// [initialData.appId]: { nodes: convertedNodes, edges: initialEdges }
// }));
// }
};

@ -0,0 +1,30 @@
/*
*
* */
// 组件编排画布数据处理(组件编排)
import { convertFlowData } from '@/utils/convertFlowData';
import { Edge } from '@xyflow/react';
import { updateCanvasDataMap } from '@/store/ideContainer';
export const projectFlowHandle = (initialData, useDefault, setNodes, setEdges, dispatch, canvasDataMap) => {
const {
nodes: convertedNodes,
edges: convertedEdges
} = convertFlowData(initialData?.main?.components || initialData?.compData?.components, useDefault);
// 为所有边添加类型
const initialEdges: Edge[] = convertedEdges.map(edge => ({
...edge,
type: 'custom'
}));
setNodes(convertedNodes);
setEdges(initialEdges);
if (initialData?.appId) {
dispatch(updateCanvasDataMap({
...canvasDataMap,
[initialData.appId]: { nodes: convertedNodes, edges: initialEdges }
}));
}
};

@ -6,12 +6,19 @@ import LogBar from './logBar';
import RightSideBar from './rightSideBar';
import NavBar, { NavBarRef } from './navBar';
import { Selected } from '@/pages/ideContainer/types';
import { updateInfo, updateProjectComponentData } from '@/store/ideContainer';
import {
updateCurrentAppData,
updateInfo,
updateProjectComponentData,
updateSocketId,
updateEventList
} from '@/store/ideContainer';
import { useDispatch, useSelector } from 'react-redux';
import { getAppListBySceneId } from '@/api/apps';
import { getProjectComp } from '@/api/scene';
import ProjectContainer from '@/pages/orchestration/project';
import ComplexContainer from '@/pages/orchestration/complex';
import ApplicationContainer from '@/pages/orchestration/application';
import EventContainer from '@/pages/orchestration/event';
import GlobalVarContainer from '@/pages/orchestration/globalVar';
@ -20,7 +27,11 @@ import ComponentList from '@/pages/componentDevelopment/componentList';
import ComponentCoding from '@/pages/componentDevelopment/componentCoding';
import ComponentDeployment from '@/pages/componentDevelopment/componentDeployment';
import ComponentTest from '@/pages/componentDevelopment/componentTest';
import { getUserToken } from '@/api/user';
import useWebSocket from '@/hooks/useWebSocket';
import { Message } from '@arco-design/web-react';
import { queryEventItemBySceneId } from '@/api/event';
import { updateNodeStatus } from '@/store/ideContainer';
type UrlParamsOptions = {
identity?: string;
@ -36,7 +47,7 @@ const EnvConfig = () => <div style={{ height: '100%', width: '100%' }}>环境配
// 所有可显示的组件路径列表
const ALL_PATHS = [
'compFlow', 'appFlow', 'compList', 'appInstance', 'event', 'globalVar', 'appCompList',
'compFlow', 'complexFlow', 'appFlow', 'compList', 'appInstance', 'event', 'globalVar', 'appCompList',
'myComponents', 'matchingComponents', 'componentReview', 'componentList',
'componentCoding', 'componentDeployment', 'componentTest', 'envConfig'
];
@ -52,13 +63,69 @@ function IDEContainer() {
const dispatch = useDispatch();
const navBarRef = useRef<NavBarRef>(null);
// 初始化WebSocket hook
const ws = useWebSocket({
onOpen: () => {
console.log('WebSocket连接已建立');
},
onClose: () => {
console.log('WebSocket连接已关闭');
},
onError: (event) => {
console.error('WebSocket错误:', event);
},
onMessage: (event) => {
console.log('收到WebSocket消息:', event.data);
// 这里可以处理从后端收到的消息,例如日志更新等
const socketMessage = JSON.parse(event.data);
if (socketMessage?.socketId) dispatch(updateSocketId(socketMessage.socketId));
// 处理节点状态更新
if (socketMessage?.nodeLog) {
const { nodeId, state } = socketMessage.nodeLog;
// 将状态映射为前端使用的状态
let status = 'waiting';
switch (state) {
case 0: // 运行中
status = 'running';
break;
case 1: // 运行成功
status = 'success';
break;
case -1: // 运行失败
status = 'failed';
break;
default:// 等待运行
status = 'waiting';
break;
}
// 更新节点状态
dispatch(updateNodeStatus({ nodeId, status }));
}
}
});
const connectWS = async () => {
const res = await getUserToken();
const token = res.data;
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
let wsApi = `${protocol}://${window.location.host}/ws/v1/bpms-runtime`;
// if (window.location.host.includes('localhost')) {
wsApi = `ws://192.168.5.119/ws/v1/bpms-runtime`;
// }
const uri = `${wsApi}?x-auth0-token=${token}`;
ws.connect(uri);
};
const getAppList = async () => {
const res: any = await getAppListBySceneId({
pageSize: 999,
currPage: 1,
sceneId: urlParams.id
});
if (res.code === 200) setSubMenuData({ 'appList': res.data.list.reverse() });
if (res.code === 200) {
setSubMenuData({ ...subMenuData, 'appList': res.data.list.reverse(), 'appFlow': res.data.list.reverse() });
}
};
const getProjectCompData = async () => {
@ -68,10 +135,14 @@ function IDEContainer() {
}
};
const getEvent = async () => {
const res: any = await queryEventItemBySceneId(urlParams.id);
if (res.code === 200) dispatch(updateEventList(res.data));
};
useEffect(() => {
setUrlParams(getUrlParams(window.location.href) as UrlParamsOptions);
dispatch(updateInfo(getUrlParams(window.location.href) as UrlParamsOptions));
}, []);
useEffect(() => {
@ -81,6 +152,13 @@ function IDEContainer() {
}
}, [urlParams.id]);
useEffect(() => {
if (urlParams.identity && urlParams.identity === 'scene') {
connectWS();
getEvent();
}
}, [urlParams.identity]);
// 当selected.path变化时添加到已打开的tab集合中
useEffect(() => {
if (selected.key) {
@ -143,6 +221,8 @@ function IDEContainer() {
switch (path) {
case 'compFlow':
return <ProjectContainer selected={selected} />;
case 'complexFlow':
return <ComplexContainer selected={selected} />;
case 'appFlow':
return <ApplicationContainer />;
case 'compList':
@ -200,6 +280,8 @@ function IDEContainer() {
const menuItems = menuData[urlParams.identity];
const menuItem = findMenuItem(menuItems, key);
dispatch(updateCurrentAppData({ ...menuItem }));
if (menuItem) {
setSelected({
...menuItem,

@ -6,6 +6,13 @@ import { useSelector, useDispatch } from 'react-redux';
const TabPane = Tabs.TabPane;
interface LogMessage {
id: number;
type: string;
message: string;
timestamp: string;
}
interface LogBarProps {
a?: string;
}
@ -39,7 +46,7 @@ const LogBar: React.FC<LogBarProps> = () => {
const resizeBoxRef = useRef<HTMLDivElement>(null); // 引用 ResizeBox 容器
const { logBarStatus } = useSelector((state) => state.ideContainer);
const dispatch = useDispatch();
const [validationLogs, setValidationLogs] = useState<LogMessage[]>([]);
// 处理 Tab 点击事件
const handleTabClick = (key: string) => {
@ -56,7 +63,6 @@ const LogBar: React.FC<LogBarProps> = () => {
// 当 collapsed 状态改变时,直接更新元素的样式
useEffect(() => {
if (resizeBoxRef.current) {
resizeBoxRef.current.style.height = logBarStatus ? '250px' : '0px';
}
@ -76,6 +82,59 @@ const LogBar: React.FC<LogBarProps> = () => {
}
};
// 监听日志消息事件
useEffect(() => {
const handleLogMessage = (event: CustomEvent) => {
const { type, message, timestamp } = event.detail;
// 如果是校验类型的消息且当前校验日志tab可见则添加到校验日志中
if (type === 'validation') {
const newLog: LogMessage = {
id: Date.now(),
type,
message,
timestamp
};
setValidationLogs(prev => [...prev, newLog]);
// 自动切换到校验日志tab并展开logBar
setActiveTab('2');
dispatch(updateLogBarStatus(true));
}
};
// 添加事件监听器
document.addEventListener('logMessage', handleLogMessage as EventListener);
// 清理事件监听器
return () => {
document.removeEventListener('logMessage', handleLogMessage as EventListener);
};
}, [dispatch]);
// 渲染校验日志内容
const renderValidationLogs = () => {
return (
<div style={{ padding: '10px', maxHeight: '200px', overflowY: 'auto' }}>
{validationLogs.length === 0 ? (
<p></p>
) : (
validationLogs.map(log => (
<div key={log.id} style={{ marginBottom: '8px', padding: '4px', borderBottom: '1px solid #eee' }}>
<div style={{ fontSize: '12px', color: '#999' }}>
{new Date(log.timestamp).toLocaleString()}
</div>
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{log.message}
</div>
</div>
))
)}
</div>
);
};
return (
<>
<ResizeBox
@ -94,7 +153,7 @@ const LogBar: React.FC<LogBarProps> = () => {
>
{tabs.map((x) => (
<TabPane destroyOnHide key={x.key} title={x.title}>
{x.content}
{x.key === '2' ? renderValidationLogs() : x.content}
</TabPane>
))}
</Tabs>

@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { Card, Grid, Input, Tag, Typography, Divider, Collapse, Button, Message } from '@arco-design/web-react';
import { IconSearch, IconSync } from '@arco-design/web-react/icon';
import styles from './style/market.module.less';
@ -20,6 +20,7 @@ interface MarketProps {
const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
const [compList, setCompList] = useState<any>([]);
const [searchValue, setSearchValue] = useState(''); // 添加搜索状态
const [firstLevelCategory, setFirstLevelCategory] = useState('全部'); // 第一层分类:全部,基础,复合
const [secondLevelCategory, setSecondLevelCategory] = useState('全部'); // 第二层分类:全部,我的,公开,协同
const { projectComponentData, info } = useSelector((state: any) => state.ideContainer);
@ -77,6 +78,41 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
};
}, [compList]);
// 根据搜索值过滤组件列表
const filteredCompList = useMemo(() => {
if (!searchValue) return compList;
const filtered = { ...compList };
// 过滤基础组件 (myLibs, pubLibs, teamLibs)
['myLibs', 'pubLibs', 'teamLibs'].forEach(key => {
if (filtered[key]) {
filtered[key] = filtered[key].map(category => {
if (category && category.children) {
const filteredChildren = category.children.filter(child => {
const label = child.label || child.name || '';
return label.toLowerCase().includes(searchValue.toLowerCase());
});
return { ...category, children: filteredChildren };
}
return category;
}).filter(category => category.children && category.children.length > 0);
}
});
// 过滤复合组件 (myFlow, pubFlow)
['myFlow', 'pubFlow'].forEach(key => {
if (filtered[key]) {
filtered[key] = filtered[key].filter(item => {
const name = item.flowName || item.name || '';
return name.toLowerCase().includes(searchValue.toLowerCase());
});
}
});
return filtered;
}, [compList, searchValue]);
// 渲染Tag选择器
const renderCategoryTage = useCallback(() => {
/*
@ -199,6 +235,9 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
// 渲染组件分类
const renderComponentCategory = useCallback(() => {
// 使用过滤后的组件列表
const currentCompList = searchValue ? filteredCompList : compList;
// 根据第一层和第二层分类筛选组件
let filteredComponents = [];
@ -217,23 +256,23 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
};
// 收集符合条件的基础组件
if (conditions.includeMyLibs && compList.myLibs) {
filteredComponents = [...filteredComponents, ...compList.myLibs];
if (conditions.includeMyLibs && currentCompList.myLibs) {
filteredComponents = [...filteredComponents, ...currentCompList.myLibs];
}
if (conditions.includePubLibs && compList.pubLibs) {
filteredComponents = [...filteredComponents, ...compList.pubLibs];
if (conditions.includePubLibs && currentCompList.pubLibs) {
filteredComponents = [...filteredComponents, ...currentCompList.pubLibs];
}
if (conditions.includeTeamLibs && compList.teamLibs) {
filteredComponents = [...filteredComponents, ...compList.teamLibs];
if (conditions.includeTeamLibs && currentCompList.teamLibs) {
filteredComponents = [...filteredComponents, ...currentCompList.teamLibs];
}
// 收集符合条件的复合组件
const flowComponents = [];
if (conditions.includeMyFlow && compList.myFlow) {
flowComponents.push(...compList.myFlow.map(item => ({ ...item, isFlow: true })));
if (conditions.includeMyFlow && currentCompList.myFlow) {
flowComponents.push(...currentCompList.myFlow.map(item => ({ ...item, isFlow: true })));
}
if (conditions.includePubFlow && compList.pubFlow) {
flowComponents.push(...compList.pubFlow.map(item => ({ ...item, isFlow: true })));
if (conditions.includePubFlow && currentCompList.pubFlow) {
flowComponents.push(...currentCompList.pubFlow.map(item => ({ ...item, isFlow: true })));
}
// 合并相同标签的组件
@ -272,7 +311,7 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
if (resultComponents.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
{searchValue ? '未找到匹配的组件' : '暂无组件数据'}
</div>
);
}
@ -346,7 +385,7 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
}).filter(item => item !== null)}
</Collapse>
);
}, [compList, firstLevelCategory, secondLevelCategory]);
}, [compList, filteredCompList, firstLevelCategory, secondLevelCategory, searchValue]);
// 给账号下的组件列表(本地存储中的组件列表)增加一个是否添加至工程的初始状态
const addInitState = (componentData) => {
@ -377,6 +416,11 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
setCompList(componentData);
};
// 处理搜索输入变化
const handleSearchChange = (value: string) => {
setSearchValue(value);
};
// 从缓存中获取组件列表的信息
const getCompList = () => {
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
@ -405,6 +449,8 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
placeholder="搜索组件"
prefix={<IconSearch />}
style={{ marginRight: 16 }}
value={searchValue}
onChange={handleSearchChange}
/>
<div className={styles['filter-tags']}>
<Button

@ -53,6 +53,7 @@ const NavBar: React.ForwardRefRenderFunction<NavBarRef, NavBarProps> = ({
const key = selected.key;
const parentKey = selected.parentKey;
const title = selected.title;
const pathTitle = selected?.pathTitle || null;
// 检查tab是否已存在
const existingTab = tabs.find(tab => tab.key === (key) && tab.title === title);
@ -63,7 +64,8 @@ const NavBar: React.ForwardRefRenderFunction<NavBarRef, NavBarProps> = ({
key: key,
parentKey: parentKey,
title: title,
path: path
path: path,
pathTitle: pathTitle
};
setTabs(prev => [...prev, newTab]);
@ -128,7 +130,7 @@ const NavBar: React.ForwardRefRenderFunction<NavBarRef, NavBarProps> = ({
<TabPane
destroyOnHide
key={tab.key}
title={tab.title}
title={tab.pathTitle || tab.title}
>
</TabPane>
))}

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import styles from './style/sideBar.module.less';
import {
ResizeBox,
@ -17,9 +17,10 @@ import { menuData1, menuData2 } from './config/menuData';
import { IconSearch, IconPlus } from '@arco-design/web-react/icon';
import { Selected } from '@/pages/ideContainer/types';
import { useDispatch, useSelector } from 'react-redux';
import { updateMenuData, updateFlowData } from '@/store/ideContainer';
import { updateMenuData, updateFlowData, updateCurrentAppData } from '@/store/ideContainer';
import { addApp, getProjectEnv, editApp, deleteApp } from '@/api/apps';
import _ from 'lodash';
import { getAppInfoNew } from '@/api/appRes';
const TreeNode = Tree.Node;
const FormItem = Form.Item;
@ -56,16 +57,18 @@ const AppHandleModal = ({ appInfo, visible, type, onChangeVisible, onClose, onRe
const { info } = useSelector(state => state.ideContainer);
const [form] = Form.useForm();
if (type === 'EDIT' && appInfo) {
form.setFieldsValue({
id: appInfo?.id,
name: appInfo?.name,
description: appInfo?.description,
published: appInfo?.published === 1,
logo: 'scene04.png', // 先写死
sceneId: info.id
});
}
useEffect(() => {
if (type === 'EDIT' && appInfo && visible) {
form.setFieldsValue({
id: appInfo?.id,
name: appInfo?.name,
description: appInfo?.description,
published: appInfo?.published === 1,
logo: 'scene04.png', // 先写死
sceneId: info.id
});
}
}, [type, appInfo, visible, form, info.id]);
const onOk = async () => {
await form.validate();
@ -167,13 +170,26 @@ const SideBar: React.FC<SideBarProps> = ({
onDeleteApp
}) => {
const [menu, setMenu] = useState<MenuItemType[]>([]);
const [searchValue, setSearchValue] = useState(''); // 添加搜索状态
const [subMenuWidth, setSubMenuWidth] = useState(300); // 子菜单宽度状态
const [isSubMenuCollapsed, setIsSubMenuCollapsed] = useState(false); // 子菜单收起状态
const [activeKey, setActiveKey] = useState(0);
const [showModal, setShowModal] = useState(false);
const [modalType, setModalType] = useState<'ADD' | 'EDIT'>('ADD');
const [currentApp, setCurrentApp] = useState<any>(null);
const [contextMenu, setContextMenu] = useState<{
visible: boolean;
x: number;
y: number;
nodeData: any;
}>({
visible: false,
x: 0,
y: 0,
nodeData: null
}); // 添加右键菜单状态
const resizeBoxRef = useRef<HTMLDivElement>(null); // 引用第一个 ResizeBox 容器
const contextMenuRef = useRef<HTMLDivElement>(null); // 右键菜单引用
const { menuData } = useSelector(state => state.ideContainer);
const dispatch = useDispatch();
@ -182,30 +198,39 @@ const SideBar: React.FC<SideBarProps> = ({
case 'scene':
const menuData = _.cloneDeep(menuData1);
const newSubMenuData = _.cloneDeep(subMenuData);
const subMenuKey = Object.keys(newSubMenuData)[0];
const subMenuValue: any = Object.values(newSubMenuData)[0];
const menuIndex = menuData.findIndex(v => v.key === subMenuKey);
// 构建数据结构,这是目录默认需要的数据结构
subMenuValue.forEach(v => {
v.title = v.name;
v.key = `compFlow-${v.id}`;
v.path = 'compFlow';
v.parentKey = subMenuKey;
v.icon = '/ideContainer/icon/app.png';
v.children = [
{
title: '事件',
children: null,
icon: '/ideContainer/icon/list.png'
},
{
title: '组件列表',
children: null,
icon: '/ideContainer/icon/list.png'
}
];
// 遍历所有 subMenuKey 来构建子菜单数据结构
Object.keys(newSubMenuData).forEach(subMenuKey => {
const subMenuValue: any = _.cloneDeep(newSubMenuData[subMenuKey]);
const menuIndex = menuData.findIndex(v => v.key === subMenuKey);
if (menuIndex !== -1) {
// 构建数据结构,这是目录默认需要的数据结构
subMenuValue.forEach(v => {
v.title = v.name;
v.key = `compFlow-${v.id}`;
v.path = 'compFlow';
v.parentKey = subMenuKey;
v.icon = '/ideContainer/icon/app.png';
if (subMenuKey !== 'appFlow') {
v.children = [
{
title: '事件',
children: null,
icon: '/ideContainer/icon/list.png'
},
{
title: '组件列表',
children: null,
icon: '/ideContainer/icon/list.png'
}
];
}
});
menuData[menuIndex]['children'] = subMenuValue;
}
});
menuData[menuIndex]['children'] = subMenuValue;
dispatch(updateMenuData({ 'scene': menuData }));
return menuData;
case 'componentDevelopment' :
@ -250,12 +275,46 @@ const SideBar: React.FC<SideBarProps> = ({
onMenuSelect?.({ ...item });
};
// 根据搜索值过滤菜单数据
const filteredMenu = useMemo(() => {
if (!searchValue || activeKey === undefined || !menu[activeKey]?.children) {
return menu;
}
// 深拷贝菜单数据以避免修改原始数据
const menuCopy = _.cloneDeep(menu);
// 只对当前激活的菜单项进行过滤
if (menuCopy[activeKey]?.children) {
menuCopy[activeKey].children = menuCopy[activeKey].children.filter((item: MenuItemType) => {
// 检查当前项是否匹配搜索条件
const title = item.title || '';
const isMatch = title.toLowerCase().includes(searchValue.toLowerCase());
// 如果当前项不匹配,检查其子项是否匹配
if (!isMatch && item.children && item.children.length > 0) {
item.children = item.children.filter(child => {
const childTitle = child.title || '';
return childTitle.toLowerCase().includes(searchValue.toLowerCase());
});
// 如果有匹配的子项,或者当前项匹配,则保留该项
return item.children.length > 0;
}
return isMatch;
});
}
return menuCopy;
}, [menu, searchValue, activeKey]);
const getProjectEnvData = async (data) => {
if (!data.path || !data.key) return;
if (!data.path || !data.key || data.hasOwnProperty('compData')) return;
const parentKey = menu[activeKey]?.key;
const currentMenu = _.cloneDeep(menuData[identity]);
const index = currentMenu.findIndex(v => v.key === parentKey);
const res: any = await getProjectEnv(data.id);
const res: any = await getAppInfoNew(data.id);
if (res.code === 200) {
const children = currentMenu[index].children.find(v => v.id === data.id);
children.children[0].children = res.data.events.map(item => {
@ -269,20 +328,47 @@ const SideBar: React.FC<SideBarProps> = ({
return {
title: compTypeMap[item],
icon: item === 'appComponent' ? '/ideContainer/icon/app1.png' : '/ideContainer/icon/complexApp.png',
children: res.data.compList[item].map(item => {
children: item === 'appComponent' ? res.data.compList[item].map(title => {
return {
title: item,
title: title,
children: null,
icon: '/ideContainer/icon/tool.png'
};
}) : res.data.subs.map(info => {
return {
title: info.flowName,
children: null,
icon: '/ideContainer/icon/tool.png',
compData: info,
path: 'complexFlow',
key: info.flowId,
pathTitle: `${data.title} / ${info.flowName}`,
parentKey: 'appList'
};
})
};
});
const findMenuItem = (menuItems: any[], key: string): any => {
for (const item of menuItems) {
if (item.key === key) {
return item;
}
if (item.children) {
const found = findMenuItem(item.children, key);
if (found) return found;
}
}
return null;
};
// 更新 menuData 中的数据
dispatch(updateMenuData({ ...menuData, [identity]: currentMenu }));
// 更新 flowData 中的数据
dispatch(updateFlowData({ [data.id]: res.data.app }));
dispatch(updateFlowData({ [data.id]: res.data }));
// 更新 currentAppData 中的数据
dispatch(updateCurrentAppData({ ...findMenuItem(menuData[identity], children.key) }));
// 同时更新本地 menu 状态以触发重新渲染
setMenu(prevMenu => {
@ -333,6 +419,63 @@ const SideBar: React.FC<SideBarProps> = ({
else if (identity === 'componentDevelopment') setMenu(getMenuData());
}, [subMenuData, identity]);
// 处理搜索输入变化
const handleSearchChange = (value: string) => {
setSearchValue(value);
};
// 处理鼠标按下事件
const handleMouseDown = (e: React.MouseEvent) => {
// 明确检查右键点击
if (e.button === 2) {
e.stopPropagation();
e.preventDefault();
// 获取点击的目标元素及其数据
const target = e.target as HTMLElement;
const treeNode = target.closest('.arco-tree-node');
if (treeNode) {
// 在实际应用中,你可能需要通过 tree-node 获取对应的数据
// 这里我们简单地显示右键菜单
}
// 设置右键菜单的位置和显示
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY,
nodeData: null
});
}
};
// 劫持右键
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// 设置右键菜单的位置和显示
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY,
nodeData: null
});
};
// 点击其他地方隐藏右键菜单
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) {
setContextMenu(prev => ({ ...prev, visible: false }));
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// 渲染节点的额外操作按钮
const renderNodeExtra = (node) => {
@ -349,6 +492,7 @@ const SideBar: React.FC<SideBarProps> = ({
setCurrentApp(node.dataRef);
setModalType('EDIT');
setShowModal(true);
setContextMenu(prev => ({ ...prev, visible: false })); // 隐藏右键菜单
}}
>
<span style={{ color: 'rgba(161, 165, 194, 1)' }}>
@ -372,6 +516,7 @@ const SideBar: React.FC<SideBarProps> = ({
// 通知父组件应用已被删除
onDeleteApp?.(node.dataRef.id);
onRefresh();
setContextMenu(prev => ({ ...prev, visible: false })); // 隐藏右键菜单
}
});
}}
@ -395,6 +540,9 @@ const SideBar: React.FC<SideBarProps> = ({
top: 10,
color: '#000000'
}}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Dropdown>
);
@ -446,6 +594,8 @@ const SideBar: React.FC<SideBarProps> = ({
prefix={<IconSearch />}
placeholder={'搜索'}
style={{ width: '90%' }}
value={searchValue}
onChange={handleSearchChange}
/>
<Button
type="primary"
@ -459,25 +609,63 @@ const SideBar: React.FC<SideBarProps> = ({
</div>}
{/* 子菜单 */}
<Tree
defaultExpandedKeys={['0-0']}
selectedKeys={[]} // 移除选中样式
onSelect={async (_selectedKeys, info) => {
const selectedNode = info.node;
const originalData = selectedNode.props.dataRef;
await getProjectEnvData(originalData);
// 调用外部传入的菜单选择处理函数
originalData.key && onMenuSelect?.({ ...originalData } as Selected);
}}
style={{ background: 'transparent' }} // 移除背景色
renderExtra={renderNodeExtra}
>
{renderMenuItems(menu[activeKey]?.children)}
</Tree>
<div onContextMenu={handleContextMenu}>
<Tree
defaultExpandedKeys={['0']} // 整个属性去掉就会展开全部
selectedKeys={[]} // 移除选中样式
onMouseDown={handleMouseDown}
onSelect={async (_selectedKeys, info) => {
const selectedNode = info.node;
const originalData = selectedNode.props.dataRef;
if (selected?.parentKey === 'appList') {
await getProjectEnvData(originalData);
// 调用外部传入的菜单选择处理函数
originalData.key && onMenuSelect?.({ ...originalData } as Selected);
}
}}
style={{ background: 'transparent' }} // 移除背景色
renderExtra={selected?.parentKey === 'appList' ? renderNodeExtra : null}
>
{renderMenuItems(filteredMenu[activeKey]?.children)}
</Tree>
</div>
</div>
</ResizeBox>}
{/* 右键菜单 */}
{/*{contextMenu.visible && (*/}
{/* <div*/}
{/* ref={contextMenuRef}*/}
{/* className={styles['context-menu']}*/}
{/* style={{*/}
{/* position: 'fixed',*/}
{/* top: contextMenu.y,*/}
{/* left: contextMenu.x,*/}
{/* zIndex: 1000*/}
{/* }}*/}
{/* >*/}
{/* <Menu*/}
{/* className={styles['context-menu-dropdown']}*/}
{/* style={{*/}
{/* borderRadius: 4,*/}
{/* boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'*/}
{/* }}*/}
{/* >*/}
{/* <MenuItem*/}
{/* key="refresh"*/}
{/* onClick={() => {*/}
{/* console.log('点击');*/}
{/* // onRefresh();*/}
{/* // setContextMenu(prev => ({ ...prev, visible: false }));*/}
{/* }}*/}
{/* >*/}
{/* <span>编辑复合组件</span>*/}
{/* </MenuItem>*/}
{/* </Menu>*/}
{/* </div>*/}
{/*)}*/}
{/* 新增/编辑应用 */}
{showModal && (
<AppHandleModal

@ -70,4 +70,34 @@
display: flex;
padding: 10px;
}
}
.context-menu {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 4px 0;
.context-menu-dropdown {
border: none;
box-shadow: none;
padding: 0;
:global {
.arco-menu-inner {
padding: 4px 0;
}
.arco-menu-item {
padding: 5px 12px;
margin: 0;
height: 32px;
line-height: 22px;
&:hover {
background-color: #f5f5f5;
}
}
}
}
}

@ -4,4 +4,5 @@ export interface Selected {
key?: string;
parentKey?: string;
children?: Selected[];
pathTitle?: string;
}

@ -26,7 +26,7 @@ const CompInfo = ({ currentCompInfo }) => {
<h3></h3>
<Table
columns={columns}
data={currentCompInfo.def?.dataIns}
data={currentCompInfo.def?.dataIns || currentCompInfo.flowHousVO?.dataIns}
pagination={false}
scroll={{
y: 150
@ -34,7 +34,7 @@ const CompInfo = ({ currentCompInfo }) => {
<h3></h3>
<Table
columns={columns}
data={currentCompInfo.def?.dataOuts}
data={currentCompInfo.def?.dataOuts || currentCompInfo.flowHousVO?.dataIns}
pagination={false}
scroll={{
y: 150
@ -43,26 +43,80 @@ const CompInfo = ({ currentCompInfo }) => {
);
};
// 渲染组件外壳
const renderCompHousing = () => {
return (
<div className={styles['comp-housing']}>
<div className={styles['comp-housing-header']}>
<div className={styles['comp-housing-title']}>{currentCompInfo.name || currentCompInfo?.main?.name}</div>
</div>
<div className={styles['comp-housing-content']}>
{currentCompInfo.def ? (
<>
<div className={styles['comp-housing-content-api']}>
<div className={styles['comp-housing-content-api-in']}>
{currentCompInfo.def?.apis?.map((item: any, index) => <div key={index}>{item.id}</div>)}
</div>
<div className={styles['comp-housing-content-api-out']}>
<div>{currentCompInfo.def?.apiOut?.id}</div>
</div>
</div>
<Divider style={{ marginTop: 10, marginBottom: 10 }} />
<div className={styles['comp-housing-content-data']}>
<div className={styles['comp-housing-content-data-in']}>
{currentCompInfo.def?.dataIns?.map((item: any, index) => <div key={index}>{item.id}</div>)}
</div>
<div className={styles['comp-housing-content-data-out']}>
{currentCompInfo.def?.dataOuts?.map((item: any, index) => <div key={index}>{item.id}</div>)}
</div>
</div>
</>
) : (
<>
<div className={styles['comp-housing-content-api']}>
<div className={styles['comp-housing-content-api-in']}>
<div>contour</div>
</div>
<div className={styles['comp-housing-content-api-out']}>
<div>done</div>
</div>
</div>
<Divider style={{ marginTop: 10, marginBottom: 10 }} />
<div className={styles['comp-housing-content-data']}>
<div className={styles['comp-housing-content-data-in']}>
{currentCompInfo.flowHousVO?.dataIns?.map((item: any, index) => <div key={index}>{item.id}</div>)}
</div>
<div className={styles['comp-housing-content-data-out']}>
{currentCompInfo.flowHousVO?.dataOuts?.map((item: any, index) => <div key={index}>{item.id}</div>)}
</div>
</div>
</>
)}
</div>
</div>
);
};
return (
currentCompInfo ? (<div className={styles['comp-container']}>
<div className={styles['comp-preview']}>
<h3></h3>
<div className={styles['comp-housing']}></div>
{renderCompHousing()}
</div>
<div className={styles['comp-info']}>
<div className={styles['header']}>
<Space size={40}>
<div className={styles['title']}>{currentCompInfo.name}</div>
<div className={styles['title']}>{currentCompInfo.name || currentCompInfo?.main?.name}</div>
</Space>
</div>
<Divider style={{ borderColor: '#5484ff', marginTop: 0, marginBottom: 30 }} />
<div className={styles['extra']}>
{currentCompInfo.def ? (<div className={styles['extra']}>
<Divider style={{ borderColor: '#5484ff', marginTop: 0, marginBottom: 30 }} />
<Space size={30}>
<div className={styles['extra-font']}>{currentCompInfo.identifier}</div>
<div className={styles['extra-font']}>{currentCompInfo.componentClassify}</div>
<div className={styles['extra-font']}>{currentCompInfo.codeLanguage}</div>
</Space>
</div>
</div>) : null}
<Divider style={{ marginTop: 15, borderBottomStyle: 'dashed' }} />
<div className={styles['description']}>
{currentCompInfo.description ? currentCompInfo.description : ' - '}

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useMemo } from 'react';
import styles from './style/sideBar.module.less';
import { Button, Input, Tree } from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
@ -6,6 +6,55 @@ import { IconSearch } from '@arco-design/web-react/icon';
const TreeNode = Tree.Node;
const SideBar = ({ compList, onSelect }) => {
const [searchValue, setSearchValue] = useState('');
// 处理搜索输入变化
const handleSearchChange = (value) => {
setSearchValue(value);
};
// 处理搜索按钮点击
const handleSearch = () => {
// 搜索逻辑将在树形结构渲染时处理
};
// 递归过滤树节点
const filterTreeData = (data, searchValue) => {
if (!searchValue) return data;
if (Array.isArray(data)) {
return data.filter(item => {
const name = item.name || item?.main?.name || '';
return name.toLowerCase().includes(searchValue.toLowerCase());
});
}
if (typeof data === 'object' && data !== null) {
const filteredData = {};
Object.keys(data).forEach(key => {
if (Array.isArray(data[key])) {
const filteredArray = filterTreeData(data[key], searchValue);
if (filteredArray.length > 0) {
filteredData[key] = filteredArray;
}
} else if (typeof data[key] === 'object' && data[key] !== null) {
const filteredObject = filterTreeData(data[key], searchValue);
if (Object.keys(filteredObject).length > 0) {
filteredData[key] = filteredObject;
}
}
});
return filteredData;
}
return data;
};
// 根据搜索值过滤组件列表
const filteredCompList = useMemo(() => {
if (!searchValue) return compList;
return filterTreeData(compList, searchValue);
}, [compList, searchValue]);
const renderTreeNode = (menuItems, parentKey = '0') => {
// 标题枚举值
@ -45,13 +94,14 @@ const SideBar = ({ compList, onSelect }) => {
// 如果是数组,表示是最底层的子节点,直接渲染
if (Array.isArray(menuItems)) {
return menuItems.map((item, index) => {
const name = item.name || item?.main?.name || '';
const treeNodeProps = {
dataRef: item // 传递原始数据
};
return (<TreeNode
{...treeNodeProps}
key={`${parentKey}-${index}`}
title={item.name}
title={name}
icon={<img src={'/ideContainer/icon/compItem.png'} style={{ width: 17, height: 17 }} />}
/>);
});
@ -61,11 +111,21 @@ const SideBar = ({ compList, onSelect }) => {
return Object.keys(menuItems).map((key, index) => {
const child = menuItems[key];
const currentKey = `${parentKey}-${index}`;
const title = titleMap.get(key)?.title || key;
const icon = titleMap.get(key)?.icon || null;
const titleInfo = titleMap.get(key) || { title: key, icon: null };
const title = titleInfo.title;
const icon = titleInfo.icon;
// 如果子节点是数组或对象,继续递归
if (Array.isArray(child) || typeof child === 'object') {
// 如果搜索值存在,但当前节点下没有匹配的子节点,则不渲染该节点
const filteredChild = filterTreeData(child, searchValue);
if (searchValue && (
(Array.isArray(filteredChild) && filteredChild.length === 0) ||
(typeof filteredChild === 'object' && Object.keys(filteredChild).length === 0)
)) {
return null;
}
return (
<TreeNode key={currentKey} title={title} icon={icon}>
{renderTreeNode(child, currentKey)}
@ -74,24 +134,28 @@ const SideBar = ({ compList, onSelect }) => {
}
// 否则直接渲染叶子节点
return <TreeNode key={currentKey} title={title} />;
return <TreeNode key={currentKey} title={title} icon={icon} />;
});
};
// 处理输入框回车事件
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleSearch();
}
};
return (
<div className={styles['side-bar']}>
<div className={styles['handle-box']}>
<Input
prefix={<IconSearch />}
placeholder={'搜索组件'}
style={{ width: '90%' }}
style={{ width: '100%' }}
value={searchValue}
onChange={handleSearchChange}
onPressEnter={handleSearch}
/>
<Button
type="primary"
style={{ marginLeft: 5 }}
>
</Button>
</div>
<div className={styles['comp-list']}>
<Tree
@ -100,11 +164,14 @@ const SideBar = ({ compList, onSelect }) => {
selectedKeys={[]} // 移除选中样式
style={{ background: 'transparent' }} // 移除背景色
onSelect={(value, info) => {
if (info.node.props.dataRef.hasOwnProperty('children')) return;
if (info.node.props.dataRef?.hasOwnProperty('children')) return;
onSelect(info.node?.props?.dataRef || null);
}}
>
{renderTreeNode({ projectCompDto: compList.projectCompDto, projectFlowDto: compList.projectFlowDto })}
{renderTreeNode({
projectCompDto: filteredCompList.projectCompDto,
projectFlowDto: filteredCompList.projectFlowDto
})}
</Tree>
</div>
</div>

@ -22,11 +22,40 @@
border: 1px solid #CCCCCC;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
overflow: hidden;
.comp-housing-header {
background-color: #ffa100;
color: #ffffff;
padding: 5px 10px;
}
.comp-housing-content {
.comp-housing-content-api,
.comp-housing-content-data {
display: flex;
padding: 5px;
.comp-housing-content-api-in,
.comp-housing-content-api-out,
.comp-housing-content-data-in,
.comp-housing-content-data-out {
flex: 1;
}
.comp-housing-content-api-out,
.comp-housing-content-data-out {
text-align: right;
}
}
}
}
}
.comp-info {
flex: 1;
.header {
.title {
font-size: 22px;

@ -1,9 +1,38 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import FlowEditor from '@/pages/flowEditor/index';
import { useSelector, useDispatch } from 'react-redux';
import { getAppEventList } from '@/api/appEvent';
import { getTopicList } from '@/api/event';
import { updateEventTopicList } from '@/store/ideContainer';
const ApplicationContainer = () => {
const { info } = useSelector((state: any) => state.ideContainer);
const [appFlowList, setAppFlowList] = useState([]);
const dispatch = useDispatch();
const getAppFlowList = async () => {
const res: any = await getAppEventList(info.id);
if (res.code === 200) setAppFlowList(res.data);
};
const getEventList = async () => {
const res: any = await getTopicList(info.id);
if (res.code === 200) {
dispatch(updateEventTopicList(res.data.map(v => {
return { label: v.eventName, value: v.topic };
})));
}
};
useEffect(() => {
getEventList();
getAppFlowList();
}, []);
return (
<FlowEditor useDefault={false} />
<FlowEditor initialData={appFlowList} useDefault={false} />
);
};

@ -0,0 +1,11 @@
import React, { useEffect, useState } from 'react';
import FlowEditor from '@/pages/flowEditor/index';
const complexContainer = ({ selected }) => {
return (
<FlowEditor initialData={selected.compData || {}} useDefault={true} />
);
};
export default complexContainer;

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import styles from './style/index.module.less';
import {
Button,
@ -29,7 +29,7 @@ const HandleModal = ({ visible, onChangeVisible, onRefresh }) => {
const params = {
name: formData.name,
topic: `${formData.topic}/${info.identity}`,
topic: `${formData.topic}`,
description: formData.description,
sceneId: info.id
};
@ -94,6 +94,9 @@ const HandleModal = ({ visible, onChangeVisible, onRefresh }) => {
if (!value) {
return cb('请填写事件标识');
}
if (value.includes('**empty**')) {
return cb('非法事件标识,请重新输入');
}
return cb();
}
@ -103,7 +106,7 @@ const HandleModal = ({ visible, onChangeVisible, onRefresh }) => {
<Input placeholder="请输入事件标识" />
</FormItem>
<FormItem
field="description"
field="desc"
label="事件描述"
required
rules={[
@ -129,8 +132,27 @@ const HandleModal = ({ visible, onChangeVisible, onRefresh }) => {
const EventContainer = () => {
const [eventData, setEventData] = useState<any[]>([]);
const [visible, setVisible] = useState(false);
const [searchValue, setSearchValue] = useState('');
const { info } = useSelector((state: any) => state.ideContainer);
// 根据搜索值过滤事件数据
const filteredEventData = useMemo(() => {
if (!searchValue) return eventData;
return eventData.filter(item => {
const name = item.name || '';
const topic = item.topic || '';
const desc = item.description || '';
const searchLower = searchValue.toLowerCase();
return (
name.toLowerCase().includes(searchLower) ||
topic.toLowerCase().includes(searchLower) ||
desc.toLowerCase().includes(searchLower)
);
});
}, [eventData, searchValue]);
const columns: TableColumnProps[] = [
{
title: '序号',
@ -152,8 +174,39 @@ const EventContainer = () => {
},
{
title: '事件描述',
dataIndex: 'description',
width: '80%'
dataIndex: 'desc'
},
{
title: '订阅者',
dataIndex: 'subscribers',
render: (_, record) => {
if (record?.subscribers && record?.subscribers.length > 0) {
return (<div>
{record?.subscribers.map((item, index) => (
<div key={index}>{item.appName}</div>
))}
</div>);
}
else {
return <div>-</div>;
}
}
},
{
title: '发布者',
dataIndex: 'publisher',
render: (_, record) => {
if (record?.publisher && record?.publisher.length > 0) {
return (<div>
{record?.publisher.map((item, index) => (
<div key={index}>{item.appName}</div>
))}
</div>);
}
else {
return <div>-</div>;
}
}
},
{
title: '操作',
@ -183,7 +236,24 @@ const EventContainer = () => {
const fetchEventData = async () => {
const res: any = await queryEventItemBySceneId(info.id);
if (res && res.code === 200) setEventData(res.data);
if (res && res.code === 200) setEventData(res.data.filter(item => !item.topic.includes('**empty**')));
};
// 处理搜索输入变化
const handleSearchChange = (value: string) => {
setSearchValue(value);
};
// 处理搜索,这里可以添加防抖等优化
const handleSearch = () => {
// 搜索逻辑已经在 useMemo 中实现
};
// 处理回车键搜索
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
useEffect(() => {
@ -203,6 +273,9 @@ const EventContainer = () => {
prefix={<IconSearch />}
placeholder={'搜索'}
style={{ width: 236 }}
value={searchValue}
onChange={handleSearchChange}
onPressEnter={handleSearch}
/>
<Button
type="primary"
@ -216,7 +289,7 @@ const EventContainer = () => {
</div>
{/*数据列表*/}
<div className={styles['event-list']}>
<Table columns={columns} data={eventData} pagination={false} />
<Table columns={columns} data={filteredEventData} pagination={false} />
</div>
{visible &&

@ -4,14 +4,9 @@ import { useSelector } from 'react-redux';
const ProjectContainer = ({ selected }) => {
const { flowData } = useSelector(state => state.ideContainer);
const [selectedFlowData, setSelectedFlowData] = useState<any>({});
useEffect(() => {
setSelectedFlowData(flowData[selected.id]);
}, [selected]);
return (
<FlowEditor initialData={selectedFlowData} useDefault={true}/>
<FlowEditor initialData={flowData[selected.id] || {}} useDefault={true} />
);
};

@ -1,23 +1,38 @@
import { createSlice } from '@reduxjs/toolkit';
// 定义初始状态类型
interface IDEContainerState {
info: any;
menuData: any;
flowData: any;
canvasDataMap: any;
projectComponentData: any;
currentAppData: any;
eventList: any;
eventTopicList: any;
logBarStatus?: boolean;
socketId: string;
nodeStatusMap: Record<string, string>; // 节点状态映射
isRunning: boolean; // 是否正在运行
}
// 初始状态
const initialState: IDEContainerState = {
info: {}, // 项目信息
menuData: {}, // 菜单数据
flowData: {}, // 编排数据,即流程图的渲染数据
canvasDataMap: {}, // 每个画布的缓存信息
projectComponentData: {}, // 工程下的组件列表
logBarStatus: false
currentAppData: {}, // 当前选中的应用数据
eventList: [], // 工程下的事件列表
eventTopicList: [], // 应用编排使用的事件名和topic列表
logBarStatus: false,
socketId: '', // 工程的socketId
nodeStatusMap: {}, // 初始化节点状态映射
isRunning: false // 默认未运行
};
// 创建切片
const ideContainerSlice = createSlice({
name: 'ideContainer',
initialState,
@ -37,19 +52,53 @@ const ideContainerSlice = createSlice({
updateProjectComponentData(state, action) {
state.projectComponentData = { ...state.projectComponentData, ...action.payload };
},
updateCurrentAppData(state, action) {
state.currentAppData = action.payload;
},
updateEventList(state, action) {
state.eventList = action.payload;
},
updateEventTopicList(state, action) {
state.eventTopicList = action.payload;
},
updateLogBarStatus(state, action) {
state.logBarStatus = action.payload;
},
updateSocketId(state, action) {
state.socketId = action.payload;
},
// 更新节点状态
updateNodeStatus: (state, { payload }) => {
const { nodeId, status } = payload;
state.nodeStatusMap[nodeId] = status;
},
// 重置节点状态
resetNodeStatus: (state) => {
state.nodeStatusMap = {};
},
// 更新运行状态
updateIsRunning: (state, { payload }) => {
state.isRunning = payload;
}
}
});
// 导出动作 creators
export const {
updateInfo,
updateMenuData,
updateFlowData,
updateCanvasDataMap,
updateProjectComponentData,
updateLogBarStatus
updateCurrentAppData,
updateEventList,
updateEventTopicList,
updateLogBarStatus,
updateSocketId,
updateNodeStatus,
resetNodeStatus,
updateIsRunning
} = ideContainerSlice.actions;
// 默认导出 reducer
export default ideContainerSlice.reducer;

@ -0,0 +1,126 @@
/**
* flow editor nodes edges
* @param appFlowData - 2
* @returns nodes edges convertFlowData
*/
export const convertAppFlowData = (appFlowData: any[]) => {
const nodes: any[] = [];
const edges: any[] = [];
// 如果没有数据,返回空数组
if (!appFlowData || appFlowData.length === 0) {
return { nodes, edges };
}
// 处理每个应用流程数据项(每个应用作为一个节点)
appFlowData.forEach((app: any, index: number) => {
// 构造节点数据
const node: any = {
id: app.appId || `app_${index}`,
type: 'APP',
position: { x: 200 + index * 300, y: 200 },
data: {
title: app.name || `应用${index + 1}`,
parameters: {
// eventListenes 作为 apiIns输入
apiIns: app.eventListenes ? app.eventListenes.map((event: any) => ({
name: event.eventName,
desc: event.description || '',
dataType: '',
defaultValue: '',
topic: event.topic
})) : [],
// eventSends 作为 apiOuts输出
apiOuts: app.eventSends ? app.eventSends.map((event: any) => ({
name: event.eventName,
desc: event.description || '',
dataType: '',
defaultValue: '',
topic: event.topic
})) : [],
// 提取 dataIns 和 dataOuts 属性
dataIns: [],
dataOuts: []
},
type: 'APP',
component: {
type: 'APP',
appId: app.appId,
customDef: JSON.stringify({
eventListenes: app.eventListenes || [],
eventSends: app.eventSends || []
})
}
}
};
// 处理 dataIns来自 eventListenes 的 dataOuts
if (app.eventListenes && app.eventListenes.length > 0) {
app.eventListenes.forEach((event: any) => {
if (event.dataOuts && event.dataOuts.length > 0) {
node.data.parameters.dataIns = [...node.data.parameters.dataIns, ...event.dataOuts];
}
});
}
// 处理 dataOuts来自 eventSends 的 dataIns
if (app.eventSends && app.eventSends.length > 0) {
app.eventSends.forEach((event: any) => {
if (event.dataIns && event.dataIns.length > 0) {
node.data.parameters.dataOuts = [...node.data.parameters.dataOuts, ...event.dataIns];
}
});
}
nodes.push(node);
});
// 遍历所有节点对
for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes.length; j++) {
if (i !== j) { // 不与自己比较
const sourceNode = nodes[i];
const targetNode = nodes[j];
// 检查源节点的 eventSends (apiOuts) 和目标节点的 eventListenes (apiIns)
const sourceEvents = sourceNode.data.component?.customDef ?
JSON.parse(sourceNode.data.component.customDef).eventSends || [] :
[];
const targetEvents = targetNode.data.component?.customDef ?
JSON.parse(targetNode.data.component.customDef).eventListenes || [] :
[];
// 比较事件的 topic 是否匹配
sourceEvents.forEach((sourceEvent: any, outIndex: number) => {
targetEvents.forEach((targetEvent: any, inIndex: number) => {
// 当 topic 匹配且不是 **empty** 占位符时创建边
if (sourceEvent.topic &&
targetEvent.topic &&
sourceEvent.topic === targetEvent.topic &&
!sourceEvent.topic.includes('**empty**') &&
!targetEvent.topic.includes('**empty**')) {
edges.push({
id: `e-${sourceNode.id}-${targetNode.id}-${outIndex}-${inIndex}`,
source: sourceNode.id,
target: targetNode.id,
sourceHandle: sourceEvent.eventName,
targetHandle: targetEvent.eventName,
type: 'custom',
lineType: 'lineType<api|data>',
data: {
displayData: {
name: '事件1',
eventId: 'eventId',
topic: 'topic'
}
}
});
}
});
});
}
}
}
return { nodes, edges };
};

@ -1,5 +1,12 @@
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
import store from '@/store/index';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
/**
* flow editor nodes edges
@ -10,8 +17,9 @@ import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
export const convertFlowData = (flowData: any, useDefault = true) => {
const nodes: any[] = [];
const edges: any[] = [];
const currentProjectCompData = getCurrentProjectStoreData();
if (!flowData || Object.keys(flowData).length === 0 || flowData.main.nodeConfigs.length === 0) {
if (!flowData || Object.keys(flowData).length === 0) {
// 如果useDefault为true且flowData为空则返回默认的开始和结束节点
if (useDefault) {
return {
@ -54,99 +62,277 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
return { nodes, edges };
}
// 处理节点配置
const nodeConfigs = flowData.main?.nodeConfigs || [];
for (const nodeConfig of nodeConfigs) {
// 处理新格式的数据结构
// 先处理所有节点
const nodeEntries = Object.entries(flowData);
for (const entry of nodeEntries) {
const nodeId: string = entry[0];
const nodeConfig: any = entry[1];
// 确定节点类型
let nodeType = 'BASIC';
if (nodeConfig.nodeId === 'start') {
if (nodeId === 'start') {
nodeType = 'start';
}
else if (nodeConfig.nodeId === 'end') {
else if (nodeId === 'end') {
nodeType = 'end';
}
else if (nodeConfig.component?.type === 'LOOP_START' || nodeConfig.component?.type === 'LOOP_END') {
nodeType = 'LOOP';
}
else {
nodeType = nodeConfig.component.type;
nodeType = nodeConfig.component?.type || 'BASIC';
}
// 解析位置信息
let position = { x: 0, y: 0 };
try {
const x6Data = JSON.parse(nodeConfig.x6);
position = x6Data.position || { x: 0, y: 0 };
} catch (e) {
console.warn('Failed to parse position for node:', nodeConfig.nodeId);
}
const position = nodeConfig.position || { x: 0, y: 0 };
// 构造节点数据
const node: any = {
id: nodeConfig.nodeId,
id: nodeId,
type: nodeType,
position,
data: {
title: nodeConfig.nodeName || nodeConfig.nodeId,
title: nodeConfig.componentName || nodeId,
parameters: {
apiIns: [{
name: 'start',
desc: '',
dataType: '',
defaultValue: ''
}],
apiOuts: [{
name: nodeConfig.nodeId === 'end' ? 'end' : 'done',
desc: '',
dataType: '',
defaultValue: ''
}],
dataIns: nodeConfig.dataIns?.map((input: any) => ({
name: input.id,
desc: input.desc,
dataType: input.dataType,
defaultValue: input.defaultValue
})) || [],
dataOuts: nodeConfig.dataOuts?.map((output: any) => ({
name: output.id,
desc: output.desc,
dataType: output.dataType,
defaultValue: output.defaultValue
})) || []
apiIns: getNodeApiIns(nodeId, nodeConfig, currentProjectCompData),
apiOuts: getNodeApiOuts(nodeId, nodeConfig, currentProjectCompData),
dataIns: nodeConfig.dataIns || [],
dataOuts: nodeConfig.dataOuts || []
},
type: nodeType
type: nodeConfig.component?.type || nodeType
}
};
// 如果是机械臂节点,添加组件标识信息
// 添加组件标识信息
if (nodeConfig.component) {
node.data.component = {
compIdentifier: nodeConfig.component.compIdentifier,
compInstanceIdentifier: nodeConfig.component.compInstanceIdentifier,
customDef: nodeConfig.component?.customDef,
type: nodeConfig.component.type
};
node.data.component = { ...nodeConfig.component };
node.data.compId = nodeConfig.component.compId;
}
// 注册循环节点类型
if (nodeType === 'LOOP') {
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
if (!nodeMap.includes('LOOP')) {
registerNodeType('LOOP', LoopNode, '循环');
}
}
// 将未定义的节点动态追加进nodeTypes
// 注册其他节点类型
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
// 目前默认添加的都是系统组件/本地组件
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, LocalNode, nodeConfig.nodeName);
if (!nodeMap.includes(nodeType) && nodeType !== 'start' && nodeType !== 'end' && nodeType !== 'LOOP') {
registerNodeType(nodeType, getNodeComponent(nodeType), nodeConfig.componentName);
}
nodes.push(node);
}
// 处理连线配置
const lineConfigs = flowData.main?.lineConfigs || [];
for (const lineConfig of lineConfigs) {
const edge: any = {
id: lineConfig.id,
source: lineConfig.prev.nodeId,
target: lineConfig.next.nodeId,
sourceHandle: lineConfig.prev.endpointId,
targetHandle: lineConfig.next.endpointId
};
// 用于存储已添加的边,避免重复
const addedEdges = new Set<string>();
// 创建一个映射来存储所有连接信息
const connections = new Map<string, { source: string; target: string; sourceHandle: string; targetHandle: string }>();
// 遍历所有节点,收集连接信息
for (const entry of nodeEntries) {
const nodeId: string = entry[0];
const nodeConfig: any = entry[1];
// 处理 API 下游连接 - 确定目标节点信息
if (nodeConfig.apiDownstream && Array.isArray(nodeConfig.apiDownstream)) {
nodeConfig.apiDownstream.forEach((targetArray: string[]) => {
if (Array.isArray(targetArray)) {
targetArray.forEach(target => {
if (typeof target === 'string' && target.includes('$$')) {
const [targetNodeId, targetHandle] = target.split('$$');
const connectionKey = `${nodeId}-${targetNodeId}`;
// 存储连接信息
if (connections.has(connectionKey)) {
// 如果连接已存在,更新目标句柄
const existing = connections.get(connectionKey);
if (existing) {
connections.set(connectionKey, {
...existing,
targetHandle: targetHandle
});
}
}
else {
// 创建新的连接信息
connections.set(connectionKey, {
source: nodeId,
target: targetNodeId,
sourceHandle: '', // 将根据节点信息填充
targetHandle: targetHandle
});
}
}
});
}
});
}
// 处理 API 上游连接 - 确定源节点信息
if (nodeConfig.apiUpstream && Array.isArray(nodeConfig.apiUpstream)) {
nodeConfig.apiUpstream.forEach((sourceArray: string[]) => {
if (Array.isArray(sourceArray)) {
sourceArray.forEach(source => {
if (typeof source === 'string' && source.includes('$$')) {
const [sourceNodeId, sourceHandle] = source.split('$$');
const connectionKey = `${sourceNodeId}-${nodeId}`;
// 存储连接信息
if (connections.has(connectionKey)) {
// 如果连接已存在,更新源句柄
const existing = connections.get(connectionKey);
if (existing) {
connections.set(connectionKey, {
...existing,
sourceHandle: sourceHandle
});
}
}
else {
// 创建新的连接信息
connections.set(connectionKey, {
source: sourceNodeId,
target: nodeId,
sourceHandle: sourceHandle,
targetHandle: '' // 将根据节点信息填充
});
}
}
});
}
});
}
}
// 根据收集的连接信息生成实际的边
const connectionEntries = Array.from(connections.entries());
for (const [connectionKey, connectionInfo] of connectionEntries) {
const { source, target, sourceHandle, targetHandle } = connectionInfo;
// 获取源节点和目标节点
const sourceNode = flowData[source];
const targetNode = flowData[target];
// 确定最终的源句柄
let finalSourceHandle = sourceHandle;
// 如果源句柄未指定,则根据源节点信息确定
if (!finalSourceHandle) {
if (source === 'start') {
finalSourceHandle = 'start';
}
else if (sourceNode && sourceNode.data && sourceNode.data.parameters &&
sourceNode.data.parameters.apiOuts && sourceNode.data.parameters.apiOuts.length > 0) {
// 查找匹配的目标句柄
const matchingApiOut = sourceNode.data.parameters.apiOuts.find(
(apiOut: any) => apiOut.name === targetHandle
);
if (matchingApiOut) {
finalSourceHandle = matchingApiOut.name;
}
else {
// 如果没有精确匹配使用第一个apiOut
finalSourceHandle = sourceNode.data.parameters.apiOuts[0].name;
}
}
else if (sourceNode && sourceNode.component && sourceNode.component.type) {
// 根据节点类型获取正确的源句柄
finalSourceHandle = getNodeApiOutHandle(source, sourceNode);
}
else {
// 默认句柄
finalSourceHandle = 'done';
}
}
// 确定最终的目标句柄
let finalTargetHandle = targetHandle;
// 如果目标句柄未指定,则根据目标节点信息确定
if (!finalTargetHandle) {
if (target === 'end') {
finalTargetHandle = 'end';
}
else if (targetNode && targetNode.data && targetNode.data.parameters &&
targetNode.data.parameters.apiIns && targetNode.data.parameters.apiIns.length > 0) {
// 查找匹配的源句柄
const matchingApiIn = targetNode.data.parameters.apiIns.find(
(apiIn: any) => apiIn.name === sourceHandle
);
if (matchingApiIn) {
finalTargetHandle = matchingApiIn.name;
}
else {
// 如果没有精确匹配使用第一个apiIn
finalTargetHandle = targetNode.data.parameters.apiIns[0].name;
}
}
else {
// 默认句柄
finalTargetHandle = 'start';
}
}
edges.push(edge);
// 创建边的唯一标识符
const edgeId = `${source}-${target}-${finalSourceHandle}-${finalTargetHandle}`;
// 检查是否已添加此边
if (!addedEdges.has(edgeId)) {
addedEdges.add(edgeId);
edges.push({
id: `${edgeId}`,
source: source,
target: target,
sourceHandle: finalSourceHandle,
targetHandle: finalTargetHandle
});
}
}
// 处理数据下游连接
for (const entry of nodeEntries) {
const nodeId: string = entry[0];
const nodeConfig: any = entry[1];
if (nodeConfig.dataDownstream && Array.isArray(nodeConfig.dataDownstream)) {
nodeConfig.dataDownstream.forEach((connectionGroup: string[]) => {
// 确保 connectionGroup 是数组并且至少包含两个元素
if (Array.isArray(connectionGroup) && connectionGroup.length >= 2) {
// 第一个元素是源节点和句柄信息
const [sourceInfo, targetInfo] = connectionGroup;
if (typeof sourceInfo === 'string' && sourceInfo.includes('@@') &&
typeof targetInfo === 'string' && targetInfo.includes('@@')) {
const [sourceNodeId, sourceHandle] = sourceInfo.split('@@');
const [targetNodeId, targetHandle] = targetInfo.split('@@');
// 创建边的唯一标识符
const edgeId = `${sourceNodeId}-${targetNodeId}-${sourceHandle}-${targetHandle}`;
// 检查是否已添加此边
if (!addedEdges.has(edgeId)) {
addedEdges.add(edgeId);
edges.push({
id: `${edgeId}`,
source: sourceNodeId,
target: targetNodeId,
sourceHandle: sourceHandle,
targetHandle: targetHandle
});
}
}
}
});
}
}
return { nodes, edges };
};
@ -168,11 +354,11 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
if (nodes && nodes.length > 0) {
flowData.nodeConfigs = nodes.map(node => {
// 确定 nodeId 和 nodeName
const nodeId = node.id;
const nodeId = node.id || node.name;
const nodeName = node.data?.title || nodeId;
// 确定节点类型
let nodeType = node.type;
let nodeType = node.data.type;
// 特殊处理 start 和 end 节点
if (nodeId === 'start') {
nodeType = 'start';
@ -197,8 +383,9 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
if (node.data?.component) {
nodeConfig.component = {
type: nodeType,
compIdentifier: node.data.component.compIdentifier,
compInstanceIdentifier: node.data.component.compInstanceIdentifier
compIdentifier: node.data.component.compIdentifier || '',
compInstanceIdentifier: node.data.component.compInstanceIdentifier || '',
compId: node.data.compId || ''
};
if (node.data.component?.customDef) nodeConfig.component.customDef = node.data.component.customDef;
}
@ -208,6 +395,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
type: nodeType
};
}
if (['BASIC', 'SUB'].includes(nodeType)) nodeConfig.component.compId = node.data.compId || '';
// 处理参数信息
const parameters = node.data?.parameters || {};
@ -215,20 +403,22 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
// 处理 dataIns输入数据
if (parameters.dataIns && parameters.dataIns.length > 0) {
nodeConfig.dataIns = parameters.dataIns.map((input: any) => ({
id: input.name,
id: input.name || input.id,
desc: input.desc,
dataType: input.dataType,
defaultValue: input.defaultValue
defaultValue: input.defaultValue,
arrayType: input.arrayType || null
}));
}
// 处理 dataOuts输出数据
if (parameters.dataOuts && parameters.dataOuts.length > 0) {
nodeConfig.dataOuts = parameters.dataOuts.map((output: any) => ({
id: output.name,
id: output.name || output.id,
desc: output.desc,
dataType: output.dataType,
defaultValue: output.defaultValue
defaultValue: output.defaultValue,
arrayType: output.arrayType || null
}));
}
@ -236,7 +426,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
});
}
// 转换连线数据
// 转换连线数据
if (edges && edges.length > 0) {
flowData.lineConfigs = edges.map((edge, index) => {
// 查找源节点和目标节点以确定连线类型
@ -251,11 +441,11 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
}
// 判断是否为API类型的连线
else if (edge.sourceHandle && (edge.sourceHandle === 'apiOuts' ||
sourceNode?.data?.parameters?.apiOuts?.some((out: any) => out.name === edge.sourceHandle))) {
sourceNode?.data?.parameters?.apiOuts?.some((out: any) => (out.name || out.id) === edge.sourceHandle))) {
lineType = 'API';
}
else if (edge.targetHandle && (edge.targetHandle === 'apiIns' ||
targetNode?.data?.parameters?.apiIns?.some((inp: any) => inp.name === edge.targetHandle))) {
targetNode?.data?.parameters?.apiIns?.some((inp: any) => (inp.name || inp.id) === edge.targetHandle))) {
lineType = 'API';
}
@ -276,3 +466,339 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
return flowData;
};
/**
* React Flow nodes edges convertFlowData
* @param nodes - React Flow
* @param edges - React Flow
* @param complexKV - 使id {IDsub_ID}
* @returns convertFlowData
*/
export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: any) => {
// 初始化返回的数据结构
const flowData: any = {};
// 转换节点数据
if (nodes && nodes.length > 0) {
nodes.forEach(node => {
const nodeId = node.id;
// 构造节点配置对象
const nodeConfig: any = {
id: nodeId,
componentName: node.data?.title || nodeId,
position: node.position || { x: 0, y: 0 }
};
// 处理 component 信息
if (node.type === 'SUB' && !node.customDef) {
nodeConfig.component = {
type: 'SUB',
compId: node.data.compId,
customDef: JSON.stringify({
dataIns: node.data.parameters.dataIns,
dataOuts: node.data.parameters.dataOuts,
subflowId: complexKV[node.data.compId],
name: node.data.title
})
};
}
else if (node.data?.component) {
nodeConfig.component = { ...node.data.component };
}
else {
nodeConfig.component = null;
}
// 处理参数信息
const parameters = node.data?.parameters || {};
// 处理 apiIns输入API
if (parameters.apiIns && parameters.apiIns.length > 0) {
nodeConfig.apiIns = parameters.apiIns;
}
else {
nodeConfig.apiIns = [];
}
// 处理 apiOuts输出API
if (parameters.apiOuts && parameters.apiOuts.length > 0) {
nodeConfig.apiOuts = parameters.apiOuts;
}
else {
nodeConfig.apiOuts = [];
}
// 处理 dataIns输入数据
if (parameters.dataIns && parameters.dataIns.length > 0) {
nodeConfig.dataIns = parameters.dataIns;
}
else {
nodeConfig.dataIns = [];
}
// 处理 dataOuts输出数据
if (parameters.dataOuts && parameters.dataOuts.length > 0) {
nodeConfig.dataOuts = parameters.dataOuts;
}
else {
nodeConfig.dataOuts = [];
}
// 初始化连接数组
nodeConfig.apiDownstream = [];
nodeConfig.apiUpstream = [];
nodeConfig.dataDownstream = [];
nodeConfig.dataUpstream = [];
// 将节点配置添加到 flowData 对象中
flowData[nodeId] = nodeConfig;
});
}
// 处理连接关系
if (edges && edges.length > 0) {
// 分析边的连接关系
edges.forEach(edge => {
const sourceNode = edge.source;
const targetNode = edge.target;
const sourceHandle = edge.sourceHandle || 'done';
const targetHandle = edge.targetHandle || 'start';
// 确定连接类型API 还是 DATA
const sourceNodeData = nodes.find(n => n.id === sourceNode);
const targetNodeData = nodes.find(n => n.id === targetNode);
const isApiConnection =
(sourceNodeData?.data?.parameters?.apiOuts?.some((out: any) => out.name === sourceHandle)) ||
(targetNodeData?.data?.parameters?.apiIns?.some((inp: any) => inp.name === targetHandle)) ||
sourceHandle === 'start' || targetHandle === 'end' ||
sourceHandle === 'end' || targetHandle === 'start';
if (isApiConnection) {
// API 连接
// 添加下游连接
flowData[sourceNode].apiDownstream.push([`${targetNode}$$${targetHandle}`]);
// 添加上游连接
flowData[targetNode].apiUpstream.push([`${sourceNode}$$${sourceHandle}`]);
}
else {
// 数据连接
const dataConnection = [`${sourceNode}@@${sourceHandle}`, `${targetNode}@@${targetHandle}`];
flowData[sourceNode].dataDownstream.push(dataConnection);
flowData[targetNode].dataUpstream.push(dataConnection);
}
});
}
return flowData;
};
// 获取节点的API输入参数
const getNodeApiIns = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => {
// 对于特定类型的节点使用预定义值
if (nodeConfig.component?.type === 'LOOP_START') {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeConfig.component?.type === 'LOOP_END') {
return [{ name: 'continue', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeId === 'end') {
return [{ name: 'end', desc: '', dataType: '', defaultValue: '' }];
}
else {
const comp = currentProjectCompData.filter(item => item.id === nodeConfig?.component?.compId);
if (comp && comp.length > 0) {
return comp[0].def?.apis.map(v => {
return {
...v,
name: v.id,
desc: v.desc,
dataType: v?.dataType || '',
defaultValue: v?.defaultValue || ''
};
});
}
else {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
}
}
};
// 获取节点的API输出参数
const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => {
// 对于特定类型的节点使用预定义值
if (nodeConfig.component?.type === 'LOOP_START') {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeConfig.component?.type === 'LOOP_END') {
// 从customDef中获取apiOutIds数组
try {
const customDef = JSON.parse(nodeConfig.component?.customDef || '{}');
const apiOutIds = customDef.apiOutIds || [];
// 从"break"开始的所有项都应该作为apiOut返回
const breakIndex = apiOutIds.indexOf('break');
if (breakIndex !== -1) {
// 返回从"break"开始的所有项
return apiOutIds.slice(breakIndex).map(id => ({
name: id,
id: id,
desc: id,
dataType: '',
defaultValue: ''
}));
}
else {
// 如果没有找到"break",则返回默认值
return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }];
}
} catch (e) {
// 解析失败时返回默认值
return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }];
}
}
else if (nodeConfig.component?.type === 'SWITCH') {
// 从customDef中获取apiOutIds数组
try {
const customDef = JSON.parse(nodeConfig.component?.customDef || '{}');
const apiOutIds = customDef.apiOutIds || [];
// 从"break"开始的所有项都应该作为apiOut返回
const breakIndex = apiOutIds.indexOf('default');
if (breakIndex !== -1) {
// 返回从"break"开始的所有项
return apiOutIds.slice(breakIndex).map(id => ({
name: id,
id: id,
desc: id,
dataType: '',
defaultValue: ''
}));
}
else {
// 如果没有找到"break",则返回默认值
return [{ name: 'default', desc: '', dataType: '', defaultValue: '' }];
}
} catch (e) {
// 解析失败时返回默认值
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
}
}
else if (nodeId === 'start') {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
}
else if (nodeId === 'end') {
return [];
}
else {
const comp = currentProjectCompData.filter(item => item.id === nodeConfig?.component?.compId);
if (comp && comp.length > 0) {
return [{
...comp[0].def?.apiOut,
dataType: '',
defaultValue: ''
}];
}
else {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
}
}
};
// 获取节点的API输出句柄名称
const getNodeApiOutHandle = (nodeId: string, nodeConfig: any) => {
if (nodeConfig.component?.type === 'LOOP_START') {
return 'done';
}
else if (nodeConfig.component?.type === 'LOOP_END') {
return 'break';
}
else if (nodeId === 'start') {
return 'start';
}
else if (nodeId === 'end') {
return 'end';
}
return 'done';
};
// 获取当前工程下组件列表并扁平化处理
const getCurrentProjectStoreData = () => {
const { info, projectComponentData } = store.getState().ideContainer;
const compData = projectComponentData[info?.id] || {};
const result: any[] = [];
// 处理projectCompDto中的数据
if (compData.projectCompDto) {
const { mineComp = [], pubComp = [], teamWorkComp = [] } = compData.projectCompDto;
// 添加mineComp数据
mineComp.forEach((item: any) => {
result.push({
...item,
type: 'mineComp'
});
});
// 添加pubComp数据
pubComp.forEach((item: any) => {
result.push({
...item,
type: 'pubComp'
});
});
// 添加teamWorkComp数据
teamWorkComp.forEach((item: any) => {
result.push({
...item,
type: 'teamWorkComp'
});
});
}
// 处理projectFlowDto中的数据
if (compData.projectFlowDto) {
const { mineFlow = [], pubFlow = [] } = compData.projectFlowDto;
// 添加mineFlow数据
mineFlow.forEach((item: any) => {
result.push({
...item,
type: 'mineFlow'
});
});
// 添加pubFlow数据
pubFlow.forEach((item: any) => {
result.push({
...item,
type: 'pubFlow'
});
});
}
return result;
};
// 根据节点类型获取对应的节点组件
const getNodeComponent = (nodeType: string) => {
switch (nodeType) {
case 'BASIC':
case 'SUB':
return BasicNode;
case 'SWITCH':
return SwitchNode;
case 'IMAGE':
return ImageNode;
case 'CODE':
return CodeNode;
case 'REST':
return RestNode;
default:
return LocalNode;
}
};

@ -0,0 +1,104 @@
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
// 获取handle类型 (api或data)
const getHandleType = (handleId: string, nodeParams: any) => {
// 检查是否为api类型的handle
const apiOuts = nodeParams.apiOuts || [];
const apiIns = nodeParams.apiIns || [];
if (apiOuts.some((api: any) => (api.name || api.id) === handleId) ||
apiIns.some((api: any) => (api.name || api.id) === handleId) || (handleId.includes('loop'))) {
return 'api';
}
// 检查是否为data类型的handle
const dataOuts = nodeParams.dataOuts || [];
const dataIns = nodeParams.dataIns || [];
if (dataOuts.some((data: any) => (data.name || data.id) === handleId) ||
dataIns.some((data: any) => (data.name || data.id) === handleId)) {
return 'data';
}
// 默认为data类型
return 'data';
};
// 验证数据类型是否匹配
const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeTypes, sourceHandleId: string, targetHandleId: string) => {
const sourceParams = sourceNode.data?.parameters || {};
const targetParams = targetNode.data?.parameters || {};
// 获取源节点的输出参数
let sourceDataType = '';
const sourceApiOuts = sourceParams.apiOuts || [];
const sourceDataOuts = sourceParams.dataOuts || [];
// 查找源handle的数据类型
const sourceApi = sourceApiOuts.find((api: any) => api.name === sourceHandleId);
const sourceData = sourceDataOuts.find((data: any) => data.name === sourceHandleId);
if (sourceApi) {
sourceDataType = sourceApi.dataType || '';
}
else if (sourceData) {
sourceDataType = sourceData.dataType || '';
}
// 获取目标节点的输入参数
let targetDataType = '';
const targetApiIns = targetParams.apiIns || [];
const targetDataIns = targetParams.dataIns || [];
// 查找目标handle的数据类型
const targetApi = targetApiIns.find((api: any) => api.name === targetHandleId);
const targetData = targetDataIns.find((data: any) => data.name === targetHandleId);
if (targetApi) {
targetDataType = targetApi.dataType || '';
}
else if (targetData) {
targetDataType = targetData.dataType || '';
}
// 如果任一数据类型为空,则允许连接
if (!sourceDataType || !targetDataType) {
return true;
}
// 比较数据类型是否匹配
return sourceDataType === targetDataType;
};
// 根据节点类型获取对应的节点组件
const getNodeComponent = (nodeType: string) => {
switch (nodeType) {
case 'BASIC':
case 'SUB':
return BasicNode;
case 'SWITCH':
return SwitchNode;
case 'IMAGE':
return ImageNode;
case 'CODE':
return CodeNode;
case 'REST':
return RestNode;
default:
return LocalNode;
}
};
export {
getHandleType,
validateDataType,
getNodeComponent
};
Loading…
Cancel
Save