以下是手写的 useDraggable
函数实现,用于实现元素的拖拽功能:
typescript
import {
ref,
onMounted,
onUnmounted,
unref,
type Ref,
type ComponentPublicInstance
} from 'vue'
interface UseDraggableOptions {
initialValue?: { x: number; y: number }
axis?: 'both' | 'x' | 'y'
bounds?: {
left?: number
right?: number
top?: number
bottom?: number
}
}
export function useDraggable(
target: Ref<HTMLElement | ComponentPublicInstance | null>,
options: UseDraggableOptions = {}
) {
const { initialValue = { x: 0, y: 0 }, axis = 'both', bounds } = options
const x = ref(initialValue.x)
const y = ref(initialValue.y)
const isDragging = ref(false)
let element: HTMLElement | null = null
let startX = 0
let startY = 0
let initialX = 0
let initialY = 0
const getElement = (): HTMLElement | null => {
const el = unref(target)
return (el as ComponentPublicInstance)?.$el ?? el
}
const applyTransform = () => {
if (!element) return
element.style.transform = `translate(${x.value}px, ${y.value}px)`
}
const clamp = (value: number, min?: number, max?: number) => {
if (typeof min === 'number') value = Math.max(value, min)
if (typeof max === 'number') value = Math.min(value, max)
return value
}
const onStart = (clientX: number, clientY: number) => {
isDragging.value = true
startX = clientX
startY = clientY
initialX = x.value
initialY = y.value
}
const onMove = (clientX: number, clientY: number) => {
if (!isDragging.value) return
let deltaX = clientX - startX
let deltaY = clientY - startY
if (axis === 'x') deltaY = 0
if (axis === 'y') deltaX = 0
let newX = initialX + deltaX
let newY = initialY + deltaY
// 边界限制
if (bounds) {
newX = clamp(newX, bounds.left, bounds.right)
newY = clamp(newY, bounds.top, bounds.bottom)
}
x.value = newX
y.value = newY
applyTransform()
}
const onEnd = () => {
isDragging.value = false
}
const handleMouseDown = (e: MouseEvent) => {
e.preventDefault()
onStart(e.clientX, e.clientY)
}
const handleTouchStart = (e: TouchEvent) => {
const touch = e.touches[0]
if (touch) onStart(touch.clientX, touch.clientY)
}
const addEventListeners = () => {
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.addEventListener('touchmove', handleTouchMove)
document.addEventListener('touchend', handleTouchEnd)
}
const removeEventListeners = () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('touchmove', handleTouchMove)
document.removeEventListener('touchend', handleTouchEnd)
}
const handleMouseMove = (e: MouseEvent) => onMove(e.clientX, e.clientY)
const handleTouchMove = (e: TouchEvent) => {
const touch = e.touches[0]
if (touch) onMove(touch.clientX, touch.clientY)
}
const handleMouseUp = () => onEnd()
const handleTouchEnd = () => onEnd()
const init = () => {
element = getElement()
if (!element) return
element.style.cursor = 'grab'
element.style.userSelect = 'none'
element.style.touchAction = 'none'
element.addEventListener('mousedown', handleMouseDown)
element.addEventListener('touchstart', handleTouchStart)
}
const cleanup = () => {
if (!element) return
element.removeEventListener('mousedown', handleMouseDown)
element.removeEventListener('touchstart', handleTouchStart)
element.style.cursor = ''
element.style.userSelect = ''
element.style.touchAction = ''
}
const stop = () => {
cleanup()
removeEventListeners()
}
onMounted(() => {
init()
applyTransform()
})
onUnmounted(() => {
stop()
})
return {
x,
y,
isDragging,
stop,
start: () => init(),
applyTransform
}
}
使用示例:
vue
<template>
<div ref="draggableEl" class="draggable-box">
拖拽我
</div>
<div class="position-info">
X: {{ x }}px, Y: {{ y }}px
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from './useDraggable'
const draggableEl = ref<HTMLElement | null>(null)
const { x, y, isDragging } = useDraggable(draggableEl, {
initialValue: { x: 100, y: 100 },
axis: 'both',
bounds: {
left: 0,
top: 0,
right: window.innerWidth - 200,
bottom: window.innerHeight - 200
}
})
</script>
<style>
.draggable-box {
width: 200px;
height: 200px;
background-color: #42b883;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
position: fixed;
}
.position-info {
position: fixed;
bottom: 20px;
left: 20px;
padding: 10px;
background: rgba(0,0,0,0.7);
color: white;
border-radius: 4px;
}
</style>
实现细节说明:
-
核心功能:
- 支持鼠标和触摸屏操作
- 可限制拖拽方向(x/y轴或双向)
- 支持设置拖拽边界
- 自动处理CSS transform应用
-
响应式数据:
x
: 水平位移y
: 垂直位移isDragging
: 当前拖拽状态
-
实现亮点:
typescript// 边界限制计算 const clamp = (value: number, min?: number, max?: number) => { if (typeof min === 'number') value = Math.max(value, min) if (typeof max === 'number') value = Math.min(value, max) return value }
确保元素始终保持在指定边界内
-
事件处理优化:
- 使用文档级事件监听器处理移动事件,避免快速拖拽时丢失事件
- 同时支持鼠标和触摸事件
typescript// 统一事件处理 const onStart = (clientX: number, clientY: number) => { // 记录初始位置 } const onMove = (clientX: number, clientY: number) => { // 计算新位置 }
-
样式处理:
typescript// 自动应用transform const applyTransform = () => { if (!element) return element.style.transform = `translate(${x.value}px, ${y.value}px)` }
使用transform实现高性能位移动画
-
生命周期管理:
typescriptonMounted(() => { init() applyTransform() }) onUnmounted(() => { stop() })
确保组件挂载时初始化,卸载时清理资源
-
类型安全:
- 完整的TypeScript类型定义
- 支持Vue组件实例和原生DOM元素
配置选项说明:
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
initialValue | { x: number; y: number } | { x:0, y:0 } | 初始位置 |
axis | 'both' | 'x' | 'y' | 'both' | 允许拖拽的方向 |
bounds | { left?, right?, top?, bottom? } | undefined | 拖拽边界限制 |
返回API说明:
属性/方法 | 类型 | 说明 |
---|---|---|
x | Ref | 当前X轴位置 |
y | Ref | 当前Y轴位置 |
isDragging | Ref | 是否正在拖拽 |
stop | () => void | 停止拖拽功能 |
start | () => void | 重新启用拖拽功能 |
applyTransform | () => void | 立即应用当前transform样式 |
注意事项:
- 目标元素需要设置为
position: fixed
或position: absolute
- 边界计算需要考虑元素自身的尺寸
- 使用transform实现位移,不会影响文档流布局
- 触摸事件需要正确处理多点触控场景
该实现提供了与VueUse相似的拖拽功能,同时包含以下增强特性:
- 更严格的边界限制处理
- 更完善的触摸事件支持
- 更直观的API设计
- 更好的TypeScript类型提示