From 1222bf551d6b5c4bc5569786e36bff21f7aadba1 Mon Sep 17 00:00:00 2001 From: bujinyuan Date: Wed, 22 Apr 2026 15:16:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(phase2e-2):=20=E4=BC=9A=E8=AE=AE=E5=86=85?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E9=9D=A2=E6=9D=BF=E8=90=BD=E5=9C=B0=EF=BC=88?= =?UTF-8?q?Task=2012=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ChatPanel 组件:右侧抽屉式聊天 UI,支持发送/加载更早消息/未读清零 - store/meeting.js:扩展 chatHasMore/chatUnreadCount/chatHistoryLoaded 等状态与 loadMoreChats/markChatsRead 动作;WS 消息到达时按 id 去重 - room.vue:替换临时 toast 占位为 ChatPanel,绑定未读徽标、成员展示名映射;watch unread 自动在面板打开时清零 - 后端:SendChat WS 广播与 REST 响应补充 user_name/user_avatar,聊天面板无需额外查询即可展示昵称 Made-with: Cursor --- .../meeting/controller/meeting_controller.go | 20 +- .../app/meeting/service/meeting_service.go | 72 +++- frontend/src/components/meeting/ChatPanel.vue | 399 ++++++++++++++++++ frontend/src/pages/meeting/room.vue | 58 ++- frontend/src/store/meeting.js | 73 +++- 5 files changed, 605 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/meeting/ChatPanel.vue diff --git a/backend/go-service/app/meeting/controller/meeting_controller.go b/backend/go-service/app/meeting/controller/meeting_controller.go index 7bd8dee..7abf18d 100644 --- a/backend/go-service/app/meeting/controller/meeting_controller.go +++ b/backend/go-service/app/meeting/controller/meeting_controller.go @@ -127,17 +127,23 @@ func participantToDTO(p *model.MeetingParticipant) *dto.MeetingParticipantDTO { } // chatToDTO 将 model.MeetingChat 转 DTO -func chatToDTO(m *model.MeetingChat) *dto.MeetingChatDTO { +// userMap 可选:若提供则附带 user_name / user_avatar,否则为空 +func chatToDTO(m *model.MeetingChat, userMap map[int64]service.UserDisplayInfo) *dto.MeetingChatDTO { if m == nil { return nil } - return &dto.MeetingChatDTO{ + d := &dto.MeetingChatDTO{ ID: m.ID, RoomID: m.RoomID, UserID: m.UserID, Content: m.Content, CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"), } + if info, ok := userMap[m.UserID]; ok { + d.UserName = info.Name + d.UserAvatar = info.Avatar + } + return d } // ====== REST API ====== @@ -398,7 +404,8 @@ func (ctl *MeetingController) SendChat(c *gin.Context) { ctl.handleError(c, err, "发送会议聊天失败") return } - utils.ResponseCreated(c, dto.SendMeetingChatResponse{Message: *chatToDTO(msg)}) + userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), []int64{msg.UserID}) + utils.ResponseCreated(c, dto.SendMeetingChatResponse{Message: *chatToDTO(msg, userMap)}) } // ListChats GET /api/v1/meeting/rooms/:code/chats?before_id=&limit= @@ -419,9 +426,14 @@ func (ctl *MeetingController) ListChats(c *gin.Context) { ctl.handleError(c, err, "获取会议聊天失败") return } + userIDs := make([]int64, 0, len(msgs)) + for i := range msgs { + userIDs = append(userIDs, msgs[i].UserID) + } + userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), userIDs) list := make([]dto.MeetingChatDTO, 0, len(msgs)) for i := range msgs { - list = append(list, *chatToDTO(&msgs[i])) + list = append(list, *chatToDTO(&msgs[i], userMap)) } utils.ResponseOK(c, dto.ListMeetingChatsResponse{List: list, HasMore: hasMore}) } diff --git a/backend/go-service/app/meeting/service/meeting_service.go b/backend/go-service/app/meeting/service/meeting_service.go index 9b8bd8f..d6b041f 100644 --- a/backend/go-service/app/meeting/service/meeting_service.go +++ b/backend/go-service/app/meeting/service/meeting_service.go @@ -150,6 +150,63 @@ func (s *MeetingService) broadcastToActiveParticipants(ctx context.Context, room s.broadcaster.BroadcastToMeeting(ctx, roomID, event, data, excludeUserIDs...) } +// UserDisplayInfo 用户展示信息(昵称优先,降级为用户名;头像可空) +type UserDisplayInfo struct { + Name string + Avatar string +} + +// resolveUserDisplay 查单个用户的展示信息;查询失败/未命中时返回空字符串 +// 仅服务层内部使用,调用方失败时降级为 user_name="" 即可 +func (s *MeetingService) resolveUserDisplay(ctx context.Context, userID int64) (name string, avatar string) { + if s.userResolver == nil || userID <= 0 { + return "", "" + } + users, err := s.userResolver.GetUsersByIDs(ctx, []int64{userID}) + if err != nil || len(users) == 0 { + return "", "" + } + u := users[0] + if u.Nickname != "" { + return u.Nickname, u.Avatar + } + return u.Username, u.Avatar +} + +// ResolveUsersDisplay 批量查询用户展示信息,controller 端用于填充 DTO +// 返回 map[userID]UserDisplayInfo,查询失败时返回空 map(调用方按降级处理) +func (s *MeetingService) ResolveUsersDisplay(ctx context.Context, userIDs []int64) map[int64]UserDisplayInfo { + out := make(map[int64]UserDisplayInfo, len(userIDs)) + if s.userResolver == nil || len(userIDs) == 0 { + return out + } + // 去重 + seen := make(map[int64]struct{}, len(userIDs)) + unique := make([]int64, 0, len(userIDs)) + for _, id := range userIDs { + if id <= 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + unique = append(unique, id) + } + users, err := s.userResolver.GetUsersByIDs(ctx, unique) + if err != nil { + return out + } + for _, u := range users { + name := u.Nickname + if name == "" { + name = u.Username + } + out[u.ID] = UserDisplayInfo{Name: name, Avatar: u.Avatar} + } + return out +} + // ====== 会议生命周期 ====== // CreateRoom 创建即时会议 @@ -773,12 +830,17 @@ func (s *MeetingService) SendChatMessage(ctx context.Context, userID int64, code return nil, err } + // 附带 user_name / user_avatar,前端聊天面板无需额外拉取 + userName, userAvatar := s.resolveUserDisplay(ctx, userID) + go s.broadcastToActiveParticipants(context.Background(), room.ID, constants.MeetingWSEventChatMessage, map[string]interface{}{ - "room_code": code, - "message_id": chat.ID, - "user_id": userID, - "content": content, - "created_at": chat.CreatedAt.Format("2006-01-02 15:04:05"), + "room_code": code, + "message_id": chat.ID, + "user_id": userID, + "user_name": userName, + "user_avatar": userAvatar, + "content": content, + "created_at": chat.CreatedAt.Format("2006-01-02 15:04:05"), }, userID) logs.Debug(ctx, funcName, "会议聊天已发送", diff --git a/frontend/src/components/meeting/ChatPanel.vue b/frontend/src/components/meeting/ChatPanel.vue new file mode 100644 index 0000000..7d10a6f --- /dev/null +++ b/frontend/src/components/meeting/ChatPanel.vue @@ -0,0 +1,399 @@ + +