Files
model-platform/frontend/app/features/schedules/hooks/useCanvasNodeDrag.ts
T

103 lines
3.7 KiB
TypeScript

import { useRef, type PointerEvent as ReactPointerEvent } from "react";
import { useSchedulesStore } from "../state/useSchedulesStore";
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<HTMLDivElement>, node: ScheduleNode) => void;
moveDrag: (event: ReactPointerEvent<HTMLDivElement>) => void;
finishDrag: (event: ReactPointerEvent<HTMLDivElement>) => void;
} {
const dragRef = useRef<DragState | null>(null);
const setSchedule = useSchedulesStore((s) => s.setSchedule);
const setSelectedNodeId = useSchedulesStore((s) => s.setSelectedNodeId);
const setSelectedEdgeId = useSchedulesStore((s) => s.setSelectedEdgeId);
const startDrag = (
event: ReactPointerEvent<HTMLDivElement>,
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<HTMLDivElement>): 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<HTMLDivElement>): 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 };
}