Compare commits

...

5 Commits

Author SHA1 Message Date
cuitw c375780052 联网内容修改 1 month ago
cuitw 64dbbbe495 Merge branch 'mh' of http://124.70.32.114:3100/woka/cesium-map-object into ctw 1 month ago
menghao 347d791d39 编辑/选中状态2.0 1 month ago
menghao 3592696855 Merge branch 'ctw' of http://124.70.32.114:3100/woka/cesium-map-object into mh 1 month ago
menghao 14783654fa 编辑/选中状态 1 month ago
  1. 76
      ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java
  2. 34
      ruoyi-ui/src/utils/websocket.js
  3. 7
      ruoyi-ui/src/views/cesiumMap/index.vue
  4. 90
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  5. 184
      ruoyi-ui/src/views/childRoom/index.vue
  6. 7
      ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue
  7. 2
      ruoyi-ui/vue.config.js

76
ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java

@ -1,8 +1,10 @@
package com.ruoyi.websocket.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
@ -19,6 +21,7 @@ import com.ruoyi.system.domain.Rooms;
import com.ruoyi.system.service.IRoomsService;
import com.ruoyi.websocket.dto.RoomMemberDTO;
import com.ruoyi.websocket.service.RoomChatService;
import com.ruoyi.websocket.service.RoomRoomStateService;
import com.ruoyi.websocket.service.RoomWebSocketService;
/**
@ -41,6 +44,9 @@ public class RoomWebSocketController {
@Autowired
private IRoomsService roomsService;
@Autowired
private RoomRoomStateService roomRoomStateService;
private static final String TYPE_JOIN = "JOIN";
private static final String TYPE_LEAVE = "LEAVE";
private static final String TYPE_PING = "PING";
@ -58,9 +64,16 @@ public class RoomWebSocketController {
private static final String TYPE_SYNC_PLATFORM_ICONS = "SYNC_PLATFORM_ICONS";
private static final String TYPE_SYNC_ROOM_DRAWINGS = "SYNC_ROOM_DRAWINGS";
private static final String TYPE_SYNC_PLATFORM_STYLES = "SYNC_PLATFORM_STYLES";
/** 新用户加入时下发的房间状态(可见航线等) */
private static final String TYPE_ROOM_STATE = "ROOM_STATE";
/** 对象编辑锁定:某成员进入编辑,其他人看到锁定 */
private static final String TYPE_OBJECT_EDIT_LOCK = "OBJECT_EDIT_LOCK";
/** 对象编辑解锁 */
private static final String TYPE_OBJECT_EDIT_UNLOCK = "OBJECT_EDIT_UNLOCK";
/**
* 处理房间消息JOINLEAVEPINGCHATPRIVATE_CHATSYNC_*
* 处理房间消息JOINLEAVEPINGCHATPRIVATE_CHATOBJECT_VIEWOBJECT_EDIT_LOCK
*/
@MessageMapping("/room/{roomId}")
public void handleRoomMessage(@DestinationVariable Long roomId, @Payload String payload,
@ -100,9 +113,47 @@ public class RoomWebSocketController {
handleSyncRoomDrawings(roomId, body, sessionId);
} else if (TYPE_SYNC_PLATFORM_STYLES.equals(type)) {
handleSyncPlatformStyles(roomId, body, sessionId);
} else if (TYPE_OBJECT_EDIT_LOCK.equals(type)) {
handleObjectEditLock(roomId, sessionId, loginUser, body);
} else if (TYPE_OBJECT_EDIT_UNLOCK.equals(type)) {
handleObjectEditUnlock(roomId, sessionId, loginUser, body);
}
}
/** 广播:某成员锁定某对象进入编辑 */
private void handleObjectEditLock(Long roomId, String sessionId, LoginUser loginUser, Map<String, Object> body) {
String objectType = body != null ? String.valueOf(body.get("objectType")) : null;
Object objectIdObj = body != null ? body.get("objectId") : null;
if (objectType == null || objectIdObj == null) return;
Map<String, Object> editor = new HashMap<>();
editor.put("userId", loginUser.getUserId());
editor.put("userName", loginUser.getUsername());
editor.put("nickName", loginUser.getUser().getNickName());
editor.put("sessionId", sessionId);
Map<String, Object> msg = new HashMap<>();
msg.put("type", TYPE_OBJECT_EDIT_LOCK);
msg.put("objectType", objectType);
msg.put("objectId", objectIdObj);
msg.put("editor", editor);
messagingTemplate.convertAndSend("/topic/room/" + roomId, msg);
}
/** 广播:某成员解锁某对象(结束编辑) */
private void handleObjectEditUnlock(Long roomId, String sessionId, LoginUser loginUser, Map<String, Object> body) {
String objectType = body != null ? String.valueOf(body.get("objectType")) : null;
Object objectIdObj = body != null ? body.get("objectId") : null;
if (objectType == null || objectIdObj == null) return;
Map<String, Object> msg = new HashMap<>();
msg.put("type", TYPE_OBJECT_EDIT_UNLOCK);
msg.put("objectType", objectType);
msg.put("objectId", objectIdObj);
msg.put("sessionId", sessionId);
messagingTemplate.convertAndSend("/topic/room/" + roomId, msg);
}
@SuppressWarnings("unchecked")
private Map<String, Object> parsePayload(String payload) {
try {
@ -136,6 +187,14 @@ public class RoomWebSocketController {
chatHistoryMsg.put("type", TYPE_CHAT_HISTORY);
chatHistoryMsg.put("messages", chatHistory);
messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", chatHistoryMsg);
Set<Long> visibleRouteIds = roomRoomStateService.getVisibleRouteIds(roomId);
if (visibleRouteIds != null && !visibleRouteIds.isEmpty()) {
Map<String, Object> roomStateMsg = new HashMap<>();
roomStateMsg.put("type", TYPE_ROOM_STATE);
roomStateMsg.put("visibleRouteIds", new ArrayList<>(visibleRouteIds));
messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", roomStateMsg);
}
}
private void handleLeave(Long roomId, String sessionId, LoginUser loginUser) {
@ -237,13 +296,26 @@ public class RoomWebSocketController {
messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", resp);
}
/** 广播航线显隐变更,供其他设备实时同步 */
/** 广播航线显隐变更,供其他设备实时同步;并持久化到 Redis 供新加入用户同步 */
private void handleSyncRouteVisibility(Long roomId, Map<String, Object> body, String sessionId) {
if (body == null || !body.containsKey("routeId")) return;
Object routeIdObj = body.get("routeId");
boolean visible = body.get("visible") != null && Boolean.TRUE.equals(body.get("visible"));
Long routeId = null;
if (routeIdObj instanceof Number) {
routeId = ((Number) routeIdObj).longValue();
} else if (routeIdObj != null) {
try {
routeId = Long.parseLong(routeIdObj.toString());
} catch (NumberFormatException ignored) {}
}
if (routeId != null) {
roomRoomStateService.updateRouteVisibility(roomId, routeId, visible);
}
Map<String, Object> msg = new HashMap<>();
msg.put("type", TYPE_SYNC_ROUTE_VISIBILITY);
msg.put("routeId", body.get("routeId"));
msg.put("visible", body.get("visible") != null && Boolean.TRUE.equals(body.get("visible")));
msg.put("visible", visible);
msg.put("senderSessionId", sessionId);
messagingTemplate.convertAndSend("/topic/room/" + roomId, msg);
}

34
ruoyi-ui/src/utils/websocket.js

@ -23,6 +23,9 @@ const WS_BASE = process.env.VUE_APP_BASE_API || '/dev-api'
* @param {Function} options.onSyncPlatformIcons - 平台图标变更同步回调 (senderUserId) => {}
* @param {Function} options.onSyncRoomDrawings - 空域图形变更同步回调 (senderUserId) => {}
* @param {Function} options.onSyncPlatformStyles - 探测区/威力区样式变更同步回调 (senderUserId) => {}
* @param {Function} options.onRoomState - 新加入时收到的房间状态 (visibleRouteIds: number[]) => {}
* @param {Function} options.onObjectEditLock - 对象被某成员编辑锁定 (msg: { objectType, objectId, editor }) => {}
* @param {Function} options.onObjectEditUnlock - 对象编辑解锁 (msg: { objectType, objectId, sessionId }) => {}
* @param {Function} options.onConnected - 连接成功回调
* @param {Function} options.onDisconnected - 断开回调
* @param {Function} options.onError - 错误回调
@ -43,6 +46,9 @@ export function createRoomWebSocket(options) {
onSyncPlatformIcons,
onSyncRoomDrawings,
onSyncPlatformStyles,
onRoomState,
onObjectEditLock,
onObjectEditUnlock,
onConnected,
onDisconnected,
onError,
@ -163,6 +169,26 @@ export function createRoomWebSocket(options) {
}
}
/** 发送:当前成员锁定某对象进入编辑 */
function sendObjectEditLock(objectType, objectId) {
if (client && client.connected) {
client.publish({
destination: '/app/room/' + roomId,
body: JSON.stringify({ type: 'OBJECT_EDIT_LOCK', objectType, objectId })
})
}
}
/** 发送:当前成员解锁某对象(结束编辑) */
function sendObjectEditUnlock(objectType, objectId) {
if (client && client.connected) {
client.publish({
destination: '/app/room/' + roomId,
body: JSON.stringify({ type: 'OBJECT_EDIT_UNLOCK', objectType, objectId })
})
}
}
function startHeartbeat() {
stopHeartbeat()
heartbeatTimer = setInterval(sendPing, 30000)
@ -197,6 +223,10 @@ export function createRoomWebSocket(options) {
onSyncRoomDrawings && onSyncRoomDrawings(body.senderSessionId)
} else if (type === 'SYNC_PLATFORM_STYLES') {
onSyncPlatformStyles && onSyncPlatformStyles(body.senderSessionId)
} else if (type === 'OBJECT_EDIT_LOCK' && body.objectType != null && body.objectId != null && body.editor) {
onObjectEditLock && onObjectEditLock(body)
} else if (type === 'OBJECT_EDIT_UNLOCK' && body.objectType != null && body.objectId != null) {
onObjectEditUnlock && onObjectEditUnlock(body)
}
} catch (e) {
console.warn('[WebSocket] parse message error:', e)
@ -213,6 +243,8 @@ export function createRoomWebSocket(options) {
onChatHistory && onChatHistory(body.messages)
} else if (type === 'PRIVATE_CHAT_HISTORY' && body.targetUserId != null && Array.isArray(body.messages)) {
onPrivateChatHistory && onPrivateChatHistory(body.targetUserId, body.messages)
} else if (type === 'ROOM_STATE' && Array.isArray(body.visibleRouteIds)) {
onRoomState && onRoomState(body.visibleRouteIds)
}
} catch (e) {
console.warn('[WebSocket] parse private message error:', e)
@ -293,6 +325,8 @@ export function createRoomWebSocket(options) {
sendSyncPlatformIcons,
sendSyncRoomDrawings,
sendSyncPlatformStyles,
sendObjectEditLock,
sendObjectEditUnlock,
get connected() {
return client && client.connected
}

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

@ -367,6 +367,11 @@ export default {
type: Object,
default: () => ({})
},
/** 被其他成员编辑锁定的航线 ID 列表(禁止拖拽航点等) */
routeLockedByOtherIds: {
type: Array,
default: () => []
},
/** 推演时间轴:相对 K 的分钟数(用于导弹按时间轴显示/隐藏与位置插值) */
deductionTimeMinutes: {
type: Number,
@ -3616,6 +3621,8 @@ export default {
const dbId = props.dbId && (props.dbId.getValue ? props.dbId.getValue() : props.dbId);
if (routeId == null || dbId == null) return;
if (this.routeLocked[routeId]) return;
const rid = Number(routeId);
if (Array.isArray(this.routeLockedByOtherIds) && this.routeLockedByOtherIds.indexOf(rid) !== -1) return;
const entity = pickedObject.id;
const carto = Cesium.Cartographic.fromCartesian(entity.position.getValue(Cesium.JulianDate.now()));
this.waypointDragPending = {

90
ruoyi-ui/src/views/childRoom/RightPanel.vue

@ -59,6 +59,9 @@
<div class="tree-item-info">
<div class="tree-item-name">{{ route.name }}</div>
<div class="tree-item-meta">{{ route.points }}{{ $t('rightPanel.points') }}</div>
<div v-if="routeLockedByOther(route.id)" class="route-locked-tag">
<i class="el-icon-lock"></i> {{ routeLockedByName(route.id) }} 正在编辑
</div>
</div>
<el-tag
v-if="route.conflict"
@ -71,12 +74,28 @@
<div class="tree-item-actions">
<i class="el-icon-view" title="显示/隐藏" @click.stop="handleToggleRouteVisibility(route)"></i>
<i
v-if="!routeLockedByOther(route.id)"
:class="routeLocked[route.id] ? 'el-icon-lock' : 'el-icon-unlock'"
:title="routeLocked[route.id] ? '解锁' : '上锁'"
@click.stop="$emit('toggle-route-lock', route)"
></i>
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenRouteDialog(route)"></i>
<i class="el-icon-delete" title="删除" @click.stop="$emit('delete-route', route)"></i>
<i
v-else
class="el-icon-lock route-locked-by-other-icon"
title="被他人锁定,无法操作"
></i>
<i
class="el-icon-edit"
:title="routeLockedByOther(route.id) ? '被他人锁定,无法编辑' : '编辑'"
:class="{ 'action-disabled': routeLockedByOther(route.id) }"
@click.stop="!routeLockedByOther(route.id) && handleOpenRouteDialog(route)"
></i>
<i
class="el-icon-delete"
:class="{ 'action-disabled': routeLockedByOther(route.id) }"
title="删除"
@click.stop="!routeLockedByOther(route.id) && $emit('delete-route', route)"
></i>
</div>
</div>
<!-- 航点列表 -->
@ -296,6 +315,16 @@ export default {
type: Object,
default: () => ({})
},
/** 协作:谁正在编辑(锁定)该航线 routeId -> { userId, userName, nickName, sessionId } */
routeLockedBy: {
type: Object,
default: () => ({})
},
/** 当前用户 ID,用于判断“正在查看/锁定”是否为自己 */
currentUserId: {
type: [Number, String],
default: null
},
selectedPlanId: {
type: [String, Number],
default: null
@ -383,17 +412,9 @@ export default {
})
this.$emit('select-plan', { id: null })
} else {
// 线广
this.activeRouteIds.forEach(activeId => {
const activeRoute = this.routes.find(r => r.id === activeId);
if (activeRoute) {
this.$emit('toggle-route-visibility', activeRoute, { fromPlanSwitch: true });
}
});
//
// 线线
this.expandedPlans = [];
this.expandedRoutes = [];
// --- ---
this.expandedPlans.push(planId)
const plan = this.plans.find(p => p.id === planId)
if (plan) {
@ -503,9 +524,22 @@ export default {
getRouteClasses(routeId) {
return {
active: this.activeRouteIds.includes(routeId)
active: this.activeRouteIds.includes(routeId),
'locked-by-other': this.routeLockedByOther(routeId)
}
},
/** 是否有其他成员正在编辑(锁定)该航线 */
routeLockedByOther(routeId) {
const e = this.routeLockedBy[routeId]
if (!e) return false
const myId = this.currentUserId != null ? Number(this.currentUserId) : null
const uid = e.userId != null ? Number(e.userId) : null
return myId !== uid
},
routeLockedByName(routeId) {
const e = this.routeLockedBy[routeId]
return (e && (e.nickName || e.userName)) || ''
},
handleOpenWaypointDialog(point,index,total) {
this.$emit('open-waypoint-dialog', {
...point,
@ -741,6 +775,38 @@ export default {
color: #999;
}
.route-locked-tag {
font-size: 11px;
color: #909399;
margin-top: 2px;
}
.route-locked-tag .el-icon-lock {
margin-right: 2px;
font-size: 12px;
}
.tree-item.route-item.locked-by-other .tree-item-header {
background: rgba(240, 242, 245, 0.95) !important;
border-left: 3px solid #c0c4cc;
}
.tree-item-actions i.action-disabled {
color: #c0c4cc;
cursor: not-allowed;
opacity: 0.7;
}
.tree-item-actions i.action-disabled:hover {
background: transparent;
transform: none;
}
.route-locked-by-other-icon {
color: #909399;
cursor: default;
}
.tree-item-actions {
display: flex;
gap: 8px;

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

@ -15,6 +15,7 @@
:coordinateFormat="coordinateFormat"
:bottomPanelVisible="bottomPanelVisible"
:route-locked="routeLocked"
:route-locked-by-other-ids="routeLockedByOtherRouteIds"
:deduction-time-minutes="deductionMinutesFromK"
:room-id="currentRoomId"
@draw-complete="handleMapDrawComplete"
@ -176,6 +177,8 @@
:routes="routes"
:active-route-ids="activeRouteIds"
:route-locked="routeLocked"
:route-locked-by="routeLockedBy"
:current-user-id="currentUserId"
:selected-route-details="selectedRouteDetails"
:conflicts="conflicts"
:conflict-count="conflictCount"
@ -511,6 +514,10 @@ export default {
screenshotDataUrl: '',
screenshotFileName: '',
// 线 WebSocket 广
routeLockedBy: {}, // routeId -> { userId, userName, nickName, sessionId }
routeEditLockedId: null, // 线 ID
//
roomCode: 'JTF-7-ALPHA',
onlineCount: 0,
@ -616,6 +623,8 @@ export default {
plans: [],
activeRightTab: 'plan',
activeRouteIds: [], // 线ID
/** 新加入时收到的房间可见航线 ID,若 routes 未加载则暂存,getList 完成后应用 */
roomStatePendingVisibleRouteIds: [],
/** 航线上锁状态:routeId -> true 上锁,与地图右键及右侧列表锁图标同步 */
routeLocked: {},
// runConflictCheck 线
@ -679,6 +688,14 @@ export default {
this.timeProgress = progress;
});
}
},
/** 航线编辑弹窗关闭时:发送编辑解锁,让其他成员可编辑 */
showRouteDialog(visible) {
if (visible) return;
if (this.routeEditLockedId != null && this.wsConnection && this.wsConnection.sendObjectEditUnlock) {
this.wsConnection.sendObjectEditUnlock('route', this.routeEditLockedId);
this.routeEditLockedId = null;
}
}
},
computed: {
@ -720,6 +737,17 @@ export default {
if (!this.addHoldContext) return '';
if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。';
return `${this.addHoldContext.fromName}${this.addHoldContext.toName} 之间添加盘旋,到计划时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`;
},
/** 被其他成员编辑锁定的航线 ID 列表,供地图禁止拖拽等 */
routeLockedByOtherRouteIds() {
const myId = this.currentUserId;
if (myId == null) return Object.keys(this.routeLockedBy).map(id => Number(id));
const ids = [];
Object.keys(this.routeLockedBy).forEach(rid => {
const editor = this.routeLockedBy[rid];
if (editor && Number(editor.userId) !== Number(myId)) ids.push(Number(rid));
});
return ids;
}
},
mounted() {
@ -760,6 +788,10 @@ export default {
},
// wpId waypointIndex waypointIndex
async handleOpenWaypointEdit(wpId, routeId, waypointIndex) {
if (routeId != null && this.isRouteLockedByOther(routeId)) {
this.$message.warning('该航线正被其他成员编辑,无法修改航点');
return;
}
if (waypointIndex != null && (wpId == null || wpId === undefined)) {
try {
const response = await getRoutes(routeId);
@ -842,9 +874,23 @@ export default {
});
},
/** 该航线是否被其他成员编辑锁定(非自己) */
isRouteLockedByOther(routeId) {
const lockedBy = this.routeLockedBy[routeId];
if (!lockedBy) return false;
const myId = this.currentUserId != null ? Number(this.currentUserId) : null;
const uid = lockedBy.userId != null ? Number(lockedBy.userId) : null;
return myId !== uid;
},
// 线
async handleOpenRouteEdit(routeId) {
console.log(`>>> [父组件接收] 航线 ID: ${routeId}`);
if (this.isRouteLockedByOther(routeId)) {
const lockedBy = this.routeLockedBy[routeId];
const name = lockedBy && (lockedBy.nickName || lockedBy.userName) || '其他成员';
this.$message.warning(`${name} 正在编辑该航线,请稍后再试`);
return;
}
try {
const response = await getRoutes(routeId);
if (response.code === 200 && response.data) {
@ -871,6 +917,10 @@ export default {
/** 右键航点“向前/向后增加航点”:进入放置模式,传入 waypoints 给地图预览 */
handleAddWaypointAt({ routeId, waypointIndex, mode }) {
if (this.isRouteLockedByOther(routeId)) {
this.$message.warning('该航线正被其他成员编辑,无法添加航点');
return;
}
if (this.routeLocked[routeId]) {
this.$message.info('该航线已上锁,请先解锁');
return;
@ -998,6 +1048,10 @@ export default {
/** 右键航点“切换盘旋航点”:普通航点设为盘旋(圆形默认),盘旋航点设为普通;支持首尾航点 */
async handleToggleWaypointHold({ routeId, dbId, waypointIndex }) {
if (this.isRouteLockedByOther(routeId)) {
this.$message.warning('该航线正被其他成员编辑,无法修改');
return;
}
if (this.routeLocked[routeId]) {
this.$message.info('该航线已上锁,请先解锁');
return;
@ -1134,6 +1188,10 @@ export default {
/** 地图上拖拽航点结束:将新位置写回数据库并刷新显示 */
async handleWaypointPositionChanged({ dbId, routeId, lat, lng, alt }) {
if (this.isRouteLockedByOther(routeId)) {
this.$message.warning('该航线正被其他成员编辑,无法拖拽修改');
return;
}
let waypoints = null;
let route = null;
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
@ -1303,6 +1361,21 @@ export default {
this.wsOnlineMembers = this.wsOnlineMembers.filter(m => m.id !== sessionId && m.id !== member.sessionId);
this.onlineCount = this.wsOnlineMembers.length;
this.mySyncSessionIds = this.mySyncSessionIds.filter(s => s !== sessionId && s !== member.sessionId);
//
Object.keys(this.routeLockedBy).forEach(rid => {
if (this.routeLockedBy[rid] && this.routeLockedBy[rid].sessionId === sessionId) {
this.$delete(this.routeLockedBy, rid);
}
});
},
onObjectEditLock: (msg) => {
if (msg.objectType !== 'route' || msg.objectId == null) return;
this.$set(this.routeLockedBy, msg.objectId, msg.editor);
},
onObjectEditUnlock: (msg) => {
if (msg.objectType !== 'route' || msg.objectId == null) return;
const cur = this.routeLockedBy[msg.objectId];
if (cur && cur.sessionId === msg.sessionId) this.$delete(this.routeLockedBy, msg.objectId);
},
onChatMessage: (msg) => {
this.chatMessages = [...this.chatMessages, msg];
@ -1346,6 +1419,9 @@ export default {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncPlatformStyles();
},
onRoomState: (visibleRouteIds) => {
this.applyRoomStateVisibleRoutes(visibleRouteIds);
},
onConnected: () => {},
onDisconnected: () => {
this.onlineCount = 0;
@ -1353,6 +1429,7 @@ export default {
this.mySyncSessionIds = [];
this.chatMessages = [];
this.privateChatMessages = {};
this.routeLockedBy = {};
},
onError: (err) => {
console.warn('[WebSocket]', err);
@ -1364,12 +1441,31 @@ export default {
this.wsConnection.disconnect();
this.wsConnection = null;
}
this.roomStatePendingVisibleRouteIds = [];
this.wsOnlineMembers = [];
this.mySyncSessionIds = [];
this.onlineCount = 0;
this.chatMessages = [];
this.privateChatMessages = {};
},
/** 应用房间状态中的可见航线(新加入用户同步;若 routes 未加载则暂存) */
async applyRoomStateVisibleRoutes(visibleRouteIds) {
if (!visibleRouteIds || !Array.isArray(visibleRouteIds) || visibleRouteIds.length === 0) return;
if (!this.routes || this.routes.length === 0) {
this.roomStatePendingVisibleRouteIds = visibleRouteIds;
return;
}
for (const routeId of visibleRouteIds) {
await this.applySyncRouteVisibility(routeId, true);
}
},
/** 应用暂存的房间可见航线(getList 完成后调用) */
async applyRoomStatePending() {
if (this.roomStatePendingVisibleRouteIds.length === 0) return;
const pending = [...this.roomStatePendingVisibleRouteIds];
this.roomStatePendingVisibleRouteIds = [];
await this.applyRoomStateVisibleRoutes(pending);
},
/** 判断是否为当前连接发出的同步消息(避免自己发的消息再应用一次) */
isMySyncSession(senderSessionId) {
if (!senderSessionId) return false;
@ -1621,9 +1717,18 @@ export default {
openRouteDialog(route) {
this.selectedRoute = route;
this.showRouteDialog = true;
// 线广
if (this.wsConnection && this.wsConnection.sendObjectEditLock && route && route.id != null) {
this.wsConnection.sendObjectEditLock('route', route.id);
this.routeEditLockedId = route.id;
}
},
// 线
async updateRoute(updatedRoute) {
if (this.isRouteLockedByOther(updatedRoute.id)) {
this.$message.warning('该航线正被其他成员编辑,无法保存');
return;
}
const index = this.routes.findIndex(r => r.id === updatedRoute.id);
if (index === -1) return;
try {
@ -1732,6 +1837,10 @@ export default {
}
},
async handleDeleteRoute(route) {
if (this.isRouteLockedByOther(route.id)) {
this.$message.warning('该航线正被其他成员编辑,无法删除');
return;
}
try {
//
await this.$confirm(`确定要彻底删除航线 "${route.name}" 吗?`, '提示', {
@ -1835,6 +1944,7 @@ export default {
}).catch(() => {});
}
}
this.$nextTick(() => this.applyRoomStatePending());
}
} catch (error) {
console.error("数据加载失败:", error);
@ -1989,6 +2099,10 @@ export default {
//
openWaypointDialog(data) {
console.log(">>> [父组件接收] 编辑航点详情:", data);
if (data && data.routeId != null && this.isRouteLockedByOther(data.routeId)) {
this.$message.warning('该航线正被其他成员编辑,无法修改');
return;
}
//
this.selectedWaypoint = data;
this.showWaypointDialog = true;
@ -1996,6 +2110,11 @@ export default {
/** 航点编辑保存:更新数据库并同步地图显示 */
async updateWaypoint(updatedWaypoint) {
if (!this.selectedRouteDetails || !this.selectedRouteDetails.waypoints) return;
const routeId = updatedWaypoint.routeId != null ? updatedWaypoint.routeId : this.selectedRouteDetails.id;
if (this.isRouteLockedByOther(routeId)) {
this.$message.warning('该航线正被其他成员编辑,无法保存');
return;
}
try {
if (this.$refs.cesiumMap && updatedWaypoint.turnAngle > 0) {
updatedWaypoint.turnRadius = this.$refs.cesiumMap.getWaypointRadius(updatedWaypoint);
@ -3519,25 +3638,72 @@ export default {
const minutes = absMin % 60;
return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
},
selectPlan(plan) {
async selectPlan(plan) {
if (plan && plan.id) {
this.selectedPlanId = plan.id;
this.selectedPlanDetails = plan;
} else {
this.selectedPlanId = null;
this.selectedPlanDetails = null;
this.selectedRouteId = null;
this.selectedRouteDetails = null;
return;
}
//
this.selectedRouteId = null;
this.selectedRouteDetails = null;
this.activeRouteIds = [];
// /线 routeId
if (this.$refs.cesiumMap) {
[...this.activeRouteIds].forEach(routeId => {
this.$refs.cesiumMap.removeRouteById(routeId);
});
}
// /线
if (this.$refs.cesiumMap && this.$refs.cesiumMap.clearAllWaypoints) {
this.$refs.cesiumMap.clearAllWaypoints();
// activeRouteIds线
const planRouteIds = this.activeRouteIds.filter(id => {
const r = this.routes.find(x => x.id === id);
return r && r.scenarioId === plan.id;
});
if (planRouteIds.length > 0) {
for (const routeId of planRouteIds) {
const route = this.routes.find(r => r.id === routeId);
if (!route) continue;
try {
const res = await getRoutes(route.id);
if (res.code !== 200 || !res.data) continue;
const waypoints = res.data.waypoints || [];
const routeIndex = this.routes.findIndex(r => r.id === route.id);
if (routeIndex > -1) {
this.$set(this.routes, routeIndex, { ...this.routes[routeIndex], waypoints });
}
if (waypoints.length > 0 && this.$refs.cesiumMap) {
const roomId = this.currentRoomId;
if (roomId && route.platformId) {
try {
const styleRes = await getPlatformStyle({ roomId, routeId: route.id, platformId: route.platformId });
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(route.id, styleRes.data);
} catch (_) {}
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
}
} catch (_) {}
}
const firstId = planRouteIds[0];
const firstRoute = this.routes.find(r => r.id === firstId);
if (firstRoute && firstRoute.waypoints) {
this.selectedRouteId = firstRoute.id;
this.selectedRouteDetails = {
id: firstRoute.id,
name: firstRoute.callSign || firstRoute.name,
waypoints: firstRoute.waypoints,
platformId: firstRoute.platformId,
platform: firstRoute.platform,
attributes: firstRoute.attributes
};
}
} else {
this.selectedRouteId = null;
this.selectedRouteDetails = null;
}
console.log(`>>> [切换成功] 已进入方案: ${plan && plan.name},地图已清空,列表已展开。`);
console.log(`>>> [切换成功] 已进入方案: ${plan.name},已恢复显示 ${planRouteIds.length} 条航线`);
},
/** 切换航线:实现多选/开关逻辑 */
async selectRoute(route) {

7
ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue

@ -331,11 +331,14 @@ export default {
},
computed: {
displayOnlineMembers() {
return (this.onlineMembers && this.onlineMembers.length > 0) ? this.onlineMembers : this._mockOnlineMembers;
const list = (this.onlineMembers && this.onlineMembers.length > 0) ? this.onlineMembers : this._mockOnlineMembers;
return Array.isArray(list) ? list : [];
},
chatableMembers() {
const list = this.displayOnlineMembers;
if (!Array.isArray(list)) return [];
const cur = this.currentUserId != null ? Number(this.currentUserId) : null;
return this.displayOnlineMembers.filter(m => m.userId != null && Number(m.userId) !== cur);
return list.filter(m => m.userId != null && Number(m.userId) !== cur);
},
displayChatMessages() {
return this.chatMessages || [];

2
ruoyi-ui/vue.config.js

@ -14,7 +14,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
// 引入 CopyWebpackPlugin 用于复制 Cesium 静态资源
const CopyWebpackPlugin = require('copy-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '网络化任务规划系统' // 网页标题
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const baseUrl = 'http://127.0.0.1:8080' // 后端接口
const port = process.env.port || process.env.npm_config_port || 80 // 端口

Loading…
Cancel
Save