149 lines
3.8 KiB
TypeScript
149 lines
3.8 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import { useScale } from "@/contexts/ScaleContext";
|
|
|
|
interface DragState<T = any> {
|
|
isDragging: boolean;
|
|
dragData: T | null;
|
|
mousePosition: { x: number; y: number };
|
|
}
|
|
|
|
/**
|
|
* 自定义拖拽 Hook
|
|
* 支持页面 scale 缩放的拖拽功能
|
|
*/
|
|
export function useDragAndDrop<T = any>() {
|
|
const { scale, offsetX, offsetY } = useScale();
|
|
const [state, setState] = useState<DragState<T>>({
|
|
isDragging: false,
|
|
dragData: null,
|
|
mousePosition: { x: 0, y: 0 },
|
|
});
|
|
const isDraggingRef = useRef(false);
|
|
|
|
// 开始拖拽
|
|
const startDrag = useCallback((data: T, event?: React.MouseEvent) => {
|
|
if (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
// 立即获取鼠标初始位置
|
|
isDraggingRef.current = true;
|
|
setState({
|
|
isDragging: true,
|
|
dragData: data,
|
|
mousePosition: { x: event.clientX, y: event.clientY },
|
|
});
|
|
} else {
|
|
isDraggingRef.current = true;
|
|
setState({
|
|
isDragging: true,
|
|
dragData: data,
|
|
mousePosition: { x: 0, y: 0 },
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
// 结束拖拽
|
|
const endDrag = useCallback(() => {
|
|
isDraggingRef.current = false;
|
|
setState({
|
|
isDragging: false,
|
|
dragData: null,
|
|
mousePosition: { x: 0, y: 0 },
|
|
});
|
|
}, []);
|
|
|
|
// 监听鼠标移动 - 使用独立的事件监听器
|
|
useEffect(() => {
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
if (!isDraggingRef.current) return;
|
|
setState((prev) => ({
|
|
...prev,
|
|
mousePosition: { x: e.clientX, y: e.clientY },
|
|
}));
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
// 拖拽结束的逻辑由外部处理
|
|
// 这里只是提供一个安全机制
|
|
};
|
|
|
|
// 始终添加事件监听器,在handleMouseMove内部检查拖拽状态
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
window.addEventListener("mouseup", handleMouseUp);
|
|
|
|
return () => {
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
window.removeEventListener("mouseup", handleMouseUp);
|
|
};
|
|
}, []); // 空依赖数组,只在组件挂载/卸载时执行
|
|
|
|
// 将屏幕坐标转换为缩放后的页面坐标
|
|
const screenToScaledCoords = useCallback(
|
|
(screenX: number, screenY: number) => {
|
|
return {
|
|
x: (screenX - offsetX) / scale,
|
|
y: (screenY - offsetY) / scale,
|
|
};
|
|
},
|
|
[scale, offsetX, offsetY]
|
|
);
|
|
|
|
// 检查点是否在元素内
|
|
const isPointInElement = useCallback(
|
|
(pointX: number, pointY: number, element: HTMLElement | null): boolean => {
|
|
if (!element) return false;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
return (
|
|
pointX >= rect.left &&
|
|
pointX <= rect.right &&
|
|
pointY >= rect.top &&
|
|
pointY <= rect.bottom
|
|
);
|
|
},
|
|
[]
|
|
);
|
|
|
|
return {
|
|
isDragging: state.isDragging,
|
|
dragData: state.dragData,
|
|
mousePosition: state.mousePosition,
|
|
startDrag,
|
|
endDrag,
|
|
screenToScaledCoords,
|
|
isPointInElement,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 放置区域 Hook
|
|
* 用于检测拖拽元素是否悬停在放置区域上
|
|
*/
|
|
export function useDropZone() {
|
|
const dropZoneRef = useRef<HTMLDivElement>(null);
|
|
const [isOver, setIsOver] = useState(false);
|
|
|
|
// 检查鼠标是否在放置区域内
|
|
const checkIsOver = useCallback((mouseX: number, mouseY: number) => {
|
|
if (!dropZoneRef.current) return false;
|
|
|
|
const rect = dropZoneRef.current.getBoundingClientRect();
|
|
const over =
|
|
mouseX >= rect.left &&
|
|
mouseX <= rect.right &&
|
|
mouseY >= rect.top &&
|
|
mouseY <= rect.bottom;
|
|
|
|
// 只有当状态真正改变时才更新
|
|
setIsOver((prev) => (prev !== over ? over : prev));
|
|
return over;
|
|
}, []);
|
|
|
|
return {
|
|
dropZoneRef,
|
|
isOver,
|
|
checkIsOver,
|
|
};
|
|
}
|