Files
dyf-vue-ui/src/views/controlCenter/historyjectory/trackplayback.vue

450 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="track-player">
<!-- 地图容器 -->
<div id="amap-container" class="map-container"></div>
<div class="content_top">
<div class="content_layout">
<div v-if="deviceGroups && deviceGroups.length > 0">
<el-timeline style="margin-left: -50px;">
<!-- 日期分组循环 -->
<el-timeline-item v-for="(group, groupIndex) in deviceGroups" :key="groupIndex"
:timestamp="group.date" placement="top" icon="Time" color="rgba(0, 198, 250, 1)">
<!-- 设备项循环 -->
<div class="device-item">
<!-- 设备名称 + 时间 -->
<div class="device-header">
<span class="device-name">{{ group.deviceName }}</span>
<span class="device-time">{{ formatTimestampToHM(group.detailList[0].timestamp) }}</span>
</div>
<!-- 地点信息 -->
<div class="device-place">
<div class="d_f">
<div class="Stime">初始地点</div>
<div class="Sloation">{{
group.startLocation }}</div>
</div>
<div class="d_f">
<div class="Stime">结束地点</div>
<div class="Sloation">{{
group.endLocation }}</div>
</div>
</div>
<!-- 轨迹播放按钮 -->
<div class="control_btt">
<el-button class="control_btn" @click="handlePlayPause(group)">轨迹播放</el-button>
</div>
</div>
</el-timeline-item>
</el-timeline>
</div>
<div v-if="deviceGroups == null" class="nodata">
<img src="@/assets/images/nodata.png" alt="" class="nodataImg">
<div class="title">暂无数据</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import api from "@/api/controlCenter/historyjectory/index"
import { formatTimestampToHM } from "@/utils/function"
const route = useRoute();
// -------------------------- 状态管理 --------------------------
// 地图实例
const mapRef = ref<AMap.Map | null>(null);
// 轨迹线实例
const polylineRef = ref<AMap.Polyline | null>(null);
// 移动标记点实例
const markerRef = ref<AMap.Marker | null>(null);
// 轨迹数据(初始为空,从设备列表动态获取)
const trackPoints = ref<[number, number][]>([]); // 移除硬编码默认值
// 设备分组数据(包含各设备的轨迹点)
const deviceGroups = ref([])
// 播放状态
const isPlaying = ref<boolean>(false);
const isLoading = ref<boolean>(true);
const playSpeed = ref<number>(1);
const currentIndex = ref<number>(0);
const playTimer = ref<number | null>(null);
// -------------------------- 地图初始化 --------------------------
const initMap = () => {
try {
if (!window.AMap) {
throw new Error('高德地图API加载失败');
}
// 创建地图实例:不设置 center 和 zoom由 setBounds 自动调整
const map = new AMap.Map('amap-container', {
resizeEnable: true, // 允许地图自适应容器大小
// 移除 center、zoom 等固定视角的配置
});
mapRef.value = map;
// 创建轨迹线(初始无数据)
const polyline = new AMap.Polyline({
path: [],
strokeColor: '#3886ff',
strokeWeight: 10,
strokeOpacity: 0.8,
});
map.add(polyline);
polylineRef.value = polyline;
// 创建标记点(初始位置临时设为 [0, 0],播放时会覆盖)
const marker = new AMap.Marker({
position: [0, 0],
icon: new AMap.Icon({
size: new AMap.Size(40, 40),
image: 'https://webapi.amap.com/images/car.png',
imageSize: new AMap.Size(40, 40),
}),
anchor: 'center',
});
map.add(marker);
markerRef.value = marker;
isLoading.value = false;
fetchTrackData(); // 数据请求移到地图初始化后
} catch (error) {
console.error('地图初始化失败:', error);
isLoading.value = false;
}
};
// 单独的接口请求函数
const fetchTrackData = () => {
const data = {
id: route.params.id as string
};
api.getLocationHistoryDetail(data).then((res) => {
if (res.code === 200) {
deviceGroups.value = res.data;
// 数据加载完成后,自动播放第一个设备的轨迹
if (deviceGroups.value&&deviceGroups.value.length > 0) {
const firstGroup = deviceGroups.value[0];
const firstDetailList = firstGroup.detailList;
// 检查轨迹列表是否有数据
if (firstDetailList && firstDetailList.length > 0) {
const firstPoint = firstDetailList[0];
const centerLng = Number(firstPoint.longitude);
const centerLat = Number(firstPoint.latitude);
// 验证经纬度有效性
if (!isNaN(centerLng) && !isNaN(centerLat) && mapRef.value) {
// 设置地图中心为第一个轨迹点
mapRef.value.setCenter([centerLng, centerLat]);
// 设置合适的缩放级别16为街道级清晰无需手动放大
mapRef.value.setZoom(10);
}
}
}
} else {
ElMessage.error('获取轨迹数据失败');
}
})
};
// -------------------------- 轨迹播放核心逻辑 --------------------------
// 平滑移动标记点
const smoothMoveMarker = (
start: [number, number],
end: [number, number],
duration: number
) => {
const startLngLat = new AMap.LngLat(start[0], start[1]);
const endLngLat = new AMap.LngLat(end[0], end[1]);
const distance = startLngLat.distance(endLngLat);
const speed = distance / duration;
let startTime = Date.now();
const moveFrame = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentLng = start[0] + (end[0] - start[0]) * progress;
const currentLat = start[1] + (end[1] - start[1]) * progress;
markerRef.value?.setPosition([currentLng, currentLat]);
if (progress < 1) {
playTimer.value = requestAnimationFrame(moveFrame);
} else {
currentIndex.value += 1;
if (currentIndex.value < trackPoints.value.length - 1) {
playNextSegment();
} else {
// 播放完成
isPlaying.value = false;
currentIndex.value = 0;
}
}
};
playTimer.value = requestAnimationFrame(moveFrame);
};
// 播放下一段轨迹
const playNextSegment = () => {
if (currentIndex.value >= trackPoints.value.length - 1) return;
const startPoint = trackPoints.value[currentIndex.value];
const endPoint = trackPoints.value[currentIndex.value + 1];
const baseDuration = 500;
const duration = baseDuration / playSpeed.value;
smoothMoveMarker(startPoint, endPoint, duration);
};
// 重置播放状态(切换设备时调用)
const resetPlayState = () => {
// 停止当前播放
if (isPlaying.value && playTimer.value) {
cancelAnimationFrame(playTimer.value);
}
// 重置状态
isPlaying.value = false;
currentIndex.value = 0;
};
// 播放/暂停切换接收设备的trackPoints
const handlePlayPause = (group: any) => {
const trackPointList = group.detailList;
const validTrackPoints = trackPointList.map((point: any) => {
const lng = Number(point.longitude); // 经度
const lat = Number(point.latitude); // 纬度
// 验证经纬度有效性避免NaN
return isNaN(lng) || isNaN(lat) ? null : [lng, lat] as [number, number];
}).filter((point: any) => point !== null); // 过滤无效点
if (validTrackPoints.length < 2) {
ElMessage.warning('有效轨迹点不足2个无法播放');
return;
}
// 3. 重置播放状态并赋值轨迹数据
resetPlayState();
trackPoints.value = validTrackPoints;
// 4. 开始播放
isPlaying.value = true;
playNextSegment();
};
// -------------------------- 生命周期与监听 --------------------------
onMounted(() => {
initMap();
});
onUnmounted(() => {
if (playTimer.value) {
cancelAnimationFrame(playTimer.value);
}
mapRef.value?.remove(polylineRef.value!);
mapRef.value?.remove(markerRef.value!);
});
// 监听轨迹数据变化(更新轨迹线和地图范围)
watch(trackPoints, (newPoints) => {
if (!mapRef.value || !polylineRef.value || !markerRef.value) return;
if (newPoints.length === 0) return;
// 1. 更新轨迹线
polylineRef.value.setPath(newPoints);
// 2. 获取第一个轨迹点(作为地图中心)
const firstPoint = newPoints[0]; // [经度, 纬度]
const centerLngLat = new AMap.LngLat(firstPoint[0], firstPoint[1]);
// 3. 地图聚焦到第一个点并设置固定zoom16为街道级足够大无需手动放大
mapRef.value.setCenter(centerLngLat); // 设中心
mapRef.value.setZoom(15); // 固定缩放级别1-20越大越近
// 4. 标记点移到第一个点
markerRef.value.setPosition(firstPoint);
}, { deep: true });
</script>
<style scoped>
/* 样式保持不变 */
:deep .el-timeline-item__tail {
border-left: 1px solid rgba(0, 198, 250, 0.2);
}
:deep .el-timeline-item__timestamp {
color: rgba(2, 124, 251, 1);
font-size: 16px;
}
.track-player {
width: 100%;
height: 90vh;
position: relative;
}
.map-container {
width: 100%;
height: 100%;
}
.content_top {
width: 300px;
border-radius: 4px;
box-shadow: 0px 0px 6px 0px rgba(0, 34, 96, 0.1);
background: #fff;
height: 88vh;
position: absolute;
z-index: 1;
top: 10px;
left: 10px;
overflow-y: auto;
}
.Stime {
color: rgba(56, 64, 79, 0.6);
white-space: nowrap;
}
.Sloation {
color: rgba(56, 64, 79, 1);
width: 150px;
display: inline-block;
}
.d_f {
display: flex;
}
.content_layout {
padding: 20px;
}
/* 时间线整体 */
.el-timeline {
margin-left: 20px;
/* 调整时间线与左侧的间距 */
}
/* 时间戳(日期) */
.el-timeline-item__timestamp {
font-weight: bold;
margin-bottom: 8px;
color: rgba(2, 124, 251, 1);
}
/* 设备项容器 */
.device-item {
background-color: #f9fafc;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* 设备名称 + 时间 行 */
.device-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
/* 设备名称 */
.device-name {
font-weight: 500;
}
/* 设备时间 */
.device-time {
color: rgba(97, 128, 183, 1);
font-size: 14px;
}
/* 设备型号 */
.device-model {
color: #999;
font-size: 13px;
margin-bottom: 6px;
}
/* 地点信息 */
.device-place p {
margin: 4px 0;
font-size: 13px;
color: #333;
}
.date_group {
margin-bottom: 10px;
padding: 20px;
}
.date_header {
padding: 8px 12px;
border-radius: 4px 4px 0 0;
font-weight: 500;
}
.device_item {
padding: 10px 12px;
background-color: #f7f8fc;
border-radius: 4px;
display: flex;
flex-direction: column;
position: relative;
}
.device_info {
margin-bottom: 8px;
}
.device_name {
font-weight: 500;
font-size: 14px;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
}
.device_model {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.time_info {
font-size: 15px;
color: rgba(97, 128, 183, 1);
}
.place_info {
font-size: 13px;
color: #666;
line-height: 2;
}
.place_info p {
margin: 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.control_btn {
font-size: 14px;
padding: 4px 12px;
background: rgba(2, 124, 251, 0.06);
color: #027cfb;
border: none;
border-radius: 4px;
align-self: flex-end;
cursor: pointer;
transition: background 0.2s;
}
.control_btt {
text-align: end;
}
.nodata{
text-align: center;
transform: translate(-1%,100%);
left:50%;
height: 30vh;
}
.nodataImg{
width: 130px;
}
.title{
color: #666;
padding-top: 20px;
}
</style>