658 lines
16 KiB
Vue
658 lines
16 KiB
Vue
<template>
|
|
<div>
|
|
<el-dialog v-model="dialogVisible" lock-scroll :title="chatTitle" width="900px" :before-close="handleClose" destroy-on-close>
|
|
<div class="chat-container">
|
|
<!-- Messages area -->
|
|
<div ref="messageContainer" class="chat-messages">
|
|
<div
|
|
v-for="(message, index) in messages"
|
|
:key="index"
|
|
:class="['message-item', message.fromId === userStore.userInfoRes.id ? 'message-right' : 'message-left']"
|
|
>
|
|
<!-- 左侧消息 -->
|
|
<template v-if="message.fromId !== userStore.userInfoRes.id">
|
|
<div class="avatar"><el-avatar :size="40" :src="message.fromId === userStore.userInfoRes.id ? userStore.userInfoRes.avatar : userAvatar" /></div>
|
|
<div class="message-content left">
|
|
<div v-if="message.msgType === 0" class="text whitespace-pre-wrap">{{ message.content }}</div>
|
|
<div v-else-if="message.msgType === 1" class="image-container">
|
|
<img :src="message.content" alt="Shared image" class="message-image" />
|
|
</div>
|
|
<div v-else>{{ message.content.split('/').pop() }}</div>
|
|
<!-- <div class="time text-left!">{{ message.createTime && formatTime(message.createTime) }}</div> -->
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 右侧消息 -->
|
|
<template v-else>
|
|
<div class="message-content right">
|
|
<div v-if="message.msgType === 0" class="text whitespace-pre-wrap">{{ message.content }}</div>
|
|
<div v-else-if="message.msgType === 1" class="image-container">
|
|
<img :src="message.content" alt="Shared image" class="message-image" />
|
|
</div>
|
|
<!-- <div class="time color-#fff!">{{ message.createTime && formatTime(message.createTime) }}</div> -->
|
|
</div>
|
|
<div class="avatar"><el-avatar :size="40" :src="message.fromId === userStore.userInfoRes.id ? userStore.userInfoRes.avatar : userAvatar" /></div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chat sidebar/info panel -->
|
|
<div class="chat-sidebar">
|
|
<div class="info-section">
|
|
<h3>概述</h3>
|
|
<p>{{ chatTitle }}</p>
|
|
</div>
|
|
<div class="info-section">
|
|
<h3>主要内容</h3>
|
|
<p>{{ chatDescription }}</p>
|
|
</div>
|
|
|
|
<!-- Participants/emoji section -->
|
|
<div class="participants-section">
|
|
<div class="emoji-grid">
|
|
<div v-for="(emoji, index) in emojis" :key="index" class="emoji-item">
|
|
<div class="emoji-avatar">
|
|
<img :src="emoji.avatar" alt="emoji" />
|
|
</div>
|
|
<span>{{ emoji.nickname }}</span>
|
|
</div>
|
|
</div>
|
|
<!-- <div class="view-more" @click="showMoreEmojis"> 查看更多 >> </div> -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Input area -->
|
|
<div class="input-container">
|
|
<div class="toolbar">
|
|
<div ref="emojiButtonRef" class="emoji-button-container">
|
|
<button class="emoji-button" @click="toggleEmojiPanel"
|
|
><el-icon class="tool-icon emoji-trigger"><Sunrise /></el-icon
|
|
></button>
|
|
<div v-if="showEmojiPanel" class="emoji-panel">
|
|
<div class="emoji-grid-popup">
|
|
<div v-for="emoji in commonEmojis" :key="emoji" class="emoji-item-popup" @click="sendEmojiMessage(emoji)">
|
|
{{ emoji }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="image-button" @click="triggerImageUpload"
|
|
><el-icon class="tool-icon"><Picture /></el-icon
|
|
></button>
|
|
<input ref="imageInputRef" type="file" accept="image/*" style="display: none" @change="handleImageUpload" />
|
|
</div>
|
|
<div class="input-wrapper">
|
|
<textarea
|
|
v-model="newMessage"
|
|
type="textarea"
|
|
:rows="2"
|
|
placeholder="输入消息..."
|
|
resize="none"
|
|
maxlength="255"
|
|
@keydown.enter.prevent="(e: any) => (e.shiftKey ? (newMessage += '\n') : sendMessage(0))"
|
|
></textarea>
|
|
</div>
|
|
<button class="send-button" @click="sendMessage(0)">发送</button>
|
|
</div>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { upload } from '~/api/common/index'
|
|
import { Picture, Sunrise } from '@element-plus/icons-vue'
|
|
import { getGroupMembers } from '~/api/channel/index'
|
|
import type { msgType, PageResultMessageRespVO, MemberUserRespDTO } from '~/api/channel/types'
|
|
import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
|
// import dayjs from 'dayjs'
|
|
import useUserStore from '~/stores/user'
|
|
const userStore = useUserStore()
|
|
|
|
const props = defineProps({
|
|
channelId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
chatTitle: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
chatDescription: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
})
|
|
|
|
// 模拟数据
|
|
const userAvatar = 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
|
|
// 添加图片上传相关的ref
|
|
const imageInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
// Chat data with types
|
|
const chatTitle = ref<string>(props.chatTitle)
|
|
const chatDescription = ref<string>(props.chatDescription)
|
|
|
|
// Messages data with type
|
|
const messages = ref<PageResultMessageRespVO['list']>([])
|
|
|
|
// Emojis/participants with type
|
|
const emojis = ref<MemberUserRespDTO[]>([])
|
|
|
|
// Refs with types
|
|
const newMessage = ref<string>('')
|
|
const messageContainer = ref()
|
|
const dialogVisible = defineModel<boolean>('isChat', { required: true })
|
|
|
|
// 表情面板状态
|
|
const showEmojiPanel = ref<boolean>(false)
|
|
|
|
// 常用表情列表
|
|
const commonEmojis = ref<string[]>([
|
|
'😀',
|
|
'😃',
|
|
'😄',
|
|
'😁',
|
|
'😆',
|
|
'😅',
|
|
'😂',
|
|
'🤣',
|
|
'😊',
|
|
'😇',
|
|
'🙂',
|
|
'🙃',
|
|
'😉',
|
|
'😌',
|
|
'😍',
|
|
'🥰',
|
|
'😘',
|
|
'😗',
|
|
'😙',
|
|
'😚',
|
|
'😋',
|
|
'😛',
|
|
'😝',
|
|
'😜',
|
|
'🤪',
|
|
'🤨',
|
|
'🧐',
|
|
'🤓',
|
|
'😎',
|
|
'🤩',
|
|
'🥳',
|
|
'😏',
|
|
'😒',
|
|
'😞',
|
|
'😔',
|
|
'😟',
|
|
'😕',
|
|
'🙁',
|
|
'☹️',
|
|
'😣',
|
|
])
|
|
|
|
// 添加 ref 用于表情按钮容器
|
|
const emojiButtonRef = ref<HTMLElement | null>(null)
|
|
|
|
// Methods with type annotations
|
|
const sendMessage = (msgType: msgType = 0): void => {
|
|
if (!newMessage.value.trim()) return
|
|
|
|
const message = {
|
|
msgType: msgType,
|
|
content: newMessage.value,
|
|
toId: props.channelId, // 对方userId
|
|
userId: userStore.userInfoRes.id, // 当前用户id
|
|
fromId: userStore.userInfoRes.id, // 当前用户id
|
|
createTime: new Date().toISOString(), // 当前时间戳
|
|
}
|
|
|
|
userStore.mqttClient?.publish(`zbjk_message_group/${props.channelId}`, JSON.stringify(message))
|
|
messages.value.push(message)
|
|
newMessage.value = ''
|
|
|
|
nextTick(() => {
|
|
scrollToBottom()
|
|
})
|
|
}
|
|
|
|
const scrollToBottom = (): void => {
|
|
if (messageContainer.value) {
|
|
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
|
}
|
|
}
|
|
|
|
const handleClose = (done: () => void): void => {
|
|
// 取消订阅
|
|
userStore.mqttClient?.unsubscribe(`zbjk_message_group/${props.channelId}`)
|
|
done()
|
|
}
|
|
|
|
// const formatTime = (time: string | number): string => {
|
|
// return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
|
// }
|
|
|
|
// 修改点击处理函数
|
|
const handleClickOutside = (e: MouseEvent): void => {
|
|
if (!emojiButtonRef.value) return
|
|
|
|
// 如果面板没显示,不需要处理
|
|
if (!showEmojiPanel.value) return
|
|
|
|
// 检查点击是否在表情按钮容器外
|
|
if (!emojiButtonRef.value.contains(e.target as Node)) {
|
|
showEmojiPanel.value = false
|
|
}
|
|
}
|
|
|
|
// 修改切换面板函数
|
|
const toggleEmojiPanel = (e: MouseEvent): void => {
|
|
e.stopPropagation() // 阻止事件冒泡
|
|
showEmojiPanel.value = !showEmojiPanel.value
|
|
}
|
|
|
|
// 修改发送表情消息函数
|
|
const sendEmojiMessage = (emoji: string): void => {
|
|
newMessage.value += emoji
|
|
showEmojiPanel.value = false
|
|
}
|
|
|
|
// 触发图片上传
|
|
const triggerImageUpload = (): void => {
|
|
if (imageInputRef.value) {
|
|
imageInputRef.value.click()
|
|
}
|
|
}
|
|
|
|
// 处理图片上传
|
|
const handleImageUpload = async (event: Event): Promise<void> => {
|
|
const target = event.target as HTMLInputElement
|
|
const files = target.files
|
|
|
|
if (files && files.length > 0) {
|
|
const file = files[0]
|
|
|
|
// 检查文件类型 0:文本 1:图片 2:视频 3:文件
|
|
const msgType = file.type.startsWith('image/') ? 1 : file.type.startsWith('video/') ? 2 : 3
|
|
const formData = new FormData()
|
|
formData.append('fieldName', file?.name)
|
|
formData.append('file', file as Blob)
|
|
const res = await upload('/prod-api/app-api/infra/file/upload', formData)
|
|
if (res.code === 0) {
|
|
const imageUrl = res.data
|
|
if (msgType === 1) {
|
|
// 预加载图片
|
|
const img = new Image()
|
|
img.src = imageUrl
|
|
img.onload = () => {
|
|
newMessage.value = imageUrl
|
|
sendMessage(msgType)
|
|
}
|
|
} else {
|
|
newMessage.value = imageUrl
|
|
sendMessage(msgType)
|
|
}
|
|
}
|
|
|
|
// 重置文件输入以允许重复选择同一文件
|
|
if (imageInputRef.value) {
|
|
imageInputRef.value.value = ''
|
|
}
|
|
}
|
|
}
|
|
|
|
// 获取群组成员
|
|
const getGroupMemberlist = async () => {
|
|
const res = await getGroupMembers({ channelId: props.channelId })
|
|
if (res.code === 0) {
|
|
emojis.value = res.data
|
|
}
|
|
}
|
|
|
|
// Lifecycle hooks
|
|
onMounted(() => {
|
|
scrollToBottom()
|
|
document.addEventListener('click', handleClickOutside)
|
|
userStore.mqttClient?.onMessage((topic: string, message: any) => {
|
|
if (topic.indexOf(`zbjk_message_group`) === -1) return
|
|
const msg = JSON.parse(message)
|
|
if (msg.userId === userStore.userInfoRes.id) return
|
|
console.log('接收消息---------', topic, message)
|
|
msg.msgType = Number(msg.msgType)
|
|
messages.value.push(msg)
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
})
|
|
|
|
// Watchers
|
|
watch(
|
|
() => messages.value,
|
|
() => {
|
|
nextTick(() => {
|
|
scrollToBottom()
|
|
})
|
|
},
|
|
{ deep: true }
|
|
)
|
|
watch(
|
|
() => props.channelId,
|
|
(val) => {
|
|
if (val) {
|
|
getGroupMemberlist()
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
</script>
|
|
|
|
<style scoped>
|
|
::v-deep(.el-dialog__body) {
|
|
padding: 0px !important;
|
|
overflow-y: hidden;
|
|
}
|
|
.chat-container {
|
|
display: grid;
|
|
grid-template-columns: 1fr 300px;
|
|
grid-template-rows: 0px 1fr 160px;
|
|
grid-template-areas:
|
|
'header header'
|
|
'messages sidebar'
|
|
'input sidebar';
|
|
height: 64vh;
|
|
background-color: #f0f2f5;
|
|
font-family: Arial, sans-serif;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.chat-messages {
|
|
grid-area: messages;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
border-left: 1px solid #eee;
|
|
background-color: #fff;
|
|
}
|
|
|
|
.message-item {
|
|
display: flex;
|
|
margin-bottom: 15px;
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
}
|
|
|
|
.message-left {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.message-right {
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background-color: #eee;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #666;
|
|
font-weight: bold;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.message-content {
|
|
max-width: 60%;
|
|
padding: 10px;
|
|
margin: 0 10px;
|
|
position: relative;
|
|
}
|
|
|
|
.message-content.left {
|
|
background-color: #ffffff;
|
|
border-radius: 12px;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.message-content.right {
|
|
background-color: #1a65ff;
|
|
border-radius: 12px;
|
|
color: #fff;
|
|
}
|
|
|
|
.nickname {
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.text {
|
|
word-break: break-word;
|
|
line-height: 1.4;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.time {
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-top: 5px;
|
|
text-align: right;
|
|
}
|
|
|
|
/* 文件消息样式 */
|
|
.message-content.file {
|
|
background-color: #f8f9fa;
|
|
padding: 12px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.chat-sidebar {
|
|
grid-area: sidebar;
|
|
padding: 20px;
|
|
background-color: #f8fafc;
|
|
border-left: 1px solid #eee;
|
|
border-right: 1px solid #eee;
|
|
border-bottom: 1px solid #eee;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.info-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.info-section h3 {
|
|
font-size: 16px;
|
|
color: #666;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.participants-section {
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.emoji-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 10px;
|
|
}
|
|
|
|
.emoji-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.emoji-item:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.emoji-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.emoji-item:hover .emoji-avatar {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.emoji-item span {
|
|
font-size: 12px;
|
|
color: #888;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.view-more {
|
|
text-align: right;
|
|
margin-top: 10px;
|
|
color: #1976d2;
|
|
cursor: pointer;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.view-more:hover {
|
|
color: #1565c0;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.input-container {
|
|
grid-area: input;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-top: 1px solid #eee;
|
|
background-color: white;
|
|
border-left: 1px solid #eee;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
padding: 10px;
|
|
gap: 10px;
|
|
}
|
|
|
|
.toolbar button {
|
|
background: none;
|
|
border: none;
|
|
font-size: 20px;
|
|
color: #666;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.input-wrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
padding: 0 10px;
|
|
}
|
|
|
|
textarea {
|
|
flex: 1;
|
|
border: none;
|
|
resize: none;
|
|
padding: 10px;
|
|
font-size: 14px;
|
|
outline: none;
|
|
}
|
|
|
|
.send-button {
|
|
margin: 10px;
|
|
padding: 5px 20px;
|
|
background-color: #1a65ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
align-self: flex-end;
|
|
}
|
|
|
|
.emoji-button-container {
|
|
position: relative;
|
|
}
|
|
|
|
.emoji-panel {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 0;
|
|
background: white;
|
|
border: 1px solid #eee;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
padding: 10px;
|
|
z-index: 1000;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.emoji-grid-popup {
|
|
display: grid;
|
|
grid-template-columns: repeat(8, 1fr);
|
|
gap: 5px;
|
|
width: 320px;
|
|
}
|
|
|
|
.emoji-item-popup {
|
|
width: 30px;
|
|
height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
font-size: 20px;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.emoji-item-popup:hover {
|
|
transform: scale(1.2);
|
|
background-color: #f5f5f5;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.toolbar button {
|
|
background: none;
|
|
border: none;
|
|
font-size: 20px;
|
|
color: #666;
|
|
cursor: pointer;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.toolbar button:hover {
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.message-content.file {
|
|
background-color: #f8f9fa;
|
|
padding: 12px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.message-image {
|
|
max-width: 100%;
|
|
max-height: 300px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.image-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
</style>
|