diff --git a/README.md b/README.md index c87e042..5c5d8f2 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/next.config.js b/next.config.js index be69c8a..043ac15 100644 --- a/next.config.js +++ b/next.config.js @@ -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; }, diff --git a/package.json b/package.json index 17c9030..88598b9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b079d57..ba27a2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/api/appEvent.ts b/src/api/appEvent.ts new file mode 100644 index 0000000..e22fb24 --- /dev/null +++ b/src/api/appEvent.ts @@ -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(`${urlPrefix}/appEvent/${id}`); +} + +// 获取工程下的所有应用事件 +export function getAppEventList(id: any) { + return axios.get(`${urlPrefix}/appEvent/${id}/list`); +} + +// 更新事件 +export function updateAppEvent(id: any, data: any) { + return axios.post(`${urlPrefix}/appEvent/${id}/update`, data); +} \ No newline at end of file diff --git a/src/api/appRes.ts b/src/api/appRes.ts index 58772ee..3781f29 100644 --- a/src/api/appRes.ts +++ b/src/api/appRes.ts @@ -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( diff --git a/src/api/event.ts b/src/api/event.ts index d0ce188..34db91a 100644 --- a/src/api/event.ts +++ b/src/api/event.ts @@ -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`); +} + diff --git a/src/components/FlowEditor/NodeStatusIndicator.tsx b/src/components/FlowEditor/NodeStatusIndicator.tsx new file mode 100644 index 0000000..0a95819 --- /dev/null +++ b/src/components/FlowEditor/NodeStatusIndicator.tsx @@ -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
; + case 'running': + return
; + case 'success': + return
; + case 'failed': + return
; + default: + return null; + } + }; + + return ( +
+ {getStatusIndicator()} +
+ ); +}; + +export default NodeStatusIndicator; \ No newline at end of file diff --git a/src/components/FlowEditor/node/appNode/AppNode.tsx b/src/components/FlowEditor/node/appNode/AppNode.tsx new file mode 100644 index 0000000..a5071d5 --- /dev/null +++ b/src/components/FlowEditor/node/appNode/AppNode.tsx @@ -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 ( +
+
+ {title} + +
+ +
+ ); +}; + +export default AppNode; \ No newline at end of file diff --git a/src/components/FlowEditor/node/basicNode/BasicNode.tsx b/src/components/FlowEditor/node/basicNode/BasicNode.tsx index 6e04c51..bbf834e 100644 --- a/src/components/FlowEditor/node/basicNode/BasicNode.tsx +++ b/src/components/FlowEditor/node/basicNode/BasicNode.tsx @@ -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 (
{title} +
- - {/**/}
); diff --git a/src/components/FlowEditor/node/codeNode/CodeNode.tsx b/src/components/FlowEditor/node/codeNode/CodeNode.tsx new file mode 100644 index 0000000..bb26cbf --- /dev/null +++ b/src/components/FlowEditor/node/codeNode/CodeNode.tsx @@ -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 ; +}; + +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 ( +
+
+ {setIcon()} + {title} + +
+ +
+ ); +}; + +export default CodeNode; \ No newline at end of file diff --git a/src/components/FlowEditor/node/endNode/EndNode.tsx b/src/components/FlowEditor/node/endNode/EndNode.tsx index 6eb7d3a..55d60f9 100644 --- a/src/components/FlowEditor/node/endNode/EndNode.tsx +++ b/src/components/FlowEditor/node/endNode/EndNode.tsx @@ -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 (
{title} +
- - {/**/}
); diff --git a/src/components/FlowEditor/node/imageNode/ImageNode.tsx b/src/components/FlowEditor/node/imageNode/ImageNode.tsx new file mode 100644 index 0000000..46fa3ea --- /dev/null +++ b/src/components/FlowEditor/node/imageNode/ImageNode.tsx @@ -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 ; +}; + +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 ( +
+
+ {setIcon()} + {title} + +
+ +
+ ); +}; + +export default ImageNode; \ No newline at end of file diff --git a/src/components/FlowEditor/node/index.tsx b/src/components/FlowEditor/node/index.tsx index df79e70..0d39d40 100644 --- a/src/components/FlowEditor/node/index.tsx +++ b/src/components/FlowEditor/node/index.tsx @@ -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 = { '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 = { 'start': '开始节点', 'end': '结束节点', - 'basic': '基础节点' + 'basic': '基础节点', + 'sub': '复合节点', + 'app': '应用节点', + 'code': '代码节点', + 'image': '图片节点', + 'rest': 'REST节点', + 'switch': '条件节点', + 'loop': '循环节点' }; // 注册新节点类型的函数 diff --git a/src/components/FlowEditor/node/localNode/LocalNode.tsx b/src/components/FlowEditor/node/localNode/LocalNode.tsx index 8d347bf..1356e72 100644 --- a/src/components/FlowEditor/node/localNode/LocalNode.tsx +++ b/src/components/FlowEditor/node/localNode/LocalNode.tsx @@ -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 (
{setIcon(data.type)} {title} +
- {/**/}
); diff --git a/src/components/FlowEditor/node/loopNode/LoopNode.tsx b/src/components/FlowEditor/node/loopNode/LoopNode.tsx new file mode 100644 index 0000000..476ae44 --- /dev/null +++ b/src/components/FlowEditor/node/loopNode/LoopNode.tsx @@ -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([]); + 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 ; + } + else { + return ; + } + }; + + 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 ( +
+
+ {setIcon()} + {title} + +
+ + {/* 顶部连接点,用于标识循环开始和结束节点是一组 */} + + + +
+ ); +}; + +export default LoopNode; \ No newline at end of file diff --git a/src/components/FlowEditor/node/restNode/RestNode.tsx b/src/components/FlowEditor/node/restNode/RestNode.tsx new file mode 100644 index 0000000..5323bda --- /dev/null +++ b/src/components/FlowEditor/node/restNode/RestNode.tsx @@ -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 ; +}; + +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 ( +
+
+ {setIcon()} + {title} + +
+ +
+ ); +}; + +export default RestNode; \ No newline at end of file diff --git a/src/components/FlowEditor/node/startNode/StartNode.tsx b/src/components/FlowEditor/node/startNode/StartNode.tsx index cbc76ef..97417b9 100644 --- a/src/components/FlowEditor/node/startNode/StartNode.tsx +++ b/src/components/FlowEditor/node/startNode/StartNode.tsx @@ -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 (
{title} +
- - {/**/}
); diff --git a/src/components/FlowEditor/node/style/baseOther.module.less b/src/components/FlowEditor/node/style/baseOther.module.less index 0c64bcc..7732144 100644 --- a/src/components/FlowEditor/node/style/baseOther.module.less +++ b/src/components/FlowEditor/node/style/baseOther.module.less @@ -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); + } } \ No newline at end of file diff --git a/src/components/FlowEditor/node/switchNode/SwitchNode.tsx b/src/components/FlowEditor/node/switchNode/SwitchNode.tsx new file mode 100644 index 0000000..888cf8f --- /dev/null +++ b/src/components/FlowEditor/node/switchNode/SwitchNode.tsx @@ -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([]); + 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 ; + }; + + 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 ( +
+
+ {setIcon()} + {title} + +
+ + +
+ ); +}; + +export default SwitchNode; \ No newline at end of file diff --git a/src/components/FlowEditor/nodeEditors/BasicNodeEditor.tsx b/src/components/FlowEditor/nodeEditors/BasicNodeEditor.tsx index 126e45f..487b58e 100644 --- a/src/components/FlowEditor/nodeEditors/BasicNodeEditor.tsx +++ b/src/components/FlowEditor/nodeEditors/BasicNodeEditor.tsx @@ -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 = ({ - 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 (
@@ -21,17 +107,53 @@ const BasicNodeEditor: React.FC = ({ onChange={(value) => updateNodeData('description', value)} /> + 组件信息 +
+ 组件类型: +
{currentCompInfo?.type}
+
+
+ 组件名称: +
{currentCompInfo?.name}
+
+
+ 组件描述: +
{currentCompInfo?.description}
+
{ + setLanguage(value); + setValue(defaultCode[value]); + }} + > + {options.map((option, index) => ( + + ))} + + + + ); +}; + +export default CodeMirrorComp; \ No newline at end of file diff --git a/src/components/FlowEditor/nodeEditors/components/ConditionEditor.tsx b/src/components/FlowEditor/nodeEditors/components/ConditionEditor.tsx deleted file mode 100644 index 02e8f7d..0000000 --- a/src/components/FlowEditor/nodeEditors/components/ConditionEditor.tsx +++ /dev/null @@ -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 = ({ nodeData, updateNodeData }) => { - return ( - <> - 输入参数 - { - updateNodeData('parameters', { - ...nodeData.parameters, - dataIns: data - }); - }} - /> - - ); -}; - -export default ConditionEditor; \ No newline at end of file diff --git a/src/components/FlowEditor/nodeEditors/components/ConditionsTable.tsx b/src/components/FlowEditor/nodeEditors/components/ConditionsTable.tsx new file mode 100644 index 0000000..af646ae --- /dev/null +++ b/src/components/FlowEditor/nodeEditors/components/ConditionsTable.tsx @@ -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 = ({ + initialData, + nodeData, + onUpdateData, + type = 'LOOP' + }) => { + const [data, setData] = useState([]); + const [rowData, setRowData] = useState({}); + const [apiOutsList, setApiOutsList] = useState([]); + const [leftList, setLeftList] = useState([]); + + const columns = [ + { + title: '序号', + dataIndex: 'index', + render: (_: any, record: TableDataItem, i) => ( + {i + 1} + ) + }, + { + title: '逻辑出口', + dataIndex: 'apiOutId', + render: (_: any, record: TableDataItem) => ( + { + // 仅更新本地状态,不立即触发保存 + 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) => ( + { + // 仅更新本地状态,不立即触发保存 + 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) => ( +
+ { + // 仅更新本地状态,不立即触发保存 + 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={'请输入'} + /> + ) : ()} +
+ ) + }, + { + title: '操作', + dataIndex: 'op', + render: (_: any, record: TableDataItem) => ( + + ) + } + ]; + + 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 ( + <> + + + + ); +}; + +export default ConditionsTable; \ No newline at end of file diff --git a/src/components/FlowEditor/nodeEditors/components/EventListenEditor.tsx b/src/components/FlowEditor/nodeEditors/components/EventListenEditor.tsx index ac880ea..01a9a20 100644 --- a/src/components/FlowEditor/nodeEditors/components/EventListenEditor.tsx +++ b/src/components/FlowEditor/nodeEditors/components/EventListenEditor.tsx @@ -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 = ({ nodeData, updateNodeData }) => { - const [eventList, setEventList] = useState(tempEventList); + const [eventList, setEventList] = useState(); + const { currentAppData } = useSelector(state => state.ideContainer); + + const getEventList = async () => { + const res = await queryEventItemBySceneId(currentAppData.sceneId); + setEventList(res.data); + }; + + useEffect(() => { + getEventList(); + }, []); return ( <> + + + updateNodeData('title', value)} + /> + + 事件选择 { updateNodeData('component', { ...data }); - }}> + }} /> + 输出参数 + { + updateNodeData('parameters', { + ...nodeData.parameters, + dataOuts: data + }); + }} + /> ); }; diff --git a/src/components/FlowEditor/nodeEditors/components/EventSelect.tsx b/src/components/FlowEditor/nodeEditors/components/EventSelect.tsx index 873bfe1..1c3a1f2 100644 --- a/src/components/FlowEditor/nodeEditors/components/EventSelect.tsx +++ b/src/components/FlowEditor/nodeEditors/components/EventSelect.tsx @@ -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 = ({ eventList, type, onUpdateData }) => { +const EventSelect: React.FC = ({ nodeData, eventList, type, onRefresh, onUpdateData }) => { const [options, setOptions] = useState([]); + const [specialOptions, setSpecialOptions] = useState({}); const [form] = Form.useForm(); const [showModal, setShowModal] = useState(false); + const [currentEvent, setCurrentEvent] = useState(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 = ({ 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 = ({ 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 = ({ eventList, type, onUpdateData return ( <>