React MoveBoxComponent

npm i react-spring

import React, { useState, useRef, useEffect } from "react";
import { useSpring, animated } from "@react-spring/web";

interface IPosition {
  x: number;
  y: number;
}

interface IBoxSize {
  width: number;
  height: number;
}

// 检查是否应该将窗口贴边或中间移动
const checkSnapPosition = (
  containerBoxSize: IBoxSize,
  moveBoxSize: IBoxSize,
  pos: IPosition,
  snapMode: "always" | "smart" = "smart"
): IPosition => {
  const sideMargin = 21; // 边缘的最小间距
  const containerWidth = containerBoxSize.width;
  const containerHeight = containerBoxSize.height;
  const moveBoxWidth = moveBoxSize.width;
  const moveBoxHeight = moveBoxSize.height;

  // 计算安全边界
  const maxX = containerWidth - moveBoxWidth - sideMargin;
  const maxY = containerHeight - moveBoxHeight;
  const posX = Math.min(Math.max(pos.x, sideMargin), maxX);
  const posY = Math.min(Math.max(pos.y, 0), maxY);

  // 始终贴边模式
  if (snapMode === "always") {
    const distanceToLeft = posX;
    const distanceToRight = containerWidth - posX - moveBoxWidth;
    const distanceToTop = posY;
    const distanceToBottom = containerHeight - posY - moveBoxHeight;

    const minDistance = Math.min(
      distanceToLeft,
      distanceToRight,
      distanceToTop,
      distanceToBottom
    );

    if (minDistance === distanceToLeft) return { x: sideMargin, y: posY };
    if (minDistance === distanceToRight) return { x: maxX, y: posY };
    if (minDistance === distanceToTop) return { x: posX, y: sideMargin };
    return { x: posX, y: containerHeight - moveBoxHeight };
  }

  // 智能贴边模式
  const SNAP_THRESHOLD = 100; // 中间区域阈值
  const posCoefficient = containerHeight / containerWidth;

  const isInMiddleX = posX > SNAP_THRESHOLD && posX < maxX - SNAP_THRESHOLD;
  const isInMiddleY = posY > SNAP_THRESHOLD && posY < maxY - SNAP_THRESHOLD;

  if (isInMiddleX && isInMiddleY) {
    return { x: posX, y: posY };
  }

  const isLeftDown = posY / posX > posCoefficient;
  const isRightDown = posY / (containerWidth - posX) > posCoefficient;

  if (isLeftDown && isRightDown) {
    return { x: posX, y: containerHeight - moveBoxHeight - sideMargin };
  }
  if (isLeftDown) return { x: sideMargin, y: posY };
  if (isRightDown) return { x: maxX, y: posY };

  return { x: posX, y: sideMargin };
};

function MoveBoxComponent() {
  const [position, setPosition] = useState<IPosition>({ x: 0, y: 0 }); // moveBox 的位置
  const isDragging = useRef<boolean>(false); // 是否正在拖动
  const offset = useRef({ x: 0, y: 0 }); // 鼠标按下时的偏移量
  const containerBoxSize = { width: 500, height: 500 }; // container的尺寸
  const moveBoxSize = { width: 64, height: 64 }; // moveBox的尺寸

  // 使用 react-spring 创建动画
  const [springProps, api] = useSpring(() => ({}));

  useEffect(() => {
    initBoxPosition();
  }, []);

  const initBoxPosition = async () => {
    const screenSize = {
      width: window.screen.availWidth,
      height: window.screen.availHeight,
    };
    // 初始位置设置在右下角贴边
    const pos = {
      x: screenSize.width,
      y: screenSize.height,
    };
    const snapPosition = checkSnapPosition(containerBoxSize, moveBoxSize, pos);
    // 更新位置
    setPosition(snapPosition);
    // 使用动画效果移动到初始位置
    api.start({
      from: { x: 0, y: 0 },
      to: snapPosition,
      config: { tension: 300, friction: 130 },
    });
  };

  // 处理鼠标按下事件,初始化拖动
  const handleMouseDown = (e: React.MouseEvent) => {
    isDragging.current = true;

    // 记录鼠标按下时的偏移
    offset.current = {
      x: e.clientX - position.x,
      y: e.clientY - position.y,
    };

    // 禁用选中文本,防止拖动时文本被选择
    document.body.style.userSelect = "none";

    // 添加 mousemove 和 mouseup 事件监听
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseup", handleMouseUp);
  };

  // 处理鼠标移动事件,更新 moveBox 的位置
  const handleMouseMove = (e: MouseEvent) => {
    if (!isDragging.current) return;
    const pos = {
      x: e.clientX - offset.current.x,
      y: e.clientY - offset.current.y,
    };
    // 更新位置
    setPosition(pos);
    // 使用动画效果
    api.start({
      from: pos,
      to: pos,
      config: { tension: 300, friction: 50 },
    });
  };

  // 处理鼠标松开事件,停止拖动
  const handleMouseUp = (e: MouseEvent) => {
    isDragging.current = false;

    const pos = {
      x: e.clientX - offset.current.x,
      y: e.clientY - offset.current.y,
    };
    // 检查是否需要贴边或吸附
    const snapPosition = checkSnapPosition(containerBoxSize, moveBoxSize, pos);
    // 更新位置
    setPosition(snapPosition);
    // 使用动画效果
    api.start({
      from: pos,
      to: snapPosition,
      config: { tension: 300, friction: 50 },
    });

    // 移除事件监听
    window.removeEventListener("mousemove", handleMouseMove);
    window.removeEventListener("mouseup", handleMouseUp);

    // 恢复文本选择
    document.body.style.userSelect = "auto";
  };

  const handleContextMenu = (e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
    console.log("右键菜单");
  };

  const handleClick = () => {
    console.log("单击事件");
  };

  const handleDoubleClick = () => {
    console.log("双击事件");
  };

  return (
    <div
      id='container'
      style={{
        width: containerBoxSize.width,
        height: containerBoxSize.height,
        border: "1px solid #ccc",
        margin: "100px auto",
        position: "relative",
      }}
    >
      {/* moveBox */}
      <animated.div
        style={{
          width: moveBoxSize.width,
          height: moveBoxSize.height,
          borderRadius: "50%",
          backgroundImage: "",
          backgroundSize: "contain",
          backgroundRepeat: "no-repeat",
          backgroundPosition: "center",
          boxShadow: "0px 0px 5px 1px rgba(0, 0, 0, 0.32)",
          cursor: "pointer",
          position: "absolute",
          // top: position.y, // 无需动画时可放开注释,且注释 springProps
          // left: position.x, // 无需动画时可放开注释,且注释 springProps
          ...springProps, // 使用动画效果
        }}
        onMouseDown={handleMouseDown}
        onClick={handleClick}
        onDoubleClick={handleDoubleClick}
        onContextMenu={handleContextMenu}
      ></animated.div>
    </div>
  );
}

export default MoveBoxComponent;

效果图如下:
image

posted @ 2025-07-15 09:18  苏沐~  阅读(9)  评论(0)    收藏  举报