修改平台概览样式
This commit is contained in:
parent
921c2e8e5d
commit
196240b21f
@ -6,3 +6,6 @@ VITE_APP_ENV = 'development'
|
|||||||
|
|
||||||
# 青岛机场无人驾驶车辆协同云平台/开发环境
|
# 青岛机场无人驾驶车辆协同云平台/开发环境
|
||||||
VITE_APP_BASE_API = '/dev-api'
|
VITE_APP_BASE_API = '/dev-api'
|
||||||
|
|
||||||
|
# WebSocket配置
|
||||||
|
VITE_APP_WEBSOCKET_URL=ws://10.0.0.126:8080/collision
|
||||||
|
|||||||
@ -7,5 +7,7 @@ VITE_APP_ENV = 'production'
|
|||||||
# 青岛机场无人驾驶车辆协同云平台/生产环境
|
# 青岛机场无人驾驶车辆协同云平台/生产环境
|
||||||
VITE_APP_BASE_API = '/prod-api'
|
VITE_APP_BASE_API = '/prod-api'
|
||||||
|
|
||||||
|
VITE_APP_WEBSOCKET_URL='ws://10.0.0.124:8080/collision'
|
||||||
|
|
||||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||||
VITE_BUILD_COMPRESS = gzip
|
VITE_BUILD_COMPRESS = gzip
|
||||||
@ -7,5 +7,7 @@ VITE_APP_ENV = 'staging'
|
|||||||
# 青岛机场无人驾驶车辆协同云平台/生产环境
|
# 青岛机场无人驾驶车辆协同云平台/生产环境
|
||||||
VITE_APP_BASE_API = '/stage-api'
|
VITE_APP_BASE_API = '/stage-api'
|
||||||
|
|
||||||
|
VITE_APP_WEBSOCKET_URL='ws://10.0.0.124:8080/collision'
|
||||||
|
|
||||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||||
VITE_BUILD_COMPRESS = gzip
|
VITE_BUILD_COMPRESS = gzip
|
||||||
37
README.md
37
README.md
@ -125,3 +125,40 @@ function getUserOptions() {
|
|||||||
|
|
||||||
使用SockJS + STOMP协议
|
使用SockJS + STOMP协议
|
||||||
滑出蓝色 滑入黄色
|
滑出蓝色 滑入黄色
|
||||||
|
|
||||||
|
20250711关键改进说明
|
||||||
|
1. 平滑动画核心系统
|
||||||
|
动画循环引擎:使用 requestAnimationFrame 创建流畅的60FPS动画循环
|
||||||
|
运动预测算法:
|
||||||
|
记录车辆移动历史轨迹(最多3个点)
|
||||||
|
计算运动方向向量用于预测后续位置
|
||||||
|
物理引擎:
|
||||||
|
基于真实速度计算每帧最大移动距离
|
||||||
|
方向插值时考虑车速影响(车速越高转向越慢)
|
||||||
|
2. 新数据结构
|
||||||
|
vehicleAnimations:存储所有平滑动画所需数据
|
||||||
|
当前位置和方向
|
||||||
|
目标位置和方向
|
||||||
|
最后更新时间
|
||||||
|
预测向量
|
||||||
|
vehicleMotionHistory:存储最近位置点用于轨迹预测
|
||||||
|
3. 增强的位置更新逻辑
|
||||||
|
收到新位置时记录历史点
|
||||||
|
计算历史点间的移动向量
|
||||||
|
设置目标位置时包含预测偏移量
|
||||||
|
当超过300ms没有新数据时使用预测向量继续移动
|
||||||
|
4. 性能优化
|
||||||
|
距离阈值检查(0.1米)避免不必要的计算
|
||||||
|
角度归一化处理0-360度边界情况
|
||||||
|
使用缓动函数使运动曲线更自然
|
||||||
|
5. 暴露控制接口
|
||||||
|
通过defineExpose提供了三个新方法:
|
||||||
|
|
||||||
|
startVehicleSmoothing():启动平滑动画
|
||||||
|
stopVehicleSmoothing():停止平滑动画
|
||||||
|
resetVehicleAnimations():重置所有动画数据
|
||||||
|
使用建议
|
||||||
|
将此组件整合到现有项目中
|
||||||
|
调用startVehicleSmoothing()启动平滑效果
|
||||||
|
处理WebSocket消息时继续调用updateVehiclePosition()
|
||||||
|
当组件隐藏时调用stopVehicleSmoothing()节省资源
|
||||||
70
src/api/monitor/carRunInfo.js
Normal file
70
src/api/monitor/carRunInfo.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 车辆运动信息管理
|
||||||
|
// 查询车辆运动信息列表
|
||||||
|
export function listCarRunInfo(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出车辆运动信息列表
|
||||||
|
export function exportCarRunInfo(query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/export',
|
||||||
|
method: 'post',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车辆ID查询车辆运动信息列表
|
||||||
|
export function listCarRunInfoByVehicleId(vehicleId, query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/vehicle/' + vehicleId,
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车牌号查询车辆运动信息列表
|
||||||
|
export function listCarRunInfoByLicensePlate(licensePlate, query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/plate/' + licensePlate,
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车辆ID查询车辆最新位置信息
|
||||||
|
export function getLatestLocationByVehicleId(vehicleId) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/latest/vehicle/' + vehicleId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车牌号查询车辆最新位置信息
|
||||||
|
export function getLatestLocationByLicensePlate(licensePlate) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/latest/plate/' + licensePlate,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除调度日志
|
||||||
|
export function delJobLog(jobLogId) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/jobLog/' + jobLogId,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空调度日志
|
||||||
|
export function cleanJobLog() {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/jobLog/clean',
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
src/assets/images/alarm_car.png
Normal file
BIN
src/assets/images/alarm_car.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/images/arrow.png
Normal file
BIN
src/assets/images/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 869 B |
BIN
src/assets/images/close_icon.png
Normal file
BIN
src/assets/images/close_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 B |
BIN
src/assets/images/warning_car.png
Normal file
BIN
src/assets/images/warning_car.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/images/weather_icon.png
Normal file
BIN
src/assets/images/weather_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 550 B |
BIN
src/assets/images/weather_station.png
Normal file
BIN
src/assets/images/weather_station.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 B |
@ -34,7 +34,8 @@
|
|||||||
<div class="task-row3">
|
<div class="task-row3">
|
||||||
<span class="point start">起点</span>
|
<span class="point start">起点</span>
|
||||||
<span class="label">{{ item.start }}</span>
|
<span class="label">{{ item.start }}</span>
|
||||||
<span class="arrow">——></span>
|
<!-- <span class="arrow">——></span> -->
|
||||||
|
<img class="arrow" src="@/assets/images/arrow.png" alt="arrow">
|
||||||
<span class="point end">终点</span>
|
<span class="point end">终点</span>
|
||||||
<span class="label">{{ item.end }}</span>
|
<span class="label">{{ item.end }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -52,18 +53,18 @@
|
|||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span class="panel-title">轨迹详情</span>
|
<span class="panel-title">轨迹详情</span>
|
||||||
<el-button size="small" type="primary" class="replay-btn">回放</el-button>
|
<el-button size="small" type="primary" class="replay-btn" @click="handleReplay">回放</el-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 第二行 -->
|
<!-- 第二行 -->
|
||||||
<div class="panel-info">
|
<div class="panel-info">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="carno">QN001</span>
|
<span class="carno">{{ activeTask?.licensePlate || activeVehicleId || "未选择车辆" }}</span>
|
||||||
<span class="info-item">最大时速 <b>91km/h</b></span>
|
<span class="info-item">最大时速 <b>{{ trackDetails.maxSpeed }}km/h</b></span>
|
||||||
<span class="info-item">平均时速 <b>28km/h</b></span>
|
<span class="info-item">平均时速 <b>{{ trackDetails.averageSpeed }}km/h</b></span>
|
||||||
<span class="info-item">总里程 <b>63.3km</b></span>
|
<span class="info-item">总里程 <b>{{ trackDetails.totalDistance }}km</b></span>
|
||||||
<span class="info-item">耗时 <b>20min</b></span>
|
<span class="info-item">耗时 <b>{{ trackDetails.totalTime }}min</b></span>
|
||||||
<span class="info-item warn">冲突告警 <b>1</b></span>
|
<span class="info-item warn">冲突告警 <b>{{ trackDetails.warnings }}</b></span>
|
||||||
<span class="info-item prewarn">冲突预警 <b>1</b></span>
|
<span class="info-item prewarn">冲突预警 <b>{{ trackDetails.preWarnings }}</b></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 第三行:进度条 -->
|
<!-- 第三行:进度条 -->
|
||||||
@ -79,7 +80,7 @@
|
|||||||
<!-- 预警/告警红旗 -->
|
<!-- 预警/告警红旗 -->
|
||||||
<div
|
<div
|
||||||
v-for="flag in flags"
|
v-for="flag in flags"
|
||||||
:key="flag.time"
|
:key="flag.label"
|
||||||
class="progress-flag"
|
class="progress-flag"
|
||||||
:style="{left: flag.percent+'%'}"
|
:style="{left: flag.percent+'%'}"
|
||||||
:title="flag.label"
|
:title="flag.label"
|
||||||
@ -95,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bottom">
|
<div class="progress-bottom">
|
||||||
<span class="start-time">2024-09-10 12:00:00</span>
|
<span class="start-time">{{ trackDetails.startTime || "未开始" }}</span>
|
||||||
<div class="speed-select">
|
<div class="speed-select">
|
||||||
<el-dropdown @command="setSpeed">
|
<el-dropdown @command="setSpeed">
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
@ -110,7 +111,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<span class="end-time">2024-09-10 12:20:00</span>
|
<span class="end-time">{{ trackDetails.endTime || "未结束" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -120,8 +121,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed, onMounted, defineProps, watchEffect } from "vue";
|
||||||
import { Search } from "@element-plus/icons-vue";
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
import {
|
||||||
|
listCarRunInfo,
|
||||||
|
exportCarRunInfo,
|
||||||
|
listCarRunInfoByVehicleId,
|
||||||
|
listCarRunInfoByLicensePlate,
|
||||||
|
getLatestLocationByVehicleId,
|
||||||
|
getLatestLocationByLicensePlate
|
||||||
|
} from "@/api/monitor/carRunInfo";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = ref({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
timeRange: [],
|
||||||
|
vehicleId: '',
|
||||||
|
licensePlate: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const runInfoList = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
// 车辆信息模拟数据
|
// 车辆信息模拟数据
|
||||||
const carInfo = {
|
const carInfo = {
|
||||||
@ -134,59 +158,254 @@ const carInfo = {
|
|||||||
phone: "15689742356",
|
phone: "15689742356",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 任务模拟数据
|
// 任务模拟数据(将由接口数据替换)
|
||||||
const tasks = [
|
const tasks = ref([]);
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
no: "001",
|
|
||||||
name: "东园区驱鸟",
|
|
||||||
time: "2024年8月16日 18:12:06--2024年8月18日 18:12:09",
|
|
||||||
start: "航站楼01西门",
|
|
||||||
end: "航站楼02东门",
|
|
||||||
status: "已完成",
|
|
||||||
speed: "28km/h",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
no: "002",
|
|
||||||
name: "西园区驱鸟",
|
|
||||||
time: "2024年8月16日 18:12:06--2024年8月18日 18:12:09",
|
|
||||||
start: "航站楼03西门",
|
|
||||||
end: "航站楼04东门",
|
|
||||||
status: "已完成",
|
|
||||||
speed: "27km/h",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "南园区巡逻",
|
|
||||||
time: "2024年8月16日 18:12:09",
|
|
||||||
status: "已完成",
|
|
||||||
speed: "29km/h",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "北园区巡逻",
|
|
||||||
time: "2024年8月16日 18:12:09",
|
|
||||||
status: "已完成",
|
|
||||||
speed: "26km/h",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const search = ref("");
|
const search = ref("");
|
||||||
const activeId = ref(tasks[0].id);
|
const activeId = ref(0);
|
||||||
|
|
||||||
|
// 接收vehicle属性
|
||||||
|
const props = defineProps({
|
||||||
|
vehicle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 轨迹详情数据
|
||||||
|
const trackDetails = ref({
|
||||||
|
maxSpeed: "0",
|
||||||
|
averageSpeed: "0",
|
||||||
|
totalDistance: "0",
|
||||||
|
totalTime: "0",
|
||||||
|
warnings: "0",
|
||||||
|
preWarnings: "0",
|
||||||
|
startTime: "",
|
||||||
|
endTime: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeVehicleId = ref("");
|
||||||
|
|
||||||
|
// 监听vehicle属性变化
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.vehicle?.carId) {
|
||||||
|
activeVehicleId.value = props.vehicle.carId;
|
||||||
|
// 更新查询参数
|
||||||
|
queryParams.value.vehicleId = props.vehicle.carId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取车辆运动信息列表数据
|
||||||
|
async function getRunInfoList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await listCarRunInfo(queryParams.value);
|
||||||
|
// console.log("-----------------------------------,", response);
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
runInfoList.value = response.rows || [];
|
||||||
|
total.value = response.total;
|
||||||
|
|
||||||
|
// 转换数据格式为任务列表
|
||||||
|
// 根据车辆ID对数据进行分组处理
|
||||||
|
const groupByVehicle = {};
|
||||||
|
runInfoList.value.forEach(item => {
|
||||||
|
if (!groupByVehicle[item.vehicleId]) {
|
||||||
|
groupByVehicle[item.vehicleId] = [];
|
||||||
|
}
|
||||||
|
groupByVehicle[item.vehicleId].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将分组后的数据转换为任务列表格式
|
||||||
|
const tasksList = [];
|
||||||
|
Object.keys(groupByVehicle).forEach((vehicleId, index) => {
|
||||||
|
const vehicleData = groupByVehicle[vehicleId];
|
||||||
|
const firstPoint = vehicleData[0];
|
||||||
|
const lastPoint = vehicleData[vehicleData.length - 1];
|
||||||
|
|
||||||
|
// 计算平均速度
|
||||||
|
const avgSpeed = (vehicleData.reduce((sum, point) => sum + point.speed, 0) / vehicleData.length).toFixed(2);
|
||||||
|
|
||||||
|
tasksList.push({
|
||||||
|
id: parseInt(vehicleId),
|
||||||
|
no: vehicleId,
|
||||||
|
name: `车辆${firstPoint.licensePlate}轨迹`,
|
||||||
|
time: `${firstPoint.timestamp}--${lastPoint.timestamp}`,
|
||||||
|
start: `经度${firstPoint.longitude},纬度${firstPoint.latitude}`,
|
||||||
|
end: `经度${lastPoint.longitude},纬度${lastPoint.latitude}`,
|
||||||
|
status: "已完成",
|
||||||
|
speed: `${avgSpeed}km/h`,
|
||||||
|
licensePlate: firstPoint.licensePlate,
|
||||||
|
points: vehicleData
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.value = tasksList;
|
||||||
|
|
||||||
|
// 设置默认选中第一项
|
||||||
|
if (tasks.value.length > 0) {
|
||||||
|
activeId.value = tasks.value[0].id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.msg || '获取车辆运动信息列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取车辆运动信息列表异常', error);
|
||||||
|
ElMessage.error('获取车辆运动信息列表异常');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车辆ID获取轨迹详情
|
||||||
|
async function getTrackDetailByVehicleId(vehicleId) {
|
||||||
|
if (!vehicleId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await listCarRunInfoByVehicleId(vehicleId, {});
|
||||||
|
if (response.code === 200) {
|
||||||
|
const trackData = response.data || [];
|
||||||
|
// 处理轨迹数据...
|
||||||
|
console.log('车辆轨迹数据:', trackData);
|
||||||
|
// 这里可以处理轨迹点、告警点等
|
||||||
|
// 假设返回的数据格式与tasks中的points一致
|
||||||
|
processTrackPoints(trackData);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.msg || '获取车辆轨迹详情失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取车辆轨迹详情异常', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车牌号获取车辆最新位置
|
||||||
|
async function getLatestLocation(licensePlate) {
|
||||||
|
if (!licensePlate) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getLatestLocationByLicensePlate(licensePlate);
|
||||||
|
if (response.code === 200) {
|
||||||
|
const locationData = response.data;
|
||||||
|
// 处理位置数据...
|
||||||
|
console.log('车辆最新位置:', locationData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取车辆最新位置异常', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filteredTasks = computed(() => {
|
const filteredTasks = computed(() => {
|
||||||
if (!search.value) return tasks;
|
if (!search.value) return tasks.value;
|
||||||
return tasks.filter(
|
return tasks.value.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.name.includes(search.value) ||
|
t.name.includes(search.value) ||
|
||||||
t.no.includes(search.value) ||
|
t.no.includes(search.value) ||
|
||||||
t.id.toString().includes(search.value)
|
t.id.toString().includes(search.value)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const activeTask = computed(() => tasks.find((t) => t.id === activeId.value));
|
|
||||||
|
const activeTask = computed(() => tasks.value.find((t) => t.id === activeId.value));
|
||||||
|
|
||||||
function selectTask(item) {
|
function selectTask(item) {
|
||||||
activeId.value = item.id;
|
activeId.value = item.id;
|
||||||
|
|
||||||
|
// 从任务中提取轨迹数据
|
||||||
|
if (item.points && item.points.length > 0) {
|
||||||
|
processTrackPoints(item.points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理轨迹点数据
|
||||||
|
function processTrackPoints(points) {
|
||||||
|
// 按时间排序
|
||||||
|
points.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||||
|
|
||||||
|
// 计算最大速度
|
||||||
|
const maxSpeed = Math.max(...points.map(p => p.speed)).toFixed(2);
|
||||||
|
|
||||||
|
// 计算平均速度
|
||||||
|
const avgSpeed = (points.reduce((sum, p) => sum + p.speed, 0) / points.length).toFixed(2);
|
||||||
|
|
||||||
|
// 计算总距离 (简单估算,实际应该使用更复杂的地理距离计算)
|
||||||
|
let totalDistance = 0;
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const prev = points[i-1];
|
||||||
|
const curr = points[i];
|
||||||
|
// 使用直线距离简单估算
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
Math.pow(curr.longitude - prev.longitude, 2) +
|
||||||
|
Math.pow(curr.latitude - prev.latitude, 2)
|
||||||
|
) * 111000; // 粗略转换为米
|
||||||
|
totalDistance += dist;
|
||||||
|
}
|
||||||
|
const distanceKm = (totalDistance / 1000).toFixed(2);
|
||||||
|
|
||||||
|
// 计算总时间
|
||||||
|
const startTime = new Date(points[0].timestamp);
|
||||||
|
const endTime = new Date(points[points.length-1].timestamp);
|
||||||
|
const totalMinutes = Math.round((endTime - startTime) / (60 * 1000));
|
||||||
|
|
||||||
|
// 更新轨迹详情
|
||||||
|
trackDetails.value = {
|
||||||
|
maxSpeed: maxSpeed,
|
||||||
|
averageSpeed: avgSpeed,
|
||||||
|
totalDistance: distanceKm,
|
||||||
|
totalTime: totalMinutes > 0 ? totalMinutes.toString() : "1",
|
||||||
|
warnings: "0", // 实际应从数据中获取告警信息
|
||||||
|
preWarnings: "0", // 实际应从数据中获取预警信息
|
||||||
|
startTime: points[0].timestamp,
|
||||||
|
endTime: points[points.length-1].timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理告警标记点 (示例,实际应根据数据判断)
|
||||||
|
flags.value = [
|
||||||
|
{
|
||||||
|
percent: 20,
|
||||||
|
label: "告警 " + points[Math.floor(points.length * 0.2)].timestamp
|
||||||
|
},
|
||||||
|
{
|
||||||
|
percent: 60,
|
||||||
|
label: "预警 " + points[Math.floor(points.length * 0.6)].timestamp
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回放按钮点击处理函数
|
||||||
|
function handleReplay() {
|
||||||
|
// 获取当前选中任务
|
||||||
|
const activeTask = tasks.value.find(t => t.id === activeId.value);
|
||||||
|
|
||||||
|
if (activeTask) {
|
||||||
|
if (activeTask.points && activeTask.points.length > 0) {
|
||||||
|
// 如果任务已包含轨迹点数据,直接处理
|
||||||
|
processTrackPoints(activeTask.points);
|
||||||
|
ElMessage.success('开始轨迹回放');
|
||||||
|
} else {
|
||||||
|
// 如果没有轨迹点数据,根据车辆ID获取
|
||||||
|
getTrackDetailByVehicleId(activeTask.id);
|
||||||
|
}
|
||||||
|
} else if (queryParams.value.vehicleId) {
|
||||||
|
// 使用查询参数中的车辆ID
|
||||||
|
getTrackDetailByVehicleId(queryParams.value.vehicleId);
|
||||||
|
} else if (props.vehicle?.carId) {
|
||||||
|
// 使用传入的车辆ID
|
||||||
|
getTrackDetailByVehicleId(props.vehicle.carId);
|
||||||
|
} else {
|
||||||
|
// 获取列表数据
|
||||||
|
getRunInfoList();
|
||||||
|
ElMessage.info('请先选择一个车辆任务');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出车辆运动信息
|
||||||
|
async function handleExport() {
|
||||||
|
try {
|
||||||
|
await exportCarRunInfo(queryParams.value);
|
||||||
|
ElMessage.success('导出成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出车辆运动信息异常', error);
|
||||||
|
ElMessage.error('导出车辆运动信息异常');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const progress = ref(30); // 进度百分比
|
const progress = ref(30); // 进度百分比
|
||||||
@ -195,11 +414,8 @@ const showTooltip = ref(false);
|
|||||||
const tooltipTime = ref("");
|
const tooltipTime = ref("");
|
||||||
const tooltipLeft = ref(0);
|
const tooltipLeft = ref(0);
|
||||||
|
|
||||||
// 预警/告警红旗模拟数据
|
// 预警/告警红旗模拟数据(将由接口数据替换)
|
||||||
const flags = [
|
const flags = ref([]);
|
||||||
{ percent: 20, label: "告警 12:04:00" },
|
|
||||||
{ percent: 60, label: "预警 12:12:00" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 拖动进度条
|
// 拖动进度条
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
@ -247,6 +463,13 @@ function handleProgressClick(e) {
|
|||||||
function setSpeed(val) {
|
function setSpeed(val) {
|
||||||
speed.value = val;
|
speed.value = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getRunInfoList();
|
||||||
|
// getLatestLocation("鲁B579");
|
||||||
|
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
<div class="panel-content" v-else>
|
<div class="panel-content" v-else>
|
||||||
<div class="layer-group">
|
<div class="layer-group">
|
||||||
<div class="group-title">道路图层</div>
|
<!-- <div class="group-title">道路图层</div> -->
|
||||||
<div class="layer-grid-full">
|
<div class="layer-grid-full">
|
||||||
<div class="layer-item">
|
<div class="layer-item">
|
||||||
<label class="checkbox-container">
|
<label class="checkbox-container">
|
||||||
@ -203,7 +203,7 @@ async function loadCustomRoadLayer() {
|
|||||||
customRoadVectorLayer = new VectorLayer({
|
customRoadVectorLayer = new VectorLayer({
|
||||||
source,
|
source,
|
||||||
style: new Style({
|
style: new Style({
|
||||||
stroke: new Stroke({ color: '#888', width: 4 })
|
stroke: new Stroke({ color: '#C9C9C9', width: 1 })
|
||||||
}),
|
}),
|
||||||
zIndex: 2, // 确保在标准道路图层之上
|
zIndex: 2, // 确保在标准道路图层之上
|
||||||
visible: showCustomRoadLayer.value
|
visible: showCustomRoadLayer.value
|
||||||
@ -266,12 +266,12 @@ async function addRoadLayer() {
|
|||||||
|
|
||||||
// 创建两个图层样式 - 一个用于普通状态,一个用于闪烁状态
|
// 创建两个图层样式 - 一个用于普通状态,一个用于闪烁状态
|
||||||
const normalStyle = new Style({
|
const normalStyle = new Style({
|
||||||
stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.4)', width: 4 }),
|
stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.4)', width: 1 }),
|
||||||
fill: new Fill({ color: 'rgba(0, 0, 0, 0.5)' })
|
fill: new Fill({ color: 'rgba(0, 0, 0, 0.2)' })
|
||||||
});
|
});
|
||||||
|
|
||||||
const flashStyle = new Style({
|
const flashStyle = new Style({
|
||||||
stroke: new Stroke({ color: 'rgba(255, 0, 0, 0.8)', width: 4 }),
|
stroke: new Stroke({ color: 'rgba(255, 0, 0, 0.8)', width: 1 }),
|
||||||
fill: new Fill({ color: 'rgba(255, 0, 0, 0.3)' })
|
fill: new Fill({ color: 'rgba(255, 0, 0, 0.3)' })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
167
src/components/map/controls/README.md
Normal file
167
src/components/map/controls/README.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# 车辆移动控制系统组件重构20250711
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
原有的 `VehicleMovementControl.vue` 文件过于庞大(1647行),不利于维护和查阅。本次重构将其拆分为多个专注的组件,提高代码的可维护性和可读性。
|
||||||
|
|
||||||
|
## 组件结构
|
||||||
|
|
||||||
|
### 主组件
|
||||||
|
- **VehicleMovementControlRefactored.vue** - 重构后的主组件,整合所有子组件
|
||||||
|
|
||||||
|
### 子组件
|
||||||
|
|
||||||
|
#### 1. VehicleAnimationSystem.vue
|
||||||
|
**功能**: 车辆平滑动画系统
|
||||||
|
- 平滑动画逻辑
|
||||||
|
- 运动预测算法
|
||||||
|
- 缓动函数
|
||||||
|
- 动画循环管理
|
||||||
|
|
||||||
|
**主要方法**:
|
||||||
|
- `startAnimationLoop()` - 启动动画循环
|
||||||
|
- `stopAnimationLoop()` - 停止动画循环
|
||||||
|
- `resetAnimations()` - 重置动画数据
|
||||||
|
- `initVehicleAnimation()` - 初始化车辆动画
|
||||||
|
- `updateVehicleAnimationTarget()` - 更新动画目标
|
||||||
|
|
||||||
|
#### 2. VehicleLabelSystem.vue
|
||||||
|
**功能**: 车辆标签系统
|
||||||
|
- 标签创建和更新
|
||||||
|
- 标签样式管理
|
||||||
|
- 标签位置计算
|
||||||
|
|
||||||
|
**主要方法**:
|
||||||
|
- `updateVehicleLabel()` - 更新车辆标签
|
||||||
|
- `removeVehicleLabel()` - 移除车辆标签
|
||||||
|
- `updateAllLabels()` - 更新所有标签位置
|
||||||
|
- `setLabelVisibility()` - 设置标签可见性
|
||||||
|
|
||||||
|
#### 3. VehicleDetailPopup.vue
|
||||||
|
**功能**: 车辆详情弹窗
|
||||||
|
- 车辆详情显示
|
||||||
|
- 弹窗位置管理
|
||||||
|
- 详情数据展示
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
- `visible` - 是否显示弹窗
|
||||||
|
- `detail` - 车辆详情数据
|
||||||
|
- `popupStyle` - 弹窗样式
|
||||||
|
|
||||||
|
#### 4. WeatherStationPopup.vue
|
||||||
|
**功能**: 气象监测弹窗
|
||||||
|
- 气象数据显示
|
||||||
|
- 气象数据更新
|
||||||
|
- 气象弹窗样式
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
- `visible` - 是否显示弹窗
|
||||||
|
|
||||||
|
#### 5. AlertNotificationSystem.vue
|
||||||
|
**功能**: 告警提示系统
|
||||||
|
- 告警消息显示
|
||||||
|
- 告警类型管理
|
||||||
|
- 告警动画效果
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
- `alertMessage` - 告警消息
|
||||||
|
- `alertType` - 告警类型
|
||||||
|
|
||||||
|
#### 6. VehicleStyleManager.vue
|
||||||
|
**功能**: 车辆样式管理
|
||||||
|
- 车辆图标选择
|
||||||
|
- 样式创建
|
||||||
|
- 状态样式管理
|
||||||
|
|
||||||
|
**主要方法**:
|
||||||
|
- `getVehicleStyle()` - 获取车辆样式
|
||||||
|
- `getVehicleIcon()` - 获取车辆图标
|
||||||
|
- `createVehicleStyle()` - 创建车辆样式
|
||||||
|
- `updateVehicleStyle()` - 更新车辆样式
|
||||||
|
|
||||||
|
## 重构优势
|
||||||
|
|
||||||
|
### 1. 代码组织
|
||||||
|
- **单一职责**: 每个组件只负责一个特定功能
|
||||||
|
- **模块化**: 功能独立,便于单独测试和维护
|
||||||
|
- **可复用**: 子组件可以在其他地方复用
|
||||||
|
|
||||||
|
### 2. 维护性
|
||||||
|
- **易于定位**: 问题可以快速定位到具体组件
|
||||||
|
- **易于修改**: 修改某个功能不会影响其他功能
|
||||||
|
- **易于扩展**: 新增功能可以创建新的子组件
|
||||||
|
|
||||||
|
### 3. 性能优化
|
||||||
|
- **按需加载**: 可以按需加载子组件
|
||||||
|
- **独立更新**: 子组件可以独立更新,减少不必要的重渲染
|
||||||
|
|
||||||
|
### 4. 团队协作
|
||||||
|
- **并行开发**: 不同开发者可以同时开发不同组件
|
||||||
|
- **代码审查**: 小文件更容易进行代码审查
|
||||||
|
- **知识传递**: 新团队成员更容易理解代码结构
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 替换原有组件
|
||||||
|
```vue
|
||||||
|
<!-- 在父组件中使用重构后的组件 -->
|
||||||
|
<VehicleMovementControlRefactored :map="map" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 单独使用子组件
|
||||||
|
```vue
|
||||||
|
<!-- 单独使用动画系统 -->
|
||||||
|
<VehicleAnimationSystem
|
||||||
|
:map="map"
|
||||||
|
:vehicle-source="vehicleSource"
|
||||||
|
:vehicles="vehicles"
|
||||||
|
:get-vehicle-style="getVehicleStyle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 单独使用标签系统 -->
|
||||||
|
<VehicleLabelSystem
|
||||||
|
:map="map"
|
||||||
|
:vehicles="vehicles"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 1. 备份原文件
|
||||||
|
```bash
|
||||||
|
cp VehicleMovementControl.vue VehicleMovementControl.vue.backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 替换组件引用
|
||||||
|
在父组件中,将:
|
||||||
|
```vue
|
||||||
|
<VehicleMovementControl :map="map" />
|
||||||
|
```
|
||||||
|
替换为:
|
||||||
|
```vue
|
||||||
|
<VehicleMovementControlRefactored :map="map" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试功能
|
||||||
|
确保所有原有功能正常工作:
|
||||||
|
- 车辆位置更新
|
||||||
|
- 车辆动画
|
||||||
|
- 标签显示
|
||||||
|
- 详情弹窗
|
||||||
|
- 气象弹窗
|
||||||
|
- 告警提示
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **依赖关系**: 主组件依赖所有子组件,确保所有子组件都已创建
|
||||||
|
2. **Props传递**: 子组件通过props接收数据,确保数据流正确
|
||||||
|
3. **事件通信**: 子组件通过emit向父组件发送事件
|
||||||
|
4. **样式隔离**: 每个组件都有自己的样式,避免样式冲突
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. **状态管理**: 考虑使用Pinia进行全局状态管理
|
||||||
|
2. **类型安全**: 添加TypeScript类型定义
|
||||||
|
3. **单元测试**: 为每个子组件编写单元测试
|
||||||
|
4. **文档完善**: 为每个组件添加详细的API文档
|
||||||
|
5. **性能监控**: 添加性能监控和优化
|
||||||
688
src/components/map/controls/VehicleAnimationSystem.vue
Normal file
688
src/components/map/controls/VehicleAnimationSystem.vue
Normal file
@ -0,0 +1,688 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 动画系统不需要模板内容 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { Style, Icon } from 'ol/style';
|
||||||
|
|
||||||
|
// 定义props
|
||||||
|
const props = defineProps({
|
||||||
|
map: Object,
|
||||||
|
vehicleSource: Object,
|
||||||
|
vehicles: Object,
|
||||||
|
getVehicleStyle: Function
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动画相关变量
|
||||||
|
const animationFrameId = ref(null);
|
||||||
|
const lastAnimationTime = ref(0);
|
||||||
|
const vehicleAnimations = ref({}); // 存储平滑动画数据
|
||||||
|
const vehicleMotionHistory = ref({}); // 存储车辆移动历史
|
||||||
|
const PREDICTION_THRESHOLD = 50; // 从100ms降低到50ms,更早开始预测
|
||||||
|
const MAX_PREDICTION_TIME = 15000; // 从10000ms增加到15000ms,延长预测时间
|
||||||
|
|
||||||
|
// 物理模拟参数
|
||||||
|
const ACCELERATION = 0.15; // 加速度系数
|
||||||
|
const DECELERATION = 0.25; // 减速度系数
|
||||||
|
const MAX_TURN_RATE = 100; // 最大转向速率(度/秒)
|
||||||
|
const MIN_TURN_RATE = 20; // 最小转向速率(度/秒)
|
||||||
|
const INERTIA_FACTOR = 0.97; // 从0.95增加到0.97,增强惯性效果
|
||||||
|
const PREDICTION_STRENGTH = 6; // 从8降低到6,减少预测激进性
|
||||||
|
const SPEED_SMOOTHING = 0.94; // 从0.92增加到0.94,增强速度平滑效果
|
||||||
|
const POSITION_SMOOTHING = 0.12; // 从0.15降低到0.12,增强位置平滑效果
|
||||||
|
const MIN_MOVE_THRESHOLD = 0.00005; // 从0.0001降低到0.00005,确保更持续的移动
|
||||||
|
const CONTINUOUS_MOVEMENT = true; // 启用连续移动模式
|
||||||
|
const MIN_SPEED = 2.0; // 从1.5增加到2.0,提高最低保持速度
|
||||||
|
const PREDICTION_DECAY_RATE = 0.998; // 从0.995进一步增加到0.998,减缓预测衰减
|
||||||
|
const PATH_PREDICTION_ENABLED = true; // 启用路径预测
|
||||||
|
const PATH_PREDICTION_POINTS = 16; // 从12增加到16,使用更多历史点
|
||||||
|
const CONTINUOUS_MOVEMENT_THRESHOLD = 500; // 从1000ms降低到500ms,更快触发连续移动
|
||||||
|
|
||||||
|
// 缓动函数 - 平滑的加减速
|
||||||
|
function easeInOutQuad(t) {
|
||||||
|
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓动函数 - 更自然的启动
|
||||||
|
function easeOutCubic(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓动函数 - 更自然的减速
|
||||||
|
function easeOutQuint(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动动画循环
|
||||||
|
function startAnimationLoop() {
|
||||||
|
// 确保只有一个动画循环在运行
|
||||||
|
if (animationFrameId.value) {
|
||||||
|
cancelAnimationFrame(animationFrameId.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAnimationTime.value = 0; // 重置时间,确保第一帧计算正确
|
||||||
|
|
||||||
|
const animate = (timestamp) => {
|
||||||
|
if (!lastAnimationTime.value) lastAnimationTime.value = timestamp;
|
||||||
|
const deltaTime = timestamp - lastAnimationTime.value;
|
||||||
|
lastAnimationTime.value = timestamp;
|
||||||
|
|
||||||
|
// 限制deltaTime,提高最小帧率到60fps
|
||||||
|
const clampedDeltaTime = Math.min(deltaTime, 16.67); // 约60fps的最小帧率
|
||||||
|
|
||||||
|
updateVehicleAnimations(clampedDeltaTime);
|
||||||
|
|
||||||
|
animationFrameId.value = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
animationFrameId.value = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新车辆动画
|
||||||
|
function updateVehicleAnimations(deltaTime) {
|
||||||
|
if (!props.vehicleSource || !props.map) return;
|
||||||
|
|
||||||
|
const currentTime = performance.now();
|
||||||
|
|
||||||
|
Object.keys(props.vehicles).forEach(id => {
|
||||||
|
const vehicle = props.vehicles[id];
|
||||||
|
if (!vehicle) return;
|
||||||
|
|
||||||
|
const animData = vehicleAnimations.value[id];
|
||||||
|
if (!animData) return;
|
||||||
|
|
||||||
|
// 确保车辆总是在移动,即使没有新的位置更新
|
||||||
|
ensureContinuousMovement(animData, vehicle, currentTime, deltaTime);
|
||||||
|
|
||||||
|
const timeSinceLastUpdate = currentTime - animData.lastUpdated;
|
||||||
|
|
||||||
|
// 1. 计算目标位置(基于预测)
|
||||||
|
let targetPosition = [...animData.targetPosition];
|
||||||
|
let targetHeading = animData.targetHeading;
|
||||||
|
|
||||||
|
// 平滑速度过渡 - 当前速度逐渐接近目标速度
|
||||||
|
if (animData.currentSpeed === undefined) {
|
||||||
|
animData.currentSpeed = animData.speed;
|
||||||
|
} else {
|
||||||
|
// 使用更平滑的速度过渡
|
||||||
|
const speedSmoothingFactor = vehicle.speedViolation ? 0.96 : SPEED_SMOOTHING;
|
||||||
|
animData.currentSpeed = animData.currentSpeed * speedSmoothingFactor +
|
||||||
|
animData.speed * (1 - speedSmoothingFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentSpeed = animData.currentSpeed;
|
||||||
|
|
||||||
|
// 根据时间应用不同程度的预测
|
||||||
|
if (timeSinceLastUpdate > PREDICTION_THRESHOLD && animData.predictionVector) {
|
||||||
|
// 计算预测因子,随时间增长但有上限
|
||||||
|
const predictTime = Math.min(timeSinceLastUpdate, MAX_PREDICTION_TIME);
|
||||||
|
const predictFactor = (predictTime - PREDICTION_THRESHOLD) / 1000 * 0.8;
|
||||||
|
|
||||||
|
// 应用预测向量,考虑当前速度
|
||||||
|
const speedFactor = Math.min(1.2, currentSpeed / 25); // 速度越高预测越远
|
||||||
|
|
||||||
|
// 计算预测位置
|
||||||
|
const predictedPosition = [
|
||||||
|
animData.targetPosition[0] + animData.predictionVector[0] * predictFactor * speedFactor,
|
||||||
|
animData.targetPosition[1] + animData.predictionVector[1] * predictFactor * speedFactor
|
||||||
|
];
|
||||||
|
|
||||||
|
// 平滑过渡到预测位置
|
||||||
|
targetPosition = predictedPosition;
|
||||||
|
|
||||||
|
// 预测时逐渐降低速度,但不要完全停止
|
||||||
|
if (predictTime > 2000) {
|
||||||
|
// 长时间无更新,缓慢降低速度,但保持最低速度
|
||||||
|
const slowdownFactor = Math.max(0.5, 1 - (predictTime - 2000) / 10000);
|
||||||
|
currentSpeed = Math.max(currentSpeed * slowdownFactor, MIN_SPEED); // 确保最低速度不为0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 计算当前位置到目标位置的向量
|
||||||
|
const dx = targetPosition[0] - animData.position[0];
|
||||||
|
const dy = targetPosition[1] - animData.position[1];
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// 3. 如果距离足够远或启用了连续移动,应用平滑移动
|
||||||
|
if (distance > MIN_MOVE_THRESHOLD || CONTINUOUS_MOVEMENT) {
|
||||||
|
// 计算理想移动距离(基于速度和时间)
|
||||||
|
const idealDistance = (currentSpeed * 1000 / 3600) * (deltaTime / 1000);
|
||||||
|
|
||||||
|
// 根据距离动态调整移动比例
|
||||||
|
let moveRatio;
|
||||||
|
|
||||||
|
// 超速车辆使用更平滑的移动
|
||||||
|
const positionSmoothingFactor = vehicle.speedViolation ? POSITION_SMOOTHING * 0.8 : POSITION_SMOOTHING;
|
||||||
|
|
||||||
|
if (distance < idealDistance * 1.5) {
|
||||||
|
// 接近目标时减速(使用更强的缓动)
|
||||||
|
moveRatio = Math.min(positionSmoothingFactor * 1.5, idealDistance / distance);
|
||||||
|
moveRatio = easeOutQuint(moveRatio);
|
||||||
|
} else {
|
||||||
|
// 正常行驶时使用标准缓动
|
||||||
|
moveRatio = Math.min(positionSmoothingFactor, idealDistance / distance);
|
||||||
|
moveRatio = easeOutCubic(moveRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复逻辑:确保最小移动和惯性移动是互斥的
|
||||||
|
// 如果距离很小且有预测向量,使用预测向量进行微小移动
|
||||||
|
if (distance < MIN_MOVE_THRESHOLD && animData.predictionVector) {
|
||||||
|
// 使用预测向量作为移动方向,但使用很小的移动量
|
||||||
|
const minMoveAmount = (currentSpeed * 1000 / 3600) * (deltaTime / 1000) * 0.1;
|
||||||
|
|
||||||
|
// 归一化预测向量
|
||||||
|
const predLen = Math.sqrt(
|
||||||
|
animData.predictionVector[0] * animData.predictionVector[0] +
|
||||||
|
animData.predictionVector[1] * animData.predictionVector[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (predLen > 0) {
|
||||||
|
const normPredX = animData.predictionVector[0] / predLen;
|
||||||
|
const normPredY = animData.predictionVector[1] / predLen;
|
||||||
|
|
||||||
|
// 应用最小移动量
|
||||||
|
animData.position[0] += normPredX * minMoveAmount;
|
||||||
|
animData.position[1] += normPredY * minMoveAmount;
|
||||||
|
|
||||||
|
// 保存本次移动向量用于下一帧惯性计算
|
||||||
|
animData.lastDx = normPredX * minMoveAmount;
|
||||||
|
animData.lastDy = normPredY * minMoveAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 距离足够大时,应用惯性移动
|
||||||
|
else {
|
||||||
|
if (animData.lastDx !== undefined && animData.lastDy !== undefined) {
|
||||||
|
// 超速车辆使用更强的惯性
|
||||||
|
const inertiaFactor = vehicle.speedViolation ? INERTIA_FACTOR * 1.1 : INERTIA_FACTOR;
|
||||||
|
|
||||||
|
const inertiaX = animData.lastDx * inertiaFactor;
|
||||||
|
const inertiaY = animData.lastDy * inertiaFactor;
|
||||||
|
|
||||||
|
// 混合新方向和惯性方向
|
||||||
|
const newDx = dx * moveRatio;
|
||||||
|
const newDy = dy * moveRatio;
|
||||||
|
|
||||||
|
// 根据速度和距离调整惯性影响
|
||||||
|
const inertiaWeight = Math.min(0.85, currentSpeed / 70) *
|
||||||
|
Math.min(1.0, distance / 8); // 调整惯性权重计算
|
||||||
|
|
||||||
|
// 计算新位置(混合惯性和目标方向)
|
||||||
|
const nextX = animData.position[0] + newDx * (1 - inertiaWeight) + inertiaX * inertiaWeight;
|
||||||
|
const nextY = animData.position[1] + newDy * (1 - inertiaWeight) + inertiaY * inertiaWeight;
|
||||||
|
|
||||||
|
// 应用位置更新
|
||||||
|
animData.position[0] = nextX;
|
||||||
|
animData.position[1] = nextY;
|
||||||
|
|
||||||
|
// 保存本次移动向量用于下一帧惯性计算
|
||||||
|
animData.lastDx = nextX - animData.position[0];
|
||||||
|
animData.lastDy = nextY - animData.position[1];
|
||||||
|
} else {
|
||||||
|
// 首次移动,没有惯性
|
||||||
|
animData.position[0] += dx * moveRatio;
|
||||||
|
animData.position[1] += dy * moveRatio;
|
||||||
|
|
||||||
|
// 初始化移动向量
|
||||||
|
animData.lastDx = dx * moveRatio;
|
||||||
|
animData.lastDy = dy * moveRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新地图上的要素位置
|
||||||
|
const feature = props.vehicleSource.getFeatureById(id);
|
||||||
|
if (feature) {
|
||||||
|
feature.getGeometry().setCoordinates(animData.position);
|
||||||
|
|
||||||
|
// 更新车辆对象中的位置信息,确保标签能够正确跟随
|
||||||
|
if (props.vehicles[id]) {
|
||||||
|
props.vehicles[id].position = animData.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际移动方向(用于更自然的转向)
|
||||||
|
if (Math.abs(animData.lastDx) > 0.001 || Math.abs(animData.lastDy) > 0.001) {
|
||||||
|
const movementHeading = (Math.atan2(animData.lastDy, animData.lastDx) * 180 / Math.PI + 90) % 360;
|
||||||
|
|
||||||
|
// 平滑过渡到新的移动方向
|
||||||
|
if (animData.movementHeading === undefined) {
|
||||||
|
animData.movementHeading = movementHeading;
|
||||||
|
} else {
|
||||||
|
// 计算角度差,确保走最短路径
|
||||||
|
const headingDiff = ((movementHeading - animData.movementHeading + 540) % 360) - 180;
|
||||||
|
animData.movementHeading = (animData.movementHeading + headingDiff * 0.2 + 360) % 360;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 平滑转向逻辑 - 考虑移动方向和目标方向
|
||||||
|
// 首先确定最佳目标方向(结合目标朝向和实际移动方向)
|
||||||
|
let bestTargetHeading = targetHeading;
|
||||||
|
|
||||||
|
// 如果有移动方向且速度足够高,部分考虑移动方向
|
||||||
|
if (animData.movementHeading !== undefined && currentSpeed > 5) {
|
||||||
|
// 速度越高,移动方向的权重越大
|
||||||
|
const movementWeight = Math.min(0.6, currentSpeed / 70);
|
||||||
|
|
||||||
|
// 计算角度差,确保选择最短路径
|
||||||
|
const headingDiff = ((animData.movementHeading - targetHeading + 540) % 360) - 180;
|
||||||
|
|
||||||
|
// 如果移动方向和目标方向相差不大,部分采用移动方向
|
||||||
|
if (Math.abs(headingDiff) < 100) {
|
||||||
|
bestTargetHeading = targetHeading + headingDiff * movementWeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用平滑转向
|
||||||
|
const headingDiff = bestTargetHeading - animData.heading;
|
||||||
|
if (Math.abs(headingDiff) > 0.1) {
|
||||||
|
// 归一化角度差值(确保走最短路径)
|
||||||
|
const normalizedDiff = ((headingDiff + 180) % 360) - 180;
|
||||||
|
|
||||||
|
// 基于速度动态调整转向速率
|
||||||
|
// 低速时转向快,高速时转向慢
|
||||||
|
const turnRate = MAX_TURN_RATE - (MAX_TURN_RATE - MIN_TURN_RATE) * Math.min(1, currentSpeed / 60);
|
||||||
|
|
||||||
|
// 超速车辆转向更平滑
|
||||||
|
const actualTurnRate = vehicle.speedViolation ? turnRate * 0.8 : turnRate;
|
||||||
|
|
||||||
|
// 计算本帧转向量
|
||||||
|
const turnAmount = Math.sign(normalizedDiff) *
|
||||||
|
Math.min(Math.abs(normalizedDiff), actualTurnRate * deltaTime / 1000);
|
||||||
|
|
||||||
|
// 应用转向
|
||||||
|
animData.heading += turnAmount;
|
||||||
|
animData.heading = (animData.heading + 360) % 360;
|
||||||
|
|
||||||
|
// 更新车辆样式(朝向)
|
||||||
|
const feature = props.vehicleSource.getFeatureById(id);
|
||||||
|
if (feature && props.getVehicleStyle) {
|
||||||
|
// 减少样式更新频率,避免闪烁
|
||||||
|
if (!animData.lastStyleUpdateTime || currentTime - animData.lastStyleUpdateTime > 500) {
|
||||||
|
feature.setStyle(props.getVehicleStyle(id, currentSpeed, animData.heading));
|
||||||
|
animData.lastStyleUpdateTime = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前速度用于下一帧
|
||||||
|
animData.currentSpeed = currentSpeed;
|
||||||
|
|
||||||
|
// 更新历史轨迹
|
||||||
|
updateVehiclePathHistory(id, animData, currentTime);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新车辆历史轨迹
|
||||||
|
function updateVehiclePathHistory(id, animData, currentTime) {
|
||||||
|
// 初始化历史轨迹数组(如果不存在)
|
||||||
|
if (!animData.pathHistory) {
|
||||||
|
animData.pathHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每150ms记录一次位置,提高记录频率
|
||||||
|
if (!animData.lastPathRecordTime || currentTime - animData.lastPathRecordTime > 150) {
|
||||||
|
// 添加当前位置到历史轨迹
|
||||||
|
animData.pathHistory.push({
|
||||||
|
time: currentTime,
|
||||||
|
position: [...animData.position],
|
||||||
|
heading: animData.heading,
|
||||||
|
speed: animData.currentSpeed
|
||||||
|
});
|
||||||
|
|
||||||
|
// 增加历史轨迹长度,提供更多数据点用于预测
|
||||||
|
if (animData.pathHistory.length > PATH_PREDICTION_POINTS + 4) {
|
||||||
|
animData.pathHistory.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
animData.lastPathRecordTime = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保车辆持续移动,即使没有新的位置更新
|
||||||
|
function ensureContinuousMovement(animData, vehicle, currentTime, deltaTime) {
|
||||||
|
// 检查上次更新时间,使用更低的阈值
|
||||||
|
const timeSinceLastUpdate = currentTime - animData.lastUpdated;
|
||||||
|
|
||||||
|
// 如果超过阈值没有位置更新,且车辆当前速度大于0
|
||||||
|
if (timeSinceLastUpdate > CONTINUOUS_MOVEMENT_THRESHOLD && animData.currentSpeed > 0) {
|
||||||
|
// 如果启用了路径预测,使用历史轨迹预测未来路径
|
||||||
|
if (PATH_PREDICTION_ENABLED && animData.pathHistory && animData.pathHistory.length >= 3) {
|
||||||
|
const predictedPath = predictFuturePath(animData);
|
||||||
|
if (predictedPath) {
|
||||||
|
// 平滑过渡到预测向量,而不是直接替换
|
||||||
|
if (!animData.predictionVector) {
|
||||||
|
animData.predictionVector = predictedPath.vector;
|
||||||
|
} else {
|
||||||
|
// 平滑混合当前预测向量和新预测向量,使用更平滑的过渡
|
||||||
|
animData.predictionVector = [
|
||||||
|
animData.predictionVector[0] * 0.8 + predictedPath.vector[0] * 0.2, // 从0.7/0.3调整为0.8/0.2
|
||||||
|
animData.predictionVector[1] * 0.8 + predictedPath.vector[1] * 0.2
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平滑过渡到预测位置,使用更平滑的过渡
|
||||||
|
animData.targetPosition = [
|
||||||
|
animData.targetPosition[0] * 0.8 + predictedPath.position[0] * 0.2, // 从0.7/0.3调整为0.8/0.2
|
||||||
|
animData.targetPosition[1] * 0.8 + predictedPath.position[1] * 0.2
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果没有预测向量或预测向量太小,创建一个基于当前朝向的预测向量
|
||||||
|
else if (!animData.predictionVector ||
|
||||||
|
(Math.abs(animData.predictionVector[0]) < 0.001 && Math.abs(animData.predictionVector[1]) < 0.001)) {
|
||||||
|
|
||||||
|
// 根据车辆朝向创建预测向量
|
||||||
|
const headingRad = (animData.heading - 90) * Math.PI / 180;
|
||||||
|
const predX = Math.cos(headingRad) * animData.currentSpeed * 0.18; // 从0.15增加到0.18
|
||||||
|
const predY = Math.sin(headingRad) * animData.currentSpeed * 0.18;
|
||||||
|
|
||||||
|
animData.predictionVector = [predX, predY];
|
||||||
|
|
||||||
|
// 更新目标位置为当前位置加上预测向量
|
||||||
|
animData.targetPosition = [
|
||||||
|
animData.position[0] + predX * 15, // 从12增加到15
|
||||||
|
animData.position[1] + predY * 15
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// 预测向量存在但需要缓慢衰减,避免突然停止
|
||||||
|
// 使用更高的衰减率
|
||||||
|
animData.predictionVector = [
|
||||||
|
animData.predictionVector[0] * PREDICTION_DECAY_RATE,
|
||||||
|
animData.predictionVector[1] * PREDICTION_DECAY_RATE
|
||||||
|
];
|
||||||
|
|
||||||
|
// 更新目标位置
|
||||||
|
animData.targetPosition = [
|
||||||
|
animData.position[0] + animData.predictionVector[0] * 15, // 从12增加到15
|
||||||
|
animData.position[1] + animData.predictionVector[1] * 15
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓慢降低速度,但保持更高的最低速度
|
||||||
|
if (!animData.lastSpeedReduction || currentTime - animData.lastSpeedReduction > 1000) {
|
||||||
|
animData.speed = Math.max(animData.speed * 0.98, MIN_SPEED); // 从0.97增加到0.98,进一步减缓速度降低率
|
||||||
|
animData.lastSpeedReduction = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于历史轨迹预测未来路径
|
||||||
|
function predictFuturePath(animData) {
|
||||||
|
if (!animData.pathHistory || animData.pathHistory.length < 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最近的几个历史点
|
||||||
|
const history = animData.pathHistory;
|
||||||
|
const points = history.length;
|
||||||
|
|
||||||
|
// 计算平均移动向量
|
||||||
|
let totalDx = 0;
|
||||||
|
let totalDy = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
|
||||||
|
// 从最近的点开始,给予更高权重
|
||||||
|
for (let i = points - 2; i >= 0; i--) {
|
||||||
|
const curr = history[i + 1];
|
||||||
|
const prev = history[i];
|
||||||
|
|
||||||
|
// 计算两点之间的向量
|
||||||
|
const dx = curr.position[0] - prev.position[0];
|
||||||
|
const dy = curr.position[1] - prev.position[1];
|
||||||
|
|
||||||
|
// 计算时间差
|
||||||
|
const dt = curr.time - prev.time;
|
||||||
|
|
||||||
|
// 改进权重计算,使用指数衰减而不是线性衰减
|
||||||
|
// 这会使最近的点有更大的影响力
|
||||||
|
const weight = Math.pow(0.8, (points - i - 1)) / Math.max(1, dt / 1000);
|
||||||
|
|
||||||
|
totalDx += dx * weight;
|
||||||
|
totalDy += dy * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有有效的移动向量,返回null
|
||||||
|
if (totalWeight <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算加权平均向量
|
||||||
|
const avgDx = totalDx / totalWeight;
|
||||||
|
const avgDy = totalDy / totalWeight;
|
||||||
|
|
||||||
|
// 计算向量长度
|
||||||
|
const vectorLength = Math.sqrt(avgDx * avgDx + avgDy * avgDy);
|
||||||
|
|
||||||
|
// 如果向量太小,返回null
|
||||||
|
if (vectorLength < 0.001) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归一化向量并应用当前速度
|
||||||
|
const normalizedDx = avgDx / vectorLength;
|
||||||
|
const normalizedDy = avgDy / vectorLength;
|
||||||
|
|
||||||
|
// 根据当前速度缩放向量,增加平滑因子
|
||||||
|
const speedFactor = animData.currentSpeed * 0.12; // 增加到0.12
|
||||||
|
const predictionVector = [
|
||||||
|
normalizedDx * speedFactor,
|
||||||
|
normalizedDy * speedFactor
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算预测位置,增加预测距离
|
||||||
|
const predictedPosition = [
|
||||||
|
animData.position[0] + normalizedDx * speedFactor * 12, // 增加到12
|
||||||
|
animData.position[1] + normalizedDy * speedFactor * 12
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
vector: predictionVector,
|
||||||
|
position: predictedPosition
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化车辆动画数据
|
||||||
|
function initVehicleAnimation(id, coordinates, heading, speed) {
|
||||||
|
vehicleAnimations.value[id] = {
|
||||||
|
position: [...coordinates],
|
||||||
|
targetPosition: [...coordinates],
|
||||||
|
heading: heading,
|
||||||
|
targetHeading: heading,
|
||||||
|
speed: speed,
|
||||||
|
currentSpeed: speed,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
predictionVector: null,
|
||||||
|
lastDx: 0,
|
||||||
|
lastDy: 0,
|
||||||
|
movementHeading: heading,
|
||||||
|
// 添加速度历史记录用于平滑过渡
|
||||||
|
speedHistory: [speed, speed, speed],
|
||||||
|
pathHistory: [], // 初始化路径历史
|
||||||
|
lastPathRecordTime: Date.now() // 初始化路径记录时间
|
||||||
|
};
|
||||||
|
|
||||||
|
vehicleMotionHistory.value[id] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新车辆动画目标
|
||||||
|
function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
|
||||||
|
const animData = vehicleAnimations.value[id] || {};
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 记录历史轨迹点
|
||||||
|
vehicleMotionHistory.value[id] = vehicleMotionHistory.value[id] || [];
|
||||||
|
vehicleMotionHistory.value[id].push({
|
||||||
|
time: now,
|
||||||
|
position: coordinates,
|
||||||
|
heading: heading,
|
||||||
|
speed: speed
|
||||||
|
});
|
||||||
|
|
||||||
|
// 增加历史点数量
|
||||||
|
if (vehicleMotionHistory.value[id].length > 10) { // 从8增加到10
|
||||||
|
vehicleMotionHistory.value[id].shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算预测向量 - 使用更复杂的算法
|
||||||
|
let predictionVector = [0, 0];
|
||||||
|
|
||||||
|
if (vehicleMotionHistory.value[id].length >= 2) {
|
||||||
|
// 基本预测 - 使用最近两点
|
||||||
|
const history = vehicleMotionHistory.value[id];
|
||||||
|
const latest = history[history.length - 1];
|
||||||
|
const previous = history[history.length - 2];
|
||||||
|
|
||||||
|
const dt = Math.max(1, latest.time - previous.time);
|
||||||
|
const dx = (latest.position[0] - previous.position[0]);
|
||||||
|
const dy = (latest.position[1] - previous.position[1]);
|
||||||
|
|
||||||
|
// 基础预测向量 - 考虑时间间隔
|
||||||
|
const baseSpeed = Math.sqrt(dx*dx + dy*dy) * (1000 / dt); // 单位时间内的移动距离
|
||||||
|
const normalizedDx = baseSpeed > 0 ? dx / baseSpeed : 0;
|
||||||
|
const normalizedDy = baseSpeed > 0 ? dy / baseSpeed : 0;
|
||||||
|
|
||||||
|
// 减小预测强度
|
||||||
|
predictionVector = [
|
||||||
|
normalizedDx * (PREDICTION_STRENGTH * 0.9) * baseSpeed,
|
||||||
|
normalizedDy * (PREDICTION_STRENGTH * 0.9) * baseSpeed
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果有更多历史点,使用加权平均提高预测准确性
|
||||||
|
if (vehicleMotionHistory.value[id].length >= 3) {
|
||||||
|
// 使用多个历史点计算平均移动趋势
|
||||||
|
let totalWeight = 0;
|
||||||
|
let weightedDx = 0;
|
||||||
|
let weightedDy = 0;
|
||||||
|
|
||||||
|
// 从最近到最远遍历历史点,使用更多的历史点
|
||||||
|
for (let i = history.length - 2; i >= 0 && i >= history.length - 7; i--) { // 从5增加到7
|
||||||
|
const curr = history[i+1];
|
||||||
|
const prev = history[i];
|
||||||
|
|
||||||
|
const pointDt = Math.max(1, curr.time - prev.time);
|
||||||
|
const pointDx = curr.position[0] - prev.position[0];
|
||||||
|
const pointDy = curr.position[1] - prev.position[1];
|
||||||
|
|
||||||
|
// 使用指数衰减权重
|
||||||
|
const weight = Math.pow(0.85, (history.length - i - 1));
|
||||||
|
totalWeight += weight;
|
||||||
|
|
||||||
|
weightedDx += pointDx * weight;
|
||||||
|
weightedDy += pointDy * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
// 归一化加权向量
|
||||||
|
weightedDx /= totalWeight;
|
||||||
|
weightedDy /= totalWeight;
|
||||||
|
|
||||||
|
// 混合最新向量和加权历史向量,增加历史向量的权重
|
||||||
|
predictionVector = [
|
||||||
|
predictionVector[0] * 0.6 + weightedDx * PREDICTION_STRENGTH * 0.4, // 从0.7/0.3调整为0.6/0.4
|
||||||
|
predictionVector[1] * 0.6 + weightedDy * PREDICTION_STRENGTH * 0.4
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据速度调整预测强度,使用更平滑的曲线
|
||||||
|
const speedFactor = Math.min(1.8, Math.pow(speed / 15, 0.8)); // 使用幂函数使曲线更平滑
|
||||||
|
predictionVector = [
|
||||||
|
predictionVector[0] * speedFactor,
|
||||||
|
predictionVector[1] * speedFactor
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果车辆几乎静止,减弱预测但保持最低预测强度
|
||||||
|
if (speed < 2) {
|
||||||
|
predictionVector = [
|
||||||
|
predictionVector[0] * 0.4, // 从0.3增加到0.4
|
||||||
|
predictionVector[1] * 0.4
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存速度历史用于平滑过渡,增加历史点
|
||||||
|
if (!animData.speedHistory) {
|
||||||
|
animData.speedHistory = [speed, speed, speed, speed]; // 增加一个点
|
||||||
|
} else {
|
||||||
|
animData.speedHistory.push(speed);
|
||||||
|
if (animData.speedHistory.length > 4) { // 从3增加到4
|
||||||
|
animData.speedHistory.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算平滑速度(避免速度突变)
|
||||||
|
const smoothedSpeed = animData.speedHistory.reduce((sum, s) => sum + s, 0) /
|
||||||
|
animData.speedHistory.length;
|
||||||
|
|
||||||
|
// 更新动画数据 - 保留当前位置,只更新目标位置
|
||||||
|
vehicleAnimations.value[id] = {
|
||||||
|
...animData,
|
||||||
|
targetPosition: coordinates,
|
||||||
|
targetHeading: heading,
|
||||||
|
position: animData.position || coordinates,
|
||||||
|
speed: smoothedSpeed, // 使用平滑后的速度
|
||||||
|
lastUpdated: now,
|
||||||
|
predictionVector,
|
||||||
|
speedHistory: animData.speedHistory
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止动画循环
|
||||||
|
function stopAnimationLoop() {
|
||||||
|
if (animationFrameId.value) {
|
||||||
|
cancelAnimationFrame(animationFrameId.value);
|
||||||
|
animationFrameId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置动画数据
|
||||||
|
function resetAnimations() {
|
||||||
|
vehicleAnimations.value = {};
|
||||||
|
vehicleMotionHistory.value = {};
|
||||||
|
|
||||||
|
Object.keys(props.vehicles).forEach(id => {
|
||||||
|
const vehicle = props.vehicles[id];
|
||||||
|
if (vehicle) {
|
||||||
|
vehicleAnimations.value[id] = {
|
||||||
|
position: [...vehicle.position],
|
||||||
|
targetPosition: [...vehicle.position],
|
||||||
|
heading: vehicle.heading,
|
||||||
|
targetHeading: vehicle.heading,
|
||||||
|
speed: vehicle.speed,
|
||||||
|
currentSpeed: vehicle.speed,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
predictionVector: null,
|
||||||
|
lastDx: 0,
|
||||||
|
lastDy: 0,
|
||||||
|
movementHeading: vehicle.heading,
|
||||||
|
speedHistory: [vehicle.speed, vehicle.speed, vehicle.speed],
|
||||||
|
pathHistory: [], // 重置路径历史
|
||||||
|
lastPathRecordTime: Date.now() // 重置路径记录时间
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时启动动画
|
||||||
|
onMounted(() => {
|
||||||
|
startAnimationLoop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAnimationLoop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
startAnimationLoop,
|
||||||
|
stopAnimationLoop,
|
||||||
|
resetAnimations,
|
||||||
|
initVehicleAnimation,
|
||||||
|
updateVehicleAnimationTarget,
|
||||||
|
vehicleAnimations,
|
||||||
|
vehicleMotionHistory
|
||||||
|
});
|
||||||
|
</script>
|
||||||
206
src/components/map/controls/VehicleDetailPopup.vue
Normal file
206
src/components/map/controls/VehicleDetailPopup.vue
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<!-- 点击车辆详情弹窗 -->
|
||||||
|
<template>
|
||||||
|
<div class="vehicle-detail-container" v-if="visible" :style="popupStyle">
|
||||||
|
<div class="vehicle-detail-box">
|
||||||
|
<div class="vehicle-detail-header">
|
||||||
|
<span>详情</span>
|
||||||
|
<img class="vehicle-detail-close" src="../../../assets/images/close_icon.png" alt="" @click="close" />
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-content">
|
||||||
|
<div class="vehicle-detail-title">
|
||||||
|
<span>{{ detail.id }} ({{ detail.type }})</span>
|
||||||
|
<div class="vehicle-detail-status-container">
|
||||||
|
<div class="vehicle-detail-status" :class="{'status-running': detail.status === '任务中', 'status-idle': detail.status === '待命'}">{{ detail.status }}</div>
|
||||||
|
<div class="vehicle-detail-online">在线</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-container">
|
||||||
|
<div class="vehicle-detail-info-row">
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">任务开始时间:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.startTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">当前位置:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.currentLocation }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-row">
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">任务起点:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.startLocation }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">任务终点:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.endLocation }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-row">
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">行驶总里程:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.totalDistance }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">电量剩余:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.battery }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-row">
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">负责人:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.manager }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-detail-info-item">
|
||||||
|
<span class="vehicle-info-label">联系电话:</span>
|
||||||
|
<span class="vehicle-info-value">{{ detail.phone }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
id: 'QN001',
|
||||||
|
type: '驱鸟车',
|
||||||
|
status: '任务中',
|
||||||
|
startTime: '11-19 11:30',
|
||||||
|
currentLocation: 'A区T3点',
|
||||||
|
startLocation: 'T1航站楼',
|
||||||
|
endLocation: 'T3航站楼',
|
||||||
|
totalDistance: '1.3km',
|
||||||
|
battery: '60%',
|
||||||
|
manager: '张三',
|
||||||
|
phone: '18661910988'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
popupStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
left: '0px',
|
||||||
|
top: '0px'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 车辆详情弹窗样式 */
|
||||||
|
.vehicle-detail-container {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2000;
|
||||||
|
width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-box {
|
||||||
|
background-color: #424851;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom:1px solid #777777;
|
||||||
|
background-color: #424851;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-close {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bolder;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-status {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-online {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: #fff;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-running {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-idle {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-info-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-detail-info-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info-label {
|
||||||
|
color: #EEEEEE;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info-value {
|
||||||
|
color: #EEEEEE;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
299
src/components/map/controls/VehicleLabelSystem.vue
Normal file
299
src/components/map/controls/VehicleLabelSystem.vue
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 标签系统不需要模板内容 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import Overlay from 'ol/Overlay';
|
||||||
|
|
||||||
|
// 定义props
|
||||||
|
const props = defineProps({
|
||||||
|
map: Object,
|
||||||
|
vehicles: Object
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导入背景图片
|
||||||
|
import labelBg from '../../../assets/images/label_bg.png';
|
||||||
|
import airportBg from '../../../assets/images/airport_bg.png';
|
||||||
|
import airportOutBg from '../../../assets/images/airport_out.png';
|
||||||
|
import alarmBg from '../../../assets/images/alarm_bg.png';
|
||||||
|
import warningBg from '../../../assets/images/warning_bg.png';
|
||||||
|
|
||||||
|
// 更新车辆标签
|
||||||
|
function updateVehicleLabel(id, position, speed, violationInfo = null) {
|
||||||
|
if (!props.map || !props.vehicles[id]) return;
|
||||||
|
|
||||||
|
// 先移除可能存在的旧标签,确保不会重复显示
|
||||||
|
removeVehicleLabel(id);
|
||||||
|
|
||||||
|
const vehicle = props.vehicles[id];
|
||||||
|
const isAircraftIn = vehicle.isAircraftIn;
|
||||||
|
const isAircraftOut = vehicle.isAircraftOut;
|
||||||
|
const isUnmannedVehicle = vehicle.isUnmannedVehicle;
|
||||||
|
const isSpecialVehicle = vehicle.isSpecialVehicle;
|
||||||
|
const isShuttleVehicle = vehicle.isShuttleVehicle;
|
||||||
|
const violationType = vehicle.violationType;
|
||||||
|
const hasInfoStatus = vehicle.info;
|
||||||
|
const hasWarningStatus = vehicle.warning;
|
||||||
|
const hasAlarmStatus = vehicle.alarm;
|
||||||
|
const hasCriticalStatus = vehicle.critical;
|
||||||
|
const hasSpeedViolation = vehicle.speedViolation;
|
||||||
|
|
||||||
|
let typeKey = vehicle.type;
|
||||||
|
if (isAircraftIn) typeKey = 'AIRCRAFT_IN';
|
||||||
|
else if (isAircraftOut) typeKey = 'AIRCRAFT_OUT';
|
||||||
|
else if (isUnmannedVehicle) typeKey = 'UNMANNED_VEHICLE';
|
||||||
|
else if (isSpecialVehicle) typeKey = 'SPECIAL_VEHICLE';
|
||||||
|
else if (isShuttleVehicle) typeKey = 'SHUTTLE_VEHICLE';
|
||||||
|
|
||||||
|
let backgroundImage;
|
||||||
|
|
||||||
|
// 首先检查告警状态
|
||||||
|
if (hasSpeedViolation) {
|
||||||
|
// 超速车辆使用warning_bg.png背景
|
||||||
|
backgroundImage = warningBg;
|
||||||
|
} else if (violationType || hasInfoStatus || hasWarningStatus || hasCriticalStatus || hasAlarmStatus) {
|
||||||
|
backgroundImage = warningBg; // 使用告警背景
|
||||||
|
} else if (isAircraftIn) {
|
||||||
|
// 滑入航空器(CA开头)使用airport_out.png文本背景
|
||||||
|
backgroundImage = airportOutBg;
|
||||||
|
} else if (isAircraftOut) {
|
||||||
|
// 滑出航空器(MU开头)使用airport_bg.png文本背景
|
||||||
|
backgroundImage = airportBg;
|
||||||
|
} else if (isUnmannedVehicle) {
|
||||||
|
// 无人车使用label_bg.png文本背景
|
||||||
|
backgroundImage = labelBg;
|
||||||
|
} else if (isSpecialVehicle) {
|
||||||
|
// 特勤车使用label_bg.png文本背景
|
||||||
|
backgroundImage = labelBg;
|
||||||
|
} else if (isShuttleVehicle) {
|
||||||
|
// 摆渡车使用label_bg.png文本背景
|
||||||
|
backgroundImage = labelBg;
|
||||||
|
} else {
|
||||||
|
// 默认背景
|
||||||
|
backgroundImage = labelBg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelDiv = document.createElement('div');
|
||||||
|
labelDiv.className = `vehicle-label ${isAircraftIn ? 'vehicle-aircraft-in' : ''} ${isAircraftOut ? 'vehicle-aircraft-out' : ''} ${isUnmannedVehicle ? 'vehicle-unmanned' : ''} ${isSpecialVehicle ? 'vehicle-special' : ''} ${isShuttleVehicle ? 'vehicle-shuttle' : ''}`;
|
||||||
|
|
||||||
|
// 添加告警状态类
|
||||||
|
if (violationType) labelDiv.className += ' vehicle-violation';
|
||||||
|
if (hasInfoStatus) labelDiv.className += ' vehicle-info';
|
||||||
|
if (hasWarningStatus) labelDiv.className += ' vehicle-warning';
|
||||||
|
if (hasAlarmStatus) labelDiv.className += ' vehicle-alarm';
|
||||||
|
if (hasCriticalStatus) labelDiv.className += ' vehicle-critical';
|
||||||
|
if (hasSpeedViolation) labelDiv.className += ' vehicle-speed-violation';
|
||||||
|
|
||||||
|
let labelText = '';
|
||||||
|
|
||||||
|
// 超速车辆特殊处理标签内容 - 只显示超速相关信息,避免闪烁和重复显示
|
||||||
|
if (violationInfo && violationInfo.isSpeedViolation && violationInfo.actualValue !== undefined) {
|
||||||
|
// 通过violationInfo传入的超速信息 - 这是主要的超速状态显示方式
|
||||||
|
labelText = `${id} ${violationInfo.actualValue.toFixed(1)} km/h`;
|
||||||
|
if (violationInfo.description) {
|
||||||
|
labelText += `<br>${violationInfo.description}`;
|
||||||
|
}
|
||||||
|
if (violationInfo.limitValue !== undefined) {
|
||||||
|
labelText += `<br>限速: ${violationInfo.limitValue}km/h`;
|
||||||
|
}
|
||||||
|
if (violationInfo.ruleName) {
|
||||||
|
labelText += `<br>根据${violationInfo.ruleName}请减速慢行!`;
|
||||||
|
}
|
||||||
|
} else if (hasSpeedViolation && vehicle.limitValue !== undefined && vehicle.actualValue !== undefined) {
|
||||||
|
// 备用的超速状态显示方式
|
||||||
|
labelText = `${id} ${vehicle.actualValue.toFixed(1)} km/h`;
|
||||||
|
if (vehicle.description) {
|
||||||
|
labelText += `<br>${vehicle.description}`;
|
||||||
|
}
|
||||||
|
labelText += `<br>限速: ${vehicle.limitValue}km/h`;
|
||||||
|
if (vehicle.ruleName) {
|
||||||
|
labelText += `<br>根据${vehicle.ruleName}请减速慢行!`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 正常车辆显示 - 避免显示0.00km/h的闪烁
|
||||||
|
if (!hasSpeedViolation && speed > 0.1) {
|
||||||
|
labelText = `${id} ${speed.toFixed(1)} km/h`;
|
||||||
|
} else if (!hasSpeedViolation) {
|
||||||
|
// 如果速度为0或很小,只显示车辆ID,不显示速度避免闪烁
|
||||||
|
labelText = `${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violationType && vehicle.description) {
|
||||||
|
labelText += `<br>${vehicle.description}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violationType && vehicle.limitValue !== undefined && vehicle.actualValue !== undefined) {
|
||||||
|
labelText += `<br>限速: ${vehicle.limitValue}km/h 实际: ${vehicle.actualValue}km/h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理传入的违规信息(非超速情况)
|
||||||
|
if (violationInfo && !violationInfo.isSpeedViolation) {
|
||||||
|
if (violationInfo.description) {
|
||||||
|
labelText += `<br>${violationInfo.description}`;
|
||||||
|
}
|
||||||
|
if (violationInfo.limitValue !== undefined && violationInfo.actualValue !== undefined) {
|
||||||
|
labelText += `<br>限速: ${violationInfo.limitValue}km/h 实际: ${violationInfo.actualValue}km/h`;
|
||||||
|
}
|
||||||
|
if (violationInfo.ruleName) {
|
||||||
|
labelText += `<br>根据${violationInfo.ruleName}请减速慢行!`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labelDiv.innerHTML = labelText;
|
||||||
|
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
|
||||||
|
labelDiv.style.backgroundSize = '100% 100%';
|
||||||
|
labelDiv.style.padding = '5px 10px';
|
||||||
|
labelDiv.style.color = '#fff';
|
||||||
|
|
||||||
|
const overlay = new Overlay({
|
||||||
|
element: labelDiv,
|
||||||
|
position: position,
|
||||||
|
positioning: 'bottom-center',
|
||||||
|
offset: [0, -30],
|
||||||
|
stopEvent: false,
|
||||||
|
insertFirst: true, // 确保在DOM中优先插入
|
||||||
|
autoPan: false, // 禁用自动平移
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新引用
|
||||||
|
props.vehicles[id].overlay = overlay;
|
||||||
|
props.vehicles[id].labelDiv = labelDiv;
|
||||||
|
|
||||||
|
// 添加到地图
|
||||||
|
props.map.addOverlay(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除车辆标签
|
||||||
|
function removeVehicleLabel(id) {
|
||||||
|
if (!props.map || !props.vehicles[id]) return;
|
||||||
|
|
||||||
|
if (props.vehicles[id].overlay) {
|
||||||
|
props.map.removeOverlay(props.vehicles[id].overlay);
|
||||||
|
props.vehicles[id].overlay = null;
|
||||||
|
props.vehicles[id].labelDiv = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新所有标签位置
|
||||||
|
function updateAllLabels() {
|
||||||
|
Object.keys(props.vehicles).forEach(id => {
|
||||||
|
const vehicle = props.vehicles[id];
|
||||||
|
if (vehicle.feature) {
|
||||||
|
const coordinates = vehicle.feature.getGeometry().getCoordinates();
|
||||||
|
// 如果车辆处于超速状态,不在这里更新标签,避免与超速状态标签冲突
|
||||||
|
if (!vehicle.speedViolation) {
|
||||||
|
updateVehicleLabel(id, coordinates, vehicle.speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置标签可见性
|
||||||
|
function setLabelVisibility(id, visible) {
|
||||||
|
if (!props.map || !props.vehicles[id]) return;
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
// 如果车辆处于超速状态,不在这里更新标签,避免与超速状态标签冲突
|
||||||
|
if (props.vehicles[id].position && !props.vehicles[id].speedViolation) {
|
||||||
|
updateVehicleLabel(id, props.vehicles[id].position, props.vehicles[id].speed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removeVehicleLabel(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
updateVehicleLabel,
|
||||||
|
removeVehicleLabel,
|
||||||
|
updateAllLabels,
|
||||||
|
setLabelVisibility
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 车辆标签样式 */
|
||||||
|
:deep(.vehicle-label) {
|
||||||
|
position: absolute;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
min-width: 80px;
|
||||||
|
min-height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 30;
|
||||||
|
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
|
||||||
|
will-change: transform, left, top;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑入航空器标签样式 */
|
||||||
|
:deep(.vehicle-aircraft-in) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑出航空器标签样式 */
|
||||||
|
:deep(.vehicle-aircraft-out) {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无人车标签样式 */
|
||||||
|
:deep(.vehicle-unmanned) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特勤车标签样式 */
|
||||||
|
:deep(.vehicle-special) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 摆渡车标签样式 */
|
||||||
|
:deep(.vehicle-shuttle) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 告警标签样式 */
|
||||||
|
:deep(.vehicle-alarm) {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预警标签样式 */
|
||||||
|
:deep(.vehicle-warning) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 严重违规标签样式 */
|
||||||
|
:deep(.vehicle-critical) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超速违规标签样式 */
|
||||||
|
:deep(.vehicle-speed-violation) {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: normal;
|
||||||
|
width: 180px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息级别(超速等)标签样式 */
|
||||||
|
:deep(.vehicle-info) {
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: normal;
|
||||||
|
width: 160px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,677 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="vehicle-movement-control">
|
|
||||||
<!-- 连接状态指示器 -->
|
|
||||||
<div class="ws-status" :class="{ connected: wsConnected }">
|
|
||||||
<span v-if="wsConnected">已连接</span>
|
|
||||||
<span v-else>未连接</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- 告警/预警提示框 -->
|
|
||||||
<!-- <div class="alert-container" v-if="alertMessage"> -->
|
|
||||||
<div class="alert-container">
|
|
||||||
<div class="alert-box alert-flash" :class="{'alert-warning': alertType === 'warning', 'alert-danger': alertType === 'alarm'}">
|
|
||||||
<div class="alert-row">
|
|
||||||
<img v-if="alertType === 'alarm'" class="alert-icon" :src="alarmIcon" alt="alarm" />
|
|
||||||
<img v-else-if="alertType === 'warning'" class="alert-icon" :src="warnIcon" alt="warning" />
|
|
||||||
<span class="alert-title">
|
|
||||||
{{ alertType === 'alarm' ? '冲突告警' : '冲突预警' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="alert-content alert-desc">
|
|
||||||
{{ alertMessage }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
|
||||||
import { Vector as VectorSource } from 'ol/source';
|
|
||||||
import { Vector as VectorLayer } from 'ol/layer';
|
|
||||||
import { Style, Icon } from 'ol/style';
|
|
||||||
import Feature from 'ol/Feature';
|
|
||||||
import Point from 'ol/geom/Point';
|
|
||||||
import Overlay from 'ol/Overlay';
|
|
||||||
import { transform } from 'ol/proj';
|
|
||||||
// 导入WebSocketService
|
|
||||||
import WebSocketService, { createWebSocket } from '../../../utils/websocket.js';
|
|
||||||
|
|
||||||
// 为SockJS提供polyfill
|
|
||||||
if (typeof window !== 'undefined' && !window.global) {
|
|
||||||
window.global = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入车辆图标
|
|
||||||
import carIcon from '../../../assets/images/noPeopleCar.png';
|
|
||||||
import aircraftIcon from '../../../assets/images/Aircraft.png';
|
|
||||||
import aircraft1Icon from '../../../assets/images/Aircraft1.png';
|
|
||||||
import labelBg from '../../../assets/images/label_bg.png';
|
|
||||||
import airportBg from '../../../assets/images/airport_bg.png';
|
|
||||||
import airportOutBg from '../../../assets/images/airport_out.png';
|
|
||||||
import alarmBg from '../../../assets/images/alarm_bg.png';
|
|
||||||
import warningBg from '../../../assets/images/warning_bg.png';
|
|
||||||
import alarmIcon from '../../../assets/images/alarm_icon.png';
|
|
||||||
import warnIcon from '../../../assets/images/warn_icon.png';
|
|
||||||
// 定义props接收地图实例
|
|
||||||
const props = defineProps({
|
|
||||||
map: Object
|
|
||||||
});
|
|
||||||
|
|
||||||
// WebSocket连接状态
|
|
||||||
const wsConnected = ref(false);
|
|
||||||
let wsService = null;
|
|
||||||
|
|
||||||
// 车辆数据存储
|
|
||||||
const vehicles = ref({});
|
|
||||||
let vehicleLayer = null;
|
|
||||||
let vehicleSource = null;
|
|
||||||
|
|
||||||
// 告警/预警状态
|
|
||||||
const alertMessage = ref('与航空器Y117距离小于600m!请及时避让');
|
|
||||||
const alertType = ref('warning'); // 'warning' 或 'alarm'
|
|
||||||
let alertTimer = null;
|
|
||||||
|
|
||||||
// 显示告警/预警消息
|
|
||||||
function showAlert(message, type, duration = 5000) {
|
|
||||||
// 清除可能存在的定时器
|
|
||||||
if (alertTimer) {
|
|
||||||
clearTimeout(alertTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置消息和类型
|
|
||||||
alertMessage.value = message;
|
|
||||||
alertType.value = type; // 'warning' 或 'alarm'
|
|
||||||
|
|
||||||
// 设置自动消失
|
|
||||||
alertTimer = setTimeout(() => {
|
|
||||||
alertMessage.value = '';
|
|
||||||
alertType.value = '';
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建车辆图层
|
|
||||||
function createVehicleLayer() {
|
|
||||||
if (!props.map) return;
|
|
||||||
|
|
||||||
// 创建矢量数据源
|
|
||||||
vehicleSource = new VectorSource();
|
|
||||||
|
|
||||||
// 创建矢量图层
|
|
||||||
vehicleLayer = new VectorLayer({
|
|
||||||
source: vehicleSource,
|
|
||||||
zIndex: 20, // 确保在其他图层之上
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加图层到地图
|
|
||||||
props.map.addLayer(vehicleLayer);
|
|
||||||
console.log('车辆移动图层已创建');
|
|
||||||
|
|
||||||
// 设置地图监听器
|
|
||||||
setupMapListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新车辆位置
|
|
||||||
function updateVehiclePosition(vehicleData) {
|
|
||||||
if (!vehicleSource || !props.map) return;
|
|
||||||
|
|
||||||
const { object_id, object_type, position, heading, speed } = vehicleData;
|
|
||||||
console.log(`接收到位置数据: ${object_id}`, position);
|
|
||||||
|
|
||||||
let coordinates;
|
|
||||||
|
|
||||||
|
|
||||||
coordinates = transform(
|
|
||||||
[position.longitude, position.latitude],
|
|
||||||
'EPSG:4326',
|
|
||||||
props.map.getView().getProjection()
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// 检查该车辆是否已存在
|
|
||||||
let feature = vehicleSource.getFeatureById(object_id);
|
|
||||||
|
|
||||||
// 根据车辆ID前缀和类型进行分类
|
|
||||||
// 1. 判断航空器类型
|
|
||||||
const isAircraftIn = object_type.toLowerCase().includes('aircraft') && !object_id.toLowerCase().includes('ac001');
|
|
||||||
const isAircraftOut = object_id.toLowerCase().includes('ac001');
|
|
||||||
const isAircraft = isAircraftIn || isAircraftOut;
|
|
||||||
|
|
||||||
// 2. 判断无人车类型
|
|
||||||
const isUnmannedVehicle = object_type === 'UNMANNED_VEHICLE' || object_id.toLowerCase().startsWith('qn');
|
|
||||||
|
|
||||||
// 3. 判断特勤车类型
|
|
||||||
const isSpecialVehicle = object_id.toLowerCase().startsWith('tq');
|
|
||||||
|
|
||||||
// 4. 判断摆渡车类型
|
|
||||||
const isShuttleVehicle = object_id.toLowerCase().startsWith('bd');
|
|
||||||
|
|
||||||
// 选择图标 - 根据类型选择不同图标
|
|
||||||
let iconSrc;
|
|
||||||
if (isAircraftIn) {
|
|
||||||
iconSrc = aircraftIcon; // 滑入航空器
|
|
||||||
} else if (isAircraftOut) {
|
|
||||||
iconSrc = aircraft1Icon; // 滑出航空器
|
|
||||||
} else {
|
|
||||||
iconSrc = carIcon; // 普通车辆
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建样式
|
|
||||||
const style = new Style({
|
|
||||||
image: new Icon({
|
|
||||||
src: iconSrc,
|
|
||||||
scale: 1.5,
|
|
||||||
anchor: [0.5, 0.5],
|
|
||||||
rotation: ((heading - 72) * Math.PI) / 180,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!feature) {
|
|
||||||
// 创建新的Feature
|
|
||||||
feature = new Feature({
|
|
||||||
geometry: new Point(coordinates),
|
|
||||||
name: `${object_type} ${object_id}`,
|
|
||||||
type: object_type,
|
|
||||||
speed: speed,
|
|
||||||
isAircraftIn: isAircraftIn,
|
|
||||||
isAircraftOut: isAircraftOut,
|
|
||||||
isAircraft: isAircraft,
|
|
||||||
isUnmannedVehicle: isUnmannedVehicle,
|
|
||||||
isSpecialVehicle: isSpecialVehicle,
|
|
||||||
isShuttleVehicle: isShuttleVehicle
|
|
||||||
});
|
|
||||||
|
|
||||||
feature.setId(object_id);
|
|
||||||
feature.setStyle(style);
|
|
||||||
vehicleSource.addFeature(feature);
|
|
||||||
|
|
||||||
// 存储车辆信息
|
|
||||||
vehicles.value[object_id] = {
|
|
||||||
id: object_id,
|
|
||||||
type: object_type,
|
|
||||||
position: coordinates,
|
|
||||||
heading: heading,
|
|
||||||
speed: speed,
|
|
||||||
feature: feature,
|
|
||||||
isAircraftIn: isAircraftIn,
|
|
||||||
isAircraftOut: isAircraftOut,
|
|
||||||
isAircraft: isAircraft,
|
|
||||||
isUnmannedVehicle: isUnmannedVehicle,
|
|
||||||
isSpecialVehicle: isSpecialVehicle,
|
|
||||||
isShuttleVehicle: isShuttleVehicle
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建标签 - 使用updateVehicleLabel来确保一致性
|
|
||||||
updateVehicleLabel(object_id, coordinates, speed);
|
|
||||||
} else {
|
|
||||||
// 更新现有Feature
|
|
||||||
feature.getGeometry().setCoordinates(coordinates);
|
|
||||||
feature.setStyle(style);
|
|
||||||
|
|
||||||
// 更新存储的车辆信息
|
|
||||||
vehicles.value[object_id] = {
|
|
||||||
...vehicles.value[object_id],
|
|
||||||
position: coordinates,
|
|
||||||
heading: heading,
|
|
||||||
speed: speed,
|
|
||||||
isAircraftIn: isAircraftIn,
|
|
||||||
isAircraftOut: isAircraftOut,
|
|
||||||
isAircraft: isAircraft,
|
|
||||||
isUnmannedVehicle: isUnmannedVehicle,
|
|
||||||
isSpecialVehicle: isSpecialVehicle,
|
|
||||||
isShuttleVehicle: isShuttleVehicle
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新标签位置和内容
|
|
||||||
updateVehicleLabel(object_id, coordinates, speed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新车辆标签
|
|
||||||
function updateVehicleLabel(id, position, speed) {
|
|
||||||
if (!props.map || !vehicles.value[id]) return;
|
|
||||||
|
|
||||||
console.log(`更新标签位置: ${id}, 位置: [${position[0]}, ${position[1]}]`);
|
|
||||||
|
|
||||||
// 如果存在旧的overlay,先移除它
|
|
||||||
if (vehicles.value[id].overlay) {
|
|
||||||
props.map.removeOverlay(vehicles.value[id].overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取车辆类型信息
|
|
||||||
const vehicle = vehicles.value[id];
|
|
||||||
const isAircraftIn = vehicle.isAircraftIn;
|
|
||||||
const isAircraftOut = vehicle.isAircraftOut;
|
|
||||||
const isUnmannedVehicle = vehicle.isUnmannedVehicle;
|
|
||||||
const isSpecialVehicle = vehicle.isSpecialVehicle;
|
|
||||||
const isShuttleVehicle = vehicle.isShuttleVehicle;
|
|
||||||
const alarm = vehicle.alarm;
|
|
||||||
const warning = vehicle.warning;
|
|
||||||
|
|
||||||
|
|
||||||
// 选择背景图片 - 根据车辆类型选择不同背景
|
|
||||||
let backgroundImage;
|
|
||||||
if (isAircraftOut) {
|
|
||||||
backgroundImage = airportOutBg; // 滑出航空器背景 - 黄色
|
|
||||||
} else if (isAircraftIn) {
|
|
||||||
backgroundImage = airportBg; // 滑入航空器背景 - 蓝色
|
|
||||||
}else if(alarm){
|
|
||||||
backgroundImage = alarmBg; // 告警背景
|
|
||||||
} else if(warning){
|
|
||||||
backgroundImage = warningBg; // 预警背景
|
|
||||||
}else {
|
|
||||||
backgroundImage = labelBg; // 普通车辆背景
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新创建标签元素
|
|
||||||
const labelDiv = document.createElement('div');
|
|
||||||
labelDiv.className = `vehicle-label ${isAircraftIn ? 'vehicle-aircraft-in' : ''} ${isAircraftOut ? 'vehicle-aircraft-out' : ''} ${isUnmannedVehicle ? 'vehicle-unmanned' : ''} ${isSpecialVehicle ? 'vehicle-special' : ''} ${isShuttleVehicle ? 'vehicle-shuttle' : ''}`;
|
|
||||||
|
|
||||||
// 根据车辆类型设置不同的标签内容
|
|
||||||
let labelText = '';
|
|
||||||
labelText = `${id} ${speed.toFixed(2)} km/h`;
|
|
||||||
labelDiv.innerHTML = labelText;
|
|
||||||
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
|
|
||||||
labelDiv.style.backgroundSize = '100% 100%';
|
|
||||||
labelDiv.style.color = '#fff';
|
|
||||||
labelDiv.style.padding = '5px 10px';
|
|
||||||
|
|
||||||
// 创建新的Overlay
|
|
||||||
const overlay = new Overlay({
|
|
||||||
element: labelDiv,
|
|
||||||
position: position,
|
|
||||||
positioning: 'bottom-center', // 标签底部中心对准位置点
|
|
||||||
offset: [0, -30], // 向上偏移,确保在图标上方
|
|
||||||
stopEvent: false,
|
|
||||||
insertFirst: true, // 确保在DOM中优先插入
|
|
||||||
autoPan: false, // 禁用自动平移
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新引用
|
|
||||||
vehicles.value[id].overlay = overlay;
|
|
||||||
vehicles.value[id].labelDiv = labelDiv;
|
|
||||||
|
|
||||||
// 添加到地图
|
|
||||||
props.map.addOverlay(overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听地图变化,确保标签位置正确更新
|
|
||||||
function setupMapListeners() {
|
|
||||||
if (!props.map) return;
|
|
||||||
|
|
||||||
// 监听地图移动结束事件
|
|
||||||
props.map.on('moveend', () => {
|
|
||||||
// 更新所有标签位置
|
|
||||||
Object.keys(vehicles.value).forEach(id => {
|
|
||||||
const vehicle = vehicles.value[id];
|
|
||||||
if (vehicle.feature) {
|
|
||||||
const coordinates = vehicle.feature.getGeometry().getCoordinates();
|
|
||||||
updateVehicleLabel(id, coordinates, vehicle.speed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时初始化
|
|
||||||
onMounted(() => {
|
|
||||||
// 创建图层并连接WebSocket
|
|
||||||
if (props.map) {
|
|
||||||
createVehicleLayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接WebSocket
|
|
||||||
connectWebSocket();
|
|
||||||
|
|
||||||
// 设置定时发送心跳
|
|
||||||
const pingInterval = setInterval(() => {
|
|
||||||
if (wsService && wsConnected.value) {
|
|
||||||
sendPing();
|
|
||||||
}
|
|
||||||
}, 30000); // 每30秒发送一次心跳
|
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(pingInterval);
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 连接WebSocket
|
|
||||||
function connectWebSocket() {
|
|
||||||
try {
|
|
||||||
// 使用WebSocketService创建连接
|
|
||||||
const wsUrl = 'ws://10.0.0.124:8080/collision';
|
|
||||||
console.log(`正在连接WebSocket: ${wsUrl}`);
|
|
||||||
|
|
||||||
wsService = createWebSocket(wsUrl, {
|
|
||||||
reconnectInterval: 3000,
|
|
||||||
maxReconnectAttempts: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
// 注册事件监听
|
|
||||||
wsService.on('open', (event) => {
|
|
||||||
console.log('WebSocket连接成功!');
|
|
||||||
wsConnected.value = true;
|
|
||||||
|
|
||||||
// 连接成功后自动订阅消息
|
|
||||||
setTimeout(() => {
|
|
||||||
sendSubscribe();
|
|
||||||
}, 1000); // 延迟1秒发送订阅,确保连接稳定
|
|
||||||
});
|
|
||||||
|
|
||||||
wsService.on('message', (data) => {
|
|
||||||
handleWsMessage(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
wsService.on('error', (event) => {
|
|
||||||
console.error('WebSocket错误:', event);
|
|
||||||
wsConnected.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
wsService.on('close', (event) => {
|
|
||||||
console.log(`WebSocket连接关闭: ${event.code} - ${event.reason}`);
|
|
||||||
wsConnected.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
wsService.on('reconnect_failed', () => {
|
|
||||||
console.error('WebSocket重连失败,已达到最大重试次数');
|
|
||||||
wsConnected.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建WebSocket连接失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理WebSocket消息
|
|
||||||
function handleWsMessage(message) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(message);
|
|
||||||
|
|
||||||
// 根据消息类型处理
|
|
||||||
switch (data.type) {
|
|
||||||
case 'connection':
|
|
||||||
console.log(`连接确认: ${data.message}`);
|
|
||||||
break;
|
|
||||||
case 'position_update':
|
|
||||||
// 确保payload存在
|
|
||||||
if (data.payload && data.payload.object_id) {
|
|
||||||
updateVehiclePosition(data.payload);
|
|
||||||
} else {
|
|
||||||
console.error('位置更新消息格式错误:', data);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'pong':
|
|
||||||
console.log('收到心跳响应');
|
|
||||||
break;
|
|
||||||
case 'collision_warning':
|
|
||||||
console.log('收到碰撞预警:', data.payload);
|
|
||||||
// 显示预警信息
|
|
||||||
if (data.payload) {
|
|
||||||
// 获取车辆ID和预警信息
|
|
||||||
const vehicleId = data.payload.object_id || '未知车辆';
|
|
||||||
const distance = data.payload.distance || 0;
|
|
||||||
const message = `预警:${vehicleId} 与其他车辆距离${distance.toFixed(1)}米,请注意避让!`;
|
|
||||||
showAlert(message, 'warning', 8000);
|
|
||||||
|
|
||||||
// 如果需要在地图上标记该车辆的告警状态
|
|
||||||
if (vehicles.value[vehicleId]) {
|
|
||||||
vehicles.value[vehicleId].warning = true;
|
|
||||||
// 如果车辆已有位置信息,更新标签显示
|
|
||||||
if (vehicles.value[vehicleId].position) {
|
|
||||||
updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'rule_violation':
|
|
||||||
console.log('收到规则违规:', data.payload);
|
|
||||||
// 显示告警信息
|
|
||||||
if (data.payload) {
|
|
||||||
// 获取车辆ID和告警信息
|
|
||||||
const vehicleId = data.payload.object_id || '未知车辆';
|
|
||||||
const violationType = data.payload.violation_type || '未知违规';
|
|
||||||
const message = `告警:${vehicleId} 发生${violationType},请立即处理!`;
|
|
||||||
showAlert(message, 'alarm', 10000);
|
|
||||||
|
|
||||||
// 如果需要在地图上标记该车辆的告警状态
|
|
||||||
if (vehicles.value[vehicleId]) {
|
|
||||||
vehicles.value[vehicleId].alarm = true;
|
|
||||||
// 如果车辆已有位置信息,更新标签显示
|
|
||||||
if (vehicles.value[vehicleId].position) {
|
|
||||||
updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'vehicle_command':
|
|
||||||
console.log('收到车辆控制指令:', data.payload);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// 其他类型的消息可以根据需要处理
|
|
||||||
console.log(`收到其他类型消息: ${data.type}`, data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('处理WebSocket消息出错:', e, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送心跳
|
|
||||||
function sendPing() {
|
|
||||||
if (wsService) {
|
|
||||||
wsService.send('ping');
|
|
||||||
console.log('发送心跳: ping');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅消息
|
|
||||||
function sendSubscribe() {
|
|
||||||
if (wsService) {
|
|
||||||
const message = JSON.stringify({
|
|
||||||
type: 'subscribe',
|
|
||||||
topics: ['position_update', 'collision_warning', 'rule_violation'],
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
wsService.send(message);
|
|
||||||
console.log('发送订阅请求');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理资源
|
|
||||||
function cleanup() {
|
|
||||||
// 断开WebSocket连接
|
|
||||||
if (wsService) {
|
|
||||||
wsService.close();
|
|
||||||
wsService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除图层
|
|
||||||
if (vehicleLayer && props.map) {
|
|
||||||
props.map.removeLayer(vehicleLayer);
|
|
||||||
vehicleLayer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除标签
|
|
||||||
if (props.map) {
|
|
||||||
Object.values(vehicles.value).forEach(vehicle => {
|
|
||||||
if (vehicle.overlay) {
|
|
||||||
console.log(`移除标签: ${vehicle.id}`);
|
|
||||||
props.map.removeOverlay(vehicle.overlay);
|
|
||||||
vehicle.overlay = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空车辆数据
|
|
||||||
vehicles.value = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听地图实例变化
|
|
||||||
watch(() => props.map, (newMap) => {
|
|
||||||
if (newMap) {
|
|
||||||
createVehicleLayer();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 向外暴露方法
|
|
||||||
defineExpose({
|
|
||||||
updateVehiclePosition,
|
|
||||||
wsConnected,
|
|
||||||
sendPing,
|
|
||||||
sendSubscribe
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.vehicle-movement-control {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws-status {
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
background-color: rgba(255, 87, 34, 0.8);
|
|
||||||
color: white;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws-status.connected {
|
|
||||||
background-color: rgba(76, 175, 80, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 告警/预警容器样式 */
|
|
||||||
.alert-container {
|
|
||||||
position: fixed;
|
|
||||||
top: 20%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 2000;
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
max-width: 550px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-box {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content:center;
|
|
||||||
align-items: center;
|
|
||||||
animation: alertSlideIn 1s ease;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
/* margin-bottom: 8px; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-icon {
|
|
||||||
width: 56px;
|
|
||||||
height: 50px;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-title {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-desc {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #fff;
|
|
||||||
margin-left: 44px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 闪烁动画 */
|
|
||||||
.alert-flash {
|
|
||||||
animation: alertFlash 1s steps(2, start) infinite, alertSlideIn 0.5s ease;
|
|
||||||
}
|
|
||||||
@keyframes alertFlash {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.4; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预警样式 - 黄色背景 */
|
|
||||||
.alert-warning {
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
max-width: 550px;
|
|
||||||
background: url(../../../assets/images/warn_report.png) no-repeat;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 告警样式 - 红色背景 */
|
|
||||||
.alert-danger {
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
max-width: 550px;
|
|
||||||
background: url(../../../assets/images/alarm_report.png) no-repeat;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 车辆标签样式 */
|
|
||||||
:deep(.vehicle-label) {
|
|
||||||
position: absolute;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
text-align: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
min-width: 80px;
|
|
||||||
min-height: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 30; /* 确保标签在图标上方 */
|
|
||||||
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease; /* 平滑移动效果 */
|
|
||||||
will-change: transform, left, top; /* 提示浏览器优化这些属性的变化 */
|
|
||||||
bottom: 0; /* 确保标签底部对齐定位点 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滑入航空器标签样式 */
|
|
||||||
:deep(.vehicle-aircraft-in) {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滑出航空器标签样式 */
|
|
||||||
:deep(.vehicle-aircraft-out) {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 无人车标签样式 */
|
|
||||||
:deep(.vehicle-unmanned) {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 特勤车标签样式 */
|
|
||||||
:deep(.vehicle-special) {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 摆渡车标签样式 */
|
|
||||||
:deep(.vehicle-shuttle) {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
1097
src/components/map/controls/VehicleMovementControlRefactored.vue
Normal file
1097
src/components/map/controls/VehicleMovementControlRefactored.vue
Normal file
File diff suppressed because it is too large
Load Diff
220
src/components/map/controls/VehicleStyleManager.vue
Normal file
220
src/components/map/controls/VehicleStyleManager.vue
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 样式管理器不需要模板内容 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Style, Icon } from 'ol/style';
|
||||||
|
|
||||||
|
// 导入车辆图标
|
||||||
|
import carIcon from '../../../assets/images/noPeopleCar.png';
|
||||||
|
import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 滑入航空器使用黄色图标
|
||||||
|
import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 滑出航空器使用蓝色图标
|
||||||
|
import warningCarIcon from '../../../assets/images/warning_car.png';
|
||||||
|
import unmannedVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 无人车图标
|
||||||
|
import specialVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 特勤车图标
|
||||||
|
import shuttleVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 摆渡车图标
|
||||||
|
|
||||||
|
// 定义props
|
||||||
|
const props = defineProps({
|
||||||
|
vehicles: Object
|
||||||
|
});
|
||||||
|
|
||||||
|
// 状态锁定时间(毫秒)- 超速状态锁定时间更长,确保图标稳定
|
||||||
|
const STATUS_LOCK_TIME = 20000; // 20秒锁定时间,避免图标来回切换
|
||||||
|
|
||||||
|
// 获取车辆样式
|
||||||
|
function getVehicleStyle(id, speed, heading) {
|
||||||
|
const vehicle = props.vehicles[id];
|
||||||
|
if (!vehicle) return new Style({});
|
||||||
|
|
||||||
|
// 初始化状态锁定属性(如果不存在)
|
||||||
|
if (vehicle.statusLock === undefined) {
|
||||||
|
vehicle.statusLock = {
|
||||||
|
active: false,
|
||||||
|
type: null,
|
||||||
|
until: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const hasViolation = vehicle.violationType;
|
||||||
|
const hasInfoStatus = vehicle.info;
|
||||||
|
const hasWarningStatus = vehicle.warning;
|
||||||
|
const hasAlarmStatus = vehicle.alarm;
|
||||||
|
const hasCriticalStatus = vehicle.critical;
|
||||||
|
const hasSpeedViolation = vehicle.speedViolation;
|
||||||
|
|
||||||
|
// 检查是否有状态锁定
|
||||||
|
if (vehicle.statusLock.active && now < vehicle.statusLock.until) {
|
||||||
|
// 状态锁定中,直接返回锁定的图标
|
||||||
|
return new Style({
|
||||||
|
image: new Icon({
|
||||||
|
src: vehicle.statusLock.type === 'speedViolation' ? warningCarIcon : vehicle.cachedIconSrc || carIcon,
|
||||||
|
scale: 1.5,
|
||||||
|
anchor: [0.5, 0.5],
|
||||||
|
rotation: ((heading - 72) * Math.PI) / 180,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else if (vehicle.statusLock.active) {
|
||||||
|
// 状态锁定已过期,清除锁定
|
||||||
|
vehicle.statusLock.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超速状态特殊处理 - 确保图标稳定,不来回切换
|
||||||
|
if (hasSpeedViolation) {
|
||||||
|
// 如果还没有锁定状态或锁定已过期,重新锁定
|
||||||
|
if (!vehicle.statusLock.active || now > vehicle.statusLock.until) {
|
||||||
|
vehicle.statusLock = {
|
||||||
|
active: true,
|
||||||
|
type: 'speedViolation',
|
||||||
|
until: now + STATUS_LOCK_TIME
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回警告图标,确保超速状态下图标保持一致
|
||||||
|
return new Style({
|
||||||
|
image: new Icon({
|
||||||
|
src: warningCarIcon,
|
||||||
|
scale: 1.5,
|
||||||
|
anchor: [0.5, 0.5],
|
||||||
|
rotation: ((heading - 72) * Math.PI) / 180,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非超速状态下的缓存处理
|
||||||
|
if (vehicle.cachedIconSrc && !hasSpeedViolation) {
|
||||||
|
// 如果已有缓存图标且不是超速状态,检查是否需要更新
|
||||||
|
const lastIconTime = vehicle.lastIconUpdateTime || 0;
|
||||||
|
|
||||||
|
// 如果最后一次更新时间在2秒内,继续使用缓存的图标
|
||||||
|
if (now - lastIconTime < 2000) {
|
||||||
|
return new Style({
|
||||||
|
image: new Icon({
|
||||||
|
src: vehicle.cachedIconSrc,
|
||||||
|
scale: 1.5,
|
||||||
|
anchor: [0.5, 0.5],
|
||||||
|
rotation: ((heading - 72) * Math.PI) / 180,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let iconSrc;
|
||||||
|
|
||||||
|
// 首先检查告警状态
|
||||||
|
if (hasSpeedViolation) {
|
||||||
|
// 超速车辆使用警告图标
|
||||||
|
iconSrc = warningCarIcon;
|
||||||
|
} else if (hasViolation || hasInfoStatus || hasWarningStatus || hasCriticalStatus || hasAlarmStatus) {
|
||||||
|
// 其他违规车辆使用警告图标
|
||||||
|
iconSrc = warningCarIcon;
|
||||||
|
|
||||||
|
// 锁定告警状态一段时间(比超速状态短)
|
||||||
|
vehicle.statusLock = {
|
||||||
|
active: true,
|
||||||
|
type: 'alert',
|
||||||
|
until: now + STATUS_LOCK_TIME / 2
|
||||||
|
};
|
||||||
|
} else if (vehicle.isAircraftIn) {
|
||||||
|
// 滑入航空器(MU开头)使用黄色Aircraft1.png图标
|
||||||
|
iconSrc = aircraftInIcon;
|
||||||
|
} else if (vehicle.isAircraftOut) {
|
||||||
|
// 滑出航空器(CA开头)使用蓝色Aircraft.png图标
|
||||||
|
iconSrc = aircraftOutIcon;
|
||||||
|
} else if (vehicle.isUnmannedVehicle) {
|
||||||
|
// 无人车使用noPeopleCar.png图标
|
||||||
|
iconSrc = unmannedVehicleIcon;
|
||||||
|
} else if (vehicle.isSpecialVehicle) {
|
||||||
|
// 特勤车
|
||||||
|
iconSrc = specialVehicleIcon;
|
||||||
|
} else if (vehicle.isShuttleVehicle) {
|
||||||
|
// 摆渡车
|
||||||
|
iconSrc = shuttleVehicleIcon;
|
||||||
|
} else {
|
||||||
|
// 默认车辆图标
|
||||||
|
iconSrc = carIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在非超速状态下才缓存图标
|
||||||
|
if (!hasSpeedViolation) {
|
||||||
|
vehicle.cachedIconSrc = iconSrc;
|
||||||
|
vehicle.lastIconUpdateTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建样式
|
||||||
|
return new Style({
|
||||||
|
image: new Icon({
|
||||||
|
src: iconSrc,
|
||||||
|
scale: 1.5,
|
||||||
|
anchor: [0.5, 0.5],
|
||||||
|
rotation: ((heading - 72) * Math.PI) / 180,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车辆类型和状态选择图标
|
||||||
|
function getVehicleIcon(vehicle) {
|
||||||
|
if (!vehicle) return carIcon;
|
||||||
|
|
||||||
|
const hasInfoStatus = vehicle.info;
|
||||||
|
const hasWarningStatus = vehicle.warning;
|
||||||
|
const hasAlarmStatus = vehicle.alarm;
|
||||||
|
const hasCriticalStatus = vehicle.critical;
|
||||||
|
|
||||||
|
// 首先检查告警状态
|
||||||
|
if (hasInfoStatus || hasWarningStatus || hasCriticalStatus || hasAlarmStatus) {
|
||||||
|
return warningCarIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后根据车辆类型选择图标
|
||||||
|
if (vehicle.isAircraftIn) {
|
||||||
|
// 滑入航空器(CA开头)使用黄色Aircraft1.png图标
|
||||||
|
return aircraftInIcon;
|
||||||
|
} else if (vehicle.isAircraftOut) {
|
||||||
|
// 滑出航空器(MU开头)使用蓝色Aircraft.png图标
|
||||||
|
return aircraftOutIcon;
|
||||||
|
} else if (vehicle.isUnmannedVehicle) {
|
||||||
|
// 无人车使用noPeopleCar.png图标
|
||||||
|
return unmannedVehicleIcon;
|
||||||
|
} else if (vehicle.isSpecialVehicle) {
|
||||||
|
// 特勤车
|
||||||
|
return specialVehicleIcon;
|
||||||
|
} else if (vehicle.isShuttleVehicle) {
|
||||||
|
// 摆渡车
|
||||||
|
return shuttleVehicleIcon;
|
||||||
|
} else {
|
||||||
|
return carIcon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建车辆样式
|
||||||
|
function createVehicleStyle(vehicle, heading) {
|
||||||
|
const iconSrc = getVehicleIcon(vehicle);
|
||||||
|
|
||||||
|
return new Style({
|
||||||
|
image: new Icon({
|
||||||
|
src: iconSrc,
|
||||||
|
scale: 1.5,
|
||||||
|
anchor: [0.5, 0.5],
|
||||||
|
rotation: ((heading - 72) * Math.PI) / 180,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新车辆样式
|
||||||
|
function updateVehicleStyle(id, heading) {
|
||||||
|
const vehicle = props.vehicles[id];
|
||||||
|
if (!vehicle) return null;
|
||||||
|
|
||||||
|
return createVehicleStyle(vehicle, heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
getVehicleStyle,
|
||||||
|
getVehicleIcon,
|
||||||
|
createVehicleStyle,
|
||||||
|
updateVehicleStyle
|
||||||
|
});
|
||||||
|
</script>
|
||||||
187
src/components/map/controls/WeatherStationPopup.vue
Normal file
187
src/components/map/controls/WeatherStationPopup.vue
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
|
||||||
|
<!-- 气象监测站弹窗 -->
|
||||||
|
<template>
|
||||||
|
<div class="weather-station-container" v-if="visible">
|
||||||
|
<div class="weather-station-box">
|
||||||
|
<div class="weather-station-header">
|
||||||
|
<span>气象监测站</span>
|
||||||
|
<img class="weather-station-close" src="../../../assets/images/close_icon.png" alt="" @click="close" />
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-content">
|
||||||
|
<div class="weather-station-title">
|
||||||
|
<span>信号灯XH001 | 实时监测</span>
|
||||||
|
<img class="weather-station-status" src="../../../assets/images/weather_icon.png" alt="气象监测站" />
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-info-container">
|
||||||
|
<div class="weather-station-info-row">
|
||||||
|
<div class="weather-station-info-item">
|
||||||
|
<span class="weather-info-label">温度:</span>
|
||||||
|
<span class="weather-info-value">{{ weatherData.temperature }}°C</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-info-item">
|
||||||
|
<span class="weather-info-label">湿度:</span>
|
||||||
|
<span class="weather-info-value">{{ weatherData.humidity }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-info-row">
|
||||||
|
<div class="weather-station-info-item">
|
||||||
|
<span class="weather-info-label">风速:</span>
|
||||||
|
<span class="weather-info-value">{{ weatherData.windSpeed }}m/s</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-info-item">
|
||||||
|
<span class="weather-info-label">能见度:</span>
|
||||||
|
<span class="weather-info-value">{{ weatherData.visibility }}km</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-info-row">
|
||||||
|
<div class="weather-station-info-item">
|
||||||
|
<span class="weather-info-label">风向:</span>
|
||||||
|
<span class="weather-info-value">{{ weatherData.windDirection }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-info-item">
|
||||||
|
<span class="weather-info-label">湿滑程度:</span>
|
||||||
|
<span class="weather-info-value">{{ weatherData.wetness }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="weather-station-info-row">
|
||||||
|
<div class="weather-station-info-item">
|
||||||
|
<span class="weather-info-label">气压:</span>
|
||||||
|
<span class="weather-info-value">{{ weatherData.pressure }}hPa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
// 气象数据
|
||||||
|
const weatherData = ref({
|
||||||
|
temperature: '22.5',
|
||||||
|
humidity: '68',
|
||||||
|
windSpeed: '3.2',
|
||||||
|
visibility: '8.5',
|
||||||
|
pressure: '1013.2',
|
||||||
|
windDirection: '北风',
|
||||||
|
wetness: '干燥',
|
||||||
|
updateTime: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟更新气象数据
|
||||||
|
function updateWeatherData() {
|
||||||
|
weatherData.value = {
|
||||||
|
temperature: '22.5',
|
||||||
|
humidity: '68',
|
||||||
|
windSpeed: '3.2',
|
||||||
|
visibility: '8.5',
|
||||||
|
pressure: '1013.2',
|
||||||
|
windDirection: '北风',
|
||||||
|
wetness: '干燥',
|
||||||
|
updateTime: Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时设置定时更新
|
||||||
|
onMounted(() => {
|
||||||
|
const weatherUpdateInterval = setInterval(() => {
|
||||||
|
updateWeatherData();
|
||||||
|
}, 60000); // 每分钟更新一次气象数据
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(weatherUpdateInterval);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 气象监测站样式 */
|
||||||
|
.weather-station-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 100px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 2000;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-box {
|
||||||
|
background-color: #424851;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom:1px solid #777777;
|
||||||
|
background-color: #424851;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-close {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-content {
|
||||||
|
padding: 12px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-status {
|
||||||
|
color: #4CAF50;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-info-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-station-info-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-info-label {
|
||||||
|
color: #EEEEEE;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-info-value {
|
||||||
|
color: #EEEEEE;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -25,6 +25,13 @@
|
|||||||
>
|
>
|
||||||
超界告警
|
超界告警
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: activeTab === 'speed' }"
|
||||||
|
@click="activeTab = 'speed'"
|
||||||
|
>
|
||||||
|
超速告警
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-actions">
|
<div class="tab-actions">
|
||||||
|
|
||||||
@ -41,6 +48,7 @@
|
|||||||
<div class="alarm-icon" :class="item.level">
|
<div class="alarm-icon" :class="item.level">
|
||||||
<img v-if="item.type === 'car'" src="@/assets/images/clarm_conflict.png" class="alarm-img" alt="车辆冲突" />
|
<img v-if="item.type === 'car'" src="@/assets/images/clarm_conflict.png" class="alarm-img" alt="车辆冲突" />
|
||||||
<img v-else-if="item.type === 'report'" src="@/assets/images/clarm_over.png" class="alarm-img" alt="超界告警" />
|
<img v-else-if="item.type === 'report'" src="@/assets/images/clarm_over.png" class="alarm-img" alt="超界告警" />
|
||||||
|
<img v-else-if="item.type === 'speed'" src="@/assets/images/clarm_speed.png" class="alarm-img" alt="超速告警" />
|
||||||
<i v-else class="alarm-dot"></i>
|
<i v-else class="alarm-dot"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="alarm-content">
|
<div class="alarm-content">
|
||||||
@ -100,6 +108,24 @@ const alarmList = ref([
|
|||||||
date: '2025-03-19 10:30',
|
date: '2025-03-19 10:30',
|
||||||
level: 'medium',
|
level: 'medium',
|
||||||
type: 'report'
|
type: 'report'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carId: 'QN002',
|
||||||
|
carType: '牵引车',
|
||||||
|
time: 'T10:15—10: 18超速行驶',
|
||||||
|
description: ',速度达到85km/h',
|
||||||
|
date: '2025-03-19 10:18',
|
||||||
|
level: 'high',
|
||||||
|
type: 'speed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carId: 'QN003',
|
||||||
|
carType: '摆渡车',
|
||||||
|
time: 'T10:22—10: 25超速行驶',
|
||||||
|
description: ',速度达到78km/h',
|
||||||
|
date: '2025-03-19 10:25',
|
||||||
|
level: 'medium',
|
||||||
|
type: 'speed'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -126,7 +152,7 @@ defineExpose({
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left:70px;
|
left:70px;
|
||||||
top: 15%;
|
top: 15%;
|
||||||
width: 360px;
|
width: 400px;
|
||||||
background-color: rgba(41, 44, 56, 0.95);
|
background-color: rgba(41, 44, 56, 0.95);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
@ -181,16 +207,17 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
padding: 0 16px;
|
padding: 0 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #A0A8B7;
|
color: #A0A8B7;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item.active {
|
.tab-item.active {
|
||||||
|
|||||||
@ -109,7 +109,7 @@
|
|||||||
>
|
>
|
||||||
<div class="track-timeline">
|
<div class="track-timeline">
|
||||||
<div class="timeline-dot" :class="{ 'active': idx === 0 }"></div>
|
<div class="timeline-dot" :class="{ 'active': idx === 0 }"></div>
|
||||||
<div class="timeline-line" v-if="idx !== (car.trackList || defaultTrackList).length - 1"></div>
|
<div class="timeline-line"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-content">
|
<div class="track-content">
|
||||||
<div class="track-time">{{ item.time }}</div>
|
<div class="track-time">{{ item.time }}</div>
|
||||||
@ -404,15 +404,14 @@ function getBatteryTempClass(temp) {
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #8ec6ff;
|
background-color: #fff;
|
||||||
margin-top: 4px;
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-dot.active {
|
.timeline-dot.active {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background-color: #1a6dff;
|
background-color: #6DB8FF;
|
||||||
box-shadow: 0 0 8px 2px rgba(26, 109, 255, 0.6);
|
box-shadow: 0 0 8px 2px rgba(26, 109, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,8 +421,8 @@ function getBatteryTempClass(temp) {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: calc(100% + 7px);
|
height: calc(100% - 10px);
|
||||||
background: linear-gradient(to bottom, #8ec6ff 50%, transparent 50%);
|
background: linear-gradient(to bottom, #536C8F 50%, transparent 50%);
|
||||||
background-size: 2px 8px;
|
background-size: 2px 8px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -350,7 +350,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-btn.task {
|
.status-btn.task {
|
||||||
background-color: #00B1EB;
|
background-color: #5690E7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-btn.idle {
|
.status-btn.idle {
|
||||||
@ -409,7 +409,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.online-status.online {
|
.online-status.online {
|
||||||
background-color: #00B1EB;
|
background-color: #5690E7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.online-status.offline {
|
.online-status.offline {
|
||||||
|
|||||||
@ -230,20 +230,7 @@ export const dynamicRoutes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/monitor/job-log',
|
|
||||||
component: Layout,
|
|
||||||
hidden: true,
|
|
||||||
permissions: ['monitor:job:list'],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'index/:jobId(\\d+)',
|
|
||||||
component: () => import('@/views/monitor/job/log'),
|
|
||||||
name: 'JobLog',
|
|
||||||
meta: { title: '调度日志', activeMenu: '/monitor/job' }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/tool/gen-edit',
|
path: '/tool/gen-edit',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
@ -115,7 +115,7 @@
|
|||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>服务器地址:</label>
|
<label>服务器地址:</label>
|
||||||
<select id="collisionServerSelect">
|
<select id="collisionServerSelect">
|
||||||
<option value="ws://10.0.0.124:8080/collision">冲突检测WebSocket</option>
|
<option value="ws://10.0.0.126:8080/collision">冲突检测WebSocket</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -81,8 +81,20 @@ let wsInstance = null;
|
|||||||
export function createWebSocket(url, options) {
|
export function createWebSocket(url, options) {
|
||||||
if (!wsInstance) {
|
if (!wsInstance) {
|
||||||
wsInstance = new WebSocketService(url, options);
|
wsInstance = new WebSocketService(url, options);
|
||||||
|
} else if (wsInstance.url !== url) {
|
||||||
|
// 如果URL改变,重新创建实例
|
||||||
|
wsInstance.close();
|
||||||
|
wsInstance = new WebSocketService(url, options);
|
||||||
}
|
}
|
||||||
return wsInstance;
|
return wsInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置WebSocket实例,用于强制重新创建实例
|
||||||
|
export function resetWebSocketInstance() {
|
||||||
|
if (wsInstance) {
|
||||||
|
wsInstance.close();
|
||||||
|
wsInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default WebSocketService;
|
export default WebSocketService;
|
||||||
@ -191,7 +191,7 @@ const cardList = [
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.vehicle-detail-content {
|
.vehicle-detail-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -383,13 +383,13 @@ const cardList = [
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
img {
|
|
||||||
|
}
|
||||||
|
.custom-tabs-label img {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
filter: brightness(0) invert(1); /* 默认将图标变为白色 */
|
filter: brightness(0) invert(1); /* 默认将图标变为白色 */
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tabs) {
|
:deep(.el-tabs) {
|
||||||
--el-tabs-header-background-color: #1e2233;
|
--el-tabs-header-background-color: #1e2233;
|
||||||
--el-tabs-border-color: #303850;
|
--el-tabs-border-color: #303850;
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
<div class="search-area">
|
<div class="search-area">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.licensePlateNumber"
|
v-model="queryParams.licensePlate"
|
||||||
placeholder="请输入车牌号查询"
|
placeholder="请输入车牌号查询"
|
||||||
clearable
|
clearable
|
||||||
prefix-icon="Search"
|
prefix-icon="Search"
|
||||||
@ -59,7 +59,7 @@
|
|||||||
<el-button link text type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
<el-button link text type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="车牌号" prop="licensePlateNumber" align="left" />
|
<el-table-column label="车牌号" prop="licensePlate" align="left" />
|
||||||
<el-table-column label="车辆类型" align="left">
|
<el-table-column label="车辆类型" align="left">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ getVehicleTypeName(scope.row.typeId) }}
|
{{ getVehicleTypeName(scope.row.typeId) }}
|
||||||
@ -104,8 +104,8 @@
|
|||||||
<el-form ref="vehicleFormRef" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="vehicleFormRef" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="车牌号" prop="licensePlateNumber">
|
<el-form-item label="车牌号" prop="licensePlate">
|
||||||
<el-input v-model="form.licensePlateNumber" placeholder="请输入车牌号" />
|
<el-input v-model="form.licensePlate" placeholder="请输入车牌号" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
@ -282,7 +282,7 @@ const statistics = computed(() => {
|
|||||||
const queryParams = ref({
|
const queryParams = ref({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
licensePlateNumber: undefined,
|
licensePlate: undefined,
|
||||||
typeId: undefined
|
typeId: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -299,7 +299,7 @@ const importDialog = reactive({
|
|||||||
// 表单数据
|
// 表单数据
|
||||||
const form = ref({
|
const form = ref({
|
||||||
vehicleId: undefined,
|
vehicleId: undefined,
|
||||||
licensePlateNumber: '',
|
licensePlate: '',
|
||||||
typeId: '',
|
typeId: '',
|
||||||
brand: '',
|
brand: '',
|
||||||
owningUnit: '',
|
owningUnit: '',
|
||||||
@ -310,7 +310,7 @@ const form = ref({
|
|||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const rules = {
|
const rules = {
|
||||||
licensePlateNumber: [
|
licensePlate: [
|
||||||
{ required: true, message: "车牌号不能为空", trigger: "blur" }
|
{ required: true, message: "车牌号不能为空", trigger: "blur" }
|
||||||
],
|
],
|
||||||
typeId: [
|
typeId: [
|
||||||
@ -399,7 +399,7 @@ function resetQuery() {
|
|||||||
queryParams.value = {
|
queryParams.value = {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
licensePlateNumber: undefined,
|
licensePlate: undefined,
|
||||||
typeId: undefined
|
typeId: undefined
|
||||||
};
|
};
|
||||||
handleQuery();
|
handleQuery();
|
||||||
@ -521,7 +521,7 @@ function beforeImageUpload(file) {
|
|||||||
function reset() {
|
function reset() {
|
||||||
form.value = {
|
form.value = {
|
||||||
vehicleId: undefined,
|
vehicleId: undefined,
|
||||||
licensePlateNumber: '',
|
licensePlate: '',
|
||||||
typeId: '',
|
typeId: '',
|
||||||
brand: '',
|
brand: '',
|
||||||
owningUnit: '',
|
owningUnit: '',
|
||||||
|
|||||||
@ -7,6 +7,49 @@
|
|||||||
@setCategoryVisibility="handleSetCategoryVisibility"
|
@setCategoryVisibility="handleSetCategoryVisibility"
|
||||||
/>
|
/>
|
||||||
<CarAlarm />
|
<CarAlarm />
|
||||||
|
|
||||||
|
<!-- 告警统计卡片 -->
|
||||||
|
<div class="alarm-stats-card">
|
||||||
|
<div class="stats-header">
|
||||||
|
<span class="stats-title">告警统计</span>
|
||||||
|
<span class="stats-time">{{ currentTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-item" :class="{ 'has-new': hasNewWarning }">
|
||||||
|
<i class="stats-icon warning-icon">⚠</i>
|
||||||
|
<div class="stats-text">
|
||||||
|
<span class="stats-count">{{ warningCount }}</span>
|
||||||
|
<span class="stats-label">冲突预警</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="hasNewWarning" class="new-badge"></span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-item" :class="{ 'has-new': hasNewAlert }">
|
||||||
|
<i class="stats-icon alert-icon">🚨</i>
|
||||||
|
<div class="stats-text">
|
||||||
|
<span class="stats-count">{{ alertCount }}</span>
|
||||||
|
<span class="stats-label">冲突告警</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="hasNewAlert" class="new-badge"></span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-item" :class="{ 'has-new': hasNewBoundary }">
|
||||||
|
<i class="stats-icon boundary-icon">🚧</i>
|
||||||
|
<div class="stats-text">
|
||||||
|
<span class="stats-count">{{ boundaryCount }}</span>
|
||||||
|
<span class="stats-label">越界告警</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="hasNewBoundary" class="new-badge"></span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-item" :class="{ 'has-new': hasNewSpeed }">
|
||||||
|
<i class="stats-icon speed-icon">⚡</i>
|
||||||
|
<div class="stats-text">
|
||||||
|
<span class="stats-count">{{ speedCount }}</span>
|
||||||
|
<span class="stats-label">超速告警</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="hasNewSpeed" class="new-badge"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 报警通知面板 -->
|
<!-- 报警通知面板 -->
|
||||||
<AlarmNotification v-if="showAlarmPanel" @close="showAlarmPanel = false" />
|
<AlarmNotification v-if="showAlarmPanel" @close="showAlarmPanel = false" />
|
||||||
<!-- 报警通知按钮 -->
|
<!-- 报警通知按钮 -->
|
||||||
@ -26,7 +69,7 @@
|
|||||||
<Eventlist v-if="showEventList" />
|
<Eventlist v-if="showEventList" />
|
||||||
|
|
||||||
<!-- 车辆移动控制组件 -->
|
<!-- 车辆移动控制组件 -->
|
||||||
<VehicleMovementControl :map="map" ref="vehicleMovementRef" v-if="map" />
|
<VehicleMovementControlRefactored :map="map" ref="vehicleMovementRef" v-if="map" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -46,12 +89,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
import { ref, onMounted, onUnmounted, watch, computed, onActivated } from 'vue';
|
||||||
import OpenLayersMap from '../../components/map/OpenLayersMap.vue';
|
import OpenLayersMap from '../../components/map/OpenLayersMap.vue';
|
||||||
import CarAlarm from '../../components/map/info/carClarm.vue';
|
import CarAlarm from '../../components/map/info/carClarm.vue';
|
||||||
import Eventlist from '../../components/map/info/eventlist.vue';
|
import Eventlist from '../../components/map/info/eventlist.vue';
|
||||||
import AlarmNotification from '../../components/map/info/AlarmNotification.vue';
|
import AlarmNotification from '../../components/map/info/AlarmNotification.vue';
|
||||||
import VehicleMovementControl from '../../components/map/controls/VehicleMovementControl.vue';
|
import VehicleMovementControlRefactored from '../../components/map/controls/VehicleMovementControlRefactored.vue';
|
||||||
import btn_left from '../../assets/images/btn_left.png';
|
import btn_left from '../../assets/images/btn_left.png';
|
||||||
import btn_right from '../../assets/images/btn_right.png';
|
import btn_right from '../../assets/images/btn_right.png';
|
||||||
|
|
||||||
@ -62,6 +105,63 @@ const mapRef = ref(null);
|
|||||||
const vehicleMovementRef = ref(null);
|
const vehicleMovementRef = ref(null);
|
||||||
const map = ref(null); // 存储地图实例
|
const map = ref(null); // 存储地图实例
|
||||||
|
|
||||||
|
// 告警统计数据
|
||||||
|
const warningCount = ref(2); // 冲突预警
|
||||||
|
const alertCount = ref(1); // 冲突告警
|
||||||
|
const boundaryCount = ref(3); // 越界告警
|
||||||
|
const speedCount = ref(5); // 超速告警
|
||||||
|
|
||||||
|
// 新告警标记
|
||||||
|
const hasNewWarning = ref(false);
|
||||||
|
const hasNewAlert = ref(false);
|
||||||
|
const hasNewBoundary = ref(false);
|
||||||
|
const hasNewSpeed = ref(false);
|
||||||
|
|
||||||
|
// 当前时间
|
||||||
|
const currentTime = ref('');
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
function updateCurrentTime() {
|
||||||
|
const now = new Date();
|
||||||
|
currentTime.value = now.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟接收新告警
|
||||||
|
function simulateNewAlarm() {
|
||||||
|
// 随机选择一种告警类型
|
||||||
|
const alarmType = Math.floor(Math.random() * 4);
|
||||||
|
|
||||||
|
switch(alarmType) {
|
||||||
|
case 0:
|
||||||
|
warningCount.value++;
|
||||||
|
hasNewWarning.value = true;
|
||||||
|
setTimeout(() => { hasNewWarning.value = false; }, 10000);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
alertCount.value++;
|
||||||
|
hasNewAlert.value = true;
|
||||||
|
setTimeout(() => { hasNewAlert.value = false; }, 10000);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
boundaryCount.value++;
|
||||||
|
hasNewBoundary.value = true;
|
||||||
|
setTimeout(() => { hasNewBoundary.value = false; }, 10000);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
speedCount.value++;
|
||||||
|
hasNewSpeed.value = true;
|
||||||
|
setTimeout(() => { hasNewSpeed.value = false; }, 10000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新总告警数量
|
||||||
|
alarmCount.value = warningCount.value + alertCount.value + boundaryCount.value + speedCount.value;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取车辆分类数据,从VehicleMovementControl组件中获取
|
// 获取车辆分类数据,从VehicleMovementControl组件中获取
|
||||||
const vehicleCategories = computed(() => {
|
const vehicleCategories = computed(() => {
|
||||||
return vehicleMovementRef.value?.vehicleCategories || {};
|
return vehicleMovementRef.value?.vehicleCategories || {};
|
||||||
@ -124,9 +224,33 @@ function importRouteData() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 组件激活时检查WebSocket状态
|
||||||
|
onActivated(() => {
|
||||||
|
console.log('平台概览组件被激活');
|
||||||
|
// 延迟检查,确保子组件已激活
|
||||||
|
setTimeout(() => {
|
||||||
|
if (vehicleMovementRef.value) {
|
||||||
|
const wsConnected = vehicleMovementRef.value.wsConnected;
|
||||||
|
console.log('WebSocket连接状态:', wsConnected);
|
||||||
|
|
||||||
|
// 如果WebSocket未连接,尝试重连
|
||||||
|
if (!wsConnected) {
|
||||||
|
console.log('WebSocket未连接,尝试重新连接');
|
||||||
|
vehicleMovementRef.value.reconnectWebSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.querySelector('.app-main')?.classList.add('platform-no-padding');
|
document.querySelector('.app-main')?.classList.add('platform-no-padding');
|
||||||
|
|
||||||
|
// 初始化时间
|
||||||
|
updateCurrentTime();
|
||||||
|
|
||||||
|
// 设置定时器更新时间
|
||||||
|
const timeInterval = setInterval(updateCurrentTime, 1000);
|
||||||
|
|
||||||
// 获取地图实例
|
// 获取地图实例
|
||||||
if (mapRef.value) {
|
if (mapRef.value) {
|
||||||
// 等待地图实例初始化完成
|
// 等待地图实例初始化完成
|
||||||
@ -138,6 +262,16 @@ onMounted(() => {
|
|||||||
// testVehicleMovement();
|
// testVehicleMovement();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timeInterval);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置模拟告警定时器(仅开发环境)
|
||||||
|
if (isDevelopment) {
|
||||||
|
setInterval(simulateNewAlarm, 5000); // 每5秒模拟一次新告警
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@ -164,6 +298,146 @@ watch(() => mapRef.value?.map, (newMap) => {
|
|||||||
z-index:1;
|
z-index:1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 告警统计卡片 */
|
||||||
|
.alarm-stats-card {
|
||||||
|
position: absolute;
|
||||||
|
top: 25%;
|
||||||
|
left: 20px;
|
||||||
|
width: 240px;
|
||||||
|
background-color: rgba(41, 44, 56, 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
z-index: 1000;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px; /* 减小内边距 */
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #A0A8B7;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 10px; /* 减小内边距 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* 改为水平布局 */
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(41, 44, 56, 0.95);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(52, 122, 226, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-new {
|
||||||
|
background-color: rgba(255, 77, 79, 0.15);
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
|
||||||
|
.stats-count {
|
||||||
|
color: #FF4D4F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 10px; /* 图标和文字之间的间距 */
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
color: #FFA500;
|
||||||
|
background-color: rgba(255, 165, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-icon {
|
||||||
|
color: #FF4D4F;
|
||||||
|
background-color: rgba(255, 77, 79, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boundary-icon {
|
||||||
|
color: #FF6B6B;
|
||||||
|
background-color: rgba(255, 107, 107, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-icon {
|
||||||
|
color: #FFD700;
|
||||||
|
background-color: rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-count {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #A0A8B7;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #FF4D4F;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 5px rgba(255, 77, 79, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(255, 77, 79, 0.15);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgba(255, 77, 79, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(255, 77, 79, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 报警通知按钮 */
|
/* 报警通知按钮 */
|
||||||
.alarm-btn {
|
.alarm-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
// https://cn.vitejs.dev/config/#server-proxy
|
// https://cn.vitejs.dev/config/#server-proxy
|
||||||
'/dev-api': {
|
'/dev-api': {
|
||||||
// target: 'http://10.0.0.17:8099',//昊天
|
// target: 'http://10.0.0.17:8099',//昊天
|
||||||
target: 'http://10.0.0.124:8080',//田哥
|
target: 'http://10.0.0.126:8080',//田哥
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user