|
@@ -4,6 +4,7 @@
|
|
|
<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"
|
|
@@ -17,7 +18,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
+
|
|
|
<!-- 右侧聊天区域 -->
|
|
|
<div class="chat-area">
|
|
|
<template v-if="currentUser">
|
|
@@ -25,7 +26,11 @@
|
|
|
<span>{{ currentUser.nickname }}</span>
|
|
|
</div>
|
|
|
<div class="message-list" ref="messageList">
|
|
|
- <div v-for="message in currentMessages.vos" :key="message.senderId" class="message-item"
|
|
|
+ <div v-if="isLoading" class="loading-messages">
|
|
|
+ <el-icon class="is-loading"><Loading /></el-icon>
|
|
|
+ <span>加载更多消息...</span>
|
|
|
+ </div>
|
|
|
+ <div v-for="message in messages" :key="message.senderId" class="message-item"
|
|
|
:class="{ 'message-self': message.senderId === userId }">
|
|
|
<el-avatar :size="40"
|
|
|
:src="message.senderId === userId ? userStore.avatar : currentMessages.conversationAvatar" />
|
|
@@ -64,47 +69,52 @@
|
|
|
</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({
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <script setup>
|
|
|
+ import { ref, computed, onMounted, nextTick, onUnmounted } from 'vue'
|
|
|
+ import dayjs from 'dayjs'
|
|
|
+ import { ElMessage } from 'element-plus'
|
|
|
+ import { getList, sendMsg, getListConversationInfo, setRead,getHistoryMsg } from '@/api/conversation'
|
|
|
+ import useUserStore from '@/store/modules/user'
|
|
|
+ import WebSocketClient from './WebSocketClient.js';
|
|
|
+ import {debounce} from '@/utils/index.js';
|
|
|
+
|
|
|
+ const userStore = useUserStore();
|
|
|
+
|
|
|
+ const userId = computed(() => {
|
|
|
+ return userStore.user.userId
|
|
|
+ })
|
|
|
+ console.log("TCL: userId -> userId", userId)
|
|
|
+
|
|
|
+
|
|
|
+ // 模拟消息数据
|
|
|
+ const messages = ref([])
|
|
|
+
|
|
|
+ const searchQuery = ref('')
|
|
|
+ const currentUser = ref(null)
|
|
|
+ const messageInput = ref('')
|
|
|
+ const messageList = ref(null)
|
|
|
+ const isLoading = ref(false)
|
|
|
+ const currentPage = ref(1)
|
|
|
+ const hasMore = ref(true)
|
|
|
+
|
|
|
+ // 用户列表
|
|
|
+ const filteredUsers = ref([]);
|
|
|
+
|
|
|
+ // 当前用户的消息
|
|
|
+ const currentMessages = ref({
|
|
|
vos: []
|
|
|
-})
|
|
|
-
|
|
|
-const isToday = (date) => {
|
|
|
+ })
|
|
|
+
|
|
|
+ const isToday = (date) => {
|
|
|
return dayjs(date).isSame(dayjs(), 'day');
|
|
|
-};
|
|
|
-
|
|
|
-const isYesterday = (date) => {
|
|
|
+ };
|
|
|
+
|
|
|
+ const isYesterday = (date) => {
|
|
|
return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day');
|
|
|
-};
|
|
|
-const handlerData = (dates) => {
|
|
|
+ };
|
|
|
+ const handlerData = (dates) => {
|
|
|
const date = dayjs(dates);
|
|
|
if (isToday(dates)) {
|
|
|
return date.format('HH:MM');;
|
|
@@ -113,45 +123,106 @@ const handlerData = (dates) => {
|
|
|
} else {
|
|
|
return date.format('YY/MM/DD'); // 或者其他格式如 'YYYY年MM月DD日'
|
|
|
}
|
|
|
-}
|
|
|
-// 格式化时间
|
|
|
-const formatTime = (time) => {
|
|
|
+ }
|
|
|
+ // 格式化时间
|
|
|
+ const formatTime = (time) => {
|
|
|
return handlerData(time)
|
|
|
-}
|
|
|
-
|
|
|
-// 选择用户
|
|
|
-const selectUser = async(user) => {
|
|
|
+ }
|
|
|
+
|
|
|
+ // 选择用户
|
|
|
+ const selectUser = async(user) => {
|
|
|
try {
|
|
|
console.log("TCL: selectUser -> user", user)
|
|
|
+ currentPage.value = 1
|
|
|
+ hasMore.value = true
|
|
|
const res = await getListConversationInfo({
|
|
|
- conversationRecordId: user.conversationRecordId,
|
|
|
+ conversationRecordId: user.conversationRecordId,
|
|
|
+ pageNum: currentPage.value,
|
|
|
+ pageSize: 20
|
|
|
})
|
|
|
const data = res.data;
|
|
|
data.vos = data.vos.reverse(); //倒叙处理
|
|
|
currentMessages.value = data;
|
|
|
+ messages.value= data.vos;
|
|
|
currentUser.value = user
|
|
|
- nextTick(() => {
|
|
|
- scrollToBottom()
|
|
|
- })
|
|
|
+ nextTick(() => {
|
|
|
+ scrollToBottom()
|
|
|
+ })
|
|
|
} catch (error) {
|
|
|
|
|
|
}
|
|
|
-
|
|
|
-}
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载更多消息
|
|
|
+ const loadMoreMessages =debounce(async () => {
|
|
|
+ if (isLoading.value || !hasMore.value || !currentUser.value) return
|
|
|
+
|
|
|
+ // isLoading.value = true
|
|
|
+ // console.log('xx');
|
|
|
+ // setTimeout(() => {
|
|
|
+ // isLoading.value = false
|
|
|
+ // }, 1000)
|
|
|
+
|
|
|
+ // return
|
|
|
+
|
|
|
+ try {
|
|
|
+ isLoading.value = true
|
|
|
+ const oldScrollHeight = messageList.value.scrollHeight
|
|
|
+ const conversationMsgRecordId = messages.value[messages.value.length - 1].conversationMsgRecordId;
|
|
|
+ const res = await getHistoryMsg({
|
|
|
+ conversationMsgRecordId,
|
|
|
+ conversationRecordId: currentUser.value.conversationRecordId
|
|
|
+ })
|
|
|
+
|
|
|
+ const data = res.data
|
|
|
+ console.log("TCL: loadMoreMessages -> data", data)
|
|
|
+ if (data && data.length > 0) {
|
|
|
+ const data_reverse = data.reverse()
|
|
|
+ console.log("TCL: loadMoreMessages -> data_reverse", data_reverse)
|
|
|
+ const mes = [...data, ...messages.value]
|
|
|
+ messages.value = mes;
|
|
|
+ console.log("TCL: loadMoreMessages -> messages.value", messages.value)
|
|
|
+
|
|
|
+ } else {
|
|
|
+ hasMore.value = false
|
|
|
+ }
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ const newScrollHeight = messageList.value.scrollHeight
|
|
|
+ messageList.value.scrollTop = newScrollHeight - oldScrollHeight
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载更多消息失败:', error)
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ }
|
|
|
+ },500)
|
|
|
+
|
|
|
+ // 处理滚动事件
|
|
|
+ const handleScroll = () => {
|
|
|
+ if (!messageList.value) return
|
|
|
+
|
|
|
+ const { scrollTop } = messageList.value
|
|
|
|
|
|
-// 发送消息
|
|
|
-const sendMessage = async (contentType) => {
|
|
|
+ if (scrollTop < 50 && !isLoading.value) {
|
|
|
+
|
|
|
+ loadMoreMessages()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送消息
|
|
|
+ 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,
|
|
@@ -161,9 +232,9 @@ const sendMessage = async (contentType) => {
|
|
|
senderId: userId.value,
|
|
|
system: 3,
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
|
|
|
-
|
|
|
+
|
|
|
await sendMsg(newMessage);
|
|
|
currentMessages.value.vos.push(newMessage);
|
|
|
messageInput.value = ''
|
|
@@ -172,46 +243,46 @@ const sendMessage = async (contentType) => {
|
|
|
})
|
|
|
} 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 = () => {
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 处理图片上传
|
|
|
+ // 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 () => {
|
|
|
+ }
|
|
|
+
|
|
|
+ const getMList = async () => {
|
|
|
try {
|
|
|
const res = await getList({
|
|
|
system: 3
|
|
@@ -219,24 +290,24 @@ const getMList = async () => {
|
|
|
filteredUsers.value = res.rows;
|
|
|
res.rows[0] && selectUser(res.rows[0])
|
|
|
} catch (error) {
|
|
|
-
|
|
|
+
|
|
|
}
|
|
|
-}
|
|
|
-let ws = null;
|
|
|
-
|
|
|
-
|
|
|
-const soketInit = () => {
|
|
|
+ }
|
|
|
+ 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('连接断开')
|
|
@@ -249,195 +320,218 @@ const soketInit = () => {
|
|
|
ws.on('message', (res) => {
|
|
|
console.log('收到消息:', res)
|
|
|
const data = JSON.parse(res.data);
|
|
|
- console.log("TCL: soketInit -> data", 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(() => {
|
|
|
-
|
|
|
+ }
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
soketInit();
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
getMList();//获取用户列表
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.chat-container {
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ // 添加滚动监听
|
|
|
+ if (messageList.value) {
|
|
|
+ messageList.value.addEventListener('scroll', handleScroll)
|
|
|
+ }
|
|
|
+ }, 500)
|
|
|
+ })
|
|
|
+
|
|
|
+ onUnmounted(() => {
|
|
|
+ // 移除滚动监听
|
|
|
+ if (messageList.value) {
|
|
|
+ messageList.value.removeEventListener('scroll', handleScroll)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ </script>
|
|
|
+
|
|
|
+ <style scoped>
|
|
|
+ .chat-container {
|
|
|
display: flex;
|
|
|
height: 100vh;
|
|
|
background-color: #f5f5f5;
|
|
|
-}
|
|
|
-
|
|
|
-.user-list {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-list {
|
|
|
width: 300px;
|
|
|
background-color: #fff;
|
|
|
border-right: 1px solid #e6e6e6;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
-}
|
|
|
-
|
|
|
-.user-list-header {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-list-header {
|
|
|
padding: 10px;
|
|
|
border-bottom: 1px solid #e6e6e6;
|
|
|
-}
|
|
|
-
|
|
|
-.user-list-content {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-list-content {
|
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
|
-}
|
|
|
-
|
|
|
-.user-item {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
padding: 10px;
|
|
|
cursor: pointer;
|
|
|
transition: background-color 0.3s;
|
|
|
-}
|
|
|
-
|
|
|
-.user-item:hover {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-item:hover {
|
|
|
background-color: #f5f5f5;
|
|
|
-}
|
|
|
-
|
|
|
-.user-item.active {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-item.active {
|
|
|
background-color: #e6f7ff;
|
|
|
-}
|
|
|
-
|
|
|
-.user-info {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-info {
|
|
|
flex: 1;
|
|
|
margin-left: 10px;
|
|
|
overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.user-name {
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-name {
|
|
|
font-weight: bold;
|
|
|
margin-bottom: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.last-message {
|
|
|
+ }
|
|
|
+
|
|
|
+ .last-message {
|
|
|
color: #999;
|
|
|
font-size: 12px;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
-}
|
|
|
-
|
|
|
-.message-time {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-time {
|
|
|
font-size: 12px;
|
|
|
color: #999;
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-.chat-area {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ .chat-area {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
background-color: #fff;
|
|
|
-}
|
|
|
-
|
|
|
-.chat-header {
|
|
|
+ }
|
|
|
+
|
|
|
+ .chat-header {
|
|
|
padding: 10px;
|
|
|
border-bottom: 1px solid #e6e6e6;
|
|
|
font-weight: bold;
|
|
|
-}
|
|
|
-
|
|
|
-.message-list {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-list {
|
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
|
padding: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.message-item {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-item {
|
|
|
display: flex;
|
|
|
margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.message-item.message-self {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-item.message-self {
|
|
|
flex-direction: row-reverse;
|
|
|
-}
|
|
|
-
|
|
|
-.message-content {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-content {
|
|
|
margin: 0 10px;
|
|
|
max-width: 60%;
|
|
|
-}
|
|
|
-
|
|
|
-.message-time {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-time {
|
|
|
font-size: 12px;
|
|
|
color: #999;
|
|
|
margin-bottom: 4px;
|
|
|
text-align: left;
|
|
|
-}
|
|
|
-
|
|
|
-.message-bubble {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-bubble {
|
|
|
background-color: #f5f5f5;
|
|
|
padding: 10px;
|
|
|
border-radius: 4px;
|
|
|
word-break: break-all;
|
|
|
|
|
|
-}
|
|
|
-
|
|
|
-.message-self .message-bubble {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-self .message-bubble {
|
|
|
background-color: #95ec69;
|
|
|
text-align: right;
|
|
|
width: 100%;
|
|
|
-}
|
|
|
-
|
|
|
-.message-image {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-image {
|
|
|
max-width: 200px;
|
|
|
max-height: 200px;
|
|
|
border-radius: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.message-input {
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-input {
|
|
|
border-top: 1px solid #e6e6e6;
|
|
|
padding: 10px;
|
|
|
display: flex;
|
|
|
-}
|
|
|
-
|
|
|
-.input-toolbar {
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-toolbar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
padding: 8px 0;
|
|
|
-}
|
|
|
-
|
|
|
-.input-toolbar .el-button {
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-toolbar .el-button {
|
|
|
padding: 4px 8px;
|
|
|
font-size: 14px;
|
|
|
-}
|
|
|
-
|
|
|
-.input-toolbar .el-button:hover {
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-toolbar .el-button:hover {
|
|
|
background-color: #f5f5f5;
|
|
|
border-radius: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.input-area {
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-area {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
padding-top: 10px;
|
|
|
flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.input-area .el-textarea {
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-area .el-textarea {
|
|
|
flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.no-chat-selected {
|
|
|
+ }
|
|
|
+
|
|
|
+ .no-chat-selected {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
height: 100%;
|
|
|
color: #999;
|
|
|
font-size: 16px;
|
|
|
-}
|
|
|
-</style>
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading-messages {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 10px;
|
|
|
+ color: #999;
|
|
|
+ font-size: 14px;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading-messages .el-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+ </style>
|