UI升级
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="h-screen w-screen flex text-sm sm:text-base bg-dark" @contextmenu.prevent>
|
||||
<!-- 左侧功能导航 (UI微调:增加阴影和圆角) -->
|
||||
<!-- 左侧功能导航 -->
|
||||
<div class="w-16 bg-dark border-r border-gray-800 flex flex-col items-center py-6 gap-6 z-20 hidden md:flex shrink-0 shadow-xl">
|
||||
<Avatar
|
||||
:name="authStore.user?.name"
|
||||
@@ -9,7 +9,6 @@
|
||||
class="cursor-pointer hover:ring-2 ring-primary transition-all duration-300 transform hover:scale-110 shadow-lg"
|
||||
@click="showProfileModal = true"
|
||||
/>
|
||||
<!-- ...导航图标保持不变,增加 hover scale... -->
|
||||
<div
|
||||
class="text-gray-400 hover:text-primary cursor-pointer transition transform hover:scale-110 relative w-10 h-10 flex items-center justify-center rounded-xl hover:bg-white/5"
|
||||
:class="{ 'text-primary bg-white/5': currentTab === 'chat' }"
|
||||
@@ -36,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间侧边栏 (UI微调) -->
|
||||
<!-- 中间侧边栏 (会话列表) -->
|
||||
<div
|
||||
v-show="currentTab === 'chat' && (!isMobile || !chatVisible)"
|
||||
class="w-full md:w-80 bg-panel border-r border-gray-800 flex flex-col z-10 h-full shrink-0"
|
||||
@@ -57,43 +56,78 @@
|
||||
<div
|
||||
v-for="conv in filteredConversations"
|
||||
:key="conv.id"
|
||||
class="flex items-center p-3 cursor-pointer rounded-2xl transition-all duration-200 relative group border border-transparent"
|
||||
class="flex items-center p-3 cursor-pointer rounded-2xl transition-all duration-300 relative group border"
|
||||
:class="{
|
||||
'bg-gradient-to-r from-primary/20 to-transparent border-primary/30': chatStore.currentTarget && (chatStore.currentTarget.user_id === conv.target_id || chatStore.currentTarget.id === conv.target_id),
|
||||
'hover:bg-white/5 hover:border-white/5': !chatStore.currentTarget || (chatStore.currentTarget.user_id !== conv.target_id && chatStore.currentTarget.id !== conv.target_id),
|
||||
'bg-black/20': conv.is_top,
|
||||
// 选中状态:恢复渐变背景,边框变细且带有透明度,增加微弱阴影
|
||||
'bg-gradient-to-r from-primary/20 via-primary/5 to-transparent border-primary/30 shadow-[0_0_20px_rgba(var(--primary-rgb),0.1)]': chatStore.currentTarget && (chatStore.currentTarget.user_id === conv.target_id || chatStore.currentTarget.id === conv.target_id),
|
||||
|
||||
// 置顶状态 (非选中时):深色背景,无边框
|
||||
'bg-black/20 border-transparent': conv.is_top && (!chatStore.currentTarget || (chatStore.currentTarget.user_id !== conv.target_id && chatStore.currentTarget.id !== conv.target_id)),
|
||||
|
||||
// 普通状态:透明边框,Hover 变亮
|
||||
'border-transparent hover:bg-white/5': !conv.is_top && (!chatStore.currentTarget || (chatStore.currentTarget.user_id !== conv.target_id && chatStore.currentTarget.id !== conv.target_id))
|
||||
}"
|
||||
@click="selectChatByConversation(conv)"
|
||||
@contextmenu.stop="showConversationMenu($event, conv)"
|
||||
>
|
||||
<!-- 头像区域 -->
|
||||
<div class="relative shrink-0">
|
||||
<Avatar
|
||||
:name="conv.name"
|
||||
:avatar="conv.avatar || conv.name?.charAt(0)"
|
||||
:name="conv.displayName"
|
||||
:avatar="conv.displayAvatar || conv.displayName?.charAt(0)"
|
||||
:color="undefined"
|
||||
size="contact"
|
||||
rounded="xl"
|
||||
class="transition transform group-hover:scale-105 shadow-md"
|
||||
:class="{'ring-2 ring-pink-500/50': conv.is_special_care}"
|
||||
/>
|
||||
<div
|
||||
v-if="(conv.unread_count || 0) > 0"
|
||||
class="absolute -top-1.5 -right-1.5 bg-red-500 text-white text-[10px] min-w-[18px] h-[18px] flex items-center justify-center rounded-full border-2 border-panel font-bold shadow-sm animate-bounce"
|
||||
>
|
||||
{{ conv.unread_count }}
|
||||
<!-- 特别关心标识(头像角标) -->
|
||||
<div v-if="conv.is_special_care" class="absolute -bottom-1 -right-1 bg-pink-500 text-white rounded-full p-0.5 border-2 border-panel shadow-sm">
|
||||
<i class="fas fa-heart text-[8px] block"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="flex-1 min-w-0 ml-3">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<span class="font-semibold truncate text-gray-200 text-sm group-hover:text-white transition">
|
||||
{{ conv.name || '未知' }}
|
||||
<!-- 昵称 -->
|
||||
<span
|
||||
class="font-semibold truncate text-sm transition flex items-center gap-1"
|
||||
:class="conv.is_special_care ? 'text-pink-400' : 'text-gray-200 group-hover:text-white'"
|
||||
>
|
||||
{{ conv.displayName }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 group-hover:text-gray-400">{{ formatTime(conv.last_time || Date.now()) }}</span>
|
||||
<!-- 时间 -->
|
||||
<span class="text-xs text-gray-500 group-hover:text-gray-400 shrink-0">{{ formatTime(conv.last_time || Date.now()) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 truncate flex items-center h-4 group-hover:text-gray-300">
|
||||
<i v-if="conv.is_top" class="fas fa-thumbtack text-yellow-500 mr-1 text-[10px]"></i>
|
||||
<span v-if="conv.is_muted" class="fas fa-bell-slash text-gray-600 mr-1 text-[10px]"></span>
|
||||
<span>{{ conv.last_message || '' }}</span>
|
||||
|
||||
<div class="flex justify-between items-center h-5">
|
||||
<!-- 消息摘要 -->
|
||||
<div class="text-xs text-gray-400 truncate flex items-center group-hover:text-gray-300 flex-1 min-w-0 mr-2">
|
||||
<!-- 置顶标识 -->
|
||||
<i v-if="conv.is_top" class="fas fa-thumbtack text-primary/70 mr-1.5 text-[10px] transform rotate-45"></i>
|
||||
<span v-if="conv.is_muted" class="fas fa-bell-slash text-gray-600 mr-1 text-[10px]"></span>
|
||||
<span class="truncate">{{ conv.last_message || '' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 优化后的未读徽标 -->
|
||||
<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="[
|
||||
'bg-red-500 text-white hover:bg-emerald-500' // 默认红,悬浮绿
|
||||
]"
|
||||
title="标记已读"
|
||||
@click.stop="handleMarkRead(conv)"
|
||||
>
|
||||
<!-- 默认显示数字 -->
|
||||
<span class="text-[10px] block group-hover/badge:hidden transition-all duration-200">
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +144,7 @@
|
||||
<div class="flex items-center gap-3 cursor-pointer group" @click="backToList">
|
||||
<i class="fas fa-chevron-left md:hidden text-gray-400 text-lg p-2 -ml-2"></i>
|
||||
<Avatar
|
||||
:name="chatStore.currentTarget.user?.name || chatStore.currentTarget.remark_name"
|
||||
:name="chatStore.currentTarget.remark_name || chatStore.currentTarget.user?.name"
|
||||
:avatar="chatStore.currentTarget.user?.avatar || chatStore.currentTarget.remark_name?.charAt(0)"
|
||||
:color="chatStore.currentTarget.color"
|
||||
size="sm"
|
||||
@@ -132,8 +166,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 (包含滚动逻辑) -->
|
||||
<!-- 修复: 添加 flex flex-col 确保 MessageList 的 flex-1 生效 -->
|
||||
<!-- 消息列表 -->
|
||||
<div class="flex-1 overflow-hidden relative flex flex-col min-h-0">
|
||||
<MessageList
|
||||
ref="messageListRef"
|
||||
@@ -184,19 +217,18 @@
|
||||
<p class="text-sm mt-3 opacity-60 font-light">Design for Developer</p>
|
||||
</div>
|
||||
|
||||
<!-- 联系人页面:左侧联系人列表 + 右侧联系人操作区(QQ 风格) -->
|
||||
<!-- 联系人页面 -->
|
||||
<div
|
||||
v-if="currentTab === 'contact'"
|
||||
class="flex flex-1 bg-panel h-full"
|
||||
>
|
||||
<!-- 左侧联系人列表(分组 & QQ 风格) -->
|
||||
<!-- 左侧联系人列表 -->
|
||||
<div class="w-80 border-r border-gray-800 shrink-0">
|
||||
<ContactCircle />
|
||||
</div>
|
||||
|
||||
<!-- 右侧:根据 leftPanelMode 和选中好友显示不同内容 -->
|
||||
<!-- 右侧 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 默认空页面 -->
|
||||
<div
|
||||
v-if="contactStore.leftPanelMode === 'default' && !contactStore.selectedContact"
|
||||
class="flex-1 flex flex-col items-center justify-center bg-dark text-gray-500 h-full"
|
||||
@@ -208,26 +240,20 @@
|
||||
<p class="text-sm mt-3 opacity-60 font-light">Design for Developer</p>
|
||||
</div>
|
||||
|
||||
<!-- 好友资料卡视图 -->
|
||||
<ContactDetailCard
|
||||
v-else-if="contactStore.selectedContact"
|
||||
@switch-to-chat="currentTab = 'chat'"
|
||||
/>
|
||||
|
||||
<!-- 好友管理器视图 -->
|
||||
<ContactView v-else-if="contactStore.leftPanelMode === 'friend-manager'" />
|
||||
|
||||
<!-- 好友通知视图 -->
|
||||
<FriendNotifyView v-else-if="contactStore.leftPanelMode === 'friend-notify'" />
|
||||
|
||||
<!-- 群通知视图 -->
|
||||
<GroupNotifyView v-else-if="contactStore.leftPanelMode === 'group-notify'" />
|
||||
</div>
|
||||
</div>
|
||||
<ContextMenu />
|
||||
<FileConfirmModal :show="fileModal.show" :type="fileModal.type" :preview="fileModal.preview" :name="fileModal.name" :size="fileModal.size" @close="fileModal.show = false" @confirm="confirmSendFile" />
|
||||
|
||||
<!-- Profile Modal (kept simple) -->
|
||||
<!-- Profile Modal -->
|
||||
<div v-if="showProfileModal && authStore.user" class="fixed inset-0 bg-black/80 z-[60] flex items-center justify-center p-4 backdrop-blur-sm" @click.self="showProfileModal = false">
|
||||
<div class="bg-panel rounded-2xl w-80 shadow-2xl border border-gray-700 overflow-hidden animate-fade-in">
|
||||
<div class="h-24 bg-gradient-to-r from-indigo-600 to-purple-600"></div>
|
||||
@@ -287,7 +313,7 @@ const { showContextMenu } = useContextMenu()
|
||||
const webrtcStore = useWebRTCStore()
|
||||
const webrtc = webrtcStore.webrtc
|
||||
|
||||
// State - 从本地存储恢复
|
||||
// State
|
||||
const currentTab = ref<'chat' | 'contact'>(storage.getCurrentTab())
|
||||
const searchQuery = ref('')
|
||||
const inputText = ref('')
|
||||
@@ -302,15 +328,39 @@ const showScrollBottomBtn = ref(false)
|
||||
const unreadCount = ref(0)
|
||||
const isNearBottom = ref(true)
|
||||
const loadingHistory = ref(false)
|
||||
const currentPage = ref<Record<string, number>>({}) // 记录每个房间已加载的页码
|
||||
const hasMoreHistory = ref<Record<string, boolean>>({}) // 记录每个房间是否还有更多历史消息
|
||||
const currentPage = ref<Record<string, number>>({})
|
||||
const hasMoreHistory = ref<Record<string, boolean>>({})
|
||||
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
|
||||
|
||||
// --- 数据计算优化:合并联系人状态 (置顶、特别关心) ---
|
||||
const filteredConversations = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return conversationStore.conversations.filter(conv => {
|
||||
const name = (conv.name || conv.target_user?.name || '').toLowerCase()
|
||||
return name.includes(query)
|
||||
|
||||
// 1. 基础映射:合并 contactStore 中的最新状态
|
||||
let convs = conversationStore.conversations.map(conv => {
|
||||
// 查找对应的联系人信息,以获取最新的 is_top, is_special_care 等状态
|
||||
const contact = chatStore.contacts.find(c => c.user_id === conv.target_id || c.id === conv.target_id)
|
||||
return {
|
||||
...conv,
|
||||
// 优先使用联系人中的状态 (因为右键菜单更新的是 contactStore)
|
||||
is_top: contact?.is_top ?? conv.is_top,
|
||||
is_muted: contact?.is_muted ?? conv.is_muted,
|
||||
is_special_care: contact?.is_special_care ?? false,
|
||||
// 使用备注名优先
|
||||
displayName: contact?.remark_name || contact?.user?.name || conv.name || '未知',
|
||||
displayAvatar: contact?.user?.avatar || conv.avatar
|
||||
}
|
||||
})
|
||||
|
||||
// 2. 搜索过滤
|
||||
if (query) {
|
||||
convs = convs.filter(c => c.displayName.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
// 3. 排序:置顶优先 > 时间倒序
|
||||
return convs.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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -321,11 +371,9 @@ const currentMessages = computed(() => {
|
||||
})
|
||||
|
||||
function getRoomId(contact: Contact): string {
|
||||
// 优先使用 contact.room_id(雪花ID),如果没有则使用旧的生成方式作为后备
|
||||
if (contact.room_id) {
|
||||
return contact.room_id
|
||||
}
|
||||
// 后备方案:使用旧的格式(兼容旧数据)
|
||||
const userIds = [authStore.user!.id, contact.user_id || contact.id || contact.contact_user_id].sort()
|
||||
return userIds.join('_')
|
||||
}
|
||||
@@ -333,13 +381,11 @@ function getRoomId(contact: Contact): string {
|
||||
// 滚动处理
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.target as HTMLElement
|
||||
// 阈值设为 100px
|
||||
const threshold = 100
|
||||
const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight
|
||||
|
||||
isNearBottom.value = distanceToBottom < threshold
|
||||
|
||||
// 如果接近底部,隐藏按钮并清空未读
|
||||
if (isNearBottom.value) {
|
||||
showScrollBottomBtn.value = false
|
||||
unreadCount.value = 0
|
||||
@@ -363,49 +409,38 @@ function scrollToBottom(smooth = true) {
|
||||
}
|
||||
|
||||
async function selectChat(contact: Contact) {
|
||||
// 保存选中的会话和房间ID
|
||||
storage.setSelectedConversation(contact.user_id || contact.id)
|
||||
const roomId = getRoomId(contact)
|
||||
storage.setSelectedRoomId(roomId)
|
||||
chatStore.setCurrentTarget(contact)
|
||||
chatVisible.value = true
|
||||
unreadCount.value = 0 // 切换聊天时重置
|
||||
unreadCount.value = 0
|
||||
|
||||
// 重置 hasMoreHistory 状态
|
||||
hasMoreHistory.value[roomId] = true
|
||||
|
||||
// 如果还没有加载过消息,初始化加载第一页
|
||||
const existingMessages = chatStore.getRoomMessages(roomId)
|
||||
if (existingMessages.length === 0) {
|
||||
currentPage.value[roomId] = 1
|
||||
try {
|
||||
const response = await messageApi.getMessages(roomId, 1, 50)
|
||||
console.log('Messages API response:', response)
|
||||
|
||||
// response 已经是 PaginatedResponse,直接访问 data
|
||||
if (response && response.data && Array.isArray(response.data)) {
|
||||
const msgs = response.data.map((msg: ChatMessage) => ({
|
||||
...msg,
|
||||
isSelf: msg.sender_user_id === authStore.user!.id,
|
||||
extra: typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra,
|
||||
}))
|
||||
console.log('Processed messages:', msgs)
|
||||
|
||||
// 确保消息按时间顺序排列(reverse 后最新的在最后)
|
||||
const sortedMsgs = msgs.reverse()
|
||||
chatStore.setRoomMessages(roomId, sortedMsgs)
|
||||
console.log('Messages set to store, roomId:', roomId, 'count:', sortedMsgs.length)
|
||||
|
||||
// 如果返回的消息数量少于 pageSize,说明没有更多历史消息了
|
||||
if (response.data.length < 50) {
|
||||
hasMoreHistory.value[roomId] = false
|
||||
}
|
||||
|
||||
// 等待 DOM 更新后再滚动
|
||||
await nextTick()
|
||||
scrollToBottom(false) // 初始进入直接跳到底部
|
||||
scrollToBottom(false)
|
||||
} else {
|
||||
console.warn('No messages in response:', response)
|
||||
hasMoreHistory.value[roomId] = false
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -414,35 +449,28 @@ async function selectChat(contact: Contact) {
|
||||
hasMoreHistory.value[roomId] = false
|
||||
}
|
||||
} else {
|
||||
// 如果已有消息,直接滚动到底部
|
||||
console.log('Using existing messages, count:', existingMessages.length)
|
||||
await nextTick()
|
||||
scrollToBottom(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多历史消息(触顶加载)
|
||||
async function loadMoreMessages() {
|
||||
if (!chatStore.currentTarget || loadingHistory.value) return
|
||||
|
||||
const roomId = getRoomId(chatStore.currentTarget)
|
||||
const currentMessages = chatStore.getRoomMessages(roomId)
|
||||
|
||||
// 如果当前没有消息,不加载
|
||||
if (currentMessages.length === 0) return
|
||||
|
||||
// 获取当前页码,如果没有则初始化为1
|
||||
const page = (currentPage.value[roomId] || 1) + 1
|
||||
|
||||
loadingHistory.value = true
|
||||
|
||||
try {
|
||||
// 记录加载前的滚动位置和第一条消息的高度
|
||||
const container = document.getElementById('msgListContainer')
|
||||
const oldScrollTop = container?.scrollTop || 0
|
||||
const oldScrollHeight = container?.scrollHeight || 0
|
||||
|
||||
// 加载历史消息
|
||||
const pageSize = 50
|
||||
const response = await messageApi.getMessages(roomId, page, pageSize)
|
||||
|
||||
@@ -453,20 +481,16 @@ async function loadMoreMessages() {
|
||||
extra: typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra,
|
||||
}))
|
||||
|
||||
// 将新消息添加到现有消息数组的前面(历史消息)
|
||||
const existingMessages = [...currentMessages]
|
||||
const allMessages = [...newMsgs.reverse(), ...existingMessages]
|
||||
chatStore.setRoomMessages(roomId, allMessages)
|
||||
|
||||
// 更新页码
|
||||
currentPage.value[roomId] = page
|
||||
|
||||
// 如果返回的消息数量少于 pageSize,说明没有更多历史消息了
|
||||
if (response.data.length < pageSize) {
|
||||
hasMoreHistory.value[roomId] = false
|
||||
}
|
||||
|
||||
// 等待DOM更新后恢复滚动位置
|
||||
await nextTick()
|
||||
if (container) {
|
||||
const newScrollHeight = container.scrollHeight
|
||||
@@ -474,9 +498,7 @@ async function loadMoreMessages() {
|
||||
container.scrollTop = oldScrollTop + heightDiff
|
||||
}
|
||||
} else {
|
||||
// 没有更多消息了
|
||||
hasMoreHistory.value[roomId] = false
|
||||
console.log('没有更多历史消息')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load more messages:', error)
|
||||
@@ -486,24 +508,17 @@ async function loadMoreMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
// 通过会话点击打开聊天
|
||||
async function selectChatByConversation(conv: Conversation) {
|
||||
// 在联系人列表中找到对应联系人(用于沿用现有 chatStore 结构)
|
||||
const contact = chatStore.contacts.find(
|
||||
c => c.user_id === conv.target_id || c.id === conv.target_id,
|
||||
)
|
||||
if (!contact) {
|
||||
// 如果本地没有联系人数据,可以在这里补一次联系人列表加载;当前先简单返回
|
||||
return
|
||||
}
|
||||
await selectChat(contact)
|
||||
// 清空该会话未读
|
||||
await conversationStore.clearUnread(conv.target_id)
|
||||
}
|
||||
|
||||
// ... Send Logic (Text, File, Audio) ...
|
||||
// (保持大部分原有逻辑,但在发送成功后调用 scrollToBottom)
|
||||
|
||||
async function sendMessage(type: number, content: string, extra: any = {}, duration = 0) {
|
||||
if (!chatStore.currentTarget) return
|
||||
const roomId = getRoomId(chatStore.currentTarget)
|
||||
@@ -513,7 +528,6 @@ async function sendMessage(type: number, content: string, extra: any = {}, durat
|
||||
room_id: roomId, message_type: type, content, duration, extra: JSON.stringify(extra)
|
||||
}
|
||||
|
||||
// Optimistic UI update
|
||||
const message: ChatMessage = {
|
||||
id: Date.now(), room_id: roomId, sender_user_id: authStore.user!.id,
|
||||
receiver_user_id: chatStore.currentTarget.user_id || chatStore.currentTarget.id,
|
||||
@@ -522,14 +536,13 @@ async function sendMessage(type: number, content: string, extra: any = {}, durat
|
||||
chatStore.addMessage(roomId, message)
|
||||
chatStore.updateContactLastMsg(chatStore.currentTarget.id, getMsgSummary(message), Date.now())
|
||||
|
||||
scrollToBottom(true) // 发送消息后自动滚动
|
||||
scrollToBottom(true)
|
||||
|
||||
try {
|
||||
await messageApi.sendMessage(payload)
|
||||
} catch(e) { toastStore.error('发送失败'); } // Should handle remove optimistic msg on fail
|
||||
} catch(e) { toastStore.error('发送失败'); }
|
||||
}
|
||||
|
||||
// WebSocket 消息处理核心优化
|
||||
function handleWebSocketMessage(message: ChatMessage) {
|
||||
if (message.message_type === 6) return
|
||||
const roomId = message.room_id
|
||||
@@ -542,17 +555,14 @@ function handleWebSocketMessage(message: ChatMessage) {
|
||||
if (contact) {
|
||||
chatStore.updateContactLastMsg(contact.id, getMsgSummary(message), Date.now())
|
||||
|
||||
// 检查是否是当前选中的聊天(通过 room_id 匹配)
|
||||
const currentRoomId = chatStore.currentTarget ? getRoomId(chatStore.currentTarget) : null
|
||||
const isCurrentChat = currentRoomId === roomId
|
||||
|
||||
conversationStore.handleMessageUpdate(message, message.isSelf === true, isCurrentChat)
|
||||
if (!message.isSelf) {
|
||||
// 如果不是当前聊天窗口,或者是当前窗口但用户不在底部
|
||||
if (!isCurrentChat) {
|
||||
chatStore.incrementUnread(contact.id)
|
||||
} else {
|
||||
// 当前窗口
|
||||
if (isNearBottom.value) {
|
||||
scrollToBottom(true)
|
||||
} else {
|
||||
@@ -561,7 +571,6 @@ function handleWebSocketMessage(message: ChatMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
// 系统通知
|
||||
if ('Notification' in window && Notification.permission === 'granted' && document.hidden) {
|
||||
new Notification(contact.remark_name || contact.user?.name || '新消息', {
|
||||
body: getMsgSummary(message),
|
||||
@@ -572,24 +581,76 @@ function handleWebSocketMessage(message: ChatMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions (backToList, handleLogout, getMsgSummary, etc.) keep same...
|
||||
function backToList() { if (isMobile.value) chatVisible.value = false }
|
||||
function handleLogout() { authStore.logout(); wsManager.disconnect(); router.push('/login') }
|
||||
function getMsgSummary(msg: ChatMessage): string { const types: Record<number, string> = { 1: '[图片]', 2: '[语音]', 3: '[视频]', 8: '[文件]' }; return types[msg.message_type] || msg.content }
|
||||
async function startCall(type: 'audio' | 'video') {
|
||||
if (!chatStore.currentTarget) return
|
||||
const receiverUserId = chatStore.currentTarget.user_id || chatStore.currentTarget.id
|
||||
// 直接传入 room_id
|
||||
const roomId = getRoomId(chatStore.currentTarget)
|
||||
const result = await webrtc.startCall(type, receiverUserId, roomId, chatStore.currentTarget)
|
||||
if (!result) {
|
||||
// 显示错误提示
|
||||
toastStore.error('获取房间ID失败,无法发起通话')
|
||||
}
|
||||
}
|
||||
function showContactMenu(event: MouseEvent, contact: Contact) { showContextMenu(event, [{ label: '删除会话', icon: 'fas fa-trash', danger: true, action: () => {} }]) }
|
||||
|
||||
// ---------------- 新增功能函数 ----------------
|
||||
|
||||
// 会话右键菜单
|
||||
function showConversationMenu(event: MouseEvent, conv: any) {
|
||||
showContextMenu(event, [
|
||||
{
|
||||
label: conv.is_top ? '取消置顶' : '置顶会话',
|
||||
icon: 'fas fa-thumbtack',
|
||||
action: async () => {
|
||||
// 查找对应联系人以更新状态
|
||||
const contact = chatStore.contacts.find(c => c.user_id === conv.target_id || c.id === conv.target_id)
|
||||
if (contact) {
|
||||
try {
|
||||
await contactApi.updateContact(contact.id, { is_top: !contact.is_top })
|
||||
// 手动更新store中的状态,触发 filteredConversations 重新计算
|
||||
contact.is_top = !contact.is_top
|
||||
// 强制刷新会话列表 (触发 computed 更新)
|
||||
await conversationStore.loadConversations()
|
||||
toastStore.success(contact.is_top ? '已置顶' : '已取消置顶')
|
||||
} catch(e) {
|
||||
toastStore.error('操作失败')
|
||||
}
|
||||
} else {
|
||||
toastStore.warning('无法找到对应联系人')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '标记已读',
|
||||
icon: 'fas fa-check-double',
|
||||
action: async () => {
|
||||
await conversationStore.clearUnread(conv.target_id)
|
||||
toastStore.success('已标记已读')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '删除会话',
|
||||
icon: 'fas fa-trash-alt',
|
||||
danger: true,
|
||||
action: () => {
|
||||
if(confirm('确认删除该会话记录吗?')) {
|
||||
conversationStore.conversations = conversationStore.conversations.filter(c => c.id !== conv.id)
|
||||
toastStore.success('会话已移除')
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
async function handleMarkRead(conv: Conversation) {
|
||||
await conversationStore.clearUnread(conv.target_id)
|
||||
toastStore.success('已标记为已读')
|
||||
}
|
||||
|
||||
// ----------------------------------------------
|
||||
|
||||
function showChatOptionsMenu(event: MouseEvent, contact: Contact) { showContextMenu(event, [{ label: '清空记录', icon: 'fas fa-eraser', danger: true, action: () => chatStore.clearRoomMessages(getRoomId(contact)) }]) }
|
||||
// File handlers...
|
||||
function handleSendText() { if (!inputText.value.trim() || !chatStore.currentTarget) return; sendMessage(0, inputText.value); inputText.value = '' }
|
||||
function handleFileSelect(type: number, file: File) {
|
||||
fileModal.value = { show: true, type, preview: '', name: file.name, size: file.size, file }
|
||||
@@ -606,16 +667,13 @@ async function confirmSendFile() {
|
||||
fileModal.value.show = false; toastStore.success('发送成功')
|
||||
} catch (e: any) { toastStore.error('发送失败') }
|
||||
}
|
||||
// Recording handlers same...
|
||||
function handleRecordStart() { isRecording.value = true }
|
||||
async function handleRecordStop(blob: Blob, duration: number) { isRecording.value = false; if (!chatStore.currentTarget) return; try { const audioFile = new File([blob], `audio.webm`, { type: 'audio/webm' }); toastStore.info('正在上传语音...'); const attachment = await attachmentApi.uploadAttachment(audioFile, 'file'); await sendMessage(2, attachment.file_url, { duration: `00:${duration}`, url: attachment.file_url }, duration) } catch (e) { toastStore.error('发送失败') } }
|
||||
function handleRecordCancel() { isRecording.value = false }
|
||||
async function loadContacts() { try { const contacts = await contactApi.getContacts(); chatStore.setContacts(contacts) } catch (e) { console.error(e) } }
|
||||
|
||||
// 监听 currentTab 变化并保存
|
||||
watch(currentTab, (newTab) => {
|
||||
storage.setCurrentTab(newTab)
|
||||
// 当切换到联系人模块时,清除选中的会话和房间ID
|
||||
if (newTab === 'contact') {
|
||||
chatStore.setCurrentTarget(null)
|
||||
storage.setSelectedConversation('')
|
||||
@@ -628,7 +686,6 @@ onMounted(async () => {
|
||||
const isValid = await authStore.checkAuth()
|
||||
if (!isValid) { router.push('/login'); return }
|
||||
}
|
||||
// Remove permission request here!
|
||||
if (authStore.user) {
|
||||
await wsManager.connect(authStore.user.id)
|
||||
wsManager.onMessage(handleWebSocketMessage)
|
||||
@@ -637,12 +694,10 @@ onMounted(async () => {
|
||||
await loadContacts()
|
||||
await conversationStore.loadConversations()
|
||||
|
||||
// 恢复选中的会话(优先使用 room_id 查找)
|
||||
const savedRoomId = storage.getSelectedRoomId()
|
||||
const savedTargetId = storage.getSelectedConversation()
|
||||
|
||||
if (savedRoomId) {
|
||||
// 优先使用 room_id 查找联系人
|
||||
const contact = chatStore.contacts.find(c => {
|
||||
const contactRoomId = getRoomId(c)
|
||||
return contactRoomId === savedRoomId
|
||||
@@ -653,7 +708,6 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 room_id 或找不到,使用 targetId 查找
|
||||
if (savedTargetId) {
|
||||
const contact = chatStore.contacts.find(c => c.user_id === savedTargetId || c.id === savedTargetId)
|
||||
if (contact) {
|
||||
@@ -681,4 +735,5 @@ onUnmounted(() => {
|
||||
.fade-slide-enter-from, .fade-slide-leave-to { opacity: 0; transform: translateY(20px); }
|
||||
.animate-fade-in { animation: fadeIn 0.3s ease-out; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
||||
.animate-pulse-fast { animation: pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user