diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java index 26c4a13..060ff4e 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/WhiteboardController.java @@ -1,17 +1,30 @@ package com.ruoyi.web.controller; import java.util.List; +import java.util.HashMap; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.framework.config.ServerConfig; +import com.ruoyi.system.domain.RoomUserProfile; +import com.ruoyi.system.service.IRoomUserProfileService; import com.ruoyi.websocket.service.WhiteboardRoomService; /** @@ -24,6 +37,12 @@ public class WhiteboardController extends BaseController { @Autowired private WhiteboardRoomService whiteboardRoomService; + @Autowired + private IRoomUserProfileService roomUserProfileService; + + @Autowired + private ServerConfig serverConfig; + /** 获取房间下所有白板列表 */ @GetMapping("/{roomId}/whiteboards") public AjaxResult list(@PathVariable Long roomId) { @@ -60,4 +79,85 @@ public class WhiteboardController extends BaseController { boolean ok = whiteboardRoomService.deleteWhiteboard(roomId, whiteboardId); return ok ? success() : error("删除失败"); } + + /** 保存白板平台样式(Redis Key: whiteboard:scheme:{schemeId}:platform:{platformInstanceId}:style) */ + @PostMapping("/whiteboard/platform/style") + public AjaxResult savePlatformStyle(@RequestBody java.util.Map body) { + if (body == null) return error("参数不能为空"); + String schemeId = body.get("schemeId") == null ? null : String.valueOf(body.get("schemeId")); + String platformInstanceId = body.get("platformInstanceId") == null ? null : String.valueOf(body.get("platformInstanceId")); + Object style = body.get("style"); + if (schemeId == null || schemeId.trim().isEmpty() || platformInstanceId == null || platformInstanceId.trim().isEmpty() || style == null) { + return error("schemeId、platformInstanceId、style 不能为空"); + } + whiteboardRoomService.saveWhiteboardPlatformStyle(schemeId, platformInstanceId, style); + return success(); + } + + /** 获取白板平台样式 */ + @GetMapping("/whiteboard/platform/style") + public AjaxResult getPlatformStyle(@RequestParam String schemeId, @RequestParam String platformInstanceId) { + Object style = whiteboardRoomService.getWhiteboardPlatformStyle(schemeId, platformInstanceId); + return success(style); + } + + /** 删除白板平台样式 */ + @DeleteMapping("/whiteboard/platform/style") + public AjaxResult deletePlatformStyle(@RequestParam String schemeId, @RequestParam String platformInstanceId) { + whiteboardRoomService.deleteWhiteboardPlatformStyle(schemeId, platformInstanceId); + return success(); + } + + @GetMapping("/user/profile") + public AjaxResult getCurrentUserProfile() { + LoginUser loginUser = getLoginUser(); + RoomUserProfile profile = roomUserProfileService.getOrInitByUser(loginUser.getUserId(), loginUser.getUsername()); + return success(buildProfilePayload(loginUser, profile)); + } + + @PutMapping("/user/profile") + public AjaxResult updateCurrentUserProfile(@RequestBody Map body) { + LoginUser loginUser = getLoginUser(); + String displayName = body != null && body.get("displayName") != null ? String.valueOf(body.get("displayName")).trim() : null; + if (StringUtils.isEmpty(displayName)) { + return error("用户名不能为空"); + } + if (displayName.length() > 32) { + return error("用户名长度不能超过32个字符"); + } + RoomUserProfile profile = roomUserProfileService.updateDisplayName(loginUser.getUserId(), displayName, loginUser.getUsername()); + return success(buildProfilePayload(loginUser, profile)); + } + + @PostMapping("/user/profile/avatar") + public AjaxResult uploadCurrentUserAvatar(@RequestParam("avatarfile") MultipartFile file) throws Exception { + if (file == null || file.isEmpty()) { + return error("请选择头像图片"); + } + LoginUser loginUser = getLoginUser(); + RoomUserProfile oldProfile = roomUserProfileService.getOrInitByUser(loginUser.getUserId(), loginUser.getUsername()); + String oldAvatar = oldProfile != null ? oldProfile.getAvatar() : null; + + String avatar = FileUploadUtils.upload(RuoYiConfig.getRoomUserAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true); + RoomUserProfile profile = roomUserProfileService.updateAvatar(loginUser.getUserId(), avatar, loginUser.getUsername()); + + // 仅删除本模块上传目录中的旧头像,避免误删系统头像或外链头像 + if (StringUtils.isNotEmpty(oldAvatar) && oldAvatar.startsWith("/room-user-avatar/")) { + FileUtils.deleteFile(RuoYiConfig.getProfile() + FileUtils.stripPrefix(oldAvatar)); + } + + AjaxResult ajax = success(buildProfilePayload(loginUser, profile)); + ajax.put("imgUrl", avatar); + ajax.put("fullImgUrl", avatar.startsWith("http") ? avatar : (serverConfig.getUrl() + avatar)); + return ajax; + } + + private Map buildProfilePayload(LoginUser loginUser, RoomUserProfile profile) { + Map payload = new HashMap<>(); + payload.put("userId", loginUser.getUserId()); + payload.put("userName", loginUser.getUsername()); + payload.put("displayName", profile != null ? profile.getDisplayName() : loginUser.getUsername()); + payload.put("avatar", profile != null ? profile.getAvatar() : null); + return payload; + } } 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 4c007a1..57e9bb3 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 @@ -16,9 +16,12 @@ import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.websocket.config.LoginUserPrincipal; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.Rooms; +import com.ruoyi.system.domain.RoomUserProfile; import com.ruoyi.system.service.IRoomsService; +import com.ruoyi.system.service.IRoomUserProfileService; import com.ruoyi.websocket.dto.RoomMemberDTO; import com.ruoyi.websocket.service.RoomChatService; +import com.ruoyi.websocket.service.RoomOnlineMemberBroadcastService; import com.ruoyi.websocket.service.RoomWebSocketService; /** @@ -41,6 +44,12 @@ public class RoomWebSocketController { @Autowired private IRoomsService roomsService; + @Autowired + private RoomOnlineMemberBroadcastService roomOnlineMemberBroadcastService; + + @Autowired + private IRoomUserProfileService roomUserProfileService; + private static final String TYPE_JOIN = "JOIN"; private static final String TYPE_LEAVE = "LEAVE"; private static final String TYPE_PING = "PING"; @@ -49,9 +58,7 @@ public class RoomWebSocketController { private static final String TYPE_CHAT_HISTORY = "CHAT_HISTORY"; private static final String TYPE_PRIVATE_CHAT_HISTORY = "PRIVATE_CHAT_HISTORY"; private static final String TYPE_PRIVATE_CHAT_HISTORY_REQUEST = "PRIVATE_CHAT_HISTORY_REQUEST"; - private static final String TYPE_MEMBER_JOINED = "MEMBER_JOINED"; 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"; @@ -119,11 +126,12 @@ public class RoomWebSocketController { String objectType = body != null ? String.valueOf(body.get("objectType")) : null; Object objectIdObj = body != null ? body.get("objectId") : null; if (objectType == null || objectIdObj == null) return; + RoomUserProfile profile = roomUserProfileService.getOrInitByUser(loginUser.getUserId(), loginUser.getUsername()); Map editor = new HashMap<>(); editor.put("userId", loginUser.getUserId()); editor.put("userName", loginUser.getUsername()); - editor.put("nickName", loginUser.getUser().getNickName()); + editor.put("nickName", profile != null ? profile.getDisplayName() : loginUser.getUser().getNickName()); editor.put("sessionId", sessionId); Map msg = new HashMap<>(); @@ -163,18 +171,7 @@ public class RoomWebSocketController { RoomMemberDTO member = buildMember(loginUser, sessionId, roomId, body); roomWebSocketService.joinRoom(roomId, sessionId, member); - - List members = roomWebSocketService.getRoomMembers(roomId); - Map memberListMsg = new HashMap<>(); - memberListMsg.put("type", TYPE_MEMBER_LIST); - memberListMsg.put("members", members); - - Map joinedMsg = new HashMap<>(); - joinedMsg.put("type", TYPE_MEMBER_JOINED); - joinedMsg.put("member", member); - - String topic = "/topic/room/" + roomId; - messagingTemplate.convertAndSend(topic, memberListMsg); + roomOnlineMemberBroadcastService.broadcastAfterMembershipChange(roomId); List chatHistory = roomChatService.getGroupChatHistory(roomId); Map chatHistoryMsg = new HashMap<>(); @@ -195,6 +192,7 @@ public class RoomWebSocketController { msg.put("sessionId", sessionId); messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + roomOnlineMemberBroadcastService.broadcastAfterMembershipChange(roomId); } private void handlePing(Long roomId, String sessionId, LoginUser loginUser) { @@ -211,11 +209,12 @@ public class RoomWebSocketController { String content = body != null && body.containsKey("content") ? String.valueOf(body.get("content")) : ""; if (content.isEmpty()) return; + RoomUserProfile profile = roomUserProfileService.getOrInitByUser(loginUser.getUserId(), loginUser.getUsername()); Map sender = new HashMap<>(); sender.put("userId", loginUser.getUserId()); sender.put("userName", loginUser.getUsername()); - sender.put("nickName", loginUser.getUser().getNickName()); - sender.put("avatar", loginUser.getUser().getAvatar()); + sender.put("nickName", profile != null ? profile.getDisplayName() : loginUser.getUser().getNickName()); + sender.put("avatar", profile != null ? profile.getAvatar() : loginUser.getUser().getAvatar()); sender.put("sessionId", sessionId); Map msg = new HashMap<>(); @@ -238,15 +237,16 @@ public class RoomWebSocketController { String targetUserName = String.valueOf(targetUserNameObj); if (targetUserName.equals(loginUser.getUsername())) return; - List members = roomWebSocketService.getRoomMembers(roomId); + List members = roomOnlineMemberBroadcastService.aggregateMembersForView(roomId); boolean targetInRoom = members.stream().anyMatch(m -> targetUserName.equals(m.getUserName())); if (!targetInRoom) return; + RoomUserProfile profile = roomUserProfileService.getOrInitByUser(loginUser.getUserId(), loginUser.getUsername()); Map sender = new HashMap<>(); sender.put("userId", loginUser.getUserId()); sender.put("userName", loginUser.getUsername()); - sender.put("nickName", loginUser.getUser().getNickName()); - sender.put("avatar", loginUser.getUser().getAvatar()); + sender.put("nickName", profile != null ? profile.getDisplayName() : loginUser.getUser().getNickName()); + sender.put("avatar", profile != null ? profile.getAvatar() : loginUser.getUser().getAvatar()); sender.put("sessionId", sessionId); Map msg = new HashMap<>(); @@ -272,7 +272,7 @@ public class RoomWebSocketController { Long targetUserId = targetUserIdObj instanceof Number ? ((Number) targetUserIdObj).longValue() : null; if (targetUserId == null) return; - List members = roomWebSocketService.getRoomMembers(roomId); + List members = roomOnlineMemberBroadcastService.aggregateMembersForView(roomId); boolean targetInRoom = members.stream().anyMatch(m -> targetUserId.equals(m.getUserId())); if (!targetInRoom) return; @@ -324,11 +324,13 @@ public class RoomWebSocketController { } private RoomMemberDTO buildMember(LoginUser loginUser, String sessionId, Long roomId, Map body) { + RoomUserProfile profile = roomUserProfileService.getOrInitByUser(loginUser.getUserId(), loginUser.getUsername()); RoomMemberDTO dto = new RoomMemberDTO(); dto.setUserId(loginUser.getUserId()); dto.setUserName(loginUser.getUsername()); - dto.setNickName(loginUser.getUser().getNickName()); - dto.setAvatar(loginUser.getUser().getAvatar()); + dto.setNickName(profile != null ? profile.getDisplayName() : loginUser.getUser().getNickName()); + dto.setAvatar(profile != null ? profile.getAvatar() : loginUser.getUser().getAvatar()); + dto.setRoomId(roomId); dto.setSessionId(sessionId); dto.setDeviceId(body != null && body.containsKey("deviceId") ? String.valueOf(body.get("deviceId")) : "default"); dto.setJoinedAt(System.currentTimeMillis()); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/dto/RoomMemberDTO.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/dto/RoomMemberDTO.java index 1f642fb..d09da7e 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/websocket/dto/RoomMemberDTO.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/dto/RoomMemberDTO.java @@ -12,6 +12,11 @@ public class RoomMemberDTO implements Serializable { /** 用户ID */ private Long userId; + /** + * 该成员会话所属的房间ID(用于前端按“当前房间”统计在线人数)。 + * 大房间聚合下发给子房间时,仍保留成员的原始房间来源。 + */ + private Long roomId; /** 用户账号 */ private String userName; /** 用户昵称 */ @@ -35,6 +40,14 @@ public class RoomMemberDTO implements Serializable { this.userId = userId; } + public Long getRoomId() { + return roomId; + } + + public void setRoomId(Long roomId) { + this.roomId = roomId; + } + public String getUserName() { return userName; } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/listener/WebSocketDisconnectListener.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/listener/WebSocketDisconnectListener.java index 86fdbdb..3060c67 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/websocket/listener/WebSocketDisconnectListener.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/listener/WebSocketDisconnectListener.java @@ -8,6 +8,7 @@ import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionDisconnectEvent; import com.ruoyi.websocket.dto.RoomSessionInfo; +import com.ruoyi.websocket.service.RoomOnlineMemberBroadcastService; import com.ruoyi.websocket.service.RoomWebSocketService; /** @@ -27,6 +28,9 @@ public class WebSocketDisconnectListener implements ApplicationListener aggregateMembersForView(Long viewRoomId) { + if (viewRoomId == null) { + return new ArrayList<>(); + } + Rooms room = roomsService.selectRoomsById(viewRoomId); + if (room == null) { + return new ArrayList<>(); + } + LinkedHashMap bySession = new LinkedHashMap<>(); + if (room.getParentId() != null) { + putAllMembers(bySession, viewRoomId, roomWebSocketService.getRoomMembers(viewRoomId)); + putAllMembers(bySession, room.getParentId(), roomWebSocketService.getRoomMembers(room.getParentId())); + } else { + putAllMembers(bySession, viewRoomId, roomWebSocketService.getRoomMembers(viewRoomId)); + Rooms query = new Rooms(); + query.setParentId(viewRoomId); + List children = roomsService.selectRoomsList(query); + if (children != null) { + for (Rooms child : children) { + if (child != null && child.getId() != null) { + putAllMembers(bySession, child.getId(), roomWebSocketService.getRoomMembers(child.getId())); + } + } + } + } + return new ArrayList<>(bySession.values()); + } + + private void putAllMembers(Map bySession, Long sourceRoomId, List list) { + if (list == null) { + return; + } + for (RoomMemberDTO m : list) { + if (m != null && m.getSessionId() != null) { + // 兼容历史数据:老成员可能没有 roomId 字段 + if (m.getRoomId() == null) { + m.setRoomId(sourceRoomId); + } + bySession.put(m.getSessionId(), m); + } + } + } + + /** + * 在指定房间发生加入/离开后,刷新该房间、其父房间(若有)或所有子房间(若为大房间)的在线列表推送。 + */ + public void broadcastAfterMembershipChange(Long changedRoomId) { + if (changedRoomId == null) { + return; + } + Rooms changed = roomsService.selectRoomsById(changedRoomId); + if (changed == null) { + return; + } + Set topicRoomIds = new HashSet<>(); + topicRoomIds.add(changedRoomId); + if (changed.getParentId() != null) { + topicRoomIds.add(changed.getParentId()); + } else { + Rooms query = new Rooms(); + query.setParentId(changedRoomId); + List children = roomsService.selectRoomsList(query); + if (children != null) { + for (Rooms child : children) { + if (child != null && child.getId() != null) { + topicRoomIds.add(child.getId()); + } + } + } + } + for (Long tid : topicRoomIds) { + Map msg = new HashMap<>(); + msg.put("type", TYPE_MEMBER_LIST); + msg.put("members", aggregateMembersForView(tid)); + messagingTemplate.convertAndSend("/topic/room/" + tid, msg); + } + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java index ab42731..d229f7d 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java @@ -18,6 +18,7 @@ public class WhiteboardRoomService { private static final String ROOM_WHITEBOARDS_PREFIX = "room:"; private static final String ROOM_WHITEBOARDS_SUFFIX = ":whiteboards"; + private static final String WHITEBOARD_SCHEME_PLATFORM_STYLE_PREFIX = "whiteboard:scheme:"; private static final int EXPIRE_DAYS = 60; @Autowired @@ -28,6 +29,10 @@ public class WhiteboardRoomService { return ROOM_WHITEBOARDS_PREFIX + roomId + ROOM_WHITEBOARDS_SUFFIX; } + private String whiteboardPlatformStyleKey(String schemeId, String platformInstanceId) { + return WHITEBOARD_SCHEME_PLATFORM_STYLE_PREFIX + schemeId + ":platform:" + platformInstanceId + ":style"; + } + /** 获取房间下所有白板列表 */ @SuppressWarnings("unchecked") public List listWhiteboards(Long roomId) { @@ -121,4 +126,31 @@ public class WhiteboardRoomService { String key = whiteboardsKey(roomId); redisTemplate.opsForValue().set(key, list, EXPIRE_DAYS, TimeUnit.DAYS); } + + /** 保存白板平台样式(颜色、大小等) */ + public void saveWhiteboardPlatformStyle(String schemeId, String platformInstanceId, Object style) { + if (schemeId == null || schemeId.trim().isEmpty() || platformInstanceId == null || platformInstanceId.trim().isEmpty() || style == null) { + return; + } + String key = whiteboardPlatformStyleKey(schemeId, platformInstanceId); + redisTemplate.opsForValue().set(key, style, EXPIRE_DAYS, TimeUnit.DAYS); + } + + /** 获取白板平台样式(颜色、大小等) */ + public Object getWhiteboardPlatformStyle(String schemeId, String platformInstanceId) { + if (schemeId == null || schemeId.trim().isEmpty() || platformInstanceId == null || platformInstanceId.trim().isEmpty()) { + return null; + } + String key = whiteboardPlatformStyleKey(schemeId, platformInstanceId); + return redisTemplate.opsForValue().get(key); + } + + /** 删除白板平台样式(平台删除时可清理) */ + public void deleteWhiteboardPlatformStyle(String schemeId, String platformInstanceId) { + if (schemeId == null || schemeId.trim().isEmpty() || platformInstanceId == null || platformInstanceId.trim().isEmpty()) { + return; + } + String key = whiteboardPlatformStyleKey(schemeId, platformInstanceId); + redisTemplate.delete(key); + } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java index 29281cf..39397c8 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java @@ -105,6 +105,14 @@ public class RuoYiConfig } /** + * 获取协同房间用户头像上传路径(与平台文件目录隔离) + */ + public static String getRoomUserAvatarPath() + { + return getProfile() + "/room-user-avatar"; + } + + /** * 获取下载路径 */ public static String getDownloadPath() diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomUserProfile.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomUserProfile.java new file mode 100644 index 0000000..4024c49 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomUserProfile.java @@ -0,0 +1,47 @@ +package com.ruoyi.system.domain; + +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 协同房间用户资料(显示名、头像) + */ +public class RoomUserProfile extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + private Long userId; + private String displayName; + private String avatar; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RoomUserProfileMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RoomUserProfileMapper.java new file mode 100644 index 0000000..853aa25 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RoomUserProfileMapper.java @@ -0,0 +1,12 @@ +package com.ruoyi.system.mapper; + +import com.ruoyi.system.domain.RoomUserProfile; + +public interface RoomUserProfileMapper { + + RoomUserProfile selectByUserId(Long userId); + + int insertRoomUserProfile(RoomUserProfile profile); + + int updateRoomUserProfile(RoomUserProfile profile); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IRoomUserProfileService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IRoomUserProfileService.java new file mode 100644 index 0000000..1574807 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IRoomUserProfileService.java @@ -0,0 +1,12 @@ +package com.ruoyi.system.service; + +import com.ruoyi.system.domain.RoomUserProfile; + +public interface IRoomUserProfileService { + + RoomUserProfile getOrInitByUser(Long userId, String defaultUserName); + + RoomUserProfile updateDisplayName(Long userId, String displayName, String defaultUserName); + + RoomUserProfile updateAvatar(Long userId, String avatar, String defaultUserName); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomUserProfileServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomUserProfileServiceImpl.java new file mode 100644 index 0000000..1a5032c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomUserProfileServiceImpl.java @@ -0,0 +1,63 @@ +package com.ruoyi.system.service.impl; + +import java.util.Date; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.RoomUserProfile; +import com.ruoyi.system.mapper.RoomUserProfileMapper; +import com.ruoyi.system.service.IRoomUserProfileService; + +@Service +public class RoomUserProfileServiceImpl implements IRoomUserProfileService { + + public static final String DEFAULT_AVATAR = + "https://cube.elemecdn.com/0/88dd03f9bf287d08f58fbcf58fddbf4a8c6/avatar.png"; + + @Autowired + private RoomUserProfileMapper roomUserProfileMapper; + + @Override + public RoomUserProfile getOrInitByUser(Long userId, String defaultUserName) { + if (userId == null) { + return null; + } + RoomUserProfile profile = roomUserProfileMapper.selectByUserId(userId); + if (profile != null) { + return profile; + } + RoomUserProfile created = new RoomUserProfile(); + created.setUserId(userId); + created.setDisplayName(StringUtils.isNotEmpty(defaultUserName) ? defaultUserName : "用户" + userId); + created.setAvatar(DEFAULT_AVATAR); + Date now = new Date(); + created.setCreateTime(now); + created.setUpdateTime(now); + roomUserProfileMapper.insertRoomUserProfile(created); + return roomUserProfileMapper.selectByUserId(userId); + } + + @Override + public RoomUserProfile updateDisplayName(Long userId, String displayName, String defaultUserName) { + RoomUserProfile current = getOrInitByUser(userId, defaultUserName); + if (current == null) { + return null; + } + current.setDisplayName(StringUtils.isNotEmpty(displayName) ? displayName : current.getDisplayName()); + current.setUpdateTime(new Date()); + roomUserProfileMapper.updateRoomUserProfile(current); + return roomUserProfileMapper.selectByUserId(userId); + } + + @Override + public RoomUserProfile updateAvatar(Long userId, String avatar, String defaultUserName) { + RoomUserProfile current = getOrInitByUser(userId, defaultUserName); + if (current == null) { + return null; + } + current.setAvatar(StringUtils.isNotEmpty(avatar) ? avatar : current.getAvatar()); + current.setUpdateTime(new Date()); + roomUserProfileMapper.updateRoomUserProfile(current); + return roomUserProfileMapper.selectByUserId(userId); + } +} diff --git a/ruoyi-system/src/main/resources/mapper/system/RoomUserProfileMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RoomUserProfileMapper.xml new file mode 100644 index 0000000..1078afd --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/RoomUserProfileMapper.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + select id, user_id, display_name, avatar, create_time, update_time + from room_user_profile + + + + + + insert into room_user_profile + + user_id, + display_name, + avatar, + create_time, + update_time, + + + #{userId}, + #{displayName}, + #{avatar}, + #{createTime}, + #{updateTime}, + + + + + update room_user_profile + + display_name = #{displayName}, + avatar = #{avatar}, + update_time = #{updateTime}, + + where user_id = #{userId} + + diff --git a/ruoyi-ui/src/api/system/whiteboard.js b/ruoyi-ui/src/api/system/whiteboard.js index 73d9f43..2b6943f 100644 --- a/ruoyi-ui/src/api/system/whiteboard.js +++ b/ruoyi-ui/src/api/system/whiteboard.js @@ -41,3 +41,58 @@ export function deleteWhiteboard(roomId, whiteboardId) { method: 'delete' }) } + +/** 保存白板平台样式(Redis key: whiteboard:scheme:{schemeId}:platform:{platformInstanceId}:style) */ +export function saveWhiteboardPlatformStyle(data) { + return request({ + url: '/room/whiteboard/platform/style', + method: 'post', + data: data || {} + }) +} + +/** 获取白板平台样式 */ +export function getWhiteboardPlatformStyle(params) { + return request({ + url: '/room/whiteboard/platform/style', + method: 'get', + params: params || {} + }) +} + +/** 删除白板平台样式 */ +export function deleteWhiteboardPlatformStyle(params) { + return request({ + url: '/room/whiteboard/platform/style', + method: 'delete', + params: params || {} + }) +} + +/** 获取当前用户协同资料(显示名、头像) */ +export function getRoomUserProfile() { + return request({ + url: '/room/user/profile', + method: 'get' + }) +} + +/** 更新当前用户协同显示名 */ +export function updateRoomUserProfile(data) { + return request({ + url: '/room/user/profile', + method: 'put', + data: data || {} + }) +} + +/** 上传当前用户协同头像(独立目录) */ +export function uploadRoomUserAvatar(file) { + const formData = new FormData() + formData.append('avatarfile', file) + return request({ + url: '/room/user/profile/avatar', + method: 'post', + data: formData + }) +} diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue index d75ff09..9c66f78 100644 --- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue +++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue @@ -1,5 +1,6 @@ @@ -653,6 +691,12 @@ export default { showColorPickerFor: null, showWidthPicker: false, showSizePicker: false, + sizePickerType: '', + showWhiteboardPlatformStyleDialog: false, + whiteboardPlatformStyleForm: { + color: '#008aff', + iconScale: 1.5 + }, showOpacityPicker: false, showFontSizePicker: false, showBearingTypeMenu: false, @@ -664,6 +708,7 @@ export default { ], presetWidths: [1, 2, 3, 4, 5, 6, 8, 10, 12], presetSizes: [6, 8, 10, 12, 14, 16, 18, 20, 24], + presetPlatformScales: [0.8, 1, 1.2, 1.5, 1.8, 2.2, 2.6, 3], presetOpacities: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], presetFontSizes: [8, 10, 12, 14, 16, 18, 20, 24, 28, 32] } @@ -755,6 +800,34 @@ export default { handleShowTransformBox() { this.$emit('show-transform-box') }, + openWhiteboardPlatformStyleDialog() { + const color = (this.entityData && this.entityData.color) || '#008aff' + const iconScale = this.entityData && this.entityData.iconScale != null ? Number(this.entityData.iconScale) : 1.5 + this.whiteboardPlatformStyleForm = { + color, + iconScale: Number.isFinite(iconScale) ? iconScale : 1.5 + } + this.showWhiteboardPlatformStyleDialog = true + }, + handleWhiteboardColorActiveChange(color) { + if (color) this.whiteboardPlatformStyleForm.color = color + }, + confirmWhiteboardPlatformStyle() { + const color = this.whiteboardPlatformStyleForm.color || '#008aff' + const iconScale = Number(this.whiteboardPlatformStyleForm.iconScale) + if (!Number.isFinite(iconScale) || iconScale <= 0) { + this.$message && this.$message.warning('请选择有效大小') + return + } + this.showWhiteboardPlatformStyleDialog = false + this.$emit('apply-whiteboard-platform-style', { + id: this.entityData && this.entityData.id, + color, + iconScale + }) + // 兜底关闭菜单,避免菜单残留 + this.$emit('close-menu') + }, handleToggleRouteLabel() { this.$emit('toggle-route-label') @@ -927,15 +1000,17 @@ export default { this.showWidthPicker = false }, - toggleSizePicker() { + toggleSizePicker(type = 'default') { if (this.showSizePicker) { this.showSizePicker = false + this.sizePickerType = '' } else { // 隐藏其他选择器 this.showColorPickerFor = null this.showWidthPicker = false this.showOpacityPicker = false this.showFontSizePicker = false + this.sizePickerType = type this.showSizePicker = true } }, @@ -943,6 +1018,7 @@ export default { selectSize(size) { this.$emit('update-property', 'size', size) this.showSizePicker = false + this.sizePickerType = '' }, toggleOpacityPicker() { diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 9c1fa70..0e65945 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -31,7 +31,6 @@ /> @@ -8742,6 +8743,16 @@ export default { entity.polyline.width = data.width } break + case 'platformIcon': + if (entity.billboard) { + if (data.iconScale != null) { + this.updatePlatformIconBillboardSize(data) + } + if (data.color) { + entity.billboard.color = Cesium.Color.fromCssColorString(data.color) + } + } + break case 'text': { this.ensureMapScreenDomLabelRegistry() const tkey = entity.id ? `map-dom-maptext-text-${entity.id}` : null @@ -11600,6 +11611,20 @@ export default { // 白板实体:通知父组件更新 contentByTime if (entityData.isWhiteboard && this.getDrawingEntityTypes().includes(entityData.type)) { this.$emit('whiteboard-drawing-updated', this.serializeWhiteboardDrawingEntity(entityData)) + } else if (entityData.isWhiteboard && entityData.type === 'platformIcon') { + this.$emit('whiteboard-platform-updated', { + id: entityData.id, + type: 'platformIcon', + platformId: entityData.platformId, + platform: entityData.platform || {}, + platformName: entityData.platformName || (entityData.platform && entityData.platform.name) || '', + lat: entityData.lat, + lng: entityData.lng, + heading: entityData.heading != null ? entityData.heading : 0, + iconScale: entityData.iconScale != null ? entityData.iconScale : 1.5, + label: entityData.label || '', + color: entityData.color || '#ffffff' + }) } else if (this.getDrawingEntityTypes().includes(entityData.type)) { this.notifyDrawingEntitiesChanged() } @@ -11637,6 +11662,54 @@ export default { this.viewer.scene.requestRender() this.$message && this.$message.success('已显示伸缩框') }, + /** 白板平台右键“颜色与大小”统一弹窗:点确定后一次性应用并回传父组件保存 */ + applyWhiteboardPlatformStyleFromMenu(payload) { + const targetId = payload && payload.id + let ed = null + if (targetId && this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[targetId]) { + ed = this.whiteboardEntityDataMap[targetId] + } else if (this.contextMenu && this.contextMenu.entityData) { + ed = this.contextMenu.entityData + } + if (!ed || ed.type !== 'platformIcon' || !ed.isWhiteboard) { + this.contextMenu.visible = false + return + } + const color = (payload && payload.color) || ed.color || '#008aff' + const rawScale = payload && payload.iconScale != null ? Number(payload.iconScale) : (ed.iconScale != null ? Number(ed.iconScale) : 1.5) + const iconScale = Math.max(0.2, Math.min(3, Number.isFinite(rawScale) ? rawScale : 1.5)) + ed.color = color + ed.iconScale = iconScale + this.updateEntityStyle(ed) + if (this.whiteboardEntityDataMap && ed.id && this.whiteboardEntityDataMap[ed.id]) { + this.whiteboardEntityDataMap[ed.id].color = color + this.whiteboardEntityDataMap[ed.id].iconScale = iconScale + } + this.$emit('whiteboard-platform-updated', { + id: ed.id, + type: 'platformIcon', + platformId: ed.platformId, + platform: ed.platform || {}, + platformName: ed.platformName || (ed.platform && ed.platform.name) || '', + lat: ed.lat, + lng: ed.lng, + heading: ed.heading != null ? ed.heading : 0, + iconScale, + label: ed.label || '', + color + }) + // 样式更新单独事件,避免被位置更新事件覆盖 + this.$emit('whiteboard-platform-style-updated', { + id: ed.id, + color, + iconScale + }) + this.contextMenu.visible = false + if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) { + this.viewer.scene.requestRender() + } + this.$message && this.$message.success('白板平台样式已更新') + }, /** 右键「在此之前插入航线」:先画平台前的航点,右键结束时将平台作为最后一站 */ handleStartRouteBeforePlatform() { @@ -12289,12 +12362,36 @@ export default { existing.lng = ed.lng existing.heading = ed.heading != null ? ed.heading : 0 existing.iconScale = ed.iconScale != null ? ed.iconScale : 1.5 + existing.color = ed.color || existing.color || '#008aff' existing.entity.position = Cesium.Cartesian3.fromDegrees(ed.lng, ed.lat) existing.entity.billboard.rotation = Math.PI / 2 - (existing.heading * Math.PI / 180) this.updatePlatformIconBillboardSize(existing) + if (existing.entity.billboard) { + existing.entity.billboard.color = Cesium.Color.fromCssColorString(existing.color) + } + this.ensureWhiteboardPlatformColorableImage(existing) if (existing.transformHandles) this.updateTransformHandlePositions(existing) if (this.viewer.scene && this.viewer.scene.requestRender) this.viewer.scene.requestRender() }, + /** 白板平台图标转白底后再着色,避免黑色图标乘色后看不出颜色变化 */ + ensureWhiteboardPlatformColorableImage(entityData) { + if (!entityData || !entityData.entity || !entityData.entity.billboard) return + if (entityData._whiteboardIconPrepared) return + const platform = entityData.platform || {} + const iconUrl = platform.imageUrl || platform.iconUrl + const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl() + this.loadAndWhitenImage(imageSrc).then((whiteImage) => { + if (!entityData || !entityData.entity || !entityData.entity.billboard) return + entityData.entity.billboard.image = whiteImage + if (entityData.color) { + entityData.entity.billboard.color = Cesium.Color.fromCssColorString(entityData.color) + } + entityData._whiteboardIconPrepared = true + if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) { + this.viewer.scene.requestRender() + } + }).catch(() => {}) + }, /** 白板平台图标:添加 billboard,并存入 whiteboardEntityDataMap 以支持拖拽/旋转 */ addWhiteboardPlatformIcon(ed) { const platform = ed.platform || {} @@ -12304,6 +12401,7 @@ export default { const lat = ed.lat const lng = ed.lng const heading = ed.heading != null ? ed.heading : 0 + const color = ed.color || '#008aff' const rotation = Math.PI / 2 - (heading * Math.PI / 180) const size = this.PLATFORM_ICON_BASE_SIZE * (ed.iconScale != null ? ed.iconScale : 1.5) const cartesian = Cesium.Cartesian3.fromDegrees(lng, lat) @@ -12315,6 +12413,7 @@ export default { image: imageSrc, width: size, height: size, + color: Cesium.Color.fromCssColorString(color), verticalOrigin: Cesium.VerticalOrigin.CENTER, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, rotation, @@ -12322,9 +12421,23 @@ export default { translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6) } }) - const entityData = { id, entity, lat, lng, heading, platform, platformName: platform.name, label: ed.label, type: 'platformIcon', isWhiteboard: true, iconScale: ed.iconScale != null ? ed.iconScale : 1.5 } + const entityData = { + id, + entity, + lat, + lng, + heading, + color, + platform, + platformName: platform.name, + label: ed.label, + type: 'platformIcon', + isWhiteboard: true, + iconScale: ed.iconScale != null ? ed.iconScale : 1.5 + } this.whiteboardEntityDataMap = this.whiteboardEntityDataMap || {} this.whiteboardEntityDataMap[id] = entityData + this.ensureWhiteboardPlatformColorableImage(entityData) }, /** 白板空域/图形实体:复用 importEntity 逻辑但不加入 allEntities */ addWhiteboardDrawingEntity(ed) { diff --git a/ruoyi-ui/src/views/childRoom/TopHeader.vue b/ruoyi-ui/src/views/childRoom/TopHeader.vue index e6b518c..86909d5 100644 --- a/ruoyi-ui/src/views/childRoom/TopHeader.vue +++ b/ruoyi-ui/src/views/childRoom/TopHeader.vue @@ -332,7 +332,7 @@
- +
diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index f2b03b0..9f3c93e 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -51,6 +51,7 @@ @platform-style-saved="onPlatformStyleSaved" @whiteboard-draw-complete="handleWhiteboardDrawComplete" @whiteboard-platform-updated="handleWhiteboardPlatformUpdated" + @whiteboard-platform-style-updated="handleWhiteboardPlatformStyleUpdated" @whiteboard-entity-deleted="handleWhiteboardEntityDeleted" @whiteboard-drawing-updated="handleWhiteboardDrawingUpdated" />
@@ -166,6 +167,7 @@ @layer-favorites="layerFavorites" @route-favorites="routeFavorites" @show-online-members="showOnlineMembersDialog" + @edit-user-profile="openUserProfileDialog" /> + + this.getList()); @@ -1936,6 +1964,53 @@ export default { showOnlineMembersDialog() { this.showOnlineMembers = true; }, + openUserProfileDialog() { + this.showUserProfileDialog = true; + }, + async fetchRoomUserProfile() { + try { + const res = await getRoomUserProfile(); + const p = res.data || {}; + this.roomUserProfile = { + userId: p.userId, + userName: p.userName || this.$store.getters.name || '', + displayName: p.displayName || p.userName || this.$store.getters.name || '', + avatar: p.avatar || '' + }; + this.userAvatar = this.resolveAvatarUrl(this.roomUserProfile.avatar) || this.userAvatar; + } catch (e) { + // 保持默认头像,不阻断页面逻辑 + } + }, + handleUserProfileUpdated(profile) { + if (!profile) return; + this.avatarRefreshTs = Date.now(); + const fixedAvatar = profile.avatar || profile.imgUrl || profile.fullImgUrl || this.roomUserProfile.avatar || ''; + const instantPreview = profile.localAvatarPreview || profile.previewAvatarUrl || ''; + this.roomUserProfile = { + ...this.roomUserProfile, + ...profile, + avatar: fixedAvatar + }; + this.userAvatar = instantPreview || this.resolveAvatarUrl(this.roomUserProfile.avatar) || this.userAvatar; + // 更新当前在线成员列表中的本人展示,避免等待重连后才生效 + const myId = this.currentUserId; + if (myId != null && Array.isArray(this.wsOnlineMembers)) { + this.wsOnlineMembers = this.wsOnlineMembers.map(m => { + if (Number(m.userId) !== Number(myId)) return m; + return { + ...m, + name: this.roomUserProfile.displayName || m.name, + avatar: instantPreview || this.resolveAvatarUrl(this.roomUserProfile.avatar) || m.avatar + }; + }); + } + if (fixedAvatar) { + this.$nextTick(() => { + this.fetchRoomUserProfile(); + }); + } + }, onOperationRollbackDone() { // 回滚后同时刷新房间内航线/航点和平台库列表 this.getList(); @@ -1964,14 +2039,22 @@ export default { return m; }).filter(Boolean); }, + resolveAvatarUrl(avatar) { + if (!avatar) return ''; + if (avatar.startsWith('http')) return avatar; + const baseUrl = process.env.VUE_APP_BACKEND_URL || (window.location.origin + (process.env.VUE_APP_BASE_API || '')); + const fullUrl = baseUrl + avatar; + const sep = fullUrl.includes('?') ? '&' : '?'; + return `${fullUrl}${sep}t=${this.avatarRefreshTs}`; + }, sendPrivateChat(targetUserId, targetUserName, content) { if (!this.wsConnection || !this.wsConnection.sendPrivateChat) return; this.wsConnection.sendPrivateChat(targetUserId, targetUserName, content); const sender = { userId: this.currentUserId, userName: this.$store.getters.name, - nickName: this.$store.getters.nickName || this.$store.getters.name, - avatar: this.$store.getters.avatar || '' + nickName: this.roomUserProfile.displayName || this.$store.getters.nickName || this.$store.getters.name, + avatar: this.roomUserProfile.avatar || '' }; const msg = { type: 'PRIVATE_CHAT', sender, targetUserId, targetUserName, content, timestamp: Date.now() }; const list = this.privateChatMessages[targetUserId] || []; @@ -1992,9 +2075,11 @@ export default { role: m.role === 'owner' ? '房主' : (m.role === 'admin' ? '管理员' : '成员'), status: '在线', isEditing: false, - avatar: m.avatar ? (m.avatar.startsWith('http') ? m.avatar : (baseUrl + m.avatar)) : '' + avatar: m.avatar ? (m.avatar.startsWith('http') ? m.avatar : (baseUrl + m.avatar)) : '', + // 来源房间:用于统计“本房间真实在线人数” + roomId: m.roomId })); - this.onlineCount = this.wsOnlineMembers.length; + this.onlineCount = this.wsOnlineMembers.filter(x => x.roomId != null && String(x.roomId) === String(this.currentRoomId)).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); }, @@ -2008,11 +2093,12 @@ export default { role: member.role === 'owner' ? '房主' : (member.role === 'admin' ? '管理员' : '成员'), status: '在线', isEditing: false, - avatar: member.avatar ? (member.avatar.startsWith('http') ? member.avatar : (baseUrl + member.avatar)) : '' + avatar: member.avatar ? (member.avatar.startsWith('http') ? member.avatar : (baseUrl + member.avatar)) : '', + roomId: member.roomId }; if (!this.wsOnlineMembers.find(x => x.id === m.id)) { this.wsOnlineMembers = [...this.wsOnlineMembers, m]; - this.onlineCount = this.wsOnlineMembers.length; + this.onlineCount = this.wsOnlineMembers.filter(x => x.roomId != null && String(x.roomId) === String(this.currentRoomId)).length; } const myId = this.$store.getters.id; if (myId != null && String(member.userId) === String(myId) && member.sessionId) { @@ -2023,7 +2109,7 @@ export default { }, onMemberLeft: (member, sessionId) => { this.wsOnlineMembers = this.wsOnlineMembers.filter(m => m.id !== sessionId && m.id !== member.sessionId); - this.onlineCount = this.wsOnlineMembers.length; + this.onlineCount = this.wsOnlineMembers.filter(x => x.roomId != null && String(x.roomId) === String(this.currentRoomId)).length; this.mySyncSessionIds = this.mySyncSessionIds.filter(s => s !== sessionId && s !== member.sessionId); // 该成员离开后清除其编辑锁定状态 Object.keys(this.routeLockedBy).forEach(rid => { @@ -3930,22 +4016,83 @@ export default { this.saveCurrentWhiteboard({ contentByTime }) }, - /** 白板平台拖拽/旋转后更新 contentByTime 并保存 */ + /** 白板平台拖拽/旋转后更新 contentByTime 并保存(仅位置与朝向) */ handleWhiteboardPlatformUpdated(entityData) { if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !entityData || !entityData.id) return const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } const ents = [...(currentContent.entities || [])] - const idx = ents.findIndex(e => e.id === entityData.id) + const normalizeId = (id) => { + if (id == null) return '' + const str = String(id) + return str.startsWith('wb_') ? str.slice(3) : str + } + const targetId = String(entityData.id) + const targetIdNormalized = normalizeId(targetId) + const idx = ents.findIndex((e) => { + const eid = String((e && e.id) || '') + return eid === targetId || normalizeId(eid) === targetIdNormalized + }) if (idx >= 0) { - const updated = { ...ents[idx], lat: entityData.lat, lng: entityData.lng, heading: entityData.heading != null ? entityData.heading : 0 } - if (entityData.iconScale != null) updated.iconScale = entityData.iconScale + const updated = { + ...ents[idx], + lat: entityData.lat, + lng: entityData.lng, + heading: entityData.heading != null ? entityData.heading : 0 + } + // 样式(颜色/大小)交由专门事件保存,避免位置更新覆盖样式 ents[idx] = updated contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } this.saveCurrentWhiteboard({ contentByTime }) } }, + /** 白板平台样式更新(颜色/大小)单独落库,避免被位置事件覆盖 */ + handleWhiteboardPlatformStyleUpdated(stylePayload) { + if (!this.currentWhiteboard || !stylePayload || !stylePayload.id) return + const normalizeId = (id) => { + if (id == null) return '' + const str = String(id) + return str.startsWith('wb_') ? str.slice(3) : str + } + const targetId = String(stylePayload.id) + const targetIdNormalized = normalizeId(targetId) + let platformInstanceId = targetId + let styleColor = stylePayload.color + let styleScale = stylePayload.iconScale + + // 尝试在当前时间块查找实体,拿到稳定 id 以及最新样式 + const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } + const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } + const ents = [...(currentContent.entities || [])] + const idx = ents.findIndex((e) => { + const eid = String((e && e.id) || '') + return eid === targetId || normalizeId(eid) === targetIdNormalized + }) + if (idx >= 0) { + const updated = { ...ents[idx] } + if (stylePayload.color) updated.color = stylePayload.color + if (stylePayload.iconScale != null) updated.iconScale = stylePayload.iconScale + ents[idx] = updated + contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } + this.saveCurrentWhiteboard({ contentByTime }) + platformInstanceId = updated.id || targetId + styleColor = updated.color + styleScale = updated.iconScale + } + + // 无有效样式不写默认值,避免误覆盖 + if (!styleColor || styleScale == null) return + saveWhiteboardPlatformStyle({ + schemeId: this.currentWhiteboard.id, + platformInstanceId, + style: { + color: styleColor, + iconScale: styleScale + } + }).catch(() => {}) + }, + /** 白板平台从右键菜单删除后,从 contentByTime 移除并保存 */ handleWhiteboardEntityDeleted(entityData) { if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !entityData || !entityData.id) return @@ -3954,6 +4101,10 @@ export default { const ents = (currentContent.entities || []).filter(e => e.id !== entityData.id) contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } this.saveCurrentWhiteboard({ contentByTime }) + deleteWhiteboardPlatformStyle({ + schemeId: this.currentWhiteboard.id, + platformInstanceId: entityData.id + }).catch(() => {}) }, /** 白板空域/图形编辑后(调整位置、修改属性等)更新 contentByTime 并保存 */ diff --git a/ruoyi-ui/src/views/dialogs/UserProfileDialog.vue b/ruoyi-ui/src/views/dialogs/UserProfileDialog.vue new file mode 100644 index 0000000..ed38ca8 --- /dev/null +++ b/ruoyi-ui/src/views/dialogs/UserProfileDialog.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/sql/room_user_profile.sql b/sql/room_user_profile.sql new file mode 100644 index 0000000..418e836 --- /dev/null +++ b/sql/room_user_profile.sql @@ -0,0 +1,11 @@ +-- 协同房间用户资料表(显示名 + 头像) +CREATE TABLE IF NOT EXISTS room_user_profile ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + user_id BIGINT NOT NULL COMMENT 'sys_user.user_id', + display_name VARCHAR(64) NOT NULL COMMENT '协同显示名', + avatar VARCHAR(255) DEFAULT NULL COMMENT '协同头像URL/资源路径', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (id), + UNIQUE KEY uk_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='协同房间用户资料';