From 16a8b85051b0bc0c5c48d1cd1a44defdcb94825b Mon Sep 17 00:00:00 2001 From: cuitw <1051735452@qq.com> Date: Wed, 4 Mar 2026 10:34:47 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=94=E7=BD=91=E7=9A=84=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomWebSocketController.java | 62 +++++- .../com/ruoyi/common/constant/CacheConstants.java | 5 + .../java/com/ruoyi/common/utils/ServletUtils.java | 14 +- .../handle/AuthenticationEntryPointImpl.java | 3 +- .../ruoyi/framework/web/service/TokenService.java | 30 ++- ruoyi-ui/.env.development | 2 +- ruoyi-ui/src/utils/request.js | 19 +- ruoyi-ui/src/utils/websocket.js | 70 +++++++ ruoyi-ui/src/views/cesiumMap/index.vue | 174 +++++++++------- ruoyi-ui/src/views/childRoom/RightPanel.vue | 5 +- ruoyi-ui/src/views/childRoom/index.vue | 221 ++++++++++++++++++++- ruoyi-ui/vue.config.js | 4 +- 12 files changed, 519 insertions(+), 90 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..9bac0e8 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,14 @@ 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_SYNC_ROUTE_VISIBILITY = "SYNC_ROUTE_VISIBILITY"; + private static final String TYPE_SYNC_WAYPOINTS = "SYNC_WAYPOINTS"; + 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"; /** - * 处理房间消息:JOIN、LEAVE、PING、CHAT、PRIVATE_CHAT + * 处理房间消息:JOIN、LEAVE、PING、CHAT、PRIVATE_CHAT、SYNC_* */ @MessageMapping("/room/{roomId}") public void handleRoomMessage(@DestinationVariable Long roomId, @Payload String payload, @@ -85,6 +90,16 @@ public class RoomWebSocketController { handlePrivateChat(roomId, sessionId, loginUser, body); } else if (TYPE_PRIVATE_CHAT_HISTORY_REQUEST.equals(type)) { handlePrivateChatHistoryRequest(roomId, loginUser, body); + } else if (TYPE_SYNC_ROUTE_VISIBILITY.equals(type)) { + handleSyncRouteVisibility(roomId, body, sessionId); + } else if (TYPE_SYNC_WAYPOINTS.equals(type)) { + handleSyncWaypoints(roomId, body, sessionId); + } else if (TYPE_SYNC_PLATFORM_ICONS.equals(type)) { + handleSyncPlatformIcons(roomId, body, sessionId); + } else if (TYPE_SYNC_ROOM_DRAWINGS.equals(type)) { + handleSyncRoomDrawings(roomId, body, sessionId); + } else if (TYPE_SYNC_PLATFORM_STYLES.equals(type)) { + handleSyncPlatformStyles(roomId, body, sessionId); } } @@ -222,6 +237,51 @@ public class RoomWebSocketController { messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", resp); } + /** 广播航线显隐变更,供其他设备实时同步 */ + private void handleSyncRouteVisibility(Long roomId, Map body, String sessionId) { + if (body == null || !body.containsKey("routeId")) return; + Map 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("senderSessionId", sessionId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + + /** 广播航点变更,供其他设备实时同步 */ + private void handleSyncWaypoints(Long roomId, Map body, String sessionId) { + if (body == null || !body.containsKey("routeId")) return; + Map msg = new HashMap<>(); + msg.put("type", TYPE_SYNC_WAYPOINTS); + msg.put("routeId", body.get("routeId")); + msg.put("senderSessionId", sessionId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + + /** 广播平台图标变更,供其他设备实时同步 */ + private void handleSyncPlatformIcons(Long roomId, Map body, String sessionId) { + Map msg = new HashMap<>(); + msg.put("type", TYPE_SYNC_PLATFORM_ICONS); + msg.put("senderSessionId", sessionId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + + /** 广播空域图形变更,供其他设备实时同步 */ + private void handleSyncRoomDrawings(Long roomId, Map body, String sessionId) { + Map msg = new HashMap<>(); + msg.put("type", TYPE_SYNC_ROOM_DRAWINGS); + msg.put("senderSessionId", sessionId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + + /** 广播探测区/威力区样式变更,供其他设备实时同步 */ + private void handleSyncPlatformStyles(Long roomId, Map body, String sessionId) { + Map msg = new HashMap<>(); + msg.put("type", TYPE_SYNC_PLATFORM_STYLES); + msg.put("senderSessionId", sessionId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + private RoomMemberDTO buildMember(LoginUser loginUser, String sessionId, Long roomId, Map body) { RoomMemberDTO dto = new RoomMemberDTO(); dto.setUserId(loginUser.getUserId()); diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java index 0080343..1992954 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -13,6 +13,11 @@ public class CacheConstants public static final String LOGIN_TOKEN_KEY = "login_tokens:"; /** + * 用户ID -> 当前活跃 token 映射(用于单设备登录:新登录顶掉旧会话) + */ + public static final String LOGIN_USER_ID_TOKEN_KEY = "login_tokens:user:"; + + /** * 验证码 redis key */ public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java index febb603..afe546d 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java @@ -138,9 +138,21 @@ public class ServletUtils */ public static void renderString(HttpServletResponse response, String string) { + renderString(response, string, 200); + } + + /** + * 将字符串渲染到客户端(可指定 HTTP 状态码,用于 401 等认证失败场景) + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + * @param status HTTP 状态码 + */ + public static void renderString(HttpServletResponse response, String string, int status) + { try { - response.setStatus(200); + response.setStatus(status); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java index 93b7032..e37d20d 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java @@ -29,6 +29,7 @@ public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, S { int code = HttpStatus.UNAUTHORIZED; String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); - ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + // 使用 HTTP 401 状态码,便于前端 axios error 拦截器识别并触发重新登录(含被顶掉场景) + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)), HttpStatus.UNAUTHORIZED); } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java index dd4af67..b3a1318 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java @@ -94,30 +94,51 @@ public class TokenService } /** - * 删除用户身份信息 + * 删除用户身份信息(同时清除用户- token 映射,便于单设备登录) */ public void delLoginUser(String token) { if (StringUtils.isNotEmpty(token)) { String userKey = getTokenKey(token); + LoginUser loginUser = redisCache.getCacheObject(userKey); + if (loginUser != null && loginUser.getUserId() != null) { + redisCache.deleteObject(CacheConstants.LOGIN_USER_ID_TOKEN_KEY + loginUser.getUserId()); + } redisCache.deleteObject(userKey); } } /** - * 创建令牌 + * 创建令牌(单设备登录:新登录会顶掉该账号在其他设备的旧会话) * * @param loginUser 用户信息 * @return 令牌 */ public String createToken(LoginUser loginUser) { + // 踢掉该账号在其他设备的旧会话 + Long userId = loginUser.getUserId(); + if (userId != null) { + String userTokenKey = CacheConstants.LOGIN_USER_ID_TOKEN_KEY + userId; + String oldToken = redisCache.getCacheObject(userTokenKey); + if (StringUtils.isNotEmpty(oldToken)) { + redisCache.deleteObject(getTokenKey(oldToken)); + log.info("用户[{}]新设备登录,已踢掉旧会话 token={}", loginUser.getUsername(), oldToken); + } + } + String token = IdUtils.fastUUID(); loginUser.setToken(token); setUserAgent(loginUser); refreshToken(loginUser); + // 记录该用户当前活跃的 token(用于下次登录时踢掉) + if (userId != null) { + String userTokenKey = CacheConstants.LOGIN_USER_ID_TOKEN_KEY + userId; + redisCache.setCacheObject(userTokenKey, token, expireTime, TimeUnit.MINUTES); + } + Map claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); @@ -152,6 +173,11 @@ public class TokenService // 根据uuid将loginUser缓存 String userKey = getTokenKey(loginUser.getToken()); redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + // 同步刷新用户- token 映射的过期时间 + if (loginUser.getUserId() != null) { + String userTokenKey = CacheConstants.LOGIN_USER_ID_TOKEN_KEY + loginUser.getUserId(); + redisCache.setCacheObject(userTokenKey, loginUser.getToken(), expireTime, TimeUnit.MINUTES); + } } /** 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/request.js b/ruoyi-ui/src/utils/request.js index 6d5bf60..892176c 100644 --- a/ruoyi-ui/src/utils/request.js +++ b/ruoyi-ui/src/utils/request.js @@ -85,7 +85,7 @@ service.interceptors.response.use(res => { if (code === 401) { if (!isRelogin.show) { isRelogin.show = true - MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { + MessageBox.confirm('登录状态已过期或您的账号已在其他设备登录,请重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { isRelogin.show = false store.dispatch('LogOut').then(() => { location.href = '/index' @@ -109,6 +109,23 @@ service.interceptors.response.use(res => { } }, error => { + // HTTP 401:token 失效或被顶掉,触发重新登录(与 success 中 res.data.code===401 一致) + const status = error.response && error.response.status + const code = error.response && error.response.data && error.response.data.code + if (status === 401 || code === 401) { + if (!isRelogin.show) { + isRelogin.show = true + MessageBox.confirm('登录状态已过期或您的账号已在其他设备登录,请重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { + isRelogin.show = false + store.dispatch('LogOut').then(() => { + location.href = '/index' + }) + }).catch(() => { + isRelogin.show = false + }) + } + return Promise.reject('无效的会话,或者会话已过期,请重新登录。') + } console.log('err' + error) let { message } = error if (message == "Network Error") { diff --git a/ruoyi-ui/src/utils/websocket.js b/ruoyi-ui/src/utils/websocket.js index bc097c0..9f37b15 100644 --- a/ruoyi-ui/src/utils/websocket.js +++ b/ruoyi-ui/src/utils/websocket.js @@ -18,6 +18,11 @@ 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.onSyncRouteVisibility - 航线显隐同步回调 (routeId, visible, senderUserId) => {} + * @param {Function} options.onSyncWaypoints - 航点变更同步回调 (routeId, senderUserId) => {} + * @param {Function} options.onSyncPlatformIcons - 平台图标变更同步回调 (senderUserId) => {} + * @param {Function} options.onSyncRoomDrawings - 空域图形变更同步回调 (senderUserId) => {} + * @param {Function} options.onSyncPlatformStyles - 探测区/威力区样式变更同步回调 (senderUserId) => {} * @param {Function} options.onConnected - 连接成功回调 * @param {Function} options.onDisconnected - 断开回调 * @param {Function} options.onError - 错误回调 @@ -33,6 +38,11 @@ export function createRoomWebSocket(options) { onPrivateChat, onChatHistory, onPrivateChatHistory, + onSyncRouteVisibility, + onSyncWaypoints, + onSyncPlatformIcons, + onSyncRoomDrawings, + onSyncPlatformStyles, onConnected, onDisconnected, onError, @@ -108,6 +118,51 @@ export function createRoomWebSocket(options) { } } + function sendSyncRouteVisibility(routeId, visible) { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'SYNC_ROUTE_VISIBILITY', routeId, visible }) + }) + } + } + + function sendSyncWaypoints(routeId) { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'SYNC_WAYPOINTS', routeId }) + }) + } + } + + function sendSyncPlatformIcons() { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'SYNC_PLATFORM_ICONS' }) + }) + } + } + + function sendSyncRoomDrawings() { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'SYNC_ROOM_DRAWINGS' }) + }) + } + } + + function sendSyncPlatformStyles() { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'SYNC_PLATFORM_STYLES' }) + }) + } + } + function startHeartbeat() { stopHeartbeat() heartbeatTimer = setInterval(sendPing, 30000) @@ -132,6 +187,16 @@ export function createRoomWebSocket(options) { onMemberLeft && onMemberLeft(body.member, body.sessionId) } else if (type === 'CHAT' && body.sender) { onChatMessage && onChatMessage(body) + } else if (type === 'SYNC_ROUTE_VISIBILITY' && body.routeId != null) { + onSyncRouteVisibility && onSyncRouteVisibility(body.routeId, !!body.visible, body.senderSessionId) + } else if (type === 'SYNC_WAYPOINTS' && body.routeId != null) { + onSyncWaypoints && onSyncWaypoints(body.routeId, body.senderSessionId) + } else if (type === 'SYNC_PLATFORM_ICONS') { + onSyncPlatformIcons && onSyncPlatformIcons(body.senderSessionId) + } else if (type === 'SYNC_ROOM_DRAWINGS') { + onSyncRoomDrawings && onSyncRoomDrawings(body.senderSessionId) + } else if (type === 'SYNC_PLATFORM_STYLES') { + onSyncPlatformStyles && onSyncPlatformStyles(body.senderSessionId) } } catch (e) { console.warn('[WebSocket] parse message error:', e) @@ -223,6 +288,11 @@ export function createRoomWebSocket(options) { sendChat, sendPrivateChat, sendPrivateChatHistoryRequest, + sendSyncRouteVisibility, + sendSyncWaypoints, + sendSyncPlatformIcons, + sendSyncRoomDrawings, + sendSyncPlatformStyles, get connected() { return client && client.connected } diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index e642190..ff77ed0 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -5762,34 +5762,45 @@ export default { addLineEntity(positions, pointEntities = []) { this.entityCounter++ const id = `line_${this.entityCounter}` - const entity = this.viewer.entities.add({ - id: id, - name: `测距 ${this.entityCounter}`, - polyline: { - positions: positions, - width: this.defaultStyles.line.width, - material: Cesium.Color.fromCssColorString(this.defaultStyles.line.color), - arcType: Cesium.ArcType.NONE - } - }) const entityData = { id, type: 'line', points: positions.map(p => this.cartesianToLatLng(p)), positions: positions, - entity: entity, + entity: null, pointEntities: pointEntities, // 存储点实体 color: this.defaultStyles.line.color, width: this.defaultStyles.line.width, label: `测距 ${this.entityCounter}`, bearingType: 'true' // 默认真方位 } + // 有可编辑点时使用 CallbackProperty 实现拖拽时的流畅实时渲染(与绘制时一致) + const positionsProp = pointEntities.length > 0 + ? new Cesium.CallbackProperty(() => { + return entityData.pointEntities.map(pe => { + const pos = pe.position.getValue(Cesium.JulianDate.now()) + return pos || Cesium.Cartesian3.ZERO + }) + }, false) + : positions + const entity = this.viewer.entities.add({ + id: id, + name: `测距 ${this.entityCounter}`, + polyline: { + positions: positionsProp, + width: this.defaultStyles.line.width, + material: Cesium.Color.fromCssColorString(this.defaultStyles.line.color), + arcType: Cesium.ArcType.NONE + } + }) + entityData.entity = entity this.allEntities.push(entityData) this.updateLineSegmentLabels(entityData) entity.clickHandler = (e) => { this.selectEntity(entityData) e.stopPropagation() } + this.notifyDrawingEntitiesChanged() return entityData }, addPolygonEntity(positions) { @@ -6614,7 +6625,7 @@ export default { platformColor: this.editPlatformForm.iconColor || '#000000' }; savePlatformStyle(styleData).then(() => { - // console.log("样式保存成功"); + this.$emit('platform-style-saved'); }).catch(err => { console.error("样式保存失败", err); }); @@ -6935,7 +6946,7 @@ export default { powerZoneOpacity: existing.powerZoneOpacity, powerZoneVisible: existing.powerZoneVisible } - savePlatformStyle(styleData).catch(() => {}) + savePlatformStyle(styleData).then(() => this.$emit('platform-style-saved')).catch(() => {}) }).catch(() => {}) } }, @@ -7009,7 +7020,7 @@ export default { powerZoneOpacity: existing.powerZoneOpacity, powerZoneVisible: existing.powerZoneVisible } - savePlatformStyle(styleData).then(doDrawAndClose).catch(() => doDrawAndClose()) + savePlatformStyle(styleData).then(() => { this.$emit('platform-style-saved'); doDrawAndClose(); }).catch(() => doDrawAndClose()) }).catch(() => doDrawAndClose()) } else { doDrawAndClose() @@ -7063,7 +7074,7 @@ export default { powerZoneOpacity: opacity, powerZoneVisible: visible } - savePlatformStyle(styleData).catch(() => {}) + savePlatformStyle(styleData).then(() => this.$emit('platform-style-saved')).catch(() => {}) }).catch(() => {}) } }, @@ -7139,7 +7150,7 @@ export default { powerZoneOpacity: opacity, powerZoneVisible: visible } - savePlatformStyle(styleData).then(doDrawAndClose).catch(() => doDrawAndClose()) + savePlatformStyle(styleData).then(() => { this.$emit('platform-style-saved'); doDrawAndClose(); }).catch(() => doDrawAndClose()) }).catch(() => doDrawAndClose()) } else { doDrawAndClose() @@ -7176,7 +7187,7 @@ export default { if (currentRoomId && Number(platformId) > 0) { getPlatformStyle({ roomId: currentRoomId, routeId, platformId }).then(res => { const existing = res.data || {} - savePlatformStyle({ ...existing, roomId: currentRoomId, routeId, platformId, detectionZoneVisible: nextVisible }).catch(() => {}) + savePlatformStyle({ ...existing, roomId: currentRoomId, routeId, platformId, detectionZoneVisible: nextVisible }).then(() => this.$emit('platform-style-saved')).catch(() => {}) }).catch(() => {}) } return @@ -7201,7 +7212,7 @@ export default { if (roomId && platformId) { getPlatformStyle({ roomId, routeId: 0, platformId }).then(res => { const existing = res.data || {} - savePlatformStyle({ ...existing, roomId: String(roomId), routeId: 0, platformId, detectionZoneVisible: nextVisible }).catch(() => {}) + savePlatformStyle({ ...existing, roomId: String(roomId), routeId: 0, platformId, detectionZoneVisible: nextVisible }).then(() => this.$emit('platform-style-saved')).catch(() => {}) }).catch(() => {}) } } @@ -7238,7 +7249,7 @@ export default { if (currentRoomId && Number(platformId) > 0) { getPlatformStyle({ roomId: currentRoomId, routeId, platformId }).then(res => { const existing = res.data || {} - savePlatformStyle({ ...existing, roomId: currentRoomId, routeId, platformId, powerZoneVisible: nextVisible }).catch(() => {}) + savePlatformStyle({ ...existing, roomId: currentRoomId, routeId, platformId, powerZoneVisible: nextVisible }).then(() => this.$emit('platform-style-saved')).catch(() => {}) }).catch(() => {}) } return @@ -7264,7 +7275,7 @@ export default { if (roomId && platformId) { getPlatformStyle({ roomId, routeId: 0, platformId }).then(res => { const existing = res.data || {} - savePlatformStyle({ ...existing, roomId: String(roomId), routeId: 0, platformId, powerZoneVisible: nextVisible }).catch(() => {}) + savePlatformStyle({ ...existing, roomId: String(roomId), routeId: 0, platformId, powerZoneVisible: nextVisible }).then(() => this.$emit('platform-style-saved')).catch(() => {}) }).catch(() => {}) } } @@ -7686,9 +7697,9 @@ export default { }); }, // ================== 空域/威力区图形持久化 ================== - /** 需要持久化到方案的空域图形类型(不含平台图标、航线、测距点线) */ + /** 需要持久化到方案的空域图形类型(含测距;不含平台图标、航线) */ getDrawingEntityTypes() { - return ['polygon', 'rectangle', 'circle', 'sector', 'arrow', 'text', 'image', 'powerZone'] + return ['line', 'polygon', 'rectangle', 'circle', 'sector', 'arrow', 'text', 'image', 'powerZone'] }, /** 空域/威力区图形增删时通知父组件,用于自动保存到房间(从房间加载时不触发) */ notifyDrawingEntitiesChanged() { @@ -7703,7 +7714,11 @@ export default { case 'point': data = { lat: entity.lat, lng: entity.lng }; break case 'line': - data = { points: entity.points || [] }; break + data = { + points: entity.points || [], + width: entity.width != null ? entity.width : 3, + bearingType: entity.bearingType || 'true' + }; break case 'polygon': data = { points: entity.points || [], @@ -7813,6 +7828,10 @@ export default { if (!entity && item.id) entity = this.viewer.entities.getById(item.id) if (entity) this.viewer.entities.remove(entity) if (item.type === 'powerZone' && item.centerEntity) this.viewer.entities.remove(item.centerEntity) + if (item.type === 'line') { + if (item.pointEntities) item.pointEntities.forEach(pe => { try { this.viewer.entities.remove(pe) } catch (_) {} }) + if (item.segmentLabelEntities) item.segmentLabelEntities.forEach(le => { try { this.viewer.entities.remove(le) } catch (_) {} }) + } } catch (e) { console.warn('clearDrawingEntities:', e) } }) this.allEntities = this.allEntities.filter(item => !types.includes(item.type)) @@ -7937,25 +7956,67 @@ export default { break case 'line': { const linePositions = entityData.data.points.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat)) - entity = this.viewer.entities.add({ - polyline: { - positions: linePositions, - width: 3, - material: Cesium.Color.fromCssColorString(color), - arcType: Cesium.ArcType.NONE - } - }) + const lineWidth = entityData.data.width != null ? entityData.data.width : 3 + const pointEntities = [] + for (let i = 0; i < linePositions.length; i++) { + const pos = linePositions[i] + this.entityCounter++ + const pointId = `point_${this.entityCounter}` + const isStartPoint = i === 0 + const pointEntity = this.viewer.entities.add({ + id: pointId, + position: pos, + point: { + pixelSize: this.defaultStyles.point.size, + color: Cesium.Color.fromCssColorString(this.defaultStyles.point.color), + outlineColor: Cesium.Color.WHITE, + outlineWidth: 2 + }, + label: { + text: isStartPoint ? '起点' : '', + font: '14px Microsoft YaHei, sans-serif', + fillColor: Cesium.Color.BLACK, + backgroundColor: Cesium.Color.WHITE.withAlpha(0.8), + showBackground: true, + horizontalOrigin: Cesium.HorizontalOrigin.LEFT, + verticalOrigin: Cesium.VerticalOrigin.CENTER, + pixelOffset: new Cesium.Cartesian2(15, 0), + disableDepthTestDistance: Number.POSITIVE_INFINITY + } + }) + pointEntities.push(pointEntity) + } const lineEntityData = { - id: entity.id, + id: `line_import_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, type: 'line', label: entityData.label || '测距', color: color, - entity, + entity: null, points: entityData.data.points, positions: linePositions, - pointEntities: [], + pointEntities, + width: lineWidth, bearingType: entityData.data.bearingType || 'true' } + const positionsProp = pointEntities.length > 0 + ? new Cesium.CallbackProperty(() => { + return lineEntityData.pointEntities.map(pe => { + const p = pe.position.getValue(Cesium.JulianDate.now()) + return p || Cesium.Cartesian3.ZERO + }) + }, false) + : linePositions + entity = this.viewer.entities.add({ + id: lineEntityData.id, + name: lineEntityData.label, + polyline: { + positions: positionsProp, + width: lineWidth, + material: Cesium.Color.fromCssColorString(color), + arcType: Cesium.ArcType.NONE + } + }) + lineEntityData.entity = entity this.allEntities.push(lineEntityData) this.updateLineSegmentLabels(lineEntityData) return @@ -8523,62 +8584,33 @@ export default { } } }, Cesium.ScreenSpaceEventType.LEFT_DOWN) - // 鼠标移动事件:移动点 + // 鼠标移动事件:移动点(仅更新点位置,折线通过 CallbackProperty 自动实时渲染,与绘制时同样流畅) this.pointMovementHandler.setInputAction((movement) => { if (isMoving && selectedPoint && selectedLineEntity) { const newPosition = this.getClickPosition(movement.endPosition) if (newPosition) { - // 更新点的位置 + // 直接更新点实体位置,折线通过 CallbackProperty 每帧自动读取,无需移除/重建实体 selectedPoint.position = newPosition - // 创建新的位置数组,确保 Cesium 能够检测到变化 - const newPositions = [...selectedLineEntity.positions] - newPositions[pointIndex] = newPosition - // 移除旧的线段实体 - this.viewer.entities.remove(selectedLineEntity.entity) - // 清除所有可能存在的重复线段 - const entitiesToRemove = [] - this.viewer.entities.values.forEach(e => { - if (e.id && e.id === selectedLineEntity.id) { - entitiesToRemove.push(e) - } - }) - entitiesToRemove.forEach(e => { - this.viewer.entities.remove(e) - }) - // 创建新的线段实体 - const newEntity = this.viewer.entities.add({ - id: selectedLineEntity.id, - name: selectedLineEntity.label, - polyline: { - positions: newPositions, - width: selectedLineEntity.width, - material: Cesium.Color.fromCssColorString(selectedLineEntity.color), - arcType: Cesium.ArcType.NONE - } - }) - // 更新线实体的引用和位置数组 - selectedLineEntity.entity = newEntity - selectedLineEntity.positions = newPositions - // 更新点数据 + // 同步 positions 和 points,供 updateLineSegmentLabels 使用 + selectedLineEntity.positions[pointIndex] = newPosition selectedLineEntity.points[pointIndex] = this.cartesianToLatLng(newPosition) // 更新各段终点标签(累计长度和角度) this.updateLineSegmentLabels(selectedLineEntity) - - // 强制刷新地图渲染 - this.viewer.scene.requestRender() } } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE) // 鼠标释放事件:结束移动 this.pointMovementHandler.setInputAction(() => { + // 若刚拖拽了测距线,触发持久化与多账号同步 + if (isMoving && selectedLineEntity && selectedLineEntity.type === 'line') { + this.notifyDrawingEntitiesChanged() + } // 恢复相机控制器 if (originalCameraController !== null) { this.viewer.scene.screenSpaceCameraController.enableInputs = originalCameraController originalCameraController = null } - - isMoving = false selectedPoint = null selectedLineEntity = null diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index cb80ba7..228e8ea 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -383,12 +383,11 @@ export default { }) this.$emit('select-plan', { id: null }) } else { - // 展开新方案前,先清空所有已显示的航线和已展开的方案 - // 清除当前地图上显示的所有航线实体(通知父组件清空 activeRouteIds 并调用地图移除方法) + // 展开新方案前,先清空所有已显示的航线和已展开的方案(仅本地清除,不广播,避免影响其他账号) this.activeRouteIds.forEach(activeId => { const activeRoute = this.routes.find(r => r.id === activeId); if (activeRoute) { - this.$emit('toggle-route-visibility', activeRoute); + this.$emit('toggle-route-visibility', activeRoute, { fromPlanSwitch: true }); } }); // 重置展开状态,确保只有一个方案是展开的 diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 6d4dbc5..1e7db10 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -35,7 +35,8 @@ @platform-icon-updated="onPlatformIconUpdated" @platform-icon-removed="onPlatformIconRemoved" @viewer-ready="onViewerReady" - @drawing-entities-changed="onDrawingEntitiesChanged" /> + @drawing-entities-changed="onDrawingEntitiesChanged" + @platform-style-saved="onPlatformStyleSaved" />