From 4c390d209b45d516fbe2f7552d26048687c83972 Mon Sep 17 00:00:00 2001
From: chenyao <1219716595@qq.com>
Date: Wed, 05 Aug 2026 14:29:25 +0800
Subject: [PATCH] feat:tab切换太快的时候内容不对应
---
applications/drone-command/src/components/map-container/common-cesium-map.vue | 428 +++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 410 insertions(+), 18 deletions(-)
diff --git a/applications/drone-command/src/components/map-container/common-cesium-map.vue b/applications/drone-command/src/components/map-container/common-cesium-map.vue
index eec995b..f1c9be7 100644
--- a/applications/drone-command/src/components/map-container/common-cesium-map.vue
+++ b/applications/drone-command/src/components/map-container/common-cesium-map.vue
@@ -1,11 +1,51 @@
<template>
- <div :id="mapId" class="common-cesium-map"></div>
+ <div :id="mapId" class="common-cesium-map" @contextmenu.prevent>
+ <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>
+ </template>
+ </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 +96,26 @@
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)
+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 = {
+ boundary: 'jaBoundarySource',
+ line: 'jaBoundaryLineSource',
+ label: 'jaBoundaryLabelSource',
+}
let adminBoundarySources = null
let initialized = false
let removeCameraListener = null
@@ -69,7 +123,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 +131,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 +151,146 @@
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
+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, options = {}) => {
+ // clickImgSrc.value = item.url
+ selectedImgIndex.value = index
+ 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, { animate = false, duration = POSITION_MOVE_DURATION } = {}) => {
+ 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) {
+ if (animate) {
+ animatePositionMarker(position, duration)
+ } else {
+ stopPositionAnimation()
+ positionEntity.position = position
+ }
+ } else {
+ positionEntity = viewer.entities.add({
+ position,
+ billboard: {
+ image: positionIcon,
+ width: 36,
+ height: 36,
+ 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,
@@ -115,21 +304,224 @@
})
onBeforeUnmount(() => {
+ stopPositionAnimation()
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, clickWeekTime, 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;
+ box-sizing: border-box;
+ background: rgba(0, 0, 0, 0.42);
+ 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;
+ position: absolute;
+ left: 15%;
+ width: 70%;
+ height: 100%;
+ 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;
+ }
+
+ &.is-dragging .card-item {
+ cursor: grabbing;
+ }
+
+ .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 {
+ margin-top: 44px;
+ }
+
+ .time-line {
+ position: absolute;
+ left: 41px;
+ right: 41px;
+ bottom: 36px;
+ height: 1px;
+ background: rgba(237, 237, 237, 0.6);
+ z-index: 0;
+ pointer-events: none;
+ }
+ }
+ }
}
</style>
--
Gitblit v1.9.3