im
YunaiV 2026-04-30 21:38:17 +08:00
parent fd1ba30bdb
commit 9f1fc9ef78
4 changed files with 36 additions and 22 deletions

View File

@ -115,9 +115,10 @@ import Icon from '@/components/Icon/src/Icon.vue'
import { useConversationStore } from '../../../../store/conversationStore'
import { useFriendStore } from '../../../../store/friendStore'
import { getMemberDisplayName } from '../../../../../utils/user'
import { getMemberDisplayName } from '@/views/im/utils/user'
import { useGroupStore } from '../../../../store/groupStore'
import { ImConversationType } from '../../../../../utils/constants'
import { ImConversationType } from '@/views/im/utils/constants'
import { getConversationKey } from '@/views/im/utils/conversation'
import { CommonStatusEnum } from '@/utils/constants'
import MessageItem from './MessageItem.vue'
import MessageInput from '../input/MessageInput.vue'
@ -145,7 +146,7 @@ const isGroup = computed(
*/
const messageInputKey = computed(() => {
const conv = conversationStore.activeConversation
return conv ? `${conv.type}-${conv.targetId}` : 'none'
return conv ? getConversationKey(conv) : 'none'
})
/** "是否停留在底部"的阈值:距离底部 < 80px 视为底部 */

View File

@ -37,20 +37,20 @@
<template v-if="!showPinnedSection">
<ConversationItem
v-for="conversation in pinnedConversations"
:key="`${conversation.type}-${conversation.targetId}`"
:key="getConversationKey(conversation)"
:conversation="conversation"
/>
</template>
<div v-else class="bg-[var(--el-fill-color-light)]">
<ConversationItem
v-for="conversation in renderedPinnedConversations"
:key="`${conversation.type}-${conversation.targetId}`"
:key="getConversationKey(conversation)"
:conversation="conversation"
/>
<!-- 折叠头放在置顶区底部对齐 WeChat mac展开 / 折叠态共用仅在还有"可折叠"内容或当前已展开时出现 -->
<div
v-if="foldablePinnedConversations.length > 0 || pinnedExpanded"
v-if="pinnedGroups.foldable.length > 0 || pinnedExpanded"
class="flex items-center justify-between px-4 py-2.5 cursor-pointer transition-colors text-13px text-[var(--el-text-color-regular)] border-y border-[var(--el-border-color-lighter)] hover:bg-[var(--el-fill-color)]"
@click="togglePinnedExpanded"
>
@ -59,7 +59,7 @@
{{
pinnedExpanded
? '折叠置顶聊天'
: `${foldablePinnedConversations.length} 个置顶聊天`
: `${pinnedGroups.foldable.length} 个置顶聊天`
}}
</span>
<Icon
@ -73,7 +73,7 @@
<!-- 普通会话 -->
<ConversationItem
v-for="conversation in normalConversations"
:key="`${conversation.type}-${conversation.targetId}`"
:key="getConversationKey(conversation)"
:conversation="conversation"
/>
@ -107,6 +107,7 @@ import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore'
import { StorageKeys } from '../../../utils/storage'
import { ImConversationType } from '../../../utils/constants'
import { getConversationKey } from '../../../utils/conversation'
import { CommonStatusEnum } from '@/utils/constants'
import type { Conversation, Friend, FriendLite } from '../../types'
import ResizableAside from '../../components/ResizableAside.vue'
@ -160,24 +161,29 @@ const pinnedConversations = computed(() => filteredConversations.value.filter((c
/** 非置顶会话:折叠态下始终铺开在折叠头之下 */
const normalConversations = computed(() => filteredConversations.value.filter((c) => !c.top))
/** 置顶 + 无未读 / 免打扰且非激活:折叠时藏在折叠头之下,决定折叠头计数 + 是否要显示折叠头 */
const foldablePinnedConversations = computed(() =>
pinnedConversations.value.filter((c) => !isActiveConversation(c) && !hasUnreadBadge(c))
)
/**
* 折叠时只渲未读 + 当前激活穿透折叠展开时渲全部置顶
* 置顶分两堆visible折叠头之上 = 未读 + 当前激活/ foldable折叠头之下一次 partition 完成
*
* 展开后不沿用visible 在前 + foldable 在后的分组会让点击折叠区某条跨组上跳
* 上一条激活的会从 visible 掉到 foldable视觉上像"互换位置" lastSendTime 自然顺序铺最稳
* 当前激活会话也"钉" visible避免点开未读置顶 立刻被读 列表一闪重排回折叠的体验
*/
const renderedPinnedConversations = computed(() => {
if (pinnedExpanded.value) {
return pinnedConversations.value
const pinnedGroups = computed(() => {
const visible: Conversation[] = []
const foldable: Conversation[] = []
for (const conversation of pinnedConversations.value) {
if (isActiveConversation(conversation) || hasUnreadBadge(conversation)) {
visible.push(conversation)
} else {
foldable.push(conversation)
}
}
return pinnedConversations.value.filter((c) => isActiveConversation(c) || hasUnreadBadge(c))
return { visible, foldable }
})
/** 折叠时只渲 visible未读 / 激活穿透);展开时渲全部 —— 展开后不分组,避免点击折叠区跨组上跳 */
const renderedPinnedConversations = computed(() =>
pinnedExpanded.value ? pinnedConversations.value : pinnedGroups.value.visible
)
/** 与会话项右上角红点的可见条件保持一致:免打扰不亮,无未读不亮 */
function hasUnreadBadge(conversation: Conversation): boolean {
return !conversation.muted && (conversation.unreadCount || 0) > 0
@ -186,7 +192,7 @@ function hasUnreadBadge(conversation: Conversation): boolean {
/** 是否为当前激活会话 */
function isActiveConversation(conversation: Conversation): boolean {
const active = conversationStore.activeConversation
return !!active && active.type === conversation.type && active.targetId === conversation.targetId
return !!active && getConversationKey(active) === getConversationKey(conversation)
}
/**

View File

@ -11,6 +11,11 @@ import { parseMessage, resolveTipText, type TextMessage } from './message'
import { getSenderDisplayName } from './user'
import type { Message } from '../home/types'
/** 会话主键:`type-targetId` 拼成稳定字符串,给 v-for :key、active 比对、map key 等场景共用 */
export function getConversationKey(conversation: { type: number; targetId: number }): string {
return `${conversation.type}-${conversation.targetId}`
}
/** 撤回提示文案:自己撤回固定文案,对方撤回带 sender 名(实时算 + fallbackName 兜底) */
export function buildRecallTip(
senderId: number,

View File

@ -54,7 +54,9 @@ export const StorageKeys = {
`groupMembers:${userId}:${groupId}`,
/** 侧边栏宽度localStorage三个 Tab 共用一份记忆,对齐微信(拖一次到处一致)。 */
asideWidth: 'im:aside'
asideWidth: 'im:aside',
/** 会话列表置顶折叠展开态localStorage轻量 UI 偏好。 */
conversationPinnedExpanded: 'im:conversation:pinnedExpanded'
} as const
/** 取当前登录用户编号;返回 0 表示未登录,调用方一律早 return 不写无主 key */