具体目标
1、拖拽功能完好使用
2、不入侵业务
3、边界值比如不能拖拽出浏览器外
整体架构流程
三步走
鼠标按下:将鼠标按下时的位置记录并与被拖拽元素的左上角位置进行比较,以计算出鼠标按下点相对于被拖拽元素的偏移,
鼠标移动:根据鼠标指针的移动来更新被拖拽元素的位置,确保元素跟随鼠标的移动。
鼠标抬起:移除事件
具体代码实现
方案一 hooks写法
// 创建useDraggable.ts
export const useDraggable = (): Ref<HTMLDivElement | null> => {
// 声明一个 ref,用于存储 div 元素的引用
const divRef = ref<HTMLDivElement | null>(null)
// 声明一些变量,用于存储鼠标或触摸位置以及拖拽状态
let offsetX = 0 // 鼠标点击或触摸点距离 div 左侧的偏移
let offsetY = 0 // 鼠标点击或触摸点距离 div 顶部的偏移
let isDragging = false // 是否正在拖拽中
// 禁用页面滚动的函数
const disablePageScroll = () => {
document.body.style.overflow = 'hidden'
}
// 启用页面滚动的函数
const enablePageScroll = () => {
document.body.style.overflow = 'auto'
}
// 开始拖拽,禁用页面滚动
const startDragging = () => {
isDragging = true
disablePageScroll()
}
// 停止拖拽,启用页面滚动,并稍后重新启用点击事件
const stopDragging = () => {
isDragging = false
enablePageScroll()
setTimeout(() => {
if (divRef.value) {
divRef.value.style.pointerEvents = 'auto'
}
}, 100)
}
// 处理鼠标移动或触摸移动事件
const handleMouseMove = (event: MouseEvent | TouchEvent) => {
requestAnimationFrame(() => {
if (isDragging && divRef.value) {
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY
const x = clientX - offsetX
const y = clientY - offsetY
// 阻止事件传播,避免干扰正常滚动
event.stopPropagation()
event.preventDefault()
// 获取浏览器窗口的最大可视区域宽度和高度
const maxX = window.innerWidth - (divRef.value.clientWidth || 0)
const maxY = window.innerHeight - (divRef.value.clientHeight || 0)
// 设置 div 的位置,确保不超出窗口范围
divRef.value.style.left = `${Math.min(maxX, Math.max(0, x))}px`
divRef.value.style.top = `${Math.min(maxY, Math.max(0, y))}px`
// 禁用 div 上的点击事件,以避免拖拽时触发点击事件
divRef.value.style.pointerEvents = 'none'
}
})
}
// 处理鼠标松开或触摸结束事件
const handleMouseUp = () => {
// 停止拖拽,恢复点击事件
stopDragging()
// 移除鼠标移动事件和触摸移动事件的监听器
document.removeEventListener('touchmove', handleMouseMove)
document.removeEventListener('mousemove', handleMouseMove)
}
// 处理鼠标按下或触摸开始事件
const handleMouseDown = (event: MouseEvent | TouchEvent) => {
if (!divRef.value) return
// 获取鼠标点击或触摸点相对于 div 左侧和顶部的偏移
offsetX = 'touches' in event ? event.touches[0].clientX - divRef.value.offsetLeft : event.clientX - divRef.value.offsetLeft
offsetY = 'touches' in event ? event.touches[0].clientY - divRef.value.offsetTop : event.clientY - divRef.value.offsetTop
// 开始拖拽,添加鼠标移动和触摸移动事件监听器
startDragging()
document.addEventListener('mousemove', handleMouseMove, {
passive: false, // 阻止默认滚动行为
})
document.addEventListener('touchmove', handleMouseMove, {
passive: false, // 阻止默认滚动行为
})
// 添加鼠标松开和触摸结束事件监听器
document.addEventListener('mouseup', handleMouseUp)
document.addEventListener('touchend', handleMouseUp)
}
// 在组件挂载时,添加鼠标按下和触摸开始事件监听器
onMounted(() => {
if (divRef.value) {
divRef.value.addEventListener('mousedown', handleMouseDown)
divRef.value.addEventListener('touchstart', handleMouseDown)
}
})
// 在组件卸载时,移除事件监听器
onUnmounted(() => {
if (divRef.value) {
divRef.value.removeEventListener('mousedown', handleMouseDown)
divRef.value.removeEventListener('touchstart', handleMouseDown)
}
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('touchend', handleMouseUp)
})
// 返回 div 元素的引用,可以在组件中使用该引用来创建可拖拽的元素
return divRef
}
hooks的使用方法如下
<template>
<div
ref="draggableDiv"
class="it-layout-aside"
>古德古德~</div>
</template>
<script setup lang="tsx">
import { useDraggable } from '~/hooks/useDraggable'
const draggableDiv = useDraggable()
</script>
<style lang="stylus" scoped>
.it-layout-aside
flexCenter()
position fixed
bottom 100px
right 10px
width 60px
height 60px
border-radius 50%
background rgba(0,0,0,.5)
opacity 0.8
color #fff
font-size 40px
cursor move
&:hover
opacity 1
</style>
方案二 自定义指令写法
const vDraggable = {
mounted(el: HTMLElement) {
let offsetX = 0
let offsetY = 0
let isDragging = false
el.addEventListener('mousedown', event => {
isDragging = false
offsetX = event.clientX - el.offsetLeft
offsetY = event.clientY - el.offsetTop
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging && (Math.abs(event.clientX - offsetX) > 5 || Math.abs(event.clientY - offsetY) > 5)) {
isDragging = true
}
if (isDragging) {
const x = e.clientX - offsetX
const y = e.clientY - offsetY
el.style.left = `${Math.min(window.innerWidth - el.clientWidth, Math.max(0, x))}px`
el.style.top = `${Math.min(window.innerHeight - el.clientHeight, Math.max(0, y))}px`
el.style.pointerEvents = 'none'
}
}
const handleMouseUp = () => {
// 设置拖动状态为false
isDragging = false
setTimeout(() => {
el.style.pointerEvents = 'auto'
}, 100)
// 移除鼠标移动和松开事件
document.removeEventListener('mousemove', handleMouseMove)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
document.removeEventListener('mouseup', handleMouseUp)
}
})
},
}
自定义指令的方法使用如下
<template>
<div
v-draggable
class="it-layout-aside"
>你潮嘛~</div>
</template>
<style lang="stylus" scoped>
.it-layout-aside
flexCenter()
position fixed
bottom 100px
right 10px
width 60px
height 60px
border-radius 50%
background rgba(0,0,0,.5)
opacity 0.8
color #fff
font-size 40px
cursor move
&:hover
opacity 1
</style>