修改平台概览样式

This commit is contained in:
renna 2025-07-14 11:05:40 +08:00
parent 921c2e8e5d
commit 196240b21f
33 changed files with 4464 additions and 2108 deletions

View File

@ -6,3 +6,6 @@ VITE_APP_ENV = 'development'
# 青岛机场无人驾驶车辆协同云平台/开发环境
VITE_APP_BASE_API = '/dev-api'
# WebSocket配置
VITE_APP_WEBSOCKET_URL=ws://10.0.0.126:8080/collision

View File

@ -7,5 +7,7 @@ VITE_APP_ENV = 'production'
# 青岛机场无人驾驶车辆协同云平台/生产环境
VITE_APP_BASE_API = '/prod-api'
VITE_APP_WEBSOCKET_URL='ws://10.0.0.124:8080/collision'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip

View File

@ -7,5 +7,7 @@ VITE_APP_ENV = 'staging'
# 青岛机场无人驾驶车辆协同云平台/生产环境
VITE_APP_BASE_API = '/stage-api'
VITE_APP_WEBSOCKET_URL='ws://10.0.0.124:8080/collision'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip

View File

@ -124,4 +124,41 @@ function getUserOptions() {
}
使用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()节省资源

View 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'
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/images/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

View File

@ -34,7 +34,8 @@
<div class="task-row3">
<span class="point start">起点</span>
<span class="label">{{ item.start }}</span>
<span class="arrow">&gt;</span>
<!-- <span class="arrow">&gt;</span> -->
<img class="arrow" src="@/assets/images/arrow.png" alt="arrow">
<span class="point end">终点</span>
<span class="label">{{ item.end }}</span>
</div>
@ -52,18 +53,18 @@
<div class="panel-header">
<span class="dot"></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 class="panel-info">
<div class="info-row">
<span class="carno">QN001</span>
<span class="info-item">最大时速 <b>91km/h</b></span>
<span class="info-item">平均时速 <b>28km/h</b></span>
<span class="info-item">总里程 <b>63.3km</b></span>
<span class="info-item">耗时 <b>20min</b></span>
<span class="info-item warn">冲突告警 <b>1</b></span>
<span class="info-item prewarn">冲突预警 <b>1</b></span>
<span class="carno">{{ activeTask?.licensePlate || activeVehicleId || "未选择车辆" }}</span>
<span class="info-item">最大时速 <b>{{ trackDetails.maxSpeed }}km/h</b></span>
<span class="info-item">平均时速 <b>{{ trackDetails.averageSpeed }}km/h</b></span>
<span class="info-item">总里程 <b>{{ trackDetails.totalDistance }}km</b></span>
<span class="info-item">耗时 <b>{{ trackDetails.totalTime }}min</b></span>
<span class="info-item warn">冲突告警 <b>{{ trackDetails.warnings }}</b></span>
<span class="info-item prewarn">冲突预警 <b>{{ trackDetails.preWarnings }}</b></span>
</div>
</div>
<!-- 第三行进度条 -->
@ -79,7 +80,7 @@
<!-- 预警/告警红旗 -->
<div
v-for="flag in flags"
:key="flag.time"
:key="flag.label"
class="progress-flag"
:style="{left: flag.percent+'%'}"
:title="flag.label"
@ -95,7 +96,7 @@
</div>
</div>
<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">
<el-dropdown @command="setSpeed">
<span class="el-dropdown-link">
@ -110,7 +111,7 @@
</template>
</el-dropdown>
</div>
<span class="end-time">2024-09-10 12:20:00</span>
<span class="end-time">{{ trackDetails.endTime || "未结束" }}</span>
</div>
</div>
</div>
@ -120,8 +121,31 @@
</template>
<script setup>
import { ref, computed } from "vue";
import { ref, computed, onMounted, defineProps, watchEffect } from "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 = {
@ -134,59 +158,254 @@ const carInfo = {
phone: "15689742356",
};
//
const tasks = [
{
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 tasks = 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);
//
// taskspoints
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(() => {
if (!search.value) return tasks;
return tasks.filter(
if (!search.value) return tasks.value;
return tasks.value.filter(
(t) =>
t.name.includes(search.value) ||
t.no.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) {
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); //
@ -195,11 +414,8 @@ const showTooltip = ref(false);
const tooltipTime = ref("");
const tooltipLeft = ref(0);
// /
const flags = [
{ percent: 20, label: "告警 12:04:00" },
{ percent: 60, label: "预警 12:12:00" }
];
// /
const flags = ref([]);
//
let dragging = false;
@ -247,6 +463,13 @@ function handleProgressClick(e) {
function setSpeed(val) {
speed.value = val;
}
//
onMounted(() => {
getRunInfoList();
// getLatestLocation("B579");
});
</script>
<style scoped lang="scss">

File diff suppressed because it is too large Load Diff

View File

@ -64,7 +64,7 @@
<div class="panel-content" v-else>
<div class="layer-group">
<div class="group-title">道路图层</div>
<!-- <div class="group-title">道路图层</div> -->
<div class="layer-grid-full">
<div class="layer-item">
<label class="checkbox-container">
@ -203,7 +203,7 @@ async function loadCustomRoadLayer() {
customRoadVectorLayer = new VectorLayer({
source,
style: new Style({
stroke: new Stroke({ color: '#888', width: 4 })
stroke: new Stroke({ color: '#C9C9C9', width: 1 })
}),
zIndex: 2, //
visible: showCustomRoadLayer.value
@ -266,12 +266,12 @@ async function addRoadLayer() {
// -
const normalStyle = new Style({
stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.4)', width: 4 }),
fill: new Fill({ color: 'rgba(0, 0, 0, 0.5)' })
stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.4)', width: 1 }),
fill: new Fill({ color: 'rgba(0, 0, 0, 0.2)' })
});
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)' })
});

View 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. **性能监控**: 添加性能监控和优化

View 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; // 100ms50ms
const MAX_PREDICTION_TIME = 15000; // 10000ms15000ms
//
const ACCELERATION = 0.15; //
const DECELERATION = 0.25; //
const MAX_TURN_RATE = 100; // (/)
const MIN_TURN_RATE = 20; // (/)
const INERTIA_FACTOR = 0.97; // 0.950.97
const PREDICTION_STRENGTH = 6; // 86
const SPEED_SMOOTHING = 0.94; // 0.920.94
const POSITION_SMOOTHING = 0.12; // 0.150.12
const MIN_MOVE_THRESHOLD = 0.00005; // 0.00010.00005
const CONTINUOUS_MOVEMENT = true; //
const MIN_SPEED = 2.0; // 1.52.0
const PREDICTION_DECAY_RATE = 0.998; // 0.9950.998
const PATH_PREDICTION_ENABLED = true; //
const PATH_PREDICTION_POINTS = 16; // 1216使
const CONTINUOUS_MOVEMENT_THRESHOLD = 500; // 1000ms500ms
// -
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;
// deltaTime60fps
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.30.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.30.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.150.18
const predY = Math.sin(headingRad) * animData.currentSpeed * 0.18;
animData.predictionVector = [predX, predY];
//
animData.targetPosition = [
animData.position[0] + predX * 15, // 1215
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, // 1215
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.970.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) { // 810
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--) { // 57
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.30.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.30.4
predictionVector[1] * 0.4
];
}
}
//
if (!animData.speedHistory) {
animData.speedHistory = [speed, speed, speed, speed]; //
} else {
animData.speedHistory.push(speed);
if (animData.speedHistory.length > 4) { // 34
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>

View 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>

View 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) {
// 0ID
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>

View File

@ -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';
// SockJSpolyfill
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

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View File

@ -25,6 +25,13 @@
>
超界告警
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'speed' }"
@click="activeTab = 'speed'"
>
超速告警
</div>
</div>
<div class="tab-actions">
@ -41,6 +48,7 @@
<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-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>
</div>
<div class="alarm-content">
@ -100,6 +108,24 @@ const alarmList = ref([
date: '2025-03-19 10:30',
level: 'medium',
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;
left:70px;
top: 15%;
width: 360px;
width: 400px;
background-color: rgba(41, 44, 56, 0.95);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
@ -181,16 +207,17 @@ defineExpose({
}
.tab-item {
padding: 0 16px;
padding: 0 12px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-size: 13px;
color: #A0A8B7;
cursor: pointer;
position: relative;
transition: all 0.3s;
white-space: nowrap;
}
.tab-item.active {

View File

@ -109,7 +109,7 @@
>
<div class="track-timeline">
<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 class="track-content">
<div class="track-time">{{ item.time }}</div>
@ -404,15 +404,14 @@ function getBatteryTempClass(temp) {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #8ec6ff;
margin-top: 4px;
background-color: #fff;
z-index: 2;
}
.timeline-dot.active {
width: 14px;
height: 14px;
background-color: #1a6dff;
background-color: #6DB8FF;
box-shadow: 0 0 8px 2px rgba(26, 109, 255, 0.6);
}
@ -422,8 +421,8 @@ function getBatteryTempClass(temp) {
left: 50%;
transform: translateX(-50%);
width: 2px;
height: calc(100% + 7px);
background: linear-gradient(to bottom, #8ec6ff 50%, transparent 50%);
height: calc(100% - 10px);
background: linear-gradient(to bottom, #536C8F 50%, transparent 50%);
background-size: 2px 8px;
z-index: 1;
}

View File

@ -350,7 +350,7 @@ onMounted(() => {
}
.status-btn.task {
background-color: #00B1EB;
background-color: #5690E7;
}
.status-btn.idle {
@ -409,7 +409,7 @@ onMounted(() => {
}
.online-status.online {
background-color: #00B1EB;
background-color: #5690E7;
}
.online-status.offline {

View File

@ -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',
component: Layout,

View File

@ -115,7 +115,7 @@
<div class="control-group">
<label>服务器地址:</label>
<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>
</div>

View File

@ -81,8 +81,20 @@ let wsInstance = null;
export function createWebSocket(url, options) {
if (!wsInstance) {
wsInstance = new WebSocketService(url, options);
} else if (wsInstance.url !== url) {
// 如果URL改变重新创建实例
wsInstance.close();
wsInstance = new WebSocketService(url, options);
}
return wsInstance;
}
// 重置WebSocket实例用于强制重新创建实例
export function resetWebSocketInstance() {
if (wsInstance) {
wsInstance.close();
wsInstance = null;
}
}
export default WebSocketService;

View File

@ -191,7 +191,7 @@ const cardList = [
];
</script>
<style scoped>
<style scoped lang="scss">
.vehicle-detail-content {
width: 100%;
height: 100%;
@ -383,13 +383,13 @@ const cardList = [
display: flex;
align-items: center;
gap: 4px;
img {
}
.custom-tabs-label img {
width: 16px;
height: 16px;
filter: brightness(0) invert(1); /* 默认将图标变为白色 */
}
}
:deep(.el-tabs) {
--el-tabs-header-background-color: #1e2233;
--el-tabs-border-color: #303850;

View File

@ -6,7 +6,7 @@
<!-- 搜索区域 -->
<div class="search-area">
<el-input
v-model="queryParams.licensePlateNumber"
v-model="queryParams.licensePlate"
placeholder="请输入车牌号查询"
clearable
prefix-icon="Search"
@ -59,7 +59,7 @@
<el-button link text type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</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">
<template #default="scope">
{{ getVehicleTypeName(scope.row.typeId) }}
@ -104,8 +104,8 @@
<el-form ref="vehicleFormRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="车牌号" prop="licensePlateNumber">
<el-input v-model="form.licensePlateNumber" placeholder="请输入车牌号" />
<el-form-item label="车牌号" prop="licensePlate">
<el-input v-model="form.licensePlate" placeholder="请输入车牌号" />
</el-form-item>
</el-col>
<el-col :span="24">
@ -282,7 +282,7 @@ const statistics = computed(() => {
const queryParams = ref({
pageNum: 1,
pageSize: 10,
licensePlateNumber: undefined,
licensePlate: undefined,
typeId: undefined
});
@ -299,7 +299,7 @@ const importDialog = reactive({
//
const form = ref({
vehicleId: undefined,
licensePlateNumber: '',
licensePlate: '',
typeId: '',
brand: '',
owningUnit: '',
@ -310,7 +310,7 @@ const form = ref({
//
const rules = {
licensePlateNumber: [
licensePlate: [
{ required: true, message: "车牌号不能为空", trigger: "blur" }
],
typeId: [
@ -399,7 +399,7 @@ function resetQuery() {
queryParams.value = {
pageNum: 1,
pageSize: 10,
licensePlateNumber: undefined,
licensePlate: undefined,
typeId: undefined
};
handleQuery();
@ -521,7 +521,7 @@ function beforeImageUpload(file) {
function reset() {
form.value = {
vehicleId: undefined,
licensePlateNumber: '',
licensePlate: '',
typeId: '',
brand: '',
owningUnit: '',

View File

@ -7,6 +7,49 @@
@setCategoryVisibility="handleSetCategoryVisibility"
/>
<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" />
<!-- 报警通知按钮 -->
@ -26,7 +69,7 @@
<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>
<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 CarAlarm from '../../components/map/info/carClarm.vue';
import Eventlist from '../../components/map/info/eventlist.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_right from '../../assets/images/btn_right.png';
@ -62,6 +105,63 @@ const mapRef = ref(null);
const vehicleMovementRef = 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
const vehicleCategories = computed(() => {
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(() => {
document.querySelector('.app-main')?.classList.add('platform-no-padding');
//
updateCurrentTime();
//
const timeInterval = setInterval(updateCurrentTime, 1000);
//
if (mapRef.value) {
//
@ -138,6 +262,16 @@ onMounted(() => {
// testVehicleMovement();
}, 1000);
}
//
onUnmounted(() => {
clearInterval(timeInterval);
});
//
if (isDevelopment) {
setInterval(simulateNewAlarm, 5000); // 5
}
});
onUnmounted(() => {
@ -164,6 +298,146 @@ watch(() => mapRef.value?.map, (newMap) => {
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 {
position: absolute;

View File

@ -32,7 +32,7 @@ export default defineConfig(({ mode, command }) => {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
// target: 'http://10.0.0.17:8099',//昊天
target: 'http://10.0.0.124:8080',//田哥
target: 'http://10.0.0.126:8080',//田哥
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
}