Compare commits

...

3 Commits

  1. 13
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomPlatformIconController.java
  2. 85
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java
  3. 165
      ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlatformStyleDTO.java
  4. 9
      ruoyi-ui/src/api/system/routes.js
  5. 98
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  6. 122
      ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue
  7. 3976
      ruoyi-ui/src/views/cesiumMap/index.vue
  8. 9
      ruoyi-ui/src/views/childRoom/LeftMenu.vue
  9. 245
      ruoyi-ui/src/views/childRoom/index.vue

13
ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomPlatformIconController.java

@ -2,6 +2,7 @@ package com.ruoyi.web.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
@ -21,6 +22,9 @@ public class RoomPlatformIconController extends BaseController {
@Autowired
private IRoomPlatformIconService roomPlatformIconService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 按房间ID查询该房间下所有地图平台图标不分页
*/
@ -53,12 +57,19 @@ public class RoomPlatformIconController extends BaseController {
}
/**
* 删除
* 删除同步删除该实例在 Redis 中的平台样式避免残留威力区/探测区
*/
@PreAuthorize("@ss.hasPermi('system:roomPlatformIcon:remove')")
@Log(title = "房间地图平台图标", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public AjaxResult remove(@PathVariable Long id) {
RoomPlatformIcon icon = roomPlatformIconService.selectById(id);
if (icon != null && icon.getRoomId() != null) {
String key = "room:" + icon.getRoomId() + ":platformIcons:platforms";
redisTemplate.opsForHash().delete(key, String.valueOf(id));
String oldKey = "room:" + icon.getRoomId() + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, String.valueOf(id));
}
return toAjax(roomPlatformIconService.deleteById(id));
}
}

85
ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java

@ -58,34 +58,97 @@ public class RoutesController extends BaseController
/**
* 保存平台样式到 Redis
* 独立平台routeId=0若传 platformIconInstanceId 则按实例存储每个拖上去的图标一套样式否则按 platformId 存储兼容旧逻辑
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@PostMapping("/savePlatformStyle")
public AjaxResult savePlatformStyle(@RequestBody PlatformStyleDTO dto)
{
if (dto.getRoomId() == null || dto.getRouteId() == null || dto.getPlatformId() == null) {
if (dto.getRoomId() == null || dto.getRouteId() == null) {
return AjaxResult.error("参数不完整");
}
String key = "room:" + dto.getRoomId() + ":route:" + dto.getRouteId() + ":platforms";
redisTemplate.opsForHash().put(key, String.valueOf(dto.getPlatformId()), JSON.toJSONString(dto));
Long routeId = dto.getRouteId();
boolean isPlatformIcon = routeId != null && routeId == 0L;
if (isPlatformIcon) {
if (dto.getPlatformIconInstanceId() == null && dto.getPlatformId() == null) {
return AjaxResult.error("独立平台样式需传 platformIconInstanceId 或 platformId");
}
} else {
if (dto.getPlatformId() == null) {
return AjaxResult.error("参数不完整");
}
}
String key = isPlatformIcon
? ("room:" + dto.getRoomId() + ":platformIcons:platforms")
: ("room:" + dto.getRoomId() + ":route:" + routeId + ":platforms");
String hashField = isPlatformIcon && dto.getPlatformIconInstanceId() != null
? String.valueOf(dto.getPlatformIconInstanceId())
: String.valueOf(dto.getPlatformId());
redisTemplate.opsForHash().put(key, hashField, JSON.toJSONString(dto));
if (isPlatformIcon && dto.getPlatformIconInstanceId() != null) {
String oldKey = "room:" + dto.getRoomId() + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, hashField);
} else if (isPlatformIcon) {
String oldKey = "room:" + dto.getRoomId() + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, String.valueOf(dto.getPlatformId()));
}
return success();
}
/**
* Redis 获取平台样式
* 独立平台routeId=0优先用 platformIconInstanceId 取该实例样式未传时用 platformId兼容旧数据
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping("/getPlatformStyle")
public AjaxResult getPlatformStyle(PlatformStyleDTO dto)
{
if (dto.getRoomId() == null || dto.getRouteId() == null || dto.getPlatformId() == null) {
if (dto.getRoomId() == null || dto.getRouteId() == null) {
return AjaxResult.error("参数不完整");
}
String key = "room:" + dto.getRoomId() + ":route:" + dto.getRouteId() + ":platforms";
Object val = redisTemplate.opsForHash().get(key, String.valueOf(dto.getPlatformId()));
if (val != null) {
return success(JSON.parseObject(val.toString(), PlatformStyleDTO.class));
Long routeId = dto.getRouteId();
boolean isPlatformIcon = routeId != null && routeId == 0L;
if (isPlatformIcon) {
if (dto.getPlatformIconInstanceId() == null && dto.getPlatformId() == null) {
return AjaxResult.error("独立平台样式需传 platformIconInstanceId 或 platformId");
}
} else {
if (dto.getPlatformId() == null) {
return AjaxResult.error("参数不完整");
}
}
String key = isPlatformIcon
? ("room:" + dto.getRoomId() + ":platformIcons:platforms")
: ("room:" + dto.getRoomId() + ":route:" + routeId + ":platforms");
String hashField = isPlatformIcon && dto.getPlatformIconInstanceId() != null
? String.valueOf(dto.getPlatformIconInstanceId())
: String.valueOf(dto.getPlatformId());
Object val = redisTemplate.opsForHash().get(key, hashField);
if (val == null && isPlatformIcon) {
String oldKey = "room:" + dto.getRoomId() + ":route:0:platforms";
val = redisTemplate.opsForHash().get(oldKey, hashField);
}
if (val != null) return success(JSON.parseObject(val.toString(), PlatformStyleDTO.class));
return success();
}
/**
* 删除独立平台图标样式按实例删除避免删除图标后仍残留威力区/探测区
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@DeleteMapping("/platformIconStyle")
public AjaxResult deletePlatformIconStyle(@RequestParam Long roomId, @RequestParam Long platformIconInstanceId)
{
if (roomId == null || platformIconInstanceId == null) {
return AjaxResult.error("roomId 与 platformIconInstanceId 不能为空");
}
String key = "room:" + roomId + ":platformIcons:platforms";
redisTemplate.opsForHash().delete(key, String.valueOf(platformIconInstanceId));
String oldKey = "room:" + roomId + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, String.valueOf(platformIconInstanceId));
return success();
}
@ -440,10 +503,16 @@ public class RoutesController extends BaseController
int rows = routesService.deleteRoutesByIds(ids);
if (rows > 0) {
for (Long routeId : ids) {
// 清除该航线在所有房间下的导弹参数
Set<String> keys = redisTemplate.keys("missile:params:*:" + routeId + ":*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
// 同步删除该航线在所有房间下的平台样式(探测区/威力区等),避免库表已删但 Redis 仍残留
Set<String> styleKeys = redisTemplate.keys("room:*:route:" + routeId + ":platforms");
if (styleKeys != null && !styleKeys.isEmpty()) {
redisTemplate.delete(styleKeys);
}
}
}
return toAjax(rows);

165
ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlatformStyleDTO.java

@ -1,6 +1,7 @@
package com.ruoyi.system.domain.dto;
import java.io.Serializable;
import java.util.List;
/**
* 平台样式 DTO
@ -11,7 +12,10 @@ public class PlatformStyleDTO implements Serializable {
private String roomId;
private Long routeId;
private Long platformId;
/** 独立平台图标实例 ID(拖到地图上的每条记录的 id,非平台类型 id)。routeId=0 时用此字段作 Redis hash field,实现每个实例独立样式 */
private Long platformIconInstanceId;
/** 平台名称 */
private String platformName;
@ -27,6 +31,12 @@ public class PlatformStyleDTO implements Serializable {
/** 平台颜色 */
private String platformColor;
/** 多探测区配置(支持叠加与独立显隐) */
private List<DetectionZoneDTO> detectionZones;
/** 多威力区配置(支持叠加与独立显隐) */
private List<PowerZoneDTO> powerZones;
/** 探测区半径(千米),整圆 */
private Double detectionZoneRadius;
/** 探测区填充颜色 */
@ -47,6 +57,151 @@ public class PlatformStyleDTO implements Serializable {
/** 威力区是否在地图上显示 */
private Boolean powerZoneVisible;
public static class DetectionZoneDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** zone 唯一 id(前端生成,后端原样保存) */
private String zoneId;
/** 探测区半径(千米) */
private Double radiusKm;
/** 探测区填充颜色(css rgba 或 hex) */
private String color;
/** 探测区透明度 0-1 */
private Double opacity;
/** 是否在地图上显示 */
private Boolean visible;
public String getZoneId() {
return zoneId;
}
public void setZoneId(String zoneId) {
this.zoneId = zoneId;
}
public Double getRadiusKm() {
return radiusKm;
}
public void setRadiusKm(Double radiusKm) {
this.radiusKm = radiusKm;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Double getOpacity() {
return opacity;
}
public void setOpacity(Double opacity) {
this.opacity = opacity;
}
public Boolean getVisible() {
return visible;
}
public void setVisible(Boolean visible) {
this.visible = visible;
}
}
public static class PowerZoneDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** zone 唯一 id(前端生成,后端原样保存) */
private String zoneId;
/** 威力区半径(千米) */
private Double radiusKm;
/** 威力区扇形夹角(度) */
private Double angleDeg;
/** 威力区填充颜色(css rgba 或 hex) */
private String color;
/** 威力区透明度 0-1 */
private Double opacity;
/** 是否在地图上显示 */
private Boolean visible;
public String getZoneId() {
return zoneId;
}
public void setZoneId(String zoneId) {
this.zoneId = zoneId;
}
public Double getRadiusKm() {
return radiusKm;
}
public void setRadiusKm(Double radiusKm) {
this.radiusKm = radiusKm;
}
public Double getAngleDeg() {
return angleDeg;
}
public void setAngleDeg(Double angleDeg) {
this.angleDeg = angleDeg;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Double getOpacity() {
return opacity;
}
public void setOpacity(Double opacity) {
this.opacity = opacity;
}
public Boolean getVisible() {
return visible;
}
public void setVisible(Boolean visible) {
this.visible = visible;
}
}
public List<DetectionZoneDTO> getDetectionZones() {
return detectionZones;
}
public void setDetectionZones(List<DetectionZoneDTO> detectionZones) {
this.detectionZones = detectionZones;
}
public List<PowerZoneDTO> getPowerZones() {
return powerZones;
}
public void setPowerZones(List<PowerZoneDTO> powerZones) {
this.powerZones = powerZones;
}
public String getRoomId() {
return roomId;
}
@ -71,6 +226,14 @@ public class PlatformStyleDTO implements Serializable {
this.platformId = platformId;
}
public Long getPlatformIconInstanceId() {
return platformIconInstanceId;
}
public void setPlatformIconInstanceId(Long platformIconInstanceId) {
this.platformIconInstanceId = platformIconInstanceId;
}
public String getPlatformName() {
return platformName;
}

9
ruoyi-ui/src/api/system/routes.js

@ -65,6 +65,15 @@ export function getPlatformStyle(query) {
})
}
// 删除独立平台图标样式(按实例删除,避免删除图标后残留威力区/探测区)
export function deletePlatformIconStyle(roomId, platformIconInstanceId) {
return request({
url: '/system/routes/platformIconStyle',
method: 'delete',
params: { roomId, platformIconInstanceId }
})
}
// 保存4T数据到Redis(禁用防重复提交,因拖拽/调整大小可能快速连续触发保存)
export function save4TData(data) {
return request({

98
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue

@ -7,7 +7,17 @@
<span>切换选择 ({{ (pickIndex || 0) + 1 }}/{{ pickList.length }})</span>
</div>
</div>
<div class="menu-section" v-if="!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')">
<!-- 框选多平台仅批量删除 -->
<div class="menu-section" v-if="entityData && entityData.type === 'platformBoxSelection'">
<div class="menu-title">框选平台{{ entityData.count }} </div>
<div class="menu-item" @click="handleDeleteBoxSelection">
<span class="menu-icon">🗑</span>
<span>删除全部框选平台</span>
</div>
</div>
<div class="menu-section" v-if="(!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')) && (!entityData || entityData.type !== 'platformBoxSelection')">
<div class="menu-item" @click="handleDelete">
<span class="menu-icon">🗑</span>
<span>删除</span>
@ -101,6 +111,14 @@
<span class="menu-icon">📋</span>
<span>复制</span>
</div>
<div v-if="!isRouteLocked" class="menu-item" @click="handleRouteSegmentSplit">
<span class="menu-icon"></span>
<span>拆分航段</span>
</div>
<div v-if="!isRouteLocked" class="menu-item" @click="handleRouteSegmentCopy">
<span class="menu-icon">📄</span>
<span>拆分复制</span>
</div>
<div class="menu-item" @click="handleSingleRouteDeduction">
<span class="menu-icon"></span>
<span>单条航线推演</span>
@ -194,6 +212,21 @@
<span>磁方位</span>
</div>
</div>
<template v-if="toolMode === 'ranging'">
<div class="menu-item" @click="toggleRangingUnitMenu">
<span class="menu-icon">📏</span>
<span>距离单位</span>
<span class="menu-value">{{ rangingDistanceUnit === 'nm' ? '海里' : '公里' }}</span>
</div>
<div class="sub-menu" v-if="showRangingUnitMenu">
<div class="sub-menu-item" @click="selectRangingUnit('km')" :class="{ active: rangingDistanceUnit === 'km' }">
<span>公里km</span>
</div>
<div class="sub-menu-item" @click="selectRangingUnit('nm')" :class="{ active: rangingDistanceUnit === 'nm' }">
<span>海里1 海里 = 1852 </span>
</div>
</div>
</template>
</div>
<!-- 点特有选项 -->
@ -504,6 +537,24 @@
</div>
</div>
<!-- 探测区单个区 -->
<div class="menu-section" v-if="entityData && entityData.type === 'detectionZone'">
<div class="menu-title">探测区</div>
<div class="menu-item menu-item-sub" @click="handleToggleDetectionZone">
<span class="menu-icon">{{ detectionZoneVisible ? '👁' : '👁‍🗨' }}</span>
<span>{{ detectionZoneVisible ? '隐藏该探测区' : '显示该探测区' }}</span>
</div>
</div>
<!-- 威力区单个区 -->
<div class="menu-section" v-if="entityData && entityData.type === 'powerZone'">
<div class="menu-title">威力区</div>
<div class="menu-item menu-item-sub" @click="handleTogglePowerZone">
<span class="menu-icon">{{ powerZoneVisible ? '👁' : '👁‍🗨' }}</span>
<span>{{ powerZoneVisible ? '隐藏该威力区' : '显示该威力区' }}</span>
</div>
</div>
<!-- 白板平台仅显示伸缩框用于旋转 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon' && entityData.isWhiteboard">
<div class="menu-title">白板平台</div>
@ -550,6 +601,14 @@ export default {
powerZoneVisible: {
type: Boolean,
default: true
},
toolMode: {
type: String,
default: 'airspace'
},
rangingDistanceUnit: {
type: String,
default: 'km'
}
},
data() {
@ -565,6 +624,7 @@ export default {
showOpacityPicker: false,
showFontSizePicker: false,
showBearingTypeMenu: false,
showRangingUnitMenu: false,
presetColors: [
'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
'#FF6600', '#663399', '#999999', '#000000', '#FFFFFF', '#FF99CC',
@ -614,6 +674,10 @@ export default {
this.$emit('delete')
},
handleDeleteBoxSelection() {
this.$emit('delete-box-selected-platforms')
},
handleAdjustPosition() {
this.$emit('adjust-airspace-position')
},
@ -672,6 +736,14 @@ export default {
this.$emit('copy-route')
},
handleRouteSegmentSplit() {
this.$emit('route-segment-split')
},
handleRouteSegmentCopy() {
this.$emit('route-segment-copy')
},
handleSingleRouteDeduction() {
this.$emit('single-route-deduction', this.entityData.routeId)
},
@ -764,6 +836,8 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showBearingTypeMenu = false
this.showRangingUnitMenu = false
this.showColorPickerFor = property
}
},
@ -782,6 +856,8 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showBearingTypeMenu = false
this.showRangingUnitMenu = false
this.showWidthPicker = true
}
},
@ -876,6 +952,7 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showRangingUnitMenu = false
}
},
@ -883,6 +960,25 @@ export default {
//
this.$emit('update-property', 'bearingType', bearingType)
this.showBearingTypeMenu = false
},
toggleRangingUnitMenu() {
this.showRangingUnitMenu = !this.showRangingUnitMenu
if (this.showRangingUnitMenu) {
this.showColorPickerFor = null
this.showWidthPicker = false
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showBearingTypeMenu = false
}
},
selectRangingUnit(unit) {
if (unit === 'km' || unit === 'nm') {
this.$emit('ranging-distance-unit', unit)
}
this.showRangingUnitMenu = false
}
}
}

122
ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue

@ -0,0 +1,122 @@
<template>
<div class="map-screen-dom-labels-layer">
<div
v-for="item in items"
v-show="item.visible"
:key="item.id"
class="map-screen-dom-label"
:class="item.themeClass"
:style="item.wrapperStyle"
>
<template v-if="item.kind === 'platform'">
<div
class="map-screen-dom-label__platform-name"
:style="{ fontSize: item.titleSizePx + 'px' }"
>
{{ item.name }}
</div>
<div
class="map-screen-dom-label__platform-stats"
:style="{ fontSize: item.statSizePx + 'px' }"
>
h: {{ item.altitude }}m &nbsp; v: {{ item.speed }}km/h &nbsp; s: {{ item.heading }}°
</div>
</template>
<template v-else-if="item.kind === 'waypoint' || item.kind === 'mapText'">
<span class="map-screen-dom-label__transparent-text" :style="item.transparentTextStyle">{{ item.text }}</span>
</template>
<template v-else>
<span
class="map-screen-dom-label__plain"
:style="{ whiteSpace: item.multiline ? 'pre-line' : 'nowrap' }"
>{{ item.text }}</span>
</template>
</div>
</div>
</template>
<script>
export default {
name: 'MapScreenDomLabels',
props: {
items: {
type: Array,
default: () => []
}
}
}
</script>
<style scoped>
.map-screen-dom-labels-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9998;
overflow: hidden;
}
.map-screen-dom-label {
position: absolute;
pointer-events: none;
white-space: nowrap;
}
/* 与 HoverTooltip 一致的深色提示条 */
.map-screen-dom-label--tooltip {
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
line-height: 1.35;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
/* 飞机平台标牌:白底黑字、机名蓝色,尺寸紧凑 */
.map-screen-dom-label--platform-card {
background: rgba(255, 255, 255, 0.96);
color: #1a1a1a;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
line-height: 1.25;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
.map-screen-dom-label--platform {
min-width: 0;
text-align: center;
}
.map-screen-dom-label__platform-name {
font-weight: 600;
line-height: 1.2;
margin-bottom: 2px;
color: #0078ff;
}
.map-screen-dom-label__platform-stats {
font-weight: 400;
color: #1a1a1a;
}
/* 航点 / 插入文字 / 空域威力区命名:透明底,描边式字 */
.map-screen-dom-label--maptext {
background: transparent;
padding: 0 2px;
border-radius: 0;
box-shadow: none;
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
line-height: 1.2;
}
.map-screen-dom-label__transparent-text {
display: inline-block;
}
</style>

3976
ruoyi-ui/src/views/cesiumMap/index.vue

File diff suppressed because it is too large

9
ruoyi-ui/src/views/childRoom/LeftMenu.vue

@ -29,7 +29,7 @@
v-for="item in localMenuItems"
:key="item.id"
class="menu-item"
:class="{ active: activeMenu === item.id, 'dragging': isDragging, 'edit-mode': isEditMode }"
:class="{ active: isItemActive(item), 'dragging': isDragging, 'edit-mode': isEditMode }"
@click="handleSelectMenu(item)"
@contextmenu.prevent="handleRightClick(item)"
:title="item.name"
@ -143,6 +143,13 @@ export default {
return icon && typeof icon === 'string' && !icon.startsWith('el-icon-')
},
/** 框选等项通过图标编辑添加时 id 为随机字符串,activeMenu 固定为 platformBoxSelect,需按 action 匹配高亮 */
isItemActive(item) {
if (this.activeMenu === item.id) return true
if (this.activeMenu === 'platformBoxSelect' && item.action === 'platformBoxSelect') return true
return false
},
handleHide() {
this.$emit('hide')
},

245
ruoyi-ui/src/views/childRoom/index.vue

@ -22,6 +22,8 @@
:route-locked-by-other-ids="routeLockedByOtherRouteIds"
:deduction-time-minutes="deductionMinutesFromK"
:room-id="currentRoomId"
:platform-box-select-mode="platformBoxSelectMode"
@request-exit-platform-box-select="onRequestExitPlatformBoxSelect"
@draw-complete="handleMapDrawComplete"
@route-lock-changed="handleRouteLockChanged"
@drawing-points-update="missionDrawingPointsCount = $event"
@ -30,6 +32,8 @@
@open-waypoint-dialog="handleOpenWaypointEdit"
@open-route-dialog="handleOpenRouteEdit"
@copy-route="handleCopyRoute"
@route-segment-pick="handleRouteSegmentPick"
@route-segment-placed="handleRouteSegmentPlaced"
@single-route-deduction="handleSingleRouteDeduction"
@route-copy-placed="handleRouteCopyPlaced"
@add-waypoint-at="handleAddWaypointAt"
@ -39,6 +43,7 @@
@missile-deleted="handleMissileDeleted"
@scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated"
@platform-icons-batch-updated="onPlatformIconsBatchUpdated"
@platform-icon-removed="onPlatformIconRemoved"
@viewer-ready="onViewerReady"
@drawing-entities-changed="onDrawingEntitiesChanged"
@ -68,7 +73,7 @@
<div class="red-dot"></div>
<i class="el-icon-s-unfold icon-inside"></i>
</div>
<el-dialog title="保存新航线" :visible.sync="showNameDialog" width="30%" :append-to-body="true" @open="onSaveRouteDialogOpen" @close="tempMapPlatform = null; saveDialogScenarioId = null">
<el-dialog title="保存新航线" :visible.sync="showNameDialog" width="30%" :append-to-body="true" @open="onSaveRouteDialogOpen" @close="onSaveNewRouteDialogClose">
<el-form label-width="80px">
<el-form-item label="航线名称">
<el-input v-model="newRouteName" placeholder="例如:航线一"></el-input>
@ -559,6 +564,8 @@ export default {
return {
drawDom:false,
airspaceDrawDom:false,
/** 左侧菜单「框选平台」:多选房间平台图标并整体平移 */
platformBoxSelectMode: false,
/** 是否允许地图拖动(由顶部小手图标切换,默认关闭) */
mapDragEnabled: false,
// 线
@ -589,6 +596,8 @@ export default {
showNameDialog: false,
newRouteName: '',
tempMapPoints: [],
/** 拆分航段:保存新航线成功后从原航线删除这些航点 id */
pendingSegmentSplitAfterNewRoute: null,
/** 从平台右键「在此之前/在此之后插入航线」完成时传入,保存航线时用作 platformId */
tempMapPlatform: null,
/** 保存新航线弹窗内选择的方案 ID(弹窗内可直接选方案,无需先展开侧边) */
@ -643,6 +652,7 @@ export default {
{ id: '4t', name: '4T', icon: 'T' },
{ id: 'start', name: '冲突', icon: 'chongtu' },
{ id: 'insert', name: '平台', icon: 'el-icon-s-platform' },
{ id: 'platformBoxSelect', name: '框选平台', icon: 'el-icon-crop', action: 'platformBoxSelect' },
{ id: 'pattern', name: '空域', icon: 'ky' },
{ id: 'deduction', name: '推演', icon: 'el-icon-video-play' },
{ id: 'modify', name: '测距', icon: 'cj' },
@ -670,6 +680,7 @@ export default {
},
// -
topNavItems: [
{ id: 'platformBoxSelect', name: '框选平台', icon: 'el-icon-crop' },
{ id: 'routeEdit', name: '航线编辑', icon: 'el-icon-edit-outline' },
{ id: 'militaryMarking', name: '军事标绘', icon: 'el-icon-crop' },
{ id: 'attributeEdit', name: '属性修改', icon: 'el-icon-setting' },
@ -1584,11 +1595,58 @@ export default {
/** 复制航线已放置:用当前偏移后的航点打开「保存新航线」弹窗 */
handleRouteCopyPlaced(points) {
this.pendingSegmentSplitAfterNewRoute = null;
this.tempMapPoints = points || [];
this.tempMapPlatform = null;
this.showNameDialog = true;
},
/** 右键拆分航段/拆分复制:拉取航点后进入地图两点选范围 */
async handleRouteSegmentPick({ routeId, mode }) {
if (routeId == null) return;
if (this.routeLocked[routeId]) {
this.$message.info('该航线已上锁,请先解锁后再操作');
return;
}
if (this.isRouteLockedByOther(routeId)) {
this.$message.warning('该航线正被其他成员编辑,请稍后再试');
return;
}
try {
const res = await getRoutes(routeId);
if (res.code !== 200 || !res.data) {
this.$message.error('获取航线数据失败');
return;
}
const waypoints = res.data.waypoints || [];
if (waypoints.length < 2) {
this.$message.warning('航线航点不足,无法选取航段');
return;
}
if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.startRouteSegmentPickMode === 'function') {
const routeStyle = this.parseRouteStyle(res.data.attributes);
this.$refs.cesiumMap.startRouteSegmentPickMode(routeId, waypoints, mode, routeStyle);
}
} catch (e) {
this.$message.error('获取航线数据失败');
console.error(e);
}
},
/** 航段预览已放置:打开保存弹窗;拆分航段时记下待从原航线删除的航点 id */
handleRouteSegmentPlaced(payload) {
const { mode, sourceRouteId, removedWaypointIds, points } = payload || {};
if (!points || points.length < 2) return;
this.tempMapPoints = points;
this.tempMapPlatform = null;
if (mode === 'split' && sourceRouteId != null && removedWaypointIds && removedWaypointIds.length) {
this.pendingSegmentSplitAfterNewRoute = { sourceRouteId, waypointIds: removedWaypointIds };
} else {
this.pendingSegmentSplitAfterNewRoute = null;
}
this.showNameDialog = true;
},
/** 地图上拖拽航点结束:将新位置写回数据库并刷新显示 */
async handleWaypointPositionChanged({ dbId, routeId, lat, lng, alt }) {
if (this.isRouteLockedByOther(routeId)) {
@ -2114,12 +2172,19 @@ export default {
const styleRes = await getPlatformStyle({ roomId: rId, routeId, platformId: route.platformId });
if (styleRes.data && this.$refs.cesiumMap) {
this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
if (styleRes.data.detectionZoneVisible !== false && styleRes.data.detectionZoneRadius != null && Number(styleRes.data.detectionZoneRadius) > 0) {
this.$refs.cesiumMap.ensureDetectionZoneForRoute(routeId, styleRes.data.detectionZoneRadius, styleRes.data.detectionZoneColor || 'rgba(0, 150, 255, 0.35)', styleRes.data.detectionZoneOpacity);
}
if (styleRes.data.powerZoneVisible !== false && styleRes.data.powerZoneRadius != null && Number(styleRes.data.powerZoneRadius) > 0) {
this.$refs.cesiumMap.ensurePowerZoneForRoute(routeId, styleRes.data.powerZoneRadius, styleRes.data.powerZoneAngle ?? 120, styleRes.data.powerZoneColor || 'rgba(255, 0, 0, 0.3)', styleRes.data.powerZoneOpacity);
}
const normalized = this.$refs.cesiumMap.normalizeZonesFromStyle(styleRes.data);
(normalized.detectionZones || []).forEach(dz => {
if (!dz) return;
if (dz.radiusKm == null || Number(dz.radiusKm) <= 0) return;
const opacity = dz.visible === false ? 0 : dz.opacity;
this.$refs.cesiumMap.ensureDetectionZoneForRoute(routeId, dz.zoneId, dz.radiusKm, dz.color, opacity);
});
(normalized.powerZones || []).forEach(pz => {
if (!pz) return;
if (pz.radiusKm == null || Number(pz.radiusKm) <= 0) return;
const opacity = pz.visible === false ? 0 : pz.opacity;
this.$refs.cesiumMap.ensurePowerZoneForRoute(routeId, pz.zoneId, pz.radiusKm, pz.angleDeg ?? 120, pz.color, opacity);
});
}
} catch (_) {}
}
@ -2470,6 +2535,26 @@ export default {
});
this.routes = allRoutes;
// / routeId
// activeRouteIds routes 线
const existingIdSet = new Set(allRoutes.map(r => String(r.id)));
const missingRouteIds = (this.activeRouteIds || []).filter(id => !existingIdSet.has(String(id)));
if (missingRouteIds.length > 0 && this.$refs.cesiumMap) {
missingRouteIds.forEach((routeId) => {
// removeRouteById 线//
this.$refs.cesiumMap.removeRouteById(routeId);
// /removeRouteById
this.$refs.cesiumMap.removeDetectionZoneByRouteId(routeId);
this.$refs.cesiumMap.removePowerZoneByRouteId(routeId);
});
}
this.activeRouteIds = (this.activeRouteIds || []).filter(id => existingIdSet.has(String(id)));
// 线/
if (this.selectedRouteId != null && !existingIdSet.has(String(this.selectedRouteId))) {
this.selectedRouteId = null;
this.selectedRouteDetails = null;
}
// 线 roomId
if (this.activeRouteIds.length > 0 && this.$refs.cesiumMap) {
const roomId = this.currentRoomId;
@ -2534,6 +2619,7 @@ export default {
this.drawDom = false;
return;
}
this.pendingSegmentSplitAfterNewRoute = null;
this.tempMapPoints = points;
this.tempMapPlatform = (options && (options.platformId != null || options.platform)) ? options : null;
this.showNameDialog = true;
@ -2543,6 +2629,12 @@ export default {
this.saveDialogScenarioId = this.selectedPlanId || (this.plans[0] && this.plans[0].id) || null;
},
onSaveNewRouteDialogClose() {
this.tempMapPlatform = null;
this.saveDialogScenarioId = null;
this.pendingSegmentSplitAfterNewRoute = null;
},
openAddHoldDuringDrawing() {
this.addHoldContext = { mode: 'drawing' };
this.addHoldForm = { holdType: 'hold_circle', edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: 60 };
@ -2617,6 +2709,21 @@ export default {
return;
}
const pendingSplit = this.pendingSegmentSplitAfterNewRoute;
if (pendingSplit && pendingSplit.sourceRouteId != null && Array.isArray(pendingSplit.waypointIds) && pendingSplit.waypointIds.length > 0) {
const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {};
try {
for (const wid of pendingSplit.waypointIds) {
await delWaypoints(wid, roomIdParam);
}
this.wsConnection?.sendSyncWaypoints?.(pendingSplit.sourceRouteId);
} catch (splitErr) {
console.error(splitErr);
this.$message.warning('新航线已保存,但从原航线切除所选航段失败,请手动删除原航线上的对应航点');
}
}
this.pendingSegmentSplitAfterNewRoute = null;
// 1.
this.selectedRouteId = newRouteId;
this.selectedRouteDetails = JSON.parse(JSON.stringify(savedRoute));
@ -3332,7 +3439,8 @@ export default {
'toggleRoute': () => this.toggleRoute(),
'layerFavorites': () => this.layerFavorites(),
'routeFavorites': () => this.routeFavorites(),
'refresh': () => this.captureMapScreenshot()
'refresh': () => this.captureMapScreenshot(),
'platformBoxSelect': () => this.togglePlatformBoxSelectMenu()
}
if (actionMap[actionId]) {
@ -3340,6 +3448,27 @@ export default {
}
},
/** 左侧「框选平台」:与菜单项 id 无关,统一用 action: platformBoxSelect 触发(含图标编辑添加的随机 id 项) */
togglePlatformBoxSelectMenu() {
this.platformBoxSelectMode = !this.platformBoxSelectMode
this.activeMenu = this.platformBoxSelectMode ? 'platformBoxSelect' : ''
if (!this.platformBoxSelectMode) {
if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.exitPlatformBoxSelectMode === 'function') {
this.$refs.cesiumMap.exitPlatformBoxSelectMode()
}
} else {
this.drawDom = false
this.airspaceDrawDom = false
this.isRightPanelHidden = true
}
},
/** 地图内右键退出框选(子组件 emit) */
onRequestExitPlatformBoxSelect() {
this.platformBoxSelectMode = false
this.activeMenu = ''
},
/** 截图:隐藏上下左右菜单只保留地图,用 postRender + readPixels 避免 WebGL 缓冲被清空导致黑屏 */
async captureMapScreenshot() {
const cm = this.$refs.cesiumMap
@ -4010,7 +4139,7 @@ export default {
map.setPlatformIconServerId(entityData.id, res.data.id, this.currentRoomId)
entityData.serverId = res.data.id
entityData.roomId = this.currentRoomId
this.$message.success('平台图标已保存到当前房间')
//
this.wsConnection?.sendSyncPlatformIcons?.()
}
} catch (e) {
@ -4035,6 +4164,27 @@ export default {
}).catch(() => {})
}, 500)
},
/** 框选整体拖拽结束:批量写库(避免多次 emit 被防抖合并为只保存最后一个) */
onPlatformIconsBatchUpdated(list) {
if (!list || !list.length) return
const payloads = list.filter(e => e && e.serverId)
if (!payloads.length) return
Promise.all(
payloads.map(ed =>
updateRoomPlatformIcon({
id: ed.serverId,
lng: ed.lng,
lat: ed.lat,
heading: ed.heading != null ? ed.heading : 0,
iconScale: ed.iconScale != null ? ed.iconScale : 1
})
)
)
.then(() => {
this.wsConnection?.sendSyncPlatformIcons?.()
})
.catch(() => {})
},
/** 平台图标从地图删除时同步删除服务端记录 */
onPlatformIconRemoved({ serverId }) {
if (!serverId) return
@ -4191,6 +4341,21 @@ export default {
},
selectMenu(item) {
// action handleMenuAction handleMenuAction
if (item.action === 'platformBoxSelect') {
return
}
// id action
if (item.id === 'platformBoxSelect') {
this.togglePlatformBoxSelectMenu()
return
}
if (this.platformBoxSelectMode) {
this.platformBoxSelectMode = false
if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.exitPlatformBoxSelectMode === 'function') {
this.$refs.cesiumMap.exitPlatformBoxSelectMode()
}
}
this.activeMenu = item.id;
if (item.action) {
this.handleMenuAction(item.action)
@ -5542,6 +5707,62 @@ export default {
if (cache) {
routeIdToTimeline[routeId] = cache;
routeIdsWithTimeline.push(routeId);
//
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = cache;
const routeName = route.name || `航线${route.id}`;
(earlyArrivalLegs || []).forEach(leg => {
const earlyMin = leg.earlyMinutes != null ? Math.round(leg.earlyMinutes * 10) / 10 : 0;
const earlyStr = earlyMin >= 0.1 ? `${earlyMin} 分钟` : `${Math.round(earlyMin * 60)}`;
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)';
const kTimeStr = this.minutesToStartTime(leg.scheduled);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'early_arrival',
title: '提前到达',
routeName,
routeIds: [routeId],
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
time: this.minutesToStartTime(leg.actualArrival),
suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`,
severity: 'high'
});
});
(lateArrivalLegs || []).forEach(leg => {
const kTimeStr = leg.actualArrival != null && Number.isFinite(leg.actualArrival) ? this.minutesToStartTime(leg.actualArrival) : '';
const part2 = kTimeStr ? ` ② 或将下一航点相对K时调至 ${kTimeStr} 或更晚` : ' ② 或将下一航点相对K时调晚';
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'late_arrival',
title: '无法按时到达',
routeName,
routeIds: [routeId],
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h${part2} ③ 调整上游航段速度或时间`,
severity: 'high'
});
});
(holdDelayConflicts || []).forEach(conf => {
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'hold_delay',
title: '盘旋时间不足',
routeName,
routeIds: [routeId],
fromWaypoint: conf.fromName,
toWaypoint: conf.toName,
time: this.minutesToStartTime(conf.setExitTime),
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined,
suggestion: `实际切出将延迟 ${conf.delaySeconds} 秒。① 延长该盘旋点相对K时 ② 定时盘旋调转弯半径,非定时调上一航点速度或本点相对K时 ③ 微调上下游航点相对K时`,
severity: 'high',
holdCenter: conf.holdCenter,
positionLng: conf.holdCenter && conf.holdCenter.lng,
positionLat: conf.holdCenter && conf.holdCenter.lat,
positionAlt: conf.holdCenter && conf.holdCenter.alt
});
});
} else {
let pathData = null;
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) {
@ -5554,7 +5775,11 @@ export default {
routeIdToTimeline[routeId] = {
segments: timeline.segments,
path: pathData && pathData.path ? pathData.path : null,
segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null
segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null,
//
earlyArrivalLegs: timeline.earlyArrivalLegs || [],
lateArrivalLegs: timeline.lateArrivalLegs || [],
holdDelayConflicts: timeline.holdDelayConflicts || []
};
this._setConflictTimelineCache(routeId, route.waypoints, minMinutes, maxMinutes, routeIdToTimeline[routeId]);
routeIdsWithTimeline.push(routeId);

Loading…
Cancel
Save