Add path conflict display toggle and clear aircraft alerts

This commit is contained in:
sladro 2026-04-30 11:41:29 +08:00
parent 3be4f2a90b
commit cd95a16904
11 changed files with 628 additions and 29 deletions

View File

@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "airport-qingdao-vue3"
[setup]
script = ""
[[actions]]
name = "运行"
icon = "run"
command = "npm run build:prod"

Binary file not shown.

View File

@ -21,6 +21,10 @@
<div class="settings-item" @click="openWarningDistance">修改预警距离</div>
<div class="settings-item" @click="openRouteModify">查询路由</div>
<div class="settings-item" @click="drawTestRoute">绘制测试路由</div>
<div class="settings-item settings-switch-item" @click.stop>
<span>路径冲突显示</span>
<el-switch v-model="pathConflictDisplayEnabled" size="small" />
</div>
</div>
<VehicleRunningEditDialog :visible="showVehicleEditDialog" @close="showVehicleEditDialog = false" />
@ -58,8 +62,9 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from "vue";
import { computed, ref, onMounted, onUnmounted, watch } from "vue";
import { ElMessage } from "element-plus";
import useSettingsStore from '@/store/modules/settings';
import LayerSwitcher from "./LayerSwitcher.vue";
import VehicleRunningEditDialog from "./VehicleRunningEditDialog.vue";
import WarningDistanceDialog from "./WarningDistanceDialog.vue";
@ -102,6 +107,19 @@ const showSettingsPanel = ref(false);
const showVehicleEditDialog = ref(false);
const showWarningDistanceDialog = ref(false);
const showRouteModifyDialog = ref(false);
const settingsStore = useSettingsStore();
const pathConflictDisplayEnabled = computed({
get: () => settingsStore.pathConflictDisplayEnabled !== false,
set: (val) => {
settingsStore.changeSetting({ key: 'pathConflictDisplayEnabled', value: val });
const layoutSetting = JSON.parse(localStorage.getItem('layout-setting')) || {};
localStorage.setItem('layout-setting', JSON.stringify({
...layoutSetting,
pathConflictDisplayEnabled: val
}));
}
});
// ()
const rotation = ref(0);
@ -466,6 +484,12 @@ defineExpose({
background: rgba(0, 150, 255, 0.12);
}
.settings-switch-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.settings-item.disabled {
opacity: 0.5;
cursor: not-allowed;

View File

@ -65,8 +65,8 @@ function createNewLabel(vehicleId, coordinates, speed, alertInfo) {
const overlay = new Overlay({
element: labelElement,
position: coordinates,
offset: [0, -30], // 使
positioning: 'bottom-center',
offset: [34, -34], //
positioning: 'bottom-left',
stopEvent: false,
insertFirst: true, // DOM
autoPan: false, //
@ -353,7 +353,7 @@ defineExpose({
/* 标签容器样式 */
.vehicle-label {
position: absolute;
transform: translate(-50%, -100%);
transform: none;
z-index: 1000;
pointer-events: auto;
}
@ -364,7 +364,8 @@ defineExpose({
color: #000;
padding: 10px;
padding-bottom: 0px;
min-width: 170px;
min-width: 280px;
max-width: 380px;
box-shadow: 0 10px 15px rgba(2, 2, 2, 0.25);
border: 1px solid #40506a;
}
@ -372,6 +373,8 @@ defineExpose({
/* 简化标签样式 */
.simple-label {
padding: 6px 10px;
min-width: 150px;
max-width: 220px;
}
/* 标签头部样式 */
@ -405,11 +408,15 @@ defineExpose({
.tab-buttons {
display: flex;
gap: 5px;
margin-bottom: 8px;
justify-content: space-around;
margin-bottom: 6px;
justify-content: flex-start;
align-items: center;
}
.tab-content {
min-width: 0;
}
.tab-button {
border: none;
width: 24px;
@ -488,23 +495,37 @@ defineExpose({
}
.tab-pane.active {
display: block;
display: flex;
align-items: center;
min-width: 0;
}
/* 告警消息样式 */
.alert-message {
font-size: 12px;
font-weight: Medium;
line-height: 1.4;
padding: 5px;
line-height: 1.2;
padding: 2px 0;
border-radius: 2px;
background-color: transparent;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 具体告警提示 */
.alert{
font-size: 14px;
font-weight: bold;
margin: 5px;
margin: 0 8px 0 0;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
}
.alert img {
width: 14px;
height: 14px;
margin-right: 4px;
}
/* 超速信息样式 */

View File

@ -44,10 +44,38 @@
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, onMounted, onUnmounted, watch, onActivated, onDeactivated, defineProps, defineEmits } from 'vue';
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';
@ -61,6 +89,8 @@ 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';
@ -92,6 +122,9 @@ aircraftRouteIconImg.src = aircraftRouteIcon;
// const aircraftOutIconImg = new Image();
// aircraftOutIconImg.src = aircraftOutIcon;
const DEFAULT_VEHICLE_ICON_SCALE = 0.75;
const AIRCRAFT_ICON_SCALE = DEFAULT_VEHICLE_ICON_SCALE * 1.3;
// SockJSpolyfill
if (typeof window !== 'undefined' && !window.global) {
window.global = window;
@ -126,6 +159,8 @@ 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);
@ -161,6 +196,45 @@ 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 || {};
@ -263,8 +337,11 @@ 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);
@ -300,6 +377,11 @@ function markConflictStatus(vehicleId, level, message, ttlMs) {
}
const timerKey = `conflict:${vehicleId}`;
if (!(Number(ttlMs) > 0)) {
clearStatusTimer(timerKey);
return;
}
setStatusTimer(
timerKey,
() => {
@ -429,15 +511,19 @@ function formatDateTime(date) {
function handleAlertShow({ message, type, duration = 5000 }) {
if (alertTimer) {
clearTimeout(alertTimer);
alertTimer = null;
}
alertMessage.value = message;
alertType.value = type;
alertTimer = setTimeout(() => {
alertMessage.value = '';
alertType.value = '';
}, duration);
if (Number(duration) > 0) {
alertTimer = setTimeout(() => {
alertMessage.value = '';
alertType.value = '';
alertTimer = null;
}, duration);
}
}
// /
@ -445,6 +531,15 @@ 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;
@ -485,6 +580,37 @@ function createAircraftRouteLayer() {
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) {
@ -633,7 +759,7 @@ function updateVehiclePosition(vehicleData) {
iconStyle = new Style({
image: new Icon({
src: iconSrc,
scale: 0.75,
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
anchor: [0.5, 0.5],
rotation: rotationRad, // 使
})
@ -744,7 +870,7 @@ function updateVehiclePosition(vehicleData) {
? new Style({
image: new Icon({
src: iconSrc,
scale: 0.75,
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
anchor: [0.5, 0.5],
rotation: rotationRad,
}),
@ -854,10 +980,19 @@ function handleWsMessage(message) {
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}`);
handlePathConflictAlert(data.payload);
if (isNewPathConflictPayload(data.payload)) {
handlePathConflictMessage(data);
} else {
handlePathConflictAlert(data.payload);
}
break;
case 'rule_violation':
@ -1681,6 +1816,123 @@ function removeAircraftRoute(flightNo) {
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) {
@ -1696,7 +1948,6 @@ function handlePathConflictAlert(payload) {
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) {
@ -2085,7 +2336,7 @@ function getDefaultVehicleStyle(vehicle) {
return new Style({
image: new Icon({
src: iconSrc,
scale: 0.75,
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
anchor: [0.5, 0.5],
rotation: rotationRad,
})
@ -2181,7 +2432,7 @@ function sendSubscribe() {
if (wsService) {
const message = JSON.stringify({
type: 'subscribe',
topics: ['position_update', 'collision_warning', 'rule_violation'],
topics: ['position_update', 'collision_warning', 'path_conflict_status', 'path_conflict_alert', 'rule_violation'],
timestamp: Date.now()
});
wsService.send(message);
@ -2234,9 +2485,16 @@ function cleanup() {
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) {
@ -2258,6 +2516,7 @@ onMounted(() => {
if (props.map) {
createVehicleLayer();
createAircraftRouteLayer(); // 线
createPathConflictLayer();
}
// WebSocket
@ -2344,6 +2603,14 @@ watch(() => props.map, (newMap) => {
if (newMap) {
createVehicleLayer();
createAircraftRouteLayer(); // 线
createPathConflictLayer();
}
});
watch(pathConflictDisplayEnabled, (enabled) => {
if (!enabled) {
clearPathConflictDisplay();
clearAlertMessage();
}
});
@ -2640,7 +2907,7 @@ function createDefaultStyle(iconSrc, heading) {
return new Style({
image: new Icon({
src: iconSrc, // 使
scale: 0.75,
scale: (iconSrc === aircraftIcon || iconSrc === aircraftRouteIcon) ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
anchor: [0.5, 0.5],
rotation: rotationRad, //
})
@ -2692,4 +2959,60 @@ function createDefaultStyle(iconSrc, heading) {
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>

View File

@ -27,6 +27,9 @@ const props = defineProps({
vehicles: Object
});
const DEFAULT_ICON_SCALE = 1.0;
const AIRCRAFT_ICON_SCALE = DEFAULT_ICON_SCALE * 1.3;
//
function getVehicleStyle(id, speed, heading) {
const vehicle = props.vehicles[id];
@ -47,11 +50,11 @@ function getVehicleStyle(id, speed, heading) {
}
//
return createDefaultStyle(iconSrc, heading);
return createDefaultStyle(iconSrc, heading, vehicle.isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_ICON_SCALE);
}
//
function createDefaultStyle(iconSrc, heading) {
function createDefaultStyle(iconSrc, heading, scale = DEFAULT_ICON_SCALE) {
// heading
const validHeading = heading !== undefined ? Number(heading) : 0;
@ -64,7 +67,7 @@ function createDefaultStyle(iconSrc, heading) {
return new Style({
image: new Icon({
src: iconSrc,
scale: 1.0,
scale,
anchor: [0.5, 0.5],
rotation: rotationRad, // 使
})

View File

@ -70,6 +70,13 @@
</span>
</div>
<div class="drawer-item">
<span>路径冲突显示</span>
<span class="comp-style">
<el-switch v-model="pathConflictDisplayEnabled" class="drawer-switch" />
</span>
</div>
<el-divider />
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
@ -141,6 +148,13 @@ const dynamicTitle = computed({
}
})
const pathConflictDisplayEnabled = computed({
get: () => storeSettings.value.pathConflictDisplayEnabled,
set: (val) => {
settingsStore.changeSetting({ key: 'pathConflictDisplayEnabled', value: val })
}
})
function themeChange(val) {
settingsStore.changeSetting({ key: 'theme', value: val })
theme.value = val;
@ -158,6 +172,7 @@ function saveSetting() {
"fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle,
"pathConflictDisplayEnabled": storeSettings.value.pathConflictDisplayEnabled,
"sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme
};
@ -238,4 +253,4 @@ defineExpose({
margin: -3px 8px 0px 0px;
}
}
</style>
</style>

View File

@ -37,6 +37,11 @@ export default {
*/
dynamicTitle: false,
/**
* 是否显示路径冲突点过程状态和告警
*/
pathConflictDisplayEnabled: true,
/**
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.

View File

@ -1,7 +1,7 @@
import defaultSettings from '@/settings'
import { useDynamicTitle } from '@/utils/dynamicTitle'
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, pathConflictDisplayEnabled } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
@ -17,7 +17,8 @@ const useSettingsStore = defineStore(
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
pathConflictDisplayEnabled: storageSetting.pathConflictDisplayEnabled === undefined ? pathConflictDisplayEnabled : storageSetting.pathConflictDisplayEnabled
}),
actions: {
// 修改布局设置

View File

@ -0,0 +1,103 @@
export function getPathConflictDisplayEnabled(settings = {}) {
return settings.pathConflictDisplayEnabled === undefined
? true
: settings.pathConflictDisplayEnabled !== false
}
export function normalizePathConflictMessage(message) {
const payload = message?.payload || {}
if (message?.type === 'path_conflict_alert' && payload.messageType === 'PATH_CONFLICT_RESUME') {
return {
kind: 'resume',
key: getConflictKey(payload) || payload.vehicleName || payload.object1?.objectName || '',
aircraftName: payload.aircraftName || payload.object1?.objectName || '',
vehicleName: payload.vehicleName || payload.object2?.objectName || payload.object1?.objectName || '',
rawData: payload,
}
}
if (message?.type === 'path_conflict_status') {
return buildConflict('status', payload, {
title: '计算中',
level: 'STATUS',
})
}
if (message?.type === 'path_conflict_alert') {
const level = payload.alertLevel || (payload.alertType === 'CONFLICT_ALERT' || payload.alert === true ? 'CRITICAL' : 'WARNING')
return buildConflict('alert', payload, {
title: getAlertTitle(level),
level,
})
}
return null
}
export function isNewPathConflictPayload(payload) {
if (!payload) return false
return Boolean(
payload.messageType === 'PATH_CONFLICT_RESUME' ||
payload.aircraftName ||
payload.vehicleName ||
payload.aircraftDistanceToConflictMeters !== undefined ||
payload.vehicleDistanceToConflictMeters !== undefined ||
payload.aircraftAlertThresholdMeters !== undefined ||
payload.vehicleAlertThresholdMeters !== undefined ||
payload.vehicleMovingTowardConflictPoint !== undefined ||
payload.conflictPoint ||
payload.position
)
}
function buildConflict(kind, payload, extra) {
const aircraftName = payload.aircraftName || payload.object1?.objectName || ''
const vehicleName = payload.vehicleName || payload.object2?.objectName || payload.object1?.objectName || ''
return {
kind,
...extra,
key: getConflictKey(payload) || [aircraftName, vehicleName].filter(Boolean).join('::'),
conflictPoint: payload.conflictPoint || payload.position || null,
aircraftName,
vehicleName,
aircraftDistance: payload.aircraftDistanceToConflictMeters,
vehicleDistance: payload.vehicleDistanceToConflictMeters || payload.object2Distance,
aircraftThreshold: payload.aircraftAlertThresholdMeters,
vehicleThreshold: payload.vehicleAlertThresholdMeters,
vehicleMovingToward: payload.vehicleMovingTowardConflictPoint,
directionLockStatus: payload.directionLockStatus,
calculationStatus: payload.calculationStatus,
directionLockText: getDirectionLockText(payload),
directionLockFailed: isDirectionLockFailed(payload),
directionLockReason: payload.directionLockReason || '',
object1ForwardDistance: payload.object1ForwardDistanceMeters,
object2ForwardDistance: payload.object2ForwardDistanceMeters,
rawData: payload,
}
}
function getConflictKey(payload) {
const aircraftName = payload.aircraftName || ''
const vehicleName = payload.vehicleName || payload.object2?.objectName || payload.object1?.objectName || ''
return [aircraftName, vehicleName].filter(Boolean).join('::')
}
function getAlertTitle(level) {
if (level === 'EMERGENCY') return '紧急告警'
if (level === 'CRITICAL') return '冲突告警'
return '冲突预警'
}
function isDirectionLockFailed(payload) {
return payload.directionLockStatus === 'DIRECTION_INVALID' ||
payload.calculationStatus === 'DIRECTION_LOCK_FAILED'
}
function getDirectionLockText(payload) {
if (payload.directionLockStatus === undefined && payload.calculationStatus === undefined) return ''
if (isDirectionLockFailed(payload)) return '方向无法锁定'
if (payload.directionLockStatus === 'DIRECTION_LOCKED') return '方向已锁定'
return ''
}

View File

@ -0,0 +1,93 @@
import assert from 'node:assert/strict'
import {
getPathConflictDisplayEnabled,
isNewPathConflictPayload,
normalizePathConflictMessage,
} from '../src/utils/pathConflictDisplay.mjs'
assert.equal(getPathConflictDisplayEnabled({}), true)
assert.equal(getPathConflictDisplayEnabled({ pathConflictDisplayEnabled: false }), false)
const status = normalizePathConflictMessage({
type: 'path_conflict_status',
payload: {
conflictPoint: { latitude: 36.1, longitude: 120.2 },
aircraftName: 'A01',
vehicleName: 'V01',
aircraftDistanceToConflictMeters: 120,
vehicleDistanceToConflictMeters: 80,
aircraftAlertThresholdMeters: 200,
vehicleAlertThresholdMeters: 100,
vehicleMovingTowardConflictPoint: true,
directionLockStatus: 'DIRECTION_LOCKED',
calculationStatus: 'MONITORING',
directionLockReason: '',
object1ForwardDistanceMeters: 118,
object2ForwardDistanceMeters: 78,
},
})
assert.equal(status.kind, 'status')
assert.equal(status.title, '计算中')
assert.deepEqual(status.conflictPoint, { latitude: 36.1, longitude: 120.2 })
assert.equal(status.key, 'A01::V01')
assert.equal(status.directionLockText, '方向已锁定')
assert.equal(status.directionLockFailed, false)
assert.equal(status.directionLockReason, '')
assert.equal(status.object1ForwardDistance, 118)
assert.equal(status.object2ForwardDistance, 78)
const directionInvalid = normalizePathConflictMessage({
type: 'path_conflict_status',
payload: {
aircraftName: 'A01',
vehicleName: 'V01',
directionLockStatus: 'DIRECTION_INVALID',
calculationStatus: 'DIRECTION_LOCK_FAILED',
directionLockReason: '冲突点在两侧',
},
})
assert.equal(directionInvalid.directionLockText, '方向无法锁定')
assert.equal(directionInvalid.directionLockFailed, true)
assert.equal(directionInvalid.directionLockReason, '冲突点在两侧')
const resume = normalizePathConflictMessage({
type: 'path_conflict_alert',
payload: {
messageType: 'PATH_CONFLICT_RESUME',
aircraftName: 'A01',
vehicleName: 'V01',
},
})
assert.equal(resume.kind, 'resume')
assert.equal(resume.key, 'A01::V01')
assert.equal(resume.aircraftName, 'A01')
assert.equal(resume.vehicleName, 'V01')
const alert = normalizePathConflictMessage({
type: 'path_conflict_alert',
payload: {
alertLevel: 'EMERGENCY',
conflictPoint: { latitude: 36.1, longitude: 120.2 },
aircraftName: 'A01',
vehicleName: 'V01',
},
})
assert.equal(alert.kind, 'alert')
assert.equal(alert.level, 'EMERGENCY')
assert.equal(alert.title, '紧急告警')
assert.equal(isNewPathConflictPayload({
object1: { objectName: 'V01' },
object2: { objectName: 'V02' },
alertType: 'CONFLICT_WARNING',
}), false)
assert.equal(isNewPathConflictPayload({
aircraftName: 'A01',
vehicleName: 'V01',
conflictPoint: { latitude: 36.1, longitude: 120.2 },
}), true)