94 lines
2.1 KiB
Vue
94 lines
2.1 KiB
Vue
|
<template>
|
||
|
<div
|
||
|
ref="bubbleRef"
|
||
|
class="floating-bubble"
|
||
|
:style="bubbleStyle"
|
||
|
@touchstart="onTouchStart"
|
||
|
@touchmove="onTouchMove"
|
||
|
@touchend="onTouchEnd"
|
||
|
>
|
||
|
<slot></slot>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script setup>
|
||
|
import { ref, computed } from 'vue'
|
||
|
import { useThrottleFn } from '@vueuse/core'
|
||
|
|
||
|
const props = defineProps({
|
||
|
offset: {
|
||
|
type: Number,
|
||
|
default: 0
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const bubbleRef = ref(null)
|
||
|
const position = ref({
|
||
|
right: props.offset,
|
||
|
bottom: 100
|
||
|
})
|
||
|
|
||
|
let startTouch = { x: 0, y: 0 }
|
||
|
let startPos = { right: 0, bottom: 0 }
|
||
|
let isDragging = false
|
||
|
|
||
|
const bubbleStyle = computed(() => ({
|
||
|
position: 'fixed',
|
||
|
right: `${position.value.right}px`,
|
||
|
bottom: `${position.value.bottom}px`,
|
||
|
zIndex: 99999,
|
||
|
transition: isDragging ? 'none' : 'all 0.3s'
|
||
|
}))
|
||
|
|
||
|
const onTouchStart = (event) => {
|
||
|
isDragging = true
|
||
|
const touch = event.touches[0]
|
||
|
startTouch = { x: touch.clientX, y: touch.clientY }
|
||
|
startPos = { ...position.value }
|
||
|
}
|
||
|
|
||
|
const onTouchMove = useThrottleFn((event) => {
|
||
|
if (!isDragging) return
|
||
|
event.preventDefault()
|
||
|
|
||
|
const touch = event.touches[0]
|
||
|
const { clientWidth, clientHeight } = document.documentElement
|
||
|
const rect = bubbleRef.value.getBoundingClientRect()
|
||
|
|
||
|
// 计算新位置
|
||
|
const deltaX = startTouch.x - touch.clientX
|
||
|
const deltaY = startTouch.y - touch.clientY
|
||
|
const newRight = startPos.right + deltaX
|
||
|
const newBottom = startPos.bottom + deltaY
|
||
|
|
||
|
// 边界检测
|
||
|
position.value = {
|
||
|
right: Math.min(Math.max(newRight, props.offset),
|
||
|
clientWidth - rect.width - props.offset),
|
||
|
bottom: Math.min(Math.max(newBottom, props.offset),
|
||
|
clientHeight - rect.height - props.offset)
|
||
|
}
|
||
|
}, 16)
|
||
|
|
||
|
const onTouchEnd = () => {
|
||
|
if (!isDragging) return
|
||
|
isDragging = false
|
||
|
|
||
|
const { clientWidth } = document.documentElement
|
||
|
const rect = bubbleRef.value.getBoundingClientRect()
|
||
|
const left = clientWidth - position.value.right - rect.width
|
||
|
|
||
|
// 吸附
|
||
|
position.value.right = left < clientWidth / 2
|
||
|
? clientWidth - rect.width - props.offset
|
||
|
: props.offset
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<style scoped>
|
||
|
.floating-bubble {
|
||
|
touch-action: none;
|
||
|
user-select: none;
|
||
|
}
|
||
|
</style>
|