feat(web): 增加流程图连线限制

This commit is contained in:
v-zhangjc9
2025-06-24 14:07:07 +08:00
parent 1cba0f4422
commit 566dfef208
2 changed files with 166 additions and 102 deletions

View File

@@ -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": {
@@ -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),

View File

@@ -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({