最近正好项目中有个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>
样式你们自己适配,不弄了,最终效果
发表评论