吉安感知网项目-前端
shuishen
2026-01-12 5667f2b4fc7555ba678d5aef13b096cd38da64db
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>
.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>