修改平台概览UI
This commit is contained in:
parent
b8640adfc8
commit
ca53570ff8
132
CLAUDE.md
Normal file
132
CLAUDE.md
Normal 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
101
README.md
@ -26,87 +26,6 @@ yarn dev
|
|||||||
超图地图开发使用基础
|
超图地图开发使用基础
|
||||||
1、前期准备工作熟悉超图地图服务以及基本的操作。
|
1、前期准备工作熟悉超图地图服务以及基本的操作。
|
||||||
2、熟悉Openlayers以及Leaflet相关操作
|
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关键改进说明
|
20250711关键改进说明
|
||||||
1. 平滑动画核心系统
|
1. 平滑动画核心系统
|
||||||
@ -162,3 +70,12 @@ resetVehicleAnimations():重置所有动画数据
|
|||||||
调用startVehicleSmoothing()启动平滑效果
|
调用startVehicleSmoothing()启动平滑效果
|
||||||
处理WebSocket消息时继续调用updateVehiclePosition()
|
处理WebSocket消息时继续调用updateVehiclePosition()
|
||||||
当组件隐藏时调用stopVehicleSmoothing()节省资源
|
当组件隐藏时调用stopVehicleSmoothing()节省资源
|
||||||
|
|
||||||
|
### 运行方向分析:
|
||||||
|
地图上方(heading=72)
|
||||||
|
|
|
||||||
|
|
|
||||||
|
地图左侧(heading=342) --+-- 地图右侧(heading=162)
|
||||||
|
|
|
||||||
|
|
|
||||||
|
地图下方(heading=252)
|
||||||
65
public/quyu.json
Normal file
65
public/quyu.json
Normal 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
49
public/quyu1.json
Normal 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
77
public/quyu2.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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> -->
|
<!-- <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>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -129,6 +145,14 @@ let roadVectorLayer = null;
|
|||||||
const showCustomRoadLayer = ref(true); // 默认显示
|
const showCustomRoadLayer = ref(true); // 默认显示
|
||||||
let customRoadVectorLayer = null;
|
let customRoadVectorLayer = null;
|
||||||
|
|
||||||
|
// 新增:区域1图层状态
|
||||||
|
const showArea1Layer = ref(true); // 默认显示
|
||||||
|
let area1VectorLayer = null;
|
||||||
|
|
||||||
|
// 新增:区域2图层状态
|
||||||
|
const showArea2Layer = ref(true); // 默认显示
|
||||||
|
let area2VectorLayer = null;
|
||||||
|
|
||||||
// 向父组件发出事件
|
// 向父组件发出事件
|
||||||
const emit = defineEmits(['layerChange', 'setCategoryVisibility']);
|
const emit = defineEmits(['layerChange', 'setCategoryVisibility']);
|
||||||
|
|
||||||
@ -159,6 +183,12 @@ onMounted(() => {
|
|||||||
if (showRoadLayer.value) {
|
if (showRoadLayer.value) {
|
||||||
addRoadLayer();
|
addRoadLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化区域1图层
|
||||||
|
loadArea1Layer();
|
||||||
|
|
||||||
|
// 初始化区域2图层
|
||||||
|
loadArea2Layer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,6 +202,12 @@ watch(() => props.map, (newMap) => {
|
|||||||
if (showRoadLayer.value) {
|
if (showRoadLayer.value) {
|
||||||
addRoadLayer();
|
addRoadLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化区域1图层
|
||||||
|
loadArea1Layer();
|
||||||
|
|
||||||
|
// 初始化区域2图层
|
||||||
|
loadArea2Layer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -349,9 +385,145 @@ watch(showRoadLayer, (val) => {
|
|||||||
else removeRoadLayer();
|
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(() => {
|
onUnmounted(() => {
|
||||||
removeRoadLayer();
|
removeRoadLayer();
|
||||||
removeCustomRoadLayer();
|
removeCustomRoadLayer();
|
||||||
|
removeArea1Layer();
|
||||||
|
removeArea2Layer();
|
||||||
stopFenceFlashing();
|
stopFenceFlashing();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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. **性能监控**: 添加性能监控和优化
|
|
||||||
@ -25,19 +25,18 @@ const MAX_PREDICTION_TIME = 15000; // 从10000ms增加到15000ms,延长预测
|
|||||||
// 物理模拟参数
|
// 物理模拟参数
|
||||||
const ACCELERATION = 0.15; // 加速度系数
|
const ACCELERATION = 0.15; // 加速度系数
|
||||||
const DECELERATION = 0.25; // 减速度系数
|
const DECELERATION = 0.25; // 减速度系数
|
||||||
const MAX_TURN_RATE = 100; // 最大转向速率(度/秒)
|
const INERTIA_FACTOR = 0.99; // 从0.985增加到0.99,进一步增强惯性效果,减少抖动
|
||||||
const MIN_TURN_RATE = 20; // 最小转向速率(度/秒)
|
const PREDICTION_STRENGTH = 3; // 从4降低到3,进一步减少预测激进性,避免过度预测造成抖动
|
||||||
const INERTIA_FACTOR = 0.97; // 从0.95增加到0.97,增强惯性效果
|
const SPEED_SMOOTHING = 0.98; // 从0.97增加到0.98,进一步增强速度平滑效果
|
||||||
const PREDICTION_STRENGTH = 6; // 从8降低到6,减少预测激进性
|
const POSITION_SMOOTHING = 0.06; // 从0.08降低到0.06,进一步减小单次位置变化幅度,增强平滑度
|
||||||
const SPEED_SMOOTHING = 0.94; // 从0.92增加到0.94,增强速度平滑效果
|
const MIN_MOVE_THRESHOLD = 0.000005; // 从0.00001降低到0.000005,确保更平滑的微小移动
|
||||||
const POSITION_SMOOTHING = 0.12; // 从0.15降低到0.12,增强位置平滑效果
|
const CONTINUOUS_MOVEMENT = true; // 保持启用连续移动模式
|
||||||
const MIN_MOVE_THRESHOLD = 0.00005; // 从0.0001降低到0.00005,确保更持续的移动
|
const MIN_SPEED = 0.5; // 从1.0降低到0.5,进一步降低最低保持速度,减少抖动
|
||||||
const CONTINUOUS_MOVEMENT = true; // 启用连续移动模式
|
const PREDICTION_DECAY_RATE = 0.9995; // 从0.999增加到0.9995,进一步减缓预测衰减
|
||||||
const MIN_SPEED = 2.0; // 从1.5增加到2.0,提高最低保持速度
|
const PATH_PREDICTION_ENABLED = true; // 保持启用路径预测
|
||||||
const PREDICTION_DECAY_RATE = 0.998; // 从0.995进一步增加到0.998,减缓预测衰减
|
const PATH_PREDICTION_POINTS = 15; // 从20降低到15,减少计算量
|
||||||
const PATH_PREDICTION_ENABLED = true; // 启用路径预测
|
const CONTINUOUS_MOVEMENT_THRESHOLD = 200; // 从300ms降低到200ms,更快触发连续移动,减少停顿感
|
||||||
const PATH_PREDICTION_POINTS = 16; // 从12增加到16,使用更多历史点
|
const POSITION_UPDATE_THRESHOLD = 3; // 从5降低到3,进一步减少位置更新阈值
|
||||||
const CONTINUOUS_MOVEMENT_THRESHOLD = 500; // 从1000ms降低到500ms,更快触发连续移动
|
|
||||||
|
|
||||||
// 缓动函数 - 平滑的加减速
|
// 缓动函数 - 平滑的加减速
|
||||||
function easeInOutQuad(t) {
|
function easeInOutQuad(t) {
|
||||||
@ -99,14 +98,13 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
|
|
||||||
// 1. 计算目标位置(基于预测)
|
// 1. 计算目标位置(基于预测)
|
||||||
let targetPosition = [...animData.targetPosition];
|
let targetPosition = [...animData.targetPosition];
|
||||||
let targetHeading = animData.targetHeading;
|
|
||||||
|
|
||||||
// 平滑速度过渡 - 当前速度逐渐接近目标速度
|
// 平滑速度过渡 - 当前速度逐渐接近目标速度
|
||||||
if (animData.currentSpeed === undefined) {
|
if (animData.currentSpeed === undefined) {
|
||||||
animData.currentSpeed = animData.speed;
|
animData.currentSpeed = animData.speed;
|
||||||
} else {
|
} else {
|
||||||
// 使用更平滑的速度过渡
|
// 使用更平滑的速度过渡
|
||||||
const speedSmoothingFactor = vehicle.speedViolation ? 0.96 : SPEED_SMOOTHING;
|
const speedSmoothingFactor = vehicle.speedViolation ? 0.98 : SPEED_SMOOTHING;
|
||||||
animData.currentSpeed = animData.currentSpeed * speedSmoothingFactor +
|
animData.currentSpeed = animData.currentSpeed * speedSmoothingFactor +
|
||||||
animData.speed * (1 - speedSmoothingFactor);
|
animData.speed * (1 - speedSmoothingFactor);
|
||||||
}
|
}
|
||||||
@ -117,10 +115,10 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
if (timeSinceLastUpdate > PREDICTION_THRESHOLD && animData.predictionVector) {
|
if (timeSinceLastUpdate > PREDICTION_THRESHOLD && animData.predictionVector) {
|
||||||
// 计算预测因子,随时间增长但有上限
|
// 计算预测因子,随时间增长但有上限
|
||||||
const predictTime = Math.min(timeSinceLastUpdate, MAX_PREDICTION_TIME);
|
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.8降低到0.6,减少预测激进性
|
||||||
|
|
||||||
// 应用预测向量,考虑当前速度
|
// 应用预测向量,考虑当前速度
|
||||||
const speedFactor = Math.min(1.2, currentSpeed / 25); // 速度越高预测越远
|
const speedFactor = Math.min(1.0, currentSpeed / 30); // 从1.2/25降低到1.0/30,减少高速预测
|
||||||
|
|
||||||
// 计算预测位置
|
// 计算预测位置
|
||||||
const predictedPosition = [
|
const predictedPosition = [
|
||||||
@ -134,7 +132,7 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
// 预测时逐渐降低速度,但不要完全停止
|
// 预测时逐渐降低速度,但不要完全停止
|
||||||
if (predictTime > 2000) {
|
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/10000调整为0.6/15000,减缓减速率
|
||||||
currentSpeed = Math.max(currentSpeed * slowdownFactor, MIN_SPEED); // 确保最低速度不为0
|
currentSpeed = Math.max(currentSpeed * slowdownFactor, MIN_SPEED); // 确保最低速度不为0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,6 +142,11 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
const dy = targetPosition[1] - animData.position[1];
|
const dy = targetPosition[1] - animData.position[1];
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// 忽略过小的位置更新,减少抖动
|
||||||
|
if (distance < MIN_MOVE_THRESHOLD && !CONTINUOUS_MOVEMENT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 如果距离足够远或启用了连续移动,应用平滑移动
|
// 3. 如果距离足够远或启用了连续移动,应用平滑移动
|
||||||
if (distance > MIN_MOVE_THRESHOLD || CONTINUOUS_MOVEMENT) {
|
if (distance > MIN_MOVE_THRESHOLD || CONTINUOUS_MOVEMENT) {
|
||||||
// 计算理想移动距离(基于速度和时间)
|
// 计算理想移动距离(基于速度和时间)
|
||||||
@ -153,11 +156,11 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
let moveRatio;
|
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.5降低到1.2,更早开始减速
|
||||||
// 接近目标时减速(使用更强的缓动)
|
// 接近目标时减速(使用更强的缓动)
|
||||||
moveRatio = Math.min(positionSmoothingFactor * 1.5, idealDistance / distance);
|
moveRatio = Math.min(positionSmoothingFactor * 1.2, idealDistance / distance); // 从1.5降低到1.2
|
||||||
moveRatio = easeOutQuint(moveRatio);
|
moveRatio = easeOutQuint(moveRatio);
|
||||||
} else {
|
} else {
|
||||||
// 正常行驶时使用标准缓动
|
// 正常行驶时使用标准缓动
|
||||||
@ -169,7 +172,7 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
// 如果距离很小且有预测向量,使用预测向量进行微小移动
|
// 如果距离很小且有预测向量,使用预测向量进行微小移动
|
||||||
if (distance < MIN_MOVE_THRESHOLD && animData.predictionVector) {
|
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.1降低到0.05,减小微小移动幅度
|
||||||
|
|
||||||
// 归一化预测向量
|
// 归一化预测向量
|
||||||
const predLen = Math.sqrt(
|
const predLen = Math.sqrt(
|
||||||
@ -194,7 +197,7 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
else {
|
else {
|
||||||
if (animData.lastDx !== undefined && animData.lastDy !== undefined) {
|
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.1降低到1.05,减少超速车辆惯性过强
|
||||||
|
|
||||||
const inertiaX = animData.lastDx * inertiaFactor;
|
const inertiaX = animData.lastDx * inertiaFactor;
|
||||||
const inertiaY = animData.lastDy * inertiaFactor;
|
const inertiaY = animData.lastDy * inertiaFactor;
|
||||||
@ -204,8 +207,8 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
const newDy = dy * moveRatio;
|
const newDy = dy * moveRatio;
|
||||||
|
|
||||||
// 根据速度和距离调整惯性影响
|
// 根据速度和距离调整惯性影响
|
||||||
const inertiaWeight = Math.min(0.85, currentSpeed / 70) *
|
const inertiaWeight = Math.min(0.75, currentSpeed / 80) * // 从0.85/70降低到0.75/80,减少惯性权重
|
||||||
Math.min(1.0, distance / 8); // 调整惯性权重计算
|
Math.min(0.9, distance / 10); // 从1.0/8调整为0.9/10,减少短距离惯性
|
||||||
|
|
||||||
// 计算新位置(混合惯性和目标方向)
|
// 计算新位置(混合惯性和目标方向)
|
||||||
const nextX = animData.position[0] + newDx * (1 - inertiaWeight) + inertiaX * inertiaWeight;
|
const nextX = animData.position[0] + newDx * (1 - inertiaWeight) + inertiaX * inertiaWeight;
|
||||||
@ -238,69 +241,18 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
if (props.vehicles[id]) {
|
if (props.vehicles[id]) {
|
||||||
props.vehicles[id].position = animData.position;
|
props.vehicles[id].position = animData.position;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 计算实际移动方向(用于更自然的转向)
|
// 更新车辆样式,传递正确的heading值
|
||||||
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 (feature && props.getVehicleStyle) {
|
||||||
// 减少样式更新频率,避免闪烁
|
// 获取车辆的当前heading值
|
||||||
if (!animData.lastStyleUpdateTime || currentTime - animData.lastStyleUpdateTime > 500) {
|
const vehicle = props.vehicles[id];
|
||||||
feature.setStyle(props.getVehicleStyle(id, currentSpeed, animData.heading));
|
const currentHeading = vehicle ? vehicle.heading : 0;
|
||||||
animData.lastStyleUpdateTime = currentTime;
|
|
||||||
|
// 只在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 ||
|
else if (!animData.predictionVector ||
|
||||||
(Math.abs(animData.predictionVector[0]) < 0.001 && Math.abs(animData.predictionVector[1]) < 0.001)) {
|
(Math.abs(animData.predictionVector[0]) < 0.001 && Math.abs(animData.predictionVector[1]) < 0.001)) {
|
||||||
|
|
||||||
// 根据车辆朝向创建预测向量
|
// 使用最近两个历史点计算预测向量
|
||||||
const headingRad = (animData.heading - 90) * Math.PI / 180;
|
if (animData.pathHistory && animData.pathHistory.length >= 2) {
|
||||||
const predX = Math.cos(headingRad) * animData.currentSpeed * 0.18; // 从0.15增加到0.18
|
const latestPoints = animData.pathHistory.slice(-2);
|
||||||
const predY = Math.sin(headingRad) * animData.currentSpeed * 0.18;
|
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.predictionVector = [predX, predY];
|
||||||
|
|
||||||
// 更新目标位置为当前位置加上预测向量
|
// 更新目标位置为当前位置加上预测向量
|
||||||
animData.targetPosition = [
|
animData.targetPosition = [
|
||||||
animData.position[0] + predX * 15, // 从12增加到15
|
animData.position[0] + predX * 15,
|
||||||
animData.position[1] + predY * 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 {
|
} else {
|
||||||
// 预测向量存在但需要缓慢衰减,避免突然停止
|
// 预测向量存在但需要缓慢衰减,避免突然停止
|
||||||
// 使用更高的衰减率
|
// 使用更高的衰减率
|
||||||
@ -488,15 +463,15 @@ function initVehicleAnimation(id, coordinates, heading, speed) {
|
|||||||
vehicleAnimations.value[id] = {
|
vehicleAnimations.value[id] = {
|
||||||
position: [...coordinates],
|
position: [...coordinates],
|
||||||
targetPosition: [...coordinates],
|
targetPosition: [...coordinates],
|
||||||
heading: heading,
|
heading: heading, // 保存heading值
|
||||||
targetHeading: heading,
|
targetHeading: heading, // 保存目标heading值
|
||||||
speed: speed,
|
speed: speed,
|
||||||
currentSpeed: speed,
|
currentSpeed: speed,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
predictionVector: null,
|
predictionVector: null,
|
||||||
lastDx: 0,
|
lastDx: 0,
|
||||||
lastDy: 0,
|
lastDy: 0,
|
||||||
movementHeading: heading,
|
lastHeading: heading, // 添加lastHeading字段用于跟踪heading变化
|
||||||
// 添加速度历史记录用于平滑过渡
|
// 添加速度历史记录用于平滑过渡
|
||||||
speedHistory: [speed, speed, speed],
|
speedHistory: [speed, speed, speed],
|
||||||
pathHistory: [], // 初始化路径历史
|
pathHistory: [], // 初始化路径历史
|
||||||
@ -516,7 +491,7 @@ function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
|
|||||||
vehicleMotionHistory.value[id].push({
|
vehicleMotionHistory.value[id].push({
|
||||||
time: now,
|
time: now,
|
||||||
position: coordinates,
|
position: coordinates,
|
||||||
heading: heading,
|
heading: heading, // 保存正确的heading值
|
||||||
speed: speed
|
speed: speed
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -620,7 +595,8 @@ function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
|
|||||||
vehicleAnimations.value[id] = {
|
vehicleAnimations.value[id] = {
|
||||||
...animData,
|
...animData,
|
||||||
targetPosition: coordinates,
|
targetPosition: coordinates,
|
||||||
targetHeading: heading,
|
targetHeading: heading, // 保存目标heading值
|
||||||
|
heading: animData.heading || heading, // 保留当前heading,如果不存在则使用新的heading
|
||||||
position: animData.position || coordinates,
|
position: animData.position || coordinates,
|
||||||
speed: smoothedSpeed, // 使用平滑后的速度
|
speed: smoothedSpeed, // 使用平滑后的速度
|
||||||
lastUpdated: now,
|
lastUpdated: now,
|
||||||
@ -648,15 +624,15 @@ function resetAnimations() {
|
|||||||
vehicleAnimations.value[id] = {
|
vehicleAnimations.value[id] = {
|
||||||
position: [...vehicle.position],
|
position: [...vehicle.position],
|
||||||
targetPosition: [...vehicle.position],
|
targetPosition: [...vehicle.position],
|
||||||
heading: vehicle.heading,
|
heading: vehicle.heading || 0, // 使用车辆的heading值
|
||||||
targetHeading: vehicle.heading,
|
targetHeading: vehicle.heading || 0, // 使用车辆的heading值
|
||||||
speed: vehicle.speed,
|
speed: vehicle.speed,
|
||||||
currentSpeed: vehicle.speed,
|
currentSpeed: vehicle.speed,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
predictionVector: null,
|
predictionVector: null,
|
||||||
lastDx: 0,
|
lastDx: 0,
|
||||||
lastDy: 0,
|
lastDy: 0,
|
||||||
movementHeading: vehicle.heading,
|
lastHeading: vehicle.heading || 0, // 使用车辆的heading值
|
||||||
speedHistory: [vehicle.speed, vehicle.speed, vehicle.speed],
|
speedHistory: [vehicle.speed, vehicle.speed, vehicle.speed],
|
||||||
pathHistory: [], // 重置路径历史
|
pathHistory: [], // 重置路径历史
|
||||||
lastPathRecordTime: Date.now() // 重置路径记录时间
|
lastPathRecordTime: Date.now() // 重置路径记录时间
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 标签系统不需要模板内容 -->
|
<!-- 标签容器,不在模板中渲染任何内容,标签由JS动态创建 -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import Overlay from 'ol/Overlay';
|
import { Overlay } from 'ol';
|
||||||
|
|
||||||
// 定义props
|
// 定义props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -12,288 +12,462 @@ const props = defineProps({
|
|||||||
vehicles: Object
|
vehicles: Object
|
||||||
});
|
});
|
||||||
|
|
||||||
// 导入背景图片
|
// 存储标签覆盖物
|
||||||
import labelBg from '../../../assets/images/label_bg.png';
|
const vehicleLabels = ref({});
|
||||||
import airportBg from '../../../assets/images/airport_bg.png';
|
// 添加标签更新时间记录
|
||||||
import airportOutBg from '../../../assets/images/airport_out.png';
|
const labelUpdateTimes = ref({});
|
||||||
import alarmBg from '../../../assets/images/alarm_bg.png';
|
// 定义更新间隔常量(毫秒)
|
||||||
import warningBg from '../../../assets/images/warning_bg.png';
|
const UPDATE_INTERVAL = 500; // 每500ms更新一次标签
|
||||||
|
|
||||||
// 更新车辆标签
|
// 创建或更新车辆标签
|
||||||
function updateVehicleLabel(id, position, speed, violationInfo = null) {
|
function updateVehicleLabel(vehicleId, coordinates, speed, alertInfo) {
|
||||||
if (!props.map || !props.vehicles[id]) return;
|
if (!props.map) return;
|
||||||
|
|
||||||
// 先移除可能存在的旧标签,确保不会重复显示
|
// 获取当前时间
|
||||||
removeVehicleLabel(id);
|
const now = Date.now();
|
||||||
|
|
||||||
const vehicle = props.vehicles[id];
|
// 检查是否满足更新频率限制
|
||||||
const isAircraftIn = vehicle.isAircraftIn;
|
// 如果这是首次更新,或者距离上次更新已经过去了足够的时间
|
||||||
const isAircraftOut = vehicle.isAircraftOut;
|
if (!labelUpdateTimes.value[vehicleId] ||
|
||||||
const isUnmannedVehicle = vehicle.isUnmannedVehicle;
|
(now - labelUpdateTimes.value[vehicleId]) > UPDATE_INTERVAL) {
|
||||||
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';
|
if (vehicleLabels.value[vehicleId]) {
|
||||||
else if (isAircraftOut) typeKey = 'AIRCRAFT_OUT';
|
// 更新现有标签位置
|
||||||
else if (isUnmannedVehicle) typeKey = 'UNMANNED_VEHICLE';
|
vehicleLabels.value[vehicleId].setPosition(coordinates);
|
||||||
else if (isSpecialVehicle) typeKey = 'SPECIAL_VEHICLE';
|
|
||||||
else if (isShuttleVehicle) typeKey = 'SHUTTLE_VEHICLE';
|
|
||||||
|
|
||||||
let backgroundImage;
|
// 更新标签内容
|
||||||
|
updateLabelContent(vehicleId, speed, alertInfo);
|
||||||
// 首先检查告警状态
|
|
||||||
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 {
|
} 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' : ''}`;
|
labelUpdateTimes.value[vehicleId] = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加告警状态类
|
// 创建新标签
|
||||||
if (violationType) labelDiv.className += ' vehicle-violation';
|
function createNewLabel(vehicleId, coordinates, speed, alertInfo) {
|
||||||
if (hasInfoStatus) labelDiv.className += ' vehicle-info';
|
// 创建标签DOM元素
|
||||||
if (hasWarningStatus) labelDiv.className += ' vehicle-warning';
|
const labelElement = document.createElement('div');
|
||||||
if (hasAlarmStatus) labelDiv.className += ' vehicle-alarm';
|
labelElement.className = 'vehicle-label';
|
||||||
if (hasCriticalStatus) labelDiv.className += ' vehicle-critical';
|
labelElement.id = `label-${vehicleId}`;
|
||||||
if (hasSpeedViolation) labelDiv.className += ' vehicle-speed-violation';
|
|
||||||
|
|
||||||
let labelText = '';
|
// 设置标签内容
|
||||||
|
const labelContent = createLabelContent(vehicleId, speed, alertInfo);
|
||||||
// 超速车辆特殊处理标签内容 - 只显示超速相关信息,避免闪烁和重复显示
|
labelElement.innerHTML = labelContent;
|
||||||
if (violationInfo && violationInfo.isSpeedViolation && violationInfo.actualValue !== undefined) {
|
|
||||||
// 通过violationInfo传入的超速信息 - 这是主要的超速状态显示方式
|
|
||||||
labelText = `${id} ${violationInfo.actualValue.toFixed(1)} km/h`;
|
|
||||||
if (violationInfo.description) {
|
|
||||||
labelText += `<br>${violationInfo.description}`;
|
|
||||||
}
|
|
||||||
if (violationInfo.limitValue !== undefined) {
|
|
||||||
labelText += `<br>限速: ${violationInfo.limitValue}km/h`;
|
|
||||||
}
|
|
||||||
if (violationInfo.ruleName) {
|
|
||||||
labelText += `<br>根据${violationInfo.ruleName}请减速慢行!`;
|
|
||||||
}
|
|
||||||
} else if (hasSpeedViolation && vehicle.limitValue !== undefined && vehicle.actualValue !== undefined) {
|
|
||||||
// 备用的超速状态显示方式
|
|
||||||
labelText = `${id} ${vehicle.actualValue.toFixed(1)} km/h`;
|
|
||||||
if (vehicle.description) {
|
|
||||||
labelText += `<br>${vehicle.description}`;
|
|
||||||
}
|
|
||||||
labelText += `<br>限速: ${vehicle.limitValue}km/h`;
|
|
||||||
if (vehicle.ruleName) {
|
|
||||||
labelText += `<br>根据${vehicle.ruleName}请减速慢行!`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 正常车辆显示 - 避免显示0.00km/h的闪烁
|
|
||||||
if (!hasSpeedViolation && speed > 0.1) {
|
|
||||||
labelText = `${id} ${speed.toFixed(1)} km/h`;
|
|
||||||
} else if (!hasSpeedViolation) {
|
|
||||||
// 如果速度为0或很小,只显示车辆ID,不显示速度避免闪烁
|
|
||||||
labelText = `${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (violationType && vehicle.description) {
|
|
||||||
labelText += `<br>${vehicle.description}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (violationType && vehicle.limitValue !== undefined && vehicle.actualValue !== undefined) {
|
|
||||||
labelText += `<br>限速: ${vehicle.limitValue}km/h 实际: ${vehicle.actualValue}km/h`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理传入的违规信息(非超速情况)
|
|
||||||
if (violationInfo && !violationInfo.isSpeedViolation) {
|
|
||||||
if (violationInfo.description) {
|
|
||||||
labelText += `<br>${violationInfo.description}`;
|
|
||||||
}
|
|
||||||
if (violationInfo.limitValue !== undefined && violationInfo.actualValue !== undefined) {
|
|
||||||
labelText += `<br>限速: ${violationInfo.limitValue}km/h 实际: ${violationInfo.actualValue}km/h`;
|
|
||||||
}
|
|
||||||
if (violationInfo.ruleName) {
|
|
||||||
labelText += `<br>根据${violationInfo.ruleName}请减速慢行!`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labelDiv.innerHTML = labelText;
|
|
||||||
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
|
|
||||||
labelDiv.style.backgroundSize = '100% 100%';
|
|
||||||
labelDiv.style.padding = '5px 10px';
|
|
||||||
labelDiv.style.color = '#fff';
|
|
||||||
|
|
||||||
|
// 创建覆盖物
|
||||||
const overlay = new Overlay({
|
const overlay = new Overlay({
|
||||||
element: labelDiv,
|
element: labelElement,
|
||||||
position: position,
|
position: coordinates,
|
||||||
|
offset: [0, -30], // 偏移量,使标签位于图标上方
|
||||||
positioning: 'bottom-center',
|
positioning: 'bottom-center',
|
||||||
offset: [0, -30],
|
|
||||||
stopEvent: false,
|
stopEvent: false,
|
||||||
insertFirst: true, // 确保在DOM中优先插入
|
insertFirst: true, // 确保在DOM中优先插入
|
||||||
autoPan: false, // 禁用自动平移
|
autoPan: false, // 禁用自动平移
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新引用
|
// 将覆盖物添加到地图
|
||||||
props.vehicles[id].overlay = overlay;
|
|
||||||
props.vehicles[id].labelDiv = labelDiv;
|
|
||||||
|
|
||||||
// 添加到地图
|
|
||||||
props.map.addOverlay(overlay);
|
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) {
|
function removeVehicleLabel(vehicleId) {
|
||||||
if (!props.map || !props.vehicles[id]) return;
|
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);
|
function setLabelVisibility(vehicleId, visible) {
|
||||||
props.vehicles[id].overlay = null;
|
if (vehicleLabels.value[vehicleId]) {
|
||||||
props.vehicles[id].labelDiv = null;
|
const labelElement = vehicleLabels.value[vehicleId].getElement();
|
||||||
|
if (labelElement) {
|
||||||
|
labelElement.style.display = visible ? 'block' : 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新所有标签位置
|
// 更新所有标签位置
|
||||||
function updateAllLabels() {
|
function updateAllLabels() {
|
||||||
Object.keys(props.vehicles).forEach(id => {
|
if (!props.map || !props.vehicles) return;
|
||||||
const vehicle = props.vehicles[id];
|
|
||||||
if (vehicle.feature) {
|
Object.keys(vehicleLabels.value).forEach(vehicleId => {
|
||||||
const coordinates = vehicle.feature.getGeometry().getCoordinates();
|
if (props.vehicles[vehicleId] && props.vehicles[vehicleId].position) {
|
||||||
// 如果车辆处于超速状态,不在这里更新标签,避免与超速状态标签冲突
|
vehicleLabels.value[vehicleId].setPosition(props.vehicles[vehicleId].position);
|
||||||
if (!vehicle.speedViolation) {
|
|
||||||
updateVehicleLabel(id, coordinates, vehicle.speed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置标签可见性
|
// 组件卸载时清理资源
|
||||||
function setLabelVisibility(id, visible) {
|
onUnmounted(() => {
|
||||||
if (!props.map || !props.vehicles[id]) return;
|
if (props.map) {
|
||||||
|
Object.values(vehicleLabels.value).forEach(overlay => {
|
||||||
if (visible) {
|
props.map.removeOverlay(overlay);
|
||||||
// 如果车辆处于超速状态,不在这里更新标签,避免与超速状态标签冲突
|
});
|
||||||
if (props.vehicles[id].position && !props.vehicles[id].speedViolation) {
|
|
||||||
updateVehicleLabel(id, props.vehicles[id].position, props.vehicles[id].speed);
|
|
||||||
}
|
}
|
||||||
} else {
|
vehicleLabels.value = {};
|
||||||
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({
|
defineExpose({
|
||||||
updateVehicleLabel,
|
updateVehicleLabel,
|
||||||
removeVehicleLabel,
|
removeVehicleLabel,
|
||||||
updateAllLabels,
|
setLabelVisibility,
|
||||||
setLabelVisibility
|
updateAllLabels
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
/* 车辆标签样式 */
|
/* 标签容器样式 */
|
||||||
:deep(.vehicle-label) {
|
.vehicle-label {
|
||||||
position: absolute;
|
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;
|
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;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
color: #ccc;
|
||||||
pointer-events: none;
|
}
|
||||||
transform: translateX(-50%);
|
|
||||||
text-align: center;
|
/* 标签页按钮样式 */
|
||||||
background-repeat: no-repeat;
|
.tab-buttons {
|
||||||
background-position: center;
|
display: flex;
|
||||||
min-width: 80px;
|
gap: 5px;
|
||||||
min-height: 24px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
background-color: rgba(80, 80, 80, 0.5);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 30;
|
cursor: pointer;
|
||||||
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
|
padding: 0;
|
||||||
will-change: transform, left, top;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滑入航空器标签样式 */
|
.tab-button.active {
|
||||||
:deep(.vehicle-aircraft-in) {
|
background-color: rgba(52, 152, 219, 0.7);
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滑出航空器标签样式 */
|
.tab-button.current {
|
||||||
:deep(.vehicle-aircraft-out) {
|
background-color: rgba(52, 152, 219, 1);
|
||||||
color: #333;
|
box-shadow: 0 0 5px rgba(52, 152, 219, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 无人车标签样式 */
|
.tab-button.disabled {
|
||||||
:deep(.vehicle-unmanned) {
|
background-color: rgba(80, 80, 80, 0.3);
|
||||||
color: #fff;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 特勤车标签样式 */
|
.tab-button.disabled .tab-icon {
|
||||||
:deep(.vehicle-special) {
|
opacity: 0.5;
|
||||||
color: #fff;
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 摆渡车标签样式 */
|
/* 标签图标样式 */
|
||||||
:deep(.vehicle-shuttle) {
|
.tab-icon {
|
||||||
color: #fff;
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 告警标签样式 */
|
.warning-icon {
|
||||||
:deep(.vehicle-alarm) {
|
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>');
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 预警标签样式 */
|
.alarm-icon {
|
||||||
:deep(.vehicle-warning) {
|
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>');
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 严重违规标签样式 */
|
.speed-icon {
|
||||||
:deep(.vehicle-critical) {
|
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>');
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 超速违规标签样式 */
|
.boundary-icon {
|
||||||
:deep(.vehicle-speed-violation) {
|
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>');
|
||||||
color: #fff;
|
}
|
||||||
font-weight: bold;
|
|
||||||
|
.tab-pane {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 告警消息样式 */
|
||||||
|
.alert-message {
|
||||||
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
white-space: normal;
|
padding: 5px;
|
||||||
width: 180px;
|
border-radius: 3px;
|
||||||
text-align: center;
|
/* background-color: rgba(255, 255, 255, 0.1); */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 信息级别(超速等)标签样式 */
|
/* 超速信息样式 */
|
||||||
:deep(.vehicle-info) {
|
.speed-info {
|
||||||
color: #fff;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
white-space: normal;
|
}
|
||||||
width: 160px;
|
|
||||||
text-align: center;
|
.limit-value {
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actual-value {
|
||||||
|
color: #FF4D4F;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -63,8 +63,21 @@ import VehicleStyleManager from './VehicleStyleManager.vue';
|
|||||||
|
|
||||||
// 导入默认图标
|
// 导入默认图标
|
||||||
import carIcon from '../../../assets/images/noPeopleCar.png';
|
import carIcon from '../../../assets/images/noPeopleCar.png';
|
||||||
import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 滑入航空器使用黄色图标
|
import aircraftIcon from '../../../assets/images/Aircraft.png'; // 航空器使用Aircraft.png图标
|
||||||
import aircraftOutIcon from '../../../assets/images/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;
|
||||||
|
|
||||||
// 为SockJS提供polyfill
|
// 为SockJS提供polyfill
|
||||||
if (typeof window !== 'undefined' && !window.global) {
|
if (typeof window !== 'undefined' && !window.global) {
|
||||||
@ -235,6 +248,10 @@ function updateVehiclePosition(vehicleData) {
|
|||||||
|
|
||||||
const { object_id, object_type, position, heading, speed } = 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;
|
let coordinates;
|
||||||
|
|
||||||
coordinates = transform(
|
coordinates = transform(
|
||||||
@ -248,56 +265,67 @@ function updateVehiclePosition(vehicleData) {
|
|||||||
// 根据object_type正确分类车辆
|
// 根据object_type正确分类车辆
|
||||||
let vehicleType = object_type.toUpperCase();
|
let vehicleType = object_type.toUpperCase();
|
||||||
|
|
||||||
// 判断航空器类型
|
// 判断车辆类型
|
||||||
// CA开头的航班是滑入航空器,使用黄色Aircraft1.png图标和airport_out.png文本背景
|
const isAircraft = vehicleType === 'AIRCRAFT'; // 航空器使用aircraft.png
|
||||||
// MU开头的航班是滑出航空器,使用蓝色Aircraft.png图标和airport_bg.png文本背景
|
const isUnmannedVehicle = vehicleType === 'UNMANNED_VEHICLE'; // 无人车
|
||||||
const isAircraftOut = vehicleType === 'AIRCRAFT' && object_id.toLowerCase().includes('ca');
|
const isSpecialVehicle = vehicleType === 'SPECIAL_VEHICLE'; // 特种车辆
|
||||||
const isAircraftIn = vehicleType === 'AIRCRAFT' && object_id.toLowerCase().includes('mu');
|
|
||||||
const isAircraft = vehicleType === 'AIRCRAFT';
|
|
||||||
|
|
||||||
// 判断地面车辆类型
|
// 确保heading是有效的数值
|
||||||
const isUnmannedVehicle = vehicleType === 'UNMANNED_VEHICLE'; // 无人车,使用noPeopleCar.png图标和label_bg.png文本背景
|
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||||||
const isSpecialVehicle = vehicleType === 'AIRPORT_VEHICLE'; // 特勤车
|
|
||||||
const isShuttleVehicle = vehicleType === 'SHUTTLE_VEHICLE'; // 摆渡车
|
|
||||||
|
|
||||||
if (!feature) {
|
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({
|
feature = new Feature({
|
||||||
geometry: new Point(coordinates),
|
geometry: new Point(coordinates),
|
||||||
name: `${object_type} ${object_id}`,
|
name: `${object_type} ${object_id}`,
|
||||||
type: object_type,
|
type: object_type,
|
||||||
speed: speed,
|
speed: speed,
|
||||||
isAircraftIn: isAircraftIn,
|
|
||||||
isAircraftOut: isAircraftOut,
|
|
||||||
isAircraft: isAircraft,
|
isAircraft: isAircraft,
|
||||||
isUnmannedVehicle: isUnmannedVehicle,
|
isUnmannedVehicle: isUnmannedVehicle,
|
||||||
isSpecialVehicle: isSpecialVehicle,
|
isSpecialVehicle: isSpecialVehicle
|
||||||
isShuttleVehicle: isShuttleVehicle
|
|
||||||
});
|
});
|
||||||
|
|
||||||
feature.setId(object_id);
|
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 {
|
} else {
|
||||||
// 提供一个默认样式,根据车辆类型选择图标
|
iconStyle = new Style({
|
||||||
feature.setStyle(defaultGetVehicleStyle(object_id, speed, heading));
|
image: new Icon({
|
||||||
|
src: carIcon,
|
||||||
|
scale: 1.5,
|
||||||
|
anchor: [0.5, 0.5],
|
||||||
|
rotation: rotationRad, // 使用计算好的旋转角度
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feature.setStyle(iconStyle);
|
||||||
vehicleSource.addFeature(feature);
|
vehicleSource.addFeature(feature);
|
||||||
|
|
||||||
vehicles.value[object_id] = {
|
// 保存feature引用到车辆数据对象
|
||||||
id: object_id,
|
vehicles.value[object_id].feature = feature;
|
||||||
type: object_type,
|
|
||||||
position: coordinates,
|
|
||||||
heading: heading,
|
|
||||||
speed: speed,
|
|
||||||
feature: feature,
|
|
||||||
isAircraftIn: isAircraftIn,
|
|
||||||
isAircraftOut: isAircraftOut,
|
|
||||||
isAircraft: isAircraft,
|
|
||||||
isUnmannedVehicle: isUnmannedVehicle,
|
|
||||||
isSpecialVehicle: isSpecialVehicle,
|
|
||||||
isShuttleVehicle: isShuttleVehicle
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化动画数据
|
// 初始化动画数据
|
||||||
if (animationSystem.value) {
|
if (animationSystem.value) {
|
||||||
@ -317,7 +345,7 @@ function updateVehiclePosition(vehicleData) {
|
|||||||
vehicles.value[object_id] = {
|
vehicles.value[object_id] = {
|
||||||
...vehicles.value[object_id],
|
...vehicles.value[object_id],
|
||||||
position: coordinates,
|
position: coordinates,
|
||||||
heading: heading,
|
heading: validHeading,
|
||||||
speed: speed
|
speed: speed
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -326,6 +354,24 @@ function updateVehiclePosition(vehicleData) {
|
|||||||
if (labelSystem.value && !vehicles.value[object_id].speedViolation) {
|
if (labelSystem.value && !vehicles.value[object_id].speedViolation) {
|
||||||
labelSystem.value.updateVehicleLabel(object_id, coordinates, speed);
|
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) {
|
function handleWsMessage(message) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(message);
|
const data = JSON.parse(message);
|
||||||
|
console.log('收到消息:', data);
|
||||||
|
|
||||||
// 根据消息类型处理
|
// 根据消息类型处理
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'connection':
|
case 'connection':
|
||||||
console.log(`连接确认: ${data.message}`);
|
console.log(`连接确认: ${data.message}`);
|
||||||
break;
|
break;
|
||||||
case 'position_update':
|
|
||||||
// 确保payload存在
|
|
||||||
if (data.payload && data.payload.object_id) {
|
|
||||||
// 检查是否需要清除车辆的告警状态
|
|
||||||
clearVehicleAlertStatus(data.payload);
|
|
||||||
|
|
||||||
// 更新车辆位置
|
case 'position_update':
|
||||||
updateVehiclePosition(data.payload);
|
// 处理车辆位置更新
|
||||||
} else {
|
console.log(`位置更新: ${data.payload?.object_id} (${data.payload?.object_type})`);
|
||||||
console.error('位置更新消息格式错误:', data);
|
handlePositionUpdate(data.payload);
|
||||||
}
|
|
||||||
break;
|
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':
|
case 'pong':
|
||||||
console.log('收到心跳响应');
|
console.log('收到心跳响应');
|
||||||
break;
|
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]) {
|
if (vehicles.value[vehicleId]) {
|
||||||
vehicles.value[vehicleId].warning = true;
|
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) {
|
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;
|
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 || '交通规则';
|
|
||||||
|
|
||||||
// 检查是否为超速违规
|
default:
|
||||||
const isSpeedViolation = violationType === 'SPEED_VIOLATION';
|
console.log(`未知的规则违规类型: ${violationType}`);
|
||||||
// 检查是否为超速结束
|
// 默认按越界处理
|
||||||
const isSpeedViolationEnd = violationType === 'SPEED_VIOLATION_END';
|
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}`);
|
console.log(`检测到超速违规: ${vehicleId}, 实际速度: ${actualValue}, 限速: ${limitValue}`);
|
||||||
|
|
||||||
|
// 显示超速告警提示
|
||||||
|
showAlert(`⚠️ 超速告警:${vehicleId} ${description}`, 'warning', 8000);
|
||||||
|
|
||||||
// 在地图上标记该车辆的超速状态
|
// 在地图上标记该车辆的超速状态
|
||||||
if (vehicles.value[vehicleId]) {
|
if (vehicles.value[vehicleId]) {
|
||||||
// 避免重复设置超速状态导致闪烁
|
// 避免重复设置超速状态导致闪烁
|
||||||
@ -483,7 +639,7 @@ function handleWsMessage(message) {
|
|||||||
|
|
||||||
// 更新车辆图标为超速警告图标(只在首次设置或状态改变时更新)
|
// 更新车辆图标为超速警告图标(只在首次设置或状态改变时更新)
|
||||||
if (!alreadyInSpeedViolation) {
|
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].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
|
||||||
vehicles.value[vehicleId].lastIconUpdateTime = Date.now();
|
vehicles.value[vehicleId].lastIconUpdateTime = Date.now();
|
||||||
}
|
}
|
||||||
@ -513,6 +669,12 @@ function handleWsMessage(message) {
|
|||||||
speedViolationTimers[vehicleId] = setTimeout(() => {
|
speedViolationTimers[vehicleId] = setTimeout(() => {
|
||||||
// 确保车辆仍然存在
|
// 确保车辆仍然存在
|
||||||
if (vehicles.value[vehicleId]) {
|
if (vehicles.value[vehicleId]) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log(`超速状态超时: ${vehicleId}, 自动清除超速状态`);
|
console.log(`超速状态超时: ${vehicleId}, 自动清除超速状态`);
|
||||||
|
|
||||||
// 检查是否可以清除状态锁定
|
// 检查是否可以清除状态锁定
|
||||||
@ -538,140 +700,43 @@ function handleWsMessage(message) {
|
|||||||
}
|
}
|
||||||
}, 20000); // 20秒超时,确保状态稳定
|
}, 20000); // 20秒超时,确保状态稳定
|
||||||
}
|
}
|
||||||
return; // 已处理超速情况,不再进入下面的alertLevel处理
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是超速结束,清除超速状态
|
// 处理越界告警
|
||||||
if (isSpeedViolationEnd) {
|
function handleUnauthorizedEntry(vehicleId, payload) {
|
||||||
console.log(`检测到超速结束: ${vehicleId}`);
|
const description = payload.description || '越界告警';
|
||||||
|
const alertLevel = payload.alertLevel || 'CRITICAL';
|
||||||
|
const ruleName = payload.ruleName || '区域控制';
|
||||||
|
|
||||||
// 在地图上清除该车辆的超速状态
|
console.log(`检测到越界告警: ${vehicleId}, ${description}, 规则: ${ruleName}`);
|
||||||
if (vehicles.value[vehicleId]) {
|
|
||||||
// 检查是否可以清除状态锁定
|
|
||||||
if (!vehicles.value[vehicleId].statusLock ||
|
|
||||||
!vehicles.value[vehicleId].statusLock.active ||
|
|
||||||
Date.now() > vehicles.value[vehicleId].statusLock.until) {
|
|
||||||
|
|
||||||
// 清除超速状态,使用专门的函数
|
// 显示越界告警提示
|
||||||
clearSpeedViolationStatus(vehicleId);
|
showAlert(`⚠️ 越界告警:${vehicleId} ${description}`, 'critical', 10000);
|
||||||
} else {
|
|
||||||
// 状态仍然锁定,延迟清除
|
|
||||||
console.log(`车辆${vehicleId}状态仍然锁定,延迟清除超速状态`);
|
|
||||||
|
|
||||||
// 设置计时器,等待锁定结束
|
// 在地图上标记该车辆的越界状态
|
||||||
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]) {
|
if (vehicles.value[vehicleId]) {
|
||||||
vehicles.value[vehicleId].critical = true;
|
vehicles.value[vehicleId].critical = true;
|
||||||
vehicles.value[vehicleId].alarm = false;
|
vehicles.value[vehicleId].alarm = false;
|
||||||
vehicles.value[vehicleId].warning = false;
|
vehicles.value[vehicleId].warning = false;
|
||||||
vehicles.value[vehicleId].info = 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));
|
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, {
|
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
|
||||||
description: description,
|
description: description,
|
||||||
limitValue: limitValue,
|
ruleName: ruleName,
|
||||||
actualValue: actualValue
|
isUnauthorizedEntry: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case 'WARNING': // 超速 - 这个case已经在上面的SPEED_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) {
|
|||||||
|
|
||||||
// 更新标签显示为正常状态(只显示车辆ID,不显示0.00km/h)
|
// 更新标签显示为正常状态(只显示车辆ID,不显示0.00km/h)
|
||||||
if (vehicle.position && labelSystem.value) {
|
if (vehicle.position && labelSystem.value) {
|
||||||
|
// 先移除可能存在的旧标签,避免重复显示
|
||||||
|
labelSystem.value.removeVehicleLabel(vehicleId);
|
||||||
|
|
||||||
|
// 创建新的标准标签
|
||||||
labelSystem.value.updateVehicleLabel(vehicleId, vehicle.position, vehicle.speed > 0.1 ? vehicle.speed : 0);
|
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];
|
const vehicle = vehicles.value[id];
|
||||||
if (!vehicle) return createDefaultStyle(carIcon, heading);
|
if (!vehicle) return createDefaultStyle(carIcon, heading);
|
||||||
|
|
||||||
// 根据车辆类型选择图标
|
// 只根据车辆类型选择图标,不根据事件状态改变
|
||||||
if (vehicle.isAircraftIn) {
|
if (vehicle.isAircraft) {
|
||||||
// 滑入航空器(CA开头)使用黄色Aircraft1.png图标
|
// 航空器使用Aircraft.png图标
|
||||||
return createDefaultStyle(aircraftInIcon, heading);
|
return createDefaultStyle(aircraftIcon, 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);
|
|
||||||
} else {
|
} else {
|
||||||
// 默认车辆图标
|
// 其他所有车辆使用noPeopleCar.png图标
|
||||||
return createDefaultStyle(carIcon, heading);
|
return createDefaultStyle(carIcon, heading);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建默认样式的辅助函数
|
// 创建默认样式的辅助函数
|
||||||
function createDefaultStyle(iconSrc, heading) {
|
function createDefaultStyle(iconSrc, heading) {
|
||||||
|
// 确保heading是有效的数值并正确转换为弧度
|
||||||
|
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||||||
|
const rotationRad = (validHeading * Math.PI) / 180;
|
||||||
|
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new Icon({
|
image: new Icon({
|
||||||
src: iconSrc,
|
src: iconSrc, // 使用传入的图标源
|
||||||
scale: 1.5,
|
scale: 1.5,
|
||||||
anchor: [0.5, 0.5],
|
anchor: [0.5, 0.5],
|
||||||
rotation: ((heading - 72) * Math.PI) / 180,
|
rotation: rotationRad, // 正确应用旋转角度
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,214 +7,62 @@ import { Style, Icon } from 'ol/style';
|
|||||||
|
|
||||||
// 导入车辆图标
|
// 导入车辆图标
|
||||||
import carIcon from '../../../assets/images/noPeopleCar.png';
|
import carIcon from '../../../assets/images/noPeopleCar.png';
|
||||||
import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 滑入航空器使用黄色图标
|
import aircraftIcon from '../../../assets/images/aircraft.png'; // 航空器图标
|
||||||
import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 滑出航空器使用蓝色图标
|
// 不再使用告警状态图标
|
||||||
import warningCarIcon from '../../../assets/images/warning_car.png';
|
// import warningCarIcon from '../../../assets/images/warning_car.png'; // 预警车辆图标
|
||||||
import unmannedVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 无人车图标
|
// import alarmCarIcon from '../../../assets/images/alarm_car.png'; // 告警车辆图标
|
||||||
import specialVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 特勤车图标
|
// import criticalCarIcon from '../../../assets/images/warning_car.png'; // 越界车辆图标(使用warning_car.png替代)
|
||||||
import shuttleVehicleIcon from '../../../assets/images/noPeopleCar.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
|
// 定义props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
vehicles: Object
|
vehicles: Object
|
||||||
});
|
});
|
||||||
|
|
||||||
// 状态锁定时间(毫秒)- 超速状态锁定时间更长,确保图标稳定
|
|
||||||
const STATUS_LOCK_TIME = 20000; // 20秒锁定时间,避免图标来回切换
|
|
||||||
|
|
||||||
// 获取车辆样式
|
// 获取车辆样式
|
||||||
function getVehicleStyle(id, speed, heading) {
|
function getVehicleStyle(id, speed, heading) {
|
||||||
const vehicle = props.vehicles[id];
|
const vehicle = props.vehicles[id];
|
||||||
if (!vehicle) return new Style({});
|
if (!vehicle) return createDefaultStyle(carIcon, heading);
|
||||||
|
|
||||||
// 初始化状态锁定属性(如果不存在)
|
// 只根据车辆类型选择图标,不根据状态变化
|
||||||
if (vehicle.statusLock === undefined) {
|
const isAircraft = vehicle.isAircraft;
|
||||||
vehicle.statusLock = {
|
|
||||||
active: false,
|
|
||||||
type: null,
|
|
||||||
until: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
// 根据车辆类型选择图标
|
||||||
const hasViolation = vehicle.violationType;
|
const iconSrc = isAircraft ? aircraftIcon : carIcon;
|
||||||
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({
|
return createDefaultStyle(iconSrc, heading);
|
||||||
image: new Icon({
|
|
||||||
src: iconSrc,
|
|
||||||
scale: 1.5,
|
|
||||||
anchor: [0.5, 0.5],
|
|
||||||
rotation: ((heading - 72) * Math.PI) / 180,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据车辆类型和状态选择图标
|
// 创建默认样式
|
||||||
function getVehicleIcon(vehicle) {
|
function createDefaultStyle(iconSrc, heading) {
|
||||||
if (!vehicle) return carIcon;
|
// 确保heading是有效的数值
|
||||||
|
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||||||
|
|
||||||
const hasInfoStatus = vehicle.info;
|
// 统一使用(heading - 72)的偏移量计算旋转角度
|
||||||
const hasWarningStatus = vehicle.warning;
|
const rotationRad = ((validHeading - 72) * Math.PI) / 180;
|
||||||
const hasAlarmStatus = vehicle.alarm;
|
|
||||||
const hasCriticalStatus = vehicle.critical;
|
|
||||||
|
|
||||||
// 首先检查告警状态
|
// 添加调试信息
|
||||||
if (hasInfoStatus || hasWarningStatus || hasCriticalStatus || hasAlarmStatus) {
|
console.log(`VehicleStyleManager: heading=${validHeading}, rotation=${rotationRad}弧度, ${rotationRad * 180 / Math.PI}度`);
|
||||||
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({
|
return new Style({
|
||||||
image: new Icon({
|
image: new Icon({
|
||||||
src: iconSrc,
|
src: iconSrc,
|
||||||
scale: 1.5,
|
scale: 1.5,
|
||||||
anchor: [0.5, 0.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({
|
defineExpose({
|
||||||
getVehicleStyle,
|
getVehicleStyle
|
||||||
getVehicleIcon,
|
|
||||||
createVehicleStyle,
|
|
||||||
updateVehicleStyle
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -221,6 +221,7 @@ function updateCarList(newList) {
|
|||||||
|
|
||||||
const selectedCar = ref(null);
|
const selectedCar = ref(null);
|
||||||
function showDetail(car) {
|
function showDetail(car) {
|
||||||
|
// 移除对不存在的moveToTarget函数的调用
|
||||||
selectedCar.value = car;
|
selectedCar.value = car;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,31 +11,20 @@
|
|||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
max-width: 1920px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
padding: 20px;
|
padding: 2px;
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
.test-section {
|
.test-section {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 20px 0;
|
margin: 2px 0;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
.test-section h2 {
|
|
||||||
color: #555;
|
|
||||||
margin-top: 0;
|
|
||||||
border-bottom: 2px solid #007bff;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
.control-group {
|
.control-group {
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -86,7 +75,7 @@
|
|||||||
.log-container {
|
.log-container {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 300px;
|
height: 960px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: white;
|
background: white;
|
||||||
@ -106,16 +95,15 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>冲突检测WebSocket测试工具</h1>
|
|
||||||
|
|
||||||
<!-- 冲突检测WebSocket测试 -->
|
<!-- 冲突检测WebSocket测试 -->
|
||||||
<div class="test-section">
|
<div class="test-section">
|
||||||
<h2>🚗 冲突检测WebSocket (原生协议)</h2>
|
<h3>🚗 冲突检测WebSocket</h3>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>服务器地址:</label>
|
<label>服务器地址:</label>
|
||||||
<select id="collisionServerSelect">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -136,7 +124,7 @@
|
|||||||
|
|
||||||
<!-- 控制面板 -->
|
<!-- 控制面板 -->
|
||||||
<div class="test-section">
|
<div class="test-section">
|
||||||
<h2>🎛️ 控制面板</h2>
|
<h3>🎛️ 控制面板</h3>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button onclick="clearLogs()">清空日志</button>
|
<button onclick="clearLogs()">清空日志</button>
|
||||||
<button onclick="showConnectionStatus()">显示连接状态</button>
|
<button onclick="showConnectionStatus()">显示连接状态</button>
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
<el-table-column label="车牌号" prop="licensePlate" align="left" />
|
<el-table-column label="车牌号" prop="licensePlate" align="left" />
|
||||||
<el-table-column label="车辆类型" align="left">
|
<el-table-column label="车辆类型" align="left">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ getVehicleTypeName(scope.row.typeId) }}
|
{{ scope.row.typeDisplayName || '未知类型' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="品牌" prop="brand" align="left" />
|
<el-table-column label="品牌" prop="brand" align="left" />
|
||||||
@ -544,6 +544,7 @@ function handleRemoveImage() {
|
|||||||
|
|
||||||
/** 获取车辆类型名称 */
|
/** 获取车辆类型名称 */
|
||||||
function getVehicleTypeName(typeId) {
|
function getVehicleTypeName(typeId) {
|
||||||
|
// 保留此函数以兼容旧代码,但在表格中直接使用typeDisplayName
|
||||||
return vehicleTypeMap.value[typeId] || '';
|
return vehicleTypeMap.value[typeId] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,7 +115,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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="createBy" />
|
||||||
<el-table-column label="创建时间" prop="createTime" />
|
<el-table-column label="创建时间" prop="createTime" />
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -170,11 +174,7 @@ const { proxy } = getCurrentInstance();
|
|||||||
|
|
||||||
// Tab相关
|
// Tab相关
|
||||||
const editableTabsValue = ref("");
|
const editableTabsValue = ref("");
|
||||||
const editableTabs = ref([
|
const editableTabs = ref([]);
|
||||||
{ label: "无人车", name: "无人车", content: "" },
|
|
||||||
{ label: "特勤车", name: "特勤车", content: "" },
|
|
||||||
{ label: "普通车", name: "普通车", content: "" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const showAddTabDialog = ref(false);
|
const showAddTabDialog = ref(false);
|
||||||
const newTabName = ref("");
|
const newTabName = ref("");
|
||||||
@ -197,9 +197,15 @@ const addTab = () => {
|
|||||||
|
|
||||||
// 构造一级类型数据
|
// 构造一级类型数据
|
||||||
const submitData = {
|
const submitData = {
|
||||||
typeName: name,
|
displayNameCn: name,
|
||||||
|
displayNameEn: name, // 英文名称默认与中文相同
|
||||||
|
typeCode: generateTypeCode(name), // 生成唯一的typeCode
|
||||||
|
typeName: name.toUpperCase().replace(/\s+/g, "_"), // 生成typeName
|
||||||
level: 1,
|
level: 1,
|
||||||
parentId: null,
|
isLeaf: false,
|
||||||
|
enabled: true,
|
||||||
|
parentCode: null,
|
||||||
|
topLevelCode: null
|
||||||
};
|
};
|
||||||
|
|
||||||
// 调用接口添加一级类型
|
// 调用接口添加一级类型
|
||||||
@ -211,7 +217,9 @@ const addTab = () => {
|
|||||||
getFirstTypeList();
|
getFirstTypeList();
|
||||||
|
|
||||||
// 切换到新添加的标签
|
// 切换到新添加的标签
|
||||||
editableTabsValue.value = name;
|
setTimeout(() => {
|
||||||
|
editableTabsValue.value = submitData.typeCode;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// 清空输入框并关闭弹窗
|
// 清空输入框并关闭弹窗
|
||||||
newTabName.value = "";
|
newTabName.value = "";
|
||||||
@ -220,41 +228,65 @@ const addTab = () => {
|
|||||||
// 获取新标签下的数据
|
// 获取新标签下的数据
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getList();
|
getList();
|
||||||
}, 100);
|
}, 200);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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) => {
|
const deleteCustomTab = (tabName) => {
|
||||||
// 查找要删除的标签对应的typeId
|
// 查找要删除的标签对应的typeId
|
||||||
const firstType = firstTypeList.value.find((item) => item.value === tabName);
|
const firstType = firstTypeList.value.find((item) => item.value === tabName);
|
||||||
if (!firstType || !firstType.typeId) return;
|
if (!firstType || !firstType.typeId) return;
|
||||||
|
|
||||||
// 先检查该一级类型下是否有二级类型
|
// 先检查该一级类型下是否有二级类型
|
||||||
listVehicle_type({ firstType: tabName }).then((res) => {
|
listVehicle_type({ topLevelCode: tabName }).then((res) => {
|
||||||
if (res.data && Array.isArray(res.data)) {
|
if (res.rows && Array.isArray(res.rows)) {
|
||||||
// 过滤出当前选中tab对应的二级类型数据
|
// 过滤出当前选中tab对应的二级类型数据
|
||||||
const currentTabData = res.data.filter(
|
const currentTabData = res.rows.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.level === 2 &&
|
item.level === 2 &&
|
||||||
item.parentId &&
|
item.topLevelCode === tabName &&
|
||||||
res.data.some(
|
item.parentCode === tabName
|
||||||
(parent) =>
|
|
||||||
parent.typeId === item.parentId && parent.typeName === tabName
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (currentTabData.length > 0) {
|
if (currentTabData.length > 0) {
|
||||||
// 如果有二级类型,提示不能删除
|
// 如果有二级类型,提示不能删除
|
||||||
proxy.$modal.msgError("该二级类型已绑定车辆,不可删除");
|
proxy.$modal.msgError("该类型下有二级类型,不可删除");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有二级类型,则可以删除
|
// 如果没有二级类型,则可以删除
|
||||||
proxy.$modal
|
proxy.$modal
|
||||||
.confirm(`确定删除 ${tabName} 类型吗?`, "删除")
|
.confirm(`确定删除 ${firstType.label} 类型吗?`, "删除")
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return delVehicle_type(firstType.typeId);
|
return delVehicle_type(firstType.typeId);
|
||||||
})
|
})
|
||||||
@ -310,11 +342,8 @@ const rules = ref({
|
|||||||
typeName: [{ required: true, message: "二级类型不能为空", trigger: "blur" }],
|
typeName: [{ required: true, message: "二级类型不能为空", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstTypeList = ref([
|
// 车辆类型选项
|
||||||
{ label: "无人车", value: "无人车" },
|
const firstTypeList = ref([]);
|
||||||
{ label: "特勤车", value: "特勤车" },
|
|
||||||
{ label: "普通车", value: "普通车" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** 查询车辆类型列表 */
|
/** 查询车辆类型列表 */
|
||||||
function getList() {
|
function getList() {
|
||||||
@ -324,25 +353,21 @@ function getList() {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
// 根据接口返回的数据结构进行过滤,只显示当前选中tab对应的二级类型数据
|
// 根据接口返回的数据结构进行过滤,只显示当前选中tab对应的二级类型数据
|
||||||
if (res.data && Array.isArray(res.data)) {
|
if (res.rows && Array.isArray(res.rows)) {
|
||||||
// 过滤出当前选中tab对应的二级类型数据
|
// 过滤出当前选中tab对应的二级类型数据
|
||||||
const currentTabData = res.data.filter(
|
const currentTabData = res.rows.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.level === 2 &&
|
item.level === 2 &&
|
||||||
item.parentId &&
|
item.topLevelCode === editableTabsValue.value &&
|
||||||
res.data.some(
|
item.parentCode === editableTabsValue.value
|
||||||
(parent) =>
|
|
||||||
parent.typeId === item.parentId &&
|
|
||||||
parent.typeName === editableTabsValue.value
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 转换数据结构以适应表格显示
|
// 转换数据结构以适应表格显示
|
||||||
vehicleTypeList.value = currentTabData.map((item) => ({
|
vehicleTypeList.value = currentTabData.map((item) => ({
|
||||||
typeId: item.typeId,
|
typeId: item.typeId,
|
||||||
firstType: editableTabsValue.value,
|
firstType: editableTabsValue.value,
|
||||||
typeName: item.typeName,
|
typeName: item.displayNameCn,
|
||||||
createBy: item.createBy || "系统管理员",
|
createBy: item.createBy,
|
||||||
createTime: item.createTime,
|
createTime: item.createTime,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -392,7 +417,16 @@ function handleUpdate(row) {
|
|||||||
reset();
|
reset();
|
||||||
const typeId = row.typeId || ids.value[0];
|
const typeId = row.typeId || ids.value[0];
|
||||||
getVehicle_type(typeId).then((response) => {
|
getVehicle_type(typeId).then((response) => {
|
||||||
form.value = response.data || {};
|
// 确保使用displayNameCn作为typeName显示
|
||||||
|
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;
|
open.value = true;
|
||||||
title.value = "修改车辆类型";
|
title.value = "修改车辆类型";
|
||||||
});
|
});
|
||||||
@ -405,21 +439,12 @@ function submitForm() {
|
|||||||
// 根据接口要求调整提交的数据结构
|
// 根据接口要求调整提交的数据结构
|
||||||
const submitData = {
|
const submitData = {
|
||||||
typeId: form.value.typeId,
|
typeId: form.value.typeId,
|
||||||
typeName: form.value.typeName,
|
displayNameCn: form.value.typeName,
|
||||||
parentId: null,
|
|
||||||
level: 2,
|
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) {
|
if (form.value.typeId != undefined) {
|
||||||
updateVehicle_type(submitData).then((response) => {
|
updateVehicle_type(submitData).then((response) => {
|
||||||
proxy.$modal.msgSuccess("修改成功");
|
proxy.$modal.msgSuccess("修改成功");
|
||||||
@ -489,21 +514,21 @@ function cancel() {
|
|||||||
// 初始化时获取一级类型数据
|
// 初始化时获取一级类型数据
|
||||||
function getFirstTypeList() {
|
function getFirstTypeList() {
|
||||||
listVehicle_type({ level: 1 }).then((res) => {
|
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);
|
||||||
|
|
||||||
// 更新firstTypeList,保留typeId信息
|
// 更新firstTypeList,保留typeId信息
|
||||||
firstTypeList.value = firstTypes.map((item) => ({
|
firstTypeList.value = firstTypes.map((item) => ({
|
||||||
label: item.typeName,
|
label: item.displayNameCn,
|
||||||
value: item.typeName,
|
value: item.typeCode,
|
||||||
typeId: item.typeId,
|
typeId: item.typeId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 更新可编辑的tab列表
|
// 更新可编辑的tab列表
|
||||||
editableTabs.value = firstTypes.map((item) => ({
|
editableTabs.value = firstTypes.map((item) => ({
|
||||||
label: item.typeName,
|
label: item.displayNameCn,
|
||||||
name: item.typeName,
|
name: item.typeCode,
|
||||||
content: "",
|
content: "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
<!-- 车辆移动控制组件 -->
|
<!-- 车辆移动控制组件 -->
|
||||||
<VehicleMovementControlRefactored :map="map" ref="vehicleMovementRef" v-if="map" />
|
<VehicleMovementControlRefactored :map="map" ref="vehicleMovementRef" v-if="map" />
|
||||||
|
<!-- <VehicleDisplayControl :map="map" ref="vehicleMovementRef" v-if="map" /> -->
|
||||||
|
|
||||||
|
|
||||||
<!-- 绘制工具栏 -->
|
<!-- 绘制工具栏 -->
|
||||||
@ -78,12 +78,12 @@
|
|||||||
<div class="toolbar-title">绘制工具</div>
|
<div class="toolbar-title">绘制工具</div>
|
||||||
<div class="toolbar-buttons">
|
<div class="toolbar-buttons">
|
||||||
<button @click="startDrawLine" class="toolbar-button">绘制路线</button>
|
<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="clearDraw" class="toolbar-button clear">清除</button>
|
||||||
<button @click="exportRouteData" class="toolbar-button export">导出数据</button>
|
<button @click="exportRouteData" class="toolbar-button export">导出数据</button>
|
||||||
<button @click="importRouteData" class="toolbar-button import">导入数据</button>
|
<button @click="importRouteData" class="toolbar-button import">导入数据</button>
|
||||||
</div>
|
</div>
|
||||||
</div>-->
|
</div> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -486,8 +486,8 @@ watch(() => mapRef.value?.map, (newMap) => {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 2001;
|
z-index: 2001;
|
||||||
width: 40px;
|
width: 30px;
|
||||||
height: 80px;
|
height: 58px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: right 0.3s ease;
|
transition: right 0.3s ease;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user