241 lines
7.5 KiB
Vue
241 lines
7.5 KiB
Vue
<template>
|
||
<div class="chat-room-main">
|
||
<!-- 聊天室头部 -->
|
||
<header class="chat-header">
|
||
<slot name="header">
|
||
<div class="header-default">
|
||
<h3>{{ roomName }}</h3>
|
||
</div>
|
||
</slot>
|
||
</header>
|
||
|
||
<!-- 消息容器 -->
|
||
<div ref="messagesRef" class="messages-container">
|
||
<!-- Jitsi Meet会议iframe -->
|
||
<div v-if="showMeeting && meetingUrl" class="meeting-container">
|
||
<IframeView :src="meetingUrl" />
|
||
</div>
|
||
|
||
<!-- 聊天消息列表 -->
|
||
<div class="messages-list">
|
||
<div
|
||
v-for="message in messages"
|
||
:key="message.messageId"
|
||
class="message-row"
|
||
:class="message.senderId === currentUserId ? 'is-me' : 'other'"
|
||
>
|
||
<!-- 头像 -->
|
||
<div class="message-avatar">
|
||
<img :src="FILE_DOWNLOAD_URL + message.senderAvatar" />
|
||
</div>
|
||
|
||
<!-- 消息内容 -->
|
||
<div class="message-content-wrapper">
|
||
<div class="message-bubble">
|
||
<p class="message-text">{{ message.content }}</p>
|
||
|
||
<!-- 文件列表 -->
|
||
<div v-if="message.files && message.files.length > 0" class="message-files">
|
||
<div
|
||
v-for="file in message.files"
|
||
:key="file"
|
||
class="file-item"
|
||
@click="$emit('download-file', file)"
|
||
>
|
||
<div class="file-icon">
|
||
<FileText :size="16" />
|
||
</div>
|
||
<div class="file-info">
|
||
<div class="file-name">附件</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 输入区域 -->
|
||
<footer class="input-area">
|
||
<!-- 操作按钮区域 -->
|
||
<div class="action-buttons">
|
||
<!-- 发起会议按钮(始终显示) -->
|
||
<button class="action-btn" @click="$emit('start-meeting')">
|
||
<Video :size="18" />
|
||
发起会议
|
||
</button>
|
||
|
||
<!-- 额外的操作按钮插槽 -->
|
||
<slot name="action-area"></slot>
|
||
</div>
|
||
|
||
<!-- 输入框 -->
|
||
<div class="input-wrapper">
|
||
<div class="input-card">
|
||
<div class="input-row">
|
||
<textarea
|
||
ref="textareaRef"
|
||
v-model="inputText"
|
||
@input="adjustHeight"
|
||
@keydown="handleKeyDown"
|
||
placeholder="输入消息..."
|
||
class="chat-textarea"
|
||
/>
|
||
</div>
|
||
<div class="toolbar-row">
|
||
<div class="toolbar-left">
|
||
<button class="tool-btn" @click="selectFiles" title="上传文件">
|
||
<Paperclip :size="18" />
|
||
</button>
|
||
<input
|
||
ref="fileInputRef"
|
||
type="file"
|
||
multiple
|
||
style="display: none"
|
||
@change="handleFileSelect"
|
||
/>
|
||
</div>
|
||
<button
|
||
class="send-btn"
|
||
:class="{ active: inputText.trim() }"
|
||
:disabled="!inputText.trim()"
|
||
@click="sendMessage"
|
||
>
|
||
<Send :size="18" />
|
||
发送
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, nextTick } from 'vue'
|
||
import { FileText, Video, Paperclip, Send } from 'lucide-vue-next'
|
||
import IframeView from '@/components/iframe/IframeView.vue'
|
||
|
||
interface ChatMessageVO {
|
||
messageId: string
|
||
senderId: string
|
||
senderName: string
|
||
senderAvatar: string
|
||
content: string
|
||
files: string[]
|
||
sendTime: string
|
||
}
|
||
|
||
interface Props {
|
||
messages: ChatMessageVO[]
|
||
currentUserId: string
|
||
roomName?: string
|
||
meetingUrl?: string
|
||
showMeeting?: boolean
|
||
fileDownloadUrl?: string
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
roomName: '聊天室',
|
||
showMeeting: false,
|
||
fileDownloadUrl: ''
|
||
})
|
||
|
||
const FILE_DOWNLOAD_URL = props.fileDownloadUrl
|
||
|
||
const emit = defineEmits<{
|
||
'send-message': [content: string, files: File[]]
|
||
'start-meeting': []
|
||
'download-file': [fileId: string]
|
||
}>()
|
||
|
||
defineSlots<{
|
||
header?: () => any
|
||
'action-area'?: () => any
|
||
}>()
|
||
|
||
const inputText = ref('')
|
||
const selectedFiles = ref<File[]>([])
|
||
const messagesRef = ref<HTMLElement | null>(null)
|
||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||
|
||
// 发送消息
|
||
const sendMessage = () => {
|
||
if (!inputText.value.trim() && selectedFiles.value.length === 0) return
|
||
|
||
emit('send-message', inputText.value.trim(), selectedFiles.value)
|
||
|
||
inputText.value = ''
|
||
selectedFiles.value = []
|
||
|
||
if (textareaRef.value) {
|
||
textareaRef.value.style.height = 'auto'
|
||
}
|
||
|
||
scrollToBottom()
|
||
}
|
||
|
||
// 选择文件
|
||
const selectFiles = () => {
|
||
fileInputRef.value?.click()
|
||
}
|
||
|
||
// 处理文件选择
|
||
const handleFileSelect = (e: Event) => {
|
||
const files = (e.target as HTMLInputElement).files
|
||
if (!files || files.length === 0) return
|
||
|
||
selectedFiles.value = Array.from(files)
|
||
}
|
||
|
||
// 滚动到底部
|
||
const scrollToBottom = () => {
|
||
nextTick(() => {
|
||
if (messagesRef.value) {
|
||
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
||
}
|
||
})
|
||
}
|
||
|
||
// 自动调整输入框高度
|
||
const adjustHeight = () => {
|
||
const el = textareaRef.value
|
||
if (el) {
|
||
el.style.height = 'auto'
|
||
el.style.height = Math.min(el.scrollHeight, 150) + 'px'
|
||
}
|
||
}
|
||
|
||
// 键盘事件
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault()
|
||
sendMessage()
|
||
}
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (time: string) => {
|
||
const date = new Date(time)
|
||
const now = new Date()
|
||
const diff = now.getTime() - date.getTime()
|
||
|
||
if (diff < 60000) return '刚刚'
|
||
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
||
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
||
|
||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
|
||
}
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
scrollToBottom
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import url("./ChatRoom.scss");
|
||
</style> |