feat(web): 实现循环节点中新增节点
This commit is contained in:
@@ -1,19 +1,20 @@
|
|||||||
import {PlusCircleFilled, RollbackOutlined, SaveFilled} from '@ant-design/icons'
|
import {RollbackOutlined, SaveFilled} from '@ant-design/icons'
|
||||||
import {Background, BackgroundVariant, Controls, MiniMap, Panel, ReactFlow} from '@xyflow/react'
|
import {Background, BackgroundVariant, Controls, MiniMap, Panel, ReactFlow} from '@xyflow/react'
|
||||||
import {Button, Dropdown, message, Popconfirm, Space} from 'antd'
|
import {Button, message, Popconfirm, Space} from 'antd'
|
||||||
import {arrToMap, isEqual, randomId, unique} from 'licia'
|
import {arrToMap} from 'licia'
|
||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import {useNavigate} from 'react-router'
|
import {useNavigate} from 'react-router'
|
||||||
import styled from 'styled-components'
|
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, checkSave} from './FlowChecker.tsx'
|
||||||
import {useNodeDrag} from './Helper.tsx'
|
import {useNodeDrag} from './Helper.tsx'
|
||||||
import {NodeRegistry, NodeRegistryMap} from './NodeRegistry.tsx'
|
import {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 {flowDotColor, type FlowEditorProps} from './types.ts'
|
import {flowDotColor, type FlowEditorProps} from './types.ts'
|
||||||
|
import AddNodeButton from './component/AddNodeButton.tsx'
|
||||||
|
|
||||||
const FlowableDiv = styled.div`
|
const FlowableDiv = styled.div`
|
||||||
.react-flow__node.selectable {
|
.react-flow__node.selectable {
|
||||||
@@ -111,53 +112,7 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
>
|
>
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
<Space className="toolbar">
|
<Space className="toolbar">
|
||||||
<Dropdown
|
<AddNodeButton/>
|
||||||
menu={{
|
|
||||||
items: unique(NodeRegistry.map(i => i.group))
|
|
||||||
.map(group => ({
|
|
||||||
type: 'group',
|
|
||||||
label: group,
|
|
||||||
children: NodeRegistry.filter(i => isEqual(group, i.group))
|
|
||||||
.map(i => ({key: i.key, label: i.name, icon: i.icon})),
|
|
||||||
})),
|
|
||||||
onClick: ({key}) => {
|
|
||||||
try {
|
|
||||||
if (commonInfo.debug) {
|
|
||||||
console.info('Add', key, JSON.stringify({nodes, edges, data}))
|
|
||||||
}
|
|
||||||
checkAddNode(key, nodes, edges)
|
|
||||||
|
|
||||||
let nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
|
|
||||||
let define = NodeRegistryMap[key]
|
|
||||||
|
|
||||||
setDataById(
|
|
||||||
nodeId,
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
name: define.name,
|
|
||||||
description: define.description,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
addNode({
|
|
||||||
id: nodeId,
|
|
||||||
type: key,
|
|
||||||
position: {x: 100, y: 100},
|
|
||||||
data: {},
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
// @ts-ignore
|
|
||||||
messageApi.error(e.toString())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="default">
|
|
||||||
<PlusCircleFilled/>
|
|
||||||
新增节点
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="返回上一页"
|
title="返回上一页"
|
||||||
description="未保存的流程图将会被丢弃,确认是否返回"
|
description="未保存的流程图将会被丢弃,确认是否返回"
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const useNodeDrag = (deps: DependencyList) => {
|
|||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
let newPosition = {
|
let newPosition = {
|
||||||
x: max(min(node.position.x, (parentNode.measured?.width ?? 0) - (node.measured?.width ?? 0) - 28), 28),
|
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),
|
y: max(min(node.position.y, (parentNode.measured?.height ?? 0) - (node.measured?.height ?? 0) - 28), 130),
|
||||||
}
|
}
|
||||||
setNode({
|
setNode({
|
||||||
...node,
|
...node,
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import {isEqual, randomId, unique} from 'licia'
|
||||||
|
import {NodeRegistry, NodeRegistryMap} from '../NodeRegistry.tsx'
|
||||||
|
import {commonInfo} from '../../../util/amis.tsx'
|
||||||
|
import {checkAddNode} from '../FlowChecker.tsx'
|
||||||
|
import {Button, Dropdown} from 'antd'
|
||||||
|
import {PlusCircleFilled} from '@ant-design/icons'
|
||||||
|
import {useDataStore} from '../store/DataStore.ts'
|
||||||
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
|
import type {ButtonProps} from 'antd/lib'
|
||||||
|
|
||||||
|
export type AddNodeButtonProps = {
|
||||||
|
parentId?: string
|
||||||
|
} & ButtonProps
|
||||||
|
|
||||||
|
const AddNodeButton = (props: AddNodeButtonProps) => {
|
||||||
|
const {data, setDataById} = useDataStore()
|
||||||
|
const {nodes, addNode, edges,} = useFlowStore()
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: unique(NodeRegistry.map(i => i.group))
|
||||||
|
.map(group => ({
|
||||||
|
type: 'group',
|
||||||
|
label: group,
|
||||||
|
children: NodeRegistry
|
||||||
|
.filter(i => isEqual(group, i.group))
|
||||||
|
// 循环节点里不能再嵌套循环节点
|
||||||
|
.filter(i => !props.parentId || (props.parentId && !isEqual(i.key, 'loop-node')))
|
||||||
|
.map(i => ({key: i.key, label: i.name, icon: i.icon})),
|
||||||
|
})),
|
||||||
|
onClick: ({key}) => {
|
||||||
|
try {
|
||||||
|
if (commonInfo.debug) {
|
||||||
|
console.info('Add', key, JSON.stringify({nodes, edges, data}))
|
||||||
|
}
|
||||||
|
checkAddNode(key, nodes, edges)
|
||||||
|
|
||||||
|
let nodeId = randomId(10, 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
|
||||||
|
let define = NodeRegistryMap[key]
|
||||||
|
|
||||||
|
setDataById(
|
||||||
|
nodeId,
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
name: define.name,
|
||||||
|
description: define.description,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
addNode({
|
||||||
|
id: nodeId,
|
||||||
|
type: key,
|
||||||
|
position: {x: 100, y: 130},
|
||||||
|
data: {},
|
||||||
|
// 如果是循环节点就将节点加入到循环节点中
|
||||||
|
...(props.parentId ? {
|
||||||
|
parentId: props.parentId,
|
||||||
|
extent: 'parent',
|
||||||
|
} : {})
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// @ts-ignore
|
||||||
|
messageApi.error(e.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button {...props}>
|
||||||
|
<PlusCircleFilled/>
|
||||||
|
新增节点
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNodeButton
|
||||||
@@ -3,14 +3,15 @@ import {classnames} from 'amis'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {flowBackgroundColor, flowDotColor} from '../types.ts'
|
import {flowBackgroundColor, flowDotColor} from '../types.ts'
|
||||||
import AmisNode, {nodeClassName, NormalNodeHandler} from './AmisNode.tsx'
|
import AmisNode, {nodeClassName, NormalNodeHandler} from './AmisNode.tsx'
|
||||||
|
import AddNodeButton from '../component/AddNodeButton.tsx'
|
||||||
|
|
||||||
const LoopNode = (props: NodeProps) => {
|
const LoopNode = (props: NodeProps) => {
|
||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
className={classnames('w-full', 'h-full', nodeClassName('loop'))}
|
className={classnames('w-full', 'h-full', nodeClassName('loop'))}
|
||||||
style={{
|
style={{
|
||||||
minWidth: '256px',
|
minWidth: '350px',
|
||||||
minHeight: '110px'
|
minHeight: '200px'
|
||||||
}}
|
}}
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
extraNodeDescription={
|
extraNodeDescription={
|
||||||
@@ -32,12 +33,13 @@ const LoopNode = (props: NodeProps) => {
|
|||||||
color={flowDotColor}
|
color={flowDotColor}
|
||||||
bgColor={flowBackgroundColor}
|
bgColor={flowBackgroundColor}
|
||||||
/>
|
/>
|
||||||
|
<AddNodeButton className="mt-2 ml-2" parentId={props.id}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
handler={<NormalNodeHandler/>}
|
handler={<NormalNodeHandler/>}
|
||||||
resize={{
|
resize={{
|
||||||
minWidth: 256,
|
minWidth: 350,
|
||||||
minHeight: 208,
|
minHeight: 290,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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": "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}'))
|
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": 135\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">
|
||||||
|
|||||||
Reference in New Issue
Block a user