功能: 地图图片自由鼠标拖动 以鼠标当前位置为中心点缩放 图片上方自定义建筑文字层 文字层跟随图片同步缩放、定位 效果:纯 CSS + JS 实现,不依赖地图库,性能流畅,文字永远和地图位置绑定。
dart
import React, { useState, useRef, useEffect } from 'react';
import BW from './img/泊位.png';
import BWgif from './img/泊位图.gif';
const MapViewer = () => {
// 地图容器DOM引用,用于获取容器尺寸、边界、绑定事件
const containerRef = useRef(null);
// 地图内容层DOM引用(图片+文字共同的父元素)
const mapContentRef = useRef(null);
// 缩放比例:默认1倍
const [scale, setScale] = useState(1);
// 地图偏移位置:控制拖动的X、Y坐标
const [position, setPosition] = useState({ x: 0, y: 0 });
// 鼠标在地图容器内的实时坐标(用于右上角显示)
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
// 是否正在拖动(使用ref,不触发重渲染,性能更好)
const isDragging = useRef(false);
// 记录拖动开始时的鼠标起点位置
const startPos = useRef({ x: 0, y: 0 });
// 记录地图当前最新位置(同步state,用于缩放计算)
const currentPos = useRef({ x: 0, y: 0 });
// ------------------------------
// 鼠标按下事件:开始拖动
// ------------------------------
const handleMouseDown = (e) => {
// 标记开始拖动
isDragging.current = true;
// 记录起点:鼠标位置 - 当前地图偏移量
startPos.current = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
// 鼠标样式变为抓取中
containerRef.current.style.cursor = 'grabbing';
};
// ------------------------------
// 全局鼠标监听:拖动 + 实时鼠标位置
// ------------------------------
useEffect(() => {
const handleMouseMove = (e) => {
// -------- 1. 实时更新鼠标在容器内的坐标 --------
if (containerRef.current) {
// 获取容器边界
// 容器顶部 距离 浏览器窗口顶部 的距离
// 容器左侧 距离 浏览器窗口左侧 的距离
const rect = containerRef.current.getBoundingClientRect();
// 计算鼠标相对于容器的X、Y(取整)
setMousePosition({
x: (e.clientX - rect.left).toFixed(0),
y: (e.clientY - rect.top).toFixed(0),
});
}
// -------- 2. 处理拖动逻辑 --------
if (!isDragging.current) return; // 没在拖动直接退出
// 计算新的偏移位置
const newX = e.clientX - startPos.current.x;
const newY = e.clientY - startPos.current.y;
// 更新位置
setPosition({ x: newX, y: newY });
// 同步到ref,给缩放使用
currentPos.current = { x: newX, y: newY };
};
// 鼠标松开:结束拖动
const handleMouseUp = () => {
isDragging.current = false;
containerRef.current.style.cursor = 'grab';
};
// 绑定全局事件
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
// 组件销毁时清除事件,防止内存泄漏
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [position]); // 依赖position,保证位置计算正确
// ------------------------------
// 鼠标滚轮:以鼠标位置为中心缩放(核心功能)
// ------------------------------
const handleWheel = (e) => {
// 阻止浏览器默认滚动行为
e.preventDefault();
const container = containerRef.current;
if (!container) return;
// 获取容器边界
const rect = container.getBoundingClientRect();
// 鼠标相对于容器的坐标
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 计算新缩放:滚轮向上+0.1,向下-0.1
const delta = e.deltaY > 0 ? -0.1 : 0.1;
// 限制缩放范围:最小0.5,最大3倍
const newScale = Math.min(Math.max(0.5, scale + delta), 3);
// 关键算法:让缩放中心点 = 鼠标当前位置
const scaleRatio = newScale / scale;
const newX = mouseX - (mouseX - currentPos.current.x) * scaleRatio;
const newY = mouseY - (mouseY - currentPos.current.y) * scaleRatio;
// 更新缩放和位置
setScale(newScale);
setPosition({ x: newX, y: newY });
currentPos.current = { x: newX, y: newY };
};
// ------------------------------
// 地图建筑文字标记(可自行增删改)
// x、y是相对于图片的绝对坐标
// ------------------------------
const mapMarkers = [
{ id: 1, x: 540, y: 392, text: '泊位1' },
{ id: 2, x: 540, y: 500, text: '泊位2' },
{ id: 3, x: 540, y: 690, text: '泊位3' },
{ id: 4, x: 134, y: 132, text: '泊位4' },
];
// ------------------------------
// 页面渲染
// ------------------------------
return (
<div style={{ width: '100vw', height: '100vh', margin: 0, padding: 0, overflow: 'hidden' }}>
{/* 地图容器:可视区域,监听拖动和滚轮 */}
<div
ref={containerRef}
onMouseDown={handleMouseDown}
onWheel={handleWheel}
style={{
width: '100%',
height: '100%',
background: '#f0f0f0',
overflow: 'hidden',
position: 'relative',
cursor: 'grab', // 默认鼠标样式
}}
>
{/* 右上角:实时定位信息面板 */}
<div
style={{
position: 'absolute',
top: 16,
right: 214,
background: 'rgba(0,0,0,0.7)',
color: '#fff',
padding: '10px 14px',
borderRadius: 8,
fontSize: 14,
zIndex: 999, // 确保在最上层
userSelect: 'none',
pointerEvents: 'none', // 不干扰鼠标操作
minWidth: 220,
}}
>
<div>
鼠标坐标:X {mousePosition.x} / Y {mousePosition.y}
</div>
<div>
地图偏移:X {position.x.toFixed(0)} / Y {position.y.toFixed(0)}
</div>
<div>缩放比例:{scale.toFixed(2)}</div>
</div>
{/* 地图内容层:图片 + 文字层,统一进行位移和缩放 */}
<div
ref={mapContentRef}
style={{
position: 'absolute',
// 核心样式:拖动 + 缩放
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
transformOrigin: '0 0', // 左上角为变换原点,配合鼠标缩放算法
transition: 'transform 0.1s ease', // 缩放平滑过渡
}}
>
{/* 地图底图 */}
<img
// src="https://picsum.photos/id/1016/800/600"
src={BW}
alt="地图"
style={{
width: 1720,
height: 812,
userSelect: 'none', // 禁止选中图片
pointerEvents: 'none', // 不干扰鼠标事件
}}
/>
{/* 文字标记层:跟随地图同步缩放、定位 */}
{mapMarkers.map((marker) => (
<div
key={marker.id}
style={{
position: 'absolute',
left: marker.x, // 文字X坐标
top: marker.y, // 文字Y坐标
transform: 'translate(-50%, -50%)', // 中心点对齐
fontSize: 18,
fontWeight: 'bold',
color: '#fff',
backgroundColor: 'rgba(0,0,0,0.6)',
padding: '4px 8px',
borderRadius: 4,
whiteSpace: 'nowrap',
pointerEvents: 'none',
}}
>
{marker.text}
</div>
))}
</div>
</div>
</div>
);
};
export default MapViewer;