From 40b9cbc1c86b1d7d721d93529eb5acdee73e5b98 Mon Sep 17 00:00:00 2001 From: cuitw <1051735452@qq.com> Date: Mon, 2 Mar 2026 16:38:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoomWebSocketController.java | 101 ++++- .../ruoyi/websocket/service/RoomChatService.java | 109 +++++ ruoyi-ui/src/assets/styles/ruoyi.scss | 6 + ruoyi-ui/src/lang/en.js | 13 +- ruoyi-ui/src/lang/zh.js | 13 +- ruoyi-ui/src/utils/websocket.js | 77 +++- ruoyi-ui/src/views/childRoom/index.vue | 115 ++++- ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue | 209 +++++++++ ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue | 483 +++++++++++++++------ ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue | 174 ++++++-- ruoyi-ui/src/views/dialogs/RouteEditDialog.vue | 243 +++++++++-- 11 files changed, 1308 insertions(+), 235 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/websocket/service/RoomChatService.java create mode 100644 ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue 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 e9b66ba..2d75553 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 @@ -18,6 +18,7 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.Rooms; import com.ruoyi.system.service.IRoomsService; import com.ruoyi.websocket.dto.RoomMemberDTO; +import com.ruoyi.websocket.service.RoomChatService; import com.ruoyi.websocket.service.RoomWebSocketService; /** @@ -35,18 +36,26 @@ public class RoomWebSocketController { private RoomWebSocketService roomWebSocketService; @Autowired + private RoomChatService roomChatService; + + @Autowired private IRoomsService roomsService; private static final String TYPE_JOIN = "JOIN"; private static final String TYPE_LEAVE = "LEAVE"; private static final String TYPE_PING = "PING"; + private static final String TYPE_CHAT = "CHAT"; + private static final String TYPE_PRIVATE_CHAT = "PRIVATE_CHAT"; + 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"; /** - * 处理房间消息:JOIN、LEAVE、PING + * 处理房间消息:JOIN、LEAVE、PING、CHAT、PRIVATE_CHAT */ @MessageMapping("/room/{roomId}") public void handleRoomMessage(@DestinationVariable Long roomId, @Payload String payload, @@ -70,6 +79,12 @@ public class RoomWebSocketController { handleLeave(roomId, sessionId, loginUser); } else if (TYPE_PING.equals(type)) { handlePing(roomId, sessionId, loginUser); + } else if (TYPE_CHAT.equals(type)) { + handleChat(roomId, sessionId, loginUser, body); + } else if (TYPE_PRIVATE_CHAT.equals(type)) { + handlePrivateChat(roomId, sessionId, loginUser, body); + } else if (TYPE_PRIVATE_CHAT_HISTORY_REQUEST.equals(type)) { + handlePrivateChatHistoryRequest(roomId, loginUser, body); } } @@ -100,6 +115,12 @@ public class RoomWebSocketController { String topic = "/topic/room/" + roomId; messagingTemplate.convertAndSend(topic, memberListMsg); + + List chatHistory = roomChatService.getGroupChatHistory(roomId); + Map chatHistoryMsg = new HashMap<>(); + chatHistoryMsg.put("type", TYPE_CHAT_HISTORY); + chatHistoryMsg.put("messages", chatHistory); + messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", chatHistoryMsg); } private void handleLeave(Long roomId, String sessionId, LoginUser loginUser) { @@ -123,6 +144,84 @@ public class RoomWebSocketController { messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", msg); } + /** 群聊:广播给房间内所有人 */ + private void handleChat(Long roomId, String sessionId, LoginUser loginUser, Map body) { + String content = body != null && body.containsKey("content") ? String.valueOf(body.get("content")) : ""; + if (content.isEmpty()) return; + + 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("sessionId", sessionId); + + Map msg = new HashMap<>(); + msg.put("type", TYPE_CHAT); + msg.put("sender", sender); + msg.put("content", content); + msg.put("timestamp", System.currentTimeMillis()); + + roomChatService.saveGroupChat(roomId, loginUser.getUserId(), msg); + messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); + } + + /** 私聊:发送给指定用户(仅限同房间成员) */ + private void handlePrivateChat(Long roomId, String sessionId, LoginUser loginUser, Map body) { + Object targetUserNameObj = body != null ? body.get("targetUserName") : null; + Object targetUserIdObj = body != null ? body.get("targetUserId") : null; + String content = body != null && body.containsKey("content") ? String.valueOf(body.get("content")) : ""; + if (targetUserNameObj == null || content.isEmpty()) return; + + String targetUserName = String.valueOf(targetUserNameObj); + if (targetUserName.equals(loginUser.getUsername())) return; + + List members = roomWebSocketService.getRoomMembers(roomId); + boolean targetInRoom = members.stream().anyMatch(m -> targetUserName.equals(m.getUserName())); + if (!targetInRoom) return; + + 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("sessionId", sessionId); + + Map msg = new HashMap<>(); + msg.put("type", TYPE_PRIVATE_CHAT); + msg.put("sender", sender); + msg.put("targetUserId", targetUserIdObj); + msg.put("targetUserName", targetUserName); + msg.put("content", content); + msg.put("timestamp", System.currentTimeMillis()); + + Long targetUserId = targetUserIdObj instanceof Number ? ((Number) targetUserIdObj).longValue() : null; + if (targetUserId != null) { + roomChatService.savePrivateChat(loginUser.getUserId(), targetUserId, msg); + } + messagingTemplate.convertAndSendToUser(targetUserName, "/queue/private", msg); + } + + /** 私聊历史请求:返回与指定用户的聊天记录 */ + private void handlePrivateChatHistoryRequest(Long roomId, LoginUser loginUser, Map body) { + Object targetUserIdObj = body != null ? body.get("targetUserId") : null; + if (targetUserIdObj == null) return; + + Long targetUserId = targetUserIdObj instanceof Number ? ((Number) targetUserIdObj).longValue() : null; + if (targetUserId == null) return; + + List members = roomWebSocketService.getRoomMembers(roomId); + boolean targetInRoom = members.stream().anyMatch(m -> targetUserId.equals(m.getUserId())); + if (!targetInRoom) return; + + List history = roomChatService.getPrivateChatHistory(loginUser.getUserId(), targetUserId); + Map resp = new HashMap<>(); + resp.put("type", TYPE_PRIVATE_CHAT_HISTORY); + resp.put("targetUserId", targetUserId); + resp.put("messages", history); + messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", resp); + } + private RoomMemberDTO buildMember(LoginUser loginUser, String sessionId, Long roomId, Map body) { RoomMemberDTO dto = new RoomMemberDTO(); dto.setUserId(loginUser.getUserId()); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/RoomChatService.java b/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/RoomChatService.java new file mode 100644 index 0000000..b43e2a8 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/websocket/service/RoomChatService.java @@ -0,0 +1,109 @@ +package com.ruoyi.websocket.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import com.alibaba.fastjson2.JSON; + +/** + * 房间聊天 Redis 持久化服务 + * 使用 Sorted Set 存储,score 为时间戳,超过 30 天自动清理 + * + * @author ruoyi + */ +@Service +public class RoomChatService { + + private static final String ROOM_CHAT_PREFIX = "room:"; + private static final String ROOM_CHAT_USER_SUFFIX = ":user:"; + private static final String ROOM_CHAT_SUFFIX = ":chat"; + private static final String ROOM_CHAT_USERS_SUFFIX = ":chat:users"; + private static final String PRIVATE_CHAT_PREFIX = "chat:private:"; + private static final long EXPIRE_DAYS = 30; + private static final long EXPIRE_MS = EXPIRE_DAYS * 24 * 60 * 60 * 1000L; + + @Autowired + @Qualifier("stringObjectRedisTemplate") + private RedisTemplate redisTemplate; + + private String roomChatKey(Long roomId, Long userId) { + return ROOM_CHAT_PREFIX + roomId + ROOM_CHAT_USER_SUFFIX + userId + ROOM_CHAT_SUFFIX; + } + + private String roomChatUsersKey(Long roomId) { + return ROOM_CHAT_PREFIX + roomId + ROOM_CHAT_USERS_SUFFIX; + } + + private String privateChatKey(Long userId1, Long userId2) { + long a = userId1 != null ? userId1 : 0; + long b = userId2 != null ? userId2 : 0; + return PRIVATE_CHAT_PREFIX + Math.min(a, b) + ":" + Math.max(a, b); + } + + /** + * 保存群聊消息(按 roomId + userId 分 key 存储) + */ + public void saveGroupChat(Long roomId, Long senderUserId, Object msg) { + if (senderUserId == null) return; + String key = roomChatKey(roomId, senderUserId); + long now = System.currentTimeMillis(); + String member = msg instanceof String ? (String) msg : JSON.toJSONString(msg); + redisTemplate.opsForZSet().add(key, member, now); + redisTemplate.opsForZSet().removeRangeByScore(key, Double.NEGATIVE_INFINITY, now - EXPIRE_MS); + redisTemplate.opsForSet().add(roomChatUsersKey(roomId), String.valueOf(senderUserId)); + } + + /** + * 获取群聊历史(最近 30 天内,按时间升序,合并所有用户) + */ + public List getGroupChatHistory(Long roomId) { + String usersKey = roomChatUsersKey(roomId); + Set userIds = redisTemplate.opsForSet().members(usersKey); + if (userIds == null || userIds.isEmpty()) return new ArrayList<>(); + + long min = System.currentTimeMillis() - EXPIRE_MS; + List merged = new ArrayList<>(); + for (Object uid : userIds) { + String key = roomChatKey(roomId, Long.valueOf(String.valueOf(uid))); + Set> tuples = + redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, Double.POSITIVE_INFINITY); + if (tuples != null) { + for (ZSetOperations.TypedTuple t : tuples) { + merged.add(new Object[] { t.getValue(), t.getScore() != null ? t.getScore() : 0.0 }); + } + } + } + merged.sort((a, b) -> Double.compare((Double) a[1], (Double) b[1])); + List result = new ArrayList<>(); + for (Object[] arr : merged) { + result.add(arr[0]); + } + return result; + } + + /** + * 保存私聊消息 + */ + public void savePrivateChat(Long userId1, Long userId2, Object msg) { + String key = privateChatKey(userId1, userId2); + long now = System.currentTimeMillis(); + String member = msg instanceof String ? (String) msg : JSON.toJSONString(msg); + redisTemplate.opsForZSet().add(key, member, now); + redisTemplate.opsForZSet().removeRangeByScore(key, Double.NEGATIVE_INFINITY, now - EXPIRE_MS); + } + + /** + * 获取私聊历史(最近 30 天内,按时间升序) + */ + public List getPrivateChatHistory(Long userId1, Long userId2) { + String key = privateChatKey(userId1, userId2); + long min = System.currentTimeMillis() - EXPIRE_MS; + Set set = redisTemplate.opsForZSet().rangeByScore(key, min, Double.POSITIVE_INFINITY); + return set != null ? new ArrayList<>(set) : new ArrayList<>(); + } +} diff --git a/ruoyi-ui/src/assets/styles/ruoyi.scss b/ruoyi-ui/src/assets/styles/ruoyi.scss index e1ba082..1f20e8d 100644 --- a/ruoyi-ui/src/assets/styles/ruoyi.scss +++ b/ruoyi-ui/src/assets/styles/ruoyi.scss @@ -81,6 +81,12 @@ margin-top: 6vh !important; } +/* 全局弹窗:取消背景变暗,点击遮罩不影响地图操作(遮罩透明且不拦截鼠标事件) */ +.v-modal { + background-color: transparent !important; + pointer-events: none !important; +} + .el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body { overflow: auto; overflow-x: hidden; diff --git a/ruoyi-ui/src/lang/en.js b/ruoyi-ui/src/lang/en.js index 3d55431..4ad098c 100644 --- a/ruoyi-ui/src/lang/en.js +++ b/ruoyi-ui/src/lang/en.js @@ -51,12 +51,7 @@ export default { powerZone: 'Power Zone', threatZone: 'Threat Zone' }, - tools: { - routeCalculation: 'Route Calculation', - conflictDisplay: 'Conflict Display', - dataMaterials: 'Data Materials', - coordinateConversion: 'Coordinate Conversion' - }, + options: { settings: 'Settings', pageLayout: 'Page Layout', @@ -150,7 +145,11 @@ export default { rollbackConfirmText: 'Are you sure you want to rollback to the selected operation?', rollbackWarning: 'This operation will undo this operation and all subsequent changes, cannot be recovered!', confirmRollback: 'Confirm Rollback', - groupChat: 'Group Chat - Online Members Communication', + groupChat: 'Group Chat', + privateChat: 'Private Chat', + selectMember: 'Select', + selectMemberToChat: 'Select a member to start private chat', + selectMemberFirst: 'Please select a member first', onlineCount: ' people online', inputMessage: 'Please enter message...', send: 'Send', diff --git a/ruoyi-ui/src/lang/zh.js b/ruoyi-ui/src/lang/zh.js index 11c1864..0d91067 100644 --- a/ruoyi-ui/src/lang/zh.js +++ b/ruoyi-ui/src/lang/zh.js @@ -51,12 +51,7 @@ export default { powerZone: '威力区', threatZone: '威胁区' }, - tools: { - routeCalculation: '航线计算', - conflictDisplay: '冲突显示', - dataMaterials: '数据资料', - coordinateConversion: '坐标换算' - }, + options: { settings: '设置', pageLayout: '页面布局', @@ -150,7 +145,11 @@ export default { rollbackConfirmText: '确定要回滚到所选操作吗?', rollbackWarning: '此操作将撤销该操作及其后的所有更改,不可恢复!', confirmRollback: '确定回滚', - groupChat: '群聊 - 在线成员交流', + groupChat: '群聊', + privateChat: '私聊', + selectMember: '选择对象', + selectMemberToChat: '选择成员开始私聊', + selectMemberFirst: '请先选择要私聊的成员', onlineCount: '人在线', inputMessage: '请输入消息...', send: '发送', diff --git a/ruoyi-ui/src/utils/websocket.js b/ruoyi-ui/src/utils/websocket.js index c8d43c8..bc097c0 100644 --- a/ruoyi-ui/src/utils/websocket.js +++ b/ruoyi-ui/src/utils/websocket.js @@ -14,6 +14,10 @@ const WS_BASE = process.env.VUE_APP_BASE_API || '/dev-api' * @param {Function} options.onMembers - 收到成员列表回调 (members) => {} * @param {Function} options.onMemberJoined - 成员加入回调 (member) => {} * @param {Function} options.onMemberLeft - 成员离开回调 (member, sessionId) => {} + * @param {Function} options.onChatMessage - 群聊消息回调 (msg) => {} + * @param {Function} options.onPrivateChat - 私聊消息回调 (msg) => {} + * @param {Function} options.onChatHistory - 群聊历史回调 (messages) => {} + * @param {Function} options.onPrivateChatHistory - 私聊历史回调 (targetUserId, messages) => {} * @param {Function} options.onConnected - 连接成功回调 * @param {Function} options.onDisconnected - 断开回调 * @param {Function} options.onError - 错误回调 @@ -25,6 +29,10 @@ export function createRoomWebSocket(options) { onMembers, onMemberJoined, onMemberLeft, + onChatMessage, + onPrivateChat, + onChatHistory, + onPrivateChatHistory, onConnected, onDisconnected, onError, @@ -32,7 +40,8 @@ export function createRoomWebSocket(options) { } = options let client = null - let subscription = null + let roomSubscription = null + let privateSubscription = null let heartbeatTimer = null let reconnectAttempts = 0 const maxReconnectAttempts = 10 @@ -72,6 +81,33 @@ export function createRoomWebSocket(options) { } } + function sendChat(content) { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'CHAT', content }) + }) + } + } + + function sendPrivateChat(targetUserId, targetUserName, content) { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'PRIVATE_CHAT', targetUserId, targetUserName, content }) + }) + } + } + + function sendPrivateChatHistoryRequest(targetUserId) { + if (client && client.connected) { + client.publish({ + destination: '/app/room/' + roomId, + body: JSON.stringify({ type: 'PRIVATE_CHAT_HISTORY_REQUEST', targetUserId }) + }) + } + } + function startHeartbeat() { stopHeartbeat() heartbeatTimer = setInterval(sendPing, 30000) @@ -84,7 +120,7 @@ export function createRoomWebSocket(options) { } } - function handleMessage(message) { + function handleRoomMessage(message) { try { const body = JSON.parse(message.body) const type = body.type @@ -94,12 +130,30 @@ export function createRoomWebSocket(options) { onMemberJoined && onMemberJoined(body.member) } else if (type === 'MEMBER_LEFT' && body.member) { onMemberLeft && onMemberLeft(body.member, body.sessionId) + } else if (type === 'CHAT' && body.sender) { + onChatMessage && onChatMessage(body) } } catch (e) { console.warn('[WebSocket] parse message error:', e) } } + function handlePrivateMessage(message) { + try { + const body = JSON.parse(message.body) + const type = body.type + if (type === 'PRIVATE_CHAT' && body.sender) { + onPrivateChat && onPrivateChat(body) + } else if (type === 'CHAT_HISTORY' && Array.isArray(body.messages)) { + onChatHistory && onChatHistory(body.messages) + } else if (type === 'PRIVATE_CHAT_HISTORY' && body.targetUserId != null && Array.isArray(body.messages)) { + onPrivateChatHistory && onPrivateChatHistory(body.targetUserId, body.messages) + } + } catch (e) { + console.warn('[WebSocket] parse private message error:', e) + } + } + function connect() { const token = getToken() if (!token) { @@ -115,7 +169,8 @@ export function createRoomWebSocket(options) { heartbeatOutgoing: 0, onConnect: () => { reconnectAttempts = 0 - subscription = client.subscribe('/topic/room/' + roomId, handleMessage) + roomSubscription = client.subscribe('/topic/room/' + roomId, handleRoomMessage) + privateSubscription = client.subscribe('/user/queue/private', handlePrivateMessage) sendJoin() startHeartbeat() onConnected && onConnected() @@ -126,7 +181,8 @@ export function createRoomWebSocket(options) { }, onWebSocketClose: () => { stopHeartbeat() - subscription = null + roomSubscription = null + privateSubscription = null onDisconnected && onDisconnected() } }) @@ -136,9 +192,13 @@ export function createRoomWebSocket(options) { function disconnect() { stopHeartbeat() sendLeave() - if (subscription) { - subscription.unsubscribe() - subscription = null + if (roomSubscription) { + roomSubscription.unsubscribe() + roomSubscription = null + } + if (privateSubscription) { + privateSubscription.unsubscribe() + privateSubscription = null } if (client) { client.deactivate() @@ -160,6 +220,9 @@ export function createRoomWebSocket(options) { return { disconnect, reconnect: connect, + sendChat, + sendPrivateChat, + sendPrivateChatHistoryRequest, get connected() { return client && client.connected } diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 19b8681..6d4dbc5 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -75,25 +75,13 @@ - - - - - - -

航线的任务时间将以此 K 时为基准进行加减;航点表时间为相对 K 的分钟数。房主/管理员可随时再次点击「作战时间」修改 K 时。

-
- -
+ + - + - + - + @@ -431,6 +428,7 @@ import WaypointEditDialog from '@/views/dialogs/WaypointEditDialog' import ScaleDialog from '@/views/dialogs/ScaleDialog' import ExternalParamsDialog from '@/views/dialogs/ExternalParamsDialog' import PageLayoutDialog from '@/views/dialogs/PageLayoutDialog' +import KTimeSetDialog from '@/views/dialogs/KTimeSetDialog' import LeftMenu from './LeftMenu' import RightPanel from './RightPanel' import BottomLeftPanel from './BottomLeftPanel' @@ -457,6 +455,7 @@ export default { ScaleDialog, ExternalParamsDialog, PageLayoutDialog, + KTimeSetDialog, LeftMenu, RightPanel, BottomLeftPanel, @@ -516,6 +515,8 @@ export default { onlineCount: 0, wsOnlineMembers: [], wsConnection: null, + chatMessages: [], + privateChatMessages: {}, combatTime: 'K+00:00:00', // 进入房间时固定作战时间,不随真实时间走 astroTime: '', roomDetail: null, @@ -669,6 +670,10 @@ export default { } }, computed: { + currentUserId() { + const id = this.$store.getters.id; + return id != null ? Number(id) : null; + }, isRoomOwner() { if (!this.roomDetail || this.roomDetail.ownerId == null) return false; const myId = this.$store.getters.id; @@ -1199,6 +1204,42 @@ export default { showOnlineMembersDialog() { this.showOnlineMembers = true; }, + sendChat(content) { + if (this.wsConnection && this.wsConnection.sendChat) { + this.wsConnection.sendChat(content); + } + }, + sendPrivateChatHistoryRequest(targetUserId) { + if (this.wsConnection && this.wsConnection.sendPrivateChatHistoryRequest) { + this.wsConnection.sendPrivateChatHistoryRequest(targetUserId); + } + }, + normalizeChatMessages(messages) { + if (!messages || !Array.isArray(messages)) return []; + return messages.map(m => { + if (typeof m === 'string') { + try { + return JSON.parse(m); + } catch (e) { + return null; + } + } + return m; + }).filter(Boolean); + }, + 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 || '' + }; + const msg = { type: 'PRIVATE_CHAT', sender, targetUserId, targetUserName, content, timestamp: Date.now() }; + const list = this.privateChatMessages[targetUserId] || []; + this.$set(this.privateChatMessages, targetUserId, [...list, msg]); + }, connectRoomWebSocket() { if (!this.currentRoomId) return; this.disconnectRoomWebSocket(); @@ -1208,6 +1249,8 @@ export default { onMembers: (members) => { this.wsOnlineMembers = members.map(m => ({ id: m.sessionId || m.userId, + userId: m.userId, + userName: m.userName, name: m.nickName || m.userName, role: m.role === 'owner' ? '房主' : (m.role === 'admin' ? '管理员' : '成员'), status: '在线', @@ -1220,6 +1263,8 @@ export default { const baseUrl = (process.env.VUE_APP_BACKEND_URL || (window.location.origin + (process.env.VUE_APP_BASE_API || ''))); const m = { id: member.sessionId || member.userId, + userId: member.userId, + userName: member.userName, name: member.nickName || member.userName, role: member.role === 'owner' ? '房主' : (member.role === 'admin' ? '管理员' : '成员'), status: '在线', @@ -1235,10 +1280,34 @@ export default { this.wsOnlineMembers = this.wsOnlineMembers.filter(m => m.id !== sessionId && m.id !== member.sessionId); this.onlineCount = this.wsOnlineMembers.length; }, + onChatMessage: (msg) => { + this.chatMessages = [...this.chatMessages, msg]; + }, + onPrivateChat: (msg) => { + const senderId = msg.sender && msg.sender.userId; + if (!senderId) return; + const list = this.privateChatMessages[senderId] || []; + this.$set(this.privateChatMessages, senderId, [...list, msg]); + }, + onChatHistory: (messages) => { + const history = this.normalizeChatMessages(messages); + const maxTs = history.length ? Math.max(...history.map(m => m.timestamp || 0)) : 0; + const newer = this.chatMessages.filter(m => (m.timestamp || 0) > maxTs); + this.chatMessages = [...history, ...newer]; + }, + onPrivateChatHistory: (targetUserId, messages) => { + const history = this.normalizeChatMessages(messages); + const existing = this.privateChatMessages[targetUserId] || []; + const maxTs = history.length ? Math.max(...history.map(m => m.timestamp || 0)) : 0; + const newer = existing.filter(m => (m.timestamp || 0) > maxTs); + this.$set(this.privateChatMessages, targetUserId, [...history, ...newer]); + }, onConnected: () => {}, onDisconnected: () => { this.onlineCount = 0; this.wsOnlineMembers = []; + this.chatMessages = []; + this.privateChatMessages = {}; }, onError: (err) => { console.warn('[WebSocket]', err); @@ -1252,6 +1321,8 @@ export default { } this.wsOnlineMembers = []; this.onlineCount = 0; + this.chatMessages = []; + this.privateChatMessages = {}; }, // 平台编辑弹窗相关方法 openPlatformDialog(platform) { diff --git a/ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue b/ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue new file mode 100644 index 0000000..be7dc3a --- /dev/null +++ b/ruoyi-ui/src/views/dialogs/KTimeSetDialog.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue index e2426b0..879ed38 100644 --- a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue +++ b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue @@ -1,13 +1,10 @@ diff --git a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue index 984d576..2d2fc18 100644 --- a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue @@ -1,12 +1,12 @@