From 16d319bb7c1dfb2d50749cc4135b34da34f046ea Mon Sep 17 00:00:00 2001
From: renna <576157508@qq.com>
Date: Wed, 16 Jul 2025 14:19:46 +0800
Subject: [PATCH] =?UTF-8?q?=E8=BD=A8=E8=BF=B9=E5=9B=9E=E6=94=BE=E9=A1=B5?=
=?UTF-8?q?=E9=9D=A2=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../vehicle-smooth-movement/requirements.md | 67 --
CLAUDE.md | 132 ----
src/components/car/detail/TrackPlayback.vue | 634 ++++++++++++------
3 files changed, 446 insertions(+), 387 deletions(-)
delete mode 100644 .kiro/specs/vehicle-smooth-movement/requirements.md
delete mode 100644 CLAUDE.md
diff --git a/.kiro/specs/vehicle-smooth-movement/requirements.md b/.kiro/specs/vehicle-smooth-movement/requirements.md
deleted file mode 100644
index 72b571b..0000000
--- a/.kiro/specs/vehicle-smooth-movement/requirements.md
+++ /dev/null
@@ -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 动画性能应该保持稳定
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 356d355..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/src/components/car/detail/TrackPlayback.vue b/src/components/car/detail/TrackPlayback.vue
index 804b791..724c913 100644
--- a/src/components/car/detail/TrackPlayback.vue
+++ b/src/components/car/detail/TrackPlayback.vue
@@ -49,26 +49,52 @@
-
+
-
-
-
-
{{ activeTask?.licensePlate || activeVehicleId || "未选择车辆" }}
-
最大时速 {{ trackDetails.maxSpeed }}km/h
-
平均时速 {{ trackDetails.averageSpeed }}km/h
-
总里程 {{ trackDetails.totalDistance }}km
-
耗时 {{ trackDetails.totalTime }}min
-
冲突告警 {{ trackDetails.warnings }}
-
冲突预警 {{ trackDetails.preWarnings }}
+
+
+
+
+ {{ licensePlate }}
+ 车牌号
+
+ {{ trackDetails.maxSpeed }}km/h
+ 最大时速
+
+
+ {{ trackDetails.averageSpeed }}km/h
+ 平均时速
+
+
+ {{ trackDetails.totalDistance }}km
+ 行驶总里程
+
+
+ {{ trackDetails.totalTime }}min
+ 行驶耗时
+
+
+ {{ trackDetails.warnings }}
+ 冲突告警
+
+
+ {{ trackDetails.preWarnings }}
+ 冲突预警
+
+
-
-
+
+
+
-
-
{{ trackDetails.startTime || "未开始" }}
-
-
-
- {{ speed }}x
-
-
-
- 1x
- 2x
- 4x
-
-
-
-
-
{{ trackDetails.endTime || "未结束" }}
+
+
+
+ {{ speed }}x
+
+
+
+ 1x
+ 2x
+ 4x
+
+
+
+
+
+
+ 开始时间 {{ trackDetails.startTime || "2025-05-01 12:09:49" }}
+ 结束时间 {{ trackDetails.endTime || "2025-05-01 14:09:49" }}
+
@@ -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();
+
});
@@ -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;
+ }
}