101先生

开发一个在线聊天

在线聊天技术选型

在线聊天因为涉及到互相通信,所以采用socket.io

前端框架 vue2

打包工具 vite

在线gitee地址: https://gitee.com/service-chat/service-chat

整体架构

初始化之后的效果如下:

init 初始化

init 主要是从url参数中获取用户的id,然后调用signalrService

    // 初始化
    init() {
      this.sender.id = parseInt(this.$route.query.sendId);
      if (!(this.sender.id > 0)) {
        alert("请添加sendId参数");
        return false;
      }
      // 当前产品
      let product = this.$store.state.productList.filter(
        (x) => x.Id === this.$route.query.productId
      );
      if (product.length > 0) {
        // 卡片信息内容
        this.browseCard.Id = product[0].Id;
        this.browseCard.Name = product[0].Name;
        this.browseCard.ShortDescription = product[0].ShortDescription;
        this.browseCard.DefaultPictureUrl = product[0].DefaultPictureUrl;
        this.browseCard.Amount = "编码:" + product[0].ProductCode;
        this.browseCard.Type = 1;
      }
      // 当前用户
      let userInfo = this.$store.state.userList.filter(
        (x) => x.id == this.sender.id
      )[0];
      // 快速回复
      this.fastReplay = this.$store.state.fastReply;
      if (userInfo) {
        this.sender.name = userInfo.name;
        // 修改昵称时的临时记录昵称
        this.temporaryUserName = userInfo.name;
        this.sender.isService = userInfo.isService;
        this.sender.receptNum = userInfo.receptNum;
        // 修改接待用户数量时的临时记录接待用户数量
        this.temporaryReceptNumber = userInfo.receptNum;
      } else {
        alert("请保证sendId参数在userList.json文件中存在");
        return false;
      }
      // 发送欢迎语
      let welCome = this.$store.state.robotReply.filter(
        (x) => x.Answer.indexOf("欢迎语") !== -1
      );
      if (welCome.length > 0) {
        this.signalrService(welCome[0], 1, 4, false);
      }
    },

signalrService

当初次初始化的时候,只是把当前的内容发送到当前会话内容里边去。

// 1.信息组装
// 发送者身份:0 机器人,1 客服员,2.会员
// 信息类型 :0 文本,1 图片,2 表情,3 商品卡片/订单卡片,4 机器人回复
    signalrService(
      content,
      identity,
      type,
      isSendOther = true,
      isRobot = false
    ) {
      // 发送信息
      if (this.sendState) {
        let createDate = this.nowTime();
        let noCode = +new Date();
        this.infoTemplate = {
          SendId: this.sender.id,
          ReviceId: isRobot ? 0 : this.revicer.id,
          Content: content,
          Identity: identity,
          Type: type,
          State: isRobot || !this.sender.onlineState ? 1 : 0,
          // 发送时间戳
          NoCode: noCode,
          OutTradeNo: this.revicer.outTradeNo,
          CreateDateUtc: createDate,
          Title: null,
          Description: null,
          Label: null,
          Thumbnail: null,
          NoSend: true,
        };
        // 发送到当前会员内容里边中
        this.toSendInfo(this.infoTemplate);
        if (isSendOther) {
          this.sendMsg(this.infoTemplate);
        }
        this.sendState = isRobot || !this.sender.onlineState ? true : false;
        this.sendInfo = type === 2 ? this.sendInfo : "";
        this.toBottom(100);
      } else {
        this.showMsg("发送太快啦,请稍后再试");
      }
    }

和机器人对话

如果客服是机器人的话,用户依然可以发送一些信息给机器人,比如发送一些信息,效果如下:

当然也可以点击机器人发送过来的信息,比如查看如何操作退款,如何操作提货等

发送信息给机器人

可以和机器人聊天,可以把一些用户常见的问题,形成标准答案,当用户输入的问题的时候,如果用户输入的问题在问题库里边,可以直接按照标准问题答案进行回复。

发送消息给机器人是使用的sendToRobot

 // 机器人聊天
sendToRobot() {
  console.log(1223);
  if (this.sendInfo != "") {
    let createDate = this.nowTime();
    let noCode = +new Date();
    let content = this.sendInfo;
    this.sendInfo = "";
    // 封装消息
    this.infoTemplate = {
      SendId: this.sender.id,
      ReviceId: 0,
      Content: content,
      Identity: 2,
      Type: 0,
      State: 0,
      NoCode: noCode,
      OutTradeNo: null,
      CreateDateUtc: createDate,
      Title: null,
      Description: null,
      Label: null,
      Thumbnail: null,
      NoSend: true,
    };
    // 把消息加入到消息会话内容里边
    this.toSendInfo(this.infoTemplate);
    // 把信息拉到最低下,因为消息需要展示最新的
    this.toBottom(100);
    // 触发socket的sendToRobot事件
    this.socket.emit("sendToRobot", this.infoTemplate);
    // 设定一个时间,如果超过了固定时间,就设置为发送失败
    this.sendFailed(this.infoTemplate);
  } else {
    return null;
  }
}

在后端接收sendToRobot事件,然后看看是否有发送过来问题的固定答案,然后触发changOrShowMsg

//发送信息给机器人
socket.on("sendToRobot", (data) => {
  let welCome = robotReply.filter(
    (x) => x.Answer.indexOf(data.Content) !== -1
  );
  socket.emit("reviceFromRobot", {
    content:
      welCome.length > 0
        ? welCome[0]
        : "非常对不起哦,不知道怎么回答这个问题呢,我会努力学习的。",
    flag: welCome.length > 0 ? true : false,
  });
  socket.emit("changOrShowMsg", data);
});

当前端接收到changOrShowMsg后,把消息设置为发送成功

// 修改信息状态
this.socket.on("changOrShowMsg", (data) => {
  this.sendState = true;
  // 清除sendFailed设置的定时器,然后设置成功
  clearTimeout(this.msgTimer);
  this.conversition.forEach((x) => {
    if (x.NoCode !== null && x.NoCode === data.NoCode) {
      x.State = 1;
    }
  });
});

人工聊天

如果觉得客服机器人不能满足需求的时候,可以通过点击转人工转人工客服,和京东淘宝都类似,因为很多情况下,机器人都不能满足用户的需求,所以需要转人工

客服不在线

调用函数是callPeople

// 呼叫客服
callPeople() {
  // 显示loading
  this.loading();
  // 呼叫客服
  this.joinChat();
},

呼叫客服,其实就是看看有没有客服在线

//加入会话
joinChat() {
  // 呼叫客服
  this.socket.emit("joinChat", {
    SendId: this.sender.id,
    ReviceId: this.revicer.id,
    SendName: this.sender.name,
    ReviceName: this.revicer.name,
    IsService: this.sender.isService,
    NoCode: this.noCode,
  });
},

在后端监听joinChat事件,逻辑比较清晰,就是监听到有用户想加入进来的时候,判断当前的是否有客服在线,如果有客服在线,则看下是否有空闲时间的客服,如果每个客服都很忙,达到了最大服务用户数量,则显示客服较忙,稍微再等会,如果有空闲的客服,则把客服分配服务于当前用户。

// 加入聊天
socket.on("joinChat", (data) => {
  let serviceList = null;
  let index = 0;
  // 如果发送消息的不是客服
  if (!data.IsService) {
    // 当前登录的客服列表
    serviceList = users.filter((x) => x.IsService === true);
    // 当前登录的客服列表的人数
    let serviceCount = serviceList.length;
    for (let i = serviceCount - 1; i >= 0; i--) {
      let item = serviceList[i];
      // 当前登录的用户列表
      let number = users.filter((x) => x.ReviceId === item.SendId).length;
      // 当前客服可以接待的最大用户数量
      let num = userList.filter((x) => x.id === item.SendId)[0].receptNum;
      // 如果当前登录的用户数量大于当前客服可以接待的数量,把该客服删除
      if (number >= num) {
        serviceList.splice(i, 1);
      }
    }
    // 如果当前登录的客服数量大于0并且每个客服已经达到的最大的服务用户数量
    if (serviceCount > 0 && serviceList.length <= 0) {
      socket.emit("joinError", {
        msg: "当前咨询人数较多,请稍后再试",
      });
      return;
      // 还有剩余客服
    } else if (serviceList.length > 0) {
      // 随机分配客服
      index = randomNum(0, serviceList.length - 1);
      socket.emit("joinTip", {
        ReviceName: serviceList[index].SendName,
        ReviceId: serviceList[index].SendId,
        ReviceOutTradeNo: serviceList[index].OutTradeNo,
      });
      // 让会员加入房间
      socket.join(serviceList[index].OutTradeNo);
      // 如果没有客服在线,则返回暂无客服在线
    } else {
      socket.emit("joinError", {
        msg: "暂无客服在线",
      });
      return;
    }
  } else {
    // 如果发送消息的是客服,则加入到聊天室里边
    socket.join(socket.id);
  }
  // 若该用户已登录,将旧设备登录的用户强制下线,多个用户多端登录
  let oldUser = users.filter((x) => x.SendId === data.SendId);
  if (oldUser.length > 0) {
    socket.to(oldUser[0].OutTradeNo).emit("squeezeOut", {
      noCode: oldUser[0].NoCode,
    });
  }
  // 存在用户信息时将旧记录删除并且重新记录
  users = users.filter((x) => x.SendId !== data.SendId);
  let user = {
    SendId: data.SendId,
    SendName: data.SendName,
    ReviceId: serviceList ? serviceList[index].SendId : data.ReviceId,
    ReviceName: serviceList ? serviceList[index].SendName : data.ReviceName,
    NoCode: data.NoCode,
    OutTradeNo: socket.id,
    Room: data.IsService ? socket.io : serviceList[index].OutTradeNo,
    IsService: data.IsService,
    IsSelect: false,
    SessionContent: data.SendName + "加入会话",
    UnRead: 0,
    CloseSession: false,
  };
  // 用户重新加入
  users.push(user);

  // 把登录成功的sendId记录下来
  socket.SendId = data.SendId;
  io.emit("joinSuccess", {
    user,
    users,
  });
});

前面没有上线客服,所以当用户想转人工的时候,只能显示暂无客服,现在看下客服端是什么样的。

效果如下:

客服可以设置上线或者离线,当客服上线之后,这个时候,当用户选择客服聊天后,就可以选择客服了。

调用

// 修改在线状态
changeOnLine() {
  if (!this.sender.onlineState) {
    this.loading();
    // 客服上线
    this.socket.emit("joinChat", {
      SendId: this.sender.id,
      SendName: this.sender.name,
      ReviceId: -1,
      ReviceName: this.revicer.name,
      IsService: true,
      NoCode: this.noCode,
    });
  } else {
    // 离线
    this.loading();
    this.isSelectSession = false;
    this.socket.emit("offLine", {
      SendId: this.sender.id,
      NoCode: this.noCode,
    });
  }
},

后端如果接收到客服上线,就把客服加入到socket,也就是joinChat

// 如果发送消息的是客服,则加入到聊天室里边
socket.join(socket.id);

如果客服已经在线了,就可以转人工和客服聊天了,

 // 随机分配客服
index = randomNum(0, serviceList.length - 1);
socket.emit("joinTip", {
  ReviceName: serviceList[index].SendName,
  ReviceId: serviceList[index].SendId,
  ReviceOutTradeNo: serviceList[index].OutTradeNo,
});
// 让会员加入房间
socket.join(serviceList[index].OutTradeNo);

可以看到后端接收到信息后,触发joinTip,然后用户就可以和客服聊天了。

发送信息,通过后端通过sendMsg来处理

// 发送消息
socket.on("sendMsg", (data) => {
  // 设置用户未读
  users.map((x) => {
    if (x.SendId === data.SendId) {
      x.SessionContent = data.Content;
      x.UnRead = 1;
      return x;
    }
  });
  //
  let sender = users.filter((x) => x.SendId === data.SendId);
  let revicer = users.filter((x) => x.SendId === data.ReviceId);
  if (sender.length < 0) {
    socket.emit("offLineTip", {
      msg: "您已掉线,请重新连接",
    });
    return;
  }
  if (revicer.length < 0) {
    socket.emit("offLineTip", {
      msg: "对方已离线",
    });
    return;
  }
  data.State = 1;
  // 向socket触发reviceMsg
  socket.to(data.OutTradeNo).emit("reviceMsg", data);
  socket.emit("changOrShowMsg", data);
});

可以看到,是通过socket.to(data.OutTradeNo).emit("reviceMsg", data); 来触发

// 接收信息
this.socket.on("reviceMsg", (data) => {
  if (this.sender.isService && data.ReviceId == this.sender.id) {
    this.playMusic();
    this.currentSessionPeople.forEach((x) => {
      if (x.SendId === data.SendId) {
        if (!x.IsSelect) x.UnRead++;
        switch (data.Type) {
          case 0:
            x.SessionContent = data.Content;
            break;
          case 1:
            x.SessionContent = "图片";
            break;
          case 2:
            x.SessionContent = "表情";
            break;
          case 3:
            x.SessionContent = "卡片";
            break;
        }
      }
    });
  }
  if (this.sender.onlineState) this.toSendInfo(data);
});

发送图片

不管是用户或者是客服发送图片都是调用sendMsg

//发送图片
sendImage(e) {
  const fileObj = e.target.files[0];
  let identity = this.sender.isService ? 1 : 2;
  if (fileObj != null) {
    // 判断是否是图片
    if (!/image\/\w+/.test(fileObj.type)) {
      return alert("请选择图片文件!", { icon: 5, time: 1000 });
    }
    var fd = new FormData();
    fd.append("file", fileObj);
    // 判断图片大小
    if (fileObj.size > 1024 * 1024 * 2 && fileObj.size < 1024 * 1024 * 10) {
      let reader = new FileReader();
      reader.readAsDataURL(fileObj);
      reader.onload = (e) => {
        let image = new Image(); //新建一个img标签(还没嵌入DOM节点)
        image.src = e.target.result;
        image.onload = () => {
          let canvas = document.createElement("canvas"),
            context = canvas.getContext("2d"),
            imageWidth = image.width / 2, //压缩后图片的大小
            imageHeight = image.height / 2,
            data = "";
          canvas.width = imageWidth;
          canvas.height = imageHeight;
          context.drawImage(image, 0, 0, imageWidth, imageHeight);
          data = canvas.toDataURL("image/jpeg");
          let newFile = this.dataURLtoFile(data); //压缩完成
          fd = new FormData();
          fd.append("file", newFile);
          // 显示出来
          this.signalrService(data, identity, 1);
          this.$refs.referenceUpload.value = null;
        };
      };
    } else if (fileObj.size > 1024 * 1024 * 10) {
      return alert("上传图片不能超过10M!", { icon: 5, time: 1000 });
    } else {
      let reader = new FileReader();
      reader.readAsDataURL(fileObj);
      reader.onload = (e) => {
        this.signalrService(e.target.result, identity, 1);
        this.$refs.referenceUpload.value = null;
      };
    }
  }
},

后面的处理就和发送文字类似了

发送表情

发送表情是直接把图片作为发送内容进行发送的,使用如下代码:

<template v-for="(item, index) in expressions">
  <li>
    <img
      class="customerSendExpression"
      v-bind:src="item.image"
      v-bind:title="item.title"
      @click="toSend(item.image, 2, 2)"
    />
  </li>
</template>

本文由mdnice多平台发布

posted on 2022-08-18 11:43  邹小邹168  阅读(114)  评论(0编辑  收藏  举报

导航