吉安感知网项目-前端
chenyao
2 days ago 7224851bf14cab706d36f695331488941ed4aba6
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -5,33 +5,47 @@
         :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 v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.rightCollapsed }">
         <div class="layer-control-wrap" ref="layerWrapRef">
            <div class="layer-control" @click="toggleLayerPanel">
            <div class="layer-control" @click.stop="toggleLayerPanel">
               <img :src="layerControlIcon" alt="图层控制" />
            </div>
            <div v-if="showLayerPanel" class="layer-panel">
               <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>
            <transition name="layer-panel-slide">
               <div v-if="showLayerPanel" class="layer-panel">
                  <div class="panel-title">图层管理</div>
                  <div class="panel-content">
                     <el-tree ref="layerTreeRef" class="command-tree map-layer-tree" :data="layerTree"
                     icon="el-icon-arrow-down-bold"
                        show-checkbox default-expand-all node-key="key" :props="layerTreeProps"
                        :default-checked-keys="treeCheckedKeys" @check="handleLayerCheck" />
                  </div>
               </div>
               <div class="panel-title">图层管理</div>
               <div class="panel-content">
                  <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>
            </transition>
         </div>
      </div>
      <div v-if="props.showLayerControl" class="base-layer-switch-root" :class="{ collapsed: props.rightCollapsed }">
         <div class="base-layer-switch-wrap" @mouseenter="showBaseLayerPanel = true"
            @mouseleave="showBaseLayerPanel = false">
            <div class="base-layer-trigger">
               <img class="base-layer-trigger-thumb" :src="currentBaseLayer.icon" :alt="currentBaseLayer.label">
               <div class="base-layer-trigger-label">{{ currentBaseLayer.shortLabel }}</div>
            </div>
            <transition name="layer-panel-slide">
               <div v-if="showBaseLayerPanel" class="base-layer-panel">
                  <div class="base-map-options">
                     <div class="base-map-card" :class="{ active: baseLayerKey === 'base-satellite' }"
                        @click="handleBaseLayerSelect('base-satellite')">
                        <img class="base-map-thumb" :src="yxIcon" alt="">
                        <div class="base-map-label">卫星地图</div>
                     </div>
                     <div class="base-map-card" :class="{ active: baseLayerKey === 'base-standard' }"
                        @click="handleBaseLayerSelect('base-standard')">
                        <img class="base-map-thumb" :src="dzIcon" alt="">
                        <div class="base-map-label">标准地图</div>
                     </div>
                  </div>
               </div>
            </transition>
         </div>
      </div>
@@ -45,6 +59,7 @@
</template>
<script setup>
import * as Cesium from 'cesium'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
@@ -77,6 +92,7 @@
const DETAIL_HEIGHT = 10000
const POLYGON_HEIGHT_M = 0.2
const DRONE_TRACK_DURATION_S = 30 * 60
const FIXED_DRONE_TRACK_IDS = ['6000000000609', '6000000000608']
const props = defineProps({
   allDevices: {
@@ -87,7 +103,7 @@
      type: Array,
      default: () => [],
   },
   leftCollapsed: {
   rightCollapsed: {
      type: Boolean,
      default: false,
   },
@@ -131,13 +147,14 @@
let favoritePulseElapsed = 0
const favoritePulseEntities = []
const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
const PULSE_MIN_RADIUS_M = 2
const PULSE_MAX_RADIUS_M = 30
const PULSE_MIN_RADIUS_M = 20
const PULSE_MAX_RADIUS_M = 80
const PULSE_DURATION_S = 1.4
const detailVisible = ref(true)
const clusterVisible = ref(false)
const countyCenterMap = new Map()
const showLayerPanel = ref(false)
const showBaseLayerPanel = ref(false)
const layerWrapRef = ref(null)
const layerTreeRef = ref(null)
const selectedDevice = ref(null)
@@ -152,6 +169,11 @@
const baseLayerKeys = ['base-standard', 'base-satellite']
const defaultCheckedKeys = ['ja-terrain', 'admin', 'city-base']
const baseLayerKey = ref('base-satellite')
const baseLayerMeta = {
   'base-standard': { icon: dzIcon, label: '标准地图', shortLabel: '标准' },
   'base-satellite': { icon: yxIcon, label: '卫星地图', shortLabel: '影像' },
}
const currentBaseLayer = computed(() => baseLayerMeta[baseLayerKey.value] || baseLayerMeta['base-satellite'])
const treeCheckedKeys = ref([...defaultCheckedKeys])
const layerTree = ref([
   {
@@ -216,93 +238,17 @@
   return canvas
}
class PulseSphereMaterialProperty {
   constructor(color, speed = 1.2, bands = 3.0, softness = 0.18) {
      this._definitionChanged = new Cesium.Event()
      this.color = color
      this.speed = speed
      this.bands = bands
      this.softness = softness
   }
   get isConstant() {
      return true
   }
   get definitionChanged() {
      return this._definitionChanged
   }
   getType() {
      return 'PulseSphereMaterial'
   }
   getValue(_time, result = {}) {
      result.color = this.color
      result.time = (performance.now() / 1000) * this.speed
      result.bands = this.bands
      result.softness = this.softness
      return result
   }
   equals(other) {
      return (
         other instanceof PulseSphereMaterialProperty &&
         Cesium.Color.equals(this.color, other.color) &&
         this.speed === other.speed &&
         this.bands === other.bands &&
         this.softness === other.softness
      )
   }
}
let pulseSphereMaterial = null
const getPulseSphereMaterial = () => {
   if (pulseSphereMaterial) return pulseSphereMaterial
   Cesium.Material._materialCache.addMaterial('PulseSphereMaterial', {
      fabric: {
         type: 'PulseSphereMaterial',
         uniforms: {
            color: pulseBaseColor.withAlpha(0.45),
            time: 0,
            bands: 3.0,
            softness: 0.18,
         },
         source: `
            czm_material czm_getMaterial(czm_materialInput materialInput)
            {
               czm_material material = czm_getDefaultMaterial(materialInput);
               vec3 normalEC = normalize(materialInput.normalEC);
               vec3 viewEC = normalize(-materialInput.positionToEyeEC);
               float ndv = max(dot(normalEC, viewEC), 0.0);
               float fresnel = pow(1.0 - ndv, 1.6);
               float wave = sin((ndv * bands + time) * 6.2831853);
               float ring = smoothstep(-softness, softness, wave);
               float alpha = (ring * 0.6 + fresnel * 0.4) * color.a;
               material.diffuse = color.rgb;
               material.alpha = alpha;
               return material;
            }
         `,
      },
      translucent: () => true,
   })
   pulseSphereMaterial = new PulseSphereMaterialProperty(pulseBaseColor.withAlpha(0.45), 1.1, 3.2, 0.22)
   return pulseSphereMaterial
}
const createFavoritePulseEntity = (billboard) => {
   if (!viewer || !billboard) return
   const material = getPulseSphereMaterial()
   const pulse = {
      material,
      offset: Math.random() * PULSE_DURATION_S,
      entity: null,
   }
   const getPulseRadius3D = () => {
      const radius = getPulseRadius(pulse)
      return new Cesium.Cartesian3(radius, radius, radius)
   }
   pulse.entity = viewer.entities.add({
      position: new Cesium.CallbackProperty(() => getBillboardPosition(billboard), false),
      ellipsoid: {
         radii: new Cesium.CallbackProperty(() => getPulseRadius3D(), false),
         material,
      point: {
         pixelSize: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
         color: pulseBaseColor.withAlpha(0.4),
      },
   })
   favoritePulseEntities.push(pulse)
@@ -378,10 +324,10 @@
   if (deviceRingOutlinePrimitives.length) {
      deviceRingOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
   }
   if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection)
   if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection)
   if (droneTrackPolylineCollection) cockpitPrimitiveLayer.add(droneTrackPolylineCollection)
   if (droneTrackBillboardCollection) cockpitPrimitiveLayer.add(droneTrackBillboardCollection)
   if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection)
   if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection)
}
const clearDeviceEntities = () => {
@@ -525,86 +471,86 @@
   const basePoints = [
      [
         {
            longitude: 114.964975,
            latitude: 27.134855,
            longitude: 114.929475,
            latitude: 27.136575,
            height: 200
         },
         {
            longitude: 114.963966,
            latitude: 27.132836,
            longitude: 114.928466,
            latitude: 27.134556,
            height: 200
         },
         {
            longitude: 114.963966,
            latitude: 27.132836,
            longitude: 114.928466,
            latitude: 27.134556,
            height: 320
         },
         {
            longitude: 114.963974,
            latitude:  27.132677,
            longitude: 114.928474,
            latitude: 27.134397,
            height: 320
         },
         {
            longitude: 114.963432,
            latitude: 27.131665,
            longitude: 114.927932,
            latitude: 27.133385,
            height: 320
         },
         {
            longitude: 114.962865,
            latitude: 27.130377,
            longitude: 114.927365,
            latitude: 27.132097,
            height: 320
         },
         {
            longitude: 114.965318,
            latitude: 27.129925,
            longitude: 114.929818,
            latitude: 27.131645,
            height: 320
         },
         {
            longitude: 114.960144,
            latitude: 27.134902,
            longitude: 114.924644,
            latitude: 27.136622,
            height: 120
         },
      ],
      [
         {
            longitude: 114.931063,
            latitude: 27.095052,
            longitude: 114.936386,
            latitude: 26.825890,
            height: 120
         },
         {
            longitude: 114.928653,
            latitude: 27.096307,
            longitude: 114.933976,
            latitude: 26.827145,
            height: 120
         },
         {
            longitude: 114.925958,
            latitude: 27.096963,
            longitude: 114.931281,
            latitude: 26.827801,
            height: 120
         },
         {
            longitude: 114.925958,
            latitude: 27.096963,
            longitude: 114.931281,
            latitude: 26.827801,
            height: 400
         },
         {
            longitude: 114.925836,
            latitude: 27.096102,
            longitude: 114.931159,
            latitude: 26.826940,
            height: 400
         },
         {
            longitude: 114.928384,
            latitude: 27.095866,
            longitude: 114.933707,
            latitude: 26.826704,
            height: 400
         },
         {
            longitude: 114.929874,
            latitude: 27.096095,
            longitude: 114.935197,
            latitude: 26.826933,
            height: 400
         },
         {
            longitude: 114.931048,
            latitude: 27.097666,
            longitude: 114.936371,
            latitude: 26.828504,
            height: 400
         },
      ],
@@ -704,11 +650,11 @@
      const pos = isEntityPosition
         ? getBillboardPosition(track.billboard)
         : Cesium.Cartesian3.lerp(
               track.positions[seg],
               track.positions[seg + 1],
               ratio,
               new Cesium.Cartesian3()
           )
            track.positions[seg],
            track.positions[seg + 1],
            ratio,
            new Cesium.Cartesian3()
         )
      if (!pos) return
      if (!isEntityPosition) {
         track.billboard.position = pos
@@ -794,7 +740,9 @@
   droneTrackPolylineCollection.show = detailVisible.value
   droneTrackRuntime = []
   const baseTrackColor = Cesium.Color.fromCssColorString('red')
      ; (list || []).forEach((item, trackIndex) => {
      ; (FIXED_DRONE_TRACK_IDS || []).forEach((trackId, trackIndex) => {
         const item = (list || []).find(entry => String(entry?.id) === String(trackId))
         if (!item) return
         const position = getDevicePosition(item)
         if (!position) return
         const points = buildSimulatedTrackPoints({ ...position, height: item.flightHeightM, trackIndex })
@@ -818,7 +766,7 @@
            width: 3,
            material: trackMaterial,
         })
         const droneId = `drone-alarm-${item?.alarmRecordId ?? item?.id ?? trackIndex}`
         const droneId = `drone-alarm-${trackId}`
         const startTime = viewer.clock.startTime
         const stopTime = viewer.clock.stopTime
         const positionProperty = new Cesium.SampledPositionProperty()
@@ -853,7 +801,7 @@
            },
         })
         const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
         dronePickMap.set(droneId, {
      dronePickMap.set(droneId, {
            data: {
               ...item,
               flightHeightM: item.flightHeightM ?? points[0].height,
@@ -921,6 +869,7 @@
            width: 40,
            height: 56,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
         })
         billboard.id = entry.entityId
         devicePickMap.set(entry.entityId, { data: entry.data, billboard })
@@ -1049,7 +998,7 @@
   if (!trimmed) return null
   try {
      return JSON.parse(trimmed)
   } catch (error) {}
   } catch (error) { }
   return null
}
@@ -1102,7 +1051,7 @@
                  .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]
                  const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
                  if (positions.length >= 3) {
                     shapes.push({
                        positions,
@@ -1310,6 +1259,7 @@
            width: 40,
            height: 56,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
         })
      })
   }
@@ -1361,9 +1311,11 @@
)
watch(
   () => props.leftCollapsed,
   () => props.rightCollapsed,
   isCollapsed => {
      if (isCollapsed) showLayerPanel.value = false
      if (!isCollapsed) return
      showLayerPanel.value = false
      showBaseLayerPanel.value = false
   }
)
@@ -1402,7 +1354,7 @@
   setDroneVisibility(!showCluster)
}
const handleMapReady = ({ viewer: mapViewer, publicCesium: mapPublic }) => {
const handleMapReady = async ({ viewer: mapViewer, publicCesium: mapPublic }) => {
   if (mapReadyHandled) return
   mapReadyHandled = true
   viewer = mapViewer
@@ -1412,10 +1364,10 @@
   const height = viewer?.camera?.positionCartographic?.height
   const stage = getStageByHeight(height)
   updateStageDisplay(stage)
   await loadCommandPosts()
   renderDeviceEntities(props.allDevices)
   loadPartitions()
   loadAggregation()
   loadCommandPosts()
   renderSimulatedDroneTrack(props.alarmDrones)
   initDeviceClickHandler()
}
@@ -1424,7 +1376,7 @@
   updateStageDisplay(stage)
}
const handleClickOutside = event => {
const handleLayerClickOutside = event => {
   if (!showLayerPanel.value) return
   const target = event.target
   if (layerWrapRef.value?.contains(target)) return
@@ -1432,13 +1384,13 @@
}
onMounted(() => {
   document.addEventListener('click', handleClickOutside)
   document.addEventListener('click', handleLayerClickOutside)
   const map = mapRef.value?.getMap()
   if (map?.viewer) handleMapReady(map)
})
onBeforeUnmount(() => {
   document.removeEventListener('click', handleClickOutside)
   document.removeEventListener('click', handleLayerClickOutside)
   clearDeviceEntities()
   clearPartitionEntities()
   clearAggregationEntities()
@@ -1452,6 +1404,20 @@
   cockpitPrimitiveLayer = null
   viewer = null
   publicCesium = null
})
const getMap = () => {
   return mapRef.value?.getMap()
}
// 暴露给父组件调用
const flyTo = (options) => {
   mapRef.value?.flyToPoint(options)
}
defineExpose({
   getMap,
   flyTo
})
</script>
@@ -1472,14 +1438,14 @@
.layer-control-root {
   position: absolute;
   left: 337px;
   bottom: 22px;
   z-index: 9;
   right: 337px;
   top: 95px;
   z-index: 11;
   transition: transform 0.3s ease-in-out;
   pointer-events: none;
   &.collapsed {
      transform: translateX(-317px);
      transform: translateX(317px);
   }
}
@@ -1506,12 +1472,13 @@
   display: flex;
   flex-direction: column;
   position: absolute;
   left: 66px;
   bottom: 0;
   right: 66px;
   top: 0;
   width: 160px;
   max-height: 442px;
   background: #191932;
   border-radius: 8px 8px 8px 8px;
   z-index: 99;
   .panel-title {
      padding: 0 16px;
@@ -1534,50 +1501,136 @@
      overflow: auto;
   }
   .base-map-title {
      margin-bottom: 8px;
      font-size: 12px;
      color: #ffffff;
      font-weight: 600;
   }
}
   .base-map-options {
      padding: 16px;
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 8px;
   }
.base-layer-switch-root {
   position: absolute;
   right: 337px;
   bottom: 78px;
   z-index: 11;
   transition: transform 0.3s ease-in-out;
   pointer-events: none;
   .base-map-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 6px;
   &.collapsed {
      transform: translateX(317px);
   }
}
.base-layer-switch-wrap {
   position: relative;
   pointer-events: auto;
}
.base-layer-trigger {
   width: 55px;
   padding: 8px 0;
   border-radius: 8px;
   background: #191932;
   display: flex;
   flex-direction: column;
   align-items: center;
   gap: 6px;
   cursor: pointer;
}
.base-layer-trigger-thumb {
   width: 36px;
   height: 36px;
   border-radius: 6px;
   display: block;
   // border: 2px solid #fff;
   background-size: cover;
   background-position: center;
   box-sizing: border-box;
}
.base-layer-trigger-label {
   margin-top: 8px;
   font-size: 12px;
   line-height: 1;
   color: #ffffff;
   font-weight: 700;
}
.base-layer-panel {
   position: absolute;
   right: calc(100% + 10px);
   bottom: 0;
   margin-bottom: 0;
   width: 160px;
   background: #191932;
   border-radius: 8px;
   z-index: 99;
   &::after {
      content: '';
      position: absolute;
      right: -10px;
      top: 0;
      width: 10px;
      height: 100%;
      background: transparent;
   }
}
.base-map-options {
   padding: 8px;
   display: grid;
   grid-template-columns: repeat(2, minmax(0, 1fr));
   gap: 8px;
}
.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: 36px;
      height: 36px;
      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;
         }
         .base-map-label {
            color: #2ea8ff;
         }
      }
      // border: 2px solid #fff;
      background-size: cover;
      background-position: center;
      box-sizing: border-box;
   }
   .base-map-label {
      margin-top: 8px;
      font-size: 12px;
      line-height: 1;
      color: #ffffff;
      font-weight: 700;
   }
   // &.active {
   //    .base-map-thumb {
   //       border: 2px solid #2ea8ff;
   //    }
   //    .base-map-label {
   //       color: #2ea8ff;
   //    }
   // }
}
.layer-panel-slide-enter-active,
.layer-panel-slide-leave-active {
   transition: transform 0.24s ease, opacity 0.24s ease;
   transform-origin: right center;
}
.layer-panel-slide-enter-from,
.layer-panel-slide-leave-to {
   transform: translateX(24px);
   opacity: 0;
}
</style>