VUE3悬浮移动窗示例

示例1

<template>
	<div class="floating-window" :class="floatWindowClass" @click="openWindows" :style="windowStyle" @touchstart="startDrag" ref="windowRef">
		<uni-icons type="compose" size="30"></uni-icons>
	</div>
	
	<view class="mask-window" :class="maskWindowClass" @click="closeWindows">
	</view>
	
</template>

<script setup>
	import { ref, reactive, computed, onMounted } from 'vue';
	import { onHide,onShow } from "@dcloudio/uni-app";

    const isShowFloatWindow = ref(0);
	const isShowMaskWindow = ref(false);
	
	const floatWindowClass = computed(()=> {
		if(isShowFloatWindow.value === 1){
			return 'animationFloatWindows1';
		}else if(isShowFloatWindow.value === 2){
			return 'animationFloatWindows2';
		}else{
			return false;
		}
	})
	const maskWindowClass = computed(() => {
		return isShowMaskWindow.value ? 'visible' : false;
	})
	
	const openWindows = () => {
		isShowFloatWindow.value = 1;
		isShowMaskWindow.value = true;
	}
	
	const closeWindows = (event) => {
		event.stopPropagation();
		isShowFloatWindow.value = 2;
		isShowMaskWindow.value = false;
	}
	
	onHide(() => {
		isShowFloatWindow.value = 0;
	})


	const windowRef = ref(null);
	const isDragging = ref(false);
	const position = reactive({ x: 10, y: 10 });
	const offset = reactive({ x:null, y:null});
	const container = reactive({ width:null, height:null});
	const floating = reactive({ width:40, height:40});
	const horizontal = ref('right');
	const vertical = ref('bottom');

	const windowStyle = computed(() => ({
		top: `${position.y}px`,
		left: `${position.x}px`,
		width: `${floating.width}px`,
		height: `${floating.height}px`,
		pointerEvents: isDragging.value ? 'none' : 'auto'
	}));
	

	onMounted(() => {
		container.width = window.innerWidth;
		container.height = window.innerHeight;
		
		if(horizontal.value === 'right'){
			position.x = container.width - position.x - floating.width;
		}
		if(vertical.value === 'bottom'){
			position.y = container.height - position.y - floating.height;
		}
	});

	const startDrag = (e) => {
		if (e.touches.length > 0) {
			
			isDragging.value = true;
			const rect = windowRef.value.getBoundingClientRect();
			
			//计算偏差
			offset.x = e.touches[0].clientX - rect.left;
			offset.y = e.touches[0].clientY - rect.top;
			
			//添加事件
			document.addEventListener('touchmove', onDrag);
			document.addEventListener('touchend', stopDrag);
		}
	};

	const onDrag = (e) => {
		if (isDragging.value) {
			// 阻止默认行为,例如滚动
			e.preventDefault(); 

			let newX = e.touches[0].clientX - offset.x;
			let newY = e.touches[0].clientY - offset.y;

			// 检查边界
			if (newX < 0) newX = 0;
			if (newY < 0) newY = 0;
			if (newX + floating.width > container.width) newX = container.width - floating.width;
			if (newY + floating.height > container.height) newY = container.height - floating.height;

			position.x = newX;
			position.y = newY;
		}
	};

	const stopDrag = () => {
		isDragging.value = false;
		document.removeEventListener('touchmove', onDrag);
		document.removeEventListener('touchend', stopDrag);
	};

</script>

<style lang="scss" scoped>

	.floating-window {
		touch-action: none; // 阻止触控操作
	}

	.floating-window {
		position: fixed;
		z-index: 999;
		color: black;
		display: block;
		background-color: white;
	}
	
	.mask-window{
		position: fixed;
		top: 0;
		left: 0;
		width: 100vw;
		height: 100vh;
		background-color: rgba(0, 0, 0, 0.2);
		z-index: 998;
		visibility: hidden;
		opacity: 0;
		transition: visibility 0.5s, opacity 0.5s linear;
	}
	
	.visible{
		visibility: visible;
		opacity: 1;
	}

	.animationFloatWindows1{
		animation: floatwindows1 0.5s forwards;
	}
	.animationFloatWindows2{
		animation: floatwindows2 0.5s forwards;
	}
	
	@keyframes floatwindows1 {
		from{

		}
		to{
			width: 100vw;
			height: 70vh;
			top: 0;
			left: 0;

		}
	}
	
	@keyframes floatwindows2 {
		from{
			width: 100vw;
			height: 70vh;
			top: 0;
			left: 0;
		}
		to{
			
		
		}
	}


</style>

示例2

<template>
	<view class="body">
		<view class="test1" :class="test1Class" @click="toggleWindows">
			<view class="head">
				计数器
			</view>
		</view>
		<view class="test2" :class="test2Class" @click="closeWindows">
			
		</view>
	</view>
</template>

<script setup>
	import {computed,ref} from "vue";
	import {onHide,onShow} from "@dcloudio/uni-app";

	const isShow1 = ref(0);
	const isShow2 = ref(0);
	
	const test1Class = computed(()=> {
		if(isShow1.value === 1){
			return 'amimationClass1';
		}else if(isShow1.value === 2){
			return 'amimationClass2';
		}else{
			return false;
		}
	})
	const test2Class = computed(()=> {
		if(isShow2.value === 1){
			return 'amimationClassTwo';
		}else if(isShow2.value === 2){
			return 'amimationClassOne';
		}else{
			return false;
		}
	})
	
	const toggleWindows = () => {
		isShow1.value = 1;
		isShow2.value = 1;
	}
	
	const closeWindows = () => {
		isShow1.value = 2;
		isShow2.value = 2;
	}
	
	onHide(() => {
		isShow1.value = 0;
		isShow2.value = 0;
	})

</script>

<style lang="scss" scoped>
	
.body{
	.test1{
		position: fixed;
		bottom: calc(20rpx + var(--window-bottom));
		right: 20rpx;
		background-color: white;
		border: 1px solid #ccc;
		width: 40px;
		height: 40px;
		z-index: 999;
	}
	.test2{
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		z-index: 998;
	}
	.amimationClass1{
		animation: mymove1 0.5s forwards;
	}
	.amimationClass2{
		animation: mymove2 0.5s forwards;
	}
	.amimationClassOne{
		animation: mymoveone 1s forwards;
	}
	.amimationClassTwo{
		animation: mymovetwo 1s forwards;
	}
}
@keyframes mymove1 {
	from {
		width: 40px;
		height: 40px;
	}
	to{
		width: calc(100% - 40rpx);
		height: 70vh;
	}
}
@keyframes mymove2 {
	from {
		width: calc(100% - 40rpx);
		height: 70vh;
		
	}
	to{
		width: 40px;
		height: 40px;
	}
}

@keyframes mymoveone {
	from {
		background-color: rgba(0, 0, 0, 0.2);
		display: block;
	}
	to{
		background-color: rgba(0, 0, 0, 0);
		display: none;
	}
}
@keyframes mymovetwo {
	from {
		background-color: rgba(0, 0, 0, 0);
		display: none;
		
	}
	to{
		background-color: rgba(0, 0, 0, 0.2);
		display: block;
	}
}

</style>

示例3

<template>
	<div class="move-box" :style="boxStyle" @mousedown="startDrag" @touchstart="startDrag" ref="boxRef">
		<slot></slot>
	</div>
</template>

<script setup>
	import { ref, reactive, computed, onMounted } from 'vue';
	
	const props = defineProps({
		width:{
			type: Number,
			default: 40
		},
		height: {
			type: Number,
			default: 40
		},
		x: {
			type: Number,
			default: 10
		},
		y: {
			type: Number,
			default: 10
		},
		horizontal: {
			validator(value) {
				return ['left', 'right'].includes(value);
			}
		},
		vertical: {
			validator(value) {
				return ['top', 'bottom'].includes(value);
			}
		}
	})

	const boxRef = ref(null);
	const isDragging = ref(false);
	const offset = reactive({ x:null, y:null});
	const container = reactive({ width:null, height:null});
	
	const position = reactive({ x: null, y: null });
	position.x = props.x;
	position.y = props.y;
	
	const floating = reactive({ width:null, height:null });
	floating.width = props.width;
	floating.height = props.height;
	
	const horizontal = ref(null);
	horizontal.value = props.horizontal;
	
	const vertical = ref(null);
	vertical.value = props.vertical; 
	
	const paddings = reactive({
		top: 60,    // 顶部内边距
		right: 10,  // 右侧内边距
		bottom: 10, // 底部内边距
		left: 10    // 左侧内边距
	});

	const boxStyle = computed(() => ({
		top: `${position.y}px`,
		left: `${position.x}px`,
		width: `${floating.width}px`,
		height: `${floating.height}px`,
		pointerEvents: isDragging.value ? 'none' : 'auto'
	}));
	
	const getScreenSize = () => new Promise((resolve) => {
	    uni.getSystemInfo({
	        success: (res) => {
	            resolve({ width: res.windowWidth, height: res.windowHeight });
	        }
	    });
	});
	

	onMounted(async () => {
		const screenSize = await getScreenSize();
		container.width = screenSize.width - paddings.left - paddings.right;
		container.height = screenSize.height - paddings.top - paddings.bottom;
		
		if(horizontal.value === 'right'){
			position.x = container.width - position.x - floating.width + paddings.right;
		}
		if(vertical.value === 'bottom'){
			position.y = container.height - position.y - floating.height + paddings.bottom;
		}
		// 确保 DOM 更新完成后再访问 boxRef
		await uni.createSelectorQuery().in(this).select('.move-box').boundingClientRect(rect => {
		    if (rect) {
		      console.log('Bounding Client Rect:', rect);
		    } else {
		      console.error('Failed to get bounding client rect');
		    }
		}).exec();
		  
	});

	const startDrag = (e) => {
		if ((e.touches && e.touches.length > 0) || (e.buttons && e.buttons === 1)) {
			
			isDragging.value = true;
			
			// 确保 boxRef 已经被正确赋值
			if (boxRef.value) {
			    uni.createSelectorQuery().in(this).select('.move-box').boundingClientRect(rect => {
			        if (rect) {
						// 计算偏差
						offset.x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
						offset.y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
			
						// 添加事件
						document.addEventListener('touchmove', onDrag);
						document.addEventListener('touchend', stopDrag);
						document.addEventListener('mousemove', onDrag);
						document.addEventListener('mouseup', stopDrag);
			        } else {
			          console.error('Failed to get bounding client rect in startDrag');
			        }
			    }).exec();
			} else {
			    console.error('boxRef is not set in startDrag');
			}
		}
	};

	const onDrag = (e) => {
		if (isDragging.value) {
			// 阻止默认行为,例如滚动
			e.preventDefault(); 

			let newX = (e.touches ? e.touches[0].clientX : e.clientX) - offset.x;
			let newY = (e.touches ? e.touches[0].clientY : e.clientY) - offset.y;

			// 检查边界
			if (newX < 0) newX = paddings.left;
			if (newY < 0) newY = paddings.top;
			if (newX + floating.width > container.width + paddings.left) newX = container.width + paddings.left - floating.width;
			if (newY + floating.height > container.height + paddings.top) newY = container.height + paddings.top - floating.height;

			position.x = newX;
			position.y = newY;
		}
	};

	const stopDrag = () => {
		isDragging.value = false;
		document.removeEventListener('touchmove', onDrag);
		document.removeEventListener('touchend', stopDrag);
		document.removeEventListener('mousemove', onDrag);
		document.removeEventListener('mouseup', stopDrag);
	};

</script>

<style lang="scss" scoped>
	.move-box {
		touch-action: none; // 阻止触控操作
		position: fixed;
		z-index: 1000;
		display: flex;
		justify-content: center;
		align-items: center;
		background-color: $caipiao-font-color-red;
		border-radius: 50%;
		
	}
</style>