'# CesiumJS第3篇,前端使用Vue和Element实现可拖动弹框效果,并使用mixins混入特性实现,不影响底层元素操作(Cesium上可拖拽的实时视频播放弹框,底层元素可以正常操作)
一、背景与问题
在CesiumJS中实现动态UI交互时,常常需要在三维场景上叠加可操作的弹框组件。例如:在Cesium地图上展示实时视频播放窗口,要求弹框支持拖动定位,同时保持Cesium底层地图的正常交互(如点击模型、调整视角等)。
传统实现方式存在两个关键问题:
- 事件冲突:Element UI的弹框(如el-dialog)默认会覆盖在Cesium容器上,导致事件冒泡被阻断
- 层级管理:需要确保弹框始终在Cesium容器之上,但又不干扰Cesium的底层元素操作
本文将通过Vue的mixins特性,结合Element UI的弹框组件,实现一个可拖动的、不影响Cesium底层交互的弹框系统。
二、基本原理
1. 事件冒泡控制
Cesium的事件处理依赖于浏览器的事件冒泡机制。当弹框覆盖在Cesium容器上时,需要通过event.stopPropagation()阻止事件冒泡,但又不能完全阻断Cesium的事件监听。
2. 层级管理
通过CSS的z-index属性确保弹框始终在Cesium容器之上,同时通过pointer-events控制元素的可点击性。
3. 混入模式(Mixins)
通过Vue的mixins机制,将弹框的拖动逻辑封装成可复用的组件,避免直接操作DOM带来的耦合性。
三、环境准备
1. 项目依赖
npm install cesium @vue/cli2. 项目结构
src/
├── components/
│ └── DraggableDialog.vue
├── views/
│ └── CesiumView.vue
├── mixins/
│ └── DialogMixin.js
└── App.vue3. CSS样式
/* 全局样式 */
#cesiumContainer {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1;
}
.draggable-dialog {
position: absolute;
z-index: 2;
pointer-events: auto;
}四、核心实现
1. 弹框组件:DraggableDialog.vue
<template>
<div class="draggable-dialog" ref="dialog">
<el-dialog
v-model="showDialog"
:visible.sync="showDialog"
:title="title"
:width="width"
:close-on-click-modal="false"
@close="handleClose"
@open="handleOpen"
:custom-class="customClass"
>
<slot></slot>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'DraggableDialog',
props: {
title: {
type: String,
default: '弹框标题'
},
width: {
type: String,
default: '40%'
}
},
data() {
return {
showDialog: false,
isDragging: false,
mousePosition: { x: 0, y: 0 }
};
},
mounted() {
this.initDrag();
},
methods: {
initDrag() {
const dialog = this.$refs.dialog;
dialog.addEventListener('mousedown', (e) => {
if (e.target === dialog) {
this.isDragging = true;
this.mousePosition = {
x: e.clientX - dialog.offsetLeft,
y: e.clientY - dialog.offsetTop
};
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
}
});
},
onMouseMove(e) {
if (this.isDragging) {
const dialog = this.$refs.dialog;
dialog.style.left = `${e.clientX - this.mousePosition.x}px`;
dialog.style.top = `${e.clientY - this.mousePosition.y}px`;
}
},
onMouseUp() {
this.isDragging = false;
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
},
handleOpen() {
this.showDialog = true;
},
handleClose() {
this.showDialog = false;
}
}
};
</script>关键代码解释:
- 使用
mousedown事件绑定拖拽逻辑,通过e.target === dialog确保只在弹框外层触发 - 使用
mousemove和mouseup控制拖动行为 - 通过
pointer-events: auto确保弹框可交互
2. 混入模块:DialogMixin.js
export default {
data() {
return {
isDialogOpen: false
};
},
methods: {
openDialog(title, width) {
this.isDialogOpen = true;
this.$refs.dialog.title = title || '默认标题';
this.$refs.dialog.width = width || '40%';
},
closeDialog() {
this.isDialogOpen = false;
}
}
};关键代码解释:
- 通过
$refs获取弹框组件实例 - 提供统一的
openDialog和closeDialog接口 - 通过
title和width参数控制弹框内容
3. Cesium场景组件:CesiumView.vue
<template>
<div>
<div id="cesiumContainer"></div>
<DraggableDialog ref="dialog" />
</div>
</template>
<script>
import { initCesium } from './utils/cesiumUtils';
import DraggableDialog from './components/DraggableDialog.vue';
export default {
name: 'CesiumView',
components: { DraggableDialog },
mounted() {
initCesium(this.$el.querySelector('#cesiumContainer'));
this.$refs.dialog.$on('close', this.handleDialogClose);
},
methods: {
handleDialogClose() {
// 处理弹框关闭后的逻辑
}
}
};
</script>关键代码解释:
- 将Cesium容器设置为固定定位
- 通过
ref获取弹框组件实例 - 在弹框关闭时执行相应逻辑
五、完整案例
1. 实时视频播放弹框示例
<template>
<div>
<CesiumView />
<DraggableDialog
ref="dialog"
title="实时视频播放"
width="60%"
>
<video controls autoplay>
<source src="https://example.com/video.mp4" type="video/mp4">
您的浏览器不支持视频标签。
</video>
</DraggableDialog>
</div>
</template>2. Cesium场景初始化代码(cesiumUtils.js)
export function initCesium(container) {
const viewer = new Cesium.Viewer(container, {
terrain: Cesium.Terrain.fromWorldTerrain(),
sceneMode: Cesium.SceneMode.SCENE3D,
baseLayerPicker: false
});
// 添加点击事件
viewer.scene.postRender.addEventListener(() => {
console.log('Cesium 场景渲染完成');
});
// 添加模型
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
model: {
uri: 'https://example.com/models/airplane.glb'
}
});
}运行效果:
- 在Cesium场景中点击模型时,弹框不会干扰交互
- 弹框支持拖动定位,但不会影响Cesium的事件处理
- 视频播放窗口始终在Cesium容器之上
六、源码解析
1. 拖拽逻辑实现
initDrag() {
const dialog = this.$refs.dialog;
dialog.addEventListener('mousedown', (e) => {
if (e.target === dialog) {
this.isDragging = true;
this.mousePosition = {
x: e.clientX - dialog.offsetLeft,
y: e.clientY - dialog.offsetTop
};
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
}
});
}关键点:
- 通过
e.target === dialog确保只在弹框外层触发 - 使用
offsetLeft和offsetTop计算相对位置 - 使用全局事件监听避免元素层级影响
2. 事件冒泡控制
onMouseMove(e) {
if (this.isDragging) {
const dialog = this.$refs.dialog;
dialog.style.left = `${e.clientX - this.mousePosition.x}px`;
dialog.style.top = `${e.clientY - this.mousePosition.y}px`;
}
}关键点:
- 使用
style.left和style.top动态定位 - 通过CSS设置
position: absolute实现自由定位 - 通过
pointer-events: auto确保交互正常
七、进阶使用
1. 动态绑定弹框内容
<template>
<DraggableDialog ref="dialog" :title="videoTitle">
<video :src="videoUrl" controls autoplay></video>
</DraggableDialog>
</template>
<script>
export default {
data() {
return {
videoTitle: '实时视频',
videoUrl: 'https://example.com/video.mp4'
};
}
};
</script>2. 多个弹框管理
export default {
data() {
return {
dialogs: []
};
},
methods: {
createDialog(title, content) {
const dialog = this.$createElement('DraggableDialog', {
props: {
title,
width: '50%'
},
children: [content]
});
this.dialogs.push(dialog);
}
}
};3. 与Cesium事件联动
viewer.scene.postRender.addEventListener(() => {
this.$refs.dialog.openDialog('场景更新', '60%');
});八、性能与工程实践
1. 性能优化
- 事件节流:在拖动过程中使用
requestAnimationFrame优化动画性能 - 内存管理:在组件销毁时移除事件监听
- CSS优化:使用
transform代替left/top实现更流畅的动画
2. 异常处理
onMouseUp() {
this.isDragging = false;
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
// 防止内存泄漏
this.mousePosition = null;
}3. 安全考虑
- XSS防护:对动态插入的HTML内容进行消毒处理
- CORS配置:确保视频资源的CORS头配置正确
- 权限控制:对弹框内容进行权限校验
九、常见问题与踩坑
1. 事件冒泡问题
错误示例:
dialog.addEventListener('click', (e) => {
e.stopPropagation();
});问题分析:
完全阻止事件冒泡会导致Cesium的事件处理失效
解决方案:
dialog.addEventListener('click', (e) => {
if (e.target === dialog) {
e.stopPropagation();
}
});2. 层级管理问题
错误示例:
.draggable-dialog {
z-index: 1000;
}问题分析:
Cesium容器的z-index默认为1,可能导致弹框被覆盖
解决方案:
#cesiumContainer {
z-index: 1;
}
.draggable-dialog {
z-index: 2;
}3. 拖动卡顿问题
错误示例:
onMouseMove(e) {
dialog.style.left = `${e.clientX}px`;
dialog.style.top = `${e.clientY}px`;
}性能优化:
onMouseMove(e) {
const dialog = this.$refs.dialog;
dialog.style.transform = `translate(${e.clientX - this.mousePosition.x}px, ${e.clientY - this.mousePosition.y}px)`;
}十、最佳实践
- 使用mixins封装逻辑:确保组件可复用且不耦合
- 严格控制事件冒泡:只在必要时阻止事件传播
- 合理设置z-index:确保弹框始终在Cesium容器之上
- 使用CSS动画:替代直接修改left/top属性
- 使用Vue的响应式机制:动态绑定弹框内容和位置
十一、总结
通过结合Vue的mixins特性、Element UI的弹框组件和CesiumJS的三维场景,我们实现了一个可拖动且不影响底层交互的弹框系统。该方案在以下场景中特别有用:
- 需要叠加自定义UI组件的Cesium项目
- 需要动态控制弹框内容的场景
- 需要支持拖动定位的交互需求
但需要注意以下限制:
- 事件处理复杂度增加
- 需要特别注意CSS层级管理
- 可能影响性能优化
在实际开发中,建议根据具体需求选择合适的实现方案。对于需要频繁操作Cesium元素的场景,应优先考虑使用Cesium的内置事件处理机制,而非完全依赖弹框组件。