轨迹回放页面修改
This commit is contained in:
parent
01088e3c9d
commit
16d319bb7c
@ -1,67 +0,0 @@
|
||||
# 车辆平滑移动优化需求文档
|
||||
|
||||
## 介绍
|
||||
|
||||
当前地图上的车辆移动存在"平滑行驶一小段然后停顿再平滑行驶"的问题,需要优化为完全平滑的连续行驶效果。系统每秒接收一次车辆位置数据,但当前的动画实现在数据间隔期间会出现停顿现象,影响用户体验。
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1
|
||||
|
||||
**用户故事:** 作为系统操作员,我希望地图上的车辆能够完全平滑地移动,这样我可以更直观地监控车辆的实时状态。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 系统接收到车辆位置更新数据 THEN 车辆应该平滑地从当前位置移动到新位置
|
||||
2. WHEN 两次位置数据之间存在时间间隔 THEN 车辆应该继续保持平滑移动而不是停顿
|
||||
3. WHEN 车辆速度发生变化 THEN 移动速度应该平滑过渡而不是突然改变
|
||||
|
||||
### 需求 2
|
||||
|
||||
**用户故事:** 作为系统操作员,我希望车辆的移动轨迹能够预测性地延续,这样即使在数据传输延迟时也能保持视觉连续性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 超过预设时间没有接收到新的位置数据 THEN 系统应该基于历史轨迹预测车辆移动方向
|
||||
2. WHEN 接收到新的位置数据 THEN 预测轨迹应该平滑地调整到实际位置
|
||||
3. WHEN 车辆停止移动 THEN 预测移动应该逐渐减速直至停止
|
||||
|
||||
### 需求 3
|
||||
|
||||
**用户故事:** 作为系统操作员,我希望车辆的旋转角度变化也能平滑过渡,这样车辆转向时看起来更自然。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 车辆heading角度发生变化 THEN 车辆图标应该平滑旋转到新角度
|
||||
2. WHEN 角度变化较大时 THEN 应该选择最短旋转路径进行过渡
|
||||
3. WHEN 车辆移动方向与heading不一致时 THEN 应该优先使用实际移动方向进行旋转
|
||||
|
||||
### 需求 4
|
||||
|
||||
**用户故事:** 作为系统操作员,我希望动画系统能够自适应不同的数据接收频率,这样无论网络状况如何都能保持平滑效果。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 数据接收频率为每秒1次 THEN 动画应该在1秒内平滑完成位置过渡
|
||||
2. WHEN 数据接收出现延迟 THEN 动画应该自动调整插值时间以保持平滑
|
||||
3. WHEN 数据接收频率变化 THEN 动画参数应该动态调整以适应新频率
|
||||
|
||||
### 需求 5
|
||||
|
||||
**用户故事:** 作为系统操作员,我希望车辆标签能够始终跟随车辆图标移动,这样我可以持续看到车辆的实时信息。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 车辆图标移动时 THEN 车辆标签应该同步移动
|
||||
2. WHEN 车辆处于动画移动状态 THEN 标签位置应该实时更新
|
||||
3. WHEN 地图缩放或平移时 THEN 标签应该保持正确的相对位置
|
||||
|
||||
### 需求 6
|
||||
|
||||
**用户故事:** 作为系统操作员,我希望动画系统具有良好的性能表现,这样即使有多辆车同时移动也不会影响系统响应速度。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 同时有多辆车进行动画移动 THEN 系统帧率应该保持在30fps以上
|
||||
2. WHEN 动画运行时 THEN CPU使用率不应该显著增加
|
||||
3. WHEN 车辆数量增加时 THEN 动画性能应该保持稳定
|
||||
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
|
||||
@ -49,26 +49,52 @@
|
||||
<div class="map-img-placeholder"></div>
|
||||
<!-- 轨迹详情卡片(左上角) -->
|
||||
<div class="track-detail-panel">
|
||||
<!-- 第一行 -->
|
||||
<!-- 第一行:标题和回放按钮两端对齐 -->
|
||||
<div class="panel-header">
|
||||
<span class="dot"></span>
|
||||
<span class="panel-title">轨迹详情</span>
|
||||
<div class="header-left">
|
||||
<span class="dot"></span>
|
||||
<span class="panel-title">轨迹详情</span>
|
||||
</div>
|
||||
<el-button size="small" type="primary" class="replay-btn" @click="handleReplay">回放</el-button>
|
||||
</div>
|
||||
<!-- 第二行 -->
|
||||
<div class="panel-info">
|
||||
<div class="info-row">
|
||||
<span class="carno">{{ activeTask?.licensePlate || activeVehicleId || "未选择车辆" }}</span>
|
||||
<span class="info-item">最大时速 <b>{{ trackDetails.maxSpeed }}km/h</b></span>
|
||||
<span class="info-item">平均时速 <b>{{ trackDetails.averageSpeed }}km/h</b></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 class="vehicle-stats-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ licensePlate }}</span>
|
||||
<span class="stat-label">车牌号</span>
|
||||
</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 class="panel-progress">
|
||||
|
||||
<!-- 第三行:进度条和倍速 -->
|
||||
<div class="progress-row">
|
||||
<div class="progress-bar-wrap"
|
||||
@mousemove="handleProgressHover"
|
||||
@mouseleave="showTooltip = false"
|
||||
@ -85,35 +111,34 @@
|
||||
:style="{left: flag.percent+'%'}"
|
||||
:title="flag.label"
|
||||
>
|
||||
<svg width="14" height="18" viewBox="0 0 14 18">
|
||||
<polygon points="2,2 12,5 2,8" fill="#E34D59"/>
|
||||
<rect x="1" y="2" width="2" height="14" fill="#E34D59"/>
|
||||
</svg>
|
||||
<div class="flag-icon"></div>
|
||||
</div>
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<div v-if="showTooltip" class="progress-tooltip" :style="{left: tooltipLeft+'px'}">
|
||||
{{ tooltipTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-bottom">
|
||||
<span class="start-time">{{ trackDetails.startTime || "未开始" }}</span>
|
||||
<div class="speed-select">
|
||||
<el-dropdown @command="setSpeed">
|
||||
<span class="el-dropdown-link">
|
||||
{{ speed }}x <i class="el-icon-arrow-down"></i>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="1">1x</el-dropdown-item>
|
||||
<el-dropdown-item command="2">2x</el-dropdown-item>
|
||||
<el-dropdown-item command="4">4x</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<span class="end-time">{{ trackDetails.endTime || "未结束" }}</span>
|
||||
<div class="speed-select">
|
||||
<el-dropdown @command="setSpeed">
|
||||
<span class="el-dropdown-link">
|
||||
{{ speed }}x <i class="el-icon-arrow-down"></i>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="1">1x</el-dropdown-item>
|
||||
<el-dropdown-item command="2">2x</el-dropdown-item>
|
||||
<el-dropdown-item command="4">4x</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</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>
|
||||
@ -129,7 +154,8 @@ import {
|
||||
listCarRunInfoByVehicleId,
|
||||
listCarRunInfoByLicensePlate,
|
||||
getLatestLocationByVehicleId,
|
||||
getLatestLocationByLicensePlate
|
||||
getLatestLocationByLicensePlate,
|
||||
batchQueryVehicleTrajectory
|
||||
} from "@/api/monitor/carRunInfo";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
@ -174,6 +200,7 @@ const props = defineProps({
|
||||
|
||||
// 轨迹详情数据
|
||||
const trackDetails = ref({
|
||||
licensePlate: "",
|
||||
maxSpeed: "0",
|
||||
averageSpeed: "0",
|
||||
totalDistance: "0",
|
||||
@ -226,16 +253,19 @@ async function getRunInfoList() {
|
||||
// 计算平均速度
|
||||
const avgSpeed = (vehicleData.reduce((sum, point) => sum + point.speed, 0) / vehicleData.length).toFixed(2);
|
||||
|
||||
// 确保licensePlate字段存在
|
||||
const licensePlate = firstPoint.licensePlate || `车辆${vehicleId}`;
|
||||
|
||||
tasksList.push({
|
||||
id: parseInt(vehicleId),
|
||||
no: vehicleId,
|
||||
name: `车辆${firstPoint.licensePlate}轨迹`,
|
||||
name: `车辆${licensePlate}轨迹`,
|
||||
time: `${firstPoint.timestamp}--${lastPoint.timestamp}`,
|
||||
start: `经度${firstPoint.longitude},纬度${firstPoint.latitude}`,
|
||||
end: `经度${lastPoint.longitude},纬度${lastPoint.latitude}`,
|
||||
status: "已完成",
|
||||
speed: `${avgSpeed}km/h`,
|
||||
licensePlate: firstPoint.licensePlate,
|
||||
licensePlate: licensePlate,
|
||||
points: vehicleData
|
||||
});
|
||||
});
|
||||
@ -245,6 +275,8 @@ async function getRunInfoList() {
|
||||
// 设置默认选中第一项
|
||||
if (tasks.value.length > 0) {
|
||||
activeId.value = tasks.value[0].id;
|
||||
// 显示第一个任务的轨迹详情
|
||||
selectTask(tasks.value[0]);
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
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) {
|
||||
const locationData = response.data;
|
||||
// 处理位置数据...
|
||||
console.log('车辆最新位置:', locationData);
|
||||
const trajectoryData = response.data || response.rows || [];
|
||||
|
||||
// 按车辆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) {
|
||||
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 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) {
|
||||
activeId.value = item.id;
|
||||
|
||||
// 从任务中提取轨迹数据
|
||||
if (item.points && item.points.length > 0) {
|
||||
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));
|
||||
|
||||
// 获取第一个点的车牌号(如果存在)
|
||||
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);
|
||||
|
||||
@ -345,8 +450,9 @@ function processTrackPoints(points) {
|
||||
const endTime = new Date(points[points.length-1].timestamp);
|
||||
const totalMinutes = Math.round((endTime - startTime) / (60 * 1000));
|
||||
|
||||
// 更新轨迹详情
|
||||
// 更新轨迹详情,保留车牌号
|
||||
trackDetails.value = {
|
||||
licensePlate: firstPoint?.licensePlate || currentLicensePlate || "暂无车牌号", // 使用新车牌号或保留原有车牌号
|
||||
maxSpeed: maxSpeed,
|
||||
averageSpeed: avgSpeed,
|
||||
totalDistance: distanceKm,
|
||||
@ -373,30 +479,50 @@ function processTrackPoints(points) {
|
||||
// 回放按钮点击处理函数
|
||||
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 (activeTask.points && activeTask.points.length > 0) {
|
||||
if (currentTask) {
|
||||
if (currentTask.points && currentTask.points.length > 0) {
|
||||
// 如果任务已包含轨迹点数据,直接处理
|
||||
processTrackPoints(activeTask.points);
|
||||
processTrackPoints(currentTask.points);
|
||||
ElMessage.success('开始轨迹回放');
|
||||
} else {
|
||||
// 如果没有轨迹点数据,根据车辆ID获取
|
||||
getTrackDetailByVehicleId(activeTask.id);
|
||||
// 使用批量查询获取轨迹回放数据
|
||||
const vehicleIds = [currentTask.id.toString()];
|
||||
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||
}
|
||||
} else if (queryParams.value.vehicleId) {
|
||||
// 使用查询参数中的车辆ID
|
||||
getTrackDetailByVehicleId(queryParams.value.vehicleId);
|
||||
// 使用查询参数中的车辆ID进行批量查询
|
||||
const vehicleIds = [queryParams.value.vehicleId];
|
||||
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||
} else if (props.vehicle?.carId) {
|
||||
// 使用传入的车辆ID
|
||||
getTrackDetailByVehicleId(props.vehicle.carId);
|
||||
// 使用传入的车辆ID进行批量查询
|
||||
const vehicleIds = [props.vehicle.carId];
|
||||
getBatchTrajectoryPlayback(vehicleIds, queryParams.value.timeRange);
|
||||
} else {
|
||||
// 获取列表数据
|
||||
getRunInfoList();
|
||||
ElMessage.info('请先选择一个车辆任务');
|
||||
// 如果有多个车辆,批量查询所有车辆的轨迹
|
||||
if (tasks.value.length > 0) {
|
||||
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() {
|
||||
try {
|
||||
@ -466,9 +592,28 @@ function setSpeed(val) {
|
||||
|
||||
// 页面加载时获取数据
|
||||
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>
|
||||
|
||||
@ -616,126 +761,239 @@ onMounted(() => {
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
.dot {
|
||||
width: 10px;
|
||||
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 {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
.carno {
|
||||
color: #4ea1ff;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
gap: 10px;
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #347ae2;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.info-item {
|
||||
color: #b0b8c9;
|
||||
b { color: #fff; font-weight: 500; }
|
||||
&.warn b { color: #e34d59; }
|
||||
&.prewarn b { color: #f7b500; }
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
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 {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
flex: 1;
|
||||
}
|
||||
.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: -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;
|
||||
|
||||
.speed-select {
|
||||
.el-dropdown-link {
|
||||
color: #4ea1ff;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第四行:开始和结束时间
|
||||
.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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user