吉安感知网项目-前端
罗广辉
2026-08-13 1d552a3bad95caae4af15322df28e6240b797693
applications/drone-command/src/views/recordManage/historyTracks/TrajectoryDiaLog.vue
@@ -1,29 +1,97 @@
<template>
   <el-dialog v-model="visible" :title="dialogTitle" @closed="handleClosed" destroy-on-close>
      <div>
         <div>无人机名称:{{ formData.droneName }}</div>
         <div>序列号: {{ formData.serialNo }}</div>
         <div>告警时间: {{ formData.alarmTime }}</div>
         <div>触发原因: {{ formData.triggerType }}</div>
         <div>停留时长: {{ formData.stayDuration }}</div>
<template>
   <el-dialog
      class="command-page-map-view-dialog"
      v-model="visible"
      :show-close="false"
      :close-on-click-modal="false"
      @closed="handleClosed"
      destroy-on-close
   >
      <div class="dialog-container">
         <div class="left-container">
            <CommonCesiumMap
               ref="mapRef"
               class="leftMap command-cesium"
               :active="visible"
               :flat-mode="false"
               :terrain="true"
               :layer-mode="4"
               :boundary="false"
               :list="list"
               @point-change="handlePointChange"
            >
               <template #timeline-prefix>
                  <el-icon class="track-play-btn" :size="30" @click.stop="handlePlayClick">
                     <VideoPause v-if="isPlaying" />
                     <VideoPlay v-else />
                  </el-icon>
               </template>
            </CommonCesiumMap>
         </div>
         <div class="right-container">
            <div class="header">
               <span>{{ dialogTitle }}</span>
               <el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
            </div>
            <div class="content">
               <div class="detail-title">轨迹信息</div>
               <el-row>
                  <el-col :span="24">
                     <div class="label">无人机名称</div>
                     <div class="val">{{ formData.droneName }}</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">无人机序列号</div>
                     <div class="val">{{ formData.serialNo }}</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">告警时间</div>
                     <div class="val">{{ formData.alarmTime }}</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">触发原因</div>
                     <div class="val">{{ formData.triggerType }}</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">停留时长</div>
                     <div class="val">{{ formData.stayDuration }}</div>
                  </el-col>
               </el-row>
               <div class="detail-title">轨迹点</div>
               <div class="command-table-container">
                  <div class="command-table-content">
                     <el-table ref="pointTableRef" class="command-table" :data="list" :row-class-name="getRowClassName">
                        <el-table-column type="index" width="60" label="序号" />
                        <el-table-column label="经纬度">
                           <template #default="{ row }">{{ row.longitude }},{{ row.latitude }}</template>
                        </el-table-column>
                        <el-table-column prop="createTime" label="时间" />
                     </el-table>
                  </div>
               </div>
            </div>
            <div class="footer">
               <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">
                  {{ dialogReadonly ? '关闭' : '取消' }}
               </el-button>
            </div>
         </div>
      </div>
      <el-table :data="list">
         <el-table-column type="index" width="60" label="序号" />
         <el-table-column prop="areaCode" label="areaCode" />
         <el-table-column prop="createTime" label="createTime" />
         <el-table-column prop="latitude" label="latitude" />
         <el-table-column prop="longitude" label="longitude" />
      </el-table>
      <template #footer>
         <el-button @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
      </template>
   </el-dialog>
</template>
<script setup>
import { computed, ref } from 'vue'
import { Close, VideoPause, VideoPlay } from '@element-plus/icons-vue'
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { fwDroneFlightRecordDetailApi } from '@/views/recordManage/historyTracks/fwDroneFlightRecord'
import { fwDroneFlightRecordDetailPageApi } from '@/views/recordManage/historyTracks/flyTrajectory'
import * as Cesium from 'cesium'
import { ArrowLineMaterialProperty } from '@/utils/cesium/Material'
import droneIcon from '@/assets/images/dataCockpit/map/drone.png'
const initForm = () => ({
   droneName: '',
@@ -33,11 +101,11 @@
   stayDuration: '',
})
let list = ref([])
const emit = defineEmits(['success'])
const formData = ref(initForm()) // 表单数据
const visible = ref(false) // 弹框显隐
const dialogMode = ref('add') // 弹框模式
const dialogReadonly = computed(() => dialogMode.value === 'view')
const mapRef = ref(null)
const dialogTitle = computed(() => {
   if (dialogMode.value === 'edit') {
      return '编辑'
@@ -59,18 +127,153 @@
   const res = await fwDroneFlightRecordDetailApi({ id: formData.value.id })
   const res1 = await fwDroneFlightRecordDetailPageApi({ flightRecordId: formData.value.id })
   list.value = res1.data.data.records ?? []
   drawFlightPath()
   formData.value = res?.data?.data ?? {}
}
let arrowLineMaterialProperty = new ArrowLineMaterialProperty({
   color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1),
   directionColor: new Cesium.Color(1, 1, 1, 1),
   outlineColor: new Cesium.Color(1, 1, 1, 1),
   outlineWidth: 0,
   speed: 5,
})
const MOCK_FLIGHT_HEIGHT = 120
// 角度转c3
function degreesToC3({ longitude, latitude, height = 0 }) {
   return Cesium.Cartesian3.fromDegrees(longitude, latitude, height)
}
// 渲染航线
function drawFlightPath() {
   const positions = list.value.map(p => degreesToC3({ ...p, height: MOCK_FLIGHT_HEIGHT }))
   if (!positions.length) return
   droneLineEntity && geometricSource.entities.remove(droneLineEntity)
   droneLineEntity = geometricSource.entities.add({
      polyline: {
         width: 4,
         positions: positions,
         material: arrowLineMaterialProperty,
         clampToGround: false,
      },
   })
   startDroneEntity && geometricSource.entities.remove(startDroneEntity)
   startDroneEntity = geometricSource.entities.add({
      position: positions[positions.length - 1],
      billboard: {
         image: droneIcon,
         width: 40,
         height: 40,
         verticalOrigin: Cesium.VerticalOrigin.CENTER,
         heightReference: Cesium.HeightReference.NONE,
      },
   })
   viewer.flyTo(droneLineEntity, { duration: 0 })
}
// 关闭后重置
function handleClosed() {
   stopTrackPlay()
   formData.value = initForm()
   list.value = []
   selectedPointIndex.value = 0
   viewer = null
   geometricSource = null
   droneLineEntity = null
   startDroneEntity = null
}
// 当前选中的轨迹点索引(与地图时间轴联动)
const selectedPointIndex = ref(0)
const pointTableRef = ref(null)
const isPlaying = ref(false)
const PLAY_INTERVAL = 1000
let playTimer = null
let playIndex = 0
// 停止轨迹播放
function stopTrackPlay() {
   if (playTimer) {
      clearTimeout(playTimer)
      playTimer = null
   }
   isPlaying.value = false
}
// 播放下一个轨迹点
function playNextPoint() {
   if (!list.value.length || playIndex >= list.value.length) {
      stopTrackPlay()
      return
   }
   mapRef.value?.clickWeekTime?.(list.value[playIndex], playIndex, {
      animate: true,
      duration: PLAY_INTERVAL,
   })
   playIndex += 1
   playTimer = setTimeout(() => {
      playTimer = null
      if (playIndex >= list.value.length) {
         stopTrackPlay()
         return
      }
      playNextPoint()
   }, PLAY_INTERVAL)
}
// 切换轨迹播放状态
function handlePlayClick() {
   if (isPlaying.value) {
      stopTrackPlay()
      return
   }
   if (!list.value.length) return
   mapRef.value?.clickWeekTime?.(list.value[0], 0)
   if (list.value.length === 1) return
   playIndex = 1
   isPlaying.value = true
   playNextPoint()
}
// 地图上点击轨迹点后,同步高亮表格对应行,并滚动到该行
function handlePointChange({ index } = {}) {
   if (typeof index !== 'number') return
   selectedPointIndex.value = index
   scrollToSelectedRow()
}
// 表格行较多时自动滚动到选中行
async function scrollToSelectedRow() {
   await nextTick()
   const rowEl = pointTableRef.value?.$el?.querySelector('.selected-row')
   rowEl?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
}
function getRowClassName({ row, rowIndex }) {
   const classNames = []
   if (row?.isAlarm === 1) classNames.push('alarm-row')
   if (rowIndex === selectedPointIndex.value) classNames.push('selected-row')
   return classNames.join(' ')
}
let viewer, geometricSource, droneLineEntity, startDroneEntity
function initMap() {
   if (viewer) return
   const map = mapRef.value?.getMap?.()
   if (!map?.viewer) return
   viewer = map.viewer
   geometricSource = new Cesium.CustomDataSource('geometricSource')
   viewer.dataSources.add(geometricSource)
}
// 打开弹框
async function open({ mode, row } = {}) {
   stopTrackPlay()
   dialogMode.value = mode || 'add'
   visible.value = true
   await nextTick()
   initMap()
   if (dialogMode.value === 'add') {
      formData.value = initForm()
   } else {
@@ -80,4 +283,46 @@
}
defineExpose({ open })
// 卸载时清理播放定时器
onBeforeUnmount(() => {
   stopTrackPlay()
})
</script>
<style scoped lang="scss">
.left-container {
   min-width: 0;
   overflow: hidden;
}
.right-container {
   position: relative;
   flex-shrink: 0;
   z-index: 1;
   background: #1a1a2a;
}
:deep(.alarm-row) {
   background: #2b2b4c !important;
   td {
      background-color: transparent !important;
   }
}
// 与地图时间轴联动的选中行高亮
:deep(.selected-row) {
   background: #14335f !important;
   td {
      background-color: transparent !important;
      color: #4faaff !important;
   }
}
.track-play-btn {
   color: #ffffff;
   cursor: pointer;
}
</style>