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>