|
|
|
@ -218,7 +218,6 @@ |
|
|
|
<div class="popup-hide-btn" @click="hideKTimePopup" title="隐藏K时"> |
|
|
|
<i class="el-icon-arrow-down"></i> |
|
|
|
</div> |
|
|
|
<p class="deduction-hint">仅推演当前展示的航线;K 时可随时由房主/管理员在右上角「作战时间」处修改。</p> |
|
|
|
<div class="timeline-controls"> |
|
|
|
<div class="current-time blue-time"> |
|
|
|
<i class="el-icon-time"></i> |
|
|
|
@ -261,14 +260,6 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div v-if="deductionWarnings.length > 0 || hasEarlyArrivalLegs" class="deduction-warnings"> |
|
|
|
<i class="el-icon-warning-outline"></i> |
|
|
|
<span>{{ deductionWarnings[0] || '存在航段将提前到达下一航点。' }}</span> |
|
|
|
<el-tooltip v-if="deductionWarnings.length > 1" :content="deductionWarnings.join(';')" placement="top"> |
|
|
|
<span class="warnings-more">等 {{ deductionWarnings.length }} 条</span> |
|
|
|
</el-tooltip> |
|
|
|
<el-button v-if="hasEarlyArrivalLegs" type="text" size="mini" @click="openAddHoldFromFirstEarly">在此添加盘旋</el-button> |
|
|
|
</div> |
|
|
|
<el-dialog :title="addHoldDialogTitle" :visible.sync="showAddHoldDialog" width="420px" append-to-body> |
|
|
|
<div v-if="addHoldContext" class="add-hold-tip">{{ addHoldDialogTip }}</div> |
|
|
|
<el-form :model="addHoldForm" label-width="100px" size="small"> |
|
|
|
@ -598,26 +589,9 @@ export default { |
|
|
|
activeRouteIds: [], // 存储当前所有选中的航线ID |
|
|
|
/** 航线上锁状态:routeId -> true 上锁,与地图右键及右侧列表锁图标同步 */ |
|
|
|
routeLocked: {}, |
|
|
|
// 冲突数据 |
|
|
|
conflictCount: 2, |
|
|
|
conflicts: [ |
|
|
|
{ |
|
|
|
id: 1, |
|
|
|
title: '航线空间冲突', |
|
|
|
routes: ['Alpha进场航线', 'Beta巡逻航线'], |
|
|
|
time: 'K+01:20:00', |
|
|
|
position: 'E116° N39°', |
|
|
|
severity: 'high' |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: 2, |
|
|
|
title: '时间窗口重叠', |
|
|
|
routes: ['侦察覆盖区', '无人机巡逻'], |
|
|
|
time: 'K+02:15:00', |
|
|
|
position: 'E117° N38°', |
|
|
|
severity: 'medium' |
|
|
|
} |
|
|
|
], |
|
|
|
// 冲突数据(由 runConflictCheck 根据当前航线与时间轴计算真实问题) |
|
|
|
conflictCount: 0, |
|
|
|
conflicts: [], |
|
|
|
|
|
|
|
// 平台数据 |
|
|
|
activePlatformTab: 'air', |
|
|
|
@ -743,12 +717,16 @@ export default { |
|
|
|
const response = await getRoutes(routeId); |
|
|
|
if (response.code === 200 && response.data) { |
|
|
|
const fullRouteData = response.data; |
|
|
|
// 同步更新父组件状态,保持和 selectRoute 方法一致的结构 |
|
|
|
const fromList = this.routes.find(r => r.id === routeId); |
|
|
|
// 同步更新父组件状态,合并 list 中的 platform 以便拖拽后重绘平台不丢失 |
|
|
|
this.selectedRouteId = fullRouteData.id; |
|
|
|
this.selectedRouteDetails = { |
|
|
|
id: fullRouteData.id, |
|
|
|
name: fullRouteData.callSign, |
|
|
|
waypoints: fullRouteData.waypoints || [] |
|
|
|
waypoints: fullRouteData.waypoints || [], |
|
|
|
platformId: fromList?.platformId, |
|
|
|
platform: fromList?.platform, |
|
|
|
attributes: fromList?.attributes |
|
|
|
}; |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
@ -908,11 +886,13 @@ export default { |
|
|
|
if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged); |
|
|
|
} |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
// 平台信息来自 list 的 route(selectedRouteDetails 来自 getRoutes 可能不含 platformId/platform) |
|
|
|
const routeForPlatform = this.routes.find(r => r.id === routeId) || route; |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints( |
|
|
|
waypoints, |
|
|
|
routeId, |
|
|
|
route.platformId, |
|
|
|
route.platform, |
|
|
|
routeForPlatform.platformId, |
|
|
|
routeForPlatform.platform, |
|
|
|
this.parseRouteStyle(route.attributes) |
|
|
|
); |
|
|
|
} |
|
|
|
@ -1000,11 +980,11 @@ export default { |
|
|
|
// 关闭弹窗 |
|
|
|
this.showPlatformDialog = false; |
|
|
|
}, |
|
|
|
/** 新建航线时写入数据库的默认样式(与地图默认显示一致:紫色实线线宽3) */ |
|
|
|
/** 新建航线时写入数据库的默认样式(与地图默认显示一致:墨绿色实线线宽3) */ |
|
|
|
getDefaultRouteAttributes() { |
|
|
|
const defaultAttrs = { |
|
|
|
waypointStyle: { pixelSize: 7, color: '#ffffff', outlineColor: '#0078FF', outlineWidth: 2 }, |
|
|
|
lineStyle: { style: 'solid', width: 3, color: '#800080', gapColor: '#000000', dashLength: 20 } |
|
|
|
lineStyle: { style: 'solid', width: 3, color: '#2E5C3E', gapColor: '#000000', dashLength: 20 } |
|
|
|
}; |
|
|
|
return JSON.stringify(defaultAttrs); |
|
|
|
}, |
|
|
|
@ -1238,9 +1218,24 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 2. 构造数据(含盘旋航点的 pointType、holdParams;地图标签默认字号 14、颜色 #333333) |
|
|
|
// 新建航线时:首尾航点转弯坡度固定为 0,中间可编辑航点默认 45° |
|
|
|
// 默认相对 K 时:按“路程÷默认速度”累加;用向上取整避免因整数分钟舍去导致冲突检测误报“无法按时到达” |
|
|
|
const wpCount = this.tempMapPoints.length; |
|
|
|
const finalWaypoints = this.tempMapPoints.map((p, index) => { |
|
|
|
let cumulativeMinutes = 0; |
|
|
|
const pointsWithStartTime = this.tempMapPoints.map((p, index) => { |
|
|
|
const startTime = this.minutesToStartTime(index === 0 ? 0 : Math.ceil(cumulativeMinutes)); |
|
|
|
if (index < wpCount - 1) { |
|
|
|
const next = this.tempMapPoints[index + 1]; |
|
|
|
const dist = this.segmentDistance( |
|
|
|
{ lat: p.lat, lng: p.lng, alt: p.alt != null ? p.alt : 5000 }, |
|
|
|
{ lat: next.lat, lng: next.lng, alt: next.alt != null ? next.alt : 5000 } |
|
|
|
); |
|
|
|
const speedKmh = Number(p.speed) || 800; |
|
|
|
cumulativeMinutes += (dist / 1000) * (60 / speedKmh); |
|
|
|
} |
|
|
|
return { ...p, startTime }; |
|
|
|
}); |
|
|
|
// 新建航线时:首尾航点转弯坡度固定为 0,中间可编辑航点默认 45° |
|
|
|
const finalWaypoints = pointsWithStartTime.map((p, index) => { |
|
|
|
const isFirstOrLast = index === 0 || index === wpCount - 1; |
|
|
|
const defaultTurnAngle = isFirstOrLast ? 0.0 : 45.0; |
|
|
|
return { |
|
|
|
@ -1249,7 +1244,7 @@ export default { |
|
|
|
lng: p.lng, |
|
|
|
alt: p.alt != null ? p.alt : 5000.0, |
|
|
|
speed: p.speed != null ? p.speed : 800.0, |
|
|
|
startTime: p.startTime || 'K+00:00:00', |
|
|
|
startTime: p.startTime, |
|
|
|
turnAngle: p.turnAngle != null ? p.turnAngle : defaultTurnAngle, |
|
|
|
labelFontSize: p.labelFontSize != null ? p.labelFontSize : 14, |
|
|
|
labelColor: p.labelColor || '#333333', |
|
|
|
@ -2354,11 +2349,14 @@ export default { |
|
|
|
const pos = { lng: p.lng, lat: p.lat, alt: p.alt }; |
|
|
|
return { |
|
|
|
segments: [{ startTime: globalMin, endTime: globalMax, startPos: pos, endPos: pos, type: 'wait' }], |
|
|
|
warnings |
|
|
|
warnings, |
|
|
|
earlyArrivalLegs: [], |
|
|
|
lateArrivalLegs: [] |
|
|
|
}; |
|
|
|
} |
|
|
|
const effectiveTime = [points[0].minutes]; |
|
|
|
const segments = []; |
|
|
|
const lateArrivalLegs = []; // 无法按时到达的航段,供冲突检测用 |
|
|
|
const path = pathData && pathData.path; |
|
|
|
const segmentEndIndices = pathData && pathData.segmentEndIndices; |
|
|
|
const holdArcRanges = pathData && pathData.holdArcRanges || {}; |
|
|
|
@ -2425,6 +2423,13 @@ export default { |
|
|
|
warnings.push( |
|
|
|
`某航段:距离约 ${(dist / 1000).toFixed(1)}km,计划 ${(scheduled - points[i].minutes).toFixed(0)} 分钟,当前速度 ${speedKmh}km/h 无法按时到达,约需 ≥${Math.ceil(requiredSpeedKmh)}km/h,请调整相对K时或速度。` |
|
|
|
); |
|
|
|
lateArrivalLegs.push({ |
|
|
|
legIndex: i, |
|
|
|
fromName: waypoints[i].name, |
|
|
|
toName: waypoints[i + 1].name, |
|
|
|
requiredSpeedKmh: Math.ceil(requiredSpeedKmh), |
|
|
|
speedKmh |
|
|
|
}); |
|
|
|
} else if (actualArrival < scheduled - 0.5) { |
|
|
|
warnings.push('存在航段将提前到达下一航点,平台将在该点等待至计划时间再飞往下一段。'); |
|
|
|
} |
|
|
|
@ -2449,7 +2454,7 @@ export default { |
|
|
|
earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name }); |
|
|
|
} |
|
|
|
} |
|
|
|
return { segments, warnings, earlyArrivalLegs }; |
|
|
|
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs }; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 从路径片段中按比例 t(0~1)取点:按弧长比例插值 */ |
|
|
|
@ -2800,10 +2805,14 @@ export default { |
|
|
|
const waypoints = fullRouteData.waypoints || []; |
|
|
|
this.activeRouteIds.push(route.id); |
|
|
|
this.selectedRouteId = fullRouteData.id; |
|
|
|
// 合并 list 中的 platformId/platform,以便拖拽航点后重绘时平台图标不丢失 |
|
|
|
this.selectedRouteDetails = { |
|
|
|
id: fullRouteData.id, |
|
|
|
name: fullRouteData.callSign, |
|
|
|
waypoints: waypoints |
|
|
|
waypoints: waypoints, |
|
|
|
platformId: route.platformId, |
|
|
|
platform: route.platform, |
|
|
|
attributes: route.attributes |
|
|
|
}; |
|
|
|
|
|
|
|
// 更新 routes 数组中对应航线的 waypoints 字段 |
|
|
|
@ -2952,11 +2961,15 @@ export default { |
|
|
|
const lastId = this.activeRouteIds[this.activeRouteIds.length - 1]; |
|
|
|
getRoutes(lastId).then(res => { |
|
|
|
if (res.code === 200 && res.data) { |
|
|
|
const fromList = this.routes.find(r => r.id === lastId); |
|
|
|
this.selectedRouteId = res.data.id; |
|
|
|
this.selectedRouteDetails = { |
|
|
|
id: res.data.id, |
|
|
|
name: res.data.callSign, |
|
|
|
waypoints: res.data.waypoints || [] |
|
|
|
waypoints: res.data.waypoints || [], |
|
|
|
platformId: fromList?.platformId, |
|
|
|
platform: fromList?.platform, |
|
|
|
attributes: fromList?.attributes |
|
|
|
}; |
|
|
|
} |
|
|
|
}).catch(e => { |
|
|
|
@ -2973,10 +2986,58 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 冲突操作 |
|
|
|
// 冲突操作:根据当前展示的航线与时间轴计算真实问题(提前到达、无法按时到达) |
|
|
|
runConflictCheck() { |
|
|
|
this.conflictCount = 2; |
|
|
|
this.$message.warning('检测到2处航线冲突'); |
|
|
|
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(); |
|
|
|
|
|
|
|
routeIds.forEach(routeId => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || route.waypoints.length < 2) return; |
|
|
|
let pathData = null; |
|
|
|
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(route.waypoints); |
|
|
|
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices) { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
} |
|
|
|
const { earlyArrivalLegs, lateArrivalLegs } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
|
|
|
|
const routeName = route.name || `航线${route.id}`; |
|
|
|
(earlyArrivalLegs || []).forEach(leg => { |
|
|
|
list.push({ |
|
|
|
id: id++, |
|
|
|
title: '提前到达', |
|
|
|
routeName, |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
|
suggestion: '该航段将提前到达下一航点,建议在此段加入盘旋或延后下一航点计划时间。', |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
|
list.push({ |
|
|
|
id: id++, |
|
|
|
title: '无法按时到达', |
|
|
|
routeName, |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
suggestion: `当前速度不足,建议将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h,或延后下一航点计划时间。`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
this.conflicts = list; |
|
|
|
this.conflictCount = list.length; |
|
|
|
if (list.length > 0) { |
|
|
|
this.$message.warning(`检测到 ${list.length} 处航线时间问题`); |
|
|
|
} else { |
|
|
|
this.$message.success('未发现航线时间冲突'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
viewConflict(conflict) { |
|
|
|
@ -3011,7 +3072,7 @@ export default { |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
|
|
|
|
/* 地图背景 - 保持不变 */ |
|
|
|
/* 地图背景:使用相对路径便于 IDE 与构建解析;若需图片请将 map-background.png 放到 src/assets/ */ |
|
|
|
.map-background { |
|
|
|
position: absolute; |
|
|
|
top: 0; |
|
|
|
@ -3019,8 +3080,7 @@ export default { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
background: linear-gradient(135deg, #1a2f4b 0%, #2c3e50 100%); |
|
|
|
/* 正确的写法,直接复制这行替换 */ |
|
|
|
background: url('~@/assets/map-background.png'); |
|
|
|
/* 若已存在 src/assets/map-background.png,可改为:background: url('../../assets/map-background.png'); 并注释掉上一行 */ |
|
|
|
background-size: cover; |
|
|
|
background-position: center; |
|
|
|
z-index: 1; |
|
|
|
@ -3244,40 +3304,6 @@ export default { |
|
|
|
line-height: 1.5; |
|
|
|
} |
|
|
|
|
|
|
|
.deduction-hint { |
|
|
|
margin: 0 0 8px 0; |
|
|
|
font-size: 12px; |
|
|
|
color: #909399; |
|
|
|
line-height: 1.4; |
|
|
|
} |
|
|
|
|
|
|
|
.deduction-warnings { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 6px; |
|
|
|
margin-top: 8px; |
|
|
|
padding: 6px 10px; |
|
|
|
background: rgba(230, 162, 60, 0.15); |
|
|
|
border: 1px solid rgba(230, 162, 60, 0.5); |
|
|
|
border-radius: 6px; |
|
|
|
font-size: 12px; |
|
|
|
color: #b88230; |
|
|
|
} |
|
|
|
.deduction-warnings i { |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
.deduction-warnings span { |
|
|
|
flex: 1; |
|
|
|
overflow: hidden; |
|
|
|
text-overflow: ellipsis; |
|
|
|
white-space: nowrap; |
|
|
|
} |
|
|
|
.deduction-warnings .warnings-more { |
|
|
|
flex-shrink: 0; |
|
|
|
color: #008aff; |
|
|
|
cursor: help; |
|
|
|
} |
|
|
|
|
|
|
|
.popup-hide-btn { |
|
|
|
position: absolute; |
|
|
|
top: -28px; |
|
|
|
|