diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java new file mode 100644 index 0000000..26c4a13 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java @@ -0,0 +1,63 @@ +package com.ruoyi.web.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.websocket.service.WhiteboardRoomService; + +/** + * 白板 Controller:房间维度的白板 CRUD,数据存 Redis + */ +@RestController +@RequestMapping("/room") +public class WhiteboardController extends BaseController { + + @Autowired + private WhiteboardRoomService whiteboardRoomService; + + /** 获取房间下所有白板列表 */ + @GetMapping("/{roomId}/whiteboards") + public AjaxResult list(@PathVariable Long roomId) { + List list = whiteboardRoomService.listWhiteboards(roomId); + return success(list); + } + + /** 获取单个白板详情 */ + @GetMapping("/{roomId}/whiteboard/{whiteboardId}") + public AjaxResult get(@PathVariable Long roomId, @PathVariable String whiteboardId) { + Object wb = whiteboardRoomService.getWhiteboard(roomId, whiteboardId); + if (wb == null) return error("白板不存在"); + return success(wb); + } + + /** 创建白板 */ + @PostMapping("/{roomId}/whiteboard") + public AjaxResult create(@PathVariable Long roomId, @RequestBody Object whiteboard) { + Object created = whiteboardRoomService.createWhiteboard(roomId, whiteboard); + return success(created); + } + + /** 更新白板 */ + @PutMapping("/{roomId}/whiteboard/{whiteboardId}") + public AjaxResult update(@PathVariable Long roomId, @PathVariable String whiteboardId, + @RequestBody Object whiteboard) { + boolean ok = whiteboardRoomService.updateWhiteboard(roomId, whiteboardId, whiteboard); + return ok ? success() : error("更新失败"); + } + + /** 删除白板 */ + @DeleteMapping("/{roomId}/whiteboard/{whiteboardId}") + public AjaxResult delete(@PathVariable Long roomId, @PathVariable String whiteboardId) { + boolean ok = whiteboardRoomService.deleteWhiteboard(roomId, whiteboardId); + return ok ? success() : error("删除失败"); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java new file mode 100644 index 0000000..1423da3 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java @@ -0,0 +1,124 @@ +package com.ruoyi.websocket.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import com.alibaba.fastjson2.JSON; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +/** + * 白板房间服务:按房间维度将白板数据存储于 Redis + */ +@Service +public class WhiteboardRoomService { + + private static final String ROOM_WHITEBOARDS_PREFIX = "room:"; + private static final String ROOM_WHITEBOARDS_SUFFIX = ":whiteboards"; + private static final int EXPIRE_HOURS = 24; + + @Autowired + @Qualifier("stringObjectRedisTemplate") + private RedisTemplate redisTemplate; + + private String whiteboardsKey(Long roomId) { + return ROOM_WHITEBOARDS_PREFIX + roomId + ROOM_WHITEBOARDS_SUFFIX; + } + + /** 获取房间下所有白板列表 */ + @SuppressWarnings("unchecked") + public List listWhiteboards(Long roomId) { + if (roomId == null) return new ArrayList<>(); + String key = whiteboardsKey(roomId); + Object raw = redisTemplate.opsForValue().get(key); + if (raw == null) return new ArrayList<>(); + if (raw instanceof List) return (List) raw; + if (raw instanceof String) { + try { + return JSON.parseArray((String) raw); + } catch (Exception e) { + return new ArrayList<>(); + } + } + return new ArrayList<>(); + } + + /** 获取单个白板详情 */ + public Object getWhiteboard(Long roomId, String whiteboardId) { + List list = listWhiteboards(roomId); + for (Object item : list) { + if (item instanceof java.util.Map) { + Object id = ((java.util.Map) item).get("id"); + if (whiteboardId.equals(String.valueOf(id))) return item; + } + } + return null; + } + + /** 创建白板 */ + public Object createWhiteboard(Long roomId, Object whiteboard) { + if (roomId == null || whiteboard == null) return null; + List list = listWhiteboards(roomId); + String id = UUID.randomUUID().toString(); + Object wb = whiteboard; + if (wb instanceof java.util.Map) { + ((java.util.Map) wb).put("id", id); + if (!((java.util.Map) wb).containsKey("name")) { + ((java.util.Map) wb).put("name", "草稿"); + } + if (!((java.util.Map) wb).containsKey("timeBlocks")) { + ((java.util.Map) wb).put("timeBlocks", new ArrayList()); + } + if (!((java.util.Map) wb).containsKey("contentByTime")) { + ((java.util.Map) wb).put("contentByTime", new java.util.HashMap()); + } + } + list.add(wb); + saveWhiteboards(roomId, list); + return wb; + } + + /** 更新白板 */ + public boolean updateWhiteboard(Long roomId, String whiteboardId, Object whiteboard) { + if (roomId == null || whiteboardId == null || whiteboard == null) return false; + List list = listWhiteboards(roomId); + for (int i = 0; i < list.size(); i++) { + Object item = list.get(i); + if (item instanceof java.util.Map) { + Object id = ((java.util.Map) item).get("id"); + if (whiteboardId.equals(String.valueOf(id))) { + if (whiteboard instanceof java.util.Map) { + ((java.util.Map) whiteboard).put("id", whiteboardId); + } + list.set(i, whiteboard); + saveWhiteboards(roomId, list); + return true; + } + } + } + return false; + } + + /** 删除白板 */ + public boolean deleteWhiteboard(Long roomId, String whiteboardId) { + if (roomId == null || whiteboardId == null) return false; + List list = listWhiteboards(roomId); + boolean removed = list.removeIf(item -> { + if (item instanceof java.util.Map) { + Object id = ((java.util.Map) item).get("id"); + return whiteboardId.equals(String.valueOf(id)); + } + return false; + }); + if (removed) saveWhiteboards(roomId, list); + return removed; + } + + private void saveWhiteboards(Long roomId, List list) { + String key = whiteboardsKey(roomId); + redisTemplate.opsForValue().set(key, list, EXPIRE_HOURS, TimeUnit.HOURS); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/MissionScenario.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/MissionScenario.java index 605eb17..f8b9214 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/MissionScenario.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/MissionScenario.java @@ -38,6 +38,9 @@ public class MissionScenario extends BaseEntity @Excel(name = "是否为当前默认展示方案") private Integer isActive; + /** 大房间ID(查询用,非持久化):传入时查询 room_id IN (该大房间下所有子房间ID) */ + private Long parentRoomId; + public void setId(Long id) { this.id = id; @@ -98,6 +101,16 @@ public class MissionScenario extends BaseEntity return isActive; } + public Long getParentRoomId() + { + return parentRoomId; + } + + public void setParentRoomId(Long parentRoomId) + { + this.parentRoomId = parentRoomId; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java index b12ba07..e8188ef 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java @@ -47,6 +47,12 @@ public class Routes extends BaseEntity { private List waypoints; + /** 方案ID列表(查询用,非持久化):传入时查询 scenario_id IN (...) */ + private java.util.List scenarioIds; + + /** 方案ID逗号分隔(查询用,便于前端传参):如 "1,2,3" */ + private String scenarioIdsStr; + /** 关联的平台信息(仅用于 API 返回,非数据库字段) */ private java.util.Map platform; @@ -98,6 +104,34 @@ public class Routes extends BaseEntity { this.waypoints = waypoints; } + public java.util.List getScenarioIds() { + return scenarioIds; + } + + public void setScenarioIds(java.util.List scenarioIds) { + this.scenarioIds = scenarioIds; + } + + public String getScenarioIdsStr() { + return scenarioIdsStr; + } + + public void setScenarioIdsStr(String scenarioIdsStr) { + this.scenarioIdsStr = scenarioIdsStr; + if (scenarioIdsStr != null && !scenarioIdsStr.trim().isEmpty()) { + java.util.List list = new java.util.ArrayList<>(); + for (String s : scenarioIdsStr.split(",")) { + s = s.trim(); + if (!s.isEmpty()) { + try { + list.add(Long.parseLong(s)); + } catch (NumberFormatException ignored) {} + } + } + this.scenarioIds = list.isEmpty() ? null : list; + } + } + public java.util.Map getPlatform() { return platform; } diff --git a/ruoyi-system/src/main/resources/mapper/system/MissionScenarioMapper.xml b/ruoyi-system/src/main/resources/mapper/system/MissionScenarioMapper.xml index 152b447..d1eccc6 100644 --- a/ruoyi-system/src/main/resources/mapper/system/MissionScenarioMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/MissionScenarioMapper.xml @@ -20,7 +20,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - and scenario_id = #{scenarioId} + and scenario_id in + #{sid} + + and scenario_id = #{scenarioId} and platform_id = #{platformId} and call_sign = #{callSign} and attributes = #{attributes} diff --git a/ruoyi-ui/src/api/system/whiteboard.js b/ruoyi-ui/src/api/system/whiteboard.js new file mode 100644 index 0000000..73d9f43 --- /dev/null +++ b/ruoyi-ui/src/api/system/whiteboard.js @@ -0,0 +1,43 @@ +import request from '@/utils/request' + +/** 获取房间下所有白板列表 */ +export function listWhiteboards(roomId) { + return request({ + url: `/room/${roomId}/whiteboards`, + method: 'get' + }) +} + +/** 获取单个白板详情 */ +export function getWhiteboard(roomId, whiteboardId) { + return request({ + url: `/room/${roomId}/whiteboard/${whiteboardId}`, + method: 'get' + }) +} + +/** 创建白板 */ +export function createWhiteboard(roomId, data) { + return request({ + url: `/room/${roomId}/whiteboard`, + method: 'post', + data: data || {} + }) +} + +/** 更新白板 */ +export function updateWhiteboard(roomId, whiteboardId, data) { + return request({ + url: `/room/${roomId}/whiteboard/${whiteboardId}`, + method: 'put', + data: data || {} + }) +} + +/** 删除白板 */ +export function deleteWhiteboard(roomId, whiteboardId) { + return request({ + url: `/room/${roomId}/whiteboard/${whiteboardId}`, + method: 'delete' + }) +} diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue index a3c4508..dac82db 100644 --- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue +++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue @@ -404,9 +404,13 @@ - - @@ -551,6 +564,10 @@ export default { this.$emit('start-route-after-platform', this.entityData) }, + handleShowTransformBox() { + this.$emit('show-transform-box') + }, + handleToggleRouteLabel() { this.$emit('toggle-route-label') }, diff --git a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue index 55218e3..4a496f4 100644 --- a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue +++ b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue @@ -13,6 +13,19 @@ + +
+
+ {{ opt.label }} +
+
@@ -35,10 +48,19 @@ export default { toolMode: { type: String, default: 'airspace' // 'airspace' or 'ranging' + }, + auxiliaryLineConstraint: { + type: String, + default: 'none' // 'none' | 'horizontal' | 'vertical' } }, data() { return { + auxiliaryConstraintOptions: [ + { value: 'none', label: '自由' }, + { value: 'horizontal', label: '水平' }, + { value: 'vertical', label: '竖直' } + ], // 完整工具列表(空域模式,移除点和线) allToolbarItems: [ { id: 'mouse', name: '鼠标', icon: 'cursor' }, @@ -161,6 +183,36 @@ export default { font-size: 16px; } +.auxiliary-constraint-row { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid rgba(0, 138, 255, 0.2); +} + +.constraint-option { + font-size: 12px; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + text-align: center; + color: #666; + background: rgba(255, 255, 255, 0.6); + transition: all 0.2s; +} + +.constraint-option:hover { + background: rgba(0, 138, 255, 0.1); + color: #008aff; +} + +.constraint-option.active { + background: rgba(0, 138, 255, 0.2); + color: #008aff; +} + .toolbar-item:disabled { opacity: 0.5; cursor: not-allowed; diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 0b52f5f..088f4f0 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -7,11 +7,13 @@ :drawing-mode="drawingMode" :has-entities="allEntities.length > 0" :tool-mode="toolMode" + :auxiliary-line-constraint="auxiliaryLineConstraint" @toggle-drawing="toggleDrawing" @clear-all="clearAll" @export-data="exportData" @import-data="importData" @locate="handleLocate" + @auxiliary-line-constraint="auxiliaryLineConstraint = $event" /> [] } }, @@ -476,6 +488,10 @@ export default { opacity: 0.3, visible: true }, + /** 白板模式:进入时保存的主地图实体原始 show 状态,退出时还原 */ + whiteboardHiddenEntityShows: {}, + /** 白板平台实体 id -> entityData,用于拖拽/旋转时更新并回传 */ + whiteboardEntityDataMap: {}, /** 独立平台图标(非航线)的探测区/威力区样式缓存:platformIconId -> { ... } */ platformIconZoneStyles: {}, /** 探测区/威力区弹窗预选颜色(可与透明度组合) */ @@ -525,6 +541,8 @@ export default { text: { color: '#000000', font: '14px PingFang SC, Microsoft YaHei, Helvetica Neue, sans-serif', backgroundColor: 'rgba(255, 255, 255, 0.92)' }, image: { width: 150, height: 150 } }, + /** 辅助线:水平/竖直约束,'none' | 'horizontal' | 'vertical' */ + auxiliaryLineConstraint: 'none', // 鼠标经纬度 coordinatesText: '经度: --, 纬度: --', currentCoordinates: null, @@ -657,6 +675,29 @@ export default { if (!val) { this.clearMissilePreview() } + }, + whiteboardMode: { + handler(active) { + this.applyWhiteboardMode(active) + if (active) { + // 双 nextTick 确保父组件已更新 whiteboardEntities(如从主地图切回时 loadWhiteboards 完成) + this.$nextTick(() => { + this.$nextTick(() => { + const ents = this.whiteboardEntities || [] + if (ents.length > 0) this.renderWhiteboardEntities(ents) + }) + }) + } + } + }, + whiteboardEntities: { + deep: true, + immediate: true, + handler(entities) { + if (this.whiteboardMode && entities && entities.length > 0) { + this.renderWhiteboardEntities(entities) + } + } } }, computed: { @@ -1646,9 +1687,15 @@ export default { /** 从手柄实体 id 解析出平台图标 entityData 与类型(rotate / scale-0~3) */ getPlatformIconDataFromHandleId(handleEntityId) { if (!handleEntityId || typeof handleEntityId !== 'string') return null; + const findEd = (baseId) => { + const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId); + if (ed) return ed; + if (baseId.startsWith('wb_') && this.whiteboardEntityDataMap) return this.whiteboardEntityDataMap[baseId]; + return null; + }; if (handleEntityId.endsWith('-rotate-handle')) { const baseId = handleEntityId.replace(/-rotate-handle$/, ''); - const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId); + const ed = findEd(baseId); return ed ? { entityData: ed, type: 'rotate' } : null; } const scaleIdx = handleEntityId.lastIndexOf('-scale-'); @@ -1656,7 +1703,7 @@ export default { const baseId = handleEntityId.substring(0, scaleIdx); const cornerIndex = parseInt(handleEntityId.substring(scaleIdx + 7), 10); if (!isNaN(cornerIndex) && cornerIndex >= 0 && cornerIndex <= 3) { - const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId); + const ed = findEd(baseId); return ed ? { entityData: ed, type: 'scale', cornerIndex } : null; } } @@ -3702,10 +3749,10 @@ export default { /** * 从 Redis 加载指定房间+航线列表的导弹数据并创建实体(会先清空当前导弹实体) - * routePlatforms: [{ routeId, platformId }] + * routePlatforms: [{ routeId, platformId, roomId? }],roomId 可选,大房间时按航线所属方案用 plan.roomId */ async loadMissilesFromRedis(roomId, routePlatforms) { - if (!this.viewer || !this.viewer.entities || !roomId || !Array.isArray(routePlatforms)) return + if (!this.viewer || !this.viewer.entities || !Array.isArray(routePlatforms)) return // 先清空当前所有导弹实体(包括 routePlatforms 为空时,如删除航线后需清除地图上的导弹) while (this.missileEntities.length) { const { entity } = this.missileEntities.pop() @@ -3715,9 +3762,12 @@ export default { if (this.viewer.scene) this.viewer.scene.requestRender() return } - for (const { routeId, platformId } of routePlatforms) { + for (const item of routePlatforms) { + const { routeId, platformId } = item + const rId = item.roomId != null ? item.roomId : roomId + if (!rId) continue try { - const res = await getMissileParams({ roomId, routeId, platformId: platformId != null ? platformId : 0 }) + const res = await getMissileParams({ roomId: rId, routeId, platformId: platformId != null ? platformId : 0 }) let data = res.data if (!data) continue const arr = Array.isArray(data) ? data : [data] @@ -3825,6 +3875,8 @@ export default { } this.loadOfflineMap() this.setup2DConstraints() + // 禁用右键拖拽缩放,仅保留滚轮缩放 + this.viewer.scene.screenSpaceCameraController.zoomEventTypes = Cesium.CameraEventType.WHEEL // 移除 Cesium 自带的双击缩放,使双击实体/空白均无任何效果 try { const handler = this.viewer.screenSpaceEventHandler || (this.viewer.cesiumWidget && this.viewer.cesiumWidget.screenSpaceEventHandler) @@ -4254,6 +4306,9 @@ export default { } } } + if (!entityData && idStr.startsWith('wb_')) { + entityData = this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[idStr] + } if (!entityData) { // 查找对应的实体数据 entityData = this.allEntities.find(e => e.entity === pickedEntity || e === pickedEntity) @@ -4466,7 +4521,10 @@ export default { return; } } - const entityData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === picked.id); + let entityData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === picked.id); + if (!entityData && idStr.startsWith('wb_')) { + entityData = this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[idStr]; + } if (entityData) { this.pendingDragIcon = entityData; this.dragStartScreenPos = { x: click.position.x, y: click.position.y }; @@ -4582,22 +4640,39 @@ export default { } this.clickedOnEmpty = false; if (this.draggingRotateHandle) { - this.$emit('platform-icon-updated', this.draggingRotateHandle); + if (this.draggingRotateHandle.isWhiteboard) { + this.$emit('whiteboard-platform-updated', this.draggingRotateHandle); + } else { + this.$emit('platform-icon-updated', this.draggingRotateHandle); + } this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; this.draggingRotateHandle = null; } if (this.draggingScaleHandle) { - this.$emit('platform-icon-updated', this.draggingScaleHandle.entityData); + const ed = this.draggingScaleHandle.entityData; + if (ed && ed.isWhiteboard) { + this.$emit('whiteboard-platform-updated', ed); + } else { + this.$emit('platform-icon-updated', ed); + } this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; this.draggingScaleHandle = null; } if (this.draggingPlatformIcon) { - this.$emit('platform-icon-updated', this.draggingPlatformIcon); + if (this.draggingPlatformIcon.isWhiteboard) { + this.$emit('whiteboard-platform-updated', this.draggingPlatformIcon); + } else { + this.$emit('platform-icon-updated', this.draggingPlatformIcon); + } this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; this.draggingPlatformIcon = null; } if (this.rotatingPlatformIcon) { - this.$emit('platform-icon-updated', this.rotatingPlatformIcon); + if (this.rotatingPlatformIcon.isWhiteboard) { + this.$emit('whiteboard-platform-updated', this.rotatingPlatformIcon); + } else { + this.$emit('platform-icon-updated', this.rotatingPlatformIcon); + } this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; this.rotatingPlatformIcon = null; this.platformIconRotateTip = ''; @@ -5100,14 +5175,26 @@ export default { }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); }, finishLineDrawing() { - // 将预览线段转换为最终线段 if (this.drawingPoints.length > 1) { - // 移除预览线段 if (this.tempPreviewEntity) { this.viewer.entities.remove(this.tempPreviewEntity); this.tempPreviewEntity = null; } - // 创建最终的实线实体(updateLineSegmentLabels 会在 addLineEntity 中设置各段终点标签,含累计长度和角度) + if (this.whiteboardMode) { + const points = this.drawingPoints.map(p => this.cartesianToLatLng(p)) + const entityData = { + id: 'wb_line_' + Date.now(), + type: 'line', + label: '测距', + color: this.defaultStyles.line ? this.defaultStyles.line.color : '#008aff', + data: { points, width: (this.defaultStyles.line && this.defaultStyles.line.width) || 2 } + } + this.$emit('whiteboard-draw-complete', entityData) + this.drawingPoints = [] + this.drawingPointEntities = [] + this.tempEntity = null + return null + } const entity = this.addLineEntity([...this.drawingPoints], [...this.drawingPointEntities]); // 计算长度 const length = this.calculateLineLength([...this.drawingPoints]); @@ -5203,6 +5290,20 @@ export default { }, finishPolygonDrawing() { const positions = [...this.drawingPoints] + if (this.whiteboardMode && positions.length >= 3) { + const points = positions.map(p => this.cartesianToLatLng(p)) + const entityData = { + id: 'wb_polygon_' + Date.now(), + type: 'polygon', + label: '面', + color: this.defaultStyles.polygon.color, + data: { points, opacity: 0, width: this.defaultStyles.polygon.width } + } + this.$emit('whiteboard-draw-complete', entityData) + this.drawingPoints = [] + if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null } + return + } const entity = this.addPolygonEntity(positions) // 计算面积 const area = this.calculatePolygonArea(positions) @@ -5300,6 +5401,29 @@ export default { }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); }, finishRectangleDrawing() { + if (this.whiteboardMode && this.drawingPoints.length >= 2) { + const rect = Cesium.Rectangle.fromCartesianArray(this.drawingPoints) + const west = Cesium.Math.toDegrees(rect.west) + const south = Cesium.Math.toDegrees(rect.south) + const east = Cesium.Math.toDegrees(rect.east) + const north = Cesium.Math.toDegrees(rect.north) + const entityData = { + id: 'wb_rectangle_' + Date.now(), + type: 'rectangle', + label: '矩形', + color: this.defaultStyles.rectangle.color, + data: { + coordinates: { west, south, east, north }, + points: this.drawingPoints.map(p => this.cartesianToLatLng(p)), + opacity: this.defaultStyles.rectangle.opacity, + width: this.defaultStyles.rectangle.width + } + } + this.$emit('whiteboard-draw-complete', entityData) + this.drawingPoints = [] + if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null } + return + } // 1. 获取最终的矩形范围对象 const rect = Cesium.Rectangle.fromCartesianArray(this.drawingPoints); // 2. 移除动态预览的临时实体 @@ -5539,13 +5663,25 @@ export default { }, finishCircleDrawing(edgePosition) { const centerPoint = this.drawingPoints[0]; - // 1. 计算最终半径 const radius = Cesium.Cartesian3.distance(centerPoint, edgePosition); - // 2. 移除动态预览实体 if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; } + if (this.whiteboardMode) { + const center = this.cartesianToLatLng(centerPoint) + const entityData = { + id: 'wb_circle_' + Date.now(), + type: 'circle', + label: '圆形', + color: this.defaultStyles.circle.color, + data: { center: { lat: center.lat, lng: center.lng }, radius, opacity: this.defaultStyles.circle.opacity, width: this.defaultStyles.circle.width } + } + this.$emit('whiteboard-draw-complete', entityData) + this.drawingPoints = [] + this.activeCursorPosition = null + return + } // 3. 创建最终显示的静态实体 this.entityCounter++; const id = `circle_${this.entityCounter}`; @@ -5734,11 +5870,23 @@ export default { const radius = Cesium.Cartesian3.distance(centerPoint, radiusPoint); const startAngle = this.calculatePointAngle(centerPoint, radiusPoint); const endAngle = this.calculatePointAngle(centerPoint, anglePoint); - // 1. 移除动态预览实体 if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; } + if (this.whiteboardMode) { + const center = this.cartesianToLatLng(centerPoint) + const entityData = { + id: 'wb_sector_' + Date.now(), + type: 'sector', + label: '扇形', + color: this.defaultStyles.sector.color, + data: { center: { lat: center.lat, lng: center.lng }, radius, startAngle, endAngle, opacity: this.defaultStyles.sector.opacity, width: this.defaultStyles.sector.width } + } + this.$emit('whiteboard-draw-complete', entityData) + this.drawingPoints = [] + return + } // 2. 生成扇形顶点 const positions = this.generateSectorPositions(centerPoint, radius, startAngle, endAngle); // 3. 创建最终显示的静态实体 @@ -5886,6 +6034,20 @@ export default { calculateDistance(point1, point2) { return Cesium.Cartesian3.distance(point1, point2); }, + /** 根据辅助线约束修正第二点坐标(水平:同纬度;竖直:同经度) */ + applyAuxiliaryLineConstraint(firstPos, secondPos) { + if (!firstPos || !secondPos || this.auxiliaryLineConstraint === 'none') return secondPos + const firstLL = this.cartesianToLatLng(firstPos) + const secondLL = this.cartesianToLatLng(secondPos) + if (!firstLL || !secondLL) return secondPos + if (this.auxiliaryLineConstraint === 'horizontal') { + return Cesium.Cartesian3.fromDegrees(secondLL.lng, firstLL.lat) + } + if (this.auxiliaryLineConstraint === 'vertical') { + return Cesium.Cartesian3.fromDegrees(firstLL.lng, secondLL.lat) + } + return secondPos + }, // 绘制辅助线(两点定线段,默认黑色 1px) startAuxiliaryLineDrawing() { this.drawingPoints = [] @@ -5893,15 +6055,22 @@ export default { if (this.tempPreviewEntity) this.viewer.entities.remove(this.tempPreviewEntity) this.tempEntity = null this.tempPreviewEntity = null + const that = this this.drawingHandler.setInputAction((movement) => { - const newPosition = this.getClickPosition(movement.endPosition) + let newPosition = that.getClickPosition(movement.endPosition) + if (newPosition && that.drawingPoints.length === 1) { + newPosition = that.applyAuxiliaryLineConstraint(that.drawingPoints[0], newPosition) + } if (newPosition) { - this.activeCursorPosition = newPosition - if (this.viewer.scene.requestRenderMode) this.viewer.scene.requestRender() + that.activeCursorPosition = newPosition + if (that.viewer.scene.requestRenderMode) that.viewer.scene.requestRender() } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE) this.drawingHandler.setInputAction((click) => { - const position = this.getClickPosition(click.position) + let position = this.getClickPosition(click.position) + if (position && this.drawingPoints.length === 1) { + position = this.applyAuxiliaryLineConstraint(this.drawingPoints[0], position) + } if (position) { this.drawingPoints.push(position) if (this.drawingPoints.length === 1) { @@ -5935,6 +6104,19 @@ export default { this.viewer.entities.remove(this.tempPreviewEntity) this.tempPreviewEntity = null } + if (this.whiteboardMode) { + const points = this.drawingPoints.map(p => this.cartesianToLatLng(p)) + const entityData = { + id: 'wb_auxiliaryLine_' + Date.now(), + type: 'auxiliaryLine', + label: '辅助线', + color: this.defaultStyles.auxiliaryLine.color, + data: { points, width: this.defaultStyles.auxiliaryLine.width } + } + this.$emit('whiteboard-draw-complete', entityData) + this.drawingPoints = [] + return null + } const entity = this.addAuxiliaryLineEntity([...this.drawingPoints]) this.drawingPoints = [] this.notifyDrawingEntitiesChanged() @@ -6041,14 +6223,24 @@ export default { }, // 完成箭头绘制 finishArrowDrawing() { - // 将预览箭头转换为最终箭头 if (this.drawingPoints.length > 1) { - // 移除预览箭头 if (this.tempPreviewEntity) { this.viewer.entities.remove(this.tempPreviewEntity); this.tempPreviewEntity = null; } - // 创建最终的箭头实体 + if (this.whiteboardMode) { + const points = this.drawingPoints.map(p => this.cartesianToLatLng(p)) + const entityData = { + id: 'wb_arrow_' + Date.now(), + type: 'arrow', + label: '箭头', + color: this.defaultStyles.arrow.color, + data: { points, width: this.defaultStyles.arrow.width } + } + this.$emit('whiteboard-draw-complete', entityData) + this.drawingPoints = [] + return null + } const entity = this.addArrowEntity([...this.drawingPoints]); // 重置绘制点数组,保持绘制状态以继续绘制 this.drawingPoints = []; @@ -6121,10 +6313,20 @@ export default { }, // 添加文本实体 addTextEntity(position, text) { + const { lat, lng } = this.cartesianToLatLng(position) + if (this.whiteboardMode) { + const entityData = { + id: 'wb_text_' + Date.now(), + type: 'text', + label: text, + color: this.defaultStyles.text.color, + data: { position: { lat, lng }, text, lat, lng, fontSize: 14, color: this.defaultStyles.text.color } + } + this.$emit('whiteboard-draw-complete', entityData) + return + } this.entityCounter++ const id = `text_${this.entityCounter}` - // 获取经纬度坐标 - const {lat, lng} = this.cartesianToLatLng(position) const entity = this.viewer.entities.add({ id: id, name: `文本 ${this.entityCounter}`, @@ -6248,10 +6450,19 @@ export default { }, // 添加图片实体 addImageEntity(position, imageUrl) { + const { lat, lng } = this.cartesianToLatLng(position) + if (this.whiteboardMode) { + const entityData = { + id: 'wb_image_' + Date.now(), + type: 'image', + label: '图片', + data: { lat, lng, imageUrl, width: this.defaultStyles.image.width, height: this.defaultStyles.image.height } + } + this.$emit('whiteboard-draw-complete', entityData) + return + } this.entityCounter++ const id = `image_${this.entityCounter}` - // 获取经纬度坐标 - const {lat, lng} = this.cartesianToLatLng(position) const entity = this.viewer.entities.add({ id: id, name: `图片 ${this.entityCounter}`, @@ -7349,12 +7560,15 @@ export default { } break; } - case 'circle': + case 'circle': { entityData.entity.position = newCenter; const radius = entityData.radius || 1000; entityData.entity.polyline.positions = this.generateCirclePositions(newCenter, radius); - entityData.points = [this.cartesianToLatLng(newCenter), entityData.points && entityData.points[1] ? entityData.points[1] : this.cartesianToLatLng(newCenter)]; + const circleCenterLL = this.cartesianToLatLng(newCenter); + entityData.center = circleCenterLL; + entityData.points = [circleCenterLL, entityData.points && entityData.points[1] ? entityData.points[1] : circleCenterLL]; break; + } case 'sector': { const newCenterLL = this.cartesianToLatLng(newCenter); entityData.center = newCenterLL; @@ -7429,7 +7643,11 @@ export default { break; } entityData.entity.show = true; - this.notifyDrawingEntitiesChanged(); + if (entityData.isWhiteboard) { + this.$emit('whiteboard-drawing-updated', this.serializeWhiteboardDrawingEntity(entityData)); + } else { + this.notifyDrawingEntitiesChanged(); + } }, /** 清除空域位置调整模式 */ @@ -8313,12 +8531,15 @@ export default { deleteEntityFromContextMenu() { if (this.contextMenu.entityData) { const entityData = this.contextMenu.entityData - if (entityData.id) { + if (entityData.isWhiteboard) { + this.$emit('whiteboard-entity-deleted', entityData) + if (entityData.entity) this.viewer.entities.remove(entityData.entity) + if (this.whiteboardEntityDataMap && entityData.id) delete this.whiteboardEntityDataMap[entityData.id] + } else if (entityData.id) { this.removeEntity(entityData.id) } else if (entityData.entity && entityData.entity.id) { this.removeEntity(entityData.entity.id) } - // 隐藏右键菜单 this.contextMenu.visible = false } }, @@ -8458,11 +8679,12 @@ export default { } // 更新实体样式 this.updateEntityStyle(entityData) - // 若修改的是空域/威力区图形的样式,触发自动保存到房间,避免刷新后样式丢失 - if (this.getDrawingEntityTypes().includes(entityData.type)) { + // 白板实体:通知父组件更新 contentByTime + if (entityData.isWhiteboard && this.getDrawingEntityTypes().includes(entityData.type)) { + this.$emit('whiteboard-drawing-updated', this.serializeWhiteboardDrawingEntity(entityData)) + } else if (this.getDrawingEntityTypes().includes(entityData.type)) { this.notifyDrawingEntitiesChanged() } - // 隐藏右键菜单 this.contextMenu.visible = false } }, @@ -8474,7 +8696,9 @@ export default { this.contextMenu.visible = false return } - const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === fromMenu.id) || fromMenu + let ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === fromMenu.id) + if (!ed && fromMenu.id && this.whiteboardEntityDataMap) ed = this.whiteboardEntityDataMap[fromMenu.id] + if (!ed) ed = fromMenu if (!ed.entity) { this.contextMenu.visible = false return @@ -8738,6 +8962,67 @@ export default { if (this.loadingDrawingsFromRoom) return this.$emit('drawing-entities-changed') }, + /** 白板绘制实体序列化(排除 entity 引用,供父组件更新 contentByTime;输出可复用的 data 结构) */ + serializeWhiteboardDrawingEntity(entityData) { + if (!entityData || !entityData.id) return null + const base = { id: entityData.id, type: entityData.type, color: entityData.color || '#008aff' } + const data = entityData.data ? { ...entityData.data } : {} + switch (entityData.type) { + case 'polygon': + if (entityData.points && entityData.points.length >= 3) { + data.points = entityData.points + data.opacity = entityData.opacity != null ? entityData.opacity : 0 + data.width = entityData.width != null ? entityData.width : 2 + data.borderColor = entityData.borderColor || entityData.color + } + break + case 'rectangle': + if (entityData.points && entityData.points.length >= 2) { + const lngs = entityData.points.map(p => p.lng) + const lats = entityData.points.map(p => p.lat) + data.coordinates = { west: Math.min(...lngs), south: Math.min(...lats), east: Math.max(...lngs), north: Math.max(...lats) } + data.points = entityData.points + } + data.opacity = entityData.opacity != null ? entityData.opacity : 0 + data.width = entityData.width != null ? entityData.width : 2 + data.borderColor = entityData.borderColor || entityData.color + break + case 'circle': + data.center = entityData.center || (entityData.points && entityData.points[0] ? entityData.points[0] : null) + data.radius = entityData.radius != null ? entityData.radius : 1000 + data.opacity = entityData.opacity != null ? entityData.opacity : 0 + data.width = entityData.width != null ? entityData.width : 2 + data.borderColor = entityData.borderColor || entityData.color + break + case 'sector': + data.center = entityData.center || (entityData.points && entityData.points[0] ? entityData.points[0] : null) + data.radius = entityData.radius != null ? entityData.radius : 1000 + data.startAngle = entityData.startAngle != null ? entityData.startAngle : 0 + data.endAngle = entityData.endAngle != null ? entityData.endAngle : Math.PI * 0.5 + data.opacity = entityData.opacity != null ? entityData.opacity : 0.5 + data.width = entityData.width != null ? entityData.width : 2 + data.borderColor = entityData.borderColor || entityData.color + break + case 'line': + case 'auxiliaryLine': + if (entityData.points && entityData.points.length >= 2) { + data.points = entityData.points + data.width = entityData.width != null ? entityData.width : 2 + data.color = entityData.color || entityData.data?.color + } + break + case 'arrow': + if (entityData.points && entityData.points.length >= 2) { + data.points = entityData.points + data.width = entityData.width != null ? entityData.width : 12 + data.color = entityData.color || entityData.data?.color + } + break + default: + return { ...base, data: entityData.data || {} } + } + return { ...base, data } + }, /** 将单个实体序列化为可存储的 JSON 结构 */ serializeEntityForSave(entity) { const base = { id: entity.id, type: entity.type, label: entity.label || '', color: entity.color || '#008aff' } @@ -8878,6 +9163,402 @@ export default { }) this.allEntities = this.allEntities.filter(item => !types.includes(item.type)) }, + /** 从屏幕坐标获取经纬度(用于白板拖放等) */ + getLatLngFromScreen(clientX, clientY) { + if (!this.viewer || !this.viewer.scene || !this.viewer.scene.canvas) return null + const canvas = this.viewer.scene.canvas + const rect = canvas.getBoundingClientRect() + const x = clientX - rect.left + const y = clientY - rect.top + const cartesian = this.viewer.camera.pickEllipsoid(new Cesium.Cartesian2(x, y), this.viewer.scene.globe.ellipsoid) + if (!cartesian) return null + return this.cartesianToLatLng(cartesian) + }, + /** 白板模式:遍历 viewer 全部实体,隐藏非白板实体(含航点、航线、平台等),退出时还原 */ + applyWhiteboardMode(active) { + if (!this.viewer) return + const now = Cesium.JulianDate.now() + if (active) { + this.whiteboardHiddenEntityShows = {} + this.viewer.entities.values.forEach(entity => { + const id = entity.id + if (!id || String(id).startsWith('wb_')) return + let origShow = true + if (entity.show != null) { + origShow = typeof entity.show.getValue === 'function' ? entity.show.getValue(now) : entity.show + } + this.whiteboardHiddenEntityShows[id] = origShow + entity.show = false + }) + this.renderWhiteboardEntities(this.whiteboardEntities || []) + } else { + Object.keys(this.whiteboardHiddenEntityShows || {}).forEach(id => { + const entity = this.viewer.entities.getById(id) + if (entity) entity.show = this.whiteboardHiddenEntityShows[id] + }) + this.whiteboardHiddenEntityShows = {} + this.clearWhiteboardEntities() + } + }, + /** 清除白板实体(id 以 wb_ 开头) */ + clearWhiteboardEntities() { + if (!this.viewer) return + const toRemove = [] + this.viewer.entities.values.forEach(e => { + if (e.id && String(e.id).startsWith('wb_')) toRemove.push(e) + }) + toRemove.forEach(e => this.viewer.entities.remove(e)) + this.whiteboardEntityDataMap = {} + }, + /** 渲染白板实体到地图(平台图标:已存在则就地更新,避免清除重建导致渲染闪烁/消失) */ + renderWhiteboardEntities(entities) { + if (!this.viewer || !Array.isArray(entities)) return + const scene = this.viewer.scene + const wantIds = new Set() + entities.forEach(ed => { + if (ed.type === 'platformIcon') { + const id = (ed.id || '').startsWith('wb_') ? ed.id : ('wb_' + (ed.id || '')) + if (id) wantIds.add(id) + } else { + const id = (ed.id || '').startsWith('wb_') ? ed.id : ('wb_' + (ed.id || '')) + if (id) wantIds.add(id) + } + }) + + this.whiteboardEntityDataMap = this.whiteboardEntityDataMap || {} + const toRemove = [] + this.viewer.entities.values.forEach(e => { + if (e.id && String(e.id).startsWith('wb_') && !wantIds.has(e.id)) toRemove.push(e) + }) + toRemove.forEach(e => this.viewer.entities.remove(e)) + Object.keys(this.whiteboardEntityDataMap).forEach(id => { + if (!wantIds.has(id)) { + const ed = this.whiteboardEntityDataMap[id] + if (ed && ed.entity) this.viewer.entities.remove(ed.entity) + delete this.whiteboardEntityDataMap[id] + } + }) + + entities.forEach(ed => { + try { + if (ed.type === 'platformIcon') { + const id = (ed.id || 'wb_platform_' + Date.now()).startsWith('wb_') ? ed.id : 'wb_' + ed.id + const existing = this.whiteboardEntityDataMap[id] + if (existing && existing.entity) { + this.updateWhiteboardPlatformIconInPlace(existing, ed) + } else { + this.addWhiteboardPlatformIcon(ed) + } + } else { + this.addWhiteboardDrawingEntity(ed) + } + } catch (err) { + console.warn('renderWhiteboardEntity failed', ed?.type, err) + } + }) + + if (scene && scene.requestRender) { + scene.requestRender() + setTimeout(() => scene.requestRender(), 50) + } + }, + /** 就地更新白板平台图标(不销毁实体,避免 requestRenderMode 下渲染消失) */ + updateWhiteboardPlatformIconInPlace(existing, ed) { + existing.lat = ed.lat + existing.lng = ed.lng + existing.heading = ed.heading != null ? ed.heading : 0 + existing.iconScale = ed.iconScale != null ? ed.iconScale : 1.5 + existing.entity.position = Cesium.Cartesian3.fromDegrees(ed.lng, ed.lat) + existing.entity.billboard.rotation = Math.PI / 2 - (existing.heading * Math.PI / 180) + this.updatePlatformIconBillboardSize(existing) + if (existing.transformHandles) this.updateTransformHandlePositions(existing) + if (this.viewer.scene && this.viewer.scene.requestRender) this.viewer.scene.requestRender() + }, + /** 白板平台图标:添加 billboard,并存入 whiteboardEntityDataMap 以支持拖拽/旋转 */ + addWhiteboardPlatformIcon(ed) { + const platform = ed.platform || {} + const iconUrl = platform.imageUrl || platform.iconUrl + const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl() + const id = (ed.id || 'wb_platform_' + Date.now()).startsWith('wb_') ? ed.id : 'wb_' + ed.id + const lat = ed.lat + const lng = ed.lng + const heading = ed.heading != null ? ed.heading : 0 + const rotation = Math.PI / 2 - (heading * Math.PI / 180) + const size = this.PLATFORM_ICON_BASE_SIZE * (ed.iconScale != null ? ed.iconScale : 1.5) + const cartesian = Cesium.Cartesian3.fromDegrees(lng, lat) + const entity = this.viewer.entities.add({ + id, + name: ed.label || platform.name || '平台', + position: cartesian, + billboard: { + image: imageSrc, + width: size, + height: size, + verticalOrigin: Cesium.VerticalOrigin.CENTER, + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, + rotation, + scaleByDistance: new Cesium.NearFarScalar(500, 1.2, 200000, 0.35), + translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6) + } + }) + const entityData = { id, entity, lat, lng, heading, platform, platformName: platform.name, label: ed.label, type: 'platformIcon', isWhiteboard: true, iconScale: ed.iconScale != null ? ed.iconScale : 1.5 } + this.whiteboardEntityDataMap = this.whiteboardEntityDataMap || {} + this.whiteboardEntityDataMap[id] = entityData + }, + /** 白板空域/图形实体:复用 importEntity 逻辑但不加入 allEntities */ + addWhiteboardDrawingEntity(ed) { + const id = (ed.id || 'wb_draw_' + Date.now()).startsWith('wb_') ? ed.id : 'wb_' + ed.id + const entityData = { ...ed, id } + if (ed.data) { + entityData.data = ed.data + } + const existing = this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[id] + if (existing && existing.entity) { + this.viewer.entities.remove(existing.entity) + delete this.whiteboardEntityDataMap[id] + } + this.importEntityAsWhiteboard(entityData) + }, + /** 仅添加图形到 viewer,不加入 allEntities(用于白板) */ + importEntityAsWhiteboard(entityData) { + const color = entityData.color || '#008aff' + const types = this.getDrawingEntityTypes() + if (!types.includes(entityData.type)) return + const data = entityData.data || {} + let entity + switch (entityData.type) { + case 'rectangle': { + const rectCoords = data.coordinates || (data.points && data.points.length >= 2 + ? { west: Math.min(...data.points.map(p => p.lng)), south: Math.min(...data.points.map(p => p.lat)), east: Math.max(...data.points.map(p => p.lng)), north: Math.max(...data.points.map(p => p.lat)) } + : null) + if (!rectCoords) return + const rectOpacity = data.opacity != null ? data.opacity : 0 + const rectWidth = data.width != null ? data.width : 2 + const rectBorderColor = data.borderColor || color + const southwest = Cesium.Cartesian3.fromDegrees(rectCoords.west, rectCoords.south) + const southeast = Cesium.Cartesian3.fromDegrees(rectCoords.east, rectCoords.south) + const northeast = Cesium.Cartesian3.fromDegrees(rectCoords.east, rectCoords.north) + const northwest = Cesium.Cartesian3.fromDegrees(rectCoords.west, rectCoords.north) + entity = this.viewer.entities.add({ + id: entityData.id, + rectangle: { + coordinates: Cesium.Rectangle.fromDegrees(rectCoords.west, rectCoords.south, rectCoords.east, rectCoords.north), + material: Cesium.Color.fromCssColorString(color).withAlpha(rectOpacity) + }, + polyline: { + positions: [southwest, southeast, northeast, northwest, southwest], + width: rectWidth, + material: Cesium.Color.fromCssColorString(rectBorderColor), + arcType: Cesium.ArcType.NONE + } + }) + entityData.points = [this.cartesianToLatLng(southwest), this.cartesianToLatLng(southeast), this.cartesianToLatLng(northeast), this.cartesianToLatLng(northwest)] + entityData.opacity = rectOpacity + entityData.width = rectWidth + entityData.borderColor = rectBorderColor + break + } + case 'polygon': { + const polygonPositions = (data.points || []).map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat)) + if (polygonPositions.length < 3) return + const polyOpacity = data.opacity != null ? data.opacity : 0 + const polyWidth = data.width != null ? data.width : 2 + const polyBorderColor = data.borderColor || color + entity = this.viewer.entities.add({ + id: entityData.id, + polygon: { + hierarchy: polygonPositions, + material: Cesium.Color.fromCssColorString(color).withAlpha(polyOpacity) + }, + polyline: { + positions: [...polygonPositions, polygonPositions[0]], + width: polyWidth, + material: Cesium.Color.fromCssColorString(polyBorderColor), + arcType: Cesium.ArcType.NONE + } + }) + entityData.positions = polygonPositions + entityData.points = data.points || polygonPositions.map(p => this.cartesianToLatLng(p)) + entityData.opacity = polyOpacity + entityData.width = polyWidth + entityData.borderColor = polyBorderColor + break + } + case 'circle': { + const center = data.center || {} + const radius = data.radius || 1000 + const circleCenter = Cesium.Cartesian3.fromDegrees(center.lng || 0, center.lat || 0) + const circlePositions = this.generateCirclePositions(circleCenter, radius) + const circleOpacity = data.opacity != null ? data.opacity : 0 + const circleWidth = data.width != null ? data.width : 2 + const circleBorderColor = data.borderColor || color + entity = this.viewer.entities.add({ + id: entityData.id, + position: circleCenter, + ellipse: { + semiMajorAxis: radius, + semiMinorAxis: radius, + material: Cesium.Color.fromCssColorString(color).withAlpha(circleOpacity), + arcType: Cesium.ArcType.NONE + }, + polyline: { + positions: circlePositions, + width: circleWidth, + material: Cesium.Color.fromCssColorString(circleBorderColor), + arcType: Cesium.ArcType.NONE + } + }) + entityData.radius = radius + entityData.center = center + entityData.points = [center.lng != null && center.lat != null ? center : this.cartesianToLatLng(circleCenter)] + entityData.opacity = circleOpacity + entityData.width = circleWidth + entityData.borderColor = circleBorderColor + break + } + case 'powerZone': { + const center = data.center || {} + const radius = (data.radius || 50000) * 1000 + const centerPos = Cesium.Cartesian3.fromDegrees(center.lng || 0, center.lat || 0) + const zoneColor = data.color || color + const zoneOpacity = data.opacity != null ? data.opacity : 0.3 + entity = this.viewer.entities.add({ + id: entityData.id, + position: centerPos, + ellipse: { + semiMajorAxis: radius, + semiMinorAxis: radius, + material: Cesium.Color.fromCssColorString(zoneColor).withAlpha(zoneOpacity), + arcType: Cesium.ArcType.NONE + } + }) + break + } + case 'text': { + const pos = data.position || (data.lat != null && data.lng != null ? { lat: data.lat, lng: data.lng } : null) + const text = data.text || entityData.label || '' + if (!pos || (pos.lng == null && pos.lat == null)) return + const lng = pos.lng != null ? pos.lng : (data.lng != null ? data.lng : 0) + const lat = pos.lat != null ? pos.lat : (data.lat != null ? data.lat : 0) + entity = this.viewer.entities.add({ + id: entityData.id, + position: Cesium.Cartesian3.fromDegrees(lng, lat), + label: { + text, + font: (data.fontSize || 14) + 'px sans-serif', + fillColor: Cesium.Color.fromCssColorString(data.color || color), + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + pixelOffset: new Cesium.Cartesian2(0, -10) + } + }) + break + } + case 'sector': { + const center = data.center || {} + const radius = data.radius || 1000 + const startAngle = data.startAngle != null ? data.startAngle : 0 + const endAngle = data.endAngle != null ? data.endAngle : Math.PI * 0.5 + const centerPos = Cesium.Cartesian3.fromDegrees(center.lng || 0, center.lat || 0) + const positions = this.generateSectorPositions(centerPos, radius, startAngle, endAngle) + const sectorOpacity = data.opacity != null ? data.opacity : 0.5 + const sectorWidth = data.width != null ? data.width : 2 + const sectorBorderColor = data.borderColor || color + entity = this.viewer.entities.add({ + id: entityData.id, + polygon: { + hierarchy: new Cesium.PolygonHierarchy(positions), + material: Cesium.Color.fromCssColorString(color).withAlpha(sectorOpacity) + }, + polyline: { + positions: positions, + width: sectorWidth, + material: Cesium.Color.fromCssColorString(sectorBorderColor), + arcType: Cesium.ArcType.NONE + } + }) + entityData.center = center + entityData.radius = radius + entityData.startAngle = startAngle + entityData.endAngle = endAngle + entityData.positions = positions + entityData.opacity = sectorOpacity + entityData.width = sectorWidth + entityData.borderColor = sectorBorderColor + break + } + case 'line': + case 'auxiliaryLine': { + const pts = (data.points || []).map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat)) + if (pts.length < 2) return + const lineWidth = data.width != null ? data.width : 2 + const lineColor = data.color || color + entity = this.viewer.entities.add({ + id: entityData.id, + polyline: { + positions: pts, + width: lineWidth, + material: Cesium.Color.fromCssColorString(lineColor), + arcType: Cesium.ArcType.NONE + } + }) + entityData.positions = pts + entityData.points = data.points || pts.map(p => this.cartesianToLatLng(p)) + entityData.width = lineWidth + entityData.color = lineColor + break + } + case 'arrow': { + const arrowPts = (data.points || []).map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat)) + if (arrowPts.length < 2) return + const arrowWidth = data.width != null ? data.width : 12 + const arrowColor = data.color || color + entity = this.viewer.entities.add({ + id: entityData.id, + polyline: { + positions: arrowPts, + width: arrowWidth, + material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.fromCssColorString(arrowColor)), + arcType: Cesium.ArcType.NONE, + widthInMeters: false + } + }) + entityData.positions = arrowPts + entityData.points = data.points || arrowPts.map(p => this.cartesianToLatLng(p)) + entityData.width = arrowWidth + entityData.color = arrowColor + break + } + case 'image': { + if (!data.imageUrl || data.lat == null || data.lng == null) return + const imgPos = Cesium.Cartesian3.fromDegrees(data.lng, data.lat) + entity = this.viewer.entities.add({ + id: entityData.id, + position: imgPos, + billboard: { + image: data.imageUrl, + width: data.width != null ? data.width : 64, + height: data.height != null ? data.height : 64, + verticalOrigin: Cesium.VerticalOrigin.CENTER, + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, + scaleByDistance: new Cesium.NearFarScalar(200, 1.12, 1200000, 0.72), + translucencyByDistance: new Cesium.NearFarScalar(300, 1.0, 600000, 0.88) + } + }) + break + } + default: + return + } + if (entity && entityData.id) { + entityData.entity = entity + entityData.isWhiteboard = true + this.whiteboardEntityDataMap = this.whiteboardEntityDataMap || {} + this.whiteboardEntityDataMap[entityData.id] = entityData + } + }, /** 从房间/方案加载的 frontend_drawings JSON 恢复空域图形(先清空当前图形再导入;加载期间不触发自动保存) */ loadFrontendDrawings(data) { if (!this.viewer) return diff --git a/ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue b/ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue index d3de847..386bb15 100644 --- a/ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue +++ b/ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue @@ -5,6 +5,7 @@ ref="timeline" @timeline-hidden="onTimelineHidden" :room-id="roomId" + :is-parent-room="isParentRoom" />
@@ -22,6 +23,10 @@ 六步法
+
+ + 白板 +
@@ -60,6 +65,10 @@ export default { roomId: { type: [Number, String], default: null + }, + isParentRoom: { + type: Boolean, + default: false } }, computed: { @@ -125,6 +134,10 @@ export default { this.isExpanded = false this.updateBottomPanelVisible() }, + toggleWhiteboard() { + this.$emit('open-whiteboard') + this.isExpanded = false + }, saveProgress(overlayProgress) { if (!this.progressStorageKey) return try { diff --git a/ruoyi-ui/src/views/childRoom/BottomTimeline.vue b/ruoyi-ui/src/views/childRoom/BottomTimeline.vue index 580997a..70fcb96 100644 --- a/ruoyi-ui/src/views/childRoom/BottomTimeline.vue +++ b/ruoyi-ui/src/views/childRoom/BottomTimeline.vue @@ -26,12 +26,13 @@ :class="{ active: segment.active, passed: segment.passed }" :style="{ left: segment.position + '%' }" @click="openSegmentDialog(segment)" - :title="segment.time + ' - ' + segment.name" + :title="segment.time + ' - ' + segment.name + (segment.roomName ? ' [' + segment.roomName + ']' : '')" >
{{ segment.time }}
{{ segment.name }}
+
{{ segment.roomName }}
@@ -107,6 +108,7 @@
{{ segment.time }}
{{ segment.name }}
+
{{ segment.roomName }}
{{ segment.description }}
@@ -173,6 +175,10 @@ {{ currentSegment.name }}
+
+ + {{ currentSegment.roomName }} +
{{ currentSegment.description }} @@ -194,6 +200,7 @@ + + diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 0a92091..de84ee5 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -9,9 +9,11 @@ @drop="handleMapDrop" > - + @platform-style-saved="onPlatformStyleSaved" + @whiteboard-draw-complete="handleWhiteboardDrawComplete" + @whiteboard-platform-updated="handleWhiteboardPlatformUpdated" + @whiteboard-entity-deleted="handleWhiteboardEntityDeleted" + @whiteboard-drawing-updated="handleWhiteboardDrawingUpdated" />
- + +
+ + + this.getList()); } } }, @@ -726,6 +771,7 @@ export default { ); }, canSetKTime() { + if (this.roomDetail && this.roomDetail.parentId == null) return false; return this.isRoomOwner || this.isAdmin; }, /** 格式化的 K 时(基准时刻),供右上角显示 */ @@ -746,6 +792,27 @@ export default { if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。'; return `在 ${this.addHoldContext.fromName} 与 ${this.addHoldContext.toName} 之间添加盘旋,到计划时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`; }, + /** 白板模式下当前时间块应显示的实体(继承逻辑:当前时刻 = 上一时刻 + 本时刻差异) */ + whiteboardDisplayEntities() { + if (!this.showWhiteboardPanel || !this.currentWhiteboard || !this.currentWhiteboardTimeBlock) return [] + const wb = this.currentWhiteboard + const contentByTime = wb.contentByTime || {} + const timeBlocks = (wb.timeBlocks || []).slice().sort((a, b) => this.compareWhiteboardTimeBlock(a, b)) + const idx = timeBlocks.indexOf(this.currentWhiteboardTimeBlock) + if (idx < 0) return [] + const merged = {} + for (let i = 0; i <= idx; i++) { + const tb = timeBlocks[i] + const ents = (contentByTime[tb] && contentByTime[tb].entities) || [] + ents.forEach(e => { + const id = e.id || (e.data && e.data.id) + if (id) merged[id] = e + else merged['_noid_' + Math.random()] = e + }) + } + return Object.values(merged) + }, + /** 被其他成员编辑锁定的航线 ID 列表,供地图禁止拖拽等 */ routeLockedByOtherRouteIds() { const myId = this.currentUserId; @@ -786,9 +853,12 @@ export default { created() { this.currentRoomId = this.$route.query.roomId; console.log("从路由接收到的真实房间 ID:", this.currentRoomId); - this.getList(); this.getPlatformList(); - if (this.currentRoomId) this.getRoomDetail(); + if (this.currentRoomId) { + this.getRoomDetail(() => this.getList()); + } else { + this.getList(); + } }, methods: { handleBottomPanelVisible(visible) { @@ -1577,9 +1647,13 @@ export default { if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data); }).catch(() => {}); }, - /** 收到其他设备的空域图形变更同步:拉取最新房间数据并重绘 */ + /** 收到其他设备的空域图形变更同步:拉取最新房间数据并重绘;大房间时重新合并子房间标绘 */ applySyncRoomDrawings() { if (!this.currentRoomId || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadFrontendDrawings !== 'function') return; + if (this.roomDetail && this.roomDetail.parentId == null) { + this.loadRoomDrawings(); + return; + } getRooms(this.currentRoomId).then(res => { if (res.code === 200 && res.data) { this.roomDetail = this.roomDetail ? { ...this.roomDetail, frontendDrawings: res.data.frontendDrawings } : { ...res.data }; @@ -1895,19 +1969,32 @@ export default { const { skipRoomPlatformIcons = false } = opts; try { const roomId = this.$route.query.roomId || this.currentRoomId; - const scenarioRes = await listScenario({ roomId: roomId }); + const isParentRoom = this.roomDetail && this.roomDetail.parentId == null; + const scenarioParams = isParentRoom + ? { parentRoomId: roomId, pageNum: 1, pageSize: 9999 } + : { roomId: roomId, pageNum: 1, pageSize: 9999 }; + const scenarioRes = await listScenario(scenarioParams); if (scenarioRes.code === 200) { this.plans = scenarioRes.rows.map(s => ({ id: s.id, name: s.name, + roomId: s.roomId, frontendDrawings: s.frontendDrawings || null, routes: [] })); } - // 获取所有航线 - const routeRes = await listRoutes({ pageNum: 1, pageSize: 9999 }); + // 获取航线:大房间时只请求本大房间子房间方案的航线,小房间时请求全部后按方案过滤 + const planIds = this.plans.map(p => p.id); + const routeParams = isParentRoom && planIds.length > 0 + ? { scenarioIdsStr: planIds.join(','), pageNum: 1, pageSize: 9999 } + : { pageNum: 1, pageSize: 9999 }; + const routeRes = await listRoutes(routeParams); if (routeRes.code === 200) { - const allRoutes = routeRes.rows.map(item => ({ + let routeRows = routeRes.rows || []; + if (isParentRoom && planIds.length > 0) { + routeRows = routeRows.filter(r => planIds.includes(r.scenarioId)); + } + const allRoutes = routeRows.map(item => ({ id: item.id, name: item.callSign, platformId: item.platformId, @@ -1924,15 +2011,17 @@ export default { }); this.routes = allRoutes; - // 先预取所有展示中航线的平台样式,再渲染,避免平台图标先黑后变色 + // 先预取所有展示中航线的平台样式,再渲染,避免平台图标先黑后变色(大房间时用方案所属子房间的 roomId) if (this.activeRouteIds.length > 0 && this.$refs.cesiumMap) { const roomId = this.currentRoomId; await Promise.all(this.activeRouteIds.map(async (id) => { const route = this.routes.find(r => r.id === id); if (!route || !route.waypoints || route.waypoints.length === 0) return; - if (roomId && route.platformId) { + const plan = this.plans.find(p => p.id === route.scenarioId); + const styleRoomId = (isParentRoom && plan && plan.roomId) ? plan.roomId : roomId; + if (styleRoomId && route.platformId) { try { - const res = await getPlatformStyle({ roomId, routeId: id, platformId: route.platformId }); + const res = await getPlatformStyle({ roomId: styleRoomId, routeId: id, platformId: route.platformId }); if (res.data) this.$refs.cesiumMap.setPlatformStyle(id, res.data); } catch (_) {} } @@ -1947,13 +2036,24 @@ export default { }); }); } - // 加载该房间下保存的地图平台图标(删除航线等操作时跳过,避免清空再重画导致平台图标闪一下) + // 加载该房间下保存的地图平台图标(大房间时加载所有子房间的图标) if (!skipRoomPlatformIcons) { const rId = roomId || this.currentRoomId; if (rId && this.$refs.cesiumMap && typeof this.$refs.cesiumMap.loadRoomPlatformIcons === 'function') { - listRoomPlatformIcons(rId).then(res => { - if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data); - }).catch(() => {}); + if (isParentRoom) { + listRooms({ pageNum: 1, pageSize: 999, parentId: rId }).then(roomsRes => { + const childRooms = roomsRes.rows || roomsRes || []; + childRooms.forEach(child => { + listRoomPlatformIcons(child.id).then(res => { + if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(child.id, res.data); + }).catch(() => {}); + }); + }).catch(() => {}); + } else { + listRoomPlatformIcons(rId).then(res => { + if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data); + }).catch(() => {}); + } } } this.$nextTick(() => this.applyRoomStatePending()); @@ -2241,22 +2341,68 @@ export default { const min = parseInt(m[3], 10); return sign * (h * 60 + min); }, - getRoomDetail() { - if (!this.currentRoomId) return; + getRoomDetail(callback) { + if (!this.currentRoomId) { callback && callback(); return; } getRooms(this.currentRoomId).then(res => { if (res.code === 200 && res.data) { this.roomDetail = res.data; this.$nextTick(() => this.loadRoomDrawings()); + if (res.data.parentId == null) { + listRooms({ pageNum: 1, pageSize: 999, parentId: this.currentRoomId }).then(roomsRes => { + const rows = roomsRes.rows || roomsRes || []; + this.childRoomKTimes = rows + .filter(r => r.kAnchorTime) + .map(r => ({ name: r.name || `房间${r.id}`, kAnchorTime: r.kAnchorTime })); + this.selectedChildKTimeIndex = 0; + }).catch(() => { this.childRoomKTimes = []; }); + } else { + this.childRoomKTimes = []; + } } - }).catch(() => {}); + callback && callback(); + }).catch(() => { callback && callback(); }); }, - /** 加载当前房间的空域/威力区图形(与房间 ID 绑定,进入该房间即显示) */ - loadRoomDrawings() { + /** 加载当前房间的空域/威力区图形(与房间 ID 绑定,进入该房间即显示);大房间时合并所有子房间的 frontend_drawings */ + async loadRoomDrawings() { if (!this.roomDetail || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadFrontendDrawings !== 'function') return; - if (this.roomDetail.frontendDrawings) { - this.$refs.cesiumMap.loadFrontendDrawings(this.roomDetail.frontendDrawings); + const isParentRoom = this.roomDetail.parentId == null; + if (isParentRoom) { + const roomsRes = await listRooms({ pageNum: 1, pageSize: 999, parentId: this.currentRoomId }).catch(() => ({ rows: [] })); + const childRooms = roomsRes.rows || roomsRes || []; + const allEntities = []; + for (const child of childRooms) { + const fd = child.frontendDrawings; + if (!fd) continue; + let payload = typeof fd === 'string' ? (() => { try { return JSON.parse(fd) } catch (_) { return null } })() : fd; + if (!payload || !Array.isArray(payload.entities) || payload.entities.length === 0) continue; + const prefix = `room_${child.id}_`; + payload.entities.forEach(e => { + const cloned = JSON.parse(JSON.stringify(e)); + if (cloned.id) cloned.id = prefix + cloned.id; + allEntities.push(cloned); + }); + } + if (this.roomDetail.frontendDrawings) { + let parentPayload = typeof this.roomDetail.frontendDrawings === 'string' + ? (() => { try { return JSON.parse(this.roomDetail.frontendDrawings) } catch (_) { return null } })() + : this.roomDetail.frontendDrawings; + if (parentPayload && Array.isArray(parentPayload.entities)) { + parentPayload.entities.forEach(e => { + if (e.id && !/^room_\d+_/.test(String(e.id))) allEntities.push(JSON.parse(JSON.stringify(e))); + }); + } + } + if (allEntities.length > 0) { + this.$refs.cesiumMap.loadFrontendDrawings({ entities: allEntities }); + } else { + this.$refs.cesiumMap.clearDrawingEntities(); + } } else { - this.$refs.cesiumMap.clearDrawingEntities(); + if (this.roomDetail.frontendDrawings) { + this.$refs.cesiumMap.loadFrontendDrawings(this.roomDetail.frontendDrawings); + } else { + this.$refs.cesiumMap.clearDrawingEntities(); + } } }, /** 地图 viewer 就绪时加载当前房间图形(可能 getRoomDetail 尚未返回,此处再试一次) */ @@ -2568,6 +2714,222 @@ export default { this.$message.success('已触发下载,请在浏览器保存对话框中选择保存位置') }, + /** 白板:K+HH:MM:SS 时间块比较 */ + compareWhiteboardTimeBlock(a, b) { + const parse = (s) => { + const m = /K\+(\d+):(\d+):(\d+)/.exec(s) + if (!m) return 0 + return parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10) + } + return parse(a) - parse(b) + }, + + /** 白板:进入/退出白板模式 */ + async toggleWhiteboardMode() { + if (this.showWhiteboardPanel) { + this.handleWhiteboardExit() + return + } + this.showWhiteboardPanel = true + this.drawDom = false + this.airspaceDrawDom = false + this.whiteboardAirspaceDraw = false + this.isRightPanelHidden = true + this.show4TPanel = false + await this.loadWhiteboards() + if (this.whiteboards.length === 0) { + await this.handleWhiteboardCreate() + } else { + this.currentWhiteboard = this.whiteboards[0] + const blocks = this.currentWhiteboard.timeBlocks || [] + this.currentWhiteboardTimeBlock = blocks.length > 0 ? blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b))[0] : null + } + }, + + async loadWhiteboards() { + if (!this.currentRoomId) return + try { + const res = await listWhiteboards(this.currentRoomId) + this.whiteboards = (res.data || res) || [] + } catch (e) { + this.whiteboards = [] + } + }, + + handleWhiteboardSelect(id) { + const wb = this.whiteboards.find(w => w.id === id) + if (wb) { + this.currentWhiteboard = wb + const blocks = (wb.timeBlocks || []).slice().sort((a, b) => this.compareWhiteboardTimeBlock(a, b)) + this.currentWhiteboardTimeBlock = blocks.length > 0 ? blocks[0] : null + } + }, + + async handleWhiteboardCreate(name) { + if (!this.currentRoomId) { + this.$message.warning('请先进入任务房间') + return + } + try { + const res = await createWhiteboard(this.currentRoomId, { name: name || '白板方案' }) + const wb = res.data || res + this.whiteboards.push(wb) + this.currentWhiteboard = wb + this.currentWhiteboardTimeBlock = null + this.$message.success('已创建白板') + } catch (e) { + this.$message.error('创建白板失败') + } + }, + + async handleWhiteboardRename(id, name) { + if (!this.currentRoomId || !id || !name) return + const wb = this.whiteboards.find(w => w.id === id) + if (!wb) return + try { + const updated = { ...wb, name } + await updateWhiteboard(this.currentRoomId, id, updated) + wb.name = name + if (this.currentWhiteboard && this.currentWhiteboard.id === id) this.currentWhiteboard = updated + this.$message.success('已重命名') + } catch (e) { + this.$message.error('重命名失败') + } + }, + async handleWhiteboardDelete(id) { + if (!this.currentRoomId || !id) return + try { + await deleteWhiteboard(this.currentRoomId, id) + this.whiteboards = this.whiteboards.filter(w => w.id !== id) + if (this.currentWhiteboard && this.currentWhiteboard.id === id) { + this.currentWhiteboard = this.whiteboards[0] || null + this.currentWhiteboardTimeBlock = this.currentWhiteboard && (this.currentWhiteboard.timeBlocks || []).length > 0 + ? (this.currentWhiteboard.timeBlocks || []).sort((a, b) => this.compareWhiteboardTimeBlock(a, b))[0] + : null + } + this.$message.success('已删除') + } catch (e) { + this.$message.error('删除失败') + } + }, + handleWhiteboardExit() { + this.showWhiteboardPanel = false + this.whiteboardAirspaceDraw = false + this.currentWhiteboard = null + this.currentWhiteboardTimeBlock = null + this.loadRoomDrawings() + }, + + handleWhiteboardTimeBlockSelect(tb) { + this.currentWhiteboardTimeBlock = tb + }, + + async handleWhiteboardAddTimeBlock(tb) { + if (!this.currentWhiteboard) return + const blocks = [...(this.currentWhiteboard.timeBlocks || [])] + if (blocks.includes(tb)) { + this.$message.warning('该时间块已存在') + return + } + blocks.push(tb) + blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b)) + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + contentByTime[tb] = { entities: [] } + await this.saveCurrentWhiteboard({ timeBlocks: blocks, contentByTime }) + this.currentWhiteboardTimeBlock = tb + }, + + async handleWhiteboardRenameTimeBlock(oldTb, newTb) { + if (!this.currentWhiteboard || oldTb === newTb) return + const blocks = [...(this.currentWhiteboard.timeBlocks || [])] + const idx = blocks.indexOf(oldTb) + if (idx < 0) return + blocks[idx] = newTb + blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b)) + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + if (contentByTime[oldTb]) { + contentByTime[newTb] = contentByTime[oldTb] + delete contentByTime[oldTb] + } + await this.saveCurrentWhiteboard({ timeBlocks: blocks, contentByTime }) + this.currentWhiteboardTimeBlock = newTb + }, + + async handleWhiteboardDeleteTimeBlock(tb) { + if (!this.currentWhiteboard) return + const blocks = (this.currentWhiteboard.timeBlocks || []).filter(t => t !== tb) + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + delete contentByTime[tb] + await this.saveCurrentWhiteboard({ timeBlocks: blocks, contentByTime }) + this.currentWhiteboardTimeBlock = blocks.length > 0 ? blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b))[0] : null + }, + + handleWhiteboardDrawModeChange(mode) { + this.whiteboardAirspaceDraw = mode === 'airspace' + }, + + handleWhiteboardDrawComplete(entityData) { + if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock) return + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + currentContent.entities = [...(currentContent.entities || []), entityData] + contentByTime[this.currentWhiteboardTimeBlock] = currentContent + this.saveCurrentWhiteboard({ contentByTime }) + }, + + /** 白板平台拖拽/旋转后更新 contentByTime 并保存 */ + handleWhiteboardPlatformUpdated(entityData) { + if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !entityData || !entityData.id) return + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + const ents = [...(currentContent.entities || [])] + const idx = ents.findIndex(e => e.id === entityData.id) + if (idx >= 0) { + const updated = { ...ents[idx], lat: entityData.lat, lng: entityData.lng, heading: entityData.heading != null ? entityData.heading : 0 } + if (entityData.iconScale != null) updated.iconScale = entityData.iconScale + ents[idx] = updated + contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } + this.saveCurrentWhiteboard({ contentByTime }) + } + }, + + /** 白板平台从右键菜单删除后,从 contentByTime 移除并保存 */ + handleWhiteboardEntityDeleted(entityData) { + if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !entityData || !entityData.id) return + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + const ents = (currentContent.entities || []).filter(e => e.id !== entityData.id) + contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } + this.saveCurrentWhiteboard({ contentByTime }) + }, + + /** 白板空域/图形编辑后(调整位置、修改属性等)更新 contentByTime 并保存 */ + handleWhiteboardDrawingUpdated(entityData) { + if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !entityData || !entityData.id) return + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + const ents = [...(currentContent.entities || [])] + const idx = ents.findIndex(e => e.id === entityData.id) + if (idx >= 0) { + ents[idx] = entityData + contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } + this.saveCurrentWhiteboard({ contentByTime }) + } + }, + + async saveCurrentWhiteboard(patch) { + if (!this.currentRoomId || !this.currentWhiteboard) return + const wb = { ...this.currentWhiteboard, ...patch } + try { + await updateWhiteboard(this.currentRoomId, wb.id, wb) + this.currentWhiteboard = wb + const i = this.whiteboards.findIndex(w => w.id === wb.id) + if (i >= 0) this.whiteboards[i] = wb + } catch (e) { + this.$message.error('保存白板失败') + } + }, + handleDeleteMenuItem(deletedItem) { const index = this.menuItems.findIndex(item => item.id === deletedItem.id) if (index > -1) { @@ -2702,14 +3064,48 @@ export default { if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'copy' }, - /** 将平台图标放置到地图上,并保存到当前房间 */ + /** 将平台图标放置到地图上,并保存到当前房间;白板模式下添加到白板当前时间块 */ async handleMapDrop(ev) { ev.preventDefault() const raw = ev.dataTransfer && ev.dataTransfer.getData('application/json') if (!raw) return try { - const platform = JSON.parse(raw) + const data = JSON.parse(raw) + const platform = data.type === 'whiteboardPlatform' ? data.platform : data const map = this.$refs.cesiumMap + + if (this.showWhiteboardPanel && data.type === 'whiteboardPlatform') { + if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock) { + this.$message.warning('请先选择或添加时间块') + return + } + const pos = map && typeof map.getLatLngFromScreen === 'function' ? map.getLatLngFromScreen(ev.clientX, ev.clientY) : null + if (!pos) { + this.$message.warning('请将图标放置到地图有效区域内') + return + } + const entityId = 'wb_' + Date.now() + '_' + Math.random().toString(36).slice(2) + const entity = { + id: entityId, + type: 'platformIcon', + platformId: platform.id, + platform, + platformName: platform.name || '', + lat: pos.lat, + lng: pos.lng, + heading: 0, + label: platform.name || '平台', + color: platform.color || '#008aff' + } + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + currentContent.entities = [...(currentContent.entities || []), entity] + contentByTime[this.currentWhiteboardTimeBlock] = currentContent + await this.saveCurrentWhiteboard({ contentByTime }) + this.$message.success('已添加到白板') + return + } + if (!map || typeof map.addPlatformIconFromDrag !== 'function') return const entityData = map.addPlatformIconFromDrag(platform, ev.clientX, ev.clientY) if (!entityData || !this.currentRoomId) return @@ -2934,6 +3330,9 @@ export default { } else if (item.id === '4t') { // 4T:切换4T悬浮窗显示 this.show4TPanel = !this.show4TPanel; + } else if (item.id === 'whiteboard') { + // 白板:进入/退出白板模式 + this.toggleWhiteboardMode(); } else if (item.id === 'start') { // 如果当前已经是冲突标签页,则关闭右侧面板 if (this.activeRightTab === 'conflict' && !this.isRightPanelHidden) { @@ -3055,16 +3454,21 @@ export default { // 右上角作战时间随时与推演时间轴同步(无论是否展示航线) this.combatTime = this.currentTime; this.updateDeductionPositions(); - // 有房间且已选航线时,按房间+航线组合从 Redis 加载导弹(仅变化时加载一次) + // 有房间且已选航线时,按房间+航线组合从 Redis 加载导弹(仅变化时加载一次);大房间时用 plan.roomId const roomId = this.currentRoomId; const routeIds = (this.activeRouteIds || []).slice().sort((a, b) => (a - b)); + const isParentRoom = this.roomDetail && this.roomDetail.parentId == null; if (roomId != null && routeIds.length > 0 && this.$refs.cesiumMap && typeof this.$refs.cesiumMap.loadMissilesFromRedis === 'function') { - const loadKey = roomId + '-' + routeIds.join(','); + const routePlatforms = this.routes + .filter(r => routeIds.includes(r.id)) + .map(r => { + const plan = this.plans.find(p => p.id === r.scenarioId); + const rId = (isParentRoom && plan && plan.roomId) ? plan.roomId : roomId; + return { routeId: r.id, platformId: r.platformId != null ? r.platformId : 0, roomId: rId }; + }); + const loadKey = routePlatforms.map(r => `${r.roomId}-${r.routeId}`).sort().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); } @@ -3113,14 +3517,20 @@ export default { this.reloadMissilesAfterRouteChange(); }, - /** 根据当前选中的航线重新加载导弹(删除航线后需调用以清除地图上的导弹) */ + /** 根据当前选中的航线重新加载导弹(删除航线后需调用以清除地图上的导弹);大房间时用 plan.roomId */ reloadMissilesAfterRouteChange() { const roomId = this.currentRoomId; if (roomId == null || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadMissilesFromRedis !== 'function') return; + const isParentRoom = this.roomDetail && this.roomDetail.parentId == null; const routeIds = (this.activeRouteIds || []).slice().sort((a, b) => (a - b)); const routePlatforms = this.routes .filter(r => routeIds.includes(r.id)) - .map(r => ({ routeId: r.id, platformId: r.platformId != null ? r.platformId : 0 })); + .map(r => { + const plan = this.plans.find(p => p.id === r.scenarioId); + const rId = (isParentRoom && plan && plan.roomId) ? plan.roomId : roomId; + return { routeId: r.id, platformId: r.platformId != null ? r.platformId : 0, roomId: rId }; + }); + this._missilesLoadKey = routePlatforms.map(r => `${r.roomId}-${r.routeId}`).sort().join(','); this.$refs.cesiumMap.loadMissilesFromRedis(roomId, routePlatforms); }, diff --git a/ruoyi-ui/src/views/selectRoom/index.vue b/ruoyi-ui/src/views/selectRoom/index.vue index 15a3e34..92d884a 100644 --- a/ruoyi-ui/src/views/selectRoom/index.vue +++ b/ruoyi-ui/src/views/selectRoom/index.vue @@ -21,7 +21,7 @@
@@ -90,8 +90,9 @@ @@ -169,6 +170,25 @@ export default { computed: { getParentRooms() { return this.rooms.filter(room => room.parentId === null) + }, + /** 选中的房间对象 */ + selectedRoomObj() { + if (!this.selectedRoom) return null + return this.rooms.find(r => r.id === this.selectedRoom) + }, + /** 是否可进入:小房间直接进;大房间需有子房间才能进 */ + canEnterRoom() { + if (!this.selectedRoom || !this.selectedRoomObj) return false + if (this.selectedRoomObj.parentId != null) return true // 小房间 + return this.getChildRooms(this.selectedRoom).length > 0 // 大房间需有子房间 + }, + /** 进入按钮禁用时的提示 */ + enterRoomDisabledTip() { + if (!this.selectedRoom) return '请先选择房间' + if (!this.selectedRoomObj) return '' + if (this.selectedRoomObj.parentId != null) return '' + if (this.getChildRooms(this.selectedRoom).length === 0) return '该大房间暂无子房间,无法进入' + return '' } }, methods: { @@ -185,8 +205,9 @@ export default { }).catch(() => {}) }, getList() { - listRooms().then(response => { - this.rooms = response.rows || response; + // 传 pageSize 拉取全部房间,避免分页只返回前 10 条导致新建房间不显示 + listRooms({ pageNum: 1, pageSize: 9999 }).then(response => { + this.rooms = response.rows || response || []; }).catch(err => { console.error("加载列表失败,请检查后端接口权限", err); }); @@ -194,12 +215,21 @@ export default { getChildRooms(parentId) { return this.rooms.filter(room => room.parentId === parentId) }, + /** 大房间点击:第一次展开,第二次选中 */ + handleParentRoomClick(room) { + const isExpanded = this.expandedRooms.includes(room.id) + if (!isExpanded) { + this.expandedRooms.push(room.id) + } else { + this.selectedRoom = room.id + } + }, toggleRoomExpansion(room) { const index = this.expandedRooms.indexOf(room.id) if (index > -1) { this.expandedRooms.splice(index, 1) } else { - this.expandedRooms = [room.id] + this.expandedRooms.push(room.id) } }, selectRoom(room) {