uniapp(vue3) 拖拽元素支持小程序h5

uniapp(vue3) 拖拽元素支持小程序h5

拖拽组件

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>
​

使用组件

xml 复制代码
vue
<template>
  <view class="drag-container">
    <DragEl :initSize="dragElSize" ref="dragElRef">
      <template #default>
        <view style="width: 200rpx; height: 100rpx" class="bg-[#f0f0f0] p-4 rounded-md">
          <text>拖动元素</text>
        </view>
      </template>
    </DragEl>
  </view>
</template>
​
<script setup lang="ts">
import DragEl from '@/pages-word/components/DragEl/index.vue'
​
const dragElSize = ref({ width: 0, height: 0 })
const dragElRef = ref(null)
​
const getElementSize = () => {
  // 1. 创建选择器实例
  const query = uni.createSelectorQuery().in(dragElRef.value) // 关键:.in(this) 绑定当前组件实例(必填,否则可能获取不到)
​
  // 2. 选择目标元素,获取布局信息
  query
    .select('.drag-el') // 通过 id 选择元素(也可用 .class 选择,如 .box)
    .boundingClientRect((rect) => {
      // rect 包含元素的布局信息
      if (rect) {
        dragElSize.value = { width: rect.width, height: rect.height }
      } else {
        console.log('未找到目标元素')
      }
    })
    .exec() // 执行查询(必须调用,否则回调不触发)
}
onMounted(() => {
  nextTick(() => {
    getElementSize()
  })
})
</script>
​
<style scoped lang="scss">
.dd {
  position: absolute;
}
</style>
​
相关推荐
~无忧花开~4 小时前
JavaScript实现PDF本地预览技巧
开发语言·前端·javascript
小时前端5 小时前
“能说说事件循环吗?”—— 我从候选人回答中看到的浏览器与Node.js核心差异
前端·面试·浏览器
IT_陈寒5 小时前
Vite 5.0实战:10个你可能不知道的性能优化技巧与插件生态深度解析
前端·人工智能·后端
SAP庖丁解码5 小时前
【SAP Web Dispatcher负载均衡】
运维·前端·负载均衡
天蓝色的鱼鱼5 小时前
Ant Design 6.0 正式发布:前端开发者的福音与革新
前端·react.js·ant design
HIT_Weston5 小时前
38、【Ubuntu】【远程开发】拉出内网 Web 服务:构建静态网页(一)
linux·前端·ubuntu
零一科技6 小时前
Vue3拓展:自定义权限指令
前端·vue.js
im_AMBER6 小时前
AI井字棋项目开发笔记
前端·笔记·学习·算法
小时前端6 小时前
Vuex 响应式原理剖析:构建可靠的前端状态管理
前端·面试·vuex
xiaoxue..6 小时前
深入理解浏览器渲染流程:从HTML/CSS/JS到像素呈现
前端·javascript·css·html