一、最终效果

二、具体详情请看movable-area与movable-view官方文档说明
三、组件源码
html
<template>
<movable-area class="movable-area" @touchend="onTouchend">
<movable-view class="movable-view" :x="x" :y="y" direction="all" @change="onChange">
<view class="addBtn" @tap="handleClick">{{title}}</view>
<slot />
</movable-view>
</movable-area>
</template>
<script lang="ts" setup>
import { debounce } from "@/utils";
defineProps({
title: {
type: String
}
});
const emits = defineEmits(["click"]);
const x = ref(0);
const y = ref(0);
const screenWidth = ref(0);
const screenHeight = ref(0);
onMounted(() => {
uni.getSystemInfo({
success: res => {
screenWidth.value = res.windowWidth;
screenHeight.value = res.windowHeight;
// 初始位置在屏幕右下角
y.value = screenHeight.value - 200;
x.value = screenWidth.value - 70;
}
});
});
// 拖动坐标更新(防抖)
const onChange = (e: { detail: { x: number; y: number } }) => {
debounce(() => {
x.value = e.detail.x;
y.value = e.detail.y;
}, 500);
};
// 触摸结束时吸附边缘
const onTouchend = () => {
nextTick(() => {
const threshold = 50; // 吸附阈值(rpx)
if (Math.abs(x.value - 0) < threshold) {
x.value = 0;
} else if (Math.abs(x.value - screenWidth.value) < threshold) {
x.value = screenWidth.value;
}
if (Math.abs(y.value - 0) < threshold) {
y.value = 0;
} else if (Math.abs(y.value - screenHeight.value) < threshold) {
y.value = screenHeight.value;
}
});
};
const handleClick = () => {
emits("click");
};
</script>
<style lang="scss">
.movable-area {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: calc(100vh - 100px);
pointer-events: none; /* 关键样式 */
z-index: 9999;
.movable-view {
pointer-events: auto; /* 关键样式 */
width: 100rpx;
height: 100rpx;
will-change: transform;
.addBtn {
border-radius: 50%;
width: 40px;
height: 40px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 14px;
padding: 8px;
box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.3);
background: #355db4;
text-align: center;
}
}
}
</style>