2 Commits

Author SHA1 Message Date
v-zhangjc9
04bc9a2c16 feat(web): 完成循环节点的基本配置 2025-07-14 19:10:58 +08:00
v-zhangjc9
c77395fec4 feat(web): 升级部份依赖 2025-07-14 12:16:13 +08:00
17 changed files with 237 additions and 85 deletions

View File

@@ -16,15 +16,15 @@
"@echofly/fetch-event-source": "^3.0.2", "@echofly/fetch-event-source": "^3.0.2",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@lightenna/react-mermaid-diagram": "^1.0.21", "@lightenna/react-mermaid-diagram": "^1.0.21",
"@xyflow/react": "^12.8.1", "@xyflow/react": "^12.8.2",
"ahooks": "^3.9.0", "ahooks": "^3.9.0",
"amis": "^6.12.0", "amis": "^6.12.0",
"antd": "^5.26.3", "antd": "^5.26.4",
"axios": "^1.10.0", "axios": "^1.10.0",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"licia": "^1.48.0", "licia": "^1.48.0",
"mermaid": "^11.8.0", "mermaid": "^11.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -41,7 +41,7 @@
"globals": "^16.3.0", "globals": "^16.3.0",
"sass": "^1.89.2", "sass": "^1.89.2",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"vite": "^7.0.2", "vite": "^7.0.4",
"vite-plugin-javascript-obfuscator": "^3.1.0", "vite-plugin-javascript-obfuscator": "^3.1.0",
"vitest": "^3.2.4" "vitest": "^3.2.4"
} }

View File

@@ -25,10 +25,10 @@ importers:
version: 6.7.2 version: 6.7.2
'@lightenna/react-mermaid-diagram': '@lightenna/react-mermaid-diagram':
specifier: ^1.0.21 specifier: ^1.0.21
version: 1.0.21(mermaid@11.8.0)(react@18.3.1) version: 1.0.21(mermaid@11.8.1)(react@18.3.1)
'@xyflow/react': '@xyflow/react':
specifier: ^12.8.1 specifier: ^12.8.2
version: 12.8.1(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 12.8.2(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ahooks: ahooks:
specifier: ^3.9.0 specifier: ^3.9.0
version: 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -36,7 +36,7 @@ importers:
specifier: ^6.12.0 specifier: ^6.12.0
version: 6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-ui@6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(office-viewer@0.3.14(echarts@5.5.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-ui@6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(office-viewer@0.3.14(echarts@5.5.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd: antd:
specifier: ^5.26.3 specifier: ^5.26.4
version: 5.26.4(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 5.26.4(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
axios: axios:
specifier: ^1.10.0 specifier: ^1.10.0
@@ -51,8 +51,8 @@ importers:
specifier: ^1.48.0 specifier: ^1.48.0
version: 1.48.0 version: 1.48.0
mermaid: mermaid:
specifier: ^11.8.0 specifier: ^11.8.1
version: 11.8.0 version: 11.8.1
react: react:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1 version: 18.3.1
@@ -86,7 +86,7 @@ importers:
version: 18.3.7(@types/react@18.3.23) version: 18.3.7(@types/react@18.3.23)
'@vitejs/plugin-react-swc': '@vitejs/plugin-react-swc':
specifier: ^3.10.2 specifier: ^3.10.2
version: 3.10.2(vite@7.0.2(sass@1.89.2)) version: 3.10.2(vite@7.0.4(sass@1.89.2))
globals: globals:
specifier: ^16.3.0 specifier: ^16.3.0
version: 16.3.0 version: 16.3.0
@@ -97,8 +97,8 @@ importers:
specifier: ~5.8.3 specifier: ~5.8.3
version: 5.8.3 version: 5.8.3
vite: vite:
specifier: ^7.0.2 specifier: ^7.0.4
version: 7.0.2(sass@1.89.2) version: 7.0.4(sass@1.89.2)
vite-plugin-javascript-obfuscator: vite-plugin-javascript-obfuscator:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0 version: 3.1.0
@@ -518,8 +518,8 @@ packages:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true hasBin: true
'@mermaid-js/parser@0.6.0': '@mermaid-js/parser@0.6.1':
resolution: {integrity: sha512-7DNESgpyZ5WG1SIkrYafVBhWmImtmQuoxOO1lawI3gQYWxBX3v1FW3IyuuRfKJAO06XrZR71W0Kif5VEGGd4VA==} resolution: {integrity: sha512-lCQNpV8R4lgsGcjX5667UiuDLk2micCtjtxR1YKbBXvN5w2v+FeLYoHrTSSrjwXdMcDYvE4ZBPvKT31dfeSmmA==}
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
@@ -1071,14 +1071,14 @@ packages:
'@vitest/utils@3.2.4': '@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
'@xyflow/react@12.8.1': '@xyflow/react@12.8.2':
resolution: {integrity: sha512-t5Rame4Gc/540VcOZd28yFe9Xd8lyjKUX+VTiyb1x4ykNXZH5zyDmsu+lj9je2O/jGBVb0pj1Vjcxrxyn+Xk2g==} resolution: {integrity: sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==}
peerDependencies: peerDependencies:
react: '>=17' react: '>=17'
react-dom: '>=17' react-dom: '>=17'
'@xyflow/system@0.0.65': '@xyflow/system@0.0.66':
resolution: {integrity: sha512-AliQPQeurQMoNlOdySnRoDQl9yDSA/1Lqi47Eo0m98lHcfrTdD9jK75H0tiGj+0qRC10SKNUXyMkT0KL0opg4g==} resolution: {integrity: sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==}
abbrev@1.1.1: abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
@@ -2324,8 +2324,8 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
mermaid@11.8.0: mermaid@11.8.1:
resolution: {integrity: sha512-uAZUwnBiqREZcUrFw3G5iQ5Pj3hTYUP95EZc3ec/nGBzHddJZydzYGE09tGZDBS1VoSoDn0symZ85FmypSTo5g==} resolution: {integrity: sha512-VSXJLqP1Sqw5sGr273mhvpPRhXwE6NlmMSqBZQw+yZJoAJkOIPPn/uT3teeCBx60Fkt5zEI3FrH2eVT0jXRDzw==}
micromark-core-commonmark@2.0.3: micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -3485,8 +3485,8 @@ packages:
vite-plugin-javascript-obfuscator@3.1.0: vite-plugin-javascript-obfuscator@3.1.0:
resolution: {integrity: sha512-sf4JFlG1iUPl7bLXHGOy+bKWOQUFyXzJFWa+n2S2xMMvyfM+V9R40HhpZoIF1eAjifArM1SF7fbSFIaTuUIbPA==} resolution: {integrity: sha512-sf4JFlG1iUPl7bLXHGOy+bKWOQUFyXzJFWa+n2S2xMMvyfM+V9R40HhpZoIF1eAjifArM1SF7fbSFIaTuUIbPA==}
vite@7.0.2: vite@7.0.4:
resolution: {integrity: sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==} resolution: {integrity: sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -4157,9 +4157,9 @@ snapshots:
'@kurkle/color@0.3.4': {} '@kurkle/color@0.3.4': {}
'@lightenna/react-mermaid-diagram@1.0.21(mermaid@11.8.0)(react@18.3.1)': '@lightenna/react-mermaid-diagram@1.0.21(mermaid@11.8.1)(react@18.3.1)':
dependencies: dependencies:
mermaid: 11.8.0 mermaid: 11.8.1
react: 18.3.1 react: 18.3.1
'@mapbox/node-pre-gyp@1.0.11': '@mapbox/node-pre-gyp@1.0.11':
@@ -4178,7 +4178,7 @@ snapshots:
- supports-color - supports-color
optional: true optional: true
'@mermaid-js/parser@0.6.0': '@mermaid-js/parser@0.6.1':
dependencies: dependencies:
langium: 3.3.1 langium: 3.3.1
@@ -4620,11 +4620,11 @@ snapshots:
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react-swc@3.10.2(vite@7.0.2(sass@1.89.2))': '@vitejs/plugin-react-swc@3.10.2(vite@7.0.4(sass@1.89.2))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.11 '@rolldown/pluginutils': 1.0.0-beta.11
'@swc/core': 1.12.9 '@swc/core': 1.12.9
vite: 7.0.2(sass@1.89.2) vite: 7.0.4(sass@1.89.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@swc/helpers' - '@swc/helpers'
@@ -4636,13 +4636,13 @@ snapshots:
chai: 5.2.0 chai: 5.2.0
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.0.2(sass@1.89.2))': '@vitest/mocker@3.2.4(vite@7.0.4(sass@1.89.2))':
dependencies: dependencies:
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.17 magic-string: 0.30.17
optionalDependencies: optionalDependencies:
vite: 7.0.2(sass@1.89.2) vite: 7.0.4(sass@1.89.2)
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -4670,9 +4670,9 @@ snapshots:
loupe: 3.1.4 loupe: 3.1.4
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@xyflow/react@12.8.1(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@xyflow/react@12.8.2(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@xyflow/system': 0.0.65 '@xyflow/system': 0.0.66
classcat: 5.0.5 classcat: 5.0.5
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
@@ -4681,7 +4681,7 @@ snapshots:
- '@types/react' - '@types/react'
- immer - immer
'@xyflow/system@0.0.65': '@xyflow/system@0.0.66':
dependencies: dependencies:
'@types/d3-drag': 3.0.7 '@types/d3-drag': 3.0.7
'@types/d3-interpolate': 3.0.4 '@types/d3-interpolate': 3.0.4
@@ -6235,11 +6235,11 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.23 '@types/react': 18.3.23
mermaid@11.8.0: mermaid@11.8.1:
dependencies: dependencies:
'@braintree/sanitize-url': 7.1.1 '@braintree/sanitize-url': 7.1.1
'@iconify/utils': 2.3.0 '@iconify/utils': 2.3.0
'@mermaid-js/parser': 0.6.0 '@mermaid-js/parser': 0.6.1
'@types/d3': 7.4.3 '@types/d3': 7.4.3
cytoscape: 3.32.0 cytoscape: 3.32.0
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.32.0) cytoscape-cose-bilkent: 4.1.0(cytoscape@3.32.0)
@@ -6744,7 +6744,7 @@ snapshots:
dependencies: dependencies:
'@babel/runtime': 7.27.6 '@babel/runtime': 7.27.6
'@rc-component/mini-decimal': 1.1.0 '@rc-component/mini-decimal': 1.1.0
classnames: 2.3.2 classnames: 2.5.1
rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
@@ -6841,7 +6841,7 @@ snapshots:
rc-progress@3.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): rc-progress@3.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.27.6 '@babel/runtime': 7.27.6
classnames: 2.3.2 classnames: 2.5.1
rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
@@ -7671,7 +7671,7 @@ snapshots:
debug: 4.4.1 debug: 4.4.1
es-module-lexer: 1.7.0 es-module-lexer: 1.7.0
pathe: 2.0.3 pathe: 2.0.3
vite: 7.0.2(sass@1.89.2) vite: 7.0.4(sass@1.89.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- jiti - jiti
@@ -7691,7 +7691,7 @@ snapshots:
anymatch: 3.1.3 anymatch: 3.1.3
javascript-obfuscator: 4.1.1 javascript-obfuscator: 4.1.1
vite@7.0.2(sass@1.89.2): vite@7.0.4(sass@1.89.2):
dependencies: dependencies:
esbuild: 0.25.5 esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2) fdir: 6.4.6(picomatch@4.0.2)
@@ -7707,7 +7707,7 @@ snapshots:
dependencies: dependencies:
'@types/chai': 5.2.2 '@types/chai': 5.2.2
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.0.2(sass@1.89.2)) '@vitest/mocker': 3.2.4(vite@7.0.4(sass@1.89.2))
'@vitest/pretty-format': 3.2.4 '@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4 '@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4 '@vitest/snapshot': 3.2.4
@@ -7725,7 +7725,7 @@ snapshots:
tinyglobby: 0.2.14 tinyglobby: 0.2.14
tinypool: 1.1.1 tinypool: 1.1.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vite: 7.0.2(sass@1.89.2) vite: 7.0.4(sass@1.89.2)
vite-node: 3.2.4(sass@1.89.2) vite-node: 3.2.4(sass@1.89.2)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:

View File

@@ -28,6 +28,7 @@ export const sourceNodeNotFoundError = () => new CheckError(200, '连线起始
export const targetNodeNotFoundError = () => new CheckError(201, '连线目标节点未找到') export const targetNodeNotFoundError = () => new CheckError(201, '连线目标节点未找到')
export const nodeToSelfError = () => new CheckError(203, '节点不能直连自身') export const nodeToSelfError = () => new CheckError(203, '节点不能直连自身')
export const hasCycleError = () => new CheckError(204, '禁止流程循环') export const hasCycleError = () => new CheckError(204, '禁止流程循环')
export const differentParent = () => new CheckError(205, '子流程禁止连接外部节点')
const hasCycle = (sourceNode: Node, targetNode: Node, nodes: Node[], edges: Edge[], visited = new Set<string>()) => { const hasCycle = (sourceNode: Node, targetNode: Node, nodes: Node[], edges: Edge[], visited = new Set<string>()) => {
if (visited.has(targetNode.id)) return false if (visited.has(targetNode.id)) return false
@@ -48,6 +49,10 @@ export const checkAddConnection: (connection: Connection, nodes: Node[], edges:
throw targetNodeNotFoundError() throw targetNodeNotFoundError()
} }
if (!isEqual(sourceNode.parentId, targetNode.parentId)) {
throw differentParent()
}
// 禁止流程出现环,必须是有向无环图 // 禁止流程出现环,必须是有向无环图
if (isEqual(sourceNode.id, targetNode.id)) { if (isEqual(sourceNode.id, targetNode.id)) {
throw nodeToSelfError() throw nodeToSelfError()

View File

@@ -8,11 +8,12 @@ import styled from 'styled-components'
import '@xyflow/react/dist/style.css' import '@xyflow/react/dist/style.css'
import {commonInfo} from '../../util/amis.tsx' import {commonInfo} from '../../util/amis.tsx'
import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx' import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx'
import {useNodeDrag} from './Helper.tsx'
import {NodeRegistry, NodeRegistryMap} from './NodeRegistry.tsx' import {NodeRegistry, NodeRegistryMap} from './NodeRegistry.tsx'
import {useContextStore} from './store/ContextStore.ts' import {useContextStore} from './store/ContextStore.ts'
import {useDataStore} from './store/DataStore.ts' import {useDataStore} from './store/DataStore.ts'
import {useFlowStore} from './store/FlowStore.ts' import {useFlowStore} from './store/FlowStore.ts'
import type {FlowEditorProps} from './types.ts' import {flowDotColor, type FlowEditorProps} from './types.ts'
const FlowableDiv = styled.div` const FlowableDiv = styled.div`
.react-flow__node.selectable { .react-flow__node.selectable {
@@ -75,10 +76,17 @@ function FlowEditor(props: FlowEditorProps) {
setInputSchema(props.inputSchema) setInputSchema(props.inputSchema)
}, [props.graphData]) }, [props.graphData])
const {
onNodeDragStart,
onNodeDrag,
onNodeDragEnd,
} = useNodeDrag([props.graphData])
return ( return (
<FlowableDiv className="h-full w-full"> <FlowableDiv className="h-full w-full">
{contextHolder} {contextHolder}
<ReactFlow <ReactFlow
className="rounded-xl"
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
@@ -97,6 +105,9 @@ function FlowEditor(props: FlowEditorProps) {
}} }}
// @ts-ignore // @ts-ignore
nodeTypes={arrToMap(Object.keys(NodeRegistryMap), key => NodeRegistryMap[key]!.component)} nodeTypes={arrToMap(Object.keys(NodeRegistryMap), key => NodeRegistryMap[key]!.component)}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragEnd}
> >
<Panel position="top-right"> <Panel position="top-right">
<Space className="toolbar"> <Space className="toolbar">
@@ -176,7 +187,12 @@ function FlowEditor(props: FlowEditorProps) {
</Panel> </Panel>
<Controls/> <Controls/>
<MiniMap/> <MiniMap/>
<Background variant={BackgroundVariant.Cross} gap={20} size={3}/> <Background
variant={BackgroundVariant.Cross}
gap={20}
size={3}
color={flowDotColor}
/>
</ReactFlow> </ReactFlow>
</FlowableDiv> </FlowableDiv>
) )

View File

@@ -1,7 +1,9 @@
import {type Edge, getIncomers, type Node} from '@xyflow/react' import {type Edge, getIncomers, type Node} from '@xyflow/react'
import type {Option} from 'amis/lib/Schema' import type {Option} from 'amis/lib/Schema'
import {find, has, isEmpty, isEqual, unique} from 'licia' import {find, has, isEmpty, isEqual, max, min, unique} from 'licia'
import {type DependencyList, type MouseEvent as ReactMouseEvent, useCallback, useRef} from 'react'
import Queue from 'yocto-queue' import Queue from 'yocto-queue'
import {useFlowStore} from './store/FlowStore.ts'
import type {InputFormOptions, InputFormOptionsGroup} from './types.ts' import type {InputFormOptions, InputFormOptionsGroup} from './types.ts'
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => { export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
@@ -82,3 +84,43 @@ export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSch
})), })),
] ]
} }
// 处理循环节点的边界问题
export const useNodeDrag = (deps: DependencyList) => {
const currentPosition = useRef({x: 0, y: 0} as { x: number, y: number })
const {setNode, getNodeById} = useFlowStore()
const onNodeDragStart = useCallback(() => {
}, deps)
const onNodeDrag = useCallback((event: ReactMouseEvent, node: Node) => {
event.stopPropagation()
if (node.parentId) {
let parentNode = getNodeById(node.parentId)
if (parentNode) {
let newPosition = {
x: max(min(node.position.x, (parentNode.measured?.width ?? 0) - (node.measured?.width ?? 0) - 28), 28),
y: max(min(node.position.y, (parentNode.measured?.height ?? 0) - (node.measured?.height ?? 0) - 28), 90),
}
setNode({
...node,
position: newPosition,
})
currentPosition.current = newPosition
}
}
}, deps)
const onNodeDragEnd = useCallback((_event: ReactMouseEvent, node: Node) => {
if (node.parentId) {
setNode({
...node,
position: currentPosition.current,
})
}
}, deps)
return {
onNodeDragStart,
onNodeDrag,
onNodeDragEnd,
}
}

View File

@@ -1,13 +1,13 @@
import {has, isEmpty} from 'licia' import {has, isEmpty} from 'licia'
import type {JSX} from 'react'
import {getAllIncomerNodeOutputVariables} from './Helper.tsx' import {getAllIncomerNodeOutputVariables} from './Helper.tsx'
import CodeNode from './node/CodeNode.tsx' import CodeNode from './node/CodeNode.tsx'
import KnowledgeNode from './node/KnowledgeNode.tsx' import KnowledgeNode from './node/KnowledgeNode.tsx'
import LlmNode from './node/LlmNode.tsx' import LlmNode from './node/LlmNode.tsx'
import LoopNode from './node/LoopNode.tsx'
import OutputNode from './node/OutputNode.tsx' import OutputNode from './node/OutputNode.tsx'
import SwitchNode from './node/SwitchNode.tsx' import SwitchNode from './node/SwitchNode.tsx'
import TemplateNode from './node/TemplateNode.tsx' import TemplateNode from './node/TemplateNode.tsx'
import type {NodeChecker} from './types.ts' import type {NodeChecker, NodeDefine} from './types.ts'
const inputSingleVariableChecker: (field: string) => NodeChecker = field => { const inputSingleVariableChecker: (field: string) => NodeChecker = field => {
return (id, inputSchema, nodes, edges, data) => { return (id, inputSchema, nodes, edges, data) => {
@@ -54,16 +54,6 @@ const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, d
return {error: false} return {error: false}
} }
type NodeDefine = {
key: string,
group: string,
name: string,
icon: JSX.Element,
description: string,
component: any,
checkers: NodeChecker[],
}
export const NodeRegistry: NodeDefine[] = [ export const NodeRegistry: NodeDefine[] = [
{ {
key: 'llm-node', key: 'llm-node',
@@ -110,6 +100,15 @@ export const NodeRegistry: NodeDefine[] = [
component: SwitchNode, component: SwitchNode,
checkers: [], checkers: [],
}, },
{
key: 'loop-node',
group: '逻辑节点',
name: '循环',
icon: <i className="fa fa-repeat"/>,
description: '实现循环执行流程',
component: LoopNode,
checkers: [],
},
{ {
key: 'output-node', key: 'output-node',
group: '输出节点', group: '输出节点',

View File

@@ -1,8 +1,8 @@
import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons' import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons'
import {type Edge, Handle, type Node, type NodeProps, NodeToolbar, Position} from '@xyflow/react' import {type Edge, Handle, type Node, type NodeProps, NodeResizeControl, NodeToolbar, Position} from '@xyflow/react'
import type {Schema} from 'amis' import {type ClassName, classnames, type Schema} from 'amis'
import {Button, Card, Drawer, Space, Tooltip} from 'antd' import {Button, Drawer, Space, Tooltip} from 'antd'
import {type JSX, useCallback, useState} from 'react' import {type CSSProperties, type JSX, useCallback, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx' import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx'
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
@@ -95,20 +95,16 @@ export function outputsFormColumns(editable: boolean = false, required: boolean
} }
type AmisNodeProps = { type AmisNodeProps = {
className: ClassName,
style?: CSSProperties,
nodeProps: NodeProps nodeProps: NodeProps
extraNodeDescription?: JSX.Element extraNodeDescription?: JSX.Element
handler: JSX.Element handler: JSX.Element
columnSchema?: () => Schema[] columnSchema?: () => Schema[]
resize?: { minWidth: number, minHeight: number }
} }
const AmisNodeContainerDiv = styled.div` const AmisNodeContainerDiv = styled.div`
.ant-card {
.ant-card-actions {
& > li {
margin: 0;
}
}
}
` `
export const StartNodeHandler = () => { export const StartNodeHandler = () => {
@@ -128,11 +124,18 @@ export const NormalNodeHandler = () => {
) )
} }
export const nodeClassName = (name: string) => {
return `flow-node flow-node-${name}`
}
const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
className,
style,
nodeProps, nodeProps,
extraNodeDescription, extraNodeDescription,
handler, handler,
columnSchema, columnSchema,
resize,
}) => { }) => {
const {removeNode} = useFlowStore() const {removeNode} = useFlowStore()
const {getDataById, setDataById, removeDataById} = useDataStore() const {getDataById, setDataById, removeDataById} = useDataStore()
@@ -236,7 +239,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
removeDataById(id) removeDataById(id)
}, []) }, [])
return ( return (
<AmisNodeContainerDiv className="w-64"> <AmisNodeContainerDiv className={classnames(className, 'w-64')} style={style}>
<Drawer <Drawer
title="节点编辑" title="节点编辑"
open={editDrawerOpen} open={editDrawerOpen}
@@ -278,17 +281,27 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
</Tooltip> </Tooltip>
</Space> </Space>
</NodeToolbar> </NodeToolbar>
<Card <div className="node-card h-full flex flex-col bg-white rounded-md border border-gray-100 border-solid">
className="node-card" <div
title={nodeName} className="node-card-header items-center flex justify-between p-2 border-t-0 border-l-0 border-r-0 border-b border-gray-100 border-solid">
extra={<span className="text-gray-300 text-xs">{id}</span>} <span className="font-bold">{nodeName}</span>
size="small" <span className="text-gray-300 text-sm">{id}</span>
> </div>
<div className="card-description p-2 text-secondary text-sm"> <div className="node-card-description flex flex-col flex-1 p-2 text-secondary text-sm">
<div className="node-card-description-node">
{nodeDescription} {nodeDescription}
</div>
<div className="node-card-description-extra flex-1 mt-1">
{extraNodeDescription} {extraNodeDescription}
</div> </div>
</Card> </div>
</div>
{resize ? <>
<NodeResizeControl
minWidth={resize.minWidth}
minHeight={resize.minHeight}
/>
</> : undefined}
{handler} {handler}
</AmisNodeContainerDiv> </AmisNodeContainerDiv>
) )

View File

@@ -4,7 +4,7 @@ import React, {useCallback, useEffect} from 'react'
import {useContextStore} from '../store/ContextStore.ts' import {useContextStore} from '../store/ContextStore.ts'
import {useDataStore} from '../store/DataStore.ts' import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts' import {useFlowStore} from '../store/FlowStore.ts'
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
const languageMap: Record<string, string> = { const languageMap: Record<string, string> = {
'javascript': 'Javascript', 'javascript': 'Javascript',
@@ -62,6 +62,7 @@ const CodeNode = (props: NodeProps) => {
], [props.id]) ], [props.id])
return ( return (
<AmisNode <AmisNode
className={nodeClassName('code')}
nodeProps={props} nodeProps={props}
extraNodeDescription={ extraNodeDescription={
nodeData?.type nodeData?.type

View File

@@ -4,7 +4,7 @@ import {commonInfo} from '../../../util/amis.tsx'
import {useContextStore} from '../store/ContextStore.ts' import {useContextStore} from '../store/ContextStore.ts'
import {useDataStore} from '../store/DataStore.ts' import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts' import {useFlowStore} from '../store/FlowStore.ts'
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
const KnowledgeNode = (props: NodeProps) => { const KnowledgeNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
@@ -79,6 +79,7 @@ const KnowledgeNode = (props: NodeProps) => {
], [props.id]) ], [props.id])
return ( return (
<AmisNode <AmisNode
className={nodeClassName('knowledge')}
nodeProps={props} nodeProps={props}
columnSchema={columnsSchema} columnSchema={columnsSchema}
handler={<NormalNodeHandler/>} handler={<NormalNodeHandler/>}

View File

@@ -4,7 +4,7 @@ import React, {useCallback, useEffect} from 'react'
import {useContextStore} from '../store/ContextStore.ts' import {useContextStore} from '../store/ContextStore.ts'
import {useDataStore} from '../store/DataStore.ts' import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts' import {useFlowStore} from '../store/FlowStore.ts'
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
const modelMap: Record<string, string> = { const modelMap: Record<string, string> = {
qwen3: 'Qwen3', qwen3: 'Qwen3',
@@ -57,6 +57,7 @@ const LlmNode = (props: NodeProps) => {
], [props.id]) ], [props.id])
return ( return (
<AmisNode <AmisNode
className={nodeClassName('llm')}
nodeProps={props} nodeProps={props}
extraNodeDescription={ extraNodeDescription={
nodeData?.model nodeData?.model

View File

@@ -0,0 +1,46 @@
import {Background, BackgroundVariant, type NodeProps} from '@xyflow/react'
import {classnames} from 'amis'
import React from 'react'
import {flowBackgroundColor, flowDotColor} from '../types.ts'
import AmisNode, {nodeClassName, NormalNodeHandler} from './AmisNode.tsx'
const LoopNode = (props: NodeProps) => {
return (
<AmisNode
className={classnames('w-full', 'h-full', nodeClassName('loop'))}
style={{
minWidth: '256px',
minHeight: '110px'
}}
nodeProps={props}
extraNodeDescription={
<div
className="nodrag relative h-full w-full"
style={{
minHeight: '8rem',
}}
>
<Background
id={`loop-background-${props.id}`}
className="rounded-xl"
variant={BackgroundVariant.Cross}
gap={20}
size={3}
style={{
zIndex: 0,
}}
color={flowDotColor}
bgColor={flowBackgroundColor}
/>
</div>
}
handler={<NormalNodeHandler/>}
resize={{
minWidth: 256,
minHeight: 208,
}}
/>
)
}
export default React.memo(LoopNode)

View File

@@ -4,7 +4,7 @@ import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
import {useContextStore} from '../store/ContextStore.ts' import {useContextStore} from '../store/ContextStore.ts'
import {useDataStore} from '../store/DataStore.ts' import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts' import {useFlowStore} from '../store/FlowStore.ts'
import AmisNode, {EndNodeHandler} from './AmisNode.tsx' import AmisNode, {EndNodeHandler, nodeClassName} from './AmisNode.tsx'
const OutputNode = (props: NodeProps) => { const OutputNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore() const {getNodes, getEdges} = useFlowStore()
@@ -33,6 +33,7 @@ const OutputNode = (props: NodeProps) => {
return ( return (
<AmisNode <AmisNode
className={nodeClassName('output')}
nodeProps={props} nodeProps={props}
columnSchema={columnsSchema} columnSchema={columnsSchema}
handler={<EndNodeHandler/>} handler={<EndNodeHandler/>}

View File

@@ -1,7 +1,7 @@
import {Handle, type NodeProps, Position} from '@xyflow/react' import {Handle, type NodeProps, Position} from '@xyflow/react'
import {Tag} from 'antd' import {Tag} from 'antd'
import React from 'react' import React from 'react'
import AmisNode from './AmisNode.tsx' import AmisNode, {nodeClassName} from './AmisNode.tsx'
const cases = [ const cases = [
{ {
@@ -18,6 +18,7 @@ const cases = [
const SwitchNode = (props: NodeProps) => { const SwitchNode = (props: NodeProps) => {
return ( return (
<AmisNode <AmisNode
className={nodeClassName('switch')}
nodeProps={props} nodeProps={props}
extraNodeDescription={ extraNodeDescription={
<div className="mt-2"> <div className="mt-2">

View File

@@ -4,7 +4,7 @@ import React, {useCallback, useEffect} from 'react'
import {useContextStore} from '../store/ContextStore.ts' import {useContextStore} from '../store/ContextStore.ts'
import {useDataStore} from '../store/DataStore.ts' import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts' import {useFlowStore} from '../store/FlowStore.ts'
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx' import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
default: '默认', default: '默认',
@@ -75,6 +75,7 @@ const TemplateNode = (props: NodeProps) => {
return ( return (
<AmisNode <AmisNode
className={nodeClassName('template')}
nodeProps={props} nodeProps={props}
extraNodeDescription={ extraNodeDescription={
nodeData?.type nodeData?.type

View File

@@ -19,6 +19,7 @@ export const useFlowStore = create<{
addNode: (node: Node) => void, addNode: (node: Node) => void,
removeNode: (id: string) => void, removeNode: (id: string) => void,
setNodes: (nodes: Node[]) => void, setNodes: (nodes: Node[]) => void,
setNode: (node: Node) => void,
edges: Edge[], edges: Edge[],
getEdges: () => Edge[], getEdges: () => Edge[],
@@ -42,6 +43,16 @@ export const useFlowStore = create<{
}) })
}, },
setNodes: nodes => set({nodes}), setNodes: nodes => set({nodes}),
setNode: node => {
set({
nodes: get().nodes.map(n => {
if (isEqual(node.id, n.id)) {
return node
}
return n
}),
})
},
edges: [], edges: [],
getEdges: () => get().edges, getEdges: () => get().edges,

View File

@@ -1,4 +1,8 @@
import type {Edge, Node} from '@xyflow/react' import type {Edge, Node} from '@xyflow/react'
import type {JSX} from 'react'
export const flowBackgroundColor = "#fafafa"
export const flowDotColor = "#dedede"
export type InputFormOptions = { export type InputFormOptions = {
label: string label: string
@@ -24,3 +28,13 @@ export type FlowEditorProps = {
graphData: GraphData, graphData: GraphData,
onGraphDataChange: (graphData: GraphData) => void, onGraphDataChange: (graphData: GraphData) => void,
} }
export type NodeDefine = {
key: string,
group: string,
name: string,
icon: JSX.Element,
description: string,
component: any,
checkers: NodeChecker[],
}

View File

@@ -4,7 +4,7 @@ import type {GraphData} from '../components/flow/types.ts'
function Test() { function Test() {
// language=JSON // language=JSON
const [graphData] = useState<GraphData>(JSON.parse('{\n "nodes": [\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 92\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 130\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 130\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "MzEitlOusl",\n "sourceHandle": "source",\n "target": "bivXSpiLaI",\n "targetHandle": "target",\n "id": "xy-edge__MzEitlOuslsource-bivXSpiLaItarget"\n }\n ],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n }\n }\n}')) const [graphData] = useState<GraphData>(JSON.parse('{\n "nodes": [\n {\n "id": "QxNrkChBWQ",\n "type": "loop-node",\n "position": {\n "x": 742,\n "y": 119\n },\n "data": {},\n "measured": {\n "width": 458,\n "height": 368\n },\n "selected": true,\n "dragging": false,\n "width": 458,\n "height": 368,\n "resizing": false\n },\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 92\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false,\n "extent": "parent",\n "parentId": "QxNrkChBWQ"\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n },\n "QxNrkChBWQ": {\n "node": {\n "name": "循环",\n "description": "实现循环执行流程"\n },\n "finished": true\n }\n }\n}'))
return ( return (
<div className="h-screen"> <div className="h-screen">