Files
WAI_Project_UNIX/uni_modules/cool-ui/components/cl-float-view/cl-float-view.uvue

284 lines
6.4 KiB
Plaintext
Raw Normal View History

2025-07-21 16:47:04 +08:00
<template>
<view
class="cl-float-view"
:style="viewStyle"
@touchstart="onTouchStart"
@touchmove.stop.prevent="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<slot></slot>
</view>
</template>
<script setup lang="ts">
import { router, usePage } from "@/cool";
import { computed, reactive } from "vue";
defineOptions({
name: "cl-float-view"
});
const props = defineProps({
// 图层
zIndex: {
type: Number,
default: 500
},
// 尺寸
size: {
type: Number,
default: 40
},
// 左边距
left: {
type: Number,
default: 10
},
// 底部距离
bottom: {
type: Number,
default: 10
},
// 距离边缘的间距
gap: {
type: Number,
default: 10
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 不吸附边缘
noSnapping: {
type: Boolean,
default: false
}
});
// 获取设备屏幕信息
const { screenWidth, statusBarHeight, screenHeight } = uni.getWindowInfo();
// 页面实例
const page = usePage();
/**
* 悬浮按钮位置状态类型定义
*/
type Position = {
x: number; // 水平位置(左边距)
y: number; // 垂直位置(相对底部的距离)
isDragging: boolean; // 是否正在拖拽中
};
/**
* 悬浮按钮位置状态管理
* 控制按钮在屏幕上的位置和拖拽状态
*/
const position = reactive<Position>({
x: props.left, // 初始左边距10px
y: props.bottom, // 初始距离底部10px
isDragging: false // 初始状态为非拖拽
});
/**
* 拖拽操作状态类型定义
*/
type DragState = {
startX: number; // 拖拽开始时的X坐标
startY: number; // 拖拽开始时的Y坐标
};
/**
* 拖拽操作状态管理
* 记录拖拽过程中的关键信息
*/
const dragState = reactive<DragState>({
startX: 0,
startY: 0
});
/**
* 动态位置样式计算
* 根据当前位置和拖拽状态计算组件的CSS样式
*/
const viewStyle = computed(() => {
2025-07-29 22:30:23 +08:00
const style = {};
2025-07-21 16:47:04 +08:00
// 额外的底部偏移
let bottomOffset = 0;
// 标签页需要额外减去标签栏高度和安全区域
if (page.hasCustomTabBar()) {
bottomOffset += page.getTabBarHeight();
}
// 设置水平位置
2025-07-29 22:30:23 +08:00
style["left"] = `${position.x}px`;
2025-07-21 16:47:04 +08:00
// 设置垂直位置(从底部计算)
2025-07-29 22:30:23 +08:00
style["bottom"] = `${bottomOffset + position.y}px`;
2025-07-21 16:47:04 +08:00
// 设置z-index
2025-07-29 22:30:23 +08:00
style["z-index"] = props.zIndex;
2025-07-21 16:47:04 +08:00
// 设置尺寸
2025-07-29 22:30:23 +08:00
style["width"] = `${props.size}px`;
2025-07-21 16:47:04 +08:00
// 设置高度
2025-07-29 22:30:23 +08:00
style["height"] = `${props.size}px`;
2025-07-21 16:47:04 +08:00
// 非拖拽状态下添加过渡动画,使移动更平滑
if (!position.isDragging) {
2025-07-29 22:30:23 +08:00
style["transition-duration"] = "300ms";
2025-07-21 16:47:04 +08:00
}
return style;
});
/**
* 计算垂直方向的边界限制
* @returns 返回最大Y坐标值距离底部的最大距离
*/
function calculateMaxY(): number {
let maxY = screenHeight - props.size;
// 根据导航栏状态调整顶部边界
if (router.isCustomNavbarPage()) {
// 自定义导航栏页面,只需减去状态栏高度
maxY -= statusBarHeight;
} else {
// 默认导航栏页面,减去导航栏高度(44px)和状态栏高度
maxY -= 44 + statusBarHeight;
}
// 标签页需要额外减去标签栏高度和安全区域
if (router.isTabPage()) {
maxY -= page.getTabBarHeight();
}
return maxY;
}
// 计算垂直边界
const maxY = calculateMaxY();
/**
* 触摸开始事件处理
* 初始化拖拽状态,记录起始位置和时间
*/
function onTouchStart(e: TouchEvent) {
// 如果禁用,直接返回
if (props.disabled) return;
// 确保有触摸点存在
if (e.touches.length > 0) {
const touch = e.touches[0];
// 记录拖拽开始的位置
dragState.startX = touch.clientX;
dragState.startY = touch.clientY;
// 标记为拖拽状态,关闭过渡动画
position.isDragging = true;
}
}
/**
* 触摸移动事件处理
* 实时更新按钮位置,实现拖拽效果
*/
function onTouchMove(e: TouchEvent) {
// 如果不在拖拽状态或没有触摸点,直接返回
if (!position.isDragging || e.touches.length == 0) return;
// 阻止默认的滚动行为
e.preventDefault();
const touch = e.touches[0];
// 计算相对于起始位置的偏移量
const deltaX = touch.clientX - dragState.startX;
const deltaY = dragState.startY - touch.clientY; // Y轴方向相反屏幕坐标系向下为正我们的bottom向上为正
// 计算新的位置
let newX = position.x + deltaX;
let newY = position.y + deltaY;
// 水平方向边界限制:确保按钮不超出屏幕左右边界
newX = Math.max(0, Math.min(screenWidth - props.size, newX));
// 垂直方向边界限制
let minY = 0;
// 非标签页时,底部需要考虑安全区域
if (!router.isTabPage()) {
minY += page.getSafeAreaHeight("bottom");
}
// 确保按钮不超出屏幕上下边界
newY = Math.max(minY, Math.min(maxY, newY));
// 更新按钮位置
position.x = newX;
position.y = newY;
// 更新拖拽起始点,为下次移动计算做准备
dragState.startX = touch.clientX;
dragState.startY = touch.clientY;
}
/**
* 执行边缘吸附逻辑
* 拖拽结束后自动将按钮吸附到屏幕边缘
*/
function performEdgeSnapping() {
const edgeThreshold = 60; // 吸附触发阈值(像素)
const edgePadding = props.gap; // 距离边缘的间距
// 判断按钮当前更靠近左边还是右边
const centerX = screenWidth / 2;
const isLeftSide = position.x < centerX;
// 水平方向吸附逻辑
if (position.x < edgeThreshold) {
// 距离左边缘很近,吸附到左边
position.x = edgePadding;
} else if (position.x > screenWidth - props.size - edgeThreshold) {
// 距离右边缘很近,吸附到右边
position.x = screenWidth - props.size - edgePadding;
} else if (isLeftSide) {
// 在左半屏且不在边缘阈值内,吸附到左边
position.x = edgePadding;
} else {
// 在右半屏且不在边缘阈值内,吸附到右边
position.x = screenWidth - props.size - edgePadding;
}
// 垂直方向边界修正
const verticalPadding = props.gap;
if (position.y > maxY - verticalPadding) {
position.y = maxY - verticalPadding;
}
if (position.y < verticalPadding) {
position.y = verticalPadding;
}
}
/**
* 触摸结束事件处理
* 结束拖拽状态并执行边缘吸附
*/
function onTouchEnd() {
// 如果不在拖拽状态,直接返回
if (!position.isDragging) return;
// 结束拖拽状态,恢复过渡动画
position.isDragging = false;
// 执行边缘吸附逻辑
if (!props.noSnapping) {
performEdgeSnapping();
}
}
</script>
<style lang="scss" scoped>
.cl-float-view {
@apply fixed;
}
</style>