轻松搞定拖拽缩放、移动,不怕领导叫我写拖拽!!
经常会碰到需要拖拽缩放的情况,只要有思路,实现起来会非常顺畅。
功能的核心是鼠标放在四个边和角上,拖拽把容器放大或缩小
功能演示
缩放:
移动:
演示网址:宝藏导航
缩放设计思路
使用css绘制四条边和四个角, 通过css定位,控制四根线和四个角在对应的位置 监听鼠标点击和移动事件 在移动的过程中,改变容器的大小
核心设计
基础html结构
<template><!-- 使用 v-if 判断是否插入到 body 中 --><!-- 创建一个容器,支持拖拽,使用 ref 引用该容器 --><divref="draggableContainer"class="draggable-container"@mousedown="startDrag":style="containerStyle"><!-- 插槽,用户可以将其他内容插入到这个容器中 --><slot></slot><!-- 创建缩放控制点,每个控制点代表一个边角,使用 v-for 循环渲染 --><spanv-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. 计算拖拽后新宽度
根据:容器新宽度 = 容器初始宽度 + 鼠标移动距离
拖拽容器右边:
// 计算鼠标的移动距离 deltaXconst 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记录下。
<spanv-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 引用该容器 --><divref="draggableContainer"class="draggable-container"@mousedown="startDrag":style="containerStyle"><!-- 插槽,用户可以将其他内容插入到这个容器中 --><slot></slot><!-- 创建缩放控制点,每个控制点代表一个边角,使用 v-for 循环渲染 --><spanv-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 }, // 容器的初始 X 位置top: { type: Number, default: 0 }, // 容器的初始 Y 位置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, // 鼠标初始 X 坐标originMouseY: 0, // 鼠标初始 Y 坐标originContainX: 0, // 容器初始 X 坐标originContainY: 0, // 容器初始 Y 坐标originWidth: 0, // 容器初始宽度originHeight: 0, // 容器初始高度resizeType: "", // 当前缩放类型};},computed: {// 计算容器的样式containerStyle() {return {top: `${this.position.y}px`, // 设置容器的 top 样式left: `${this.position.x}px`, // 设置容器的 left 样式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
本文实现了拖拽移动和缩放的功能,同学们也可以根据需要往上面添加自己的改动。希望对您有所帮助!