经常会碰到需要拖拽缩放的情况,只要有思路,实现起来会非常顺畅。
功能的核心是鼠标放在四个边和角上,拖拽把容器放大或缩小
功能演示
缩放:

移动:
 
演示网址:宝藏导航
缩放设计思路
- 使用css绘制四条边和四个角
- 通过css定位,控制四根线和四个角在对应的位置
- 监听鼠标点击和移动事件
- 在移动的过程中,改变容器的大小
核心设计
基础html结构
<template>
  
  
  <div
    ref="draggableContainer"
    class="draggable-container"
    @mousedown="startDrag"
    :style="containerStyle"
  >
    
    <slot></slot>
    
    <span
      v-for="type in resizeTypes"
      :key="type"
      :class="`${type}-resize`"
      @mousedown="startResize($event, type)"
    ></span>
  </div>
</template>
基础data数据:
data: {
      
      resizeTypes: ["lt", "t", "rt", "r", "rb", "b", "lb", "l"],
      
      position: { x: this.left, y: this.top }, 
      size: { width: this.width, height: this.height }, 
  }
核心代码和思路
容器新宽度 = 容器初始宽度 + 鼠标移动距离
通过上面公式,我们需要记录
- 容器的初始宽度
- 鼠标移动距离 = 鼠标新位置 - 鼠标初始距离
1. 记录容器和鼠标初始状态
startResize(event) {
    
    this.originMouseX = event.clientX;
    this.originMouseY = event.clientY;
    
    this.originWidth = this.size.width;
    this.originHeight = this.size.height;
},
2. 计算拖拽后新宽度
根据:容器新宽度 = 容器初始宽度 + 鼠标移动距离
拖拽容器右边:
const deltaX = event.clientX - this.originMouseX; 
newWidth = this.originWidth + deltaX; 
拖拽容器右下角:
当我们拖拽容器右下角,容器的宽和高都会改变。我们需要把这个拆分成两个步骤来解决。
const deltaX = event.clientX - this.originMouseX; 
newWidth = this.originWidth + deltaX;
const deltaY = event.clientY - this.originMouseY; 
newHeight = this.originHeight + deltaY;
拖拽左边和上边:
拖拽左边的时候,左边的定位不能始终都是在原来的位置。
假设: 
    我们的初始位置是 left: 200px。左边向左拖拽50px后,需要变为left: 150px
首先我们需要在开始的时候记录容器的初始位置
startResize(event) {
    
    this.originMouseX = event.clientX;
    this.originMouseY = event.clientY;
    
    this.originWidth = this.size.width;
    this.originHeight = this.size.height;
    
    
    this.originContainX = this.position.x;
    this.originContainY = this.position.y;
}
改变宽高的同时,改变容器左上角的位置
const deltaX = event.clientX - this.originMouseX; 
newWidth = this.originWidth - deltaX;
this.position.x = this.originContainX + deltaX;
3. 确定拖拽的是哪条边
我们在点击的时候会传递type,使用变量把type记录下。
<span
  v-for="type in resizeTypes"
  :key="type"
  :class="`${type}-resize`"
  @mousedown="startResize($event, type)"
></span>
startResize(event, type) {
  this.resizeType = type; 
  ......
}
handleResize() {
const deltaX = event.clientX - this.originMouseX; 
const deltaY = event.clientY - this.originMouseY; 
let newWidth = this.originWidth;
let newHeight = this.originHeight;
switch (this.resizeType) {
    case "lt": 
      newWidth = this.originWidth - deltaX;
      this.size.width = newWidth;
      this.position.x = this.originContainX + deltaX;
      
      newHeight = this.originHeight - deltaY;
      this.size.height = newHeight;
      this.position.y = this.originContainY + deltaY;
      break;
      
    case "t": 
      newHeight = this.originHeight - deltaY;
      this.size.height = newHeight;
      this.position.y = this.originContainY + deltaY;
      break;
      右边,右下角同理......
}
4.设置最小的拖拽宽和高
如果新拖拽的宽度,已经小于最小宽度。拖拽时不进行任何改动。
switch (this.resizeType) {
    case "lt": 
      newWidth = this.originWidth - deltaX;
      newHeight = this.originHeight - deltaY;
      if (newWidth >= this.minWidth) {
        this.position.x = this.originContainX + deltaX;
        this.size.width = newWidth;
      }
      if (newHeight >= this.minHeight) {
        this.position.y = this.originContainY + deltaY;
        this.size.height = newHeight;
      }
      break;
    case "t": 
      newHeight = this.originHeight - deltaY;
      if (newHeight >= this.minHeight) {
        this.position.y = this.originContainY + deltaY;
        this.size.height = newHeight;
      }
      break;
      
      右边边,右下角同理......
}
5.添加拖拽移动
拖拽移动的详细内容,笔者写的另一篇文章:拖拽移动详细思路
下面的完整代码是结合了拖拽移动和缩放整合在一起,一个较为完整的拖拽组件
完整代码
<template>
  <!-- 使用 v-if 判断是否插入到 body 中 -->
  <!-- 创建一个容器,支持拖拽,使用 ref 引用该容器 -->
  <div
    ref="draggableContainer"
    class="draggable-container"
    @mousedown="startDrag"
    :style="containerStyle"
  >
    
    <slot></slot>
    
    <span
      v-for="type in resizeTypes"
      :key="type"
      :class="`${type}-resize`"
      @mousedown="startResize($event, type)"
    ></span>
  </div>
</template>
<script>
export default {
  props: {
    zIndex: { type: Number, default: 1 }, 
    left: { type: Number, default: 0 }, 
    top: { type: Number, default: 0 }, 
    width: { type: Number, default: 300 }, 
    height: { type: Number, default: 300 }, 
    minWidth: { type: Number, default: 100 }, 
    minHeight: { type: Number, default: 100 }, 
  },
  data() {
    return {
      
      resizeTypes: ["lt", "t", "rt", "r", "rb", "b", "lb", "l"],
      
      position: { x: this.left, y: this.top }, 
      size: { width: this.width, height: this.height }, 
      originMouseX: 0, 
      originMouseY: 0, 
      originContainX: 0, 
      originContainY: 0, 
      originWidth: 0, 
      originHeight: 0, 
      resizeType: "", 
    };
  },
  computed: {
    
    containerStyle() {
      return {
        top: `${this.position.y}px`, 
        left: `${this.position.x}px`, 
        width: `${this.size.width}px`, 
        height: `${this.size.height}px`, 
        zIndex: this.zIndex, 
      };
    },
  },
  methods: {
    
     * 拖拽逻辑
     */
    startDrag(event) {
      
      this.originMouseX = event.clientX;
      this.originMouseY = event.clientY;
      
      this.originContainX = this.position.x;
      this.originContainY = this.position.y;
      
      document.addEventListener("mousemove", this.handleDrag);
      document.addEventListener("mouseup", this.stopDrag);
    },
    handleDrag(event) {
      this.position.x = this.originContainX + event.clientX - this.originMouseX;
      this.position.y = this.originContainY + event.clientY - this.originMouseY;
    },
    
     * 缩放逻辑
     */
    startResize(event, type) {
      this.resizeType = type; 
      
      this.originMouseX = event.clientX;
      this.originMouseY = event.clientY;
      
      this.originWidth = this.size.width;
      this.originHeight = this.size.height;
      
      this.originContainX = this.position.x;
      this.originContainY = this.position.y;
      
      event.stopPropagation(); 
      
      document.addEventListener("mousemove", this.handleResize);
      document.addEventListener("mouseup", this.stopDrag);
    },
    handleResize(event) {
      const deltaX = event.clientX - this.originMouseX; 
      const deltaY = event.clientY - this.originMouseY; 
      let newWidth = this.originWidth;
      let newHeight = this.originHeight;
      
      switch (this.resizeType) {
        case "lt": 
          newWidth = this.originWidth - deltaX;
          newHeight = this.originHeight - deltaY;
          if (newWidth >= this.minWidth) {
            this.position.x = this.originContainX + deltaX;
            this.size.width = newWidth;
          }
          if (newHeight >= this.minHeight) {
            this.position.y = this.originContainY + deltaY;
            this.size.height = newHeight;
          }
          break;
        case "t": 
          newHeight = this.originHeight - deltaY;
          if (newHeight >= this.minHeight) {
            this.position.y = this.originContainY + deltaY;
            this.size.height = newHeight;
          }
          break;
        case "rt": 
          newWidth = this.originWidth + deltaX;
          newHeight = this.originHeight - deltaY;
          if (newWidth >= this.minWidth) {
            this.size.width = newWidth;
          }
          if (newHeight >= this.minHeight) {
            this.position.y = this.originContainY + deltaY;
            this.size.height = newHeight;
          }
          break;
        case "r": 
          newWidth = this.originWidth + deltaX;
          if (newWidth >= this.minWidth) {
            this.size.width = newWidth;
          }
          break;
        case "rb": 
          newWidth = this.originWidth + deltaX;
          newHeight = this.originHeight + deltaY;
          if (newWidth >= this.minWidth) {
            this.size.width = newWidth;
          }
          if (newHeight >= this.minHeight) {
            this.size.height = newHeight;
          }
          break;
        case "b": 
          newHeight = this.originHeight + deltaY;
          if (newHeight >= this.minHeight) {
            this.size.height = newHeight;
          }
          break;
        case "lb": 
          newWidth = this.originWidth - deltaX;
          newHeight = this.originHeight + deltaY;
          if (newWidth >= this.minWidth) {
            this.position.x = this.originContainX + deltaX;
            this.size.width = newWidth;
          }
          if (newHeight >= this.minHeight) {
            this.size.height = newHeight;
          }
          break;
        case "l": 
          newWidth = this.originWidth - deltaX;
          if (newWidth >= this.minWidth) {
            this.position.x = this.originContainX + deltaX;
            this.size.width = newWidth;
          }
          break;
      }
    },
    
     * 停止拖拽或缩放
     * 清除事件监听器
     */
    stopDrag() {
      document.removeEventListener("mousemove", this.handleDrag);
      document.removeEventListener("mousemove", this.handleResize);
      document.removeEventListener("mouseup", this.stopDrag);
    },
  },
  
  beforeDestroy() {
    this.stopDrag();
  },
};
</script>
<style lang="scss" scoped>
$lineOffset: -6px;
$cornerOffset: -8px;
/* 拖拽容器的样式 */
.draggable-container {
  position: fixed; /* 绝对定位 */
  cursor: move; /* 鼠标移动时显示抓手指针 */
  user-select: none; /* 禁止选中文本 */
  background-color: #ccc;
  span {
    position: absolute;
    display: block;
  }
  /* 左边和右边 */
  .l-resize,
  .r-resize {
    width: 8px;
    height: 100%;
    top: 0;
    cursor: w-resize;
  }
  .l-resize {
    left: $lineOffset;
  }
  .r-resize {
    right: $lineOffset;
  }
  /* 上边和下边 */
  .t-resize,
  .b-resize {
    width: 100%;
    height: 8px;
    left: 0;
    cursor: s-resize;
  }
  .t-resize {
    top: $lineOffset;
  }
  .b-resize {
    bottom: $lineOffset;
  }
  /* 四个角 */
  .lt-resize,
  .rt-resize,
  .rb-resize,
  .lb-resize {
    width: 15px;
    height: 15px;
    z-index: 10;
  }
  .lt-resize,
  .lb-resize {
    left: $cornerOffset;
  }
  .lt-resize,
  .rt-resize {
    top: $cornerOffset;
  }
  .rt-resize,
  .rb-resize {
    right: $cornerOffset;
  }
  .rb-resize,
  .lb-resize {
    bottom: $cornerOffset;
  }
  .lt-resize,
  .rb-resize {
    cursor: se-resize;
  }
  .rt-resize,
  .lb-resize {
    cursor: sw-resize;
  }
}
</style>
组件引用
<DraggableContainer
  :width="400"
  :height="400"
  :min-height="300"
  :min-width="300"
>
  <div>能拖动我了</div>
</DraggableContainer>
总结
部分代码设计参考了著名第三方库vxe-modal的设计思路:vxe-modal
本文实现了拖拽移动和缩放的功能,同学们也可以根据需要往上面添加自己的改动。希望对您有所帮助!