vue实现带自动吸附功能的悬浮球

<template>
  <div
    ref="floatDragRef"
    class="float-position"
    :style="{ left: left + 'px', top: top + 'px', zIndex: zIndex }"
    @touchmove.prevent
    @mousemove.prevent
    @mousedown="handleMouseDown"
    @mouseup="handleMouseUp"
  >
    <svg
      t="1630029318602"
      class="icon"
      viewBox="0 0 1024 1024"
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      p-id="1244"
      width="200"
      height="200"
    >
      <path
        d="M554.376075 850.102995l0.208185 87.874926 170.711774-0.14573v85.355887l-209.038649 0.187367c-1.39484 0.124911-2.727225 0.41637-4.163702 0.41637s-2.706406-0.291459-4.163702-0.41637l-208.997011 0.187366v-85.230975l170.33704-0.14573-0.208185-88.041474A383.643483 383.643483 0 0 1 128.200378 469.061825h84.772969a298.37087 298.37087 0 0 0 294.769268 297.704678c1.290748-0.124911 2.539858-0.395552 3.872243-0.395551s2.498221 0.270641 3.76815 0.374733a298.350052 298.350052 0 0 0 294.60272-297.704678h85.751438a383.664302 383.664302 0 0 1-341.361091 381.061988z m-42.240755-168.047005A213.160713 213.160713 0 0 1 298.93297 468.936914h0.458007l-0.374733-255.65129a213.160713 213.160713 0 0 1 426.300608-1.936121c0 0.374733 0.124911 0.728648 0.124911 1.124199l0.374733 256.463212a42.782036 42.782036 0 0 1-0.791103 7.890215 213.035802 213.035802 0 0 1-212.890073 205.228861z m128.32529-213.223168l-0.374734-255.65129h-0.333096a127.721552 127.721552 0 0 0-255.422286 0h-0.166548l0.374733 255.65129v1.061744a127.659097 127.659097 0 0 0 255.318194-0.957652h0.624555a0.895196 0.895196 0 0 0-0.124911-0.104092z m-129.366215-42.532214h2.081851H510.990302z"
        fill="#fff"
        p-id="1245"
      ></path>
    </svg>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'

// Props
const props = defineProps({
  distanceRight: {
    type: Number,
    default: 0
  },
  distanceBottom: {
    type: Number,
    default: 100
  },
  isScrollHidden: {
    type: Boolean,
    default: false
  },
  isCanDraggable: {
    type: Boolean,
    default: true
  },
  zIndex: {
    type: Number,
    default: 50
  },
  value: {
    type: String,
    default: "悬浮球!"
  }
})

// Emits
const emit = defineEmits(['handlepaly'])

// Refs
const floatDragRef = ref(null)

// Reactive state
const clientWidth = ref(0)
const clientHeight = ref(0)
const left = ref(0)
const top = ref(0)
const timer = ref(null)
const currentTop = ref(0)
const mousedownX = ref(0)
const mousedownY = ref(0)
const canClick = ref(false)
let floatDragDom = null

// 初始化窗口尺寸
const initWindowSize = () => {
  clientWidth.value = document.documentElement.clientWidth
  clientHeight.value = document.documentElement.clientHeight
}

// 边界检查
const checkBoundary = () => {
  if (left.value < 0) left.value = 0
  if (top.value < 0) top.value = 0
  if (left.value + floatDragDom.width > clientWidth.value) {
    left.value = clientWidth.value - floatDragDom.width
  }
  if (top.value + floatDragDom.height > clientHeight.value) {
    top.value = clientHeight.value - floatDragDom.height
  }
}

// 判断元素显示位置(贴边)
const checkDraggablePosition = () => {
  if (left.value + floatDragDom.width / 2 >= clientWidth.value / 2) {
    left.value = clientWidth.value - floatDragDom.width
  } else {
    left.value = 0
  }
  if (top.value < 0) {
    top.value = 0
  }
  if (top.value + floatDragDom.height >= clientHeight.value) {
    top.value = clientHeight.value - floatDragDom.height
  }
}

// 滚动结束处理
const handleScrollEnd = () => {
  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
  if (scrollTop === currentTop.value) {
    if (left.value + floatDragDom.width / 2 >= clientWidth.value / 2) {
      left.value = clientWidth.value - floatDragDom.width
    } else {
      left.value = 0
    }
    clearTimeout(timer.value)
  }
}

// 滚动监听
const handleScroll = () => {
  if (timer.value) clearTimeout(timer.value)
  timer.value = setTimeout(() => {
    handleScrollEnd()
  }, 200)
  
  currentTop.value = document.documentElement.scrollTop || document.body.scrollTop
  
  const currentCenterX = left.value + floatDragDom.width / 2
  if (currentCenterX > clientWidth.value / 2) {
    left.value = clientWidth.value
  } else {
    left.value = -floatDragDom.width
  }
}

// 窗口大小改变处理
const handleResize = () => {
  clientWidth.value = document.documentElement.clientWidth
  clientHeight.value = document.documentElement.clientHeight
  checkDraggablePosition()
}

// 鼠标/触摸事件处理
const handleMouseDown = (e) => {
  if (!props.isCanDraggable) return
  
  const event = e || window.event
  mousedownX.value = event.screenX
  mousedownY.value = event.screenY
  
  const floatDragWidth = floatDragDom.width / 2
  const floatDragHeight = floatDragDom.height / 2
  
  if (event.preventDefault) {
    event.preventDefault()
  }
  
  canClick.value = false
  
  if (floatDragRef.value) {
    floatDragRef.value.style.transition = 'none'
  }
  
  const handleMouseMove = (e) => {
    const event = e || window.event
    left.value = event.clientX - floatDragWidth
    top.value = event.clientY - floatDragHeight
    checkBoundary()
  }
  
  const handleMouseUp = (e) => {
    const event = e || window.event
    // 判断只是单纯的点击,没有拖拽
    if (mousedownY.value === event.screenY && mousedownX.value === event.screenX) {
      emit('handlepaly')
    }
    document.removeEventListener('mousemove', handleMouseMove)
    document.removeEventListener('mouseup', handleMouseUp)
    checkDraggablePosition()
    if (floatDragRef.value) {
      floatDragRef.value.style.transition = 'all 0.3s'
    }
  }
  
  document.addEventListener('mousemove', handleMouseMove)
  document.addEventListener('mouseup', handleMouseUp)
}

const handleMouseUp = () => {
  // 鼠标松开时的处理(用于防止事件冲突)
}

// 触摸事件
const handleTouchStart = () => {
  if (!props.isCanDraggable) return
  canClick.value = false
  if (floatDragRef.value) {
    floatDragRef.value.style.transition = 'none'
  }
}

const handleTouchMove = (e) => {
  if (!props.isCanDraggable) return
  canClick.value = true
  if (e.targetTouches.length === 1) {
    const touch = e.targetTouches[0]
    left.value = touch.clientX - floatDragDom.width / 2
    top.value = touch.clientY - floatDragDom.height / 2
    checkBoundary()
  }
}

const handleTouchEnd = () => {
  if (!props.isCanDraggable) return
  if (!canClick.value) return // 解决点击事件和touch事件冲突的问题
  if (floatDragRef.value) {
    floatDragRef.value.style.transition = 'all 0.3s'
  }
  checkDraggablePosition()
}

// 初始化拖拽功能
const initDraggable = () => {
  if (!floatDragRef.value) return
  
  floatDragRef.value.addEventListener('touchstart', handleTouchStart)
  floatDragRef.value.addEventListener('touchmove', handleTouchMove)
  floatDragRef.value.addEventListener('touchend', handleTouchEnd)
}

// 生命周期
onMounted(() => {
  initWindowSize()
  
  if (props.isCanDraggable) {
    nextTick(() => {
      if (floatDragRef.value) {
        floatDragDom = floatDragRef.value.getBoundingClientRect()
        // 设置初始位置
        left.value = clientWidth.value - floatDragDom.width - props.distanceRight
        top.value = clientHeight.value - floatDragDom.height - props.distanceBottom
        initDraggable()
      }
    })
  }
  
  if (props.isScrollHidden) {
    window.addEventListener('scroll', handleScroll)
  }
  window.addEventListener('resize', handleResize)
})

onBeforeUnmount(() => {
  if (props.isScrollHidden) {
    window.removeEventListener('scroll', handleScroll)
  }
  window.removeEventListener('resize', handleResize)
  
  // 清理定时器
  if (timer.value) {
    clearTimeout(timer.value)
  }
  
  // 清理事件监听
  if (floatDragRef.value) {
    floatDragRef.value.removeEventListener('touchstart', handleTouchStart)
    floatDragRef.value.removeEventListener('touchmove', handleTouchMove)
    floatDragRef.value.removeEventListener('touchend', handleTouchEnd)
  }
})
</script>

<style>
/* 注意:这个样式会影响全局,建议移除或调整 */
/* html,
body {
  overflow: hidden;
} */
</style>

<style scoped lang="scss">
.float-position {
  position: absolute;
  z-index: 10003;
  right: 0;
  top: 70%;
  width: 3.6em;
  height: 3.6em;
  background: deepskyblue;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0px 0px 10px 2px skyblue;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.8em;
  user-select: none;
  cursor: pointer;
  
  &:active {
    cursor: grabbing;
  }
}

.cart {
  border-radius: 50%;
  width: 5em;
  height: 5em;
  display: flex;
  align-items: center;
  justify-content: center;
}

.header-notice {
  display: inline-block;
  transition: all 0.3s;

  span {
    vertical-align: initial;
  }

  .notice-badge {
    color: inherit;

    .header-notice-icon {
      font-size: 16px;
      padding: 4px;
    }
  }
}

.drag-ball .drag-content {
  overflow-wrap: break-word;
  font-size: 14px;
  color: #fff;
  letter-spacing: 2px;
}
</style>

参考文章:https://www.jb51.net/article/244694.htm

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • """1.个性化消息: 将用户的姓名存到一个变量中,并向该用户显示一条消息。显示的消息应非常简单,如“Hello ...
    她即我命阅读 5,331评论 0 6
  • 1、expected an indented block 冒号后面是要写上一定的内容的(新手容易遗忘这一点); 缩...
    庵下桃花仙阅读 1,103评论 1 2
  • 一、工具箱(多种工具共用一个快捷键的可同时按【Shift】加此快捷键选取)矩形、椭圆选框工具 【M】移动工具 【V...
    墨雅丫阅读 1,610评论 0 0
  • 跟随樊老师和伙伴们一起学习心理知识提升自已,已经有三个月有余了,这一段时间因为天气的原因休课,顺便整理一下之前学习...
    学习思考行动阅读 1,017评论 0 2
  • 一脸愤怒的她躺在了床上,好几次甩开了他抱过来的双手,到最后还坚决的翻了个身,只留给他一个冷漠的背影。 多次尝试抱她...
    海边的蓝兔子阅读 1,024评论 1 4

友情链接更多精彩内容