Bladeren bron

消息中心

chenjj 3 maanden geleden
bovenliggende
commit
36b78ce9cb
6 gewijzigde bestanden met toevoegingen van 747 en 2 verwijderingen
  1. 1 0
      package.json
  2. 3 0
      pnpm-lock.yaml
  3. 82 0
      src/api/conversation.js
  4. 216 0
      src/views/message/WebSocketClient.js
  5. 443 0
      src/views/message/index.vue
  6. 2 2
      vite.config.js

+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "@vueuse/core": "10.11.0",
     "axios": "0.28.1",
     "clipboard": "2.0.11",
+    "dayjs": "^1.11.13",
     "echarts": "5.5.1",
     "element-plus": "2.7.6",
     "file-saver": "2.0.5",

+ 3 - 0
pnpm-lock.yaml

@@ -23,6 +23,9 @@ importers:
       clipboard:
         specifier: 2.0.11
         version: 2.0.11
+      dayjs:
+        specifier: ^1.11.13
+        version: 1.11.13
       echarts:
         specifier: 5.5.1
         version: 5.5.1

+ 82 - 0
src/api/conversation.js

@@ -0,0 +1,82 @@
+import request from '@/utils/request'
+
+
+
+//查询会话记录列表
+export function getList(data) {
+    return request({
+        url: `/core/conversation/list`,
+        method: 'post',
+        data
+    })
+}
+
+
+//发送
+export function sendMsg(data) {
+    return request({
+        url: `/core/conversation/sendMsg`,
+        method: 'post',
+        data
+    })
+}
+
+
+//订单页面 点击沟通 获取会话记录详细信息
+export function getOrderConversationInfo(data) {
+    return request({
+        url: `/core/conversation/getOrderConversationInfo`,
+        method: 'post',
+        data
+    })
+}
+
+//消息列表页面 点击会话 获取会话记录详细信息
+export function getListConversationInfo(params) {
+    return request({
+        url: `/core/conversation/getListConversationInfo`,
+        method: 'get',
+        params
+    })
+}
+
+
+//帮助与客服,获取与客服会话记录和详细信息
+export function getHelpConversation(data) {
+    return request({
+        url: `/core/conversation/getHelpConversation`,
+        method: 'post',
+        data
+    })
+}
+
+
+//打开会话框时,设置已读
+export function setRead(data) {
+    return request({
+        url: `/core/conversation/setRead`,
+        method: 'post',
+        data
+    })
+}
+
+
+
+//删除会话记录
+export function conversationRemove(data) {
+    return request({
+        url: `/core/conversation/remove`,
+        method: 'post',
+        data
+    })
+}
+
+
+//获取历史聊天记录,目前设置为获取10条
+export function getHistoryMsg(data) {
+    return request({
+        url: `/core/conversation/getHistoryMsg`,
+        method: 'post',
+        data
+    })
+}

+ 216 - 0
src/views/message/WebSocketClient.js

@@ -0,0 +1,216 @@
+
+class WebSocketClient {
+    constructor(userId, options = {}) {
+      this.system = '3';
+      this.userId = userId;
+      // const baseURL = import.meta.env.VITE_APP_BASE_API
+      const baseURL = 'https://goldshulin.com/prod-api';
+      const url = baseURL.split('/')[2];
+      const header = baseURL.split('/')[0] === 'https:' ? 'wss' : 'ws';
+      this.url = `${header}://${url}/websocket/${this.system}/${this.userId}`;
+			console.log("TCL: WebSocketClient -> constructor -> this.url", this.url)
+      this.options = {
+        reconnectInterval: 3000, // 重连间隔时间
+        heartbeatInterval: 30000, // 心跳间隔时间
+        heartbeatMessage: 'ping', // 心跳消息
+        ...options
+      }
+      
+      this.ws = null
+      this.reconnectTimer = null
+      this.heartbeatTimer = null
+      this.messageCallbacks = new Map()
+      this.isConnected = false
+      this.isReconnecting = false
+    }
+  
+    // 连接WebSocket
+    connect() {
+      try {
+        this.ws = new WebSocket(this.url)
+        this.bindEvents()
+      } catch (error) {
+        console.error('WebSocket连接错误:', error)
+        this.reconnect()
+      }
+    }
+  
+    // 绑定事件
+    bindEvents() {
+      this.ws.onopen = () => {
+        console.log('WebSocket连接成功')
+        this.isConnected = true
+        this.isReconnecting = false
+        this.startHeartbeat()
+        this.emit('connect')
+      }
+  
+      this.ws.onclose = () => {
+        console.log('WebSocket连接关闭')
+        this.isConnected = false
+        this.stopHeartbeat()
+        this.emit('disconnect')
+        this.reconnect()
+      }
+  
+      this.ws.onerror = (error) => {
+        console.error('WebSocket错误:', error)
+        this.emit('error', error)
+      }
+  
+      this.ws.onmessage = (event) => {
+        try {
+          const message = JSON.parse(event.data)
+          // 处理心跳响应
+          if (message === 'pong') {
+            return
+          }
+          this.handleMessage(message)
+        } catch (error) {
+          console.error('消息解析错误:', error)
+        }
+      }
+    }
+  
+    // 重连
+    reconnect() {
+      if (this.isReconnecting) return
+      
+      this.isReconnecting = true
+      this.reconnectTimer = setTimeout(() => {
+        console.log('尝试重新连接...')
+        this.connect()
+      }, this.options.reconnectInterval)
+    }
+  
+    // 开始心跳
+    startHeartbeat() {
+      this.heartbeatTimer = setInterval(() => {
+        this.send(this.options.heartbeatMessage)
+      }, this.options.heartbeatInterval)
+    }
+  
+    // 停止心跳
+    stopHeartbeat() {
+      if (this.heartbeatTimer) {
+        clearInterval(this.heartbeatTimer)
+        this.heartbeatTimer = null
+      }
+    }
+  
+    // 发送消息
+    send(data) {
+      if (!this.isConnected) {
+        console.warn('WebSocket未连接')
+        return false
+      }
+  
+      try {
+        const message = typeof data === 'string' ? data : JSON.stringify(data)
+        this.ws.send(message)
+        return true
+      } catch (error) {
+        console.error('发送消息错误:', error)
+        return false
+      }
+    }
+  
+    // 处理接收到的消息
+    handleMessage(message) {
+      const { type, data } = message
+      if (this.messageCallbacks.has(type)) {
+        this.messageCallbacks.get(type).forEach(callback => callback(data))
+      }
+      this.emit('message', message)
+    }
+  
+    // 订阅消息
+    on(type, callback) {
+      if (!this.messageCallbacks.has(type)) {
+        this.messageCallbacks.set(type, new Set())
+      }
+      this.messageCallbacks.get(type).add(callback)
+    }
+  
+    // 取消订阅
+    off(type, callback) {
+      if (this.messageCallbacks.has(type)) {
+        if (callback) {
+          this.messageCallbacks.get(type).delete(callback)
+        } else {
+          this.messageCallbacks.delete(type)
+        }
+      }
+    }
+  
+    // 事件发射器
+    emit(event, data) {
+      if (this.messageCallbacks.has(event)) {
+        this.messageCallbacks.get(event).forEach(callback => callback(data))
+      }
+    }
+  
+    // 关闭连接
+    close() {
+      if (this.ws) {
+        this.ws.close()
+      }
+      this.stopHeartbeat()
+      if (this.reconnectTimer) {
+        clearTimeout(this.reconnectTimer)
+        this.reconnectTimer = null
+      }
+    }
+  }
+  
+  // 使用示例
+  /*
+  const ws = new WebSocketClient('ws://your-websocket-server.com', {
+    reconnectInterval: 3000,
+    heartbeatInterval: 30000,
+    heartbeatMessage: 'ping'
+  })
+  
+  // 连接WebSocket
+  ws.connect()
+  
+  // 监听连接事件
+  ws.on('connect', () => {
+    console.log('连接成功')
+  })
+  
+  // 监听断开连接事件
+  ws.on('disconnect', () => {
+    console.log('连接断开')
+  })
+  
+  // 监听错误事件
+  ws.on('error', (error) => {
+    console.error('发生错误:', error)
+  })
+  
+  // 监听消息事件
+  ws.on('message', (message) => {
+    console.log('收到消息:', message)
+  })
+  
+  // 订阅特定类型的消息
+  ws.on('chat', (data) => {
+    console.log('收到聊天消息:', data)
+  })
+  
+  // 发送消息
+  ws.send({
+    type: 'chat',
+    data: {
+      content: 'Hello!',
+      timestamp: Date.now()
+    }
+  })
+  
+  // 关闭连接
+  ws.close()
+  */
+  
+  export default WebSocketClient
+  

+ 443 - 0
src/views/message/index.vue

@@ -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>

+ 2 - 2
vite.config.js

@@ -39,8 +39,8 @@ export default defineConfig(({ mode, command }) => {
         '/dev-api': {
           // target: 'http://192.168.100.139:9527',
           // target: 'https://zybooks.tech/prod-api', 
-          // target: 'http://192.168.100.127:9527',
-          target: 'http://192.168.100.122:9527',
+          target: 'http://192.168.100.128:9527',
+          // target: 'https://goldshulin.com/prod-api',
           changeOrigin: true,
           rewrite: (p) => p.replace(/^\/dev-api/, '')
         },