需求背景
需要实现一个上百点批量同时存在的 popup 弹框,为了提高用户体验
1.重叠的弹框,需要隐藏下一层级的 popup
2.为了让用户尽可能看到较全的弹框,需要做弹框的自动避让
解决效果
index.vue
<!--/**
* @author: liuk
* @date: 2024-08-20
* @describe:数值
*/-->
<template>
<div class="numericalValue-wrap">
<teleport to="body">
<ul v-show="showTip && item.visible"
v-for="(item,index) in listData" :key="index"
:class="['surveyStation-popup','sectionEntityDom'+index,'section-popup',item.offsetPopupBoxType,
item?.levelOverflow >= 0.01 ? 'waterlevel-overflow' : ''
]"
:style="{
transform: `translate(${item.AABB?.offsetX || 0}px, ${item.AABB?.offsetY ||0}px)`}">
<li>名称:<span class="label">{{ index }}</span></li>
<li>编号:<span class="label">{{ index }}</span></li>
<li>
水位:
<span class="num">{{ item.waterLevel }}</span>m
<span style="color:red" v-if="item.levelOverflow>= 0.01">{{ item.levelOverflow.toFixed(2) }}↑</span>
</li>
<li>流量:<span class="num">{{ item.flow }}</span> mm</li>
</ul>
</teleport>
</div>
</template>
<script lang="ts" setup>
import {onMounted, onUnmounted, reactive, toRefs} from "vue";
const model = reactive({
showTip: true,
listData: [],
popupPoss: [],
curId: "",
dialogVisible: false
})
const {showTip, showGrid, popupPoss, listData, curId, dialogVisible} = toRefs(model)
onMounted(() => {
getlist()
viewer.dataSources.add(sectionDatasource);
handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(onMouseMove, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction(onMouseClick, Cesium.ScreenSpaceEventType.LEFT_CLICK);
viewer.camera.percentageChanged = 0;
viewer.scene.camera.changed.addEventListener(showPopupBox);
})
onUnmounted(() => {
sectionDatasource.entities.removeAll()
handler.destroy()
viewer.dataSources.remove(sectionDatasource);
viewer.scene.camera.changed.removeEventListener(showPopupBox);
})
const getlist = () => {
const data = [
{
"ctr_points_lonlat": [
[113.04510386306632,25.748247970488464],
[113.04619931039747,25.746722270257674]
],
},
/* ... */
]
setTimeout(() => {
model.listData = data || []
model.popupPoss = new Array(data.length).fill("").map(() => ({}))
addTip(data)
}, 500)
}
// 地图逻辑
import {usemapStore} from "@/store/modules/cesiumMap";
import mittBus from "@/utils/mittBus";
const sectionDatasource = new Cesium.CustomDataSource("section");
const mapStore = usemapStore()
let handler, PreSelEntity
const viewer = mapStore.getCesiumViewer();
const addTip = (data) => {
data.forEach(item => {
sectionDatasource.entities.add({
customType: "sectionEntity",
id: item.label,
data: item,
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(item.ctr_points_lonlat.flat()),
material: Cesium.Color.fromCssColorString("yellow").withAlpha(1),
width: 5,
}
})
})
}
const onMouseMove = (movement) => {
if (PreSelEntity) {
PreSelEntity.polyline.material = Cesium.Color.fromCssColorString("yellow").withAlpha(1)
PreSelEntity = null
}
const pickedObject = viewer.scene.pick(movement.endPosition);
if (!Cesium.defined(pickedObject) || !Cesium.defined(pickedObject.id)) return
const entity = pickedObject.id;
if (!(entity instanceof Cesium.Entity) || entity.customType !== "sectionEntity") return
entity.polyline.material = Cesium.Color.fromCssColorString("red").withAlpha(1)
if (entity !== PreSelEntity) PreSelEntity = entity;
}
const onMouseClick = (movement) => {
const pickedObject = viewer.scene.pick(movement.position);
if (!Cesium.defined(pickedObject) || !Cesium.defined(pickedObject.id)) return
const entity = pickedObject.id;
if (!(entity instanceof Cesium.Entity) || entity.customType !== "sectionEntity") return
model.curId = entity.id
}
const offsetPopupBoxOptions = {
top: [-0.5, -1],
bottom: [-0.5, 0],
right: [0, -0.5],
left: [-1, -0.5],
}
const showPopupBox = () => {
if (!showTip.value) return
// 碰撞检测
const {left, top, bottom, right} = viewer.container.getBoundingClientRect()
const CanvasWidth = parseInt(getComputedStyle(viewer.container).width)
model.listData.forEach(async (item, index) => {
let width, height, area
if (!item.AABB) {
const dom = document.querySelector(`.sectionEntityDom${props.isDualMap ? 'DualMap' : ''}${index}`)
width = parseInt(getComputedStyle(dom).width) + 2 * parseInt(getComputedStyle(dom).padding.split(" ")[1])
height = parseInt(getComputedStyle(dom).height) + 2 * parseInt(getComputedStyle(dom).padding.split(" ")[0])
area = width * height
item.AABB = {width, height, area: width * height}
} else {
width = item.AABB.width
height = item.AABB.height
area = item.AABB.area
}
const curPosition = Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, 0);
let x, y;
try {
const obj = viewer.scene.cartesianToCanvasCoordinates(curPosition)
x = obj.x
y = obj.y
} catch (e) {
item.visible = false
}
if (!x) return
if (index === 0) {
item.offsetPopupBoxType = "top";
item.AABB.offsetX = x + offsetPopupBoxOptions["top"][0] * width + (props.isDualMap ? CanvasWidth : 0)
item.AABB.offsetY = y + offsetPopupBoxOptions["top"][1] * height
item.visible = true
return
}
const offsetPopupBoxKeys = Object.keys(offsetPopupBoxOptions)
const toChecks = model.listData.slice(0, index) // 需要测试碰撞的单位
offsetPopupBoxKeys.some((type) => {
item.offsetPopupBoxType = ""
item.AABB.offsetX = x + offsetPopupBoxOptions[type][0] * width + (props.isDualMap ? CanvasWidth : 0)
item.AABB.offsetY = y + offsetPopupBoxOptions[type][1] * height
const check = toChecks.every(checkItem => {
const box1 = checkItem.AABB
const box2 = item.AABB
let intersectionArea = 0 // 相交面积
// 计算在每个轴上的重叠部分
const overlapX = Math.min(box1.offsetX + box1.width, box2.offsetX + box2.width) - Math.max(box1.offsetX, box2.offsetX);
const overlapY = Math.min(box1.offsetY + box1.height, box2.offsetY + box2.height) - Math.max(box1.offsetY, box2.offsetY);
// 如果在两个轴上都有重叠,则计算相交区域的面积
if (overlapX > 0 && overlapY > 0) intersectionArea = overlapX * overlapY;
return intersectionArea <= area * 0.05;
});
if (check) {
item.offsetPopupBoxType = type
}
return check
})
switch (true) { // 屏幕边界限制
case item.AABB.offsetX + width <= right && item.AABB.offsetX >= left && item.AABB.offsetY >= top && item.AABB.offsetY + height <= bottom:
item.visible = !!item.offsetPopupBoxType;
break
default:
item.visible = false;
break
}
})
}
</script>
<style lang="scss">
.surveyStation-popup {
position: fixed;
top: 0;
left: 0;
z-index: 3;
margin: 0;
padding: 7px 15px;
list-style: none;
background: rgba(5, 9, 9, 0.6);
border-radius: 4px;
font-size: 14px;
color: #fff;
cursor: default;
--w: 24px;
--h: 10px;
&::before {
content: "";
background-color: rgba(0, 0, 0, 0.7);
position: absolute;
bottom: 0;
left: 50%;
width: var(--w);
height: var(--h);
transform: translate(-50%, 100%) translateY(-0.5px);
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
&.ponint-list::before{
display: none;
}
&.map2d {
margin-left: -15px; // 二维图片底座尺寸大小
margin-top: -50px;
}
&.map3d {
margin-left: -15px; // 三维图片底座尺寸大小
margin-top: -100px;
}
.ponint-list-li {
cursor: pointer;
&:hover {
background: rgba(204, 204, 204, .6);
}
}
}
.section-popup {
--w: 24px;
--h: 10px;
width: 150px;
height: 80px;
margin-top: -10px;
&::before {
content: "";
background-color: rgba(0, 0, 0, 0.7);
position: absolute;
bottom: 0;
left: 50%;
width: 24px;
height: 10px;
transform: translate(-50%, 100%) translateY(-0.5px);
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
&.top {
margin-top: calc(var(--h) * -1);
&::before {
top: auto;
bottom: 0;
right: auto;
left: 50%;
width: var(--w);
height: var(--h);
transform: translate(-50%, 100%) translateY(-0.5px);
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
}
&.bottom {
margin-top: var(--h);
&::before {
top: 0;
bottom: auto;
right: auto;
left: 50%;
width: var(--w);
height: var(--h);
transform: translate(-50%, -100%) translateY(0.5px);
clip-path: polygon(50% 0, 0 100%, 100% 100%);
}
}
&.right {
margin-left: var(--h);
&::before {
top: 50%;
bottom: auto;
right: auto;
left: 0;
width: var(--h);
height: var(--w);
transform: translate(-100%, -50%) translateX(0.5px);
clip-path: polygon(100% 0, 0 50%, 100% 100%);
}
}
&.left {
margin-left: calc(var(--h) * -1);
&::before {
top: 50%;
bottom: auto;
right: 0;
left: auto;
width: var(--h);
height: var(--w);
transform: translate(100%, -50%) translateX(-0.5px);
clip-path: polygon(0 100%, 0 0, 100% 50%);
}
}
&.waterlevel-overflow {
animation: dm-yj-breathe 800ms ease-in-out infinite;
animation-direction: alternate;
}
.label {
color: #00ff00;
}
.num {
color: orange;
}
}
</style>