fix(im): 修复 IM 前端批量 UX 状态问题
parent
29b257b8cd
commit
3949e0c89f
|
|
@ -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' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
// 订阅模块级 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<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())} ` +
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/** 抽屉关闭时复位 + 停语音 */
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue