feat(web): 增加流程图连线限制
This commit is contained in:
@@ -1,5 +1,16 @@
|
|||||||
import {PlusCircleFilled, SaveFilled} from '@ant-design/icons'
|
import {PlusCircleFilled, SaveFilled} from '@ant-design/icons'
|
||||||
import {Background, BackgroundVariant, Controls, MiniMap, type Node, type NodeProps, ReactFlow} from '@xyflow/react'
|
import {
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
type Connection,
|
||||||
|
Controls,
|
||||||
|
getIncomers,
|
||||||
|
getOutgoers,
|
||||||
|
MiniMap,
|
||||||
|
type Node,
|
||||||
|
type NodeProps,
|
||||||
|
ReactFlow,
|
||||||
|
} from '@xyflow/react'
|
||||||
import {useMount} from 'ahooks'
|
import {useMount} from 'ahooks'
|
||||||
import type {Schema} from 'amis'
|
import type {Schema} from 'amis'
|
||||||
import {Button, Drawer, Dropdown, message, Space} from 'antd'
|
import {Button, Drawer, Dropdown, message, Space} from 'antd'
|
||||||
@@ -57,6 +68,7 @@ function FlowEditor() {
|
|||||||
const {data, setData, getDataById, setDataById} = useDataStore()
|
const {data, setData, getDataById, setDataById} = useDataStore()
|
||||||
const {
|
const {
|
||||||
nodes,
|
nodes,
|
||||||
|
getNodeById,
|
||||||
addNode,
|
addNode,
|
||||||
removeNode,
|
removeNode,
|
||||||
setNodes,
|
setNodes,
|
||||||
@@ -137,6 +149,56 @@ function FlowEditor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkNode = (type: string) => {
|
||||||
|
if (isEqual(type, 'start-amis-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) {
|
||||||
|
throw new Error('只能存在1个开始节点')
|
||||||
|
}
|
||||||
|
if (isEqual(type, 'end-amis-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) {
|
||||||
|
throw new Error('只能存在1个结束节点')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkConnection = (connection: Connection) => {
|
||||||
|
let sourceNode = getNodeById(connection.source)
|
||||||
|
if (!sourceNode) {
|
||||||
|
throw new Error('连线起始节点未找到')
|
||||||
|
}
|
||||||
|
let targetNode = getNodeById(connection.target)
|
||||||
|
if (!targetNode) {
|
||||||
|
throw new Error('连线目标节点未找到')
|
||||||
|
}
|
||||||
|
console.log(sourceNode, targetNode, connection)
|
||||||
|
// 禁止短路整个流程
|
||||||
|
if (isEqual('start-amis-node', sourceNode.type) && isEqual('end-amis-node', targetNode.type)) {
|
||||||
|
throw new Error('开始节点不能直连结束节点')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁止流程出现环,必须是有向无环图
|
||||||
|
const hasCycle = (node: Node, visited = new Set()) => {
|
||||||
|
if (visited.has(node.id)) return false
|
||||||
|
visited.add(node.id)
|
||||||
|
for (const outgoer of getOutgoers(node, nodes, edges)) {
|
||||||
|
if (isEqual(outgoer.id, sourceNode?.id)) return true
|
||||||
|
if (hasCycle(outgoer, visited)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isEqual(sourceNode.id, targetNode.id)) {
|
||||||
|
throw new Error('节点不能直连自身')
|
||||||
|
} else if (hasCycle(targetNode)) {
|
||||||
|
throw new Error('禁止流程循环')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasShortcut = (node: Node, visited = new Set()) => {
|
||||||
|
if (visited.has(node.id)) return false
|
||||||
|
visited.add(node.id)
|
||||||
|
for (const incomer of getIncomers(node, nodes, edges)) {
|
||||||
|
if (isEqual(incomer.id, sourceNode?.id)) return true
|
||||||
|
if (hasShortcut(incomer, visited)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(getIncomers(targetNode, nodes, edges))
|
||||||
|
}
|
||||||
|
|
||||||
useMount(() => {
|
useMount(() => {
|
||||||
// language=JSON
|
// language=JSON
|
||||||
let initialData = JSON.parse(`{
|
let initialData = JSON.parse(`{
|
||||||
@@ -191,11 +253,6 @@ function FlowEditor() {
|
|||||||
"source": "BMFP3Eov94",
|
"source": "BMFP3Eov94",
|
||||||
"target": "nCm-ij5I6o",
|
"target": "nCm-ij5I6o",
|
||||||
"id": "xy-edge__BMFP3Eov94-nCm-ij5I6o"
|
"id": "xy-edge__BMFP3Eov94-nCm-ij5I6o"
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "nCm-ij5I6o",
|
|
||||||
"target": "PYK8LjduQ1",
|
|
||||||
"id": "xy-edge__nCm-ij5I6o-PYK8LjduQ1"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"data": {
|
"data": {
|
||||||
@@ -220,7 +277,7 @@ function FlowEditor() {
|
|||||||
"systemPrompt": "你是个沙雕"
|
"systemPrompt": "你是个沙雕"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
let initialNodes = initialData['nodes'] ?? []
|
let initialNodes = initialData['nodes'] ?? []
|
||||||
let initialEdges = initialData['edges'] ?? []
|
let initialEdges = initialData['edges'] ?? []
|
||||||
|
|
||||||
@@ -254,15 +311,8 @@ function FlowEditor() {
|
|||||||
menu={{
|
menu={{
|
||||||
items: nodeDef.map(def => ({key: def.key, label: def.name})),
|
items: nodeDef.map(def => ({key: def.key, label: def.name})),
|
||||||
onClick: ({key}) => {
|
onClick: ({key}) => {
|
||||||
if (isEqual(key, 'start-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) {
|
try {
|
||||||
messageApi.error('只能存在1个开始节点')
|
checkNode(key)
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isEqual(key, 'end-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) {
|
|
||||||
messageApi.error('只能存在1个结束节点')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
addNode({
|
addNode({
|
||||||
id: randomId(10),
|
id: randomId(10),
|
||||||
type: key,
|
type: key,
|
||||||
@@ -274,6 +324,10 @@ function FlowEditor() {
|
|||||||
editNode,
|
editNode,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// @ts-ignore
|
||||||
|
messageApi.error(e.message)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -298,7 +352,15 @@ function FlowEditor() {
|
|||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={(connection) => {
|
||||||
|
try {
|
||||||
|
checkConnection(connection)
|
||||||
|
onConnect(connection)
|
||||||
|
} catch (e) {
|
||||||
|
// @ts-ignore
|
||||||
|
messageApi.error(e.message)
|
||||||
|
}
|
||||||
|
}}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
nodeTypes={arrToMap(
|
nodeTypes={arrToMap(
|
||||||
nodeDef.map(def => def.key),
|
nodeDef.map(def => def.key),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import {create} from 'zustand/react'
|
|
||||||
import {
|
import {
|
||||||
addEdge,
|
addEdge,
|
||||||
applyEdgeChanges,
|
applyEdgeChanges,
|
||||||
@@ -7,13 +6,15 @@ import {
|
|||||||
type Node,
|
type Node,
|
||||||
type OnConnect,
|
type OnConnect,
|
||||||
type OnEdgesChange,
|
type OnEdgesChange,
|
||||||
type OnNodesChange
|
type OnNodesChange,
|
||||||
} from '@xyflow/react'
|
} from '@xyflow/react'
|
||||||
import {filter, isEqual} from 'licia'
|
import {filter, find, isEqual} from 'licia'
|
||||||
|
import {create} from 'zustand/react'
|
||||||
|
|
||||||
export const useFlowStore = create<{
|
export const useFlowStore = create<{
|
||||||
nodes: Node[],
|
nodes: Node[],
|
||||||
onNodesChange: OnNodesChange,
|
onNodesChange: OnNodesChange,
|
||||||
|
getNodeById: (id: string) => Node | undefined,
|
||||||
addNode: (node: Node) => void,
|
addNode: (node: Node) => void,
|
||||||
removeNode: (id: string) => void,
|
removeNode: (id: string) => void,
|
||||||
setNodes: (nodes: Node[]) => void,
|
setNodes: (nodes: Node[]) => void,
|
||||||
@@ -30,6 +31,7 @@ export const useFlowStore = create<{
|
|||||||
nodes: applyNodeChanges(changes, get().nodes),
|
nodes: applyNodeChanges(changes, get().nodes),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getNodeById: (id: string) => find(get().nodes, node => isEqual(node.id, id)),
|
||||||
addNode: node => set({nodes: get().nodes.concat(node)}),
|
addNode: node => set({nodes: get().nodes.concat(node)}),
|
||||||
removeNode: id => {
|
removeNode: id => {
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in New Issue
Block a user