Merge branch 'main' of http://10.0.0.99:4000/Ren/airport-qingdao-vue3
This commit is contained in:
commit
72795efc0b
132
CLAUDE.md
132
CLAUDE.md
@ -1,132 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -19,6 +19,15 @@ export function exportCarRunInfo(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量查询车辆轨迹
|
||||||
|
export function batchQueryVehicleTrajectory(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/trajectory/batch',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 根据车辆ID查询车辆运动信息列表
|
// 根据车辆ID查询车辆运动信息列表
|
||||||
export function listCarRunInfoByVehicleId(vehicleId, query) {
|
export function listCarRunInfoByVehicleId(vehicleId, query) {
|
||||||
return request({
|
return request({
|
||||||
@ -28,6 +37,42 @@ export function listCarRunInfoByVehicleId(vehicleId, query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据车辆ID查询车辆轨迹
|
||||||
|
export function getVehicleTrajectory(vehicleId, query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/trajectory/' + vehicleId,
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车辆ID查询轨迹统计信息
|
||||||
|
export function getTrajectoryStatistics(vehicleId, query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/trajectory/' + vehicleId + '/statistics',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车辆ID查询轨迹回放数据
|
||||||
|
export function getTrajectoryPlayback(vehicleId, query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/trajectory/' + vehicleId + '/playback',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据车辆ID查询区域内轨迹
|
||||||
|
export function getAreaTrajectory(vehicleId, query) {
|
||||||
|
return request({
|
||||||
|
url: '/system/vehicle_location/trajectory/' + vehicleId + '/area',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 根据车牌号查询车辆运动信息列表
|
// 根据车牌号查询车辆运动信息列表
|
||||||
export function listCarRunInfoByLicensePlate(licensePlate, query) {
|
export function listCarRunInfoByLicensePlate(licensePlate, query) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<!-- TrackPlayback.vue(轨迹回放页面UI还原) -->
|
<!-- TrackPlayback.vue(轨迹回放页面) -->
|
||||||
<template>
|
<template>
|
||||||
<div class="track-playback-content">
|
<div class="track-playback-content">
|
||||||
<!-- 左侧搜索+列表 -->
|
<!-- 左侧搜索+列表 -->
|
||||||
@ -49,26 +49,52 @@
|
|||||||
<div class="map-img-placeholder"></div>
|
<div class="map-img-placeholder"></div>
|
||||||
<!-- 轨迹详情卡片(左上角) -->
|
<!-- 轨迹详情卡片(左上角) -->
|
||||||
<div class="track-detail-panel">
|
<div class="track-detail-panel">
|
||||||
<!-- 第一行 -->
|
<!-- 第一行:标题和回放按钮两端对齐 -->
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="dot"></span>
|
<div class="header-left">
|
||||||
<span class="panel-title">轨迹详情</span>
|
<span class="dot"></span>
|
||||||
|
<span class="panel-title">轨迹详情</span>
|
||||||
|
</div>
|
||||||
<el-button size="small" type="primary" class="replay-btn" @click="handleReplay">回放</el-button>
|
<el-button size="small" type="primary" class="replay-btn" @click="handleReplay">回放</el-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 第二行 -->
|
|
||||||
<div class="panel-info">
|
<!-- 第二行:车辆信息一行显示 -->
|
||||||
<div class="info-row">
|
<div class="vehicle-stats-row">
|
||||||
<span class="carno">{{ activeTask?.licensePlate || activeVehicleId || "未选择车辆" }}</span>
|
<div class="stat-item">
|
||||||
<span class="info-item">最大时速 <b>{{ trackDetails.maxSpeed }}km/h</b></span>
|
<span class="stat-value">{{ licensePlate }}</span>
|
||||||
<span class="info-item">平均时速 <b>{{ trackDetails.averageSpeed }}km/h</b></span>
|
<span class="stat-label">车牌号</span>
|
||||||
<span class="info-item">总里程 <b>{{ trackDetails.totalDistance }}km</b></span>
|
|
||||||
<span class="info-item">耗时 <b>{{ trackDetails.totalTime }}min</b></span>
|
|
||||||
<span class="info-item warn">冲突告警 <b>{{ trackDetails.warnings }}</b></span>
|
|
||||||
<span class="info-item prewarn">冲突预警 <b>{{ trackDetails.preWarnings }}</b></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value">{{ trackDetails.maxSpeed }}km/h</span>
|
||||||
|
<span class="stat-label">最大时速</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value">{{ trackDetails.averageSpeed }}km/h</span>
|
||||||
|
<span class="stat-label">平均时速</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value">{{ trackDetails.totalDistance }}km</span>
|
||||||
|
<span class="stat-label">行驶总里程</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value">{{ trackDetails.totalTime }}min</span>
|
||||||
|
<span class="stat-label">行驶耗时</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item warn">
|
||||||
|
<span class="stat-value">{{ trackDetails.warnings }}</span>
|
||||||
|
<span class="stat-label">冲突告警</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item prewarn">
|
||||||
|
<span class="stat-value">{{ trackDetails.preWarnings }}</span>
|
||||||
|
<span class="stat-label">冲突预警</span>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="current-time">
|
||||||
|
<span class="time-value">13:46:53</span>
|
||||||
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<!-- 第三行:进度条 -->
|
|
||||||
<div class="panel-progress">
|
<!-- 第三行:进度条和倍速 -->
|
||||||
|
<div class="progress-row">
|
||||||
<div class="progress-bar-wrap"
|
<div class="progress-bar-wrap"
|
||||||
@mousemove="handleProgressHover"
|
@mousemove="handleProgressHover"
|
||||||
@mouseleave="showTooltip = false"
|
@mouseleave="showTooltip = false"
|
||||||
@ -85,35 +111,34 @@
|
|||||||
:style="{left: flag.percent+'%'}"
|
:style="{left: flag.percent+'%'}"
|
||||||
:title="flag.label"
|
:title="flag.label"
|
||||||
>
|
>
|
||||||
<svg width="14" height="18" viewBox="0 0 14 18">
|
<div class="flag-icon"></div>
|
||||||
<polygon points="2,2 12,5 2,8" fill="#E34D59"/>
|
|
||||||
<rect x="1" y="2" width="2" height="14" fill="#E34D59"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 鼠标悬浮提示 -->
|
<!-- 鼠标悬浮提示 -->
|
||||||
<div v-if="showTooltip" class="progress-tooltip" :style="{left: tooltipLeft+'px'}">
|
<div v-if="showTooltip" class="progress-tooltip" :style="{left: tooltipLeft+'px'}">
|
||||||
{{ tooltipTime }}
|
{{ tooltipTime }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bottom">
|
<div class="speed-select">
|
||||||
<span class="start-time">{{ trackDetails.startTime || "未开始" }}</span>
|
<el-dropdown @command="setSpeed">
|
||||||
<div class="speed-select">
|
<span class="el-dropdown-link">
|
||||||
<el-dropdown @command="setSpeed">
|
{{ speed }}x <i class="el-icon-arrow-down"></i>
|
||||||
<span class="el-dropdown-link">
|
</span>
|
||||||
{{ speed }}x <i class="el-icon-arrow-down"></i>
|
<template #dropdown>
|
||||||
</span>
|
<el-dropdown-menu>
|
||||||
<template #dropdown>
|
<el-dropdown-item command="1">1x</el-dropdown-item>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-item command="2">2x</el-dropdown-item>
|
||||||
<el-dropdown-item command="1">1x</el-dropdown-item>
|
<el-dropdown-item command="4">4x</el-dropdown-item>
|
||||||
<el-dropdown-item command="2">2x</el-dropdown-item>
|
</el-dropdown-menu>
|
||||||
<el-dropdown-item command="4">4x</el-dropdown-item>
|
</template>
|
||||||
</el-dropdown-menu>
|
</el-dropdown>
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
|
||||||
<span class="end-time">{{ trackDetails.endTime || "未结束" }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 第四行:开始和结束时间 -->
|
||||||
|
<div class="time-row">
|
||||||
|
<span class="start-time">开始时间 {{ trackDetails.startTime || "2025-05-01 12:09:49" }}</span>
|
||||||
|
<span class="end-time">结束时间 {{ trackDetails.endTime || "2025-05-01 14:09:49" }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +154,8 @@ import {
|
|||||||
listCarRunInfoByVehicleId,
|
listCarRunInfoByVehicleId,
|
||||||
listCarRunInfoByLicensePlate,
|
listCarRunInfoByLicensePlate,
|
||||||
getLatestLocationByVehicleId,
|
getLatestLocationByVehicleId,
|
||||||
getLatestLocationByLicensePlate
|
getLatestLocationByLicensePlate,
|
||||||
|
batchQueryVehicleTrajectory
|
||||||
} from "@/api/monitor/carRunInfo";
|
} from "@/api/monitor/carRunInfo";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
@ -174,6 +200,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
// 轨迹详情数据
|
// 轨迹详情数据
|
||||||
const trackDetails = ref({
|
const trackDetails = ref({
|
||||||
|
licensePlate: "",
|
||||||
maxSpeed: "0",
|
maxSpeed: "0",
|
||||||
averageSpeed: "0",
|
averageSpeed: "0",
|
||||||
totalDistance: "0",
|
totalDistance: "0",
|
||||||
@ -226,16 +253,19 @@ async function getRunInfoList() {
|
|||||||
// 计算平均速度
|
// 计算平均速度
|
||||||
const avgSpeed = (vehicleData.reduce((sum, point) => sum + point.speed, 0) / vehicleData.length).toFixed(2);
|
const avgSpeed = (vehicleData.reduce((sum, point) => sum + point.speed, 0) / vehicleData.length).toFixed(2);
|
||||||
|
|
||||||
|
// 确保licensePlate字段存在
|
||||||
|
const licensePlate = firstPoint.licensePlate || `车辆${vehicleId}`;
|
||||||
|
|
||||||
tasksList.push({
|
tasksList.push({
|
||||||
id: parseInt(vehicleId),
|
id: parseInt(vehicleId),
|
||||||
no: vehicleId,
|
no: vehicleId,
|
||||||
name: `车辆${firstPoint.licensePlate}轨迹`,
|
name: `车辆${licensePlate}轨迹`,
|
||||||
time: `${firstPoint.timestamp}--${lastPoint.timestamp}`,
|
time: `${firstPoint.timestamp}--${lastPoint.timestamp}`,
|
||||||
start: `经度${firstPoint.longitude},纬度${firstPoint.latitude}`,
|
start: `经度${firstPoint.longitude},纬度${firstPoint.latitude}`,
|
||||||
end: `经度${lastPoint.longitude},纬度${lastPoint.latitude}`,
|
end: `经度${lastPoint.longitude},纬度${lastPoint.latitude}`,
|
||||||
status: "已完成",
|
status: "已完成",
|
||||||
speed: `${avgSpeed}km/h`,
|
speed: `${avgSpeed}km/h`,
|
||||||
licensePlate: firstPoint.licensePlate,
|
licensePlate: licensePlate,
|
||||||
points: vehicleData
|
points: vehicleData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -245,6 +275,8 @@ async function getRunInfoList() {
|
|||||||
// 设置默认选中第一项
|
// 设置默认选中第一项
|
||||||
if (tasks.value.length > 0) {
|
if (tasks.value.length > 0) {
|
||||||
activeId.value = tasks.value[0].id;
|
activeId.value = tasks.value[0].id;
|
||||||
|
// 显示第一个任务的轨迹详情
|
||||||
|
selectTask(tasks.value[0]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(response.msg || '获取车辆运动信息列表失败');
|
ElMessage.error(response.msg || '获取车辆运动信息列表失败');
|
||||||
@ -257,40 +289,88 @@ async function getRunInfoList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据车辆ID获取轨迹详情
|
|
||||||
async function getTrackDetailByVehicleId(vehicleId) {
|
|
||||||
if (!vehicleId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await listCarRunInfoByVehicleId(vehicleId, {});
|
|
||||||
if (response.code === 200) {
|
|
||||||
const trackData = response.data || [];
|
|
||||||
// 处理轨迹数据...
|
|
||||||
console.log('车辆轨迹数据:', trackData);
|
|
||||||
// 这里可以处理轨迹点、告警点等
|
|
||||||
// 假设返回的数据格式与tasks中的points一致
|
|
||||||
processTrackPoints(trackData);
|
|
||||||
} else {
|
|
||||||
ElMessage.error(response.msg || '获取车辆轨迹详情失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取车辆轨迹详情异常', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据车牌号获取车辆最新位置
|
|
||||||
async function getLatestLocation(licensePlate) {
|
|
||||||
if (!licensePlate) return;
|
|
||||||
|
// 批量查询车辆轨迹回放数据
|
||||||
|
async function getBatchTrajectoryPlayback(vehicleIds, timeRange) {
|
||||||
|
if (!vehicleIds || vehicleIds.length === 0) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await getLatestLocationByLicensePlate(licensePlate);
|
const requestData = {
|
||||||
|
vehicleIds: vehicleIds,
|
||||||
|
startTime: timeRange?.[0] || '',
|
||||||
|
endTime: timeRange?.[1] || '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 1000 // 获取更多数据用于回放
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await batchQueryVehicleTrajectory(requestData);
|
||||||
|
console.log('批量轨迹查询结果:', response);
|
||||||
|
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
const locationData = response.data;
|
const trajectoryData = response.data || response.rows || [];
|
||||||
// 处理位置数据...
|
|
||||||
console.log('车辆最新位置:', locationData);
|
// 按车辆ID分组处理轨迹数据
|
||||||
|
const groupedTrajectories = {};
|
||||||
|
trajectoryData.forEach(point => {
|
||||||
|
if (!groupedTrajectories[point.vehicleId]) {
|
||||||
|
groupedTrajectories[point.vehicleId] = [];
|
||||||
|
}
|
||||||
|
groupedTrajectories[point.vehicleId].push(point);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新任务列表,添加轨迹回放数据
|
||||||
|
const updatedTasks = [];
|
||||||
|
Object.keys(groupedTrajectories).forEach(vehicleId => {
|
||||||
|
const points = groupedTrajectories[vehicleId];
|
||||||
|
points.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||||
|
|
||||||
|
const firstPoint = points[0];
|
||||||
|
const lastPoint = points[points.length - 1];
|
||||||
|
const avgSpeed = (points.reduce((sum, p) => sum + (p.speed || 0), 0) / points.length).toFixed(2);
|
||||||
|
|
||||||
|
// 确保licensePlate字段存在
|
||||||
|
const licensePlate = firstPoint.licensePlate || `车辆${vehicleId}`;
|
||||||
|
|
||||||
|
updatedTasks.push({
|
||||||
|
id: parseInt(vehicleId),
|
||||||
|
no: vehicleId,
|
||||||
|
name: `车辆${licensePlate}轨迹回放`,
|
||||||
|
time: `${firstPoint.timestamp}--${lastPoint.timestamp}`,
|
||||||
|
start: `经度${firstPoint.longitude},纬度${firstPoint.latitude}`,
|
||||||
|
end: `经度${lastPoint.longitude},纬度${lastPoint.latitude}`,
|
||||||
|
status: "回放就绪",
|
||||||
|
speed: `${avgSpeed}km/h`,
|
||||||
|
licensePlate: licensePlate,
|
||||||
|
points: points,
|
||||||
|
isPlayback: true // 标记为回放数据
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合并或替换任务列表
|
||||||
|
if (updatedTasks.length > 0) {
|
||||||
|
tasks.value = updatedTasks;
|
||||||
|
// 默认选中第一个并显示其轨迹详情
|
||||||
|
if (tasks.value.length > 0) {
|
||||||
|
activeId.value = tasks.value[0].id;
|
||||||
|
selectTask(tasks.value[0]);
|
||||||
|
}
|
||||||
|
ElMessage.success(`成功加载${updatedTasks.length}个车辆的轨迹回放数据`);
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('未找到轨迹回放数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.msg || '批量查询车辆轨迹失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取车辆最新位置异常', error);
|
console.error('批量查询车辆轨迹异常', error);
|
||||||
|
ElMessage.error('批量查询车辆轨迹异常');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,12 +386,29 @@ const filteredTasks = computed(() => {
|
|||||||
|
|
||||||
const activeTask = computed(() => tasks.value.find((t) => t.id === activeId.value));
|
const activeTask = computed(() => tasks.value.find((t) => t.id === activeId.value));
|
||||||
|
|
||||||
|
// 计算车牌号
|
||||||
|
const licensePlate = computed(() => {
|
||||||
|
if (activeTask.value?.licensePlate) {
|
||||||
|
return activeTask.value.licensePlate;
|
||||||
|
} else if (trackDetails.value.licensePlate) {
|
||||||
|
return trackDetails.value.licensePlate;
|
||||||
|
} else if (props.vehicle?.licensePlate) {
|
||||||
|
return props.vehicle.licensePlate;
|
||||||
|
}
|
||||||
|
return "暂无车牌号";
|
||||||
|
});
|
||||||
|
|
||||||
function selectTask(item) {
|
function selectTask(item) {
|
||||||
activeId.value = item.id;
|
activeId.value = item.id;
|
||||||
|
|
||||||
// 从任务中提取轨迹数据
|
// 从任务中提取轨迹数据
|
||||||
if (item.points && item.points.length > 0) {
|
if (item.points && item.points.length > 0) {
|
||||||
processTrackPoints(item.points);
|
processTrackPoints(item.points);
|
||||||
|
|
||||||
|
// 更新轨迹详情中的车牌号
|
||||||
|
if (item.licensePlate) {
|
||||||
|
trackDetails.value.licensePlate = item.licensePlate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,6 +417,14 @@ function processTrackPoints(points) {
|
|||||||
// 按时间排序
|
// 按时间排序
|
||||||
points.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
points.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||||
|
|
||||||
|
// 获取第一个点的车牌号(如果存在)
|
||||||
|
const firstPoint = points[0];
|
||||||
|
const currentLicensePlate = trackDetails.value.licensePlate; // 保存当前车牌号
|
||||||
|
|
||||||
|
if (firstPoint && firstPoint.licensePlate) {
|
||||||
|
trackDetails.value.licensePlate = firstPoint.licensePlate;
|
||||||
|
}
|
||||||
|
|
||||||
// 计算最大速度
|
// 计算最大速度
|
||||||
const maxSpeed = Math.max(...points.map(p => p.speed)).toFixed(2);
|
const maxSpeed = Math.max(...points.map(p => p.speed)).toFixed(2);
|
||||||
|
|
||||||
@ -345,8 +450,9 @@ function processTrackPoints(points) {
|
|||||||
const endTime = new Date(points[points.length-1].timestamp);
|
const endTime = new Date(points[points.length-1].timestamp);
|
||||||
const totalMinutes = Math.round((endTime - startTime) / (60 * 1000));
|
const totalMinutes = Math.round((endTime - startTime) / (60 * 1000));
|
||||||
|
|
||||||
// 更新轨迹详情
|
// 更新轨迹详情,保留车牌号
|
||||||
trackDetails.value = {
|
trackDetails.value = {
|
||||||
|
licensePlate: firstPoint?.licensePlate || currentLicensePlate || "暂无车牌号", // 使用新车牌号或保留原有车牌号
|
||||||
maxSpeed: maxSpeed,
|
maxSpeed: maxSpeed,
|
||||||
averageSpeed: avgSpeed,
|
averageSpeed: avgSpeed,
|
||||||
totalDistance: distanceKm,
|
totalDistance: distanceKm,
|
||||||
@ -373,30 +479,50 @@ function processTrackPoints(points) {
|
|||||||
// 回放按钮点击处理函数
|
// 回放按钮点击处理函数
|
||||||
function handleReplay() {
|
function handleReplay() {
|
||||||
// 获取当前选中任务
|
// 获取当前选中任务
|
||||||
const activeTask = tasks.value.find(t => t.id === activeId.value);
|
const currentTask = tasks.value.find(t => t.id === activeId.value);
|
||||||
|
|
||||||
if (activeTask) {
|
if (currentTask) {
|
||||||
if (activeTask.points && activeTask.points.length > 0) {
|
if (currentTask.points && currentTask.points.length > 0) {
|
||||||
// 如果任务已包含轨迹点数据,直接处理
|
// 如果任务已包含轨迹点数据,直接处理
|
||||||
processTrackPoints(activeTask.points);
|
processTrackPoints(currentTask.points);
|
||||||
ElMessage.success('开始轨迹回放');
|
ElMessage.success('开始轨迹回放');
|
||||||
} else {
|
} else {
|
||||||
// 如果没有轨迹点数据,根据车辆ID获取
|
// 使用批量查询获取轨迹回放数据
|
||||||
getTrackDetailByVehicleId(activeTask.id);
|
const vehicleIds = [currentTask.id.toString()];
|
||||||
|
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||||
}
|
}
|
||||||
} else if (queryParams.value.vehicleId) {
|
} else if (queryParams.value.vehicleId) {
|
||||||
// 使用查询参数中的车辆ID
|
// 使用查询参数中的车辆ID进行批量查询
|
||||||
getTrackDetailByVehicleId(queryParams.value.vehicleId);
|
const vehicleIds = [queryParams.value.vehicleId];
|
||||||
|
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||||
} else if (props.vehicle?.carId) {
|
} else if (props.vehicle?.carId) {
|
||||||
// 使用传入的车辆ID
|
// 使用传入的车辆ID进行批量查询
|
||||||
getTrackDetailByVehicleId(props.vehicle.carId);
|
const vehicleIds = [props.vehicle.carId];
|
||||||
|
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||||
} else {
|
} else {
|
||||||
// 获取列表数据
|
// 如果有多个车辆,批量查询所有车辆的轨迹
|
||||||
getRunInfoList();
|
if (tasks.value.length > 0) {
|
||||||
ElMessage.info('请先选择一个车辆任务');
|
const vehicleIds = tasks.value.map(task => task.id.toString());
|
||||||
|
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||||
|
} else {
|
||||||
|
// 获取列表数据
|
||||||
|
getRunInfoList();
|
||||||
|
ElMessage.info('请先选择一个车辆任务');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量加载所有车辆轨迹回放数据
|
||||||
|
function loadAllTrajectoryPlayback() {
|
||||||
|
if (tasks.value.length === 0) {
|
||||||
|
ElMessage.warning('暂无车辆数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vehicleIds = tasks.value.map(task => task.id.toString());
|
||||||
|
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||||
|
}
|
||||||
|
|
||||||
// 导出车辆运动信息
|
// 导出车辆运动信息
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
try {
|
try {
|
||||||
@ -466,9 +592,28 @@ function setSpeed(val) {
|
|||||||
|
|
||||||
// 页面加载时获取数据
|
// 页面加载时获取数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getRunInfoList();
|
// 如果从父组件传入了车辆信息,初始化车牌号和默认数据
|
||||||
// getLatestLocation("鲁B579");
|
if (props.vehicle) {
|
||||||
|
if (props.vehicle.licensePlate) {
|
||||||
|
trackDetails.value.licensePlate = props.vehicle.licensePlate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化一个默认的轨迹详情数据
|
||||||
|
trackDetails.value = {
|
||||||
|
licensePlate: props.vehicle.licensePlate || "暂无车牌号",
|
||||||
|
maxSpeed: "0",
|
||||||
|
averageSpeed: "0",
|
||||||
|
totalDistance: "0",
|
||||||
|
totalTime: "0",
|
||||||
|
warnings: "0",
|
||||||
|
preWarnings: "0",
|
||||||
|
startTime: "",
|
||||||
|
endTime: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getRunInfoList();
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -616,126 +761,239 @@ onMounted(() => {
|
|||||||
.panel-header {
|
.panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
justify-content: space-between;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
.dot {
|
|
||||||
width: 10px;
|
.header-left {
|
||||||
height: 10px;
|
|
||||||
background: #347ae2;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.panel-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #4ea1ff;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
.replay-btn {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.panel-info {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 18px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
gap: 10px;
|
||||||
.carno {
|
|
||||||
color: #4ea1ff;
|
.dot {
|
||||||
font-weight: bold;
|
width: 10px;
|
||||||
font-size: 15px;
|
height: 10px;
|
||||||
|
background: #347ae2;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.info-item {
|
|
||||||
color: #b0b8c9;
|
.panel-title {
|
||||||
b { color: #fff; font-weight: 500; }
|
font-size: 16px;
|
||||||
&.warn b { color: #e34d59; }
|
font-weight: bold;
|
||||||
&.prewarn b { color: #f7b500; }
|
color: #4ea1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-btn {
|
||||||
|
// 按钮样式保持默认
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 第二行:车辆信息一行显示
|
||||||
|
.vehicle-stats-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.vehicle-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
// min-width: 80px;
|
||||||
|
padding:3px 10px;
|
||||||
|
|
||||||
|
|
||||||
|
.vehicle-number {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8e99;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
// min-width: 60px;
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8e99;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warn .stat-value {
|
||||||
|
color: #e34d59;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.prewarn .stat-value {
|
||||||
|
color: #f7b500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(52, 122, 226, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
min-width: 80px;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4ea1ff;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.panel-progress {
|
// 第三行:进度条和倍速
|
||||||
margin-top: 8px;
|
.progress-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.progress-bar-wrap {
|
.progress-bar-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.progress-bar-bg {
|
|
||||||
position: absolute;
|
.speed-select {
|
||||||
left: 0; top: 0; right: 0; bottom: 0;
|
.el-dropdown-link {
|
||||||
background: #343744;
|
color: #4ea1ff;
|
||||||
border-radius: 4px;
|
cursor: pointer;
|
||||||
}
|
font-weight: bold;
|
||||||
.progress-bar-fg {
|
font-size: 14px;
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0; bottom: 0;
|
|
||||||
background: linear-gradient(90deg, #347ae2, #4ea1ff);
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 8px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.progress-thumb {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border: 3px solid #347ae2;
|
|
||||||
border-radius: 50%;
|
|
||||||
z-index: 2;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 2px 8px rgba(52,122,226,0.12);
|
|
||||||
}
|
|
||||||
.progress-flag {
|
|
||||||
position: absolute;
|
|
||||||
top: -14px;
|
|
||||||
z-index: 3;
|
|
||||||
width: 14px;
|
|
||||||
height: 18px;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.progress-tooltip {
|
|
||||||
position: absolute;
|
|
||||||
top: -32px;
|
|
||||||
background: #23263a;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-shadow: 0 2px 8px rgba(52,122,226,0.10);
|
|
||||||
z-index: 10;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.progress-bottom {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #b0b8c9;
|
|
||||||
.speed-select {
|
|
||||||
margin: 0 16px;
|
|
||||||
.el-dropdown-link {
|
|
||||||
color: #4ea1ff;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.start-time, .end-time {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #b0b8c9;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 第四行:开始和结束时间
|
||||||
|
.time-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #b0b8c9;
|
||||||
|
|
||||||
|
.start-time, .end-time {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #b0b8c9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度条相关样式
|
||||||
|
.progress-bar-bg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; right: 0; bottom: 0;
|
||||||
|
background: #343744;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
background: linear-gradient(90deg, #347ae2, #4ea1ff);
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 3px solid #347ae2;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(52,122,226,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-flag {
|
||||||
|
position: absolute;
|
||||||
|
top: -16px;
|
||||||
|
z-index: 3;
|
||||||
|
width: 16px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.flag-icon {
|
||||||
|
position: relative;
|
||||||
|
width: 2px;
|
||||||
|
height: 18px;
|
||||||
|
background: #e34d59;
|
||||||
|
|
||||||
|
// 旗杆
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 2px;
|
||||||
|
height: 18px;
|
||||||
|
background: #e34d59;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小红旗
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 10px solid #e34d59;
|
||||||
|
border-top: 3px solid transparent;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
// 添加小三角形缺口效果
|
||||||
|
clip-path: polygon(0 0, 80% 0, 70% 50%, 80% 100%, 0 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
top: -32px;
|
||||||
|
background: #23263a;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 2px 8px rgba(52,122,226,0.10);
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -22,21 +22,15 @@ const vehicleMotionHistory = ref({}); // 存储车辆移动历史
|
|||||||
const PREDICTION_THRESHOLD = 50; // 从100ms降低到50ms,更早开始预测
|
const PREDICTION_THRESHOLD = 50; // 从100ms降低到50ms,更早开始预测
|
||||||
const MAX_PREDICTION_TIME = 15000; // 从10000ms增加到15000ms,延长预测时间
|
const MAX_PREDICTION_TIME = 15000; // 从10000ms增加到15000ms,延长预测时间
|
||||||
|
|
||||||
// 物理模拟参数
|
// 物理模拟参数 - 优化为完全平滑移动
|
||||||
const ACCELERATION = 0.15; // 加速度系数
|
const SPEED_SMOOTHING = 0.3; // 大幅降低速度平滑系数,让速度快速响应
|
||||||
const DECELERATION = 0.25; // 减速度系数
|
const POSITION_SMOOTHING = 1.0; // 设为1.0,每帧直接移动到理想位置,确保完全平滑
|
||||||
const INERTIA_FACTOR = 0.99; // 从0.985增加到0.99,进一步增强惯性效果,减少抖动
|
const MIN_MOVE_THRESHOLD = 0.0000001; // 极小的移动阈值
|
||||||
const PREDICTION_STRENGTH = 3; // 从4降低到3,进一步减少预测激进性,避免过度预测造成抖动
|
const CONTINUOUS_MOVEMENT = true; // 启用连续移动模式
|
||||||
const SPEED_SMOOTHING = 0.98; // 从0.97增加到0.98,进一步增强速度平滑效果
|
const MIN_SPEED = 1.0; // 适中的最低保持速度
|
||||||
const POSITION_SMOOTHING = 0.06; // 从0.08降低到0.06,进一步减小单次位置变化幅度,增强平滑度
|
const PREDICTION_DECAY_RATE = 0.99; // 更快的预测衰减
|
||||||
const MIN_MOVE_THRESHOLD = 0.000005; // 从0.00001降低到0.000005,确保更平滑的微小移动
|
const CONTINUOUS_MOVEMENT_THRESHOLD = 50; // 大幅降低阈值,几乎立即触发连续移动
|
||||||
const CONTINUOUS_MOVEMENT = true; // 保持启用连续移动模式
|
const INTERPOLATION_STEPS = 60; // 每秒60帧的插值步数
|
||||||
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) {
|
function easeInOutQuad(t) {
|
||||||
@ -78,7 +72,7 @@ function startAnimationLoop() {
|
|||||||
animationFrameId.value = requestAnimationFrame(animate);
|
animationFrameId.value = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新车辆动画
|
// 更新车辆动画 - 简化版本,专注于平滑移动
|
||||||
function updateVehicleAnimations(deltaTime) {
|
function updateVehicleAnimations(deltaTime) {
|
||||||
if (!props.vehicleSource || !props.map) return;
|
if (!props.vehicleSource || !props.map) return;
|
||||||
|
|
||||||
@ -91,176 +85,91 @@ function updateVehicleAnimations(deltaTime) {
|
|||||||
const animData = vehicleAnimations.value[id];
|
const animData = vehicleAnimations.value[id];
|
||||||
if (!animData) return;
|
if (!animData) return;
|
||||||
|
|
||||||
// 确保车辆总是在移动,即使没有新的位置更新
|
// 确保车辆持续移动
|
||||||
ensureContinuousMovement(animData, vehicle, currentTime, deltaTime);
|
ensureContinuousMovement(animData, vehicle, currentTime, deltaTime);
|
||||||
|
|
||||||
const timeSinceLastUpdate = currentTime - animData.lastUpdated;
|
// 平滑速度过渡
|
||||||
|
|
||||||
// 1. 计算目标位置(基于预测)
|
|
||||||
let targetPosition = [...animData.targetPosition];
|
|
||||||
|
|
||||||
// 平滑速度过渡 - 当前速度逐渐接近目标速度
|
|
||||||
if (animData.currentSpeed === undefined) {
|
if (animData.currentSpeed === undefined) {
|
||||||
animData.currentSpeed = animData.speed;
|
animData.currentSpeed = animData.speed;
|
||||||
} else {
|
} else {
|
||||||
// 使用更平滑的速度过渡
|
animData.currentSpeed = animData.currentSpeed * SPEED_SMOOTHING +
|
||||||
const speedSmoothingFactor = vehicle.speedViolation ? 0.98 : SPEED_SMOOTHING;
|
animData.speed * (1 - SPEED_SMOOTHING);
|
||||||
animData.currentSpeed = animData.currentSpeed * speedSmoothingFactor +
|
|
||||||
animData.speed * (1 - speedSmoothingFactor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentSpeed = animData.currentSpeed;
|
let currentSpeed = Math.max(animData.currentSpeed, MIN_SPEED);
|
||||||
|
|
||||||
// 根据时间应用不同程度的预测
|
// 计算到目标位置的向量
|
||||||
if (timeSinceLastUpdate > PREDICTION_THRESHOLD && animData.predictionVector) {
|
const dx = animData.targetPosition[0] - animData.position[0];
|
||||||
// 计算预测因子,随时间增长但有上限
|
const dy = animData.targetPosition[1] - animData.position[1];
|
||||||
const predictTime = Math.min(timeSinceLastUpdate, MAX_PREDICTION_TIME);
|
|
||||||
const predictFactor = (predictTime - PREDICTION_THRESHOLD) / 1000 * 0.6; // 从0.8降低到0.6,减少预测激进性
|
|
||||||
|
|
||||||
// 应用预测向量,考虑当前速度
|
|
||||||
const speedFactor = Math.min(1.0, currentSpeed / 30); // 从1.2/25降低到1.0/30,减少高速预测
|
|
||||||
|
|
||||||
// 计算预测位置
|
|
||||||
const predictedPosition = [
|
|
||||||
animData.targetPosition[0] + animData.predictionVector[0] * predictFactor * speedFactor,
|
|
||||||
animData.targetPosition[1] + animData.predictionVector[1] * predictFactor * speedFactor
|
|
||||||
];
|
|
||||||
|
|
||||||
// 平滑过渡到预测位置
|
|
||||||
targetPosition = predictedPosition;
|
|
||||||
|
|
||||||
// 预测时逐渐降低速度,但不要完全停止
|
|
||||||
if (predictTime > 2000) {
|
|
||||||
// 长时间无更新,缓慢降低速度,但保持最低速度
|
|
||||||
const slowdownFactor = Math.max(0.6, 1 - (predictTime - 2000) / 15000); // 从0.5/10000调整为0.6/15000,减缓减速率
|
|
||||||
currentSpeed = Math.max(currentSpeed * slowdownFactor, MIN_SPEED); // 确保最低速度不为0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 计算当前位置到目标位置的向量
|
|
||||||
const dx = targetPosition[0] - animData.position[0];
|
|
||||||
const dy = targetPosition[1] - animData.position[1];
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
// 忽略过小的位置更新,减少抖动
|
// 计算基于速度和时间的理想移动距离
|
||||||
if (distance < MIN_MOVE_THRESHOLD && !CONTINUOUS_MOVEMENT) {
|
const idealDistance = (currentSpeed * 1000 / 3600) * (deltaTime / 1000);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 如果距离足够远或启用了连续移动,应用平滑移动
|
// 始终移动,确保完全平滑 - 最终优化版本
|
||||||
if (distance > MIN_MOVE_THRESHOLD || CONTINUOUS_MOVEMENT) {
|
let moveDistance;
|
||||||
// 计算理想移动距离(基于速度和时间)
|
|
||||||
const idealDistance = (currentSpeed * 1000 / 3600) * (deltaTime / 1000);
|
// 简化移动逻辑,确保始终平滑移动
|
||||||
|
if (distance > 0.0001) {
|
||||||
// 根据距离动态调整移动比例
|
// 有明确目标时,计算合适的移动距离
|
||||||
let moveRatio;
|
if (distance > idealDistance) {
|
||||||
|
// 距离较远,按理想速度移动
|
||||||
// 超速车辆使用更平滑的移动
|
moveDistance = idealDistance;
|
||||||
const positionSmoothingFactor = vehicle.speedViolation ? POSITION_SMOOTHING * 0.7 : POSITION_SMOOTHING;
|
|
||||||
|
|
||||||
if (distance < idealDistance * 1.2) { // 从1.5降低到1.2,更早开始减速
|
|
||||||
// 接近目标时减速(使用更强的缓动)
|
|
||||||
moveRatio = Math.min(positionSmoothingFactor * 1.2, idealDistance / distance); // 从1.5降低到1.2
|
|
||||||
moveRatio = easeOutQuint(moveRatio);
|
|
||||||
} else {
|
} else {
|
||||||
// 正常行驶时使用标准缓动
|
// 距离较近,使用更激进的移动策略确保快速到达
|
||||||
moveRatio = Math.min(positionSmoothingFactor, idealDistance / distance);
|
// 在16ms内(约60fps)移动更多距离,避免停顿
|
||||||
moveRatio = easeOutCubic(moveRatio);
|
moveDistance = Math.max(distance * 0.5, idealDistance * 0.3);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 已到达目标或距离极小,使用预测向量保持移动
|
||||||
|
moveDistance = idealDistance * 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算移动方向和更新位置
|
||||||
|
if (distance > 0.0001) {
|
||||||
|
const moveRatio = Math.min(moveDistance / distance, 1.0);
|
||||||
|
|
||||||
|
// 直接更新位置
|
||||||
|
animData.position[0] += dx * moveRatio;
|
||||||
|
animData.position[1] += dy * moveRatio;
|
||||||
|
} else if (animData.predictionVector) {
|
||||||
|
// 使用预测向量继续移动
|
||||||
|
const predLen = Math.sqrt(
|
||||||
|
animData.predictionVector[0] * animData.predictionVector[0] +
|
||||||
|
animData.predictionVector[1] * animData.predictionVector[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (predLen > 0) {
|
||||||
|
animData.position[0] += (animData.predictionVector[0] / predLen) * moveDistance;
|
||||||
|
animData.position[1] += (animData.predictionVector[1] / predLen) * moveDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新地图上的要素位置
|
||||||
|
const feature = props.vehicleSource.getFeatureById(id);
|
||||||
|
if (feature) {
|
||||||
|
feature.getGeometry().setCoordinates(animData.position);
|
||||||
|
|
||||||
|
// 更新车辆对象中的位置信息
|
||||||
|
if (props.vehicles[id]) {
|
||||||
|
props.vehicles[id].position = animData.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复逻辑:确保最小移动和惯性移动是互斥的
|
// 更新车辆样式
|
||||||
// 如果距离很小且有预测向量,使用预测向量进行微小移动
|
if (props.getVehicleStyle) {
|
||||||
if (distance < MIN_MOVE_THRESHOLD && animData.predictionVector) {
|
const currentHeading = vehicle ? vehicle.heading : 0;
|
||||||
// 使用预测向量作为移动方向,但使用很小的移动量
|
|
||||||
const minMoveAmount = (currentSpeed * 1000 / 3600) * (deltaTime / 1000) * 0.05; // 从0.1降低到0.05,减小微小移动幅度
|
|
||||||
|
|
||||||
// 归一化预测向量
|
if (!animData.lastHeading || Math.abs(animData.lastHeading - currentHeading) > 1) {
|
||||||
const predLen = Math.sqrt(
|
feature.setStyle(props.getVehicleStyle(id, currentSpeed, currentHeading));
|
||||||
animData.predictionVector[0] * animData.predictionVector[0] +
|
animData.lastHeading = currentHeading;
|
||||||
animData.predictionVector[1] * animData.predictionVector[1]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (predLen > 0) {
|
|
||||||
const normPredX = animData.predictionVector[0] / predLen;
|
|
||||||
const normPredY = animData.predictionVector[1] / predLen;
|
|
||||||
|
|
||||||
// 应用最小移动量
|
|
||||||
animData.position[0] += normPredX * minMoveAmount;
|
|
||||||
animData.position[1] += normPredY * minMoveAmount;
|
|
||||||
|
|
||||||
// 保存本次移动向量用于下一帧惯性计算
|
|
||||||
animData.lastDx = normPredX * minMoveAmount;
|
|
||||||
animData.lastDy = normPredY * minMoveAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 距离足够大时,应用惯性移动
|
|
||||||
else {
|
|
||||||
if (animData.lastDx !== undefined && animData.lastDy !== undefined) {
|
|
||||||
// 超速车辆使用更强的惯性
|
|
||||||
const inertiaFactor = vehicle.speedViolation ? INERTIA_FACTOR * 1.05 : INERTIA_FACTOR; // 从1.1降低到1.05,减少超速车辆惯性过强
|
|
||||||
|
|
||||||
const inertiaX = animData.lastDx * inertiaFactor;
|
|
||||||
const inertiaY = animData.lastDy * inertiaFactor;
|
|
||||||
|
|
||||||
// 混合新方向和惯性方向
|
|
||||||
const newDx = dx * moveRatio;
|
|
||||||
const newDy = dy * moveRatio;
|
|
||||||
|
|
||||||
// 根据速度和距离调整惯性影响
|
|
||||||
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;
|
|
||||||
const nextY = animData.position[1] + newDy * (1 - inertiaWeight) + inertiaY * inertiaWeight;
|
|
||||||
|
|
||||||
// 应用位置更新
|
|
||||||
animData.position[0] = nextX;
|
|
||||||
animData.position[1] = nextY;
|
|
||||||
|
|
||||||
// 保存本次移动向量用于下一帧惯性计算
|
|
||||||
animData.lastDx = nextX - animData.position[0];
|
|
||||||
animData.lastDy = nextY - animData.position[1];
|
|
||||||
} else {
|
|
||||||
// 首次移动,没有惯性
|
|
||||||
animData.position[0] += dx * moveRatio;
|
|
||||||
animData.position[1] += dy * moveRatio;
|
|
||||||
|
|
||||||
// 初始化移动向量
|
|
||||||
animData.lastDx = dx * moveRatio;
|
|
||||||
animData.lastDy = dy * moveRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新地图上的要素位置
|
|
||||||
const feature = props.vehicleSource.getFeatureById(id);
|
|
||||||
if (feature) {
|
|
||||||
feature.getGeometry().setCoordinates(animData.position);
|
|
||||||
|
|
||||||
// 更新车辆对象中的位置信息,确保标签能够正确跟随
|
|
||||||
if (props.vehicles[id]) {
|
|
||||||
props.vehicles[id].position = animData.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新车辆样式,传递正确的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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前速度用于下一帧
|
// 保存当前速度
|
||||||
animData.currentSpeed = currentSpeed;
|
animData.currentSpeed = currentSpeed;
|
||||||
|
|
||||||
// 更新历史轨迹
|
// 简化的历史轨迹更新
|
||||||
updateVehiclePathHistory(id, animData, currentTime);
|
updateVehiclePathHistory(id, animData, currentTime);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -282,8 +191,8 @@ function updateVehiclePathHistory(id, animData, currentTime) {
|
|||||||
speed: animData.currentSpeed
|
speed: animData.currentSpeed
|
||||||
});
|
});
|
||||||
|
|
||||||
// 增加历史轨迹长度,提供更多数据点用于预测
|
// 保持最多10个历史点
|
||||||
if (animData.pathHistory.length > PATH_PREDICTION_POINTS + 4) {
|
if (animData.pathHistory.length > 10) {
|
||||||
animData.pathHistory.shift();
|
animData.pathHistory.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,92 +200,46 @@ function updateVehiclePathHistory(id, animData, currentTime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保车辆持续移动,即使没有新的位置更新
|
// 确保车辆持续移动,即使没有新的位置更新 - 简化版本
|
||||||
function ensureContinuousMovement(animData, vehicle, currentTime, deltaTime) {
|
function ensureContinuousMovement(animData, vehicle, currentTime, deltaTime) {
|
||||||
// 检查上次更新时间,使用更低的阈值
|
|
||||||
const timeSinceLastUpdate = currentTime - animData.lastUpdated;
|
const timeSinceLastUpdate = currentTime - animData.lastUpdated;
|
||||||
|
|
||||||
// 如果超过阈值没有位置更新,且车辆当前速度大于0
|
// 如果超过阈值没有位置更新,创建简单的预测移动
|
||||||
if (timeSinceLastUpdate > CONTINUOUS_MOVEMENT_THRESHOLD && animData.currentSpeed > 0) {
|
if (timeSinceLastUpdate > CONTINUOUS_MOVEMENT_THRESHOLD && animData.currentSpeed > 0) {
|
||||||
// 如果启用了路径预测,使用历史轨迹预测未来路径
|
// 简单的预测:基于最后的移动方向继续移动
|
||||||
if (PATH_PREDICTION_ENABLED && animData.pathHistory && animData.pathHistory.length >= 3) {
|
if (!animData.predictionVector) {
|
||||||
const predictedPath = predictFuturePath(animData);
|
// 如果没有预测向量,使用历史轨迹的最后方向
|
||||||
if (predictedPath) {
|
|
||||||
// 平滑过渡到预测向量,而不是直接替换
|
|
||||||
if (!animData.predictionVector) {
|
|
||||||
animData.predictionVector = predictedPath.vector;
|
|
||||||
} else {
|
|
||||||
// 平滑混合当前预测向量和新预测向量,使用更平滑的过渡
|
|
||||||
animData.predictionVector = [
|
|
||||||
animData.predictionVector[0] * 0.8 + predictedPath.vector[0] * 0.2, // 从0.7/0.3调整为0.8/0.2
|
|
||||||
animData.predictionVector[1] * 0.8 + predictedPath.vector[1] * 0.2
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 平滑过渡到预测位置,使用更平滑的过渡
|
|
||||||
animData.targetPosition = [
|
|
||||||
animData.targetPosition[0] * 0.8 + predictedPath.position[0] * 0.2, // 从0.7/0.3调整为0.8/0.2
|
|
||||||
animData.targetPosition[1] * 0.8 + predictedPath.position[1] * 0.2
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果没有预测向量或预测向量太小,创建一个基于历史数据的预测向量
|
|
||||||
else if (!animData.predictionVector ||
|
|
||||||
(Math.abs(animData.predictionVector[0]) < 0.001 && Math.abs(animData.predictionVector[1]) < 0.001)) {
|
|
||||||
|
|
||||||
// 使用最近两个历史点计算预测向量
|
|
||||||
if (animData.pathHistory && animData.pathHistory.length >= 2) {
|
if (animData.pathHistory && animData.pathHistory.length >= 2) {
|
||||||
const latestPoints = animData.pathHistory.slice(-2);
|
const latest = animData.pathHistory[animData.pathHistory.length - 1];
|
||||||
const dx = latestPoints[1].position[0] - latestPoints[0].position[0];
|
const previous = animData.pathHistory[animData.pathHistory.length - 2];
|
||||||
const dy = latestPoints[1].position[1] - latestPoints[0].position[1];
|
|
||||||
const len = Math.sqrt(dx*dx + dy*dy);
|
|
||||||
|
|
||||||
if (len > 0.001) {
|
const dx = latest.position[0] - previous.position[0];
|
||||||
const normalizedDx = dx / len;
|
const dy = latest.position[1] - previous.position[1];
|
||||||
const normalizedDy = dy / len;
|
const len = Math.sqrt(dx * dx + dy * dy);
|
||||||
const predX = normalizedDx * animData.currentSpeed * 0.18;
|
|
||||||
const predY = normalizedDy * animData.currentSpeed * 0.18;
|
if (len > 0) {
|
||||||
animData.predictionVector = [predX, predY];
|
// 创建单位方向向量
|
||||||
|
animData.predictionVector = [dx / len, dy / len];
|
||||||
// 更新目标位置为当前位置加上预测向量
|
|
||||||
animData.targetPosition = [
|
|
||||||
animData.position[0] + predX * 15,
|
|
||||||
animData.position[1] + predY * 15
|
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
// 如果历史点距离太近,使用默认的前进向量
|
// 默认向前移动
|
||||||
animData.predictionVector = [0, animData.currentSpeed * 0.18];
|
animData.predictionVector = [0, 1];
|
||||||
animData.targetPosition = [
|
|
||||||
animData.position[0],
|
|
||||||
animData.position[1] + animData.currentSpeed * 0.18 * 15
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有足够的历史数据,使用默认的前进向量
|
// 默认向前移动
|
||||||
animData.predictionVector = [0, animData.currentSpeed * 0.18];
|
animData.predictionVector = [0, 1];
|
||||||
animData.targetPosition = [
|
|
||||||
animData.position[0],
|
|
||||||
animData.position[1] + animData.currentSpeed * 0.18 * 15
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 预测向量存在但需要缓慢衰减,避免突然停止
|
|
||||||
// 使用更高的衰减率
|
|
||||||
animData.predictionVector = [
|
|
||||||
animData.predictionVector[0] * PREDICTION_DECAY_RATE,
|
|
||||||
animData.predictionVector[1] * PREDICTION_DECAY_RATE
|
|
||||||
];
|
|
||||||
|
|
||||||
// 更新目标位置
|
|
||||||
animData.targetPosition = [
|
|
||||||
animData.position[0] + animData.predictionVector[0] * 15, // 从12增加到15
|
|
||||||
animData.position[1] + animData.predictionVector[1] * 15
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓慢降低速度,但保持更高的最低速度
|
// 基于预测向量更新目标位置
|
||||||
if (!animData.lastSpeedReduction || currentTime - animData.lastSpeedReduction > 1000) {
|
const moveDistance = (animData.currentSpeed * 1000 / 3600) * 1.0; // 1秒的移动距离
|
||||||
animData.speed = Math.max(animData.speed * 0.98, MIN_SPEED); // 从0.97增加到0.98,进一步减缓速度降低率
|
animData.targetPosition = [
|
||||||
|
animData.position[0] + animData.predictionVector[0] * moveDistance,
|
||||||
|
animData.position[1] + animData.predictionVector[1] * moveDistance
|
||||||
|
];
|
||||||
|
|
||||||
|
// 缓慢降低速度,但保持最低速度
|
||||||
|
if (!animData.lastSpeedReduction || currentTime - animData.lastSpeedReduction > 2000) {
|
||||||
|
animData.speed = Math.max(animData.speed * 0.95, MIN_SPEED);
|
||||||
animData.lastSpeedReduction = currentTime;
|
animData.lastSpeedReduction = currentTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -481,124 +344,48 @@ function initVehicleAnimation(id, coordinates, heading, speed) {
|
|||||||
vehicleMotionHistory.value[id] = [];
|
vehicleMotionHistory.value[id] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新车辆动画目标
|
// 更新车辆动画目标 - 简化版本,专注于平滑过渡
|
||||||
function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
|
function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
|
||||||
const animData = vehicleAnimations.value[id] || {};
|
const animData = vehicleAnimations.value[id] || {};
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// 记录历史轨迹点
|
// 简化的预测向量计算
|
||||||
vehicleMotionHistory.value[id] = vehicleMotionHistory.value[id] || [];
|
|
||||||
vehicleMotionHistory.value[id].push({
|
|
||||||
time: now,
|
|
||||||
position: coordinates,
|
|
||||||
heading: heading, // 保存正确的heading值
|
|
||||||
speed: speed
|
|
||||||
});
|
|
||||||
|
|
||||||
// 增加历史点数量
|
|
||||||
if (vehicleMotionHistory.value[id].length > 10) { // 从8增加到10
|
|
||||||
vehicleMotionHistory.value[id].shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算预测向量 - 使用更复杂的算法
|
|
||||||
let predictionVector = [0, 0];
|
let predictionVector = [0, 0];
|
||||||
|
|
||||||
if (vehicleMotionHistory.value[id].length >= 2) {
|
// 如果有当前位置,计算移动方向
|
||||||
// 基本预测 - 使用最近两点
|
if (animData.position) {
|
||||||
const history = vehicleMotionHistory.value[id];
|
const dx = coordinates[0] - animData.position[0];
|
||||||
const latest = history[history.length - 1];
|
const dy = coordinates[1] - animData.position[1];
|
||||||
const previous = history[history.length - 2];
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
const dt = Math.max(1, latest.time - previous.time);
|
if (distance > 0.0001) {
|
||||||
const dx = (latest.position[0] - previous.position[0]);
|
// 创建单位方向向量
|
||||||
const dy = (latest.position[1] - previous.position[1]);
|
predictionVector = [dx / distance, dy / distance];
|
||||||
|
|
||||||
// 基础预测向量 - 考虑时间间隔
|
|
||||||
const baseSpeed = Math.sqrt(dx*dx + dy*dy) * (1000 / dt); // 单位时间内的移动距离
|
|
||||||
const normalizedDx = baseSpeed > 0 ? dx / baseSpeed : 0;
|
|
||||||
const normalizedDy = baseSpeed > 0 ? dy / baseSpeed : 0;
|
|
||||||
|
|
||||||
// 减小预测强度
|
|
||||||
predictionVector = [
|
|
||||||
normalizedDx * (PREDICTION_STRENGTH * 0.9) * baseSpeed,
|
|
||||||
normalizedDy * (PREDICTION_STRENGTH * 0.9) * baseSpeed
|
|
||||||
];
|
|
||||||
|
|
||||||
// 如果有更多历史点,使用加权平均提高预测准确性
|
|
||||||
if (vehicleMotionHistory.value[id].length >= 3) {
|
|
||||||
// 使用多个历史点计算平均移动趋势
|
|
||||||
let totalWeight = 0;
|
|
||||||
let weightedDx = 0;
|
|
||||||
let weightedDy = 0;
|
|
||||||
|
|
||||||
// 从最近到最远遍历历史点,使用更多的历史点
|
|
||||||
for (let i = history.length - 2; i >= 0 && i >= history.length - 7; i--) { // 从5增加到7
|
|
||||||
const curr = history[i+1];
|
|
||||||
const prev = history[i];
|
|
||||||
|
|
||||||
const pointDt = Math.max(1, curr.time - prev.time);
|
|
||||||
const pointDx = curr.position[0] - prev.position[0];
|
|
||||||
const pointDy = curr.position[1] - prev.position[1];
|
|
||||||
|
|
||||||
// 使用指数衰减权重
|
|
||||||
const weight = Math.pow(0.85, (history.length - i - 1));
|
|
||||||
totalWeight += weight;
|
|
||||||
|
|
||||||
weightedDx += pointDx * weight;
|
|
||||||
weightedDy += pointDy * weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWeight > 0) {
|
|
||||||
// 归一化加权向量
|
|
||||||
weightedDx /= totalWeight;
|
|
||||||
weightedDy /= totalWeight;
|
|
||||||
|
|
||||||
// 混合最新向量和加权历史向量,增加历史向量的权重
|
|
||||||
predictionVector = [
|
|
||||||
predictionVector[0] * 0.6 + weightedDx * PREDICTION_STRENGTH * 0.4, // 从0.7/0.3调整为0.6/0.4
|
|
||||||
predictionVector[1] * 0.6 + weightedDy * PREDICTION_STRENGTH * 0.4
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据速度调整预测强度,使用更平滑的曲线
|
|
||||||
const speedFactor = Math.min(1.8, Math.pow(speed / 15, 0.8)); // 使用幂函数使曲线更平滑
|
|
||||||
predictionVector = [
|
|
||||||
predictionVector[0] * speedFactor,
|
|
||||||
predictionVector[1] * speedFactor
|
|
||||||
];
|
|
||||||
|
|
||||||
// 如果车辆几乎静止,减弱预测但保持最低预测强度
|
|
||||||
if (speed < 2) {
|
|
||||||
predictionVector = [
|
|
||||||
predictionVector[0] * 0.4, // 从0.3增加到0.4
|
|
||||||
predictionVector[1] * 0.4
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存速度历史用于平滑过渡,增加历史点
|
// 保存速度历史用于平滑过渡
|
||||||
if (!animData.speedHistory) {
|
if (!animData.speedHistory) {
|
||||||
animData.speedHistory = [speed, speed, speed, speed]; // 增加一个点
|
animData.speedHistory = [speed, speed, speed];
|
||||||
} else {
|
} else {
|
||||||
animData.speedHistory.push(speed);
|
animData.speedHistory.push(speed);
|
||||||
if (animData.speedHistory.length > 4) { // 从3增加到4
|
if (animData.speedHistory.length > 3) {
|
||||||
animData.speedHistory.shift();
|
animData.speedHistory.shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算平滑速度(避免速度突变)
|
// 计算平滑速度
|
||||||
const smoothedSpeed = animData.speedHistory.reduce((sum, s) => sum + s, 0) /
|
const smoothedSpeed = animData.speedHistory.reduce((sum, s) => sum + s, 0) /
|
||||||
animData.speedHistory.length;
|
animData.speedHistory.length;
|
||||||
|
|
||||||
// 更新动画数据 - 保留当前位置,只更新目标位置
|
// 更新动画数据
|
||||||
vehicleAnimations.value[id] = {
|
vehicleAnimations.value[id] = {
|
||||||
...animData,
|
...animData,
|
||||||
targetPosition: coordinates,
|
targetPosition: coordinates,
|
||||||
targetHeading: heading, // 保存目标heading值
|
targetHeading: heading,
|
||||||
heading: animData.heading || heading, // 保留当前heading,如果不存在则使用新的heading
|
heading: animData.heading || heading,
|
||||||
position: animData.position || coordinates,
|
position: animData.position || coordinates,
|
||||||
speed: smoothedSpeed, // 使用平滑后的速度
|
speed: smoothedSpeed,
|
||||||
lastUpdated: now,
|
lastUpdated: now,
|
||||||
predictionVector,
|
predictionVector,
|
||||||
speedHistory: animData.speedHistory
|
speedHistory: animData.speedHistory
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user