实现图片拖动、鼠标中心点缩放、文字层跟随功能

功能: 地图图片自由鼠标拖动 以鼠标当前位置为中心点缩放 图片上方自定义建筑文字层 文字层跟随图片同步缩放、定位 效果:纯 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;
相关推荐
|晴 天|2 小时前
前端项目多平台部署:GitHub Pages + Vercel + Cloudflare Pages 实战教程
前端·javascript·vue.js
ZC跨境爬虫2 小时前
UI前端美化技能提升日志day2:图片优化、字体本地化与设计美感解析
前端·javascript·ui·状态模式
yivifu2 小时前
接近完善的HTML双行夹批显示方案
前端·javascript·html·html双行夹批
M ? A2 小时前
Vue转React终极指南:VuReact全特性语义对照
前端·javascript·vue.js·react.js·面试·开源·vureact
捧月华如2 小时前
HTML/CSS基础:构建网页的骨架与样式
前端·css·html
weixin199701080162 小时前
《比比网商品详情页前端性能优化实战》
前端
bcbobo21cn2 小时前
Three.js绘制三角形网格平面
前端·javascript·平面·三角形面·基础材质
rleS IONS2 小时前
分布式WEB应用中会话管理的变迁之路
前端·分布式
就叫飞六吧2 小时前
在线考试翻页抓取题目导出js
开发语言·前端·javascript