feat(im): 优化整体 message 包结构

im
YunaiV 2026-04-28 09:29:40 +08:00
parent 122b1ba748
commit 29a03ef03d
10 changed files with 164 additions and 116 deletions

View File

@ -1,7 +1,7 @@
<template>
<!--
群成员单行
跨子域复用@候选 (MentionPicker) / 已读列表 (MessageReadStatus) / 群成员宫格 (ChatGroupSide)
跨子域复用@候选 (MentionPicker) / 已读列表 (MessageReadStatus) / 群成员宫格 (ConversationGroupSide)
-->
<div
class="relative flex items-center px-[5px] box-border whitespace-nowrap"
@ -27,9 +27,9 @@
<script lang="ts" setup>
import { computed } from 'vue'
import UserAvatar from '../../../components/UserAvatar.vue'
import UserAvatar from './UserAvatar.vue'
defineOptions({ name: 'ImChatGroupMember' })
defineOptions({ name: 'ImGroupMember' })
/** 群成员结构(跨多处使用,放这里做窄接口;独立于 types/index.ts */
export interface GroupMemberLite {

View File

@ -48,7 +48,7 @@
</ResizableAside>
<!-- 右侧聊天面板 -->
<ChatPanel />
<MessagePanel />
<!-- 添加朋友 / 发起群聊弹窗 -->
<AddFriendDialog v-model="addFriendVisible" @added="handleFriendAdded" />
@ -73,7 +73,7 @@ import type { Friend } from '../../types'
import type { FriendLite } from '../friend/components/FriendItem.vue'
import ResizableAside from '../../components/ResizableAside.vue'
import ConversationItem from './components/conversation/ConversationItem.vue'
import ChatPanel from './components/ChatPanel.vue'
import MessagePanel from './components/message/MessagePanel.vue'
import AddFriendDialog from '../friend/components/AddFriendDialog.vue'
import CreateGroupDialog from '../group/components/CreateGroupDialog.vue'
@ -115,7 +115,7 @@ async function handleFriendAdded() {
await friendStore.loadFriends(true)
}
/** 建群成功后刷新群列表,并直接打开新群会话(自动选中并渲染到右侧 ChatPanel */
/** 建群成功后刷新群列表,并直接打开新群会话(自动选中并渲染到右侧 MessagePanel */
async function handleGroupCreated(groupId: number) {
await groupStore.loadGroups(true)
const group = groupStore.getGroup(groupId)

View File

@ -1,20 +1,13 @@
<template>
<!-- TODO @AI不够对齐微信如果让你改时需要提醒我给你图片 -->
<!-- TODO @AI新建一个 components/side -->
<!--
聊天面板右侧信息抽屉
- 抽屉形态 v-model 控制由父组件 ChatPanel 管理开关
- 群成员宫格 + 邀请 / 移除按钮仅群主 + 群信息表单 + 退群按钮
- page 引用 pages/group/components/ 下的 Dialog 组件
- TODO TODO @AI群模块后端 API 对接后替换 saveGroup / quit / remove
-->
<!-- 聊天面板右侧信息抽屉群成员宫格 + 群信息表单 + 退群 / 移除等动作 -->
<el-drawer
v-model="visible"
:with-header="false"
direction="rtl"
size="360px"
append-to-body
modal-class="im-chat-group-side__modal"
modal-class="im-conversation-group-side__modal"
>
<div class="flex flex-col h-full p-2.5">
<!-- 群成员区 -->
@ -39,7 +32,7 @@
@click="inviteVisible = true"
>
<div
class="im-chat-group-side__tool-btn flex items-center justify-center w-[38px] h-[38px] text-[18px] cursor-pointer border border-dashed border-[var(--el-border-color)] rounded text-[var(--el-text-color-regular)] hover:text-[#409eff] hover:border-[#409eff]"
class="im-conversation-group-side__tool-btn flex items-center justify-center w-[38px] h-[38px] text-[18px] cursor-pointer border border-dashed border-[var(--el-border-color)] rounded text-[var(--el-text-color-regular)] hover:text-[#409eff] hover:border-[#409eff]"
>
<Icon icon="ant-design:plus-outlined" />
</div>
@ -54,7 +47,7 @@
@click="removeVisible = true"
>
<div
class="im-chat-group-side__tool-btn flex items-center justify-center w-[38px] h-[38px] text-[18px] cursor-pointer border border-dashed border-[var(--el-border-color)] rounded text-[var(--el-text-color-regular)] hover:text-[#409eff] hover:border-[#409eff]"
class="im-conversation-group-side__tool-btn flex items-center justify-center w-[38px] h-[38px] text-[18px] cursor-pointer border border-dashed border-[var(--el-border-color)] rounded text-[var(--el-text-color-regular)] hover:text-[#409eff] hover:border-[#409eff]"
>
<Icon icon="ant-design:minus-outlined" />
</div>
@ -120,21 +113,26 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import Icon from '@/components/Icon/src/Icon.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useUserStore } from '@/store/modules/user'
import { CommonStatusEnum } from '@/utils/constants'
import GroupMemberGrid from '../../group/components/GroupMemberGrid.vue'
import AddGroupMemberDialog from '../../group/components/AddGroupMemberDialog.vue'
import { updateGroup } from '@/api/im/group'
import { quitGroup, removeGroupMember, updateGroupMember } from '@/api/im/group/member'
import { useConversationStore } from '../../../../store/conversationStore'
import { useGroupStore } from '../../../../store/groupStore'
import { ImConversationType } from '../../../../../utils/constants'
import GroupMemberGrid from '../../../group/components/GroupMemberGrid.vue'
import AddGroupMemberDialog from '../../../group/components/AddGroupMemberDialog.vue'
import GroupMemberSelector, {
type GroupMemberFlag
} from '../../group/components/GroupMemberSelector.vue'
import type { GroupLite } from '../../group/components/GroupItem.vue'
import type { GroupMemberLite } from './ChatGroupMember.vue'
import type { FriendLite } from '../../friend/components/FriendItem.vue'
} from '../../../group/components/GroupMemberSelector.vue'
import type { GroupLite } from '../../../group/components/GroupItem.vue'
import type { GroupMemberLite } from '../../../../components/GroupMember.vue'
import type { FriendLite } from '../../../friend/components/FriendItem.vue'
defineOptions({ name: 'ImChatGroupSide' })
defineOptions({ name: 'ImConversationGroupSide' })
const props = withDefaults(
defineProps<{
@ -156,6 +154,9 @@ const emit = defineEmits<{
}>()
const userStore = useUserStore()
const conversationStore = useConversationStore()
const groupStore = useGroupStore()
const message = useMessage()
const visible = computed({
get: () => props.modelValue,
@ -167,11 +168,6 @@ const editing = ref(false)
const inviteVisible = ref(false)
const removeVisible = ref(false)
/**
* 群信息表单本地副本
* 不能直接 v-model propvue/no-mutating-props用本地 reactive 承接
* 保存时通过 emit / API 把变更回写给父组件
*/
const formData = reactive({
name: '',
notice: '',
@ -189,9 +185,7 @@ watch(
)
const myId = computed(() => Number(userStore.getUser?.id) || 0)
const isOwner = computed(() => props.group != null && props.group.ownerId === myId.value)
const ownerName = computed(() => {
if (!props.group) {
return ''
@ -199,7 +193,6 @@ const ownerName = computed(() => {
const owner = props.members.find((m) => m.userId === props.group!.ownerId)
return owner?.showNickName || '-'
})
const filteredMembers = computed(() =>
props.members.filter(
(member) =>
@ -208,36 +201,92 @@ const filteredMembers = computed(() =>
)
)
// TODO /im/group/modify
/**
* 保存群信息
* - 群主群名 / 群公告 /im/group/update自己在群里的昵称 /im/group-member/update
* - 普通成员仅自己的群昵称群名 / 公告 disabled但兜底防表单被绕开
*/
async function handleSave() {
ElMessage.info('群信息保存接口待接入,当前为占位实现')
editing.value = false
}
// TODO /im/group/quit
async function handleQuit() {
try {
await ElMessageBox.confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出', {
type: 'warning'
})
ElMessage.info('退出群聊接口待接入,当前为占位实现')
} catch {
//
}
}
// TODO /im/group/member/remove
function handleRemoveComplete(members: GroupMemberFlag[]) {
if (members.length === 0) {
if (!props.group) {
return
}
ElMessage.info(`移除成员接口待接入,选择了 ${members.length} 位成员`)
const groupId = props.group.id
try {
// 1. / disabled
if (isOwner.value) {
await updateGroup({
id: groupId,
name: formData.name,
notice: formData.notice
})
}
// 2. displayUserName
await updateGroupMember({
groupId,
displayUserName: formData.remarkNickName
})
// 3. emit reload MessagePanel reloadGroupData +
message.success('保存成功')
editing.value = false
emit('reload')
} catch (error) {
console.error('[IM ConversationGroupSide] 保存群信息失败', { groupId }, error)
message.error('保存失败')
}
}
/** 退出群聊(普通成员入口;群主退出走"解散群"是另一条路径,这里不处理) */
async function handleQuit() {
if (!props.group) {
return
}
// 1. ElMessageBox reject return
try {
await message.confirm('退出群聊后将不再接收群里的消息,确认退出吗?', '确认退出')
} catch {
return
}
const groupId = props.group.id
try {
// 2. /im/group/quit
await quitGroup(groupId)
// 3. + store /
conversationStore.removeConversation(ImConversationType.GROUP, groupId)
groupStore.removeGroup(groupId)
// 4.
message.success('已退出群聊')
visible.value = false
} catch (error) {
console.error('[IM ConversationGroupSide] 退出群聊失败', { groupId }, error)
message.error('退出群聊失败')
}
}
/** 移除群成员(仅群主入口)*/
async function handleRemoveComplete(members: GroupMemberFlag[]) {
if (!props.group || members.length === 0) {
return
}
const groupId = props.group.id
try {
// 1. userId N
await removeGroupMember({
groupId,
memberUserIds: members.map((member) => member.userId)
})
// 2. emit reload UI
message.success(`已移除 ${members.length} 位成员`)
emit('reload')
} catch (error) {
console.error('[IM ConversationGroupSide] 移除群成员失败', { groupId }, error)
message.error('移除成员失败')
}
}
</script>
<style scoped>
/* el-icon 的全局 color 在暗色模式下会被主题盖过;:deep(svg) 锁 fill 到当前色 */
.im-chat-group-side__tool-btn :deep(svg) {
.im-conversation-group-side__tool-btn :deep(svg) {
fill: currentColor !important;
}
</style>

View File

@ -1,9 +1,8 @@
<template>
<!-- TODO @AI不够对齐微信如果让你改时需要提醒我给你图片 -->
<!-- TODO @AI新建一个 components/side -->
<!--
私聊侧边抽屉
- 抽屉形态 v-model 控制由父组件 ChatPanel 管理开关
- 抽屉形态 v-model 控制由父组件 MessagePanel 管理开关
- 顶部好友头像 + 昵称
- 操作消息免打扰 / 置顶聊天
- 与会话列表右键菜单同语义免打扰联动 friendStore.setMuted
@ -26,7 +25,7 @@
</div>
</div>
<el-divider class="im-chat-private-side__divider" />
<el-divider class="im-conversation-private-side__divider" />
<!-- 操作项 -->
<div class="flex flex-col gap-3.5 text-sm">
@ -46,13 +45,13 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useConversationStore } from '../../../store/conversationStore'
import { useFriendStore } from '../../../store/friendStore'
import { ImConversationType } from '../../../../utils/constants'
import type { Conversation, Friend } from '../../../types'
import UserAvatar from '../../../components/UserAvatar.vue'
import { useConversationStore } from '../../../../store/conversationStore'
import { useFriendStore } from '../../../../store/friendStore'
import { ImConversationType } from '../../../../../utils/constants'
import type { Conversation, Friend } from '../../../../types'
import UserAvatar from '../../../../components/UserAvatar.vue'
defineOptions({ name: 'ImChatPrivateSide' })
defineOptions({ name: 'ImConversationPrivateSide' })
const props = withDefaults(
defineProps<{
@ -98,7 +97,7 @@ function onTopChange(value: boolean | string | number) {
<style scoped>
/* el-divider 默认 margin 较大,压成 8px和群聊抽屉视觉对齐 */
.im-chat-private-side__divider {
.im-conversation-private-side__divider {
margin: 8px 0;
}
</style>

View File

@ -41,7 +41,7 @@
</div>
<!-- 真成员行 -->
<ChatGroupMember
<GroupMember
v-for="(member, idx) in memberItems"
:key="member.userId"
:member="member"
@ -65,7 +65,7 @@ import Icon from '@/components/Icon/src/Icon.vue'
import { useUserStore } from '@/store/modules/user'
import { CommonStatusEnum } from '@/utils/constants'
import { IM_AT_ALL_NICKNAME, IM_AT_ALL_USER_ID } from '@/views/im/utils/constants'
import ChatGroupMember, { type GroupMemberLite } from '../ChatGroupMember.vue'
import GroupMember, { type GroupMemberLite } from '../../../../components/GroupMember.vue'
defineOptions({ name: 'ImMentionPicker' })

View File

@ -135,7 +135,7 @@ import {
import EmojiPicker from './EmojiPicker.vue'
import MentionPicker from './MentionPicker.vue'
import VoiceRecorder from './VoiceRecorder.vue'
import type { GroupMemberLite } from '../ChatGroupMember.vue'
import type { GroupMemberLite } from '../../../../components/GroupMember.vue'
defineOptions({ name: 'ImMessageInput' })
@ -416,7 +416,7 @@ const isGroup = computed(
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
)
/** 从 groupStore 读当前激活群的成员(切会话时由 ChatPanel 预拉) */
/** 从 groupStore 读当前激活群的成员(切会话时由 MessagePanel 预拉) */
const groupMembers = computed<GroupMemberLite[]>(() => {
const conversation = conversationStore.activeConversation
if (!conversation || conversation.type !== ImConversationType.GROUP) {

View File

@ -94,7 +94,7 @@
</div>
</div>
</el-popover>
<!-- 群成员仅群聊popover 内自带搜索 + ChatGroupMember 列表 -->
<!-- 群成员仅群聊popover 内自带搜索 + GroupMember 列表 -->
<el-popover
v-if="isGroup"
v-model:visible="memberPopoverVisible"
@ -117,7 +117,7 @@
</template>
</el-input>
<div class="max-h-[360px] overflow-y-auto mt-2">
<ChatGroupMember
<GroupMember
v-for="member in filteredMembersForPicker"
:key="member.userId"
:member="member"
@ -321,7 +321,7 @@ import {
} from '../../../../../utils/message'
import type { Message } from '../../../../types'
import UserAvatar from '../../../../components/UserAvatar.vue'
import ChatGroupMember, { type GroupMemberLite } from '../ChatGroupMember.vue'
import GroupMember, { type GroupMemberLite } from '../../../../components/GroupMember.vue'
defineOptions({ name: 'ImMessageHistory' })
@ -330,7 +330,7 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
// "" ChatPanel +
// "" MessagePanel +
locate: [messageId: number]
}>()
@ -697,7 +697,7 @@ function openFile(url?: string) {
}
/**
* 定位到聊天位置emit ChatPanel scrollIntoView + 短暂高亮再关掉自己
* 定位到聊天位置emit MessagePanel scrollIntoView + 短暂高亮再关掉自己
* messageId === 0本地占位消息跳过还没拿到真实 idDOM 上没法 querySelector
*/
function locateMessage(messageId: number) {

View File

@ -240,7 +240,7 @@ import { useMessageSender } from '../../../../composables/useMessageSender'
import type { Message } from '../../../../types'
import MessageReadStatus from './MessageReadStatus.vue'
import UserAvatar from '../../../../components/UserAvatar.vue'
import type { GroupMemberLite } from '../ChatGroupMember.vue'
import type { GroupMemberLite } from '../../../../components/GroupMember.vue'
defineOptions({ name: 'ImMessageItem' })

View File

@ -14,7 +14,7 @@
<Icon
icon="ant-design:profile-outlined"
:size="20"
class="chat-panel__header-icon cursor-pointer"
class="message-panel__header-icon cursor-pointer"
@click="historyVisible = true"
/>
</el-tooltip>
@ -24,7 +24,7 @@
<Icon
icon="ant-design:more-outlined"
:size="20"
class="chat-panel__header-icon cursor-pointer"
class="message-panel__header-icon cursor-pointer"
@click="toggleSide"
/>
</el-tooltip>
@ -49,16 +49,16 @@
v-for="msg in messages"
:key="msg.id || msg.clientMessageId"
:data-message-id="msg.id || ''"
class="chat-panel__message-anchor"
class="message-panel__message-anchor"
>
<MessageItem :message="msg" />
</div>
<!-- 回到底部浮动按钮滚动不在底部时显示 -->
<transition name="chat-panel__jump-fade">
<transition name="message-panel__jump-fade">
<div
v-if="showJumpToBottom"
class="chat-panel__jump-bottom sticky bottom-3 left-1/2 inline-flex gap-1.5 items-center w-fit mx-auto px-3.5 py-1.5 text-xs text-[#409eff] bg-[var(--el-bg-color-overlay)] rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.12)] cursor-pointer hover:text-white hover:bg-[#409eff]"
class="message-panel__jump-bottom sticky bottom-3 left-1/2 inline-flex gap-1.5 items-center w-fit mx-auto px-3.5 py-1.5 text-xs text-[#409eff] bg-[var(--el-bg-color-overlay)] rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.12)] cursor-pointer hover:text-white hover:bg-[#409eff]"
@click="scrollToBottom(true)"
>
<Icon icon="ant-design:down-outlined" :size="14" />
@ -78,7 +78,7 @@
<MessageInput :key="messageInputKey" />
<!-- 右侧信息抽屉群聊 / 私聊各自一份 -->
<ChatGroupSide
<ConversationGroupSide
v-if="isGroup"
v-model="sideVisible"
:group="groupInfo"
@ -86,7 +86,7 @@
:friends="groupFriends"
@reload="reloadGroupData"
/>
<ChatPrivateSide
<ConversationPrivateSide
v-else
v-model="sideVisible"
:conversation="conversationStore.activeConversation"
@ -109,21 +109,21 @@
import { ref, watch, nextTick, computed } from 'vue'
import Icon from '@/components/Icon/src/Icon.vue'
import { useConversationStore } from '../../../store/conversationStore'
import { useFriendStore } from '../../../store/friendStore'
import { useGroupStore } from '../../../store/groupStore'
import { ImConversationType } from '../../../../utils/constants'
import { useConversationStore } from '../../../../store/conversationStore'
import { useFriendStore } from '../../../../store/friendStore'
import { useGroupStore } from '../../../../store/groupStore'
import { ImConversationType } from '../../../../../utils/constants'
import { CommonStatusEnum } from '@/utils/constants'
import MessageItem from './message/MessageItem.vue'
import MessageInput from './input/MessageInput.vue'
import MessageHistory from './message/MessageHistory.vue'
import ChatGroupSide from './ChatGroupSide.vue'
import ChatPrivateSide from './ChatPrivateSide.vue'
import type { GroupLite } from '../../group/components/GroupItem.vue'
import type { GroupMemberLite } from './ChatGroupMember.vue'
import type { FriendLite } from '../../friend/components/FriendItem.vue'
import MessageItem from './MessageItem.vue'
import MessageInput from '../input/MessageInput.vue'
import MessageHistory from './MessageHistory.vue'
import ConversationGroupSide from '../conversation/ConversationGroupSide.vue'
import ConversationPrivateSide from '../conversation/ConversationPrivateSide.vue'
import type { GroupLite } from '../../../group/components/GroupItem.vue'
import type { GroupMemberLite } from '../../../../components/GroupMember.vue'
import type { FriendLite } from '../../../friend/components/FriendItem.vue'
defineOptions({ name: 'ImChatPanel' })
defineOptions({ name: 'ImMessagePanel' })
const conversationStore = useConversationStore()
const friendStore = useFriendStore()
@ -212,13 +212,13 @@ const groupFriends = computed<FriendLite[]>(() =>
*/
function ensureGroupData(groupId: number) {
groupStore.loadGroupInfo(groupId).catch((error) => {
console.warn('[IM ChatPanel] loadGroupInfo 失败', { groupId }, error)
console.warn('[IM MessagePanel] loadGroupInfo 失败', { groupId }, error)
})
groupStore.loadGroupMembers(groupId).catch((error) => {
console.warn('[IM ChatPanel] loadGroupMembers 失败', { groupId }, error)
console.warn('[IM MessagePanel] loadGroupMembers 失败', { groupId }, error)
})
friendStore.loadFriends().catch((error) => {
console.warn('[IM ChatPanel] loadFriends 失败', { groupId }, error)
console.warn('[IM MessagePanel] loadFriends 失败', { groupId }, error)
})
}
@ -238,7 +238,7 @@ function reloadGroupData() {
const historyVisible = ref(false)
/**
* 信息抽屉的开关 ChatPanel 本地 UI 状态
* 信息抽屉的开关 MessagePanel 本地 UI 状态
*
* 群聊 / 私聊共用一个 ref模板里 v-if-else 决定挂哪个抽屉同一时刻只有一个组件
* DOM 所以一个布尔够用早先拆成 sideVisible + privateSideVisible 是冗余
@ -324,9 +324,9 @@ async function handleLocate(messageId: number) {
return
}
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
target.classList.add('chat-panel__message-anchor--highlight')
target.classList.add('message-panel__message-anchor--highlight')
setTimeout(() => {
target.classList.remove('chat-panel__message-anchor--highlight')
target.classList.remove('message-panel__message-anchor--highlight')
}, 1600)
}
@ -379,20 +379,20 @@ watch(
<style scoped>
/* el-icon .el-icon{color:var(--color,inherit)} UnoCSS :deep + !important
颜色直接引用 Element Plus 主题变量暗色模式自动切到更亮的灰 */
.chat-panel__header-icon,
.chat-panel__header-icon :deep(svg) {
.message-panel__header-icon,
.message-panel__header-icon :deep(svg) {
color: var(--el-text-color-regular) !important;
fill: currentColor !important;
transition: color 0.15s;
}
.chat-panel__header-icon:hover,
.chat-panel__header-icon:hover :deep(svg) {
.message-panel__header-icon:hover,
.message-panel__header-icon:hover :deep(svg) {
color: var(--el-color-primary) !important;
}
/* sticky + translate fit-content transform -50%
UnoCSS 表达 transform+transition value 不太方便这里用最小的 scoped CSS 承接 */
.chat-panel__jump-bottom {
.message-panel__jump-bottom {
transform: translateX(-50%);
transition:
opacity 0.2s,
@ -400,23 +400,23 @@ watch(
}
/* MessageHistory "定位" 跳过来时短暂高亮1.6s 后由 JS 移除 class配合 transition 缓出黄底 */
.chat-panel__message-anchor {
.message-panel__message-anchor {
transition: background-color 0.6s ease;
}
.chat-panel__message-anchor--highlight {
.message-panel__message-anchor--highlight {
background-color: var(--el-color-warning-light-9);
}
/* 回到底部按钮的 Vue transition 钩子类名 */
.chat-panel__jump-fade-enter-active,
.chat-panel__jump-fade-leave-active {
.message-panel__jump-fade-enter-active,
.message-panel__jump-fade-leave-active {
transition:
opacity 0.2s,
transform 0.2s;
}
.chat-panel__jump-fade-enter-from,
.chat-panel__jump-fade-leave-to {
.message-panel__jump-fade-enter-from,
.message-panel__jump-fade-leave-to {
opacity: 0;
transform: translate(-50%, 20px);
}

View File

@ -25,7 +25,7 @@
<el-tab-pane :label="`已读(${readMembers.length})`" name="read">
<PagedScroller :items="readMembers" :page-size="20" class="h-75">
<template #default="{ item }">
<ChatGroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
<GroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
</template>
</PagedScroller>
<div
@ -38,7 +38,7 @@
<el-tab-pane :label="`未读(${unreadMembers.length})`" name="unread">
<PagedScroller :items="unreadMembers" :page-size="20" class="h-75">
<template #default="{ item }">
<ChatGroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
<GroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
</template>
</PagedScroller>
<div
@ -60,14 +60,14 @@ import { CommonStatusEnum } from '@/utils/constants'
import { ImConversationType, ImGroupReceiptStatus } from '../../../../../utils/constants'
import type { Message } from '../../../../types'
import { useConversationStore } from '../../../../store/conversationStore'
import ChatGroupMember, { type GroupMemberLite } from '../ChatGroupMember.vue'
import GroupMember, { type GroupMemberLite } from '../../../../components/GroupMember.vue'
import PagedScroller from '../../../../components/PagedScroller.vue'
defineOptions({ name: 'ImMessageReadStatus' })
const props = defineProps<{
message: Message
// ChatPanel.groupMembers
// MessagePanel.groupMembers
groupMembers: GroupMemberLite[]
// loadReadUsers /im/message/group/get-read-users
groupId: number