修复了一些逻辑漏洞

This commit is contained in:
2025-12-05 16:21:27 +08:00
parent 594656289f
commit 02e885fbfe
6 changed files with 316 additions and 64 deletions

View File

@@ -34,4 +34,9 @@ export function deleteConversation(targetId: string) {
})
}
// 根据 room_id 获取或创建会话
export function getConversationByRoom(roomId: string) {
return request.get<Conversation>(`/conversations/by-room/${roomId}`)
}

View File

@@ -53,7 +53,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { ChatMessage } from '@/types/api'
import { formatTime } from '@/utils/format'
import { formatMessageTime } from '@/utils/format'
import { isSystemOrNotifyMessage, getMessageTypeIcon, getMessageSummary } from '@/utils/messageTypes'
import { MessageType } from '@/types/message'
import TextBubble from './bubble/Text.vue'
@@ -84,16 +84,8 @@ const bubbleShapeClass = computed(() => {
}
})
// 使用 format.ts 中的逻辑,但可以根据需要微调为 HH:mm
// 使用相对时间格式化
const formattedTime = computed(() => {
const date = new Date(props.message.created_at)
// 如果是当天,只显示时间,否则显示日期+时间
const now = new Date()
const isToday = date.toDateString() === now.toDateString()
if (isToday) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
return formatTime(props.message.created_at)
return formatMessageTime(props.message.created_at)
})
</script>

View File

@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
import type { Conversation } from '@/types/conversation'
import type { ChatMessage } from '@/types/api'
import * as conversationApi from '@/api/modules/conversation'
import * as groupApi from '@/api/modules/room'
import { getMessageSummary } from '@/utils/messageTypes'
import { useChatStore } from './chat'
@@ -30,13 +31,21 @@ export const useConversationStore = defineStore('conversation', () => {
// 将后端的 room 字段映射为前端的 target_group
const targetGroup = c.room || c.target_group
// 转换 last_time 为时间戳(确保排序正确)
let lastTime = c.last_time
if (typeof lastTime === 'string') {
lastTime = new Date(lastTime).getTime()
} else if (!lastTime) {
lastTime = 0
}
return {
...c,
room_type: roomType,
is_group: isGroup,
// 群聊显示群名称,单聊显示用户名称
name: isGroup
? (targetGroup?.room_name || targetGroup?.name || c.name || '未知群聊')
? (targetGroup?.room_name || targetGroup?.name || c.name || c.room_id || '群聊')
: (c.target_user?.name || c.name || '未知用户'),
avatar: isGroup
? (targetGroup?.room_avatar || targetGroup?.avatar || c.avatar || '')
@@ -45,6 +54,9 @@ export const useConversationStore = defineStore('conversation', () => {
room_id: isGroup ? (c.room_id || c.target_id) : c.room_id,
member_count: targetGroup?.member_count,
owner_id: targetGroup?.owner_id,
// 确保 last_time 是时间戳
last_time: lastTime,
last_message_time: c.last_time || c.last_message_time,
// 映射 room 为 target_group前端期望的字段名
target_group: targetGroup ? {
id: targetGroup.room_id || targetGroup.id,
@@ -57,6 +69,30 @@ export const useConversationStore = defineStore('conversation', () => {
created_at: targetGroup.created_at,
} : undefined,
}
}).sort((a, b) => {
// 确保排序:置顶优先 > 时间倒序(最新在最上面)
if (a.is_top !== b.is_top) return a.is_top ? -1 : 1
return (b.last_time || 0) - (a.last_time || 0)
})
// 对于群聊信息缺失的会话,尝试异步获取群信息
conversations.value.forEach(async (conv) => {
if (conv.is_group && conv.room_id && !conv.target_group && !conv.name) {
try {
const groupInfo = await groupApi.getGroup(conv.room_id)
// 更新会话的群信息
const index = conversations.value.findIndex(c => c.id === conv.id)
if (index !== -1) {
conversations.value[index].target_group = groupInfo as any
conversations.value[index].name = groupInfo.room_name || groupInfo.name || conv.room_id
conversations.value[index].avatar = groupInfo.room_avatar || groupInfo.avatar
conversations.value[index].member_count = groupInfo.member_count
conversations.value[index].owner_id = groupInfo.owner_id
}
} catch (error) {
console.error(`Failed to fetch group info for room ${conv.room_id}:`, error)
}
}
})
} catch (error) {
console.error('Fetch conversations failed:', error)
@@ -117,6 +153,7 @@ export const useConversationStore = defineStore('conversation', () => {
return
}
// 更新后重新排序,确保最新消息在最上面
sortConversations()
}
@@ -177,6 +214,7 @@ export const useConversationStore = defineStore('conversation', () => {
loadConversations,
handleMessageUpdate,
incrementUnread,
clearUnread
clearUnread,
sortConversations
}
})

View File

@@ -3,7 +3,8 @@
*/
/**
* 格式化时间
* 格式化时间(相对时间)
* 显示刚刚、X分钟前、X小时前、昨天、X天前、具体日期
*/
export function formatTime(timestamp: number | string): string {
const date = new Date(typeof timestamp === 'string' ? timestamp : timestamp)
@@ -14,14 +15,104 @@ export function formatTime(timestamp: number | string): string {
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
if (days > 0) {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
} else if (hours > 0) {
return `${hours}小时前`
} else if (minutes > 0) {
// 刚刚1分钟内
if (seconds < 60) {
return '刚刚'
}
// X分钟前1小时内
if (minutes < 60) {
return `${minutes}分钟前`
}
// X小时前24小时内
if (hours < 24) {
return `${hours}小时前`
}
// 昨天
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
if (date.toDateString() === yesterday.toDateString()) {
return '昨天'
}
// X天前7天内
if (days < 7) {
return `${days}天前`
}
// 超过7天显示具体日期
const currentYear = now.getFullYear()
const messageYear = date.getFullYear()
if (currentYear === messageYear) {
// 今年:显示月-日
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
} else {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
// 去年或更早:显示年-月-日
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric' })
}
}
/**
* 格式化消息时间(用于聊天记录)
* 显示刚刚、X分钟前、X小时前、昨天 HH:mm、MM-DD HH:mm、YYYY-MM-DD HH:mm
*/
export function formatMessageTime(timestamp: number | string): string {
const date = new Date(typeof timestamp === 'string' ? timestamp : timestamp)
const now = new Date()
const diff = now.getTime() - date.getTime()
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
// 刚刚1分钟内
if (seconds < 60) {
return '刚刚'
}
// X分钟前1小时内
if (minutes < 60) {
return `${minutes}分钟前`
}
// X小时前24小时内
if (hours < 24) {
return `${hours}小时前`
}
// 昨天 HH:mm
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
if (date.toDateString() === yesterday.toDateString()) {
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
}
// 本周内:显示星期几 HH:mm
const weekAgo = new Date(now)
weekAgo.setDate(weekAgo.getDate() - 7)
if (date > weekAgo) {
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const weekday = weekdays[date.getDay()]
return `${weekday} ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
}
// 今年MM-DD HH:mm
const currentYear = now.getFullYear()
const messageYear = date.getFullYear()
if (currentYear === messageYear) {
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${month}-${day} ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
} else {
// 去年或更早YYYY-MM-DD HH:mm
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}-${month}-${day} ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
}
}

View File

@@ -137,20 +137,22 @@
<!-- 优化后的未读徽标 -->
<div
v-if="(conv.unread_count || 0) > 0"
class="h-[18px] min-w-[18px] px-1.5 rounded-full flex items-center justify-center font-bold shadow-sm cursor-pointer transition-all duration-300 shrink-0 group/badge hover:scale-110"
class="h-[18px] min-w-[18px] px-1.5 rounded-full flex items-center justify-center font-bold shadow-sm cursor-pointer transition-all duration-300 shrink-0 hover:scale-110"
:class="[
'bg-red-500 text-white hover:bg-emerald-500' // 默认红,悬浮绿
]"
title="标记已读"
@click.stop="handleMarkRead(conv)"
@mouseenter="hoveredBadgeId = conv.id"
@mouseleave="hoveredBadgeId = null"
>
<!-- 默认显示数字 -->
<span class="text-[10px] block group-hover/badge:hidden transition-all duration-200">
<span v-if="hoveredBadgeId !== conv.id" class="text-[10px]">
{{ conv.unread_count > 99 ? '99+' : conv.unread_count }}
</span>
<!-- 悬浮显示对号 -->
<i class="fas fa-check text-[10px] hidden group-hover/badge:block animate-pulse-fast"></i>
<i v-if="hoveredBadgeId === conv.id" class="fas fa-check text-[10px]"></i>
</div>
</div>
</div>
@@ -295,6 +297,15 @@
</div>
<ContextMenu />
<FileConfirmModal :show="fileModal.show" :type="fileModal.type" :preview="fileModal.preview" :name="fileModal.name" :size="fileModal.size" @close="fileModal.show = false" @confirm="confirmSendFile" />
<ConfirmModal
:show="showDeleteConversationConfirm"
title="删除会话"
message="确认删除该会话记录吗?"
type="danger"
confirm-text="确认删除"
@confirm="handleDeleteConversation"
@cancel="showDeleteConversationConfirm = false; pendingDeleteConversation = null"
/>
<!-- 创建群聊弹窗 -->
<SelectContactsModal
@@ -408,6 +419,7 @@ import { wsManager } from '@/api/websocket'
import * as messageApi from '@/api/modules/message'
import * as contactApi from '@/api/modules/contact'
import * as attachmentApi from '@/api/modules/attachment'
import * as conversationApi from '@/api/modules/conversation'
import { formatTime, generateColor } from '@/utils/format'
import { getMessageSummary } from '@/utils/messageTypes'
import { storage } from '@/utils/storage'
@@ -418,6 +430,7 @@ import { useWebRTCStore } from '@/stores/webrtc'
import Avatar from '@/components/common/Avatar.vue'
import ContextMenu from '@/components/common/ContextMenu.vue'
import FileConfirmModal from '@/components/common/FileConfirmModal.vue'
import ConfirmModal from '@/components/common/ConfirmModal.vue'
import MessageList from '@/components/chat/MessageList.vue'
import MessageInput from '@/components/chat/MessageInput.vue'
import ContactView from '@/views/contact/ContactView.vue'
@@ -451,7 +464,10 @@ const selectedUser = ref<Contact | User | null>(null)
const isRecording = ref(false)
const fileModal = ref({ show: false, type: 0, preview: '', name: '', size: 0, file: null as File | null })
const showCreateGroupModal = ref(false)
const showDeleteConversationConfirm = ref(false)
const pendingDeleteConversation = ref<Conversation | null>(null)
const showGroupInfoPanel = ref(false)
const hoveredBadgeId = ref<number | null>(null)
const refreshingConversations = ref(false)
const roomMembers = ref<Record<string, Record<string, { name: string; avatar?: string }>>>({})
@@ -827,8 +843,113 @@ function handleWebSocketMessage(message: ChatMessage) {
return
}
chatStore.addMessage(roomId, message)
// 先检查会话是否存在,确保消息添加到正确的会话
// 对于群聊消息,使用 room_id 查找;对于私聊消息,使用 target_id 查找
const isGroupMessage = message.room_id && (message.room_id.startsWith('group_') || message.receiver_user_id === message.room_id)
let existingConv = null
let actualRoomId = roomId
if (isGroupMessage) {
// 群聊消息:使用 room_id 查找会话
existingConv = conversationStore.conversations.find(c => c.room_id === roomId)
} else {
// 私聊消息:使用 target_id 查找会话
const targetId = message.isSelf ? message.receiver_user_id : message.sender_user_id
existingConv = conversationStore.conversations.find(c => c.target_id === targetId || c.room_id === roomId)
// 如果找到会话,使用会话的 room_id 确保匹配
if (existingConv && existingConv.room_id) {
actualRoomId = existingConv.room_id
}
}
// 如果会话不存在,先获取或创建会话,然后再添加消息
if (!existingConv && !message.isSelf && roomId) {
const now = Date.now()
const lastRefresh = (window as any).__lastConversationRefresh || 0
// 防抖1秒内最多请求一次
if (now - lastRefresh > 1000) {
(window as any).__lastConversationRefresh = now
// 调用新接口获取会话信息,只添加新会话到列表
conversationApi.getConversationByRoom(roomId).then((conv: any) => {
// 检查会话是否已存在(可能在其他地方已添加)
const exists = conversationStore.conversations.find(c =>
c.room_id === conv.room_id ||
(c.target_id === conv.target_id && c.type === conv.type)
)
if (!exists) {
// 转换格式并添加到列表(使用与 loadConversations 相同的格式)
const isGroup = conv.type === 2
// 获取群信息(与 loadConversations 保持一致)
const targetGroup = conv.room || conv.target_group
const formattedConv: Conversation = {
id: conv.id,
target_id: conv.target_id,
room_id: conv.room_id,
type: conv.type,
is_group: isGroup,
is_top: conv.is_top || false,
is_muted: conv.is_muted || false,
unread_count: conv.unread_count || 0,
last_message: conv.last_message || '',
last_message_time: conv.last_time,
// 使用与 loadConversations 相同的逻辑获取名称
name: isGroup
? (targetGroup?.room_name || targetGroup?.name || conv.name || conv.room_id || '群聊')
: (conv.target_user?.name || conv.name || '未知用户'),
display_name: isGroup
? (targetGroup?.room_name || targetGroup?.name || conv.name || conv.room_id || '群聊')
: (conv.target_user?.name || conv.name || '未知用户'),
avatar: isGroup
? (targetGroup?.room_avatar || targetGroup?.avatar || conv.avatar || '')
: (conv.target_user?.avatar || conv.avatar || ''),
user: conv.target_user,
room: conv.room,
target_group: targetGroup ? {
id: targetGroup.room_id || targetGroup.id,
room_id: targetGroup.room_id,
room_type: 'group',
name: targetGroup.room_name || targetGroup.name,
avatar: targetGroup.room_avatar || targetGroup.avatar,
owner_id: targetGroup.owner_id,
member_count: targetGroup.member_count,
created_at: targetGroup.created_at,
} : undefined
}
conversationStore.conversations.push(formattedConv)
// 添加新会话后重新排序
conversationStore.sortConversations()
}
// 会话创建后,使用正确的 room_id 添加消息
const finalRoomId = conv.room_id || roomId
chatStore.addMessage(finalRoomId, message)
processMessageAfterConversation(message, finalRoomId)
}).catch((error) => {
console.error('Failed to get conversation by room:', error)
// 如果接口失败,仍然添加消息(使用原始 roomId然后刷新列表
chatStore.addMessage(roomId, message)
processMessageAfterConversation(message, roomId)
conversationStore.loadConversations()
})
return // 异步处理,先返回
} else {
// 防抖期间,仍然添加消息(使用原始 roomId
actualRoomId = roomId
}
} else if (existingConv) {
// 会话存在,使用会话的 room_id 确保匹配
actualRoomId = existingConv.room_id || roomId
}
// 添加消息到正确的会话(使用确认后的 roomId
chatStore.addMessage(actualRoomId, message)
// 处理消息的后续逻辑
processMessageAfterConversation(message, actualRoomId)
}
// 处理消息的后续逻辑(会话确认后)
function processMessageAfterConversation(message: ChatMessage, roomId: string) {
const contact = chatStore.contacts.find(c => c.user_id === message.sender_user_id || c.id === message.sender_user_id)
// 检查是否为群聊消息,如果是且当前正在查看该群聊,检查群成员信息
@@ -845,31 +966,6 @@ function handleWebSocketMessage(message: ChatMessage) {
}
}
// 检查会话是否存在
// 对于群聊消息,使用 room_id 查找;对于私聊消息,使用 target_id 查找
const isGroupMessage = message.room_id && (message.room_id.startsWith('group_') || message.receiver_user_id === message.room_id)
let existingConv = null
if (isGroupMessage) {
// 群聊消息:使用 room_id 查找会话
existingConv = conversationStore.conversations.find(c => c.room_id === roomId)
} else {
// 私聊消息:使用 target_id 查找会话
const targetId = message.isSelf ? message.receiver_user_id : message.sender_user_id
existingConv = conversationStore.conversations.find(c => c.target_id === targetId || c.room_id === roomId)
}
// 如果会话不存在,刷新会话列表(添加防抖,避免频繁刷新)
if (!existingConv && !message.isSelf) {
const now = Date.now()
const lastRefresh = (window as any).__lastConversationRefresh || 0
// 防抖1秒内最多刷新一次
if (now - lastRefresh > 1000) {
(window as any).__lastConversationRefresh = now
conversationStore.loadConversations()
}
}
const isCurrentChat = currentRoomId === roomId
if (contact) {
@@ -998,10 +1094,8 @@ function showConversationMenu(event: MouseEvent, conv: any) {
icon: 'fas fa-trash-alt',
danger: true,
action: () => {
if(confirm('确认删除该会话记录吗?')) {
conversationStore.conversations = conversationStore.conversations.filter(c => c.id !== conv.id)
toastStore.success('会话已移除')
}
pendingDeleteConversation.value = conv
showDeleteConversationConfirm.value = true
}
}
])
@@ -1012,6 +1106,15 @@ async function handleMarkRead(conv: Conversation) {
toastStore.success('已标记为已读')
}
function handleDeleteConversation() {
if (!pendingDeleteConversation.value) return
const conv = pendingDeleteConversation.value
conversationStore.conversations = conversationStore.conversations.filter(c => c.id !== conv.id)
toastStore.success('会话已移除')
showDeleteConversationConfirm.value = false
pendingDeleteConversation.value = null
}
// ----------------------------------------------
function showChatOptionsMenu(event: MouseEvent, contact: Contact) {

View File

@@ -387,6 +387,15 @@
@confirm="handleDeleteGroup"
@cancel="showDeleteGroupConfirm = false"
/>
<ConfirmModal
:show="showDeleteContactConfirm"
title="删除好友"
:message="deleteContactMessage"
type="danger"
confirm-text="确认删除"
@confirm="handleDeleteContact"
@cancel="showDeleteContactConfirm = false; pendingDeleteContact = null"
/>
<!-- 创建群聊弹窗 -->
<SelectContactsModal
@@ -502,6 +511,13 @@ const groupModalName = ref('')
const currentGroupId = ref<number | null>(null)
const groupInputRef = ref<HTMLInputElement | null>(null)
const showDeleteGroupConfirm = ref(false)
const showDeleteContactConfirm = ref(false)
const pendingDeleteContact = ref<Contact | null>(null)
const deleteContactMessage = computed(() => {
const name = pendingDeleteContact.value?.remark_name || pendingDeleteContact.value?.user?.name || '未知用户'
return `确定删除好友 "${name}" 吗?`
})
const showRemarkModal = ref(false)
const tempRemark = ref('')
@@ -749,17 +765,9 @@ function showContactMenu(e: MouseEvent, contact: Contact) {
label: '删除好友',
icon: 'fas fa-trash-alt',
danger: true,
action: async () => {
try {
if(confirm(`确定删除好友 "${contact.remark_name || contact.user?.name}" 吗?`)) {
await contactApi.deleteContact(contact.id)
chatStore.contacts = chatStore.contacts.filter(c => c.id !== contact.id)
if(contactStore.selectedContact?.id === contact.id) contactStore.setSelectedContact(null)
toastStore.success('已删除')
}
} catch(e: any) {
toastStore.error(e.message)
}
action: () => {
pendingDeleteContact.value = contact
showDeleteContactConfirm.value = true
}
}
])
@@ -825,6 +833,21 @@ async function handleDeleteGroup() {
}
}
async function handleDeleteContact() {
if (!pendingDeleteContact.value) return
const contact = pendingDeleteContact.value
try {
await contactApi.deleteContact(contact.id)
chatStore.contacts = chatStore.contacts.filter(c => c.id !== contact.id)
if(contactStore.selectedContact?.id === contact.id) contactStore.setSelectedContact(null)
toastStore.success('已删除')
showDeleteContactConfirm.value = false
pendingDeleteContact.value = null
} catch(e: any) {
toastStore.error(e.message || '删除失败')
}
}
// --- 辅助功能 ---
async function submitRemark() {