vue3 让元素可以拖动指令v-draggable
第一种情况:仅需移动
仅需要使得某个容器可以拖动,仅此而已
定义指令
// v-draggable.ts
type DraggableElement = HTMLElement & {
_cleanupDrag?: () => void;
};
function setupDraggable(el: DraggableElement) {
// 清除旧的拖拽监听器(避免重复绑定)
el._cleanupDrag?.();
let startX = 0;
let startY = 0;
let currentX = 0;
let currentY = 0;
// 初始化位置信息
el.dataset.currentX = String(currentX);
el.dataset.currentY = String(currentY);
// 设置样式
el.style.position = "fixed";
el.style.cursor = "move";
el.style.transform = `translate(${currentX}px, ${currentY}px)`;
// 鼠标按下时,准备拖动
const onMouseDown = (event: MouseEvent) => {
event.preventDefault();
startX = event.clientX - Number(el.dataset.currentX);
startY = event.clientY - Number(el.dataset.currentY);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
// 拖动过程中
const onMouseMove = (event: MouseEvent) => {
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
const { width, height } = el.getBoundingClientRect();
const maxX = window.innerWidth - width;
const maxY = window.innerHeight - height;
const newX = Math.max(0, Math.min(deltaX, maxX));
const newY = Math.max(0, Math.min(deltaY, maxY));
el.dataset.currentX = String(newX);
el.dataset.currentY = String(newY);
el.style.transform = `translate(${newX}px, ${newY}px)`;
};
// 拖动结束,移除监听器
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
// 绑定初始事件
el.addEventListener("mousedown", onMouseDown);
// 提供清理函数
el._cleanupDrag = () => {
el.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}
// Vue 指令定义
const vDraggable = {
mounted(el: DraggableElement) {
setupDraggable(el);
},
unmounted(el: DraggableElement) {
el._cleanupDrag?.();
},
};
export default vDraggable;
使用
app.vue中使用
<template>
<div class="app">
<div class="card" v-draggable></div>
</div>
</template>
<script setup lang="ts">
import vDraggable from "../directives/v-draggable";
</script>
<style scoped>
.card {
width: 100px;
height: 100px;
background-color: red;
}
</style>
效果

第二种情况:放大缩小
加上了工具栏,上边有放大缩小的按钮,并且根据点击放大缩小的按钮,指令要监听传入的窗口状态更新大小和位置
定义指令
// v-draggable.ts
type DraggableElement = HTMLElement & {
_cleanupDrag?: () => void;
};
const minSize = {
width: 260,
height: 150,
};
const maxSize = {
width: 400,
height: 200,
};
function setupDraggable(el: DraggableElement, params) {
// 清除旧的拖拽监听器(避免重复绑定)
el._cleanupDrag?.();
let startX = 0;
let startY = 0;
let currentX = 20;
let currentY = 20;
// 计算位置
if (params.big) {
// 如果是大就--->居中对齐
const rect = el.getBoundingClientRect();
currentX = (window.innerWidth - rect.width) / 2;
currentY = (window.innerHeight - rect.height) / 2;
}
// 初始化位置信息
el.dataset.currentX = String(currentX);
el.dataset.currentY = String(currentY);
// 设置样式
el.style.width = params.big ? `${maxSize.width}px` : `${minSize.width}px`;
el.style.height = params.big ? `${maxSize.height}px` : `${minSize.height}px`;
el.style.position = "fixed";
el.style.cursor = "move";
el.style.transform = `translate(${currentX}px, ${currentY}px)`;
// 鼠标按下时,准备拖动
const onMouseDown = (event: MouseEvent) => {
event.preventDefault();
startX = event.clientX - Number(el.dataset.currentX);
startY = event.clientY - Number(el.dataset.currentY);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
// 拖动过程中
const onMouseMove = (event: MouseEvent) => {
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
const { width, height } = el.getBoundingClientRect();
const maxX = window.innerWidth - width;
const maxY = window.innerHeight - height;
const newX = Math.max(0, Math.min(deltaX, maxX));
const newY = Math.max(0, Math.min(deltaY, maxY));
el.dataset.currentX = String(newX);
el.dataset.currentY = String(newY);
el.style.transform = `translate(${newX}px, ${newY}px)`;
};
// 拖动结束,移除监听器
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
// 绑定初始事件
el.addEventListener("mousedown", onMouseDown);
// 提供清理函数
el._cleanupDrag = () => {
el.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}
// Vue 指令定义
const vDraggable = {
mounted(el: DraggableElement, binding: any) {
setupDraggable(el, binding.value);
},
updated(el: DraggableElement, binding: any) {
setupDraggable(el, binding.value);
},
unmounted(el: DraggableElement) {
el._cleanupDrag?.();
},
};
export default vDraggable;
使用
app.vue
<template>
<div class="app">
<div class="card" v-draggable="{ big: winBig }">
<div class="tool">
<span @click="setWinBig" v-if="winBig">-</span>
<span @click="setWinBig" v-else>+</span>
</div>
<div class="content"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import vDraggable from "../directives/v-draggable";
const winBig = ref(false);
const setWinBig = () => {
winBig.value = !winBig.value;
console.log(winBig.value);
};
</script>
<style scoped lang="scss">
.card {
display: flex;
flex-direction: column;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
.tool {
gap: 20px;
box-sizing: border-box;
height: 40px;
width: 100%;
background-color: white;
display: flex;
align-items: center;
justify-content: end;
padding: 10px;
border-bottom: rgba(0, 0, 0, 0.1) solid 1px;
>span{
cursor: pointer;
}
}
.content {
overflow: auto;
flex: 1;
}
}
</style>
效果

第三种情况: iframe
如果在上一种情况的 content 里放入 iframe,你再拖动就会出现粘手现象,这是因为:你的鼠标在拖动中进入了 iframe,浏览器会将事件“转交”给 iframe 的上下文(也就是 iframe 里面的页面),于是外部页面就无法再监听 mousemove 和 mouseup 事件,拖动就中断了。
解决方案:用“透明遮罩层”挡住 iframe
// v-draggable.ts
type DraggableElement = HTMLElement & {
_cleanupDrag?: () => void;
};
// 如果不是 iframe 则不用遮罩
function addMask() {
if (document.querySelector(".drag-mask")) return;
const mask = document.createElement("div");
mask.style.position = "fixed";
mask.style.top = "0";
mask.style.left = "0";
mask.style.width = "100vw";
mask.style.height = "100vh";
mask.style.zIndex = "9999"; // 保证在所有元素之上
mask.style.cursor = "move";
mask.style.background = "transparent"; // 保证你还能看见 iframe
mask.className = "drag-mask";
document.body.appendChild(mask);
}
function removeMask() {
const mask = document.querySelector(".drag-mask");
if (mask) document.body.removeChild(mask);
}
const minSize = {
width: 260,
height: 150,
};
const maxSize = {
width: 400,
height: 200,
};
function setupDraggable(el: DraggableElement, params) {
// 清除旧的拖拽监听器(避免重复绑定)
el._cleanupDrag?.();
let startX = 0;
let startY = 0;
let currentX = 20;
let currentY = 20;
// 计算位置
if (params.big) {
// 如果是大就--->居中对齐
const rect = el.getBoundingClientRect();
currentX = (window.innerWidth - rect.width) / 2;
currentY = (window.innerHeight - rect.height) / 2;
}
// 初始化位置信息
el.dataset.currentX = String(currentX);
el.dataset.currentY = String(currentY);
// 设置样式
el.style.width = params.big ? `${maxSize.width}px` : `${minSize.width}px`;
el.style.height = params.big ? `${maxSize.height}px` : `${minSize.height}px`;
el.style.position = "fixed";
el.style.cursor = "move";
el.style.transform = `translate(${currentX}px, ${currentY}px)`;
// 鼠标按下时,准备拖动
const onMouseDown = (event: MouseEvent) => {
event.preventDefault();
startX = event.clientX - Number(el.dataset.currentX);
startY = event.clientY - Number(el.dataset.currentY);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
// 拖动过程中
const onMouseMove = (event: MouseEvent) => {
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
// 如果移动距离超过 3px,添加遮罩
const movedDistance = Math.sqrt(
(deltaX - Number(el.dataset.currentX)) ** 2 +
(deltaY - Number(el.dataset.currentY)) ** 2
);
if (movedDistance > 3) {
addMask();
}
const { width, height } = el.getBoundingClientRect();
const maxX = window.innerWidth - width;
const maxY = window.innerHeight - height;
const newX = Math.max(0, Math.min(deltaX, maxX));
const newY = Math.max(0, Math.min(deltaY, maxY));
el.dataset.currentX = String(newX);
el.dataset.currentY = String(newY);
el.style.transform = `translate(${newX}px, ${newY}px)`;
};
// 拖动结束,移除监听器
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
removeMask();
};
// 绑定初始事件
el.addEventListener("mousedown", onMouseDown);
// 提供清理函数
el._cleanupDrag = () => {
el.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
removeMask();
};
}
// Vue 指令定义
const vDraggable = {
mounted(el: DraggableElement, binding: any) {
setupDraggable(el, binding.value);
},
updated(el: DraggableElement, binding: any) {
setupDraggable(el, binding.value);
},
unmounted(el: DraggableElement) {
el._cleanupDrag?.();
},
};
export default vDraggable;
// app.vue
<template>
<div class="app">
<div class="card" v-draggable="{ big: winBig }">
<div class="tool">
<span @click="setWinBig" v-if="winBig">-</span>
<span @click="setWinBig" v-else>+</span>
</div>
<div class="content">
<iframe
src="https://www.cnblogs.com"
width="100%"
height="100%"
frameborder="0"
></iframe>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import vDraggable from "../directives/v-draggable";
const winBig = ref(false);
const setWinBig = () => {
winBig.value = !winBig.value;
console.log(winBig.value);
};
</script>
<style scoped lang="scss">
.card {
display: flex;
flex-direction: column;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
.tool {
gap: 20px;
box-sizing: border-box;
height: 40px;
width: 100%;
background-color: white;
display: flex;
align-items: center;
justify-content: end;
padding: 10px;
border-bottom: rgba(0, 0, 0, 0.1) solid 1px;
> span {
cursor: pointer;
}
}
.content {
overflow: auto;
flex: 1;
}
}
</style>

浙公网安备 33010602011771号