✨ feat(im): 优化整体 message 包结构
parent
122b1ba748
commit
29a03ef03d
|
|
@ -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 {
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 prop(vue/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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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' })
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(本地占位消息)跳过——还没拿到真实 id,DOM 上没法 querySelector
|
||||
*/
|
||||
function locateMessage(messageId: number) {
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue