import { ref, onMounted, onUnmounted, nextTick, readonly, type Ref } from 'vue' import interact from 'interactjs' export interface DragOptions { draggable?: boolean inertia?: boolean autoScroll?: boolean restrictToViewport?: boolean onDragStart?: () => void onDragMove?: (event: any) => void onDragEnd?: () => void onPositionChange?: (position: { x: number; y: number }) => void } export interface DragState { isDragging: boolean position: { x: number; y: number } } export function useDrag( elementRef: Ref, options: DragOptions = {} ) { const { draggable = true, inertia = true, autoScroll = true, restrictToViewport = true, onDragStart, onDragMove, onDragEnd, onPositionChange } = options // 拖拽状态 const isDragging = ref(false) const position = ref({ x: 0, y: 0 }) const interactable = ref(null) // 拖拽开始处理 const handleDragStart = (_event: any) => { isDragging.value = true onDragStart?.() } // 拖拽移动处理 const handleMove = (event: any) => { const target = event.target // 计算新位置:当前位置 + 位移量 const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy // 更新位置状态 position.value = { x, y } // 通过 CSS transform 实现元素位移 target.style.transform = `translate(${x}px, ${y}px)` // 保存位置数据到 DOM 属性 target.setAttribute('data-x', x) target.setAttribute('data-y', y) // 调用外部回调 onDragMove?.(event) onPositionChange?.(position.value) } // 拖拽结束处理 const handleDragEnd = () => { // 延时重置拖拽状态,避免与点击事件冲突 setTimeout(() => { isDragging.value = false onDragEnd?.() }, 0) } // 初始化拖拽功能 const initDrag = () => { if (!draggable || !elementRef.value) return const interactableInstance = interact(elementRef.value).draggable({ inertia, autoScroll, modifiers: restrictToViewport ? [ interact.modifiers.restrictRect({ restriction: { top: window.scrollY, left: window.scrollX, right: window.scrollX + window.innerWidth, bottom: window.scrollY + window.innerHeight - 48 }, endOnly: true // 仅在拖拽结束时应用边界限制 }) ] : [], listeners: { start: handleDragStart, move: handleMove, end: handleDragEnd } }) interactable.value = interactableInstance return interactableInstance } // 销毁拖拽功能 const destroyDrag = () => { if (interactable.value) { interactable.value.unset() interactable.value = null } } // 重新初始化拖拽(用于窗口大小变化等场景) const reinitDrag = () => { destroyDrag() nextTick(() => { initDrag() }) } // 设置位置 const setPosition = (x: number, y: number) => { if (!elementRef.value) return position.value = { x, y } elementRef.value.style.transform = `translate(${x}px, ${y}px)` elementRef.value.setAttribute('data-x', x.toString()) elementRef.value.setAttribute('data-y', y.toString()) } // 重置位置 const resetPosition = () => { setPosition(0, 0) } // 获取当前位置 const getPosition = () => { return { ...position.value } } // 组件挂载时初始化 onMounted(async () => { await nextTick() initDrag() // 监听窗口大小变化 const handleResize = () => { reinitDrag() } window.addEventListener('resize', handleResize) // 清理函数 onUnmounted(() => { destroyDrag() window.removeEventListener('resize', handleResize) }) }) return { // 状态 isDragging: readonly(isDragging), position: readonly(position), // 方法 initDrag, destroyDrag, reinitDrag, setPosition, resetPosition, getPosition } }