<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>
vue实现带自动吸附功能的悬浮球
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。