|
|
|
@ -36,6 +36,26 @@ |
|
|
|
<el-button type="primary" @click="confirmSaveNewRoute">确 定</el-button> |
|
|
|
</div> |
|
|
|
</el-dialog> |
|
|
|
|
|
|
|
<!-- 设定/修改 K 时弹窗(房主或管理员可随时打开并修改) --> |
|
|
|
<el-dialog title="设定 / 修改 K 时" :visible.sync="showKTimeSetDialog" width="420px" :append-to-body="true"> |
|
|
|
<el-form label-width="90px"> |
|
|
|
<el-form-item label="K 时(基准)"> |
|
|
|
<el-date-picker |
|
|
|
v-model="kTimeForm.dateTime" |
|
|
|
type="datetime" |
|
|
|
value-format="yyyy-MM-dd HH:mm:ss" |
|
|
|
placeholder="选择日期和时间" |
|
|
|
style="width: 100%" |
|
|
|
/> |
|
|
|
</el-form-item> |
|
|
|
<p class="k-time-tip">航线的任务时间将以此 K 时为基准进行加减;航点表时间为相对 K 的分钟数。房主/管理员可随时再次点击「作战时间」修改 K 时。</p> |
|
|
|
</el-form> |
|
|
|
<div slot="footer" class="dialog-footer"> |
|
|
|
<el-button @click="showKTimeSetDialog = false">取 消</el-button> |
|
|
|
<el-button type="primary" @click="saveKTime">确 定</el-button> |
|
|
|
</div> |
|
|
|
</el-dialog> |
|
|
|
</div> |
|
|
|
<!-- 顶部导航栏 --> |
|
|
|
<top-header |
|
|
|
@ -43,9 +63,12 @@ |
|
|
|
:online-count="onlineCount" |
|
|
|
:combat-time="combatTime" |
|
|
|
:astro-time="astroTime" |
|
|
|
:room-detail="roomDetail" |
|
|
|
:can-set-k-time="canSetKTime" |
|
|
|
:user-avatar="userAvatar" |
|
|
|
:is-icon-edit-mode="isIconEditMode" |
|
|
|
@select-nav="selectTopNav" |
|
|
|
@set-k-time="openKTimeSetDialog" |
|
|
|
@save-plan="savePlan" |
|
|
|
@import-plan-file="importPlanFile" |
|
|
|
@import-acd="importACD" |
|
|
|
@ -156,6 +179,7 @@ |
|
|
|
<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> |
|
|
|
@ -198,8 +222,13 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div v-if="deductionWarnings.length > 0" 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> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 在线成员弹窗 --> |
|
|
|
@ -301,6 +330,7 @@ import { listScenario,addScenario,delScenario} from "@/api/system/scenario"; |
|
|
|
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes } from "@/api/system/routes"; |
|
|
|
import { updateWaypoints } from "@/api/system/waypoints"; |
|
|
|
import { listLib,addLib,delLib} from "@/api/system/lib"; |
|
|
|
import { getRooms, updateRooms } from "@/api/system/rooms"; |
|
|
|
import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue"; |
|
|
|
export default { |
|
|
|
name: 'MissionPlanningView', |
|
|
|
@ -353,6 +383,9 @@ export default { |
|
|
|
onlineCount: 30, |
|
|
|
combatTime: 'K+01:30:45', |
|
|
|
astroTime: '', |
|
|
|
roomDetail: null, |
|
|
|
showKTimeSetDialog: false, |
|
|
|
kTimeForm: { dateTime: null }, |
|
|
|
|
|
|
|
// 左侧菜单栏 |
|
|
|
isMenuHidden: true, // 是否完全隐藏左侧菜单 |
|
|
|
@ -468,6 +501,8 @@ export default { |
|
|
|
// 时间控制 |
|
|
|
timeProgress: 45, |
|
|
|
currentTime: 'K+01:15:30', |
|
|
|
deductionMinutesFromK: 0, |
|
|
|
deductionWarnings: [], |
|
|
|
isPlaying: false, |
|
|
|
playbackSpeed: 1, |
|
|
|
playbackInterval: null, |
|
|
|
@ -476,6 +511,33 @@ export default { |
|
|
|
userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png', |
|
|
|
}; |
|
|
|
}, |
|
|
|
watch: { |
|
|
|
timeProgress: { |
|
|
|
handler() { |
|
|
|
this.updateTimeFromProgress(); |
|
|
|
}, |
|
|
|
immediate: true |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
isRoomOwner() { |
|
|
|
if (!this.roomDetail || this.roomDetail.ownerId == null) return false; |
|
|
|
const myId = this.$store.getters.id; |
|
|
|
return String(myId) === String(this.roomDetail.ownerId); |
|
|
|
}, |
|
|
|
isAdmin() { |
|
|
|
const roles = this.$store.getters.roles || []; |
|
|
|
const id = this.$store.getters.id; |
|
|
|
return ( |
|
|
|
roles.includes('admin') || |
|
|
|
String(id) === '1' || |
|
|
|
(Array.isArray(roles) && roles.some(r => String(r).toLowerCase() === 'admin')) |
|
|
|
); |
|
|
|
}, |
|
|
|
canSetKTime() { |
|
|
|
return this.isRoomOwner || this.isAdmin; |
|
|
|
} |
|
|
|
}, |
|
|
|
mounted() { |
|
|
|
this.getList(); |
|
|
|
// 初始化时左侧菜单隐藏 |
|
|
|
@ -503,6 +565,7 @@ export default { |
|
|
|
console.log("从路由接收到的真实房间 ID:", this.currentRoomId); |
|
|
|
this.getList(); |
|
|
|
this.getPlatformList(); |
|
|
|
if (this.currentRoomId) this.getRoomDetail(); |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
// 处理从地图点击传来的编辑请求 |
|
|
|
@ -956,12 +1019,27 @@ export default { |
|
|
|
} else { |
|
|
|
updatedWaypoint.turnRadius = 0; |
|
|
|
} |
|
|
|
const response = await updateWaypoints(updatedWaypoint); |
|
|
|
// 明确构造后端需要的字段,确保 startTime(相对K时)一定会被提交并更新到数据库 |
|
|
|
const payload = { |
|
|
|
id: updatedWaypoint.id, |
|
|
|
routeId: updatedWaypoint.routeId, |
|
|
|
name: updatedWaypoint.name, |
|
|
|
seq: updatedWaypoint.seq, |
|
|
|
lat: updatedWaypoint.lat, |
|
|
|
lng: updatedWaypoint.lng, |
|
|
|
alt: updatedWaypoint.alt, |
|
|
|
speed: updatedWaypoint.speed, |
|
|
|
startTime: (updatedWaypoint.startTime != null && updatedWaypoint.startTime !== '') |
|
|
|
? updatedWaypoint.startTime |
|
|
|
: 'K+00:00:00', |
|
|
|
turnAngle: updatedWaypoint.turnAngle |
|
|
|
}; |
|
|
|
const response = await updateWaypoints(payload); |
|
|
|
if (response.code === 200) { |
|
|
|
const index = this.selectedRouteDetails.waypoints.findIndex(p => p.id === updatedWaypoint.id); |
|
|
|
if (index !== -1) { |
|
|
|
// 更新本地数据 |
|
|
|
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint }); |
|
|
|
// 更新本地数据(用已提交的 payload 保证 startTime 等与数据库一致) |
|
|
|
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint, ...payload }); |
|
|
|
// 通知地图组件同步更新 |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
// 更新航点图标 |
|
|
|
@ -998,18 +1076,72 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
updateCombatTime() { |
|
|
|
// 模拟作战时间(K时)的更新 |
|
|
|
// 这里简单模拟,实际应该根据业务逻辑计算 |
|
|
|
const now = new Date(); |
|
|
|
const baseSeconds = 5400; // 1小时30分钟 = 5400秒 |
|
|
|
const currentSeconds = now.getSeconds() + now.getMinutes() * 60 + now.getHours() * 3600; |
|
|
|
const combatSeconds = baseSeconds + (currentSeconds % 86400); |
|
|
|
|
|
|
|
const hours = Math.floor(combatSeconds / 3600); |
|
|
|
const minutes = Math.floor((combatSeconds % 3600) / 60); |
|
|
|
const seconds = combatSeconds % 60; |
|
|
|
|
|
|
|
this.combatTime = `K+${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
|
|
|
if (this.roomDetail && this.roomDetail.kAnchorTime) { |
|
|
|
const k0 = new Date(this.roomDetail.kAnchorTime).getTime(); |
|
|
|
const now = Date.now(); |
|
|
|
const offsetMs = now - k0; |
|
|
|
const sign = offsetMs >= 0 ? '+' : '-'; |
|
|
|
const absMs = Math.abs(offsetMs); |
|
|
|
const hours = Math.floor(absMs / 3600000); |
|
|
|
const minutes = Math.floor((absMs % 3600000) / 60000); |
|
|
|
const seconds = Math.floor((absMs % 60000) / 1000); |
|
|
|
this.combatTime = `K${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
|
|
|
} else { |
|
|
|
this.combatTime = '未设定'; |
|
|
|
} |
|
|
|
}, |
|
|
|
getRoomDetail() { |
|
|
|
if (!this.currentRoomId) return; |
|
|
|
getRooms(this.currentRoomId).then(res => { |
|
|
|
if (res.code === 200 && res.data) this.roomDetail = res.data; |
|
|
|
}).catch(() => {}); |
|
|
|
}, |
|
|
|
/** 将任意日期字符串格式化为 yyyy-MM-dd HH:mm:ss,供日期选择器使用 */ |
|
|
|
formatKTimeForPicker(val) { |
|
|
|
if (!val) return null; |
|
|
|
const d = new Date(val); |
|
|
|
if (isNaN(d.getTime())) return null; |
|
|
|
const y = d.getFullYear(); |
|
|
|
const m = (d.getMonth() + 1).toString().padStart(2, '0'); |
|
|
|
const day = d.getDate().toString().padStart(2, '0'); |
|
|
|
const h = d.getHours().toString().padStart(2, '0'); |
|
|
|
const min = d.getMinutes().toString().padStart(2, '0'); |
|
|
|
const s = d.getSeconds().toString().padStart(2, '0'); |
|
|
|
return `${y}-${m}-${day} ${h}:${min}:${s}`; |
|
|
|
}, |
|
|
|
openKTimeSetDialog() { |
|
|
|
if (!this.canSetKTime) { |
|
|
|
this.$message.info('仅房主或管理员可设定或修改 K 时'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.currentRoomId) { |
|
|
|
this.$message.warning('请先进入任务房间'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.roomDetail || !this.roomDetail.id) { |
|
|
|
this.$message.warning('房间信息加载中或未找到,请稍后再试'); |
|
|
|
return; |
|
|
|
} |
|
|
|
const existing = this.roomDetail.kAnchorTime |
|
|
|
? this.formatKTimeForPicker(this.roomDetail.kAnchorTime) |
|
|
|
: null; |
|
|
|
this.kTimeForm.dateTime = existing || this.formatKTimeForPicker(this.astroTime) || this.formatKTimeForPicker(new Date()); |
|
|
|
this.showKTimeSetDialog = true; |
|
|
|
}, |
|
|
|
saveKTime() { |
|
|
|
if (!this.roomDetail || !this.kTimeForm.dateTime) { |
|
|
|
this.$message.warning('请选择 K 时'); |
|
|
|
return; |
|
|
|
} |
|
|
|
updateRooms({ id: this.roomDetail.id, kAnchorTime: this.kTimeForm.dateTime }).then(res => { |
|
|
|
if (res.code === 200) { |
|
|
|
this.$message.success('K 时已设定'); |
|
|
|
this.showKTimeSetDialog = false; |
|
|
|
this.getRoomDetail(); |
|
|
|
} else { |
|
|
|
this.$message.error(res.msg || '设定失败'); |
|
|
|
} |
|
|
|
}).catch(() => this.$message.error('设定失败')); |
|
|
|
}, |
|
|
|
|
|
|
|
// 左侧菜单栏操作 |
|
|
|
@ -1401,6 +1533,9 @@ export default { |
|
|
|
} else if (item.id === 'deduction') { |
|
|
|
// 点击推演按钮,显示/隐藏K时弹出框 |
|
|
|
this.showKTimePopup = !this.showKTimePopup; |
|
|
|
if (this.showKTimePopup) { |
|
|
|
this.$nextTick(() => this.updateTimeFromProgress()); |
|
|
|
} |
|
|
|
// 点击推演时,也停止地图绘制状态 |
|
|
|
this.drawDom = false; |
|
|
|
this.airspaceDrawDom = false; |
|
|
|
@ -1440,7 +1575,7 @@ export default { |
|
|
|
if (this.timeProgress >= 100) { |
|
|
|
this.timeProgress = 0; |
|
|
|
} |
|
|
|
this.updateTimeFromProgress(); |
|
|
|
// 时间显示与平台位置由 watch timeProgress 触发 updateTimeFromProgress |
|
|
|
}, 100); |
|
|
|
}, |
|
|
|
|
|
|
|
@ -1470,15 +1605,168 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
updateTimeFromProgress() { |
|
|
|
const totalSeconds = Math.floor(this.timeProgress * 72); |
|
|
|
const hours = Math.floor(totalSeconds / 3600) - 2; |
|
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60); |
|
|
|
const seconds = totalSeconds % 60; |
|
|
|
|
|
|
|
const sign = hours >= 0 ? '+' : '-'; |
|
|
|
const absHours = Math.abs(hours); |
|
|
|
|
|
|
|
this.currentTime = `K${sign}${String(absHours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const span = Math.max(0, maxMinutes - minMinutes) || 120; |
|
|
|
const currentMinutesFromK = minMinutes + (this.timeProgress / 100) * span; |
|
|
|
this.deductionMinutesFromK = currentMinutesFromK; |
|
|
|
|
|
|
|
const sign = currentMinutesFromK >= 0 ? '+' : '-'; |
|
|
|
const absMin = Math.abs(Math.floor(currentMinutesFromK)); |
|
|
|
const hours = Math.floor(absMin / 60); |
|
|
|
const minutes = absMin % 60; |
|
|
|
this.currentTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`; |
|
|
|
this.updateDeductionPositions(); |
|
|
|
}, |
|
|
|
|
|
|
|
/** 仅针对当前展示的航线(activeRouteIds):从这些航线的航点中取推演时间范围(相对 K 的分钟数) */ |
|
|
|
getDeductionTimeRange() { |
|
|
|
let minMinutes = 0; |
|
|
|
let maxMinutes = 120; |
|
|
|
const minutesList = []; |
|
|
|
this.activeRouteIds.forEach(routeId => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || !route.waypoints.length) return; |
|
|
|
route.waypoints.forEach(wp => { |
|
|
|
const m = this.waypointStartTimeToMinutes(wp.startTime); |
|
|
|
minutesList.push(m); |
|
|
|
}); |
|
|
|
}); |
|
|
|
if (minutesList.length > 0) { |
|
|
|
minMinutes = Math.min(...minutesList); |
|
|
|
maxMinutes = Math.max(...minutesList); |
|
|
|
if (maxMinutes <= minMinutes) maxMinutes = minMinutes + 120; |
|
|
|
} |
|
|
|
return { minMinutes, maxMinutes }; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 将航点 startTime 字符串转为相对 K 的分钟数 */ |
|
|
|
waypointStartTimeToMinutes(s) { |
|
|
|
if (!s || typeof s !== 'string') return 0; |
|
|
|
const m = s.match(/K([+-])(\d{2}):(\d{2})/); |
|
|
|
if (!m) return 0; |
|
|
|
const sign = m[1] === '+' ? 1 : -1; |
|
|
|
const h = parseInt(m[2], 10); |
|
|
|
const min = parseInt(m[3], 10); |
|
|
|
return sign * (h * 60 + min); |
|
|
|
}, |
|
|
|
|
|
|
|
/** 两航点间近似距离(米),含高度差 */ |
|
|
|
segmentDistance(wp1, wp2) { |
|
|
|
const R = 6371000; |
|
|
|
const lat1 = (wp1.lat * Math.PI) / 180; |
|
|
|
const lat2 = (wp2.lat * Math.PI) / 180; |
|
|
|
const dlat = ((wp2.lat - wp1.lat) * Math.PI) / 180; |
|
|
|
const dlng = ((wp2.lng - wp1.lng) * Math.PI) / 180; |
|
|
|
const a = Math.sin(dlat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlng / 2) ** 2; |
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
|
|
|
const horizontal = R * c; |
|
|
|
const dalt = (wp2.alt || 0) - (wp1.alt || 0); |
|
|
|
return Math.sqrt(horizontal * horizontal + dalt * dalt); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 按速度与计划时间构建航线时间轴:含飞行段与“提前到达则等待”的等待段,并做航段校验。 |
|
|
|
* 若所有航点相对 K 时相同,则在 [globalMin, globalMax] 内按航点顺序均匀分布,避免闪现。 |
|
|
|
*/ |
|
|
|
buildRouteTimeline(waypoints, globalMin, globalMax) { |
|
|
|
const warnings = []; |
|
|
|
if (!waypoints || waypoints.length === 0) return { segments: [], warnings }; |
|
|
|
const points = waypoints.map(wp => ({ |
|
|
|
lng: parseFloat(wp.lng), |
|
|
|
lat: parseFloat(wp.lat), |
|
|
|
alt: Number(wp.alt) || 0, |
|
|
|
minutes: this.waypointStartTimeToMinutes(wp.startTime), |
|
|
|
speed: Number(wp.speed) || 800 |
|
|
|
})); |
|
|
|
const allSame = points.every(p => p.minutes === points[0].minutes); |
|
|
|
if (allSame && points.length > 1) { |
|
|
|
const span = Math.max(globalMax - globalMin, 1); |
|
|
|
points.forEach((p, i) => { |
|
|
|
p.minutes = globalMin + (span * i) / (points.length - 1); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
points.sort((a, b) => a.minutes - b.minutes); |
|
|
|
} |
|
|
|
if (points.length === 1) { |
|
|
|
const p = points[0]; |
|
|
|
const pos = { lng: p.lng, lat: p.lat, alt: p.alt }; |
|
|
|
return { |
|
|
|
segments: [{ startTime: globalMin, endTime: globalMax, startPos: pos, endPos: pos, type: 'wait' }], |
|
|
|
warnings |
|
|
|
}; |
|
|
|
} |
|
|
|
const effectiveTime = [points[0].minutes]; |
|
|
|
const segments = []; |
|
|
|
for (let i = 0; i < points.length - 1; i++) { |
|
|
|
const dist = this.segmentDistance(points[i], points[i + 1]); |
|
|
|
const speedKmh = points[i].speed || 800; |
|
|
|
const travelMin = (dist / 1000) * (60 / speedKmh); |
|
|
|
const actualArrival = effectiveTime[i] + travelMin; |
|
|
|
const scheduled = points[i + 1].minutes; |
|
|
|
if (travelMin > 0 && scheduled - points[i].minutes > 0) { |
|
|
|
const requiredSpeedKmh = (dist / 1000) / ((scheduled - points[i].minutes) / 60); |
|
|
|
if (actualArrival > scheduled) { |
|
|
|
warnings.push( |
|
|
|
`某航段:距离约 ${(dist / 1000).toFixed(1)}km,计划 ${(scheduled - points[i].minutes).toFixed(0)} 分钟,当前速度 ${speedKmh}km/h 无法按时到达,约需 ≥${Math.ceil(requiredSpeedKmh)}km/h,请调整相对K时或速度。` |
|
|
|
); |
|
|
|
} else if (actualArrival < scheduled - 0.5) { |
|
|
|
warnings.push('存在航段将提前到达下一航点,平台将在该点等待至计划时间再飞往下一段。'); |
|
|
|
} |
|
|
|
} |
|
|
|
effectiveTime[i + 1] = Math.max(actualArrival, scheduled); |
|
|
|
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
|
const posNext = { lng: points[i + 1].lng, lat: points[i + 1].lat, alt: points[i + 1].alt }; |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly' }); |
|
|
|
if (actualArrival < effectiveTime[i + 1]) { |
|
|
|
segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait' }); |
|
|
|
} |
|
|
|
} |
|
|
|
return { segments, warnings }; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 从时间轴中取当前推演时间对应的位置 */ |
|
|
|
getPositionFromTimeline(segments, minutesFromK) { |
|
|
|
if (!segments || segments.length === 0) return null; |
|
|
|
if (minutesFromK <= segments[0].startTime) return segments[0].startPos; |
|
|
|
const last = segments[segments.length - 1]; |
|
|
|
if (minutesFromK >= last.endTime) return last.endPos; |
|
|
|
for (let i = 0; i < segments.length; i++) { |
|
|
|
const s = segments[i]; |
|
|
|
if (minutesFromK < s.endTime) { |
|
|
|
const t = (minutesFromK - s.startTime) / (s.endTime - s.startTime); |
|
|
|
if (s.type === 'wait') return s.startPos; |
|
|
|
return { |
|
|
|
lng: s.startPos.lng + (s.endPos.lng - s.startPos.lng) * t, |
|
|
|
lat: s.startPos.lat + (s.endPos.lat - s.startPos.lat) * t, |
|
|
|
alt: s.startPos.alt + (s.endPos.alt - s.startPos.alt) * t |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
return last.endPos; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;返回 { position, warnings } */ |
|
|
|
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) { |
|
|
|
if (!waypoints || waypoints.length === 0) return { position: null, warnings: [] }; |
|
|
|
const { segments, warnings } = this.buildRouteTimeline(waypoints, globalMin, globalMax); |
|
|
|
const position = this.getPositionFromTimeline(segments, minutesFromK); |
|
|
|
return { position, warnings }; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 仅根据当前展示的航线(activeRouteIds)更新平台图标位置,并汇总航段提示 */ |
|
|
|
updateDeductionPositions() { |
|
|
|
if (!this.$refs.cesiumMap || !this.$refs.cesiumMap.updatePlatformPosition) return; |
|
|
|
const minutesFromK = this.deductionMinutesFromK != null ? this.deductionMinutesFromK : 0; |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const allWarnings = []; |
|
|
|
this.activeRouteIds.forEach(routeId => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || route.waypoints.length === 0) return; |
|
|
|
const { position, warnings } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes); |
|
|
|
if (warnings && warnings.length) allWarnings.push(...warnings); |
|
|
|
if (position) this.$refs.cesiumMap.updatePlatformPosition(routeId, position); |
|
|
|
}); |
|
|
|
this.deductionWarnings = [...new Set(allWarnings)]; |
|
|
|
}, |
|
|
|
|
|
|
|
// 时间控制(保留用于底部时间轴) |
|
|
|
@ -1497,9 +1785,14 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
formatTimeTooltip(val) { |
|
|
|
const hours = Math.floor(val / 4); |
|
|
|
const minutes = (val % 4) * 15; |
|
|
|
return `K+${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`; |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const span = Math.max(0, maxMinutes - minMinutes) || 120; |
|
|
|
const minutesFromK = minMinutes + (val / 100) * span; |
|
|
|
const sign = minutesFromK >= 0 ? '+' : '-'; |
|
|
|
const absMin = Math.abs(Math.floor(minutesFromK)); |
|
|
|
const hours = Math.floor(absMin / 60); |
|
|
|
const minutes = absMin % 60; |
|
|
|
return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`; |
|
|
|
}, |
|
|
|
selectPlan(plan) { |
|
|
|
if (plan && plan.id) { |
|
|
|
@ -1971,6 +2264,47 @@ export default { |
|
|
|
pointer-events: auto; |
|
|
|
} |
|
|
|
|
|
|
|
.k-time-tip { |
|
|
|
font-size: 12px; |
|
|
|
color: #909399; |
|
|
|
margin: 8px 0 0; |
|
|
|
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; |
|
|
|
|