194 lines
6.0 KiB
Vue
194 lines
6.0 KiB
Vue
<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>
|