401 lines
8.9 KiB
Vue
401 lines
8.9 KiB
Vue
<template>
|
|
<div class="flex items-start gap-3 mb-4" :class="isSent ? 'flex-row-reverse' : 'flex-row'">
|
|
<!-- 头像 -->
|
|
<div
|
|
class="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-sm flex-shrink-0"
|
|
:style="avatarStyle"
|
|
>
|
|
{{ senderInfo.avatar }}
|
|
</div>
|
|
|
|
<!-- 消息内容区域 -->
|
|
<div class="flex flex-col max-w-xs md:max-w-md lg:max-w-lg xl:max-w-xl" :class="isSent ? 'items-end' : 'items-start'">
|
|
<!-- 发送者名称 -->
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1 px-2">
|
|
{{ senderInfo.name }}
|
|
</div>
|
|
|
|
<!-- 消息气泡 -->
|
|
<div
|
|
class="rounded-2xl p-4 shadow-sm relative"
|
|
:class="bubbleClasses"
|
|
>
|
|
<!-- 图片消息 -->
|
|
<div
|
|
v-if="message.type === 'image'"
|
|
class="cursor-pointer relative"
|
|
@click="previewMedia(message.content, 'image')"
|
|
>
|
|
<img
|
|
:src="message.content"
|
|
:alt="'图片消息'"
|
|
class="rounded-lg max-w-full"
|
|
@error="handleImageError"
|
|
/>
|
|
<div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity bg-black/50 rounded-lg">
|
|
<i class="fas fa-search-plus text-white text-xl"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 视频消息 -->
|
|
<div
|
|
v-else-if="message.type === 'video'"
|
|
class="cursor-pointer relative video-message"
|
|
@click="previewMedia(message.content, 'video')"
|
|
>
|
|
<video
|
|
:src="message.content"
|
|
class="rounded-lg max-w-full video-thumbnail"
|
|
preload="metadata"
|
|
@error="handleVideoError"
|
|
/>
|
|
<div class="video-overlay">
|
|
<div class="play-button">
|
|
<i class="fas fa-play"></i>
|
|
</div>
|
|
<div class="video-duration" v-if="message.duration">
|
|
{{ formatDuration(message.duration) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- URL链接消息 -->
|
|
<div v-else-if="message.type === 'url'" class="url-message">
|
|
<div class="url-preview" @click="openUrl(message.url)">
|
|
<div class="url-favicon">
|
|
<img :src="message.favicon" :alt="message.domain" v-if="message.favicon" />
|
|
<i class="fas fa-link" v-else></i>
|
|
</div>
|
|
<div class="url-content">
|
|
<div class="url-title">{{ message.title || message.url }}</div>
|
|
<div class="url-description" v-if="message.description">
|
|
{{ message.description }}
|
|
</div>
|
|
<div class="url-domain">{{ message.domain }}</div>
|
|
</div>
|
|
<div class="url-thumbnail" v-if="message.thumbnail">
|
|
<img :src="message.thumbnail" :alt="message.title" />
|
|
</div>
|
|
</div>
|
|
<div class="url-text" v-if="message.text">
|
|
{{ message.text }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 音频消息 -->
|
|
<AudioMessage v-else-if="message.type === 'audio'" :message="message" :is-sent="isSent" />
|
|
|
|
<!-- 文本消息 -->
|
|
<div v-else class="text-base leading-relaxed whitespace-pre-wrap">
|
|
<span v-html="formatTextWithLinks(message.content)"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 消息时间和状态 -->
|
|
<div class="flex items-center gap-1 mt-1 text-xs text-gray-400 dark:text-gray-500 px-2">
|
|
<span>{{ message.time }}</span>
|
|
<i v-if="isSent && message.read" class="fas fa-check-double text-green-500"></i>
|
|
<i v-else-if="isSent && !message.read" class="fas fa-clock text-gray-400"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue';
|
|
import { useMediaPreview } from '@/composables/useMediaPreview';
|
|
import { useThemeStore } from '@/stores/theme';
|
|
import AudioMessage from './AudioMessage.vue';
|
|
|
|
const previewMedia = useMediaPreview();
|
|
const themeStore = useThemeStore();
|
|
|
|
const props = defineProps({
|
|
message: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
isSent: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
senderInfo: {
|
|
type: Object,
|
|
required: true
|
|
}
|
|
});
|
|
|
|
const avatarStyle = computed(() => ({
|
|
background: props.senderInfo.color
|
|
}));
|
|
|
|
const bubbleClasses = computed(() => {
|
|
const baseClasses = ['message-bubble'];
|
|
|
|
if (props.isSent) {
|
|
baseClasses.push('sent');
|
|
} else {
|
|
baseClasses.push('received');
|
|
}
|
|
|
|
if (themeStore.isDarkMode) {
|
|
baseClasses.push('dark');
|
|
}
|
|
|
|
return baseClasses;
|
|
});
|
|
|
|
const formatDuration = (seconds) => {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const formatTextWithLinks = (text) => {
|
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
return text.replace(urlRegex, '<a href="$1" target="_blank" rel="noopener noreferrer" class="text-link">$1</a>');
|
|
};
|
|
|
|
const openUrl = (url) => {
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.message-bubble {
|
|
position: relative;
|
|
max-width: 100%;
|
|
word-wrap: break-word;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.message-bubble.sent {
|
|
background: linear-gradient(135deg, #4361ee, #3f37c9);
|
|
color: white;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
|
|
.message-bubble.received {
|
|
background: white;
|
|
color: #1a202c;
|
|
border: 1px solid #e2e8f0;
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
|
|
.message-bubble.received.dark {
|
|
background: #374151;
|
|
color: #f7fafc;
|
|
border-color: #4b5563;
|
|
}
|
|
|
|
.video-message {
|
|
position: relative;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.video-thumbnail {
|
|
width: 100%;
|
|
height: auto;
|
|
max-height: 200px;
|
|
object-fit: cover;
|
|
display: block;
|
|
}
|
|
|
|
.video-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.video-message:hover .video-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
.play-button {
|
|
width: 60px;
|
|
height: 60px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 24px;
|
|
backdrop-filter: blur(10px);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.play-button:hover {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.video-duration {
|
|
position: absolute;
|
|
bottom: 8px;
|
|
right: 8px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.url-message {
|
|
max-width: 400px;
|
|
}
|
|
|
|
.url-preview {
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
background: white;
|
|
}
|
|
|
|
.url-preview:hover {
|
|
border-color: #4361ee;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 20px rgba(67, 97, 238, 0.15);
|
|
}
|
|
|
|
.message-bubble.sent .url-preview {
|
|
border-color: rgba(255, 255, 255, 0.3);
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.message-bubble.received.dark .url-preview {
|
|
border-color: #4b5563;
|
|
background: #4b5563;
|
|
}
|
|
|
|
.url-preview {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
padding: 12px;
|
|
gap: 12px;
|
|
}
|
|
|
|
.url-favicon {
|
|
width: 20px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.url-favicon img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.url-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.url-title {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
margin-bottom: 4px;
|
|
color: #1a202c;
|
|
line-height: 1.4;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.message-bubble.sent .url-title {
|
|
color: white;
|
|
}
|
|
|
|
.message-bubble.received.dark .url-title {
|
|
color: #f7fafc;
|
|
}
|
|
|
|
.url-description {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
margin-bottom: 4px;
|
|
line-height: 1.3;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.message-bubble.sent .url-description {
|
|
color: rgba(255, 255, 255, 0.8);
|
|
}
|
|
|
|
.message-bubble.received.dark .url-description {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.url-domain {
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.message-bubble.sent .url-domain {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
|
|
.message-bubble.received.dark .url-domain {
|
|
color: #6b7280;
|
|
}
|
|
|
|
.url-thumbnail {
|
|
width: 60px;
|
|
height: 60px;
|
|
flex-shrink: 0;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.url-thumbnail img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.url-text {
|
|
margin-top: 8px;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
:deep(.text-link) {
|
|
color: #4361ee;
|
|
text-decoration: underline;
|
|
transition: color 0.2s ease;
|
|
}
|
|
|
|
:deep(.text-link:hover) {
|
|
color: #3f37c9;
|
|
}
|
|
|
|
.message-bubble.sent :deep(.text-link) {
|
|
color: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
.message-bubble.sent :deep(.text-link:hover) {
|
|
color: white;
|
|
}
|
|
</style>
|