3019 lines
97 KiB
Vue
3019 lines
97 KiB
Vue
<template>
|
||
<!-- 包含航空器路由的绘制 -->
|
||
<!-- 气象监测站弹窗 -->
|
||
<!-- <WeatherStationPopup
|
||
:visible="weatherStationVisible"
|
||
@close="weatherStationVisible = false"
|
||
/> -->
|
||
|
||
<!-- 车辆详情弹窗 -->
|
||
<VehicleDetailPopup
|
||
:visible="vehicleDetailVisible"
|
||
:detail="vehicleDetail"
|
||
:popup-style="detailPopupStyle"
|
||
@close="vehicleDetailVisible = false"
|
||
/>
|
||
|
||
<!-- 告警/预警提示框 -->
|
||
<!-- <AlarmNotification
|
||
|
||
ref="alarmNotification"
|
||
@close="handleAlarmClose"
|
||
/> -->
|
||
|
||
<!-- 车辆动画系统 -->
|
||
<VehicleAnimationSystem
|
||
ref="animationSystem"
|
||
:map="map"
|
||
:vehicle-source="vehicleSource"
|
||
:vehicles="vehicles"
|
||
:get-vehicle-style="styleManager?.value?.getVehicleStyle || defaultGetVehicleStyle"
|
||
:apply-style="applyVehicleStyleWithVisibility"
|
||
:apply-label-position="applyVehicleLabelPosition"
|
||
/>
|
||
|
||
<!-- 车辆标签系统 -->
|
||
<VehicleLabelSystem
|
||
ref="labelSystem"
|
||
:map="map"
|
||
:vehicles="vehicles"
|
||
/>
|
||
|
||
<!-- 车辆样式管理器 -->
|
||
<VehicleStyleManager
|
||
ref="styleManager"
|
||
:vehicles="vehicles"
|
||
/>
|
||
|
||
<div
|
||
v-if="pathConflictDisplayEnabled && currentPathConflict"
|
||
class="path-conflict-card"
|
||
:class="`level-${currentPathConflict.level}`"
|
||
>
|
||
<div class="path-conflict-header">
|
||
<span>{{ currentPathConflict.title }}</span>
|
||
<span class="path-conflict-level">{{ getConflictLevelText(currentPathConflict.level) }}</span>
|
||
</div>
|
||
<div class="path-conflict-body">
|
||
<div>{{ currentPathConflict.aircraftName || '未知航空器' }} / {{ currentPathConflict.vehicleName || '未知车辆' }}</div>
|
||
<div v-if="currentPathConflict.directionLockText" class="path-conflict-muted">
|
||
方向锁定:{{ currentPathConflict.directionLockText }}
|
||
</div>
|
||
<div>飞机距离:{{ formatConflictDistance(currentPathConflict.aircraftDistance) }}</div>
|
||
<div>车辆距离:{{ formatConflictDistance(currentPathConflict.vehicleDistance) }}</div>
|
||
<div v-if="hasConflictForwardDistance(currentPathConflict)" class="path-conflict-debug">
|
||
前向距离:对象1 {{ formatConflictDistance(currentPathConflict.object1ForwardDistance) }} / 对象2 {{ formatConflictDistance(currentPathConflict.object2ForwardDistance) }}
|
||
</div>
|
||
<div
|
||
v-if="currentPathConflict.directionLockFailed && currentPathConflict.directionLockReason"
|
||
class="path-conflict-reason"
|
||
>
|
||
原因:{{ currentPathConflict.directionLockReason }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted, watch, onActivated, onDeactivated, defineProps, defineEmits } from 'vue';
|
||
import { Vector as VectorSource } from 'ol/source';
|
||
import { Vector as VectorLayer } from 'ol/layer';
|
||
import { Style, Icon, Stroke, Fill, Circle, Text } from 'ol/style';
|
||
import Feature from 'ol/Feature';
|
||
import Point from 'ol/geom/Point';
|
||
import { LineString } from 'ol/geom';
|
||
import { transform } from 'ol/proj';
|
||
import GeoJSON from 'ol/format/GeoJSON';
|
||
import proj4 from 'proj4';
|
||
import { register } from 'ol/proj/proj4.js';
|
||
import { get as getProj } from 'ol/proj';
|
||
import WebSocketService, { createWebSocket, resetWebSocketInstance } from '../../../utils/websocket.js';
|
||
import useVehicleDisplayStore from '@/store/modules/vehicleDisplay';
|
||
import useSettingsStore from '@/store/modules/settings';
|
||
import { getPathConflictDisplayEnabled, isNewPathConflictPayload, normalizePathConflictMessage } from '@/utils/pathConflictDisplay.mjs';
|
||
|
||
// 导入子组件
|
||
import VehicleAnimationSystem from './VehicleAnimationSystem.vue';
|
||
import VehicleLabelSystem from './VehicleLabelSystem.vue';
|
||
import VehicleDetailPopup from './VehicleDetailPopup.vue';
|
||
import WeatherStationPopup from './WeatherStationPopup.vue';
|
||
import AlarmNotification from '../info/AlarmNotification.vue'; // 导入告警通知组件
|
||
import VehicleStyleManager from './VehicleStyleManager.vue';
|
||
|
||
// 导入默认图标
|
||
import carIcon from '../../../assets/images/noPeopleCar.svg';
|
||
import specialCar from '../../../assets/images/specialCar.svg';
|
||
import aircraftIcon from '../../../assets/images/Aircraft.png'; // 航空器使用Aircraft.png图标
|
||
import aircraftRouteIcon from '../../../assets/images/Aircraft1.png';
|
||
// 不再使用不同状态的航空器图标
|
||
// import aircraftInIcon from '../../../assets/images/Aircraft1.png'; // 滑入航空器使用黄色图标
|
||
// import aircraftOutIcon from '../../../assets/images/Aircraft.png'; // 滑出航空器使用蓝色图标
|
||
|
||
// 预加载图标资源,确保图标立即可用
|
||
const carIconImg = new Image();
|
||
carIconImg.src = carIcon;
|
||
const aircraftIconImg = new Image();
|
||
aircraftIconImg.src = aircraftIcon;
|
||
const aircraftRouteIconImg = new Image();
|
||
aircraftRouteIconImg.src = aircraftRouteIcon;
|
||
// 不再预加载不同状态的航空器图标
|
||
// const aircraftInIconImg = new Image();
|
||
// aircraftInIconImg.src = aircraftInIcon;
|
||
// const aircraftOutIconImg = new Image();
|
||
// aircraftOutIconImg.src = aircraftOutIcon;
|
||
|
||
const DEFAULT_VEHICLE_ICON_SCALE = 0.75;
|
||
const AIRCRAFT_ICON_SCALE = DEFAULT_VEHICLE_ICON_SCALE * 1.3;
|
||
|
||
// 为SockJS提供polyfill
|
||
if (typeof window !== 'undefined' && !window.global) {
|
||
window.global = window;
|
||
}
|
||
|
||
// 定义props接收地图实例
|
||
const props = defineProps({
|
||
map: Object
|
||
});
|
||
|
||
// 定义emit事件
|
||
const emit = defineEmits(['vehicle-details-updated']);
|
||
|
||
// 注册 EPSG:4528 坐标系
|
||
if (typeof window !== 'undefined' && proj4) {
|
||
// 检查是否已经注册过该坐标系
|
||
if (!proj4.defs('EPSG:4528')) {
|
||
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",
|
||
);
|
||
console.log('已注册 EPSG:4528 坐标系');
|
||
}
|
||
|
||
// 注册到 OpenLayers
|
||
register(proj4);
|
||
}
|
||
console.log("注册EPSG:4528",getProj('EPSG:4528'));
|
||
// 子组件引用
|
||
const animationSystem = ref(null);
|
||
const labelSystem = ref(null);
|
||
const styleManager = ref(null);
|
||
const alarmNotification = ref(null); // 告警通知组件引用
|
||
const vehicleDisplayStore = useVehicleDisplayStore();
|
||
const settingsStore = useSettingsStore();
|
||
const pathConflictDisplayEnabled = computed(() => getPathConflictDisplayEnabled(settingsStore));
|
||
|
||
// 气象监测站数据
|
||
const weatherStationVisible = ref(true);
|
||
|
||
// 车辆详情数据
|
||
const vehicleDetailVisible = ref(false);
|
||
const vehicleDetail = ref({
|
||
id: 'QN001',
|
||
type: '驱鸟车',
|
||
status: '任务中',
|
||
startTime: '11-19 11:30',
|
||
currentLocation: 'A区T3点',
|
||
startLocation: 'T1航站楼',
|
||
endLocation: 'T3航站楼',
|
||
totalDistance: '1.3km',
|
||
battery: '60%',
|
||
manager: '张三',
|
||
phone: '18661910988'
|
||
});
|
||
|
||
// 弹窗位置样式
|
||
const detailPopupStyle = ref({
|
||
left: '0px',
|
||
top: '0px'
|
||
});
|
||
|
||
// WebSocket连接状态
|
||
const wsConnected = ref(false);
|
||
let wsService = null;
|
||
let reconnectTimer = null;
|
||
|
||
// 车辆数据存储
|
||
const vehicles = ref({});
|
||
let vehicleLayer = null;
|
||
let vehicleSource = null;
|
||
let pathConflictLayer = null;
|
||
let pathConflictSource = null;
|
||
const pathConflicts = ref({});
|
||
|
||
const conflictLevelPriority = {
|
||
EMERGENCY: 4,
|
||
CRITICAL: 3,
|
||
WARNING: 2,
|
||
STATUS: 1
|
||
};
|
||
|
||
const currentPathConflict = computed(() => {
|
||
const list = Object.values(pathConflicts.value || {});
|
||
if (!list.length) return null;
|
||
return list.sort((a, b) => {
|
||
const levelDiff = (conflictLevelPriority[b.level] || 0) - (conflictLevelPriority[a.level] || 0);
|
||
if (levelDiff) return levelDiff;
|
||
return (b.updatedAt || 0) - (a.updatedAt || 0);
|
||
})[0];
|
||
});
|
||
|
||
function getConflictLevelText(level) {
|
||
if (level === 'EMERGENCY') return '紧急';
|
||
if (level === 'CRITICAL') return '告警';
|
||
if (level === 'WARNING') return '预警';
|
||
return '计算';
|
||
}
|
||
|
||
function formatConflictDistance(value) {
|
||
const num = Number(value);
|
||
if (!Number.isFinite(num)) return '--';
|
||
return `${Math.round(num)}米`;
|
||
}
|
||
|
||
function hasConflictForwardDistance(conflict) {
|
||
if (!conflict) return false;
|
||
return Number.isFinite(Number(conflict.object1ForwardDistance)) ||
|
||
Number.isFinite(Number(conflict.object2ForwardDistance));
|
||
}
|
||
|
||
function refreshVehicleVisibility() {
|
||
const all = vehicles.value || {};
|
||
Object.keys(all).forEach((vehicleId) => {
|
||
const vehicle = all[vehicleId];
|
||
if (!vehicle?.feature) return;
|
||
|
||
const typeKey = getVehicleTypeKey(vehicle);
|
||
const enabled = typeKey === 'AIRCRAFT' ? true : vehicleDisplayStore.isVehicleEnabled(vehicleId);
|
||
if (!enabled) {
|
||
vehicle.feature.setStyle(new Style({}));
|
||
if (labelSystem.value?.setLabelVisibility) {
|
||
labelSystem.value.setLabelVisibility(vehicleId, false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setVehicleStyleRespectVisibility(vehicleId, getDefaultVehicleStyle(vehicle));
|
||
if (labelSystem.value?.setLabelVisibility) {
|
||
labelSystem.value.setLabelVisibility(vehicleId, isLabelAllowedForVehicle(vehicleId));
|
||
}
|
||
});
|
||
}
|
||
|
||
watch(
|
||
() => vehicleDisplayStore.enabledVehicleIds,
|
||
() => {
|
||
refreshVehicleVisibility();
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
watch(
|
||
() => vehicleDisplayStore.vehicleTypeOverrides,
|
||
() => {
|
||
refreshVehicleVisibility();
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
// 车辆超速状态超时计时器存储
|
||
const speedViolationTimers = {};
|
||
|
||
// 飞机路线数据存储
|
||
const aircraftRoutes = ref({}); // 存储飞机路线数据,键为flightNo
|
||
let aircraftRouteLayer = null; // 飞机路线图层
|
||
let aircraftRouteSource = null; // 飞机路线数据源
|
||
|
||
// 飞机路线定时器存储
|
||
const aircraftRouteTimers = {}; // 存储每个航班路线的定时器
|
||
const aircraftRouteProgress = {};
|
||
|
||
function applyVehicleLabelPosition(vehicleId, coordinates) {
|
||
if (labelSystem.value && labelSystem.value.updateLabelPosition) {
|
||
labelSystem.value.updateLabelPosition(vehicleId, coordinates);
|
||
}
|
||
}
|
||
|
||
// 动态收集所有车辆/航空器类别
|
||
const vehicleCategories = ref({
|
||
'AIRCRAFT_IN': { visible: true, showLabel: true, name: '滑入航空器' },
|
||
'AIRCRAFT_OUT': { visible: true, showLabel: true, name: '滑出航空器' },
|
||
'AIRCRAFT': { visible: true, showLabel: true, name: '其他航空器' },
|
||
'UNMANNED_VEHICLE': { visible: true, showLabel: true, name: '无人车' },
|
||
'AIRPORT_VEHICLE': { visible: true, showLabel: true, name: '特勤车' },
|
||
'SHUTTLE_VEHICLE': { visible: true, showLabel: true, name: '摆渡车' }
|
||
});
|
||
|
||
// 监听vehicleCategories变化,用于调试
|
||
watch(vehicleCategories, (newCategories) => {
|
||
console.log('VehicleMovementControl: vehicleCategories 发生变化:', newCategories);
|
||
}, { deep: true });
|
||
|
||
// 告警/预警状态
|
||
const alertMessage = ref('');
|
||
const alertType = ref('');
|
||
let alertTimer = null;
|
||
|
||
// 违规/冲突状态计时器
|
||
const violationStatusTimers = ref({});
|
||
|
||
function clearStatusTimer(key) {
|
||
const timers = violationStatusTimers.value || {};
|
||
const t = timers[key];
|
||
if (t) {
|
||
clearTimeout(t);
|
||
delete timers[key];
|
||
violationStatusTimers.value = timers;
|
||
}
|
||
}
|
||
|
||
function setStatusTimer(key, fn, ms) {
|
||
clearStatusTimer(key);
|
||
const timers = violationStatusTimers.value || {};
|
||
timers[key] = setTimeout(fn, ms);
|
||
violationStatusTimers.value = timers;
|
||
}
|
||
|
||
function clearConflictStatus(vehicleId) {
|
||
const v = vehicles.value?.[vehicleId];
|
||
if (!v) return;
|
||
|
||
clearStatusTimer(`conflict:${vehicleId}`);
|
||
v.warning = false;
|
||
v.alarm = false;
|
||
v.lastConflictLevel = undefined;
|
||
v.lastConflictAt = undefined;
|
||
|
||
if (v.position && labelSystem.value) {
|
||
labelSystem.value.removeVehicleLabel(vehicleId);
|
||
updateVehicleLabelIfAllowed(vehicleId, v.position, v.speed > 0.1 ? v.speed : 0);
|
||
}
|
||
|
||
if (v.feature && styleManager.value) {
|
||
setVehicleStyleRespectVisibility(vehicleId, styleManager.value.getVehicleStyle(vehicleId, v.speed, v.heading));
|
||
}
|
||
}
|
||
|
||
function markConflictStatus(vehicleId, level, message, ttlMs) {
|
||
const v = vehicles.value?.[vehicleId];
|
||
if (!v) return;
|
||
|
||
const now = Date.now();
|
||
v.lastConflictAt = now;
|
||
v.lastConflictLevel = level;
|
||
|
||
if (level === 'alarm') {
|
||
v.alarm = true;
|
||
v.warning = false;
|
||
} else if (!v.alarm) {
|
||
v.warning = true;
|
||
}
|
||
|
||
if (v.position) {
|
||
updateVehicleLabelIfAllowed(vehicleId, v.position, v.speed, {
|
||
description: message,
|
||
isWarning: level === 'warning',
|
||
isAlarm: level === 'alarm'
|
||
});
|
||
}
|
||
|
||
const timerKey = `conflict:${vehicleId}`;
|
||
if (!(Number(ttlMs) > 0)) {
|
||
clearStatusTimer(timerKey);
|
||
return;
|
||
}
|
||
|
||
setStatusTimer(
|
||
timerKey,
|
||
() => {
|
||
const latest = vehicles.value?.[vehicleId];
|
||
if (!latest) return;
|
||
if (latest.lastConflictAt !== now) return;
|
||
clearConflictStatus(vehicleId);
|
||
clearStatusTimer(timerKey);
|
||
},
|
||
Number(ttlMs) > 0 ? Number(ttlMs) : 10000
|
||
);
|
||
}
|
||
|
||
// 告警列表管理
|
||
const alarmList = ref([]); // 存储所有告警信息
|
||
const maxAlarmCount = 100; // 最大告警数量,防止列表过长
|
||
const alarmVisible = ref(true); // 控制告警面板显示状态
|
||
|
||
// 添加告警到列表
|
||
function addAlarm(alarm) {
|
||
// 确保alarm对象包含所有必要字段
|
||
const completeAlarm = {
|
||
id: `alarm-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // 生成唯一ID
|
||
carId: alarm.carId || alarm.vehicleId || alarm.objectId || '未知车辆',
|
||
carType: alarm.carType || alarm.vehicleType || alarm.objectType || '未知类型',
|
||
time: alarm.time || formatTimeRange(new Date()),
|
||
description: alarm.description || '未知告警',
|
||
date: alarm.date || formatDateTime(new Date()),
|
||
level: alarm.level || 'medium', // high, medium, low
|
||
type: alarm.type || 'other', // car(冲突), report(越界), speed(超速), other
|
||
...alarm // 保留原始字段
|
||
};
|
||
|
||
// 添加到列表开头
|
||
alarmList.value.unshift(completeAlarm);
|
||
|
||
// 限制列表长度
|
||
if (alarmList.value.length > maxAlarmCount) {
|
||
alarmList.value = alarmList.value.slice(0, maxAlarmCount);
|
||
}
|
||
|
||
// 如果告警面板已打开,更新告警面板
|
||
updateAlarmNotification();
|
||
|
||
return completeAlarm.id; // 返回告警ID,便于后续引用
|
||
}
|
||
|
||
// 更新告警
|
||
function updateAlarm(alarmId, updates) {
|
||
const index = alarmList.value.findIndex(alarm => alarm.id === alarmId);
|
||
if (index !== -1) {
|
||
alarmList.value[index] = { ...alarmList.value[index], ...updates };
|
||
updateAlarmNotification();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 删除告警
|
||
function removeAlarm(alarmId) {
|
||
const index = alarmList.value.findIndex(alarm => alarm.id === alarmId);
|
||
if (index !== -1) {
|
||
alarmList.value.splice(index, 1);
|
||
updateAlarmNotification();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 清空告警列表
|
||
function clearAlarms() {
|
||
alarmList.value = [];
|
||
updateAlarmNotification();
|
||
}
|
||
|
||
// 更新告警面板
|
||
function updateAlarmNotification() {
|
||
if (alarmNotification.value) {
|
||
alarmNotification.value.updateAlarmList(alarmList.value);
|
||
}
|
||
}
|
||
|
||
// 切换告警面板显示状态
|
||
function toggleAlarmNotification() {
|
||
alarmVisible.value = !alarmVisible.value;
|
||
if (alarmNotification.value) {
|
||
if (alarmVisible.value) {
|
||
alarmNotification.value.show();
|
||
} else {
|
||
alarmNotification.value.hide();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理告警面板关闭事件
|
||
function handleAlarmClose() {
|
||
alarmVisible.value = false;
|
||
console.log('告警面板已关闭');
|
||
}
|
||
|
||
// 格式化时间范围,例如:T10:20-10:25
|
||
function formatTimeRange(date, durationMinutes = 5) {
|
||
const startTime = new Date(date);
|
||
const endTime = new Date(date);
|
||
endTime.setMinutes(endTime.getMinutes() + durationMinutes);
|
||
|
||
const startHour = startTime.getHours().toString().padStart(2, '0');
|
||
const startMinute = startTime.getMinutes().toString().padStart(2, '0');
|
||
const endHour = endTime.getHours().toString().padStart(2, '0');
|
||
const endMinute = endTime.getMinutes().toString().padStart(2, '0');
|
||
|
||
return `T${startHour}:${startMinute}-${endHour}:${endMinute}`;
|
||
}
|
||
|
||
// 格式化日期时间,例如:2025-03-19 10:30
|
||
function formatDateTime(date) {
|
||
const year = date.getFullYear();
|
||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||
const day = date.getDate().toString().padStart(2, '0');
|
||
const hour = date.getHours().toString().padStart(2, '0');
|
||
const minute = date.getMinutes().toString().padStart(2, '0');
|
||
|
||
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||
}
|
||
|
||
// 处理告警显示
|
||
function handleAlertShow({ message, type, duration = 5000 }) {
|
||
if (alertTimer) {
|
||
clearTimeout(alertTimer);
|
||
alertTimer = null;
|
||
}
|
||
|
||
alertMessage.value = message;
|
||
alertType.value = type;
|
||
|
||
if (Number(duration) > 0) {
|
||
alertTimer = setTimeout(() => {
|
||
alertMessage.value = '';
|
||
alertType.value = '';
|
||
alertTimer = null;
|
||
}, duration);
|
||
}
|
||
}
|
||
|
||
// 显示告警/预警消息
|
||
function showAlert(message, type, duration = 5000) {
|
||
handleAlertShow({ message, type, duration });
|
||
}
|
||
|
||
function clearAlertMessage() {
|
||
if (alertTimer) {
|
||
clearTimeout(alertTimer);
|
||
alertTimer = null;
|
||
}
|
||
alertMessage.value = '';
|
||
alertType.value = '';
|
||
}
|
||
|
||
// 创建车辆图层
|
||
function createVehicleLayer() {
|
||
if (!props.map) return;
|
||
if (vehicleLayer) return;
|
||
|
||
vehicleSource = new VectorSource();
|
||
|
||
vehicleLayer = new VectorLayer({
|
||
source: vehicleSource,
|
||
zIndex: 20,
|
||
});
|
||
|
||
props.map.addLayer(vehicleLayer);
|
||
|
||
setupMapListeners();
|
||
|
||
props.map.on('click', handleMapClick);
|
||
}
|
||
|
||
// 创建飞机路线图层
|
||
function createAircraftRouteLayer() {
|
||
if (!props.map) return;
|
||
if (aircraftRouteLayer) return;
|
||
|
||
aircraftRouteSource = new VectorSource();
|
||
|
||
aircraftRouteLayer = new VectorLayer({
|
||
source: aircraftRouteSource,
|
||
zIndex: 15, // 确保在车辆图层下方
|
||
style: new Style({
|
||
stroke: new Stroke({
|
||
color: '#3388ff',
|
||
width: 3
|
||
})
|
||
})
|
||
});
|
||
|
||
props.map.addLayer(aircraftRouteLayer);
|
||
}
|
||
|
||
function createPathConflictLayer() {
|
||
if (!props.map) return;
|
||
if (pathConflictLayer) return;
|
||
|
||
pathConflictSource = new VectorSource();
|
||
pathConflictLayer = new VectorLayer({
|
||
source: pathConflictSource,
|
||
zIndex: 2100,
|
||
});
|
||
props.map.addLayer(pathConflictLayer);
|
||
}
|
||
|
||
function getPathConflictStyle(conflict) {
|
||
const color = '#3b82f6';
|
||
|
||
return new Style({
|
||
image: new Circle({
|
||
radius: conflict.kind === 'alert' ? 10 : 8,
|
||
fill: new Fill({ color }),
|
||
stroke: new Stroke({ color: '#ffffff', width: 3 })
|
||
}),
|
||
text: new Text({
|
||
text: conflict.title,
|
||
offsetY: -22,
|
||
font: '12px sans-serif',
|
||
fill: new Fill({ color: '#ffffff' }),
|
||
stroke: new Stroke({ color: 'rgba(0,0,0,0.75)', width: 3 })
|
||
})
|
||
});
|
||
}
|
||
|
||
// 处理地图点击事件
|
||
function handleMapClick(event) {
|
||
const feature = props.map.forEachFeatureAtPixel(event.pixel, function(feature) {
|
||
return feature;
|
||
});
|
||
|
||
if (feature) {
|
||
const objectId = feature.getId();
|
||
if (objectId && vehicles.value[objectId]) {
|
||
const vehicle = vehicles.value[objectId];
|
||
|
||
// vehicleDetail.value = {
|
||
// id: objectId,
|
||
// type: vehicle.type,
|
||
// status: '任务中',
|
||
// startTime: '11-19 11:30',
|
||
// currentLocation: '当前位置',
|
||
// startLocation: 'T1航站楼',
|
||
// endLocation: 'T3航站楼',
|
||
// totalDistance: '1.3km',
|
||
// battery: '60%',
|
||
// manager: '张三',
|
||
// phone: '18661910988'
|
||
// };
|
||
// 使用实时数据填充车辆详情
|
||
vehicleDetail.value = {
|
||
id: objectId,
|
||
type: vehicle.type || '未知类型',
|
||
status: vehicle.status || '任务中',
|
||
startTime: vehicle.startTime || '11-19 11:30',
|
||
currentLocation: vehicle.currentLocation || '当前位置',
|
||
startLocation: vehicle.startLocation || 'T1航站楼',
|
||
endLocation: vehicle.endLocation || 'T3航站楼',
|
||
totalDistance: vehicle.totalDistance || '1.3km',
|
||
battery: vehicle.soc || vehicle.battery || '60%',
|
||
manager: vehicle.manager || vehicle.location || '张三',
|
||
phone: vehicle.phone || vehicle.contactPhone || '18661910988'
|
||
};
|
||
const pixel = event.pixel;
|
||
const viewportPosition = props.map.getViewport().getBoundingClientRect();
|
||
|
||
detailPopupStyle.value = {
|
||
left: (viewportPosition.left + pixel[0] + 10) + 'px',
|
||
top: (viewportPosition.top + pixel[1] + 10) + 'px'
|
||
};
|
||
|
||
vehicleDetailVisible.value = true;
|
||
} else {
|
||
vehicleDetailVisible.value = false;
|
||
}
|
||
} else {
|
||
vehicleDetailVisible.value = false;
|
||
}
|
||
}
|
||
|
||
// 监听地图变化,确保标签位置正确更新
|
||
function setupMapListeners() {
|
||
if (!props.map) return;
|
||
|
||
// 监听地图移动结束事件
|
||
props.map.on('moveend', () => {
|
||
if (labelSystem.value) {
|
||
labelSystem.value.updateAllLabels();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新车辆位置
|
||
function updateVehiclePosition(vehicleData) {
|
||
if (!vehicleSource || !props.map) return;
|
||
|
||
const { object_id, object_type, position, heading, speed } = vehicleData;
|
||
|
||
// 计算正确的旋转角度
|
||
// 在OpenLayers中,0度是向上,顺时针增加
|
||
// 而heading是0度为北,顺时针增加
|
||
// 由于地图旋转了72度,heading=72对应地图上方
|
||
const rotationRad = ((heading - 72) * Math.PI) / 180;
|
||
console.log(`车辆${object_id}的heading值: ${heading}, 旋转角度计算: (${heading} - 72) * π/180 = ${rotationRad} 弧度, ${(heading - 72)}度, 车辆类型: ${object_type}`);
|
||
|
||
let coordinates;
|
||
|
||
coordinates = transform(
|
||
[position.longitude, position.latitude],
|
||
'EPSG:4326',
|
||
props.map.getView().getProjection()
|
||
);
|
||
|
||
let feature = vehicleSource.getFeatureById(object_id);
|
||
|
||
const overrideType = vehicleDisplayStore.getVehicleTypeOverride(object_id);
|
||
const effectiveTypeRaw = String((overrideType || object_type || '')).trim();
|
||
const rawType = String(object_type || '').trim();
|
||
const vehicleType = effectiveTypeRaw.toUpperCase();
|
||
|
||
const isAircraft = vehicleType === 'AIRCRAFT' || vehicleType === 'AIRCRAFT_IN' || vehicleType === 'AIRCRAFT_OUT' || vehicleType === 'HANGKONG';
|
||
const isUnmannedVehicle = vehicleType === 'UNMANNED_VEHICLE' || vehicleType === 'WUREN' || effectiveTypeRaw.includes('无人');
|
||
const isSpecialVehicle = vehicleType === 'SPECIAL_VEHICLE' || vehicleType === 'TEQIN' || effectiveTypeRaw.includes('特勤');
|
||
const hasAircraftRoute = isAircraft && !!aircraftRoutes.value[object_id];
|
||
|
||
// 确保heading是有效的数值
|
||
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||
if (hasAircraftRoute) {
|
||
tryAutoRemoveAircraftRouteByPosition(object_id, coordinates);
|
||
}
|
||
|
||
if (!feature) {
|
||
// 先创建车辆数据对象,确保在创建feature前就有类型信息
|
||
vehicles.value[object_id] = {
|
||
id: object_id,
|
||
type: effectiveTypeRaw || rawType,
|
||
position: coordinates,
|
||
heading: validHeading,
|
||
speed: speed,
|
||
isAircraft: isAircraft,
|
||
isUnmannedVehicle: isUnmannedVehicle,
|
||
isSpecialVehicle: isSpecialVehicle,
|
||
hasAircraftRoute: hasAircraftRoute,
|
||
lastHeading: validHeading // 初始化lastHeading
|
||
};
|
||
|
||
feature = new Feature({
|
||
geometry: new Point(coordinates),
|
||
name: `${effectiveTypeRaw || rawType} ${object_id}`,
|
||
type: effectiveTypeRaw || rawType,
|
||
speed: speed,
|
||
isAircraft: isAircraft,
|
||
isUnmannedVehicle: isUnmannedVehicle,
|
||
isSpecialVehicle: isSpecialVehicle
|
||
});
|
||
|
||
feature.setId(object_id);
|
||
|
||
// 直接根据车辆类型选择正确的图标,不使用默认图标
|
||
let iconStyle;
|
||
let iconSrc;
|
||
|
||
if (isAircraft) {
|
||
iconSrc = hasAircraftRoute ? aircraftRouteIcon : aircraftIcon;
|
||
} else if (isUnmannedVehicle) {
|
||
iconSrc = carIcon; // 无人车使用noPeopleCar.svg
|
||
} else {
|
||
iconSrc = specialCar; // 其他车辆使用specialCar.svg
|
||
}
|
||
|
||
iconStyle = new Style({
|
||
image: new Icon({
|
||
src: iconSrc,
|
||
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||
anchor: [0.5, 0.5],
|
||
rotation: rotationRad, // 使用计算好的旋转角度
|
||
})
|
||
});
|
||
|
||
{
|
||
const typeKey = getVehicleTypeKey(vehicles.value[object_id]);
|
||
const category = vehicleCategories.value?.[typeKey];
|
||
const enabled = typeKey === 'AIRCRAFT' ? true : vehicleDisplayStore.isVehicleEnabled(object_id);
|
||
const visible = enabled && !!(category && category.visible);
|
||
feature.setStyle(visible ? iconStyle : new Style({}));
|
||
}
|
||
vehicleSource.addFeature(feature);
|
||
|
||
// 保存feature引用到车辆数据对象
|
||
vehicles.value[object_id].feature = feature;
|
||
|
||
// 初始化动画数据
|
||
if (animationSystem.value) {
|
||
animationSystem.value.initVehicleAnimation(object_id, coordinates, heading, speed);
|
||
}
|
||
|
||
// 创建标签(受左侧类别文本开关控制)
|
||
if (labelSystem.value) {
|
||
updateVehicleLabelIfAllowed(object_id, coordinates, speed);
|
||
}
|
||
|
||
// 应用当前分类设置到新创建的车辆
|
||
if (vehicleCategories.value) {
|
||
const typeKey = getVehicleTypeKey(vehicles.value[object_id]);
|
||
const category = vehicleCategories.value?.[typeKey];
|
||
if (category && !category.visible && vehicles.value[object_id].feature) {
|
||
vehicles.value[object_id].feature.setStyle(new Style({}));
|
||
}
|
||
}
|
||
} else {
|
||
// 更新动画目标
|
||
if (animationSystem.value) {
|
||
animationSystem.value.updateVehicleAnimationTarget(object_id, coordinates, heading, speed);
|
||
}
|
||
|
||
// 更新车辆数据,包括可能从position_update消息中获取的详情数据
|
||
const previousVehicleData = { ...vehicles.value[object_id] };
|
||
vehicles.value[object_id] = {
|
||
...vehicles.value[object_id],
|
||
type: effectiveTypeRaw || rawType || vehicles.value[object_id]?.type,
|
||
position: coordinates,
|
||
heading: validHeading,
|
||
speed: speed,
|
||
isAircraft: isAircraft,
|
||
isUnmannedVehicle: isUnmannedVehicle,
|
||
isSpecialVehicle: isSpecialVehicle,
|
||
hasAircraftRoute: hasAircraftRoute,
|
||
// 更新车辆详情数据(如果在payload中提供)
|
||
...(vehicleData.carId && { carId: vehicleData.carId }),
|
||
...(vehicleData.type && { type: vehicleData.type }),
|
||
...(vehicleData.brand && { brand: vehicleData.brand }),
|
||
...(vehicleData.organization && { organization: vehicleData.organization }),
|
||
...(vehicleData.routeStatus && { routeStatus: vehicleData.routeStatus }),
|
||
...(vehicleData.status && { status: vehicleData.status }),
|
||
...(vehicleData.chargeStatus && { chargeStatus: vehicleData.chargeStatus }),
|
||
...(vehicleData.voltage && { voltage: vehicleData.voltage }),
|
||
...(vehicleData.current && { current: vehicleData.current }),
|
||
...(vehicleData.soc && { soc: vehicleData.soc }),
|
||
...(vehicleData.lastTime && { lastTime: vehicleData.lastTime }),
|
||
...(vehicleData.batteryLevel && { batteryLevel: vehicleData.batteryLevel }),
|
||
...(vehicleData.location && { location: vehicleData.location }),
|
||
...(vehicleData.contactPhone && { contactPhone: vehicleData.contactPhone }),
|
||
...(vehicleData.battery && { battery: vehicleData.battery }),
|
||
...(vehicleData.runningStatus && { runningStatus: vehicleData.runningStatus })
|
||
};
|
||
|
||
// 如果车辆详情有更新,则发射事件通知其他组件
|
||
const currentVehicleData = vehicles.value[object_id];
|
||
if (hasVehicleDetailsChanged(previousVehicleData, currentVehicleData)) {
|
||
emit('vehicle-details-updated', currentVehicleData);
|
||
}
|
||
|
||
// 更新标签位置,确保标签始终跟随图标
|
||
// 但是如果车辆处于超速状态,不在这里更新标签,避免与超速状态标签冲突
|
||
if (labelSystem.value && !vehicles.value[object_id].speedViolation) {
|
||
updateVehicleLabelIfAllowed(object_id, coordinates, speed);
|
||
}
|
||
|
||
// 更新车辆图标样式,确保旋转角度正确应用,并尊重分类显示状态
|
||
if (feature) {
|
||
// 根据车辆类型选择正确的图标
|
||
let iconSrc;
|
||
const vehicle = vehicles.value[object_id];
|
||
|
||
if (isAircraft) {
|
||
iconSrc = hasAircraftRoute ? aircraftRouteIcon : aircraftIcon; // 航空器使用aircraft.png
|
||
} else if (
|
||
vehicle &&
|
||
(String(vehicle.type || '').includes('无人') || String(vehicle.type || '').toUpperCase() === 'UNMANNED_VEHICLE' || String(vehicle.type || '').toUpperCase() === 'WUREN')
|
||
) {
|
||
iconSrc = carIcon; // 无人车使用noPeopleCar.svg
|
||
} else {
|
||
iconSrc = specialCar; // 其他车辆使用specialCar.svg
|
||
}
|
||
|
||
const typeKey = getVehicleTypeKey(vehicle);
|
||
const category = vehicleCategories.value?.[typeKey];
|
||
const enabled = typeKey === 'AIRCRAFT' ? true : vehicleDisplayStore.isVehicleEnabled(object_id);
|
||
const visible = enabled && !!(category && category.visible);
|
||
feature.setStyle(
|
||
visible
|
||
? new Style({
|
||
image: new Icon({
|
||
src: iconSrc,
|
||
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||
anchor: [0.5, 0.5],
|
||
rotation: rotationRad,
|
||
}),
|
||
})
|
||
: new Style({})
|
||
);
|
||
|
||
// 更新lastHeading
|
||
vehicles.value[object_id].lastHeading = validHeading;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查车辆详情是否有变化
|
||
function hasVehicleDetailsChanged(previous, current) {
|
||
// 检查关键详情字段是否有变化
|
||
return (
|
||
previous.carId !== current.carId ||
|
||
previous.type !== current.type ||
|
||
previous.brand !== current.brand ||
|
||
previous.organization !== current.organization ||
|
||
previous.routeStatus !== current.routeStatus ||
|
||
previous.status !== current.status ||
|
||
previous.chargeStatus !== current.chargeStatus ||
|
||
previous.voltage !== current.voltage ||
|
||
previous.current !== current.current ||
|
||
previous.soc !== current.soc ||
|
||
previous.lastTime !== current.lastTime ||
|
||
previous.batteryLevel !== current.batteryLevel ||
|
||
previous.location !== current.location ||
|
||
previous.contactPhone !== current.contactPhone ||
|
||
previous.battery !== current.battery ||
|
||
previous.runningStatus !== current.runningStatus
|
||
);
|
||
}
|
||
|
||
// 连接WebSocket
|
||
function connectWebSocket() {
|
||
// 重置WebSocket实例以强制创建新实例
|
||
resetWebSocketInstance();
|
||
|
||
try {
|
||
// 使用WebSocketService创建连接
|
||
const wsUrl = window.APP_CONFIG?.WS_BASE_URL;
|
||
console.log(`正在连接WebSocket: ${wsUrl}`);
|
||
|
||
wsService = createWebSocket(wsUrl, {
|
||
reconnectInterval: 3000,
|
||
maxReconnectAttempts: 5
|
||
});
|
||
|
||
// 注册事件监听
|
||
wsService.on('open', (event) => {
|
||
console.log('WebSocket连接成功!');
|
||
wsConnected.value = true;
|
||
|
||
// 连接成功后自动订阅消息
|
||
setTimeout(() => {
|
||
sendSubscribe();
|
||
}, 1000); // 延迟1秒发送订阅,确保连接稳定
|
||
});
|
||
|
||
wsService.on('message', (data) => {
|
||
handleWsMessage(data);
|
||
});
|
||
|
||
wsService.on('error', (event) => {
|
||
console.error('WebSocket错误:', event);
|
||
wsConnected.value = false;
|
||
});
|
||
|
||
wsService.on('close', (event) => {
|
||
console.log(`WebSocket连接关闭: ${event.code} - ${event.reason}`);
|
||
wsConnected.value = false;
|
||
});
|
||
|
||
wsService.on('reconnect_failed', () => {
|
||
console.error('WebSocket重连失败,已达到最大重试次数');
|
||
wsConnected.value = false;
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('创建WebSocket连接失败:', error);
|
||
}
|
||
}
|
||
|
||
// 处理WebSocket消息
|
||
function handleWsMessage(message) {
|
||
try {
|
||
const data = JSON.parse(message);
|
||
console.log('收到消息:', data);
|
||
|
||
// 根据消息类型处理
|
||
switch (data.type) {
|
||
case 'connection':
|
||
console.log(`连接确认: ${data.message}`);
|
||
break;
|
||
|
||
case 'position_update':
|
||
// 处理车辆位置更新
|
||
console.log(`位置更新: ${data.payload?.object_id} (${data.payload?.object_type})`);
|
||
handlePositionUpdate(data.payload);
|
||
break;
|
||
|
||
case 'collision_warning':
|
||
console.log(`碰撞预警: ${data.payload?.message || data.payload?.object1?.objectName || ''}`);
|
||
handlePathConflictAlert(data.payload);
|
||
break;
|
||
|
||
case 'path_conflict_status':
|
||
console.log(`路径冲突计算状态: ${data.payload?.aircraftName || ''} ${data.payload?.vehicleName || ''}`);
|
||
handlePathConflictMessage(data);
|
||
break;
|
||
|
||
case 'path_conflict_alert':
|
||
// 处理冲突告警和预警
|
||
console.log(`冲突告警/预警: ${data.payload?.object1?.objectName || data.payload?.messageType}`);
|
||
if (isNewPathConflictPayload(data.payload)) {
|
||
handlePathConflictMessage(data);
|
||
} else {
|
||
handlePathConflictAlert(data.payload);
|
||
}
|
||
break;
|
||
|
||
case 'rule_violation':
|
||
// 处理规则违规(超速和越界)
|
||
console.log(`规则违规: ${data.payload?.ruleName || data.payload?.violationType}`);
|
||
handleRuleViolation(data.payload);
|
||
break;
|
||
|
||
case 'pong':
|
||
console.log('收到心跳响应');
|
||
break;
|
||
|
||
case 'vehicle_command':
|
||
console.log('收到车辆控制指令:', data.payload);
|
||
break;
|
||
|
||
case 'aircraftRouteUpdate':
|
||
console.log('收到飞机路由更新:', data.data);
|
||
handleAircraftRouteUpdate(data.data);
|
||
break;
|
||
|
||
case 'intersection_traffic_light_status':
|
||
// 处理路口红绿灯状态更新
|
||
console.log('收到路口红绿灯状态更新:', data.payload);
|
||
handleIntersectionTrafficLightStatus(data.payload);
|
||
break;
|
||
|
||
case 'vehicle_status_update':
|
||
// 处理车辆状态更新消息
|
||
console.log('收到车辆状态更新:', data.payload);
|
||
handleVehicleStatusUpdate(data.payload);
|
||
break;
|
||
|
||
case 'trafficLight':
|
||
console.log('收到红绿灯消息:', data.payload);
|
||
handleTrafficLightWsMessage(data.payload);
|
||
break;
|
||
|
||
case 'trafficlight':
|
||
case 'TrafficLight':
|
||
console.log('收到红绿灯消息(大小写变体):', data.payload);
|
||
handleTrafficLightWsMessage(data.payload);
|
||
break;
|
||
|
||
case 'FLIGHT_NOTIFICATION':
|
||
// 处理航班进出港通知消息
|
||
console.log('收到航班进出港通知:', data.payload);
|
||
handleFlightNotification(data.payload);
|
||
break;
|
||
|
||
default:
|
||
// 其他类型的消息可以根据需要处理
|
||
if (data?.payload?.serviceType === 'rsu-traffic-lights') {
|
||
console.log('检测到 rsu-traffic-lights 服务消息,按红绿灯处理:', data.payload);
|
||
handleTrafficLightWsMessage(data.payload);
|
||
} else {
|
||
console.log(`未知消息类型: ${data.type}`, data);
|
||
}
|
||
break;
|
||
}
|
||
} catch (e) {
|
||
console.error('处理WebSocket消息出错:', e, message);
|
||
}
|
||
}
|
||
|
||
// 处理航班进出港通知消息
|
||
function handleFlightNotification(payload) {
|
||
if (!payload || !payload.flightNo) {
|
||
console.error('航班进出港通知消息格式错误:', payload);
|
||
return;
|
||
}
|
||
|
||
// 解构消息中的关键信息
|
||
const {
|
||
flightNo,
|
||
flightType,
|
||
eventType,
|
||
runway,
|
||
seat,
|
||
notificationLevel,
|
||
eventDescription
|
||
} = payload;
|
||
|
||
// 记录航班通知信息
|
||
console.log(`航班通知: ${flightNo} ${eventType} ${flightType}`, {
|
||
runway,
|
||
seat,
|
||
notificationLevel,
|
||
eventDescription
|
||
});
|
||
|
||
// TODO: 后续根据需求添加具体的处理逻辑
|
||
// 可能包括:
|
||
// 1. 在地图上显示航班相关信息
|
||
// 2. 发送通知到相关组件
|
||
// 3. 更新航班状态数据
|
||
// 4. 触发告警或提示
|
||
}
|
||
|
||
// 处理车辆状态更新消息
|
||
function handleVehicleStatusUpdate(payload) {
|
||
if (!payload || !payload.vehicleId) {
|
||
console.error('车辆状态更新消息格式错误:', payload);
|
||
return;
|
||
}
|
||
|
||
const { vehicleId, statusData } = payload;
|
||
|
||
// 检查是否存在该车辆
|
||
if (!vehicles.value[vehicleId]) {
|
||
console.warn(`未找到车辆 ${vehicleId},无法更新状态`);
|
||
return;
|
||
}
|
||
|
||
// 保存更新前的车辆详情用于比较
|
||
const previousVehicleData = { ...vehicles.value[vehicleId] };
|
||
|
||
// 更新车辆详细状态信息
|
||
if (statusData) {
|
||
// 车辆基本信息
|
||
if (statusData.vehicleInfo) {
|
||
const vehicleInfo = statusData.vehicleInfo;
|
||
vehicles.value[vehicleId] = {
|
||
...vehicles.value[vehicleId],
|
||
carId: vehicleInfo.vehicleId || vehicles.value[vehicleId].carId,
|
||
type: vehicleInfo.vehicleType || vehicles.value[vehicleId].type,
|
||
brand: vehicleInfo.brand || vehicles.value[vehicleId].brand,
|
||
organization: vehicleInfo.owningUnit || vehicles.value[vehicleId].organization
|
||
};
|
||
}
|
||
|
||
// 运行状态
|
||
if (statusData.operationalStatus) {
|
||
const ops = statusData.operationalStatus;
|
||
const operationalMode = ops.operationalMode || vehicles.value[vehicleId].routeStatus;
|
||
const systemHealth = ops.systemHealth;
|
||
let statusText = vehicles.value[vehicleId].status;
|
||
|
||
// 根据系统健康状态和运行模式确定状态文本
|
||
if (systemHealth === 'HEALTHY') {
|
||
statusText = operationalMode === 'AUTONOMOUS' ? '任务中' : '在线';
|
||
} else if (systemHealth === 'FAULT') {
|
||
statusText = '故障';
|
||
} else if (systemHealth === 'OFFLINE') {
|
||
statusText = '离线';
|
||
}
|
||
|
||
vehicles.value[vehicleId] = {
|
||
...vehicles.value[vehicleId],
|
||
routeStatus: operationalMode,
|
||
status: statusText,
|
||
chargeStatus: ops.powerStatus || vehicles.value[vehicleId].chargeStatus
|
||
};
|
||
}
|
||
|
||
// 运动状态
|
||
if (statusData.motionStatus) {
|
||
const motion = statusData.motionStatus;
|
||
if (motion.velocity) {
|
||
vehicles.value[vehicleId] = {
|
||
...vehicles.value[vehicleId],
|
||
speed: motion.velocity.speed !== undefined ? motion.velocity.speed : vehicles.value[vehicleId].speed
|
||
};
|
||
}
|
||
|
||
// 更新位置信息用于显示
|
||
if (motion.position) {
|
||
const { latitude, longitude } = motion.position;
|
||
vehicles.value[vehicleId] = {
|
||
...vehicles.value[vehicleId],
|
||
currentLocation: `(${latitude.toFixed(6)}, ${longitude.toFixed(6)})`
|
||
};
|
||
}
|
||
}
|
||
|
||
// 电池状态
|
||
if (statusData.batteryStatus?.mainBattery) {
|
||
const battery = statusData.batteryStatus.mainBattery;
|
||
vehicles.value[vehicleId] = {
|
||
...vehicles.value[vehicleId],
|
||
soc: battery.chargeLevel !== undefined ? `${battery.chargeLevel}%` : vehicles.value[vehicleId].soc,
|
||
voltage: battery.voltage !== undefined ? `${battery.voltage}V` : vehicles.value[vehicleId].voltage,
|
||
current: battery.current !== undefined ? `${battery.current}A` : vehicles.value[vehicleId].current,
|
||
battery: battery.chargeLevel !== undefined ? `${battery.chargeLevel}%` : vehicles.value[vehicleId].battery
|
||
};
|
||
}
|
||
|
||
// 通信状态
|
||
if (statusData.communicationStatus) {
|
||
const comm = statusData.communicationStatus;
|
||
vehicles.value[vehicleId] = {
|
||
...vehicles.value[vehicleId],
|
||
lastTime: comm.lastUpdateTime ? new Date(comm.lastUpdateTime).toLocaleString() :
|
||
new Date().toLocaleString() // 使用当前时间作为最后更新时间
|
||
};
|
||
}
|
||
|
||
// 任务上下文信息
|
||
if (statusData.missionContext?.currentMission) {
|
||
const mission = statusData.missionContext.currentMission;
|
||
vehicles.value[vehicleId] = {
|
||
...vehicles.value[vehicleId],
|
||
// 任务开始时间 - 从时间戳转换为可读格式
|
||
startTime: mission.startTime ?
|
||
new Date(mission.startTime).toLocaleString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
}).replace(/\//g, '-').replace(/,/, '') :
|
||
vehicles.value[vehicleId].startTime,
|
||
// 总里程 - 从米转换为公里
|
||
totalDistance: mission.totalMileage !== undefined ?
|
||
`${(mission.totalMileage / 1000).toFixed(1)}km` :
|
||
vehicles.value[vehicleId].totalDistance,
|
||
// 任务进度
|
||
progress: mission.progress !== undefined ?
|
||
`${mission.progress.toFixed(1)}%` :
|
||
vehicles.value[vehicleId].progress,
|
||
// 任务ID和类型
|
||
missionId: mission.missionId || vehicles.value[vehicleId].missionId,
|
||
missionType: mission.missionType || vehicles.value[vehicleId].missionType
|
||
};
|
||
}
|
||
}
|
||
|
||
// 如果当前正在显示该车辆的详情弹窗,实时更新弹窗数据
|
||
if (vehicleDetailVisible.value && vehicleDetail.value.id === vehicleId) {
|
||
const updatedVehicle = vehicles.value[vehicleId];
|
||
vehicleDetail.value = {
|
||
...vehicleDetail.value,
|
||
id: vehicleId,
|
||
type: updatedVehicle.type || vehicleDetail.value.type,
|
||
status: updatedVehicle.status || vehicleDetail.value.status,
|
||
startTime: updatedVehicle.startTime || vehicleDetail.value.startTime,
|
||
currentLocation: updatedVehicle.currentLocation || vehicleDetail.value.currentLocation,
|
||
totalDistance: updatedVehicle.totalDistance || vehicleDetail.value.totalDistance,
|
||
battery: updatedVehicle.battery || vehicleDetail.value.battery,
|
||
manager: updatedVehicle.manager || updatedVehicle.location || vehicleDetail.value.manager,
|
||
phone: updatedVehicle.phone || updatedVehicle.contactPhone || vehicleDetail.value.phone
|
||
};
|
||
}
|
||
|
||
// 如果车辆详情有更新,则发射事件通知其他组件
|
||
const currentVehicleData = vehicles.value[vehicleId];
|
||
if (hasVehicleDetailsChanged(previousVehicleData, currentVehicleData)) {
|
||
emit('vehicle-details-updated', currentVehicleData);
|
||
}
|
||
}
|
||
|
||
// 处理路口红绿灯状态更新
|
||
function handleIntersectionTrafficLightStatus(payload) {
|
||
// 检查是否在父组件中存在更新红绿灯图层的方法
|
||
if (window.updateTrafficLightStatus) {
|
||
window.updateTrafficLightStatus(payload);
|
||
}
|
||
}
|
||
|
||
function handleTrafficLightWsMessage(payload) {
|
||
const sd = payload && payload.serviceData ? payload.serviceData : payload;
|
||
const phases = Array.isArray(sd && sd.phases) ? sd.phases : [];
|
||
let status = 'unknown';
|
||
if (phases.length > 0) {
|
||
const colors = phases.map(p => Number(p.phaseColor));
|
||
if (colors.includes(3)) status = 'red';
|
||
else if (colors.includes(4)) status = 'yellow';
|
||
else if (colors.includes(2)) status = 'yellow';
|
||
else if (colors.includes(1)) status = 'green';
|
||
else status = 'unknown';
|
||
} else {
|
||
const s = Number((sd && sd.trafficLightsStatus) ?? payload.trafficLightsStatus);
|
||
if (s === 3) status = 'red';
|
||
else if (s === 4) status = 'yellow';
|
||
else if (s === 2) status = 'yellow';
|
||
else if (s === 1) status = 'green';
|
||
}
|
||
if (window.updateTrafficLightStatus) {
|
||
window.updateTrafficLightStatus({ status });
|
||
}
|
||
}
|
||
|
||
// 处理位置更新消息
|
||
function handlePositionUpdate(payload) {
|
||
if (!payload || !payload.object_id) {
|
||
console.error('位置更新消息格式错误:', payload);
|
||
return;
|
||
}
|
||
|
||
// 清除车辆的告警状态(如果需要)
|
||
clearVehicleAlertStatus(payload);
|
||
|
||
// 更新车辆位置
|
||
updateVehiclePosition(payload);
|
||
}
|
||
|
||
// 处理飞机路由更新消息
|
||
function handleAircraftRouteUpdate(payload) {
|
||
if (!payload || !payload.flightNo || !payload.routeGeometry) {
|
||
console.error('飞机路由更新消息格式错误:', payload);
|
||
return;
|
||
}
|
||
|
||
const { flightNo, routeType, routeStatus, routeGeometry } = payload;
|
||
|
||
const geoJsonObject = parseGeoJsonLike(routeGeometry);
|
||
if (geoJsonObject) {
|
||
updateAircraftRouteFromGeoJson(flightNo, geoJsonObject, routeType, routeStatus);
|
||
return;
|
||
}
|
||
|
||
// 解析LINESTRING格式的routeGeometry
|
||
const coordinates = parseLineStringGeometry(routeGeometry);
|
||
if (!coordinates || coordinates.length === 0) {
|
||
console.error('解析routeGeometry失败:', routeGeometry);
|
||
return;
|
||
}
|
||
|
||
// 更新或创建路线
|
||
updateAircraftRoute(flightNo, coordinates, routeType, routeStatus);
|
||
}
|
||
|
||
function parseGeoJsonLike(value) {
|
||
if (!value) return null
|
||
if (typeof value === 'object') {
|
||
const t = String(value?.type || '')
|
||
if (t === 'Feature' || t === 'FeatureCollection' || t === 'GeometryCollection') return value
|
||
return null
|
||
}
|
||
if (typeof value !== 'string') return null
|
||
const s = value.trim()
|
||
if (!s || (s[0] !== '{' && s[0] !== '[')) return null
|
||
try {
|
||
const obj = JSON.parse(s)
|
||
const t = String(obj?.type || '')
|
||
if (t === 'Feature' || t === 'FeatureCollection' || t === 'GeometryCollection') return obj
|
||
return null
|
||
} catch (e) {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function removeAircraftRouteFeatures(flightNo) {
|
||
if (!aircraftRouteSource) return
|
||
const all = aircraftRouteSource.getFeatures()
|
||
all.forEach((f) => {
|
||
const id = String(f?.getId?.() || '')
|
||
const fFlight = f?.get?.('flightNo')
|
||
if (fFlight === flightNo || id === flightNo || (id && id.startsWith(`${flightNo}-`))) {
|
||
aircraftRouteSource.removeFeature(f)
|
||
}
|
||
})
|
||
}
|
||
|
||
function updateAircraftRouteFromGeoJson(flightNo, geoJsonObject, routeType, routeStatus) {
|
||
if (!aircraftRouteSource || !props.map) return
|
||
|
||
if (aircraftRouteTimers[flightNo]) {
|
||
clearTimeout(aircraftRouteTimers[flightNo])
|
||
delete aircraftRouteTimers[flightNo]
|
||
}
|
||
|
||
removeAircraftRouteFeatures(flightNo)
|
||
|
||
const viewProj = props.map.getView().getProjection()
|
||
const geojson = new GeoJSON()
|
||
const rawFeatures = geojson.readFeatures(geoJsonObject, {
|
||
dataProjection: viewProj,
|
||
featureProjection: viewProj
|
||
})
|
||
|
||
let dataProj = viewProj
|
||
const firstLine = rawFeatures.find((f) => f?.getGeometry?.()?.getType?.() === 'LineString')
|
||
const firstCoords = firstLine?.getGeometry?.()?.getCoordinates?.()
|
||
const firstCoord = Array.isArray(firstCoords) && firstCoords.length ? firstCoords[0] : null
|
||
if (Array.isArray(firstCoord) && firstCoord.length === 2) {
|
||
const x = Number(firstCoord[0])
|
||
const y = Number(firstCoord[1])
|
||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||
const looksLikeLngLat = Math.abs(x) <= 180 && Math.abs(y) <= 90
|
||
dataProj = looksLikeLngLat ? 'EPSG:4326' : 'EPSG:4528'
|
||
}
|
||
}
|
||
|
||
const features = dataProj === viewProj
|
||
? rawFeatures
|
||
: geojson.readFeatures(geoJsonObject, {
|
||
dataProjection: dataProj,
|
||
featureProjection: viewProj
|
||
})
|
||
|
||
let firstLineCoords = null
|
||
features.forEach((f, idx) => {
|
||
const existingId = f.getId()
|
||
if (existingId === undefined || existingId === null || String(existingId).trim() === '') {
|
||
f.setId(`${flightNo}-geo-${idx}`)
|
||
} else {
|
||
f.setId(`${flightNo}-geo-${existingId}`)
|
||
}
|
||
f.set('flightNo', flightNo)
|
||
if (routeType) f.set('routeType', routeType)
|
||
if (routeStatus) f.set('routeStatus', routeStatus)
|
||
if (f.getGeometry()?.getType?.() === 'LineString' && !firstLineCoords) {
|
||
firstLineCoords = f.getGeometry().getCoordinates()
|
||
f.setStyle(getRouteStyle(routeType, routeStatus))
|
||
}
|
||
aircraftRouteSource.addFeature(f)
|
||
})
|
||
|
||
if (firstLineCoords && firstLineCoords.length) {
|
||
aircraftRoutes.value[flightNo] = {
|
||
flightNo,
|
||
routeType,
|
||
routeStatus,
|
||
coordinates: firstLineCoords,
|
||
feature: null,
|
||
lastUpdateTime: Date.now()
|
||
}
|
||
aircraftRouteProgress[flightNo] = { maxIndexSeen: 0, endHitCount: 0, lastIndex: 0 }
|
||
}
|
||
|
||
if (vehicles.value[flightNo]) {
|
||
const currentVehicle = vehicles.value[flightNo]
|
||
const isAircraftVehicle = currentVehicle.isAircraft || currentVehicle.type?.toUpperCase() === 'AIRCRAFT'
|
||
if (isAircraftVehicle) {
|
||
vehicles.value[flightNo] = { ...currentVehicle, hasAircraftRoute: true }
|
||
const v = vehicles.value[flightNo]
|
||
if (v.feature) {
|
||
if (styleManager.value) {
|
||
setVehicleStyleRespectVisibility(flightNo, styleManager.value.getVehicleStyle(flightNo, v.speed, v.heading))
|
||
} else {
|
||
setVehicleStyleRespectVisibility(flightNo, getDefaultVehicleStyle(v))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
aircraftRouteTimers[flightNo] = setTimeout(() => {
|
||
removeAircraftRoute(flightNo)
|
||
}, 300000)
|
||
}
|
||
|
||
// 解析LINESTRING格式的字符串,提取坐标点
|
||
function parseLineStringGeometry(lineString) {
|
||
// 移除LINESTRING前缀和括号
|
||
const coordsString = lineString.replace(/LINESTRING\s*\(|\)/gi, '');
|
||
|
||
// 分割坐标点
|
||
const coordPairs = coordsString.split(',').map(pair => pair.trim());
|
||
|
||
// 解析每个坐标点
|
||
return coordPairs.map(pair => {
|
||
const [lon, lat] = pair.split(' ').map(Number);
|
||
return [lon, lat];
|
||
});
|
||
}
|
||
|
||
function computeClosestRouteIndex(routeCoords, positionCoord, startIndexHint = 0) {
|
||
if (!Array.isArray(routeCoords) || routeCoords.length === 0) return 0;
|
||
if (!positionCoord || positionCoord.length !== 2) return 0;
|
||
const startIndex = Math.max(0, Math.min(Math.floor(startIndexHint || 0), routeCoords.length - 1));
|
||
let bestIndex = startIndex;
|
||
let bestD2 = Infinity;
|
||
for (let i = startIndex; i < routeCoords.length; i++) {
|
||
const dx = routeCoords[i][0] - positionCoord[0];
|
||
const dy = routeCoords[i][1] - positionCoord[1];
|
||
const d2 = dx * dx + dy * dy;
|
||
if (d2 < bestD2) {
|
||
bestD2 = d2;
|
||
bestIndex = i;
|
||
}
|
||
}
|
||
return bestIndex;
|
||
}
|
||
|
||
function tryAutoRemoveAircraftRouteByPosition(flightNo, positionCoord) {
|
||
const route = aircraftRoutes.value?.[flightNo];
|
||
if (!route || !Array.isArray(route.coordinates) || route.coordinates.length < 2) return;
|
||
const end = route.coordinates[route.coordinates.length - 1];
|
||
const dx = end[0] - positionCoord[0];
|
||
const dy = end[1] - positionCoord[1];
|
||
const distToEnd = Math.sqrt(dx * dx + dy * dy);
|
||
|
||
const progress = aircraftRouteProgress[flightNo] || { maxIndexSeen: 0, endHitCount: 0, lastIndex: 0 };
|
||
const nearestIndex = computeClosestRouteIndex(route.coordinates, positionCoord, Math.max(0, progress.maxIndexSeen - 20));
|
||
|
||
progress.lastIndex = nearestIndex;
|
||
progress.maxIndexSeen = Math.max(progress.maxIndexSeen, nearestIndex);
|
||
|
||
const endIndexThreshold = Math.max(0, route.coordinates.length - 2);
|
||
const reachedRouteEnd = progress.maxIndexSeen >= endIndexThreshold;
|
||
const movedBeyondStart = progress.maxIndexSeen >= Math.min(3, endIndexThreshold);
|
||
const endDistanceThreshold = 20;
|
||
|
||
if (reachedRouteEnd && movedBeyondStart && distToEnd <= endDistanceThreshold) {
|
||
progress.endHitCount = (progress.endHitCount || 0) + 1;
|
||
} else {
|
||
progress.endHitCount = 0;
|
||
}
|
||
|
||
aircraftRouteProgress[flightNo] = progress;
|
||
|
||
if (progress.endHitCount >= 3) {
|
||
removeAircraftRoute(flightNo);
|
||
}
|
||
}
|
||
|
||
// 更新或创建飞机路线
|
||
function updateAircraftRoute(flightNo, coordinates, routeType, routeStatus) {
|
||
if (!aircraftRouteSource || !props.map) return;
|
||
console.log('原始航线坐标:', coordinates);
|
||
|
||
// 清除该航班之前的定时器(如果存在)
|
||
if (aircraftRouteTimers[flightNo]) {
|
||
clearTimeout(aircraftRouteTimers[flightNo]);
|
||
delete aircraftRouteTimers[flightNo];
|
||
}
|
||
|
||
const isLngLat4326 = Array.isArray(coordinates) && coordinates.length > 0 && coordinates.every(coord => {
|
||
if (!Array.isArray(coord) || coord.length !== 2) return false
|
||
const x = Number(coord[0])
|
||
const y = Number(coord[1])
|
||
return Number.isFinite(x) && Number.isFinite(y) && Math.abs(x) <= 180 && Math.abs(y) <= 90
|
||
})
|
||
const sourceProjection = isLngLat4326 ? 'EPSG:4326' : 'EPSG:4528'
|
||
|
||
const projectedCoords = coordinates.map(coord =>
|
||
transform(coord, sourceProjection, props.map.getView().getProjection())
|
||
)
|
||
console.log('处理后的坐标:', projectedCoords);
|
||
|
||
// 获取起点和终点坐标
|
||
const startPoint = projectedCoords[0];
|
||
const endPoint = projectedCoords[projectedCoords.length - 1];
|
||
|
||
// 检查是否已存在该航班的路线
|
||
let feature = aircraftRouteSource.getFeatureById(flightNo);
|
||
let startFeature = aircraftRouteSource.getFeatureById(`${flightNo}-start`);
|
||
let endFeature = aircraftRouteSource.getFeatureById(`${flightNo}-end`);
|
||
|
||
if (!feature) {
|
||
// 创建新的LineString几何体
|
||
const lineGeometry = new LineString(projectedCoords);
|
||
|
||
// 创建新的Feature
|
||
feature = new Feature({
|
||
geometry: lineGeometry,
|
||
name: `Flight ${flightNo} Route`,
|
||
flightNo: flightNo,
|
||
routeType: routeType,
|
||
routeStatus: routeStatus
|
||
});
|
||
|
||
// 设置ID和样式
|
||
feature.setId(flightNo);
|
||
feature.setStyle(getRouteStyle(routeType, routeStatus));
|
||
|
||
// 添加到数据源
|
||
aircraftRouteSource.addFeature(feature);
|
||
|
||
// 创建起点标记
|
||
createRoutePointMarker(flightNo, 'start', startPoint, routeType);
|
||
|
||
// 创建终点标记
|
||
createRoutePointMarker(flightNo, 'end', endPoint, routeType);
|
||
|
||
// 存储路线信息
|
||
aircraftRoutes.value[flightNo] = {
|
||
flightNo: flightNo,
|
||
routeType: routeType,
|
||
routeStatus: routeStatus,
|
||
coordinates: projectedCoords,
|
||
feature: feature,
|
||
lastUpdateTime: Date.now() // 记录最后更新时间
|
||
};
|
||
aircraftRouteProgress[flightNo] = { maxIndexSeen: 0, endHitCount: 0, lastIndex: 0 };
|
||
} else {
|
||
// 更新现有Feature的几何体和属性
|
||
feature.getGeometry().setCoordinates(projectedCoords);
|
||
feature.set('routeType', routeType);
|
||
feature.set('routeStatus', routeStatus);
|
||
|
||
// 更新样式
|
||
feature.setStyle(getRouteStyle(routeType, routeStatus));
|
||
|
||
// 更新起点标记位置
|
||
updateRoutePointMarker(flightNo, 'start', startPoint);
|
||
|
||
// 更新终点标记位置
|
||
updateRoutePointMarker(flightNo, 'end', endPoint);
|
||
|
||
// 更新存储的路线信息
|
||
aircraftRoutes.value[flightNo] = {
|
||
...aircraftRoutes.value[flightNo],
|
||
routeType: routeType,
|
||
routeStatus: routeStatus,
|
||
coordinates: projectedCoords,
|
||
lastUpdateTime: Date.now() // 更新最后更新时间
|
||
};
|
||
|
||
if (aircraftRouteProgress[flightNo]) {
|
||
const p = aircraftRouteProgress[flightNo];
|
||
p.maxIndexSeen = Math.min(p.maxIndexSeen || 0, Math.max(0, projectedCoords.length - 1));
|
||
p.lastIndex = Math.min(p.lastIndex || 0, Math.max(0, projectedCoords.length - 1));
|
||
p.endHitCount = 0;
|
||
}
|
||
}
|
||
|
||
if (vehicles.value[flightNo]) {
|
||
const currentVehicle = vehicles.value[flightNo];
|
||
const isAircraftVehicle = currentVehicle.isAircraft || currentVehicle.type?.toUpperCase() === 'AIRCRAFT';
|
||
if (isAircraftVehicle) {
|
||
vehicles.value[flightNo] = {
|
||
...currentVehicle,
|
||
hasAircraftRoute: true
|
||
};
|
||
const v = vehicles.value[flightNo];
|
||
if (v.feature) {
|
||
if (styleManager.value) {
|
||
setVehicleStyleRespectVisibility(
|
||
flightNo,
|
||
styleManager.value.getVehicleStyle(flightNo, v.speed, v.heading)
|
||
);
|
||
} else {
|
||
setVehicleStyleRespectVisibility(flightNo, getDefaultVehicleStyle(v));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 设置新的定时器:如果在5分钟内没有收到新的更新消息,则移除路线
|
||
// 这样可以防止路线永久显示,同时允许通过新消息来刷新显示时间
|
||
aircraftRouteTimers[flightNo] = setTimeout(() => {
|
||
console.log(`航班 ${flightNo} 路线超时,5分钟内未收到更新消息,移除路线`);
|
||
removeAircraftRoute(flightNo);
|
||
}, 300000); // 5分钟后移除
|
||
|
||
console.log(`航班 ${flightNo} 路线已更新,将在收到下次更新消息时刷新显示时间`);
|
||
}
|
||
|
||
// 根据路线类型和状态获取样式
|
||
function getRouteStyle(routeType, routeStatus) {
|
||
let color = '#3388ff'; // 默认蓝色
|
||
let width = 3;
|
||
let lineDash = undefined;
|
||
|
||
// 根据路线类型设置颜色
|
||
if (routeType === 'IN') {
|
||
color = '#292C38'; // 深蓝色表示滑入
|
||
} else if (routeType === 'OUT') {
|
||
color = '#27AE60'; // 蓝色表示滑出
|
||
} else if (routeType === 'TEST') {
|
||
color = '#FF8C00'; // 测试路由使用橙色
|
||
}
|
||
|
||
// // 根据路线状态设置线型
|
||
// if (routeStatus === 'PLANNED') {
|
||
// lineDash = [4, 8]; // 计划中的路线使用虚线
|
||
// width = 2;
|
||
// } else if (routeStatus === 'ACTIVE') {
|
||
// width = 4; // 活动中的路线使用粗线
|
||
// } else if (routeStatus === 'COMPLETE') {
|
||
// width = 2; // 完成的路线使用细线
|
||
// color = color + '80'; // 添加透明度
|
||
// }
|
||
|
||
return new Style({
|
||
stroke: new Stroke({
|
||
color: color,
|
||
width: width,
|
||
lineDash: lineDash
|
||
})
|
||
});
|
||
}
|
||
|
||
// 获取点样式(起点或终点)
|
||
function getPointStyle(pointType, routeType, flightNo) {
|
||
// 根据路线类型选择颜色
|
||
let color = '#3388ff'; // 默认蓝色
|
||
if (routeType === 'IN') {
|
||
color = '#292C38'; // 深蓝色表示滑入
|
||
} else if (routeType === 'OUT') {
|
||
color = '#27AE60'; // 绿色表示滑出
|
||
} else if (routeType === 'TEST') {
|
||
color = '#FF8C00'; // 测试路由点使用橙色
|
||
}
|
||
|
||
// 创建圆点样式
|
||
const circleStyle = new Style({
|
||
image: new Circle({
|
||
radius: 6,
|
||
fill: new Fill({
|
||
color: color
|
||
}),
|
||
stroke: new Stroke({
|
||
color: '#ffffff',
|
||
width: 2
|
||
})
|
||
}),
|
||
// 添加文本标签
|
||
text: new Text({
|
||
text: pointType === 'start' ? '起点' : '终点',
|
||
font: 'bold 12px Arial',
|
||
fill: new Fill({
|
||
color: '#ffffff'
|
||
}),
|
||
stroke: new Stroke({
|
||
color: '#000000',
|
||
width: 3
|
||
}),
|
||
offsetX: 0,
|
||
offsetY: pointType === 'start' ? -15 : -15, // 文字位置偏移
|
||
textAlign: 'center',
|
||
textBaseline: 'bottom'
|
||
}),
|
||
zIndex: 10 // 确保点标记显示在线的上方
|
||
});
|
||
|
||
return circleStyle;
|
||
}
|
||
|
||
// 创建路线点标记(起点或终点)
|
||
function createRoutePointMarker(flightNo, pointType, coordinates, routeType) {
|
||
// 创建点几何体
|
||
const pointGeometry = new Point(coordinates);
|
||
|
||
// 创建Feature
|
||
const pointFeature = new Feature({
|
||
geometry: pointGeometry,
|
||
name: `${flightNo} ${pointType === 'start' ? '起点' : '终点'}`,
|
||
flightNo: flightNo,
|
||
pointType: pointType
|
||
});
|
||
|
||
// 设置ID
|
||
pointFeature.setId(`${flightNo}-${pointType}`);
|
||
|
||
// 设置样式
|
||
pointFeature.setStyle(getPointStyle(pointType, routeType, flightNo));
|
||
|
||
// 添加到数据源
|
||
aircraftRouteSource.addFeature(pointFeature);
|
||
|
||
return pointFeature;
|
||
}
|
||
|
||
// 更新路线点标记位置
|
||
function updateRoutePointMarker(flightNo, pointType, coordinates) {
|
||
const pointFeature = aircraftRouteSource.getFeatureById(`${flightNo}-${pointType}`);
|
||
|
||
if (pointFeature) {
|
||
// 更新几何体
|
||
pointFeature.getGeometry().setCoordinates(coordinates);
|
||
} else {
|
||
// 如果不存在,创建新的点标记
|
||
const routeType = aircraftRoutes.value[flightNo]?.routeType || 'UNKNOWN';
|
||
createRoutePointMarker(flightNo, pointType, coordinates, routeType);
|
||
}
|
||
}
|
||
|
||
// 移除飞机路线
|
||
function removeAircraftRoute(flightNo) {
|
||
if (!aircraftRouteSource) return;
|
||
|
||
// 清除该航班的定时器(如果存在)
|
||
if (aircraftRouteTimers[flightNo]) {
|
||
clearTimeout(aircraftRouteTimers[flightNo]);
|
||
delete aircraftRouteTimers[flightNo];
|
||
console.log(`已清除航班 ${flightNo} 的路线定时器`);
|
||
}
|
||
|
||
removeAircraftRouteFeatures(flightNo)
|
||
|
||
// 移除路线Feature
|
||
const feature = aircraftRouteSource.getFeatureById(flightNo);
|
||
if (feature) {
|
||
aircraftRouteSource.removeFeature(feature);
|
||
}
|
||
|
||
// 移除起点Feature
|
||
const startFeature = aircraftRouteSource.getFeatureById(`${flightNo}-start`);
|
||
if (startFeature) {
|
||
aircraftRouteSource.removeFeature(startFeature);
|
||
}
|
||
|
||
// 移除终点Feature
|
||
const endFeature = aircraftRouteSource.getFeatureById(`${flightNo}-end`);
|
||
if (endFeature) {
|
||
aircraftRouteSource.removeFeature(endFeature);
|
||
}
|
||
|
||
// 从存储中移除
|
||
if (aircraftRoutes.value[flightNo]) {
|
||
delete aircraftRoutes.value[flightNo];
|
||
}
|
||
if (aircraftRouteProgress[flightNo]) {
|
||
delete aircraftRouteProgress[flightNo];
|
||
}
|
||
|
||
if (vehicles.value[flightNo]) {
|
||
const currentVehicle = vehicles.value[flightNo];
|
||
const isAircraftVehicle = currentVehicle.isAircraft || currentVehicle.type?.toUpperCase() === 'AIRCRAFT';
|
||
if (isAircraftVehicle) {
|
||
vehicles.value[flightNo] = {
|
||
...currentVehicle,
|
||
hasAircraftRoute: false
|
||
};
|
||
const v = vehicles.value[flightNo];
|
||
if (v.feature) {
|
||
if (styleManager.value) {
|
||
setVehicleStyleRespectVisibility(
|
||
flightNo,
|
||
styleManager.value.getVehicleStyle(flightNo, v.speed, v.heading)
|
||
);
|
||
} else {
|
||
setVehicleStyleRespectVisibility(flightNo, getDefaultVehicleStyle(v));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`航班 ${flightNo} 的路线已完全移除`);
|
||
}
|
||
|
||
function handlePathConflictMessage(message) {
|
||
if (!pathConflictDisplayEnabled.value) {
|
||
clearPathConflictDisplay();
|
||
return;
|
||
}
|
||
|
||
const conflict = normalizePathConflictMessage(message);
|
||
if (!conflict) return;
|
||
|
||
if (conflict.kind === 'resume') {
|
||
const matchedConflicts = getPathConflictsToClear(conflict.key || conflict.vehicleName);
|
||
clearPathConflict(conflict.key || conflict.vehicleName);
|
||
const statusIds = new Set([conflict.vehicleName, conflict.aircraftName]);
|
||
matchedConflicts.forEach(item => {
|
||
statusIds.add(item.vehicleName);
|
||
statusIds.add(item.aircraftName);
|
||
});
|
||
statusIds.forEach(id => {
|
||
if (id) clearConflictStatus(id);
|
||
});
|
||
if (!currentPathConflict.value) clearAlertMessage();
|
||
return;
|
||
}
|
||
|
||
const previous = pathConflicts.value?.[conflict.key];
|
||
upsertPathConflict(conflict);
|
||
|
||
if (conflict.kind === 'alert') {
|
||
const alarmLevel = conflict.level === 'WARNING' ? 'medium' : 'high';
|
||
if (!previous || previous.kind !== 'alert' || previous.level !== conflict.level) {
|
||
addAlarm({
|
||
carId: conflict.vehicleName || '未知车辆',
|
||
carType: vehicles.value[conflict.vehicleName]?.type || '路径冲突',
|
||
time: `${formatTimeRange(new Date())} ${conflict.aircraftName || '航空器'}与${conflict.vehicleName || '车辆'}`,
|
||
description: conflict.title,
|
||
level: alarmLevel,
|
||
type: 'car',
|
||
rawData: conflict.rawData
|
||
});
|
||
}
|
||
|
||
showAlert(`${conflict.title}:${conflict.aircraftName || '航空器'} 与 ${conflict.vehicleName || '车辆'}`, conflict.level === 'WARNING' ? 'warning' : 'alarm', 0);
|
||
markConflictStatus(conflict.vehicleName, conflict.level === 'WARNING' ? 'warning' : 'alarm', conflict.title);
|
||
markConflictStatus(conflict.aircraftName, conflict.level === 'WARNING' ? 'warning' : 'alarm', conflict.title);
|
||
}
|
||
}
|
||
|
||
function upsertPathConflict(conflict) {
|
||
const key = conflict.key || `${Date.now()}`;
|
||
const next = {
|
||
...(pathConflicts.value[key] || {}),
|
||
...conflict,
|
||
key,
|
||
updatedAt: Date.now()
|
||
};
|
||
pathConflicts.value = {
|
||
...pathConflicts.value,
|
||
[key]: next
|
||
};
|
||
|
||
renderPathConflictFeature(next);
|
||
}
|
||
|
||
function renderPathConflictFeature(conflict) {
|
||
if (!pathConflictSource || !conflict.conflictPoint) return;
|
||
|
||
const latitude = Number(conflict.conflictPoint.latitude);
|
||
const longitude = Number(conflict.conflictPoint.longitude);
|
||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return;
|
||
|
||
const coordinates = transform([longitude, latitude], 'EPSG:4326', 'EPSG:4528');
|
||
let feature = pathConflictSource.getFeatureById(conflict.key);
|
||
if (!feature) {
|
||
feature = new Feature({
|
||
geometry: new Point(coordinates),
|
||
conflictType: 'path_conflict'
|
||
});
|
||
feature.setId(conflict.key);
|
||
pathConflictSource.addFeature(feature);
|
||
} else {
|
||
feature.getGeometry().setCoordinates(coordinates);
|
||
}
|
||
|
||
feature.setStyle(getPathConflictStyle(conflict));
|
||
}
|
||
|
||
function clearPathConflict(key) {
|
||
const next = { ...(pathConflicts.value || {}) };
|
||
const keys = Object.keys(next).filter(itemKey => {
|
||
const item = next[itemKey];
|
||
return itemKey === key || item.vehicleName === key || item.aircraftName === key;
|
||
});
|
||
|
||
keys.forEach(itemKey => {
|
||
if (pathConflictSource) {
|
||
const feature = pathConflictSource.getFeatureById(itemKey);
|
||
if (feature) pathConflictSource.removeFeature(feature);
|
||
}
|
||
delete next[itemKey];
|
||
});
|
||
|
||
pathConflicts.value = next;
|
||
}
|
||
|
||
function getPathConflictsToClear(key) {
|
||
return Object.entries(pathConflicts.value || {})
|
||
.filter(([itemKey, item]) => itemKey === key || item.vehicleName === key || item.aircraftName === key)
|
||
.map(([, item]) => item);
|
||
}
|
||
|
||
function clearPathConflictDisplay() {
|
||
pathConflicts.value = {};
|
||
if (pathConflictSource) {
|
||
pathConflictSource.clear();
|
||
}
|
||
}
|
||
|
||
// 处理冲突告警和预警
|
||
function handlePathConflictAlert(payload) {
|
||
if (!payload) {
|
||
console.error('冲突告警消息格式错误:', payload);
|
||
return;
|
||
}
|
||
|
||
console.log('收到冲突告警/预警:', payload);
|
||
|
||
const object1 = payload.object1 || {};
|
||
const object2 = payload.object2 || {};
|
||
const vehicleId = object1.objectName || '未知车辆';
|
||
const otherVehicleId = object2.objectName || '未知车辆';
|
||
const distance = payload.object2Distance || 0;
|
||
const message = payload.message || `与${otherVehicleId}可能发生冲突`;
|
||
const isAlert = (payload.alertType === 'CONFLICT_ALERT') || (payload.alert === true) || (payload.alertLevel === 'CRITICAL');
|
||
const isWarning = (payload.alertType === 'CONFLICT_WARNING') || (payload.warning === true);
|
||
if (!isAlert && isWarning) {
|
||
console.log('处理冲突预警:', vehicleId, otherVehicleId);
|
||
const warningMessage = `预警:${message}`;
|
||
showAlert(warningMessage, 'warning', 8000);
|
||
|
||
// 添加到告警列表
|
||
addAlarm({
|
||
carId: vehicleId,
|
||
carType: vehicles.value[vehicleId]?.type || '未知类型',
|
||
time: `${formatTimeRange(new Date())}与${otherVehicleId}发生冲`,
|
||
description: '突预警',
|
||
level: 'medium',
|
||
type: 'car',
|
||
rawData: payload
|
||
});
|
||
|
||
// 冲突预警默认10秒无新消息自动清除
|
||
markConflictStatus(vehicleId, 'warning', message, 10000);
|
||
markConflictStatus(otherVehicleId, 'warning', message, 10000);
|
||
} else if (isAlert) {
|
||
console.log('处理冲突告警:', vehicleId, otherVehicleId);
|
||
const alertMessage = `⚠️ 告警:${message}`;
|
||
showAlert(alertMessage, 'alarm', 10000);
|
||
|
||
// 添加到告警列表
|
||
addAlarm({
|
||
carId: vehicleId,
|
||
carType: vehicles.value[vehicleId]?.type || '未知类型',
|
||
time: `${formatTimeRange(new Date())}与${otherVehicleId}发生冲`,
|
||
description: '突告警',
|
||
level: 'high',
|
||
type: 'car',
|
||
rawData: payload
|
||
});
|
||
|
||
// 冲突告警默认15秒无新消息自动清除
|
||
markConflictStatus(vehicleId, 'alarm', message, 15000);
|
||
markConflictStatus(otherVehicleId, 'alarm', message, 15000);
|
||
} else {
|
||
console.log(`未知的冲突消息类型: ${payload.messageType || payload.alertType}`);
|
||
}
|
||
}
|
||
|
||
// 处理规则违规(超速和越界)
|
||
function handleRuleViolation(payload) {
|
||
if (!payload) {
|
||
console.error('规则违规消息格式错误:', payload);
|
||
return;
|
||
}
|
||
|
||
console.log('收到规则违规:', payload);
|
||
|
||
const vehicleId = payload.object_id || payload.vehicleId || payload.vehicleLicense || '未知车辆';
|
||
const description = payload.description || '';
|
||
const limitValue = payload.limitValue;
|
||
const actualValue = payload.actualValue;
|
||
const ruleName = payload.ruleName || '交通规则';
|
||
const violationType = payload.violationType || '';
|
||
|
||
// 根据violationType区分超速和越界
|
||
switch (violationType.toUpperCase()) {
|
||
|
||
case 'SPEED':
|
||
// 超速违规
|
||
handleSpeedViolation(vehicleId, payload);
|
||
break;
|
||
case 'ACCESS':
|
||
// 越界处理
|
||
handleUnauthorizedEntry(vehicleId, payload);
|
||
break;
|
||
|
||
default:
|
||
console.log(`未知的规则违规类型: ${violationType}`);
|
||
// 默认按越界处理
|
||
handleUnauthorizedEntry(vehicleId, payload);
|
||
}
|
||
}
|
||
|
||
// 处理超速违规
|
||
function handleSpeedViolation(vehicleId, payload) {
|
||
const actualValue = payload.actualValue;
|
||
const limitValue = payload.limitValue;
|
||
const description = payload.description || '超速违规';
|
||
const ruleName = payload.ruleName || '速度限制';
|
||
|
||
console.log(`检测到超速违规: ${vehicleId}, 实际速度: ${actualValue}, 限速: ${limitValue}`);
|
||
|
||
// 显示超速告警提示
|
||
showAlert(`⚠️ 超速告警:${vehicleId} ${description}`, 'warning', 8000);
|
||
|
||
// 添加到告警列表,提供更详细的信息
|
||
addAlarm({
|
||
carId: vehicleId,
|
||
carType: vehicles.value[vehicleId]?.type || '未知类型',
|
||
time: `${formatTimeRange(new Date())}超速行驶`,
|
||
description: `,速度达到${actualValue}km/h`,
|
||
level: actualValue > limitValue * 1.5 ? 'high' : 'medium', // 超速50%以上为高级别告警
|
||
type: 'speed',
|
||
limitValue: limitValue,
|
||
actualValue: actualValue,
|
||
ruleName: ruleName,
|
||
rawData: payload
|
||
});
|
||
|
||
// 在地图上标记该车辆的超速状态
|
||
if (vehicles.value[vehicleId]) {
|
||
// 避免重复设置超速状态导致闪烁
|
||
const alreadyInSpeedViolation = vehicles.value[vehicleId].speedViolation;
|
||
|
||
// 强制设置超速状态,确保图标正确显示
|
||
vehicles.value[vehicleId].info = false;
|
||
vehicles.value[vehicleId].alarm = false;
|
||
vehicles.value[vehicleId].warning = false; // 清除warning状态,使用speedViolation状态
|
||
vehicles.value[vehicleId].critical = false;
|
||
vehicles.value[vehicleId].speedViolation = true; // 标记为超速状态
|
||
vehicles.value[vehicleId].limitValue = limitValue;
|
||
vehicles.value[vehicleId].actualValue = actualValue;
|
||
vehicles.value[vehicleId].description = description;
|
||
vehicles.value[vehicleId].ruleName = ruleName;
|
||
vehicles.value[vehicleId].lastSpeedViolationTime = Date.now(); // 记录最后一次超速时间
|
||
|
||
// 设置状态锁定,确保一段时间内不会改变状态
|
||
if (!vehicles.value[vehicleId].statusLock) {
|
||
vehicles.value[vehicleId].statusLock = {
|
||
active: false,
|
||
type: null,
|
||
until: 0
|
||
};
|
||
}
|
||
|
||
// 锁定超速状态20秒,确保图标不会来回切换
|
||
vehicles.value[vehicleId].statusLock = {
|
||
active: true,
|
||
type: 'speedViolation',
|
||
until: Date.now() + 20000 // 20秒锁定
|
||
};
|
||
|
||
// 清除任何可能存在的缓存图标,确保使用警告图标
|
||
vehicles.value[vehicleId].cachedIconSrc = null;
|
||
|
||
// 更新车辆图标为超速警告图标(只在首次设置或状态改变时更新)
|
||
if (!alreadyInSpeedViolation) {
|
||
if (vehicles.value[vehicleId].feature && styleManager.value) {
|
||
setVehicleStyleRespectVisibility(
|
||
vehicleId,
|
||
styleManager.value.getVehicleStyle(
|
||
vehicleId,
|
||
vehicles.value[vehicleId].speed,
|
||
vehicles.value[vehicleId].heading
|
||
)
|
||
);
|
||
vehicles.value[vehicleId].lastIconUpdateTime = Date.now();
|
||
}
|
||
}
|
||
|
||
// 更新标签显示为超速状态(只显示超速相关信息)
|
||
if (vehicles.value[vehicleId].position && labelSystem.value) {
|
||
// 先移除可能存在的旧标签,避免重复显示
|
||
labelSystem.value.removeVehicleLabel(vehicleId);
|
||
|
||
// 然后创建新的超速状态标签
|
||
updateVehicleLabelIfAllowed(vehicleId, vehicles.value[vehicleId].position, actualValue || vehicles.value[vehicleId].speed, {
|
||
description: description || `超速违规`,
|
||
limitValue: limitValue,
|
||
actualValue: actualValue,
|
||
ruleName: ruleName,
|
||
isSpeedViolation: true // 标记为超速状态,避免显示默认信息
|
||
});
|
||
}
|
||
|
||
// 设置或重置超速状态自动清除计时器
|
||
if (speedViolationTimers[vehicleId]) {
|
||
clearTimeout(speedViolationTimers[vehicleId]);
|
||
}
|
||
|
||
// 设置新的超时计时器,如果在一定时间内(如20秒)没有再次接收到超速消息,则自动清除超速状态
|
||
speedViolationTimers[vehicleId] = setTimeout(() => {
|
||
// 确保车辆仍然存在
|
||
if (vehicles.value[vehicleId]) {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
console.log(`超速状态超时: ${vehicleId}, 自动清除超速状态`);
|
||
|
||
// 检查是否可以清除状态锁定
|
||
if (!vehicles.value[vehicleId].statusLock ||
|
||
!vehicles.value[vehicleId].statusLock.active ||
|
||
Date.now() > vehicles.value[vehicleId].statusLock.until) {
|
||
|
||
// 清除超速状态
|
||
clearSpeedViolationStatus(vehicleId);
|
||
|
||
// 清除计时器引用
|
||
delete speedViolationTimers[vehicleId];
|
||
} else {
|
||
// 状态仍然锁定,延迟清除
|
||
console.log(`车辆${vehicleId}状态仍然锁定,延迟清除超速状态`);
|
||
|
||
// 重新设置计时器,等待锁定结束
|
||
speedViolationTimers[vehicleId] = setTimeout(() => {
|
||
clearSpeedViolationStatus(vehicleId);
|
||
delete speedViolationTimers[vehicleId];
|
||
}, vehicles.value[vehicleId].statusLock.until - Date.now());
|
||
}
|
||
}
|
||
}, 20000); // 20秒超时,确保状态稳定
|
||
}
|
||
}
|
||
|
||
// 处理越界告警
|
||
function handleUnauthorizedEntry(vehicleId, payload) {
|
||
const description = payload.description || '越界告警';
|
||
const alertLevel = payload.alertLevel || 'CRITICAL';
|
||
const ruleName = payload.ruleName || '区域控制';
|
||
|
||
console.log(`检测到越界告警: ${vehicleId}, ${description}, 规则: ${ruleName}`);
|
||
|
||
// 显示越界告警提示
|
||
showAlert(`⚠️ 越界告警:${vehicleId} ${description}`, 'critical', 10000);
|
||
|
||
// 添加到告警列表
|
||
addAlarm({
|
||
carId: vehicleId,
|
||
carType: vehicles.value[vehicleId]?.type || '未知类型',
|
||
time: `${formatTimeRange(new Date())}越界`,
|
||
description: '越界告警',
|
||
level: 'high',
|
||
type: 'report', // 修改类型为report,与AlarmNotification.vue中的标签页类型匹配
|
||
rawData: payload
|
||
});
|
||
console.log('越界告警------------------------------------', vehicles.value[vehicleId].type);
|
||
// 在地图上标记该车辆的越界状态
|
||
if (vehicles.value[vehicleId]) {
|
||
vehicles.value[vehicleId].critical = true;
|
||
vehicles.value[vehicleId].alarm = false;
|
||
vehicles.value[vehicleId].warning = false;
|
||
vehicles.value[vehicleId].info = false;
|
||
vehicles.value[vehicleId].speedViolation = false;
|
||
vehicles.value[vehicleId].description = description;
|
||
vehicles.value[vehicleId].ruleName = ruleName;
|
||
|
||
// 更新车辆图标为越界警告图标
|
||
if (vehicles.value[vehicleId].feature && styleManager.value) {
|
||
setVehicleStyleRespectVisibility(
|
||
vehicleId,
|
||
styleManager.value.getVehicleStyle(
|
||
vehicleId,
|
||
vehicles.value[vehicleId].speed,
|
||
vehicles.value[vehicleId].heading
|
||
)
|
||
);
|
||
}
|
||
|
||
// 更新标签显示
|
||
if (vehicles.value[vehicleId].position && labelSystem.value) {
|
||
updateVehicleLabelIfAllowed(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed, {
|
||
description: description,
|
||
ruleName: ruleName,
|
||
isUnauthorizedEntry: true
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清除车辆超速状态的专门函数
|
||
function clearSpeedViolationStatus(vehicleId) {
|
||
const vehicle = vehicles.value[vehicleId];
|
||
if (!vehicle) return;
|
||
|
||
console.log(`开始清除车辆${vehicleId}的超速状态`);
|
||
|
||
// 检查是否可以清除状态锁定
|
||
if (vehicle.statusLock && vehicle.statusLock.active && Date.now() < vehicle.statusLock.until) {
|
||
console.log(`车辆${vehicleId}状态锁定中,暂不清除超速状态`);
|
||
return;
|
||
}
|
||
|
||
// 清除所有告警状态
|
||
vehicle.info = false;
|
||
vehicle.alarm = false;
|
||
vehicle.warning = false;
|
||
vehicle.critical = false;
|
||
vehicle.speedViolation = false;
|
||
vehicle.limitValue = undefined;
|
||
vehicle.actualValue = undefined;
|
||
vehicle.description = undefined;
|
||
vehicle.ruleName = undefined;
|
||
|
||
// 清除状态锁定
|
||
if (vehicle.statusLock) {
|
||
vehicle.statusLock.active = false;
|
||
vehicle.statusLock.type = null;
|
||
vehicle.statusLock.until = 0;
|
||
}
|
||
|
||
// 清除缓存的图标,确保使用默认图标
|
||
vehicle.cachedIconSrc = null;
|
||
vehicle.lastIconUpdateTime = Date.now();
|
||
|
||
// 更新车辆图标为默认图标
|
||
if (vehicle.feature && styleManager.value) {
|
||
setVehicleStyleRespectVisibility(
|
||
vehicleId,
|
||
styleManager.value.getVehicleStyle(vehicleId, vehicle.speed, vehicle.heading)
|
||
);
|
||
}
|
||
|
||
// 更新标签显示为正常状态(只显示车辆ID,不显示0.00km/h)
|
||
if (vehicle.position && labelSystem.value) {
|
||
// 先移除可能存在的旧标签,避免重复显示
|
||
labelSystem.value.removeVehicleLabel(vehicleId);
|
||
|
||
// 创建新的标准标签
|
||
updateVehicleLabelIfAllowed(vehicleId, vehicle.position, vehicle.speed > 0.1 ? vehicle.speed : 0);
|
||
}
|
||
|
||
console.log(`已成功清除车辆${vehicleId}的超速状态`);
|
||
}
|
||
|
||
// 获取车辆类型键值
|
||
function getVehicleTypeKey(vehicle) {
|
||
if (!vehicle) return 'UNMANNED_VEHICLE';
|
||
const override = vehicleDisplayStore.getVehicleTypeOverride(vehicle.id);
|
||
const vehicleType = String(override || vehicle.type || '').toUpperCase();
|
||
if (!vehicleType) return 'UNMANNED_VEHICLE';
|
||
|
||
// 根据车辆类型和特征确定分类键值
|
||
if (vehicleType === 'AIRCRAFT' || vehicleType === 'AIRCRAFT_IN' || vehicleType === 'AIRCRAFT_OUT' || vehicleType === 'HANGKONG') {
|
||
return 'AIRCRAFT';
|
||
} else if (vehicle.isAircraft) {
|
||
return 'AIRCRAFT';
|
||
} else if (vehicleType === 'UNMANNED_VEHICLE' || vehicleType === 'WUREN' || vehicle.isUnmannedVehicle) {
|
||
return 'UNMANNED_VEHICLE';
|
||
} else if (vehicleType === 'SPECIAL_VEHICLE' || vehicleType === 'TEQIN' || vehicle.isSpecialVehicle) {
|
||
return 'AIRPORT_VEHICLE';
|
||
} else if (vehicleType === 'SHUTTLE_VEHICLE' || vehicle.isShuttleVehicle) {
|
||
return 'SHUTTLE_VEHICLE';
|
||
}
|
||
|
||
// 默认返回无人车类型
|
||
return 'UNMANNED_VEHICLE';
|
||
}
|
||
|
||
// 仅当左侧类别的 showLabel 为真时才允许更新/创建标签
|
||
function isLabelAllowedForVehicle(vehicleId) {
|
||
const vehicle = vehicles.value[vehicleId];
|
||
if (!vehicle) return false;
|
||
const typeKey = getVehicleTypeKey(vehicle);
|
||
if (typeKey !== 'AIRCRAFT' && !vehicleDisplayStore.isVehicleEnabled(vehicleId)) return false;
|
||
const category = vehicleCategories.value?.[typeKey];
|
||
return !!(category && category.showLabel);
|
||
}
|
||
|
||
function updateVehicleLabelIfAllowed(vehicleId, coordinates, speed, alertInfo) {
|
||
if (!labelSystem.value || !labelSystem.value.updateVehicleLabel) return;
|
||
if (isLabelAllowedForVehicle(vehicleId)) {
|
||
labelSystem.value.updateVehicleLabel(vehicleId, coordinates, speed, alertInfo);
|
||
} else {
|
||
// 当不允许显示时,确保现有标签保持隐藏(若已存在)
|
||
if (labelSystem.value.setLabelVisibility) {
|
||
labelSystem.value.setLabelVisibility(vehicleId, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取默认车辆样式
|
||
function getDefaultVehicleStyle(vehicle) {
|
||
if (!vehicle) return new Style({});
|
||
|
||
const typeUpper = String(vehicle.type || '').toUpperCase();
|
||
const isAircraft = vehicle.isAircraft || typeUpper === 'AIRCRAFT' || typeUpper === 'AIRCRAFT_IN' || typeUpper === 'AIRCRAFT_OUT' || typeUpper === 'HANGKONG';
|
||
const isUnmanned = vehicle.isUnmannedVehicle || typeUpper === 'UNMANNED_VEHICLE' || typeUpper === 'WUREN' || String(vehicle.type || '').includes('无人');
|
||
const iconSrc = isAircraft
|
||
? (vehicle.hasAircraftRoute ? aircraftRouteIcon : aircraftIcon)
|
||
: (isUnmanned ? carIcon : specialCar);
|
||
const heading = vehicle.heading || 0;
|
||
|
||
// 计算旋转角度
|
||
const rotationRad = ((heading - 72) * Math.PI) / 180;
|
||
|
||
return new Style({
|
||
image: new Icon({
|
||
src: iconSrc,
|
||
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||
anchor: [0.5, 0.5],
|
||
rotation: rotationRad,
|
||
})
|
||
});
|
||
}
|
||
|
||
function setVehicleStyleRespectVisibility(vehicleId, style) {
|
||
const vehicle = vehicles.value[vehicleId];
|
||
if (!vehicle || !vehicle.feature) return;
|
||
const typeKey = getVehicleTypeKey(vehicle);
|
||
const enabled = typeKey === 'AIRCRAFT' ? true : vehicleDisplayStore.isVehicleEnabled(vehicleId);
|
||
const category = vehicleCategories.value?.[typeKey];
|
||
const visible = enabled && !!(category && category.visible);
|
||
vehicle.feature.setStyle(visible ? style : new Style({}));
|
||
if (!enabled && labelSystem.value?.setLabelVisibility) {
|
||
labelSystem.value.setLabelVisibility(vehicleId, false);
|
||
}
|
||
}
|
||
|
||
function applyVehicleStyleWithVisibility(vehicleId, style) {
|
||
setVehicleStyleRespectVisibility(vehicleId, style);
|
||
}
|
||
|
||
// 默认获取车辆样式函数(用于动画系统)
|
||
// function defaultGetVehicleStyle(vehicleId, speed, heading) {
|
||
// const vehicle = vehicles.value[vehicleId];
|
||
// if (!vehicle) return new Style({});
|
||
|
||
// return getDefaultVehicleStyle(vehicle);
|
||
// }
|
||
|
||
// 清除车辆告警状态(如果需要)
|
||
function clearVehicleAlertStatus(vehicleData) {
|
||
const { object_id, speed } = vehicleData;
|
||
|
||
// 检查该车辆是否已存在且有告警状态
|
||
const existingVehicle = vehicles.value[object_id];
|
||
if (!existingVehicle) return;
|
||
|
||
// 检查是否有状态锁定
|
||
if (existingVehicle.statusLock &&
|
||
existingVehicle.statusLock.active &&
|
||
Date.now() < existingVehicle.statusLock.until) {
|
||
// 状态锁定中,不清除状态
|
||
return;
|
||
}
|
||
|
||
// 检查是否有超速状态 - 更严格的清除条件,避免图标来回切换
|
||
if (existingVehicle.speedViolation && existingVehicle.limitValue) {
|
||
// 如果当前速度低于限速值,清除超速状态
|
||
if (speed < existingVehicle.limitValue) {
|
||
// 添加更大的缓冲区,避免在临界值附近频繁切换状态
|
||
// 只有当速度低于限速值的70%时才清除超速状态,确保状态稳定
|
||
const speedBuffer = existingVehicle.limitValue * 0.7;
|
||
|
||
if (speed < speedBuffer) {
|
||
console.log(`检测到车辆${object_id}速度降低到${speed.toFixed(1)}km/h,低于限速${existingVehicle.limitValue}km/h的70%,考虑清除超速状态`);
|
||
|
||
// 检查是否应该清除超速状态
|
||
// 如果最后一次超速时间在15秒内,不清除状态以避免闪烁
|
||
const now = Date.now();
|
||
if (!existingVehicle.lastSpeedViolationTime || now - existingVehicle.lastSpeedViolationTime > 15000) {
|
||
// 检查状态锁定
|
||
if (!existingVehicle.statusLock ||
|
||
!existingVehicle.statusLock.active ||
|
||
now > existingVehicle.statusLock.until) {
|
||
// 清除超速状态
|
||
clearSpeedViolationStatus(object_id);
|
||
console.log(`已清除${object_id}的超速状态,恢复为普通状态`);
|
||
} else {
|
||
console.log(`车辆${object_id}状态锁定中,暂不清除超速状态`);
|
||
}
|
||
} else {
|
||
console.log(`车辆${object_id}刚刚处于超速状态,暂不清除状态以避免闪烁`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
|
||
// 发送心跳
|
||
function sendPing() {
|
||
if (wsService) {
|
||
wsService.send('ping');
|
||
console.log('发送心跳: ping');
|
||
}
|
||
}
|
||
|
||
// 订阅消息
|
||
function sendSubscribe() {
|
||
if (wsService) {
|
||
const message = JSON.stringify({
|
||
type: 'subscribe',
|
||
topics: ['position_update', 'collision_warning', 'path_conflict_status', 'path_conflict_alert', 'rule_violation'],
|
||
timestamp: Date.now()
|
||
});
|
||
wsService.send(message);
|
||
console.log('发送订阅请求');
|
||
}
|
||
}
|
||
|
||
// 清理资源
|
||
function cleanup() {
|
||
// 断开WebSocket连接
|
||
if (wsService) {
|
||
wsService.close();
|
||
wsService = null;
|
||
}
|
||
|
||
if (reconnectTimer) {
|
||
clearTimeout(reconnectTimer);
|
||
reconnectTimer = null;
|
||
}
|
||
|
||
// 清理所有超速状态计时器
|
||
Object.keys(speedViolationTimers).forEach(vehicleId => {
|
||
clearTimeout(speedViolationTimers[vehicleId]);
|
||
delete speedViolationTimers[vehicleId];
|
||
});
|
||
|
||
// 清理所有飞机路线定时器
|
||
Object.keys(aircraftRouteTimers).forEach(flightNo => {
|
||
clearTimeout(aircraftRouteTimers[flightNo]);
|
||
delete aircraftRouteTimers[flightNo];
|
||
});
|
||
console.log('已清理所有飞机路线定时器');
|
||
|
||
// 清理所有冲突/违规状态定时器
|
||
const statusTimers = violationStatusTimers.value || {};
|
||
Object.keys(statusTimers).forEach((key) => {
|
||
clearTimeout(statusTimers[key]);
|
||
delete statusTimers[key];
|
||
});
|
||
violationStatusTimers.value = {};
|
||
|
||
// 移除图层
|
||
if (vehicleLayer && props.map) {
|
||
props.map.removeLayer(vehicleLayer);
|
||
vehicleLayer = null;
|
||
}
|
||
|
||
// 移除飞机路线图层
|
||
if (aircraftRouteLayer && props.map) {
|
||
props.map.removeLayer(aircraftRouteLayer);
|
||
aircraftRouteLayer = null;
|
||
}
|
||
|
||
if (pathConflictLayer && props.map) {
|
||
props.map.removeLayer(pathConflictLayer);
|
||
pathConflictLayer = null;
|
||
pathConflictSource = null;
|
||
}
|
||
|
||
// 清空飞机路线数据
|
||
aircraftRoutes.value = {};
|
||
pathConflicts.value = {};
|
||
|
||
// 移除标签
|
||
if (props.map && labelSystem.value) {
|
||
Object.values(vehicles.value).forEach(vehicle => {
|
||
if (vehicle.overlay) {
|
||
console.log(`移除标签: ${vehicle.id}`);
|
||
labelSystem.value.removeVehicleLabel(vehicle.id);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 清空车辆数据
|
||
vehicles.value = {};
|
||
}
|
||
|
||
// 组件挂载时初始化
|
||
onMounted(() => {
|
||
// 创建图层并连接WebSocket
|
||
if (props.map) {
|
||
createVehicleLayer();
|
||
createAircraftRouteLayer(); // 创建飞机路线图层
|
||
createPathConflictLayer();
|
||
}
|
||
|
||
// 连接WebSocket
|
||
connectWebSocket();
|
||
|
||
const allowDebugInject =
|
||
import.meta.env.DEV ||
|
||
window.APP_CONFIG?.ENABLE_ROUTE_DEBUG === true ||
|
||
new URLSearchParams(window.location.search).has('debugRoute')
|
||
|
||
if (allowDebugInject) {
|
||
window.__injectWsMessage = (msg) => {
|
||
const raw = typeof msg === 'string' ? msg : JSON.stringify(msg)
|
||
handleWsMessage(raw)
|
||
}
|
||
window.__injectAircraftRouteUpdate = (payload, options = { fit: true }) => {
|
||
handleAircraftRouteUpdate(payload)
|
||
if (options && options.fit === false) return
|
||
try {
|
||
const flightNo = payload?.flightNo
|
||
const route = flightNo ? aircraftRoutes.value?.[flightNo] : null
|
||
if (route && route.feature && props.map) {
|
||
const extent = route.feature.getGeometry().getExtent()
|
||
props.map.getView().fit(extent, { padding: [50, 50, 50, 50], duration: 500 })
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
|
||
// 设置定时发送心跳
|
||
const pingInterval = setInterval(() => {
|
||
if (wsService && wsConnected.value) {
|
||
sendPing();
|
||
}
|
||
}, 30000); // 每30秒发送一次心跳
|
||
|
||
// 组件卸载时清理资源
|
||
onUnmounted(() => {
|
||
clearInterval(pingInterval);
|
||
if (reconnectTimer) {
|
||
clearTimeout(reconnectTimer);
|
||
reconnectTimer = null;
|
||
}
|
||
try {
|
||
delete window.__injectWsMessage
|
||
delete window.__injectAircraftRouteUpdate
|
||
} catch (e) {}
|
||
cleanup();
|
||
});
|
||
});
|
||
|
||
// 组件被激活时(切换回该路由时)
|
||
onActivated(() => {
|
||
console.log('VehicleMovementControl组件被激活');
|
||
// 如果WebSocket未连接,重新连接
|
||
if (!wsConnected.value && !wsService) {
|
||
console.log('组件激活,WebSocket未连接,正在重新连接...');
|
||
// 延迟一点时间再连接,确保DOM已完全渲染
|
||
reconnectTimer = setTimeout(() => {
|
||
connectWebSocket();
|
||
}, 500);
|
||
} else {
|
||
console.log('组件激活,WebSocket已连接');
|
||
}
|
||
});
|
||
|
||
// 组件被停用时(切换到其他路由时)
|
||
onDeactivated(() => {
|
||
console.log('VehicleMovementControl组件被停用');
|
||
// 可以选择在这里关闭WebSocket,也可以保持连接
|
||
// 如果希望在组件隐藏时断开连接,可以取消下面的注释
|
||
/*
|
||
if (wsService) {
|
||
console.log('组件停用,关闭WebSocket连接');
|
||
wsService.close();
|
||
wsService = null;
|
||
wsConnected.value = false;
|
||
}
|
||
*/
|
||
});
|
||
|
||
// 监听地图实例变化
|
||
watch(() => props.map, (newMap) => {
|
||
if (newMap) {
|
||
createVehicleLayer();
|
||
createAircraftRouteLayer(); // 创建飞机路线图层
|
||
createPathConflictLayer();
|
||
}
|
||
});
|
||
|
||
watch(pathConflictDisplayEnabled, (enabled) => {
|
||
if (!enabled) {
|
||
clearPathConflictDisplay();
|
||
clearAlertMessage();
|
||
}
|
||
});
|
||
|
||
// 向外暴露方法
|
||
defineExpose({
|
||
updateVehiclePosition,
|
||
wsConnected,
|
||
sendPing,
|
||
sendSubscribe,
|
||
vehicleCategories,
|
||
weatherStationVisible,
|
||
vehicleDetailVisible,
|
||
vehicleDetail,
|
||
alarmList, // 暴露告警列表
|
||
toggleWeatherStationVisibility() {
|
||
weatherStationVisible.value = !weatherStationVisible.value;
|
||
},
|
||
toggleVehicleDetailVisibility() {
|
||
vehicleDetailVisible.value = !vehicleDetailVisible.value;
|
||
},
|
||
updateVehicleDetail(data) {
|
||
if (data) {
|
||
vehicleDetail.value = {...vehicleDetail.value, ...data};
|
||
vehicleDetailVisible.value = true;
|
||
}
|
||
},
|
||
showVehicleDetail(vehicleId, pixelPosition) {
|
||
if (vehicles.value[vehicleId]) {
|
||
// 更新车辆详情
|
||
vehicleDetail.value = {
|
||
id: vehicleId,
|
||
type: vehicles.value[vehicleId].type,
|
||
status: '任务中',
|
||
startTime: '11-19 11:30',
|
||
currentLocation: '当前位置',
|
||
startLocation: 'T1航站楼',
|
||
endLocation: 'T3航站楼',
|
||
totalDistance: '1.3km',
|
||
battery: '60%',
|
||
manager: '张三',
|
||
phone: '18661910988'
|
||
};
|
||
|
||
// 设置弹窗位置
|
||
if (pixelPosition) {
|
||
const viewportPosition = props.map.getViewport().getBoundingClientRect();
|
||
detailPopupStyle.value = {
|
||
left: (viewportPosition.left + pixelPosition[0] + 10) + 'px',
|
||
top: (viewportPosition.top + pixelPosition[1] + 10) + 'px'
|
||
};
|
||
}
|
||
|
||
// 显示详情弹窗
|
||
vehicleDetailVisible.value = true;
|
||
}
|
||
},
|
||
setCategoryVisibility(type, { visible, showLabel }) {
|
||
console.log(`VehicleMovementControl: 设置分类 ${type} 可见性`, { visible, showLabel });
|
||
|
||
if (vehicleCategories.value[type]) {
|
||
// 更新分类设置
|
||
vehicleCategories.value[type].visible = visible;
|
||
vehicleCategories.value[type].showLabel = showLabel;
|
||
|
||
console.log(`更新分类设置完成: ${type}`, vehicleCategories.value[type]);
|
||
|
||
// 立即应用更改 - 更新所有属于此类别的车辆
|
||
Object.values(vehicles.value).forEach(vehicle => {
|
||
let vehicleTypeKey = getVehicleTypeKey(vehicle);
|
||
|
||
// 如果车辆属于当前修改的类别
|
||
if (vehicleTypeKey === type) {
|
||
console.log(`更新车辆 ${vehicle.id} (${vehicleTypeKey}) 的显示状态:`, { visible, showLabel });
|
||
|
||
// 更新图标可见性
|
||
if (vehicle.feature) {
|
||
if (visible) {
|
||
// 显示图标
|
||
if (styleManager.value) {
|
||
setVehicleStyleRespectVisibility(
|
||
vehicle.id,
|
||
styleManager.value.getVehicleStyle(vehicle.id, vehicle.speed, vehicle.heading)
|
||
);
|
||
} else {
|
||
setVehicleStyleRespectVisibility(
|
||
vehicle.id,
|
||
getDefaultVehicleStyle(vehicle)
|
||
);
|
||
}
|
||
} else {
|
||
// 隐藏图标
|
||
vehicle.feature.setStyle(new Style({}));
|
||
}
|
||
}
|
||
|
||
// 控制文本标签可见性
|
||
if (labelSystem.value) {
|
||
if (labelSystem.value.setLabelVisibility) {
|
||
labelSystem.value.setLabelVisibility(vehicle.id, showLabel);
|
||
} else if (labelSystem.value.updateVehicleLabel) {
|
||
// 如果没有setLabelVisibility方法,使用updateVehicleLabel
|
||
if (showLabel && vehicle.position) {
|
||
updateVehicleLabelIfAllowed(vehicle.id, vehicle.position, vehicle.speed);
|
||
} else {
|
||
labelSystem.value.removeVehicleLabel(vehicle.id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
},
|
||
reconnectWebSocket() {
|
||
console.log('手动重连WebSocket');
|
||
connectWebSocket();
|
||
},
|
||
|
||
// 添加平滑动画相关方法
|
||
startVehicleSmoothing() {
|
||
if (animationSystem.value) {
|
||
animationSystem.value.startAnimationLoop();
|
||
}
|
||
},
|
||
|
||
stopVehicleSmoothing() {
|
||
if (animationSystem.value) {
|
||
animationSystem.value.stopAnimationLoop();
|
||
}
|
||
},
|
||
|
||
resetVehicleAnimations() {
|
||
if (animationSystem.value) {
|
||
animationSystem.value.resetAnimations();
|
||
}
|
||
},
|
||
|
||
// 飞机路线相关方法
|
||
getAircraftRoutes() {
|
||
return aircraftRoutes.value;
|
||
},
|
||
|
||
showAircraftRoute(flightNo) {
|
||
const route = aircraftRoutes.value[flightNo];
|
||
if (route && route.feature) {
|
||
// 定位到路线
|
||
const extent = route.feature.getGeometry().getExtent();
|
||
props.map.getView().fit(extent, {
|
||
padding: [50, 50, 50, 50],
|
||
duration: 1000
|
||
});
|
||
}
|
||
},
|
||
|
||
setAircraftRouteVisibility(visible) {
|
||
if (aircraftRouteLayer) {
|
||
aircraftRouteLayer.setVisible(visible);
|
||
}
|
||
},
|
||
|
||
// 飞机路线点标记相关方法
|
||
createRoutePointMarker(flightNo, pointType, coordinates, routeType) {
|
||
return createRoutePointMarker(flightNo, pointType, coordinates, routeType);
|
||
},
|
||
|
||
updateRoutePointMarker(flightNo, pointType, coordinates) {
|
||
return updateRoutePointMarker(flightNo, pointType, coordinates);
|
||
},
|
||
|
||
removeAircraftRoute(flightNo) {
|
||
return removeAircraftRoute(flightNo);
|
||
},
|
||
|
||
debugInjectAircraftRouteUpdate(payload) {
|
||
return handleAircraftRouteUpdate(payload)
|
||
},
|
||
|
||
// 告警通知相关方法
|
||
showAlarmNotification(message, type, duration) {
|
||
if (alarmNotification.value) {
|
||
alarmNotification.value.showNotification(message, type, duration);
|
||
}
|
||
},
|
||
hideAlarmNotification() {
|
||
if (alarmNotification.value) {
|
||
alarmNotification.value.hideNotification();
|
||
}
|
||
},
|
||
handleAlarmClose() {
|
||
// 告警通知组件关闭时,可以在这里执行一些清理或状态重置
|
||
console.log('告警通知组件已关闭');
|
||
},
|
||
addAlarm(alarm) {
|
||
return addAlarm(alarm);
|
||
},
|
||
updateAlarm(alarmId, updates) {
|
||
return updateAlarm(alarmId, updates);
|
||
},
|
||
removeAlarm(alarmId) {
|
||
return removeAlarm(alarmId);
|
||
},
|
||
clearAlarms() {
|
||
return clearAlarms();
|
||
},
|
||
updateAlarmNotification() {
|
||
return updateAlarmNotification();
|
||
},
|
||
toggleAlarmNotification() {
|
||
return toggleAlarmNotification();
|
||
},
|
||
handleAircraftRouteUpdate(payload) {
|
||
return handleAircraftRouteUpdate(payload);
|
||
},
|
||
drawTestRoute() {
|
||
const routes = [
|
||
{
|
||
flightNo: '805-807',
|
||
routeType: 'TEST',
|
||
routeStatus: 'ACTIVE',
|
||
points: [
|
||
[40508513.641064, 4024186.813497],
|
||
[40508714.127776, 4024250.934516],
|
||
[40508853.166318, 4024293.929307],
|
||
[40509022.478135, 4024346.688892]
|
||
]
|
||
},
|
||
{
|
||
flightNo: 'M4 829-826-K1',
|
||
routeType: 'TEST',
|
||
routeStatus: 'ACTIVE',
|
||
points: [
|
||
[40507849.913313, 4023625.932486],
|
||
[40507732.597485, 4023991.671027],
|
||
[40507723.998527, 4023986.908527],
|
||
[40507674.841898, 4024138.30738]
|
||
]
|
||
},
|
||
{
|
||
flightNo: 'M1 K2-1-1-1#消防',
|
||
routeType: 'TEST',
|
||
routeStatus: 'ACTIVE',
|
||
points: [
|
||
[40509113.197805, 4024029.951692],
|
||
[40508997.734226, 4024393.687528],
|
||
[40509007.523809, 4024395.275028],
|
||
[40508957.783071, 4024548.361738]
|
||
]
|
||
}
|
||
];
|
||
|
||
routes.forEach(r => {
|
||
const wktPoints = (r.points || []).map((p) => `${p[0]} ${p[1]}`).join(', ')
|
||
const payload = {
|
||
flightNo: r.flightNo,
|
||
routeType: r.routeType,
|
||
routeStatus: r.routeStatus,
|
||
routeGeometry: `LINESTRING(${wktPoints})`
|
||
};
|
||
handleAircraftRouteUpdate(payload);
|
||
});
|
||
}
|
||
});
|
||
|
||
// 添加一个默认的getVehicleStyle函数
|
||
function defaultGetVehicleStyle(id, _speed, heading) {
|
||
// 检查车辆类型
|
||
const vehicle = vehicles.value[id];
|
||
if (!vehicle) return createDefaultStyle(carIcon, heading);
|
||
|
||
// 根据车辆类型选择图标,与VehicleStyleManager保持一致
|
||
if (vehicle.isAircraft) {
|
||
// 航空器使用aircraft.png图标
|
||
return createDefaultStyle(aircraftIcon, heading);
|
||
} else if (vehicle.type === '无人车' || vehicle.type === 'UNMANNED_VEHICLE') {
|
||
// 无人车使用noPeopleCar.svg图标
|
||
return createDefaultStyle(carIcon, heading);
|
||
} else {
|
||
// 其他车辆使用specialCar.svg图标
|
||
return createDefaultStyle(specialCar, heading);
|
||
}
|
||
}
|
||
|
||
// 创建默认样式的辅助函数
|
||
function createDefaultStyle(iconSrc, heading) {
|
||
// 确保heading是有效的数值并正确转换为弧度
|
||
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||
|
||
// 计算正确的旋转角度
|
||
// 在OpenLayers中,0度是向上,顺时针增加
|
||
// 而heading是0度为北,顺时针增加
|
||
// 由于地图旋转了72度,heading=72对应地图上方
|
||
const rotationRad = ((validHeading - 72) * Math.PI) / 180;
|
||
// console.log(`createDefaultStyle: heading=${validHeading}, 旋转角度计算: (${validHeading} - 72) * π/180 = ${rotationRad} 弧度, ${(validHeading - 72)}度`);
|
||
|
||
return new Style({
|
||
image: new Icon({
|
||
src: iconSrc, // 使用传入的图标源
|
||
scale: (iconSrc === aircraftIcon || iconSrc === aircraftRouteIcon) ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||
anchor: [0.5, 0.5],
|
||
rotation: rotationRad, // 应用计算后的旋转角度
|
||
})
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 告警按钮样式 */
|
||
.alarm-btn {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
width: 40px;
|
||
height: 40px;
|
||
background-color: rgba(41, 44, 56, 0.8);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
z-index: 1000;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.alarm-btn:hover {
|
||
background-color: rgba(41, 44, 56, 1);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.alarm-btn img {
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
.alarm-badge {
|
||
position: absolute;
|
||
top: -5px;
|
||
right: -5px;
|
||
background-color: #ff4d4f;
|
||
color: white;
|
||
font-size: 12px;
|
||
min-width: 18px;
|
||
height: 18px;
|
||
border-radius: 9px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.path-conflict-card {
|
||
position: absolute;
|
||
left: 28px;
|
||
top: calc(23% + 160px);
|
||
width: 246px;
|
||
color: #fff;
|
||
z-index: 1000;
|
||
background: rgba(41, 44, 56, 0.95);
|
||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 15px rgba(2, 2, 2, 0.25);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.path-conflict-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 7px 10px;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
background: rgba(52, 55, 68, 0.96);
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.14);
|
||
}
|
||
|
||
.path-conflict-level {
|
||
padding: 1px 7px;
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
background: rgba(59, 130, 246, 0.28);
|
||
}
|
||
|
||
.path-conflict-body {
|
||
padding: 8px 10px 10px;
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.path-conflict-muted,
|
||
.path-conflict-debug {
|
||
color: rgba(255, 255, 255, 0.78);
|
||
}
|
||
|
||
.path-conflict-reason {
|
||
color: rgba(255, 214, 165, 0.95);
|
||
}
|
||
|
||
.path-conflict-card.level-WARNING .path-conflict-level {
|
||
background: rgba(245, 158, 11, 0.35);
|
||
}
|
||
|
||
.path-conflict-card.level-CRITICAL .path-conflict-level,
|
||
.path-conflict-card.level-EMERGENCY .path-conflict-level {
|
||
background: rgba(239, 68, 68, 0.38);
|
||
}
|
||
</style>
|