在三维地理信息系统中,点击地图元素展示详细信息是最基础也最实用的交互功能。最近接手了一个智慧城市项目,需要在Vue框架下使用Cesium实现点击广告牌(Billboard)弹出定制化信息窗口的效果。刚开始以为就是个简单的点击事件绑定,实际开发时才发现要处理坐标转换、弹窗跟随、性能优化等一系列问题。
Cesium的Billboard相当于三维场景中的"贴图标签",常用来标记POI点。但官方文档对点击交互的实现说明比较简略,特别是弹窗跟随场景旋转这个需求,需要用到scene.postRender这个不太起眼的API。下面我就把踩坑后总结的完整方案分享给大家,包含从事件绑定到样式优化的全流程。
推荐使用vue-cli创建项目,通过npm安装cesium和vue-cesium插件:
bash复制npm install cesium @vue-cesium@next --save
在main.js中全局引入:
javascript复制import { createApp } from 'vue'
import App from './App.vue'
import VueCesium from '@vue-cesium'
const app = createApp(App)
app.use(VueCesium, {
cesiumPath: 'https://unpkg.com/cesium/Build/Cesium/Cesium.js'
})
app.mount('#app')
在组件中初始化三维场景:
html复制<template>
<div class="viewer-container">
<vc-viewer ref="viewerRef" :shouldAnimate="true">
<vc-entity
v-for="item in billboards"
:key="item.id"
:position="item.position"
:billboard="item.billboard"
/>
</vc-viewer>
<div id="custom-popup" v-show="showPopup">
<!-- 弹窗内容模板 -->
</div>
</div>
</template>
Cesium提供了ScreenSpaceEventHandler来处理屏幕空间事件:
javascript复制setup() {
const viewerRef = ref(null)
const showPopup = ref(false)
onMounted(() => {
const viewer = viewerRef.value.cesiumObject
const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas)
handler.setInputAction((movement) => {
const picked = viewer.scene.pick(movement.position)
if (picked && picked.id && picked.id.billboard) {
showPopup.value = true
// 存储当前点击的广告牌位置
currentPosition.value = picked.id.position._value
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
})
}
高频事件监听容易导致性能问题,建议:
javascript复制// 防抖实现示例
const debouncePick = _.debounce((movement) => {
// 拾取逻辑
}, 300)
handler.setInputAction(debouncePick, Cesium.ScreenSpaceEventType.LEFT_CLICK)
关键是要将三维世界坐标转换为屏幕坐标:
javascript复制viewer.scene.postRender.addEventListener(() => {
if (showPopup.value && currentPosition.value) {
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(
currentPosition.value
)
if (canvasPosition) {
const popup = document.getElementById('custom-popup')
popup.style.left = `${canvasPosition.x - popup.offsetWidth/2}px`
popup.style.top = `${canvasPosition.y - popup.offsetHeight - 20}px`
}
}
})
实测发现弹窗会随相机移动轻微抖动,解决方法:
css复制#custom-popup {
position: absolute;
transition: left 0.2s ease-out, top 0.2s ease-out;
will-change: transform;
}
推荐使用CSS实现带箭头的气泡效果:
css复制.popup-bubble {
position: absolute;
width: 280px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
padding: 15px;
}
.popup-bubble:after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
margin-left: -10px;
border-width: 10px 10px 0;
border-style: solid;
border-color: white transparent;
}
javascript复制// 点击外部关闭实现
document.addEventListener('click', (e) => {
const popup = document.getElementById('custom-popup')
if (showPopup.value && !popup.contains(e.target)) {
showPopup.value = false
}
})
结合Vue的动态组件实现灵活内容展示:
html复制<component
:is="currentComponent"
:entityData="currentEntityData"
/>
使用Map存储广告牌与数据的关联关系:
javascript复制const entityMap = new Map()
// 添加广告牌时
entityMap.set(entity.id, {
position: entity.position,
data: entity.properties
})
记得在开发过程中多使用Cesium的Debug模式,可以直观查看坐标位置:
javascript复制viewer.scene.debugShowFramesPerSecond = true
以下是核心功能的组合实现:
javascript复制// 弹窗组件
const PopupComponent = {
template: `...`,
props: ['position'],
setup(props) {
// 弹窗业务逻辑
}
}
// 主组件
export default {
components: { PopupComponent },
setup() {
// 所有状态和方法的整合
return {
// 需要暴露的响应式数据和方法
}
}
}
实际项目中我还封装了一个CesiumPopupManager类来集中管理弹窗状态,这样在多组件场景下也能保持一致的交互体验。特别是在需要同时展示多个信息弹窗时,采用发布-订阅模式来管理弹窗层级关系效果很好。