vue3手搓拖拽球体贴边隐藏,鼠标滑入完全显示并且可以拖拽到其他地方

76 阅读3分钟

一、功能描述:

  1. 贴边隐藏

    • 球体在靠近容器边界时会自动贴边,并裁剪为半圆(隐藏一半)。
  2. 鼠标滑入完全显示

    • 当鼠标滑入贴边的球体时,球体完全显示,不再隐藏在边界内。
    • 如果球体不在贴边状态,鼠标滑入时球体保持正常显示状态。
  3. 拖拽交互

    • 支持拖拽球体,拖拽时球体可以自由移动。
    • 当球体靠近边界时,会自动贴边并隐藏一半。
  4. 拖拽体的里面的内容点击事件和拖拽互不干涉影响

二、关键逻辑:

  1. 贴边时

    • 如果球体贴边且鼠标未滑入,球体裁剪为半圆并隐藏一半。
    • 如果鼠标滑入贴边的球体,球体完全显示,不再隐藏在边界内。
  2. 其他情况

    • 如果球体不在贴边状态,鼠标滑入时球体完全显示(正常状态)。
  3. 鼠标滑出时

    • 如果球体贴边,鼠标滑出后恢复裁剪状态(隐藏一半)。
    • 如果球体不在贴边状态,鼠标滑出后保持正常状态。
  4. 对冒泡事件、默认事件进行阻止

    • 如不然会影响球体内部的点击事件进行其他交互。

三、效果:

  • 贴边时

    • 默认隐藏一半。
    • 鼠标滑入时完全显示,不再隐藏。
  • 其他情况

    • 鼠标滑入时完全显示(正常状态)。
    • 鼠标滑出后恢复默认状态。

效果图1:

image.png

效果图2:

image.png

四、应用场景

1. 侧边栏或浮动菜单
2. 浮动按钮或操作按钮
3. 聊天窗口或通知面板
4. 网页广告或提示框

五、完整代码

<template>
  <div
    ref="container"
    class="container"
    @mousemove="onMouseMove"
    @mouseup="onMouseUp"
    @mouseleave="onDragEnd"
  >
    <div
      ref="ball"
      class="ball"
      :style="ballStyle"
      @mousedown="onMouseDown"
      @mouseenter="onMouseEnter"
      @mouseleave="onMouseLeave"
    >
      <div class="textBox" @click.stop="handleClick"></div>
    </div>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue';

export default {
  setup() {
    const container = ref(null);
    const ball = ref(null);
    const isDragging = ref(false);
    const offsetX = ref(0);
    const offsetY = ref(0);
    const ballX = ref(0);
    const ballY = ref(0);
    const isAtBoundary = ref(true);
    const boundarySide = ref('left');
    const isLocked = ref(true);
    const isHovered = ref(false);
    const mouseDownTime = ref(0);
    const isClickEvent = ref(false);
    
    // 常量定义
    const CLICK_MAX_DURATION = 200; // 毫秒
    const SHRINK_SCALE = 0.7; // 缩小的比例
    const SNAP_THRESHOLD = 0; // 吸附阈值

    onMounted(() => {
      const containerRect = container.value.getBoundingClientRect();
      const ballRect = ball.value.getBoundingClientRect();
      ballX.value = -ballRect.width / 2;
      ballY.value = containerRect.height / 2 - ballRect.height / 2;
    });

    const ballStyle = computed(() => {
      let left = ballX.value;
      let top = ballY.value;
      let transform = '';
      let scale = 1;

      if (isAtBoundary.value && !isHovered.value && ball.value) {
        const ballWidth = ball.value.offsetWidth;
        const ballHeight = ball.value.offsetHeight;

        if (boundarySide.value === 'left') {
          left = -ballWidth / 2;
          scale = SHRINK_SCALE;
        } else if (boundarySide.value === 'right') {
          left = container.value.offsetWidth - ballWidth / 2;
          scale = SHRINK_SCALE;
        } else if (boundarySide.value === 'top') {
          top = -ballHeight / 2;
          scale = SHRINK_SCALE;
        } else if (boundarySide.value === 'bottom') {
          top = container.value.offsetHeight - ballHeight / 2;
          scale = SHRINK_SCALE;
        }
      } else if (isHovered.value) {
        if (isAtBoundary.value) {
          if (boundarySide.value === 'left') {
            left = 0;
          } else if (boundarySide.value === 'right') {
            left = container.value.offsetWidth - ball.value.offsetWidth;
          } else if (boundarySide.value === 'top') {
            top = 0;
          } else if (boundarySide.value === 'bottom') {
            top = container.value.offsetHeight - ball.value.offsetHeight;
          }
        }
      }

      // 添加点击缩放效果
      if (isClickEvent.value) {
        scale *= 0.95;
      }

      transform = `scale(${scale})`;

      return {
        left: `${left}px`,
        top: `${top}px`,
        transform: transform,
        transition: isDragging.value ? 'none' : 'left 0.3s ease, top 0.3s ease, transform 0.3s ease',
        cursor: isDragging.value ? 'grabbing' : 'grab'
      };
    });

    const onMouseDown = (event) => {
      event.preventDefault();
      isClickEvent.value = false;
      mouseDownTime.value = Date.now();
      isLocked.value = false;
      isDragging.value = true;
      
      const ballRect = ball.value.getBoundingClientRect();
      const containerRect = container.value.getBoundingClientRect();
      
      offsetX.value = event.clientX - (ballRect.left - containerRect.left);
      offsetY.value = event.clientY - (ballRect.top - containerRect.top);
    };

    const onMouseMove = (event) => {
      if (!isDragging.value || isLocked.value) return;
      
      event.preventDefault();
      const containerRect = container.value.getBoundingClientRect();
      const ballRect = ball.value.getBoundingClientRect();

      let newX = event.clientX - containerRect.left - offsetX.value;
      let newY = event.clientY - containerRect.top - offsetY.value;

      const atLeftBoundary = newX <= SNAP_THRESHOLD;
      const atRightBoundary = newX + ballRect.width >= containerRect.width - SNAP_THRESHOLD;
      const atTopBoundary = newY <= SNAP_THRESHOLD;
      const atBottomBoundary = newY + ballRect.height >= containerRect.height - SNAP_THRESHOLD;

      if (atLeftBoundary) {
        newX = -ballRect.width / 2;
        boundarySide.value = 'left';
        isAtBoundary.value = true;
      } else if (atRightBoundary) {
        newX = containerRect.width - ballRect.width / 2;
        boundarySide.value = 'right';
        isAtBoundary.value = true;
      } else if (atTopBoundary) {
        newY = -ballRect.height / 2;
        boundarySide.value = 'top';
        isAtBoundary.value = true;
      } else if (atBottomBoundary) {
        newY = containerRect.height - ballRect.height / 2;
        boundarySide.value = 'bottom';
        isAtBoundary.value = true;
      } else {
        isAtBoundary.value = false;
      }

      ballX.value = newX;
      ballY.value = newY;
    };

    const onMouseUp = () => {
      if (isDragging.value) {
        isDragging.value = false;
        if (isAtBoundary.value) {
          isLocked.value = true;
        }
      }
    };

    const onDragEnd = () => {
      if (isDragging.value) {
        isDragging.value = false;
        if (isAtBoundary.value) {
          isLocked.value = true;
        }
      }
    };

    const onMouseEnter = () => {
      isHovered.value = true;
    };

    const onMouseLeave = () => {
      isHovered.value = false;
    };

    const handleClick = () => {
      const clickDuration = Date.now() - mouseDownTime.value;
      console.log("clickDuration", clickDuration);
      if (clickDuration <= CLICK_MAX_DURATION) {
        alert('textBox点击事件触发');
        isClickEvent.value = true;
        setTimeout(() => {
          isClickEvent.value = false;
        }, 150);
      }
    };

    return {
      container,
      ball,
      ballStyle,
      onMouseDown,
      onMouseMove,
      onMouseUp,
      onDragEnd,
      onMouseEnter,
      onMouseLeave,
      handleClick
    };
  },
};
</script>

<style>
.container {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  background-color: #f0f0f0;
}

.ball {
  position: absolute;
  width: 100px;
  height: 100px;
  background-color: #007bff;
  border-radius: 50%;
  cursor: grab;
  user-select: none;
  touch-action: none;
  transform-origin: center; /* 确保缩放以中心为基准 */
}

.ball:active {
  cursor: grabbing;
}

.textBox {
  position: absolute;
  width: 100%;
  height: 100%;
  user-select: none;
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
}
</style>