经常会碰到需要拖拽缩放的情况,只要有思路,实现起来会非常顺畅。
功能的核心是鼠标放在四个边和角上,拖拽把容器放大或缩小
功能演示
缩放:
移动:
演示网址:宝藏导航
缩放设计思路
核心设计
基础html结构
<template>
<!-- 使用 v-if 判断是否插入到 body 中 -->
<!-- 创建一个容器,支持拖拽,使用 ref 引用该容器 -->
<div
ref="draggableContainer"
class="draggable-container"
@mousedown="startDrag"
:style="containerStyle"
>
<!-- 插槽,用户可以将其他内容插入到这个容器中 -->
<slot></slot>
<!-- 创建缩放控制点,每个控制点代表一个边角,使用 v-for 循环渲染 -->
<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. 计算拖拽后新宽度
根据:容器新宽度 = 容器初始宽度 + 鼠标移动距离
拖拽容器右边:
// 计算鼠标的移动距离 deltaX
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>
<!-- 创建缩放控制点,每个控制点代表一个边角,使用 v-for 循环渲染 -->
<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 }, // 容器的初始 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
本文实现了拖拽移动和缩放的功能,同学们也可以根据需要往上面添加自己的改动。希望对您有所帮助!
该文章在 2025/1/25 9:57:41 编辑过