fix(im): 修复 IM 前端批量 UX 状态问题

im
YunaiV 2026-05-21 13:25:55 +08:00
parent 29b257b8cd
commit 3949e0c89f
6 changed files with 75 additions and 11 deletions

View File

@ -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' })
}

View File

@ -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

View File

@ -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<typeof setInterval> | 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<MuteOverlayInfo | null> {
const conversationStore = useConversationStore()
const groupStore = useGroupStore()
const userStore = useUserStore()
// 订阅模块级 tickscope 销毁时反订阅,最后一个订阅者退场后 timer 也跟着清
subscribeNowTick()
onScopeDispose(unsubscribeNowTick)
return computed(() => {
const conversation = conversationStore.activeConversation
if (!conversation || conversation.type !== ImConversationType.GROUP) {
@ -43,11 +77,11 @@ export function useMuteOverlay(): ComputedRef<MuteOverlayInfo | null> {
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())} ` +

View File

@ -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()
}
}
/** 抽屉关闭时复位 + 停语音 */

View File

@ -77,8 +77,12 @@
@click="handleGroupCall"
/>
</el-tooltip>
<!-- 信息抽屉入口 -->
<el-tooltip :content="isGroup ? '群聊信息' : '聊天信息'" placement="bottom">
<!-- 信息抽屉入口群聊有私聊仅在对方是好友时显示非好友会话抽屉内容为空没意义 -->
<el-tooltip
v-if="isGroup || (isPrivate && !showNotFriendBanner)"
:content="isGroup ? '群聊信息' : '聊天信息'"
placement="bottom"
>
<Icon
icon="ant-design:ellipsis-outlined"
:size="20"

View File

@ -32,7 +32,7 @@
v-if="readMembers.length === 0"
class="py-5 text-12px text-center text-[var(--el-text-color-disabled)]"
>
暂无已读
{{ groupMembers.length === 0 ? '群成员未加载' : '暂无已读' }}
</div>
</el-tab-pane>
<el-tab-pane :label="`未读(${unreadMembers.length})`" name="unread">
@ -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 ? '群成员未加载' : '全部已读' }}
</div>
</el-tab-pane>
</el-tabs>