From bffabd80f88c8ae25e0aaee8ed62ff6c403db32e Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Wed, 18 Mar 2026 08:37:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=B2=E7=AA=811.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/lang/en.js | 7 + ruoyi-ui/src/lang/zh.js | 7 + ruoyi-ui/src/views/cesiumMap/index.vue | 41 +++- ruoyi-ui/src/views/childRoom/RightPanel.vue | 159 ++-------------- ruoyi-ui/src/views/childRoom/index.vue | 247 +++++++++++++++++++++---- ruoyi-ui/src/views/dialogs/RouteEditDialog.vue | 8 +- 6 files changed, 285 insertions(+), 184 deletions(-) diff --git a/ruoyi-ui/src/lang/en.js b/ruoyi-ui/src/lang/en.js index 53271e1..bc7fd5b 100644 --- a/ruoyi-ui/src/lang/en.js +++ b/ruoyi-ui/src/lang/en.js @@ -118,9 +118,16 @@ export default { conflictTime: 'Conflict Time', conflictPosition: 'Conflict Position', viewDetails: 'View Details', + locate: 'Locate', resolveConflict: 'Resolve Conflict', noConflict: 'No Conflict', + noMatchFilter: 'No matches for current filter', recheck: 'Recheck', + conflictFilter: 'Type', + conflictTypeAll: 'All', + conflictTypeTime: 'Time', + conflictTypeSpace: 'Space', + conflictTypeSpectrum: 'Spectrum', air: 'Air', sea: 'Sea', ground: 'Ground' diff --git a/ruoyi-ui/src/lang/zh.js b/ruoyi-ui/src/lang/zh.js index eb35dbc..eb5e276 100644 --- a/ruoyi-ui/src/lang/zh.js +++ b/ruoyi-ui/src/lang/zh.js @@ -118,9 +118,16 @@ export default { conflictTime: '冲突时间', conflictPosition: '冲突位置', viewDetails: '查看详情', + locate: '定位', resolveConflict: '解决冲突', noConflict: '暂无冲突', + noMatchFilter: '当前筛选无匹配项', recheck: '重新检测', + conflictFilter: '类型筛选', + conflictTypeAll: '全部', + conflictTypeTime: '时间', + conflictTypeSpace: '空间', + conflictTypeSpectrum: '频谱', air: '空中', sea: '海上', ground: '地面' diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index e7ca358..116bc69 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -9975,6 +9975,43 @@ export default { .map(e => this.serializeEntityForSave(e)) return { version: '1.0', date: new Date().toISOString(), entities } }, + /** 供冲突检测用:返回当前地图上所有平台图标位置 [{ id, lng, lat, name }] */ + getPlatformIconPositions() { + if (!this.allEntities) return [] + return this.allEntities + .filter(e => e && e.type === 'platformIcon') + .map(e => { + let lng = e.lng + let lat = e.lat + if (lng == null || lat == null) { + try { + const pos = e.entity && e.entity.position && e.entity.position.getValue + ? e.entity.position.getValue(Cesium.JulianDate.now()) + : null + if (pos) { + const ll = this.cartesianToLatLng(pos) + if (ll) { lng = ll.lng; lat = ll.lat } + } + } catch (_) {} + } + return { id: e.id, lng: Number(lng), lat: Number(lat), name: e.label || e.platformName || e.name || `平台${e.id}` } + }) + .filter(p => Number.isFinite(p.lng) && Number.isFinite(p.lat)) + }, + /** 供冲突定位用:相机飞至指定经纬度与高度 */ + flyToPosition(lng, lat, alt, duration = 1.5) { + if (!this.viewer || !this.viewer.camera) return + const height = alt != null && Number.isFinite(Number(alt)) ? Number(alt) : 50000 + this.viewer.camera.flyTo({ + destination: Cesium.Cartesian3.fromDegrees(Number(lng), Number(lat), Math.max(1000, height)), + orientation: { + heading: 0, + pitch: Cesium.Math.toRadians(-60), + roll: 0 + }, + duration: duration + }) + }, /** 仅清除空域/威力区图形(不删平台图标、航线) */ clearDrawingEntities() { if (!this.allEntities || !this.viewer) return @@ -11017,12 +11054,12 @@ export default { } else { coordinateText = `${lng.toFixed(6)},${lat.toFixed(6)}` } - this.$message.success(`已定位到经度 ${coordinateText}`) + this.$message({ message: `已定位到经度 ${coordinateText}`, type: 'success', duration: 2000 }) } }, handleLocateCancel() { - this.$message.info('已取消定位') + this.$message({ message: '已取消定位', type: 'info', duration: 2000 }) }, updateSelectOptions(refName, dataList, labelField) { const select = document.querySelector(`select[ref="${refName}"]`) diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index 5f063ca..d1d0694 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -123,58 +123,6 @@ -
-
-
-
- - {{ conflict.title }} - {{ $t('rightPanel.serious') }} -
-
-
- {{ $t('rightPanel.involvedRoutes') }}: - {{ conflict.routeName || (conflict.routes && conflict.routes.join('、')) }} -
-
- 问题航段: - {{ conflict.fromWaypoint }} → {{ conflict.toWaypoint }} -
-
- {{ $t('rightPanel.conflictTime') }}: - {{ conflict.time }} -
-
- {{ $t('rightPanel.conflictPosition') }}: - {{ conflict.position }} -
-
- 建议: - {{ conflict.suggestion }} -
-
-
- - {{ $t('rightPanel.viewDetails') }} - - - {{ $t('rightPanel.resolveConflict') }} - -
-
-
-
- -

{{ $t('rightPanel.noConflict') }}

- - {{ $t('rightPanel.recheck') }} - -
-
平台列表
@@ -341,14 +289,11 @@ export default { type: Object, default: null }, - conflicts: { + /** 父组件要求展开的航线 ID 列表(如冲突定位时),会展开对应方案与航线 */ + expandRouteIds: { type: Array, default: () => [] }, - conflictCount: { - type: Number, - default: 0 - }, airPlatforms: { type: Array, default: () => [] @@ -371,6 +316,19 @@ export default { } }, watch: { + expandRouteIds(newVal) { + if (newVal && newVal.length) { + newVal.forEach(routeId => { + const r = this.routes.find(route => route.id === routeId); + if (r && r.scenarioId != null && !this.expandedPlans.includes(r.scenarioId)) { + this.expandedPlans.push(r.scenarioId); + } + if (routeId != null && !this.expandedRoutes.includes(routeId)) { + this.expandedRoutes.push(routeId); + } + }); + } + }, selectedPlanId(newId) { if (newId) { console.log('>>> [子组件同步] 检测到方案切换,自动展开 ID:', newId); @@ -548,18 +506,6 @@ export default { }); }, - handleViewConflict(conflict) { - this.$emit('view-conflict', conflict) - }, - - handleResolveConflict(conflict) { - this.$emit('resolve-conflict', conflict) - }, - - handleRunConflictCheck() { - this.$emit('run-conflict-check') - }, - handleOpenPlatformDialog(platform) { this.$emit('open-platform-dialog', platform) }, @@ -849,81 +795,6 @@ export default { padding: 10px 0; } -.conflict-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.conflict-item { - background: rgba(255, 255, 255, 0.8); - border-radius: 6px; - padding: 12px; - border: 1px solid rgba(245, 108, 108, 0.2); - transition: all 0.3s; -} - -.conflict-item:hover { - background: rgba(245, 108, 108, 0.1); - box-shadow: 0 2px 8px rgba(245, 108, 108, 0.15); -} - -.conflict-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 10px; -} - -.conflict-title { - flex: 1; - font-weight: 600; - color: #f56c6c; -} - -.conflict-details { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 10px; -} - -.detail-item { - display: flex; - gap: 8px; - font-size: 13px; -} - -.detail-item .label { - color: #999; - min-width: 70px; -} - -.detail-item .value { - color: #333; - font-weight: 500; -} - -.detail-item.suggestion .value { - white-space: normal; - word-break: break-word; - color: #008aff; -} - -.conflict-actions { - display: flex; - gap: 10px; -} - -.no-conflict { - display: flex; - flex-direction: column; - align-items: center; - gap: 15px; - padding: 40px 20px; - color: #999; -} - .platform-categories { height: 100%; } diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 9626a46..72de4e2 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -194,8 +194,7 @@ :route-locked-by="routeLockedBy" :current-user-id="currentUserId" :selected-route-details="selectedRouteDetails" - :conflicts="conflicts" - :conflict-count="conflictCount" + :expand-route-ids="expandRouteIdsForPanel" :air-platforms="airPlatforms" :sea-platforms="seaPlatforms" :ground-platforms="groundPlatforms" @@ -213,9 +212,6 @@ @cancel-route="cancelRoute" @toggle-route-visibility="(route, opts) => toggleRouteVisibility(route, opts)" @toggle-route-lock="handleToggleRouteLockFromPanel" - @view-conflict="viewConflict" - @resolve-conflict="resolveConflict" - @run-conflict-check="runConflictCheck" @open-platform-dialog="openPlatformDialog" @delete-platform="handleDeletePlatform" @open-import-dialog="showImportDialog = true" @@ -338,6 +334,7 @@ v-model="showRouteDialog" :route="selectedRoute" :room-id="currentRoomId" + :initial-tab="routeEditInitialTab" @save="updateRoute" /> @@ -415,6 +412,15 @@ :room-id="currentRoomId" /> + + + { this.runConflictCheck(); }); } else if (item.id === 'insert') { // 如果当前已经是平台标签页,则关闭右侧面板 if (this.activeRightTab === 'platform' && !this.isRightPanelHidden) { @@ -5025,13 +5052,16 @@ export default { } }, - // 冲突操作:根据当前展示的航线与时间轴计算真实问题(提前到达、无法按时到达) + // 冲突操作:时间冲突(提前到达、无法按时到达、盘旋时间不足、航线时间窗重叠)、空间冲突(航迹间隔、平台摆放、禁限区入侵)、频谱冲突 runConflictCheck() { - const list = []; - let id = 1; const routeIds = this.activeRouteIds && this.activeRouteIds.length > 0 ? this.activeRouteIds : this.routes.map(r => r.id); const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); + const config = this.conflictConfig || defaultConflictConfig; + const waypointStartTimeToMinutes = (s) => this.waypointStartTimeToMinutes(s); + + const allRaw = []; + // ---------- 时间冲突:单航线内(提前到达、无法按时到达、盘旋时间不足)---------- routeIds.forEach(routeId => { const route = this.routes.find(r => r.id === routeId); if (!route || !route.waypoints || route.waypoints.length < 2) return; @@ -5043,65 +5073,208 @@ export default { } } const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); - const routeName = route.name || `航线${route.id}`; (earlyArrivalLegs || []).forEach(leg => { - list.push({ - id: id++, + // 提前到达:给出多种可选思路,统一在提示里说明“视定速/定时约束优先调整未受约束量” + allRaw.push({ + type: CONFLICT_TYPE.TIME, + subType: 'early_arrival', title: '提前到达', routeName, + routeIds: [routeId], fromWaypoint: leg.fromName, toWaypoint: leg.toName, time: this.minutesToStartTime(leg.actualArrival), - suggestion: '该航段将提前到达下一航点,建议在此段加入盘旋或延后下一航点计划时间。', + suggestion: '该航段将提前到达下一航点。可选措施:① 适当降低本段巡航速度;② 在本段或下一航点前增加盘旋等待;③ 视任务需要调整下一航点相对K时/计划时间。若存在定速点或定时点,请优先调整未受约束的速度或时间。', severity: 'high' }); }); (lateArrivalLegs || []).forEach(leg => { - list.push({ - id: id++, + // 无法按时到达:同时提示“提速”和“调整时间/路径”等多种方案 + allRaw.push({ + type: CONFLICT_TYPE.TIME, + subType: 'late_arrival', title: '无法按时到达', routeName, + routeIds: [routeId], fromWaypoint: leg.fromName, toWaypoint: leg.toName, - suggestion: `当前速度不足,建议将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h,或延后下一航点计划时间。`, + suggestion: `当前速度不足,理论上需将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h 才能按时到达。可选措施:① 在安全范围内提高本段或前一段速度;② 适当提前下一航点相对K时/计划时间;③ 结合任务需要调整上游航段或加入盘旋缓冲。若存在定速点或定时点,请优先从未锁定的速度或时间入手。`, severity: 'high' }); }); (holdDelayConflicts || []).forEach(conf => { - list.push({ - id: id++, + // 盘旋时间不足:提示可修改盘旋圈数/时间、速度或切出时刻 + allRaw.push({ + type: CONFLICT_TYPE.TIME, + subType: 'hold_delay', title: '盘旋时间不足', routeName, + routeIds: [routeId], fromWaypoint: conf.fromName, toWaypoint: conf.toName, time: this.minutesToStartTime(conf.setExitTime), position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined, - suggestion: `警告:设定的盘旋时间不足以支撑战斗机完成最后一圈,实际切出将延迟 ${conf.delaySeconds} 秒。`, + suggestion: `警告:设定的盘旋时间不足以支撑战斗机完成最后一圈,实际切出将延迟 ${conf.delaySeconds} 秒。可选措施:① 增加盘旋圈数或调整盘旋结束时间;② 在允许范围内调整盘旋段速度;③ 结合上下游航段,微调相关航点的相对K时/计划时间。若存在定速点或定时点,请优先调整未受约束的参数。`, severity: 'high', - holdCenter: conf.holdCenter + holdCenter: conf.holdCenter, + positionLng: conf.holdCenter && conf.holdCenter.lng, + positionLat: conf.holdCenter && conf.holdCenter.lat, + positionAlt: conf.holdCenter && conf.holdCenter.alt }); }); }); - this.conflicts = list; - this.conflictCount = list.length; - if (list.length > 0) { - this.$message.warning(`检测到 ${list.length} 处航线时间问题`); + // 时间窗重叠:仅“时间段有交集”不算冲突(不同地点飞机可同时起飞)。后续若有跑道/频段等资源绑定,再按“同一资源占用时间重叠”报冲突。 + // 故此处不再调用 detectTimeWindowOverlap。 + + // ---------- 空间冲突:航迹最小间隔 ---------- + const getPositionAtMinutesForConflict = (routeId, minutesFromK) => { + const route = this.routes.find(r => r.id === routeId); + if (!route || !route.waypoints || route.waypoints.length === 0) return null; + const { position } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes, routeId); + return position; + }; + const trackConflicts = detectTrackSeparation(routeIds, minMinutes, maxMinutes, getPositionAtMinutesForConflict, config); + trackConflicts.forEach(c => allRaw.push(c)); + + // ---------- 空间冲突:摆放平台距离过小 ---------- + const platformIcons = this.$refs.cesiumMap && this.$refs.cesiumMap.getPlatformIconPositions ? this.$refs.cesiumMap.getPlatformIconPositions() : []; + const placementConflicts = detectPlatformPlacementTooClose(platformIcons, config); + placementConflicts.forEach(c => allRaw.push(c)); + + // ---------- 空间冲突:禁限区入侵 ---------- + let restrictedZones = []; + if (this.$refs.cesiumMap && this.$refs.cesiumMap.getFrontendDrawingsData) { + const drawings = this.$refs.cesiumMap.getFrontendDrawingsData(); + const entities = (drawings && drawings.entities) || []; + restrictedZones = parseRestrictedZonesFromDrawings(entities, config.restrictedZoneNameKeywords || defaultConflictConfig.restrictedZoneNameKeywords); + restrictedZones = restrictedZones.map(z => ({ ...z, points: z.points || (z.data && z.data.points) || [] })).filter(z => z.points && z.points.length >= 3); + } + const restrictedConflicts = detectRestrictedZoneIntrusion(routeIds, minMinutes, maxMinutes, getPositionAtMinutesForConflict, restrictedZones, config); + restrictedConflicts.forEach(c => allRaw.push(c)); + + // ---------- 频谱冲突(台账内两两判定)---------- + if (this.spectrumLedger && this.spectrumLedger.length >= 2) { + const spectrumConflicts = detectSpectrumConflicts(this.spectrumLedger, config); + spectrumConflicts.forEach(c => allRaw.push(c)); + } + + this.conflicts = normalizeConflictList(allRaw, 1); + this.conflictCount = this.conflicts.length; + if (this.conflicts.length > 0) { + this.$message.warning(`检测到 ${this.conflicts.length} 处冲突`); } else { - this.$message.success('未发现航线时间冲突'); + this.$message.success('未发现冲突'); } }, + /** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */ viewConflict(conflict) { - this.$message.info(`查看冲突:${conflict.title}`); + const routeIds = conflict.routeIds || []; + if (routeIds.length > 0) { + // 保留原有已展示航线,同时并入本次冲突涉及的航线 + const prevActive = this.activeRouteIds || []; + this.activeRouteIds = [...new Set([...prevActive, ...routeIds])]; + this.isRightPanelHidden = false; + this.activeRightTab = 'plan'; + this.expandRouteIdsForPanel = [...routeIds]; + + // 确保相关航线在地图上真实渲染出来(不仅仅是相机飞过去) + if (this.$refs.cesiumMap) { + routeIds.forEach(async (routeId) => { + const route = this.routes.find(r => r.id === routeId); + if (!route) return; + + // 若列表中尚未加载航点,则从后端拉取一次 + let waypoints = Array.isArray(route.waypoints) ? route.waypoints : []; + if (!waypoints.length) { + try { + const res = await getRoutes(route.id); + if (res && res.code === 200 && res.data && Array.isArray(res.data.waypoints)) { + waypoints = res.data.waypoints; + // 回写到 routes,避免后续重复请求 + const idx = this.routes.findIndex(r => r.id === route.id); + if (idx > -1) { + this.$set(this.routes, idx, { + ...this.routes[idx], + waypoints + }); + } + } + } catch (e) { + console.warn('viewConflict: 获取航线航点失败', e); + } + } + + if (waypoints.length > 0) { + // 应用平台样式(若有),保持与正常选中航线一致的效果 + const roomId = this.currentRoomId; + if (roomId && route.platformId) { + try { + const styleRes = await getPlatformStyle({ roomId, routeId, platformId: route.platformId }); + if (styleRes && styleRes.data) { + this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data); + } + } catch (_) {} + } + this.$refs.cesiumMap.renderRouteWaypoints( + waypoints, + routeId, + route.platformId, + route.platform, + this.parseRouteStyle(route.attributes) + ); + } + }); + } + } + if (conflict.positionLng != null && conflict.positionLat != null && this.$refs.cesiumMap && this.$refs.cesiumMap.flyToPosition) { + this.$refs.cesiumMap.flyToPosition(conflict.positionLng, conflict.positionLat, conflict.positionAlt, 1.5); + } + if (conflict.minutesFromK != null && Number.isFinite(conflict.minutesFromK)) { + const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); + const span = Math.max(0, maxMinutes - minMinutes) || 120; + const progress = Math.max(0, Math.min(100, ((conflict.minutesFromK - minMinutes) / span) * 100)); + this.timeProgress = progress; + } + this.$message({ message: `已定位:${conflict.title}`, type: 'info', duration: 2000 }); }, - resolveConflict(conflict) { - this.$message.success(`解决冲突:${conflict.title}`); - // 移除已解决的冲突 - this.conflicts = this.conflicts.filter(c => c.id !== conflict.id); - this.conflictCount = this.conflicts.length; + /** 解决冲突:根据建议打开该航线的航点列表(编辑航线弹窗-航点 tab),由用户修改盘旋/速度/相对K时等,不直接删除冲突 */ + async resolveConflict(conflict) { + const routeId = conflict.routeIds && conflict.routeIds[0]; + if (!routeId) { + this.$message.warning('无法确定关联航线'); + return; + } + let route = this.routes.find(r => r.id === routeId); + if (!route) { + this.$message.warning('未找到该航线'); + return; + } + if (!route.waypoints || route.waypoints.length === 0) { + try { + const res = await getRoutes(routeId); + if (res.data && res.data.waypoints) { + route = { ...route, waypoints: res.data.waypoints }; + const idx = this.routes.findIndex(r => r.id === routeId); + if (idx !== -1) this.routes.splice(idx, 1, route); + } + } catch (e) { + console.warn('获取航线航点失败', e); + } + } + this.selectedRouteId = routeId; + this.selectedRouteDetails = route.waypoints ? { ...route } : null; + this.selectedRoute = route.waypoints ? { ...route } : route; + this.routeEditInitialTab = 'waypoints'; + this.showRouteDialog = true; + if (this.wsConnection && this.wsConnection.sendObjectEditLock && route && route.id != null) { + this.wsConnection.sendObjectEditLock('route', route.id); + this.routeEditLockedId = route.id; + } + this.$message.info('请根据建议在航点列表中修改(如加入盘旋或调整相对K时/速度)'); }, // 系统功能 diff --git a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue index a68cfe6..f788535 100644 --- a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue @@ -331,7 +331,9 @@ export default { props: { value: { type: Boolean, default: false }, route: { type: Object, default: () => ({}) }, - roomId: { type: [String, Number], default: null } + roomId: { type: [String, Number], default: null }, + /** 打开时默认选中的 tab:'basic' | 'platform' | 'waypoints',解决冲突时传 'waypoints' 直接打开航点列表 */ + initialTab: { type: String, default: '' } }, data() { return { @@ -416,7 +418,11 @@ export default { visible(val) { if (val) { this.loadPosition() + if (this.initialTab === 'waypoints' || this.initialTab === 'platform') { + this.activeTab = this.initialTab + } if (this.activeTab === 'platform') this.loadPlatforms() + if (this.activeTab === 'waypoints' && this.panelWidth < 920) this.panelWidth = 920 this.skipBasicStyleSyncOnce = true } },