Browse Source
# Conflicts: # ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomPlatformIconController.java # ruoyi-ui/src/lang/en.js # ruoyi-ui/src/lang/zh.jsctw
25 changed files with 1878 additions and 452 deletions
@ -0,0 +1,219 @@ |
|||
package com.ruoyi.websocket.service; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Comparator; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.messaging.simp.SimpMessagingTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 房间内用户「当前操作」聚合:正在编辑的对象、地图选中项等。内存存储,随会话清理。 |
|||
*/ |
|||
@Service |
|||
public class RoomUserActivityService { |
|||
|
|||
private static final String TYPE_USER_ACTIVITIES = "USER_ACTIVITIES"; |
|||
|
|||
/** roomId -> sessionId -> activity row */ |
|||
private final ConcurrentHashMap<Long, ConcurrentHashMap<String, Map<String, Object>>> byRoom = |
|||
new ConcurrentHashMap<>(); |
|||
|
|||
@Autowired |
|||
private SimpMessagingTemplate messagingTemplate; |
|||
|
|||
public void onEditLock(Long roomId, String sessionId, Map<String, Object> editor, |
|||
String objectType, Object objectId, String objectLabel) { |
|||
if (roomId == null || sessionId == null || objectType == null || objectId == null) { |
|||
return; |
|||
} |
|||
String label = objectLabel != null ? objectLabel : ""; |
|||
Map<String, Object> row = getOrCreateRow(roomId, sessionId, editor); |
|||
Map<String, Object> editing = new HashMap<>(); |
|||
editing.put("objectType", objectType); |
|||
editing.put("objectId", objectId); |
|||
editing.put("objectLabel", label); |
|||
editing.put("startedAt", System.currentTimeMillis()); |
|||
row.put("editing", editing); |
|||
} |
|||
|
|||
public void onEditUnlock(Long roomId, String sessionId, String objectType, Object objectId) { |
|||
if (roomId == null || sessionId == null || objectType == null || objectId == null) { |
|||
return; |
|||
} |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m == null) { |
|||
return; |
|||
} |
|||
Map<String, Object> row = m.get(sessionId); |
|||
if (row == null) { |
|||
return; |
|||
} |
|||
@SuppressWarnings("unchecked") |
|||
Map<String, Object> editing = (Map<String, Object>) row.get("editing"); |
|||
if (editing == null) { |
|||
return; |
|||
} |
|||
if (objectType.equals(String.valueOf(editing.get("objectType"))) |
|||
&& Objects.equals(objectId, editing.get("objectId"))) { |
|||
row.remove("editing"); |
|||
} |
|||
cleanupRow(roomId, sessionId, row, m); |
|||
} |
|||
|
|||
public void onSelection(Long roomId, String sessionId, Map<String, Object> editor, |
|||
String summary, int count, List<Map<String, Object>> items) { |
|||
if (roomId == null || sessionId == null) { |
|||
return; |
|||
} |
|||
Map<String, Object> row = getOrCreateRow(roomId, sessionId, editor); |
|||
if (count <= 0 || items == null || items.isEmpty()) { |
|||
row.remove("selection"); |
|||
} else { |
|||
Map<String, Object> sel = new HashMap<>(); |
|||
sel.put("summary", summary != null ? summary : ""); |
|||
sel.put("count", count); |
|||
sel.put("items", new ArrayList<>(items)); |
|||
sel.put("updatedAt", System.currentTimeMillis()); |
|||
row.put("selection", sel); |
|||
} |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m != null) { |
|||
cleanupRow(roomId, sessionId, row, m); |
|||
} |
|||
} |
|||
|
|||
public void removeSession(Long roomId, String sessionId) { |
|||
if (roomId == null || sessionId == null) { |
|||
return; |
|||
} |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m != null) { |
|||
m.remove(sessionId); |
|||
if (m.isEmpty()) { |
|||
byRoom.remove(roomId); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public List<Map<String, Object>> snapshot(Long roomId) { |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m == null || m.isEmpty()) { |
|||
return new ArrayList<>(); |
|||
} |
|||
List<Map<String, Object>> out = new ArrayList<>(); |
|||
for (Map<String, Object> row : m.values()) { |
|||
Map<String, Object> packed = packRowForClient(row); |
|||
if (packed != null) { |
|||
out.add(packed); |
|||
} |
|||
} |
|||
out.sort(Comparator.comparing(a -> editorSortKey(a.get("editor")))); |
|||
return out; |
|||
} |
|||
|
|||
public void broadcastSnapshot(Long roomId) { |
|||
if (roomId == null) { |
|||
return; |
|||
} |
|||
Map<String, Object> msg = new HashMap<>(); |
|||
msg.put("type", TYPE_USER_ACTIVITIES); |
|||
msg.put("activities", snapshot(roomId)); |
|||
messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); |
|||
} |
|||
|
|||
public void sendSnapshotToUser(Long roomId, String userName) { |
|||
if (roomId == null || userName == null || userName.isEmpty()) { |
|||
return; |
|||
} |
|||
Map<String, Object> msg = new HashMap<>(); |
|||
msg.put("type", TYPE_USER_ACTIVITIES); |
|||
msg.put("activities", snapshot(roomId)); |
|||
messagingTemplate.convertAndSendToUser(userName, "/queue/private", msg); |
|||
} |
|||
|
|||
private static String editorSortKey(Object editorObj) { |
|||
if (!(editorObj instanceof Map)) { |
|||
return ""; |
|||
} |
|||
Map<?, ?> ed = (Map<?, ?>) editorObj; |
|||
Object nn = ed.get("nickName"); |
|||
return nn != null ? String.valueOf(nn) : ""; |
|||
} |
|||
|
|||
private Map<String, Object> getOrCreateRow(Long roomId, String sessionId, Map<String, Object> editor) { |
|||
ConcurrentHashMap<String, Map<String, Object>> m = |
|||
byRoom.computeIfAbsent(roomId, k -> new ConcurrentHashMap<>()); |
|||
return m.compute(sessionId, (sid, old) -> { |
|||
if (old != null) { |
|||
if (editor != null) { |
|||
old.put("editor", new HashMap<>(editor)); |
|||
} |
|||
return old; |
|||
} |
|||
Map<String, Object> row = new HashMap<>(); |
|||
row.put("sessionId", sessionId); |
|||
if (editor != null) { |
|||
row.put("editor", new HashMap<>(editor)); |
|||
} |
|||
return row; |
|||
}); |
|||
} |
|||
|
|||
private void cleanupRow(Long roomId, String sessionId, Map<String, Object> row, |
|||
ConcurrentHashMap<String, Map<String, Object>> m) { |
|||
boolean hasEdit = row.containsKey("editing"); |
|||
boolean hasSel = false; |
|||
if (row.containsKey("selection")) { |
|||
Object selObj = row.get("selection"); |
|||
if (selObj instanceof Map) { |
|||
Map<?, ?> sel = (Map<?, ?>) selObj; |
|||
Object c = sel.get("count"); |
|||
int cnt = c instanceof Number ? ((Number) c).intValue() : 0; |
|||
if (cnt > 0) { |
|||
hasSel = true; |
|||
} else { |
|||
row.remove("selection"); |
|||
} |
|||
} else { |
|||
row.remove("selection"); |
|||
} |
|||
} |
|||
if (!hasEdit && !hasSel) { |
|||
m.remove(sessionId); |
|||
} |
|||
if (m.isEmpty()) { |
|||
byRoom.remove(roomId); |
|||
} |
|||
} |
|||
|
|||
private Map<String, Object> packRowForClient(Map<String, Object> row) { |
|||
boolean hasEdit = row.containsKey("editing"); |
|||
boolean hasSel = false; |
|||
if (row.containsKey("selection")) { |
|||
Object selObj = row.get("selection"); |
|||
if (selObj instanceof Map) { |
|||
Map<?, ?> sel = (Map<?, ?>) selObj; |
|||
Object c = sel.get("count"); |
|||
hasSel = c instanceof Number && ((Number) c).intValue() > 0; |
|||
} |
|||
} |
|||
if (!hasEdit && !hasSel) { |
|||
return null; |
|||
} |
|||
Map<String, Object> copy = new HashMap<>(); |
|||
copy.put("sessionId", row.get("sessionId")); |
|||
copy.put("editor", row.get("editor")); |
|||
if (hasEdit) { |
|||
copy.put("editing", row.get("editing")); |
|||
} |
|||
if (hasSel) { |
|||
copy.put("selection", row.get("selection")); |
|||
} |
|||
return copy; |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,244 @@ |
|||
<template> |
|||
<span class="menu-glyph" aria-hidden="true"> |
|||
<svg |
|||
viewBox="0 0 24 24" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
class="menu-glyph-svg" |
|||
> |
|||
<g |
|||
stroke="currentColor" |
|||
stroke-width="1.25" |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
> |
|||
<!-- 删除:线框方樽式垃圾桶 --> |
|||
<g v-if="name === 'trash'"> |
|||
<path d="M10 5V4a1 1 0 011-1h2a1 1 0 011 1v1" /> |
|||
<path d="M5 7h14" /> |
|||
<path d="M8 7l1 13h6l1-13" /> |
|||
<path d="M10 11v6M14 11v6" /> |
|||
</g> |
|||
<!-- 复制 --> |
|||
<g v-else-if="name === 'copy'"> |
|||
<rect x="8" y="8" width="11" height="11" rx="1.5" /> |
|||
<path d="M5 16V5a2 2 0 012-2h9" /> |
|||
</g> |
|||
<!-- 切换选择 --> |
|||
<g v-else-if="name === 'switch'"> |
|||
<path d="M5 9a7 7 0 0113-3" /> |
|||
<path d="M5 9h4V5M19 15a7 7 0 01-13 3" /> |
|||
<path d="M19 15h-4v4" /> |
|||
</g> |
|||
<!-- 文档编辑 / 命名 --> |
|||
<g v-else-if="name === 'editDoc'"> |
|||
<path d="M14 3h5v5" /> |
|||
<path d="M4 20V13l9-9 5 5-9 9H4z" /> |
|||
<path d="M12 6l6 6" /> |
|||
</g> |
|||
<!-- 航点编辑(罗盘+定位感) --> |
|||
<g v-else-if="name === 'waypoint'"> |
|||
<circle cx="12" cy="11" r="3" /> |
|||
<path d="M12 14v6M9 20h6" /> |
|||
</g> |
|||
<!-- 速度表意:简易仪表弧 --> |
|||
<g v-else-if="name === 'speed'"> |
|||
<path d="M5 18a7 7 0 0114 0" /> |
|||
<path d="M12 13v5" /> |
|||
<circle cx="12" cy="18" r="1" fill="currentColor" stroke="none" /> |
|||
</g> |
|||
<!-- 向前:带柄箭头 --> |
|||
<g v-else-if="name === 'arrowLeft'"> |
|||
<path d="m15 6-6 6 6 6M21 12H4" /> |
|||
</g> |
|||
<!-- 向后 --> |
|||
<g v-else-if="name === 'arrowRight'"> |
|||
<path d="m9 6 6 6-6 6M3 12h17" /> |
|||
</g> |
|||
<!-- 循环 / 盘旋切换 --> |
|||
<g v-else-if="name === 'refresh'"> |
|||
<path d="M21 12a9 9 0 01-9 9 5 5 0 01-5-5" /> |
|||
<path d="M3 12a9 9 0 019-9 4 4 0 014 4" /> |
|||
<path d="M3 7v5h5M21 17v-5h-5" /> |
|||
</g> |
|||
<!-- 上锁 --> |
|||
<g v-else-if="name === 'lock'"> |
|||
<rect x="6" y="10" width="12" height="10" rx="1.5" /> |
|||
<path d="M8 10V7a4 4 0 018 0v3" /> |
|||
</g> |
|||
<!-- 解锁 --> |
|||
<g v-else-if="name === 'unlock'"> |
|||
<rect x="6" y="10" width="12" height="10" rx="1.5" /> |
|||
<path d="M8 10V7a4 4 0 017-2" /> |
|||
</g> |
|||
<!-- 拆分 --> |
|||
<g v-else-if="name === 'scissors'"> |
|||
<circle cx="8" cy="8" r="2" /> |
|||
<circle cx="8" cy="16" r="2" /> |
|||
<path d="m14 6-6 6M14 18l-6-6" /> |
|||
</g> |
|||
<!-- 拆分复制:双文档 --> |
|||
<g v-else-if="name === 'docDuplicate'"> |
|||
<path d="M8 4h8l2 2v12H8V4z" /> |
|||
<path d="M6 8h8v12H6V8z" /> |
|||
</g> |
|||
<!-- 播放 / 推演 --> |
|||
<g v-else-if="name === 'play'"> |
|||
<path d="M9 7.5v9L18 12 9 7.5z" /> |
|||
</g> |
|||
<!-- 标牌 --> |
|||
<g v-else-if="name === 'tag'"> |
|||
<path d="M3 5a2 2 0 012-2h6l10 10-7 7-9-9V5z" /> |
|||
<circle cx="8.5" cy="7.5" r="1" fill="currentColor" stroke="none" /> |
|||
</g> |
|||
<!-- 铅笔(通用编辑) --> |
|||
<g v-else-if="name === 'pencil'"> |
|||
<path d="M12 20h9" /> |
|||
<path d="m16.5 3.5 4.5 4.5L8 21H3v-5L16.5 3.5z" /> |
|||
</g> |
|||
<!-- 探测:扇形波束 --> |
|||
<g v-else-if="name === 'radar'"> |
|||
<path d="M12 21c-4 0-7-3-7-7s3-7 7-7" /> |
|||
<path d="M12 21V11" /> |
|||
<path d="M12 11 6.5 6.5" /> |
|||
<path d="M12 11 17.5 6.5" /> |
|||
</g> |
|||
<!-- 威力:同心圆靶 --> |
|||
<g v-else-if="name === 'target'"> |
|||
<circle cx="12" cy="12" r="8" /> |
|||
<circle cx="12" cy="12" r="4.5" /> |
|||
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" /> |
|||
</g> |
|||
<!-- 显示 --> |
|||
<g v-else-if="name === 'eye'"> |
|||
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z" /> |
|||
<circle cx="12" cy="12" r="3" /> |
|||
</g> |
|||
<!-- 隐藏 --> |
|||
<g v-else-if="name === 'eyeOff'"> |
|||
<path d="M3 3l18 18" /> |
|||
<path d="M10.6 10.6A3 3 0 0012 15a3 3 0 002.4-5.4" /> |
|||
<path d="M9.9 5.1A10 10 0 0117.7 5c2.5 1 4.3 2.7 5.3 4M6.3 6.3C4.5 7.5 3 9.3 2 12c1.5 4 5.5 7 10 7 1.7 0 3.4-.4 4.9-1" /> |
|||
</g> |
|||
<!-- 导弹:简易三角体 --> |
|||
<g v-else-if="name === 'missile'"> |
|||
<path d="M12 3l2 3H10l2-3z" /> |
|||
<path d="M10 6h4l-1 10H11L10 6z" /> |
|||
<path d="M9 16l-2 3M15 16l2 3" /> |
|||
</g> |
|||
<!-- 填充:几何分层方块 --> |
|||
<g v-else-if="name === 'fill'"> |
|||
<rect x="5" y="5" width="10" height="10" rx="1" /> |
|||
<path d="M5 14h10M5 11h10" /> |
|||
<path d="M8 5v10" /> |
|||
</g> |
|||
<!-- 线宽 / 标尺 --> |
|||
<g v-else-if="name === 'ruler'"> |
|||
<path d="M4 16h16" /> |
|||
<path d="M6 16V12M10 16v-6M14 16v-4M18 16v-8" /> |
|||
</g> |
|||
<!-- 方位 --> |
|||
<g v-else-if="name === 'compass'"> |
|||
<circle cx="12" cy="12" r="8" /> |
|||
<path d="m12 8 2 4-2 6-2-6 2-4z" /> |
|||
</g> |
|||
<!-- 透明度 / 蒙版意象:画框+渐隐线 --> |
|||
<g v-else-if="name === 'opacity'"> |
|||
<rect x="4" y="5" width="16" height="14" rx="1.5" /> |
|||
<path d="M7 18c2-3 4-7 8-10" /> |
|||
<path d="M7 14c2-2 4-5 7-7" /> |
|||
</g> |
|||
<!-- 边框色:双框 --> |
|||
<g v-else-if="name === 'border'"> |
|||
<rect x="5" y="5" width="14" height="14" rx="2" /> |
|||
<rect x="8.5" y="8.5" width="7" height="7" rx="1" /> |
|||
</g> |
|||
<!-- 字号 T --> |
|||
<g v-else-if="name === 'type'"> |
|||
<path d="M9 5h6M12 5v12" /> |
|||
<path d="M8 17h8" /> |
|||
</g> |
|||
<!-- 位置:地理锚点(十字+圆) --> |
|||
<g v-else-if="name === 'anchor'"> |
|||
<circle cx="12" cy="12" r="3" /> |
|||
<path d="M12 3v4M12 17v4M3 12h4M17 12h4" /> |
|||
</g> |
|||
<!-- 伸缩框 --> |
|||
<g v-else-if="name === 'transform'"> |
|||
<path d="M4 8V4h4M16 4h4v4M20 16v4h-4M8 20H4v-4" /> |
|||
</g> |
|||
<!-- 圆点大小 --> |
|||
<g v-else-if="name === 'dot'"> |
|||
<circle cx="12" cy="12" r="3" /> |
|||
<circle cx="12" cy="12" r="7" /> |
|||
</g> |
|||
<!-- 展开箭头 --> |
|||
<g v-else-if="name === 'chevronDown'"> |
|||
<path d="m6 9 6 6 6-6" /> |
|||
</g> |
|||
<g v-else-if="name === 'chevronRight'"> |
|||
<path d="m9 6 6 6-6 6" /> |
|||
</g> |
|||
<!-- 子项引导 --> |
|||
<g v-else-if="name === 'subDot'"> |
|||
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" /> |
|||
</g> |
|||
<!-- 色板 / 图标色(滴状几何) --> |
|||
<g v-else-if="name === 'droplet'"> |
|||
<path d="M12 21a5 5 0 005-5c0-3-5-9-5-9S7 13 7 16a5 5 0 005 5z" /> |
|||
</g> |
|||
<!-- 颜色与大小:滑杆+方块 --> |
|||
<g v-else-if="name === 'style'"> |
|||
<rect x="5" y="6" width="6" height="6" rx="1" /> |
|||
<path d="M15 8.5h4M15 12h4M15 15.5h4" /> |
|||
</g> |
|||
<!-- 默认占位 --> |
|||
<circle v-else cx="12" cy="12" r="4" /> |
|||
</g> |
|||
</svg> |
|||
</span> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'MenuGlyph', |
|||
props: { |
|||
name: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
small: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.menu-glyph { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
width: 18px; |
|||
height: 18px; |
|||
margin-right: 10px; |
|||
color: inherit; |
|||
vertical-align: middle; |
|||
} |
|||
.menu-glyph.is-small { |
|||
width: 14px; |
|||
height: 14px; |
|||
margin-right: 8px; |
|||
} |
|||
.menu-glyph-svg { |
|||
width: 18px; |
|||
height: 18px; |
|||
display: block; |
|||
} |
|||
.menu-glyph.is-small .menu-glyph-svg { |
|||
width: 14px; |
|||
height: 14px; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue