功能演示
基础结构
首先,我们来看看基本的 HTML 和 CSS 结构:
template:
部分
<template>
<div
class="draggable-container"
:style="containerStyle"
>
<!-- 插槽内容 -->
<slot></slot>
</div>
</template>
CSS
样式
/* 拖拽容器的样式 */
.draggable-container {
position: absolute; /* 绝对定位方便改动 left 和 top */
cursor: move; /* 鼠标样式,显示拖拽效果 */
}
我们给容器添加了 position: absolute
,这样就可以自由控制它的位置。
:style="containerStyle"
容器的位置信息可以通过计算属性来控制left
和 top
,我们只要改变计算属性里的值会很方便
动态控制容器位置
容器的位置会通过 Vue 的 计算属性 动态更新,容器的位置信息可以方便地通过 containerStyle
来控制:
computed: {
containerStyle() {
return {
top: `${this.position.top}px`,
left: `${this.position.left}px`,
width: `${this.width}px`,
height: `${this.height}px`,
zIndex: this.zIndex,
};
},
},
containerStyle
会根据 position
、width
、height
和 zIndex
等值来动态计算并返回样式对象,从而实现拖动时实时更新容器的位置。
初始化位置
容器的位置通过 父组件传值 进行控制。这样,父组件可以灵活地设置容器的初始位置。
props: {
left: { type: Number, default: 0 }, // 初始 X 位置
top: { type: Number, default: 0 }, // 初始 Y 位置
width: { type: Number, default: 300 }, // 初始宽度
height: { type: Number, default: 300 }, // 初始高度
zIndex: { type: Number, default: 1 }, // 层级
},
因为容器的位置会发生变化,所以 props
只能用作初始化值。我们需要将其复制到组件的 data
中,才修改left
和 top
:
data() {
return {
position: { left: this.left, top: this.top }, // 容器的初始位置
};
},
拖拽逻辑
📍 核心思路
- 容器移动距离 = 鼠标移动距离
- 鼠标移动距离 = 鼠标当前位置 - 鼠标初始位置
我们通过鼠标的位置得到容器的 新位置,并更新其 left
和 top
属性。
- 容器当前位置 = 鼠标当前位置 - 鼠标初始位置 + 容器初始位置
<div
class="draggable-container"
@mousedown="startDrag"
:style="containerStyle"
>
data() {
return {
position: { left: this.left, top: this.top }, // 容器的位置,初始值为pros中的left和top
originMouseX: 0, // 鼠标初始 X 坐标
originMouseY: 0, // 鼠标初始 Y 坐标
originContainX: 0, // 容器初始 X 坐标
originContainY: 0, // 容器初始 Y 坐标
};
},
// 拖拽开始
startDrag(event) {
this.originMouseX = event.clientX; // 鼠标初始 X 坐标
this.originMouseY = event.clientY; // 鼠标初始 Y 坐标
this.originContainX = this.position.left; // 容器初始 X 坐标
this.originContainY = this.position.top; // 容器初始 Y 坐标
// 拖拽移动的时候需要改变容器位置,所以要监听move事件
document.addEventListener("mousemove", this.handleDrag);
// 鼠标松开,不再有拖拽效果
document.addEventListener("mouseup", this.stopDrag);
},
// 拖拽过程: 容器当前位置 = 鼠标当前位置 - 鼠标初始位置 + 容器初始位置;
handleDrag(event) {
this.position.left = event.clientX - this.originMouseX + this.originContainX;
this.position.top = event.clientY - this.originMouseY + this.originContainY;
},
// 停止拖拽
stopDrag() {
document.removeEventListener("mousemove", this.handleDrag);
document.removeEventListener("mouseup", this.stopDrag);
},
解决定位问题
有时我们的拖拽元素可能处于某个父元素内,这时我们可能会受到父元素的影响。
我们可以通过 appendToBody
这个属性来决定是否将拖拽元素移到 body
中,以解决定位问题。
props: {
appendToBody: { type: Boolean, default: false }, // 是否插入 body
},
// this.$el是当前组件最外层的html元素,即当前组件的根元素
mounted() {
if (this.appendToBody) {
document.body.appendChild(this.$el); // 将拖拽元素插入到 body
}
},
beforeDestroy() {
const $el = this.$el;
if ($el.parentNode === document.body) {
$el.parentNode.removeChild($el); // 移除元素
}
},
父组件引用方式
<DraggableContainer :appendToBody="true" >
<div>能拖动我了</div>
</DraggableContainer>
Vue 2 完整代码示例
<template>
<div
class="draggable-container"
@mousedown="startDrag"
:style="containerStyle"
>
<!-- 插槽内容 -->
<slot></slot>
</div>
</template>
<script>
export default {
props: {
left: { type: Number, default: 0 }, // 初始 X 位置
top: { type: Number, default: 0 }, // 初始 Y 位置
width: { type: Number, default: 300 }, // 初始宽度
height: { type: Number, default: 300 }, // 初始高度
zIndex: { type: Number, default: 1 }, // 层级
appendToBody: { type: Boolean, default: false }, // 是否插入 body
},
computed: {
containerStyle() {
return {
top: `${this.position.top}px`,
left: `${this.position.left}px`,
width: `${this.width}px`,
height: `${this.height}px`,
zIndex: this.zIndex,
};
},
},
mounted() {
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
},
beforeDestroy() {
const $el = this.$el;
if ($el.parentNode === document.body) {
$el.parentNode.removeChild($el);
}
},
data() {
return {
position: { left: this.left, top: this.top }, // 容器的位置,初始值为pros中的left和top
originMouseX: 0, // 鼠标初始 X 坐标
originMouseY: 0, // 鼠标初始 Y 坐标
originContainX: 0, // 容器初始 X 坐标
originContainY: 0, // 容器初始 Y 坐标
};
},
methods: {
// 拖拽开始
startDrag(event) {
this.originMouseX = event.clientX; // 鼠标初始 X 坐标
this.originMouseY = event.clientY; // 鼠标初始 Y 坐标
this.originContainX = this.position.left; // 容器初始 X 坐标
this.originContainY = this.position.top; // 容器初始 Y 坐标
// 拖拽移动的时候需要改变容器位置,所以要监听move事件
document.addEventListener("mousemove", this.handleDrag);
// 鼠标松开,不再有拖拽效果
document.addEventListener("mouseup", this.stopDrag);
},
// 拖拽过程: 容器当前位置 = 鼠标当前位置 - 鼠标初始位置 + 容器初始位置;
handleDrag(event) {
this.position.left =
event.clientX - this.originMouseX + this.originContainX;
this.position.top =
event.clientY - this.originMouseY + this.originContainY;
},
// 停止拖拽
stopDrag() {
document.removeEventListener("mousemove", this.handleDrag);
document.removeEventListener("mouseup", this.stopDrag);
},
},
};
</script>
<style>
/* 拖拽容器的样式 */
.draggable-container {
position: absolute; /* 绝对定位方便改动 left 和 top */
cursor: move; /* 鼠标样式 */
user-select: none; /* 禁止选中文本 */
}
</style>
Vue 3 版本
在 Vue 3 中,我们可以使用 Teleport
组件将子组件直接传送到指定元素(如 body
)中,从而避免父元素的定位问题。
<template>
<Teleport to="body" :disabled="!appendToBody">
<div class="draggable-container" @mousedown="startDrag" :style="containerStyle">
<!-- 插槽内容 -->
<slot></slot>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
// 定义 props 类型
const props = defineProps({
left: { type: Number, default: 0 }, // 初始 X 位置
top: { type: Number, default: 0 }, // 初始 Y 位置
width: { type: Number, default: 300 }, // 初始宽度
height: { type: Number, default: 300 }, // 初始高度
zIndex: { type: Number, default: 1 }, // 层级
appendToBody: { type: Boolean, default: false } // 是否插入 body
});
// 定义响应式变量
const position = ref({ left: 0, top: 0 }); // 容器位置
const originMouseX = ref(0); // 鼠标初始 X 坐标
const originMouseY = ref(0); // 鼠标初始 Y 坐标
const originContainX = ref(0); // 容器初始 X 坐标
const originContainY = ref(0); // 容器初始 Y 坐标
// 计算容器样式
const containerStyle = computed(() => ({
top: `${position.value.top}px`,
left: `${position.value.left}px`,
width: `${props.width}px`,
height: `${props.height}px`,
zIndex: props.zIndex
}));
// 拖拽开始
const startDrag = (event: MouseEvent) => {
originMouseX.value = event.clientX;
originMouseY.value = event.clientY;
originContainX.value = position.value.left;
originContainY.value = position.value.top;
// 拖拽过程中监听鼠标移动
document.addEventListener('mousemove', handleDrag);
// 鼠标松开时停止拖拽
document.addEventListener('mouseup', stopDrag);
};
// 拖拽过程
const handleDrag = (event: MouseEvent) => {
position.value.left = event.clientX - originMouseX.value + originContainX.value;
position.value.top = event.clientY - originMouseY.value + originContainY.value;
};
// 停止拖拽
const stopDrag = () => {
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', stopDrag);
};
</script>
<style scoped>
/* 拖拽容器的样式 */
.draggable-container {
position: absolute; /* 绝对定位方便改动 left 和 top */
cursor: move; /* 鼠标样式 */
user-select: none; /* 禁止选中文本 */
}
</style>