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 f7818c0..4c007a1 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 @@ -1,10 +1,8 @@ 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; @@ -21,7 +19,6 @@ 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; /** @@ -44,9 +41,6 @@ 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"; @@ -188,13 +182,7 @@ public class RoomWebSocketController { chatHistoryMsg.put("messages", chatHistory); messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", chatHistoryMsg); - Set visibleRouteIds = roomRoomStateService.getVisibleRouteIds(roomId); - if (visibleRouteIds != null && !visibleRouteIds.isEmpty()) { - Map 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) { @@ -296,28 +284,9 @@ public class RoomWebSocketController { messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", resp); } - /** 广播航线显隐变更,供其他设备实时同步;并持久化到 Redis 供新加入用户同步 */ + /** 航线显隐变更:小房间内每人展示各自内容,不再广播和持久化 */ private void handleSyncRouteVisibility(Long roomId, Map 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 msg = new HashMap<>(); - msg.put("type", TYPE_SYNC_ROUTE_VISIBILITY); - msg.put("routeId", body.get("routeId")); - msg.put("visible", visible); - msg.put("senderSessionId", sessionId); - messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + // 小房间内每人展示各自内容,航线显隐不再同步给他人、不再持久化 } /** 广播航点变更,供其他设备实时同步 */ diff --git a/ruoyi-ui/src/lang/en.js b/ruoyi-ui/src/lang/en.js index 4ad098c..b7166b4 100644 --- a/ruoyi-ui/src/lang/en.js +++ b/ruoyi-ui/src/lang/en.js @@ -21,7 +21,9 @@ export default { importATO: 'Import ATO', importLayer: 'Import Layer', importRoute: 'Import Route', - export: 'Export' + export: 'Export', + exportRoute: 'Export Routes', + exportPlan: 'Export Plan' }, edit: { routeEdit: 'Route Edit', diff --git a/ruoyi-ui/src/lang/zh.js b/ruoyi-ui/src/lang/zh.js index 0d91067..481dcac 100644 --- a/ruoyi-ui/src/lang/zh.js +++ b/ruoyi-ui/src/lang/zh.js @@ -21,7 +21,9 @@ export default { importATO: '导入ATO', importLayer: '导入图层', importRoute: '导入航线', - export: '导出' + export: '导出', + exportRoute: '导出航线', + exportPlan: '导出计划' }, edit: { routeEdit: '航线编辑', diff --git a/ruoyi-ui/src/views/childRoom/TopHeader.vue b/ruoyi-ui/src/views/childRoom/TopHeader.vue index 715cc82..cc9a013 100644 --- a/ruoyi-ui/src/views/childRoom/TopHeader.vue +++ b/ruoyi-ui/src/views/childRoom/TopHeader.vue @@ -56,7 +56,20 @@ - {{ $t('topHeader.file.export') }} + + {{ $t('topHeader.file.export') }} + + + + {{ $t('topHeader.file.exportRoute') }} + {{ $t('topHeader.file.exportPlan') }} + + + @@ -497,6 +510,9 @@ export default { + exportRoute() { + this.$emit('export-routes') + }, exportPlan() { this.$emit('export-plan') }, diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index de84ee5..06df25f 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -119,6 +119,7 @@ @import-ato="importATO" @import-layer="importLayer" @import-route="importRoute" + @export-routes="openExportRoutesDialog" @export-plan="exportPlan" @route-edit="routeEdit" @military-marking="militaryMarking" @@ -385,6 +386,23 @@ @confirm="handleImportConfirm" /> + + + + + + (m.timestamp || 0) > maxTs); this.$set(this.privateChatMessages, targetUserId, [...history, ...newer]); }, - onSyncRouteVisibility: (routeId, visible, senderSessionId) => { - if (this.isMySyncSession(senderSessionId)) return; - this.applySyncRouteVisibility(routeId, visible); + onSyncRouteVisibility: () => { + // 小房间内每人展示各自内容,不应用他人的航线显隐变更 }, onSyncWaypoints: (routeId, senderSessionId) => { if (this.isMySyncSession(senderSessionId)) return; @@ -1497,8 +1525,8 @@ export default { if (this.isMySyncSession(senderSessionId)) return; this.applySyncPlatformStyles(); }, - onRoomState: (visibleRouteIds) => { - this.applyRoomStateVisibleRoutes(visibleRouteIds); + onRoomState: () => { + // 小房间内每人展示各自内容,新加入时不从他人同步可见航线 }, onConnected: () => {}, onDisconnected: () => { @@ -1983,15 +2011,16 @@ export default { routes: [] })); } - // 获取航线:大房间时只请求本大房间子房间方案的航线,小房间时请求全部后按方案过滤 + // 获取航线:仅请求当前房间方案下的航线(大房间、小房间均按 planIds 限定) const planIds = this.plans.map(p => p.id); - const routeParams = isParentRoom && planIds.length > 0 + const routeParams = planIds.length > 0 ? { scenarioIdsStr: planIds.join(','), pageNum: 1, pageSize: 9999 } : { pageNum: 1, pageSize: 9999 }; const routeRes = await listRoutes(routeParams); if (routeRes.code === 200) { let routeRows = routeRes.rows || []; - if (isParentRoom && planIds.length > 0) { + // 大房间和小房间均只保留当前房间方案下的航线(小房间时 API 可能返回全部,需按 planIds 过滤) + if (planIds.length > 0) { routeRows = routeRows.filter(r => planIds.includes(r.scenarioId)); } const allRoutes = routeRows.map(item => ({ @@ -2535,8 +2564,90 @@ export default { }, importRoute() { - this.$message.success('导入航线'); - // 这里可以添加导入航线的逻辑 + this.showImportRoutesDialog = true; + }, + openExportRoutesDialog() { + this.showExportRoutesDialog = true; + }, + /** 导出航线:获取完整数据并下载 JSON */ + async handleExportRoutes(selectedIds) { + if (!selectedIds || selectedIds.length === 0) return; + try { + const routeDataList = []; + for (const id of selectedIds) { + const res = await getRoutes(id); + if (res.code === 200 && res.data) { + const d = res.data; + const waypoints = (d.waypoints || []).map(wp => { + const { id: _id, routeId: _rid, ...rest } = wp; + return rest; + }); + routeDataList.push({ + route: { + callSign: d.callSign, + platformId: d.platformId, + platform: d.platform, + attributes: d.attributes + }, + waypoints + }); + } + } + const exportData = { + version: 1, + exportTime: new Date().toISOString(), + routes: routeDataList + }; + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `航线导出_${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + URL.revokeObjectURL(url); + this.showExportRoutesDialog = false; + this.$message.success(`已导出 ${selectedIds.length} 条航线`); + } catch (err) { + console.error('导出航线失败:', err); + this.$message.error('导出失败,请重试'); + } + }, + /** 导入航线:从 JSON 创建新航线 */ + async handleImportRoutes({ routeItems, targetScenarioId, targetPlatformId }) { + if (!routeItems || routeItems.length === 0 || !targetScenarioId) return; + const importDialog = this.$refs.importRoutesDialog; + if (importDialog && importDialog.setImporting) importDialog.setImporting(true); + try { + let successCount = 0; + for (const item of routeItems) { + const route = item.route || item; + const waypoints = item.waypoints || route.waypoints || []; + const cleanWaypoints = waypoints.map((wp, idx) => { + const { id: _id, routeId: _rid, ...rest } = wp; + return { + ...rest, + seq: idx + 1 + }; + }); + const payload = { + callSign: route.callSign || route.name || `导入航线${successCount + 1}`, + scenarioId: targetScenarioId, + platformId: (targetPlatformId != null ? targetPlatformId : route.platformId) || 1, + attributes: route.attributes || this.getDefaultRouteAttributes(), + waypoints: cleanWaypoints + }; + const res = await addRoutes(payload); + if (res.code === 200) successCount++; + } + this.showImportRoutesDialog = false; + await this.getList(); + this.$message.success(`成功导入 ${successCount} 条航线`); + } catch (err) { + console.error('导入航线失败:', err); + this.$message.error('导入失败:' + (err.message || '请重试')); + } finally { + if (importDialog && importDialog.setImporting) importDialog.setImporting(false); + } }, exportPlan() { @@ -3256,8 +3367,8 @@ export default { }, importRouteData(path) { - console.log('导入航路:', path) - this.$message.success('航路数据导入成功'); + // 统一打开导入航线弹窗(文件菜单、外部参数等入口均走此流程;浏览器无法读取本地路径,需用户选择文件) + this.showImportRoutesDialog = true; }, importLandmark(path) { @@ -4304,7 +4415,7 @@ export default { this.selectedRouteDetails = null; } } - this.wsConnection?.sendSyncRouteVisibility?.(route.id, false); + // 航线显隐仅本地生效,不同步给他人 this.$message.info(`已取消航线: ${route.name}`); return; } @@ -4352,7 +4463,7 @@ export default { } else { this.$message.warning('该航线暂无坐标数据,无法在地图展示'); } - this.wsConnection?.sendSyncRouteVisibility?.(route.id, true); + // 航线显隐仅本地生效,不同步给他人 } } catch (error) { console.error("获取航线详情失败:", error); @@ -4477,7 +4588,7 @@ export default { if (this.$refs.cesiumMap) { this.$refs.cesiumMap.removeRouteById(route.id); } - if (!fromPlanSwitch) this.wsConnection?.sendSyncRouteVisibility?.(route.id, false); + // 航线显隐仅本地生效,不同步给他人 if (this.selectedRouteDetails && this.selectedRouteDetails.id === route.id) { if (this.activeRouteIds.length > 0) { const lastId = this.activeRouteIds[this.activeRouteIds.length - 1]; @@ -4505,7 +4616,7 @@ export default { } else { // 航线已隐藏,显示它 await this.selectRoute(route); - if (!fromPlanSwitch) this.wsConnection?.sendSyncRouteVisibility?.(route.id, true); + // 航线显隐仅本地生效,不同步给他人 } }, diff --git a/ruoyi-ui/src/views/dialogs/ExportRoutesDialog.vue b/ruoyi-ui/src/views/dialogs/ExportRoutesDialog.vue new file mode 100644 index 0000000..9f4a970 --- /dev/null +++ b/ruoyi-ui/src/views/dialogs/ExportRoutesDialog.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/ruoyi-ui/src/views/dialogs/ExternalParamsDialog.vue b/ruoyi-ui/src/views/dialogs/ExternalParamsDialog.vue index c774ccd..1e03ff7 100644 --- a/ruoyi-ui/src/views/dialogs/ExternalParamsDialog.vue +++ b/ruoyi-ui/src/views/dialogs/ExternalParamsDialog.vue @@ -174,12 +174,8 @@ export default { this.$emit('import-airport', this.formData.airportPath); }, importRoute() { - if (!this.formData.routePath) { - this.$message.warning('请先选择航路数据文件'); - return; - } - this.$message.success('航路数据导入成功'); - this.$emit('import-route-data', this.formData.routePath); + // 统一由父组件打开导入航线弹窗,用户选择 JSON 文件后导入(浏览器无法读取本地路径) + this.$emit('import-route-data', this.formData.routePath || ''); }, importLandmark() { if (!this.formData.landmarkPath) { diff --git a/ruoyi-ui/src/views/dialogs/ImportRoutesDialog.vue b/ruoyi-ui/src/views/dialogs/ImportRoutesDialog.vue new file mode 100644 index 0000000..8c71c13 --- /dev/null +++ b/ruoyi-ui/src/views/dialogs/ImportRoutesDialog.vue @@ -0,0 +1,240 @@ + + + + +