ini
复制代码
<template>
<view
ref="dragRef"
class="drag-el"
:style="{ left: `${position.x}px`, top: `${position.y}px` }"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
>
<slot></slot>
</view>
</template>
<script setup lang="ts">
// 定义组件属性
const props = withDefaults(
defineProps<{
// 初始位置
initialPosition?: {
x: number
y: number
}
// 边界范围(相对于父元素)
boundary?: {
minX?: number
maxX?: number
minY?: number
maxY?: number
}
// 回弹阈值(百分比,0-1)
bounceThreshold?: number
// 是否启用回弹
enableBounce?: boolean
initSize: {
width: number
height: number
}
}>(),
{
initialPosition: () => ({ x: 0, y: 0 }),
boundary: () => ({}),
bounceThreshold: 0.5,
enableBounce: true,
},
)
const isMiniProgram = os.isXcx
// 定义事件
const emit = defineEmits<{
start: [{ x: number; y: number }]
move: [{ x: number; y: number }]
end: [{ x: number; y: number }]
bounce: [{ x: number; y: number }, 'left' | 'right']
}>()
// 组件实例引用
const dragRef = ref()
// 位置状态
const position = reactive<{ x: number; y: number }>({
x: props.initialPosition.x,
y: props.initialPosition.y,
})
// 拖拽状态
const isDragging = ref(false)
const startPosition = ref({ x: 0, y: 0 })
const elementStartPosition = ref({ x: 0, y: 0 })
// 计算拖拽边界父元素的宽高减去元素自身的宽高
const calculatedBoundary = computed(() => {
const boundary = { ...props.boundary }
// 如果没有指定边界,尝试从父元素获取
if (!boundary.maxX || !boundary.maxY) {
try {
// 首先检查是否为小程序环境
if (isMiniProgram) {
// 小程序环境,使用屏幕尺寸减去元素实际宽度(如果有)
const systemInfo = uni.getSystemInfoSync()
const { windowWidth, windowHeight } = systemInfo
if (boundary.maxX === undefined) {
boundary.maxX = windowWidth - props.initSize.width
}
if (boundary.maxY === undefined) {
boundary.maxY = windowHeight - props.initSize.height
}
} else {
const currentElement = dragRef.value.$el
// H5环境尝试获取父元素
const parent = currentElement.parentElement
if (parent) {
const parentRect = parent.getBoundingClientRect()
// 使用实际获取的元素尺寸
const elementWidth = props.initSize.width
const elementHeight = props.initSize.height
if (boundary.maxX === undefined) {
boundary.maxX = parentRect.width - elementWidth
}
if (boundary.maxY === undefined) {
boundary.maxY = parentRect.height - elementHeight
}
} else {
console.warn('无法获取父元素,使用默认边界值')
// 使用默认边界值作为备选
if (boundary.maxX === undefined) {
boundary.maxX = window.innerWidth - props.initSize.width // 假设元素宽度约为100px
}
if (boundary.maxY === undefined) {
boundary.maxY = window.innerHeight - props.initSize.height // 假设元素高度约为100px
}
}
}
} catch (error) {
console.error('获取父元素尺寸时出错:', error)
// 出错时使用默认值
if (boundary.maxX === undefined) {
boundary.maxX = 300 // 默认最大X值
}
if (boundary.maxY === undefined) {
boundary.maxY = 300 // 默认最大Y值
}
}
}
// 确保minX和minY默认为0
if (boundary.minX === undefined) {
boundary.minX = 0
}
if (boundary.minY === undefined) {
boundary.minY = 0
}
console.log('~~~~计算边界calculatedBoundary', boundary)
return boundary
})
// 处理触摸开始
const handleTouchStart = (e: TouchEvent) => {
console.log('~~~~开始handleTouchStart', e)
isDragging.value = true
// 记录拖拽开始位置
startPosition.value = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
}
elementStartPosition.value = { ...position }
emit('start', { ...position })
}
// 处理触摸移动
const handleTouchMove = (e: TouchEvent) => {
console.log('~~~~~移动handleTouchMove', e)
if (!isDragging.value) return
// 计算移动距离
const deltaX = e.touches[0].clientX - startPosition.value.x
const deltaY = e.touches[0].clientY - startPosition.value.y
updatePosition(deltaX, deltaY)
}
// 处理触摸结束
const handleTouchEnd = () => {
console.log('~~~~~结束handleTouchEnd')
finishDrag()
}
// 处理鼠标按下
const handleMouseDown = (e: MouseEvent) => {
console.log('~~~~鼠标按下handleMouseDown', e)
isDragging.value = true
startPosition.value = { x: e.clientX, y: e.clientY }
elementStartPosition.value = { ...position }
emit('start', { ...position })
// 阻止文本选择
e.preventDefault()
}
// 处理鼠标移动
const handleMouseMove = (e: MouseEvent) => {
console.log('~~~~鼠标移动handleMouseMove', e)
if (!isDragging.value) return
const deltaX = e.clientX - startPosition.value.x
const deltaY = e.clientY - startPosition.value.y
updatePosition(deltaX, deltaY)
}
// 处理鼠标松开
const handleMouseUp = () => {
console.log('~~~~鼠标松开handleMouseUp')
finishDrag()
}
// 更新位置
const updatePosition = (deltaX: number, deltaY: number) => {
console.log('~~~~更新位置updatePosition', deltaX, deltaY)
let newX = elementStartPosition.value.x + deltaX
let newY = elementStartPosition.value.y + deltaY
// 应用边界约束
const boundary = calculatedBoundary.value
// x轴最小不能小于0
if (boundary.minX !== undefined) {
newX = Math.max(boundary.minX, newX)
}
// x轴最大不能大于父元素宽度减去元素宽度
if (boundary.maxX !== undefined) {
newX = Math.min(boundary.maxX, newX)
}
// y轴最小不能小于0
if (boundary.minY !== undefined) {
newY = Math.max(boundary.minY, newY)
}
// y轴最大不能大于父元素高度减去元素高度
if (boundary.maxY !== undefined) {
newY = Math.min(boundary.maxY, newY)
}
// 更新位置
position.x = newX
position.y = newY
emit('move', { ...position })
}
// 完成拖拽,处理回弹
const finishDrag = () => {
if (!isDragging.value) return
isDragging.value = false
// 处理回弹逻辑
if (props.enableBounce && calculatedBoundary.value.maxX !== undefined) {
const maxX = calculatedBoundary.value.maxX
const threshold = props.bounceThreshold * maxX
if (position.x < threshold) {
// 回弹到左侧
animateToPosition(0, position.y, 'left')
} else if (position.x > maxX - threshold) {
// 回弹到右侧
animateToPosition(maxX, position.y, 'right')
return // 动画完成后会触发end事件
}
}
emit('end', { ...position })
}
// 兼容的动画帧请求函数,适配小程序环境
const requestAnimationFrameCompat = (callback: any) => {
// 检查是否为小程序环境
return setTimeout(callback, 16) // 约60fps
}
// 动画移动到指定位置
const animateToPosition = (targetX: number, targetY: number, direction: 'left' | 'right') => {
const startX = position.x
const startY = position.y
const duration = 300 // 动画持续时间
const startTime = Date.now()
let animationId: any = null
const animate = () => {
const currentTime = Date.now()
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// 使用缓动函数
const easeOut = 1 - (1 - progress) ** 3
position.x = startX + (targetX - startX) * easeOut
position.y = startY + (targetY - startY) * easeOut
if (progress < 1) {
animationId = requestAnimationFrameCompat(animate)
} else {
// 动画结束
position.x = targetX
position.y = targetY
emit('bounce', { ...position }, direction)
emit('end', { ...position })
}
}
animationId = requestAnimationFrameCompat(animate)
}
// 暴露方法给父组件
const setPosition = (x: number, y: number) => {
position.x = x
position.y = y
}
const resetPosition = () => {
position.x = props.initialPosition.x
position.y = props.initialPosition.y
}
defineExpose({
setPosition,
resetPosition,
position,
})
</script>
<style scoped lang="scss">
.drag-el {
position: absolute;
cursor: move; /* 鼠标指针样式 */
user-select: none; /* 防止文本选中 */
touch-action: none; /* 防止触摸事件的默认行为 */
// 增加可点击区域
&:active {
// 可以在这里添加拖拽时的视觉反馈
}
}
</style>