修改平台概览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、前期准备工作熟悉超图地图服务以及基本的操作。
|
||||
2、熟悉Openlayers以及Leaflet相关操作
|
||||
具体代码操作:
|
||||
1、定义一个地图渲染承载框<div id="replay_map" />
|
||||
2、定义一个地图实例 map: null, 定义一个渲染位置图层 layer: null
|
||||
3、初始化地图
|
||||
// 初始化地图
|
||||
initMap() {
|
||||
ol.proj.setProj4(proj4);
|
||||
proj4.defs("EPSG:4528","+proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=GRS80 +units=m +no_defs");
|
||||
var projection = new ol.proj.Projection({
|
||||
code: 'EPSG:4528',// 地图坐标系
|
||||
});
|
||||
this.map = new ol.Map({
|
||||
target: 'replay_map',//第一步设置的地图承载框
|
||||
controls: ol.control.defaults({
|
||||
attribution: false,
|
||||
rotate: false
|
||||
}),
|
||||
view: new ol.View({
|
||||
center: [40507885.133754 , 4025694.476392],//地图中心点
|
||||
zoom: 12,//初始时的缩放比例
|
||||
projection: projection,//坐标系等的设置
|
||||
rotation: 0.3
|
||||
}),
|
||||
//图层,这个是后期所有自定义的展示层
|
||||
layers: [
|
||||
new ol.layer.Tile({
|
||||
source: new ol.source.TileSuperMapRest({
|
||||
crossOrigin: 'anonymous',
|
||||
url: this.$map_url,
|
||||
extent: [40347872.25,2703739.74,40599933.05,5912395.20]
|
||||
}),
|
||||
projection: projection,
|
||||
})
|
||||
]
|
||||
});
|
||||
// 添加位置图层
|
||||
this.layer = new ol.layer.Vector({
|
||||
source: new ol.source.Vector(),
|
||||
zIndex: 2,
|
||||
});
|
||||
//把自定义展示的图层放到地图中
|
||||
this.map.addLayer(this.layer);
|
||||
},
|
||||
3、图层自定义内容
|
||||
//创建一个Feature
|
||||
const feature = new ol.Feature({
|
||||
//以一个点作为示例,可以是点、线、面
|
||||
geometry: new ol.geom.Point([item.longitude, item.latitude]),
|
||||
});
|
||||
//定义样式
|
||||
feature.setStyle(() => {
|
||||
//自定义样式
|
||||
const style = [];
|
||||
style.push(this.getStyle(type, item));
|
||||
return style;
|
||||
}
|
||||
//这个layer就是已开始定义的渲染图层
|
||||
this.layer.getSource().addFeature(feature);
|
||||
|
||||
// 得到style实例
|
||||
getStyle(type, item, status) {
|
||||
return new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
src: getImage(type, item, status),
|
||||
rotateWithView: true,
|
||||
scale: this.getScale(type),
|
||||
rotation: 0,
|
||||
}),
|
||||
zIndex: 3,
|
||||
text: new ol.style.Text({
|
||||
font: '12px 微软雅黑',
|
||||
text: this.getText(type, item),
|
||||
offsetY: -15,
|
||||
fill: new ol.style.Fill({
|
||||
color: '#515a71',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -114,17 +33,6 @@ getStyle(type, item, status) {
|
||||
|
||||
|
||||
|
||||
/** 获取用户列表 */
|
||||
function getUserOptions() {
|
||||
|
||||
getRoleUsers(3).then(res => {
|
||||
userOptions.value = res.rows;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
使用SockJS + STOMP协议
|
||||
滑出蓝色 滑入黄色
|
||||
|
||||
20250711关键改进说明
|
||||
1. 平滑动画核心系统
|
||||
@ -162,3 +70,12 @@ resetVehicleAnimations():重置所有动画数据
|
||||
调用startVehicleSmoothing()启动平滑效果
|
||||
处理WebSocket消息时继续调用updateVehiclePosition()
|
||||
当组件隐藏时调用stopVehicleSmoothing()节省资源
|
||||
|
||||
### 运行方向分析:
|
||||
地图上方(heading=72)
|
||||
|
|
||||
|
|
||||
地图左侧(heading=342) --+-- 地图右侧(heading=162)
|
||||
|
|
||||
|
|
||||
地图下方(heading=252)
|
||||
65
public/quyu.json
Normal file
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> -->
|
||||
</label>
|
||||
</div>
|
||||
<!-- 添加区域1图层选项 -->
|
||||
<div class="layer-item">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" v-model="showArea1Layer" />
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">测试区域1</span>
|
||||
</label>
|
||||
</div>
|
||||
<!-- 添加区域2图层选项 -->
|
||||
<div class="layer-item">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" v-model="showArea2Layer" />
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">测试区域2</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -129,6 +145,14 @@ let roadVectorLayer = null;
|
||||
const showCustomRoadLayer = ref(true); // 默认显示
|
||||
let customRoadVectorLayer = null;
|
||||
|
||||
// 新增:区域1图层状态
|
||||
const showArea1Layer = ref(true); // 默认显示
|
||||
let area1VectorLayer = null;
|
||||
|
||||
// 新增:区域2图层状态
|
||||
const showArea2Layer = ref(true); // 默认显示
|
||||
let area2VectorLayer = null;
|
||||
|
||||
// 向父组件发出事件
|
||||
const emit = defineEmits(['layerChange', 'setCategoryVisibility']);
|
||||
|
||||
@ -159,6 +183,12 @@ onMounted(() => {
|
||||
if (showRoadLayer.value) {
|
||||
addRoadLayer();
|
||||
}
|
||||
|
||||
// 初始化区域1图层
|
||||
loadArea1Layer();
|
||||
|
||||
// 初始化区域2图层
|
||||
loadArea2Layer();
|
||||
}
|
||||
});
|
||||
|
||||
@ -172,6 +202,12 @@ watch(() => props.map, (newMap) => {
|
||||
if (showRoadLayer.value) {
|
||||
addRoadLayer();
|
||||
}
|
||||
|
||||
// 初始化区域1图层
|
||||
loadArea1Layer();
|
||||
|
||||
// 初始化区域2图层
|
||||
loadArea2Layer();
|
||||
}
|
||||
});
|
||||
|
||||
@ -349,9 +385,145 @@ watch(showRoadLayer, (val) => {
|
||||
else removeRoadLayer();
|
||||
});
|
||||
|
||||
// 加载区域1图层
|
||||
async function loadArea1Layer() {
|
||||
// 先移除已存在的区域1图层,避免重复
|
||||
removeArea1Layer();
|
||||
if (!props.map) return;
|
||||
try {
|
||||
// 使用相对路径加载区域1文件
|
||||
const res = await fetch('./quyu1.json');
|
||||
const geojson = await res.json();
|
||||
const source = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(geojson, {
|
||||
dataProjection: 'EPSG:4326',
|
||||
featureProjection: props.map.getView().getProjection()
|
||||
})
|
||||
});
|
||||
|
||||
area1VectorLayer = new VectorLayer({
|
||||
source,
|
||||
style: new Style({
|
||||
stroke: new Stroke({ color: '#FF5733', width: 2 }),
|
||||
fill: new Fill({ color: 'rgba(255, 87, 51, 0.3)' })
|
||||
}),
|
||||
zIndex: 3, // 确保在其他图层之上
|
||||
visible: showArea1Layer.value
|
||||
});
|
||||
|
||||
props.map.addLayer(area1VectorLayer);
|
||||
console.log('loadArea1Layer: 已添加区域1图层', area1VectorLayer);
|
||||
} catch (e) {
|
||||
console.error('loadArea1Layer: 加载或添加区域1图层失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除区域1图层
|
||||
function removeArea1Layer() {
|
||||
if (!props.map) return;
|
||||
let removed = false;
|
||||
// 获取所有图层,查找类型为 VectorLayer 且 zIndex 为 3 的(即区域1图层)
|
||||
const layers = props.map.getLayers().getArray();
|
||||
for (let i = layers.length - 1; i >= 0; i--) {
|
||||
const lyr = layers[i];
|
||||
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 3) {
|
||||
props.map.removeLayer(lyr);
|
||||
removed = true;
|
||||
console.log('removeArea1Layer: 已移除区域1图层', lyr);
|
||||
}
|
||||
}
|
||||
area1VectorLayer = null;
|
||||
if (!removed) {
|
||||
console.log('removeArea1Layer: 没有找到可移除的区域1图层');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载区域2图层
|
||||
async function loadArea2Layer() {
|
||||
// 先移除已存在的区域2图层,避免重复
|
||||
removeArea2Layer();
|
||||
if (!props.map) return;
|
||||
try {
|
||||
// 使用相对路径加载区域2文件
|
||||
const res = await fetch('./quyu2.json');
|
||||
const geojson = await res.json();
|
||||
const source = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(geojson, {
|
||||
dataProjection: 'EPSG:4326',
|
||||
featureProjection: props.map.getView().getProjection()
|
||||
})
|
||||
});
|
||||
|
||||
area2VectorLayer = new VectorLayer({
|
||||
source,
|
||||
style: new Style({
|
||||
stroke: new Stroke({ color: '#3374FF', width: 2 }),
|
||||
fill: new Fill({ color: 'rgba(51, 116, 255, 0.3)' })
|
||||
}),
|
||||
zIndex: 4, // 确保在区域1图层之上
|
||||
visible: showArea2Layer.value
|
||||
});
|
||||
|
||||
props.map.addLayer(area2VectorLayer);
|
||||
console.log('loadArea2Layer: 已添加区域2图层', area2VectorLayer);
|
||||
} catch (e) {
|
||||
console.error('loadArea2Layer: 加载或添加区域2图层失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除区域2图层
|
||||
function removeArea2Layer() {
|
||||
if (!props.map) return;
|
||||
let removed = false;
|
||||
// 获取所有图层,查找类型为 VectorLayer 且 zIndex 为 4 的(即区域2图层)
|
||||
const layers = props.map.getLayers().getArray();
|
||||
for (let i = layers.length - 1; i >= 0; i--) {
|
||||
const lyr = layers[i];
|
||||
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 4) {
|
||||
props.map.removeLayer(lyr);
|
||||
removed = true;
|
||||
console.log('removeArea2Layer: 已移除区域2图层', lyr);
|
||||
}
|
||||
}
|
||||
area2VectorLayer = null;
|
||||
if (!removed) {
|
||||
console.log('removeArea2Layer: 没有找到可移除的区域2图层');
|
||||
}
|
||||
}
|
||||
|
||||
// 监听区域1显示状态变化
|
||||
watch(showArea1Layer, (val) => {
|
||||
console.log('showArea1Layer变化:', val);
|
||||
if (val) {
|
||||
if (area1VectorLayer) {
|
||||
area1VectorLayer.setVisible(true);
|
||||
} else {
|
||||
loadArea1Layer();
|
||||
}
|
||||
} else if (area1VectorLayer) {
|
||||
area1VectorLayer.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听区域2显示状态变化
|
||||
watch(showArea2Layer, (val) => {
|
||||
console.log('showArea2Layer变化:', val);
|
||||
if (val) {
|
||||
if (area2VectorLayer) {
|
||||
area2VectorLayer.setVisible(true);
|
||||
} else {
|
||||
loadArea2Layer();
|
||||
}
|
||||
} else if (area2VectorLayer) {
|
||||
area2VectorLayer.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
removeRoadLayer();
|
||||
removeCustomRoadLayer();
|
||||
removeArea1Layer();
|
||||
removeArea2Layer();
|
||||
stopFenceFlashing();
|
||||
});
|
||||
|
||||
|
||||
@ -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 DECELERATION = 0.25; // 减速度系数
|
||||
const MAX_TURN_RATE = 100; // 最大转向速率(度/秒)
|
||||
const MIN_TURN_RATE = 20; // 最小转向速率(度/秒)
|
||||
const INERTIA_FACTOR = 0.97; // 从0.95增加到0.97,增强惯性效果
|
||||
const PREDICTION_STRENGTH = 6; // 从8降低到6,减少预测激进性
|
||||
const SPEED_SMOOTHING = 0.94; // 从0.92增加到0.94,增强速度平滑效果
|
||||
const POSITION_SMOOTHING = 0.12; // 从0.15降低到0.12,增强位置平滑效果
|
||||
const MIN_MOVE_THRESHOLD = 0.00005; // 从0.0001降低到0.00005,确保更持续的移动
|
||||
const CONTINUOUS_MOVEMENT = true; // 启用连续移动模式
|
||||
const MIN_SPEED = 2.0; // 从1.5增加到2.0,提高最低保持速度
|
||||
const PREDICTION_DECAY_RATE = 0.998; // 从0.995进一步增加到0.998,减缓预测衰减
|
||||
const PATH_PREDICTION_ENABLED = true; // 启用路径预测
|
||||
const PATH_PREDICTION_POINTS = 16; // 从12增加到16,使用更多历史点
|
||||
const CONTINUOUS_MOVEMENT_THRESHOLD = 500; // 从1000ms降低到500ms,更快触发连续移动
|
||||
const INERTIA_FACTOR = 0.99; // 从0.985增加到0.99,进一步增强惯性效果,减少抖动
|
||||
const PREDICTION_STRENGTH = 3; // 从4降低到3,进一步减少预测激进性,避免过度预测造成抖动
|
||||
const SPEED_SMOOTHING = 0.98; // 从0.97增加到0.98,进一步增强速度平滑效果
|
||||
const POSITION_SMOOTHING = 0.06; // 从0.08降低到0.06,进一步减小单次位置变化幅度,增强平滑度
|
||||
const MIN_MOVE_THRESHOLD = 0.000005; // 从0.00001降低到0.000005,确保更平滑的微小移动
|
||||
const CONTINUOUS_MOVEMENT = true; // 保持启用连续移动模式
|
||||
const MIN_SPEED = 0.5; // 从1.0降低到0.5,进一步降低最低保持速度,减少抖动
|
||||
const PREDICTION_DECAY_RATE = 0.9995; // 从0.999增加到0.9995,进一步减缓预测衰减
|
||||
const PATH_PREDICTION_ENABLED = true; // 保持启用路径预测
|
||||
const PATH_PREDICTION_POINTS = 15; // 从20降低到15,减少计算量
|
||||
const CONTINUOUS_MOVEMENT_THRESHOLD = 200; // 从300ms降低到200ms,更快触发连续移动,减少停顿感
|
||||
const POSITION_UPDATE_THRESHOLD = 3; // 从5降低到3,进一步减少位置更新阈值
|
||||
|
||||
// 缓动函数 - 平滑的加减速
|
||||
function easeInOutQuad(t) {
|
||||
@ -99,14 +98,13 @@ function updateVehicleAnimations(deltaTime) {
|
||||
|
||||
// 1. 计算目标位置(基于预测)
|
||||
let targetPosition = [...animData.targetPosition];
|
||||
let targetHeading = animData.targetHeading;
|
||||
|
||||
// 平滑速度过渡 - 当前速度逐渐接近目标速度
|
||||
if (animData.currentSpeed === undefined) {
|
||||
animData.currentSpeed = animData.speed;
|
||||
} else {
|
||||
// 使用更平滑的速度过渡
|
||||
const speedSmoothingFactor = vehicle.speedViolation ? 0.96 : SPEED_SMOOTHING;
|
||||
const speedSmoothingFactor = vehicle.speedViolation ? 0.98 : SPEED_SMOOTHING;
|
||||
animData.currentSpeed = animData.currentSpeed * speedSmoothingFactor +
|
||||
animData.speed * (1 - speedSmoothingFactor);
|
||||
}
|
||||
@ -117,10 +115,10 @@ function updateVehicleAnimations(deltaTime) {
|
||||
if (timeSinceLastUpdate > PREDICTION_THRESHOLD && animData.predictionVector) {
|
||||
// 计算预测因子,随时间增长但有上限
|
||||
const predictTime = Math.min(timeSinceLastUpdate, MAX_PREDICTION_TIME);
|
||||
const predictFactor = (predictTime - PREDICTION_THRESHOLD) / 1000 * 0.8;
|
||||
const predictFactor = (predictTime - PREDICTION_THRESHOLD) / 1000 * 0.6; // 从0.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 = [
|
||||
@ -134,7 +132,7 @@ function updateVehicleAnimations(deltaTime) {
|
||||
// 预测时逐渐降低速度,但不要完全停止
|
||||
if (predictTime > 2000) {
|
||||
// 长时间无更新,缓慢降低速度,但保持最低速度
|
||||
const slowdownFactor = Math.max(0.5, 1 - (predictTime - 2000) / 10000);
|
||||
const slowdownFactor = Math.max(0.6, 1 - (predictTime - 2000) / 15000); // 从0.5/10000调整为0.6/15000,减缓减速率
|
||||
currentSpeed = Math.max(currentSpeed * slowdownFactor, MIN_SPEED); // 确保最低速度不为0
|
||||
}
|
||||
}
|
||||
@ -144,6 +142,11 @@ function updateVehicleAnimations(deltaTime) {
|
||||
const dy = targetPosition[1] - animData.position[1];
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 忽略过小的位置更新,减少抖动
|
||||
if (distance < MIN_MOVE_THRESHOLD && !CONTINUOUS_MOVEMENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 如果距离足够远或启用了连续移动,应用平滑移动
|
||||
if (distance > MIN_MOVE_THRESHOLD || CONTINUOUS_MOVEMENT) {
|
||||
// 计算理想移动距离(基于速度和时间)
|
||||
@ -153,11 +156,11 @@ function updateVehicleAnimations(deltaTime) {
|
||||
let moveRatio;
|
||||
|
||||
// 超速车辆使用更平滑的移动
|
||||
const positionSmoothingFactor = vehicle.speedViolation ? POSITION_SMOOTHING * 0.8 : POSITION_SMOOTHING;
|
||||
const positionSmoothingFactor = vehicle.speedViolation ? POSITION_SMOOTHING * 0.7 : POSITION_SMOOTHING;
|
||||
|
||||
if (distance < idealDistance * 1.5) {
|
||||
if (distance < idealDistance * 1.2) { // 从1.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);
|
||||
} else {
|
||||
// 正常行驶时使用标准缓动
|
||||
@ -169,7 +172,7 @@ function updateVehicleAnimations(deltaTime) {
|
||||
// 如果距离很小且有预测向量,使用预测向量进行微小移动
|
||||
if (distance < MIN_MOVE_THRESHOLD && animData.predictionVector) {
|
||||
// 使用预测向量作为移动方向,但使用很小的移动量
|
||||
const minMoveAmount = (currentSpeed * 1000 / 3600) * (deltaTime / 1000) * 0.1;
|
||||
const minMoveAmount = (currentSpeed * 1000 / 3600) * (deltaTime / 1000) * 0.05; // 从0.1降低到0.05,减小微小移动幅度
|
||||
|
||||
// 归一化预测向量
|
||||
const predLen = Math.sqrt(
|
||||
@ -194,7 +197,7 @@ function updateVehicleAnimations(deltaTime) {
|
||||
else {
|
||||
if (animData.lastDx !== undefined && animData.lastDy !== undefined) {
|
||||
// 超速车辆使用更强的惯性
|
||||
const inertiaFactor = vehicle.speedViolation ? INERTIA_FACTOR * 1.1 : INERTIA_FACTOR;
|
||||
const inertiaFactor = vehicle.speedViolation ? INERTIA_FACTOR * 1.05 : INERTIA_FACTOR; // 从1.1降低到1.05,减少超速车辆惯性过强
|
||||
|
||||
const inertiaX = animData.lastDx * inertiaFactor;
|
||||
const inertiaY = animData.lastDy * inertiaFactor;
|
||||
@ -204,8 +207,8 @@ function updateVehicleAnimations(deltaTime) {
|
||||
const newDy = dy * moveRatio;
|
||||
|
||||
// 根据速度和距离调整惯性影响
|
||||
const inertiaWeight = Math.min(0.85, currentSpeed / 70) *
|
||||
Math.min(1.0, distance / 8); // 调整惯性权重计算
|
||||
const inertiaWeight = Math.min(0.75, currentSpeed / 80) * // 从0.85/70降低到0.75/80,减少惯性权重
|
||||
Math.min(0.9, distance / 10); // 从1.0/8调整为0.9/10,减少短距离惯性
|
||||
|
||||
// 计算新位置(混合惯性和目标方向)
|
||||
const nextX = animData.position[0] + newDx * (1 - inertiaWeight) + inertiaX * inertiaWeight;
|
||||
@ -238,69 +241,18 @@ function updateVehicleAnimations(deltaTime) {
|
||||
if (props.vehicles[id]) {
|
||||
props.vehicles[id].position = animData.position;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算实际移动方向(用于更自然的转向)
|
||||
if (Math.abs(animData.lastDx) > 0.001 || Math.abs(animData.lastDy) > 0.001) {
|
||||
const movementHeading = (Math.atan2(animData.lastDy, animData.lastDx) * 180 / Math.PI + 90) % 360;
|
||||
// 更新车辆样式,传递正确的heading值
|
||||
if (feature && props.getVehicleStyle) {
|
||||
// 获取车辆的当前heading值
|
||||
const vehicle = props.vehicles[id];
|
||||
const currentHeading = vehicle ? vehicle.heading : 0;
|
||||
|
||||
// 平滑过渡到新的移动方向
|
||||
if (animData.movementHeading === undefined) {
|
||||
animData.movementHeading = movementHeading;
|
||||
} else {
|
||||
// 计算角度差,确保走最短路径
|
||||
const headingDiff = ((movementHeading - animData.movementHeading + 540) % 360) - 180;
|
||||
animData.movementHeading = (animData.movementHeading + headingDiff * 0.2 + 360) % 360;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 平滑转向逻辑 - 考虑移动方向和目标方向
|
||||
// 首先确定最佳目标方向(结合目标朝向和实际移动方向)
|
||||
let bestTargetHeading = targetHeading;
|
||||
|
||||
// 如果有移动方向且速度足够高,部分考虑移动方向
|
||||
if (animData.movementHeading !== undefined && currentSpeed > 5) {
|
||||
// 速度越高,移动方向的权重越大
|
||||
const movementWeight = Math.min(0.6, currentSpeed / 70);
|
||||
|
||||
// 计算角度差,确保选择最短路径
|
||||
const headingDiff = ((animData.movementHeading - targetHeading + 540) % 360) - 180;
|
||||
|
||||
// 如果移动方向和目标方向相差不大,部分采用移动方向
|
||||
if (Math.abs(headingDiff) < 100) {
|
||||
bestTargetHeading = targetHeading + headingDiff * movementWeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用平滑转向
|
||||
const headingDiff = bestTargetHeading - animData.heading;
|
||||
if (Math.abs(headingDiff) > 0.1) {
|
||||
// 归一化角度差值(确保走最短路径)
|
||||
const normalizedDiff = ((headingDiff + 180) % 360) - 180;
|
||||
|
||||
// 基于速度动态调整转向速率
|
||||
// 低速时转向快,高速时转向慢
|
||||
const turnRate = MAX_TURN_RATE - (MAX_TURN_RATE - MIN_TURN_RATE) * Math.min(1, currentSpeed / 60);
|
||||
|
||||
// 超速车辆转向更平滑
|
||||
const actualTurnRate = vehicle.speedViolation ? turnRate * 0.8 : turnRate;
|
||||
|
||||
// 计算本帧转向量
|
||||
const turnAmount = Math.sign(normalizedDiff) *
|
||||
Math.min(Math.abs(normalizedDiff), actualTurnRate * deltaTime / 1000);
|
||||
|
||||
// 应用转向
|
||||
animData.heading += turnAmount;
|
||||
animData.heading = (animData.heading + 360) % 360;
|
||||
|
||||
// 更新车辆样式(朝向)
|
||||
const feature = props.vehicleSource.getFeatureById(id);
|
||||
if (feature && props.getVehicleStyle) {
|
||||
// 减少样式更新频率,避免闪烁
|
||||
if (!animData.lastStyleUpdateTime || currentTime - animData.lastStyleUpdateTime > 500) {
|
||||
feature.setStyle(props.getVehicleStyle(id, currentSpeed, animData.heading));
|
||||
animData.lastStyleUpdateTime = currentTime;
|
||||
// 只在heading发生变化时才更新样式,避免频繁更新导致闪烁
|
||||
if (!animData.lastHeading || Math.abs(animData.lastHeading - currentHeading) > 1) {
|
||||
feature.setStyle(props.getVehicleStyle(id, currentSpeed, currentHeading));
|
||||
animData.lastHeading = currentHeading;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -368,22 +320,45 @@ function ensureContinuousMovement(animData, vehicle, currentTime, deltaTime) {
|
||||
];
|
||||
}
|
||||
}
|
||||
// 如果没有预测向量或预测向量太小,创建一个基于当前朝向的预测向量
|
||||
// 如果没有预测向量或预测向量太小,创建一个基于历史数据的预测向量
|
||||
else if (!animData.predictionVector ||
|
||||
(Math.abs(animData.predictionVector[0]) < 0.001 && Math.abs(animData.predictionVector[1]) < 0.001)) {
|
||||
|
||||
// 根据车辆朝向创建预测向量
|
||||
const headingRad = (animData.heading - 90) * Math.PI / 180;
|
||||
const predX = Math.cos(headingRad) * animData.currentSpeed * 0.18; // 从0.15增加到0.18
|
||||
const predY = Math.sin(headingRad) * animData.currentSpeed * 0.18;
|
||||
// 使用最近两个历史点计算预测向量
|
||||
if (animData.pathHistory && animData.pathHistory.length >= 2) {
|
||||
const latestPoints = animData.pathHistory.slice(-2);
|
||||
const dx = latestPoints[1].position[0] - latestPoints[0].position[0];
|
||||
const dy = latestPoints[1].position[1] - latestPoints[0].position[1];
|
||||
const len = Math.sqrt(dx*dx + dy*dy);
|
||||
|
||||
animData.predictionVector = [predX, predY];
|
||||
if (len > 0.001) {
|
||||
const normalizedDx = dx / len;
|
||||
const normalizedDy = dy / len;
|
||||
const predX = normalizedDx * animData.currentSpeed * 0.18;
|
||||
const predY = normalizedDy * animData.currentSpeed * 0.18;
|
||||
animData.predictionVector = [predX, predY];
|
||||
|
||||
// 更新目标位置为当前位置加上预测向量
|
||||
animData.targetPosition = [
|
||||
animData.position[0] + predX * 15, // 从12增加到15
|
||||
animData.position[1] + predY * 15
|
||||
];
|
||||
// 更新目标位置为当前位置加上预测向量
|
||||
animData.targetPosition = [
|
||||
animData.position[0] + predX * 15,
|
||||
animData.position[1] + predY * 15
|
||||
];
|
||||
} else {
|
||||
// 如果历史点距离太近,使用默认的前进向量
|
||||
animData.predictionVector = [0, animData.currentSpeed * 0.18];
|
||||
animData.targetPosition = [
|
||||
animData.position[0],
|
||||
animData.position[1] + animData.currentSpeed * 0.18 * 15
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// 没有足够的历史数据,使用默认的前进向量
|
||||
animData.predictionVector = [0, animData.currentSpeed * 0.18];
|
||||
animData.targetPosition = [
|
||||
animData.position[0],
|
||||
animData.position[1] + animData.currentSpeed * 0.18 * 15
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// 预测向量存在但需要缓慢衰减,避免突然停止
|
||||
// 使用更高的衰减率
|
||||
@ -488,15 +463,15 @@ function initVehicleAnimation(id, coordinates, heading, speed) {
|
||||
vehicleAnimations.value[id] = {
|
||||
position: [...coordinates],
|
||||
targetPosition: [...coordinates],
|
||||
heading: heading,
|
||||
targetHeading: heading,
|
||||
heading: heading, // 保存heading值
|
||||
targetHeading: heading, // 保存目标heading值
|
||||
speed: speed,
|
||||
currentSpeed: speed,
|
||||
lastUpdated: Date.now(),
|
||||
predictionVector: null,
|
||||
lastDx: 0,
|
||||
lastDy: 0,
|
||||
movementHeading: heading,
|
||||
lastHeading: heading, // 添加lastHeading字段用于跟踪heading变化
|
||||
// 添加速度历史记录用于平滑过渡
|
||||
speedHistory: [speed, speed, speed],
|
||||
pathHistory: [], // 初始化路径历史
|
||||
@ -516,7 +491,7 @@ function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
|
||||
vehicleMotionHistory.value[id].push({
|
||||
time: now,
|
||||
position: coordinates,
|
||||
heading: heading,
|
||||
heading: heading, // 保存正确的heading值
|
||||
speed: speed
|
||||
});
|
||||
|
||||
@ -620,7 +595,8 @@ function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
|
||||
vehicleAnimations.value[id] = {
|
||||
...animData,
|
||||
targetPosition: coordinates,
|
||||
targetHeading: heading,
|
||||
targetHeading: heading, // 保存目标heading值
|
||||
heading: animData.heading || heading, // 保留当前heading,如果不存在则使用新的heading
|
||||
position: animData.position || coordinates,
|
||||
speed: smoothedSpeed, // 使用平滑后的速度
|
||||
lastUpdated: now,
|
||||
@ -648,15 +624,15 @@ function resetAnimations() {
|
||||
vehicleAnimations.value[id] = {
|
||||
position: [...vehicle.position],
|
||||
targetPosition: [...vehicle.position],
|
||||
heading: vehicle.heading,
|
||||
targetHeading: vehicle.heading,
|
||||
heading: vehicle.heading || 0, // 使用车辆的heading值
|
||||
targetHeading: vehicle.heading || 0, // 使用车辆的heading值
|
||||
speed: vehicle.speed,
|
||||
currentSpeed: vehicle.speed,
|
||||
lastUpdated: Date.now(),
|
||||
predictionVector: null,
|
||||
lastDx: 0,
|
||||
lastDy: 0,
|
||||
movementHeading: vehicle.heading,
|
||||
lastHeading: vehicle.heading || 0, // 使用车辆的heading值
|
||||
speedHistory: [vehicle.speed, vehicle.speed, vehicle.speed],
|
||||
pathHistory: [], // 重置路径历史
|
||||
lastPathRecordTime: Date.now() // 重置路径记录时间
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<!-- 标签系统不需要模板内容 -->
|
||||
<!-- 标签容器,不在模板中渲染任何内容,标签由JS动态创建 -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Overlay } from 'ol';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
@ -12,288 +12,462 @@ const props = defineProps({
|
||||
vehicles: Object
|
||||
});
|
||||
|
||||
// 导入背景图片
|
||||
import labelBg from '../../../assets/images/label_bg.png';
|
||||
import airportBg from '../../../assets/images/airport_bg.png';
|
||||
import airportOutBg from '../../../assets/images/airport_out.png';
|
||||
import alarmBg from '../../../assets/images/alarm_bg.png';
|
||||
import warningBg from '../../../assets/images/warning_bg.png';
|
||||
// 存储标签覆盖物
|
||||
const vehicleLabels = ref({});
|
||||
// 添加标签更新时间记录
|
||||
const labelUpdateTimes = ref({});
|
||||
// 定义更新间隔常量(毫秒)
|
||||
const UPDATE_INTERVAL = 500; // 每500ms更新一次标签
|
||||
|
||||
// 更新车辆标签
|
||||
function updateVehicleLabel(id, position, speed, violationInfo = null) {
|
||||
if (!props.map || !props.vehicles[id]) return;
|
||||
// 创建或更新车辆标签
|
||||
function updateVehicleLabel(vehicleId, coordinates, speed, alertInfo) {
|
||||
if (!props.map) return;
|
||||
|
||||
// 先移除可能存在的旧标签,确保不会重复显示
|
||||
removeVehicleLabel(id);
|
||||
// 获取当前时间
|
||||
const now = Date.now();
|
||||
|
||||
const vehicle = props.vehicles[id];
|
||||
const isAircraftIn = vehicle.isAircraftIn;
|
||||
const isAircraftOut = vehicle.isAircraftOut;
|
||||
const isUnmannedVehicle = vehicle.isUnmannedVehicle;
|
||||
const isSpecialVehicle = vehicle.isSpecialVehicle;
|
||||
const isShuttleVehicle = vehicle.isShuttleVehicle;
|
||||
const violationType = vehicle.violationType;
|
||||
const hasInfoStatus = vehicle.info;
|
||||
const hasWarningStatus = vehicle.warning;
|
||||
const hasAlarmStatus = vehicle.alarm;
|
||||
const hasCriticalStatus = vehicle.critical;
|
||||
const hasSpeedViolation = vehicle.speedViolation;
|
||||
// 检查是否满足更新频率限制
|
||||
// 如果这是首次更新,或者距离上次更新已经过去了足够的时间
|
||||
if (!labelUpdateTimes.value[vehicleId] ||
|
||||
(now - labelUpdateTimes.value[vehicleId]) > UPDATE_INTERVAL) {
|
||||
|
||||
let typeKey = vehicle.type;
|
||||
if (isAircraftIn) typeKey = 'AIRCRAFT_IN';
|
||||
else if (isAircraftOut) typeKey = 'AIRCRAFT_OUT';
|
||||
else if (isUnmannedVehicle) typeKey = 'UNMANNED_VEHICLE';
|
||||
else if (isSpecialVehicle) typeKey = 'SPECIAL_VEHICLE';
|
||||
else if (isShuttleVehicle) typeKey = 'SHUTTLE_VEHICLE';
|
||||
// 检查标签是否已存在
|
||||
if (vehicleLabels.value[vehicleId]) {
|
||||
// 更新现有标签位置
|
||||
vehicleLabels.value[vehicleId].setPosition(coordinates);
|
||||
|
||||
let backgroundImage;
|
||||
// 更新标签内容
|
||||
updateLabelContent(vehicleId, speed, alertInfo);
|
||||
} else {
|
||||
// 创建新标签
|
||||
createNewLabel(vehicleId, coordinates, 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 {
|
||||
// 默认背景
|
||||
backgroundImage = labelBg;
|
||||
// 更新最后更新时间
|
||||
labelUpdateTimes.value[vehicleId] = now;
|
||||
}
|
||||
}
|
||||
|
||||
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' : ''}`;
|
||||
// 创建新标签
|
||||
function createNewLabel(vehicleId, coordinates, speed, alertInfo) {
|
||||
// 创建标签DOM元素
|
||||
const labelElement = document.createElement('div');
|
||||
labelElement.className = 'vehicle-label';
|
||||
labelElement.id = `label-${vehicleId}`;
|
||||
|
||||
// 添加告警状态类
|
||||
if (violationType) labelDiv.className += ' vehicle-violation';
|
||||
if (hasInfoStatus) labelDiv.className += ' vehicle-info';
|
||||
if (hasWarningStatus) labelDiv.className += ' vehicle-warning';
|
||||
if (hasAlarmStatus) labelDiv.className += ' vehicle-alarm';
|
||||
if (hasCriticalStatus) labelDiv.className += ' vehicle-critical';
|
||||
if (hasSpeedViolation) labelDiv.className += ' vehicle-speed-violation';
|
||||
|
||||
let labelText = '';
|
||||
|
||||
// 超速车辆特殊处理标签内容 - 只显示超速相关信息,避免闪烁和重复显示
|
||||
if (violationInfo && violationInfo.isSpeedViolation && violationInfo.actualValue !== undefined) {
|
||||
// 通过violationInfo传入的超速信息 - 这是主要的超速状态显示方式
|
||||
labelText = `${id} ${violationInfo.actualValue.toFixed(1)} km/h`;
|
||||
if (violationInfo.description) {
|
||||
labelText += `<br>${violationInfo.description}`;
|
||||
}
|
||||
if (violationInfo.limitValue !== undefined) {
|
||||
labelText += `<br>限速: ${violationInfo.limitValue}km/h`;
|
||||
}
|
||||
if (violationInfo.ruleName) {
|
||||
labelText += `<br>根据${violationInfo.ruleName}请减速慢行!`;
|
||||
}
|
||||
} else if (hasSpeedViolation && vehicle.limitValue !== undefined && vehicle.actualValue !== undefined) {
|
||||
// 备用的超速状态显示方式
|
||||
labelText = `${id} ${vehicle.actualValue.toFixed(1)} km/h`;
|
||||
if (vehicle.description) {
|
||||
labelText += `<br>${vehicle.description}`;
|
||||
}
|
||||
labelText += `<br>限速: ${vehicle.limitValue}km/h`;
|
||||
if (vehicle.ruleName) {
|
||||
labelText += `<br>根据${vehicle.ruleName}请减速慢行!`;
|
||||
}
|
||||
} else {
|
||||
// 正常车辆显示 - 避免显示0.00km/h的闪烁
|
||||
if (!hasSpeedViolation && speed > 0.1) {
|
||||
labelText = `${id} ${speed.toFixed(1)} km/h`;
|
||||
} else if (!hasSpeedViolation) {
|
||||
// 如果速度为0或很小,只显示车辆ID,不显示速度避免闪烁
|
||||
labelText = `${id}`;
|
||||
}
|
||||
|
||||
if (violationType && vehicle.description) {
|
||||
labelText += `<br>${vehicle.description}`;
|
||||
}
|
||||
|
||||
if (violationType && vehicle.limitValue !== undefined && vehicle.actualValue !== undefined) {
|
||||
labelText += `<br>限速: ${vehicle.limitValue}km/h 实际: ${vehicle.actualValue}km/h`;
|
||||
}
|
||||
|
||||
// 处理传入的违规信息(非超速情况)
|
||||
if (violationInfo && !violationInfo.isSpeedViolation) {
|
||||
if (violationInfo.description) {
|
||||
labelText += `<br>${violationInfo.description}`;
|
||||
}
|
||||
if (violationInfo.limitValue !== undefined && violationInfo.actualValue !== undefined) {
|
||||
labelText += `<br>限速: ${violationInfo.limitValue}km/h 实际: ${violationInfo.actualValue}km/h`;
|
||||
}
|
||||
if (violationInfo.ruleName) {
|
||||
labelText += `<br>根据${violationInfo.ruleName}请减速慢行!`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
labelDiv.innerHTML = labelText;
|
||||
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
|
||||
labelDiv.style.backgroundSize = '100% 100%';
|
||||
labelDiv.style.padding = '5px 10px';
|
||||
labelDiv.style.color = '#fff';
|
||||
// 设置标签内容
|
||||
const labelContent = createLabelContent(vehicleId, speed, alertInfo);
|
||||
labelElement.innerHTML = labelContent;
|
||||
|
||||
// 创建覆盖物
|
||||
const overlay = new Overlay({
|
||||
element: labelDiv,
|
||||
position: position,
|
||||
element: labelElement,
|
||||
position: coordinates,
|
||||
offset: [0, -30], // 偏移量,使标签位于图标上方
|
||||
positioning: 'bottom-center',
|
||||
offset: [0, -30],
|
||||
stopEvent: false,
|
||||
insertFirst: true, // 确保在DOM中优先插入
|
||||
autoPan: false, // 禁用自动平移
|
||||
});
|
||||
|
||||
// 更新引用
|
||||
props.vehicles[id].overlay = overlay;
|
||||
props.vehicles[id].labelDiv = labelDiv;
|
||||
|
||||
// 添加到地图
|
||||
// 将覆盖物添加到地图
|
||||
props.map.addOverlay(overlay);
|
||||
|
||||
// 存储覆盖物引用
|
||||
vehicleLabels.value[vehicleId] = overlay;
|
||||
|
||||
// 添加点击事件监听器
|
||||
setupTabClickListeners(vehicleId);
|
||||
}
|
||||
|
||||
// 更新标签内容
|
||||
function updateLabelContent(vehicleId, speed, alertInfo) {
|
||||
const labelElement = document.getElementById(`label-${vehicleId}`);
|
||||
if (!labelElement) return;
|
||||
|
||||
// 更新标签内容
|
||||
const labelContent = createLabelContent(vehicleId, speed, alertInfo);
|
||||
labelElement.innerHTML = labelContent;
|
||||
|
||||
// 重新设置点击事件监听器
|
||||
setupTabClickListeners(vehicleId);
|
||||
}
|
||||
|
||||
// 创建标签内容
|
||||
function createLabelContent(vehicleId, speed, alertInfo = {}) {
|
||||
// 获取车辆信息
|
||||
const vehicle = props.vehicles[vehicleId];
|
||||
if (!vehicle) return '';
|
||||
|
||||
// 格式化速度,保留1位小数
|
||||
const formattedSpeed = speed ? `${speed.toFixed(1)}km/h` : '0.0km/h';
|
||||
|
||||
// 检查是否存在各类告警
|
||||
const hasWarning = vehicle.warning || (alertInfo && alertInfo.isWarning);
|
||||
const hasAlarm = vehicle.alarm || (alertInfo && alertInfo.isAlarm);
|
||||
const hasSpeedViolation = vehicle.speedViolation || (alertInfo && alertInfo.isSpeedViolation);
|
||||
const hasUnauthorizedEntry = vehicle.critical || (alertInfo && alertInfo.isUnauthorizedEntry);
|
||||
|
||||
// 检查是否有任何告警
|
||||
const hasAnyAlert = hasWarning || hasAlarm || hasSpeedViolation || hasUnauthorizedEntry;
|
||||
|
||||
// 如果没有任何告警,只显示车牌号和车速
|
||||
if (!hasAnyAlert) {
|
||||
return `
|
||||
<div class="label-container simple-label">
|
||||
<div class="label-header">
|
||||
<span class="vehicle-id">${vehicleId}</span>
|
||||
<span class="vehicle-speed">${formattedSpeed}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 有告警时,显示完整的Tab栏
|
||||
let html = `
|
||||
<div class="label-container">
|
||||
<div class="label-header">
|
||||
<span class="vehicle-id">${vehicleId}</span>
|
||||
<span class="vehicle-speed">${formattedSpeed}</span>
|
||||
</div>
|
||||
<div class="label-tabs">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-button ${hasWarning ? 'active' : 'disabled'}" data-tab="warning" data-vehicle="${vehicleId}">
|
||||
<i class="tab-icon warning-icon"></i>
|
||||
</button>
|
||||
<button class="tab-button ${hasAlarm ? 'active' : 'disabled'}" data-tab="alarm" data-vehicle="${vehicleId}">
|
||||
<i class="tab-icon alarm-icon"></i>
|
||||
</button>
|
||||
<button class="tab-button ${hasSpeedViolation ? 'active' : 'disabled'}" data-tab="speed" data-vehicle="${vehicleId}">
|
||||
<i class="tab-icon speed-icon"></i>
|
||||
</button>
|
||||
<button class="tab-button ${hasUnauthorizedEntry ? 'active' : 'disabled'}" data-tab="boundary" data-vehicle="${vehicleId}">
|
||||
<i class="tab-icon boundary-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
`;
|
||||
|
||||
// 默认显示第一个有内容的标签页
|
||||
let activeTabContent = '';
|
||||
let activeTabFound = false;
|
||||
|
||||
// 冲突预警内容
|
||||
if (hasWarning) {
|
||||
const warningMessage = alertInfo?.description || '与其他车辆距离过近,请注意避让!';
|
||||
activeTabContent = `
|
||||
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="warning" data-vehicle="${vehicleId}">
|
||||
<div class="alert-message">${warningMessage}</div>
|
||||
</div>
|
||||
`;
|
||||
activeTabFound = true;
|
||||
}
|
||||
|
||||
// 冲突告警内容
|
||||
if (hasAlarm) {
|
||||
const alarmMessage = alertInfo?.description || '与其他车辆存在冲突风险!';
|
||||
activeTabContent += `
|
||||
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="alarm" data-vehicle="${vehicleId}">
|
||||
<div class="alert-message">${alarmMessage}</div>
|
||||
</div>
|
||||
`;
|
||||
if (!activeTabFound) activeTabFound = true;
|
||||
}
|
||||
|
||||
// 超速告警内容
|
||||
if (hasSpeedViolation) {
|
||||
const limitValue = alertInfo?.limitValue || vehicle.limitValue || 30;
|
||||
const actualValue = alertInfo?.actualValue || vehicle.actualValue || speed;
|
||||
activeTabContent += `
|
||||
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="speed" data-vehicle="${vehicleId}">
|
||||
<div class="speed-info">
|
||||
<div>规定速度: <span class="limit-value">${limitValue}km/h</span></div>
|
||||
<div>当前速度: <span class="actual-value">${actualValue.toFixed(1)}km/h</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
if (!activeTabFound) activeTabFound = true;
|
||||
}
|
||||
|
||||
// 越界告警内容
|
||||
if (hasUnauthorizedEntry) {
|
||||
const boundaryMessage = alertInfo?.description || '车辆已进入禁行区域!';
|
||||
activeTabContent += `
|
||||
<div class="tab-pane ${!activeTabFound ? 'active' : ''}" data-tab-content="boundary" data-vehicle="${vehicleId}">
|
||||
<div class="alert-message">${boundaryMessage}</div>
|
||||
</div>
|
||||
`;
|
||||
if (!activeTabFound) activeTabFound = true;
|
||||
}
|
||||
|
||||
html += activeTabContent;
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 设置标签页点击事件监听器
|
||||
function setupTabClickListeners(vehicleId) {
|
||||
setTimeout(() => {
|
||||
const tabButtons = document.querySelectorAll(`.tab-button[data-vehicle="${vehicleId}"]`);
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
// 移除现有事件监听器
|
||||
button.removeEventListener('click', handleTabClick);
|
||||
|
||||
// 添加新的事件监听器
|
||||
button.addEventListener('click', handleTabClick);
|
||||
});
|
||||
|
||||
// 默认选中第一个非禁用的标签页
|
||||
const firstActiveButton = document.querySelector(`.tab-button.active[data-vehicle="${vehicleId}"]`);
|
||||
if (firstActiveButton && !firstActiveButton.classList.contains('current')) {
|
||||
firstActiveButton.click();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 处理标签页点击事件
|
||||
function handleTabClick(event) {
|
||||
const button = event.currentTarget;
|
||||
const tabName = button.getAttribute('data-tab');
|
||||
const vehicleId = button.getAttribute('data-vehicle');
|
||||
|
||||
// 如果按钮被禁用,不执行任何操作
|
||||
if (button.classList.contains('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有相关元素
|
||||
const tabButtons = document.querySelectorAll(`.tab-button[data-vehicle="${vehicleId}"]`);
|
||||
const tabPanes = document.querySelectorAll(`[data-tab-content][data-vehicle="${vehicleId}"]`);
|
||||
|
||||
// 移除所有活动状态
|
||||
tabButtons.forEach(btn => btn.classList.remove('current'));
|
||||
tabPanes.forEach(pane => pane.classList.remove('active'));
|
||||
|
||||
// 设置当前活动状态
|
||||
button.classList.add('current');
|
||||
const activePane = document.querySelector(`[data-tab-content="${tabName}"][data-vehicle="${vehicleId}"]`);
|
||||
if (activePane) {
|
||||
activePane.classList.add('active');
|
||||
}
|
||||
|
||||
// 阻止事件冒泡,避免触发地图点击事件
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// 移除车辆标签
|
||||
function removeVehicleLabel(id) {
|
||||
if (!props.map || !props.vehicles[id]) return;
|
||||
function removeVehicleLabel(vehicleId) {
|
||||
if (vehicleLabels.value[vehicleId]) {
|
||||
props.map.removeOverlay(vehicleLabels.value[vehicleId]);
|
||||
delete vehicleLabels.value[vehicleId];
|
||||
}
|
||||
}
|
||||
|
||||
if (props.vehicles[id].overlay) {
|
||||
props.map.removeOverlay(props.vehicles[id].overlay);
|
||||
props.vehicles[id].overlay = null;
|
||||
props.vehicles[id].labelDiv = null;
|
||||
// 设置标签可见性
|
||||
function setLabelVisibility(vehicleId, visible) {
|
||||
if (vehicleLabels.value[vehicleId]) {
|
||||
const labelElement = vehicleLabels.value[vehicleId].getElement();
|
||||
if (labelElement) {
|
||||
labelElement.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新所有标签位置
|
||||
function updateAllLabels() {
|
||||
Object.keys(props.vehicles).forEach(id => {
|
||||
const vehicle = props.vehicles[id];
|
||||
if (vehicle.feature) {
|
||||
const coordinates = vehicle.feature.getGeometry().getCoordinates();
|
||||
// 如果车辆处于超速状态,不在这里更新标签,避免与超速状态标签冲突
|
||||
if (!vehicle.speedViolation) {
|
||||
updateVehicleLabel(id, coordinates, vehicle.speed);
|
||||
}
|
||||
if (!props.map || !props.vehicles) return;
|
||||
|
||||
Object.keys(vehicleLabels.value).forEach(vehicleId => {
|
||||
if (props.vehicles[vehicleId] && props.vehicles[vehicleId].position) {
|
||||
vehicleLabels.value[vehicleId].setPosition(props.vehicles[vehicleId].position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设置标签可见性
|
||||
function setLabelVisibility(id, visible) {
|
||||
if (!props.map || !props.vehicles[id]) return;
|
||||
|
||||
if (visible) {
|
||||
// 如果车辆处于超速状态,不在这里更新标签,避免与超速状态标签冲突
|
||||
if (props.vehicles[id].position && !props.vehicles[id].speedViolation) {
|
||||
updateVehicleLabel(id, props.vehicles[id].position, props.vehicles[id].speed);
|
||||
}
|
||||
} else {
|
||||
removeVehicleLabel(id);
|
||||
// 组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
if (props.map) {
|
||||
Object.values(vehicleLabels.value).forEach(overlay => {
|
||||
props.map.removeOverlay(overlay);
|
||||
});
|
||||
}
|
||||
}
|
||||
vehicleLabels.value = {};
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
// 监听地图变化
|
||||
watch(() => props.map, (newMap) => {
|
||||
if (newMap) {
|
||||
// 地图实例变化时,重新创建所有标签
|
||||
Object.keys(vehicleLabels.value).forEach(vehicleId => {
|
||||
if (props.vehicles[vehicleId] && props.vehicles[vehicleId].position) {
|
||||
removeVehicleLabel(vehicleId);
|
||||
updateVehicleLabel(
|
||||
vehicleId,
|
||||
props.vehicles[vehicleId].position,
|
||||
props.vehicles[vehicleId].speed
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 向外暴露方法
|
||||
defineExpose({
|
||||
updateVehicleLabel,
|
||||
removeVehicleLabel,
|
||||
updateAllLabels,
|
||||
setLabelVisibility
|
||||
setLabelVisibility,
|
||||
updateAllLabels
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 车辆标签样式 */
|
||||
:deep(.vehicle-label) {
|
||||
<style>
|
||||
/* 标签容器样式 */
|
||||
.vehicle-label {
|
||||
position: absolute;
|
||||
padding: 4px 8px;
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.label-container {
|
||||
background-color: rgba(37, 37, 37, 0.7);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
min-width: 170px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 简化标签样式 */
|
||||
.simple-label {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
/* 标签头部样式 */
|
||||
.label-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.simple-label .label-header {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.vehicle-id {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vehicle-speed {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 80px;
|
||||
min-height: 24px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 标签页按钮样式 */
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: rgba(80, 80, 80, 0.5);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 30;
|
||||
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
|
||||
will-change: transform, left, top;
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 滑入航空器标签样式 */
|
||||
:deep(.vehicle-aircraft-in) {
|
||||
color: #fff;
|
||||
.tab-button.active {
|
||||
background-color: rgba(52, 152, 219, 0.7);
|
||||
}
|
||||
|
||||
/* 滑出航空器标签样式 */
|
||||
:deep(.vehicle-aircraft-out) {
|
||||
color: #333;
|
||||
.tab-button.current {
|
||||
background-color: rgba(52, 152, 219, 1);
|
||||
box-shadow: 0 0 5px rgba(52, 152, 219, 0.7);
|
||||
}
|
||||
|
||||
/* 无人车标签样式 */
|
||||
:deep(.vehicle-unmanned) {
|
||||
color: #fff;
|
||||
.tab-button.disabled {
|
||||
background-color: rgba(80, 80, 80, 0.3);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 特勤车标签样式 */
|
||||
:deep(.vehicle-special) {
|
||||
color: #fff;
|
||||
.tab-button.disabled .tab-icon {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
/* 摆渡车标签样式 */
|
||||
:deep(.vehicle-shuttle) {
|
||||
color: #fff;
|
||||
/* 标签图标样式 */
|
||||
.tab-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* 告警标签样式 */
|
||||
:deep(.vehicle-alarm) {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
.warning-icon {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FFA500"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>');
|
||||
}
|
||||
|
||||
/* 预警标签样式 */
|
||||
:deep(.vehicle-warning) {
|
||||
color: #fff;
|
||||
.alarm-icon {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FF4D4F"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>');
|
||||
}
|
||||
|
||||
/* 严重违规标签样式 */
|
||||
:deep(.vehicle-critical) {
|
||||
color: #fff;
|
||||
.speed-icon {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FFD700"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
|
||||
}
|
||||
|
||||
/* 超速违规标签样式 */
|
||||
:deep(.vehicle-speed-violation) {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
.boundary-icon {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FF6B6B"><path d="M3 5v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2zm16 14H5V5h14v14z"/></svg>');
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 告警消息样式 */
|
||||
.alert-message {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
/* background-color: rgba(255, 255, 255, 0.1); */
|
||||
}
|
||||
|
||||
/* 信息级别(超速等)标签样式 */
|
||||
:deep(.vehicle-info) {
|
||||
color: #fff;
|
||||
/* 超速信息样式 */
|
||||
.speed-info {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
width: 160px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.limit-value {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.actual-value {
|
||||
color: #FF4D4F;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@ -63,8 +63,21 @@ import VehicleStyleManager from './VehicleStyleManager.vue';
|
||||
|
||||
// 导入默认图标
|
||||
import carIcon from '../../../assets/images/noPeopleCar.png';
|
||||
import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 滑入航空器使用黄色图标
|
||||
import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 滑出航空器使用蓝色图标
|
||||
import aircraftIcon from '../../../assets/images/Aircraft.png'; // 航空器使用Aircraft.png图标
|
||||
// 不再使用不同状态的航空器图标
|
||||
// import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 滑入航空器使用黄色图标
|
||||
// import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 滑出航空器使用蓝色图标
|
||||
|
||||
// 预加载图标资源,确保图标立即可用
|
||||
const carIconImg = new Image();
|
||||
carIconImg.src = carIcon;
|
||||
const aircraftIconImg = new Image();
|
||||
aircraftIconImg.src = aircraftIcon;
|
||||
// 不再预加载不同状态的航空器图标
|
||||
// const aircraftInIconImg = new Image();
|
||||
// aircraftInIconImg.src = aircraftInIcon;
|
||||
// const aircraftOutIconImg = new Image();
|
||||
// aircraftOutIconImg.src = aircraftOutIcon;
|
||||
|
||||
// 为SockJS提供polyfill
|
||||
if (typeof window !== 'undefined' && !window.global) {
|
||||
@ -235,6 +248,10 @@ function updateVehiclePosition(vehicleData) {
|
||||
|
||||
const { object_id, object_type, position, heading, speed } = vehicleData;
|
||||
|
||||
// 计算正确的旋转角度,不再减去72度
|
||||
const rotationRad = (heading * Math.PI) / 180;
|
||||
console.log(`车辆${object_id}的heading值: ${heading}, 计算的旋转角度: ${rotationRad} 弧度, ${rotationRad * 180 / Math.PI} 度`);
|
||||
|
||||
let coordinates;
|
||||
|
||||
coordinates = transform(
|
||||
@ -248,56 +265,67 @@ function updateVehiclePosition(vehicleData) {
|
||||
// 根据object_type正确分类车辆
|
||||
let vehicleType = object_type.toUpperCase();
|
||||
|
||||
// 判断航空器类型
|
||||
// CA开头的航班是滑入航空器,使用黄色Aircraft1.png图标和airport_out.png文本背景
|
||||
// MU开头的航班是滑出航空器,使用蓝色Aircraft.png图标和airport_bg.png文本背景
|
||||
const isAircraftOut = vehicleType === 'AIRCRAFT' && object_id.toLowerCase().includes('ca');
|
||||
const isAircraftIn = vehicleType === 'AIRCRAFT' && object_id.toLowerCase().includes('mu');
|
||||
const isAircraft = vehicleType === 'AIRCRAFT';
|
||||
// 判断车辆类型
|
||||
const isAircraft = vehicleType === 'AIRCRAFT'; // 航空器使用aircraft.png
|
||||
const isUnmannedVehicle = vehicleType === 'UNMANNED_VEHICLE'; // 无人车
|
||||
const isSpecialVehicle = vehicleType === 'SPECIAL_VEHICLE'; // 特种车辆
|
||||
|
||||
// 判断地面车辆类型
|
||||
const isUnmannedVehicle = vehicleType === 'UNMANNED_VEHICLE'; // 无人车,使用noPeopleCar.png图标和label_bg.png文本背景
|
||||
const isSpecialVehicle = vehicleType === 'AIRPORT_VEHICLE'; // 特勤车
|
||||
const isShuttleVehicle = vehicleType === 'SHUTTLE_VEHICLE'; // 摆渡车
|
||||
// 确保heading是有效的数值
|
||||
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||||
|
||||
if (!feature) {
|
||||
// 先创建车辆数据对象,确保在创建feature前就有类型信息
|
||||
vehicles.value[object_id] = {
|
||||
id: object_id,
|
||||
type: object_type,
|
||||
position: coordinates,
|
||||
heading: validHeading,
|
||||
speed: speed,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
lastHeading: validHeading // 初始化lastHeading
|
||||
};
|
||||
|
||||
feature = new Feature({
|
||||
geometry: new Point(coordinates),
|
||||
name: `${object_type} ${object_id}`,
|
||||
type: object_type,
|
||||
speed: speed,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
isSpecialVehicle: isSpecialVehicle
|
||||
});
|
||||
|
||||
feature.setId(object_id);
|
||||
// 使用默认样式,确保styleManager.value存在时才使用它的方法
|
||||
if (styleManager.value) {
|
||||
feature.setStyle(styleManager.value.getVehicleStyle(object_id, speed, heading));
|
||||
|
||||
// 直接根据车辆类型选择正确的图标,不使用默认图标
|
||||
let iconStyle;
|
||||
if (isAircraft) {
|
||||
iconStyle = new Style({
|
||||
image: new Icon({
|
||||
src: aircraftIcon,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad, // 使用计算好的旋转角度
|
||||
})
|
||||
});
|
||||
} else {
|
||||
// 提供一个默认样式,根据车辆类型选择图标
|
||||
feature.setStyle(defaultGetVehicleStyle(object_id, speed, heading));
|
||||
iconStyle = new Style({
|
||||
image: new Icon({
|
||||
src: carIcon,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad, // 使用计算好的旋转角度
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
feature.setStyle(iconStyle);
|
||||
vehicleSource.addFeature(feature);
|
||||
|
||||
vehicles.value[object_id] = {
|
||||
id: object_id,
|
||||
type: object_type,
|
||||
position: coordinates,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
feature: feature,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
};
|
||||
// 保存feature引用到车辆数据对象
|
||||
vehicles.value[object_id].feature = feature;
|
||||
|
||||
// 初始化动画数据
|
||||
if (animationSystem.value) {
|
||||
@ -317,7 +345,7 @@ function updateVehiclePosition(vehicleData) {
|
||||
vehicles.value[object_id] = {
|
||||
...vehicles.value[object_id],
|
||||
position: coordinates,
|
||||
heading: heading,
|
||||
heading: validHeading,
|
||||
speed: speed
|
||||
};
|
||||
|
||||
@ -326,6 +354,24 @@ function updateVehiclePosition(vehicleData) {
|
||||
if (labelSystem.value && !vehicles.value[object_id].speedViolation) {
|
||||
labelSystem.value.updateVehicleLabel(object_id, coordinates, speed);
|
||||
}
|
||||
|
||||
// 更新车辆图标样式,确保旋转角度正确应用
|
||||
if (feature) {
|
||||
let iconSrc = isAircraft ? aircraftIcon : carIcon;
|
||||
|
||||
// 移除角度变化阈值,确保每次角度变化都更新图标旋转
|
||||
feature.setStyle(new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad, // 使用计算好的旋转角度
|
||||
})
|
||||
}));
|
||||
|
||||
// 更新lastHeading
|
||||
vehicles.value[object_id].lastHeading = validHeading;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,290 +429,43 @@ function connectWebSocket() {
|
||||
function handleWsMessage(message) {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
console.log('收到消息:', data);
|
||||
|
||||
// 根据消息类型处理
|
||||
switch (data.type) {
|
||||
case 'connection':
|
||||
console.log(`连接确认: ${data.message}`);
|
||||
break;
|
||||
case 'position_update':
|
||||
// 确保payload存在
|
||||
if (data.payload && data.payload.object_id) {
|
||||
// 检查是否需要清除车辆的告警状态
|
||||
clearVehicleAlertStatus(data.payload);
|
||||
|
||||
// 更新车辆位置
|
||||
updateVehiclePosition(data.payload);
|
||||
} else {
|
||||
console.error('位置更新消息格式错误:', data);
|
||||
}
|
||||
case 'position_update':
|
||||
// 处理车辆位置更新
|
||||
console.log(`位置更新: ${data.payload?.object_id} (${data.payload?.object_type})`);
|
||||
handlePositionUpdate(data.payload);
|
||||
break;
|
||||
|
||||
case 'path_conflict_alert':
|
||||
// 处理冲突告警和预警
|
||||
console.log(`冲突告警/预警: ${data.payload?.object1?.objectName || data.payload?.messageType}`);
|
||||
handlePathConflictAlert(data.payload);
|
||||
break;
|
||||
|
||||
case 'rule_violation':
|
||||
// 处理规则违规(超速和越界)
|
||||
console.log(`规则违规: ${data.payload?.ruleName || data.payload?.violationType}`);
|
||||
handleRuleViolation(data.payload);
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
console.log('收到心跳响应');
|
||||
break;
|
||||
case 'collision_warning':
|
||||
console.log('收到碰撞预警:', data.payload);
|
||||
// 显示预警信息
|
||||
if (data.payload) {
|
||||
// 获取车辆ID和预警信息
|
||||
const vehicleId = data.payload.object_id || '未知车辆';
|
||||
const distance = data.payload.distance || 0;
|
||||
const message = `预警:${vehicleId} 与其他车辆距离${distance.toFixed(1)}米,请注意避让!`;
|
||||
showAlert(message, 'warning', 8000);
|
||||
|
||||
// 如果需要在地图上标记该车辆的告警状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
vehicles.value[vehicleId].warning = true;
|
||||
// 如果车辆已有位置信息,更新标签显示
|
||||
if (vehicles.value[vehicleId].position) {
|
||||
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'rule_violation':
|
||||
console.log('收到规则违规:', data.payload);
|
||||
if (data.payload) {
|
||||
// 获取车辆ID和告警信息
|
||||
const vehicleId = data.payload.object_id || data.payload.vehicleId || data.payload.vehicleLicense || '未知车辆';
|
||||
const violationType = data.payload.violationType || '未知违规';
|
||||
const alertLevel = data.payload.alertLevel || 'INFO';
|
||||
const description = data.payload.description || '';
|
||||
const limitValue = data.payload.limitValue;
|
||||
const actualValue = data.payload.actualValue;
|
||||
const ruleName = data.payload.ruleName || '交通规则';
|
||||
|
||||
// 检查是否为超速违规
|
||||
const isSpeedViolation = violationType === 'SPEED_VIOLATION';
|
||||
// 检查是否为超速结束
|
||||
const isSpeedViolationEnd = violationType === 'SPEED_VIOLATION_END';
|
||||
|
||||
// 如果是超速违规,特殊处理
|
||||
if (isSpeedViolation && alertLevel === 'WARNING') {
|
||||
console.log(`检测到超速违规: ${vehicleId}, 实际速度: ${actualValue}, 限速: ${limitValue}`);
|
||||
|
||||
// 在地图上标记该车辆的超速状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
// 避免重复设置超速状态导致闪烁
|
||||
const alreadyInSpeedViolation = vehicles.value[vehicleId].speedViolation;
|
||||
|
||||
// 强制设置超速状态,确保图标正确显示
|
||||
vehicles.value[vehicleId].info = false;
|
||||
vehicles.value[vehicleId].alarm = false;
|
||||
vehicles.value[vehicleId].warning = false; // 清除warning状态,使用speedViolation状态
|
||||
vehicles.value[vehicleId].critical = false;
|
||||
vehicles.value[vehicleId].speedViolation = true; // 标记为超速状态
|
||||
vehicles.value[vehicleId].limitValue = limitValue;
|
||||
vehicles.value[vehicleId].actualValue = actualValue;
|
||||
vehicles.value[vehicleId].description = description;
|
||||
vehicles.value[vehicleId].ruleName = ruleName;
|
||||
vehicles.value[vehicleId].lastSpeedViolationTime = Date.now(); // 记录最后一次超速时间
|
||||
|
||||
// 设置状态锁定,确保一段时间内不会改变状态
|
||||
if (!vehicles.value[vehicleId].statusLock) {
|
||||
vehicles.value[vehicleId].statusLock = {
|
||||
active: false,
|
||||
type: null,
|
||||
until: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 锁定超速状态20秒,确保图标不会来回切换
|
||||
vehicles.value[vehicleId].statusLock = {
|
||||
active: true,
|
||||
type: 'speedViolation',
|
||||
until: Date.now() + 20000 // 20秒锁定
|
||||
};
|
||||
|
||||
// 清除任何可能存在的缓存图标,确保使用警告图标
|
||||
vehicles.value[vehicleId].cachedIconSrc = null;
|
||||
|
||||
// 更新车辆图标为超速警告图标(只在首次设置或状态改变时更新)
|
||||
if (!alreadyInSpeedViolation) {
|
||||
if (vehicles.value[vehicleId].feature) {
|
||||
vehicles.value[vehicleId].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
|
||||
vehicles.value[vehicleId].lastIconUpdateTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新标签显示为超速状态(只显示超速相关信息)
|
||||
if (vehicles.value[vehicleId].position && labelSystem.value) {
|
||||
// 先移除可能存在的旧标签,避免重复显示
|
||||
labelSystem.value.removeVehicleLabel(vehicleId);
|
||||
|
||||
// 然后创建新的超速状态标签
|
||||
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, actualValue || vehicles.value[vehicleId].speed, {
|
||||
description: description || `超速违规`,
|
||||
limitValue: limitValue,
|
||||
actualValue: actualValue,
|
||||
ruleName: ruleName,
|
||||
isSpeedViolation: true // 标记为超速状态,避免显示默认信息
|
||||
});
|
||||
}
|
||||
|
||||
// 设置或重置超速状态自动清除计时器
|
||||
if (speedViolationTimers[vehicleId]) {
|
||||
clearTimeout(speedViolationTimers[vehicleId]);
|
||||
}
|
||||
|
||||
// 设置新的超时计时器,如果在一定时间内(如20秒)没有再次接收到超速消息,则自动清除超速状态
|
||||
speedViolationTimers[vehicleId] = setTimeout(() => {
|
||||
// 确保车辆仍然存在
|
||||
if (vehicles.value[vehicleId]) {
|
||||
console.log(`超速状态超时: ${vehicleId}, 自动清除超速状态`);
|
||||
|
||||
// 检查是否可以清除状态锁定
|
||||
if (!vehicles.value[vehicleId].statusLock ||
|
||||
!vehicles.value[vehicleId].statusLock.active ||
|
||||
Date.now() > vehicles.value[vehicleId].statusLock.until) {
|
||||
|
||||
// 清除超速状态
|
||||
clearSpeedViolationStatus(vehicleId);
|
||||
|
||||
// 清除计时器引用
|
||||
delete speedViolationTimers[vehicleId];
|
||||
} else {
|
||||
// 状态仍然锁定,延迟清除
|
||||
console.log(`车辆${vehicleId}状态仍然锁定,延迟清除超速状态`);
|
||||
|
||||
// 重新设置计时器,等待锁定结束
|
||||
speedViolationTimers[vehicleId] = setTimeout(() => {
|
||||
clearSpeedViolationStatus(vehicleId);
|
||||
delete speedViolationTimers[vehicleId];
|
||||
}, vehicles.value[vehicleId].statusLock.until - Date.now());
|
||||
}
|
||||
}
|
||||
}, 20000); // 20秒超时,确保状态稳定
|
||||
}
|
||||
return; // 已处理超速情况,不再进入下面的alertLevel处理
|
||||
}
|
||||
|
||||
// 如果是超速结束,清除超速状态
|
||||
if (isSpeedViolationEnd) {
|
||||
console.log(`检测到超速结束: ${vehicleId}`);
|
||||
|
||||
// 在地图上清除该车辆的超速状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
// 检查是否可以清除状态锁定
|
||||
if (!vehicles.value[vehicleId].statusLock ||
|
||||
!vehicles.value[vehicleId].statusLock.active ||
|
||||
Date.now() > vehicles.value[vehicleId].statusLock.until) {
|
||||
|
||||
// 清除超速状态,使用专门的函数
|
||||
clearSpeedViolationStatus(vehicleId);
|
||||
} 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]) {
|
||||
vehicles.value[vehicleId].critical = true;
|
||||
vehicles.value[vehicleId].alarm = false;
|
||||
vehicles.value[vehicleId].warning = 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 '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);
|
||||
console.log(`未知消息类型: ${data.type}`, data);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -674,6 +473,272 @@ function handleWsMessage(message) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理位置更新消息
|
||||
function handlePositionUpdate(payload) {
|
||||
if (!payload || !payload.object_id) {
|
||||
console.error('位置更新消息格式错误:', payload);
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除车辆的告警状态(如果需要)
|
||||
clearVehicleAlertStatus(payload);
|
||||
|
||||
// 更新车辆位置
|
||||
updateVehiclePosition(payload);
|
||||
}
|
||||
|
||||
// 处理冲突告警和预警
|
||||
function handlePathConflictAlert(payload) {
|
||||
if (!payload) {
|
||||
console.error('冲突告警消息格式错误:', payload);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('收到冲突告警/预警:', payload);
|
||||
|
||||
const object1 = payload.object1 || {};
|
||||
const object2 = payload.object2 || {};
|
||||
const vehicleId = object1.objectName || '未知车辆';
|
||||
const otherVehicleId = object2.objectName || '未知车辆';
|
||||
const distance = payload.object2Distance || 0;
|
||||
const message = payload.message || `与${otherVehicleId}可能发生冲突`;
|
||||
|
||||
// 根据messageType区分冲突告警和冲突预警
|
||||
if (payload.messageType === 'PATH_CONFLICT_ALERT' || payload.alertType === 'CONFLICT_WARNING') {
|
||||
console.log('处理冲突预警:', vehicleId, otherVehicleId);
|
||||
const warningMessage = `预警:${message}`;
|
||||
showAlert(warningMessage, 'warning', 8000);
|
||||
|
||||
// 在地图上标记该车辆的预警状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
vehicles.value[vehicleId].warning = true;
|
||||
vehicles.value[vehicleId].alarm = false;
|
||||
vehicles.value[vehicleId].critical = false;
|
||||
vehicles.value[vehicleId].info = false;
|
||||
|
||||
// 如果车辆已有位置信息,更新标签显示
|
||||
if (vehicles.value[vehicleId].position) {
|
||||
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
|
||||
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;
|
||||
|
||||
default:
|
||||
console.log(`未知的规则违规类型: ${violationType}`);
|
||||
// 默认按越界处理
|
||||
handleUnauthorizedEntry(vehicleId, payload);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理超速违规
|
||||
function handleSpeedViolation(vehicleId, payload) {
|
||||
const actualValue = payload.actualValue;
|
||||
const limitValue = payload.limitValue;
|
||||
const description = payload.description || '超速违规';
|
||||
const ruleName = payload.ruleName || '速度限制';
|
||||
|
||||
console.log(`检测到超速违规: ${vehicleId}, 实际速度: ${actualValue}, 限速: ${limitValue}`);
|
||||
|
||||
// 显示超速告警提示
|
||||
showAlert(`⚠️ 超速告警:${vehicleId} ${description}`, 'warning', 8000);
|
||||
|
||||
// 在地图上标记该车辆的超速状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
// 避免重复设置超速状态导致闪烁
|
||||
const alreadyInSpeedViolation = vehicles.value[vehicleId].speedViolation;
|
||||
|
||||
// 强制设置超速状态,确保图标正确显示
|
||||
vehicles.value[vehicleId].info = false;
|
||||
vehicles.value[vehicleId].alarm = false;
|
||||
vehicles.value[vehicleId].warning = false; // 清除warning状态,使用speedViolation状态
|
||||
vehicles.value[vehicleId].critical = false;
|
||||
vehicles.value[vehicleId].speedViolation = true; // 标记为超速状态
|
||||
vehicles.value[vehicleId].limitValue = limitValue;
|
||||
vehicles.value[vehicleId].actualValue = actualValue;
|
||||
vehicles.value[vehicleId].description = description;
|
||||
vehicles.value[vehicleId].ruleName = ruleName;
|
||||
vehicles.value[vehicleId].lastSpeedViolationTime = Date.now(); // 记录最后一次超速时间
|
||||
|
||||
// 设置状态锁定,确保一段时间内不会改变状态
|
||||
if (!vehicles.value[vehicleId].statusLock) {
|
||||
vehicles.value[vehicleId].statusLock = {
|
||||
active: false,
|
||||
type: null,
|
||||
until: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 锁定超速状态20秒,确保图标不会来回切换
|
||||
vehicles.value[vehicleId].statusLock = {
|
||||
active: true,
|
||||
type: 'speedViolation',
|
||||
until: Date.now() + 20000 // 20秒锁定
|
||||
};
|
||||
|
||||
// 清除任何可能存在的缓存图标,确保使用警告图标
|
||||
vehicles.value[vehicleId].cachedIconSrc = null;
|
||||
|
||||
// 更新车辆图标为超速警告图标(只在首次设置或状态改变时更新)
|
||||
if (!alreadyInSpeedViolation) {
|
||||
if (vehicles.value[vehicleId].feature && styleManager.value) {
|
||||
vehicles.value[vehicleId].feature.setStyle(styleManager.value.getVehicleStyle(vehicleId, vehicles.value[vehicleId].speed, vehicles.value[vehicleId].heading));
|
||||
vehicles.value[vehicleId].lastIconUpdateTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新标签显示为超速状态(只显示超速相关信息)
|
||||
if (vehicles.value[vehicleId].position && labelSystem.value) {
|
||||
// 先移除可能存在的旧标签,避免重复显示
|
||||
labelSystem.value.removeVehicleLabel(vehicleId);
|
||||
|
||||
// 然后创建新的超速状态标签
|
||||
labelSystem.value.updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, actualValue || vehicles.value[vehicleId].speed, {
|
||||
description: description || `超速违规`,
|
||||
limitValue: limitValue,
|
||||
actualValue: actualValue,
|
||||
ruleName: ruleName,
|
||||
isSpeedViolation: true // 标记为超速状态,避免显示默认信息
|
||||
});
|
||||
}
|
||||
|
||||
// 设置或重置超速状态自动清除计时器
|
||||
if (speedViolationTimers[vehicleId]) {
|
||||
clearTimeout(speedViolationTimers[vehicleId]);
|
||||
}
|
||||
|
||||
// 设置新的超时计时器,如果在一定时间内(如20秒)没有再次接收到超速消息,则自动清除超速状态
|
||||
speedViolationTimers[vehicleId] = setTimeout(() => {
|
||||
// 确保车辆仍然存在
|
||||
if (vehicles.value[vehicleId]) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
console.log(`超速状态超时: ${vehicleId}, 自动清除超速状态`);
|
||||
|
||||
// 检查是否可以清除状态锁定
|
||||
if (!vehicles.value[vehicleId].statusLock ||
|
||||
!vehicles.value[vehicleId].statusLock.active ||
|
||||
Date.now() > vehicles.value[vehicleId].statusLock.until) {
|
||||
|
||||
// 清除超速状态
|
||||
clearSpeedViolationStatus(vehicleId);
|
||||
|
||||
// 清除计时器引用
|
||||
delete speedViolationTimers[vehicleId];
|
||||
} else {
|
||||
// 状态仍然锁定,延迟清除
|
||||
console.log(`车辆${vehicleId}状态仍然锁定,延迟清除超速状态`);
|
||||
|
||||
// 重新设置计时器,等待锁定结束
|
||||
speedViolationTimers[vehicleId] = setTimeout(() => {
|
||||
clearSpeedViolationStatus(vehicleId);
|
||||
delete speedViolationTimers[vehicleId];
|
||||
}, vehicles.value[vehicleId].statusLock.until - Date.now());
|
||||
}
|
||||
}
|
||||
}, 20000); // 20秒超时,确保状态稳定
|
||||
}
|
||||
}
|
||||
|
||||
// 处理越界告警
|
||||
function handleUnauthorizedEntry(vehicleId, payload) {
|
||||
const description = payload.description || '越界告警';
|
||||
const alertLevel = payload.alertLevel || 'CRITICAL';
|
||||
const ruleName = payload.ruleName || '区域控制';
|
||||
|
||||
console.log(`检测到越界告警: ${vehicleId}, ${description}, 规则: ${ruleName}`);
|
||||
|
||||
// 显示越界告警提示
|
||||
showAlert(`⚠️ 越界告警:${vehicleId} ${description}`, 'critical', 10000);
|
||||
|
||||
// 在地图上标记该车辆的越界状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
vehicles.value[vehicleId].critical = true;
|
||||
vehicles.value[vehicleId].alarm = false;
|
||||
vehicles.value[vehicleId].warning = false;
|
||||
vehicles.value[vehicleId].info = false;
|
||||
vehicles.value[vehicleId].speedViolation = false;
|
||||
vehicles.value[vehicleId].description = description;
|
||||
vehicles.value[vehicleId].ruleName = ruleName;
|
||||
|
||||
// 更新车辆图标为越界警告图标
|
||||
if (vehicles.value[vehicleId].feature && 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: description,
|
||||
ruleName: ruleName,
|
||||
isUnauthorizedEntry: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除车辆超速状态的专门函数
|
||||
function clearSpeedViolationStatus(vehicleId) {
|
||||
const vehicle = vehicles.value[vehicleId];
|
||||
@ -716,6 +781,10 @@ function clearSpeedViolationStatus(vehicleId) {
|
||||
|
||||
// 更新标签显示为正常状态(只显示车辆ID,不显示0.00km/h)
|
||||
if (vehicle.position && labelSystem.value) {
|
||||
// 先移除可能存在的旧标签,避免重复显示
|
||||
labelSystem.value.removeVehicleLabel(vehicleId);
|
||||
|
||||
// 创建新的标准标签
|
||||
labelSystem.value.updateVehicleLabel(vehicleId, vehicle.position, vehicle.speed > 0.1 ? vehicle.speed : 0);
|
||||
}
|
||||
|
||||
@ -770,44 +839,7 @@ function clearVehicleAlertStatus(vehicleData) {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有任何其他告警状态
|
||||
const hasAlertStatus = existingVehicle.info || existingVehicle.warning ||
|
||||
existingVehicle.alarm || existingVehicle.critical;
|
||||
|
||||
// 如果有告警状态,需要检查是否应该清除
|
||||
if (hasAlertStatus) {
|
||||
// 只处理超速状态(info)的自动恢复
|
||||
// 1. 判断是否为超速状态
|
||||
const isSpeedViolation = existingVehicle.info;
|
||||
|
||||
// 2. 判断当前速度是否已降低到安全值
|
||||
// 这里使用一个默认阈值判断,实际应用中最好使用服务端返回的限速值
|
||||
// 假设超速阈值为50km/h
|
||||
const SPEED_THRESHOLD = 5; // 超速阈值,单位km/h
|
||||
const isBelowThreshold = speed < SPEED_THRESHOLD;
|
||||
|
||||
// 3. 如果是超速状态且当前速度已降低到阈值以下,清除超速状态
|
||||
if (isSpeedViolation && isBelowThreshold) {
|
||||
console.log(`检测到车辆${object_id}速度降低到${speed.toFixed(1)}km/h,低于阈值${SPEED_THRESHOLD}km/h,自动清除超速状态`);
|
||||
|
||||
// 清除超速状态
|
||||
existingVehicle.info = false;
|
||||
|
||||
// 更新车辆图标为默认图标
|
||||
if (existingVehicle.feature) {
|
||||
existingVehicle.feature.setStyle(styleManager.value.getVehicleStyle(object_id, speed, existingVehicle.heading));
|
||||
}
|
||||
|
||||
// 更新标签显示为正常状态
|
||||
if (existingVehicle.position) {
|
||||
labelSystem.value.updateVehicleLabel(object_id, existingVehicle.position, speed);
|
||||
}
|
||||
|
||||
console.log(`已清除${object_id}的超速状态,恢复为普通状态`);
|
||||
} else {
|
||||
console.log(`车辆${object_id}有告警状态,但不符合自动清除条件,保持该状态`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送心跳
|
||||
@ -1061,36 +1093,28 @@ function defaultGetVehicleStyle(id, _speed, heading) {
|
||||
const vehicle = vehicles.value[id];
|
||||
if (!vehicle) return createDefaultStyle(carIcon, heading);
|
||||
|
||||
// 根据车辆类型选择图标
|
||||
if (vehicle.isAircraftIn) {
|
||||
// 滑入航空器(CA开头)使用黄色Aircraft1.png图标
|
||||
return createDefaultStyle(aircraftInIcon, heading);
|
||||
} else if (vehicle.isAircraftOut) {
|
||||
// 滑出航空器(MU开头)使用蓝色Aircraft.png图标
|
||||
return createDefaultStyle(aircraftOutIcon, heading);
|
||||
} else if (vehicle.isUnmannedVehicle) {
|
||||
// 无人车使用noPeopleCar.png图标
|
||||
return createDefaultStyle(carIcon, heading);
|
||||
} else if (vehicle.isSpecialVehicle) {
|
||||
// 特勤车
|
||||
return createDefaultStyle(carIcon, heading);
|
||||
} else if (vehicle.isShuttleVehicle) {
|
||||
// 摆渡车
|
||||
return createDefaultStyle(carIcon, heading);
|
||||
// 只根据车辆类型选择图标,不根据事件状态改变
|
||||
if (vehicle.isAircraft) {
|
||||
// 航空器使用Aircraft.png图标
|
||||
return createDefaultStyle(aircraftIcon, heading);
|
||||
} else {
|
||||
// 默认车辆图标
|
||||
// 其他所有车辆使用noPeopleCar.png图标
|
||||
return createDefaultStyle(carIcon, heading);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认样式的辅助函数
|
||||
function createDefaultStyle(iconSrc, heading) {
|
||||
// 确保heading是有效的数值并正确转换为弧度
|
||||
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||||
const rotationRad = (validHeading * Math.PI) / 180;
|
||||
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
src: iconSrc, // 使用传入的图标源
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
rotation: rotationRad, // 正确应用旋转角度
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@ -7,214 +7,62 @@ import { Style, Icon } from 'ol/style';
|
||||
|
||||
// 导入车辆图标
|
||||
import carIcon from '../../../assets/images/noPeopleCar.png';
|
||||
import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 滑入航空器使用黄色图标
|
||||
import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 滑出航空器使用蓝色图标
|
||||
import warningCarIcon from '../../../assets/images/warning_car.png';
|
||||
import unmannedVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 无人车图标
|
||||
import specialVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 特勤车图标
|
||||
import shuttleVehicleIcon from '../../../assets/images/noPeopleCar.png'; // 摆渡车图标
|
||||
import aircraftIcon from '../../../assets/images/aircraft.png'; // 航空器图标
|
||||
// 不再使用告警状态图标
|
||||
// import warningCarIcon from '../../../assets/images/warning_car.png'; // 预警车辆图标
|
||||
// import alarmCarIcon from '../../../assets/images/alarm_car.png'; // 告警车辆图标
|
||||
// import criticalCarIcon from '../../../assets/images/warning_car.png'; // 越界车辆图标(使用warning_car.png替代)
|
||||
// import speedCarIcon from '../../../assets/images/warning_car.png'; // 超速车辆图标(使用warning_car.png替代)
|
||||
|
||||
// 不再使用航空器告警图标
|
||||
// import warningAircraftIcon from '../../../assets/images/aircraft.png';
|
||||
// import alarmAircraftIcon from '../../../assets/images/aircraft.png';
|
||||
// import criticalAircraftIcon from '../../../assets/images/aircraft.png';
|
||||
// import speedAircraftIcon from '../../../assets/images/aircraft.png';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
vehicles: Object
|
||||
});
|
||||
|
||||
// 状态锁定时间(毫秒)- 超速状态锁定时间更长,确保图标稳定
|
||||
const STATUS_LOCK_TIME = 20000; // 20秒锁定时间,避免图标来回切换
|
||||
|
||||
// 获取车辆样式
|
||||
function getVehicleStyle(id, speed, heading) {
|
||||
const vehicle = props.vehicles[id];
|
||||
if (!vehicle) return new Style({});
|
||||
if (!vehicle) return createDefaultStyle(carIcon, heading);
|
||||
|
||||
// 初始化状态锁定属性(如果不存在)
|
||||
if (vehicle.statusLock === undefined) {
|
||||
vehicle.statusLock = {
|
||||
active: false,
|
||||
type: null,
|
||||
until: 0
|
||||
};
|
||||
}
|
||||
// 只根据车辆类型选择图标,不根据状态变化
|
||||
const isAircraft = vehicle.isAircraft;
|
||||
|
||||
const now = Date.now();
|
||||
const hasViolation = vehicle.violationType;
|
||||
const hasInfoStatus = vehicle.info;
|
||||
const hasWarningStatus = vehicle.warning;
|
||||
const hasAlarmStatus = vehicle.alarm;
|
||||
const hasCriticalStatus = vehicle.critical;
|
||||
const hasSpeedViolation = vehicle.speedViolation;
|
||||
|
||||
// 检查是否有状态锁定
|
||||
if (vehicle.statusLock.active && now < vehicle.statusLock.until) {
|
||||
// 状态锁定中,直接返回锁定的图标
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: vehicle.statusLock.type === 'speedViolation' ? warningCarIcon : vehicle.cachedIconSrc || carIcon,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
})
|
||||
});
|
||||
} else if (vehicle.statusLock.active) {
|
||||
// 状态锁定已过期,清除锁定
|
||||
vehicle.statusLock.active = false;
|
||||
}
|
||||
|
||||
// 超速状态特殊处理 - 确保图标稳定,不来回切换
|
||||
if (hasSpeedViolation) {
|
||||
// 如果还没有锁定状态或锁定已过期,重新锁定
|
||||
if (!vehicle.statusLock.active || now > vehicle.statusLock.until) {
|
||||
vehicle.statusLock = {
|
||||
active: true,
|
||||
type: 'speedViolation',
|
||||
until: now + STATUS_LOCK_TIME
|
||||
};
|
||||
}
|
||||
|
||||
// 直接返回警告图标,确保超速状态下图标保持一致
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: warningCarIcon,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 非超速状态下的缓存处理
|
||||
if (vehicle.cachedIconSrc && !hasSpeedViolation) {
|
||||
// 如果已有缓存图标且不是超速状态,检查是否需要更新
|
||||
const lastIconTime = vehicle.lastIconUpdateTime || 0;
|
||||
|
||||
// 如果最后一次更新时间在2秒内,继续使用缓存的图标
|
||||
if (now - lastIconTime < 2000) {
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: vehicle.cachedIconSrc,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let iconSrc;
|
||||
|
||||
// 首先检查告警状态
|
||||
if (hasSpeedViolation) {
|
||||
// 超速车辆使用警告图标
|
||||
iconSrc = warningCarIcon;
|
||||
} else if (hasViolation || hasInfoStatus || hasWarningStatus || hasCriticalStatus || hasAlarmStatus) {
|
||||
// 其他违规车辆使用警告图标
|
||||
iconSrc = warningCarIcon;
|
||||
|
||||
// 锁定告警状态一段时间(比超速状态短)
|
||||
vehicle.statusLock = {
|
||||
active: true,
|
||||
type: 'alert',
|
||||
until: now + STATUS_LOCK_TIME / 2
|
||||
};
|
||||
} else if (vehicle.isAircraftIn) {
|
||||
// 滑入航空器(MU开头)使用黄色Aircraft1.png图标
|
||||
iconSrc = aircraftInIcon;
|
||||
} else if (vehicle.isAircraftOut) {
|
||||
// 滑出航空器(CA开头)使用蓝色Aircraft.png图标
|
||||
iconSrc = aircraftOutIcon;
|
||||
} else if (vehicle.isUnmannedVehicle) {
|
||||
// 无人车使用noPeopleCar.png图标
|
||||
iconSrc = unmannedVehicleIcon;
|
||||
} else if (vehicle.isSpecialVehicle) {
|
||||
// 特勤车
|
||||
iconSrc = specialVehicleIcon;
|
||||
} else if (vehicle.isShuttleVehicle) {
|
||||
// 摆渡车
|
||||
iconSrc = shuttleVehicleIcon;
|
||||
} else {
|
||||
// 默认车辆图标
|
||||
iconSrc = carIcon;
|
||||
}
|
||||
|
||||
// 只有在非超速状态下才缓存图标
|
||||
if (!hasSpeedViolation) {
|
||||
vehicle.cachedIconSrc = iconSrc;
|
||||
vehicle.lastIconUpdateTime = now;
|
||||
}
|
||||
// 根据车辆类型选择图标
|
||||
const iconSrc = isAircraft ? aircraftIcon : carIcon;
|
||||
|
||||
// 创建样式
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
})
|
||||
});
|
||||
return createDefaultStyle(iconSrc, heading);
|
||||
}
|
||||
|
||||
// 根据车辆类型和状态选择图标
|
||||
function getVehicleIcon(vehicle) {
|
||||
if (!vehicle) return carIcon;
|
||||
// 创建默认样式
|
||||
function createDefaultStyle(iconSrc, heading) {
|
||||
// 确保heading是有效的数值
|
||||
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||||
|
||||
const hasInfoStatus = vehicle.info;
|
||||
const hasWarningStatus = vehicle.warning;
|
||||
const hasAlarmStatus = vehicle.alarm;
|
||||
const hasCriticalStatus = vehicle.critical;
|
||||
// 统一使用(heading - 72)的偏移量计算旋转角度
|
||||
const rotationRad = ((validHeading - 72) * Math.PI) / 180;
|
||||
|
||||
// 首先检查告警状态
|
||||
if (hasInfoStatus || hasWarningStatus || hasCriticalStatus || hasAlarmStatus) {
|
||||
return warningCarIcon;
|
||||
}
|
||||
|
||||
// 然后根据车辆类型选择图标
|
||||
if (vehicle.isAircraftIn) {
|
||||
// 滑入航空器(CA开头)使用黄色Aircraft1.png图标
|
||||
return aircraftInIcon;
|
||||
} else if (vehicle.isAircraftOut) {
|
||||
// 滑出航空器(MU开头)使用蓝色Aircraft.png图标
|
||||
return aircraftOutIcon;
|
||||
} else if (vehicle.isUnmannedVehicle) {
|
||||
// 无人车使用noPeopleCar.png图标
|
||||
return unmannedVehicleIcon;
|
||||
} else if (vehicle.isSpecialVehicle) {
|
||||
// 特勤车
|
||||
return specialVehicleIcon;
|
||||
} else if (vehicle.isShuttleVehicle) {
|
||||
// 摆渡车
|
||||
return shuttleVehicleIcon;
|
||||
} else {
|
||||
return carIcon;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建车辆样式
|
||||
function createVehicleStyle(vehicle, heading) {
|
||||
const iconSrc = getVehicleIcon(vehicle);
|
||||
// 添加调试信息
|
||||
console.log(`VehicleStyleManager: heading=${validHeading}, rotation=${rotationRad}弧度, ${rotationRad * 180 / Math.PI}度`);
|
||||
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
rotation: rotationRad, // 使用统一的旋转角度计算
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 更新车辆样式
|
||||
function updateVehicleStyle(id, heading) {
|
||||
const vehicle = props.vehicles[id];
|
||||
if (!vehicle) return null;
|
||||
|
||||
return createVehicleStyle(vehicle, heading);
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
getVehicleStyle,
|
||||
getVehicleIcon,
|
||||
createVehicleStyle,
|
||||
updateVehicleStyle
|
||||
getVehicleStyle
|
||||
});
|
||||
</script>
|
||||
@ -221,6 +221,7 @@ function updateCarList(newList) {
|
||||
|
||||
const selectedCar = ref(null);
|
||||
function showDetail(car) {
|
||||
// 移除对不存在的moveToTarget函数的调用
|
||||
selectedCar.value = car;
|
||||
}
|
||||
|
||||
|
||||
@ -11,31 +11,20 @@
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 2px;
|
||||
}
|
||||
.test-section {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
margin: 2px 0;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.test-section h2 {
|
||||
color: #555;
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.control-group {
|
||||
margin: 15px 0;
|
||||
display: flex;
|
||||
@ -86,7 +75,7 @@
|
||||
.log-container {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
height: 300px;
|
||||
height: 960px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
@ -106,16 +95,15 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>冲突检测WebSocket测试工具</h1>
|
||||
|
||||
<!-- 冲突检测WebSocket测试 -->
|
||||
<div class="test-section">
|
||||
<h2>🚗 冲突检测WebSocket (原生协议)</h2>
|
||||
<h3>🚗 冲突检测WebSocket</h3>
|
||||
|
||||
<div class="control-group">
|
||||
<label>服务器地址:</label>
|
||||
<select id="collisionServerSelect">
|
||||
<option value="ws://10.0.0.126:8080/collision">冲突检测WebSocket</option>
|
||||
<option value="ws://10.0.0.126:8080/collision">10.0.0.126:8080/collision</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -136,7 +124,7 @@
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="test-section">
|
||||
<h2>🎛️ 控制面板</h2>
|
||||
<h3>🎛️ 控制面板</h3>
|
||||
<div class="control-group">
|
||||
<button onclick="clearLogs()">清空日志</button>
|
||||
<button onclick="showConnectionStatus()">显示连接状态</button>
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
<el-table-column label="车牌号" prop="licensePlate" align="left" />
|
||||
<el-table-column label="车辆类型" align="left">
|
||||
<template #default="scope">
|
||||
{{ getVehicleTypeName(scope.row.typeId) }}
|
||||
{{ scope.row.typeDisplayName || '未知类型' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="品牌" prop="brand" align="left" />
|
||||
@ -544,6 +544,7 @@ function handleRemoveImage() {
|
||||
|
||||
/** 获取车辆类型名称 */
|
||||
function getVehicleTypeName(typeId) {
|
||||
// 保留此函数以兼容旧代码,但在表格中直接使用typeDisplayName
|
||||
return vehicleTypeMap.value[typeId] || '';
|
||||
}
|
||||
|
||||
|
||||
@ -115,7 +115,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="二级类型" prop="typeName" />
|
||||
<el-table-column label="二级类型" prop="typeName">
|
||||
<template #default="scope">
|
||||
{{ scope.row.typeName }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建人" prop="createBy" />
|
||||
<el-table-column label="创建时间" prop="createTime" />
|
||||
</el-table>
|
||||
@ -170,11 +174,7 @@ const { proxy } = getCurrentInstance();
|
||||
|
||||
// Tab相关
|
||||
const editableTabsValue = ref("");
|
||||
const editableTabs = ref([
|
||||
{ label: "无人车", name: "无人车", content: "" },
|
||||
{ label: "特勤车", name: "特勤车", content: "" },
|
||||
{ label: "普通车", name: "普通车", content: "" },
|
||||
]);
|
||||
const editableTabs = ref([]);
|
||||
|
||||
const showAddTabDialog = ref(false);
|
||||
const newTabName = ref("");
|
||||
@ -197,9 +197,15 @@ const addTab = () => {
|
||||
|
||||
// 构造一级类型数据
|
||||
const submitData = {
|
||||
typeName: name,
|
||||
displayNameCn: name,
|
||||
displayNameEn: name, // 英文名称默认与中文相同
|
||||
typeCode: generateTypeCode(name), // 生成唯一的typeCode
|
||||
typeName: name.toUpperCase().replace(/\s+/g, "_"), // 生成typeName
|
||||
level: 1,
|
||||
parentId: null,
|
||||
isLeaf: false,
|
||||
enabled: true,
|
||||
parentCode: null,
|
||||
topLevelCode: null
|
||||
};
|
||||
|
||||
// 调用接口添加一级类型
|
||||
@ -211,7 +217,9 @@ const addTab = () => {
|
||||
getFirstTypeList();
|
||||
|
||||
// 切换到新添加的标签
|
||||
editableTabsValue.value = name;
|
||||
setTimeout(() => {
|
||||
editableTabsValue.value = submitData.typeCode;
|
||||
}, 100);
|
||||
|
||||
// 清空输入框并关闭弹窗
|
||||
newTabName.value = "";
|
||||
@ -220,41 +228,65 @@ const addTab = () => {
|
||||
// 获取新标签下的数据
|
||||
setTimeout(() => {
|
||||
getList();
|
||||
}, 100);
|
||||
}, 200);
|
||||
})
|
||||
.catch(() => {
|
||||
// 添加失败时不关闭弹窗,让用户可以修改后重试
|
||||
});
|
||||
};
|
||||
|
||||
// 生成typeCode的辅助函数
|
||||
function generateTypeCode(name) {
|
||||
// 生成基于名称的简短代码,例如"无人车" -> "WRC"
|
||||
const pinyin = name.split('').map(char => {
|
||||
// 这里简化处理,实际可能需要更复杂的拼音转换
|
||||
const pinyinMap = {
|
||||
'无': 'W', '人': 'R', '车': 'C',
|
||||
'特': 'T', '勤': 'Q', '普': 'P',
|
||||
'通': 'T', '航': 'H', '空': 'K',
|
||||
'接': 'J', '驳': 'B', '清': 'Q',
|
||||
'洁': 'J', '消': 'X', '防': 'F',
|
||||
'警': 'J', '巡': 'X', '逻': 'L',
|
||||
'配': 'P', '送': 'S', '运': 'Y',
|
||||
'输': 'S', '餐': 'C', '行': 'X',
|
||||
'李': 'L'
|
||||
};
|
||||
return pinyinMap[char] || char.charAt(0).toUpperCase();
|
||||
});
|
||||
|
||||
// 取前2-3个字母作为代码
|
||||
let code = pinyin.slice(0, Math.min(3, pinyin.length)).join('');
|
||||
|
||||
// 确保唯一性,添加随机数
|
||||
const randomNum = Math.floor(Math.random() * 100);
|
||||
return code + randomNum;
|
||||
}
|
||||
|
||||
const deleteCustomTab = (tabName) => {
|
||||
// 查找要删除的标签对应的typeId
|
||||
const firstType = firstTypeList.value.find((item) => item.value === tabName);
|
||||
if (!firstType || !firstType.typeId) return;
|
||||
|
||||
// 先检查该一级类型下是否有二级类型
|
||||
listVehicle_type({ firstType: tabName }).then((res) => {
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
listVehicle_type({ topLevelCode: tabName }).then((res) => {
|
||||
if (res.rows && Array.isArray(res.rows)) {
|
||||
// 过滤出当前选中tab对应的二级类型数据
|
||||
const currentTabData = res.data.filter(
|
||||
const currentTabData = res.rows.filter(
|
||||
(item) =>
|
||||
item.level === 2 &&
|
||||
item.parentId &&
|
||||
res.data.some(
|
||||
(parent) =>
|
||||
parent.typeId === item.parentId && parent.typeName === tabName
|
||||
)
|
||||
item.topLevelCode === tabName &&
|
||||
item.parentCode === tabName
|
||||
);
|
||||
|
||||
if (currentTabData.length > 0) {
|
||||
// 如果有二级类型,提示不能删除
|
||||
proxy.$modal.msgError("该二级类型已绑定车辆,不可删除");
|
||||
proxy.$modal.msgError("该类型下有二级类型,不可删除");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有二级类型,则可以删除
|
||||
proxy.$modal
|
||||
.confirm(`确定删除 ${tabName} 类型吗?`, "删除")
|
||||
.confirm(`确定删除 ${firstType.label} 类型吗?`, "删除")
|
||||
.then(function () {
|
||||
return delVehicle_type(firstType.typeId);
|
||||
})
|
||||
@ -310,11 +342,8 @@ const rules = ref({
|
||||
typeName: [{ required: true, message: "二级类型不能为空", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const firstTypeList = ref([
|
||||
{ label: "无人车", value: "无人车" },
|
||||
{ label: "特勤车", value: "特勤车" },
|
||||
{ label: "普通车", value: "普通车" },
|
||||
]);
|
||||
// 车辆类型选项
|
||||
const firstTypeList = ref([]);
|
||||
|
||||
/** 查询车辆类型列表 */
|
||||
function getList() {
|
||||
@ -324,25 +353,21 @@ function getList() {
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
// 根据接口返回的数据结构进行过滤,只显示当前选中tab对应的二级类型数据
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
if (res.rows && Array.isArray(res.rows)) {
|
||||
// 过滤出当前选中tab对应的二级类型数据
|
||||
const currentTabData = res.data.filter(
|
||||
const currentTabData = res.rows.filter(
|
||||
(item) =>
|
||||
item.level === 2 &&
|
||||
item.parentId &&
|
||||
res.data.some(
|
||||
(parent) =>
|
||||
parent.typeId === item.parentId &&
|
||||
parent.typeName === editableTabsValue.value
|
||||
)
|
||||
item.topLevelCode === editableTabsValue.value &&
|
||||
item.parentCode === editableTabsValue.value
|
||||
);
|
||||
|
||||
// 转换数据结构以适应表格显示
|
||||
vehicleTypeList.value = currentTabData.map((item) => ({
|
||||
typeId: item.typeId,
|
||||
firstType: editableTabsValue.value,
|
||||
typeName: item.typeName,
|
||||
createBy: item.createBy || "系统管理员",
|
||||
typeName: item.displayNameCn,
|
||||
createBy: item.createBy,
|
||||
createTime: item.createTime,
|
||||
}));
|
||||
|
||||
@ -392,7 +417,16 @@ function handleUpdate(row) {
|
||||
reset();
|
||||
const typeId = row.typeId || ids.value[0];
|
||||
getVehicle_type(typeId).then((response) => {
|
||||
form.value = response.data || {};
|
||||
// 确保使用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;
|
||||
title.value = "修改车辆类型";
|
||||
});
|
||||
@ -405,21 +439,12 @@ function submitForm() {
|
||||
// 根据接口要求调整提交的数据结构
|
||||
const submitData = {
|
||||
typeId: form.value.typeId,
|
||||
typeName: form.value.typeName,
|
||||
parentId: null,
|
||||
displayNameCn: form.value.typeName,
|
||||
level: 2,
|
||||
parentCode: form.value.firstType,
|
||||
topLevelCode: form.value.firstType
|
||||
};
|
||||
|
||||
// 查找一级类型对应的typeId
|
||||
if (form.value.firstType) {
|
||||
const firstType = firstTypeList.value.find(
|
||||
(item) => item.value === form.value.firstType
|
||||
);
|
||||
if (firstType && firstType.typeId) {
|
||||
submitData.parentId = firstType.typeId;
|
||||
}
|
||||
}
|
||||
|
||||
if (form.value.typeId != undefined) {
|
||||
updateVehicle_type(submitData).then((response) => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
@ -489,21 +514,21 @@ function cancel() {
|
||||
// 初始化时获取一级类型数据
|
||||
function getFirstTypeList() {
|
||||
listVehicle_type({ level: 1 }).then((res) => {
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
if (res.rows && Array.isArray(res.rows)) {
|
||||
// 过滤出一级类型数据
|
||||
const firstTypes = res.data.filter((item) => item.level === 1);
|
||||
const firstTypes = res.rows.filter((item) => item.level === 1);
|
||||
|
||||
// 更新firstTypeList,保留typeId信息
|
||||
firstTypeList.value = firstTypes.map((item) => ({
|
||||
label: item.typeName,
|
||||
value: item.typeName,
|
||||
label: item.displayNameCn,
|
||||
value: item.typeCode,
|
||||
typeId: item.typeId,
|
||||
}));
|
||||
|
||||
// 更新可编辑的tab列表
|
||||
editableTabs.value = firstTypes.map((item) => ({
|
||||
label: item.typeName,
|
||||
name: item.typeName,
|
||||
label: item.displayNameCn,
|
||||
name: item.typeCode,
|
||||
content: "",
|
||||
}));
|
||||
|
||||
|
||||
@ -70,20 +70,20 @@
|
||||
|
||||
<!-- 车辆移动控制组件 -->
|
||||
<VehicleMovementControlRefactored :map="map" ref="vehicleMovementRef" v-if="map" />
|
||||
|
||||
<!-- <VehicleDisplayControl :map="map" ref="vehicleMovementRef" v-if="map" /> -->
|
||||
|
||||
|
||||
<!-- 绘制工具栏 -->
|
||||
<!-- <div class="draw-toolbar">
|
||||
<!-- <div class="draw-toolbar">
|
||||
<div class="toolbar-title">绘制工具</div>
|
||||
<div class="toolbar-buttons">
|
||||
<button @click="startDrawLine" class="toolbar-button">绘制路线</button>
|
||||
<button @click="startDrawPolygon" class="toolbar-button">绘制点线面</button>
|
||||
<button @click="startDrawPolygon" class="toolbar-button">绘制区域</button>
|
||||
<button @click="clearDraw" class="toolbar-button clear">清除</button>
|
||||
<button @click="exportRouteData" class="toolbar-button export">导出数据</button>
|
||||
<button @click="importRouteData" class="toolbar-button import">导入数据</button>
|
||||
</div>
|
||||
</div>-->
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@ -486,8 +486,8 @@ watch(() => mapRef.value?.map, (newMap) => {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
z-index: 2001;
|
||||
width: 40px;
|
||||
height: 80px;
|
||||
width: 30px;
|
||||
height: 58px;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
transition: right 0.3s ease;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user