吉安感知网项目-前端
shuishen
2026-01-27 c1b252b4890ae7df1720955cb7caba184d78798f
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -2,41 +2,45 @@
   <div class="map-shell">
      <CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true"
         :flat-mode="false" :terrain="true" :layer-mode="4" :contour="false" :boundary="false"
         :show-admin-boundary="true" :zoom-to-boundary="true" :enable-stage-emit="true" :cluster-height="100000"
         @ready="handleMapReady" @stage-change="handleStageChange" />
         :show-admin-boundary="true" :zoom-to-boundary="true" :enable-stage-emit="true"
         :cluster-height="CLUSTER_HEIGHT" :detail-height="DETAIL_HEIGHT" @ready="handleMapReady"
         @stage-change="handleStageChange" />
      <div v-if="props.showLayerControl" 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-title">底图切换</div>
               <div class="base-map-options">
                  <div class="base-map-card" :class="{ active: baseLayerKey === 'base-standard' }"
                     @click="handleBaseLayerSelect('base-standard')">
                     <img class="base-map-thumb standard" :src="dzIcon" alt="">
                     <div class="base-map-label">标准地图</div>
                  </div>
                  <div class="base-map-card" :class="{ active: baseLayerKey === 'base-satellite' }"
                     @click="handleBaseLayerSelect('base-satellite')">
                     <img class="base-map-thumb satellite" :src="yxIcon" alt="">
                     <div class="base-map-label">卫星地图</div>
                  </div>
               </div>
               <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" />
                  <el-tree ref="layerTreeRef" class="command-tree map-layer-tree" :data="layerTree" show-checkbox
                     default-expand-all node-key="key" :props="layerTreeProps"
                     :default-checked-keys="treeCheckedKeys" @check="handleLayerCheck" />
               </div>
            </div>
         </div>
      </div>
      <DevicePopup
         :visible="popupVisible && !isDronePopup"
         :position="popupPosition"
         :device="selectedDevice"
         @close="closePopup"
      />
      <DevicePopup :visible="popupVisible && !isDronePopup" :position="popupPosition" :device="selectedDevice"
         @close="closePopup" />
      <DronePopup
         :visible="popupVisible && isDronePopup"
         :position="popupPosition"
         :drone="selectedDevice"
         :favorite="Boolean(selectedDevice?.isFavorite)"
         @close="closePopup"
         @toggle-favorite="toggleDroneFavorite"
         @signal="handleDroneSignal"
         @counter="handleDroneCounter"
      />
      <DronePopup :visible="popupVisible && isDronePopup" :position="popupPosition" :drone="selectedDevice"
         :favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="toggleDroneFavorite"
         @signal="handleDroneSignal" @counter="handleDroneCounter" />
   </div>
</template>
@@ -63,6 +67,11 @@
   createRadialGradientMaterial,
   getTexturedVertexFormat,
} from './device-map-materials'
import dzIcon from '@/assets/images/dataCockpit/map/dz-map-layer.png'
import yxIcon from '@/assets/images/dataCockpit/map/yx-map-layer.png'
const CLUSTER_HEIGHT = 100000
const DETAIL_HEIGHT = 10000
const props = defineProps({
   onlineDevices: {
@@ -88,6 +97,7 @@
const mapRef = ref(null)
let viewer = null
let publicCesium = null
let cockpitPrimitiveLayer = null
const devicePickMap = new Map()
const dronePickMap = new Map()
@@ -110,6 +120,7 @@
const countyCenterMap = new Map()
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
const layerTreeRef = ref(null)
const selectedDevice = ref(null)
const selectedTargetType = ref('device')
let selectedDeviceBillboard = null
@@ -119,17 +130,24 @@
   label: 'label',
   children: 'children',
}
const defaultCheckedKeys = ['global', 'city-base']
const baseLayerKeys = ['base-standard', 'base-satellite']
const defaultCheckedKeys = ['ja-terrain', 'admin', 'city-base']
const baseLayerKey = ref('base-satellite')
const treeCheckedKeys = ref([...defaultCheckedKeys])
const layerTree = ref([
   {
      key: 'base',
      label: '地理信息图层',
      children: [
         { key: 'global', label: '全球地形' },
         { key: 'ja-terrain', label: '吉安地形' },
         { key: 'admin', label: '行政区划' },
      ],
   },
   {
])
/**
 * {
      key: 'city',
      label: '城市CIM图层',
      children: [
@@ -147,7 +165,9 @@
         { key: 'route', label: '飞行航路' },
      ],
   },
])
 */
const adminBoundaryVisible = ref(treeCheckedKeys.value.includes('admin'))
const getDevicePosition = item => {
   const longitudeRaw = item.longitude ?? item.lng ?? item.lon
@@ -157,6 +177,14 @@
   const latitude = Number(latitudeRaw)
   if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null
   return { longitude, latitude }
}
const getDeviceRange = item => {
   if (!item) return null
   const rawRange = item.effectiveRangeKm ?? item.range ?? item.coverRadiusM
   const range = Number(rawRange)
   if (!Number.isFinite(range) || range <= 0) return null
   return range
}
const ensureCockpitPrimitiveLayer = () => {
@@ -302,7 +330,26 @@
const setClusterVisibility = visible => {
   clusterVisible.value = visible
   if (aggregationSource) aggregationSource.show = visible
   updateAggregationVisibility()
}
const updateAggregationVisibility = () => {
   if (!aggregationSource) return
   aggregationSource.show = clusterVisible.value && adminBoundaryVisible.value
}
const setDroneVisibility = visible => {
   if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
   if (droneTrackPolylineCollection) droneTrackPolylineCollection.show = visible
   if (!visible && selectedTargetType.value === 'drone') {
      closePopup()
   }
}
const getStageByHeight = height => {
   if (height == null) return 'detail'
   if (height >= CLUSTER_HEIGHT) return 'cluster'
   if (height <= DETAIL_HEIGHT) return 'detail'
   return 'mid'
}
const ensureCountyCenterMap = () => {
@@ -316,24 +363,44 @@
   })
}
const buildSimulatedTrackPoints = base => {
   ensureCountyCenterMap()
   const anchor = base || { longitude: 116.397, latitude: 39.908 }
   const offsets = [
      [0, 0, 120],
      [0.02, 0.012, 150],
      [0.04, 0.006, 180],
      [0.06, -0.008, 200],
      [0.03, -0.02, 160],
      [0.0, -0.015, 140],
      [-0.02, -0.005, 120],
      [-0.01, 0.01, 130],
const buildSimulatedTrackPoints = () => {
   return [
      {
         longitude: 114.963191,
         latitude: 27.136716,
         height: 120
      },
      {
         longitude: 114.957308,
         latitude: 27.138452,
         height: 120
      },
      {
         longitude: 114.952,
         latitude: 27.136317,
         height: 120
      },
      {
         longitude: 114.949293,
         latitude: 27.133864,
         height: 120
      },
      {
         longitude: 114.944666,
         latitude: 27.130526,
         height: 120
      },
      {
         longitude: 114.945909,
         latitude: 27.127845,
         height: 120
      },
      {
         longitude: 114.962974,
         latitude: 27.136242,
         height: 120
      },
   ]
   return offsets.map(item => ({
      longitude: anchor.longitude + item[0],
      latitude: anchor.latitude + item[1],
      height: item[2],
   }))
}
@@ -422,13 +489,11 @@
const renderSimulatedDroneTrack = () => {
   if (!viewer) return
   clearDroneTrackEntities()
   ensureCountyCenterMap()
   ensureDroneTrackCollections()
   droneTrackBillboardCollection.show = detailVisible.value
   droneTrackPolylineCollection.show = detailVisible.value
   droneTrackRuntime = []
   const centers = Array.from(countyCenterMap.entries())
   const baseCenters = centers.length
      ? centers.slice(0, 4)
      : [['默认区域', { longitude: 116.397, latitude: 39.908 }]]
   const baseCenters = [['默认区域', { longitude: 114.958541, latitude: 27.121917 }]]
   const segmentDuration = 6
   const baseTrackColor = Cesium.Color.fromCssColorString('red')
   baseCenters.forEach(([centerName, center], trackIndex) => {
@@ -454,7 +519,7 @@
      })
      const droneId = `drone-track-${trackIndex}`
      const billboard = droneTrackBillboardCollection.add({
         position: positions[0],
         position: positions[positions.length - 1],
         image: droneIcon,
         width: 36,
         height: 36,
@@ -511,7 +576,8 @@
      const position = getDevicePosition(item)
      if (!position) return
      const entityId = `online-device-${item.id ?? index}-${index}`
      addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle)
      const rangeMeters = getDeviceRange(item)
      addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle, rangeMeters)
      const billboard = deviceBillboardCollection.add({
         position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
         image: equipmentIcon,
@@ -528,9 +594,20 @@
   ringFillInstancesByStyle.forEach((instances, index) => {
      if (!instances.length) return
      const ring = RING_STYLES[index]
      const alpha = ring.alpha ?? 0.64
      const innerRatio =
         typeof ring.innerRatio === 'number'
            ? ring.innerRatio
            : ring.inner && ring.outer
               ? Math.min(Math.max(ring.inner / ring.outer, 0), 0.9)
               : 0
      const material = createRadialGradientMaterial(
         Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64),
         Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64)
         Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(alpha),
         Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(alpha),
         {
            gamma: 1.7,
            innerCutoff: innerRatio,
         }
      )
      const primitive = new Cesium.GroundPrimitive({
         geometryInstances: instances,
@@ -573,34 +650,34 @@
   const polygonInstances = []
   const lineInstances = []
   const texturedVertexFormat = getTexturedVertexFormat()
   ; (zones || []).forEach((zone, index) => {
      if (!zone?.geom) return
      const positions = getDefenseZonePositions(zone.geom)
      if (!positions.length) return
      const polygon = new Cesium.PolygonGeometry({
         polygonHierarchy: new Cesium.PolygonHierarchy(positions),
         vertexFormat: texturedVertexFormat,
      ; (zones || []).forEach((zone, index) => {
         if (!zone?.geom) return
         const positions = getDefenseZonePositions(zone.geom)
         if (!positions.length) return
         const polygon = new Cesium.PolygonGeometry({
            polygonHierarchy: new Cesium.PolygonHierarchy(positions),
            vertexFormat: texturedVertexFormat,
         })
         polygonInstances.push(
            new Cesium.GeometryInstance({
               geometry: polygon,
            })
         )
         const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
         lineInstances.push(
            new Cesium.GeometryInstance({
               geometry: new Cesium.GroundPolylineGeometry({
                  positions: linePositions,
                  width: 2,
               }),
               attributes: {
                  color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                     Cesium.Color.fromCssColorString(lineColor)
                  ),
               },
            })
         )
      })
      polygonInstances.push(
         new Cesium.GeometryInstance({
            geometry: polygon,
         })
      )
      const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
      lineInstances.push(
         new Cesium.GeometryInstance({
            geometry: new Cesium.GroundPolylineGeometry({
               positions: linePositions,
               width: 2,
            }),
            attributes: {
               color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                  Cesium.Color.fromCssColorString(lineColor)
               ),
            },
         })
      )
   })
   let primitive = null
   if (polygonInstances.length) {
      const baseColor1 = Cesium.Color.fromCssColorString(fillColor1)
@@ -629,7 +706,7 @@
const renderDefenseZones = zones => {
   if (!viewer) return
   clearDefenseZoneEntities()
   const result = buildZonePrimitives(zones, '#19D266', '#2AEDBF', '#012B11')
   const result = buildZonePrimitives(zones, '#72F3FF', '#72F3FF', '#0A7C88')
   defenseZonePrimitive = result.primitive
   defenseZoneOutlinePrimitive = result.outlinePrimitive
   if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value
@@ -640,7 +717,7 @@
const renderPartitions = zones => {
   if (!viewer) return
   clearPartitionEntities()
   const result = buildZonePrimitives(zones, '#FFCD2A', '#FFC609', '#583300')
   const result = buildZonePrimitives(zones, '#FFD772', '#FFD772', '#A86B00')
   partitionPrimitive = result.primitive
   partitionOutlinePrimitive = result.outlinePrimitive
   if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
@@ -676,7 +753,7 @@
      viewer.dataSources.add(aggregationSource)
   }
   clearAggregationEntities()
   aggregationSource.show = clusterVisible.value
   updateAggregationVisibility()
   const countMap = new Map()
      ; (list || []).forEach(item => {
         if (!item?.type) return
@@ -693,7 +770,7 @@
            width: 66.73,
            height: 43,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            // disableDepthTestDistance: Number.POSITIVE_INFINITY,
         },
         label: {
            text: `${count}`,
@@ -706,7 +783,6 @@
            pixelOffset: new Cesium.Cartesian2(0, -35),
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
         },
      })
   })
@@ -796,17 +872,17 @@
   ensureCommandPostCollection()
   commandPostBillboardCollection.removeAll()
   commandPostBillboardCollection.show = detailVisible.value
   ; (list || []).forEach((item, index) => {
      const position = getDevicePosition(item)
      if (!position) return
      commandPostBillboardCollection.add({
         position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
         image: commandPostIcon,
         width: 40,
         height: 56,
         verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
      ; (list || []).forEach((item, index) => {
         const position = getDevicePosition(item)
         if (!position) return
         commandPostBillboardCollection.add({
            position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
            image: commandPostIcon,
            width: 40,
            height: 56,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
         })
      })
   })
   reorderCockpitPrimitives()
}
@@ -851,17 +927,43 @@
   showLayerPanel.value = !showLayerPanel.value
}
const applyLayerVisibility = checkedKeys => {
   publicCesium?.switchLayers?.(baseLayerKey.value === 'base-standard' ? 0 : 4)
   const showTerrain = checkedKeys.includes('ja-terrain')
   publicCesium?.setTerrainVisible?.(showTerrain)
   const showAdmin = checkedKeys.includes('admin')
   adminBoundaryVisible.value = showAdmin
   mapRef.value?.setAdminBoundaryVisible?.(showAdmin)
   updateAggregationVisibility()
}
const handleLayerCheck = (_data, state) => {
   const checkedKeys = state?.checkedKeys ?? layerTreeRef.value?.getCheckedKeys?.() ?? []
   treeCheckedKeys.value = checkedKeys
   applyLayerVisibility(checkedKeys)
}
const handleBaseLayerSelect = key => {
   if (!baseLayerKeys.includes(key)) return
   if (baseLayerKey.value === key) return
   baseLayerKey.value = key
   applyLayerVisibility(treeCheckedKeys.value)
}
const updateStageDisplay = stage => {
   const showCluster = stage === 'cluster'
   setClusterVisibility(showCluster)
   setDetailVisibility(!showCluster)
   setDroneVisibility(!showCluster)
}
const handleMapReady = ({ viewer: mapViewer }) => {
const handleMapReady = ({ viewer: mapViewer, publicCesium: mapPublic }) => {
   viewer = mapViewer
   publicCesium = mapPublic
   ensureCockpitPrimitiveLayer()
   applyLayerVisibility(treeCheckedKeys.value)
   const height = viewer?.camera?.positionCartographic?.height
   const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
   const stage = getStageByHeight(height)
   updateStageDisplay(stage)
   renderDeviceEntities(props.onlineDevices)
   loadDefenseZones()
@@ -904,6 +1006,7 @@
   }
   cockpitPrimitiveLayer = null
   viewer = null
   publicCesium = null
})
</script>
@@ -986,86 +1089,50 @@
      overflow: auto;
   }
   ::v-deep(.el-tree) {
      background: transparent;
      color: #c3c3dd;
   .base-map-title {
      margin-bottom: 8px;
      font-size: 12px;
      color: #ffffff;
      font-weight: 600;
   }
      .el-tree-node {
         line-height: 30px !important;
   .base-map-options {
      padding: 16px;
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 8px;
   }
         .el-tree-node__content {
            padding-left: 0 !important;
            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;
   .base-map-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 6px;
      border-radius: 6px;
      color: #d8e6ff;
      font-size: 12px;
      cursor: pointer;
      transition: all 0.2s ease;
      .base-map-thumb {
         width: 46px;
         height: 46px;
         border-radius: 4px;
         background-size: cover;
         background-position: center;
         box-sizing: border-box;
      }
      &.active {
         .base-map-thumb {
            border: 2px solid #2ea8ff;
         }
         .el-tree-node__children {
            .el-tree-node__content {
               border: none;
            }
         }
         &:focus {
            .el-tree-node__content {
               background: transparent !important;
            }
         .base-map-label {
            color: #2ea8ff;
         }
      }
   }
}
.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),
.layer-panel :deep(.el-tree-node__content:focus) {
   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>