Refactor code structure and remove redundant changes
This commit is contained in:
657
pages/chat-page/index.vue
Normal file
657
pages/chat-page/index.vue
Normal file
@ -0,0 +1,657 @@
|
||||
<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 { msgType, PageResultMessageRespVO, MemberUserRespDTO } from '@/api/channel/types'
|
||||
import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
||||
// import dayjs from 'dayjs'
|
||||
import useUserStore from '@/store/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>
|
||||
Reference in New Issue
Block a user