Browse Source

白板功能

ctw
cuitw 3 weeks ago
parent
commit
afe9fc768d
  1. 63
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java
  2. 124
      ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java
  3. 13
      ruoyi-system/src/main/java/com/ruoyi/system/domain/MissionScenario.java
  4. 34
      ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java
  5. 3
      ruoyi-system/src/main/resources/mapper/system/MissionScenarioMapper.xml
  6. 5
      ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml
  7. 43
      ruoyi-ui/src/api/system/whiteboard.js
  8. 21
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  9. 52
      ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
  10. 755
      ruoyi-ui/src/views/cesiumMap/index.vue
  11. 13
      ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue
  12. 185
      ruoyi-ui/src/views/childRoom/BottomTimeline.vue
  13. 82
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  14. 579
      ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue
  15. 488
      ruoyi-ui/src/views/childRoom/index.vue
  16. 42
      ruoyi-ui/src/views/selectRoom/index.vue

63
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<Object> 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("删除失败");
}
}

124
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<String, Object> redisTemplate;
private String whiteboardsKey(Long roomId) {
return ROOM_WHITEBOARDS_PREFIX + roomId + ROOM_WHITEBOARDS_SUFFIX;
}
/** 获取房间下所有白板列表 */
@SuppressWarnings("unchecked")
public List<Object> 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<Object>) 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<Object> 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<Object> list = listWhiteboards(roomId);
String id = UUID.randomUUID().toString();
Object wb = whiteboard;
if (wb instanceof java.util.Map) {
((java.util.Map<String, Object>) wb).put("id", id);
if (!((java.util.Map<String, Object>) wb).containsKey("name")) {
((java.util.Map<String, Object>) wb).put("name", "草稿");
}
if (!((java.util.Map<String, Object>) wb).containsKey("timeBlocks")) {
((java.util.Map<String, Object>) wb).put("timeBlocks", new ArrayList<String>());
}
if (!((java.util.Map<String, Object>) wb).containsKey("contentByTime")) {
((java.util.Map<String, Object>) wb).put("contentByTime", new java.util.HashMap<String, Object>());
}
}
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<Object> 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<String, Object>) 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<Object> 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<Object> list) {
String key = whiteboardsKey(roomId);
redisTemplate.opsForValue().set(key, list, EXPIRE_HOURS, TimeUnit.HOURS);
}
}

13
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)

34
ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java

@ -47,6 +47,12 @@ public class Routes extends BaseEntity {
private List<RouteWaypoints> waypoints;
/** 方案ID列表(查询用,非持久化):传入时查询 scenario_id IN (...) */
private java.util.List<Long> scenarioIds;
/** 方案ID逗号分隔(查询用,便于前端传参):如 "1,2,3" */
private String scenarioIdsStr;
/** 关联的平台信息(仅用于 API 返回,非数据库字段) */
private java.util.Map<String, Object> platform;
@ -98,6 +104,34 @@ public class Routes extends BaseEntity {
this.waypoints = waypoints;
}
public java.util.List<Long> getScenarioIds() {
return scenarioIds;
}
public void setScenarioIds(java.util.List<Long> 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<Long> 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<String, Object> getPlatform() {
return platform;
}

3
ruoyi-system/src/main/resources/mapper/system/MissionScenarioMapper.xml

@ -20,7 +20,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectMissionScenarioList" parameterType="MissionScenario" resultMap="MissionScenarioResult">
<include refid="selectMissionScenarioVo"/>
<where>
<if test="roomId != null "> and room_id = #{roomId}</if>
<if test="parentRoomId != null"> and room_id in (select id from ry.rooms where parent_id = #{parentRoomId})</if>
<if test="parentRoomId == null and roomId != null "> and room_id = #{roomId}</if>
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
<if test="version != null and version != ''"> and version = #{version}</if>
<if test="frontendDrawings != null and frontendDrawings != ''"> and frontend_drawings = #{frontendDrawings}</if>

5
ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml

@ -20,7 +20,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectRoutesList" parameterType="Routes" resultMap="RoutesResult">
<include refid="selectRoutesVo"/>
<where>
<if test="scenarioId != null "> and scenario_id = #{scenarioId}</if>
<if test="scenarioIds != null and scenarioIds.size() > 0"> and scenario_id in
<foreach collection="scenarioIds" item="sid" open="(" separator="," close=")">#{sid}</foreach>
</if>
<if test="(scenarioIds == null or scenarioIds.size() == 0) and scenarioId != null "> and scenario_id = #{scenarioId}</if>
<if test="platformId != null "> and platform_id = #{platformId}</if>
<if test="callSign != null and callSign != ''"> and call_sign = #{callSign}</if>
<if test="attributes != null and attributes != ''"> and attributes = #{attributes}</if>

43
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'
})
}

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

@ -404,9 +404,13 @@
</div>
</div>
<!-- 平台图标拖拽到地图的图标特有选项 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon'">
<!-- 平台图标拖拽到地图的图标特有选项白板平台不显示探测区/威力区/航线 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon' && !entityData.isWhiteboard">
<div class="menu-title">平台图标</div>
<div class="menu-item" @click="handleShowTransformBox">
<span class="menu-icon">🔄</span>
<span>显示伸缩框</span>
</div>
<div class="menu-item" @click="handleDetectionZonePlatform">
<span class="menu-icon">🔍</span>
<span>探测区</span>
@ -433,6 +437,15 @@
<span>在此之后插入航线</span>
</div>
</div>
<!-- 白板平台仅显示伸缩框用于旋转 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon' && entityData.isWhiteboard">
<div class="menu-title">白板平台</div>
<div class="menu-item" @click="handleShowTransformBox">
<span class="menu-icon">🔄</span>
<span>显示伸缩框</span>
</div>
</div>
</div>
</template>
@ -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')
},

52
ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue

@ -13,6 +13,19 @@
<i v-else :class="item.icon"></i>
</div>
</div>
<!-- 辅助线水平/竖直约束选项仅空域模式且选中辅助线时显示 -->
<div v-if="toolMode === 'airspace' && drawingMode === 'auxiliaryLine'" class="auxiliary-constraint-row">
<div
v-for="opt in auxiliaryConstraintOptions"
:key="opt.value"
class="constraint-option"
:class="{ active: auxiliaryLineConstraint === opt.value }"
@click.stop="$emit('auxiliary-line-constraint', opt.value)"
:title="opt.label"
>
{{ opt.label }}
</div>
</div>
</div>
</template>
@ -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;

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

File diff suppressed because it is too large

13
ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue

@ -5,6 +5,7 @@
ref="timeline"
@timeline-hidden="onTimelineHidden"
:room-id="roomId"
:is-parent-room="isParentRoom"
/>
<div class="bottom-left-panel" v-show="showPanel">
@ -22,6 +23,10 @@
<i class="el-icon-s-operation"></i>
<span>六步法</span>
</div>
<div class="panel-item" @click="toggleWhiteboard">
<i class="el-icon-edit"></i>
<span>白板</span>
</div>
</div>
</div>
@ -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 {

185
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 + ']' : '')"
>
<div class="marker-dot"></div>
<div class="marker-tooltip">
<div class="tooltip-time">{{ segment.time }}</div>
<div class="tooltip-name">{{ segment.name }}</div>
<div v-if="segment.roomName" class="tooltip-room">{{ segment.roomName }}</div>
</div>
</div>
</div>
@ -107,6 +108,7 @@
<div class="segment-time">{{ segment.time }}</div>
<div class="segment-info">
<div class="segment-name">{{ segment.name }}</div>
<div v-if="segment.roomName" class="segment-room">{{ segment.roomName }}</div>
<div class="segment-desc">{{ segment.description }}</div>
</div>
<div class="segment-status">
@ -173,6 +175,10 @@
<label>名称</label>
<span>{{ currentSegment.name }}</span>
</div>
<div v-if="currentSegment.roomName" class="detail-row">
<label>所属房间</label>
<span>{{ currentSegment.roomName }}</span>
</div>
<div class="detail-row">
<label>描述</label>
<span>{{ currentSegment.description }}</span>
@ -194,6 +200,7 @@
<script>
import { getTimelineSegmentsByRoomId, addTimelineSegment, updateTimelineSegment, delTimelineSegment } from '@/api/system/timeline'
import { listRooms } from '@/api/system/rooms'
export default {
name: 'BottomTimeline',
@ -201,6 +208,11 @@ export default {
roomId: {
type: [Number, String],
default: null
},
/** 是否为大房间(parent_id 为 null),大房间时合并展示所有子房间的时间段 */
isParentRoom: {
type: Boolean,
default: false
}
},
data() {
@ -246,6 +258,9 @@ export default {
}
},
immediate: true
},
isParentRoom() {
if (this.roomId) this.loadTimelineSegments()
}
},
methods: {
@ -286,21 +301,26 @@ export default {
try {
this.isLoading = true
const response = await getTimelineSegmentsByRoomId(this.roomId)
if (response.code === 200 && response.data && response.data.length > 0) {
this.timelineSegments = response.data.map(item => ({
segmentId: item.segmentId,
time: item.segmentTime,
name: item.segmentName,
description: item.segmentDesc,
active: false,
passed: false,
position: 0,
triggered: false
}))
this.updateTimeline()
if (this.isParentRoom) {
await this.loadMergedTimelineSegments()
} else {
this.initDefaultSegments()
const response = await getTimelineSegmentsByRoomId(this.roomId)
if (response.code === 200 && response.data && response.data.length > 0) {
this.timelineSegments = response.data.map(item => ({
segmentId: item.segmentId,
time: item.segmentTime,
name: item.segmentName,
description: item.segmentDesc,
roomName: null,
active: false,
passed: false,
position: 0,
triggered: false
}))
this.updateTimeline()
} else {
this.initDefaultSegments()
}
}
} catch (error) {
console.error('加载时间轴数据失败:', error)
@ -310,6 +330,81 @@ export default {
}
},
/** 大房间时:查询所有子房间的时间段,相同时间的合并为一条,按时间排序,并标注所属子房间名称 */
async loadMergedTimelineSegments() {
const roomsRes = await listRooms({ pageNum: 1, pageSize: 999, parentId: this.roomId })
const childRooms = roomsRes.rows || roomsRes || []
if (childRooms.length === 0) {
this.initDefaultSegments()
return
}
const allSegments = []
for (const child of childRooms) {
try {
const res = await getTimelineSegmentsByRoomId(child.id)
if (res.code === 200 && res.data && res.data.length > 0) {
const roomName = child.name || `房间${child.id}`
res.data.forEach(item => {
allSegments.push({
segmentId: item.segmentId,
time: item.segmentTime,
name: item.segmentName,
description: item.segmentDesc,
roomName,
active: false,
passed: false,
position: 0,
triggered: false
})
})
}
} catch (_) {}
}
if (allSegments.length > 0) {
this.timelineSegments = this.mergeSegmentsByTime(allSegments)
this.updateTimeline()
} else {
this.initDefaultSegments()
}
},
/** 将相同时间的多条事件合并为一条,roomName 为所属房间名用顿号连接;名称不同时用顿号连接 */
mergeSegmentsByTime(segments) {
const byTime = {}
segments.forEach(seg => {
const t = seg.time
if (!byTime[t]) {
byTime[t] = []
}
byTime[t].push(seg)
})
const merged = []
Object.keys(byTime).forEach(t => {
const list = byTime[t]
if (list.length === 1) {
merged.push(list[0])
} else {
const roomNames = [...new Set(list.map(s => s.roomName))].join('、')
const names = [...new Set(list.map(s => s.name).filter(Boolean))]
const mergedName = names.length > 1 ? names.join('、') : (list[0].name || '')
const descs = [...new Set(list.map(s => s.description).filter(Boolean))]
const mergedDesc = descs.length > 1 ? descs.join(';') : (list[0].description || '')
merged.push({
segmentId: list[0].segmentId,
time: list[0].time,
name: mergedName,
description: mergedDesc,
roomName: roomNames,
active: false,
passed: false,
position: 0,
triggered: false
})
}
})
return merged.sort((a, b) => this.timeToSeconds(a.time) - this.timeToSeconds(b.time))
},
initDefaultTime() {
const now = new Date()
this.startTime = new Date(now)
@ -319,53 +414,7 @@ export default {
},
initDefaultSegments() {
this.timelineSegments = [
{
time: '08:00:00',
name: '任务准备',
description: '开始准备任务所需的资源和设备',
active: false,
passed: false,
position: 0,
triggered: false
},
{
time: '10:00:00',
name: '资源调配',
description: '完成资源的调配和分配',
active: false,
passed: false,
position: 0,
triggered: false
},
{
time: '12:00:00',
name: '任务执行',
description: '开始执行主要任务',
active: false,
passed: false,
position: 0,
triggered: false
},
{
time: '14:00:00',
name: '任务监控',
description: '监控任务执行进度',
active: false,
passed: false,
position: 0,
triggered: false
},
{
time: '16:00:00',
name: '任务完成',
description: '任务完成,进行总结',
active: false,
passed: false,
position: 0,
triggered: false
}
]
this.timelineSegments = []
this.updateTimeline()
},
@ -781,6 +830,18 @@ export default {
font-weight: 500;
}
.tooltip-room {
font-size: 12px;
color: #64748b;
margin-top: 4px;
}
.segment-room {
font-size: 12px;
color: #64748b;
margin-bottom: 2px;
}
.timeline-current {
position: absolute;
top: 0;

82
ruoyi-ui/src/views/childRoom/TopHeader.vue

@ -255,12 +255,25 @@
<div
class="info-box combat-time-box"
:class="{ 'clickable': true }"
@click="$emit('set-k-time')"
:class="{ 'clickable': !(childRoomKTimes && childRoomKTimes.length > 0) && canSetKTime }"
>
<i class="el-icon-timer info-icon"></i>
<div class="info-content combat-time-content">
<div v-if="kTimeDisplay" class="combat-k-time">
<template v-if="childRoomKTimes && childRoomKTimes.length > 0">
<el-dropdown trigger="click" @command="handleSelectChildKTime" class="k-time-dropdown">
<div class="combat-k-time k-time-selectable">
<span class="k-time-label">{{ currentChildKTimeItem.name }}:</span>
<span class="k-time-value">{{ formatChildKTime(currentChildKTimeItem.kAnchorTime) }}</span>
<i class="el-icon-arrow-down k-time-arrow"></i>
</div>
<el-dropdown-menu slot="dropdown" class="k-time-dropdown-menu">
<el-dropdown-item v-for="(item, idx) in childRoomKTimes" :key="idx" :command="idx">
{{ item.name }}: {{ formatChildKTime(item.kAnchorTime) }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<div v-else-if="kTimeDisplay" class="combat-k-time" @click="canSetKTime && $emit('set-k-time')">
<span class="k-time-label">K </span>
<span class="k-time-value">{{ kTimeDisplay }}</span>
</div>
@ -334,11 +347,21 @@ export default {
type: String,
default: 'K+01:30:45'
},
/** 格式化的 K 时基准时刻,如 "2025-02-06 08:00:00" */
/** 格式化的 K 时基准时刻,如 "2025-02-06 08:00:00";小房间时使用 */
kTimeDisplay: {
type: String,
default: ''
},
/** 大房间时多个子房间的 K 时,格式 [{ name, kAnchorTime }],有值时优先展示此列表 */
childRoomKTimes: {
type: Array,
default: () => []
},
/** 大房间时当前选中的子房间索引 */
selectedChildKTimeIndex: {
type: Number,
default: 0
},
astroTime: {
type: String,
default: ''
@ -388,10 +411,17 @@ export default {
}
},
computed: {
/** 显示数据库房间名,无则回退为房间编号 */
/** 大房间时当前选中的子房间 K 时项 */
currentChildKTimeItem() {
if (!this.childRoomKTimes || this.childRoomKTimes.length === 0) return { name: '-', kAnchorTime: null };
const idx = Math.max(0, Math.min(this.selectedChildKTimeIndex, this.childRoomKTimes.length - 1));
return this.childRoomKTimes[idx];
},
/** 显示数据库房间名,无则回退为房间编号;大房间时追加标识 */
roomDisplayName() {
if (this.roomDetail && this.roomDetail.name) return this.roomDetail.name;
return this.roomCode;
const name = (this.roomDetail && this.roomDetail.name) ? this.roomDetail.name : this.roomCode;
const isParentRoom = this.roomDetail && this.roomDetail.parentId == null;
return isParentRoom ? `${name}(大房间)` : name;
},
topNavItems() {
return [
@ -624,6 +654,22 @@ export default {
this.$emit('show-online-members')
},
handleSelectChildKTime(idx) {
this.$emit('select-child-k-time', idx);
},
formatChildKTime(val) {
if (!val) return '-'
const d = new Date(val)
if (isNaN(d.getTime())) return '-'
const y = d.getFullYear()
const m = (d.getMonth() + 1).toString().padStart(2, '0')
const day = d.getDate().toString().padStart(2, '0')
const h = d.getHours().toString().padStart(2, '0')
const min = d.getMinutes().toString().padStart(2, '0')
const s = d.getSeconds().toString().padStart(2, '0')
return `${y}-${m}-${day} ${h}:${min}:${s}`
},
//
savePowerZone(powerZone) {
this.$emit('save-power-zone', powerZone)
@ -953,8 +999,30 @@ export default {
.combat-time-box {
min-width: 180px;
}
.k-time-dropdown {
width: 100%;
}
.k-time-selectable {
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
}
.k-time-selectable:hover {
color: #008aff;
}
.k-time-arrow {
font-size: 12px;
margin-left: 4px;
color: #008aff;
}
.k-time-dropdown-menu .el-dropdown-menu__item {
padding: 8px 16px;
}
.combat-time-content {
gap: 6px;
flex-wrap: wrap;
}
.combat-k-time {
display: flex;

579
ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue

@ -0,0 +1,579 @@
<template>
<div class="whiteboard-panel" v-if="visible">
<!-- 第一行时间选择 | 绘制空域 | 白板方案 | 新建 | 退出 -->
<div class="wb-row wb-row-main">
<!-- 时间选择左侧 -->
<div class="wb-time-section">
<span class="wb-label">时间选择</span>
<div class="wb-time-blocks">
<el-tag
v-for="tb in sortedTimeBlocks"
:key="tb"
:type="currentTimeBlock === tb ? 'primary' : 'info'"
size="small"
class="wb-time-tag"
@click="selectTimeBlock(tb)"
>
{{ tb }}
</el-tag>
<el-button type="text" size="mini" @click="showAddTimeBlock = true" title="添加时间">
<i class="el-icon-plus"></i>
</el-button>
</div>
<el-popover placement="bottom" width="200" trigger="click" v-if="currentTimeBlock">
<div class="wb-time-edit">
<el-button type="text" size="small" @click="openModifyTimeBlock">修改时间</el-button>
<el-button type="text" size="small" style="color: #F56C6C;" @click="deleteCurrentTimeBlock">删除</el-button>
</div>
<el-button slot="reference" type="text" size="mini"><i class="el-icon-more"></i></el-button>
</el-popover>
</div>
<!-- 绘制空域时间选择右侧 -->
<div class="wb-tools-section">
<span class="wb-label">绘制</span>
<el-button size="mini" :type="drawMode === 'airspace' ? 'primary' : 'default'" @click="toggleAirspaceDraw">
空域
</el-button>
</div>
<!-- 白板方案选择新建退出与时间选择同高 -->
<div class="wb-draft-actions">
<span class="wb-label">白板方案</span>
<div class="wb-draft-select-wrap">
<el-select
v-model="currentWhiteboardId"
placeholder="选择白板方案"
size="small"
filterable
allow-create
default-first-option
class="wb-draft-select"
@change="onWhiteboardChange"
@create="(name) => $emit('create-whiteboard', name)"
>
<el-option
v-for="wb in whiteboards"
:key="wb.id"
:label="wb.name || '未命名'"
:value="wb.id"
/>
</el-select>
<el-dropdown v-if="currentWhiteboard" trigger="click" @command="onDraftCommand">
<el-button type="text" size="mini" class="wb-draft-more-btn"><i class="el-icon-more"></i></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="rename"><i class="el-icon-edit"></i> 重命名</el-dropdown-item>
<el-dropdown-item command="delete" divided><i class="el-icon-delete"></i> 删除白板方案</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<el-button type="text" size="small" @click="$emit('create-whiteboard')" title="新建白板">
<i class="el-icon-plus"></i> 新建
</el-button>
<el-button type="text" size="small" @click="exitWhiteboard" title="退出白板">
<i class="el-icon-close"></i> 退出
</el-button>
</div>
</div>
<!-- 第二行平台空中 | 海上 | 地面 -->
<div class="wb-row wb-row-platform">
<span class="wb-label">平台</span>
<el-radio-group v-model="platformFilter" size="mini" class="wb-platform-filter">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="air">空中</el-radio-button>
<el-radio-button label="sea">海上</el-radio-button>
<el-radio-button label="ground">地面</el-radio-button>
</el-radio-group>
<div class="wb-platform-grid">
<div
v-for="p in filteredPlatforms"
:key="p.id"
class="wb-platform-item"
draggable="true"
@dragstart="onPlatformDragStart($event, p)"
>
<div class="wb-platform-icon" :style="{ color: p.color || '#008aff' }">
<img v-if="isImg(p.imageUrl || p.iconUrl)" :src="formatImg(p.imageUrl || p.iconUrl)" class="wb-platform-img" />
<i v-else :class="p.icon || 'el-icon-picture-outline'"></i>
</div>
<span class="wb-platform-name">{{ p.name }}</span>
</div>
</div>
</div>
<!-- 添加时间块弹窗 -->
<el-dialog title="添加时间块" :visible.sync="showAddTimeBlock" width="400px" append-to-body @close="newTimeBlockValue = null; newTimeBlockInput = ''">
<el-form label-width="100px" size="small">
<el-form-item label="快捷选择">
<div class="time-presets">
<el-tag
v-for="preset in timeBlockPresets"
:key="preset.value"
class="preset-tag"
@click="selectTimePreset(preset.value)"
>
{{ preset.label }}
</el-tag>
</div>
</el-form-item>
<el-form-item label="选择时间">
<el-time-picker
v-model="newTimeBlockValue"
format="HH:mm:ss"
value-format="HH:mm:ss"
placeholder="选择 K+ 后的时间"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="或手动输入">
<el-input v-model="newTimeBlockInput" placeholder="如 K+00:05:00" size="small" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="showAddTimeBlock = false">取消</el-button>
<el-button type="primary" @click="addTimeBlock">确定</el-button>
</span>
</el-dialog>
<!-- 重命名白板方案弹窗 -->
<el-dialog title="重命名白板方案" :visible.sync="showRenameWhiteboardDialog" width="400px" append-to-body @open="initRenameWhiteboardDialog" @close="renameWhiteboardName = ''">
<el-form label-width="80px" size="small">
<el-form-item label="方案名称">
<el-input v-model="renameWhiteboardName" placeholder="请输入白板方案名称" @keyup.enter.native="commitRenameWhiteboardDialog" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="showRenameWhiteboardDialog = false">取消</el-button>
<el-button type="primary" @click="commitRenameWhiteboardDialog">确定</el-button>
</span>
</el-dialog>
<!-- 修改时间弹窗与新建时间块同结构 -->
<el-dialog title="修改时间" :visible.sync="showRenameTimeBlock" width="400px" append-to-body @open="initModifyTimeBlock" @close="renameTimeBlockValue = null; renameTimeBlockInput = ''">
<el-form label-width="100px" size="small">
<el-form-item label="快捷选择">
<div class="time-presets">
<el-tag
v-for="preset in timeBlockPresets"
:key="'rename-' + preset.value"
class="preset-tag"
@click="selectRenamePreset(preset.value)"
>
{{ preset.label }}
</el-tag>
</div>
</el-form-item>
<el-form-item label="选择时间">
<el-time-picker
v-model="renameTimeBlockValue"
format="HH:mm:ss"
value-format="HH:mm:ss"
placeholder="选择 K+ 后的时间"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="或手动输入">
<el-input v-model="renameTimeBlockInput" placeholder="如 K+00:10:00" size="small" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="showRenameTimeBlock = false">取消</el-button>
<el-button type="primary" @click="renameTimeBlock">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'WhiteboardPanel',
props: {
visible: {
type: Boolean,
default: false
},
roomId: {
type: [String, Number],
default: null
},
whiteboards: {
type: Array,
default: () => []
},
currentWhiteboard: {
type: Object,
default: null
},
airPlatforms: {
type: Array,
default: () => []
},
seaPlatforms: {
type: Array,
default: () => []
},
groundPlatforms: {
type: Array,
default: () => []
}
},
data() {
return {
currentWhiteboardId: null,
currentTimeBlock: null,
drawMode: null,
platformFilter: 'all',
showRenameWhiteboardDialog: false,
renameWhiteboardName: '',
showAddTimeBlock: false,
showRenameTimeBlock: false,
newTimeBlockValue: null,
newTimeBlockInput: '',
renameTimeBlockValue: null,
renameTimeBlockInput: ''
}
},
computed: {
timeBlockPresets() {
return [
{ label: 'K+0', value: 'K+00:00:00' },
{ label: 'K+5', value: 'K+00:05:00' },
{ label: 'K+10', value: 'K+00:10:00' },
{ label: 'K+15', value: 'K+00:15:00' },
{ label: 'K+30', value: 'K+00:30:00' },
{ label: 'K+60', value: 'K+01:00:00' },
{ label: 'K+2h', value: 'K+02:00:00' }
]
},
sortedTimeBlocks() {
const wb = this.currentWhiteboard
if (!wb || !Array.isArray(wb.timeBlocks)) return []
return [...wb.timeBlocks].sort((a, b) => this.compareTimeBlock(a, b))
},
allPlatforms() {
return [
...(this.airPlatforms || []),
...(this.seaPlatforms || []),
...(this.groundPlatforms || [])
]
},
filteredPlatforms() {
if (this.platformFilter === 'all') return this.allPlatforms
if (this.platformFilter === 'air') return this.airPlatforms || []
if (this.platformFilter === 'sea') return this.seaPlatforms || []
if (this.platformFilter === 'ground') return this.groundPlatforms || []
return this.allPlatforms
}
},
watch: {
currentWhiteboard: {
handler(wb) {
if (wb) {
this.currentWhiteboardId = wb.id
const blocks = wb.timeBlocks || []
if (blocks.length > 0 && !blocks.includes(this.currentTimeBlock)) {
this.currentTimeBlock = this.sortedTimeBlocks[0] || blocks[0]
} else if (blocks.length === 0) {
this.currentTimeBlock = null
}
} else {
this.currentWhiteboardId = null
this.currentTimeBlock = null
}
},
immediate: true
}
},
methods: {
/** 与 RightPanel 一致:判断是否为图片路径(支持 /profile/upload/ 等相对路径) */
isImg(url) {
if (!url || typeof url !== 'string') return false
return url.includes('/') || url.includes('data:image') || /\.(png|jpg|jpeg|gif|webp|svg)(\?|$)/i.test(url)
},
/** 与 RightPanel 一致:拼接后端地址,图片需通过完整 URL 加载 */
formatImg(url) {
if (!url) return ''
if (url.startsWith('http') || url.startsWith('//') || url.startsWith('data:')) return url
const cleanPath = (url || '').replace(/\/+/g, '/')
const backendUrl = process.env.VUE_APP_BACKEND_URL || process.env.VUE_APP_BASE_API || ''
return backendUrl ? backendUrl + cleanPath : url
},
compareTimeBlock(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)
},
onWhiteboardChange(id) {
this.$emit('select-whiteboard', id)
},
initRenameWhiteboardDialog() {
this.renameWhiteboardName = this.currentWhiteboard ? (this.currentWhiteboard.name || '白板方案') : ''
},
commitRenameWhiteboardDialog() {
const name = (this.renameWhiteboardName || '').trim()
if (!name || !this.currentWhiteboard) {
this.$message.warning('请输入方案名称')
return
}
this.$emit('rename-whiteboard', this.currentWhiteboard.id, name)
this.showRenameWhiteboardDialog = false
this.renameWhiteboardName = ''
},
onDraftCommand(cmd) {
if (cmd === 'rename' && this.currentWhiteboard) {
this.showRenameWhiteboardDialog = true
} else if (cmd === 'delete' && this.currentWhiteboard) {
this.$confirm('确定删除该白板方案吗?', '提示', {
type: 'warning'
}).then(() => this.$emit('delete-whiteboard', this.currentWhiteboard.id)).catch(() => {})
}
},
createNewWhiteboard() {
this.$emit('create-whiteboard')
},
exitWhiteboard() {
this.$emit('exit-whiteboard')
},
selectTimeBlock(tb) {
this.currentTimeBlock = tb
this.$emit('select-time-block', tb)
},
selectTimePreset(value) {
this.$emit('add-time-block', value)
this.newTimeBlockValue = null
this.newTimeBlockInput = ''
this.showAddTimeBlock = false
},
addTimeBlock() {
let timeStr = ''
if (this.newTimeBlockValue) {
timeStr = 'K+' + this.newTimeBlockValue
} else {
const input = (this.newTimeBlockInput || '').trim()
if (!input) {
this.$message.warning('请选择时间或输入格式,如 K+00:05:00')
return
}
if (!/^K\+\d+:\d{2}:\d{2}$/.test(input)) {
this.$message.warning('格式应为 K+HH:MM:SS,如 K+00:05:00')
return
}
timeStr = input
}
this.$emit('add-time-block', timeStr)
this.newTimeBlockValue = null
this.newTimeBlockInput = ''
this.showAddTimeBlock = false
},
openModifyTimeBlock() {
this.showRenameTimeBlock = true
},
initModifyTimeBlock() {
if (!this.currentTimeBlock) return
const m = /K\+(\d+):(\d{2}):(\d{2})/.exec(this.currentTimeBlock)
if (m) {
this.renameTimeBlockValue = `${String(m[1]).padStart(2, '0')}:${m[2]}:${m[3]}`
this.renameTimeBlockInput = this.currentTimeBlock
} else {
this.renameTimeBlockValue = null
this.renameTimeBlockInput = this.currentTimeBlock
}
},
selectRenamePreset(value) {
this.$emit('rename-time-block', this.currentTimeBlock, value)
this.renameTimeBlockValue = null
this.renameTimeBlockInput = ''
this.showRenameTimeBlock = false
},
renameTimeBlock() {
let timeStr = ''
if (this.renameTimeBlockValue) {
timeStr = 'K+' + this.renameTimeBlockValue
} else {
const input = (this.renameTimeBlockInput || '').trim()
if (!input) {
this.$message.warning('请选择时间或输入格式,如 K+00:05:00')
return
}
if (!/^K\+\d+:\d{2}:\d{2}$/.test(input)) {
this.$message.warning('格式应为 K+HH:MM:SS,如 K+00:05:00')
return
}
timeStr = input
}
if (!this.currentTimeBlock) return
this.$emit('rename-time-block', this.currentTimeBlock, timeStr)
this.renameTimeBlockValue = null
this.renameTimeBlockInput = ''
this.showRenameTimeBlock = false
},
deleteCurrentTimeBlock() {
if (!this.currentTimeBlock) return
this.$emit('delete-time-block', this.currentTimeBlock)
},
onPlatformDragStart(evt, platform) {
evt.dataTransfer.setData('application/json', JSON.stringify({
type: 'whiteboardPlatform',
platform: platform
}))
evt.dataTransfer.effectAllowed = 'copy'
},
toggleAirspaceDraw() {
this.drawMode = this.drawMode === 'airspace' ? null : 'airspace'
this.$emit('draw-mode-change', this.drawMode)
}
}
}
</script>
<style scoped>
.whiteboard-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(0, 138, 255, 0.2);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
z-index: 85;
padding: 10px 16px;
max-height: 200px;
overflow-y: auto;
}
.wb-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 16px;
}
.wb-row-main {
margin-bottom: 10px;
}
.wb-row-platform {
align-items: flex-start;
width: 100%;
}
.wb-time-section,
.wb-tools-section,
.wb-draft-actions {
display: flex;
align-items: center;
gap: 6px;
}
.wb-draft-actions {
margin-left: auto;
}
.wb-draft-select-wrap {
display: flex;
align-items: center;
gap: 2px;
}
.wb-draft-select {
width: 140px;
}
.wb-draft-more-btn {
padding: 4px;
}
.wb-label {
font-size: 12px;
color: #606266;
flex-shrink: 0;
}
.wb-time-blocks {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.wb-time-tag {
cursor: pointer;
}
.wb-platform-filter {
flex-shrink: 0;
}
.wb-platform-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
gap: 6px;
flex: 1;
min-width: 0;
max-height: 70px;
overflow-y: auto;
}
.wb-platform-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px;
border-radius: 4px;
cursor: grab;
background: rgba(0, 138, 255, 0.06);
transition: background 0.2s;
}
.wb-platform-item:hover {
background: rgba(0, 138, 255, 0.15);
}
.wb-platform-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.wb-platform-img {
width: 24px;
height: 24px;
object-fit: contain;
}
.wb-platform-name {
font-size: 10px;
color: #606266;
max-width: 44px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wb-time-edit {
display: flex;
gap: 8px;
}
.time-presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.preset-tag {
cursor: pointer;
}
.preset-tag:hover {
opacity: 0.85;
}
</style>

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

@ -9,9 +9,11 @@
@drop="handleMapDrop"
>
<!-- cesiummap组件 -->
<cesiumMap ref="cesiumMap" :drawDomClick="drawDom || airspaceDrawDom"
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
<cesiumMap ref="cesiumMap" :drawDomClick="drawDom || airspaceDrawDom || (showWhiteboardPanel && whiteboardAirspaceDraw)"
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom || (showWhiteboardPanel && whiteboardAirspaceDraw) ? 'airspace' : 'airspace')"
:scaleConfig="scaleConfig"
:whiteboard-mode="showWhiteboardPanel"
:whiteboard-entities="whiteboardDisplayEntities"
:coordinateFormat="coordinateFormat"
:bottomPanelVisible="bottomPanelVisible"
:map-drag-enabled="mapDragEnabled"
@ -39,7 +41,11 @@
@platform-icon-removed="onPlatformIconRemoved"
@viewer-ready="onViewerReady"
@drawing-entities-changed="onDrawingEntitiesChanged"
@platform-style-saved="onPlatformStyleSaved" />
@platform-style-saved="onPlatformStyleSaved"
@whiteboard-draw-complete="handleWhiteboardDrawComplete"
@whiteboard-platform-updated="handleWhiteboardPlatformUpdated"
@whiteboard-entity-deleted="handleWhiteboardEntityDeleted"
@whiteboard-drawing-updated="handleWhiteboardDrawingUpdated" />
<div v-show="!screenshotMode" class="map-overlay-text">
<!-- <i class="el-icon-location-outline text-3xl mb-2 block"></i> -->
<!-- <p>二维GIS地图区域</p>
@ -94,6 +100,9 @@
:online-count="onlineCount"
:combat-time="combatTime"
:k-time-display="kTimeDisplay"
:child-room-k-times="childRoomKTimes"
:selected-child-k-time-index="selectedChildKTimeIndex"
@select-child-k-time="selectedChildKTimeIndex = $event"
:astro-time="astroTime"
:room-detail="roomDetail"
:can-set-k-time="canSetKTime"
@ -210,8 +219,8 @@
@delete-platform="handleDeletePlatform"
@open-import-dialog="showImportDialog = true"
/>
<!-- 左下角工具面板 -->
<bottom-left-panel v-show="!screenshotMode" @bottom-panel-visible="handleBottomPanelVisible" @six-steps-overlay-visible="sixStepsOverlayVisible = $event" :room-id="currentRoomId" />
<!-- 左下角工具面板白板模式下隐藏避免遮挡白板 -->
<bottom-left-panel v-show="!screenshotMode && !showWhiteboardPanel" @bottom-panel-visible="handleBottomPanelVisible" @six-steps-overlay-visible="sixStepsOverlayVisible = $event" @open-whiteboard="toggleWhiteboardMode" :room-id="currentRoomId" :is-parent-room="!!(roomDetail && roomDetail.parentId == null)" />
<!-- 底部时间轴最初版本的样式- 蓝色主题 -->
<div
v-show="!screenshotMode"
@ -383,6 +392,28 @@
:room-id="currentRoomId"
/>
<!-- 白板面板底部 -->
<whiteboard-panel
v-show="showWhiteboardPanel && !screenshotMode"
:visible="showWhiteboardPanel"
:room-id="currentRoomId"
:whiteboards="whiteboards"
:current-whiteboard="currentWhiteboard"
:air-platforms="airPlatforms"
:sea-platforms="seaPlatforms"
:ground-platforms="groundPlatforms"
@select-whiteboard="handleWhiteboardSelect"
@create-whiteboard="handleWhiteboardCreate"
@rename-whiteboard="handleWhiteboardRename"
@delete-whiteboard="handleWhiteboardDelete"
@exit-whiteboard="handleWhiteboardExit"
@select-time-block="handleWhiteboardTimeBlockSelect"
@add-time-block="handleWhiteboardAddTimeBlock"
@rename-time-block="handleWhiteboardRenameTimeBlock"
@delete-time-block="handleWhiteboardDeleteTimeBlock"
@draw-mode-change="handleWhiteboardDrawModeChange"
/>
<el-dialog
title="新建方案"
:visible.sync="showPlanNameDialog"
@ -442,14 +473,16 @@ import RightPanel from './RightPanel'
import BottomLeftPanel from './BottomLeftPanel'
import TopHeader from './TopHeader'
import FourTPanel from './FourTPanel'
import WhiteboardPanel from './WhiteboardPanel'
import { createRoomWebSocket } from '@/utils/websocket';
import { listScenario, addScenario, delScenario } from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes, getPlatformStyle, getMissileParams, updateMissilePositions } from "@/api/system/routes";
import { updateWaypoints, addWaypoints, delWaypoints } from "@/api/system/waypoints";
import { listLib,addLib,delLib} from "@/api/system/lib";
import { getRooms, updateRooms } from "@/api/system/rooms";
import { getRooms, updateRooms, listRooms } from "@/api/system/rooms";
import { getMenuConfig, saveMenuConfig } from "@/api/system/userMenuConfig";
import { listByRoomId as listRoomPlatformIcons, addRoomPlatformIcon, updateRoomPlatformIcon, delRoomPlatformIcon } from "@/api/system/roomPlatformIcon";
import { listWhiteboards, getWhiteboard, createWhiteboard, updateWhiteboard, deleteWhiteboard } from "@/api/system/whiteboard";
import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue";
export default {
name: 'MissionPlanningView',
@ -468,7 +501,8 @@ export default {
RightPanel,
BottomLeftPanel,
TopHeader,
FourTPanel
FourTPanel,
WhiteboardPanel
},
data() {
return {
@ -536,6 +570,10 @@ export default {
combatTime: 'K+00:00:00', //
astroTime: '',
roomDetail: null,
/** 大房间时子房间的 K 时列表 [{ name, kAnchorTime }] */
childRoomKTimes: [],
/** 大房间时当前选中的子房间 K 时索引,用于切换展示 */
selectedChildKTimeIndex: 0,
showKTimeSetDialog: false,
kTimeForm: { dateTime: null },
saveRoomDrawingsTimer: null,
@ -616,6 +654,13 @@ export default {
// 4T4T/
show4TPanel: false,
//
showWhiteboardPanel: false,
whiteboards: [],
currentWhiteboard: null,
currentWhiteboardTimeBlock: null,
whiteboardAirspaceDraw: false,
// /
showAirport: true,
showLandmark: true,
@ -674,7 +719,7 @@ export default {
if (newRoomId != null && String(newRoomId) !== String(this.currentRoomId)) {
this.currentRoomId = newRoomId;
this.connectRoomWebSocket();
if (newRoomId) this.getRoomDetail();
if (newRoomId) this.getRoomDetail(() => 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') {
// 4T4T
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);
},

42
ruoyi-ui/src/views/selectRoom/index.vue

@ -21,7 +21,7 @@
<div
class="room-item parent-room"
:class="{ 'active': selectedRoom === room.id }"
@click="toggleRoomExpansion(room)"
@click="handleParentRoomClick(room)"
@contextmenu.prevent="showContextMenu($event, room)"
>
<div class="room-info">
@ -90,8 +90,9 @@
<button
@click="enterRoom"
class="btn-primary"
:disabled="!selectedRoom"
:class="{ 'disabled': !selectedRoom }"
:disabled="!canEnterRoom"
:class="{ 'disabled': !canEnterRoom }"
:title="enterRoomDisabledTip"
>
<i class="fa fa-sign-in"></i> 进入房间
</button>
@ -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) {

Loading…
Cancel
Save