| | |
| | | <div class="timeline-prefix"> |
| | | <slot name="timeline-prefix"></slot> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-item" v-for="(item, index) in props.list" @click="clickWeekTime(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 |
| | | 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 class="time-bottom">{{ item.createTime.slice(11) }}</div> |
| | | </div> |
| | | |
| | | </div> |
| | | </div> |
| | | <div class="time-line"></div> |
| | | </template> |
| | | </div> |
| | | </template> |
| | |
| | | |
| | | 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 = { |
| | |
| | | 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) |
| | |
| | | // 与航线相同的高度,让标记悬浮在轨迹线上,而不是贴地 |
| | | 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, |
| | |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | stopPositionAnimation() |
| | | if (removeCameraListener) removeCameraListener() |
| | | removeJaAdminBoundary(viewer, adminBoundarySources) |
| | | adminBoundarySources = null |
| | |
| | | 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; |
| | |
| | | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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: 70%; |
| | | // position: fixed; |
| | | left: 15%; |
| | | left: 41px; |
| | | right: 41px; |
| | | bottom: 36px; |
| | | 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> |