import { useRef, type PointerEvent as ReactPointerEvent } from "react"; import { useSchedulesStore } from "../state/schedulesStore"; import type { ScheduleNode } from "../../../services/api"; type DragState = { nodeId: string; pointerId: number; startClientX: number; startClientY: number; originX: number; originY: number; currentX: number; currentY: number; moved: boolean; nodeEl: HTMLDivElement; }; /** * 画布节点拖拽编排。内部持有瞬时 dragRef + 节点 DOM 引用,避免每次拖拽都触发 store * 重渲染:pointermove 期间直接改节点的 inline left/top(只动 DOM,不动 store); * 松手时把 rounded 后的位置一次性写入 store 的 setPositionDraft 模块级 Map(草稿 * 持久化)+ setSchedule(让 React 知道最终位置),刷新不丢。 */ export function useCanvasNodeDrag({ busy, linkSourceId, }: { busy: string | null | undefined; linkSourceId: string | null; }): { startDrag: (event: ReactPointerEvent, node: ScheduleNode) => void; moveDrag: (event: ReactPointerEvent) => void; finishDrag: (event: ReactPointerEvent) => void; } { const dragRef = useRef(null); const setSchedule = useSchedulesStore((s) => s.setSchedule); const setSelectedNodeId = useSchedulesStore((s) => s.setSelectedNodeId); const setSelectedEdgeId = useSchedulesStore((s) => s.setSelectedEdgeId); const startDrag = ( event: ReactPointerEvent, node: ScheduleNode, ): void => { if (event.button !== 0 || busy || linkSourceId) return; const target = event.target as HTMLElement; if (target.closest("button")) return; event.currentTarget.setPointerCapture(event.pointerId); dragRef.current = { nodeId: node.node_id, pointerId: event.pointerId, startClientX: event.clientX, startClientY: event.clientY, originX: node.position_x, originY: node.position_y, currentX: node.position_x, currentY: node.position_y, moved: false, nodeEl: event.currentTarget, }; setSelectedNodeId(node.node_id); setSelectedEdgeId(null); }; const moveDrag = (event: ReactPointerEvent): void => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; const deltaX = event.clientX - drag.startClientX; const deltaY = event.clientY - drag.startClientY; if (Math.abs(deltaX) + Math.abs(deltaY) > 3) drag.moved = true; const x = Math.max(10, drag.originX + deltaX); const y = Math.max(10, drag.originY + deltaY); drag.currentX = x; drag.currentY = y; // 只写 DOM,不动 store;拖拽期间 SchedulePage 不会因此重渲染。 drag.nodeEl.style.left = `${x}px`; drag.nodeEl.style.top = `${y}px`; }; const finishDrag = (event: ReactPointerEvent): void => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; dragRef.current = null; if (!drag.moved) return; const position = { position_x: Math.round(drag.currentX), position_y: Math.round(drag.currentY), }; // 先记草稿(持久化),再把最终位置同步进 store 让节点 div 的 inline style 与 // schedule.nodes 一致(下次重渲染/保存时不会出现位置回弹)。 useSchedulesStore.getState().setPositionDraft(drag.nodeId, position); setSchedule((value) => { if (!value) return value; return { ...value, nodes: value.nodes.map((item) => item.node_id === drag.nodeId ? { ...item, ...position } : item, ), }; }); }; return { startDrag, moveDrag, finishDrag }; }