吉安感知网项目-前端
shuishen
2026-01-30 8c450d784fe7bab2b27e3345ef9263e9f295ecaa
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -39,7 +39,7 @@
         @close="closePopup" />
      <DronePopup :visible="popupVisible && isDronePopup" :position="popupPosition" :drone="selectedDevice"
         :favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="toggleDroneFavorite"
         :favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="handleDroneFavorite"
         @signal="handleDroneSignal" @counter="handleDroneCounter" />
   </div>
</template>
@@ -48,8 +48,10 @@
import * as Cesium from 'cesium'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { geomAnalysis } from '@ztzf/utils'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi'
import { fwAreaDivideListApi } from '@/views/areaManage/partition/partitionApi'
import { fwDefenseSceneListApi } from '@/views/areaManage/sceneConfig/sceneConfigApi'
import { cockpitAggregationApi } from '@/api/dataCockpit'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
@@ -78,6 +80,10 @@
      type: Array,
      default: () => [],
   },
   alarmDrones: {
      type: Array,
      default: () => [],
   },
   leftCollapsed: {
      type: Boolean,
      default: false,
@@ -91,7 +97,7 @@
      default: true,
   },
})
const emit = defineEmits(['droneSignal', 'droneCounter'])
const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite'])
const DEFAULT_ZONE_PAGE_SIZE = 999
@@ -115,6 +121,14 @@
let droneTrackTickHandler = null
let droneTrackStartTime = null
let droneTrackRuntime = []
let favoritePulseRafId = null
let favoritePulseStartAt = 0
let favoritePulseElapsed = 0
const favoritePulseEntities = []
const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
const PULSE_MIN_RADIUS_M = 60
const PULSE_MAX_RADIUS_M = 180
const PULSE_DURATION_S = 1.6
const detailVisible = ref(true)
const clusterVisible = ref(false)
const countyCenterMap = new Map()
@@ -168,6 +182,57 @@
 */
const adminBoundaryVisible = ref(treeCheckedKeys.value.includes('admin'))
const isFavorited = item => {
   const value = item?.favorited ?? item?.isFavorite
   return value === 1 || value === '1' || value === true
}
let pulseCanvas = null
const getPulseCanvas = () => {
   if (pulseCanvas) return pulseCanvas
   const size = 256
   const canvas = document.createElement('canvas')
   canvas.width = size
   canvas.height = size
   const ctx = canvas.getContext('2d')
   const center = size / 2
   const radius = center - 2
   const gradient = ctx.createRadialGradient(center, center, radius * 0.1, center, center, radius)
   gradient.addColorStop(0, 'rgba(255,59,48,0.55)')
   gradient.addColorStop(0.35, 'rgba(255,59,48,0.35)')
   gradient.addColorStop(0.7, 'rgba(255,59,48,0.15)')
   gradient.addColorStop(1, 'rgba(255,59,48,0)')
   ctx.fillStyle = gradient
   ctx.beginPath()
   ctx.arc(center, center, radius, 0, Math.PI * 2)
   ctx.fill()
   pulseCanvas = canvas
   return canvas
}
const createFavoritePulseEntity = (billboard) => {
   if (!viewer || !billboard) return
   const material = new Cesium.ImageMaterialProperty({
      image: getPulseCanvas(),
      transparent: true,
   })
   const pulse = {
      material,
      offset: Math.random() * PULSE_DURATION_S,
      entity: null,
   }
   pulse.entity = viewer.entities.add({
      position: new Cesium.CallbackProperty(() => billboard.position, false),
      ellipse: {
         semiMajorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
         semiMinorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
         material,
         heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      },
   })
   favoritePulseEntities.push(pulse)
}
const getDevicePosition = item => {
   const longitudeRaw = item.longitude ?? item.lng ?? item.lon
@@ -307,6 +372,12 @@
   }
}
const getPulseRadius = pulse => {
   const phase = ((favoritePulseElapsed + pulse.offset) % PULSE_DURATION_S) / PULSE_DURATION_S
   const wave = phase < 0.5 ? phase * 2 : (1 - phase) * 2
   return PULSE_MIN_RADIUS_M + (PULSE_MAX_RADIUS_M - PULSE_MIN_RADIUS_M) * wave
}
const setDetailVisibility = visible => {
   detailVisible.value = visible
   if (defenseZonePrimitive) defenseZonePrimitive.show = visible
@@ -341,6 +412,11 @@
const setDroneVisibility = visible => {
   if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
   if (droneTrackPolylineCollection) droneTrackPolylineCollection.show = visible
   if (favoritePulseEntities.length) {
      favoritePulseEntities.forEach(pulse => {
         if (pulse?.entity) pulse.entity.show = visible
      })
   }
   if (!visible && selectedTargetType.value === 'drone') {
      closePopup()
   }
@@ -363,50 +439,93 @@
   })
}
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
      },
const buildSimulatedTrackPoints = center => {
   const basePoints = [
      [
         {
            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
         },
      ],
      [
         {
            longitude: 114.931063,
            latitude: 27.095052,
            height: 120
         },
         {
            longitude: 114.928653,
            latitude: 27.096307,
            height: 120
         },
         {
            longitude: 114.925958,
            latitude: 27.096963,
            height: 120
         },
         {
            longitude: 114.925836,
            latitude: 27.096102,
            height: 120
         },
         {
            longitude: 114.928384,
            latitude: 27.095866,
            height: 120
         },
         {
            longitude: 114.929874,
            latitude: 27.096095,
            height: 120
         },
         {
            longitude: 114.931048,
            latitude: 27.097666,
            height: 120
         },
      ]
   ]
   return basePoints[center.trackIndex]
}
const clearDroneTrackEntities = () => {
   if (!viewer) return
   stopDroneTrackAnimation()
   stopFavoritePulseAnimation()
   if (droneTrackBillboardCollection) {
      if (!droneTrackBillboardCollection.isDestroyed?.()) {
         removeCockpitPrimitive(droneTrackBillboardCollection)
@@ -420,6 +539,12 @@
      droneTrackPolylineCollection = null
   }
   droneTrackRuntime = []
   if (favoritePulseEntities.length) {
      favoritePulseEntities.forEach(pulse => {
         if (pulse?.entity) viewer?.entities?.remove(pulse.entity)
      })
      favoritePulseEntities.length = 0
   }
   dronePickMap.clear()
   if (selectedTargetType.value === 'drone') {
      closePopup()
@@ -486,72 +611,95 @@
   droneTrackTickHandler = null
}
const renderSimulatedDroneTrack = () => {
const startFavoritePulseAnimation = () => {
   if (!viewer || favoritePulseRafId) return
   favoritePulseStartAt = performance.now()
   const tick = now => {
      if (!viewer || viewer.isDestroyed?.() || !favoritePulseEntities.length) {
         favoritePulseRafId = null
         return
      }
      favoritePulseElapsed = (now - favoritePulseStartAt) / 1000
      viewer.scene.requestRender()
      favoritePulseRafId = requestAnimationFrame(tick)
   }
   favoritePulseRafId = requestAnimationFrame(tick)
}
const stopFavoritePulseAnimation = () => {
   if (favoritePulseRafId) {
      cancelAnimationFrame(favoritePulseRafId)
      favoritePulseRafId = null
   }
}
const renderSimulatedDroneTrack = (list) => {
   if (!viewer) return
   clearDroneTrackEntities()
   if (!list?.length) return
   ensureDroneTrackCollections()
   droneTrackBillboardCollection.show = detailVisible.value
   droneTrackPolylineCollection.show = detailVisible.value
   droneTrackRuntime = []
   const baseCenters = [['默认区域', { longitude: 114.958541, latitude: 27.121917 }]]
   const segmentDuration = 6
   const baseTrackColor = Cesium.Color.fromCssColorString('red')
   baseCenters.forEach(([centerName, center], trackIndex) => {
      const points = buildSimulatedTrackPoints(center)
      if (points.length < 2) return
      const positions = points.map(point =>
         Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
      )
      let trackMaterial = createDroneTrackMaterial({
         color: baseTrackColor,
         speed: 4.5,
         headWidth: 0.2,
         glowPower: 1.8,
         backgroundAlpha: 0.34,
      })
      if (!trackMaterial) {
         trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
      }
      droneTrackPolylineCollection.add({
         positions,
         width: 3,
         material: trackMaterial,
      })
      const droneId = `drone-track-${trackIndex}`
      const billboard = droneTrackBillboardCollection.add({
         position: positions[positions.length - 1],
         image: droneIcon,
         width: 36,
         height: 36,
         verticalOrigin: Cesium.VerticalOrigin.CENTER,
         disableDepthTestDistance: Number.POSITIVE_INFINITY,
      })
      billboard.id = droneId
      const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
      ; (list || []).forEach((item, trackIndex) => {
         const position = getDevicePosition(item)
         if (!position) return
         const points = buildSimulatedTrackPoints({ ...position, height: item.flightHeightM, trackIndex })
         if (points.length < 2) return
         const positions = points.map(point =>
            Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
         )
         let trackMaterial = createDroneTrackMaterial({
            color: baseTrackColor,
            speed: 4.5,
            headWidth: 0.2,
            glowPower: 1.8,
            backgroundAlpha: 0.34,
         })
         if (!trackMaterial) {
            trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
         }
         droneTrackPolylineCollection.add({
            positions,
            width: 3,
            material: trackMaterial,
         })
         const droneId = `drone-alarm-${item?.alarmRecordId ?? item?.id ?? trackIndex}`
         const billboard = droneTrackBillboardCollection.add({
            position: positions[positions.length - 1],
            image: droneIcon,
            width: 36,
            height: 36,
            verticalOrigin: Cesium.VerticalOrigin.CENTER,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
         })
         billboard.id = droneId
         const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
      dronePickMap.set(droneId, {
         data: {
            droneName: `无人机-${trackIndex + 1}`,
            droneSerialNo: `UAV-${trackIndex + 1}`,
            flightHeightM: points[0].height,
            flightSpeedMs: speedMs,
            longitude: points[0].longitude,
            latitude: points[0].latitude,
            flightStatus: 1,
            deviceName: centerName,
            dataSource: centerName,
            counterDeviceName: centerName,
            ...item,
            flightHeightM: item.flightHeightM ?? points[0].height,
            flightSpeedMs: item.flightSpeedMs ?? speedMs,
            longitude: item.longitude ?? points[0].longitude,
            latitude: item.latitude ?? points[0].latitude,
         },
         billboard,
      })
      droneTrackRuntime.push({
         positions,
         billboard,
         segmentDuration,
         duration: (points.length - 1) * segmentDuration,
      if (isFavorited(item)) {
         createFavoritePulseEntity(billboard)
      }
         droneTrackRuntime.push({
            positions,
            billboard,
            segmentDuration,
            duration: (points.length - 1) * segmentDuration,
         })
      })
   })
   viewer.clock.multiplier = 1
   startDroneTrackAnimation()
   startFavoritePulseAnimation()
   reorderCockpitPrimitives()
}
@@ -645,6 +793,10 @@
   return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude))
}
const getAreaTypeStyle = areaType => {
   return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
const buildZonePrimitives = (zones, lineColor, fillColor1, fillColor2) => {
   if (!viewer) return { primitive: null, outlinePrimitive: null }
   const polygonInstances = []
@@ -703,6 +855,146 @@
   return { primitive, outlinePrimitive }
}
const buildPartitionPrimitives = shapes => {
   if (!viewer) return { primitive: null, outlinePrimitive: null }
   const polygonInstances = []
   const lineInstances = []
   const vertexFormat = Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
      ; (shapes || []).forEach(shape => {
         if (!shape?.positions || shape.positions.length < 3) return
         const positions = shape.positions
         const polygon = new Cesium.PolygonGeometry({
            polygonHierarchy: new Cesium.PolygonHierarchy(positions),
            vertexFormat,
         })
         polygonInstances.push(
            new Cesium.GeometryInstance({
               geometry: polygon,
               attributes: {
                  color: Cesium.ColorGeometryInstanceAttribute.fromColor(shape.fillColor),
               },
            })
         )
         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(
                     shape.outlineColor
                  ),
               },
            })
         )
      })
   let primitive = null
   if (polygonInstances.length) {
      primitive = new Cesium.GroundPrimitive({
         geometryInstances: polygonInstances,
         appearance: new Cesium.PerInstanceColorAppearance({ translucent: true, flat: true }),
      })
      addCockpitPrimitive(primitive)
   }
   let outlinePrimitive = null
   if (lineInstances.length) {
      outlinePrimitive = new Cesium.GroundPolylinePrimitive({
         geometryInstances: lineInstances,
         appearance: new Cesium.PolylineColorAppearance(),
      })
      addCockpitPrimitive(outlinePrimitive)
   }
   return { primitive, outlinePrimitive }
}
const parseGeomJson = geomJson => {
   if (!geomJson) return null
   if (typeof geomJson === 'object') return geomJson
   if (typeof geomJson !== 'string') return null
   const trimmed = geomJson.trim()
   if (!trimmed) return null
   try {
      return JSON.parse(trimmed)
   } catch (error) {}
   return null
}
const normalizeShapePoint = point => {
   if (!point) return null
   const lng = point?.lng ?? point?.longitude
   const lat = point?.lat ?? point?.latitude
   const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
   if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
   return { lng: Number(lng), lat: Number(lat), height }
}
const buildShapePositions = (points = []) => {
   const normalized = points.map(normalizeShapePoint).filter(Boolean)
   return normalized.map(point => Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height))
}
const getShapeDisplayPoints = shape => {
   if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
      return shape.displayPoints
   }
   return shape?.points || []
}
const resolvePartitionShapes = areas => {
   const shapes = []
      ; (areas || []).forEach(area => {
         const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
         extList.forEach((item, index) => {
            const isShapePayload = item?.drawType || item?.points
            const parsed = isShapePayload ? item : parseGeomJson(item?.geomJson)
            if (!parsed) return
            const shape = {
               id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
               drawType: parsed?.drawType ?? 'polygon',
               areaType: parsed?.areaType ?? item?.areaTypeKey ?? item?.areaType ?? '',
               points: Array.isArray(parsed?.points) ? parsed.points : [],
               displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
               meta: parsed?.meta ?? null,
            }
            if (shape.drawType === 'buffer' && shape.meta?.bufferRadii?.length && shape.meta?.center) {
               const center = shape.meta.center
               const centerCartesian = Cesium.Cartesian3.fromDegrees(
                  center.lng,
                  center.lat,
                  center.height || 0
               )
               const radii = shape.meta.bufferRadii
                  .map(radius => Number(radius))
                  .filter(radius => Number.isFinite(radius) && radius > 0)
               radii.forEach((radius, levelIndex) => {
                  const positions = buildEllipsePositions(centerCartesian, radius, radius)
               const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
                  if (positions.length >= 3) {
                     shapes.push({
                        positions,
                        fillColor: style.fill,
                        outlineColor: style.outline,
                     })
                  }
               })
               return
            }
            const positions = buildShapePositions(getShapeDisplayPoints(shape))
            if (positions.length >= 3) {
               const style = getAreaTypeStyle(shape.areaType)
               shapes.push({
                  positions,
                  fillColor: style.fill,
                  outlineColor: style.outline,
               })
            }
         })
      })
   return shapes
}
const renderDefenseZones = zones => {
   if (!viewer) return
   clearDefenseZoneEntities()
@@ -717,7 +1009,8 @@
const renderPartitions = zones => {
   if (!viewer) return
   clearPartitionEntities()
   const result = buildZonePrimitives(zones, '#FFD772', '#FFD772', '#A86B00')
   const shapes = resolvePartitionShapes(zones)
   const result = buildPartitionPrimitives(shapes)
   partitionPrimitive = result.primitive
   partitionOutlinePrimitive = result.outlinePrimitive
   if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
@@ -738,8 +1031,8 @@
const loadPartitions = async () => {
   if (!viewer) return
   try {
      const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
      renderPartitions(res?.data?.data?.records ?? [])
      const res = await fwAreaDivideListApi()
      renderPartitions(res?.data?.data ?? [])
   } catch (error) {
      renderPartitions([])
   }
@@ -860,9 +1153,9 @@
const popupPosition = ref({ x: 0, y: 0 })
const popupVisible = computed(() => Boolean(selectedDevice.value))
const isDronePopup = computed(() => selectedTargetType.value === 'drone')
const toggleDroneFavorite = () => {
const handleDroneFavorite = () => {
   if (!selectedDevice.value) return
   selectedDevice.value.isFavorite = !selectedDevice.value.isFavorite
   emit('droneFavorite', selectedDevice.value)
}
const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
@@ -911,6 +1204,19 @@
   () => props.onlineDevices,
   devices => {
      renderDeviceEntities(devices || [])
   },
   { deep: true }
)
watch(
   () => props.alarmDrones,
   list => {
      renderSimulatedDroneTrack(list || [])
      if (selectedTargetType.value === 'drone' && selectedDevice.value) {
         const selectedId = selectedDevice.value.alarmRecordId ?? selectedDevice.value.id
         const match = (list || []).find(item => (item?.alarmRecordId ?? item?.id) === selectedId)
         if (match) selectedDevice.value = { ...selectedDevice.value, ...match }
      }
   },
   { deep: true }
)
@@ -970,7 +1276,7 @@
   loadPartitions()
   loadAggregation()
   loadCommandPosts()
   renderSimulatedDroneTrack()
   renderSimulatedDroneTrack(props.alarmDrones)
   initDeviceClickHandler()
}