当前位置: 代码网 > it编程>前端脚本>Vue.js > VUE+websocket实现后台管理系统实时客服聊天IM模块

VUE+websocket实现后台管理系统实时客服聊天IM模块

2024年08月02日 Vue.js 我要评论
websocket实现实时客服
最近正好项目中有个im的实时客服功能上线,研究了一下把代码发出来给大家参考。
复制到项目中替换一下api接口可以直接使用
<template>

  <div class="wrap">

    <!-- 底部 -->
    <div class="infobox">
      <!-- 左边用户列表 -->
      <div class="userlist">
        <div class="searchbox">
          <el-input
            placeholder="请输入内容"
            v-model="search"
            class="input-with-select"
            size="mini"
            @input="inquire"
          >
            <i
              class="el-icon-search el-input__icon"
              slot="suffix"
              @click="handleiconclick"
            >
            </i>
          </el-input>

        </div>
        <div class="userlistbox">
          <div
            v-for="(item, index) in userlistdata"
            :key="index"
            @click="getact(item, index)"
            :class="item.oppositeid == act ? 'userflexact' : 'userflex'"
          >
            <div class="unread-count">
              <img
                :src="(item.sendtype === 1 ? item.receiveavatarurl : item.avatarurl) || require('../../assets/images/user.png')"
                class="head_portrait2"
                style="margin-left: 20px;margin-right: 5px"
              />
              <div :class="{ 'unreadcount': item.unreadcount > 0 }"></div>
            </div>
            <div style="margin-right: 40px">
              <el-tooltip
                :content="item.sendtype === 1 ? item.receivenickname : item.nickname"
                placement="bottom"
                effect="light"
              >
                <div
                  style="color: #565656"
                  class="username"
                >
                  {{ item.sendtype === 1 ? item.receivenickname : item.nickname }}
                </div>
              </el-tooltip>
              <div class="userinfo"><span v-show="item.unreadcount">[{{ item.unreadcount }}条]</span> {{ item.content }}
              </div>
            </div>
            <div style="margin-right: 10px; font-size: 14px; color: #ccc">
              {{ dateformat(item.addtime, 'hh:mm:ss') }}
            </div>
          </div>
        </div>
      </div>
      <!-- 右边输入框和信息展示 -->
      <div class="infolist">
        <!-- 信息 -->
        <div
          v-show="act"
          class="infotop"
          ref="scrollbox"
          id="box"
        >
          <div
            :class="item.sendtype !== 1 ? 'chatinfoleft' : 'chatinforight'
              "
            v-for="(item, index) in userinfolist"
            :key="index"
          >
            <img
              :src="item.avatarurl || require('../../assets/images/user.png')"
              class="head_portrait2"
            />
            <div :class="item.sendtype !== 1 ? 'chatleft' : 'chatright'">
              <div
                class="text"
                v-if="item.picurls[0] === ''"
                v-html="item.content"
              ></div>
              <el-image
                v-else
                style="width: 70px; height: 70px"
                :src="item.picurls[0]"
                :preview-src-list="item.picurls"
              >
              </el-image>
            </div>
          </div>
        </div>
        <!-- 输入框 -->
        <div
          v-show="act"
          class="infobottom"
        >
          <div class="infoicon">
            <el-upload
              :headers="headers"
              :show-file-list="false"
              :on-success="handleavatarsuccess"
              accept="image/jpg,image/jpeg,image/png"
              :action="uploadimgurl"
              :before-upload="beforeavatarupload"
            >
              <i class="el-icon-picture-outline-round"></i>
            </el-upload>

            <!--              <i @click="extend('发送商品')" class="el-icon-sell"></i>-->
            <!--              <i @click="extend('设置')" class="el-icon-setting"></i>-->
            <!--              <i @click="extend('聊天记录')" class="el-icon-chat-dot-round"></i>-->
            <!--              <i @click="extend('更多选项')" class="el-icon-more-outline"></i>-->
          </div>
          <textarea
            type="textarea"
            class="infoinput"
            v-model="textarea"
            @keydown.enter.exact="handlepushkeyword($event)"
            @keyup.ctrl.enter="linefeed"
            :disabled="isshow == 1 ? false : true"
          />
          <div
            class="fasong"
            @click="setup"
            v-show="isshow == 1 ? true : false"
          >
            发送
          </div>
        </div>
      </div>
    </div>
  </div>

</template>

js部分

<script>
import { history, messagelist, messageread } from "@/api/onlineservice";
import { gettenantinfo } from "@/api/tenant";
import { gettenantid, gettoken } from "@/utils/auth";
import { uploadpath } from "@/api/storage";

export default {
  data () {
    return {

      headers: {
        "x-litemall-admin-token": gettoken(),
        "x-litemall-tenantid": gettenantid(),
      },
      uploadimgurl: uploadpath, // 上传的图片服务器地址
      //websocket部分
      path: `ws://192.168.0.200:6915/adminwebsocket/${this.$store.state.user.token}`, //后台的websocket地址
      ws: null, //建立的连接
      lockreconnect: false, //是否真正建立连接
      timeout: 10 * 1000, //30秒一次心跳
      timeoutobj: null, //心跳心跳倒计时
      servertimeoutobj: null, //心跳倒计时
      timeoutnum: null, //断开 重连倒计时

      // 在线状态
      state: 1,
      //搜索用户
      search: "",
      //用户列表渲染数据
      userlistdata: [],
      //用户列表筛选数据
      userlistdatas: [],
      //用户点击选中变色
      act: null,
      senduserid: null,
      // 加号弹框
      dialogvisible: false,
      //历史信息
      userinfolist: [],
      //输入框
      textarea: "",
      //滚动条距离顶部距离
      scrolltop: 0,
      //发送和输入显隐
      isshow: 0,
      dataform: {},
      sendtype: '',
    };
  },
  created () {
    console.log(this.$store.state, 111111111)
    this.gettenantinfolist()
    this.getlist()
    this.initwebpack();
  },
  beforedestroy () {
    // 离开页面后关闭连接
    this.ws.close();
    // 清除时间
    cleartimeout(this.timeoutobj);
    cleartimeout(this.servertimeoutobj);
  },
  methods: {
    handleavatarsuccess (res, file) {
      console.log(res, file)

      this.ws.send(
        json.stringify({
          avatarurl: this.dataform.tenantpicurl,
          nickname: this.dataform.tenantname,
          content: "",
          picurls: res.data.url,
          // position: "right",
          receiveuserid: this.senduserid
        })
      );
    },
    beforeavatarupload (file) {
      const isjpg = (file.type === 'image/jpeg' || file.type === 'image/jpeg' || file.type === 'image/png');
      const islt2m = file.size / 1024 / 1024 < 2;

      if (!isjpg) {
        this.$message.error('请上传图片');
      }
      if (!islt2m) {
        this.$message.error('上传头像图片大小不能超过 2mb!');
      }
      return isjpg && islt2m;
    },
    dateformat (timestamp, format) {
      if (string(timestamp).length === 10) {
        timestamp = timestamp * 1000
      }
      let date = new date(timestamp)
      let y = date.getfullyear()
      let m = date.getmonth() + 1
      let d = date.getdate()
      let hour = date.gethours()
      let min = date.getminutes()
      let sec = date.getseconds()
      if (format === 'yyyy') {
        return y // 2021
      } else if (format === 'yyyy-mm') { // 2021-07
        return y + '-' + (m < 10 ? '0' + m : m)
      } else if (format === 'yyyy-mm-dd') { // 2021-07-12
        return y + '-' + (m < 10 ? '0' + m : m) + '-' + (d < 10 ? '0' + d : d)
      } else if (format === 'hh:mm:ss') { // 10:20:35
        return (hour < 10 ? '0' + hour : hour) + ':' + (min < 10 ? '0' + min : min) + ':' + (sec < 10 ? '0' + sec : sec)
      } else if (format === 'yyyy-mm-dd hh:mm') { // 2021-07-12 10:20
        return y + '-' + (m < 10 ? '0' + m : m) + '-' + (d < 10 ? '0' + d : d) + ' ' + (hour < 10 ? '0' + hour : hour) + ':' + (min < 10 ? '0' + min : min)
      } else if (format === 'yyyy-mm-dd hh:mm:ss') { // 2021-07-12 10:20:35
        return y + '-' + (m < 10 ? '0' + m : m) + '-' + (d < 10 ? '0' + d : d) + ' ' + (hour < 10 ? '0' + hour : hour) + ':' + (min < 10 ? '0' + min : min) + ':' + (sec < 10 ? '0' + sec : sec)
      } else {
        return '--'
      }
    },
    gettenantinfolist () {
      gettenantinfo().then((response) => {
        this.dataform = response.data;
      });
    },
    getlist () {
      messagelist().then(res => {
        this.userlistdata = res.data.list
        this.userlistdatas = res.data.list
      })
    },
    //搜索icon
    handleiconclick () {
      console.log(1);
    },
    //点击用户
    getact (val, index) {
      if (this.act === val.oppositeid) return
      this.isshow = 1;
      // 点击用户切换数据时先清除监听滚动事件,防止出现没有历史数据的用户,滚动条为0,会触发滚动事件
      this.$refs.scrollbox.removeeventlistener("scroll", this.srtop);
      //点击变色
      this.act = val.oppositeid;
      this.senduserid = (val.sendtype === 1 ? val.receiveuserid : val.senduserid)
      //清空消息数组
      this.userinfolist = [];
      let params = {
        receiveuserid: val.sendtype === 1 ? val.receiveuserid : val.senduserid,
        limit: 0
      }
      history(params).then(res => {
        this.userinfolist = res.data.list
        messageread({ "senduserid": this.senduserid }).then(() => { this.getlist() })
        // 模拟一下点击用户出现历史记录的样子,实际开发中是axios请求后数组赋值然后调用setpagescrollto
        // 直接调用不生效:因为你历史数据刚给,渲染的时候盒子高度还没有成型,所以直接调用拿不到,用个定时器让他在下一轮循环中调用,盒子就已经生成了
        this.$nexttick(() => { // 一定要用nexttick
          this.setpagescrollto();
          //页面滚动条距离顶部高度等于这个盒子的高度
          this.$refs.scrollbox.scrolltop = this.$refs.scrollbox.scrollheight;
        })
      })
    },
    // 模糊搜索用户
    inquire () {

      let fuzzy = this.search;
      if (fuzzy) {
        this.userlistdata = this.userlistdatas.filter((item) => {
          return item.receivenickname.includes(fuzzy);
        });
      } else {
        this.userlistdata = this.userlistdatas;
      }
    },
    //发送
    setup () {
      console.log("发送内容:", this.textarea);
      // this.userinfolist.push({
      //   avatarurl: this.dataform.tenantname.tenantpicurl,
      //   nickname: this.dataform.tenantname,
      //   content: this.textarea,
      //   picurls: "",
      //   // position: "right",
      //   receiveuserid: this.senduserid,
      //   sendtype: 1
      // });
      this.ws.send(
        json.stringify({
          avatarurl: this.dataform.tenantpicurl,
          nickname: this.dataform.tenantname,
          content: this.textarea,
          picurls: "",
          // position: "right",
          receiveuserid: this.senduserid
        })
      );
      this.textarea = "";
      // 页面滚动到底部
      this.$nexttick(() => { // 一定要用nexttick
        this.setpagescrollto();
        //页面滚动条距离顶部高度等于这个盒子的高度
        this.$refs.scrollbox.scrolltop = this.$refs.scrollbox.scrollheight;
      })
    },
    // 监听键盘回车阻止换行并发送
    handlepushkeyword (event) {
      console.log(event);
      if (event.keycode === 13) {
        event.preventdefault(); // 阻止浏览器默认换行操作
        this.setup(); //发送文本
        return false;
      }
    },
    // 监听按的是ctrl + 回车,就换行
    linefeed () {
      console.log("换行");
      this.textarea = this.textarea + "\n";
    },
    //点击icon
    extend (val) {
      alert("你点击了:" + val);
    },
    //滚动条默认滚动到最底部
    setpagescrollto (s, c) {
      //获取中间内容盒子的可见区域高度
      this.scrolltop = document.queryselector("#box").offsetheight;
      settimeout((res) => {
        //加个定时器,防止上面高度没获取到,再获取一遍。
        if (this.scrolltop != this.$refs.scrollbox.offsetheight) {
          this.scrolltop = document.queryselector("#box").offsetheight;
        }
      }, 100);
      //scrolltop:滚动条距离顶部的距离。
      //把上面获取到的高度座位距离,把滚动条顶到最底部
      this.$refs.scrollbox.scrolltop = this.scrolltop;
      //判断是否有滚动条,有滚动条就创建一个监听滚动事件,滚动到顶部触发srtop方法
      if (this.$refs.scrollbox.scrolltop > 0) {
        this.$refs.scrollbox.addeventlistener("scroll", this.srtop);
      }
    },
    //滚动条到达顶部
    srtop () {
      //判断:当滚动条距离顶部为0时代表滚动到顶部了
      if (this.$refs.scrollbox.scrolltop == 0) {
        //逻辑简介:
        //到顶部后请求后端的方法,获取第二页的聊天记录,然后插入到现在的聊天数据前面。
        //如何插入前面:可以先把获取的数据保存在 a 变量内,然后 this.userinfolist=a.concat(this.userinfolist)把数组合并进来就可以了

        //拿聊天记录逻辑:
        //第一次调用一个请求拉历史聊天记录,发请求时参数带上页数 1 传过去,拿到的就是第一页的聊天记录,比如一次拿20条。你显示出来
        //然后向上滚动到顶部时,触发新的请求,在请求中把分页数先 +1 然后再请求,这就拿到了第二页数据,然后通过concat合并数组插入进前面,依次类推,功能完成!
        // alert("已经到顶部了");
      }
    },

    //-----------------------以下是websocket部分方法

    // 初始化websocket链接
    initwebpack () {
      if (typeof websocket === "undefined") {
        alert("您的浏览器不支持socket");
      } else {
        this.ws = new websocket(this.path); //实例
        this.ws.onopen = this.onopen; //监听链接成功
        this.ws.onmessage = this.onmessage; //监听后台返回消息
        this.ws.onclose = this.onclose; //监听链接关闭
        this.ws.onerror = this.onerror; //监听链接异常
      }
    },
    //重新连接
    reconnect () {
      var that = this;
      if (that.lockreconnect) {
        return;
      }
      that.lockreconnect = true;
      //没连接上会一直重连,设置延迟避免请求过多
      that.timeoutnum && cleartimeout(that.timeoutnum);
      that.timeoutnum = settimeout(function () {
        that.initwebpack(); //新连接
        that.lockreconnect = false;
      }, 5000);
    },
    //重置心跳
    reset () {
      var that = this;
      cleartimeout(that.timeoutobj); //清除心跳倒计时
      cleartimeout(that.servertimeoutobj); //清除超时关闭倒计时
      that.start(); //重启心跳
    },
    //开启心跳
    start () {
      var self = this;
      self.timeoutobj && cleartimeout(self.timeoutobj); //心跳倒计时如果有值就清除掉,防止重复
      self.servertimeoutobj && cleartimeout(self.servertimeoutobj); //超时关闭倒计时如果有值就清除掉,防止重复
      //然后从新开一个定时器
      self.timeoutobj = settimeout(function () {
        //这里通过readystate判断链接状态,有四个值,0:正在连接,1:已连接,2:正在断开,3:已经断开或者链接不成功
        if (self.ws.readystate == 1) {
          //如果连接正常,给后天发送一个值,可以自定义,然后后台返回我们一个信息,我们接收到后会触发onmessage方法回调
          self.ws.send(
            json.stringify({ "token": gettoken() })
          );
        } else {
          //如果检测readystate不等于1那也就代表不处在链接状态,那就是不正常的,那就调用重连方法
          self.reconnect();
        }
        //从新赋值一个超时计时器,这个定时器的作用:当你触发心跳的时候可能会出现一个情况,后台崩了,前台发了个心跳,没有回应,就不会触发onmessage方法
        //所以我们需要在这个心跳发送出去了后,再开一个定时器,用于监控心跳返回的时间,比如10秒,那么10秒内如果后台回我了,触发onmessage方法,自然就会把心跳时间和超时倒计时一起清空掉
        //也就不会触发这个关闭连接,但是如果10秒后还是没有收到回应,那么就会触发关闭连接,而关闭连接方法内又会触发重连方法,循环就走起来了。
        self.servertimeoutobj = settimeout(function () {
          //如果超时了就关闭连接
          self.ws.close();
        }, self.timeout);
      }, self.timeout);
    },
    //连接成功
    onopen () {
      if (this.ws.readystate == 1) {
        //如果连接正常,给后天发送一个值,可以自定义,然后后台返回我们一个信息,我们接收到后会触发onmessage方法回调
        this.ws.send(
          json.stringify({ "token": gettoken() })
        );
      }
      // this.reset(); //链接成功后开启心跳
    },
    //接受后台信息回调
    onmessage (e) {
      /**这里写自己的业务逻辑代码**/
      console.log("收到后台信息:", json.parse(e.data));
      let data = json.parse(e.data)
      if (this.act) {
        if (data[0].sendtype === 1) {
          this.userinfolist.push(...data);
        } else {
          if (this.act === (data[0].nickname.sendtype === 1 ? data[0].receiveuserid : data[0].senduserid)) {
            this.userinfolist.push(...data);
          }
        }

      }
      if (this.act) {
        messageread({ "senduserid": this.senduserid }).then(() => { this.getlist() })
      } else {
        this.getlist()
      }
      this.$nexttick(() => { // 一定要用nexttick
        this.setpagescrollto();
        //页面滚动条距离顶部高度等于这个盒子的高度
        this.$refs.scrollbox.scrolltop = this.$refs.scrollbox.scrollheight;
      })
      this.reset(); //收到服务器信息,心跳重置
    },
    //关闭连接回调
    onclose (e) {
      console.log("连接关闭");
      this.reconnect(); //重连
    },
    //连接异常回调
    onerror (e) {
      console.log("出现错误");
      this.reconnect(); //重连
    },
  },
};
</script>

样式你们自己适配,不弄了,最终效果

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com