diff --git a/package-lock.json b/package-lock.json index a8f9cdc..42d69a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.1.3", + "react-tracked": "^1.7.11", "reactflow": "^11.10.1", "simplebar-react": "^3.2.4", "use-immer": "^0.9.0", @@ -13863,6 +13864,11 @@ "integrity": "sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==", "dev": true }, + "node_modules/proxy-compare": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz", + "integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -14634,6 +14640,29 @@ "react-dom": ">=16.8" } }, + "node_modules/react-tracked": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.11.tgz", + "integrity": "sha512-+XXv4dJH7NnLtSD/cPVL9omra4A3KRK91L33owevXZ81r7qF/a9DdCsVZa90jMGht/V1Ym9sasbmidsJykhULQ==", + "dependencies": { + "proxy-compare": "2.4.0", + "use-context-selector": "1.4.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": "*", + "react-native": "*", + "scheduler": ">=0.19.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/reactflow": { "version": "11.10.1", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.10.1.tgz", @@ -17198,6 +17227,25 @@ "node": ">=0.10.0" } }, + "node_modules/use-context-selector": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.1.tgz", + "integrity": "sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": "*", + "react-native": "*", + "scheduler": ">=0.19.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/use-immer": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.9.0.tgz", @@ -28676,6 +28724,11 @@ "integrity": "sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==", "dev": true }, + "proxy-compare": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz", + "integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg==" + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -29209,6 +29262,15 @@ "react-router": "6.21.1" } }, + "react-tracked": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.11.tgz", + "integrity": "sha512-+XXv4dJH7NnLtSD/cPVL9omra4A3KRK91L33owevXZ81r7qF/a9DdCsVZa90jMGht/V1Ym9sasbmidsJykhULQ==", + "requires": { + "proxy-compare": "2.4.0", + "use-context-selector": "1.4.1" + } + }, "reactflow": { "version": "11.10.1", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.10.1.tgz", @@ -31226,6 +31288,12 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-context-selector": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.1.tgz", + "integrity": "sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==", + "requires": {} + }, "use-immer": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.9.0.tgz", diff --git a/package.json b/package.json index 0edb5db..ed807ea 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.1.3", + "react-tracked": "^1.7.11", "reactflow": "^11.10.1", "simplebar-react": "^3.2.4", "use-immer": "^0.9.0", diff --git a/src/FlowManager.ts b/src/FlowManager.ts index 0fb65d7..5be96c4 100644 --- a/src/FlowManager.ts +++ b/src/FlowManager.ts @@ -25,7 +25,7 @@ export interface INode { title: string component: ForwardRefExoticComponent> /** 节点链接规则 */ - riles?: Array + rules?: Array } class FlowManager { diff --git a/src/Nodes/NodeA/NodeA.module.less b/src/Nodes/NodeA/NodeA.module.less index b406171..f8e2f90 100644 --- a/src/Nodes/NodeA/NodeA.module.less +++ b/src/Nodes/NodeA/NodeA.module.less @@ -2,4 +2,34 @@ width: 40px; height: 40px; background-color: pink; + position: relative; + + &:hover { + .node-text { + opacity: 1; + } + } + + &-text { + position: absolute; + width: 100%; + height: 20px; + text-align: center; + bottom: -20px; + left: 0; + font-size: 12px; + line-height: 20px; + color: #333333; + opacity: 0; + } + + & &-handle { + width: 100%; + height: 100%; + left: 0; + top: 0; + transform: translate(0, 0); + opacity: 0; + border-radius: 0; + } } diff --git a/src/Nodes/NodeA/NodeA.tsx b/src/Nodes/NodeA/NodeA.tsx index 6cd74e2..dec9c46 100644 --- a/src/Nodes/NodeA/NodeA.tsx +++ b/src/Nodes/NodeA/NodeA.tsx @@ -1,7 +1,8 @@ -import React, { forwardRef, useImperativeHandle } from "react" +import React, { forwardRef, useEffect, useImperativeHandle } from "react" import styles from "./NodeA.module.less" import { Handle, Position } from "reactflow" import type { NodeProps } from "@reactflow/core/dist/esm/types/nodes" +import { useFlowDataSelector } from "@/context/FlowData" interface NodeAProps extends NodeProps { isMenu?: boolean @@ -11,10 +12,16 @@ export interface NodeAInstance { } +const position = [Position.Top, Position.Left, Position.Right, Position.Bottom] + const NodeA = forwardRef((props, ref) => { const { isMenu, isConnectable } = props + + const activeNode = useFlowDataSelector((store) => store.activeNode) + useEffect(() => { + }, [activeNode]) useImperativeHandle(ref, (): NodeAInstance => { return {} }) @@ -24,16 +31,35 @@ const NodeA = forwardRef((props, ref) => { { !isMenu && ( <> - - + { + position.map(i => ( + + )) + } + { + position.map(i => ( + + )) + } + NodeA ) } diff --git a/src/Nodes/NodeA/index.ts b/src/Nodes/NodeA/index.ts index 0e96649..c9370b6 100644 --- a/src/Nodes/NodeA/index.ts +++ b/src/Nodes/NodeA/index.ts @@ -8,7 +8,13 @@ export const install = () => { FlowManager.registerNode({ type: Flow.NodeType.NodeA, title: "NodeA", - component: NodeA + component: NodeA, + rules: [ + { + sourceNodeType: "", + targetNodeType: "" + } + ] }) } diff --git a/src/Nodes/NodeB/NodeB.module.less b/src/Nodes/NodeB/NodeB.module.less index 0fc747a..7df629d 100644 --- a/src/Nodes/NodeB/NodeB.module.less +++ b/src/Nodes/NodeB/NodeB.module.less @@ -2,4 +2,33 @@ width: 40px; height: 40px; background-color: orange; + + &:hover { + .node-text { + opacity: 1; + } + } + + &-text { + position: absolute; + width: 100%; + height: 20px; + text-align: center; + bottom: -20px; + left: 0; + font-size: 12px; + line-height: 20px; + color: #333333; + opacity: 0; + } + + & &-handle { + width: 100%; + height: 100%; + left: 0; + top: 0; + transform: translate(0, 0); + opacity: 0; + border-radius: 0; + } } diff --git a/src/Nodes/NodeB/NodeB.tsx b/src/Nodes/NodeB/NodeB.tsx index fa42cb2..cc78f0b 100644 --- a/src/Nodes/NodeB/NodeB.tsx +++ b/src/Nodes/NodeB/NodeB.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, useImperativeHandle } from "react" import styles from "./NodeB.module.less" import { Handle, Position } from "reactflow" import type { NodeProps } from "@reactflow/core/dist/esm/types/nodes" +import { useFlowDataSelector } from "@/context/FlowData" interface NodeBProps extends NodeProps { isMenu?: boolean @@ -15,6 +16,8 @@ const NodeB = forwardRef((props, ref) => { const { isMenu, isConnectable } = props + const activeNode = useFlowDataSelector((store) => store.activeNode) + useImperativeHandle(ref, (): NodeBInstance => { return {} }) @@ -26,14 +29,23 @@ const NodeB = forwardRef((props, ref) => { <> + NodeB ) } diff --git a/src/Nodes/NodeC/NodeC.module.less b/src/Nodes/NodeC/NodeC.module.less index d0f4176..35ef14b 100644 --- a/src/Nodes/NodeC/NodeC.module.less +++ b/src/Nodes/NodeC/NodeC.module.less @@ -2,4 +2,33 @@ width: 40px; height: 40px; background-color: deepskyblue; + + &:hover { + .node-text { + opacity: 1; + } + } + + &-text { + position: absolute; + width: 100%; + height: 20px; + text-align: center; + bottom: -20px; + left: 0; + font-size: 12px; + line-height: 20px; + color: #333333; + opacity: 0; + } + + & &-handle { + width: 100%; + height: 100%; + left: 0; + top: 0; + transform: translate(0, 0); + opacity: 0; + border-radius: 0; + } } diff --git a/src/Nodes/NodeC/NodeC.tsx b/src/Nodes/NodeC/NodeC.tsx index 6832238..1b4d61f 100644 --- a/src/Nodes/NodeC/NodeC.tsx +++ b/src/Nodes/NodeC/NodeC.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, useImperativeHandle } from "react" import styles from "./NodeC.module.less" import { Handle, Position } from "reactflow" import type { NodeProps } from "@reactflow/core/dist/esm/types/nodes" +import { useFlowDataSelector } from "@/context/FlowData" interface NodeCProps extends NodeProps { isMenu?: boolean @@ -15,6 +16,8 @@ const NodeC = forwardRef((props, ref) => { const { isMenu, isConnectable } = props + const activeNode = useFlowDataSelector((store) => store.activeNode) + useImperativeHandle(ref, (): NodeCInstance => { return {} }) @@ -26,14 +29,23 @@ const NodeC = forwardRef((props, ref) => { <> + NodeC ) } diff --git a/src/context/FlowData/index.ts b/src/context/FlowData/index.ts new file mode 100644 index 0000000..c1b9016 --- /dev/null +++ b/src/context/FlowData/index.ts @@ -0,0 +1,5 @@ +import {FlowDataProvider, useFlowDataSelector, useSetFlowDataState} from "./store" + +export type {IFlowDataStore, IFlowDataState, IFlowDataActions} from "./types" +export {useFlowDataSelector, useSetFlowDataState} +export default FlowDataProvider diff --git a/src/context/FlowData/store.tsx b/src/context/FlowData/store.tsx new file mode 100644 index 0000000..d6b15dc --- /dev/null +++ b/src/context/FlowData/store.tsx @@ -0,0 +1,13 @@ +import {useImmer} from "use-immer" +import {createContainer} from 'react-tracked'; +import type {IFlowDataState} from "./types"; + +const useFlowDataState = () => useImmer({ + activeNode: null +}); + +export const { + Provider: FlowDataProvider, + useSelector: useFlowDataSelector, + useUpdate: useSetFlowDataState, +} = createContainer any, IFlowDataState>(useFlowDataState); diff --git a/src/context/FlowData/types.ts b/src/context/FlowData/types.ts new file mode 100644 index 0000000..d16a55c --- /dev/null +++ b/src/context/FlowData/types.ts @@ -0,0 +1,14 @@ + +/** 流程数据hooks状态 */ +export interface IFlowDataState { + activeNode?: null | string +} + +/** 流程方法hooks事件 */ +export interface IFlowDataActions { + setActiveNode: (nodeId?: string) => void +} + +/** 流程数据hooks元数据 */ +export type IFlowDataStore = IFlowDataState & IFlowDataActions + diff --git a/src/hooks/useSetActiveNode.ts b/src/hooks/useSetActiveNode.ts new file mode 100644 index 0000000..b674ed2 --- /dev/null +++ b/src/hooks/useSetActiveNode.ts @@ -0,0 +1,14 @@ +import { useSetFlowDataState } from "../context/FlowData" +import type { IFlowDataState } from "../context/FlowData" +import { useCallback } from "react" + +export default () => { + + const setFlowDataState = useSetFlowDataState() + + return useCallback((nodeId?: null | string) => { + setFlowDataState((preStore: IFlowDataState) => { + preStore.activeNode = nodeId + }) + }, [setFlowDataState]) +} diff --git a/src/views/DemoA.tsx b/src/views/DemoA.tsx index dca6144..1df9e13 100644 --- a/src/views/DemoA.tsx +++ b/src/views/DemoA.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from "react" +import React, { useCallback, useMemo, useRef, useState, memo, useEffect } from "react" import ReactFlow, { MiniMap, Controls, @@ -6,6 +6,11 @@ import ReactFlow, { useNodesState, useEdgesState, addEdge, + ReactFlowProvider, + useNodes, + useStore, + useReactFlow, + useOnSelectionChange } from "reactflow" import styles from "./DemoA.module.less" import CustomNode from "./components/CustomNode" @@ -17,6 +22,8 @@ import type { ReactFlowInstance } from "@reactflow/core/dist/esm/types/instance" import { randomString } from "@/helper" import FlowManager from "@/FlowManager" import { Flow } from "@/types" +import FlowDataProvider from "../context/FlowData" +import useSetActiveNode from "../hooks/useSetActiveNode" const initialNodes = [ {id: "1", position: {x: 0, y: 0}, data: {label: "1"}}, @@ -36,6 +43,9 @@ const edgeTypes = { } function DemoA() { + const connectionNodeId = useStore((store) => store.connectionNodeId) + const setActiveNode = useSetActiveNode() + const nodeTypes = useMemo(() => { const n = FlowManager.getAllNodes().reduce((result, item) => { result[item.type] = item.component @@ -45,6 +55,10 @@ function DemoA() { }, []) const reactFlowWrapper = useRef({} as HTMLDivElement) + useEffect(() => { + setActiveNode(connectionNodeId) + }, [connectionNodeId]) + const [reactFlowInstance, setReactFlowInstance] = useState({} as ReactFlowInstance) const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) @@ -53,7 +67,7 @@ function DemoA() { const onConnect = useCallback( (connection: Connection) => { - console.log("____", connection) + // 通过connection处理两个节点的handle方向 setEdges((eds) => addEdge(connection, eds)) }, [setEdges], @@ -111,7 +125,8 @@ function DemoA() { const NodeItem = node.component return (
-
onDragStart(event, node.type) } draggable> +
onDragStart(event, node.type) } draggable>
@@ -145,4 +160,12 @@ function DemoA() { ) } -export default DemoA +export default memo(() => { + return ( + + + + + + ) +})