Files
Docker/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue
T
2026-10-16 11:57:29 +08:00

189 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 悬浮操作按钮:蓝色渐变「+」,可在屏幕内自由拖动,松手后位置写入 Storage 下次启动恢复。
* 只做外观与点击上报;新建/上传等动作语义由页面处理。
* 拖动实现:touch 事件(真机/App)+ mouse 事件(H5 桌面调试)双轨;
* 位移小于阈值视为轻点、超过才算拖动。点击上报走原生 click(onTap)而非 touchend/mouseup
* 手动 emit:保留浏览器用户激活链,否则轻点里同步拉起文件选择器等 gesture API 会被静默拦截;
* 拖动收尾置 suppressClick 吞掉随后的原生 click,防拖完误触。
*/
import { computed, onMounted, ref } from "vue";
import { useThemeTokens } from "@/composables/useThemeGradient";
const props = defineProps({
/** 初始底部偏移(rpx 数值,仅首次无存储记录时生效),供不同页面避开各自的底部元素 */
bottomOffset: {
type: Number,
default: 0,
},
});
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",
}));
/** 按钮面色跟随主题(与文件夹图形同派生,主题统一):渐变 + 同色柔光投影 */
const { accentGradient, accentShadow } = useThemeTokens();
const skinStyle = computed(() => ({
background: accentGradient.value,
boxShadow: `0 12rpx 32rpx ${accentShadow.value}`,
}));
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);
}
/** 拖动收尾后浏览器还会补发一次原生 click,置位吞掉它(轻点不置位,由 onTap 正常上报) */
let suppressClick = false;
function onDragEnd(): void {
if (!dragging) {
return;
}
dragging = false;
if (moved) {
uni.setStorageSync(STORAGE_KEY, pos.value);
suppressClick = true;
}
}
/** 唯一点击出口:原生 click(鼠标轻点/触屏补发)保留用户手势,页面才能在里面拉起文件选择器 */
function onTap(): void {
if (suppressClick) {
suppressClick = false;
return;
}
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;
}
// 注意不能 preventDefault:它会取消后续原生 click 及其用户激活,onTap 就再也收不到点击
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="[posStyle, skinStyle]" @touchstart="onTouchStart"
@touchmove.stop.prevent="onTouchMove" @touchend="onTouchEnd" @mousedown="onMouseDown" @click="onTap">
<u-icon name="plus" size="52" color="#ffffff" />
</view>
</template>
<style lang="scss" scoped>
.float-add-btn {
position: fixed;
top: 0;
left: 0;
z-index: 8;
display: flex;
align-items: center;
justify-content: center;
width: 112rpx;
height: 112rpx;
border-radius: 50%;
/* 面色与投影由 skinStyle 内联下发(主题派生),这里只留形态 */
touch-action: none;
/* 屏蔽浏览器默认滚动手势,touchmove 才不被吞 */
user-select: none;
}
</style>