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