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