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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -227,7 +227,7 @@ export const useGroupStore = defineStore('imGroupStore', {
return
}
const fresh = (list || []).map((group) => convertGroup(group))
// 合并而非全量替换:silent / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留
// 合并而非全量替换:成员缓存只在成员列表接口维护,群个人设置以群列表接口为准
const groupMap = new Map(this.groups.map((group) => [group.id, group]))
this.groups = fresh.map((group) => {
const existing = groupMap.get(group.id)
@ -238,8 +238,6 @@ export const useGroupStore = defineStore('imGroupStore', {
...group,
members: existing.members,
memberCount: existing.memberCount ?? group.memberCount,
silent: existing.silent ?? group.silent,
groupRemark: existing.groupRemark,
membersLoaded: existing.membersLoaded,
membersExpired: existing.membersExpired
}
@ -254,6 +252,24 @@ export const useGroupStore = defineStore('imGroupStore', {
})
}
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,
banned: group.banned,
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 { useMessageStore } from './messageStore'
import { useFriendStore, type FriendNotificationPayload } from './friendStore'
import { getFriendDisplayName } from '../../utils/user'
import { getFriendDisplayName, getGroupDisplayName } from '../../utils/user'
import { useGroupStore } from './groupStore'
import { useGroupRequestStore } from './groupRequestStore'
import {
@ -732,7 +732,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
{
type: ImConversationType.GROUP,
targetId: websocketMessage.groupId,
name: group?.name || String(websocketMessage.groupId),
name: group ? getGroupDisplayName(group) : String(websocketMessage.groupId),
avatar: group?.avatar || '',
silent: group?.silent
},