- 需求背景
- 解决效果
- index.vue
需求背景
需要实现一个上百点批量同时存在的 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)) returnconst entity = pickedObject.id;if (!(entity instanceof Cesium.Entity) || entity.customType !== "sectionEntity") returnentity.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)) returnconst entity = pickedObject.id;if (!(entity instanceof Cesium.Entity) || entity.customType !== "sectionEntity") returnmodel.curId = entity.id
}const offsetPopupBoxOptions = {top: [-0.5, -1],bottom: [-0.5, 0],right: [0, -0.5],left: [-1, -0.5],
}
const showPopupBox = () => {if (!model.showTip) return// 碰撞检测const {left, top, bottom, right} = viewer.container.getBoundingClientRect()model.listData.forEach(async (item, index) => {const curIndex = model.listData.findIndex(x => x.name === item.name)let width, height, areaif (!item.AABB) {const dom = document.querySelector(`.sectionEntityDom${curIndex}`)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 * heightitem.AABB = {width, height, area: width * height}} else {width = item.AABB.widthheight = item.AABB.heightarea = item.AABB.area}const longitude = (item.ctr_points_lonlat[0][0] + item.ctr_points_lonlat[1][0]) / 2const latitude = (item.ctr_points_lonlat[0][1] + item.ctr_points_lonlat[1][1]) / 2const curPosition = Cesium.Cartesian3.fromDegrees(longitude, latitude, item.heightZ);const {x, y} = viewer.scene.cartesianToCanvasCoordinates(curPosition)if (index === 0) {item.offsetPopupBoxType = "top";item.AABB.offsetX = x + offsetPopupBoxOptions["top"] * widthitem.AABB.offsetY = y + offsetPopupBoxOptions["top"] * height}const offsetPopupBoxKeys = Object.keys(offsetPopupBoxOptions)const toChecks = model.listData.slice(0, index) // 需要测试碰撞的单位offsetPopupBoxKeys.some((type) => {item.offsetPopupBoxType = ""item.AABB.offsetX = x + offsetPopupBoxOptions[type][0] * widthitem.AABB.offsetY = y + offsetPopupBoxOptions[type][1] * heightconst check = toChecks.every(checkItem => {const box1 = checkItem.AABBconst box2 = item.AABBlet 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;breakdefault:item.visible = false;break}model.listData[curIndex] = item})
}
</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>