From ca53570ff8e42977fcb6c0da89530beb566b7549 Mon Sep 17 00:00:00 2001 From: renna <576157508@qq.com> Date: Tue, 15 Jul 2025 11:49:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=B9=B3=E5=8F=B0=E6=A6=82?= =?UTF-8?q?=E8=A7=88UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 132 ++++ README.md | 103 +-- public/quyu.json | 65 ++ public/quyu1.json | 49 ++ public/quyu2.json | 77 ++ src/components/map/controls/LayerSwitcher.vue | 172 ++++ src/components/map/controls/README.md | 167 ---- .../map/controls/VehicleAnimationSystem.vue | 192 ++--- .../map/controls/VehicleLabelSystem.vue | 620 +++++++++------ .../VehicleMovementControlRefactored.vue | 746 +++++++++--------- .../map/controls/VehicleStyleManager.vue | 208 +---- src/components/map/info/eventlist.vue | 1 + src/utils/test_websocket.html | 28 +- src/views/car/park/index.vue | 3 +- src/views/car/type/index.vue | 133 ++-- src/views/platform/index.vue | 12 +- 16 files changed, 1495 insertions(+), 1213 deletions(-) create mode 100644 CLAUDE.md create mode 100644 public/quyu.json create mode 100644 public/quyu1.json create mode 100644 public/quyu2.json delete mode 100644 src/components/map/controls/README.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..356d355 --- /dev/null +++ b/CLAUDE.md @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 78fe374..f735f42 100644 --- a/README.md +++ b/README.md @@ -26,87 +26,6 @@ yarn dev 超图地图开发使用基础 1、前期准备工作熟悉超图地图服务以及基本的操作。 2、熟悉Openlayers以及Leaflet相关操作 -具体代码操作: -1、定义一个地图渲染承载框
-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. 平滑动画核心系统 @@ -161,4 +69,13 @@ resetVehicleAnimations():重置所有动画数据 将此组件整合到现有项目中 调用startVehicleSmoothing()启动平滑效果 处理WebSocket消息时继续调用updateVehiclePosition() -当组件隐藏时调用stopVehicleSmoothing()节省资源 \ No newline at end of file +当组件隐藏时调用stopVehicleSmoothing()节省资源 + +### 运行方向分析: + 地图上方(heading=72) + | + | +地图左侧(heading=342) --+-- 地图右侧(heading=162) + | + | + 地图下方(heading=252) \ No newline at end of file diff --git a/public/quyu.json b/public/quyu.json new file mode 100644 index 0000000..ba0745a --- /dev/null +++ b/public/quyu.json @@ -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" + } + } + ] + } \ No newline at end of file diff --git a/public/quyu1.json b/public/quyu1.json new file mode 100644 index 0000000..3d5abd2 --- /dev/null +++ b/public/quyu1.json @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/public/quyu2.json b/public/quyu2.json new file mode 100644 index 0000000..dce8e84 --- /dev/null +++ b/public/quyu2.json @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/src/components/map/controls/LayerSwitcher.vue b/src/components/map/controls/LayerSwitcher.vue index 2f3d2e2..4ca24f7 100644 --- a/src/components/map/controls/LayerSwitcher.vue +++ b/src/components/map/controls/LayerSwitcher.vue @@ -83,6 +83,22 @@
+ +
+ +
+ +
+ +
@@ -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(); }); diff --git a/src/components/map/controls/README.md b/src/components/map/controls/README.md deleted file mode 100644 index 153a308..0000000 --- a/src/components/map/controls/README.md +++ /dev/null @@ -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 - - -``` - -### 单独使用子组件 -```vue - - - - - -``` - -## 迁移指南 - -### 1. 备份原文件 -```bash -cp VehicleMovementControl.vue VehicleMovementControl.vue.backup -``` - -### 2. 替换组件引用 -在父组件中,将: -```vue - -``` -替换为: -```vue - -``` - -### 3. 测试功能 -确保所有原有功能正常工作: -- 车辆位置更新 -- 车辆动画 -- 标签显示 -- 详情弹窗 -- 气象弹窗 -- 告警提示 - -## 注意事项 - -1. **依赖关系**: 主组件依赖所有子组件,确保所有子组件都已创建 -2. **Props传递**: 子组件通过props接收数据,确保数据流正确 -3. **事件通信**: 子组件通过emit向父组件发送事件 -4. **样式隔离**: 每个组件都有自己的样式,避免样式冲突 - -## 后续优化建议 - -1. **状态管理**: 考虑使用Pinia进行全局状态管理 -2. **类型安全**: 添加TypeScript类型定义 -3. **单元测试**: 为每个子组件编写单元测试 -4. **文档完善**: 为每个组件添加详细的API文档 -5. **性能监控**: 添加性能监控和优化 \ No newline at end of file diff --git a/src/components/map/controls/VehicleAnimationSystem.vue b/src/components/map/controls/VehicleAnimationSystem.vue index 59a97ed..b8be4c2 100644 --- a/src/components/map/controls/VehicleAnimationSystem.vue +++ b/src/components/map/controls/VehicleAnimationSystem.vue @@ -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; - // 平滑过渡到新的移动方向 - 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 (feature && props.getVehicleStyle) { + // 获取车辆的当前heading值 + const vehicle = props.vehicles[id]; + const currentHeading = vehicle ? vehicle.heading : 0; + + // 只在heading发生变化时才更新样式,避免频繁更新导致闪烁 + if (!animData.lastHeading || Math.abs(animData.lastHeading - currentHeading) > 1) { + feature.setStyle(props.getVehicleStyle(id, currentSpeed, currentHeading)); + animData.lastHeading = currentHeading; + } } } } @@ -368,22 +320,45 @@ function ensureContinuousMovement(animData, vehicle, currentTime, deltaTime) { ]; } } - // 如果没有预测向量或预测向量太小,创建一个基于当前朝向的预测向量 + // 如果没有预测向量或预测向量太小,创建一个基于历史数据的预测向量 else if (!animData.predictionVector || (Math.abs(animData.predictionVector[0]) < 0.001 && Math.abs(animData.predictionVector[1]) < 0.001)) { - // 根据车辆朝向创建预测向量 - const headingRad = (animData.heading - 90) * Math.PI / 180; - const predX = Math.cos(headingRad) * animData.currentSpeed * 0.18; // 从0.15增加到0.18 - const predY = Math.sin(headingRad) * animData.currentSpeed * 0.18; - - animData.predictionVector = [predX, predY]; - - // 更新目标位置为当前位置加上预测向量 - animData.targetPosition = [ - animData.position[0] + predX * 15, // 从12增加到15 - animData.position[1] + predY * 15 - ]; + // 使用最近两个历史点计算预测向量 + if (animData.pathHistory && animData.pathHistory.length >= 2) { + const latestPoints = animData.pathHistory.slice(-2); + const dx = latestPoints[1].position[0] - latestPoints[0].position[0]; + const dy = latestPoints[1].position[1] - latestPoints[0].position[1]; + const len = Math.sqrt(dx*dx + dy*dy); + + if (len > 0.001) { + const normalizedDx = dx / len; + const normalizedDy = dy / len; + const predX = normalizedDx * animData.currentSpeed * 0.18; + const predY = normalizedDy * animData.currentSpeed * 0.18; + animData.predictionVector = [predX, predY]; + + // 更新目标位置为当前位置加上预测向量 + animData.targetPosition = [ + animData.position[0] + predX * 15, + 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() // 重置路径记录时间 diff --git a/src/components/map/controls/VehicleLabelSystem.vue b/src/components/map/controls/VehicleLabelSystem.vue index e3dcbca..d645552 100644 --- a/src/components/map/controls/VehicleLabelSystem.vue +++ b/src/components/map/controls/VehicleLabelSystem.vue @@ -1,10 +1,10 @@ - \ No newline at end of file diff --git a/src/components/map/controls/VehicleMovementControlRefactored.vue b/src/components/map/controls/VehicleMovementControlRefactored.vue index 4366f40..fec2a11 100644 --- a/src/components/map/controls/VehicleMovementControlRefactored.vue +++ b/src/components/map/controls/VehicleMovementControlRefactored.vue @@ -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); - } + // 处理车辆位置更新 + 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, // 正确应用旋转角度 }) }); } diff --git a/src/components/map/controls/VehicleStyleManager.vue b/src/components/map/controls/VehicleStyleManager.vue index 6c5e66e..a134b67 100644 --- a/src/components/map/controls/VehicleStyleManager.vue +++ b/src/components/map/controls/VehicleStyleManager.vue @@ -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 }); \ No newline at end of file diff --git a/src/components/map/info/eventlist.vue b/src/components/map/info/eventlist.vue index ddb3db4..f93af18 100644 --- a/src/components/map/info/eventlist.vue +++ b/src/components/map/info/eventlist.vue @@ -221,6 +221,7 @@ function updateCarList(newList) { const selectedCar = ref(null); function showDetail(car) { + // 移除对不存在的moveToTarget函数的调用 selectedCar.value = car; } diff --git a/src/utils/test_websocket.html b/src/utils/test_websocket.html index db98f1f..f5fd0c9 100644 --- a/src/utils/test_websocket.html +++ b/src/utils/test_websocket.html @@ -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 @@
-

冲突检测WebSocket测试工具

-

🚗 冲突检测WebSocket (原生协议)

+

🚗 冲突检测WebSocket

@@ -136,7 +124,7 @@
-

🎛️ 控制面板

+

🎛️ 控制面板

diff --git a/src/views/car/park/index.vue b/src/views/car/park/index.vue index 3f0a385..4e9991d 100644 --- a/src/views/car/park/index.vue +++ b/src/views/car/park/index.vue @@ -62,7 +62,7 @@ @@ -544,6 +544,7 @@ function handleRemoveImage() { /** 获取车辆类型名称 */ function getVehicleTypeName(typeId) { + // 保留此函数以兼容旧代码,但在表格中直接使用typeDisplayName return vehicleTypeMap.value[typeId] || ''; } diff --git a/src/views/car/type/index.vue b/src/views/car/type/index.vue index 888fbea..a77e3b5 100644 --- a/src/views/car/type/index.vue +++ b/src/views/car/type/index.vue @@ -115,7 +115,11 @@ - + + + @@ -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: "", })); diff --git a/src/views/platform/index.vue b/src/views/platform/index.vue index 42e6a12..65e34c6 100644 --- a/src/views/platform/index.vue +++ b/src/views/platform/index.vue @@ -70,20 +70,20 @@ - + - +
-->
@@ -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;