<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>