修改平台概览UI

This commit is contained in:
renna 2025-07-15 11:49:27 +08:00
parent b8640adfc8
commit ca53570ff8
16 changed files with 1495 additions and 1213 deletions

132
CLAUDE.md Normal file
View File

@ -0,0 +1,132 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is the **青岛机场无人驾驶车辆协同云平台** (Qingdao Airport Autonomous Vehicle Collaborative Cloud Platform), a Vue 3 based web application for managing and monitoring autonomous vehicles at Qingdao Airport. The project uses RuoYi-Vue3 as the base framework.
## Development Commands
```bash
# Install dependencies
npm install
# Development server (runs on port 6580)
npm run dev
# or
yarn dev
# Build for production
npm run build:prod
# Build for staging
npm run build:stage
# Preview production build
npm run preview
```
**Note**: This project does not have configured linting, testing, or type checking scripts. Manual code review and browser testing are the primary quality assurance methods.
## Architecture Overview
### Tech Stack
- **Frontend Framework**: Vue 3.2.45 with Composition API
- **UI Library**: Element Plus 2.2.21
- **State Management**: Pinia 2.0.22
- **Router**: Vue Router 4.1.4
- **Build Tool**: Vite 3.2.3
- **Map Integration**: OpenLayers 6.15.1 with SuperMap iclient-ol
- **Real-time Communication**: WebSocket + STOMP protocol (@stomp/stompjs, sockjs-client)
### Key Features
- **Vehicle Management**: Real-time monitoring and control of autonomous vehicles
- **Map Visualization**: OpenLayers-based mapping with SuperMap integration for airport layouts
- **Real-time Data**: WebSocket connections for live vehicle tracking and status updates
- **System Administration**: User management, role-based permissions, operational logs
### Project Structure
#### Core Directories
- `src/views/platform/` - Main platform dashboard
- `src/views/car/` - Vehicle management interfaces (monitor, park, type)
- `src/components/map/` - OpenLayers map components and controls
- `src/components/car/` - Vehicle-specific UI components
- `src/api/` - API service definitions organized by domain
- `src/utils/websocket.js` - WebSocket service for real-time communication
#### Map System Architecture
The mapping system is built around OpenLayers with these key components:
**Core Map Component**: `src/components/map/OpenLayersMap.vue`
- Initializes map with EPSG:4528 projection for airport coordinates
- Integrates SuperMap tile services for base mapping
**Control Systems**:
- `VehicleAnimationSystem.vue` - Smooth vehicle movement animations with 60FPS engine
- `VehicleMovementControlRefactored.vue` - Enhanced vehicle tracking with motion prediction
- `LayerSwitcher.vue` - Dynamic layer management
- `VehicleStyleManager.vue` - Vehicle appearance and styling
**Real-time Features**:
- Motion prediction algorithms for smooth vehicle transitions
- 300ms timeout handling for connection interruptions
- Physical engine simulation based on vehicle speed and direction
### WebSocket Integration
The application uses a custom WebSocket service (`src/utils/websocket.js`) for real-time vehicle data:
- Auto-reconnection with configurable intervals
- Event-driven message handling
- Connection state management
- STOMP protocol support for structured messaging
### Development Server Configuration
The Vite development server is configured to:
- Run on port 6580 with auto-open
- Proxy API requests to backend servers:
- `/dev-api``http://10.0.0.126:8080` (田哥 server)
- Alternative: `http://10.0.0.17:8099` (昊天 server)
### Map Coordinate System
The project uses a custom projection `EPSG:4528` for airport mapping:
```javascript
proj4.defs("EPSG:4528", "+proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=GRS80 +units=m +no_defs");
```
Center coordinates: `[40507885.133754, 4025694.476392]`
### Component Patterns
**Global Components** (auto-registered):
- `Pagination` - Standardized pagination
- `RightToolbar` - Table operation toolbar
- `FileUpload` / `ImageUpload` - File handling
- `TreeSelect` - Hierarchical selection
- `DictTag` - Dictionary value display
**Route Structure**:
- Static routes defined in `src/router/index.js`
- Dynamic routes based on user permissions
- Layout-based routing with nested components
### Performance Optimizations
**Vehicle Animation System**:
- `requestAnimationFrame` for 60FPS smooth animations
- Distance threshold checks (0.1m) to avoid unnecessary calculations
- Motion history tracking (3 points) for trajectory prediction
- Easing functions for natural movement curves
### Known Issues & TODO
As documented in README.md:
1. Left sidebar menu state not preserved on refresh
2. First-level menu missing background color when second-level menu is selected
### API Backend Integration
- Uses axios for HTTP requests with interceptors
- JWT token authentication via js-cookie
- Centralized error handling and response formatting

101
README.md
View File

@ -26,87 +26,6 @@ yarn dev
超图地图开发使用基础
1、前期准备工作熟悉超图地图服务以及基本的操作。
2、熟悉Openlayers以及Leaflet相关操作
具体代码操作:
1、定义一个地图渲染承载框<div id="replay_map" />
2、定义一个地图实例 map: null, 定义一个渲染位置图层 layer: null
3、初始化地图
// 初始化地图
initMap() {
ol.proj.setProj4(proj4);
proj4.defs("EPSG:4528","+proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=GRS80 +units=m +no_defs");
var projection = new ol.proj.Projection({
code: 'EPSG:4528',// 地图坐标系
});
this.map = new ol.Map({
target: 'replay_map',//第一步设置的地图承载框
controls: ol.control.defaults({
attribution: false,
rotate: false
}),
view: new ol.View({
center: [40507885.133754 , 4025694.476392],//地图中心点
zoom: 12,//初始时的缩放比例
projection: projection,//坐标系等的设置
rotation: 0.3
}),
//图层,这个是后期所有自定义的展示层
layers: [
new ol.layer.Tile({
source: new ol.source.TileSuperMapRest({
crossOrigin: 'anonymous',
url: this.$map_url,
extent: [40347872.25,2703739.74,40599933.05,5912395.20]
}),
projection: projection,
})
]
});
// 添加位置图层
this.layer = new ol.layer.Vector({
source: new ol.source.Vector(),
zIndex: 2,
});
//把自定义展示的图层放到地图中
this.map.addLayer(this.layer);
},
3、图层自定义内容
//创建一个Feature
const feature = new ol.Feature({
//以一个点作为示例,可以是点、线、面
geometry: new ol.geom.Point([item.longitude, item.latitude]),
});
//定义样式
feature.setStyle(() => {
//自定义样式
const style = [];
style.push(this.getStyle(type, item));
return style;
}
//这个layer就是已开始定义的渲染图层
this.layer.getSource().addFeature(feature);
// 得到style实例
getStyle(type, item, status) {
return new ol.style.Style({
image: new ol.style.Icon({
src: getImage(type, item, status),
rotateWithView: true,
scale: this.getScale(type),
rotation: 0,
}),
zIndex: 3,
text: new ol.style.Text({
font: '12px 微软雅黑',
text: this.getText(type, item),
offsetY: -15,
fill: new ol.style.Fill({
color: '#515a71',
}),
}),
});
},
@ -114,17 +33,6 @@ getStyle(type, item, status) {
/** 获取用户列表 */
function getUserOptions() {
getRoleUsers(3).then(res => {
userOptions.value = res.rows;
});
}
使用SockJS + STOMP协议
滑出蓝色 滑入黄色
20250711关键改进说明
1. 平滑动画核心系统
@ -162,3 +70,12 @@ resetVehicleAnimations():重置所有动画数据
调用startVehicleSmoothing()启动平滑效果
处理WebSocket消息时继续调用updateVehiclePosition()
当组件隐藏时调用stopVehicleSmoothing()节省资源
### 运行方向分析:
地图上方(heading=72)
|
|
地图左侧(heading=342) --+-- 地图右侧(heading=162)
|
|
地图下方(heading=252)

65
public/quyu.json Normal file
View File

@ -0,0 +1,65 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[120.082584, 36.370196],
[120.085137, 36.370196],
[120.085137, 36.365117],
[120.082584, 36.365117],
[120.082584, 36.370196]
]
]
},
"properties": {
"name": "无人车A测试区域",
"type": "polygon",
"color": "#FF5733"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[120.085765, 36.371484],
[120.087465, 36.371484],
[120.087465, 36.368099],
[120.085765, 36.368099],
[120.085765, 36.371484]
]
]
},
"properties": {
"name": "无人车B测试区域",
"type": "polygon",
"color": "#3374FF"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[120.083, 36.371],
[120.087, 36.371],
[120.087, 36.365],
[120.083, 36.365],
[120.083, 36.371]
]
]
},
"properties": {
"name": "无人车交汇测试区域",
"type": "polygon",
"color": "#33FF57"
}
}
]
}

49
public/quyu1.json Normal file
View File

@ -0,0 +1,49 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
120.08646134220226,
36.36997466676866
],
[
120.086830266589,
36.37063980458865
],
[
120.08515882034526,
36.37156622537344
],
[
120.08543839423878,
36.37073891647525
],
[
120.0851419887492,
36.370416189872486
],
[
120.0858980160674,
36.36986114517269
],
[
120.08646134220226,
36.36997466676866
]
]
]
},
"properties": {
"type": "polygon",
"area": 10388.360616276083,
"perimeter": 452.43662412430064,
"vertices": 6
}
}
]
}

77
public/quyu2.json Normal file
View File

@ -0,0 +1,77 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
120.08483597765024,
36.36489863472901
],
[
120.08549104408633,
36.36594996564427
],
[
120.08416649728491,
36.36673180599232
],
[
120.08378219088705,
36.36561378252756
],
[
120.08483597765024,
36.36489863472901
]
]
]
},
"properties": {
"type": "polygon",
"area": 11470.775100380806,
"perimeter": 439.0229879087821,
"vertices": 4
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
120.08483597765024,
36.36489863472901
],
[
120.08549104408633,
36.36594996564427
],
[
120.08416649728491,
36.36673180599232
],
[
120.08378219088705,
36.36561378252756
],
[
120.08483597765024,
36.36489863472901
]
]
]
},
"properties": {
"type": "polygon",
"area": 11470.775100380806,
"perimeter": 439.0229879087821,
"vertices": 4
}
}
]
}

View File

@ -83,6 +83,22 @@
<!-- <svg class="layer-icon-preview" width="24" height="24" viewBox="0 0 1024 1024"><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286z" fill="#FF5722"/></svg> -->
</label>
</div>
<!-- 添加区域1图层选项 -->
<div class="layer-item">
<label class="checkbox-container">
<input type="checkbox" v-model="showArea1Layer" />
<span class="checkmark"></span>
<span class="layer-name">测试区域1</span>
</label>
</div>
<!-- 添加区域2图层选项 -->
<div class="layer-item">
<label class="checkbox-container">
<input type="checkbox" v-model="showArea2Layer" />
<span class="checkmark"></span>
<span class="layer-name">测试区域2</span>
</label>
</div>
</div>
</div>
</div>
@ -129,6 +145,14 @@ let roadVectorLayer = null;
const showCustomRoadLayer = ref(true); //
let customRoadVectorLayer = null;
// 1
const showArea1Layer = ref(true); //
let area1VectorLayer = null;
// 2
const showArea2Layer = ref(true); //
let area2VectorLayer = null;
//
const emit = defineEmits(['layerChange', 'setCategoryVisibility']);
@ -159,6 +183,12 @@ onMounted(() => {
if (showRoadLayer.value) {
addRoadLayer();
}
// 1
loadArea1Layer();
// 2
loadArea2Layer();
}
});
@ -172,6 +202,12 @@ watch(() => props.map, (newMap) => {
if (showRoadLayer.value) {
addRoadLayer();
}
// 1
loadArea1Layer();
// 2
loadArea2Layer();
}
});
@ -349,9 +385,145 @@ watch(showRoadLayer, (val) => {
else removeRoadLayer();
});
// 1
async function loadArea1Layer() {
// 1
removeArea1Layer();
if (!props.map) return;
try {
// 使1
const res = await fetch('./quyu1.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
area1VectorLayer = new VectorLayer({
source,
style: new Style({
stroke: new Stroke({ color: '#FF5733', width: 2 }),
fill: new Fill({ color: 'rgba(255, 87, 51, 0.3)' })
}),
zIndex: 3, //
visible: showArea1Layer.value
});
props.map.addLayer(area1VectorLayer);
console.log('loadArea1Layer: 已添加区域1图层', area1VectorLayer);
} catch (e) {
console.error('loadArea1Layer: 加载或添加区域1图层失败', e);
}
}
// 1
function removeArea1Layer() {
if (!props.map) return;
let removed = false;
// VectorLayer zIndex 3 1
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 3) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeArea1Layer: 已移除区域1图层', lyr);
}
}
area1VectorLayer = null;
if (!removed) {
console.log('removeArea1Layer: 没有找到可移除的区域1图层');
}
}
// 2
async function loadArea2Layer() {
// 2
removeArea2Layer();
if (!props.map) return;
try {
// 使2
const res = await fetch('./quyu2.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
area2VectorLayer = new VectorLayer({
source,
style: new Style({
stroke: new Stroke({ color: '#3374FF', width: 2 }),
fill: new Fill({ color: 'rgba(51, 116, 255, 0.3)' })
}),
zIndex: 4, // 1
visible: showArea2Layer.value
});
props.map.addLayer(area2VectorLayer);
console.log('loadArea2Layer: 已添加区域2图层', area2VectorLayer);
} catch (e) {
console.error('loadArea2Layer: 加载或添加区域2图层失败', e);
}
}
// 2
function removeArea2Layer() {
if (!props.map) return;
let removed = false;
// VectorLayer zIndex 4 2
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 4) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeArea2Layer: 已移除区域2图层', lyr);
}
}
area2VectorLayer = null;
if (!removed) {
console.log('removeArea2Layer: 没有找到可移除的区域2图层');
}
}
// 1
watch(showArea1Layer, (val) => {
console.log('showArea1Layer变化:', val);
if (val) {
if (area1VectorLayer) {
area1VectorLayer.setVisible(true);
} else {
loadArea1Layer();
}
} else if (area1VectorLayer) {
area1VectorLayer.setVisible(false);
}
});
// 2
watch(showArea2Layer, (val) => {
console.log('showArea2Layer变化:', val);
if (val) {
if (area2VectorLayer) {
area2VectorLayer.setVisible(true);
} else {
loadArea2Layer();
}
} else if (area2VectorLayer) {
area2VectorLayer.setVisible(false);
}
});
onUnmounted(() => {
removeRoadLayer();
removeCustomRoadLayer();
removeArea1Layer();
removeArea2Layer();
stopFenceFlashing();
});

View File

@ -1,167 +0,0 @@
# 车辆移动控制系统组件重构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

@ -25,19 +25,18 @@ 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.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
const INERTIA_FACTOR = 0.99; // 0.9850.99
const PREDICTION_STRENGTH = 3; // 43
const SPEED_SMOOTHING = 0.98; // 0.970.98
const POSITION_SMOOTHING = 0.06; // 0.080.06
const MIN_MOVE_THRESHOLD = 0.000005; // 0.000010.000005
const CONTINUOUS_MOVEMENT = true; //
const MIN_SPEED = 0.5; // 1.00.5
const PREDICTION_DECAY_RATE = 0.9995; // 0.9990.9995
const PATH_PREDICTION_ENABLED = true; //
const PATH_PREDICTION_POINTS = 15; // 2015
const CONTINUOUS_MOVEMENT_THRESHOLD = 200; // 300ms200ms
const POSITION_UPDATE_THRESHOLD = 3; // 53
// -
function easeInOutQuad(t) {
@ -99,14 +98,13 @@ function updateVehicleAnimations(deltaTime) {
// 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;
const speedSmoothingFactor = vehicle.speedViolation ? 0.98 : SPEED_SMOOTHING;
animData.currentSpeed = animData.currentSpeed * speedSmoothingFactor +
animData.speed * (1 - speedSmoothingFactor);
}
@ -117,10 +115,10 @@ function updateVehicleAnimations(deltaTime) {
if (timeSinceLastUpdate > PREDICTION_THRESHOLD && animData.predictionVector) {
//
const predictTime = Math.min(timeSinceLastUpdate, MAX_PREDICTION_TIME);
const predictFactor = (predictTime - PREDICTION_THRESHOLD) / 1000 * 0.8;
const predictFactor = (predictTime - PREDICTION_THRESHOLD) / 1000 * 0.6; // 0.80.6
//
const speedFactor = Math.min(1.2, currentSpeed / 25); //
const speedFactor = Math.min(1.0, currentSpeed / 30); // 1.2/251.0/30
//
const predictedPosition = [
@ -134,7 +132,7 @@ function updateVehicleAnimations(deltaTime) {
//
if (predictTime > 2000) {
//
const slowdownFactor = Math.max(0.5, 1 - (predictTime - 2000) / 10000);
const slowdownFactor = Math.max(0.6, 1 - (predictTime - 2000) / 15000); // 0.5/100000.6/15000
currentSpeed = Math.max(currentSpeed * slowdownFactor, MIN_SPEED); // 0
}
}
@ -144,6 +142,11 @@ function updateVehicleAnimations(deltaTime) {
const dy = targetPosition[1] - animData.position[1];
const distance = Math.sqrt(dx * dx + dy * dy);
//
if (distance < MIN_MOVE_THRESHOLD && !CONTINUOUS_MOVEMENT) {
return;
}
// 3.
if (distance > MIN_MOVE_THRESHOLD || CONTINUOUS_MOVEMENT) {
//
@ -153,11 +156,11 @@ function updateVehicleAnimations(deltaTime) {
let moveRatio;
// 使
const positionSmoothingFactor = vehicle.speedViolation ? POSITION_SMOOTHING * 0.8 : POSITION_SMOOTHING;
const positionSmoothingFactor = vehicle.speedViolation ? POSITION_SMOOTHING * 0.7 : POSITION_SMOOTHING;
if (distance < idealDistance * 1.5) {
if (distance < idealDistance * 1.2) { // 1.51.2
// 使
moveRatio = Math.min(positionSmoothingFactor * 1.5, idealDistance / distance);
moveRatio = Math.min(positionSmoothingFactor * 1.2, idealDistance / distance); // 1.51.2
moveRatio = easeOutQuint(moveRatio);
} else {
// 使
@ -169,7 +172,7 @@ function updateVehicleAnimations(deltaTime) {
// 使
if (distance < MIN_MOVE_THRESHOLD && animData.predictionVector) {
// 使使
const minMoveAmount = (currentSpeed * 1000 / 3600) * (deltaTime / 1000) * 0.1;
const minMoveAmount = (currentSpeed * 1000 / 3600) * (deltaTime / 1000) * 0.05; // 0.10.05
//
const predLen = Math.sqrt(
@ -194,7 +197,7 @@ function updateVehicleAnimations(deltaTime) {
else {
if (animData.lastDx !== undefined && animData.lastDy !== undefined) {
// 使
const inertiaFactor = vehicle.speedViolation ? INERTIA_FACTOR * 1.1 : INERTIA_FACTOR;
const inertiaFactor = vehicle.speedViolation ? INERTIA_FACTOR * 1.05 : INERTIA_FACTOR; // 1.11.05
const inertiaX = animData.lastDx * inertiaFactor;
const inertiaY = animData.lastDy * inertiaFactor;
@ -204,8 +207,8 @@ function updateVehicleAnimations(deltaTime) {
const newDy = dy * moveRatio;
//
const inertiaWeight = Math.min(0.85, currentSpeed / 70) *
Math.min(1.0, distance / 8); //
const inertiaWeight = Math.min(0.75, currentSpeed / 80) * // 0.85/700.75/80
Math.min(0.9, distance / 10); // 1.0/80.9/10
//
const nextX = animData.position[0] + newDx * (1 - inertiaWeight) + inertiaX * inertiaWeight;
@ -238,69 +241,18 @@ function updateVehicleAnimations(deltaTime) {
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);
// heading
if (feature && props.getVehicleStyle) {
//
if (!animData.lastStyleUpdateTime || currentTime - animData.lastStyleUpdateTime > 500) {
feature.setStyle(props.getVehicleStyle(id, currentSpeed, animData.heading));
animData.lastStyleUpdateTime = currentTime;
// heading
const vehicle = props.vehicles[id];
const currentHeading = vehicle ? vehicle.heading : 0;
// heading
if (!animData.lastHeading || Math.abs(animData.lastHeading - currentHeading) > 1) {
feature.setStyle(props.getVehicleStyle(id, currentSpeed, currentHeading));
animData.lastHeading = currentHeading;
}
}
}
}
@ -368,22 +320,45 @@ function ensureContinuousMovement(animData, vehicle, currentTime, deltaTime) {
];
}
}
//
//
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;
// 使
if (animData.pathHistory && animData.pathHistory.length >= 2) {
const latestPoints = animData.pathHistory.slice(-2);
const dx = latestPoints[1].position[0] - latestPoints[0].position[0];
const dy = latestPoints[1].position[1] - latestPoints[0].position[1];
const len = Math.sqrt(dx*dx + dy*dy);
if (len > 0.001) {
const normalizedDx = dx / len;
const normalizedDy = dy / len;
const predX = normalizedDx * animData.currentSpeed * 0.18;
const predY = normalizedDy * animData.currentSpeed * 0.18;
animData.predictionVector = [predX, predY];
//
animData.targetPosition = [
animData.position[0] + predX * 15, // 1215
animData.position[0] + predX * 15,
animData.position[1] + predY * 15
];
} else {
// 使
animData.predictionVector = [0, animData.currentSpeed * 0.18];
animData.targetPosition = [
animData.position[0],
animData.position[1] + animData.currentSpeed * 0.18 * 15
];
}
} else {
// 使
animData.predictionVector = [0, animData.currentSpeed * 0.18];
animData.targetPosition = [
animData.position[0],
animData.position[1] + animData.currentSpeed * 0.18 * 15
];
}
} else {
//
// 使
@ -488,15 +463,15 @@ function initVehicleAnimation(id, coordinates, heading, speed) {
vehicleAnimations.value[id] = {
position: [...coordinates],
targetPosition: [...coordinates],
heading: heading,
targetHeading: heading,
heading: heading, // heading
targetHeading: heading, // heading
speed: speed,
currentSpeed: speed,
lastUpdated: Date.now(),
predictionVector: null,
lastDx: 0,
lastDy: 0,
movementHeading: heading,
lastHeading: heading, // lastHeadingheading
//
speedHistory: [speed, speed, speed],
pathHistory: [], //
@ -516,7 +491,7 @@ function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
vehicleMotionHistory.value[id].push({
time: now,
position: coordinates,
heading: heading,
heading: heading, // heading
speed: speed
});
@ -620,7 +595,8 @@ function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
vehicleAnimations.value[id] = {
...animData,
targetPosition: coordinates,
targetHeading: heading,
targetHeading: heading, // heading
heading: animData.heading || heading, // heading使heading
position: animData.position || coordinates,
speed: smoothedSpeed, // 使
lastUpdated: now,
@ -648,15 +624,15 @@ function resetAnimations() {
vehicleAnimations.value[id] = {
position: [...vehicle.position],
targetPosition: [...vehicle.position],
heading: vehicle.heading,
targetHeading: vehicle.heading,
heading: vehicle.heading || 0, // 使heading
targetHeading: vehicle.heading || 0, // 使heading
speed: vehicle.speed,
currentSpeed: vehicle.speed,
lastUpdated: Date.now(),
predictionVector: null,
lastDx: 0,
lastDy: 0,
movementHeading: vehicle.heading,
lastHeading: vehicle.heading || 0, // 使heading
speedHistory: [vehicle.speed, vehicle.speed, vehicle.speed],
pathHistory: [], //
lastPathRecordTime: Date.now() //

View File

@ -1,10 +1,10 @@
<template>
<!-- 标签系统不需要模板内容 -->
<!-- 标签容器不在模板中渲染任何内容标签由JS动态创建 -->
</template>
<script setup>
import { ref } from 'vue';
import Overlay from 'ol/Overlay';
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Overlay } from 'ol';
// props
const props = defineProps({
@ -12,288 +12,462 @@ const props = defineProps({
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';
//
const vehicleLabels = ref({});
//
const labelUpdateTimes = ref({});
// ()
const UPDATE_INTERVAL = 500; // 500ms
//
function updateVehicleLabel(id, position, speed, violationInfo = null) {
if (!props.map || !props.vehicles[id]) return;
//
function updateVehicleLabel(vehicleId, coordinates, speed, alertInfo) {
if (!props.map) return;
//
removeVehicleLabel(id);
//
const now = Date.now();
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;
//
//
if (!labelUpdateTimes.value[vehicleId] ||
(now - labelUpdateTimes.value[vehicleId]) > UPDATE_INTERVAL) {
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';
//
if (vehicleLabels.value[vehicleId]) {
//
vehicleLabels.value[vehicleId].setPosition(coordinates);
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;
//
updateLabelContent(vehicleId, speed, alertInfo);
} else {
//
backgroundImage = labelBg;
//
createNewLabel(vehicleId, coordinates, speed, alertInfo);
}
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}请减速慢行!`;
}
//
labelUpdateTimes.value[vehicleId] = now;
}
}
labelDiv.innerHTML = labelText;
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
labelDiv.style.backgroundSize = '100% 100%';
labelDiv.style.padding = '5px 10px';
labelDiv.style.color = '#fff';
//
function createNewLabel(vehicleId, coordinates, speed, alertInfo) {
// DOM
const labelElement = document.createElement('div');
labelElement.className = 'vehicle-label';
labelElement.id = `label-${vehicleId}`;
//
const labelContent = createLabelContent(vehicleId, speed, alertInfo);
labelElement.innerHTML = labelContent;
//
const overlay = new Overlay({
element: labelDiv,
position: position,
element: labelElement,
position: coordinates,
offset: [0, -30], // 使
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);
//
vehicleLabels.value[vehicleId] = overlay;
//
setupTabClickListeners(vehicleId);
}
//
function updateLabelContent(vehicleId, speed, alertInfo) {
const labelElement = document.getElementById(`label-${vehicleId}`);
if (!labelElement) return;
//
const labelContent = createLabelContent(vehicleId, speed, alertInfo);
labelElement.innerHTML = labelContent;
//
setupTabClickListeners(vehicleId);
}
//
function createLabelContent(vehicleId, speed, alertInfo = {}) {
//
const vehicle = props.vehicles[vehicleId];
if (!vehicle) return '';
// 1
const formattedSpeed = speed ? `${speed.toFixed(1)}km/h` : '0.0km/h';
//
const hasWarning = vehicle.warning || (alertInfo && alertInfo.isWarning);
const hasAlarm = vehicle.alarm || (alertInfo && alertInfo.isAlarm);
const hasSpeedViolation = vehicle.speedViolation || (alertInfo && alertInfo.isSpeedViolation);
const hasUnauthorizedEntry = vehicle.critical || (alertInfo && alertInfo.isUnauthorizedEntry);
//
const hasAnyAlert = hasWarning || hasAlarm || hasSpeedViolation || hasUnauthorizedEntry;
//
if (!hasAnyAlert) {
return `
<div class="label-container simple-label">
<div class="label-header">
<span class="vehicle-id">${vehicleId}</span>
<span class="vehicle-speed">${formattedSpeed}</span>
</div>
</div>
`;
}
// Tab
let html = `
<div class="label-container">
<div class="label-header">
<span class="vehicle-id">${vehicleId}</span>
<span class="vehicle-speed">${formattedSpeed}</span>
</div>
<div class="label-tabs">
<div class="tab-buttons">
<button class="tab-button ${hasWarning ? 'active' : 'disabled'}" data-tab="warning" data-vehicle="${vehicleId}">
<i class="tab-icon warning-icon"></i>
</button>
<button class="tab-button ${hasAlarm ? 'active' : 'disabled'}" data-tab="alarm" data-vehicle="${vehicleId}">
<i class="tab-icon alarm-icon"></i>
</button>
<button class="tab-button ${hasSpeedViolation ? 'active' : 'disabled'}" data-tab="speed" data-vehicle="${vehicleId}">
<i class="tab-icon speed-icon"></i>
</button>
<button class="tab-button ${hasUnauthorizedEntry ? 'active' : 'disabled'}" data-tab="boundary" data-vehicle="${vehicleId}">
<i class="tab-icon boundary-icon"></i>
</button>
</div>
<div class="tab-content">
`;
//
let activeTabContent = '';
let activeTabFound = false;
//
if (hasWarning) {
const warningMessage = alertInfo?.description || '与其他车辆距离过近,请注意避让!';
activeTabContent = `
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="warning" data-vehicle="${vehicleId}">
<div class="alert-message">${warningMessage}</div>
</div>
`;
activeTabFound = true;
}
//
if (hasAlarm) {
const alarmMessage = alertInfo?.description || '与其他车辆存在冲突风险!';
activeTabContent += `
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="alarm" data-vehicle="${vehicleId}">
<div class="alert-message">${alarmMessage}</div>
</div>
`;
if (!activeTabFound) activeTabFound = true;
}
//
if (hasSpeedViolation) {
const limitValue = alertInfo?.limitValue || vehicle.limitValue || 30;
const actualValue = alertInfo?.actualValue || vehicle.actualValue || speed;
activeTabContent += `
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="speed" data-vehicle="${vehicleId}">
<div class="speed-info">
<div>规定速度: <span class="limit-value">${limitValue}km/h</span></div>
<div>当前速度: <span class="actual-value">${actualValue.toFixed(1)}km/h</span></div>
</div>
</div>
`;
if (!activeTabFound) activeTabFound = true;
}
//
if (hasUnauthorizedEntry) {
const boundaryMessage = alertInfo?.description || '车辆已进入禁行区域!';
activeTabContent += `
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="boundary" data-vehicle="${vehicleId}">
<div class="alert-message">${boundaryMessage}</div>
</div>
`;
if (!activeTabFound) activeTabFound = true;
}
html += activeTabContent;
html += `
</div>
</div>
</div>
`;
return html;
}
//
function setupTabClickListeners(vehicleId) {
setTimeout(() => {
const tabButtons = document.querySelectorAll(`.tab-button[data-vehicle="${vehicleId}"]`);
tabButtons.forEach(button => {
//
button.removeEventListener('click', handleTabClick);
//
button.addEventListener('click', handleTabClick);
});
//
const firstActiveButton = document.querySelector(`.tab-button.active[data-vehicle="${vehicleId}"]`);
if (firstActiveButton && !firstActiveButton.classList.contains('current')) {
firstActiveButton.click();
}
}, 100);
}
//
function handleTabClick(event) {
const button = event.currentTarget;
const tabName = button.getAttribute('data-tab');
const vehicleId = button.getAttribute('data-vehicle');
//
if (button.classList.contains('disabled')) {
return;
}
//
const tabButtons = document.querySelectorAll(`.tab-button[data-vehicle="${vehicleId}"]`);
const tabPanes = document.querySelectorAll(`[data-tab-content][data-vehicle="${vehicleId}"]`);
//
tabButtons.forEach(btn => btn.classList.remove('current'));
tabPanes.forEach(pane => pane.classList.remove('active'));
//
button.classList.add('current');
const activePane = document.querySelector(`[data-tab-content="${tabName}"][data-vehicle="${vehicleId}"]`);
if (activePane) {
activePane.classList.add('active');
}
//
event.stopPropagation();
}
//
function removeVehicleLabel(id) {
if (!props.map || !props.vehicles[id]) return;
function removeVehicleLabel(vehicleId) {
if (vehicleLabels.value[vehicleId]) {
props.map.removeOverlay(vehicleLabels.value[vehicleId]);
delete vehicleLabels.value[vehicleId];
}
}
if (props.vehicles[id].overlay) {
props.map.removeOverlay(props.vehicles[id].overlay);
props.vehicles[id].overlay = null;
props.vehicles[id].labelDiv = null;
//
function setLabelVisibility(vehicleId, visible) {
if (vehicleLabels.value[vehicleId]) {
const labelElement = vehicleLabels.value[vehicleId].getElement();
if (labelElement) {
labelElement.style.display = visible ? 'block' : 'none';
}
}
}
//
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);
}
if (!props.map || !props.vehicles) return;
Object.keys(vehicleLabels.value).forEach(vehicleId => {
if (props.vehicles[vehicleId] && props.vehicles[vehicleId].position) {
vehicleLabels.value[vehicleId].setPosition(props.vehicles[vehicleId].position);
}
});
}
//
function setLabelVisibility(id, visible) {
if (!props.map || !props.vehicles[id]) return;
//
onUnmounted(() => {
if (props.map) {
Object.values(vehicleLabels.value).forEach(overlay => {
props.map.removeOverlay(overlay);
});
}
vehicleLabels.value = {};
});
if (visible) {
//
if (props.vehicles[id].position && !props.vehicles[id].speedViolation) {
updateVehicleLabel(id, props.vehicles[id].position, props.vehicles[id].speed);
}
} else {
removeVehicleLabel(id);
//
watch(() => props.map, (newMap) => {
if (newMap) {
//
Object.keys(vehicleLabels.value).forEach(vehicleId => {
if (props.vehicles[vehicleId] && props.vehicles[vehicleId].position) {
removeVehicleLabel(vehicleId);
updateVehicleLabel(
vehicleId,
props.vehicles[vehicleId].position,
props.vehicles[vehicleId].speed
);
}
});
}
});
//
//
defineExpose({
updateVehicleLabel,
removeVehicleLabel,
updateAllLabels,
setLabelVisibility
setLabelVisibility,
updateAllLabels
});
</script>
<style scoped>
/* 车辆标签样式 */
:deep(.vehicle-label) {
<style>
/* 标签容器样式 */
.vehicle-label {
position: absolute;
padding: 4px 8px;
transform: translate(-50%, -100%);
z-index: 1000;
pointer-events: auto;
}
.label-container {
background-color: rgba(37, 37, 37, 0.7);
border-radius: 4px;
color: #fff;
padding: 10px;
min-width: 170px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* 简化标签样式 */
.simple-label {
padding: 6px 10px;
}
/* 标签头部样式 */
.label-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 5px;
}
.simple-label .label-header {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
.vehicle-id {
font-weight: bold;
font-size: 14px;
}
.vehicle-speed {
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;
color: #ccc;
}
/* 标签页按钮样式 */
.tab-buttons {
display: flex;
gap: 5px;
margin-bottom: 8px;
}
.tab-button {
background-color: rgba(80, 80, 80, 0.5);
border: none;
border-radius: 4px;
width: 24px;
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;
cursor: pointer;
padding: 0;
}
/* 滑入航空器标签样式 */
:deep(.vehicle-aircraft-in) {
color: #fff;
.tab-button.active {
background-color: rgba(52, 152, 219, 0.7);
}
/* 滑出航空器标签样式 */
:deep(.vehicle-aircraft-out) {
color: #333;
.tab-button.current {
background-color: rgba(52, 152, 219, 1);
box-shadow: 0 0 5px rgba(52, 152, 219, 0.7);
}
/* 无人车标签样式 */
:deep(.vehicle-unmanned) {
color: #fff;
.tab-button.disabled {
background-color: rgba(80, 80, 80, 0.3);
cursor: not-allowed;
}
/* 特勤车标签样式 */
:deep(.vehicle-special) {
color: #fff;
.tab-button.disabled .tab-icon {
opacity: 0.5;
filter: grayscale(100%);
}
/* 摆渡车标签样式 */
:deep(.vehicle-shuttle) {
color: #fff;
/* 标签图标样式 */
.tab-icon {
width: 16px;
height: 16px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
/* 告警标签样式 */
:deep(.vehicle-alarm) {
color: #fff;
font-weight: bold;
.warning-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FFA500"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>');
}
/* 预警标签样式 */
:deep(.vehicle-warning) {
color: #fff;
.alarm-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FF4D4F"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>');
}
/* 严重违规标签样式 */
:deep(.vehicle-critical) {
color: #fff;
.speed-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FFD700"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
}
/* 超速违规标签样式 */
:deep(.vehicle-speed-violation) {
color: #fff;
font-weight: bold;
.boundary-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FF6B6B"><path d="M3 5v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2zm16 14H5V5h14v14z"/></svg>');
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
/* 告警消息样式 */
.alert-message {
font-size: 12px;
line-height: 1.4;
white-space: normal;
width: 180px;
text-align: center;
padding: 5px;
border-radius: 3px;
/* background-color: rgba(255, 255, 255, 0.1); */
}
/* 信息级别(超速等)标签样式 */
:deep(.vehicle-info) {
color: #fff;
/* 超速信息样式 */
.speed-info {
font-size: 12px;
line-height: 1.4;
white-space: normal;
width: 160px;
text-align: center;
}
.limit-value {
color: #4CAF50;
}
.actual-value {
color: #FF4D4F;
font-weight: bold;
}
</style>

View File

@ -63,8 +63,21 @@ import VehicleStyleManager from './VehicleStyleManager.vue';
//
import carIcon from '../../../assets/images/noPeopleCar.png';
import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 使
import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 使
import aircraftIcon from '../../../assets/images/Aircraft.png'; // 使Aircraft.png
// 使
// import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 使
// import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 使
//
const carIconImg = new Image();
carIconImg.src = carIcon;
const aircraftIconImg = new Image();
aircraftIconImg.src = aircraftIcon;
//
// const aircraftInIconImg = new Image();
// aircraftInIconImg.src = aircraftInIcon;
// const aircraftOutIconImg = new Image();
// aircraftOutIconImg.src = aircraftOutIcon;
// SockJSpolyfill
if (typeof window !== 'undefined' && !window.global) {
@ -235,6 +248,10 @@ function updateVehiclePosition(vehicleData) {
const { object_id, object_type, position, heading, speed } = vehicleData;
// 72
const rotationRad = (heading * Math.PI) / 180;
console.log(`车辆${object_id}的heading值: ${heading}, 计算的旋转角度: ${rotationRad} 弧度, ${rotationRad * 180 / Math.PI}`);
let coordinates;
coordinates = transform(
@ -248,56 +265,67 @@ function updateVehiclePosition(vehicleData) {
// object_type
let vehicleType = object_type.toUpperCase();
//
// CA使Aircraft1.pngairport_out.png
// MU使Aircraft.pngairport_bg.png
const isAircraftOut = vehicleType === 'AIRCRAFT' && object_id.toLowerCase().includes('ca');
const isAircraftIn = vehicleType === 'AIRCRAFT' && object_id.toLowerCase().includes('mu');
const isAircraft = vehicleType === 'AIRCRAFT';
//
const isAircraft = vehicleType === 'AIRCRAFT'; // 使aircraft.png
const isUnmannedVehicle = vehicleType === 'UNMANNED_VEHICLE'; //
const isSpecialVehicle = vehicleType === 'SPECIAL_VEHICLE'; //
//
const isUnmannedVehicle = vehicleType === 'UNMANNED_VEHICLE'; // 使noPeopleCar.pnglabel_bg.png
const isSpecialVehicle = vehicleType === 'AIRPORT_VEHICLE'; //
const isShuttleVehicle = vehicleType === 'SHUTTLE_VEHICLE'; //
// heading
const validHeading = heading !== undefined ? Number(heading) : 0;
if (!feature) {
// feature
vehicles.value[object_id] = {
id: object_id,
type: object_type,
position: coordinates,
heading: validHeading,
speed: speed,
isAircraft: isAircraft,
isUnmannedVehicle: isUnmannedVehicle,
isSpecialVehicle: isSpecialVehicle,
lastHeading: validHeading // lastHeading
};
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
isSpecialVehicle: isSpecialVehicle
});
feature.setId(object_id);
// 使styleManager.value使
if (styleManager.value) {
feature.setStyle(styleManager.value.getVehicleStyle(object_id, speed, heading));
// 使
let iconStyle;
if (isAircraft) {
iconStyle = new Style({
image: new Icon({
src: aircraftIcon,
scale: 1.5,
anchor: [0.5, 0.5],
rotation: rotationRad, // 使
})
});
} else {
//
feature.setStyle(defaultGetVehicleStyle(object_id, speed, heading));
iconStyle = new Style({
image: new Icon({
src: carIcon,
scale: 1.5,
anchor: [0.5, 0.5],
rotation: rotationRad, // 使
})
});
}
feature.setStyle(iconStyle);
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
};
// feature
vehicles.value[object_id].feature = feature;
//
if (animationSystem.value) {
@ -317,7 +345,7 @@ function updateVehiclePosition(vehicleData) {
vehicles.value[object_id] = {
...vehicles.value[object_id],
position: coordinates,
heading: heading,
heading: validHeading,
speed: speed
};
@ -326,6 +354,24 @@ function updateVehiclePosition(vehicleData) {
if (labelSystem.value && !vehicles.value[object_id].speedViolation) {
labelSystem.value.updateVehicleLabel(object_id, coordinates, speed);
}
//
if (feature) {
let iconSrc = isAircraft ? aircraftIcon : carIcon;
//
feature.setStyle(new Style({
image: new Icon({
src: iconSrc,
scale: 1.5,
anchor: [0.5, 0.5],
rotation: rotationRad, // 使
})
}));
// lastHeading
vehicles.value[object_id].lastHeading = validHeading;
}
}
}
@ -383,68 +429,178 @@ function connectWebSocket() {
function handleWsMessage(message) {
try {
const data = JSON.parse(message);
console.log('收到消息:', data);
//
switch (data.type) {
case 'connection':
console.log(`连接确认: ${data.message}`);
break;
case 'position_update':
// payload
if (data.payload && data.payload.object_id) {
//
clearVehicleAlertStatus(data.payload);
//
updateVehiclePosition(data.payload);
} else {
console.error('位置更新消息格式错误:', data);
}
case 'position_update':
//
console.log(`位置更新: ${data.payload?.object_id} (${data.payload?.object_type})`);
handlePositionUpdate(data.payload);
break;
case 'path_conflict_alert':
//
console.log(`冲突告警/预警: ${data.payload?.object1?.objectName || data.payload?.messageType}`);
handlePathConflictAlert(data.payload);
break;
case 'rule_violation':
//
console.log(`规则违规: ${data.payload?.ruleName || data.payload?.violationType}`);
handleRuleViolation(data.payload);
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);
//
case 'vehicle_command':
console.log('收到车辆控制指令:', data.payload);
break;
default:
//
console.log(`未知消息类型: ${data.type}`, data);
break;
}
} catch (e) {
console.error('处理WebSocket消息出错:', e, message);
}
}
//
function handlePositionUpdate(payload) {
if (!payload || !payload.object_id) {
console.error('位置更新消息格式错误:', payload);
return;
}
//
clearVehicleAlertStatus(payload);
//
updateVehiclePosition(payload);
}
//
function handlePathConflictAlert(payload) {
if (!payload) {
console.error('冲突告警消息格式错误:', payload);
return;
}
console.log('收到冲突告警/预警:', payload);
const object1 = payload.object1 || {};
const object2 = payload.object2 || {};
const vehicleId = object1.objectName || '未知车辆';
const otherVehicleId = object2.objectName || '未知车辆';
const distance = payload.object2Distance || 0;
const message = payload.message || `${otherVehicleId}可能发生冲突`;
// messageType
if (payload.messageType === 'PATH_CONFLICT_ALERT' || payload.alertType === 'CONFLICT_WARNING') {
console.log('处理冲突预警:', vehicleId, otherVehicleId);
const warningMessage = `预警:${message}`;
showAlert(warningMessage, 'warning', 8000);
//
if (vehicles.value[vehicleId]) {
vehicles.value[vehicleId].warning = true;
vehicles.value[vehicleId].alarm = false;
vehicles.value[vehicleId].critical = false;
vehicles.value[vehicleId].info = false;
//
if (vehicles.value[vehicleId].position) {
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
description: message,
isWarning: true
});
}
}
} else if (payload.alertType === 'CONFLICT_ALERT') {
console.log('处理冲突告警:', vehicleId, otherVehicleId);
const alertMessage = `⚠️ 告警:${message}`;
showAlert(alertMessage, 'alarm', 10000);
//
if (vehicles.value[vehicleId]) {
vehicles.value[vehicleId].alarm = true;
vehicles.value[vehicleId].warning = false;
vehicles.value[vehicleId].critical = false;
vehicles.value[vehicleId].info = false;
//
if (vehicles.value[vehicleId].feature && styleManager.value) {
vehicles.value[vehicleId].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
}
//
if (vehicles.value[vehicleId].position && labelSystem.value) {
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
description: message,
isAlarm: true
});
}
}
} else {
console.log(`未知的冲突消息类型: ${payload.messageType || payload.alertType}`);
}
}
//
function handleRuleViolation(payload) {
if (!payload) {
console.error('规则违规消息格式错误:', payload);
return;
}
console.log('收到规则违规:', payload);
const vehicleId = payload.object_id || payload.vehicleId || payload.vehicleLicense || '未知车辆';
const description = payload.description || '';
const limitValue = payload.limitValue;
const actualValue = payload.actualValue;
const ruleName = payload.ruleName || '交通规则';
const violationType = payload.violationType || '';
// violationType
switch (violationType.toUpperCase()) {
case 'SPEED':
//
handleSpeedViolation(vehicleId, payload);
break;
case 'ACCESS':
//
handleUnauthorizedEntry(vehicleId, payload);
break;
case 'rule_violation':
console.log('收到规则违规:', data.payload);
if (data.payload) {
// ID
const vehicleId = data.payload.object_id || data.payload.vehicleId || data.payload.vehicleLicense || '未知车辆';
const violationType = data.payload.violationType || '未知违规';
const alertLevel = data.payload.alertLevel || 'INFO';
const description = data.payload.description || '';
const limitValue = data.payload.limitValue;
const actualValue = data.payload.actualValue;
const ruleName = data.payload.ruleName || '交通规则';
//
const isSpeedViolation = violationType === 'SPEED_VIOLATION';
//
const isSpeedViolationEnd = violationType === 'SPEED_VIOLATION_END';
default:
console.log(`未知的规则违规类型: ${violationType}`);
//
handleUnauthorizedEntry(vehicleId, payload);
}
}
//
function handleSpeedViolation(vehicleId, payload) {
const actualValue = payload.actualValue;
const limitValue = payload.limitValue;
const description = payload.description || '超速违规';
const ruleName = payload.ruleName || '速度限制';
//
if (isSpeedViolation && alertLevel === 'WARNING') {
console.log(`检测到超速违规: ${vehicleId}, 实际速度: ${actualValue}, 限速: ${limitValue}`);
//
showAlert(`⚠️ 超速告警:${vehicleId} ${description}`, 'warning', 8000);
//
if (vehicles.value[vehicleId]) {
//
@ -483,7 +639,7 @@ function handleWsMessage(message) {
//
if (!alreadyInSpeedViolation) {
if (vehicles.value[vehicleId].feature) {
if (vehicles.value[vehicleId].feature && styleManager.value) {
vehicles.value[vehicleId].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
vehicles.value[vehicleId].lastIconUpdateTime = Date.now();
}
@ -513,6 +669,12 @@ function handleWsMessage(message) {
speedViolationTimers[vehicleId] = setTimeout(() => {
//
if (vehicles.value[vehicleId]) {
console.log(`超速状态超时: ${vehicleId}, 自动清除超速状态`);
//
@ -538,140 +700,43 @@ function handleWsMessage(message) {
}
}, 20000); // 20
}
return; // alertLevel
}
//
if (isSpeedViolationEnd) {
console.log(`检测到超速结束: ${vehicleId}`);
//
function handleUnauthorizedEntry(vehicleId, payload) {
const description = payload.description || '越界告警';
const alertLevel = payload.alertLevel || 'CRITICAL';
const ruleName = payload.ruleName || '区域控制';
//
if (vehicles.value[vehicleId]) {
//
if (!vehicles.value[vehicleId].statusLock ||
!vehicles.value[vehicleId].statusLock.active ||
Date.now() > vehicles.value[vehicleId].statusLock.until) {
console.log(`检测到越界告警: ${vehicleId}, ${description}, 规则: ${ruleName}`);
// 使
clearSpeedViolationStatus(vehicleId);
} else {
//
console.log(`车辆${vehicleId}状态仍然锁定,延迟清除超速状态`);
//
showAlert(`⚠️ 越界告警:${vehicleId} ${description}`, 'critical', 10000);
//
if (speedViolationTimers[vehicleId]) {
clearTimeout(speedViolationTimers[vehicleId]);
}
speedViolationTimers[vehicleId] = setTimeout(() => {
clearSpeedViolationStatus(vehicleId);
delete speedViolationTimers[vehicleId];
}, vehicles.value[vehicleId].statusLock.until - Date.now());
}
}
return; // alertLevel
}
// alertLevel
switch (alertLevel.toUpperCase()) {
case 'Info': //
//
showAlert(`预警:${vehicleId} ${description}`, 'warning', 10000);
//
if (vehicles.value[vehicleId]) {
vehicles.value[vehicleId].warning = true;
vehicles.value[vehicleId].alarm = false;
vehicles.value[vehicleId].critical = false;
vehicles.value[vehicleId].info = false;
//
if (vehicles.value[vehicleId].feature) {
vehicles.value[vehicleId].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
}
//
if (vehicles.value[vehicleId].position) {
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
description: description,
limitValue: limitValue,
actualValue: actualValue
});
}
}
break;
case 'ALERT': //
//
showAlert(`⚠️ 告警:${vehicleId} ${description}`, 'alarm', 10000);
//
if (vehicles.value[vehicleId]) {
vehicles.value[vehicleId].alarm = true;
vehicles.value[vehicleId].warning = false;
vehicles.value[vehicleId].critical = false;
vehicles.value[vehicleId].info = false;
//
if (vehicles.value[vehicleId].feature) {
vehicles.value[vehicleId].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
}
//
if (vehicles.value[vehicleId].position) {
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
description: description,
limitValue: limitValue,
actualValue: actualValue
});
}
}
break;
case 'CRITICAL': //
//
//
if (vehicles.value[vehicleId]) {
vehicles.value[vehicleId].critical = true;
vehicles.value[vehicleId].alarm = false;
vehicles.value[vehicleId].warning = false;
vehicles.value[vehicleId].info = false;
vehicles.value[vehicleId].speedViolation = false;
vehicles.value[vehicleId].description = description;
vehicles.value[vehicleId].ruleName = ruleName;
//
if (vehicles.value[vehicleId].feature) {
//
if (vehicles.value[vehicleId].feature && styleManager.value) {
vehicles.value[vehicleId].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
}
//
if (vehicles.value[vehicleId].position) {
if (vehicles.value[vehicleId].position && labelSystem.value) {
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
description: description,
limitValue: limitValue,
actualValue: actualValue
ruleName: ruleName,
isUnauthorizedEntry: true
});
}
}
break;
case 'WARNING': // - caseSPEED_VIOLATION
console.log(`收到WARNING级别的超速消息但已在SPEED_VIOLATION中处理: ${vehicleId}`);
break;
default:
console.log(`未处理的告警级别: ${alertLevel}`);
}
}
break;
case 'vehicle_command':
console.log('收到车辆控制指令:', data.payload);
break;
default:
//
console.log(`收到其他类型消息: ${data.type}`, data);
break;
}
} catch (e) {
console.error('处理WebSocket消息出错:', e, message);
}
}
//
@ -716,6 +781,10 @@ function clearSpeedViolationStatus(vehicleId) {
// ID0.00km/h
if (vehicle.position && labelSystem.value) {
//
labelSystem.value.removeVehicleLabel(vehicleId);
//
labelSystem.value.updateVehicleLabel(vehicleId, vehicle.position, vehicle.speed > 0.1 ? vehicle.speed : 0);
}
@ -770,44 +839,7 @@ function clearVehicleAlertStatus(vehicleData) {
}
}
//
const hasAlertStatus = existingVehicle.info || existingVehicle.warning ||
existingVehicle.alarm || existingVehicle.critical;
//
if (hasAlertStatus) {
// (info)
// 1.
const isSpeedViolation = existingVehicle.info;
// 2.
// 使使
// 50km/h
const SPEED_THRESHOLD = 5; // km/h
const isBelowThreshold = speed < SPEED_THRESHOLD;
// 3.
if (isSpeedViolation && isBelowThreshold) {
console.log(`检测到车辆${object_id}速度降低到${speed.toFixed(1)}km/h低于阈值${SPEED_THRESHOLD}km/h自动清除超速状态`);
//
existingVehicle.info = false;
//
if (existingVehicle.feature) {
existingVehicle.feature.setStyle(styleManager.value.getVehicleStyle(object_id, speed, existingVehicle.heading));
}
//
if (existingVehicle.position) {
labelSystem.value.updateVehicleLabel(object_id, existingVehicle.position, speed);
}
console.log(`已清除${object_id}的超速状态,恢复为普通状态`);
} else {
console.log(`车辆${object_id}有告警状态,但不符合自动清除条件,保持该状态`);
}
}
}
//
@ -1061,36 +1093,28 @@ function defaultGetVehicleStyle(id, _speed, heading) {
const vehicle = vehicles.value[id];
if (!vehicle) return createDefaultStyle(carIcon, heading);
//
if (vehicle.isAircraftIn) {
// (CA)使Aircraft1.png
return createDefaultStyle(aircraftInIcon, heading);
} else if (vehicle.isAircraftOut) {
// (MU)使Aircraft.png
return createDefaultStyle(aircraftOutIcon, heading);
} else if (vehicle.isUnmannedVehicle) {
// 使noPeopleCar.png
return createDefaultStyle(carIcon, heading);
} else if (vehicle.isSpecialVehicle) {
//
return createDefaultStyle(carIcon, heading);
} else if (vehicle.isShuttleVehicle) {
//
return createDefaultStyle(carIcon, heading);
//
if (vehicle.isAircraft) {
// 使Aircraft.png
return createDefaultStyle(aircraftIcon, heading);
} else {
//
// 使noPeopleCar.png
return createDefaultStyle(carIcon, heading);
}
}
//
function createDefaultStyle(iconSrc, heading) {
// heading
const validHeading = heading !== undefined ? Number(heading) : 0;
const rotationRad = (validHeading * Math.PI) / 180;
return new Style({
image: new Icon({
src: iconSrc,
src: iconSrc, // 使
scale: 1.5,
anchor: [0.5, 0.5],
rotation: ((heading - 72) * Math.PI) / 180,
rotation: rotationRad, //
})
});
}

View File

@ -7,214 +7,62 @@ 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'; //
import aircraftIcon from '../../../assets/images/aircraft.png'; //
// 使
// import warningCarIcon from '../../../assets/images/warning_car.png'; //
// import alarmCarIcon from '../../../assets/images/alarm_car.png'; //
// import criticalCarIcon from '../../../assets/images/warning_car.png'; // 使warning_car.png
// import speedCarIcon from '../../../assets/images/warning_car.png'; // 使warning_car.png
// 使
// import warningAircraftIcon from '../../../assets/images/aircraft.png';
// import alarmAircraftIcon from '../../../assets/images/aircraft.png';
// import criticalAircraftIcon from '../../../assets/images/aircraft.png';
// import speedAircraftIcon from '../../../assets/images/aircraft.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) return createDefaultStyle(carIcon, heading);
//
if (vehicle.statusLock === undefined) {
vehicle.statusLock = {
active: false,
type: null,
until: 0
};
}
//
const isAircraft = vehicle.isAircraft;
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;
}
//
const iconSrc = isAircraft ? aircraftIcon : carIcon;
//
return new Style({
image: new Icon({
src: iconSrc,
scale: 1.5,
anchor: [0.5, 0.5],
rotation: ((heading - 72) * Math.PI) / 180,
})
});
return createDefaultStyle(iconSrc, heading);
}
//
function getVehicleIcon(vehicle) {
if (!vehicle) return carIcon;
//
function createDefaultStyle(iconSrc, heading) {
// heading
const validHeading = heading !== undefined ? Number(heading) : 0;
const hasInfoStatus = vehicle.info;
const hasWarningStatus = vehicle.warning;
const hasAlarmStatus = vehicle.alarm;
const hasCriticalStatus = vehicle.critical;
// 使(heading - 72)
const rotationRad = ((validHeading - 72) * Math.PI) / 180;
//
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);
//
console.log(`VehicleStyleManager: heading=${validHeading}, rotation=${rotationRad}弧度, ${rotationRad * 180 / Math.PI}`);
return new Style({
image: new Icon({
src: iconSrc,
scale: 1.5,
anchor: [0.5, 0.5],
rotation: ((heading - 72) * Math.PI) / 180,
rotation: rotationRad, // 使
})
});
}
//
function updateVehicleStyle(id, heading) {
const vehicle = props.vehicles[id];
if (!vehicle) return null;
return createVehicleStyle(vehicle, heading);
}
//
defineExpose({
getVehicleStyle,
getVehicleIcon,
createVehicleStyle,
updateVehicleStyle
getVehicleStyle
});
</script>

View File

@ -221,6 +221,7 @@ function updateCarList(newList) {
const selectedCar = ref(null);
function showDetail(car) {
// moveToTarget
selectedCar.value = car;
}

View File

@ -11,31 +11,20 @@
background-color: #f5f5f5;
}
.container {
max-width: 800px;
max-width: 1920px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
padding: 2px;
}
.test-section {
border: 1px solid #ddd;
border-radius: 8px;
margin: 20px 0;
padding: 20px;
margin: 2px 0;
padding: 10px;
background: #fafafa;
}
.test-section h2 {
color: #555;
margin-top: 0;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
.control-group {
margin: 15px 0;
display: flex;
@ -86,7 +75,7 @@
.log-container {
border: 1px solid #ddd;
border-radius: 4px;
height: 300px;
height: 960px;
overflow-y: auto;
padding: 10px;
background: white;
@ -106,16 +95,15 @@
</head>
<body>
<div class="container">
<h1>冲突检测WebSocket测试工具</h1>
<!-- 冲突检测WebSocket测试 -->
<div class="test-section">
<h2>🚗 冲突检测WebSocket (原生协议)</h2>
<h3>🚗 冲突检测WebSocket</h3>
<div class="control-group">
<label>服务器地址:</label>
<select id="collisionServerSelect">
<option value="ws://10.0.0.126:8080/collision">冲突检测WebSocket</option>
<option value="ws://10.0.0.126:8080/collision">10.0.0.126:8080/collision</option>
</select>
</div>
@ -136,7 +124,7 @@
<!-- 控制面板 -->
<div class="test-section">
<h2>🎛️ 控制面板</h2>
<h3>🎛️ 控制面板</h3>
<div class="control-group">
<button onclick="clearLogs()">清空日志</button>
<button onclick="showConnectionStatus()">显示连接状态</button>

View File

@ -62,7 +62,7 @@
<el-table-column label="车牌号" prop="licensePlate" align="left" />
<el-table-column label="车辆类型" align="left">
<template #default="scope">
{{ getVehicleTypeName(scope.row.typeId) }}
{{ scope.row.typeDisplayName || '未知类型' }}
</template>
</el-table-column>
<el-table-column label="品牌" prop="brand" align="left" />
@ -544,6 +544,7 @@ function handleRemoveImage() {
/** 获取车辆类型名称 */
function getVehicleTypeName(typeId) {
// 使typeDisplayName
return vehicleTypeMap.value[typeId] || '';
}

View File

@ -115,7 +115,11 @@
</template>
</el-table-column>
<el-table-column label="二级类型" prop="typeName" />
<el-table-column label="二级类型" prop="typeName">
<template #default="scope">
{{ scope.row.typeName }}
</template>
</el-table-column>
<el-table-column label="创建人" prop="createBy" />
<el-table-column label="创建时间" prop="createTime" />
</el-table>
@ -170,11 +174,7 @@ const { proxy } = getCurrentInstance();
// Tab
const editableTabsValue = ref("");
const editableTabs = ref([
{ label: "无人车", name: "无人车", content: "" },
{ label: "特勤车", name: "特勤车", content: "" },
{ label: "普通车", name: "普通车", content: "" },
]);
const editableTabs = ref([]);
const showAddTabDialog = ref(false);
const newTabName = ref("");
@ -197,9 +197,15 @@ const addTab = () => {
//
const submitData = {
typeName: name,
displayNameCn: name,
displayNameEn: name, //
typeCode: generateTypeCode(name), // typeCode
typeName: name.toUpperCase().replace(/\s+/g, "_"), // typeName
level: 1,
parentId: null,
isLeaf: false,
enabled: true,
parentCode: null,
topLevelCode: null
};
//
@ -211,7 +217,9 @@ const addTab = () => {
getFirstTypeList();
//
editableTabsValue.value = name;
setTimeout(() => {
editableTabsValue.value = submitData.typeCode;
}, 100);
//
newTabName.value = "";
@ -220,41 +228,65 @@ const addTab = () => {
//
setTimeout(() => {
getList();
}, 100);
}, 200);
})
.catch(() => {
//
});
};
// typeCode
function generateTypeCode(name) {
// "" -> "WRC"
const pinyin = name.split('').map(char => {
//
const pinyinMap = {
'无': 'W', '人': 'R', '车': 'C',
'特': 'T', '勤': 'Q', '普': 'P',
'通': 'T', '航': 'H', '空': 'K',
'接': 'J', '驳': 'B', '清': 'Q',
'洁': 'J', '消': 'X', '防': 'F',
'警': 'J', '巡': 'X', '逻': 'L',
'配': 'P', '送': 'S', '运': 'Y',
'输': 'S', '餐': 'C', '行': 'X',
'李': 'L'
};
return pinyinMap[char] || char.charAt(0).toUpperCase();
});
// 2-3
let code = pinyin.slice(0, Math.min(3, pinyin.length)).join('');
//
const randomNum = Math.floor(Math.random() * 100);
return code + randomNum;
}
const deleteCustomTab = (tabName) => {
// typeId
const firstType = firstTypeList.value.find((item) => item.value === tabName);
if (!firstType || !firstType.typeId) return;
//
listVehicle_type({ firstType: tabName }).then((res) => {
if (res.data && Array.isArray(res.data)) {
listVehicle_type({ topLevelCode: tabName }).then((res) => {
if (res.rows && Array.isArray(res.rows)) {
// tab
const currentTabData = res.data.filter(
const currentTabData = res.rows.filter(
(item) =>
item.level === 2 &&
item.parentId &&
res.data.some(
(parent) =>
parent.typeId === item.parentId && parent.typeName === tabName
)
item.topLevelCode === tabName &&
item.parentCode === tabName
);
if (currentTabData.length > 0) {
//
proxy.$modal.msgError("该二级类型已绑定车辆,不可删除");
proxy.$modal.msgError("该类型下有二级类型,不可删除");
return;
}
//
proxy.$modal
.confirm(`确定删除 ${tabName} 类型吗?`, "删除")
.confirm(`确定删除 ${firstType.label} 类型吗?`, "删除")
.then(function () {
return delVehicle_type(firstType.typeId);
})
@ -310,11 +342,8 @@ const rules = ref({
typeName: [{ required: true, message: "二级类型不能为空", trigger: "blur" }],
});
const firstTypeList = ref([
{ label: "无人车", value: "无人车" },
{ label: "特勤车", value: "特勤车" },
{ label: "普通车", value: "普通车" },
]);
//
const firstTypeList = ref([]);
/** 查询车辆类型列表 */
function getList() {
@ -324,25 +353,21 @@ function getList() {
.then((res) => {
loading.value = false;
// tab
if (res.data && Array.isArray(res.data)) {
if (res.rows && Array.isArray(res.rows)) {
// tab
const currentTabData = res.data.filter(
const currentTabData = res.rows.filter(
(item) =>
item.level === 2 &&
item.parentId &&
res.data.some(
(parent) =>
parent.typeId === item.parentId &&
parent.typeName === editableTabsValue.value
)
item.topLevelCode === editableTabsValue.value &&
item.parentCode === editableTabsValue.value
);
//
vehicleTypeList.value = currentTabData.map((item) => ({
typeId: item.typeId,
firstType: editableTabsValue.value,
typeName: item.typeName,
createBy: item.createBy || "系统管理员",
typeName: item.displayNameCn,
createBy: item.createBy,
createTime: item.createTime,
}));
@ -392,7 +417,16 @@ function handleUpdate(row) {
reset();
const typeId = row.typeId || ids.value[0];
getVehicle_type(typeId).then((response) => {
form.value = response.data || {};
// 使displayNameCntypeName
if (response.data) {
form.value = {
typeId: response.data.typeId,
firstType: response.data.parentCode || editableTabsValue.value,
typeName: response.data.displayNameCn || response.data.typeName
};
} else {
form.value = {};
}
open.value = true;
title.value = "修改车辆类型";
});
@ -405,21 +439,12 @@ function submitForm() {
//
const submitData = {
typeId: form.value.typeId,
typeName: form.value.typeName,
parentId: null,
displayNameCn: form.value.typeName,
level: 2,
parentCode: form.value.firstType,
topLevelCode: form.value.firstType
};
// typeId
if (form.value.firstType) {
const firstType = firstTypeList.value.find(
(item) => item.value === form.value.firstType
);
if (firstType && firstType.typeId) {
submitData.parentId = firstType.typeId;
}
}
if (form.value.typeId != undefined) {
updateVehicle_type(submitData).then((response) => {
proxy.$modal.msgSuccess("修改成功");
@ -489,21 +514,21 @@ function cancel() {
//
function getFirstTypeList() {
listVehicle_type({ level: 1 }).then((res) => {
if (res.data && Array.isArray(res.data)) {
if (res.rows && Array.isArray(res.rows)) {
//
const firstTypes = res.data.filter((item) => item.level === 1);
const firstTypes = res.rows.filter((item) => item.level === 1);
// firstTypeListtypeId
firstTypeList.value = firstTypes.map((item) => ({
label: item.typeName,
value: item.typeName,
label: item.displayNameCn,
value: item.typeCode,
typeId: item.typeId,
}));
// tab
editableTabs.value = firstTypes.map((item) => ({
label: item.typeName,
name: item.typeName,
label: item.displayNameCn,
name: item.typeCode,
content: "",
}));

View File

@ -70,7 +70,7 @@
<!-- 车辆移动控制组件 -->
<VehicleMovementControlRefactored :map="map" ref="vehicleMovementRef" v-if="map" />
<!-- <VehicleDisplayControl :map="map" ref="vehicleMovementRef" v-if="map" /> -->
<!-- 绘制工具栏 -->
@ -78,7 +78,7 @@
<div class="toolbar-title">绘制工具</div>
<div class="toolbar-buttons">
<button @click="startDrawLine" class="toolbar-button">绘制路线</button>
<button @click="startDrawPolygon" class="toolbar-button">绘制点线面</button>
<button @click="startDrawPolygon" class="toolbar-button">绘制区域</button>
<button @click="clearDraw" class="toolbar-button clear">清除</button>
<button @click="exportRouteData" class="toolbar-button export">导出数据</button>
<button @click="importRouteData" class="toolbar-button import">导入数据</button>
@ -486,8 +486,8 @@ watch(() => mapRef.value?.map, (newMap) => {
top: 50%;
right: 0;
z-index: 2001;
width: 40px;
height: 80px;
width: 30px;
height: 58px;
transform: translateY(-50%);
cursor: pointer;
transition: right 0.3s ease;