bug修复

This commit is contained in:
2025-12-08 14:56:02 +08:00
parent 3a31ea1ba6
commit 11f2e8780e
6 changed files with 298 additions and 46 deletions

View File

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

View File

@@ -15,32 +15,49 @@
<p class="text-gray-400 text-xs">无法在此群聊中发送消息</p>
</div>
</div>
<!-- 遮罩层禁言状态 -->
<div
v-else-if="isMuted"
class="absolute inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center cursor-not-allowed"
@click.stop
@contextmenu.prevent
@mousedown.prevent
@selectstart.prevent
>
<div class="text-center px-6">
<i class="fas fa-volume-mute text-4xl text-orange-500 mb-3"></i>
<p class="text-white font-semibold text-sm mb-1">您已被禁言</p>
<p v-if="muteCountdown" class="text-orange-400 text-sm font-mono">{{ muteCountdown }}</p>
<p v-else class="text-gray-400 text-xs">无法在此群聊中发送消息</p>
</div>
</div>
<div class="flex gap-6 text-gray-400 text-lg mb-3 px-2 items-center">
<i
class="far fa-smile hover:text-primary transition-all transform hover:scale-125 hover:rotate-12"
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
:class="cannotSend ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
title="表情"
@click="!isBlocked && insertEmoji('😊')"
@click="!cannotSend && insertEmoji('😊')"
></i>
<i
class="far fa-image hover:text-green-400 transition-all transform hover:scale-125"
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!isBlocked && triggerFile(1)"
:class="cannotSend ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!cannotSend && triggerFile(1)"
title="发送图片"
></i>
<!-- 新增视频上传按钮 -->
<i
class="fas fa-video hover:text-purple-400 transition-all transform hover:scale-125"
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!isBlocked && triggerFile(3)"
:class="cannotSend ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!cannotSend && triggerFile(3)"
title="发送视频"
></i>
<i
class="fas fa-folder-plus hover:text-yellow-400 transition-all transform hover:scale-125"
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!isBlocked && triggerFile(8)"
:class="cannotSend ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!cannotSend && triggerFile(8)"
title="发送文件"
></i>
@@ -48,20 +65,20 @@
<i
v-if="isGroupChat && mentionUsers.length > 0"
class="fas fa-at hover:text-blue-400 transition-all transform hover:scale-125"
:class="isBlocked ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!isBlocked && showMentionPickerManual()"
:class="cannotSend ? 'text-gray-600 cursor-not-allowed' : 'text-gray-400 cursor-pointer'"
@click="!cannotSend && showMentionPickerManual()"
title="@群成员"
></i>
<!-- 长按录音 -->
<div
class="relative flex items-center group select-none ml-auto mr-2"
:class="isBlocked ? 'cursor-not-allowed' : 'cursor-pointer'"
@mousedown="!isBlocked && handleRecordStart()"
@touchstart.passive="!isBlocked && handleRecordStart()"
@mouseup="!isBlocked && handleRecordStop()"
@touchend.prevent="!isBlocked && handleRecordStop()"
@mouseleave="!isBlocked && handleRecordCancel()"
:class="cannotSend ? 'cursor-not-allowed' : 'cursor-pointer'"
@mousedown="!cannotSend && handleRecordStart()"
@touchstart.passive="!cannotSend && handleRecordStart()"
@mouseup="!cannotSend && handleRecordStop()"
@touchend.prevent="!cannotSend && handleRecordStop()"
@mouseleave="!cannotSend && handleRecordCancel()"
>
<div
class="w-8 h-8 rounded-full flex items-center justify-center transition-all shadow-md"
@@ -83,7 +100,7 @@
<textarea
ref="textareaRef"
:value="modelValue"
:disabled="isBlocked"
:disabled="cannotSend"
class="w-full bg-transparent border-none text-white resize-none h-10 max-h-32 py-2 px-1 text-sm focus:outline-none scrollbar-thin placeholder-gray-500 leading-6 disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="输入消息 (Enter 发送)..."
@input="handleInput"
@@ -92,7 +109,7 @@
></textarea>
<div
class="w-10 h-10 rounded-2xl flex items-center justify-center ml-2 transition-all duration-300 shadow-lg"
:class="isBlocked
:class="cannotSend
? 'bg-gray-700 text-gray-500 scale-90 rotate-90 opacity-50 cursor-not-allowed'
: (modelValue.trim() ? 'bg-primary hover:bg-indigo-500 text-white scale-100 rotate-0 cursor-pointer' : 'bg-gray-700 text-gray-500 scale-90 rotate-90 opacity-50 cursor-not-allowed')"
@click="handleSend"
@@ -142,13 +159,81 @@ const toastStore = useToastStore()
interface Props {
modelValue: string
isRecording: boolean
isBlocked?: boolean // 是否被禁言(被移除或退出群聊
isBlocked?: boolean // 是否被移除或退出群聊
isMuted?: boolean // 是否被禁言
mutedUntil?: string | null // 禁言到期时间
isGroupChat?: boolean // 是否是群聊
roomMembers?: Record<string, { name: string; avatar?: string }> // 群成员映射
}
const props = defineProps<Props>()
// 禁言倒计时
const muteCountdown = ref('')
let muteTimer: ReturnType<typeof setInterval> | 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[]]

View File

@@ -2,7 +2,7 @@
<div
class="flex items-center justify-center text-white font-bold shadow-md transition overflow-hidden"
:class="[sizeClass, roundedClass, { 'cursor-pointer hover:opacity-80': $attrs.onClick }]"
:style="isImage ? {} : { background: color || generateColor(name || avatar || '') }"
:style="customSizeStyle"
@click="$emit('click')"
>
<img
@@ -24,7 +24,7 @@ interface Props {
avatar?: string
name?: string
color?: string
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'contact'
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'contact' | number
rounded?: 'full' | 'xl' | 'lg'
}
@@ -64,16 +64,43 @@ const isImage = computed(() => {
return false
})
// 预定义尺寸映射
const sizeMap: Record<string, string> = {
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<string, string> = {}
// 背景色
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(() => {

View File

@@ -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<Record<string, string | null>>({}) // 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,
}
})

View File

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

View File

@@ -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)
})
</script>