fix(im): 修复群备注首屏展示和聊天列表名称覆盖

- 后端群 VO 返回当前用户维度的 groupRemark 和 silent
- 群列表构建时通过成员关系回填个人群设置,并继续仅对有效成员回填置顶消息
- Vue3 群列表同步时以接口返回的个人群设置为准,只保留成员缓存
- 会话名写入入口统一使用 getGroupDisplayName,避免群备注被原群名覆盖
- 空群头像且成员未加载时异步预拉群成员,用于合成群头像
- 启用 IM Maven 模块和 yudao-server 对 IM 模块的依赖
im
YunaiV 2026-06-18 08:59:04 -07:00
parent ab2fa4e6b8
commit 61c9e1acf2
9 changed files with 42 additions and 19 deletions

View File

@ -17,6 +17,8 @@ export interface ImGroupRespVO {
createTime?: string // 创建时间 createTime?: string // 创建时间
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空) pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
joinStatus?: number // 当前登录用户在该群的成员状态(参见 CommonStatusEnum0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像 joinStatus?: number // 当前登录用户在该群的成员状态(参见 CommonStatusEnum0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像
groupRemark?: string // 当前登录用户对该群的备注
silent?: boolean // 当前登录用户是否免打扰
} }
// 群消息置顶 / 取消置顶 Request VO // 群消息置顶 / 取消置顶 Request VO

View File

@ -119,7 +119,7 @@ import { ImConversationType, ImContentType, isGroupConversation } from '../../..
import { getConversationKey } from '../../../utils/conversation' import { getConversationKey } from '../../../utils/conversation'
import { buildDefaultGroupName } from '../../../utils/group' import { buildDefaultGroupName } from '../../../utils/group'
import { serializeMessage, type CardTarget } from '../../../utils/message' import { serializeMessage, type CardTarget } from '../../../utils/message'
import { isGroupQuit } from '../../../utils/user' import { getGroupDisplayName, isGroupQuit } from '../../../utils/user'
import type { Conversation, FriendLite } from '../../types' import type { Conversation, FriendLite } from '../../types'
defineOptions({ name: 'ImRecommendCardDialog' }) defineOptions({ name: 'ImRecommendCardDialog' })
@ -283,7 +283,7 @@ async function handleCreateGroupAndSend() {
const newConversation: Conversation = { const newConversation: Conversation = {
type: ImConversationType.GROUP, type: ImConversationType.GROUP,
targetId: group.id, targetId: group.id,
name: group.name || name, name: getGroupDisplayName(group) || name,
avatar: group.avatar || '', avatar: group.avatar || '',
unreadCount: 0, unreadCount: 0,
lastContent: '', lastContent: '',

View File

@ -3,7 +3,7 @@ import { useConversationStore } from '../store/conversationStore'
import { useMessageStore, type PulledMessage } from '../store/messageStore' import { useMessageStore, type PulledMessage } from '../store/messageStore'
import { useImWebSocketStore } from '../store/websocketStore' import { useImWebSocketStore } from '../store/websocketStore'
import { useFriendStore } from '../store/friendStore' import { useFriendStore } from '../store/friendStore'
import { getFriendDisplayName } from '../../utils/user' import { getFriendDisplayName, getGroupDisplayName } from '../../utils/user'
import { useGroupStore } from '../store/groupStore' import { useGroupStore } from '../store/groupStore'
import { useGroupRequestStore } from '../store/groupRequestStore' import { useGroupRequestStore } from '../store/groupRequestStore'
import { import {
@ -149,7 +149,7 @@ export const useMessagePuller = () => {
return { return {
type: ImConversationType.GROUP, type: ImConversationType.GROUP,
targetId: message.groupId, targetId: message.groupId,
name: group?.name || String(message.groupId), name: group ? getGroupDisplayName(group) : String(message.groupId),
avatar: group?.avatar || '', avatar: group?.avatar || '',
silent: group?.silent silent: group?.silent
} }

View File

@ -85,9 +85,9 @@
/> />
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<el-button size="small" @click="displayNamePopoverVisible = false">取消</el-button> <el-button size="small" @click="displayNamePopoverVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="handleSaveDisplayName" <el-button size="small" type="primary" @click="handleSaveDisplayName">
>保存</el-button 保存
> </el-button>
</div> </div>
</div> </div>
</el-popover> </el-popover>
@ -141,7 +141,7 @@ import { useMessage } from '@/hooks/web/useMessage'
import { useConversationStore } from '@/views/im/home/store/conversationStore' import { useConversationStore } from '@/views/im/home/store/conversationStore'
import { useFriendStore } from '@/views/im/home/store/friendStore' import { useFriendStore } from '@/views/im/home/store/friendStore'
import { useGroupStore } from '@/views/im/home/store/groupStore' import { useGroupStore } from '@/views/im/home/store/groupStore'
import { getFriendDisplayName } from '@/views/im/utils/user' import { getFriendDisplayName, getGroupDisplayName } from '@/views/im/utils/user'
import { ImConversationType } from '@/views/im/utils/constants' import { ImConversationType } from '@/views/im/utils/constants'
import type { Conversation, Friend } from '../../../../types' import type { Conversation, Friend } from '../../../../types'
@ -248,7 +248,7 @@ function handleGroupCreated(groupId: number) {
conversationStore.openConversation( conversationStore.openConversation(
groupId, groupId,
ImConversationType.GROUP, ImConversationType.GROUP,
group.name, getGroupDisplayName(group),
group.avatar || '', group.avatar || '',
{ silent: !!group.silent } { silent: !!group.silent }
) )

View File

@ -260,6 +260,7 @@ import { ImRtcCallMediaType, ImRtcCallStatus, ImConversationType } from '@/views
import { resolveCallEndReasonText } from '@/views/im/utils/message' import { resolveCallEndReasonText } from '@/views/im/utils/message'
import { getClientConversationId } from '@/views/im/utils/db' import { getClientConversationId } from '@/views/im/utils/db'
import { getCurrentUserId } from '@/utils/auth' import { getCurrentUserId } from '@/utils/auth'
import { getGroupDisplayName } from '@/views/im/utils/user'
import { useRtcStore } from '../../../../store/rtcStore' import { useRtcStore } from '../../../../store/rtcStore'
import { useMessageStore } from '../../../../store/messageStore' import { useMessageStore } from '../../../../store/messageStore'
@ -405,10 +406,11 @@ const groupInfo = computed<
} }
const group = groupStore.getGroup(conversation.targetId) const group = groupStore.getGroup(conversation.targetId)
const selfMember = group?.members?.find((member) => member.userId === getCurrentUserId()) const selfMember = group?.members?.find((member) => member.userId === getCurrentUserId())
const showGroupName = group ? getGroupDisplayName(group) : conversation.name
return { return {
id: conversation.targetId, id: conversation.targetId,
name: group?.name || conversation.name, name: group?.name || conversation.name,
showGroupName: group?.name || conversation.name, showGroupName,
showImage: group?.avatar || conversation.avatar, showImage: group?.avatar || conversation.avatar,
notice: group?.notice, notice: group?.notice,
remarkNickName: selfMember?.displayUserName, remarkNickName: selfMember?.displayUserName,

View File

@ -154,7 +154,7 @@ import FacePicker from '../../input/FacePicker.vue'
import { useConversationStore } from '@/views/im/home/store/conversationStore' import { useConversationStore } from '@/views/im/home/store/conversationStore'
import { useFriendStore } from '@/views/im/home/store/friendStore' import { useFriendStore } from '@/views/im/home/store/friendStore'
import { useGroupStore } from '@/views/im/home/store/groupStore' import { useGroupStore } from '@/views/im/home/store/groupStore'
import { isGroupQuit } from '@/views/im/utils/user' import { getGroupDisplayName, isGroupQuit } from '@/views/im/utils/user'
import { useMessageSender } from '@/views/im/home/composables/useMessageSender' import { useMessageSender } from '@/views/im/home/composables/useMessageSender'
import { useMessageMultiSelect } from '@/views/im/home/composables/useMessageMultiSelect' import { useMessageMultiSelect } from '@/views/im/home/composables/useMessageMultiSelect'
import { import {
@ -406,7 +406,7 @@ async function handleCreateGroupAndSend() {
const newConversation: Conversation = { const newConversation: Conversation = {
type: ImConversationType.GROUP, type: ImConversationType.GROUP,
targetId: group.id, targetId: group.id,
name: group.name || name, name: getGroupDisplayName(group) || name,
avatar: group.avatar || '', avatar: group.avatar || '',
unreadCount: 0, unreadCount: 0,
lastContent: '', lastContent: '',

View File

@ -104,6 +104,7 @@ import { useImUiStore } from '../../store/uiStore'
import { ImConversationType } from '../../../utils/constants' import { ImConversationType } from '../../../utils/constants'
import { StorageKeys } from '../../../utils/db' import { StorageKeys } from '../../../utils/db'
import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation' import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
import { getGroupDisplayName } from '../../../utils/user'
import type { Conversation } from '../../types' import type { Conversation } from '../../types'
import ResizableAside from '../../components/ResizableAside.vue' import ResizableAside from '../../components/ResizableAside.vue'
import ConversationItem from './components/conversation/ConversationItem.vue' import ConversationItem from './components/conversation/ConversationItem.vue'
@ -220,7 +221,7 @@ function handleGroupCreated(groupId: number) {
conversationStore.openConversation( conversationStore.openConversation(
groupId, groupId,
ImConversationType.GROUP, ImConversationType.GROUP,
group.name, getGroupDisplayName(group),
group.avatar || '', group.avatar || '',
{ silent: !!group.silent } { silent: !!group.silent }
) )

View File

@ -227,7 +227,7 @@ export const useGroupStore = defineStore('imGroupStore', {
return return
} }
const fresh = (list || []).map((group) => convertGroup(group)) const fresh = (list || []).map((group) => convertGroup(group))
// 合并而非全量替换:silent / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留 // 合并而非全量替换:成员缓存只在成员列表接口维护,群个人设置以群列表接口为准
const groupMap = new Map(this.groups.map((group) => [group.id, group])) const groupMap = new Map(this.groups.map((group) => [group.id, group]))
this.groups = fresh.map((group) => { this.groups = fresh.map((group) => {
const existing = groupMap.get(group.id) const existing = groupMap.get(group.id)
@ -238,8 +238,6 @@ export const useGroupStore = defineStore('imGroupStore', {
...group, ...group,
members: existing.members, members: existing.members,
memberCount: existing.memberCount ?? group.memberCount, memberCount: existing.memberCount ?? group.memberCount,
silent: existing.silent ?? group.silent,
groupRemark: existing.groupRemark,
membersLoaded: existing.membersLoaded, membersLoaded: existing.membersLoaded,
membersExpired: existing.membersExpired membersExpired: existing.membersExpired
} }
@ -254,6 +252,24 @@ export const useGroupStore = defineStore('imGroupStore', {
}) })
} }
this.saveGroupList() this.saveGroupList()
this.preloadMembersForEmptyAvatarGroups()
},
/** 预加载空群头像的成员列表,供 GroupAvatar 异步合成群头像 */
preloadMembersForEmptyAvatarGroups() {
for (const group of this.groups) {
if (
group.avatar ||
group.joinStatus === CommonStatusEnum.DISABLE ||
(group.membersLoaded && !group.membersExpired && group.members?.length)
) {
continue
}
const force = !!group.membersLoaded && !group.membersExpired && !group.members?.length
this.fetchGroupMemberList(group.id, force).catch((error) => {
console.warn('[IM groupStore] 预加载群头像成员失败', { groupId: group.id }, error)
})
}
}, },
/** 失效全部群成员缓存 */ /** 失效全部群成员缓存 */
@ -892,7 +908,9 @@ function convertGroup(group: ImGroupRespVO): Group {
mutedAll: group.mutedAll, mutedAll: group.mutedAll,
banned: group.banned, banned: group.banned,
joinApproval: group.joinApproval, joinApproval: group.joinApproval,
joinStatus: group.joinStatus joinStatus: group.joinStatus,
groupRemark: group.groupRemark,
silent: group.silent
} }
} }

View File

@ -30,7 +30,7 @@ import {
import { useConversationStore } from './conversationStore' import { useConversationStore } from './conversationStore'
import { useMessageStore } from './messageStore' import { useMessageStore } from './messageStore'
import { useFriendStore, type FriendNotificationPayload } from './friendStore' import { useFriendStore, type FriendNotificationPayload } from './friendStore'
import { getFriendDisplayName } from '../../utils/user' import { getFriendDisplayName, getGroupDisplayName } from '../../utils/user'
import { useGroupStore } from './groupStore' import { useGroupStore } from './groupStore'
import { useGroupRequestStore } from './groupRequestStore' import { useGroupRequestStore } from './groupRequestStore'
import { import {
@ -732,7 +732,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
{ {
type: ImConversationType.GROUP, type: ImConversationType.GROUP,
targetId: websocketMessage.groupId, targetId: websocketMessage.groupId,
name: group?.name || String(websocketMessage.groupId), name: group ? getGroupDisplayName(group) : String(websocketMessage.groupId),
avatar: group?.avatar || '', avatar: group?.avatar || '',
silent: group?.silent silent: group?.silent
}, },