From 14783654fa61ec17d4e4eeb60d6f98a7b81fd3ea Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Wed, 4 Mar 2026 10:45:16 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=BC=96=E8=BE=91/=E9=80=89=E4=B8=AD?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomWebSocketController.java | 86 ++++++++++++++++- ruoyi-ui/.env.development | 2 +- ruoyi-ui/src/utils/websocket.js | 60 ++++++++++++ ruoyi-ui/src/views/childRoom/RightPanel.vue | 107 ++++++++++++++++++++- ruoyi-ui/src/views/childRoom/index.vue | 70 ++++++++++++++ ruoyi-ui/vue.config.js | 2 +- 6 files changed, 321 insertions(+), 6 deletions(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java index 2d75553..c5f93fc 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java @@ -53,9 +53,17 @@ public class RoomWebSocketController { private static final String TYPE_MEMBER_LEFT = "MEMBER_LEFT"; private static final String TYPE_MEMBER_LIST = "MEMBER_LIST"; private static final String TYPE_PONG = "PONG"; + /** 对象选中/查看:某成员正在查看某条航线,广播给房间内其他人 */ + private static final String TYPE_OBJECT_VIEW = "OBJECT_VIEW"; + /** 取消对象查看 */ + private static final String TYPE_OBJECT_VIEW_CLEAR = "OBJECT_VIEW_CLEAR"; + /** 对象编辑锁定:某成员进入编辑,其他人看到锁定 */ + private static final String TYPE_OBJECT_EDIT_LOCK = "OBJECT_EDIT_LOCK"; + /** 对象编辑解锁 */ + private static final String TYPE_OBJECT_EDIT_UNLOCK = "OBJECT_EDIT_UNLOCK"; /** - * 处理房间消息:JOIN、LEAVE、PING、CHAT、PRIVATE_CHAT + * 处理房间消息:JOIN、LEAVE、PING、CHAT、PRIVATE_CHAT、OBJECT_VIEW、OBJECT_EDIT_LOCK */ @MessageMapping("/room/{roomId}") public void handleRoomMessage(@DestinationVariable Long roomId, @Payload String payload, @@ -85,9 +93,85 @@ public class RoomWebSocketController { handlePrivateChat(roomId, sessionId, loginUser, body); } else if (TYPE_PRIVATE_CHAT_HISTORY_REQUEST.equals(type)) { handlePrivateChatHistoryRequest(roomId, loginUser, body); + } else if (TYPE_OBJECT_VIEW.equals(type)) { + handleObjectView(roomId, sessionId, loginUser, body); + } else if (TYPE_OBJECT_VIEW_CLEAR.equals(type)) { + handleObjectViewClear(roomId, sessionId, loginUser, body); + } 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 handleObjectView(Long roomId, String sessionId, LoginUser loginUser, Map 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 viewer = new HashMap<>(); + viewer.put("userId", loginUser.getUserId()); + viewer.put("userName", loginUser.getUsername()); + viewer.put("nickName", loginUser.getUser().getNickName()); + viewer.put("sessionId", sessionId); + + Map msg = new HashMap<>(); + msg.put("type", TYPE_OBJECT_VIEW); + msg.put("objectType", objectType); + msg.put("objectId", objectIdObj); + msg.put("viewer", viewer); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + + /** 广播:某成员取消查看某对象 */ + private void handleObjectViewClear(Long roomId, String sessionId, LoginUser loginUser, Map 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 msg = new HashMap<>(); + msg.put("type", TYPE_OBJECT_VIEW_CLEAR); + msg.put("objectType", objectType); + msg.put("objectId", objectIdObj); + msg.put("sessionId", sessionId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + + /** 广播:某成员锁定某对象进入编辑 */ + private void handleObjectEditLock(Long roomId, String sessionId, LoginUser loginUser, Map 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 editor = new HashMap<>(); + editor.put("userId", loginUser.getUserId()); + editor.put("userName", loginUser.getUsername()); + editor.put("nickName", loginUser.getUser().getNickName()); + editor.put("sessionId", sessionId); + + Map 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 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 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 parsePayload(String payload) { try { diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development index b32bd7f..0e7127a 100644 --- a/ruoyi-ui/.env.development +++ b/ruoyi-ui/.env.development @@ -8,7 +8,7 @@ ENV = 'development' VUE_APP_BASE_API = '/dev-api' # 访问地址(绕过 /dev-api 代理,用于解决静态资源/图片访问 401 认证问题) -VUE_APP_BACKEND_URL = 'http://192.168.1.104:8080' +VUE_APP_BACKEND_URL = 'http://127.0.0.1:8080' # 路由懒加载 VUE_CLI_BABEL_TRANSPILE_MODULES = true diff --git a/ruoyi-ui/src/utils/websocket.js b/ruoyi-ui/src/utils/websocket.js index bc097c0..dc814fc 100644 --- a/ruoyi-ui/src/utils/websocket.js +++ b/ruoyi-ui/src/utils/websocket.js @@ -18,6 +18,10 @@ const WS_BASE = process.env.VUE_APP_BASE_API || '/dev-api' * @param {Function} options.onPrivateChat - 私聊消息回调 (msg) => {} * @param {Function} options.onChatHistory - 群聊历史回调 (messages) => {} * @param {Function} options.onPrivateChatHistory - 私聊历史回调 (targetUserId, messages) => {} + * @param {Function} options.onObjectViewing - 对象被某成员查看 (msg: { objectType, objectId, viewer }) => {} + * @param {Function} options.onObjectViewClear - 取消对象查看 (msg: { objectType, objectId, sessionId }) => {} + * @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 - 错误回调 @@ -33,6 +37,10 @@ export function createRoomWebSocket(options) { onPrivateChat, onChatHistory, onPrivateChatHistory, + onObjectViewing, + onObjectViewClear, + onObjectEditLock, + onObjectEditUnlock, onConnected, onDisconnected, onError, @@ -108,6 +116,46 @@ export function createRoomWebSocket(options) { } } + /** 发送:当前成员正在查看某对象(如航线) */ + function sendObjectView(objectType, objectId) { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'OBJECT_VIEW', objectType, objectId }) + }) + } + } + + /** 发送:取消查看某对象 */ + function sendObjectViewClear(objectType, objectId) { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'OBJECT_VIEW_CLEAR', objectType, objectId }) + }) + } + } + + /** 发送:当前成员锁定某对象进入编辑 */ + 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) @@ -132,6 +180,14 @@ export function createRoomWebSocket(options) { onMemberLeft && onMemberLeft(body.member, body.sessionId) } else if (type === 'CHAT' && body.sender) { onChatMessage && onChatMessage(body) + } else if (type === 'OBJECT_VIEW' && body.objectType != null && body.objectId != null && body.viewer) { + onObjectViewing && onObjectViewing(body) + } else if (type === 'OBJECT_VIEW_CLEAR' && body.objectType != null && body.objectId != null) { + onObjectViewClear && onObjectViewClear(body) + } 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) @@ -223,6 +279,10 @@ export function createRoomWebSocket(options) { sendChat, sendPrivateChat, sendPrivateChatHistoryRequest, + sendObjectView, + sendObjectViewClear, + sendObjectEditLock, + sendObjectEditUnlock, get connected() { return client && client.connected } diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index cb80ba7..3f65034 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -59,6 +59,12 @@
{{ route.name }}
{{ route.points }}{{ $t('rightPanel.points') }}
+
+ {{ routeViewingName(route.id) }} 正在查看 +
+
+ {{ routeLockedByName(route.id) }} 正在编辑 +
- - + + + @@ -296,6 +318,21 @@ export default { type: Object, default: () => ({}) }, + /** 协作:谁正在查看该航线 routeId -> { userId, userName, nickName, sessionId } */ + routeViewingBy: { + 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 @@ -504,9 +541,35 @@ export default { getRouteClasses(routeId) { return { - active: this.activeRouteIds.includes(routeId) + active: this.activeRouteIds.includes(routeId), + 'viewing-by-other': this.routeViewingByOther(routeId), + 'locked-by-other': this.routeLockedByOther(routeId) } }, + /** 是否有其他成员正在查看该航线(非自己) */ + routeViewingByOther(routeId) { + const v = this.routeViewingBy[routeId] + if (!v) return false + const myId = this.currentUserId != null ? Number(this.currentUserId) : null + const uid = v.userId != null ? Number(v.userId) : null + return myId !== uid + }, + routeViewingName(routeId) { + const v = this.routeViewingBy[routeId] + return (v && (v.nickName || v.userName)) || '' + }, + /** 是否有其他成员正在编辑(锁定)该航线 */ + 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, @@ -742,6 +805,44 @@ export default { color: #999; } +.route-viewing-tag { + font-size: 11px; + color: #008aff; + margin-top: 2px; +} + +.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; diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 6d4dbc5..18ef6e7 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -175,6 +175,9 @@ :routes="routes" :active-route-ids="activeRouteIds" :route-locked="routeLocked" + :route-viewing-by="routeViewingBy" + :route-locked-by="routeLockedBy" + :current-user-id="currentUserId" :selected-route-details="selectedRouteDetails" :conflicts="conflicts" :conflict-count="conflictCount" @@ -510,6 +513,11 @@ export default { screenshotDataUrl: '', screenshotFileName: '', + // 协作:谁正在查看/编辑哪条航线(来自 WebSocket 广播) + routeViewingBy: {}, // routeId -> { userId, userName, nickName, sessionId } + routeLockedBy: {}, // routeId -> { userId, userName, nickName, sessionId } + routeEditLockedId: null, // 本端当前编辑锁定的航线 ID,关闭弹窗时发送解锁 + // 作战信息 roomCode: 'JTF-7-ALPHA', onlineCount: 0, @@ -667,6 +675,22 @@ export default { this.timeProgress = progress; }); } + }, + /** 选中航线变化时:广播“正在查看”或取消查看,供其他成员显示 */ + selectedRouteId: { + handler(newId, oldId) { + if (!this.wsConnection || !this.wsConnection.sendObjectViewClear || !this.wsConnection.sendObjectView) return; + if (oldId != null) this.wsConnection.sendObjectViewClear('route', oldId); + if (newId != null) this.wsConnection.sendObjectView('route', newId); + } + }, + /** 航线编辑弹窗关闭时:发送编辑解锁,让其他成员可编辑 */ + showRouteDialog(visible) { + if (visible) return; + if (this.routeEditLockedId != null && this.wsConnection && this.wsConnection.sendObjectEditUnlock) { + this.wsConnection.sendObjectEditUnlock('route', this.routeEditLockedId); + this.routeEditLockedId = null; + } } }, computed: { @@ -833,6 +857,16 @@ export default { // 处理从地图点击传来的航线编辑请求 async handleOpenRouteEdit(routeId) { console.log(`>>> [父组件接收] 航线 ID: ${routeId}`); + const lockedBy = this.routeLockedBy[routeId]; + if (lockedBy) { + const myId = this.currentUserId != null ? Number(this.currentUserId) : null; + const uid = lockedBy.userId != null ? Number(lockedBy.userId) : null; + if (myId !== uid) { + const name = lockedBy.nickName || lockedBy.userName || '其他成员'; + this.$message.warning(`${name} 正在编辑该航线,请稍后再试`); + return; + } + } try { const response = await getRoutes(routeId); if (response.code === 200 && response.data) { @@ -1279,6 +1313,35 @@ export default { onMemberLeft: (member, sessionId) => { this.wsOnlineMembers = this.wsOnlineMembers.filter(m => m.id !== sessionId && m.id !== member.sessionId); this.onlineCount = this.wsOnlineMembers.length; + // 该成员离开后清除其查看/编辑状态 + Object.keys(this.routeViewingBy).forEach(rid => { + if (this.routeViewingBy[rid] && this.routeViewingBy[rid].sessionId === sessionId) { + this.$delete(this.routeViewingBy, rid); + } + }); + Object.keys(this.routeLockedBy).forEach(rid => { + if (this.routeLockedBy[rid] && this.routeLockedBy[rid].sessionId === sessionId) { + this.$delete(this.routeLockedBy, rid); + } + }); + }, + onObjectViewing: (msg) => { + if (msg.objectType !== 'route' || msg.objectId == null) return; + this.$set(this.routeViewingBy, msg.objectId, msg.viewer); + }, + onObjectViewClear: (msg) => { + if (msg.objectType !== 'route' || msg.objectId == null) return; + const cur = this.routeViewingBy[msg.objectId]; + if (cur && cur.sessionId === msg.sessionId) this.$delete(this.routeViewingBy, msg.objectId); + }, + 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]; @@ -1308,6 +1371,8 @@ export default { this.wsOnlineMembers = []; this.chatMessages = []; this.privateChatMessages = {}; + this.routeViewingBy = {}; + this.routeLockedBy = {}; }, onError: (err) => { console.warn('[WebSocket]', err); @@ -1432,6 +1497,11 @@ 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) { diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js index b80f647..7ab699a 100644 --- a/ruoyi-ui/vue.config.js +++ b/ruoyi-ui/vue.config.js @@ -15,7 +15,7 @@ const CompressionPlugin = require('compression-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题 -const baseUrl = 'http://192.168.1.104:8080' // 后端接口 +const baseUrl = 'http://127.0.0.1:8080' // 后端接口 const port = process.env.port || process.env.npm_config_port || 80 // 端口 // 定义 Cesium 源码路径 From 347d791d39dfb1c43df579d18af03cbfec27dfbe Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Wed, 4 Mar 2026 14:43:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E7=BC=96=E8=BE=91/=E9=80=89=E4=B8=AD?= =?UTF-8?q?=E7=8A=B6=E6=80=812.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomWebSocketController.java | 42 ---------- ruoyi-ui/src/utils/websocket.js | 30 ------- ruoyi-ui/src/views/cesiumMap/index.vue | 7 ++ ruoyi-ui/src/views/childRoom/RightPanel.vue | 27 ------ ruoyi-ui/src/views/childRoom/index.vue | 96 ++++++++++++++-------- ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue | 7 +- 6 files changed, 72 insertions(+), 137 deletions(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java index be3e9a4..1a41837 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java @@ -58,10 +58,6 @@ 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_OBJECT_VIEW = "OBJECT_VIEW"; - /** 取消对象查看 */ - private static final String TYPE_OBJECT_VIEW_CLEAR = "OBJECT_VIEW_CLEAR"; /** 对象编辑锁定:某成员进入编辑,其他人看到锁定 */ private static final String TYPE_OBJECT_EDIT_LOCK = "OBJECT_EDIT_LOCK"; /** 对象编辑解锁 */ @@ -109,10 +105,6 @@ public class RoomWebSocketController { handleSyncRoomDrawings(roomId, body, sessionId); } else if (TYPE_SYNC_PLATFORM_STYLES.equals(type)) { handleSyncPlatformStyles(roomId, body, sessionId); - } else if (TYPE_OBJECT_VIEW.equals(type)) { - handleObjectView(roomId, sessionId, loginUser, body); - } else if (TYPE_OBJECT_VIEW_CLEAR.equals(type)) { - handleObjectViewClear(roomId, sessionId, loginUser, body); } else if (TYPE_OBJECT_EDIT_LOCK.equals(type)) { handleObjectEditLock(roomId, sessionId, loginUser, body); } else if (TYPE_OBJECT_EDIT_UNLOCK.equals(type)) { @@ -120,40 +112,6 @@ public class RoomWebSocketController { } } - /** 广播:某成员正在查看某对象(如航线) */ - private void handleObjectView(Long roomId, String sessionId, LoginUser loginUser, Map 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 viewer = new HashMap<>(); - viewer.put("userId", loginUser.getUserId()); - viewer.put("userName", loginUser.getUsername()); - viewer.put("nickName", loginUser.getUser().getNickName()); - viewer.put("sessionId", sessionId); - - Map msg = new HashMap<>(); - msg.put("type", TYPE_OBJECT_VIEW); - msg.put("objectType", objectType); - msg.put("objectId", objectIdObj); - msg.put("viewer", viewer); - messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); - } - - /** 广播:某成员取消查看某对象 */ - private void handleObjectViewClear(Long roomId, String sessionId, LoginUser loginUser, Map 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 msg = new HashMap<>(); - msg.put("type", TYPE_OBJECT_VIEW_CLEAR); - msg.put("objectType", objectType); - msg.put("objectId", objectIdObj); - msg.put("sessionId", sessionId); - messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); - } - /** 广播:某成员锁定某对象进入编辑 */ private void handleObjectEditLock(Long roomId, String sessionId, LoginUser loginUser, Map body) { String objectType = body != null ? String.valueOf(body.get("objectType")) : null; diff --git a/ruoyi-ui/src/utils/websocket.js b/ruoyi-ui/src/utils/websocket.js index cbc8e88..c65e40c 100644 --- a/ruoyi-ui/src/utils/websocket.js +++ b/ruoyi-ui/src/utils/websocket.js @@ -23,8 +23,6 @@ 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.onObjectViewing - 对象被某成员查看 (msg: { objectType, objectId, viewer }) => {} - * @param {Function} options.onObjectViewClear - 取消对象查看 (msg: { objectType, objectId, sessionId }) => {} * @param {Function} options.onObjectEditLock - 对象被某成员编辑锁定 (msg: { objectType, objectId, editor }) => {} * @param {Function} options.onObjectEditUnlock - 对象编辑解锁 (msg: { objectType, objectId, sessionId }) => {} * @param {Function} options.onConnected - 连接成功回调 @@ -47,8 +45,6 @@ export function createRoomWebSocket(options) { onSyncPlatformIcons, onSyncRoomDrawings, onSyncPlatformStyles, - onObjectViewing, - onObjectViewClear, onObjectEditLock, onObjectEditUnlock, onConnected, @@ -171,26 +167,6 @@ export function createRoomWebSocket(options) { } } - /** 发送:当前成员正在查看某对象(如航线) */ - function sendObjectView(objectType, objectId) { - if (client && client.connected) { - client.publish({ - destination: '/app/room/' + roomId, - body: JSON.stringify({ type: 'OBJECT_VIEW', objectType, objectId }) - }) - } - } - - /** 发送:取消查看某对象 */ - function sendObjectViewClear(objectType, objectId) { - if (client && client.connected) { - client.publish({ - destination: '/app/room/' + roomId, - body: JSON.stringify({ type: 'OBJECT_VIEW_CLEAR', objectType, objectId }) - }) - } - } - /** 发送:当前成员锁定某对象进入编辑 */ function sendObjectEditLock(objectType, objectId) { if (client && client.connected) { @@ -245,10 +221,6 @@ export function createRoomWebSocket(options) { onSyncRoomDrawings && onSyncRoomDrawings(body.senderSessionId) } else if (type === 'SYNC_PLATFORM_STYLES') { onSyncPlatformStyles && onSyncPlatformStyles(body.senderSessionId) - } else if (type === 'OBJECT_VIEW' && body.objectType != null && body.objectId != null && body.viewer) { - onObjectViewing && onObjectViewing(body) - } else if (type === 'OBJECT_VIEW_CLEAR' && body.objectType != null && body.objectId != null) { - onObjectViewClear && onObjectViewClear(body) } 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) { @@ -349,8 +321,6 @@ export function createRoomWebSocket(options) { sendSyncPlatformIcons, sendSyncRoomDrawings, sendSyncPlatformStyles, - sendObjectView, - sendObjectViewClear, sendObjectEditLock, sendObjectEditUnlock, get connected() { diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index ff77ed0..ce76970 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -361,6 +361,11 @@ export default { type: Object, default: () => ({}) }, + /** 被其他成员编辑锁定的航线 ID 列表(禁止拖拽航点等) */ + routeLockedByOtherIds: { + type: Array, + default: () => [] + }, /** 推演时间轴:相对 K 的分钟数(用于导弹按时间轴显示/隐藏与位置插值) */ deductionTimeMinutes: { type: Number, @@ -3598,6 +3603,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 = { diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index 34c9e56..c9cd3f3 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -59,9 +59,6 @@
{{ route.name }}
{{ route.points }}{{ $t('rightPanel.points') }}
-
- {{ routeViewingName(route.id) }} 正在查看 -
{{ routeLockedByName(route.id) }} 正在编辑
@@ -318,11 +315,6 @@ export default { type: Object, default: () => ({}) }, - /** 协作:谁正在查看该航线 routeId -> { userId, userName, nickName, sessionId } */ - routeViewingBy: { - type: Object, - default: () => ({}) - }, /** 协作:谁正在编辑(锁定)该航线 routeId -> { userId, userName, nickName, sessionId } */ routeLockedBy: { type: Object, @@ -541,22 +533,9 @@ export default { getRouteClasses(routeId) { return { active: this.activeRouteIds.includes(routeId), - 'viewing-by-other': this.routeViewingByOther(routeId), 'locked-by-other': this.routeLockedByOther(routeId) } }, - /** 是否有其他成员正在查看该航线(非自己) */ - routeViewingByOther(routeId) { - const v = this.routeViewingBy[routeId] - if (!v) return false - const myId = this.currentUserId != null ? Number(this.currentUserId) : null - const uid = v.userId != null ? Number(v.userId) : null - return myId !== uid - }, - routeViewingName(routeId) { - const v = this.routeViewingBy[routeId] - return (v && (v.nickName || v.userName)) || '' - }, /** 是否有其他成员正在编辑(锁定)该航线 */ routeLockedByOther(routeId) { const e = this.routeLockedBy[routeId] @@ -804,12 +783,6 @@ export default { color: #999; } -.route-viewing-tag { - font-size: 11px; - color: #008aff; - margin-top: 2px; -} - .route-locked-tag { font-size: 11px; color: #909399; diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 8677d2c..9585a92 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/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,7 +177,6 @@ :routes="routes" :active-route-ids="activeRouteIds" :route-locked="routeLocked" - :route-viewing-by="routeViewingBy" :route-locked-by="routeLockedBy" :current-user-id="currentUserId" :selected-route-details="selectedRouteDetails" @@ -514,8 +514,7 @@ export default { screenshotDataUrl: '', screenshotFileName: '', - // 协作:谁正在查看/编辑哪条航线(来自 WebSocket 广播) - routeViewingBy: {}, // routeId -> { userId, userName, nickName, sessionId } + // 协作:谁正在编辑(锁定)哪条航线(来自 WebSocket 广播) routeLockedBy: {}, // routeId -> { userId, userName, nickName, sessionId } routeEditLockedId: null, // 本端当前编辑锁定的航线 ID,关闭弹窗时发送解锁 @@ -688,14 +687,6 @@ export default { }); } }, - /** 选中航线变化时:广播“正在查看”或取消查看,供其他成员显示 */ - selectedRouteId: { - handler(newId, oldId) { - if (!this.wsConnection || !this.wsConnection.sendObjectViewClear || !this.wsConnection.sendObjectView) return; - if (oldId != null) this.wsConnection.sendObjectViewClear('route', oldId); - if (newId != null) this.wsConnection.sendObjectView('route', newId); - } - }, /** 航线编辑弹窗关闭时:发送编辑解锁,让其他成员可编辑 */ showRouteDialog(visible) { if (visible) return; @@ -744,6 +735,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() { @@ -784,6 +786,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); @@ -866,18 +872,22 @@ 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}`); - const lockedBy = this.routeLockedBy[routeId]; - if (lockedBy) { - const myId = this.currentUserId != null ? Number(this.currentUserId) : null; - const uid = lockedBy.userId != null ? Number(lockedBy.userId) : null; - if (myId !== uid) { - const name = lockedBy.nickName || lockedBy.userName || '其他成员'; - this.$message.warning(`${name} 正在编辑该航线,请稍后再试`); - return; - } + 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); @@ -905,6 +915,10 @@ export default { /** 右键航点“向前/向后增加航点”:进入放置模式,传入 waypoints 给地图预览 */ handleAddWaypointAt({ routeId, waypointIndex, mode }) { + if (this.isRouteLockedByOther(routeId)) { + this.$message.warning('该航线正被其他成员编辑,无法添加航点'); + return; + } if (this.routeLocked[routeId]) { this.$message.info('该航线已上锁,请先解锁'); return; @@ -1032,6 +1046,10 @@ export default { /** 右键航点“切换盘旋航点”:普通航点设为盘旋(圆形默认),盘旋航点设为普通;支持首尾航点 */ async handleToggleWaypointHold({ routeId, dbId, waypointIndex }) { + if (this.isRouteLockedByOther(routeId)) { + this.$message.warning('该航线正被其他成员编辑,无法修改'); + return; + } if (this.routeLocked[routeId]) { this.$message.info('该航线已上锁,请先解锁'); return; @@ -1168,6 +1186,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) { @@ -1337,27 +1359,13 @@ 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.routeViewingBy).forEach(rid => { - if (this.routeViewingBy[rid] && this.routeViewingBy[rid].sessionId === sessionId) { - this.$delete(this.routeViewingBy, rid); - } - }); + // 该成员离开后清除其编辑锁定状态 Object.keys(this.routeLockedBy).forEach(rid => { if (this.routeLockedBy[rid] && this.routeLockedBy[rid].sessionId === sessionId) { this.$delete(this.routeLockedBy, rid); } }); }, - onObjectViewing: (msg) => { - if (msg.objectType !== 'route' || msg.objectId == null) return; - this.$set(this.routeViewingBy, msg.objectId, msg.viewer); - }, - onObjectViewClear: (msg) => { - if (msg.objectType !== 'route' || msg.objectId == null) return; - const cur = this.routeViewingBy[msg.objectId]; - if (cur && cur.sessionId === msg.sessionId) this.$delete(this.routeViewingBy, msg.objectId); - }, onObjectEditLock: (msg) => { if (msg.objectType !== 'route' || msg.objectId == null) return; this.$set(this.routeLockedBy, msg.objectId, msg.editor); @@ -1416,7 +1424,6 @@ export default { this.mySyncSessionIds = []; this.chatMessages = []; this.privateChatMessages = {}; - this.routeViewingBy = {}; this.routeLockedBy = {}; }, onError: (err) => { @@ -1694,6 +1701,10 @@ export default { }, // 更新航线数据(含航点表编辑后的批量持久化) 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 { @@ -1802,6 +1813,10 @@ export default { } }, async handleDeleteRoute(route) { + if (this.isRouteLockedByOther(route.id)) { + this.$message.warning('该航线正被其他成员编辑,无法删除'); + return; + } try { // 二次确认,防止误删 await this.$confirm(`确定要彻底删除航线 "${route.name}" 吗?`, '提示', { @@ -2059,6 +2074,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; @@ -2066,6 +2085,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); diff --git a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue index 879ed38..f729a6a 100644 --- a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue +++ b/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 || [];