React游戏开发:拖拽元素落点与光标位置偏移问题求解
解决React原生拖拽元素落点与光标偏移问题
问题描述
使用React开发一款带拖拽功能的游戏,核心包含SideBar和GameBoard组件,SideBar内的元素可拖拽至GameBoard。目前拖拽功能可正常运行,但元素落点与光标位置存在偏移。尝试过多种无依赖方案均未解决,不想依赖第三方库。
完整代码
App.tsx(状态管理)
// App.tsx import React, { useState } from "react"; import GameBoard from "../src/components/GameBoard/GameBoard"; import SideBar from "../src/components/SideBar/SideBar"; import { ElementData } from "./Types"; function App() { const [draggedElement, setDraggedElement] = useState<ElementData | null>(null); return ( <div className="min-h-screen flex flex-col md:flex-row bg-gray-100"> <div className="md:w-1/5 bg-fuchsia-50 p-4"> <h1 className="text-xl font-bold mb-4">Infinite Things ♾️</h1> <SideBar setDraggedElement={setDraggedElement} /> </div> <div className="md:w-4/5 bg-amber-50 flex-grow"> <GameBoard draggedElement={draggedElement} /> </div> </div> ); } export default App;
GameBoard.tsx
import React, { useState } from "react"; import Element from "../Element"; import { ElementData } from "../../Types"; interface GameBoardProps { draggedElement: ElementData | null; } const GameBoard: React.FC<GameBoardProps> = ({ draggedElement }) => { const [elements, setElements] = useState<ElementData[]>([ { id: "Water", dx: 0, dy: 0, color: "#3498db" }, { id: "Fire", dx: 0, dy: 0, color: "#e74c3c" }, { id: "Wind", dx: 0, dy: 0, color: "#2ecc71" }, ]); const [draggingElement, setDraggingElement] = useState<ElementData | null>(null); const handleDrop = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); e.currentTarget.style.border = "none"; if (draggedElement) { const isExternalDrop = !draggingElement; if (isExternalDrop) { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const newElement = { ...draggedElement, dx: x, dy: y }; setElements((prevElements) => [...prevElements, newElement]); } else { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const updatedElement = { ...draggingElement, dx: x, dy: y }; setElements((prevElements) => prevElements.map((el) => (el.id === draggingElement.id ? updatedElement : el)) ); } } setDraggingElement(null); }; const handleDragStart = ( e: React.DragEvent<HTMLDivElement>, element: ElementData ) => { console.log("drag start", element.id); setDraggingElement(element); e.dataTransfer.setData("elementId", element.id.toString()); }; const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); if (draggingElement) { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setDraggingElement((prevElement) => { if (prevElement) { return { ...prevElement, dx: x, dy: y }; } return prevElement; }); } }; const handleDragEnd = () => { setDraggingElement(null); }; const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); e.currentTarget.style.border = "2px dashed #333"; if (!draggingElement && draggedElement) { console.log("drag enter", draggedElement?.id); setDraggingElement(draggedElement); } }; const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => { e.currentTarget.style.border = "none"; }; return ( <div className="game-board bg-gray-200 relative h-full p-4" onDrop={handleDrop} onDragOver={handleDragOver} onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragEnd={handleDragEnd} > <h2 className="text-xl font-bold mb-4">Game Board</h2> <div className="min-w-0 absolute"> {elements.map((element) => ( <Element key={element.id} element={element} onDragStart={(e) => { e.dataTransfer.setData("elementId", element.id.toString()); handleDragStart(e, element); }} /> ))} </div> </div> ); }; export default GameBoard;
SideBar.tsx
// SideBar.tsx import React from "react"; import Element from "../Element"; import { ElementData } from "../../Types"; interface SideBarProps { setDraggedElement: React.Dispatch<React.SetStateAction<ElementData | null>>; } const SideBar: React.FC<SideBarProps> = ({ setDraggedElement }) => { const elements: ElementData[] = [ { id: "Water", dx: 0, dy: 0, color: "#3498db" }, { id: "Fire", dx: 0, dy: 0, color: "#e74c3c" }, { id: "Wind", dx: 0, dy: 0, color: "#2ecc71" }, ]; const handleDragStart = ( e: React.DragEvent<HTMLDivElement>, element: ElementData ) => { console.log("drag start in Sidebar for", element.id); setDraggedElement(element); }; return ( <div className="sidebar"> <h2 className="text-xl font-bold mb-4">Side Bar</h2> <div className="grid grid-cols-2 gap-4"> {elements.map((element) => ( <Element key={element.id} element={element} onDragStart={(e) => handleDragStart(e, element)} /> ))} </div> </div> ); }; export default SideBar;
Element.tsx
// Element.tsx import React from "react"; import { ElementData } from "../Types"; interface ElementProps { element: ElementData; onDragStart: (e: React.DragEvent<HTMLDivElement>, element: ElementData) => void; } function Element({ onDragStart, element }: ElementProps) { return ( <div className={`element p-4 min-w-4 text-white text-center font-bold rounded-2xl shadow-lg cursor-pointer`} onDragStart={(e) => { onDragStart(e, element); }} style={{ background: `${element.color}`, transform: `translate(${element.dx}px, ${element.dy}px)`, }} draggable > {element.id} </div> ); } export default Element;
解决方案
偏移核心原因:直接将光标位置作为元素translate的目标坐标,但元素定位原点是自身左上角,而拖拽时光标点击的是元素内部某点,导致落点偏移。解决思路是记录拖拽起始时光标在元素内的相对位置(偏移量),放置时用光标位置减去该偏移量,让元素的点击锚点与光标重合。
步骤1:更新类型定义
在Types.ts中给ElementData添加偏移量字段:
export interface ElementData { id: string; dx: number; dy: number; color: string; offsetX?: number; offsetY?: number; }
步骤2:记录拖拽起始偏移量
修改GameBoard的handleDragStart:
const handleDragStart = ( e: React.DragEvent<HTMLDivElement>, element: ElementData ) => { console.log("drag start", element.id); // 记录光标在元素内的相对位置 const offsetX = e.nativeEvent.offsetX; const offsetY = e.nativeEvent.offsetY; setDraggingElement({ ...element, offsetX, offsetY }); e.dataTransfer.setData("elementId", element.id.toString()); };
修改SideBar的handleDragStart:
const handleDragStart = ( e: React.DragEvent<HTMLDivElement>, element: ElementData ) => { console.log("drag start in Sidebar for", element.id); const offsetX = e.nativeEvent.offsetX; const offsetY = e.nativeEvent.offsetY; setDraggedElement({ ...element, offsetX, offsetY }); };
步骤3:修正落点计算
修改GameBoard的handleDrop中坐标计算逻辑:
// 外部拖拽分支 const rect = e.currentTarget.getBoundingClientRect(); // 光标位置减去偏移量,得到元素左上角的正确位置 const x = e.clientX - rect.left - (draggedElement.offsetX || 0); const y = e.clientY - rect.top - (draggedElement.offsetY || 0); // 内部拖拽分支 const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left - (draggingElement.offsetX || 0); const y = e.clientY - rect.top - (draggingElement.offsetY || 0);
步骤4:修正拖拽实时位置
修改GameBoard的handleDragOver:
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); if (draggingElement) { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setDraggingElement((prevElement) => { if (prevElement) { return { ...prevElement, dx: x - (prevElement.offsetX || 0), dy: y - (prevElement.offsetY || 0) }; } return prevElement; }); } };
完成以上修改后,元素的落点会精准对齐光标位置,且无需依赖任何第三方库。
内容的提问来源于stack exchange,提问作者Mahesh Jamdade
相关产品推荐
相关产品推荐

