diff --git a/src/views/im/home/components/group/GroupInfoCard.vue b/src/views/im/home/components/group/GroupInfoCard.vue index c91c19c8a..5d12afdee 100644 --- a/src/views/im/home/components/group/GroupInfoCard.vue +++ b/src/views/im/home/components/group/GroupInfoCard.vue @@ -29,6 +29,7 @@ import { useConversationStore } from '../../store/conversationStore' import { useGroupStore } from '../../store/groupStore' import { applyJoinGroup } from '@/api/im/group/request' import { ImConversationType, ImGroupAddSource } from '../../../utils/constants' +import { getGroupDisplayName } from '../../../utils/user' import type { GroupLite } from '../../types' defineOptions({ name: 'ImGroupInfoCard' }) @@ -58,13 +59,20 @@ onUnmounted(() => window.removeEventListener('keydown', handleKeydown)) /** 进入群聊:取本地最新群信息(含 silent / 群备注),新建或激活会话 + 跳路由 */ function handleChat(group: GroupLite) { const cached = groupStore.getGroup(group.id) + // cached 命中走 getGroupDisplayName 让群备注优先(与 contact / 会话列表的展示名一致);缺 cached 时回落 showGroupName / 原群名 + const displayName = cached + ? getGroupDisplayName(cached) + : group.showGroupName || group.name || '' + // 打开或新建会话 conversationStore.openConversation( group.id, ImConversationType.GROUP, - cached?.name || group.name || '', + displayName, cached?.avatar || group.showImage || '', { silent: !!cached?.silent } ) + + // 如果不在会话页,先跳过去(如果在了,MessagePanel 会自己感知会话变化刷新) if (router.currentRoute.value.name !== 'ImHomeConversation') { router.push({ name: 'ImHomeConversation' }) } diff --git a/src/views/im/home/components/picker/ConversationPickerPanel.vue b/src/views/im/home/components/picker/ConversationPickerPanel.vue index 7eeac432a..86da0cb06 100644 --- a/src/views/im/home/components/picker/ConversationPickerPanel.vue +++ b/src/views/im/home/components/picker/ConversationPickerPanel.vue @@ -310,11 +310,14 @@ const showRecentSection = computed( () => !keyword.value.trim() && recentForwardConversations.value.length > 0 ) -/** 已选会话列表:按 selectedKeys 数组顺序(即点击顺序)反查 */ +/** 已选会话列表:按 selectedKeys 数组顺序(即点击顺序)反查;过滤 hideSet 避免父组件动态隐藏的会话仍在右侧渲染 / 提交 */ const selectedConversations = computed(() => props.selectedKeys .map((key) => byKey.value.get(key)) - .filter((c): c is Conversation => c != null) + .filter( + (conversation): conversation is Conversation => + conversation != null && !hideSet.value.has(getConversationKey(conversation)) + ) ) /** 右栏标题文案:单选「发送给」、多选「分别发送给」 */ @@ -341,6 +344,10 @@ function handleToggle(conversation: Conversation) { if (index >= 0) { next.splice(index, 1) } else { + // 父组件标记隐藏的会话即便有路径触达也不应入选 + if (hideSet.value.has(key)) { + return + } if (props.maxSize > 0 && next.length >= props.maxSize) { message.error(`最多选择 ${props.maxSize} 个会话`) return diff --git a/src/views/im/home/composables/useMuteOverlay.ts b/src/views/im/home/composables/useMuteOverlay.ts index bf1c468ae..39a2c4b16 100644 --- a/src/views/im/home/composables/useMuteOverlay.ts +++ b/src/views/im/home/composables/useMuteOverlay.ts @@ -1,4 +1,4 @@ -import { computed, type ComputedRef } from 'vue' +import { computed, onScopeDispose, ref, type ComputedRef } from 'vue' import { useUserStore } from '@/store/modules/user' import { useConversationStore } from '../store/conversationStore' @@ -7,6 +7,35 @@ import { ImConversationType, ImGroupMemberRole } from '../../utils/constants' export type MuteOverlayInfo = { text: string; icon: string } +/** + * 模块级共享 now tick + 订阅计数: + * MessageItem v-for 时每条消息都会 useMuteOverlay(),单实例自起 timer 会变成几百个 30s 定时器; + * 改成模块级共享后所有订阅者共用一份 setInterval,订阅数清零时也清掉 timer,避免内存与时钟漂移 + */ +const sharedNow = ref(Date.now()) +let sharedTickTimer: ReturnType | null = null +let subscriberCount = 0 + +function subscribeNowTick(): void { + subscriberCount++ + if (!sharedTickTimer) { + sharedTickTimer = window.setInterval(() => { + sharedNow.value = Date.now() + }, 30_000) + } +} + +function unsubscribeNowTick(): void { + subscriberCount-- + if (subscriberCount <= 0) { + subscriberCount = 0 + if (sharedTickTimer) { + window.clearInterval(sharedTickTimer) + sharedTickTimer = null + } + } +} + /** * 当前激活会话的禁言 / 封禁状态;非群聊或无禁言时返回 null * @@ -19,6 +48,11 @@ export function useMuteOverlay(): ComputedRef { const conversationStore = useConversationStore() const groupStore = useGroupStore() const userStore = useUserStore() + + // 订阅模块级 tick;scope 销毁时反订阅,最后一个订阅者退场后 timer 也跟着清 + subscribeNowTick() + onScopeDispose(unsubscribeNowTick) + return computed(() => { const conversation = conversationStore.activeConversation if (!conversation || conversation.type !== ImConversationType.GROUP) { @@ -43,11 +77,11 @@ export function useMuteOverlay(): ComputedRef { return { text: '全群禁言中,暂时无法发送消息', icon: 'ant-design:audio-muted-outlined' } } } - // 成员禁言:muteEndTime 在未来才算 + // 成员禁言:muteEndTime 在未来才算;用响应式 sharedNow 比对,到期后下一个 tick 就让 overlay 消失 const myMember = group.members?.find((m) => m.userId === myId) if (myMember?.muteEndTime) { const endTime = new Date(myMember.muteEndTime) - if (endTime > new Date()) { + if (endTime.getTime() > sharedNow.value) { const pad = (n: number) => n.toString().padStart(2, '0') const timeStr = `${pad(endTime.getMonth() + 1)}-${pad(endTime.getDate())} ` + diff --git a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue index 93d1bfb1b..af7c85953 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue @@ -543,9 +543,16 @@ async function loadEarlier() { if (loadingMore.value || !hasMore.value || !conversation.value) { return } + // 仅 PRIVATE / GROUP 走分页接口;CHANNEL 单向广播、没有 list 接口,落到 else 会误调私聊接口(receiverId 传 channelId) + const requestedType = conversation.value.type + if ( + requestedType !== ImConversationType.PRIVATE && + requestedType !== ImConversationType.GROUP + ) { + return + } // 快照当前会话主键:await 期间用户切走 / 关闭面板时丢弃响应,避免旧会话历史被 prepend 到新会话造成串号 const requestedKey = getConversationKey(conversation.value) - const requestedType = conversation.value.type const requestedTargetId = conversation.value.targetId const requestedIsGroup = requestedType === ImConversationType.GROUP @@ -609,6 +616,10 @@ function onDialogOpen() { memberPopoverVisible.value = false memberSearchKeyword.value = '' datePickerValue.value = new Date() + // 本地无消息时立即拉一次(maxId=undefined 从最新开始),避免新设备 / 缓存清后弹窗只显示"暂无消息" + if (allMessages.value.length === 0 && hasMore.value && conversation.value) { + void loadEarlier() + } } /** 抽屉关闭时复位 + 停语音 */ diff --git a/src/views/im/home/pages/conversation/components/message/MessagePanel.vue b/src/views/im/home/pages/conversation/components/message/MessagePanel.vue index fdb4b80a6..15108cfb5 100644 --- a/src/views/im/home/pages/conversation/components/message/MessagePanel.vue +++ b/src/views/im/home/pages/conversation/components/message/MessagePanel.vue @@ -77,8 +77,12 @@ @click="handleGroupCall" /> - - + + - 暂无已读 + {{ groupMembers.length === 0 ? '群成员未加载' : '暂无已读' }} @@ -45,7 +45,7 @@ v-if="unreadMembers.length === 0" class="py-5 text-12px text-center text-[var(--el-text-color-disabled)]" > - 全部已读 + {{ groupMembers.length === 0 ? '群成员未加载' : '全部已读' }}