238 lines
6.5 KiB
Vue
238 lines
6.5 KiB
Vue
|
<template>
|
||
|
<div ref="floatPanel" :style="panelStyle" class="float-panel"
|
||
|
@mousedown="handleMouseDown"
|
||
|
@touchstart="handleTouchStart">
|
||
|
<slot></slot>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script setup>
|
||
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||
|
|
||
|
const props = defineProps({
|
||
|
position: {
|
||
|
type: Object,
|
||
|
default: () => ({})
|
||
|
},
|
||
|
fixedX: Boolean,
|
||
|
fixedY: Boolean,
|
||
|
snapEdge: {
|
||
|
type: Boolean,
|
||
|
default: false
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const floatPanel = ref(null);
|
||
|
const panelPos = ref({ x: 0, y: 0 });
|
||
|
const panelSize = ref({ width: 0, height: 0 });
|
||
|
const windowSize = ref({ width: 0, height: 0 });
|
||
|
const dragOffset = ref({ x: 0, y: 0 });
|
||
|
const wasDragged = ref(false);
|
||
|
|
||
|
// 更新面板和窗口尺寸信息
|
||
|
const updateSizes = () => {
|
||
|
if (floatPanel.value) {
|
||
|
panelSize.value = {
|
||
|
width: floatPanel.value.offsetWidth,
|
||
|
height: floatPanel.value.offsetHeight
|
||
|
};
|
||
|
}
|
||
|
windowSize.value = {
|
||
|
width: window.innerWidth,
|
||
|
height: window.innerHeight
|
||
|
};
|
||
|
};
|
||
|
|
||
|
// 初始位置计算
|
||
|
const initPosition = () => {
|
||
|
updateSizes();
|
||
|
const { left, right, top, bottom } = props.position;
|
||
|
|
||
|
if (right !== undefined) {
|
||
|
panelPos.value.x = windowSize.value.width - parseInt(right || 0) - panelSize.value.width;
|
||
|
} else if (left !== undefined) {
|
||
|
panelPos.value.x = parseInt(left || 0);
|
||
|
}
|
||
|
|
||
|
if (bottom !== undefined) {
|
||
|
panelPos.value.y = windowSize.value.height - parseInt(bottom || 0) - panelSize.value.height;
|
||
|
} else if (top !== undefined) {
|
||
|
panelPos.value.y = parseInt(top || 0);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// 面板样式
|
||
|
const panelStyle = computed(() => {
|
||
|
return {
|
||
|
left: `${panelPos.value.x}px`,
|
||
|
top: `${panelPos.value.y}px`
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// 处理鼠标按下事件
|
||
|
const handleMouseDown = (event) => {
|
||
|
// 只有点击浮动面板本身时才启动拖拽
|
||
|
if (event.target === floatPanel.value || floatPanel.value.contains(event.target)) {
|
||
|
// 判断是否是面板自身,如果是才阻止默认行为
|
||
|
if (event.target === floatPanel.value) {
|
||
|
event.preventDefault();
|
||
|
}
|
||
|
|
||
|
wasDragged.value = false;
|
||
|
|
||
|
const startX = event.clientX;
|
||
|
const startY = event.clientY;
|
||
|
|
||
|
// 计算点击位置与面板左上角的偏移
|
||
|
dragOffset.value = {
|
||
|
x: startX - panelPos.value.x,
|
||
|
y: startY - panelPos.value.y
|
||
|
};
|
||
|
|
||
|
const handleMouseMove = (e) => {
|
||
|
wasDragged.value = true;
|
||
|
|
||
|
const newX = e.clientX - dragOffset.value.x;
|
||
|
const newY = e.clientY - dragOffset.value.y;
|
||
|
|
||
|
// 应用约束
|
||
|
if (!props.fixedX) {
|
||
|
panelPos.value.x = Math.max(0, Math.min(newX, windowSize.value.width - panelSize.value.width));
|
||
|
}
|
||
|
|
||
|
if (!props.fixedY) {
|
||
|
panelPos.value.y = Math.max(0, Math.min(newY, windowSize.value.height - panelSize.value.height));
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const handleMouseUp = (e) => {
|
||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||
|
|
||
|
if (wasDragged.value && props.snapEdge) {
|
||
|
snapToEdge();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
document.addEventListener('mousemove', handleMouseMove);
|
||
|
document.addEventListener('mouseup', handleMouseUp);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// 处理触摸开始事件
|
||
|
const handleTouchStart = (event) => {
|
||
|
// 只有触摸浮动面板本身时才启动拖拽
|
||
|
if (event.target === floatPanel.value || floatPanel.value.contains(event.target)) {
|
||
|
// 判断是否是面板自身,如果是才阻止默认行为
|
||
|
if (event.target === floatPanel.value) {
|
||
|
event.preventDefault();
|
||
|
}
|
||
|
|
||
|
wasDragged.value = false;
|
||
|
|
||
|
const touch = event.touches[0];
|
||
|
const startX = touch.clientX;
|
||
|
const startY = touch.clientY;
|
||
|
|
||
|
// 计算点击位置与面板左上角的偏移
|
||
|
dragOffset.value = {
|
||
|
x: startX - panelPos.value.x,
|
||
|
y: startY - panelPos.value.y
|
||
|
};
|
||
|
|
||
|
const handleTouchMove = (e) => {
|
||
|
e.preventDefault();
|
||
|
wasDragged.value = true;
|
||
|
|
||
|
const touch = e.touches[0];
|
||
|
const newX = touch.clientX - dragOffset.value.x;
|
||
|
const newY = touch.clientY - dragOffset.value.y;
|
||
|
|
||
|
// 应用约束
|
||
|
if (!props.fixedX) {
|
||
|
panelPos.value.x = Math.max(0, Math.min(newX, windowSize.value.width - panelSize.value.width));
|
||
|
}
|
||
|
|
||
|
if (!props.fixedY) {
|
||
|
panelPos.value.y = Math.max(0, Math.min(newY, windowSize.value.height - panelSize.value.height));
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const handleTouchEnd = (e) => {
|
||
|
document.removeEventListener('touchmove', handleTouchMove);
|
||
|
document.removeEventListener('touchend', handleTouchEnd);
|
||
|
|
||
|
if (wasDragged.value && props.snapEdge) {
|
||
|
snapToEdge();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||
|
document.addEventListener('touchend', handleTouchEnd);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// 执行吸附
|
||
|
const snapToEdge = () => {
|
||
|
const centerX = windowSize.value.width / 2;
|
||
|
const panelCenterX = panelPos.value.x + panelSize.value.width / 2;
|
||
|
|
||
|
if (panelCenterX < centerX) {
|
||
|
panelPos.value.x = 0; // 吸附到左边
|
||
|
console.log('吸附到左边边缘');
|
||
|
} else {
|
||
|
panelPos.value.x = windowSize.value.width - panelSize.value.width; // 吸附到右边
|
||
|
console.log('吸附到右边边缘');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// 窗口大小变化处理
|
||
|
const handleResize = () => {
|
||
|
const oldWidth = windowSize.value.width;
|
||
|
const oldHeight = windowSize.value.height;
|
||
|
|
||
|
updateSizes();
|
||
|
|
||
|
// 保持相对右边和底部的距离
|
||
|
if (props.position.right !== undefined) {
|
||
|
const rightDistance = oldWidth - (panelPos.value.x + panelSize.value.width);
|
||
|
panelPos.value.x = windowSize.value.width - rightDistance - panelSize.value.width;
|
||
|
}
|
||
|
|
||
|
if (props.position.bottom !== undefined) {
|
||
|
const bottomDistance = oldHeight - (panelPos.value.y + panelSize.value.height);
|
||
|
panelPos.value.y = windowSize.value.height - bottomDistance - panelSize.value.height;
|
||
|
}
|
||
|
|
||
|
// 确保不超出边界
|
||
|
panelPos.value.x = Math.max(0, Math.min(panelPos.value.x, windowSize.value.width - panelSize.value.width));
|
||
|
panelPos.value.y = Math.max(0, Math.min(panelPos.value.y, windowSize.value.height - panelSize.value.height));
|
||
|
};
|
||
|
|
||
|
onMounted(() => {
|
||
|
console.log('组件挂载, snapEdge =', props.snapEdge);
|
||
|
updateSizes();
|
||
|
initPosition();
|
||
|
window.addEventListener('resize', handleResize);
|
||
|
});
|
||
|
|
||
|
onUnmounted(() => {
|
||
|
window.removeEventListener('resize', handleResize);
|
||
|
});
|
||
|
|
||
|
// 暴露方法
|
||
|
defineExpose({
|
||
|
snapToEdge
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
<style scoped>
|
||
|
.float-panel {
|
||
|
position: fixed;
|
||
|
z-index: 1000;
|
||
|
cursor: move;
|
||
|
user-select: none;
|
||
|
touch-action: none;
|
||
|
transition: left 0.2s ease-out;
|
||
|
}
|
||
|
</style>
|