Explorar o código

feat:websocket联调&工具函数等

yangfeng %!s(int64=3) %!d(string=hai) anos
pai
achega
a891ff2d35

+ 1 - 1
.env.development

@@ -2,4 +2,4 @@ NODE_ENV=development
 VUE_APP_BASE_API='/api'
 VUE_APP_BASE_URL='/'
 VUE_APP_BASE_PUBLIC='/'
-VUE_APP_WEBSOCKET_URL='ws://'
+VUE_APP_WEBSOCKET_API='/socket'

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "dayjs": "^1.11.3",
     "element-ui": "2.15.7",
     "lodash": "^4.17.21",
+    "moment": "^2.29.4",
     "register-service-worker": "^1.7.2",
     "vue": "^2.6.14",
     "vue-router": "^3.5.1",

+ 1 - 1
src/api/modules/orderCenter.js

@@ -3,7 +3,7 @@ import request from '@/api'
 // 获取订单列表
 export function getOrderList (data) {
   return request({
-    url: '/orderCenter/order/list',
+    url: '/orderApplication/order/orderList',
     method: 'post',
     data
   })

+ 3 - 0
src/assets/style/msg-layout.scss

@@ -42,6 +42,9 @@ $bg-chat-color: #F6F6F6;
       margin: 0 11px;
       background: #E0E0E0;
     }
+    ::-webkit-scrollbar{
+      width: 0;
+    }
   }
   &chat-group {
     min-width: 560px;

+ 40 - 3
src/components/CustomerInfo.vue

@@ -18,6 +18,14 @@
       <Cell title="订单信息">
         <div class="item-main order">
           <Order v-for="order in orderList" :key="order.id" :order="order"></Order>
+          <el-pagination
+            background
+            layout="prev, pager, next"
+            :page-size="listInfo.pageSize"
+            :current-page="listInfo.pageNum"
+            @current-change="handleCurrentChange"
+            :total="listInfo.total">
+          </el-pagination>
         </div>
       </Cell>
       <Cell title="服务总结">
@@ -30,10 +38,11 @@
 </template>
 
 <script>
+import { Pagination } from 'element-ui'
 import Cell from '@/components/fold-cell/'
 import Order from '@/components/order-item/'
 import Summary from '@/components/summary/'
-import { getUserInfo } from '@/api/modules/'
+import { getUserInfo, getOrderList } from '@/api/modules/'
 export default {
   name: 'CustomerInfo',
   props: {
@@ -45,7 +54,8 @@ export default {
   components: {
     Cell,
     Order,
-    Summary
+    Summary,
+    [Pagination.name]: Pagination
   },
   data () {
     return {
@@ -58,6 +68,12 @@ export default {
         keywords: []
       },
       keywords: [],
+      listInfo: {
+        pageNum: 1,
+        pageSize: 3,
+        total: 0,
+        list: []
+      },
       orderList: [
         { id: '001', time: '2021.06.15 15:45:23', status: '待付款', price: '1339' },
         { id: '002', time: '2021.09.15 15:45:23', status: '已完成', price: '1339' }
@@ -66,11 +82,12 @@ export default {
   },
   mounted () {
     this.getUserInfoFn()
+    this.getOrderList()
   },
   methods: {
     async getUserInfoFn () {
       const { data } = await getUserInfo({
-        userId: this.userId
+        uid: this.userId
       })
       if (data) {
         this.loading = false
@@ -82,6 +99,26 @@ export default {
         this.userInfo.keywords = this.formatKeywords(keywords)
       }
     },
+    async getOrderList () {
+      const { data } = await getOrderList({
+        appid: '10000',
+        user_id: this.userId,
+        page_num: this.listInfo.pageNum,
+        page_size: this.listInfo.pageSize
+      })
+      if (data) {
+        const { order_list: list, count } = data
+        this.listInfo.total = count
+        this.listInfo.list = list
+      } else {
+        this.listInfo.list = []
+        this.listInfo.total = 0
+      }
+    },
+    handleCurrentChange (currentPage) {
+      this.listInfo.pageNum = currentPage
+      this.getOrderList()
+    },
     formatKeywords (data) {
       if (!data) return
       const keyArr = []

+ 43 - 3
src/components/MessageItem.vue

@@ -1,12 +1,18 @@
 <template>
-<div class="message--item" @click="$emit('on-click')">
+<div class="message--item" :class="{'active': active}" @click="$emit('on-click')">
   <div class="head">
     <el-badge :value="badge" :hidden="!badge">
       <img :src="headImg" alt="头像">
     </el-badge>
   </div>
   <div class="content">
-    <span class="name">{{nickName}}</span>
+    <div class="name">
+      <span>
+        {{nickName}}
+        <em v-if="userType === 1" class="kf-badge">客服</em>
+      </span>
+      <span class="last-time">{{lastTime}}</span>
+    </div>
     <div class="ellipsis">
       <span class="badge-tip" v-if="showBadge">{{badgeTip}}</span>
       <span>{{showMessage}}</span>
@@ -24,6 +30,10 @@ export default {
     [Badge.name]: Badge
   },
   props: {
+    active: {
+      type: Boolean,
+      default: false
+    },
     title: {
       type: String,
       default: ''
@@ -51,6 +61,14 @@ export default {
     allNumber: {
       type: [String, Number],
       default: 0
+    },
+    lastTime: {
+      type: String,
+      default: ''
+    },
+    userType: {
+      type: Number,
+      default: 2
     }
   },
   computed: {
@@ -102,10 +120,13 @@ export default {
     }
     img {
       width: 100%;
+      border-radius: 50%;
     }
   }
   .content {
-    max-width: 170px;
+    overflow: hidden;
+    flex: 1;
+    // max-width: 170px;
     flex-shrink: 0;
     font-size: 12px;
     font-weight: 400;
@@ -118,10 +139,29 @@ export default {
     margin-right: 2px;
   }
   .name {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
     font-size: 14px;
     color: #1D1D1D;
     line-height: 22px;
     margin-bottom: 2px;
+    .last-time{
+      font-size: 12px;
+      color: #999999;
+    }
+  }
+  .kf-badge{
+    display: inline-block;
+    height: 16px;
+    margin-left: 8px;
+    padding: 0 7px;
+    color: #2ABED1;
+    font-size: 12px;
+    line-height: 16px;
+    border: 1px solid #2ABED1;
+    border-radius: 16px;
+    box-sizing: content-box;
   }
 }
 </style>

+ 13 - 1
src/components/MessageList.vue

@@ -6,7 +6,7 @@
         v-for="(item, index) in list"
         :key="index"
         v-bind="item"
-        @on-click="$emit('select', item)"
+        @on-click="onSelect(item)"
       ></message-item>
     </div>
   </div>
@@ -29,6 +29,18 @@ export default {
       type: Array,
       default: () => []
     }
+  },
+  mounted () {
+    console.log(this.list)
+  },
+  methods: {
+    onSelect (item) {
+      this.list.forEach(v => {
+        v.active = false
+      })
+      item.active = true
+      this.$emit('select', item)
+    }
   }
 }
 </script>

+ 1 - 1
src/components/action-list/index.vue

@@ -78,7 +78,7 @@ export default {
      */
     isLink: {
       type: Boolean,
-      default: true
+      default: false
     }
   },
   data () {

+ 1 - 0
src/components/aside/index.vue

@@ -126,6 +126,7 @@ export default {
   .logo {
     width: 36px;
     height: 36px;
+    border-radius: 50%;
   }
   .dot{
     display: inline-block;

+ 1 - 1
src/router/index.js

@@ -8,7 +8,7 @@ Vue.use(VueRouter)
 const routes = [
   {
     // 用户工作台
-    path: '/',
+    path: '/customer',
     name: 'CustomerView',
     component: CustomerView
   },

+ 3 - 19
src/store/modules/message.js

@@ -3,27 +3,11 @@ import { messageAdd } from '@/api/modules/'
 export default {
   namespaced: true,
   state: {
-    CommunicateList: [
-      {
-        nickName: '北京客户',
-        badge: 2,
-        message: '大会员里边大会员里边的分析功能咋用?'
-      },
-      {
-        nickName: '河南客户',
-        message: '大会员里边的分析功能咋用?'
-      }
-    ],
-    userSessionBadge: JSON.parse(sessionStorage.getItem('userSessionBadge-login-clear')) || {}, // 用户会话标识
+    // 会话标识 key为加密后的用户userId, value为加密后的会话id
+    userSessionBadge: JSON.parse(sessionStorage.getItem('userSessionBadge-login-clear')) || {},
     kfSessionBadge: JSON.parse(sessionStorage.getItem('kfSessionBadge-login-clear')) || {} // 客服会话标识
   },
-  getters: {
-    unReadCount (state) {
-      return state.CommunicateList.reduce((a, b) => {
-        return a + Number(b?.badge || 0)
-      }, 0)
-    }
-  },
+  getters: {},
   mutations: {
     // 存储用户会话标识
     setUserSessionBadge (state, data) {

+ 30 - 17
src/store/modules/webSocket.js

@@ -4,18 +4,18 @@ export default {
   state: {
     webSocket: null,
     lockReconnect: false, // websocket锁,防止重复连接
-    timeout: 15000, // 心跳频率
+    timeout: 5000, // 心跳频率
     message: null, // 接收到的消息
     timeoutObj: null,
     serverTimeoutObj: null, // 心跳倒计时
     timeoutNum: null // 断开 重连倒计时
   },
   mutations: {
-    webSocketInit (state, url) {
+    webSocketInit (state, params) {
       if (state.webSocket) {
         return
       }
-      // const realUrl = process.env.VUE_APP_WEBSOCKET_URL
+      const url = `ws://${location.host}/chatCenter/ws`
       if ('WebSocket' in window) {
         state.webSocket = new WebSocket(url)
       } else if ('MozWebSocket' in window) {
@@ -31,26 +31,31 @@ export default {
       // 建立连接
       state.webSocket.onopen = () => {
         console.log('建立链接')
-        this.commit('socketStart')
+        this.commit('webSocket/socketStart')
+        // 特殊处理 建立连接成功后需要回调
+        if (params && params.callback) {
+          params.callback && params.callback()
+        }
       }
       // 接收消息
       state.webSocket.onmessage = res => {
-        console.log('接收到消息', res)
-        if (res.data === 'heartCheck') {
-          this.commit('socketReset')
+        // console.log('接收到消息', res.data)
+        const data = JSON.parse(res.data)
+        if (data.type === 1) {
+          this.commit('webSocket/socketReset')
         } else {
-          state.msg = res
+          state.message = res
         }
       }
       // 关闭连接
       state.webSocket.onclose = () => {
         console.log('连接已断开')
-        this.commit('reconnect')
+        this.commit('webSocket/reconnect')
       }
       // 通讯异常
       state.webSocket.onerror = () => {
         console.log('通讯异常')
-        this.commit('reconnect')
+        this.commit('webSocket/reconnect')
       }
     },
     // 开启心跳
@@ -61,10 +66,10 @@ export default {
         // 这里发送一个心跳,后端收到后,返回一个心跳消息
         if (state.webSocket.readyState === 1) {
           // 如果连接正常
-          state.webSocket.send('heartCheck')
+          state.webSocket.send('{"type":1}')
         } else {
           // 否则重连
-          this.commit('reconnect')
+          this.commit('webSocket/reconnect')
         }
         state.serverTimeoutObj = setTimeout(() => {
           // 超时关闭
@@ -77,29 +82,37 @@ export default {
       clearTimeout(state.timeoutObj)
       clearTimeout(state.serverTimeoutObj)
       // 重启心跳
-      this.commit('socketStart')
+      this.commit('webSocket/socketStart')
     },
     // 重新连接
     reconnect (state) {
+      console.log(state)
+      state.webSocket = null
+      const that = this
       if (state.lockReconnect) {
         return
       }
       state.lockReconnect = true
-      // 没连接上会一直重连,30秒重试请求重连,设置延迟避免请求过多
+      // 没连接上会一直重连,5秒重试请求重连,设置延迟避免请求过多
       state.timeoutNum && clearTimeout(state.timeoutNum)
       state.timeoutNum = setTimeout(() => {
         // 新连接
-        this.commit('webSocketInit')
+        that.commit('webSocket/webSocketInit')
         state.lockReconnect = false
       }, 5000)
+    },
+    webSocketSend (state, params) {
+      if (state.webSocket.readyState === 1) {
+        state.webSocket.send(JSON.stringify(params))
+      }
     }
   },
   getters: {
     socketMsg: state => state.message
   },
   actions: {
-    webSocketInit ({ commit }, url) {
-      commit('webSocketInit', url)
+    webSocketInit ({ commit }, params) {
+      commit('webSocketInit', params)
     },
     webSocketSend ({ commit }, params) {
       commit('webSocketSend', params)

+ 11 - 0
src/utils/globalFunctions.js

@@ -1,3 +1,4 @@
+import moment from 'moment'
 // 字符串处理相关函数
 // 手机号中间4位加* ------------>
 export function addConfusionForTel (tel) {
@@ -638,3 +639,13 @@ export function isHyperlink (url) {
   const reg=/^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\/])+$/
   return reg.test(url)
 }
+
+// 根据时间字符串得到时分秒
+export function momentTime (time, fmt) {
+  /**
+   * fmt格式等用法见api文档
+   * http://momentjs.cn/docs/#/displaying/format/
+   */
+  if (!time) return
+  return moment(time).format(fmt)
+}

+ 32 - 7
src/views/ChatView.vue

@@ -4,6 +4,7 @@
       <img v-if="selectInfo.headImg" class="head-img" :src="selectInfo.headImg" alt="">
       <span>{{ selectInfo.name }}</span>
       <span v-if="selectInfo.userType === 1" class="kf-badge">客服</span>
+      <span>[在线]</span>
     </div>
     <div class="msg-content" ref="msgContentRef">
       <div class="load-more">
@@ -15,11 +16,11 @@
       </div>
       <div v-for="(item, index) in sessionList" :key="index">
         <div class="create-tip" v-if="item.title === '建立会话时间'">
-          <span>{{item.content}}</span>
+          <span>{{item.create_time}} {{formatContent(item.content)}}</span>
         </div>
         <div v-else class="msg-item" :class="{'self-item': item.fool === 1}">
           <div class="user-info">
-            <img class="head-img" :src="item.fool === 1 ? item.ownImg : item.robotImg" alt="">
+            <img class="head-img" :src="item.fool === 1 ? item.ownImg : item.robotImg" alt="" @error="defaultImg">
           </div>
           <div class="msg-info">
             <div class="push-header">
@@ -47,11 +48,11 @@
                   </el-image>
                 </div>
                 <div v-else class="msg-text-box" :class="{'self-msg': item.fool === 1}">
-                  <a v-if="isHyperlink(item.content)" :href="item.content">{{item.content}}</a>
+                  <a class="hyper-link" v-if="isHyperlink(item.content)" :href="item.content">{{item.content}}</a>
                   <span v-else v-html="item.content"></span>
                 </div>
               </div>
-              <div class="msg-status" :class="{'self-status': item.fool === 1}">
+              <div class="msg-status" v-if="item.error" :class="{'self-status': item.fool === 1}">
                 <i class="icon-error"></i>
                 <span class="reset-send-text">重新发送</span>
               </div>
@@ -69,6 +70,7 @@
     <div class="msg-footer">
       <ActionList
         :options="actionList"
+        :isLink="isLink"
         @action="onAction"
         @upload-image="uploadImage"
         @upload-attach="uploadAttach"
@@ -122,9 +124,13 @@ export default {
     actionList: {
       type: Array,
       default () {
-        return ['image', 'attach']
+        return ['image']
       }
     },
+    isLink: {
+      type: Boolean,
+      default: false
+    },
     // 主题色
     theme: {
       type: String,
@@ -140,6 +146,7 @@ export default {
   },
   data () {
     return {
+      defaultLogo: 'https://www.jianyu360.cn/common-module/public/image/auto.png',
       selectInfo: this.info,
       msgVal: '',
       sessionList: [],
@@ -153,7 +160,6 @@ export default {
       this.selectInfo = val
     },
     msgList (val) {
-      console.log(val)
       this.sessionList = val
     }
   },
@@ -165,6 +171,16 @@ export default {
   methods: {
     sizeFormatter,
     isHyperlink,
+    defaultImg (event) {
+      const img = event.srcElement
+      img.src = this.defaultLogo
+      img.onerror = null
+    },
+    formatContent (val) {
+      if (!val) return
+      const reg = /[1-2][0-9][0-9][0-9]-[0-1]{0,1}[0-9]-[0-3]{0,1}[0-9]|[0-9][0-9]:[0-9][0-9]:[0-9][0-9]|\s+|\s+$/g
+      return val.replace(reg, '')
+    },
     setScrollTop () {
       setTimeout(() => {
         this.$refs.msgContentRef.scrollTop = this.$refs.msgContentRef.scrollHeight
@@ -187,8 +203,9 @@ export default {
         this.showRate = !this.showRate
       }
     },
+    // 转人工
     onTurnPeople () {
-      // 转人工
+      this.$emit('turn')
     },
     onLoadMore () {
       this.$emit('load-more')
@@ -348,9 +365,17 @@ export default {
     padding: 8px 24px;
     border-radius: 8px;
     background: #fff;
+    .hyper-link{
+      color: $color_main;
+      text-decoration: underline;
+    }
     &.self-msg{
       background: $color_main;
       color: #fff;
+      .hyper-link{
+        color: #fff;
+        text-decoration: underline;
+      }
     }
   }
   .msg-status{

+ 206 - 49
src/views/CustomerServiceView.vue

@@ -12,8 +12,8 @@
               <span :class="{'active': tabIndex === 1 }" @click="onClickTab(1)">已结束用户(356)</span>
             </div>
             <div v-if="tabIndex === 0">
-              <message-list :list="userList" @select="onSelectCurMsg"></message-list>
-              <message-list title="排队中(2)" :list="userList" @select="onSelectCurMsg"></message-list>
+              <message-list :list="dialogueList" @select="onSelectCurMsg"></message-list>
+              <message-list title="排队中(2)" :list="queueList" @select="onSelectCurMsg"></message-list>
             </div>
             <div v-else-if="tabIndex === 1">
               <SearchFilter @search="onSearch"></SearchFilter>
@@ -24,15 +24,21 @@
       </el-container>
       <el-container v-if="showMsgBox" class="message--chat-group">
         <ChatView
-          :info="curItem"
+          :info="other"
           :actionList="getActionList"
           :listInfo="listInfo"
           :msgList="getMsgList"
-          :key="curItem.userId"
+          :session-id="other.sessionId"
+          @send="onSendMsg"
+          @load-more="onLoadMore"
+          @upload-image="onUploadImage"
+          @upload-attach="onUploadAttach"
+          :key="other.userId"
+          ref="userSide"
         >
         </ChatView>
         <div class="message--info-group">
-          <customer-info :userId="curItem.userId"></customer-info>
+          <customer-info :userId="other.userId"></customer-info>
         </div>
       </el-container>
       <el-container class="message--empty-group" v-else>
@@ -49,9 +55,9 @@ import CustomerInfo from '@/components/CustomerInfo'
 import MessageList from '@/components/MessageList'
 import SearchFilter from '@/components/search/'
 import AsideView from '@/components/aside/'
-import { mapState, mapActions } from 'vuex'
+import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
 import { getUserList, getMessageCount, createChatSession, findMessage } from '@/api/modules/'
-import { dateFormatter, isJSON } from '@/utils/'
+import { dateFormatter, isJSON, isHyperlink, momentTime } from '@/utils/'
 
 export default {
   name: 'CustomerView',
@@ -78,16 +84,17 @@ export default {
       finishList: [], // 已结束会话
       msgList: [],
       listInfo: {
-        pageSize: 10,
+        pageSize: 20,
         pageIndex: 1,
         loading: false,
         finished: false,
         total: 0
       },
       login: {
-        userType: 1
+        userType: 1,
+        itemType: 4 // 1:站内信消息 2:点对点消息 3:群消息 4:机器人消息 5:客服消息  6系统信息
       },
-      curItem: {
+      other: {
         userId: null,
         userType: null,
         sessionId: null,
@@ -98,9 +105,10 @@ export default {
     }
   },
   computed: {
-    ...mapState('message', ['userSessionBadge']),
+    ...mapState('message', ['kfSessionBadge']),
+    ...mapGetters('webSocket', ['socketMsg']),
     getActionList () {
-      return ['image', 'attach']
+      return ['image']
     },
     getMsgList () {
       const list = this.msgList
@@ -116,32 +124,96 @@ export default {
     }
   },
   watch: {
-    // socketMsg (val) {
-    //   // 监听接收到的消息,进行处理
-    // }
+    socketMsg (val) {
+      // 监听接收到的消息,进行处理
+      // console.log(val)
+      this.todoMessage(val)
+    }
   },
   created () {
     // 接入参数 userType: 1.用户 2.客服
-    this.login.userType = Number(this.$route.query.userType)
+    // this.login.userType = Number(this.$route.query.userType)
   },
   mounted () {
-    // this.webSocketInit()
+    this.getSocketUrl()
     this.getUserListFn({})
     this.getMessageCountFn()
   },
   methods: {
+    ...mapMutations({
+      setKfSessionBadge: 'message/setKfSessionBadge'
+    }),
     ...mapActions({
       webSocketInit: 'webSocket/webSocketInit',
       webSocketSend: 'webSocket/webSocketSend',
       saveMessageAction: 'message/saveMessageAction'
     }),
+    todoMessage (res) {
+      const data = JSON.parse(res.data)
+      const { content } = data.data
+      console.log(data)
+      if (data.type === 4) {
+        // 接收到的转人工消息
+        this.dialogueList = data.data.map(v => {
+          return {
+            headImg: v.headImg,
+            nickName: v.nickName,
+            badge: v.number,
+            message: v.content,
+            userId: v.rUserId,
+            userType: 2,
+            lastTime: momentTime(new Date(v.time * 1000), 'HH:mm:ss'),
+            active: false
+          }
+        })
+      } else if (data.type === 5) {
+        this.queueList = data.data.map(v => {
+          return {
+            headImg: v.headImg,
+            nickName: v.nickName,
+            badge: v.number,
+            message: v.content,
+            userId: v.rUserId,
+            userType: 2,
+            lastTime: momentTime(new Date(v.time * 1000), 'HH:mm:ss'),
+            active: false
+          }
+        })
+      } else if (data.type === 8) {
+        this.msgList.push({
+          content,
+          create_time: dateFormatter(new Date().getTime())
+        })
+        this.$refs.userSide?.setScrollTop()
+      }
+    },
+    // 获取websocket地址
+    async getSocketUrl () {
+      // const { data } = await getWebSocketNode()
+      // if (data) {
+      //   this.webSocketInit(data)
+      // }
+      this.webSocketInit({
+        url: '',
+        callback: () => {
+          // 设置客服身份
+          const kfInfo = {
+            type: 3,
+            headImg: '',
+            nickName: ''
+          }
+          this.sendSocketMsg(kfInfo)
+          this.sendSocketMsg({ type: 9 })
+          this.sendSocketMsg({ type: 10 })
+        }
+      })
+    },
     // 处理接收到的消息
     getSocketMsg (data) {
       data = JSON.parse(data)
     },
     // 发送消息
     sendSocketMsg (params) {
-      params = JSON.stringify(params)
       this.webSocketSend(params)
     },
     // 会话-已结束切换
@@ -161,11 +233,12 @@ export default {
     // 切换用户聊天窗口
     onSelectCurMsg (data) {
       console.log(data)
+      this.listInfo.pageIndex = 1
       this.msgList = []
       const { userId, nickName, userType } = data
-      this.curItem.userId = userId
-      this.curItem.name = nickName
-      this.curItem.userType = userType
+      this.other.userId = userId
+      this.other.name = nickName
+      this.other.userType = userType || 2
       this.showMsgBox = true
       this.createSessionFn()
     },
@@ -188,7 +261,9 @@ export default {
             allNumber: v.allNumber,
             message: v.content,
             userId: v.userId,
-            userType: v.userType
+            userType: v.userType,
+            lastTime: momentTime(v.create_time, 'HH:mm:ss'),
+            active: false
           }
         })
       } else {
@@ -198,74 +273,71 @@ export default {
     // 获取未读消息总量
     async getMessageCountFn () {
       const params = {
-        userType: 1,
-        entUserId: 2
+        userType: 1 // 用户类型:2用户 1客服
       }
       const { data } = await getMessageCount(params)
       this.unReadCount = data
     },
     // 创建会话
-    async createSessionFn (msgType = 4) {
-      const storageInfo = this.userSessionBadge
+    async createSessionFn (msgType = 5) {
+      const storageInfo = this.kfSessionBadge
       // 当前选择的是用户跟用户聊 不用创建会话
-      if (this.login.userType !== 1 && this.curItem.userType !== 1) {
+      if (this.login.userType === 2 && this.other.userType === 2) {
         this.getFindMessage()
       } else {
         if (storageInfo && Object.keys(storageInfo).length > 0) {
           for (const key in storageInfo) {
-            if (Number(key) === Number(this.curItem.userId) && storageInfo[key]) {
+            if (key === this.other.userId && storageInfo[key]) {
+              this.other.sessionId = storageInfo[key]
               this.getFindMessage()
               return
             }
           }
         }
         const { data } = await createChatSession({
-          userType: this.curItem.userType, // 用户类型:2用户1客服
+          userType: 1, // 用户类型:2用户1客服
           msgType: msgType, // 消息类型 ;1:站内信消息 2:点对点消息 3:群消息 4:机器人消息 5:客服消息
-          receiveEntId: this.service.entId // 机器人企业标识
           // receiveAppId: receiveAppId, // 机器人Appid
-          // receiveId: receiveId // 用户标识(userType=1时使用)
+          receiveId: this.other.userId // 用户标识(userType=1时使用)
         })
         if (data) {
-          const userStorage = {
-            [this.curItem.userId]: data
+          const kfStorage = {
+            [this.other.userId]: data
           }
           // 存储会话标识
-          this.setUserSessionBadge(userStorage)
+          this.setKfSessionBadge(kfStorage)
           // 设置首次建立连接时间
           const connectTime = dateFormatter(new Date().getTime())
-          // 获取机器人首次回复
-          this.getRobotReply()
-          this.msgList.push({
-            title: '建立会话时间',
-            content: `${connectTime} 客户和剑鱼标讯建立了会话`,
-            fool: 2,
-            create_time: connectTime
-          })
           // 保存建立会话时间
-          this.saveMessageAction({
-            ownType: 1,
+          const params = {
+            ownType: 2,
             title: '建立会话时间',
-            content: `${connectTime} 客户和剑鱼标讯建立了会话`,
+            content: `${connectTime}您和用户建立了会话`,
             item: 8,
             itemType: 6,
-            appid: 10000,
-            receiveId: data,
+            appid: '10000',
+            receiveId: this.other.userId,
+            sendId: data,
             type: 1,
             link: ''
+          }
+          this.saveMessageAction(params).then(res => {
+            params.fool = 2
+            params.create_time = dateFormatter(new Date().getTime())
+            this.msgList.push(params)
           })
         }
       }
     },
     // 分页查询聊天内容
     async getFindMessage () {
-      const msgType = this.login.userType === 2 && this.curItem.userType === 2 ? 2 : 5
+      const msgType = this.login.userType === 2 && this.other.userType === 2 ? 2 : 5
       const params = {
         userType: this.login.userType, // 当前用户身份 1:客服, 2:用户
         msgType: msgType,
         pageSize: this.listInfo.pageSize,
         pageIndex: this.listInfo.pageIndex,
-        sendId: this.curItem.userId // 对方用户身份
+        sendId: this.other.userId // 对方用户身份
       }
       this.listInfo.loading = true
       const { count, data } = await findMessage(params)
@@ -276,6 +348,91 @@ export default {
       } else {
         this.listInfo.finished = true
       }
+    },
+    // 保存文本消息
+    onSendMsg (data) {
+      const { content } = data
+      const params = {
+        receiveId: this.other.userId,
+        sendId: this.other.sessionId,
+        ownType: this.other.userType, // 对方身份
+        title: isHyperlink(content) ? '链接' : '文本',
+        item: 8,
+        itemType: 5,
+        link: isHyperlink(content) ? content : '',
+        fool: 1,
+        type: isHyperlink(content) ? 2 : 1,
+        appid: '10000',
+        ...data
+      }
+      // 保存并发送
+      this.saveMessageAction(params).then(res => {
+        const { error_code: code } = res
+        const msgSocket = {
+          type: 6,
+          contentType: isHyperlink(content) ? 2 : 1, // contentType 1: 文本 2.链接 3.附件
+          content: content,
+          sUserType: 2,
+          rUserId: this.other.userId,
+          rUserType: 1,
+          sessionId: this.other.sessionId
+        }
+        if (code === 0) {
+          this.sendSocketMsg(msgSocket)
+          this.msgList.push(params)
+        } else {
+          params.error = true
+          this.msgList.push(params)
+        }
+        this.getUserListFn({})
+        this.$refs.userSide?.setScrollTop()
+      })
+    },
+    // 保存图片消息
+    onUploadImage (data) {
+      console.log(data)
+      const params = {
+        receiveId: this.other.userId,
+        sendId: this.other.sessionId,
+        ownType: this.other.userType, // 对方身份
+        title: '图片',
+        item: 8,
+        itemType: this.other.userType === 2 ? 2 : 5,
+        link: '',
+        fool: 1,
+        type: 3,
+        appid: '10000',
+        content: JSON.stringify(data),
+        create_time: dateFormatter(new Date().getTime())
+      }
+      this.msgList.push(params)
+      this.saveMessageAction(params)
+      this.$refs.userSide?.setScrollTop()
+    },
+    // 保存附件消息
+    onUploadAttach (data) {
+      const params = {
+        receiveId: this.other.userId,
+        sendId: this.other.sessionId,
+        ownType: this.other.userType, // 对方身份
+        title: '附件',
+        item: 8,
+        itemType: this.other.userType === 2 ? 2 : 5,
+        link: '',
+        fool: 1,
+        type: 3,
+        appid: '10000',
+        content: JSON.stringify(data),
+        create_time: dateFormatter(new Date().getTime())
+      }
+      this.msgList.push(params)
+      this.saveMessageAction(params)
+      this.$refs.userSide?.setScrollTop()
+    },
+    // 加载更多
+    onLoadMore () {
+      this.listInfo.pageIndex++
+      this.getFindMessage()
     }
   }
 }

+ 278 - 127
src/views/CustomerView.vue

@@ -1,6 +1,6 @@
 <template>
   <el-container class="message--layout" :class="{'mini-layout': getMiniStatus}">
-    <AsideView v-if="!getMiniStatus"></AsideView>
+    <AsideView v-if="!getMiniStatus" :badge="unReadCount" :logo="login.headImg"></AsideView>
     <el-main class="message--main">
       <el-container v-if="!getMiniStatus" class="message--list-group">
         <div class="j-container">
@@ -10,10 +10,11 @@
           </div>
         </div>
       </el-container>
-      <el-container class="message--chat-group">
+      <el-container v-if="showMsgBox" class="message--chat-group">
         <ChatView
-          :info="service"
-          :actionList="actionList"
+          :info="other"
+          :actionList="getActionList"
+          :isLink="isTurnPeople"
           :msgList="getMsgList"
           :listInfo="listInfo"
           :session-id="other.sessionId"
@@ -21,10 +22,14 @@
           @load-more="onLoadMore"
           @upload-image="onUploadImage"
           @upload-attach="onUploadAttach"
+          @turn="onTurnPeople"
           ref="userSide"
         >
         </ChatView>
       </el-container>
+      <el-container class="message--empty-group" v-else>
+        <div class="empty-message-img"></div>
+      </el-container>
     </el-main>
   </el-container>
 </template>
@@ -34,9 +39,9 @@ import { Container, Main, Avatar, Badge } from 'element-ui'
 import ChatView from '@/views/ChatView'
 import AsideView from '@/components/aside/'
 import MessageList from '@/components/MessageList'
-import { mapState, mapMutations, mapActions } from 'vuex'
-import { robotInfo, robotReply, createChatSession, findMessage, getWebSocketNode } from '@/api/modules/'
-import { dateFormatter, isJSON, isHyperlink } from '@/utils/'
+import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
+import { robotInfo, createChatSession, findMessage, getUserList, getMessageCount, getUserInfo } from '@/api/modules/'
+import { dateFormatter, isJSON, isHyperlink, momentTime } from '@/utils/'
 
 export default {
   name: 'CustomerServiceView',
@@ -52,6 +57,7 @@ export default {
   data () {
     return {
       mini: false,
+      unReadCount: 0,
       // 所有会话
       userList: [],
       // 客服信息
@@ -59,12 +65,11 @@ export default {
         name: '剑鱼标讯',
         headImg: 'https://www.jianyu360.cn/common-module/public/image/auto.png',
         entId: '',
-        userType: 1
+        userType: 1,
+        chatStatus: 0
       },
-      // 聊天框操作配置项
-      actionList: ['image', 'attach', 'rate'],
       listInfo: {
-        pageSize: 10,
+        pageSize: 20,
         pageIndex: 1,
         loading: false,
         finished: false,
@@ -74,22 +79,48 @@ export default {
       msgList: [],
       // 当前登录用户信息
       login: {
+        nickName: '',
+        headImg: '',
         userType: 2 // 当前用户身份 1:客服, 2: 用户
       },
       // 会话对方信息
       other: {
         userType: null, // 会话对方身份(客服or用户)
-        userId: 1, // 会话对方的userId
-        sessionId: null // 会话id
-      }
+        userId: null, // 会话对方的userId
+        sessionId: null, // 会话id
+        entId: null,
+        name: '',
+        info: {},
+        itemType: null // 会话对方的类型 1:站内信消息 2:点对点消息 3:群消息 4:机器人消息 5:客服消息  6系统信息
+      },
+      showMsgBox: false
     }
   },
   computed: {
     ...mapState('message', ['userSessionBadge']),
+    ...mapGetters('webSocket', ['socketMsg']),
+    // 聊天框操作配置项
+    getActionList () {
+      if (this.other.userType === 1) {
+        return ['image', 'rate']
+      } else {
+        return ['image']
+      }
+    },
+    isTurnPeople () {
+      return this.other.userType === 1
+    },
     getMiniStatus () {
-      console.log(this.mini)
       return Boolean(this.mini)
     },
+    getItemType () {
+      const type = this.other.userType
+      if (type === 2) {
+        return 2
+      } else {
+        return 4 || 5
+      }
+    },
     getMsgList () {
       const list = this.msgList
       list.forEach(v => {
@@ -103,31 +134,22 @@ export default {
       return result
     }
   },
+  watch: {
+    socketMsg (val) {
+      console.log(val, 'val')
+      // 监听接收到的消息,进行处理
+      this.todoMessage(val)
+    }
+  },
   created () {
-    this.mini = Number(this.$route.query.mini) || 0
-    this.service.entId = String(this.$route.query.entId)
-    this.other.userId = String(this.$route.query.userId)
-    this.getRobotInfo()
+    this.mini = Number(this.$route.query.mini)
+    this.other.userId = this.$route.query.userId // 选择的用户的userId、也是客服所在的企业entId
+    this.getUserInfoFn()
     this.getSocketUrl()
+    this.getUserListFn()
+    this.getMessageCountFn()
   },
-  mounted () {
-    /**
-     * 判断当前用户是否建立会话
-     */
-    const storage = this.userSessionBadge
-    if (storage && Object.keys(storage).length > 0) {
-      console.log(storage, 'storage')
-      for (const key in storage) {
-        if (key === this.other.userId && storage[key]) {
-          this.getFindMessage()
-        } else {
-          this.createSessionFn()
-        }
-      }
-    } else {
-      this.createSessionFn()
-    }
-  },
+  mounted () {},
   methods: {
     ...mapMutations({
       setUserSessionBadge: 'message/setUserSessionBadge'
@@ -137,95 +159,149 @@ export default {
       webSocketSend: 'webSocket/webSocketSend',
       saveMessageAction: 'message/saveMessageAction'
     }),
+    // 转人工
+    onTurnPeople (content = '') {
+      this.webSocketSend({
+        type: 2,
+        content: content,
+        rUserId: '455b415651' || this.other.userId,
+        rUserType: 2,
+        headImg: this.login.headImg,
+        nickName: this.login.nickName,
+        sessionId: this.other.sessionId
+      })
+    },
+    // 处理websocket接收的数据
+    todoMessage (res) {
+      const data = JSON.parse(res.data)
+      const { content, sUserId } = data.data
+      console.log(data)
+      if (sUserId === this.other.userId) {
+        this.msgList.push({
+          content,
+          create_time: dateFormatter(new Date().getTime())
+        })
+        this.$refs.userSide?.setScrollTop()
+      }
+    },
+    // 获取用户信息
+    async getUserInfoFn () {
+      const { data } = await getUserInfo()
+      const { nickname, headimg } = data
+      this.login.nickName = nickname
+      this.login.headImg = headimg
+    },
     // 获取websocket地址
     async getSocketUrl () {
-      const { data } = await getWebSocketNode()
-      if (data) {
-        this.webSocketInit(data)
-      }
+      this.webSocketInit()
+      // const { data } = await getWebSocketNode()
+      // if (data) {
+      //   this.webSocketInit(data)
+      // }
     },
     // 切换用户聊天窗口
     onSelectCurMsg (data) {
       console.log(data)
-      // this.msgList = []
-      // const { userId, nickName, userType } = data
-      // this.curItem.userId = userId
-      // this.curItem.name = nickName
-      // this.curItem.userType = userType
-      // this.showMsgBox = true
-      // this.createSessionFn()
-    },
-    async getRobotInfo () {
-      const { data } = await robotInfo({
-        entId: this.$route.query.entId
+      const { userId, nickName, userType } = data
+      if (this.other.userId === userId && this.showMsgBox) return
+      this.listInfo.pageIndex = 1
+      this.msgList = []
+      this.other.userId = userId
+      this.other.name = nickName
+      this.other.userType = userType
+      this.showMsgBox = true
+      this.createSessionFn()
+      this.$nextTick(() => {
+        this.$refs.userSide.setScrollTop()
       })
-      if (data) {
-        this.service.name = data.nickname
-        this.service.headImg = data.headimage
-      }
     },
     // 创建会话
     async createSessionFn (msgType = 4) {
-      const { data } = await createChatSession({
-        userType: this.login.userType, // 用户类型:2用户 1客服
-        msgType: msgType, // 消息类型 ;1:站内信消息 2:点对点消息 3:群消息 4:机器人消息 5:客服消息
-        receiveEntId: this.service.entId // 机器人企业标识
-        // receiveAppId: receiveAppId, // 机器人Appid
-        // receiveId: receiveId // 用户标识(userType=1时使用)
-      })
-      if (data) {
-        this.other.sessionId = data
-        // 存储会话标识
-        const userStorage = {
-          // key: 用户或客服的userId, value: 会话id
-          [this.other.userId]: data
+      this.showMsgBox = true
+      // 先判断是否需要建立会话
+      const storageInfo = this.userSessionBadge
+      // 如果是跟用户聊 则不用创建会话
+      if (this.other.userType === 2) {
+        this.getFindMessage()
+      } else {
+        // 如果是跟客服聊 先判断与该客服是否创建过会话
+        if (storageInfo && Object.keys(storageInfo).length > 0) {
+          for (const key in storageInfo) {
+            if (key === this.other.userId && storageInfo[key]) {
+              this.other.sessionId = storageInfo[key]
+              this.getFindMessage()
+              return
+            }
+          }
         }
-        this.setUserSessionBadge(userStorage)
-        // 设置首次建立连接时间
-        const connectTime = dateFormatter(new Date().getTime())
-        // 获取机器人首次回复
-        this.getRobotReply()
-        this.msgList.push({
-          title: '建立会话时间',
-          content: `${connectTime} 客户和剑鱼标讯建立了会话`,
-          fool: 2,
-          create_time: connectTime
-        })
-        // 保存建立会话时间
-        this.saveMessageAction({
-          ownType: 1,
-          title: '建立会话时间',
-          content: `${connectTime} 客户和剑鱼标讯建立了会话`,
-          item: 8,
-          itemType: 6,
-          appid: 10000,
-          receiveId: data,
-          type: 1,
-          link: ''
+        const { data } = await createChatSession({
+          userType: this.login.userType, // 用户类型:2用户 1客服
+          msgType: msgType, // 消息类型 ;1:站内信消息 2:点对点消息 3:群消息 4:机器人消息 5:客服消息
+          receiveEntId: this.other.userId // 机器人企业标识
+          // receiveAppId: receiveAppId, // 机器人Appid
+          // receiveId: receiveId // 用户标识(userType=1时使用)
         })
+        if (data) {
+          this.other.sessionId = data
+          // 存储会话标识
+          const userStorage = {
+            // key: 用户或客服的userId, value: 会话id
+            [this.other.userId]: data
+          }
+          this.setUserSessionBadge(userStorage)
+          const connectTime = dateFormatter(new Date().getTime())
+          // 获取机器人头像昵称首次回复
+          this.getRobotInfo()
+          const params = {
+            ownType: 1,
+            title: '建立会话时间',
+            content: `${connectTime}您和剑鱼标讯建立了会话`,
+            item: 8,
+            itemType: 6, // 系统信息
+            appid: 10000,
+            receiveId: data,
+            type: 1,
+            link: ''
+          }
+          // 保存建立会话时间
+          this.saveMessageAction(params).then(res => {
+            const { error_code: code } = res
+            if (code === 0) {
+              params.create_time = dateFormatter(new Date().getTime())
+              params.fool = 2
+              this.msgList.push(params)
+            }
+          })
+        }
       }
     },
-    // 机器人获取首次自动回复
-    async getRobotReply () {
-      const { data } = await robotReply({
-        entId: this.service.entId
+    // 机器人头像、昵称、首次自动回复
+    async getRobotInfo () {
+      const { data } = await robotInfo({
+        entId: this.other.userId
       })
       if (data) {
-        this.msgList.push({
-          content: data?.reply,
-          fool: 2,
-          create_time: dateFormatter(new Date().getTime())
-        })
+        this.service.name = data.nickname
+        this.service.headImg = data.headimage
+        this.service.chatStatus = data.chatStatus // 0: 机器人 1:排队中 2:会话中
         this.saveMessageAction({
           ownType: 2,
           title: '机器人首次回复',
           content: data?.reply,
           item: 8,
-          itemType: 4,
+          itemType: 4, // 机器人信息
           appid: 10000,
-          sendId: this.other.sessionId,
+          sendId: this.other.sessionId, // 发送人标识(客服发送信息时该值为会话标识,用户聊天时该值不传)
           type: 1,
           link: ''
+        }).then(res => {
+          if (res.code === 0) {
+            this.msgList.push({
+              content: data?.reply,
+              fool: 2,
+              create_time: dateFormatter(new Date().getTime())
+            })
+          }
         })
       }
     },
@@ -253,62 +329,137 @@ export default {
     onSendMsg (data) {
       const { content } = data
       const params = {
-        receiveId: this.other.sessionId,
-        ownType: 1,
-        title: isHyperlink(content) ? '链接' : '文本消息',
+        receiveId: this.other.userType === 2 ? this.other.userId : this.other.sessionId,
+        ownType: this.other.userType, // 对方身份
+        title: isHyperlink(content) ? '链接' : '文本',
         item: 8,
-        itemType: 5,
+        itemType: this.other.userType === 2 ? 2 : 5, // 机器人是5 人工是4
         link: isHyperlink(content) ? content : '',
-        fool: 1,
-        type: 1,
+        type: isHyperlink(content) ? 2 : 1,
         appid: '10000',
         ...data
       }
-      this.msgList.push(params)
-      this.saveMessageAction(params)
-    },
-    // 加载更多
-    onLoadMore () {
-      this.listInfo.pageIndex++
-      this.getFindMessage()
+      this.saveMessageAction(params).then(res => {
+        const { error_code: code } = res
+        if (code === 0) {
+          params.fool = 1
+          this.msgList.push(params)
+          const msgSocket = {
+            type: 6,
+            content: content,
+            contentType: isHyperlink(content) ? 2 : 1,
+            sUserType: 2,
+            rUserId: '455b415651' || this.other.userId,
+            rUserType: this.other.userType,
+            sessionId: this.other.sessionId
+          }
+          this.webSocketSend(msgSocket)
+          const kfKeywords = ['客服', '人工', '人工客服']
+          if (kfKeywords.includes(content)) {
+            // 转人工客服
+            this.onTurnPeople(content)
+          }
+        } else {
+          params.error = true
+          this.msgList.push(params)
+        }
+        this.getUserListFn()
+        this.$refs.userSide?.setScrollTop()
+      })
     },
     // 发送、存储图片消息
     onUploadImage (data) {
       console.log(data)
       const params = {
-        receiveId: this.other.sessionId,
-        ownType: 1,
+        receiveId: this.other.userType === 2 ? this.other.userId : this.other.sessionId,
+        ownType: this.other.userType, // 对方身份
         title: '图片',
         item: 8,
-        itemType: 5,
+        itemType: this.other.userType === 2 ? 2 : 5,
         link: '',
         fool: 1,
-        type: 1,
+        type: 3,
         appid: '10000',
         content: JSON.stringify(data),
         create_time: dateFormatter(new Date().getTime())
       }
-      this.msgList.push(params)
-      this.saveMessageAction(params)
+      this.saveMessageAction(params).then(res => {
+        if (res.code === 0) {
+          this.msgList.push(params)
+        } else {
+          params.error = true
+          this.msgList.push(params)
+        }
+      })
       this.$refs.userSide?.setScrollTop()
     },
+    // 发送附件消息
     onUploadAttach (data) {
       const params = {
-        receiveId: this.other.sessionId,
-        ownType: 1,
+        receiveId: this.other.userType === 2 ? this.other.userId : this.other.sessionId,
+        ownType: this.other.userType, // 对方身份
         title: '附件',
         item: 8,
-        itemType: 5,
+        itemType: this.other.userType === 2 ? 2 : 5,
         link: '',
         fool: 1,
-        type: 1,
+        type: 3,
         appid: '10000',
         content: JSON.stringify(data),
         create_time: dateFormatter(new Date().getTime())
       }
-      this.msgList.push(params)
-      this.saveMessageAction(params)
+      this.saveMessageAction(params).then(res => {
+        if (res.code === 0) {
+          this.msgList.push(params)
+        } else {
+          params.error = true
+          this.msgList.push(params)
+        }
+      })
       this.$refs.userSide?.setScrollTop()
+    },
+    // 加载更多
+    onLoadMore () {
+      this.listInfo.pageIndex++
+      this.getFindMessage()
+    },
+    // 获取已咨询过的客户列表
+    async getUserListFn () {
+      const { data } = await getUserList({
+        userType: 2
+      })
+      if (data) {
+        this.userList = data.map(v => {
+          return {
+            title: v.title,
+            headImg: v.headimg,
+            nickName: v.name,
+            badge: v.number,
+            allNumber: v.allNumber,
+            message: v.content,
+            userId: v.userId,
+            userType: v.userType,
+            lastTime: momentTime(v.create_time, 'HH:mm:ss'),
+            active: false
+          }
+        })
+        this.userList.forEach(s => {
+          if (s.userId === this.other.userId) {
+            s.active = true
+          }
+        })
+      } else {
+        this.userList = []
+        this.createSessionFn()
+      }
+    },
+    // 获取未读消息总量
+    async getMessageCountFn () {
+      const params = {
+        userType: 2 // 用户类型:2用户 1客服
+      }
+      const { data } = await getMessageCount(params)
+      this.unReadCount = data
     }
   }
 }

+ 2 - 2
vue.config.js

@@ -45,8 +45,8 @@ module.exports = defineConfig({
           '^/api': ''
         }
       },
-      '^/api/orderCenter': {
-        target: 'http://192.168.21.40:8888',
+      '^/api/orderApplication': {
+        target: 'http://192.168.21.175:9999',
         changeOrigin: true,
         logLevel: 'debug',
         pathRewrite: {

+ 5 - 0
yarn.lock

@@ -4707,6 +4707,11 @@ module-alias@^2.2.2:
   resolved "https://registry.npmmirror.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0"
   integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==
 
+moment@^2.29.4:
+  version "2.29.4"
+  resolved "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
+  integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
+
 mrmime@^1.0.0:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"