本文实现:
1.弹窗跟随鼠标移动。
2.进行边界展示处理:右侧空间不足时,弹窗显示在鼠标左侧 上方空间不足时,弹窗显示在鼠标下方。
3.窗口 Resize 时重新计算弹窗位置
4.进行鼠标穿透,避免造成弹窗频繁闪烁问题。
效果:
代码:
<template>
<div
class="progress-box"
ref="propgress"
@mouseenter="handleMouseEnter"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<div class="progress-outer">
<div
class="progress-inner"
:style="{
width: `${progressInfo.percent}%`,
backgroundColor: color,
height: height
}"
></div>
</div>
<div>
<div class="percent" :style="{ color }">{{ progressInfo.percent }}%</div>
</div>
<!-- 鼠标移入时显示弹窗 -->
<div class="popUp" v-if="showPopUp" :style="{ left: `${popUpX}px`, top: `${popUpY}px`, borderColor: color }">
<div class="popUp-title">{{ progressInfo.name }}</div>
<div class="popUp-value">碳排量:{{ progressInfo.value }}</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
progressInfo: {
type: Object,
default: () => ({
name: '',
value: '',
number: '',
percent: 0
})
},
height: {
type: [Number, String],
default: '100%'
},
color: {
type: String,
default: '#3BB0A6'
}
})
const propgress = ref(null)
const showPopUp = ref(false)
const popUpX = ref(0)
const popUpY = ref(0)
const containerPadding = 10 // 弹窗与容器边缘的间距
// 鼠标进入事件
const handleMouseEnter = (e) => {
showPopUp.value = true
handleMouseMove(e) // 初始化位置
}
// 鼠标移动事件,更新弹窗位置
const handleMouseMove = (e) => {
if (!showPopUp.value || !propgress.value) return
// 获取容器位置和尺寸
const container = propgress.value.getBoundingClientRect()
// 获取弹窗尺寸
const popUp = document.querySelector('.popUp')
if (!popUp) return
const popUpWidth = popUp.offsetWidth
const popUpHeight = popUp.offsetHeight
// 计算鼠标在容器内的相对位置
let mouseX = e.clientX - container.left
let mouseY = e.clientY - container.top
// 默认显示在鼠标右下方
let calculatedX = mouseX + 10
let calculatedY = mouseY + 10
// 检测是否会超出容器边界并调整
if (calculatedX + popUpWidth > container.width - containerPadding) {
// 右侧空间不足,显示在左侧
calculatedX = Math.max(containerPadding, mouseX - popUpWidth - 10)
}
if (calculatedY + popUpHeight > container.height - containerPadding) {
// 下方空间不足,显示在上方
calculatedY = Math.max(containerPadding, mouseY - popUpHeight - 10)
}
// 更新弹窗位置
popUpX.value = calculatedX
popUpY.value = calculatedY
}
// 鼠标离开事件
const handleMouseLeave = () => {
showPopUp.value = false
}
// 窗口大小变化时调整弹窗位置
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
const handleResize = () => {
if (showPopUp.value && propgress.value) {
// 模拟鼠标位置更新弹窗
const container = propgress.value.getBoundingClientRect()
handleMouseMove({
clientX: container.left + popUpX.value,
clientY: container.top + popUpY.value
})
}
}
</script>
<style scoped lang="less">
.progress-box {
display: flex;
align-items: center;
color: #fff;
width: 100%;
height: 50%;
position: relative;
cursor: pointer;
padding-right: 10px;
}
.name {
width: 100%;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
}
.progress-outer {
width: 100%;
height: 15px;
background-image: repeating-linear-gradient(to right, #1a237e, #1e2e50 5px, transparent 4px, transparent 10px);
}
.progress-inner {
height: 100%;
background: repeating-linear-gradient(to right, #1e2e50, #1e2e50 5px, transparent 5px, transparent 10px);
transition: width 0.3s ease;
}
.percent {
margin-left: 10px;
transition: color 0.3s ease;
}
.popUp {
position: absolute;
background-color: rgba(255, 255, 255);
padding: 12px 15px;
border: 1px solid;
border-radius: 6px;
color: rgb(53, 53, 53);
font-size: 14px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
pointer-events: none; //鼠标穿透
z-index: 100;
white-space: nowrap;
transition: opacity 0.2s ease; /* 添加淡入淡出效果 */
}
.popUp-title {
margin-bottom: 4px;
font-weight: bold;
}
</style>