Files
urbanLifeline/urbanLifelineWeb/packages/shared/src/components/chatRoom/chatRoom/ChatRoom.vue
2025-12-22 17:03:37 +08:00

241 lines
7.5 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 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>