VUE3拖拽、缩放的简单实现

2,278 阅读5分钟

VUE3拖拽、缩放的简单实现

在现代Web应用中,拖拽和缩放功能是非常常见的交互方式。这些功能可以增强用户体验,使得用户能够更加直观地操作页面元素。本文将详细介绍如何使用Vue.js来实现一个简单的可拖拽、可缩放、可移动的组件。

项目背景

我们希望创建一个可以在父容器内自由移动和调整大小的子元素。这个子元素可以通过点击并拖动其边缘来进行尺寸调整,并且可以通过点击并拖动其主体部分来进行位置移动。

bandicam 2025-02-24 17-06-08-164 00_00_00-00_00_30.gif

bandicam 2025-02-24 17-06-18-034 00_00_00-00_00_30.gif

实现思路

  1. HTML结构:定义一个包含拖拽按钮的可移动区域。
  2. CSS样式:设置父容器和子元素的样式,使其具有透明背景和模糊效果,看起来像是悬浮在空中。
  3. JavaScript逻辑
    • 监听鼠标按下事件,开始拖拽或缩放。
    • 根据鼠标的移动更新元素的位置或尺寸。
    • 监听鼠标抬起事件,结束拖拽或缩放。

详细步骤

1. HTML结构

首先,在模板中定义了一个主容器.main,其中包含一个可拖拽的玻璃窗格.glass。在这个玻璃窗格内有一个可调整大小和移动的.drag-item,并且在其四周分布着八个用于调整大小的按钮.drag-btn

<template>
  <div class="main">
    <div class="glass" ref="dragWrapRef">
      <div class="drag-item" ref="dragItemRef" @mousedown="startDrag" :style="style">
        <div
          v-for="(type, index) in dragType"
          :key="index"
          class="drag-btn"
          :class="type"
          @mousedown.stop="startMove($event, type)"
        ></div>
      </div>
    </div>
  </div>
</template>

2. CSS样式

通过CSS设置了父容器和子元素的样式,包括背景颜色、模糊效果、圆角等。同时为每个调整大小的按钮设置了不同的定位属性,以便它们位于子元素的四个角落和四条边的中间。

<style lang="less" scoped>
.main {
  width: 100vw;
  height: 100vh;
  background: url('@/assets/images/background.png') no-repeat;
  background-size: 100% 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.glass {
  width: 80%;
  height: 80%;
  background: rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(10px);
  border-radius: 10px;
  position: relative;
}

.drag-item {
  width: 300px;
  height: 360px;
  position: absolute;
  background: rgba(224, 164, 164, 0.2);
  backdrop-filter: blur(10px);
  border-radius: 10px;
  cursor: grab;
}

.drag-btn {
  width: 10px;
  height: 10px;
  background-color: transparent;
  border: none;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  cursor: move;
}

.lt {
  top: 0;
  left: 0;
}
.lc {
  top: 50%;
  left: 0;
  transform: translateY(-50%);
}
.lb {
  bottom: 0;
  left: 0;
}
.ct {
  top: 0;
  left: 50%;
  transform: translateX(-50%);
}
.cb {
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
}
.rt {
  top: 0;
  right: 0;
}
.rc {
  top: 50%;
  right: 0;
  transform: translateY(-50%);
}
.rb {
  bottom: 0;
  right: 0;
}
</style>

3. JavaScript逻辑

引入依赖

使用Vue Composition API (setup语法糖),引入所需的钩子函数和其他工具函数。

<script setup>
import { ref, onMounted, computed, onUnmounted } from 'vue'
</script>
定义数据

定义了一些响应式变量来存储状态信息,例如当前的拖拽类型、初始位置和尺寸、最大最小尺寸限制等。

const dragType = ['lt', 'lc', 'lb', 'ct', 'cb', 'rt', 'rc', 'rb']

const dragWrapRef = ref(null)
const dragItemRef = ref(null)
const dragWrapRectRef = ref(null)
const resizeType = ref('')
const initialPositionX = ref(0) // 初始X位置
const initialPositionY = ref(0) // 初始Y位置
const initialMouseX = ref(0) // 初始鼠标X位置
const initialMouseY = ref(0) // 初始鼠标Y位置
const intiialWidth = ref(300) // 初始宽度
const intiialHeight = ref(360) // 初始高度
let minHeight = 360 // 最小高度
let minWidth = 300 // 最小宽度
let maxWidth = 500 // 最大宽度
let maxHeight = 600 // 最大高度
let dragBoxX = 0, // 拖动框X位置
  dragBoxY = 0 // 拖动框Y位置
let dragBoxWidth = 0, // 拖动框宽度
  dragBoxHeight = 0 // 拖动框高度
计算属性

计算出子元素的最终样式,包括位置和尺寸。

const style = computed(() => {
  return {
    top: initialPositionY.value + 'px',
    left: initialPositionX.value + 'px',
    width: intiialWidth.value + 'px',
    height: intiialHeight.value + 'px',
  }
})
处理拖拽事件

定义了三个主要的处理函数:handleDrag用于处理整体拖拽移动,handleResize用于处理各个方向上的尺寸调整,startDragstartMove分别对应启动这两种行为,而stopDrag则用于终止所有相关的事件监听器。

/**
 * 处理拖动事件
 * @param {MouseEvent} e - 鼠标事件对象
 */
function handleDrag(e) {
  let deltaX = e.clientX - initialMouseX.value
  let deltaY = e.clientY - initialMouseY.value

  let newX = initialPositionX.value + deltaX
  let newY = initialPositionY.value + deltaY

  if (isMouseInsideElement(newX, newY)) {
    initialPositionX.value = newX
    initialPositionY.value = newY

    initialMouseX.value = e.clientX
    initialMouseY.value = e.clientY
  }
}

/**
 * 开始拖动事件
 * @param {MouseEvent} e - 鼠标事件对象
 */
function startDrag(e) {
  initialMouseX.value = e.clientX
  initialMouseY.value = e.clientY
  document.addEventListener('mousemove', handleDrag)
  document.addEventListener('mouseup', stopDrag)
}

/**
 * 处理调整大小事件
 * @param {MouseEvent} e - 鼠标事件对象
 */
function handleResize(e) {
  let deltaX = e.clientX - initialMouseX.value
  let deltaY = e.clientY - initialMouseY.value
  let newWidth = dragBoxWidth
  let newHeight = dragBoxHeight

  switch (resizeType.value) {
    case 'lt':
      newWidth -= deltaX
      if (newWidth < maxWidth && newWidth > minWidth) {
        intiialWidth.value = newWidth
        initialPositionX.value = dragBoxX + deltaX
      }

      newHeight -= deltaY
      if (newHeight < maxHeight && newHeight > minHeight) {
        intiialHeight.value = newHeight
        initialPositionY.value = dragBoxY + deltaY
      }
      break
    case 'lc':
      newWidth -= deltaX
      if (newWidth < maxWidth && newWidth > minWidth) {
        intiialWidth.value = newWidth
        initialPositionX.value = dragBoxX + deltaX
      }
      break
    case 'lb':
      newWidth -= deltaX
      if (newWidth < maxWidth && newWidth > minWidth) {
        intiialWidth.value = newWidth
        initialPositionX.value = dragBoxX + deltaX
      }

      newHeight += deltaY
      if (newHeight < maxHeight && newHeight > minHeight) {
        intiialHeight.value = newHeight
      }
      break
    case 'ct':
      newHeight -= deltaY
      if (newHeight < maxHeight && newHeight > minHeight) {
        intiialHeight.value = newHeight
        initialPositionY.value = dragBoxY + deltaY
      }
      break
    case 'cb':
      newHeight += deltaY
      if (newHeight < maxHeight && newHeight > minHeight) {
        intiialHeight.value = newHeight
      }
      break
    case 'rt':
      newWidth += deltaX
      if (newWidth < maxWidth && newWidth > minWidth) {
        intiialWidth.value = newWidth
      }

      newHeight -= deltaY
      if (newHeight < maxHeight && newHeight > minHeight) {
        intiialHeight.value = newHeight
        initialPositionY.value = dragBoxY + deltaY
      }
      break
    case 'rc':
      newWidth += deltaX
      if (newWidth < maxWidth && newWidth > minWidth) {
        intiialWidth.value = newWidth
      }
      break
    case 'rb':
      newWidth += deltaX
      if (newWidth < maxWidth && newWidth > minWidth) {
        intiialWidth.value = newWidth
      }

      newHeight += deltaY
      if (newHeight < maxHeight && newHeight > minHeight) {
        intiialHeight.value = newHeight
      }
      break
  }
}

/**
 * 开始调整大小事件
 * @param {MouseEvent} e - 鼠标事件对象
 * @param {string} type - 调整大小的类型
 */
function startMove(e, type) {
  initialMouseX.value = e.clientX
  initialMouseY.value = e.clientY
  resizeType.value = type
  dragBoxX = initialPositionX.value
  dragBoxY = initialPositionY.value
  dragBoxWidth = intiialWidth.value
  dragBoxHeight = intiialHeight.value
  document.addEventListener('mousemove', handleResize)
  document.addEventListener('mouseup', stopDrag)
}

/**
 * 停止拖动或调整大小事件
 */
function stopDrag() {
  document.removeEventListener('mousemove', handleDrag)
  document.removeEventListener('mousemove', handleResize)
  document.removeEventListener('mouseup', stopDrag)
}
辅助函数

isMouseInsideElement函数用于检查新位置是否超出了父容器的边界,防止子元素移出可视范围。

/**
 * 检查鼠标是否在元素内部
 * @param {number} newX - 新的X坐标
 * @param {number} newY - 新的Y坐标
 * @returns {boolean} - 鼠标是否在元素内部
 */
function isMouseInsideElement(newX, newY) {
  return (
    newX >= 0 &&
    newX + intiialWidth.value <= dragWrapRectRef.value.width &&
    newY >= 0 &&
    newY + intiialHeight.value <= dragWrapRectRef.value.height
  )
}
生命周期钩子

在组件挂载后获取必要的DOM信息,并在卸载前清理所有的事件监听器以避免内存泄漏。

onMounted(() => {
  intiialWidth.value = dragItemRef.value.clientWidth
  intiialHeight.value = dragItemRef.value.clientHeight
  initialPositionX.value = dragItemRef.value.offsetLeft
  initialPositionY.value = dragItemRef.value.offsetTop
  dragWrapRectRef.value = dragWrapRef.value.getBoundingClientRect()
  maxWidth = dragWrapRectRef.value.width
  maxHeight = dragWrapRectRef.value.height
})

onUnmounted(() => {
  stopDrag()
})

结论

通过上述代码的编写,我们可以轻松地实现一个支持拖拽、缩放和移动功能的Vue组件。这个组件不仅能够满足基本的需求,还具备良好的性能和灵活性,可以根据具体的应用场景进行进一步的定制和优化。希望这篇文章能帮助你更好地理解和掌握这类交互功能的实现方法。

完整代码示例

以下是完整的Vue单文件组件代码:

这段代码可以直接在Vue项目中使用,只需确保路径正确即可加载背景图片。希望这篇教程对你有所帮助!