import type { Connection, EdgeComponent, NodeComponent, ValidConnectionFunc, GraphNode, NodeMouseEvent, GraphEdge, EdgeMouseEvent, } from '@vue-flow/core'; import type { Ref } from 'vue'; import type { CreateNodeModal } from '../src/components/CreateNodeModal'; import type { EdgeData, NodeData } from '../types/node'; import type { CreateEdgeModal } from '../src/components/CreateEdgeModal'; import { BasicEdge, BasicNode } from '../src/components'; import { EdgeTypeEnum, ElementsTypeEnum, NodeTypeEnum } from '../enum'; import { markRaw, toRaw, unref } from 'vue'; import { isFunction } from 'lodash-es'; import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core'; import { isInputHandle, isOutputHandle } from '../utils'; import { useAddEdges } from './useAddEdges'; import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer'; import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer'; import { isNumber } from '/@/utils/is'; import { RuleContextMenuEnum, useCreateRuleChainContextMenu } from './useRuleChainContextMenu'; import { RuleChainDetail } from '../types/ruleNode'; import { useContextMenuAction } from './useContextMenuAction'; import { useSaveAndRedo } from './useSaveAndRedo'; import { EntryCategoryComponentEnum } from '../enum/category'; import { CreateRuleChainModal } from '../src/components/CreateRuleChainModal'; import { useBasicDataTransform } from './useBasicDataTransform'; interface UseRuleFlowOptionsType { id: string; ruleChainDetail: Ref; modalActionType: { createNodeModalActionType: Ref>>; createEdgeModalActionType: Ref>>; updateNodeDrawerActionType: Ref>>; updateEdgeDrawerActionType: Ref>>; createRuleChainModalActionType: Ref>>; }; useSaveAndRedoActionType: ReturnType; } const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => { const { sourceHandle, targetHandle, source, target } = connection; return ( isOutputHandle(sourceHandle || '') && isInputHandle(targetHandle || '') && source !== target ); }; export function useRuleFlow(options: UseRuleFlowOptionsType) { const { id, ruleChainDetail, modalActionType, useSaveAndRedoActionType } = options; const { createEdgeModalActionType, updateEdgeDrawerActionType, updateNodeDrawerActionType } = modalActionType; const { triggerChange } = useSaveAndRedoActionType; const flowActionType = useVueFlow({ id, maxZoom: 1, minZoom: 1, panOnScroll: true, selectionMode: SelectionMode.Partial, nodeTypes: { [NodeTypeEnum.CUSTOM]: markRaw(BasicNode) as NodeComponent, }, edgeTypes: { [EdgeTypeEnum.CUSTOM]: markRaw(BasicEdge) as EdgeComponent, }, connectionLineOptions: { type: ConnectionLineType.Bezier, }, defaultViewport: { x: 0, y: 0, }, isValidConnection(connection, elements) { if (!elements.targetNode || !elements.sourceNode) return false; const validateList = [validateInputAndOutput]; const targetData = elements.targetNode.data as NodeData; if ( targetData.categoryConfig?.validateConnection && isFunction(targetData.categoryConfig.validateConnection) ) validateList.push(targetData.categoryConfig?.validateConnection); if (targetData.config?.validateConnection && isFunction(targetData.config.validateConnection)) validateList.push(targetData.config.validateConnection); if (!validateList.every((item) => item(connection, elements))) return false; if (validateCircularreference(elements.sourceNode!, elements.targetNode, elements.edges)) return false; return true; }, }); const { getEdges, addEdges, findEdge, findNode, setViewport, removeEdges, onConnect, onPaneReady, onNodeDoubleClick, onEdgeDoubleClick, onNodeDragStop, onNodeContextMenu, onEdgeContextMenu, onPaneContextMenu, } = flowActionType; const { getAddedgesParams } = useAddEdges(); onConnect(async (params) => { const { source } = params; const sourceNode = findNode(source); const sourceData = sourceNode?.data as NodeData; let types: string[] = []; if (sourceData && validateHasLabelConnection(sourceData)) { const { flag, data } = (await unref(createEdgeModalActionType)?.open(sourceData)) || {}; if (!flag) return; types = toRaw(unref(data)); } handleMaxConnectionPoint(sourceNode); addEdges(getAddedgesParams(params, types)); triggerChange(); }); onPaneReady(async () => { setViewport({ x: 0, y: 0, zoom: 1 }); }); onNodeDoubleClick(async ({ node }) => { handleUpdateNode(node); }); onEdgeDoubleClick(async ({ edge }) => { handleUpdateEdge(edge); }); onNodeDragStop(() => { triggerChange(); }); const { createNodeContextMenu, createElementsSelectedContextMenu, createEdgeContextMenu, createPanelContextMenu, } = useCreateRuleChainContextMenu(); const { handleContextMenuAction } = useContextMenuAction(); const { validateCanCreateRuleChain } = useBasicDataTransform(); onNodeContextMenu(async (params) => { const menuType = params.node.selected ? await createElementsSelectedContextMenu( params, unref(useSaveAndRedoActionType.changeMarker), validateCanCreateRuleChain( flowActionType.getSelectedNodes, flowActionType.getSelectedEdges ).flag ) : await createNodeContextMenu(params); if (!menuType) return; if (menuType === RuleContextMenuEnum.DETAIL) { handleUpdateNode(params.node); return; } handleContextMenuAction({ menuType, flowActionType, event: params.event, node: params.node, useSaveAndRedoActionType, modalActionType, }); }); onEdgeContextMenu(async (params) => { const isInputNode = (params.edge.sourceNode.data as NodeData).config?.key === EntryCategoryComponentEnum.INPUT; const menuType = await createEdgeContextMenu( getCreateEdgeContextMenuParams(params), isInputNode ); if (menuType) { if (menuType === RuleContextMenuEnum.DETAIL) { handleUpdateEdge(params.edge); return; } handleContextMenuAction({ menuType, flowActionType, event: params.event, useSaveAndRedoActionType, edge: params.edge, modalActionType, }); } }); onPaneContextMenu(async (params) => { const menuType = unref(flowActionType.getSelectedElements).length ? await createElementsSelectedContextMenu( getCreatePanelContextMenuParams(params), unref(useSaveAndRedoActionType.changeMarker), validateCanCreateRuleChain( flowActionType.getSelectedNodes, flowActionType.getSelectedEdges ).flag ) : await createPanelContextMenu( getCreatePanelContextMenuParams(params), unref(useSaveAndRedoActionType.changeMarker) ); if (!menuType) return; handleContextMenuAction({ menuType, flowActionType, event: params, useSaveAndRedoActionType, modalActionType, }); }); /** * @description 验证是否有连接label * @param sourceData * @returns */ function validateHasLabelConnection(sourceData: NodeData) { return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length; } /** * @description 验证是否循环引用 */ function validateCircularreference( sourceNode: GraphNode, targetNode: GraphNode, edges: GraphEdge[] ) { const sourceId = sourceNode.id; const targetId = targetNode.id; const getAllIndex = ( edges: GraphEdge[], validate: (item: GraphEdge, index: number) => boolean ) => { const indexList: number[] = []; for (let i = 0; i < edges.length; i++) { const item = edges[i]; if (validate(item, i)) { indexList.push(i); } } return indexList; }; const validate = (source: string, startSource: string) => { const nextNodesIndex = getAllIndex(edges, (item) => item.source === source); if (!nextNodesIndex.length) return false; for (const nextNodeIndex of nextNodesIndex) { const nextNode = edges[nextNodeIndex]; if (nextNode && nextNode.target === startSource) { return true; } return validate(nextNode.target, startSource); } }; return validate(targetId, sourceId); } /** * @description 处理最大链接点 * @param sourceNode * @returns */ function handleMaxConnectionPoint(sourceNode?: GraphNode) { if (!sourceNode) return; const maxConnectionPoint = unref(sourceNode).data?.config?.maxConnectionPoint; if (!maxConnectionPoint || !isNumber(maxConnectionPoint)) return; const sourceId = sourceNode.id; const connectionPool = unref(getEdges).filter((item) => item.source === sourceId); if (connectionPool.length >= maxConnectionPoint && connectionPool[0]) { removeEdges(connectionPool[0].id); } } async function handleUpdateNode(node: GraphNode) { if ((node.data as NodeData).config?.disableAction) return; const { flag, data } = (await unref(updateNodeDrawerActionType)?.open( toRaw((node as NodeData)?.data as unknown as NodeData), void 0, { id: node.id, type: ElementsTypeEnum.NODE } )) || {}; if (!flag) return; const currentNode = findNode(node.id); (currentNode!.data as NodeData).data = data; triggerChange(); } async function handleUpdateEdge(edge: GraphEdge) { if (!validateHasLabelConnection(edge.sourceNode.data)) return; if ((edge.sourceNode.data as NodeData).config?.disableAction) return; const { flag, data } = (await unref(updateEdgeDrawerActionType)?.open( toRaw(unref(edge.sourceNode?.data as unknown as NodeData)), toRaw(unref(edge.data as EdgeData)), { id: edge.id, type: ElementsTypeEnum.EDGE } )) || {}; if (!flag) return; const currentEdge = findEdge(edge.id); (currentEdge!.data as EdgeData).data = toRaw(unref(data)); triggerChange?.(); } function getCreatePanelContextMenuParams(params: Event) { return { event: params as MouseEvent, node: { data: { data: { name: '规则链' }, config: { name: unref(ruleChainDetail)?.name, backgroundColor: '#aac7e4', configurationDescriptor: { nodeDefinition: { icon: 'material-symbols:settings-ethernet', }, }, }, }, }, } as NodeMouseEvent; } function getCreateEdgeContextMenuParams(params: EdgeMouseEvent) { return { event: params.event as MouseEvent, node: { data: { data: { name: '链接' }, config: { name: unref(params.edge.data as EdgeData)?.data?.type?.join(' / '), backgroundColor: '#aac7e4', configurationDescriptor: { nodeDefinition: { icon: 'material-symbols:trending-flat', }, }, }, }, }, } as NodeMouseEvent; } return { flowActionType }; }