This commit is contained in:
2025-12-04 13:41:03 +08:00
parent 452ac7329e
commit 94bf694955
3 changed files with 121 additions and 68 deletions

View File

@@ -1,50 +1,62 @@
<template> <template>
<div <div
id="msgListContainer" id="msgListContainer"
ref="containerRef" ref="containerRef"
class="flex-1 overflow-y-auto p-4 space-y-5" class="flex-1 overflow-y-auto p-4 space-y-5"
@dragover.prevent @dragover.prevent
@drop.prevent @drop.prevent
@scroll="handleScroll" @scroll="handleScroll"
> >
<!-- 加载历史消息提示 --> <!-- 顶部加载状态提示 -->
<div v-if="loadingHistory" class="flex justify-center items-center py-4"> <div class="w-full flex justify-center py-2 min-h-[40px] items-center">
<div class="flex items-center gap-2 text-gray-400 text-sm">
<i class="fas fa-spinner fa-spin"></i> <!-- 1. 加载中 -->
<span>正在加载历史消息...</span> <div v-if="loadingHistory" class="flex items-center gap-2 text-primary/80 text-xs bg-black/20 px-3 py-1.5 rounded-full backdrop-blur-sm">
<i class="fas fa-circle-notch fa-spin"></i>
<span>加载历史记录...</span>
</div>
<!-- 2. 点击加载 (还有更多消息但未触发自动加载时显示) -->
<button
v-else-if="hasMoreHistory"
class="flex items-center gap-2 text-gray-500 hover:text-primary text-xs bg-black/10 hover:bg-white/10 px-3 py-1.5 rounded-full transition-all duration-200 cursor-pointer"
@click="$emit('load-more')"
>
<i class="fas fa-history"></i>
<span>点击加载历史消息</span>
</button>
<!-- 3. 已经到顶 -->
<div v-else-if="messages.length > 0" class="flex items-center gap-2 text-gray-600 text-[10px] opacity-50">
<span class="w-8 h-px bg-gray-700"></span>
<span>已经到顶了</span>
<span class="w-8 h-px bg-gray-700"></span>
</div> </div>
</div> </div>
<!-- 已经到顶提示 -->
<div v-else-if="!hasMoreHistory && messages.length > 0" class="flex justify-center items-center py-4">
<div class="text-gray-500 text-sm">
<span>已经到顶啦~</span>
</div>
</div>
<div <div
v-for="(msg, index) in messages" v-for="(msg, index) in messages"
:key="index" :key="index"
class="flex gap-3 msg-row group" class="flex gap-3 msg-row group"
:class="{ 'flex-row-reverse': msg.isSelf }" :class="{ 'flex-row-reverse': msg.isSelf }"
@contextmenu.stop="$emit('contextmenu', $event, msg)" @contextmenu.stop="$emit('contextmenu', $event, msg)"
> >
<!-- 头像 --> <!-- 头像 -->
<Avatar <Avatar
:name="msg.isSelf ? currentUser.name : (target.user?.name || target.remark_name)" :name="msg.isSelf ? currentUser.name : (target.user?.name || target.remark_name)"
:avatar="msg.isSelf ? currentUser.avatar : (target.user?.avatar || target.remark_name?.charAt(0))" :avatar="msg.isSelf ? currentUser.avatar : (target.user?.avatar || target.remark_name?.charAt(0))"
:color="msg.isSelf ? generateColor(currentUser.id) : target.color" :color="msg.isSelf ? generateColor(currentUser.id) : target.color"
size="sm" size="sm"
rounded="lg" rounded="lg"
class="cursor-pointer hover:opacity-80 transition self-start mt-1" class="cursor-pointer hover:opacity-80 transition self-start mt-1 shadow-md"
@click="$emit('avatar-click', msg.isSelf ? currentUser : target)" @click="$emit('avatar-click', msg.isSelf ? currentUser : target)"
/> />
<!-- 消息内容区 --> <!-- 消息内容区 -->
<div class="flex flex-col max-w-[75%] md:max-w-[60%]"> <div class="flex flex-col max-w-[75%] md:max-w-[60%]">
<div <div
class="text-[10px] text-gray-500 mb-1 px-1" class="text-[10px] text-gray-500 mb-1 px-1"
:class="{ 'text-right': msg.isSelf }" :class="{ 'text-right': msg.isSelf }"
> >
{{ msg.isSelf ? 'Me' : (target.remark_name || target.user?.name) }} {{ msg.isSelf ? 'Me' : (target.remark_name || target.user?.name) }}
</div> </div>
@@ -109,3 +121,19 @@ defineExpose({
}) })
</script> </script>
<style scoped>
/* 隐藏滚动条但保留功能 */
.overflow-y-auto::-webkit-scrollbar {
width: 4px;
}
.overflow-y-auto::-webkit-scrollbar-track {
background: transparent;
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 20px;
}
.overflow-y-auto:hover::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
}
</style>

View File

@@ -198,8 +198,8 @@
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<!-- 默认空页面 --> <!-- 默认空页面 -->
<div <div
v-if="contactStore.leftPanelMode === 'default' && !contactStore.selectedContact" v-if="contactStore.leftPanelMode === 'default' && !contactStore.selectedContact"
class="flex-1 flex flex-col items-center justify-center bg-dark text-gray-500 h-full" class="flex-1 flex flex-col items-center justify-center bg-dark text-gray-500 h-full"
> >
<div class="w-32 h-32 bg-panel rounded-full flex items-center justify-center mb-6 shadow-2xl border border-gray-800 animate-float"> <div class="w-32 h-32 bg-panel rounded-full flex items-center justify-center mb-6 shadow-2xl border border-gray-800 animate-float">
<i class="fas fa-address-book text-5xl text-primary opacity-80"></i> <i class="fas fa-address-book text-5xl text-primary opacity-80"></i>
@@ -210,8 +210,8 @@
<!-- 好友资料卡视图 --> <!-- 好友资料卡视图 -->
<ContactDetailCard <ContactDetailCard
v-else-if="contactStore.selectedContact" v-else-if="contactStore.selectedContact"
@switch-to-chat="currentTab = 'chat'" @switch-to-chat="currentTab = 'chat'"
/> />
<!-- 好友管理器视图 --> <!-- 好友管理器视图 -->
@@ -220,13 +220,8 @@
<!-- 好友通知视图 --> <!-- 好友通知视图 -->
<FriendNotifyView v-else-if="contactStore.leftPanelMode === 'friend-notify'" /> <FriendNotifyView v-else-if="contactStore.leftPanelMode === 'friend-notify'" />
<!-- 群通知视图预留 --> <!-- 群通知视图 -->
<div v-else-if="contactStore.leftPanelMode === 'group-notify'" class="flex-1 flex items-center justify-center text-gray-400"> <GroupNotifyView v-else-if="contactStore.leftPanelMode === 'group-notify'" />
<div class="text-center">
<i class="fas fa-users text-6xl mb-4 opacity-50"></i>
<p class="text-sm">群通知功能开发中</p>
</div>
</div>
</div> </div>
</div> </div>
<ContextMenu /> <ContextMenu />
@@ -280,6 +275,7 @@ import ContactView from '@/views/contact/ContactView.vue'
import ContactCircle from '@/views/contact/ContactCircle.vue' import ContactCircle from '@/views/contact/ContactCircle.vue'
import ContactDetailCard from '@/views/contact/ContactDetailCard.vue' import ContactDetailCard from '@/views/contact/ContactDetailCard.vue'
import FriendNotifyView from '@/views/contact/FriendNotifyView.vue' import FriendNotifyView from '@/views/contact/FriendNotifyView.vue'
import GroupNotifyView from '@/views/contact/GroupNotifyView.vue'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -385,7 +381,7 @@ async function selectChat(contact: Contact) {
try { try {
const response = await messageApi.getMessages(roomId, 1, 50) const response = await messageApi.getMessages(roomId, 1, 50)
console.log('Messages API response:', response) console.log('Messages API response:', response)
// response 已经是 PaginatedResponse直接访问 data // response 已经是 PaginatedResponse直接访问 data
if (response && response.data && Array.isArray(response.data)) { if (response && response.data && Array.isArray(response.data)) {
const msgs = response.data.map((msg: ChatMessage) => ({ const msgs = response.data.map((msg: ChatMessage) => ({
@@ -394,17 +390,17 @@ async function selectChat(contact: Contact) {
extra: typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra, extra: typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra,
})) }))
console.log('Processed messages:', msgs) console.log('Processed messages:', msgs)
// 确保消息按时间顺序排列reverse 后最新的在最后) // 确保消息按时间顺序排列reverse 后最新的在最后)
const sortedMsgs = msgs.reverse() const sortedMsgs = msgs.reverse()
chatStore.setRoomMessages(roomId, sortedMsgs) chatStore.setRoomMessages(roomId, sortedMsgs)
console.log('Messages set to store, roomId:', roomId, 'count:', sortedMsgs.length) console.log('Messages set to store, roomId:', roomId, 'count:', sortedMsgs.length)
// 如果返回的消息数量少于 pageSize说明没有更多历史消息了 // 如果返回的消息数量少于 pageSize说明没有更多历史消息了
if (response.data.length < 50) { if (response.data.length < 50) {
hasMoreHistory.value[roomId] = false hasMoreHistory.value[roomId] = false
} }
// 等待 DOM 更新后再滚动 // 等待 DOM 更新后再滚动
await nextTick() await nextTick()
scrollToBottom(false) // 初始进入直接跳到底部 scrollToBottom(false) // 初始进入直接跳到底部
@@ -412,7 +408,7 @@ async function selectChat(contact: Contact) {
console.warn('No messages in response:', response) console.warn('No messages in response:', response)
hasMoreHistory.value[roomId] = false hasMoreHistory.value[roomId] = false
} }
} catch (error) { } catch (error) {
console.error('Failed to load messages:', error) console.error('Failed to load messages:', error)
toastStore.error('加载消息失败') toastStore.error('加载消息失败')
hasMoreHistory.value[roomId] = false hasMoreHistory.value[roomId] = false
@@ -428,48 +424,48 @@ async function selectChat(contact: Contact) {
// 加载更多历史消息(触顶加载) // 加载更多历史消息(触顶加载)
async function loadMoreMessages() { async function loadMoreMessages() {
if (!chatStore.currentTarget || loadingHistory.value) return if (!chatStore.currentTarget || loadingHistory.value) return
const roomId = getRoomId(chatStore.currentTarget) const roomId = getRoomId(chatStore.currentTarget)
const currentMessages = chatStore.getRoomMessages(roomId) const currentMessages = chatStore.getRoomMessages(roomId)
// 如果当前没有消息,不加载 // 如果当前没有消息,不加载
if (currentMessages.length === 0) return if (currentMessages.length === 0) return
// 获取当前页码如果没有则初始化为1 // 获取当前页码如果没有则初始化为1
const page = (currentPage.value[roomId] || 1) + 1 const page = (currentPage.value[roomId] || 1) + 1
loadingHistory.value = true loadingHistory.value = true
try { try {
// 记录加载前的滚动位置和第一条消息的高度 // 记录加载前的滚动位置和第一条消息的高度
const container = document.getElementById('msgListContainer') const container = document.getElementById('msgListContainer')
const oldScrollTop = container?.scrollTop || 0 const oldScrollTop = container?.scrollTop || 0
const oldScrollHeight = container?.scrollHeight || 0 const oldScrollHeight = container?.scrollHeight || 0
// 加载历史消息 // 加载历史消息
const pageSize = 50 const pageSize = 50
const response = await messageApi.getMessages(roomId, page, pageSize) const response = await messageApi.getMessages(roomId, page, pageSize)
if (response.data && response.data.length > 0) { if (response.data && response.data.length > 0) {
const newMsgs = response.data.map((msg: ChatMessage) => ({ const newMsgs = response.data.map((msg: ChatMessage) => ({
...msg, ...msg,
isSelf: msg.sender_user_id === authStore.user!.id, isSelf: msg.sender_user_id === authStore.user!.id,
extra: typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra, extra: typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra,
})) }))
// 将新消息添加到现有消息数组的前面(历史消息) // 将新消息添加到现有消息数组的前面(历史消息)
const existingMessages = [...currentMessages] const existingMessages = [...currentMessages]
const allMessages = [...newMsgs.reverse(), ...existingMessages] const allMessages = [...newMsgs.reverse(), ...existingMessages]
chatStore.setRoomMessages(roomId, allMessages) chatStore.setRoomMessages(roomId, allMessages)
// 更新页码 // 更新页码
currentPage.value[roomId] = page currentPage.value[roomId] = page
// 如果返回的消息数量少于 pageSize说明没有更多历史消息了 // 如果返回的消息数量少于 pageSize说明没有更多历史消息了
if (response.data.length < pageSize) { if (response.data.length < pageSize) {
hasMoreHistory.value[roomId] = false hasMoreHistory.value[roomId] = false
} }
// 等待DOM更新后恢复滚动位置 // 等待DOM更新后恢复滚动位置
await nextTick() await nextTick()
if (container) { if (container) {
@@ -494,7 +490,7 @@ async function loadMoreMessages() {
async function selectChatByConversation(conv: Conversation) { async function selectChatByConversation(conv: Conversation) {
// 在联系人列表中找到对应联系人(用于沿用现有 chatStore 结构) // 在联系人列表中找到对应联系人(用于沿用现有 chatStore 结构)
const contact = chatStore.contacts.find( const contact = chatStore.contacts.find(
c => c.user_id === conv.target_id || c.id === conv.target_id, c => c.user_id === conv.target_id || c.id === conv.target_id,
) )
if (!contact) { if (!contact) {
// 如果本地没有联系人数据,可以在这里补一次联系人列表加载;当前先简单返回 // 如果本地没有联系人数据,可以在这里补一次联系人列表加载;当前先简单返回
@@ -545,11 +541,11 @@ function handleWebSocketMessage(message: ChatMessage) {
if (contact) { if (contact) {
chatStore.updateContactLastMsg(contact.id, getMsgSummary(message), Date.now()) chatStore.updateContactLastMsg(contact.id, getMsgSummary(message), Date.now())
// 检查是否是当前选中的聊天(通过 room_id 匹配) // 检查是否是当前选中的聊天(通过 room_id 匹配)
const currentRoomId = chatStore.currentTarget ? getRoomId(chatStore.currentTarget) : null const currentRoomId = chatStore.currentTarget ? getRoomId(chatStore.currentTarget) : null
const isCurrentChat = currentRoomId === roomId const isCurrentChat = currentRoomId === roomId
conversationStore.handleMessageUpdate(message, message.isSelf === true, isCurrentChat) conversationStore.handleMessageUpdate(message, message.isSelf === true, isCurrentChat)
if (!message.isSelf) { if (!message.isSelf) {
// 如果不是当前聊天窗口,或者是当前窗口但用户不在底部 // 如果不是当前聊天窗口,或者是当前窗口但用户不在底部
@@ -580,7 +576,7 @@ function handleWebSocketMessage(message: ChatMessage) {
function backToList() { if (isMobile.value) chatVisible.value = false } function backToList() { if (isMobile.value) chatVisible.value = false }
function handleLogout() { authStore.logout(); wsManager.disconnect(); router.push('/login') } 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 } 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') { async function startCall(type: 'audio' | 'video') {
if (!chatStore.currentTarget) return if (!chatStore.currentTarget) return
const receiverUserId = chatStore.currentTarget.user_id || chatStore.currentTarget.id const receiverUserId = chatStore.currentTarget.user_id || chatStore.currentTarget.id
// 直接传入 room_id // 直接传入 room_id
@@ -640,11 +636,11 @@ onMounted(async () => {
} }
await loadContacts() await loadContacts()
await conversationStore.loadConversations() await conversationStore.loadConversations()
// 恢复选中的会话(优先使用 room_id 查找) // 恢复选中的会话(优先使用 room_id 查找)
const savedRoomId = storage.getSelectedRoomId() const savedRoomId = storage.getSelectedRoomId()
const savedTargetId = storage.getSelectedConversation() const savedTargetId = storage.getSelectedConversation()
if (savedRoomId) { if (savedRoomId) {
// 优先使用 room_id 查找联系人 // 优先使用 room_id 查找联系人
const contact = chatStore.contacts.find(c => { const contact = chatStore.contacts.find(c => {
@@ -656,7 +652,7 @@ onMounted(async () => {
return return
} }
} }
// 如果没有 room_id 或找不到,使用 targetId 查找 // 如果没有 room_id 或找不到,使用 targetId 查找
if (savedTargetId) { if (savedTargetId) {
const contact = chatStore.contacts.find(c => c.user_id === savedTargetId || c.id === savedTargetId) const contact = chatStore.contacts.find(c => c.user_id === savedTargetId || c.id === savedTargetId)
@@ -664,7 +660,7 @@ onMounted(async () => {
await selectChat(contact) await selectChat(contact)
} }
} }
window.addEventListener('resize', () => isMobile.value = window.innerWidth < 768) window.addEventListener('resize', () => isMobile.value = window.innerWidth < 768)
}) })
onUnmounted(() => { onUnmounted(() => {

View File

@@ -0,0 +1,29 @@
<template>
<div class="flex-1 flex flex-col bg-panel h-full">
<!-- 头部 -->
<div class="p-6 border-b border-gray-800/50 flex justify-between items-center">
<h2 class="text-xl font-bold text-white">群通知</h2>
<button class="text-gray-400 hover:text-white transition" title="刷新">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<!-- 列表区域 -->
<div class="flex-1 overflow-y-auto p-4 custom-scrollbar">
<!-- 空状态 -->
<div class="text-center py-20 text-gray-600">
<i class="fas fa-users-slash text-5xl mb-4 opacity-50"></i>
<p class="text-sm">暂无群通知</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 暂时保留为空,后续在此处对接群通知 API
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-thumb { @apply bg-gray-700 rounded-full; }
</style>