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 }} + 冲突预警 +
+
- -
+ + +
- - - - +
{{ tooltipTime }}
-
- {{ trackDetails.startTime || "未开始" }} -
- - - {{ speed }}x - - - -
- {{ trackDetails.endTime || "未结束" }} +
+ + + {{ speed }}x + + +
+ + +
+ 开始时间 {{ 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; + } }