From 0f7e76d18b3f343ee9835d7c227bb1167a519154 Mon Sep 17 00:00:00 2001 From: Zack <43022758+bahamutww@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:24:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor(canvas):=20=E8=A7=84=E8=8C=83=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E4=BA=8B=E4=BB=B6=E7=B1=BB=E5=9E=8B=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=8B=96=E6=8B=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一将拖拽事件类型改为原生 MouseEvent | TouchEvent,防止类型签名漂移 - 新增 getDragEventPoint 以兼容 TouchEvent 的坐标与按键提取 - 优化多选拖拽判断,支持 Ctrl/meta 键及多节点选中状态 - 调整拖拽回调函数的参数类型,明确 CanvasNode 泛型 - 更新拖拽检测逻辑,提升 AI 面板拖拽悬停状态的准确性 - 修改 useCanvasSnapping 中的拖拽事件参数类型以确保一致性 --- .../theater/[id]/hooks/useCanvasSnapping.ts | 2 +- .../app/theater/[id]/hooks/useNodeDragToAI.ts | 27 +++++++++++++++---- frontend/src/app/theater/[id]/page.tsx | 16 ++++++----- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/theater/[id]/hooks/useCanvasSnapping.ts b/frontend/src/app/theater/[id]/hooks/useCanvasSnapping.ts index 03be42bf..1936943e 100644 --- a/frontend/src/app/theater/[id]/hooks/useCanvasSnapping.ts +++ b/frontend/src/app/theater/[id]/hooks/useCanvasSnapping.ts @@ -9,7 +9,7 @@ export function useCanvasSnapping(snapToGuides: boolean) { horizontal: number | null; }>({ vertical: null, horizontal: null }); - const onNodeDrag = useCallback((_: React.MouseEvent, node: Node) => { + const onNodeDrag = useCallback((_: MouseEvent | TouchEvent, node: Node) => { if (!snapToGuides) { setAlignmentLines({ vertical: null, horizontal: null }); return; diff --git a/frontend/src/app/theater/[id]/hooks/useNodeDragToAI.ts b/frontend/src/app/theater/[id]/hooks/useNodeDragToAI.ts index 0bbd4674..02f81956 100644 --- a/frontend/src/app/theater/[id]/hooks/useNodeDragToAI.ts +++ b/frontend/src/app/theater/[id]/hooks/useNodeDragToAI.ts @@ -14,6 +14,21 @@ function isPointInRect(x: number, y: number, rect: DOMRect): boolean { return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; } +/** + * 归一化 xyflow 拖拽事件(原生 MouseEvent | TouchEvent) + * TouchEvent 没有 clientX/clientY/ctrlKey,需从 changedTouches 提取 + */ +function getDragEventPoint(event: MouseEvent | TouchEvent): { clientX: number; clientY: number; ctrlKey: boolean; metaKey: boolean } { + const touch = 'touches' in event ? (event.changedTouches[0] ?? event.touches[0]) : undefined; + const source = (touch ?? event) as MouseEvent; + return { + clientX: source.clientX, + clientY: source.clientY, + ctrlKey: 'ctrlKey' in event ? event.ctrlKey : false, + metaKey: 'metaKey' in event ? event.metaKey : false, + }; +} + /** * 画布节点拖拽到 AI 面板的检测 hook * 支持多选节点拖拽(最多5个图像节点) @@ -29,9 +44,10 @@ export function useNodeDragToAI() { // 当前拖拽的节点ID列表 const draggedNodeIdsRef = useRef([]); - const onNodeDragStart = useCallback((event: React.MouseEvent, node: Node, nodes: Node[]) => { + const onNodeDragStart = useCallback((event: MouseEvent | TouchEvent, node: Node, nodes: Node[]) => { // 判断是否多选:按住 Ctrl 或节点已被选中且选中有多个节点 - const isMultiSelect = event.ctrlKey || event.metaKey || + const { ctrlKey, metaKey } = getDragEventPoint(event); + const isMultiSelect = ctrlKey || metaKey || (node.selected && nodes.filter(n => n.selected).length > 1); // 获取所有正在拖拽的节点 @@ -51,14 +67,15 @@ export function useNodeDragToAI() { isOverPanelRef.current = false; }, []); - const onNodeDrag = useCallback((event: React.MouseEvent, _node: Node) => { + const onNodeDrag = useCallback((event: MouseEvent | TouchEvent, _node: Node) => { const rect = panelRectRef.current; // 面板不存在(未打开)时,尝试检测关闭态按钮区域 const panelEl = document.querySelector(AI_PANEL_SELECTOR); const currentRect = panelEl?.getBoundingClientRect() ?? rect; currentRect && (panelRectRef.current = currentRect); - const isOver = !!currentRect && isPointInRect(event.clientX, event.clientY, currentRect); + const { clientX, clientY } = getDragEventPoint(event); + const isOver = !!currentRect && isPointInRect(clientX, clientY, currentRect); // 状态变化时才更新 store,减少渲染 const prev = isOverPanelRef.current; @@ -66,7 +83,7 @@ export function useNodeDragToAI() { (isOver !== prev) && useAIAssistantStore.getState().setIsDragOverPanel(isOver); }, []); - const onNodeDragStop = useCallback((_: React.MouseEvent, node: Node, nodes: Node[]) => { + const onNodeDragStop = useCallback((_: MouseEvent | TouchEvent, node: Node, nodes: Node[]) => { const wasOverPanel = isOverPanelRef.current; // 重置拖拽悬停状态 diff --git a/frontend/src/app/theater/[id]/page.tsx b/frontend/src/app/theater/[id]/page.tsx index 4ab68c57..5ab78949 100644 --- a/frontend/src/app/theater/[id]/page.tsx +++ b/frontend/src/app/theater/[id]/page.tsx @@ -15,11 +15,12 @@ import { ConnectionMode, BackgroundVariant, NodeTypes, + type OnNodeDrag, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { ChevronRight } from 'lucide-react'; -import { useCanvasStore } from '@/store/useCanvasStore'; +import { useCanvasStore, type CanvasNode } from '@/store/useCanvasStore'; import { Sidebar } from '@/components/canvas/Sidebar'; import { ZoomControls } from '@/components/canvas/ZoomControls'; import ScriptNode from '@/components/canvas/TextNode'; @@ -129,21 +130,22 @@ function InfiniteCanvas() { const { onNodeDragStart: onAIDragStart, onNodeDrag: onAIDrag, onNodeDragStop: onAIDragStop } = useNodeDragToAI(); // --- Composed drag callbacks: snapping + AI panel detection --- - const composedOnNodeDragStart = useCallback( - (event: React.MouseEvent, node: any, nodes: any[]) => { + // xyflow 拖拽事件为原生 MouseEvent | TouchEvent,显式标注 OnNodeDrag 防止签名漂移 + const composedOnNodeDragStart = useCallback>( + (event, node, nodes) => { onAIDragStart(event, node, nodes); }, [onAIDragStart] ); - const composedOnNodeDrag = useCallback( - (event: React.MouseEvent, node: any, nodes: any[]) => { + const composedOnNodeDrag = useCallback>( + (event, node) => { onSnappingDrag(event, node); onAIDrag(event, node); }, [onSnappingDrag, onAIDrag] ); - const composedOnNodeDragStop = useCallback( - (event: React.MouseEvent, node: any, nodes: any[]) => { + const composedOnNodeDragStop = useCallback>( + (event, node, nodes) => { onSnappingDragStop(); onAIDragStop(event, node, nodes); },