Browse Source

优化句柄

feature/0722ui
Luckyu_js 2 days ago
parent
commit
e2274a7623
  1. 18
      src/components/nodes/BaseNode.tsx
  2. 67
      src/components/nodes/NodeHandle.tsx

18
src/components/nodes/BaseNode.tsx

@ -314,17 +314,17 @@ export function BaseNode({
> >
<div <div
className={` className={`
${autoSizeContent ? "h-full w-full" : hasExpandedSettings ? "flex-1 min-h-0 w-full" : "h-full w-full"} flex flex-col overflow-visible relative ${autoSizeContent ? "h-full w-full" : hasExpandedSettings ? "flex-1 min-h-0 w-full" : "h-full w-full"} flex flex-col overflow-visible relative
${fullBleed ${fullBleed
? `${settingsExpanded ? "rounded-t-[var(--canvas-ui-radius)]" : "rounded-[var(--canvas-ui-radius)]"} border-2 border-dashed border-transparent bg-[var(--surface-2)] bg-clip-padding text-[var(--text-primary)] shadow-[var(--shadow-node)]` ? `${settingsExpanded ? "rounded-t-[var(--canvas-ui-radius)]" : "rounded-[var(--canvas-ui-radius)]"} border-2 border-dashed border-transparent bg-[var(--surface-2)] bg-clip-padding text-[var(--text-primary)] shadow-[var(--shadow-node)]`
: `border-2 border-dashed border-transparent bg-[var(--surface-2)] bg-clip-padding text-[var(--text-primary)] ${settingsExpanded ? "rounded-t-[var(--canvas-ui-radius)]" : "rounded-[var(--canvas-ui-radius)]"} shadow-[var(--shadow-node)]`} : `border-2 border-dashed border-transparent bg-[var(--surface-2)] bg-clip-padding text-[var(--text-primary)] ${settingsExpanded ? "rounded-t-[var(--canvas-ui-radius)]" : "rounded-[var(--canvas-ui-radius)]"} shadow-[var(--shadow-node)]`}
${fullBleed ? "" : (isCurrentlyExecuting || isExecuting ? "border-blue-500 ring-1 ring-blue-500/20" : "")} ${fullBleed ? "" : (isCurrentlyExecuting || isExecuting ? "border-blue-500 ring-1 ring-blue-500/20" : "")}
${fullBleed ? "" : (hasError ? "border-red-500" : "")} ${fullBleed ? "" : (hasError ? "border-red-500" : "")}
${fullBleed && selected && !settingsExpanded ? "!border-[var(--node-selected-border)] shadow-[var(--node-selected-shadow)]" : ""} ${fullBleed && selected && !settingsExpanded ? "!border-[var(--node-selected-border)] shadow-[var(--node-selected-shadow)]" : ""}
${!fullBleed && selected && !settingsExpanded ? "!border-[var(--node-selected-border)] shadow-[var(--node-selected-shadow)]" : ""} ${!fullBleed && selected && !settingsExpanded ? "!border-[var(--node-selected-border)] shadow-[var(--node-selected-shadow)]" : ""}
${!fullBleed && selected && settingsExpanded ? "!border-[var(--node-selected-border)]" : ""} ${!fullBleed && selected && settingsExpanded ? "!border-[var(--node-selected-border)]" : ""}
${className} ${className}
`} `}
data-tutorial={!hasExpandedSettings ? dataTutorial : undefined} data-tutorial={!hasExpandedSettings ? dataTutorial : undefined}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (isPreviewMode) return; if (isPreviewMode) return;

67
src/components/nodes/NodeHandle.tsx

@ -1,9 +1,10 @@
"use client"; "use client";
import { Handle, Position, useNodeId, useReactFlow, type HandleProps } from "@xyflow/react"; 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 type { CSSProperties, MouseEvent, PointerEvent, ReactNode } from "react";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { selectNodeById } from "@/store/utils/nodesById";
import { useNodeHandleMenu } from "./NodeHandleMenuContext"; import { useNodeHandleMenu } from "./NodeHandleMenuContext";
type NodeHandleVariant = "default" | "compact"; type NodeHandleVariant = "default" | "compact";
@ -15,6 +16,8 @@ type NodeHandleProps = Omit<HandleProps, "children"> & {
visible?: boolean; visible?: boolean;
}; };
const MAGNET_WRITE_EPSILON = 0.5;
function cx(...values: Array<string | false | null | undefined>) { function cx(...values: Array<string | false | null | undefined>) {
return values.filter(Boolean).join(" "); return values.filter(Boolean).join(" ");
} }
@ -91,11 +94,13 @@ export function NodeHandle({
const menu = useNodeHandleMenu(); const menu = useNodeHandleMenu();
const pointerDownRef = useRef<{ x: number; y: number } | null>(null); const pointerDownRef = useRef<{ x: number; y: number } | null>(null);
const handleRef = useRef<HTMLDivElement | null>(null); const handleRef = useRef<HTMLDivElement | null>(null);
const hitTargetRef = useRef<HTMLSpanElement | null>(null);
const iconRef = useRef<HTMLSpanElement | null>(null);
const magnetOffsetRef = useRef({ x: 0, y: 0, scale: "1" });
const [isZoneHovered, setIsZoneHovered] = useState(false); const [isZoneHovered, setIsZoneHovered] = useState(false);
const [magnetOffset, setMagnetOffset] = useState({ x: 0, y: 0 });
const isSelected = useWorkflowStore((state) => { const isSelected = useWorkflowStore((state) => {
if (!nodeId) return false; 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) => ( const isNodeHovered = useWorkflowStore((state) => (
nodeId ? state.hoveredNodeId === nodeId : false nodeId ? state.hoveredNodeId === nodeId : false
@ -146,6 +151,28 @@ export function NodeHandle({
height: INTERACTION_ZONE_SIZE, height: INTERACTION_ZONE_SIZE,
transform: zoneTransform, transform: zoneTransform,
} satisfies CSSProperties; } 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 snapVisualHandleToPointer = useCallback((clientX: number, clientY: number) => {
const rect = handleRef.current?.getBoundingClientRect(); const rect = handleRef.current?.getBoundingClientRect();
@ -174,30 +201,8 @@ export function NodeHandle({
return { x: pointerLocalX, y: pointerLocalY }; return { x: pointerLocalX, y: pointerLocalY };
})(); })();
setMagnetOffset({ applyMagnetOffset(clampedLocal.x - visualOffset.x, clampedLocal.y - visualOffset.y);
x: clampedLocal.x - visualOffset.x, }, [applyMagnetOffset, getZoom, position, visualOffset.x, visualOffset.y]);
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]);
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => { const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
pointerDownRef.current = { x: event.clientX, y: event.clientY }; pointerDownRef.current = { x: event.clientX, y: event.clientY };
@ -258,10 +263,11 @@ export function NodeHandle({
}} }}
onPointerLeave={() => { onPointerLeave={() => {
setIsZoneHovered(false); setIsZoneHovered(false);
setMagnetOffset({ x: 0, y: 0 }); applyMagnetOffset(0, 0);
}} }}
/> />
<span <span
ref={hitTargetRef}
aria-hidden="true" aria-hidden="true"
className={cx( className={cx(
"absolute left-1/2 top-1/2 block rounded-full opacity-0", "absolute left-1/2 top-1/2 block rounded-full opacity-0",
@ -269,10 +275,11 @@ export function NodeHandle({
)} )}
style={{ style={{
pointerEvents: "none", pointerEvents: "none",
transform: `translate(calc(-50% + ${visualOffset.x + magnetOffset.x}px), calc(-50% + ${visualOffset.y + magnetOffset.y}px))`, transform: visualTransform,
}} }}
/> />
<span <span
ref={iconRef}
aria-hidden="true" aria-hidden="true"
className={cx( className={cx(
"pointer-events-none absolute left-1/2 top-1/2 flex h-[25px] w-[25px] items-center justify-center leading-none transition-[transform,opacity,color] duration-300 ease-out" "pointer-events-none absolute left-1/2 top-1/2 flex h-[25px] w-[25px] items-center justify-center leading-none transition-[transform,opacity,color] duration-300 ease-out"
@ -280,7 +287,7 @@ export function NodeHandle({
style={{ style={{
color: "var(--handle-icon-color)", color: "var(--handle-icon-color)",
opacity: isVisible ? 1 : 0, opacity: isVisible ? 1 : 0,
transform: `translate(calc(-50% + ${visualOffset.x + magnetOffset.x}px), calc(-50% + ${visualOffset.y + magnetOffset.y}px)) scale(${magnetOffset.x !== 0 || magnetOffset.y !== 0 ? 1.08 : 1})`, transform: `${visualTransform} scale(var(--handle-magnet-scale, 1))`,
}} }}
> >
{icon ?? <DefaultHandleIcon />} {icon ?? <DefaultHandleIcon />}

Loading…
Cancel
Save