Browse Source

Merge branch 'ctw' of http://124.70.32.114:3100/woka/cesium-map-object into mh

# Conflicts:
#	ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java
#	ruoyi-ui/src/utils/websocket.js
#	ruoyi-ui/src/views/childRoom/index.vue
#	ruoyi-ui/vue.config.js
mh
menghao 1 month ago
parent
commit
3592696855
  1. 61
      ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java
  2. 5
      ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
  3. 14
      ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java
  4. 3
      ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java
  5. 30
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
  6. 19
      ruoyi-ui/src/utils/request.js
  7. 70
      ruoyi-ui/src/utils/websocket.js
  8. 174
      ruoyi-ui/src/views/cesiumMap/index.vue
  9. 5
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  10. 221
      ruoyi-ui/src/views/childRoom/index.vue

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

@ -53,6 +53,11 @@ 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";
/** 对象选中/查看:某成员正在查看某条航线,广播给房间内其他人 */
private static final String TYPE_OBJECT_VIEW = "OBJECT_VIEW";
/** 取消对象查看 */
@ -63,6 +68,7 @@ public class RoomWebSocketController {
private static final String TYPE_OBJECT_EDIT_UNLOCK = "OBJECT_EDIT_UNLOCK";
/**
* 处理房间消息JOINLEAVEPINGCHATPRIVATE_CHATSYNC_*
* 处理房间消息JOINLEAVEPINGCHATPRIVATE_CHATOBJECT_VIEWOBJECT_EDIT_LOCK
*/
@MessageMapping("/room/{roomId}")
@ -93,6 +99,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);
} else if (TYPE_OBJECT_VIEW.equals(type)) {
handleObjectView(roomId, sessionId, loginUser, body);
} else if (TYPE_OBJECT_VIEW_CLEAR.equals(type)) {
@ -306,6 +322,51 @@ public class RoomWebSocketController {
messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", resp);
}
/** 广播航线显隐变更,供其他设备实时同步 */
private void handleSyncRouteVisibility(Long roomId, Map<String, Object> body, String sessionId) {
if (body == null || !body.containsKey("routeId")) return;
Map<String, Object> msg = new HashMap<>();
msg.put("type", TYPE_SYNC_ROUTE_VISIBILITY);
msg.put("routeId", body.get("routeId"));
msg.put("visible", body.get("visible") != null && Boolean.TRUE.equals(body.get("visible")));
msg.put("senderSessionId", sessionId);
messagingTemplate.convertAndSend("/topic/room/" + roomId, msg);
}
/** 广播航点变更,供其他设备实时同步 */
private void handleSyncWaypoints(Long roomId, Map<String, Object> body, String sessionId) {
if (body == null || !body.containsKey("routeId")) return;
Map<String, Object> 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<String, Object> body, String sessionId) {
Map<String, Object> 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<String, Object> body, String sessionId) {
Map<String, Object> 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<String, Object> body, String sessionId) {
Map<String, Object> 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<String, Object> body) {
RoomMemberDTO dto = new RoomMemberDTO();
dto.setUserId(loginUser.getUserId());

5
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:";

14
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);

3
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);
}
}

30
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<String, Object> 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);
}
}
/**

19
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") {

70
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.onObjectViewing - 对象被某成员查看 (msg: { objectType, objectId, viewer }) => {}
* @param {Function} options.onObjectViewClear - 取消对象查看 (msg: { objectType, objectId, sessionId }) => {}
* @param {Function} options.onObjectEditLock - 对象被某成员编辑锁定 (msg: { objectType, objectId, editor }) => {}
@ -37,6 +42,11 @@ export function createRoomWebSocket(options) {
onPrivateChat,
onChatHistory,
onPrivateChatHistory,
onSyncRouteVisibility,
onSyncWaypoints,
onSyncPlatformIcons,
onSyncRoomDrawings,
onSyncPlatformStyles,
onObjectViewing,
onObjectViewClear,
onObjectEditLock,
@ -116,6 +126,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 sendObjectView(objectType, objectId) {
if (client && client.connected) {
@ -180,6 +235,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)
} 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) {
@ -279,6 +344,11 @@ export function createRoomWebSocket(options) {
sendChat,
sendPrivateChat,
sendPrivateChatHistoryRequest,
sendSyncRouteVisibility,
sendSyncWaypoints,
sendSyncPlatformIcons,
sendSyncRoomDrawings,
sendSyncPlatformStyles,
sendObjectView,
sendObjectViewClear,
sendObjectEditLock,

174
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

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

@ -420,12 +420,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 });
}
});
//

221
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" />
<div v-show="!screenshotMode" class="map-overlay-text">
<!-- <i class="el-icon-location-outline text-3xl mb-2 block"></i> -->
<!-- <p>二维GIS地图区域</p>
@ -196,7 +197,7 @@
@open-waypoint-dialog="openWaypointDialog"
@add-waypoint="addWaypoint"
@cancel-route="cancelRoute"
@toggle-route-visibility="toggleRouteVisibility"
@toggle-route-visibility="(route, opts) => toggleRouteVisibility(route, opts)"
@toggle-route-lock="handleToggleRouteLockFromPanel"
@view-conflict="viewConflict"
@resolve-conflict="resolveConflict"
@ -654,9 +655,20 @@ export default {
//
userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
/** 当前连接的 WebSocket sessionId 集合(用于过滤自己发出的同步消息,避免重复应用) */
mySyncSessionIds: [],
};
},
watch: {
'$route.query.roomId': {
handler(newRoomId) {
if (newRoomId != null && String(newRoomId) !== String(this.currentRoomId)) {
this.currentRoomId = newRoomId;
this.connectRoomWebSocket();
if (newRoomId) this.getRoomDetail();
}
}
},
timeProgress: {
handler() {
this.updateTimeFromProgress();
@ -1011,6 +1023,7 @@ export default {
this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints: sortedWaypoints };
}
this.$message.success('已添加航点');
this.wsConnection?.sendSyncWaypoints?.(routeId);
} catch (e) {
this.$message.error(e.msg || e.message || '添加航点失败');
console.error(e);
@ -1116,6 +1129,7 @@ export default {
}
}
this.$message.success(isHold ? '已设为普通航点' : '已设为盘旋航点');
this.wsConnection?.sendSyncWaypoints?.(routeId);
} catch (e) {
this.$message.error(e.msg || e.message || '切换失败');
console.error(e);
@ -1221,6 +1235,7 @@ export default {
}
this.$message.success('航点位置已更新');
this.$nextTick(() => this.updateDeductionPositions());
this.wsConnection?.sendSyncWaypoints?.(routeId);
// Redis
if (this.currentRoomId && routeForPlatform && waypoints.length > 0) {
this.updateMissilePositionsAfterRouteEdit(this.currentRoomId, routeId, routeForPlatform.platformId != null ? routeForPlatform.platformId : 0, waypoints);
@ -1292,6 +1307,8 @@ export default {
avatar: m.avatar ? (m.avatar.startsWith('http') ? m.avatar : (baseUrl + m.avatar)) : ''
}));
this.onlineCount = this.wsOnlineMembers.length;
const myId = this.$store.getters.id;
this.mySyncSessionIds = (members || []).filter(m => myId != null && String(m.userId) === String(myId)).map(m => m.sessionId).filter(Boolean);
},
onMemberJoined: (member) => {
const baseUrl = (process.env.VUE_APP_BACKEND_URL || (window.location.origin + (process.env.VUE_APP_BASE_API || '')));
@ -1309,10 +1326,17 @@ export default {
this.wsOnlineMembers = [...this.wsOnlineMembers, m];
this.onlineCount = this.wsOnlineMembers.length;
}
const myId = this.$store.getters.id;
if (myId != null && String(member.userId) === String(myId) && member.sessionId) {
if (!this.mySyncSessionIds.includes(member.sessionId)) {
this.mySyncSessionIds = [...this.mySyncSessionIds, member.sessionId];
}
}
},
onMemberLeft: (member, sessionId) => {
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) {
@ -1365,10 +1389,31 @@ export default {
const newer = existing.filter(m => (m.timestamp || 0) > maxTs);
this.$set(this.privateChatMessages, targetUserId, [...history, ...newer]);
},
onSyncRouteVisibility: (routeId, visible, senderSessionId) => {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncRouteVisibility(routeId, visible);
},
onSyncWaypoints: (routeId, senderSessionId) => {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncWaypoints(routeId);
},
onSyncPlatformIcons: (senderSessionId) => {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncPlatformIcons();
},
onSyncRoomDrawings: (senderSessionId) => {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncRoomDrawings();
},
onSyncPlatformStyles: (senderSessionId) => {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncPlatformStyles();
},
onConnected: () => {},
onDisconnected: () => {
this.onlineCount = 0;
this.wsOnlineMembers = [];
this.mySyncSessionIds = [];
this.chatMessages = [];
this.privateChatMessages = {};
this.routeViewingBy = {};
@ -1385,10 +1430,154 @@ export default {
this.wsConnection = null;
}
this.wsOnlineMembers = [];
this.mySyncSessionIds = [];
this.onlineCount = 0;
this.chatMessages = [];
this.privateChatMessages = {};
},
/** 判断是否为当前连接发出的同步消息(避免自己发的消息再应用一次) */
isMySyncSession(senderSessionId) {
if (!senderSessionId) return false;
return Array.isArray(this.mySyncSessionIds) && this.mySyncSessionIds.includes(senderSessionId);
},
/** 收到其他设备的航线显隐同步:直接应用变更,不经过 selectRoute 的 toggle 逻辑,避免互相干扰 */
async applySyncRouteVisibility(routeId, visible) {
const route = this.routes.find(r => r.id === routeId);
if (!route) return;
if (visible) {
if (this.activeRouteIds.includes(routeId)) return;
try {
const res = await getRoutes(route.id);
if (res.code !== 200 || !res.data) return;
const waypoints = res.data.waypoints || [];
this.activeRouteIds = [...this.activeRouteIds, route.id];
this.selectedRouteId = res.data.id;
this.selectedRouteDetails = {
id: res.data.id,
name: res.data.callSign,
waypoints: waypoints,
platformId: route.platformId,
platform: route.platform,
attributes: route.attributes
};
const routeIndex = this.routes.findIndex(r => r.id === route.id);
if (routeIndex > -1) {
this.$set(this.routes, routeIndex, { ...this.routes[routeIndex], waypoints });
}
if (waypoints.length > 0 && this.$refs.cesiumMap) {
const roomId = this.currentRoomId;
if (roomId && route.platformId) {
try {
const styleRes = await getPlatformStyle({ roomId, routeId: route.id, platformId: route.platformId });
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(route.id, styleRes.data);
} catch (_) {}
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
}
} catch (_) {}
} else {
const idx = this.activeRouteIds.indexOf(routeId);
if (idx > -1) {
this.activeRouteIds = this.activeRouteIds.filter(id => id !== routeId);
if (this.$refs.cesiumMap) this.$refs.cesiumMap.removeRouteById(routeId);
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
if (this.activeRouteIds.length > 0) {
const lastId = this.activeRouteIds[this.activeRouteIds.length - 1];
const res = await getRoutes(lastId);
if (res.code === 200 && res.data) {
const fromList = this.routes.find(r => r.id === lastId);
this.selectedRouteId = res.data.id;
this.selectedRouteDetails = {
id: res.data.id,
name: res.data.callSign,
waypoints: res.data.waypoints || [],
platformId: fromList?.platformId,
platform: fromList?.platform,
attributes: fromList?.attributes
};
}
} else {
this.selectedRouteId = null;
this.selectedRouteDetails = null;
}
}
}
}
},
/** 收到其他设备的航点变更同步:拉取最新数据并重绘 */
async applySyncWaypoints(routeId) {
try {
const res = await getRoutes(routeId);
if (res.code !== 200 || !res.data) return;
const waypoints = res.data.waypoints || [];
const route = this.routes.find(r => r.id === routeId);
if (route) {
this.$set(route, 'waypoints', waypoints);
}
if (this.selectedRouteId === routeId && this.selectedRouteDetails) {
this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints };
}
if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap && waypoints.length > 0) {
const r = this.routes.find(rr => rr.id === routeId);
if (r) {
this.$refs.cesiumMap.removeRouteById(routeId);
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
}
} catch (e) {
console.warn('applySyncWaypoints failed', e);
}
},
/** 收到其他设备的平台图标变更同步:拉取最新数据并重绘 */
applySyncPlatformIcons() {
const rId = this.currentRoomId;
if (!rId || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadRoomPlatformIcons !== 'function') return;
listRoomPlatformIcons(rId).then(res => {
if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data);
}).catch(() => {});
},
/** 收到其他设备的空域图形变更同步:拉取最新房间数据并重绘 */
applySyncRoomDrawings() {
if (!this.currentRoomId || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadFrontendDrawings !== 'function') return;
getRooms(this.currentRoomId).then(res => {
if (res.code === 200 && res.data) {
this.roomDetail = this.roomDetail ? { ...this.roomDetail, frontendDrawings: res.data.frontendDrawings } : { ...res.data };
if (res.data.frontendDrawings) {
this.$refs.cesiumMap.loadFrontendDrawings(res.data.frontendDrawings);
} else {
this.$refs.cesiumMap.clearDrawingEntities();
}
}
}).catch(() => {});
},
/** 收到其他设备的探测区/威力区样式变更同步:刷新平台图标与航线样式 */
async applySyncPlatformStyles() {
const rId = this.currentRoomId;
if (!rId || !this.$refs.cesiumMap) return;
if (typeof this.$refs.cesiumMap.loadRoomPlatformIcons === 'function') {
try {
const res = await listRoomPlatformIcons(rId);
if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data);
} catch (_) {}
}
for (const routeId of this.activeRouteIds || []) {
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.platformId) continue;
try {
const styleRes = await getPlatformStyle({ roomId: rId, routeId, platformId: route.platformId });
if (styleRes.data && this.$refs.cesiumMap) {
this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
if (styleRes.data.detectionZoneVisible !== false && styleRes.data.detectionZoneRadius != null && Number(styleRes.data.detectionZoneRadius) > 0) {
this.$refs.cesiumMap.ensureDetectionZoneForRoute(routeId, styleRes.data.detectionZoneRadius, styleRes.data.detectionZoneColor || 'rgba(0, 150, 255, 0.35)', styleRes.data.detectionZoneOpacity);
}
if (styleRes.data.powerZoneVisible !== false && styleRes.data.powerZoneRadius != null && Number(styleRes.data.powerZoneRadius) > 0) {
this.$refs.cesiumMap.ensurePowerZoneForRoute(routeId, styleRes.data.powerZoneRadius, styleRes.data.powerZoneAngle ?? 120, styleRes.data.powerZoneColor || 'rgba(255, 0, 0, 0.3)', styleRes.data.powerZoneOpacity);
}
}
} catch (_) {}
}
},
//
openPlatformDialog(platform) {
this.selectedPlatform = JSON.parse(JSON.stringify(platform));
@ -1566,6 +1755,7 @@ export default {
this.selectedRouteDetails.attributes = updatedRoute.attributes;
}
this.$message.success(updatedRoute.waypoints && updatedRoute.waypoints.length > 0 ? '航线与航点已保存' : '航线更新成功');
this.wsConnection?.sendSyncWaypoints?.(updatedRoute.id);
const routeStyle = updatedRoute.routeStyle || this.parseRouteStyle(updatedRoute.attributes);
if (this.$refs.cesiumMap && this.activeRouteIds.includes(updatedRoute.id)) {
const route = this.routes.find(r => r.id === updatedRoute.id);
@ -1844,6 +2034,7 @@ export default {
if (platformToRemove && this.$refs.cesiumMap) {
if (platformToRemove.serverId) {
await delRoomPlatformIcon(platformToRemove.serverId).catch(() => {});
this.wsConnection?.sendSyncPlatformIcons?.();
if (typeof this.$refs.cesiumMap.removePlatformIconByServerId === 'function') {
this.$refs.cesiumMap.removePlatformIconByServerId(platformToRemove.serverId);
}
@ -1933,6 +2124,7 @@ export default {
this.showWaypointDialog = false;
this.$message.success('航点信息已持久化至数据库');
this.$nextTick(() => this.updateDeductionPositions());
this.wsConnection?.sendSyncWaypoints?.(this.selectedRouteDetails.id);
// Redis
if (roomId && sd.waypoints && sd.waypoints.length > 0) {
this.updateMissilePositionsAfterRouteEdit(roomId, sd.id, sd.platformId != null ? sd.platformId : 0, sd.waypoints);
@ -2024,6 +2216,7 @@ export default {
const frontendDrawingsStr = JSON.stringify(drawingsData);
await updateRooms({ id: this.currentRoomId, frontendDrawings: frontendDrawingsStr });
if (this.roomDetail) this.roomDetail.frontendDrawings = frontendDrawingsStr;
this.wsConnection?.sendSyncRoomDrawings?.();
},
/** 将任意日期字符串格式化为 yyyy-MM-dd HH:mm:ss,供日期选择器使用 */
formatKTimeForPicker(val) {
@ -2473,6 +2666,7 @@ export default {
entityData.serverId = res.data.id
entityData.roomId = this.currentRoomId
this.$message.success('平台图标已保存到当前房间')
this.wsConnection?.sendSyncPlatformIcons?.()
}
} catch (e) {
console.warn('Parse platform drag data or save failed', e)
@ -2491,13 +2685,21 @@ export default {
lat: entityData.lat,
heading: entityData.heading != null ? entityData.heading : 0,
iconScale: entityData.iconScale != null ? entityData.iconScale : 1
}).then(() => {}).catch(() => {})
}).then(() => {
this.wsConnection?.sendSyncPlatformIcons?.()
}).catch(() => {})
}, 500)
},
/** 平台图标从地图删除时同步删除服务端记录 */
onPlatformIconRemoved({ serverId }) {
if (!serverId) return
delRoomPlatformIcon(serverId).then(() => {}).catch(() => {})
delRoomPlatformIcon(serverId).then(() => {
this.wsConnection?.sendSyncPlatformIcons?.()
}).catch(() => {})
},
/** 探测区/威力区样式保存后通知其他设备同步 */
onPlatformStyleSaved() {
this.wsConnection?.sendSyncPlatformStyles?.()
},
handleScaleUnitChange(unit) {
@ -3355,6 +3557,7 @@ export default {
}
}
this.$message.success('已添加盘旋航点');
this.wsConnection?.sendSyncWaypoints?.(routeId);
} catch (e) {
this.$message.error(e.msg || '添加盘旋失败');
console.error(e);
@ -3444,6 +3647,7 @@ export default {
this.selectedRouteDetails = null;
}
}
this.wsConnection?.sendSyncRouteVisibility?.(route.id, false);
this.$message.info(`已取消航线: ${route.name}`);
return;
}
@ -3490,6 +3694,7 @@ export default {
} else {
this.$message.warning('该航线暂无坐标数据,无法在地图展示');
}
this.wsConnection?.sendSyncRouteVisibility?.(route.id, true);
}
} catch (error) {
console.error("获取航线详情失败:", error);
@ -3604,16 +3809,17 @@ export default {
this.$message.success(nextLocked ? '航线已上锁,无法修改' : '航线已解锁,可以编辑');
},
toggleRouteVisibility(route) {
async toggleRouteVisibility(route, opts) {
const index = this.activeRouteIds.indexOf(route.id);
const fromPlanSwitch = opts && opts.fromPlanSwitch;
if (index > -1) {
// 线
// 使 Vue
this.activeRouteIds = this.activeRouteIds.filter(id => id !== route.id);
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];
@ -3640,7 +3846,8 @@ export default {
}
} else {
// 线
this.selectRoute(route);
await this.selectRoute(route);
if (!fromPlanSwitch) this.wsConnection?.sendSyncRouteVisibility?.(route.id, true);
}
},

Loading…
Cancel
Save