✨ feat(im): 优化 MessagePage.vue 页面,对齐微信交互
parent
e1b52be8ea
commit
1a0c11f685
|
|
@ -30,7 +30,7 @@ const props = withDefaults(
|
||||||
defaultWidth?: number // 默认宽度
|
defaultWidth?: number // 默认宽度
|
||||||
minWidth?: number // 最小宽度
|
minWidth?: number // 最小宽度
|
||||||
maxWidth?: number // 最大宽度
|
maxWidth?: number // 最大宽度
|
||||||
storageKey: string // localStorage 存储 key,必填;调用方通过 StorageKeys.asideWidth(page) 生成
|
storageKey: string // localStorage 存储 key,必填;调用方传 StorageKeys.asideWidth(三 Tab 共用一份)
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
defaultWidth: 260,
|
defaultWidth: 260,
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,132 @@
|
||||||
<!-- 消息 Tab:左侧会话列表 + 右侧聊天面板 -->
|
<!-- 消息 Tab:左侧会话列表 + 右侧聊天面板 -->
|
||||||
<div class="flex flex-1 min-w-0 h-full">
|
<div class="flex flex-1 min-w-0 h-full">
|
||||||
<!-- 左侧会话列表(可拖拽宽度) -->
|
<!-- 左侧会话列表(可拖拽宽度) -->
|
||||||
<ResizableAside :default-width="260" :storage-key="StorageKeys.asideWidth('message')">
|
<ResizableAside :default-width="260" :storage-key="StorageKeys.asideWidth">
|
||||||
<!-- TODO @AI:对齐微信的交互:1)搜索框;2)+ 号:发起群聊、添加好友 -->
|
<!-- 顶部:搜索框 + "+" 号下拉(对齐微信 PC:发起群聊 / 添加朋友) -->
|
||||||
<div
|
<div
|
||||||
class="flex flex-shrink-0 items-center h-14 px-4 text-base font-medium text-[var(--el-text-color-primary)] border-b border-[var(--el-border-color-light)]"
|
class="flex flex-shrink-0 gap-2 items-center px-4 py-2 border-b border-[var(--el-border-color-lighter)]"
|
||||||
>
|
>
|
||||||
消息
|
<el-input v-model="keyword" placeholder="搜索" clearable class="flex-1">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-dropdown trigger="click" placement="bottom">
|
||||||
|
<el-button size="small" :icon="Plus" circle />
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="createGroupVisible = true">
|
||||||
|
<Icon icon="ant-design:message-outlined" :size="16" />
|
||||||
|
<span>发起群聊</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="addFriendVisible = true">
|
||||||
|
<Icon icon="ant-design:user-add-outlined" :size="16" />
|
||||||
|
<span>添加朋友</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 会话列表主体 -->
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<ConversationItem
|
<ConversationItem
|
||||||
v-for="conversation in sortedConversations"
|
v-for="conversation in filteredConversations"
|
||||||
:key="`${conversation.type}-${conversation.targetId}`"
|
:key="`${conversation.type}-${conversation.targetId}`"
|
||||||
:conversation="conversation"
|
:conversation="conversation"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="sortedConversations.length === 0"
|
v-if="filteredConversations.length === 0"
|
||||||
class="flex items-center justify-center py-10 text-sm text-[var(--el-text-color-secondary)]"
|
class="flex items-center justify-center py-10 text-sm text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
暂无会话
|
{{ keyword ? '没有满足条件的会话' : '暂无会话' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizableAside>
|
</ResizableAside>
|
||||||
|
|
||||||
<!-- 右侧聊天面板 -->
|
<!-- 右侧聊天面板 -->
|
||||||
<ChatPanel />
|
<ChatPanel />
|
||||||
|
|
||||||
|
<!-- 添加朋友 / 发起群聊弹窗 -->
|
||||||
|
<AddFriendDialog v-model="addFriendVisible" @added="handleFriendAdded" />
|
||||||
|
<CreateGroupDialog
|
||||||
|
v-model="createGroupVisible"
|
||||||
|
:friends="friends"
|
||||||
|
@created="handleGroupCreated"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { Search, Plus } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
import Icon from '@/components/Icon/src/Icon.vue'
|
||||||
import { useConversationStore } from '../../store/conversationStore'
|
import { useConversationStore } from '../../store/conversationStore'
|
||||||
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import { StorageKeys } from '../../../utils/storage'
|
import { StorageKeys } from '../../../utils/storage'
|
||||||
|
import { ImConversationType } from '../../../utils/constants'
|
||||||
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
|
import type { Friend } from '../../types'
|
||||||
|
import type { FriendLite } from '../friend/components/FriendItem.vue'
|
||||||
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'
|
||||||
import ChatPanel from './components/ChatPanel.vue'
|
import ChatPanel from './components/ChatPanel.vue'
|
||||||
|
import AddFriendDialog from '../friend/components/AddFriendDialog.vue'
|
||||||
|
import CreateGroupDialog from '../group/components/CreateGroupDialog.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessagePage' })
|
defineOptions({ name: 'ImMessagePage' })
|
||||||
|
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
|
const friendStore = useFriendStore()
|
||||||
|
const groupStore = useGroupStore()
|
||||||
|
|
||||||
|
const keyword = ref('')
|
||||||
|
const addFriendVisible = ref(false)
|
||||||
|
const createGroupVisible = ref(false)
|
||||||
|
|
||||||
const sortedConversations = computed(() => conversationStore.getSortedConversations)
|
const sortedConversations = computed(() => conversationStore.getSortedConversations)
|
||||||
|
|
||||||
|
/** 顶部搜索框过滤会话:只按 name 模糊匹配,避免命中 lastContent 等次要字段干扰 */
|
||||||
|
const filteredConversations = computed(() => {
|
||||||
|
const keywordLower = keyword.value.trim().toLowerCase()
|
||||||
|
if (!keywordLower) {
|
||||||
|
return sortedConversations.value
|
||||||
|
}
|
||||||
|
return sortedConversations.value.filter((c) =>
|
||||||
|
(c.name || '').toLowerCase().includes(keywordLower)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** CreateGroupDialog 需要全量好友列表来勾选成员,结构与 friend / group Tab 保持一致 */
|
||||||
|
const friends = computed<FriendLite[]>(() =>
|
||||||
|
friendStore.getActiveFriends.map((friend: Friend) => ({
|
||||||
|
id: friend.friendUserId,
|
||||||
|
nickname: friend.nickname,
|
||||||
|
avatar: friend.avatar,
|
||||||
|
deleted: friend.status === CommonStatusEnum.DISABLE
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 加好友成功后强制刷新好友列表,让群聊弹窗的勾选项也能看到新好友 */
|
||||||
|
async function handleFriendAdded() {
|
||||||
|
await friendStore.loadFriends(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 建群成功后刷新群列表,并直接打开新群会话(自动选中并渲染到右侧 ChatPanel) */
|
||||||
|
async function handleGroupCreated(groupId: number) {
|
||||||
|
await groupStore.loadGroups(true)
|
||||||
|
const group = groupStore.getGroup(groupId)
|
||||||
|
if (!group) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conversationStore.openConversation(
|
||||||
|
groupId,
|
||||||
|
ImConversationType.GROUP,
|
||||||
|
group.name,
|
||||||
|
group.avatar || '',
|
||||||
|
{ muted: !!group.muted }
|
||||||
|
)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue