From 02e885fbfeabea38a5e30d0694cc007de475bb07 Mon Sep 17 00:00:00 2001 From: liqi Date: Fri, 5 Dec 2025 16:21:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/modules/conversation.ts | 5 + src/components/chat/MessageBubble.vue | 14 +-- src/stores/conversation.ts | 42 ++++++- src/utils/format.ts | 105 ++++++++++++++-- src/views/chat/ChatView.vue | 169 +++++++++++++++++++++----- src/views/contact/ContactCircle.vue | 45 +++++-- 6 files changed, 316 insertions(+), 64 deletions(-) diff --git a/src/api/modules/conversation.ts b/src/api/modules/conversation.ts index d86f4bc..4164a41 100644 --- a/src/api/modules/conversation.ts +++ b/src/api/modules/conversation.ts @@ -34,4 +34,9 @@ export function deleteConversation(targetId: string) { }) } +// 根据 room_id 获取或创建会话 +export function getConversationByRoom(roomId: string) { + return request.get(`/conversations/by-room/${roomId}`) +} + diff --git a/src/components/chat/MessageBubble.vue b/src/components/chat/MessageBubble.vue index eaf1208..dfa743a 100644 --- a/src/components/chat/MessageBubble.vue +++ b/src/components/chat/MessageBubble.vue @@ -53,7 +53,7 @@ diff --git a/src/stores/conversation.ts b/src/stores/conversation.ts index d87c8ea..6f105a1 100644 --- a/src/stores/conversation.ts +++ b/src/stores/conversation.ts @@ -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 } }) diff --git a/src/utils/format.ts b/src/utils/format.ts index e7e33c1..2a270dc 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -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' })}` } } diff --git a/src/views/chat/ChatView.vue b/src/views/chat/ChatView.vue index 3131a07..17c1dad 100644 --- a/src/views/chat/ChatView.vue +++ b/src/views/chat/ChatView.vue @@ -137,20 +137,22 @@
- + {{ conv.unread_count > 99 ? '99+' : conv.unread_count }} - +
@@ -295,6 +297,15 @@ + (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(null) const showGroupInfoPanel = ref(false) +const hoveredBadgeId = ref(null) const refreshingConversations = ref(false) const roomMembers = ref>>({}) @@ -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) { diff --git a/src/views/contact/ContactCircle.vue b/src/views/contact/ContactCircle.vue index f78d3fd..d246192 100644 --- a/src/views/contact/ContactCircle.vue +++ b/src/views/contact/ContactCircle.vue @@ -387,6 +387,15 @@ @confirm="handleDeleteGroup" @cancel="showDeleteGroupConfirm = false" /> + (null) const groupInputRef = ref(null) const showDeleteGroupConfirm = ref(false) +const showDeleteContactConfirm = ref(false) +const pendingDeleteContact = ref(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() {