吉安感知网项目-前端
chenyao
2026-07-08 2bdea5ee8c220ffbd7edfdf5f053b99501a52b10
applications/drone-command/src/components/map-container/common-cesium-map.vue
@@ -1,11 +1,36 @@
<template>
   <div :id="mapId" class="common-cesium-map"></div>
   <div :id="mapId" class="common-cesium-map" @contextmenu.prevent>
      <div class="card">
         <div class="card-content">
            <div class="card-item" v-for="(item, index) in props.list" @click="clickWeekTime(item, index)">
               <!-- <div class="time-top" :class="index === selectedImgIndex ? 'active' : ''">
                  {{ item.createTime }}
               </div> -->
               <div class="time-point">
                  <img
                     class="active"
                     v-if="index === selectedImgIndex"
                     src="@/assets/images/common/point-active.png"
                     alt=""
                  />
                  <img v-else src="@/assets/images/common/point.png" alt="" />
               </div>
               <div class="time-bottom">{{ item.createTime.slice(11) }}</div>
            </div>
         </div>
      </div>
      <div class="time-line"></div>
   </div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import * as Cesium from 'cesium'
import { onBeforeUnmount, onMounted, watch } from 'vue'
import { PublicCesium } from '@/utils/cesium/publicCesium'
import { loadJaAdminBoundary, removeJaAdminBoundary } from '@/utils/cesium/adminBoundary'
import positionIcon from '@/assets/images/dataCenter/positionicon.png'
const props = defineProps({
   active: {
@@ -56,12 +81,21 @@
      type: Number,
      default: 10000,
   },
   list: {
      type: Array,
      default: () => [],
   },
})
const emit = defineEmits(['ready', 'height-change', 'stage-change'])
const emit = defineEmits(['ready', 'height-change', 'stage-change', 'point-change'])
const mapId = props.domId || `common-cesium-map-${Math.random().toString(36).slice(2, 10)}`
const viewInstance = ref(null)
const viewer = ref(null)
let viewInstance = null
let viewer = null
const ADMIN_BOUNDARY_NAMES = {
   boundary: 'jaBoundarySource',
   line: 'jaBoundaryLineSource',
   label: 'jaBoundaryLabelSource',
}
let adminBoundarySources = null
let initialized = false
let removeCameraListener = null
@@ -69,7 +103,7 @@
const initMap = async () => {
   if (initialized) return
   initialized = true
   viewInstance.value = new PublicCesium({
   viewInstance = new PublicCesium({
      dom: mapId,
      flatMode: props.flatMode,
      terrain: props.terrain,
@@ -77,16 +111,16 @@
      boundary: props.boundary,
      contour: props.contour,
   })
   viewer.value = viewInstance.value.getViewer()
   viewer = viewInstance.getViewer()
   if (props.showAdminBoundary) {
      adminBoundarySources = await loadJaAdminBoundary(viewer.value, {
      adminBoundarySources = await loadJaAdminBoundary(viewer, {
         zoomTo: props.zoomToBoundary,
      })
   }
   if (props.enableStageEmit) {
      let lastStage = ''
      const onCameraChanged = () => {
         const height = viewer.value?.camera?.positionCartographic?.height
         const height = viewer?.camera?.positionCartographic?.height
         if (height == null) return
         emit('height-change', height)
         let stage = 'mid'
@@ -97,11 +131,72 @@
            emit('stage-change', stage)
         }
      }
      viewer.value?.camera?.changed?.addEventListener(onCameraChanged)
      removeCameraListener = () => viewer.value?.camera?.changed?.removeEventListener(onCameraChanged)
      viewer?.camera?.changed?.addEventListener(onCameraChanged)
      removeCameraListener = () => viewer?.camera?.changed?.removeEventListener(onCameraChanged)
   }
   emit('ready', { viewer: viewer.value, publicCesium: viewInstance.value })
   emit('ready', { viewer, publicCesium: viewInstance })
   // 地图就绪时,若列表已加载则默认标记选中项(首个)
   showSelectedMarker()
}
const selectedImgIndex = ref(0)
// 航线高度(与 TrajectoryDiaLog 里 MOCK_FLIGHT_HEIGHT 保持一致),让标记悬浮在轨迹线上
const FLIGHT_HEIGHT = 120
// 当前经纬度位置标记
let positionEntity = null
// 点击周期
const clickWeekTime = (item, index) => {
   // clickImgSrc.value = item.url
   selectedImgIndex.value = index
   showPositionMarker(item)
   // 通知父组件当前选中的轨迹点,用于右侧表格同步高亮
   emit('point-change', { item, index })
}
// 在地图上通过经纬度显示位置图标
const showPositionMarker = item => {
   if (!viewer) return
   const longitude = Number(item?.longitude)
   const latitude = Number(item?.latitude)
   if (Number.isNaN(longitude) || Number.isNaN(latitude)) return
   // 与航线相同的高度,让标记悬浮在轨迹线上,而不是贴地
   const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, FLIGHT_HEIGHT)
   if (positionEntity) {
      // 已存在则移动到新的位置
      positionEntity.position = position
   } else {
      positionEntity = viewer.entities.add({
         position,
         billboard: {
            image: positionIcon,
            width: 32,
            height: 40,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
         },
      })
   }
}
// 显示当前选中项的标记(默认首个),地图就绪和列表加载后都会调用
const showSelectedMarker = () => {
   const item = props.list?.[selectedImgIndex.value]
   if (item) showPositionMarker(item)
}
// 列表数据到位后,默认选中并标记首个点
watch(
   () => props.list,
   list => {
      if (list && list.length) {
         selectedImgIndex.value = 0
         showSelectedMarker()
         // 默认选中首个,同步通知父组件高亮表格首行
         emit('point-change', { item: list[0], index: 0 })
      }
   }
)
watch(
   () => props.active,
@@ -116,20 +211,206 @@
onBeforeUnmount(() => {
   if (removeCameraListener) removeCameraListener()
   removeJaAdminBoundary(viewer.value, adminBoundarySources)
   viewInstance.value?.viewerDestroy?.()
   removeJaAdminBoundary(viewer, adminBoundarySources)
   adminBoundarySources = null
   positionEntity = null
   viewInstance?.viewerDestroy?.()
})
const getMap = () => ({ viewer: viewer.value, publicCesium: viewInstance.value })
const getViewer = () => viewer.value
const getPublicCesium = () => viewInstance.value
const setAdminBoundaryVisible = async visible => {
   if (!viewer) return
   const removeByNames = () => {
      Object.values(ADMIN_BOUNDARY_NAMES).forEach(name => {
         const sources = viewer.dataSources.getByName(name) || []
         sources.forEach(source => viewer.dataSources.remove(source))
      })
   }
   const resolveSources = () => {
      if (adminBoundarySources) return adminBoundarySources
      const boundarySource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.boundary)?.[0] ?? null
      const lineSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.line)?.[0] ?? null
      const labelSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.label)?.[0] ?? null
      if (boundarySource || lineSource || labelSource) {
         adminBoundarySources = { boundarySource, lineSource, labelSource }
      }
      return adminBoundarySources
   }
   const setVisible = (source, next) => {
      if (!source) return
      source.show = next
      source.entities?.values?.forEach(entity => {
         entity.show = next
      })
   }
   const sources = resolveSources()
   if (!visible) {
      if (sources) {
         removeJaAdminBoundary(viewer, sources)
         adminBoundarySources = null
      }
      removeByNames()
      return
   }
   if (sources) {
      const { boundarySource, lineSource, labelSource } = sources
      setVisible(boundarySource, true)
      setVisible(lineSource, true)
      setVisible(labelSource, true)
      return
   }
   adminBoundarySources = await loadJaAdminBoundary(viewer, {
      zoomTo: false,
   })
}
defineExpose({ getMap, getViewer, getPublicCesium })
const zoomToAdminBoundary = async () => {
   if (!viewer) return
   const resolveSources = () => {
      if (adminBoundarySources) return adminBoundarySources
      const boundarySource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.boundary)?.[0] ?? null
      const lineSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.line)?.[0] ?? null
      const labelSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.label)?.[0] ?? null
      if (boundarySource || lineSource || labelSource) {
         adminBoundarySources = { boundarySource, lineSource, labelSource }
      }
      return adminBoundarySources
   }
   let sources = resolveSources()
   if (!sources?.boundarySource) {
      sources = await loadJaAdminBoundary(viewer, { zoomTo: false })
      adminBoundarySources = sources
   }
   const target = sources?.boundarySource
   if (!target) return
   await viewer.flyTo(target, {
      duration: 0,
      offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-75), 0),
   })
}
const getMap = () => ({ viewer, publicCesium: viewInstance })
const getViewer = () => viewer
const getPublicCesium = () => viewInstance
const flyToPoint = (options) => {
   const { longitude, latitude, height } = options.position
   viewer.camera.setView({
      destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
      orientation: {
         heading: Cesium.Math.toRadians(0), // 方向
         pitch: Cesium.Math.toRadians(-90), // 仰角
         roll: Cesium.Math.toRadians(0) // 翻滚角
      }
   })
}
defineExpose({ getMap, getViewer, getPublicCesium, flyToPoint, setAdminBoundaryVisible, zoomToAdminBoundary })
</script>
<style scoped lang="scss">
.common-cesium-map {
   width: 100%;
   height: 100%;
   position: relative;
   .card {
      position: absolute;
      bottom: 0px;
      width: 100%;
      height: 67px;
      background: rgba(0, 0, 0, 0.42);
      border-radius: 0px 0px 20px 20px;
      padding-left: 6px;
      // padding-right: 6px;
      // border: 1px solid red;
      color: #d5d5d5;
      font-size: 14px;
      .card-content {
         display: flex;
         justify-content: center;
         position: absolute;
         width: calc(100% - 6px - 6px);
         height: 100%;
         overflow-x: scroll;
         overflow-y: hidden;
         white-space: nowrap;
         .card-item {
            cursor: pointer;
         }
         & > div {
            position: relative;
         }
         .time-top {
            margin-top: 10px;
            line-height: 1;
            text-align: center;
            cursor: pointer;
         }
         .active {
            font-weight: 400;
            color: #ffffff;
         }
         .time-point {
            display: flex;
            align-items: center;
            justify-content: center;
            position: absolute;
            top: 22px;
            left: 50%;
            width: 18px;
            height: 18px;
            z-index: 1;
            transform: translate(-50%, 0);
            img {
               width: 11.2px;
               height: 11.2px;
            }
            img.active {
               width: 16.89px;
               height: 16.89px;
            }
         }
         .time-bottom {
            // border: 1px solid red;
            margin-top: 44px;
            margin-left: 8px;
            &:first-child {
               margin-left: 0;
            }
         }
         // .time-line {
         //    // position: absolute;
         //    // top: 35px;
         //    // width: 100%;
         //    position: fixed;
         //    bottom: 96px;
         //    width: 94%;
         //    border: 1px solid rgba(237, 237, 237, 0.6);
         //    z-index: 0;
         // }
      }
   }
   .time-line {
            position: absolute;
            // top: 35px;
            width: 100%;
            // position: fixed;
            bottom: 36px;
            width: 100%;
            border: 1px solid rgba(237, 237, 237, 0.6);
            z-index: 0;
         }
}
</style>