cuitw 1 month ago
parent
commit
64dbbbe495
  1. 43
      ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java
  2. 30
      ruoyi-ui/src/utils/websocket.js
  3. 7
      ruoyi-ui/src/views/cesiumMap/index.vue
  4. 80
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  5. 94
      ruoyi-ui/src/views/childRoom/index.vue
  6. 7
      ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue
  7. 2
      ruoyi-ui/vue.config.js

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

@ -58,9 +58,14 @@ 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_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 +105,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 {

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

@ -23,6 +23,8 @@ 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.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 +45,8 @@ export function createRoomWebSocket(options) {
onSyncPlatformIcons,
onSyncRoomDrawings,
onSyncPlatformStyles,
onObjectEditLock,
onObjectEditUnlock,
onConnected,
onDisconnected,
onError,
@ -163,6 +167,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 +221,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)
@ -293,6 +321,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 = {

80
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
@ -503,9 +532,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 +783,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;

94
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,
@ -679,6 +686,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 +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() {
@ -760,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);
@ -842,9 +872,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 +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;
@ -998,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;
@ -1134,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) {
@ -1303,6 +1359,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];
@ -1353,6 +1424,7 @@ export default {
this.mySyncSessionIds = [];
this.chatMessages = [];
this.privateChatMessages = {};
this.routeLockedBy = {};
},
onError: (err) => {
console.warn('[WebSocket]', err);
@ -1621,9 +1693,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 +1813,10 @@ export default {
}
},
async handleDeleteRoute(route) {
if (this.isRouteLockedByOther(route.id)) {
this.$message.warning('该航线正被其他成员编辑,无法删除');
return;
}
try {
//
await this.$confirm(`确定要彻底删除航线 "${route.name}" 吗?`, '提示', {
@ -1989,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;
@ -1996,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);

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