From e2274a7623081e434463a873e899ef1ae7604181 Mon Sep 17 00:00:00 2001 From: Luckyu_js <11670186+luckyu-js@user.noreply.gitee.com> Date: Thu, 23 Jul 2026 14:42:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=A5=E6=9F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/nodes/BaseNode.tsx | 18 ++++---- src/components/nodes/NodeHandle.tsx | 67 ++++++++++++++++------------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index fc1e6b9e..2a1c2e8a 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -314,17 +314,17 @@ export function BaseNode({ >
{ if (isPreviewMode) return; diff --git a/src/components/nodes/NodeHandle.tsx b/src/components/nodes/NodeHandle.tsx index 810c5bf1..77549e30 100644 --- a/src/components/nodes/NodeHandle.tsx +++ b/src/components/nodes/NodeHandle.tsx @@ -1,9 +1,10 @@ "use client"; import { Handle, Position, useNodeId, useReactFlow, type HandleProps } from "@xyflow/react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import type { CSSProperties, MouseEvent, PointerEvent, ReactNode } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; +import { selectNodeById } from "@/store/utils/nodesById"; import { useNodeHandleMenu } from "./NodeHandleMenuContext"; type NodeHandleVariant = "default" | "compact"; @@ -15,6 +16,8 @@ type NodeHandleProps = Omit & { visible?: boolean; }; +const MAGNET_WRITE_EPSILON = 0.5; + function cx(...values: Array) { return values.filter(Boolean).join(" "); } @@ -91,11 +94,13 @@ export function NodeHandle({ const menu = useNodeHandleMenu(); const pointerDownRef = useRef<{ x: number; y: number } | null>(null); const handleRef = useRef(null); + const hitTargetRef = useRef(null); + const iconRef = useRef(null); + const magnetOffsetRef = useRef({ x: 0, y: 0, scale: "1" }); const [isZoneHovered, setIsZoneHovered] = useState(false); - const [magnetOffset, setMagnetOffset] = useState({ x: 0, y: 0 }); const isSelected = useWorkflowStore((state) => { if (!nodeId) return false; - return state.nodes.some((node) => node.id === nodeId && node.selected); + return Boolean(selectNodeById(state.nodes, nodeId)?.selected); }); const isNodeHovered = useWorkflowStore((state) => ( nodeId ? state.hoveredNodeId === nodeId : false @@ -146,6 +151,28 @@ export function NodeHandle({ height: INTERACTION_ZONE_SIZE, transform: zoneTransform, } satisfies CSSProperties; + const visualTransform = `translate(calc(-50% + ${visualOffset.x}px + var(--handle-magnet-x, 0px)), calc(-50% + ${visualOffset.y}px + var(--handle-magnet-y, 0px)))`; + + const applyMagnetOffset = useCallback((x: number, y: number) => { + const nextScale = x !== 0 || y !== 0 ? "1.08" : "1"; + const current = magnetOffsetRef.current; + if ( + Math.abs(current.x - x) < MAGNET_WRITE_EPSILON && + Math.abs(current.y - y) < MAGNET_WRITE_EPSILON && + current.scale === nextScale + ) { + return; + } + magnetOffsetRef.current = { x, y, scale: nextScale }; + + for (const element of [hitTargetRef.current, iconRef.current]) { + if (!element) continue; + element.style.setProperty("--handle-magnet-x", `${x}px`); + element.style.setProperty("--handle-magnet-y", `${y}px`); + } + + iconRef.current?.style.setProperty("--handle-magnet-scale", nextScale); + }, []); const snapVisualHandleToPointer = useCallback((clientX: number, clientY: number) => { const rect = handleRef.current?.getBoundingClientRect(); @@ -174,30 +201,8 @@ export function NodeHandle({ return { x: pointerLocalX, y: pointerLocalY }; })(); - setMagnetOffset({ - x: clampedLocal.x - visualOffset.x, - y: clampedLocal.y - visualOffset.y, - }); - }, [getZoom, position, visualOffset.x, visualOffset.y]); - - useEffect(() => { - if (!isVisible) { - setMagnetOffset({ x: 0, y: 0 }); - return; - } - - const handlePointerMove = (event: globalThis.PointerEvent) => { - if (!isZoneHovered) { - setMagnetOffset((current) => current.x === 0 && current.y === 0 ? current : { x: 0, y: 0 }); - return; - } - - snapVisualHandleToPointer(event.clientX, event.clientY); - }; - - window.addEventListener("pointermove", handlePointerMove); - return () => window.removeEventListener("pointermove", handlePointerMove); - }, [isVisible, isZoneHovered, snapVisualHandleToPointer]); + applyMagnetOffset(clampedLocal.x - visualOffset.x, clampedLocal.y - visualOffset.y); + }, [applyMagnetOffset, getZoom, position, visualOffset.x, visualOffset.y]); const handlePointerDown = (event: PointerEvent) => { pointerDownRef.current = { x: event.clientX, y: event.clientY }; @@ -258,10 +263,11 @@ export function NodeHandle({ }} onPointerLeave={() => { setIsZoneHovered(false); - setMagnetOffset({ x: 0, y: 0 }); + applyMagnetOffset(0, 0); }} />