fix: hook
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
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;
|
||||||
|
moved: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 画布节点拖拽编排。内部持有瞬时 dragRef,避免每次拖拽都触发 store 重渲染。
|
||||||
|
* 拖拽过程中直接改 schedule.nodes 的 position;松手时把 rounded 后的位置写到 store 的
|
||||||
|
* setPositionDraft 模块级 Map,刷新不丢。
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
moved: false,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
setSchedule((current) => {
|
||||||
|
if (!current) return current;
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
nodes: current.nodes.map((item) =>
|
||||||
|
item.node_id === drag.nodeId
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
position_x: Math.max(10, drag.originX + deltaX),
|
||||||
|
position_y: Math.max(10, drag.originY + deltaY),
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 current = useSchedulesStore.getState().schedule;
|
||||||
|
const node = current?.nodes.find((item) => item.node_id === drag.nodeId);
|
||||||
|
if (!current || !node) return;
|
||||||
|
const position = {
|
||||||
|
position_x: Math.round(node.position_x),
|
||||||
|
position_y: Math.round(node.position_y),
|
||||||
|
};
|
||||||
|
useSchedulesStore.getState().setPositionDraft(node.node_id, position);
|
||||||
|
setSchedule((value) => {
|
||||||
|
if (!value) return value;
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
nodes: value.nodes.map((item) =>
|
||||||
|
item.node_id === node.node_id ? { ...item, ...position } : item,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { startDrag, moveDrag, finishDrag };
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSchedulesStore } from "../state/schedulesStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听全局 pointerdown / blur / resize / scroll / Escape,关闭当前打开的右键菜单。
|
||||||
|
* 菜单的 open/close 生命周期归菜单自己管——页面不再持有这段 effect。
|
||||||
|
*/
|
||||||
|
export function useContextMenuDismiss(): void {
|
||||||
|
const contextMenu = useSchedulesStore((s) => s.contextMenu);
|
||||||
|
const setContextMenu = useSchedulesStore((s) => s.setContextMenu);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!contextMenu) return undefined;
|
||||||
|
const close = (): void => setContextMenu(null);
|
||||||
|
const onKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
if (event.key === "Escape") close();
|
||||||
|
};
|
||||||
|
window.addEventListener("pointerdown", close);
|
||||||
|
window.addEventListener("blur", close);
|
||||||
|
window.addEventListener("resize", close);
|
||||||
|
window.addEventListener("scroll", close, true);
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointerdown", close);
|
||||||
|
window.removeEventListener("blur", close);
|
||||||
|
window.removeEventListener("resize", close);
|
||||||
|
window.removeEventListener("scroll", close, true);
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
|
}, [contextMenu, setContextMenu]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user