From 2e930770872304e6d3bb6862c54c9d9158cea178 Mon Sep 17 00:00:00 2001 From: ctw <1051735452@qq.com> Date: Tue, 3 Feb 2026 14:16:45 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E7=A9=BA=E5=9F=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/views/childRoom/index.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index f4bbca5..91ec3ce 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -363,7 +363,7 @@ export default { { id: 'file', name: '方案', icon: 'el-icon-folder-opened' }, { id: 'start', name: '冲突', icon: 'el-icon-error' }, { id: 'insert', name: '平台', icon: 'el-icon-s-platform' }, - { id: 'pattern', name: '图案', icon: 'el-icon-picture-outline-round' }, + { id: 'pattern', name: '空域', icon: 'el-icon-picture-outline-round' }, { id: 'deduction', name: '推演', icon: 'el-icon-video-play' }, { id: 'modify', name: '修改', icon: 'el-icon-edit-outline' }, { id: 'refresh', name: '刷新', icon: 'el-icon-refresh' }, @@ -556,7 +556,7 @@ export default { console.error("查找失败!账本内IDs:", waypointsList.map(w => w.id)); } }, - + // 处理从地图点击传来的航线编辑请求 async handleOpenRouteEdit(routeId) { console.log(`>>> [父组件接收] 航线 ID: ${routeId}`); @@ -583,7 +583,7 @@ export default { console.error('获取航线数据失败:', error); } }, - + // 显示在线成员弹窗 showOnlineMembersDialog() { this.showOnlineMembers = true; From 76927b44e8f73c9a2db9a2350b1237159af1a752 Mon Sep 17 00:00:00 2001 From: ctw <1051735452@qq.com> Date: Tue, 3 Feb 2026 16:56:15 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=9B=BE=E6=A0=87=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/assets/icons/svg/chongtu.svg | 1 + ruoyi-ui/src/assets/icons/svg/circle.svg | 1 + ruoyi-ui/src/assets/icons/svg/cj.svg | 1 + ruoyi-ui/src/assets/icons/svg/cursor.svg | 1 + ruoyi-ui/src/assets/icons/svg/dt.svg | 1 + ruoyi-ui/src/assets/icons/svg/jx.svg | 1 + ruoyi-ui/src/assets/icons/svg/ky.svg | 1 + ruoyi-ui/src/assets/icons/svg/plan.svg | 1 + ruoyi-ui/src/assets/icons/svg/sx.svg | 1 + ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue | 22 +++++++++++++++++----- ruoyi-ui/src/views/childRoom/LeftMenu.vue | 24 ++++++++++++++++++------ ruoyi-ui/src/views/childRoom/index.vue | 10 +++++----- 12 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 ruoyi-ui/src/assets/icons/svg/chongtu.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/circle.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/cj.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/cursor.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/dt.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/jx.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/ky.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/plan.svg create mode 100644 ruoyi-ui/src/assets/icons/svg/sx.svg diff --git a/ruoyi-ui/src/assets/icons/svg/chongtu.svg b/ruoyi-ui/src/assets/icons/svg/chongtu.svg new file mode 100644 index 0000000..cc5c41a --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/chongtu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/circle.svg b/ruoyi-ui/src/assets/icons/svg/circle.svg new file mode 100644 index 0000000..833bdf7 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/cj.svg b/ruoyi-ui/src/assets/icons/svg/cj.svg new file mode 100644 index 0000000..1a23e44 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/cj.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/cursor.svg b/ruoyi-ui/src/assets/icons/svg/cursor.svg new file mode 100644 index 0000000..d159790 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/cursor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/dt.svg b/ruoyi-ui/src/assets/icons/svg/dt.svg new file mode 100644 index 0000000..09fb981 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/dt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/jx.svg b/ruoyi-ui/src/assets/icons/svg/jx.svg new file mode 100644 index 0000000..f9441ad --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/jx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/ky.svg b/ruoyi-ui/src/assets/icons/svg/ky.svg new file mode 100644 index 0000000..0255ab7 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/ky.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/plan.svg b/ruoyi-ui/src/assets/icons/svg/plan.svg new file mode 100644 index 0000000..608c2aa --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/plan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/assets/icons/svg/sx.svg b/ruoyi-ui/src/assets/icons/svg/sx.svg new file mode 100644 index 0000000..95ca792 --- /dev/null +++ b/ruoyi-ui/src/assets/icons/svg/sx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue index de6a0f0..58d3b46 100644 --- a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue +++ b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue @@ -9,7 +9,8 @@ @click="handleItemClick(item)" :title="item.name" > - + + @@ -42,9 +43,9 @@ export default { allToolbarItems: [ { id: 'mouse', name: '鼠标', icon: 'el-icon-position' }, { id: 'polygon', name: '面', icon: 'el-icon-house' }, - { id: 'rectangle', name: '矩形', icon: 'el-icon-crop' }, - { id: 'circle', name: '圆形', icon: 'el-icon-circle-plus-outline' }, - { id: 'sector', name: '扇形', icon: 'el-icon-pie-chart' }, + { id: 'rectangle', name: '矩形', icon: 'jx' }, + { id: 'circle', name: '圆形', icon: 'circle' }, + { id: 'sector', name: '扇形', icon: 'sx' }, { id: 'arrow', name: '箭头', icon: 'el-icon-right' }, { id: 'text', name: '文本', icon: 'el-icon-document' }, { id: 'image', name: '图片', icon: 'el-icon-picture-outline' }, @@ -55,7 +56,7 @@ export default { ], // 测距模式工具列表 rangingToolbarItems: [ - { id: 'mouse', name: '鼠标', icon: 'el-icon-position' }, + { id: 'mouse', name: '鼠标', icon: 'cursor' }, { id: 'point', name: '点', icon: 'el-icon-location' }, { id: 'line', name: '线', icon: 'el-icon-edit-outline' }, { id: 'clear', name: '清除', icon: 'el-icon-delete' } @@ -72,6 +73,11 @@ export default { } }, methods: { + /** 判断是否为本地 SVG 图标(非 Element 的 el-icon-* 类名) */ + isSvgIcon(icon) { + return icon && typeof icon === 'string' && !icon.startsWith('el-icon-') + }, + handleItemClick(item) { if (item.id === 'clear') { this.$emit('clear-all') @@ -148,6 +154,12 @@ export default { box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3); } +.toolbar-item .toolbar-svg-icon { + width: 1em; + height: 1em; + font-size: 16px; +} + .toolbar-item:disabled { opacity: 0.5; cursor: not-allowed; diff --git a/ruoyi-ui/src/views/childRoom/LeftMenu.vue b/ruoyi-ui/src/views/childRoom/LeftMenu.vue index 64c3896..64b0af7 100644 --- a/ruoyi-ui/src/views/childRoom/LeftMenu.vue +++ b/ruoyi-ui/src/views/childRoom/LeftMenu.vue @@ -30,24 +30,25 @@ @contextmenu.prevent="handleRightClick(item)" :title="item.name" > - + +
- +
- +
- + {{ $t('leftMenu.delete') }} - + \ No newline at end of file + diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index f5c69ad..041697d 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -361,14 +361,14 @@ export default { // 默认菜单项配置 defaultMenuItems: [ - { id: 'file', name: '方案', icon: 'el-icon-folder-opened' }, - { id: 'start', name: '冲突', icon: 'el-icon-error' }, + { id: 'file', name: '方案', icon: 'plan' }, + { id: 'start', name: '冲突', icon: 'chongtu' }, { id: 'insert', name: '平台', icon: 'el-icon-s-platform' }, - { id: 'pattern', name: '空域', icon: 'el-icon-picture-outline-round' }, + { id: 'pattern', name: '空域', icon: 'ky' }, { id: 'deduction', name: '推演', icon: 'el-icon-video-play' }, - { id: 'modify', name: '修改', icon: 'el-icon-edit-outline' }, + { id: 'modify', name: '测距', icon: 'cj' }, { id: 'refresh', name: '刷新', icon: 'el-icon-refresh' }, - { id: 'basemap', name: '底图', icon: 'el-icon-picture' }, + { id: 'basemap', name: '底图', icon: 'dt' }, { id: 'save', name: '保存', icon: 'el-icon-document-checked' }, { id: 'import', name: '导入', icon: 'el-icon-upload2' }, { id: 'export', name: '导出', icon: 'el-icon-download' } From 8ad1c6e9e96cefab5368103752708f44ba8a30fb Mon Sep 17 00:00:00 2001 From: ctw <1051735452@qq.com> Date: Thu, 5 Feb 2026 10:56:25 +0800 Subject: [PATCH 3/4] =?UTF-8?q?K=E6=97=B6=E5=92=8C=E7=AE=80=E5=8D=95?= =?UTF-8?q?=E7=9A=84=E8=88=AA=E7=BA=BF=E5=8A=A8=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/views/cesiumMap/index.vue | 16 + ruoyi-ui/src/views/childRoom/RightPanel.vue | 13 +- ruoyi-ui/src/views/childRoom/TopHeader.vue | 34 +- ruoyi-ui/src/views/childRoom/index.vue | 394 ++++++++++++++++++++-- ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue | 59 +++- 5 files changed, 467 insertions(+), 49 deletions(-) diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index cf4a945..37896eb 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -562,6 +562,22 @@ export default { } this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}`); }, + /** 动态推演:更新某条航线的平台图标位置(position: { lng, lat, alt } 或 Cesium.Cartesian3) */ + updatePlatformPosition(routeId, position) { + if (!this.viewer) return; + const entity = this.viewer.entities.getById(`route-platform-${routeId}`); + if (!entity || !entity.position) return; + let cartesian; + if (position && position.x !== undefined && position.y !== undefined && position.z !== undefined) { + cartesian = position; + } else if (position && position.lng != null && position.lat != null) { + const alt = position.alt != null ? Number(position.alt) : 0; + cartesian = Cesium.Cartesian3.fromDegrees(Number(position.lng), Number(position.lat), alt); + } else { + return; + } + entity.position = cartesian; + }, checkCesiumLoaded() { if (typeof Cesium === 'undefined') { console.error('Cesium未加载,请检查CDN链接'); diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index d972568..3187dc1 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -85,7 +85,7 @@
{{ point.name }}
-
高度: {{ point.alt }}m | 速度: {{ point.speed }}
+
高度: {{ point.alt }}m | 速度: {{ point.speed }}
@@ -334,6 +334,17 @@ export default { } }, methods: { + /** 航点 startTime(如 K+00:40:00)格式化为简短显示:K+40 或 K-15 */ + formatWaypointKTime(startTime) { + if (!startTime || typeof startTime !== 'string') return '—'; + const m = startTime.match(/K([+-])(\d{2}):(\d{2})/); + if (!m) return startTime; + const sign = m[1]; + const h = parseInt(m[2], 10); + const min = parseInt(m[3], 10); + const totalMin = h * 60 + min; + return totalMin === 0 ? 'K+0' : `K${sign}${totalMin}`; + }, // 切换方案展开/折叠 togglePlan(planId) { const index = this.expandedPlans.indexOf(planId) diff --git a/ruoyi-ui/src/views/childRoom/TopHeader.vue b/ruoyi-ui/src/views/childRoom/TopHeader.vue index b3e3b34..b04d5d2 100644 --- a/ruoyi-ui/src/views/childRoom/TopHeader.vue +++ b/ruoyi-ui/src/views/childRoom/TopHeader.vue @@ -233,11 +233,18 @@
-
+
{{ $t('topHeader.info.combatTime') }}
-
{{ combatTime }}
+
+ {{ combatTime }} + +
@@ -312,6 +319,14 @@ export default { type: String, default: '' }, + roomDetail: { + type: Object, + default: null + }, + canSetKTime: { + type: Boolean, + default: false + }, userAvatar: { type: String, default: 'https://cube.elemecdn.com/0/88dd03f9bf287d08f58fbcf58fddbf4a8c6/avatar.png' @@ -389,9 +404,7 @@ export default { this.$emit('import-layer') }, - importRoute() { - this.$emit('import-route') - }, + exportPlan() { this.$emit('export-plan') @@ -856,6 +869,17 @@ export default { font-weight: 600; } +.info-box.clickable { + cursor: pointer; +} + +.info-box .set-k-hint { + margin-left: 4px; + font-size: 12px; + color: #008aff; + vertical-align: middle; +} + .info-icon { font-size: 20px; color: #008aff; diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 041697d..d50aaea 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -36,6 +36,26 @@ 确 定
+ + + + + + + +

航线的任务时间将以此 K 时为基准进行加减;航点表时间为相对 K 的分钟数。房主/管理员可随时再次点击「作战时间」修改 K 时。

+
+ +
+

仅推演当前展示的航线;K 时可随时由房主/管理员在右上角「作战时间」处修改。

@@ -198,8 +222,13 @@
- - +
+ + {{ deductionWarnings[0] }} + + 等 {{ deductionWarnings.length }} 条 + +
@@ -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; diff --git a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue index 6bb0006..d03dd53 100644 --- a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue @@ -47,14 +47,16 @@ - - + - + /> +
正数=K 之后,负数=K 之前。例:40 表示 K+00:40,-15 表示 K-00:15
@@ -98,7 +100,7 @@ export default { alt: 5000, speed: 800, turnAngle: 0, - startTime: '', + minutesFromK: 0, currentIndex: -1, totalPoints: 0, isBankDisabled: false @@ -118,9 +120,6 @@ export default { turnAngle: [ // 同样应用到转弯坡度 { required: true, validator: validateNumber, message: '请输入有效转弯坡度', trigger: ['blur', 'change'] } - ], - startTime: [ - { required: true, message: '请选择起始时间', trigger: 'change' } ] } }; @@ -147,7 +146,7 @@ export default { name: this.waypoint.name || '', alt: this.waypoint.alt !== undefined && this.waypoint.alt !== null ? Number(this.waypoint.alt) : 0, speed: this.waypoint.speed !== undefined && this.waypoint.speed !== null ? Number(this.waypoint.speed) : 0, - startTime: this.waypoint.startTime || '', + minutesFromK: this.startTimeToMinutes(this.waypoint.startTime), currentIndex: index, totalPoints: total, isBankDisabled: locked, @@ -166,13 +165,40 @@ export default { saveWaypoint() { this.$refs.formRef.validate((valid) => { if (valid) { + const { minutesFromK, ...rest } = this.formData; + const startTimeStr = this.minutesToStartTime(minutesFromK); this.$emit('save', { ...this.waypoint, - ...this.formData + ...rest, + startTime: startTimeStr }); this.closeDialog(); } }); + }, + /** 将 startTime 字符串(如 K+00:40:00)转为相对 K 的分钟数 */ + startTimeToMinutes(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); + }, + /** 将相对 K 的分钟数转为 startTime 字符串 */ + minutesToStartTime(m) { + const num = Number(m); + if (isNaN(num)) return 'K+00:00:00'; + if (num >= 0) { + const h = Math.floor(num / 60); + const min = num % 60; + return `K+${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:00`; + } + const abs = Math.abs(num); + const h = Math.floor(abs / 60); + const min = abs % 60; + return `K-${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:00`; } } }; @@ -262,4 +288,11 @@ export default { border-top: 1px solid #e8e8e8; gap: 10px; } + +.form-tip { + font-size: 12px; + color: #909399; + margin-top: 4px; + line-height: 1.4; +} From 3687d4ff0a9ef0de01945329cd8979715014abfc Mon Sep 17 00:00:00 2001 From: ctw <1051735452@qq.com> Date: Thu, 5 Feb 2026 13:19:48 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E9=A3=9E=E6=9C=BA=E6=9C=9D=E5=90=91?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/views/cesiumMap/index.vue | 83 ++++++++++++++++++++++++++++++++-- ruoyi-ui/src/views/childRoom/index.vue | 81 ++++++++++++++++++++++++++++----- 2 files changed, 148 insertions(+), 16 deletions(-) diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 37896eb..53ed7b2 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -422,11 +422,17 @@ export default { } }); }); - // 在起点渲染平台图标(当航线有关联平台且平台有图标时) + // 在起点渲染平台图标(当航线有关联平台且平台有图标时),默认朝向为航线方向 const iconUrl = (platform && platform.imageUrl) || (platform && platform.iconUrl); if (iconUrl && originalPositions.length > 0) { const platformBillboardId = `route-platform-${routeId}`; const fullUrl = this.formatPlatformIconUrl(iconUrl); + let initialRotation; + const pathData = this.getRoutePathWithSegmentIndices(waypoints); + if (pathData.path && pathData.path.length >= 2) { + const heading = this.computeHeadingFromPositions(pathData.path[0], pathData.path[1]); + if (heading !== undefined) initialRotation = Math.PI / 2 - heading; + } this.viewer.entities.add({ id: platformBillboardId, name: (platform && platform.name) || '平台', @@ -439,7 +445,8 @@ export default { verticalOrigin: Cesium.VerticalOrigin.CENTER, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, scaleByDistance: new Cesium.NearFarScalar(500, 2.0, 200000, 0.4), - translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6) + translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6), + ...(initialRotation !== undefined && { rotation: initialRotation }) } }); } @@ -545,6 +552,44 @@ export default { } return arc; }, + + /** + * 获取与地图绘制一致的带转弯弧的路径(用于推演时图标沿弧线运动)。 + * @param {Array} waypoints - 航点列表,需含 lng, lat, alt, speed, turnAngle + * @returns {{ path: Array<{lng,lat,alt}>, segmentEndIndices: number[] }} path 为路径点;segmentEndIndices[i] 为第 i 段(航点 i -> i+1)在 path 中的结束下标 + */ + getRoutePathWithSegmentIndices(waypoints) { + if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [] }; + const ellipsoid = this.viewer.scene.globe.ellipsoid; + const toLngLatAlt = (cartesian) => { + const carto = Cesium.Cartographic.fromCartesian(cartesian, ellipsoid); + return { + lng: Cesium.Math.toDegrees(carto.longitude), + lat: Cesium.Math.toDegrees(carto.latitude), + alt: carto.height + }; + }; + const originalPositions = waypoints.map(wp => + Cesium.Cartesian3.fromDegrees(parseFloat(wp.lng), parseFloat(wp.lat), Number(wp.alt) || 0) + ); + const path = []; + const segmentEndIndices = []; + for (let i = 0; i < waypoints.length; i++) { + const currPos = originalPositions[i]; + const radius = this.getWaypointRadius(waypoints[i]); + if (i === 0 || i === waypoints.length - 1 || radius <= 0) { + path.push(toLngLatAlt(currPos)); + } else { + const prevPos = originalPositions[i - 1]; + const nextPos = originalPositions[i + 1]; + const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius); + arcPoints.forEach(p => path.push(toLngLatAlt(p))); + } + if (i >= 1) segmentEndIndices[i - 1] = path.length - 1; + } + return { path, segmentEndIndices }; + }, + removeRouteById(routeId) { // 从地图上移除所有属于该 routeId 的实体 const entityList = this.viewer.entities.values; @@ -562,8 +607,31 @@ export default { } this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}`); }, - /** 动态推演:更新某条航线的平台图标位置(position: { lng, lat, alt } 或 Cesium.Cartesian3) */ - updatePlatformPosition(routeId, position) { + /** + * 根据当前点与另一点计算航向角(弧度),用于飞机图标朝向。 + * 航向:北为 0,顺时针为正。Cesium billboard 的 rotation 为自上而下看逆时针,故设置 rotation = -heading。 + */ + computeHeadingFromPositions(current, other) { + if (!current || !other) return undefined; + const cartesian1 = current.x !== undefined && current.y !== undefined && current.z !== undefined + ? current + : Cesium.Cartesian3.fromDegrees(Number(current.lng), Number(current.lat), Number(current.alt) || 0); + const cartesian2 = other.x !== undefined && other.y !== undefined && other.z !== undefined + ? other + : Cesium.Cartesian3.fromDegrees(Number(other.lng), Number(other.lat), Number(other.alt) || 0); + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(cartesian1); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toOther = Cesium.Cartesian3.subtract(cartesian2, cartesian1, new Cesium.Cartesian3()); + const e = Cesium.Cartesian3.dot(toOther, east); + const n = Cesium.Cartesian3.dot(toOther, north); + if (Math.abs(e) < 1e-10 && Math.abs(n) < 1e-10) return undefined; + const heading = Math.atan2(e, n); + return heading; + }, + + /** 动态推演:更新某条航线的平台图标位置与朝向(position: { lng, lat, alt } 或 Cesium.Cartesian3;directionPoint 为用于计算机头朝向的另一点,如下一位置或上一位置) */ + updatePlatformPosition(routeId, position, directionPoint) { if (!this.viewer) return; const entity = this.viewer.entities.getById(`route-platform-${routeId}`); if (!entity || !entity.position) return; @@ -577,6 +645,13 @@ export default { return; } entity.position = cartesian; + if (entity.billboard && directionPoint) { + const heading = this.computeHeadingFromPositions(position, directionPoint); + if (heading !== undefined) { + // 图标默认朝右(东),要让机头指向运动方向需逆时针转 90° 使“右”对齐北,故 rotation = π/2 - heading + entity.billboard.rotation = Math.PI / 2 - heading; + } + } }, checkCesiumLoaded() { if (typeof Cesium === 'undefined') { diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index d50aaea..93cf637 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -1716,25 +1716,70 @@ export default { 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' }); + segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i }); if (actualArrival < effectiveTime[i + 1]) { - segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait' }); + segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait', legIndex: i }); } } return { segments, warnings }; }, - /** 从时间轴中取当前推演时间对应的位置 */ - getPositionFromTimeline(segments, minutesFromK) { + /** 从路径片段中按比例 t(0~1)取点:按弧长比例插值 */ + getPositionAlongPathSlice(pathSlice, t) { + if (!pathSlice || pathSlice.length === 0) return null; + if (pathSlice.length === 1 || t <= 0) return pathSlice[0]; + if (t >= 1) return pathSlice[pathSlice.length - 1]; + let totalLen = 0; + const lengths = [0]; + for (let i = 1; i < pathSlice.length; i++) { + totalLen += this.segmentDistance(pathSlice[i - 1], pathSlice[i]); + lengths.push(totalLen); + } + const targetDist = t * totalLen; + let idx = 0; + while (idx < lengths.length - 1 && lengths[idx + 1] < targetDist) idx++; + const a = pathSlice[idx]; + const b = pathSlice[idx + 1]; + const segLen = lengths[idx + 1] - lengths[idx]; + const segT = segLen > 0 ? (targetDist - lengths[idx]) / segLen : 0; + return { + lng: a.lng + (b.lng - a.lng) * segT, + lat: a.lat + (b.lat - a.lat) * segT, + alt: a.alt + (b.alt - a.alt) * segT + }; + }, + + /** 从时间轴中取当前推演时间对应的位置;若有 path/segmentEndIndices 则沿带转弯弧的路径插值 */ + getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices) { 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; + if (minutesFromK >= last.endTime) { + // 若最后一段是等待且有弧线路径,终点取弧线终点 + if (last.type === 'wait' && path && segmentEndIndices && last.legIndex != null && last.legIndex < segmentEndIndices.length && path[segmentEndIndices[last.legIndex]]) { + return path[segmentEndIndices[last.legIndex]]; + } + 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; + if (s.type === 'wait') { + // 有弧线路径时在弧线终点等待,不跳到航点 + if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) { + const endIdx = segmentEndIndices[s.legIndex]; + if (path[endIdx]) return path[endIdx]; + } + return s.startPos; + } + // 有带弧路径且当前为飞行段且存在对应航段索引时,沿路径弧线插值(含上一段终点,保证从弧线终点匀速运动到本段终点、不闪现) + if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) { + const startIdx = s.legIndex === 0 ? 0 : segmentEndIndices[s.legIndex - 1]; + const endIdx = segmentEndIndices[s.legIndex]; + const pathSlice = path.slice(startIdx, endIdx + 1); + if (pathSlice.length > 0) return this.getPositionAlongPathSlice(pathSlice, t); + } return { lng: s.startPos.lng + (s.endPos.lng - s.startPos.lng) * t, lat: s.startPos.lat + (s.endPos.lat - s.startPos.lat) * t, @@ -1745,12 +1790,24 @@ export default { return last.endPos; }, - /** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;返回 { position, warnings } */ + /** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧路径运动;返回 { position, nextPosition, previousPosition, warnings },用于计算机头朝向 */ getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) { - if (!waypoints || waypoints.length === 0) return { position: null, warnings: [] }; + if (!waypoints || waypoints.length === 0) return { position: null, nextPosition: null, previousPosition: null, warnings: [] }; const { segments, warnings } = this.buildRouteTimeline(waypoints, globalMin, globalMax); - const position = this.getPositionFromTimeline(segments, minutesFromK); - return { position, warnings }; + let path = null; + let segmentEndIndices = null; + if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) { + const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(waypoints); + if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) { + path = ret.path; + segmentEndIndices = ret.segmentEndIndices; + } + } + const position = this.getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices); + const stepMin = 1 / 60; + const nextPosition = this.getPositionFromTimeline(segments, minutesFromK + stepMin, path, segmentEndIndices); + const previousPosition = this.getPositionFromTimeline(segments, minutesFromK - stepMin, path, segmentEndIndices); + return { position, nextPosition, previousPosition, warnings }; }, /** 仅根据当前展示的航线(activeRouteIds)更新平台图标位置,并汇总航段提示 */ @@ -1762,9 +1819,9 @@ export default { 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); + const { position, nextPosition, previousPosition, warnings } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes); if (warnings && warnings.length) allWarnings.push(...warnings); - if (position) this.$refs.cesiumMap.updatePlatformPosition(routeId, position); + if (position) this.$refs.cesiumMap.updatePlatformPosition(routeId, position, nextPosition || previousPosition); }); this.deductionWarnings = [...new Set(allWarnings)]; },