From 11f2e8780e9167125947be65938d99cdffc058a1 Mon Sep 17 00:00:00 2001 From: liqi Date: Mon, 8 Dec 2025 14:56:02 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat/GroupChatPanel.vue | 10 ++ src/components/chat/MessageInput.vue | 123 +++++++++++++++++++++---- src/components/common/Avatar.vue | 47 ++++++++-- src/stores/chat.ts | 40 ++++++++ src/utils/storage.ts | 2 + src/views/chat/ChatView.vue | 122 ++++++++++++++++++++---- 6 files changed, 298 insertions(+), 46 deletions(-) diff --git a/src/components/chat/GroupChatPanel.vue b/src/components/chat/GroupChatPanel.vue index 975df4b..f599372 100644 --- a/src/components/chat/GroupChatPanel.vue +++ b/src/components/chat/GroupChatPanel.vue @@ -648,6 +648,16 @@ async function saveAnnouncement() { watch(() => props.roomId, loadData, { immediate: true }) +// 监听群通知,自动刷新成员列表 +watch(() => chatStore.lastGroupNotification, (notif) => { + if (notif && notif.room_id === props.roomId) { + // 延迟刷新,确保后端数据已更新 + setTimeout(() => { + loadData() + }, 300) + } +}, { deep: true }) + onMounted(() => { if (chatStore.contacts.length === 0) { contactApi.getContacts().then(res => chatStore.setContacts(res)) diff --git a/src/components/chat/MessageInput.vue b/src/components/chat/MessageInput.vue index 5657881..2407616 100644 --- a/src/components/chat/MessageInput.vue +++ b/src/components/chat/MessageInput.vue @@ -15,32 +15,49 @@

无法在此群聊中发送消息

+ + +
+
+ +

您已被禁言

+

{{ muteCountdown }}

+

无法在此群聊中发送消息

+
+
@@ -48,20 +65,20 @@
// 群成员映射 } const props = defineProps() +// 禁言倒计时 +const muteCountdown = ref('') +let muteTimer: ReturnType | null = null + +// 计算是否可以发送消息(被移除或被禁言都不能发) +const cannotSend = computed(() => props.isBlocked || props.isMuted) + +// 更新禁言倒计时 +function updateMuteCountdown() { + if (!props.mutedUntil) { + muteCountdown.value = '' + return + } + + const now = new Date().getTime() + const end = new Date(props.mutedUntil).getTime() + const diff = end - now + + if (diff <= 0) { + muteCountdown.value = '' + if (muteTimer) { + clearInterval(muteTimer) + muteTimer = null + } + return + } + + // 计算剩余时间 + const days = Math.floor(diff / (1000 * 60 * 60 * 24)) + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) + const seconds = Math.floor((diff % (1000 * 60)) / 1000) + + if (days > 0) { + muteCountdown.value = `${days}天${hours}小时后解除` + } else if (hours > 0) { + muteCountdown.value = `${hours}小时${minutes}分钟后解除` + } else if (minutes > 0) { + muteCountdown.value = `${minutes}分${seconds}秒后解除` + } else { + muteCountdown.value = `${seconds}秒后解除` + } +} + +// 监听禁言状态变化 +watch(() => props.mutedUntil, (newVal) => { + if (muteTimer) { + clearInterval(muteTimer) + muteTimer = null + } + + if (newVal && props.isMuted) { + updateMuteCountdown() + muteTimer = setInterval(updateMuteCountdown, 1000) + } else { + muteCountdown.value = '' + } +}, { immediate: true }) + +onUnmounted(() => { + if (muteTimer) { + clearInterval(muteTimer) + muteTimer = null + } +}) + const emit = defineEmits<{ 'update:modelValue': [value: string] send: [mentionUserIds?: string[]] diff --git a/src/components/common/Avatar.vue b/src/components/common/Avatar.vue index 68866b4..bb3ba7a 100644 --- a/src/components/common/Avatar.vue +++ b/src/components/common/Avatar.vue @@ -2,7 +2,7 @@
{ return false }) +// 预定义尺寸映射 +const sizeMap: Record = { + xs: 'w-7 h-7 text-[10px]', + sm: 'w-9 h-9 text-xs', + md: 'w-10 h-10 text-sm', + lg: 'w-12 h-12 text-base', + xl: 'w-16 h-16 text-xl', + '2xl': 'w-24 h-24 text-2xl', + contact: 'w-11 h-11 text-lg', +} + +// 尺寸 class(仅用于预定义尺寸) const sizeClass = computed(() => { - const sizes = { - sm: 'w-9 h-9 text-xs', - md: 'w-10 h-10 text-sm', - lg: 'w-12 h-12 text-base', - xl: 'w-16 h-16 text-xl', - '2xl': 'w-24 h-24 text-2xl', - contact: 'w-11 h-11 text-lg', + if (typeof props.size === 'number') { + return '' // 数字尺寸使用 style } - return sizes[props.size] + return sizeMap[props.size] || sizeMap.md +}) + +// 自定义尺寸样式(用于数字尺寸和背景色) +const customSizeStyle = computed(() => { + const style: Record = {} + + // 背景色 + if (!isImage.value) { + style.background = props.color || generateColor(props.name || props.avatar || '') + } + + // 数字尺寸 + if (typeof props.size === 'number') { + style.width = `${props.size}px` + style.height = `${props.size}px` + // 根据尺寸计算字体大小 + style.fontSize = `${Math.max(10, props.size * 0.4)}px` + } + + return style }) const roundedClass = computed(() => { diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 3a51e5c..5fae0c5 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -100,11 +100,47 @@ export const useChatStore = defineStore('chat', () => { messages.value[roomId] = [] } + // 群通知相关状态 + const lastGroupNotification = ref<{ room_id: string; type: string; data: any } | null>(null) + const myMuteStatus = ref>({}) // room_id -> muted_until + + /** + * 设置最后的群通知(用于触发群面板刷新) + */ + function setLastGroupNotification(notification: { room_id: string; type: string; data: any }) { + lastGroupNotification.value = { ...notification, _timestamp: Date.now() } as any + } + + /** + * 设置我在某个群的禁言状态 + */ + function setMyMuteStatus(roomId: string, mutedUntil: string | null) { + myMuteStatus.value[roomId] = mutedUntil + } + + /** + * 获取我在某个群是否被禁言 + */ + function isMyMuted(roomId: string): boolean { + const mutedUntil = myMuteStatus.value[roomId] + if (!mutedUntil) return false + return new Date(mutedUntil) > new Date() + } + + /** + * 获取我的禁言到期时间 + */ + function getMyMutedUntil(roomId: string): string | null { + return myMuteStatus.value[roomId] || null + } + return { currentTarget, messages, contacts, totalUnread, + lastGroupNotification, + myMuteStatus, setCurrentTarget, addMessage, getRoomMessages, @@ -113,6 +149,10 @@ export const useChatStore = defineStore('chat', () => { updateContactLastMsg, incrementUnread, clearRoomMessages, + setLastGroupNotification, + setMyMuteStatus, + isMyMuted, + getMyMutedUntil, } }) diff --git a/src/utils/storage.ts b/src/utils/storage.ts index ecb6afb..b41e182 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -65,6 +65,8 @@ export const storage = { // Current Tab (chat/contact/moment) setCurrentTab(tab: 'chat' | 'contact' | 'moment') { localStorage.setItem(CURRENT_TAB_KEY, tab) + // 触发自定义事件通知 UI 更新 + window.dispatchEvent(new CustomEvent('app-tab-change', { detail: { tab } })) }, getCurrentTab(): 'chat' | 'contact' | 'moment' { diff --git a/src/views/chat/ChatView.vue b/src/views/chat/ChatView.vue index 2f8ef68..caa4dec 100644 --- a/src/views/chat/ChatView.vue +++ b/src/views/chat/ChatView.vue @@ -242,6 +242,8 @@ v-model="inputText" :is-recording="isRecording" :is-blocked="isBlockedFromGroup" + :is-muted="isMyMutedInCurrentGroup" + :muted-until="myMutedUntilInCurrentGroup" :is-group-chat="isGroupChat" :room-members="currentRoomMembers" @send="handleSendText" @@ -773,16 +775,32 @@ const currentRoomMembers = computed(() => { return roomMembers.value[chatStore.currentTarget.room_id] || {} }) -// 判断当前用户是否被禁言(被移除或退出群聊) +// 判断当前用户是否被移除(不在群成员列表中) const isBlockedFromGroup = computed(() => { if (!isGroupChat.value || !chatStore.currentTarget?.room_id || !authStore.user) { return false } const currentMember = roomMembers.value[chatStore.currentTarget.room_id]?.[authStore.user.id] - // 如果当前用户不在群成员列表中,则认为被禁言 + // 如果当前用户不在群成员列表中,则认为被移除 return !currentMember }) +// 判断当前用户是否在当前群中被禁言 +const isMyMutedInCurrentGroup = computed(() => { + if (!isGroupChat.value || !chatStore.currentTarget?.room_id) { + return false + } + return chatStore.isMyMuted(chatStore.currentTarget.room_id) +}) + +// 获取当前用户在当前群的禁言到期时间 +const myMutedUntilInCurrentGroup = computed(() => { + if (!isGroupChat.value || !chatStore.currentTarget?.room_id) { + return null + } + return chatStore.getMyMutedUntil(chatStore.currentTarget.room_id) +}) + // 获取当前目标名称 function getCurrentTargetName(): string { if (!chatStore.currentTarget) return '' @@ -1099,9 +1117,34 @@ function handleWebSocketMessage(message: ChatMessage) { message.isSelf = message.sender_user_id === authStore.user!.id message.extra = typeof message.extra === 'string' ? JSON.parse(message.extra || '{}') : message.extra - // 处理好友申请通知 + // 处理好友通知 if (message.message_type === 5) { const extra = message.extra as any + + // 处理好友资料更新通知 + if (extra.type === 'profile_update') { + // 更新联系人列表中的用户信息 + const contact = chatStore.contacts.find(c => c.user_id === extra.user_id || c.id === extra.user_id) + if (contact && contact.user) { + if (extra.name) contact.user.name = extra.name + if (extra.avatar) contact.user.avatar = extra.avatar + } + + // 更新当前聊天对象(如果正在和这个人聊天) + const currentTarget = chatStore.currentTarget + if (currentTarget && (currentTarget.user_id === extra.user_id || currentTarget.id === extra.user_id)) { + if (currentTarget.user) { + if (extra.name) currentTarget.user.name = extra.name + if (extra.avatar) currentTarget.user.avatar = extra.avatar + } + } + + // 刷新会话列表 + conversationStore.loadConversations() + return + } + + // 处理好友申请通知 toastStore.showToast( '收到好友申请', 'info', @@ -1120,20 +1163,52 @@ function handleWebSocketMessage(message: ChatMessage) { // 处理群聊通知 if (message.message_type === 7) { - const extra = message.extra as any - toastStore.showToast( - '群聊通知', - 'info', - 5000, - extra.title || message.content || '收到群聊通知', - { - label: '去查看', - handler: () => { - currentTab.value = 'contact' - contactStore.setLeftPanelMode('group-notify') + const extra = typeof message.extra === 'string' ? JSON.parse(message.extra || '{}') : (message.extra || {}) + const notifRoomId = extra.room_id || message.room_id + + // 根据通知类型进行不同处理 + switch (extra.type) { + case 'member_mute': + case 'member_unmute': + // 禁言/解除禁言通知 - 保存到 chatStore 以便 MessageInput 可以响应 + if (extra.target === authStore.user?.id) { + // 自己被禁言/解除禁言 + chatStore.setMyMuteStatus(notifRoomId, extra.type === 'member_mute' ? extra.muted_until : null) } - } - ) + // 触发群成员刷新 + chatStore.setLastGroupNotification({ room_id: notifRoomId, type: extra.type, data: extra }) + break + + case 'role_change': + // 角色变更通知 - 触发群成员刷新 + chatStore.setLastGroupNotification({ room_id: notifRoomId, type: extra.type, data: extra }) + break + + case 'member_invite': + case 'member_remove': + case 'member_leave': + case 'member_join': + // 成员变动通知 - 触发群成员刷新 + chatStore.setLastGroupNotification({ room_id: notifRoomId, type: extra.type, data: extra }) + break + } + + // 显示 Toast 通知(不是自己被禁言时) + if (!(extra.type === 'member_mute' && extra.target === authStore.user?.id)) { + toastStore.showToast( + '群聊通知', + 'info', + 5000, + message.content || extra.title || '收到群聊通知', + { + label: '去查看', + handler: () => { + currentTab.value = 'contact' + contactStore.setLeftPanelMode('group-notify') + } + } + ) + } return } @@ -2020,7 +2095,7 @@ onMounted(async () => { await wsManager.connect(authStore.user.id) wsManager.onMessage(handleWebSocketMessage) wsManager.onSignal(webrtc.handleSignaling) - wsManager.onMomentNotification(momentStore.handleWsNotification) + // 朋友圈通知已在 App.vue 中统一注册,避免重复 } await loadContacts() await conversationStore.loadConversations() @@ -2049,11 +2124,24 @@ onMounted(async () => { } window.addEventListener('resize', () => isMobile.value = window.innerWidth < 768) + + // 监听 tab 切换事件(用于从其他地方触发 tab 切换,如朋友圈通知) + window.addEventListener('app-tab-change', handleTabChangeEvent) }) + +// Tab 切换事件处理 +function handleTabChangeEvent(e: Event) { + const customEvent = e as CustomEvent<{ tab: 'chat' | 'contact' | 'moment' }> + if (customEvent.detail?.tab) { + currentTab.value = customEvent.detail.tab + } +} + onUnmounted(() => { wsManager.offMessage(handleWebSocketMessage) wsManager.offSignal(webrtc.handleSignaling) wsManager.offMomentNotification(momentStore.handleWsNotification) + window.removeEventListener('app-tab-change', handleTabChangeEvent) })