float-add-btn 悬浮球支持屏幕内自由拖动与位置记忆
This commit is contained in:
1 parent
4007871658
commit
3194f98ac0
1 file changed
+135
-5
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 悬浮操作按钮:右下角圆形蓝色渐变「+」,固定在底部导航之上。
|
||||
* 悬浮操作按钮:蓝色渐变「+」,可在屏幕内自由拖动,松手后位置写入 Storage 下次启动恢复。
|
||||
* 只做外观与点击上报;新建/上传等动作语义由页面处理。
|
||||
* 拖动实现:touch 事件(真机/App)+ mouse 事件(H5 桌面调试)双轨;
|
||||
* 位移小于阈值视为轻点(emit click),超过阈值才算拖动(touch 后屏蔽 mouse 兼容事件防双触发)。
|
||||
*/
|
||||
defineProps({
|
||||
/** 额外底部偏移(rpx 数值),供不同页面避开各自的底部元素 */
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
/** 初始底部偏移(rpx 数值,仅首次无存储记录时生效),供不同页面避开各自的底部元素 */
|
||||
bottomOffset: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
@@ -14,10 +18,132 @@ defineProps({
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
|
||||
const STORAGE_KEY = "float-add-btn-pos";
|
||||
|
||||
/** 位移阈值(px):低于它按轻点处理 */
|
||||
const TAP_THRESHOLD = 6;
|
||||
|
||||
const pos = ref({ x: 0, y: 0 });
|
||||
|
||||
/** 位置定妥前隐藏,避免默认位置 → 恢复位置的一帧闪跳 */
|
||||
const ready = ref(false);
|
||||
|
||||
let dragging = false;
|
||||
let moved = false;
|
||||
let startPoint = { x: 0, y: 0 };
|
||||
let originPos = { x: 0, y: 0 };
|
||||
/** touch 已处理标记:移动端浏览器会在 touch 后补发 mouse 兼容事件,防双触发 */
|
||||
let touchHandled = false;
|
||||
|
||||
function sizePx(): number {
|
||||
return uni.upx2px(112);
|
||||
}
|
||||
|
||||
/** 夹在屏幕可视区内,四边留 8px 呼吸位 */
|
||||
function clampPos(x: number, y: number): { x: number; y: number } {
|
||||
const { windowWidth, windowHeight } = uni.getSystemInfoSync();
|
||||
const size = sizePx();
|
||||
return {
|
||||
x: Math.min(Math.max(8, x), windowWidth - size - 8),
|
||||
y: Math.min(Math.max(8, y), windowHeight - size - 8),
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const { windowWidth, windowHeight } = uni.getSystemInfoSync();
|
||||
const size = sizePx();
|
||||
let x = windowWidth - size - 20;
|
||||
let y = windowHeight - size - uni.upx2px(180 + props.bottomOffset);
|
||||
const saved = uni.getStorageSync(STORAGE_KEY) as { x?: number; y?: number } | "";
|
||||
if (saved && typeof saved.x === "number" && typeof saved.y === "number") {
|
||||
x = saved.x;
|
||||
y = saved.y;
|
||||
}
|
||||
pos.value = clampPos(x, y);
|
||||
ready.value = true;
|
||||
});
|
||||
|
||||
const posStyle = computed(() => ({
|
||||
left: `${pos.value.x}px`,
|
||||
top: `${pos.value.y}px`,
|
||||
visibility: ready.value ? "visible" : "hidden",
|
||||
}));
|
||||
|
||||
function onDragStart(x: number, y: number): void {
|
||||
dragging = true;
|
||||
moved = false;
|
||||
startPoint = { x, y };
|
||||
originPos = { ...pos.value };
|
||||
}
|
||||
|
||||
function onDragMove(x: number, y: number): void {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
const dx = x - startPoint.x;
|
||||
const dy = y - startPoint.y;
|
||||
if (!moved && Math.abs(dx) < TAP_THRESHOLD && Math.abs(dy) < TAP_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
moved = true;
|
||||
pos.value = clampPos(originPos.x + dx, originPos.y + dy);
|
||||
}
|
||||
|
||||
function onDragEnd(): void {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
dragging = false;
|
||||
if (moved) {
|
||||
uni.setStorageSync(STORAGE_KEY, pos.value);
|
||||
}
|
||||
else {
|
||||
emit("click");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== touch(真机 / App)=====
|
||||
function onTouchStart(e: TouchEvent): void {
|
||||
touchHandled = true;
|
||||
const touch = e.touches[0];
|
||||
onDragStart(touch.clientX, touch.clientY);
|
||||
}
|
||||
|
||||
function onTouchMove(e: TouchEvent): void {
|
||||
const touch = e.touches[0];
|
||||
onDragMove(touch.clientX, touch.clientY);
|
||||
}
|
||||
|
||||
function onTouchEnd(): void {
|
||||
onDragEnd();
|
||||
// 下一个宏任务复位,让补发的 mouse 兼容事件先被拦截
|
||||
setTimeout(() => {
|
||||
touchHandled = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ===== mouse(H5 桌面)=====
|
||||
function onMouseDown(e: MouseEvent): void {
|
||||
if (touchHandled) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
onDragStart(e.clientX, e.clientY);
|
||||
const move = (ev: MouseEvent) => onDragMove(ev.clientX, ev.clientY);
|
||||
const up = () => {
|
||||
onDragEnd();
|
||||
window.removeEventListener("mousemove", move);
|
||||
window.removeEventListener("mouseup", up);
|
||||
};
|
||||
window.addEventListener("mousemove", move);
|
||||
window.addEventListener("mouseup", up);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="float-add-btn" :style="{ bottom: `${180 + bottomOffset}rpx` }" @tap="emit('click')">
|
||||
<view class="float-add-btn" :style="posStyle" @touchstart="onTouchStart" @touchmove.stop.prevent="onTouchMove"
|
||||
@touchend="onTouchEnd" @mousedown="onMouseDown">
|
||||
<u-icon name="plus" size="52" color="#ffffff" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -25,7 +151,8 @@ const emit = defineEmits<{
|
||||
<style lang="scss" scoped>
|
||||
.float-add-btn {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -35,5 +162,8 @@ const emit = defineEmits<{
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ec2ff 0%, #2b8fff 100%);
|
||||
box-shadow: 0 12rpx 32rpx rgba(43, 143, 255, 0.4);
|
||||
touch-action: none;
|
||||
/* 屏蔽浏览器默认滚动手势,touchmove 才不被吞 */
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user