From 5667f2b4fc7555ba678d5aef13b096cd38da64db Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Mon, 12 Jan 2026 15:45:34 +0800
Subject: [PATCH] feat:数据驾驶舱相关处理
---
applications/drone-command/src/views/dataCockpit/components/MapContainer.vue | 400 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 395 insertions(+), 5 deletions(-)
diff --git a/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue b/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
index aa94178..4576555 100644
--- a/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
+++ b/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
@@ -1,28 +1,274 @@
<template>
- <div class="ztzf-cesium map-container" id="cesium">
+ <div class="ztzf-cesium map-container" id="cesium"></div>
+ <div class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
+ <div class="layer-control-wrap" ref="layerWrapRef">
+ <div class="layer-control" @click="toggleLayerPanel">
+ <img :src="layerControlIcon" alt="图层控制">
+ </div>
+ <div v-if="showLayerPanel" class="layer-panel">
+ <div class="panel-title">图层管理</div>
+ <div class="panel-content">
+ <el-tree :data="layerTree" show-checkbox default-expand-all node-key="key" :props="layerTreeProps"
+ :default-checked-keys="defaultCheckedKeys" />
+ </div>
+ </div>
+ </div>
</div>
</template>
<script setup>
+import * as Cesium from 'cesium'
import { PublicCesium } from '@/utils/cesium/publicCesium'
+import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
+import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
+
+const props = defineProps({
+ onlineDevices: {
+ type: Array,
+ default: () => []
+ },
+ leftCollapsed: {
+ type: Boolean,
+ default: false
+ }
+})
+
let viewInstance = null
let viewer = null
+const deviceEntityIds = new Set()
+const showLayerPanel = ref(false)
+const layerWrapRef = ref(null)
+const layerTreeProps = {
+ label: 'label',
+ children: 'children'
+}
+const defaultCheckedKeys = ['global', 'city-base']
+const layerTree = ref([
+ {
+ key: 'base',
+ label: '地理信息图层',
+ children: [
+ { key: 'global', label: '全球地形' },
+ { key: 'admin', label: '行政区划' }
+ ]
+ },
+ {
+ key: 'city',
+ label: '城市CIM图层',
+ children: [
+ { key: 'city-base', label: '皖山白模' },
+ { key: 'city-grid', label: '皖山白模光栅网格' },
+ { key: 'city-tilt', label: '皖山倾斜摄影' },
+ { key: 'city-tilt-grid', label: '皖山倾斜摄影网格' }
+ ]
+ },
+ {
+ key: 'sky',
+ label: '空域要素图层',
+ children: [
+ { key: 'airspace', label: '空域边界' },
+ { key: 'route', label: '飞行航路' }
+ ]
+ }
+])
+
+const getDevicePosition = (item) => {
+ const longitudeRaw = item.longitude ?? item.lng ?? item.lon
+ const latitudeRaw = item.latitude ?? item.lat
+ if (longitudeRaw == null || latitudeRaw == null) return null
+ const longitude = Number(longitudeRaw)
+ const latitude = Number(latitudeRaw)
+ if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null
+ return { longitude, latitude }
+}
+
+const clearDeviceEntities = () => {
+ if (!viewer) return
+ deviceEntityIds.forEach((id) => {
+ viewer.entities.removeById(id)
+ })
+ deviceEntityIds.clear()
+}
+
+const RING_STYLES = [
+ { inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] },
+ { inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] },
+ { inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] }
+]
+
+const MATERIAL_TYPE = 'RadialGradientMaterial'
+let materialRegistered = false
+
+const registerRadialGradientMaterial = () => {
+ if (materialRegistered || !Cesium?.Material) return
+ materialRegistered = true
+ Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
+ fabric: {
+ type: MATERIAL_TYPE,
+ uniforms: {
+ color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
+ color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0)
+ },
+ source: `
+ czm_material czm_getMaterial(czm_materialInput materialInput) {
+ czm_material material = czm_getDefaultMaterial(materialInput);
+ vec2 st = materialInput.st - vec2(0.5);
+ float t = clamp(length(st) * 2.0, 0.0, 1.0);
+ vec4 color = mix(color1, color2, t);
+ material.diffuse = color.rgb;
+ material.alpha = color.a;
+ return material;
+ }
+ `
+ },
+ translucent: () => true
+ })
+}
+
+class RadialGradientMaterialProperty {
+ constructor(color1, color2) {
+ this._definitionChanged = new Cesium.Event()
+ this.color1 = color1
+ this.color2 = color2
+ }
+
+ get isConstant () {
+ return true
+ }
+
+ get definitionChanged () {
+ return this._definitionChanged
+ }
+
+ getType () {
+ return MATERIAL_TYPE
+ }
+
+ getValue (time, result) {
+ const target = result || {}
+ target.color1 = this.color1
+ target.color2 = this.color2
+ return target
+ }
+
+ equals (other) {
+ return (
+ other instanceof RadialGradientMaterialProperty &&
+ Cesium.Color.equals(this.color1, other.color1) &&
+ Cesium.Color.equals(this.color2, other.color2)
+ )
+ }
+}
+
+const buildCirclePositions = (center, radiusMeters, steps = 64) => {
+ const positions = []
+ const latRad = Cesium.Math.toRadians(center.latitude)
+ const metersToLat = 1 / 111320
+ const metersToLon = 1 / (111320 * Math.cos(latRad))
+ for (let i = 0; i <= steps; i += 1) {
+ const angle = Cesium.Math.toRadians((i / steps) * 360)
+ const dx = radiusMeters * Math.cos(angle)
+ const dy = radiusMeters * Math.sin(angle)
+ const lon = center.longitude + dx * metersToLon
+ const lat = center.latitude + dy * metersToLat
+ positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
+ }
+ return positions
+}
+
+const addDeviceRings = (center, baseId) => {
+ RING_STYLES.forEach((ring, index) => {
+ const outerPositions = buildCirclePositions(center, ring.outer)
+ const holes = ring.inner
+ ? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))]
+ : []
+ const entityId = `${baseId}-ring-${index}`
+ deviceEntityIds.add(entityId)
+ registerRadialGradientMaterial()
+ const [startColor, endColor] = ring.gradient
+ const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34)
+ const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34)
+ const material = new RadialGradientMaterialProperty(color1, color2)
+ viewer.entities.add({
+ id: entityId,
+ polygon: {
+ hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
+ material
+ }
+ })
+ })
+}
+
+const renderDeviceEntities = (devices) => {
+ console.log(devices, 'devices')
+
+ if (!viewer) return
+
+ clearDeviceEntities()
+ devices.forEach((item, index) => {
+ const position = getDevicePosition(item)
+ if (!position) return
+ const entityId = `online-device-${item.id ?? index}`
+ deviceEntityIds.add(entityId)
+ addDeviceRings(position, entityId)
+ viewer.entities.add({
+ id: entityId,
+ position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
+ billboard: {
+ image: equipmentIcon,
+ width: 40.34,
+ height: 40.34
+ }
+ })
+ })
+}
+
+watch(
+ () => props.onlineDevices,
+ (devices) => {
+ renderDeviceEntities(devices || [])
+ },
+ { deep: true }
+)
+
+watch(
+ () => props.leftCollapsed,
+ (isCollapsed) => {
+ if (isCollapsed) showLayerPanel.value = false
+ }
+)
+
+const toggleLayerPanel = () => {
+ showLayerPanel.value = !showLayerPanel.value
+}
+
+const handleClickOutside = (event) => {
+ if (!showLayerPanel.value) return
+ const target = event.target
+ if (layerWrapRef.value?.contains(target)) return
+ showLayerPanel.value = false
+}
+
onMounted(() => {
+ document.addEventListener('click', handleClickOutside)
viewInstance = new PublicCesium({
dom: 'cesium',
flatMode: false,
terrain: false,
layerMode: 4,
- contour: false,
+ contour: false
})
viewer = viewInstance.getViewer()
-})
+ renderDeviceEntities(props.onlineDevices)
+})
onBeforeUnmount(() => {
+ document.removeEventListener('click', handleClickOutside)
+ clearDeviceEntities()
if (viewInstance) {
- viewInstance?.destroy()
+ viewInstance?.viewerDestroy()
viewInstance = null
}
@@ -38,4 +284,148 @@
width: 100%;
height: 100%;
}
-</style>
\ No newline at end of file
+
+.layer-control-root {
+ position: absolute;
+ left: 411px;
+ bottom: 22px;
+ z-index: 9;
+ transition: transform 0.3s ease-in-out;
+ pointer-events: none;
+
+ &.collapsed {
+ transform: translateX(-401px);
+ }
+}
+
+.layer-control-wrap {
+ position: relative;
+ display: flex;
+ align-items: flex-end;
+ pointer-events: auto;
+}
+
+.layer-control {
+ width: 34px;
+ height: 34px;
+ cursor: pointer;
+
+ img {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+}
+
+.layer-panel {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ left: 46px;
+ bottom: 0;
+ width: 160px;
+ max-height: 442px;
+ background: #191932;
+ border-radius: 8px 8px 8px 8px;
+
+ .panel-title {
+ padding: 0 16px;
+ line-height: 42px;
+
+ font-family: Open Sans, Open Sans;
+ font-weight: 400;
+ font-size: 12px;
+ color: #FFFFFF;
+ text-align: left;
+ font-style: normal;
+ text-transform: none;
+
+ border-bottom: 1px solid rgba(70,70,100,0.5);
+
+ box-sizing: border-box;
+ }
+
+ .panel-content {
+ padding: 0 16px;
+ height: 0;
+ flex: 1;
+ overflow: auto;
+ }
+
+ ::v-deep(.el-tree) {
+ background: transparent;
+ color: #C3C3DD;
+ font-size: 12px;
+
+ .el-tree-node {
+ line-height: 30px !important;
+
+ .el-tree-node__content {
+ display: flex;
+ align-items: center;
+ height: 40px !important;
+ line-height: 40px !important;
+ border-bottom: 1px solid rgba(70,70,100,0.5);
+ box-sizing: border-box;
+ }
+
+ .el-tree-node__children {
+ .el-tree-node__content {
+ border: none;
+ }
+ }
+ }
+
+ }
+}
+
+
+
+.layer-panel :deep(.el-tree-node__label) {
+ color: #C3C3DD;
+}
+
+.layer-panel :deep(.el-tree-node__expand-icon) {
+ order: 3;
+ margin-left: auto;
+}
+
+.layer-panel :deep(.el-tree-node__expand-icon.is-leaf) {
+ visibility: hidden;
+}
+
+.layer-panel :deep(.el-checkbox) {
+ order: 1;
+}
+
+.layer-panel :deep(.el-tree-node__label) {
+ order: 2;
+}
+
+.layer-panel :deep(.el-tree-node__expand-icon:not(.is-leaf) ~ .el-checkbox) {
+ display: none;
+}
+
+.layer-panel :deep(.el-tree-node__content:hover) {
+ background: transparent !important;
+}
+
+.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
+ background: transparent !important;
+ color: #FFFFFF;
+}
+
+.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) {
+ color: #FFFFFF;
+}
+
+.layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
+ background-color: #023AFF;
+ border-color: #023AFF;
+}
+
+.layer-panel :deep(.el-checkbox__inner) {
+ background-color: transparent;
+ border-color: #A1A3D4;
+}
+</style>
--
Gitblit v1.9.3