|
@@ -0,0 +1,443 @@
|
|
|
+<template>
|
|
|
+ <div class="chat-container">
|
|
|
+ <!-- 左侧用户列表 -->
|
|
|
+ <div class="user-list">
|
|
|
+ <div class="user-list-header">
|
|
|
+ <el-input v-model="searchQuery" placeholder="搜索" prefix-icon="el-icon-search" clearable />
|
|
|
+ </div>
|
|
|
+ <div class="user-list-content">
|
|
|
+ <div v-for="user in filteredUsers" :key="user.userId" class="user-item"
|
|
|
+ :class="{ active: currentUser?.userId === user.userId }" @click="selectUser(user)">
|
|
|
+ <el-avatar :size="40" :src="user.conversationAvatar" />
|
|
|
+ <div class="user-info">
|
|
|
+ <div class="user-name">{{ user.userName }}</div>
|
|
|
+ <div class="last-message">{{ user.newestMsgContent }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="message-time">{{ formatTime(user.newestMsgTime) }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧聊天区域 -->
|
|
|
+ <div class="chat-area">
|
|
|
+ <template v-if="currentUser">
|
|
|
+ <div class="chat-header">
|
|
|
+ <span>{{ currentUser.nickname }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="message-list" ref="messageList">
|
|
|
+ <div v-for="message in currentMessages.vos" :key="message.senderId" class="message-item"
|
|
|
+ :class="{ 'message-self': message.senderId === userId }">
|
|
|
+ <el-avatar :size="40"
|
|
|
+ :src="message.senderId === userId ? userStore.avatar : currentMessages.conversationAvatar" />
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-time"
|
|
|
+ :style="{ textAlign: message.senderId === userId ? 'right' : 'left' }">{{
|
|
|
+ message.msgSendTime }}</div>
|
|
|
+ <div class="message-bubble">
|
|
|
+ <template v-if="message.msgType === '1'">
|
|
|
+ {{ message.msgContent }}
|
|
|
+ </template>
|
|
|
+ <template v-else-if="message.type === '2'">
|
|
|
+ <el-image :src="message.msgContent" :preview-src-list="[message.msgContent]"
|
|
|
+ fit="cover" class="message-image" />
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="message-input">
|
|
|
+ <!-- <div class="input-toolbar">
|
|
|
+ <el-upload action="#" :auto-upload="false" :show-file-list="false"
|
|
|
+ :on-change="handleImageUpload">
|
|
|
+ <el-button type="text" :icon="Picture" title="发送图片">图片</el-button>
|
|
|
+ </el-upload>
|
|
|
+ </div> -->
|
|
|
+ <div class="input-area">
|
|
|
+ <el-input v-model="messageInput" type="textarea" :rows="3" placeholder="请输入消息"
|
|
|
+ @keyup.enter.native="sendMessage" />
|
|
|
+ <el-button type="primary" @click="sendMessage('text')">发送</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div v-else class="no-chat-selected">
|
|
|
+ 请选择一个聊天对象
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted, nextTick } from 'vue'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { getList, sendMsg, getListConversationInfo, setRead } from '@/api/conversation'
|
|
|
+import useUserStore from '@/store/modules/user'
|
|
|
+import WebSocketClient from './WebSocketClient.js';
|
|
|
+const userStore = useUserStore();
|
|
|
+
|
|
|
+const userId = computed(() => {
|
|
|
+ return userStore.user.deptId
|
|
|
+})
|
|
|
+console.log("TCL: userId -> userId", userId)
|
|
|
+
|
|
|
+
|
|
|
+// 模拟消息数据
|
|
|
+const messages = ref([])
|
|
|
+
|
|
|
+const searchQuery = ref('')
|
|
|
+const currentUser = ref(null)
|
|
|
+const messageInput = ref('')
|
|
|
+const messageList = ref(null)
|
|
|
+
|
|
|
+// 用户列表
|
|
|
+const filteredUsers = ref([]);
|
|
|
+
|
|
|
+// 当前用户的消息
|
|
|
+const currentMessages = ref({
|
|
|
+ vos: []
|
|
|
+})
|
|
|
+
|
|
|
+const isToday = (date) => {
|
|
|
+ return dayjs(date).isSame(dayjs(), 'day');
|
|
|
+};
|
|
|
+
|
|
|
+const isYesterday = (date) => {
|
|
|
+ return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day');
|
|
|
+};
|
|
|
+const handlerData = (dates) => {
|
|
|
+ const date = dayjs(dates);
|
|
|
+ if (isToday(dates)) {
|
|
|
+ return date.format('HH:MM');;
|
|
|
+ } else if (isYesterday(dates)) {
|
|
|
+ return '昨天';
|
|
|
+ } else {
|
|
|
+ return date.format('YY/MM/DD'); // 或者其他格式如 'YYYY年MM月DD日'
|
|
|
+ }
|
|
|
+}
|
|
|
+// 格式化时间
|
|
|
+const formatTime = (time) => {
|
|
|
+ return handlerData(time)
|
|
|
+}
|
|
|
+
|
|
|
+// 选择用户
|
|
|
+const selectUser = async(user) => {
|
|
|
+ try {
|
|
|
+ console.log("TCL: selectUser -> user", user)
|
|
|
+ const res = await getListConversationInfo({
|
|
|
+ conversationRecordId: user.conversationRecordId,
|
|
|
+ })
|
|
|
+ const data = res.data;
|
|
|
+ data.vos = data.vos.reverse(); //倒叙处理
|
|
|
+ currentMessages.value = data;
|
|
|
+ currentUser.value = user
|
|
|
+ nextTick(() => {
|
|
|
+ scrollToBottom()
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// 发送消息
|
|
|
+const sendMessage = async (contentType) => {
|
|
|
+ try {
|
|
|
+ if (!messageInput.value.trim()) {
|
|
|
+ ElMessage.warning('消息不能为空')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!currentUser.value) {
|
|
|
+ ElMessage.warning('请先选择一个聊天对象')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const newMessage = {
|
|
|
+ conversationRecordId: currentUser.value.conversationRecordId,
|
|
|
+ msgContent: messageInput.value,
|
|
|
+ msgSendTime: dayjs(new Date().getTime()).format('YYYY-MM-DD HH:MM:SS') ,
|
|
|
+ msgType: contentType == 'text' ? '1' : '2',//消息类型 1文字消息 2图片消息
|
|
|
+ userId: userId.value,
|
|
|
+ senderId: userId.value,
|
|
|
+ system: 3,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ await sendMsg(newMessage);
|
|
|
+ currentMessages.value.vos.push(newMessage);
|
|
|
+ messageInput.value = ''
|
|
|
+ nextTick(() => {
|
|
|
+ scrollToBottom()
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.log("TCL: sendMessage -> error", error)
|
|
|
+
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 处理图片上传
|
|
|
+// const handleImageUpload = (file) => {
|
|
|
+// if (!currentUser.value) {
|
|
|
+// ElMessage.warning('请先选择一个聊天对象')
|
|
|
+// return
|
|
|
+// }
|
|
|
+
|
|
|
+// const reader = new FileReader()
|
|
|
+// reader.onload = (e) => {
|
|
|
+// const newMessage = {
|
|
|
+// id: Date.now(),
|
|
|
+// type: 'image',
|
|
|
+// content: e.target.result,
|
|
|
+// time: new Date(),
|
|
|
+// isSelf: true,
|
|
|
+// }
|
|
|
+
|
|
|
+
|
|
|
+// currentMessages.value.push(newMessage)
|
|
|
+// nextTick(() => {
|
|
|
+// scrollToBottom()
|
|
|
+// })
|
|
|
+// }
|
|
|
+// reader.readAsDataURL(file.raw)
|
|
|
+// }
|
|
|
+
|
|
|
+// 滚动到底部
|
|
|
+const scrollToBottom = () => {
|
|
|
+ if (messageList.value) {
|
|
|
+ messageList.value.scrollTop = messageList.value.scrollHeight
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getMList = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getList({
|
|
|
+ system: 3
|
|
|
+ })
|
|
|
+ filteredUsers.value = res.rows;
|
|
|
+ res.rows[0] && selectUser(res.rows[0])
|
|
|
+ } catch (error) {
|
|
|
+
|
|
|
+ }
|
|
|
+}
|
|
|
+let ws = null;
|
|
|
+
|
|
|
+
|
|
|
+const soketInit = () => {
|
|
|
+ try {
|
|
|
+ //获取账户时,连接soket
|
|
|
+ ws = new WebSocketClient(userId.value);
|
|
|
+ // 连接WebSocket
|
|
|
+ ws.connect()
|
|
|
+
|
|
|
+ // 监听连接事件
|
|
|
+ ws.on('connect', () => {
|
|
|
+ console.log('连接成功')
|
|
|
+ })
|
|
|
+
|
|
|
+ // 监听断开连接事件
|
|
|
+ ws.on('disconnect', () => {
|
|
|
+ console.log('连接断开')
|
|
|
+ })
|
|
|
+ // 监听错误事件
|
|
|
+ ws.on('error', (error) => {
|
|
|
+ console.error('发生错误:', error)
|
|
|
+ })
|
|
|
+ // 监听消息事件
|
|
|
+ ws.on('message', (res) => {
|
|
|
+ console.log('收到消息:', res)
|
|
|
+ const data = JSON.parse(res.data);
|
|
|
+ console.log("TCL: soketInit -> data", data)
|
|
|
+ if (res.type === 'msgNew') {
|
|
|
+ currentMessages.value.push({...res.data})
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.log("TCL: soketInit -> error", error)
|
|
|
+
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+
|
|
|
+ soketInit();
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ getMList();//获取用户列表
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.chat-container {
|
|
|
+ display: flex;
|
|
|
+ height: 100vh;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.user-list {
|
|
|
+ width: 300px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-right: 1px solid #e6e6e6;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.user-list-header {
|
|
|
+ padding: 10px;
|
|
|
+ border-bottom: 1px solid #e6e6e6;
|
|
|
+}
|
|
|
+
|
|
|
+.user-list-content {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.user-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.user-item:hover {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.user-item.active {
|
|
|
+ background-color: #e6f7ff;
|
|
|
+}
|
|
|
+
|
|
|
+.user-info {
|
|
|
+ flex: 1;
|
|
|
+ margin-left: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.user-name {
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.last-message {
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.message-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+.chat-area {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background-color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.chat-header {
|
|
|
+ padding: 10px;
|
|
|
+ border-bottom: 1px solid #e6e6e6;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.message-list {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-item {
|
|
|
+ display: flex;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-item.message-self {
|
|
|
+ flex-direction: row-reverse;
|
|
|
+}
|
|
|
+
|
|
|
+.message-content {
|
|
|
+ margin: 0 10px;
|
|
|
+ max-width: 60%;
|
|
|
+}
|
|
|
+
|
|
|
+.message-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.message-bubble {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ word-break: break-all;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+.message-self .message-bubble {
|
|
|
+ background-color: #95ec69;
|
|
|
+ text-align: right;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.message-image {
|
|
|
+ max-width: 200px;
|
|
|
+ max-height: 200px;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-input {
|
|
|
+ border-top: 1px solid #e6e6e6;
|
|
|
+ padding: 10px;
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.input-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.input-toolbar .el-button {
|
|
|
+ padding: 4px 8px;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.input-toolbar .el-button:hover {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.input-area {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ padding-top: 10px;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.input-area .el-textarea {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.no-chat-selected {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100%;
|
|
|
+ color: #999;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+</style>
|