經(jīng)常會(huì)碰到需要拖拽縮放的情況,只要有思路,實(shí)現(xiàn)起來(lái)會(huì)非常順暢。
功能的核心是鼠標(biāo)放在四個(gè)邊和角上,拖拽把容器放大或縮小
功能演示
縮放:

?
移動(dòng):
演示網(wǎng)址:寶藏導(dǎo)航?
縮放設(shè)計(jì)思路
- 使用css繪制四條邊和四個(gè)角
- 通過(guò)css定位,控制四根線和四個(gè)角在對(duì)應(yīng)的位置
- 監(jiān)聽(tīng)鼠標(biāo)點(diǎn)擊和移動(dòng)事件
- 在移動(dòng)的過(guò)程中,改變?nèi)萜鞯拇笮?/li>
核心設(shè)計(jì)
基礎(chǔ)html結(jié)構(gòu)
<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>
基礎(chǔ)data數(shù)據(jù):
data: {
resizeTypes: ["lt", "t", "rt", "r", "rb", "b", "lb", "l"],
position: { x: this.left, y: this.top },
size: { width: this.width, height: this.height },
}
核心代碼和思路
容器新寬度 = 容器初始寬度 + 鼠標(biāo)移動(dòng)距離
通過(guò)上面公式,我們需要記錄
- 容器的初始寬度
鼠標(biāo)移動(dòng)距離 = 鼠標(biāo)新位置 - 鼠標(biāo)初始距離
1. 記錄容器和鼠標(biāo)初始狀態(tài)
startResize(event) {
this.originMouseX = event.clientX;
this.originMouseY = event.clientY;
this.originWidth = this.size.width;
this.originHeight = this.size.height;
},
2. 計(jì)算拖拽后新寬度
根據(jù):容器新寬度 = 容器初始寬度 + 鼠標(biāo)移動(dòng)距離
拖拽容器右邊:
const deltaX = event.clientX - this.originMouseX;
newWidth = this.originWidth + deltaX;
拖拽容器右下角:
當(dāng)我們拖拽容器右下角,容器的寬和高都會(huì)改變。我們需要把這個(gè)拆分成兩個(gè)步驟來(lái)解決。
const deltaX = event.clientX - this.originMouseX;
newWidth = this.originWidth + deltaX;
const deltaY = event.clientY - this.originMouseY;
newHeight = this.originHeight + deltaY;
拖拽左邊和上邊:
拖拽左邊的時(shí)候,左邊的定位不能始終都是在原來(lái)的位置。
假設(shè):
我們的初始位置是 left: 200px
。左邊向左拖拽50px
后,需要變?yōu)?code>left: 150px
首先我們需要在開(kāi)始的時(shí)候記錄容器的初始位置
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;
}
改變寬高的同時(shí),改變?nèi)萜髯笊辖堑奈恢?/p>
const deltaX = event.clientX - this.originMouseX;
newWidth = this.originWidth - deltaX;
this.position.x = this.originContainX + deltaX;
3. 確定拖拽的是哪條邊
我們?cè)邳c(diǎn)擊的時(shí)候會(huì)傳遞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.設(shè)置最小的拖拽寬和高
如果新拖拽的寬度,已經(jīng)小于最小寬度。拖拽時(shí)不進(jìn)行任何改動(dòng)。
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.添加拖拽移動(dòng)
拖拽移動(dòng)的詳細(xì)內(nèi)容,筆者寫(xiě)的另一篇文章:拖拽移動(dòng)詳細(xì)思路
下面的完整代碼是結(jié)合了拖拽移動(dòng)和縮放整合在一起,一個(gè)較為完整的拖拽組件
完整代碼
<template>
<!-- 使用 v-if 判斷是否插入到 body 中 -->
<!-- 創(chuàng)建一個(gè)容器,支持拖拽,使用 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;
}
},
* 停止拖拽或縮放
* 清除事件監(jiān)聽(tīng)器
*/
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; /* 絕對(duì)定位 */
cursor: move; /* 鼠標(biāo)移動(dòng)時(shí)顯示抓手指針 */
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;
}
/* 四個(gè)角 */
.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>能拖動(dòng)我了</div>
</DraggableContainer>
總結(jié)
部分代碼設(shè)計(jì)參考了著名第三方庫(kù)vxe-modal的設(shè)計(jì)思路:vxe-modal
本文實(shí)現(xiàn)了拖拽移動(dòng)和縮放的功能,同學(xué)們也可以根據(jù)需要往上面添加自己的改動(dòng)。希望對(duì)您有所幫助!