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">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 悬浮操作按钮:右下角圆形蓝色渐变「+」,固定在底部导航之上。
|
* 悬浮操作按钮:蓝色渐变「+」,可在屏幕内自由拖动,松手后位置写入 Storage 下次启动恢复。
|
||||||
* 只做外观与点击上报;新建/上传等动作语义由页面处理。
|
* 只做外观与点击上报;新建/上传等动作语义由页面处理。
|
||||||
|
* 拖动实现:touch 事件(真机/App)+ mouse 事件(H5 桌面调试)双轨;
|
||||||
|
* 位移小于阈值视为轻点(emit click),超过阈值才算拖动(touch 后屏蔽 mouse 兼容事件防双触发)。
|
||||||
*/
|
*/
|
||||||
defineProps({
|
import { computed, onMounted, ref } from "vue";
|
||||||
/** 额外底部偏移(rpx 数值),供不同页面避开各自的底部元素 */
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 初始底部偏移(rpx 数值,仅首次无存储记录时生效),供不同页面避开各自的底部元素 */
|
||||||
bottomOffset: {
|
bottomOffset: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
@@ -14,10 +18,132 @@ defineProps({
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "click"): void;
|
(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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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" />
|
<u-icon name="plus" size="52" color="#ffffff" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,7 +151,8 @@ const emit = defineEmits<{
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.float-add-btn {
|
.float-add-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 40rpx;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
z-index: 8;
|
z-index: 8;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -35,5 +162,8 @@ const emit = defineEmits<{
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(135deg, #6ec2ff 0%, #2b8fff 100%);
|
background: linear-gradient(135deg, #6ec2ff 0%, #2b8fff 100%);
|
||||||
box-shadow: 0 12rpx 32rpx rgba(43, 143, 255, 0.4);
|
box-shadow: 0 12rpx 32rpx rgba(43, 143, 255, 0.4);
|
||||||
|
touch-action: none;
|
||||||
|
/* 屏蔽浏览器默认滚动手势,touchmove 才不被吞 */
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user