Files
nl-um-vue-ts/src/components/chat/MessageList.vue
2025-12-05 13:13:12 +08:00

194 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div
id="msgListContainer"
ref="containerRef"
class="flex-1 overflow-y-auto p-4 space-y-5"
@dragover.prevent
@drop.prevent
@scroll="handleScroll"
>
<!-- 顶部加载状态提示区 -->
<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-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="getSenderName(msg)"
:avatar="getSenderAvatar(msg)"
:color="msg.isSelf ? generateColor(currentUser.id) : (getMemberColor(msg.sender_user_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
v-if="isGroupChat && !msg.isSelf"
class="text-[10px] text-gray-500 mb-1 px-1"
>
{{ getSenderDisplayName(msg) }}
</div>
<!-- 单聊或自己发送的消息 -->
<div
v-else-if="!isGroupChat || 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>
<!-- 消息气泡 -->
<MessageBubble :message="msg" @click="$emit('bubble-click', $event, msg)" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { generateColor } from '@/utils/format'
import Avatar from '@/components/common/Avatar.vue'
import MessageBubble from '@/components/chat/MessageBubble.vue'
import type { ChatMessage, User, Contact } from '@/types/api'
interface Props {
messages: ChatMessage[]
currentUser: User
target: Contact
loadingHistory?: boolean
hasMoreHistory?: boolean
roomMembers?: Record<string, { name: string; avatar?: string }> // 群成员映射 roomId -> { name, avatar }
}
const props = defineProps<Props>()
const emit = defineEmits<{
'contextmenu': [event: MouseEvent, message: ChatMessage]
'avatar-click': [user: User | Contact]
'bubble-click': [event: MouseEvent, message: ChatMessage]
'load-more': []
}>()
const containerRef = ref<HTMLElement | null>(null)
// 判断是否为群聊
const isGroupChat = computed(() => props.target.is_group || props.target.room_type === 'group')
// 获取发送者名称
function getSenderName(msg: ChatMessage): string {
if (msg.isSelf) {
return props.currentUser.name
}
if (isGroupChat.value && props.roomMembers?.[msg.sender_user_id]) {
return props.roomMembers[msg.sender_user_id].name
}
return props.target.user?.name || props.target.remark_name || '未知用户'
}
// 获取发送者头像
function getSenderAvatar(msg: ChatMessage): string {
if (msg.isSelf) {
return props.currentUser.avatar
}
if (isGroupChat.value && props.roomMembers?.[msg.sender_user_id]) {
return props.roomMembers[msg.sender_user_id].avatar || getSenderName(msg).charAt(0)
}
return props.target.user?.avatar || props.target.remark_name?.charAt(0) || '?'
}
// 获取发送者显示名称(群聊中显示)
function getSenderDisplayName(msg: ChatMessage): string {
if (msg.isSelf) {
return 'Me'
}
if (isGroupChat.value && props.roomMembers?.[msg.sender_user_id]) {
return props.roomMembers[msg.sender_user_id].name
}
return props.target.user?.name || props.target.remark_name || '未知用户'
}
// 获取成员颜色
function getMemberColor(userId: string): string | undefined {
if (props.roomMembers?.[userId]) {
return generateColor(userId)
}
return undefined
}
// 节流函数
function throttle<T extends (...args: any[]) => any>(func: T, delay: number): T {
let lastCall = 0
return ((...args: any[]) => {
const now = Date.now()
if (now - lastCall >= delay) {
lastCall = now
return func(...args)
}
}) as T
}
// 滚动处理(节流优化)
const handleScroll = throttle((e: Event) => {
const target = e.target as HTMLElement
// 检测是否滚动到顶部(阈值设为 50px提前加载
if (target.scrollTop < 50 && !props.loadingHistory && props.hasMoreHistory) {
emit('load-more')
}
}, 200)
// 暴露容器引用,供父组件使用
defineExpose({
containerRef,
})
</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>