吉安感知网项目-前端
chenyao
5 hours ago 4c390d209b45d516fbe2f7552d26048687c83972
applications/drone-command/src/components/map-container/common-cesium-map.vue
@@ -1,26 +1,41 @@
<template>
   <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>
      <template v-if="props.list && props.list.length">
         <div class="card">
            <div class="timeline-prefix">
               <slot name="timeline-prefix"></slot>
            </div>
            <div
               ref="timelineRef"
               class="card-content"
               :class="{ 'is-dragging': timelineDragging }"
               @mousedown="handleTimelineMouseDown"
               @mousemove="handleTimelineMouseMove"
               @mouseup="stopTimelineDrag"
               @mouseleave="stopTimelineDrag"
            >
               <div class="timeline-track">
                  <div class="time-line"></div>
                  <div
                     class="card-item"
                     v-for="(item, index) in props.list"
                     @click="handleTimelineItemClick(item, index)"
                  >
                     <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>
      </div>
      <div class="time-line"></div>
      </template>
   </div>
</template>
@@ -89,6 +104,11 @@
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 timelineRef = ref(null)
const timelineDragging = ref(false)
let timelineStartX = 0
let timelineStartScrollLeft = 0
let timelineDidDrag = false
let viewInstance = null
let viewer = null
const ADMIN_BOUNDARY_NAMES = {
@@ -142,19 +162,89 @@
const selectedImgIndex = ref(0)
// 航线高度(与 TrajectoryDiaLog 里 MOCK_FLIGHT_HEIGHT 保持一致),让标记悬浮在轨迹线上
const FLIGHT_HEIGHT = 120
const POSITION_MOVE_DURATION = 1000
// 当前经纬度位置标记
let positionEntity = null
let positionAnimationFrame = null
// 停止位置图标移动动画
const stopPositionAnimation = () => {
   if (positionAnimationFrame !== null) {
      cancelAnimationFrame(positionAnimationFrame)
      positionAnimationFrame = null
   }
}
// 平滑移动位置图标
const animatePositionMarker = (targetPosition, duration) => {
   const currentPosition = positionEntity?.position?.getValue(viewer.clock.currentTime, new Cesium.Cartesian3())
   if (!currentPosition) {
      positionEntity.position = targetPosition
      return
   }
   stopPositionAnimation()
   const startTime = performance.now()
   const nextPosition = new Cesium.Cartesian3()
   // 更新位置图标移动进度
   const updatePosition = currentTime => {
      const progress = Math.min((currentTime - startTime) / duration, 1)
      Cesium.Cartesian3.lerp(currentPosition, targetPosition, progress, nextPosition)
      positionEntity.position.setValue(nextPosition)
      viewer.scene.requestRender()
      if (progress < 1) {
         positionAnimationFrame = requestAnimationFrame(updatePosition)
      } else {
         positionAnimationFrame = null
      }
   }
   positionAnimationFrame = requestAnimationFrame(updatePosition)
}
// 点击周期
const clickWeekTime = (item, index) => {
const clickWeekTime = (item, index, options = {}) => {
   // clickImgSrc.value = item.url
   selectedImgIndex.value = index
   showPositionMarker(item)
   showPositionMarker(item, options)
   // 通知父组件当前选中的轨迹点,用于右侧表格同步高亮
   emit('point-change', { item, index })
}
// 开始拖动时间轴
const handleTimelineMouseDown = event => {
   if (event.button !== 0) return
   timelineDragging.value = true
   timelineDidDrag = false
   timelineStartX = event.clientX
   timelineStartScrollLeft = timelineRef.value?.scrollLeft ?? 0
}
// 横向拖动时间轴
const handleTimelineMouseMove = event => {
   if (!timelineDragging.value || !timelineRef.value) return
   const offsetX = event.clientX - timelineStartX
   if (Math.abs(offsetX) > 3) timelineDidDrag = true
   if (!timelineDidDrag) return
   timelineRef.value.scrollLeft = timelineStartScrollLeft - offsetX
   event.preventDefault()
}
// 停止拖动时间轴
const stopTimelineDrag = () => {
   timelineDragging.value = false
   setTimeout(() => {
      timelineDidDrag = false
   }, 0)
}
// 点击时间轴节点
const handleTimelineItemClick = (item, index) => {
   if (timelineDidDrag) return
   clickWeekTime(item, index)
}
// 在地图上通过经纬度显示位置图标
const showPositionMarker = item => {
const showPositionMarker = (item, { animate = false, duration = POSITION_MOVE_DURATION } = {}) => {
   if (!viewer) return
   const longitude = Number(item?.longitude)
   const latitude = Number(item?.latitude)
@@ -163,15 +253,19 @@
   // 与航线相同的高度,让标记悬浮在轨迹线上,而不是贴地
   const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, FLIGHT_HEIGHT)
   if (positionEntity) {
      // 已存在则移动到新的位置
      positionEntity.position = position
      if (animate) {
         animatePositionMarker(position, duration)
      } else {
         stopPositionAnimation()
         positionEntity.position = position
      }
   } else {
      positionEntity = viewer.entities.add({
         position,
         billboard: {
            image: positionIcon,
            width: 32,
            height: 40,
            width: 36,
            height: 36,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
         },
@@ -210,6 +304,7 @@
})
onBeforeUnmount(() => {
   stopPositionAnimation()
   if (removeCameraListener) removeCameraListener()
   removeJaAdminBoundary(viewer, adminBoundarySources)
   adminBoundarySources = null
@@ -305,7 +400,7 @@
   })
}
defineExpose({ getMap, getViewer, getPublicCesium, flyToPoint, setAdminBoundaryVisible, zoomToAdminBoundary })
defineExpose({ getMap, getViewer, getPublicCesium, flyToPoint, clickWeekTime, setAdminBoundaryVisible, zoomToAdminBoundary })
</script>
<style scoped lang="scss">
@@ -318,31 +413,63 @@
      bottom: 0px;
      width: 100%;
      height: 67px;
      box-sizing: border-box;
      background: rgba(0, 0, 0, 0.42);
      border-radius: 0px 0px 20px 20px;
      padding-left: 6px;
      // padding-right: 6px;
      // border: 1px solid red;
      border-radius: 0;
      color: #d5d5d5;
      font-size: 14px;
      .timeline-prefix {
         position: absolute;
         left: 7.5%;
         top: 50%;
         display: flex;
         align-items: center;
         justify-content: center;
         z-index: 2;
         transform: translate(-50%, -50%);
      }
      .card-content {
         display: flex;
         justify-content: center;
         position: absolute;
         width: calc(100% - 6px - 6px);
         left: 15%;
         width: 70%;
         height: 100%;
         overflow-x: scroll;
         overflow-x: auto;
         overflow-y: hidden;
         white-space: nowrap;
         cursor: grab;
         scrollbar-width: none;
         &::-webkit-scrollbar {
            display: none;
         }
         &.is-dragging {
            cursor: grabbing;
            user-select: none;
         }
         .timeline-track {
            position: relative;
            display: flex;
            flex: 0 0 auto;
            height: 100%;
            margin: 0 auto;
         }
         .card-item {
            position: relative;
            flex: 0 0 82px;
            width: 82px;
            text-align: center;
            cursor: pointer;
         }
         & > div {
            position: relative;
         &.is-dragging .card-item {
            cursor: grabbing;
         }
         .time-top {
@@ -381,36 +508,20 @@
         }
         .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 {
         .time-line {
            position: absolute;
            // top: 35px;
            width: 100%;
            // position: fixed;
            left: 41px;
            right: 41px;
            bottom: 36px;
            width: 100%;
            border: 1px solid rgba(237, 237, 237, 0.6);
            height: 1px;
            background: rgba(237, 237, 237, 0.6);
            z-index: 0;
            pointer-events: none;
         }
      }
   }
}
</style>