From 631def63d0e611c298685153bb7a6d77cdeabe8a Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Fri, 27 Feb 2026 13:35:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=BC=B9=E5=8F=91=E5=B0=84=EF=BC=8C?= =?UTF-8?q?=E5=AF=BC=E5=BC=B9=E6=95=B0=E6=8D=AE=E5=AD=98redis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/web/controller/RoutesController.java | 58 +++ ruoyi-ui/src/api/system/routes.js | 18 + ruoyi-ui/src/views/cesiumMap/ContextMenu.vue | 8 + ruoyi-ui/src/views/cesiumMap/index.vue | 419 ++++++++++++++++++++- ruoyi-ui/src/views/childRoom/index.vue | 23 ++ 5 files changed, 525 insertions(+), 1 deletion(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java index f9c29ef..cfaccec 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java @@ -22,8 +22,12 @@ import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.system.domain.dto.PlatformStyleDTO; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import org.springframework.data.redis.core.RedisTemplate; +import java.util.ArrayList; + /** * 实体部署与航线Controller * @@ -74,6 +78,60 @@ public class RoutesController extends BaseController } /** + * 获取导弹发射参数列表(Redis,房间+航线+平台为 key,值为数组,每项含 angle/distance/launchTimeMinutesFromK/startLng/startLat/platformHeadingDeg) + */ + @PreAuthorize("@ss.hasPermi('system:routes:query')") + @GetMapping("/missile-params") + public AjaxResult getMissileParams(Long roomId, Long routeId, Long platformId) + { + if (roomId == null || routeId == null || platformId == null) { + return AjaxResult.error("参数不完整"); + } + String key = "missile:params:" + roomId + ":" + routeId + ":" + platformId; + Object val = redisTemplate.opsForValue().get(key); + if (val == null) { + return success(new ArrayList<>()); + } + String raw = val.toString(); + if (raw.startsWith("[")) { + return success(JSON.parseArray(raw)); + } + JSONObject single = JSON.parseObject(raw); + List list = new ArrayList<>(); + list.add(single); + return success(list); + } + + /** + * 保存导弹发射参数到 Redis(追加一条记录,支持多次发射;每条含 angle/distance/launchTimeMinutesFromK/startLng/startLat/platformHeadingDeg) + */ + @PreAuthorize("@ss.hasPermi('system:routes:edit')") + @PostMapping("/missile-params") + public AjaxResult saveMissileParams(@RequestBody java.util.Map body) + { + Object roomId = body.get("roomId"); + Object routeId = body.get("routeId"); + Object platformId = body.get("platformId"); + if (roomId == null || routeId == null || platformId == null) { + return AjaxResult.error("参数不完整"); + } + String key = "missile:params:" + roomId + ":" + routeId + ":" + platformId; + Object val = redisTemplate.opsForValue().get(key); + JSONArray list = new JSONArray(); + if (val != null) { + String raw = val.toString(); + if (raw.startsWith("[")) { + list = JSON.parseArray(raw); + } else { + list.add(JSON.parseObject(raw)); + } + } + list.add(body); + redisTemplate.opsForValue().set(key, list.toJSONString()); + return success(); + } + + /** * 查询实体部署与航线列表 */ @PreAuthorize("@ss.hasPermi('system:routes:list')") diff --git a/ruoyi-ui/src/api/system/routes.js b/ruoyi-ui/src/api/system/routes.js index cf6ad48..58c7677 100644 --- a/ruoyi-ui/src/api/system/routes.js +++ b/ruoyi-ui/src/api/system/routes.js @@ -60,3 +60,21 @@ export function getPlatformStyle(query) { params: query }) } + +// 获取导弹发射参数(Redis:房间+航线+平台为 key) +export function getMissileParams(params) { + return request({ + url: '/system/routes/missile-params', + method: 'get', + params + }) +} + +// 保存导弹发射参数到 Redis +export function saveMissileParams(data) { + return request({ + url: '/system/routes/missile-params', + method: 'post', + data + }) +} diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue index 2c8c2cf..f1ae068 100644 --- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue +++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue @@ -35,6 +35,10 @@ 威力区 + @@ -441,6 +445,10 @@ export default { this.$emit('power-zone') }, + handleLaunchMissile() { + this.$emit('launch-missile') + }, + toggleColorPicker(property) { if (this.showColorPickerFor === property) { this.showColorPickerFor = null diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 4fddb47..f76be1a 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -45,6 +45,7 @@ @copy-route="handleCopyRouteFromMenu" @edit-platform="openEditPlatformDialog" @power-zone="openPowerZoneDialog" + @launch-missile="openLaunchMissileDialog" /> @@ -142,6 +143,43 @@ + + + + + + + + + + + + 取 消 + 确 定 + + +
@@ -169,7 +207,7 @@ import RadiusDialog from '../dialogs/RadiusDialog.vue' import axios from 'axios' import request from '@/utils/request' import { getToken } from '@/utils/auth' -import { savePlatformStyle, getRoutes, getPlatformStyle } from '@/api/system/routes' +import { savePlatformStyle, getRoutes, getPlatformStyle, getMissileParams, saveMissileParams } from '@/api/system/routes' export default { name: 'CesiumMap', @@ -207,6 +245,16 @@ export default { routeLocked: { type: Object, default: () => ({}) + }, + /** 推演时间轴:相对 K 的分钟数(用于导弹按时间轴显示/隐藏与位置插值) */ + deductionTimeMinutes: { + type: Number, + default: 0 + }, + /** 当前房间 ID(用于 Redis 存储导弹参数 key:房间+航线+平台) */ + roomId: { + type: [String, Number], + default: null } }, watch: { @@ -231,6 +279,21 @@ export default { handler(newVal) { this.updateCoordinatesDisplay() } + }, + deductionTimeMinutes: { + immediate: true, + handler(val) { + this.currentDeductionMinutes = (val != null && Number.isFinite(Number(val))) ? Number(val) : 0 + this.updateMissilesByTime() + } + }, + missileDialogVisible(val) { + if (!val) { + this.clearMissilePreview() + if (this._missileDialogDragCleanup) { + this._missileDialogDragCleanup() + } + } } }, data() { @@ -288,6 +351,19 @@ export default { radius: 1000, color: 'rgba(255, 0, 0, 0.3)' }, + missileDialogVisible: false, + missileForm: { + angle: 0, + distance: 100, + platformHeadingDeg: 0 + }, + /** 导弹预览:弹窗打开时显示的轨迹线与朝向图标,关闭时移除 */ + missilePreviewLine: null, + missilePreviewIcon: null, + /** 导弹实体列表:每项 { entity, launchKMinutes, endKMinutes, startPoint, endPoint },用于按推演时间轴显示/隐藏与插值位置 */ + missileEntities: [], + /** 当前推演时间(相对K的分钟数),从 prop 同步,供导弹 CallbackProperty 读取确保拿到最新值 */ + currentDeductionMinutes: 0, // 航线飞机标牌显示状态:routeId -> true 显示 / false 隐藏,不设则默认显示 routeLabelVisible: {}, // 航线飞机标牌样式:routeId -> { fontSize, fontColor } @@ -2434,6 +2510,334 @@ export default { } } }, + openLaunchMissileDialog() { + this.contextMenu.visible = false + const entityData = this.contextMenu.entityData + if (entityData && entityData.entity) { + if (entityData.lat == null || entityData.lng == null) { + const pos = entityData.entity.position && entityData.entity.position.getValue + ? entityData.entity.position.getValue(Cesium.JulianDate.now()) + : null + if (pos) { + const ll = this.cartesianToLatLng(pos) + if (ll) { + entityData.lat = ll.lat + entityData.lng = ll.lng + } + } + } + const rot = entityData.entity.billboard && entityData.entity.billboard.rotation + const rotVal = rot && rot.getValue ? rot.getValue(Cesium.JulianDate.now()) : rot + if (rotVal != null && Number.isFinite(Number(rotVal))) { + const headingRad = Math.PI / 2 - Number(rotVal) + this.missileForm.platformHeadingDeg = ((headingRad * 180 / Math.PI) % 360 + 360) % 360 + } else { + this.missileForm.platformHeadingDeg = 0 + } + } else { + this.missileForm.platformHeadingDeg = 0 + } + this.missileDialogVisible = true + this.$nextTick(() => { + this.updateMissilePreview() + this.fetchMissileParamsFromRedis() + }) + }, + + /** 从 Redis 拉取当前房间+航线+平台的导弹参数并回填表单(支持返回数组时取最后一条作为默认) */ + fetchMissileParamsFromRedis() { + if (!this.roomId || !this.contextMenu.entityData || this.contextMenu.entityData.type !== 'routePlatform') return + const routeId = this.contextMenu.entityData.routeId + const platformId = this.contextMenu.entityData.platformId != null ? this.contextMenu.entityData.platformId : 0 + getMissileParams({ roomId: this.roomId, routeId, platformId }).then(res => { + let data = res.data + if (Array.isArray(data) && data.length > 0) data = data[data.length - 1] + if (data && (data.angle != null || data.distance != null)) { + if (data.angle != null) this.missileForm.angle = Number(data.angle) + if (data.distance != null) this.missileForm.distance = Number(data.distance) + this.updateMissilePreview() + } + }).catch(() => {}) + }, + + /** 导弹参数弹窗打开后绑定标题栏拖动 */ + onMissileDialogOpen() { + this.$nextTick(() => { + const dialog = document.querySelector('.missile-params-dialog') + const wrapper = dialog && dialog.closest('.el-dialog__wrapper') + if (!wrapper || !dialog) return + const header = wrapper.querySelector('.el-dialog__header') + if (!header) return + header.style.cursor = 'move' + let startX, startY, startLeft, startTop + const onMouseDown = (e) => { + if (e.button !== 0) return + startX = e.clientX + startY = e.clientY + const rect = wrapper.getBoundingClientRect() + startLeft = rect.left + startTop = rect.top + wrapper.style.position = 'fixed' + wrapper.style.left = startLeft + 'px' + wrapper.style.top = startTop + 'px' + wrapper.style.margin = '0' + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + } + const onMouseMove = (e) => { + const dx = e.clientX - startX + const dy = e.clientY - startY + wrapper.style.left = (startLeft + dx) + 'px' + wrapper.style.top = (startTop + dy) + 'px' + startLeft += dx + startTop += dy + startX = e.clientX + startY = e.clientY + } + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + } + header.addEventListener('mousedown', onMouseDown) + this._missileDialogDragCleanup = () => { + header.removeEventListener('mousedown', onMouseDown) + header.style.cursor = '' + delete this._missileDialogDragCleanup + } + }) + }, + + /** 清除导弹预览(轨迹线 + 朝向图标) */ + clearMissilePreview() { + if (this.viewer && this.viewer.entities) { + if (this.missilePreviewLine) { + this.viewer.entities.remove(this.missilePreviewLine) + this.missilePreviewLine = null + } + if (this.missilePreviewIcon) { + this.viewer.entities.remove(this.missilePreviewIcon) + this.missilePreviewIcon = null + } + if (this.viewer.scene) this.viewer.scene.requestRender() + } + }, + + /** 根据当前角度、距离与平台位置/航向更新导弹预览(弹窗打开时显示) */ + updateMissilePreview() { + if (!this.missileDialogVisible || !this.viewer || !this.viewer.entities) { + this.clearMissilePreview() + return + } + const entityData = this.contextMenu.entityData + if (!entityData || entityData.lat == null || entityData.lng == null) { + this.clearMissilePreview() + return + } + const platformHeadingDeg = Number(this.missileForm.platformHeadingDeg) || 0 + const angle = Number(this.missileForm.angle) || 0 + const distance = Number(this.missileForm.distance) || 100 + const actualBearingDeg = ((platformHeadingDeg + angle) % 360 + 360) % 360 + const brng = Cesium.Math.toRadians(actualBearingDeg) + const R = 6371000 + const d = distance * 1000 + const lat1 = Cesium.Math.toRadians(Number(entityData.lat)) + const lon1 = Cesium.Math.toRadians(Number(entityData.lng)) + const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d / R) + Math.cos(lat1) * Math.sin(d / R) * Math.cos(brng)) + const lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d / R) * Math.cos(lat1), Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2)) + const startPoint = Cesium.Cartesian3.fromDegrees(Number(entityData.lng), Number(entityData.lat), 1000) + const endPoint = Cesium.Cartesian3.fromDegrees(Cesium.Math.toDegrees(lon2), Cesium.Math.toDegrees(lat2), 1000) + this.clearMissilePreview() + this.missilePreviewLine = this.viewer.entities.add({ + polyline: { + positions: [startPoint, endPoint], + width: 3, + material: Cesium.Color.PURPLE.withAlpha(0.8) + }, + show: true + }) + this.missilePreviewIcon = this.viewer.entities.add({ + position: startPoint, + billboard: { + image: require('@/assets/images/missile.png'), + width: 28, + height: 28, + rotation: -brng + }, + show: true + }) + if (this.viewer.scene) this.viewer.scene.requestRender() + }, + + launchMissile() { + this.missileDialogVisible = false + this.clearMissilePreview() + const angle = this.missileForm.angle + const distance = this.missileForm.distance + const entityData = this.contextMenu.entityData + + if (!entityData) { + this.$message.error('无法获取发射平台信息') + return + } + if (entityData.lat == null || entityData.lng == null) { + if (entityData.entity && entityData.entity.position) { + const pos = entityData.entity.position.getValue(Cesium.JulianDate.now()) + if (pos) { + const ll = this.cartesianToLatLng(pos) + if (ll) { + entityData.lat = ll.lat + entityData.lng = ll.lng + } + } + } + } + if (entityData.lat == null || entityData.lng == null) { + this.$message.error('无法获取平台位置信息') + return + } + + const launchK = Number.isFinite(Number(this.deductionTimeMinutes)) ? Number(this.deductionTimeMinutes) : 0 + if (this.roomId != null && entityData.routeId != null) { + const platformId = entityData.platformId != null ? entityData.platformId : 0 + saveMissileParams({ + roomId: this.roomId, + routeId: entityData.routeId, + platformId, + angle, + distance, + launchTimeMinutesFromK: launchK, + startLng: entityData.lng, + startLat: entityData.lat, + platformHeadingDeg: Number(this.missileForm.platformHeadingDeg) || 0 + }).catch(() => {}) + } + + const platformHeadingDeg = Number(this.missileForm.platformHeadingDeg) || 0 + const actualBearingDeg = ((platformHeadingDeg + Number(angle)) % 360 + 360) % 360 + const R = 6371000 + const d = Number(distance) * 1000 + const brng = Cesium.Math.toRadians(actualBearingDeg) + const lat1 = Cesium.Math.toRadians(Number(entityData.lat)) + const lon1 = Cesium.Math.toRadians(Number(entityData.lng)) + + const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d / R) + + Math.cos(lat1) * Math.sin(d / R) * Math.cos(brng)) + const lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d / R) * Math.cos(lat1), + Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2)) + + const endLat = Cesium.Math.toDegrees(lat2) + const endLng = Cesium.Math.toDegrees(lon2) + + const durationSec = d / 1000 + const durationMinutes = durationSec / 60 + const endK = launchK + durationMinutes + + const startPoint = Cesium.Cartesian3.fromDegrees(Number(entityData.lng), Number(entityData.lat), 1000) + const endPoint = Cesium.Cartesian3.fromDegrees(endLng, endLat, 0) + const headingRad = brng + const billboardRotation = -headingRad + + const entity = this.viewer.entities.add({ + position: startPoint, + show: false, + billboard: { + image: require('@/assets/images/missile.png'), + width: 32, + height: 32, + rotation: billboardRotation + } + }) + this.missileEntities.push({ entity, launchKMinutes: launchK, endKMinutes: endK, startPoint, endPoint }) + this.updateMissilesByTime() + }, + + /** 根据当前推演时间直接更新所有导弹的显隐和位置。可由父组件调用并传入当前分钟数 minutesFromK,保证与时间轴一致 */ + updateMissilesByTime(minutesFromK) { + if (!this.viewer || !this.viewer.entities || !this.missileEntities.length) return + const curNum = (minutesFromK !== undefined && minutesFromK !== null && Number.isFinite(Number(minutesFromK))) + ? Number(minutesFromK) + : ((this.deductionTimeMinutes != null && Number.isFinite(Number(this.deductionTimeMinutes))) + ? Number(this.deductionTimeMinutes) + : this.currentDeductionMinutes) + const cur = Number.isFinite(curNum) ? curNum : 0 + this.missileEntities.forEach(({ entity, launchKMinutes, endKMinutes, startPoint, endPoint }) => { + if (cur < launchKMinutes) { + entity.show = false + entity.position = new Cesium.ConstantPositionProperty(startPoint) + } else if (cur > endKMinutes) { + entity.show = false + entity.position = new Cesium.ConstantPositionProperty(endPoint) + } else { + entity.show = true + const span = endKMinutes - launchKMinutes + const t = span <= 0 ? 1 : Math.max(0, Math.min(1, (cur - launchKMinutes) / span)) + const pos = Cesium.Cartesian3.lerp(startPoint, endPoint, t, new Cesium.Cartesian3()) + entity.position = new Cesium.ConstantPositionProperty(pos) + } + }) + if (this.viewer.scene) this.viewer.scene.requestRender() + }, + + /** + * 从 Redis 加载指定房间+航线列表的导弹数据并创建实体(会先清空当前导弹实体) + * routePlatforms: [{ routeId, platformId }] + */ + async loadMissilesFromRedis(roomId, routePlatforms) { + if (!this.viewer || !this.viewer.entities || !roomId || !Array.isArray(routePlatforms) || routePlatforms.length === 0) return + while (this.missileEntities.length) { + const { entity } = this.missileEntities.pop() + this.viewer.entities.remove(entity) + } + for (const { routeId, platformId } of routePlatforms) { + try { + const res = await getMissileParams({ roomId, routeId, platformId: platformId != null ? platformId : 0 }) + let data = res.data + if (!data) continue + const arr = Array.isArray(data) ? data : [data] + for (const item of arr) { + const launchK = item.launchTimeMinutesFromK != null ? Number(item.launchTimeMinutesFromK) : null + const startLng = item.startLng != null ? Number(item.startLng) : null + const startLat = item.startLat != null ? Number(item.startLat) : null + if (launchK == null || startLng == null || startLat == null) continue + const angle = Number(item.angle) || 0 + const distance = Number(item.distance) || 100 + const platformHeadingDeg = Number(item.platformHeadingDeg) || 0 + const actualBearingDeg = ((platformHeadingDeg + angle) % 360 + 360) % 360 + const R = 6371000 + const d = distance * 1000 + const brng = Cesium.Math.toRadians(actualBearingDeg) + const lat1 = Cesium.Math.toRadians(startLat) + const lon1 = Cesium.Math.toRadians(startLng) + const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d / R) + Math.cos(lat1) * Math.sin(d / R) * Math.cos(brng)) + const lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d / R) * Math.cos(lat1), Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2)) + const endLng = Cesium.Math.toDegrees(lon2) + const endLat = Cesium.Math.toDegrees(lat2) + const durationSec = d / 1000 + const durationMinutes = durationSec / 60 + const endK = launchK + durationMinutes + const startPoint = Cesium.Cartesian3.fromDegrees(startLng, startLat, 1000) + const endPoint = Cesium.Cartesian3.fromDegrees(endLng, endLat, 0) + const billboardRotation = -brng + const entity = this.viewer.entities.add({ + position: startPoint, + show: false, + billboard: { + image: require('@/assets/images/missile.png'), + width: 32, + height: 32, + rotation: billboardRotation + } + }) + this.missileEntities.push({ entity, launchKMinutes: launchK, endKMinutes: endK, startPoint, endPoint }) + } + } catch (e) { + // ignore per-platform errors + } + } + this.updateMissilesByTime(this.deductionTimeMinutes != null ? this.deductionTimeMinutes : 0) + if (this.viewer.scene) this.viewer.scene.requestRender() + }, + checkCesiumLoaded() { if (typeof Cesium === 'undefined') { console.error('Cesium未加载,请检查CDN链接'); @@ -2791,6 +3195,19 @@ export default { platformName, labelVisible: this.routeLabelVisible[routeId] !== false } + + // 尝试获取位置信息,以便后续使用 + const now = Cesium.JulianDate.now(); + if (pickedEntity.position) { + const pos = pickedEntity.position.getValue(now); + if (pos) { + const ll = this.cartesianToLatLng(pos); + if (ll) { + entityData.lat = ll.lat; + entityData.lng = ll.lng; + } + } + } } if (!entityData) { // 查找对应的实体数据 diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 78d2d52..be1685b 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -14,6 +14,8 @@ :scaleConfig="scaleConfig" :coordinateFormat="coordinateFormat" :route-locked="routeLocked" + :deduction-time-minutes="deductionMinutesFromK" + :room-id="currentRoomId" @draw-complete="handleMapDrawComplete" @route-lock-changed="handleRouteLockChanged" @drawing-points-update="missionDrawingPointsCount = $event" @@ -639,6 +641,8 @@ export default { isPlaying: false, playbackSpeed: 1, playbackInterval: null, + /** 导弹从 Redis 加载过的房间+航线组合 key,避免重复加载 */ + _missilesLoadKey: null, // 用户 userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png', @@ -2220,6 +2224,25 @@ export default { // 右上角作战时间随时与推演时间轴同步(无论是否展示航线) this.combatTime = this.currentTime; this.updateDeductionPositions(); + // 有房间且已选航线时,按房间+航线组合从 Redis 加载导弹(仅变化时加载一次) + const roomId = this.currentRoomId; + const routeIds = (this.activeRouteIds || []).slice().sort((a, b) => (a - b)); + if (roomId != null && routeIds.length > 0 && this.$refs.cesiumMap && typeof this.$refs.cesiumMap.loadMissilesFromRedis === 'function') { + const loadKey = roomId + '-' + routeIds.join(','); + if (this._missilesLoadKey !== loadKey) { + this._missilesLoadKey = loadKey; + const routePlatforms = this.routes + .filter(r => routeIds.includes(r.id)) + .map(r => ({ routeId: r.id, platformId: r.platformId != null ? r.platformId : 0 })); + if (routePlatforms.length > 0) { + this.$refs.cesiumMap.loadMissilesFromRedis(roomId, routePlatforms); + } + } + } + // 时间轴变化时显式通知地图更新导弹显隐与位置(保证与时间轴一致) + if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.updateMissilesByTime === 'function') { + this.$refs.cesiumMap.updateMissilesByTime(this.deductionMinutesFromK); + } }, /** 仅针对当前展示的航线(activeRouteIds):从这些航线的航点中取推演时间范围(相对 K 的分钟数) */