一、功能描述:
-
贴边隐藏:
- 球体在靠近容器边界时会自动贴边,并裁剪为半圆(隐藏一半)。
-
鼠标滑入完全显示:
- 当鼠标滑入贴边的球体时,球体完全显示,不再隐藏在边界内。
- 如果球体不在贴边状态,鼠标滑入时球体保持正常显示状态。
-
拖拽交互:
- 支持拖拽球体,拖拽时球体可以自由移动。
- 当球体靠近边界时,会自动贴边并隐藏一半。
-
拖拽体的里面的内容点击事件和拖拽互不干涉影响:
二、关键逻辑:
-
贴边时:
- 如果球体贴边且鼠标未滑入,球体裁剪为半圆并隐藏一半。
- 如果鼠标滑入贴边的球体,球体完全显示,不再隐藏在边界内。
-
其他情况:
- 如果球体不在贴边状态,鼠标滑入时球体完全显示(正常状态)。
-
鼠标滑出时:
- 如果球体贴边,鼠标滑出后恢复裁剪状态(隐藏一半)。
- 如果球体不在贴边状态,鼠标滑出后保持正常状态。
-
对冒泡事件、默认事件进行阻止
- 如不然会影响球体内部的点击事件进行其他交互。
三、效果:
-
贴边时:
- 默认隐藏一半。
- 鼠标滑入时完全显示,不再隐藏。
-
其他情况:
- 鼠标滑入时完全显示(正常状态)。
- 鼠标滑出后恢复默认状态。
效果图1:
效果图2:
四、应用场景
1. 侧边栏或浮动菜单
2. 浮动按钮或操作按钮
3. 聊天窗口或通知面板
4. 网页广告或提示框
五、完整代码
<template>
<div
ref="container"
class="container"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onDragEnd"
>
<div
ref="ball"
class="ball"
:style="ballStyle"
@mousedown="onMouseDown"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<div class="textBox" @click.stop="handleClick"></div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
const container = ref(null);
const ball = ref(null);
const isDragging = ref(false);
const offsetX = ref(0);
const offsetY = ref(0);
const ballX = ref(0);
const ballY = ref(0);
const isAtBoundary = ref(true);
const boundarySide = ref('left');
const isLocked = ref(true);
const isHovered = ref(false);
const mouseDownTime = ref(0);
const isClickEvent = ref(false);
// 常量定义
const CLICK_MAX_DURATION = 200; // 毫秒
const SHRINK_SCALE = 0.7; // 缩小的比例
const SNAP_THRESHOLD = 0; // 吸附阈值
onMounted(() => {
const containerRect = container.value.getBoundingClientRect();
const ballRect = ball.value.getBoundingClientRect();
ballX.value = -ballRect.width / 2;
ballY.value = containerRect.height / 2 - ballRect.height / 2;
});
const ballStyle = computed(() => {
let left = ballX.value;
let top = ballY.value;
let transform = '';
let scale = 1;
if (isAtBoundary.value && !isHovered.value && ball.value) {
const ballWidth = ball.value.offsetWidth;
const ballHeight = ball.value.offsetHeight;
if (boundarySide.value === 'left') {
left = -ballWidth / 2;
scale = SHRINK_SCALE;
} else if (boundarySide.value === 'right') {
left = container.value.offsetWidth - ballWidth / 2;
scale = SHRINK_SCALE;
} else if (boundarySide.value === 'top') {
top = -ballHeight / 2;
scale = SHRINK_SCALE;
} else if (boundarySide.value === 'bottom') {
top = container.value.offsetHeight - ballHeight / 2;
scale = SHRINK_SCALE;
}
} else if (isHovered.value) {
if (isAtBoundary.value) {
if (boundarySide.value === 'left') {
left = 0;
} else if (boundarySide.value === 'right') {
left = container.value.offsetWidth - ball.value.offsetWidth;
} else if (boundarySide.value === 'top') {
top = 0;
} else if (boundarySide.value === 'bottom') {
top = container.value.offsetHeight - ball.value.offsetHeight;
}
}
}
// 添加点击缩放效果
if (isClickEvent.value) {
scale *= 0.95;
}
transform = `scale(${scale})`;
return {
left: `${left}px`,
top: `${top}px`,
transform: transform,
transition: isDragging.value ? 'none' : 'left 0.3s ease, top 0.3s ease, transform 0.3s ease',
cursor: isDragging.value ? 'grabbing' : 'grab'
};
});
const onMouseDown = (event) => {
event.preventDefault();
isClickEvent.value = false;
mouseDownTime.value = Date.now();
isLocked.value = false;
isDragging.value = true;
const ballRect = ball.value.getBoundingClientRect();
const containerRect = container.value.getBoundingClientRect();
offsetX.value = event.clientX - (ballRect.left - containerRect.left);
offsetY.value = event.clientY - (ballRect.top - containerRect.top);
};
const onMouseMove = (event) => {
if (!isDragging.value || isLocked.value) return;
event.preventDefault();
const containerRect = container.value.getBoundingClientRect();
const ballRect = ball.value.getBoundingClientRect();
let newX = event.clientX - containerRect.left - offsetX.value;
let newY = event.clientY - containerRect.top - offsetY.value;
const atLeftBoundary = newX <= SNAP_THRESHOLD;
const atRightBoundary = newX + ballRect.width >= containerRect.width - SNAP_THRESHOLD;
const atTopBoundary = newY <= SNAP_THRESHOLD;
const atBottomBoundary = newY + ballRect.height >= containerRect.height - SNAP_THRESHOLD;
if (atLeftBoundary) {
newX = -ballRect.width / 2;
boundarySide.value = 'left';
isAtBoundary.value = true;
} else if (atRightBoundary) {
newX = containerRect.width - ballRect.width / 2;
boundarySide.value = 'right';
isAtBoundary.value = true;
} else if (atTopBoundary) {
newY = -ballRect.height / 2;
boundarySide.value = 'top';
isAtBoundary.value = true;
} else if (atBottomBoundary) {
newY = containerRect.height - ballRect.height / 2;
boundarySide.value = 'bottom';
isAtBoundary.value = true;
} else {
isAtBoundary.value = false;
}
ballX.value = newX;
ballY.value = newY;
};
const onMouseUp = () => {
if (isDragging.value) {
isDragging.value = false;
if (isAtBoundary.value) {
isLocked.value = true;
}
}
};
const onDragEnd = () => {
if (isDragging.value) {
isDragging.value = false;
if (isAtBoundary.value) {
isLocked.value = true;
}
}
};
const onMouseEnter = () => {
isHovered.value = true;
};
const onMouseLeave = () => {
isHovered.value = false;
};
const handleClick = () => {
const clickDuration = Date.now() - mouseDownTime.value;
console.log("clickDuration", clickDuration);
if (clickDuration <= CLICK_MAX_DURATION) {
alert('textBox点击事件触发');
isClickEvent.value = true;
setTimeout(() => {
isClickEvent.value = false;
}, 150);
}
};
return {
container,
ball,
ballStyle,
onMouseDown,
onMouseMove,
onMouseUp,
onDragEnd,
onMouseEnter,
onMouseLeave,
handleClick
};
},
};
</script>
<style>
.container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
background-color: #f0f0f0;
}
.ball {
position: absolute;
width: 100px;
height: 100px;
background-color: #007bff;
border-radius: 50%;
cursor: grab;
user-select: none;
touch-action: none;
transform-origin: center; /* 确保缩放以中心为基准 */
}
.ball:active {
cursor: grabbing;
}
.textBox {
position: absolute;
width: 100%;
height: 100%;
user-select: none;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
</style>