fix: hook

This commit is contained in:
tao.chen
2026-08-07 20:42:36 +08:00
parent 567786215e
commit b00a0e9ae1
2 changed files with 135 additions and 0 deletions
@@ -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]);
}