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>
<div
id="msgListContainer"
ref="containerRef"
class="flex-1 overflow-y-auto p-4 space-y-5"
@dragover.prevent
@drop.prevent
@scroll="handleScroll"
id="msgListContainer"
ref="containerRef"
class="flex-1 overflow-y-auto p-4 space-y-5"
@dragover.prevent
@drop.prevent
@scroll="handleScroll"
>
<!-- 加载历史消息提示 -->
<div v-if="loadingHistory" class="flex justify-center items-center py-4">
<div class="flex items-center gap-2 text-gray-400 text-sm">
<i class="fas fa-spinner fa-spin"></i>
<span>正在加载历史消息...</span>
<!-- 顶部加载状态提示 -->
<div class="w-full flex justify-center py-2 min-h-[40px] items-center">
<!-- 1. 加载中 -->
<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 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
v-for="(msg, index) in messages"
:key="index"
class="flex gap-3 msg-row group"
:class="{ 'flex-row-reverse': msg.isSelf }"
@contextmenu.stop="$emit('contextmenu', $event, msg)"
v-for="(msg, index) in messages"
:key="index"
class="flex gap-3 msg-row group"
:class="{ 'flex-row-reverse': msg.isSelf }"
@contextmenu.stop="$emit('contextmenu', $event, msg)"
>
<!-- 头像 -->
<Avatar
:name="msg.isSelf ? currentUser.name : (target.user?.name || target.remark_name)"
:avatar="msg.isSelf ? currentUser.avatar : (target.user?.avatar || target.remark_name?.charAt(0))"
:color="msg.isSelf ? generateColor(currentUser.id) : target.color"
size="sm"
rounded="lg"
class="cursor-pointer hover:opacity-80 transition self-start mt-1"
@click="$emit('avatar-click', msg.isSelf ? currentUser : target)"
:name="msg.isSelf ? currentUser.name : (target.user?.name || target.remark_name)"
:avatar="msg.isSelf ? currentUser.avatar : (target.user?.avatar || target.remark_name?.charAt(0))"
:color="msg.isSelf ? generateColor(currentUser.id) : target.color"
size="sm"
rounded="lg"
class="cursor-pointer hover:opacity-80 transition self-start mt-1 shadow-md"
@click="$emit('avatar-click', msg.isSelf ? currentUser : target)"
/>
<!-- 消息内容区 -->
<div class="flex flex-col max-w-[75%] md:max-w-[60%]">
<div
class="text-[10px] text-gray-500 mb-1 px-1"
:class="{ 'text-right': msg.isSelf }"
class="text-[10px] text-gray-500 mb-1 px-1"
:class="{ 'text-right': msg.isSelf }"
>
{{ msg.isSelf ? 'Me' : (target.remark_name || target.user?.name) }}
</div>
@@ -109,3 +121,19 @@ defineExpose({
})
</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
v-if="contactStore.leftPanelMode === 'default' && !contactStore.selectedContact"
class="flex-1 flex flex-col items-center justify-center bg-dark text-gray-500 h-full"
v-if="contactStore.leftPanelMode === 'default' && !contactStore.selectedContact"
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">
<i class="fas fa-address-book text-5xl text-primary opacity-80"></i>
@@ -210,8 +210,8 @@
<!-- 好友资料卡视图 -->
<ContactDetailCard
v-else-if="contactStore.selectedContact"
@switch-to-chat="currentTab = 'chat'"
v-else-if="contactStore.selectedContact"
@switch-to-chat="currentTab = 'chat'"
/>
<!-- 好友管理器视图 -->
@@ -220,13 +220,8 @@
<!-- 好友通知视图 -->
<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">
<div class="text-center">
<i class="fas fa-users text-6xl mb-4 opacity-50"></i>
<p class="text-sm">群通知功能开发中</p>
</div>
</div>
<!-- 群通知视图 -->
<GroupNotifyView v-else-if="contactStore.leftPanelMode === 'group-notify'" />
</div>
</div>
<ContextMenu />
@@ -280,6 +275,7 @@ import ContactView from '@/views/contact/ContactView.vue'
import ContactCircle from '@/views/contact/ContactCircle.vue'
import ContactDetailCard from '@/views/contact/ContactDetailCard.vue'
import FriendNotifyView from '@/views/contact/FriendNotifyView.vue'
import GroupNotifyView from '@/views/contact/GroupNotifyView.vue'
const router = useRouter()
const authStore = useAuthStore()
@@ -385,7 +381,7 @@ async function selectChat(contact: Contact) {
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) => ({
@@ -394,17 +390,17 @@ async function selectChat(contact: Contact) {
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) // 初始进入直接跳到底部
@@ -412,7 +408,7 @@ async function selectChat(contact: Contact) {
console.warn('No messages in response:', response)
hasMoreHistory.value[roomId] = false
}
} catch (error) {
} catch (error) {
console.error('Failed to load messages:', error)
toastStore.error('加载消息失败')
hasMoreHistory.value[roomId] = false
@@ -428,48 +424,48 @@ async function selectChat(contact: Contact) {
// 加载更多历史消息(触顶加载)
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)
if (response.data && response.data.length > 0) {
const newMsgs = 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,
}))
// 将新消息添加到现有消息数组的前面(历史消息)
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) {
@@ -494,7 +490,7 @@ 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,
c => c.user_id === conv.target_id || c.id === conv.target_id,
)
if (!contact) {
// 如果本地没有联系人数据,可以在这里补一次联系人列表加载;当前先简单返回
@@ -545,11 +541,11 @@ 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) {
// 如果不是当前聊天窗口,或者是当前窗口但用户不在底部
@@ -580,7 +576,7 @@ function handleWebSocketMessage(message: ChatMessage) {
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') {
async function startCall(type: 'audio' | 'video') {
if (!chatStore.currentTarget) return
const receiverUserId = chatStore.currentTarget.user_id || chatStore.currentTarget.id
// 直接传入 room_id
@@ -640,11 +636,11 @@ 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 => {
@@ -656,7 +652,7 @@ onMounted(async () => {
return
}
}
// 如果没有 room_id 或找不到,使用 targetId 查找
if (savedTargetId) {
const contact = chatStore.contacts.find(c => c.user_id === savedTargetId || c.id === savedTargetId)
@@ -664,7 +660,7 @@ onMounted(async () => {
await selectChat(contact)
}
}
window.addEventListener('resize', () => isMobile.value = window.innerWidth < 768)
})
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>