前言

这里是给大家写了一个简单的模板来使用,可以直接复制了调调就能用。因为这个聊天也是很容易出现的功能,所以我写一个模板静态页面放在这,以后需要这个功能的时候可以不用布局了,直接复制改改。
我的代码里面有详细的注释,相信就算是刚学完前端的萌新也是看得懂的,如果有不懂得可以提问

小程序端聊天功能和websocket模板写法点这里

点击跳转

移动端H5静态页面布局模板点这里

移动端H5 聊天室布局模板 点击跳转

移动端布局样式效果图

PC端聊天页效果图

有滚动条的

没选择用户时

模糊搜索

没滚动条的

切换状态

显示完全

换行和空格都保留文本格式显示

功能简介

1,页面布局自适应缩放
2,点击在线可以切换客服状态,忙碌,离线,退出登录等
3,模糊搜索用户列表功能
4,点击用户会变底色并切换用户的聊天信息
5,刚进入时不点击选中用户时隐藏发送按钮和禁用输入框
6,用户聊天信息有滚动条时上划到顶部会触发方法
7,用户聊天信息没有滚动条的时候不触发上划到顶部的方法
8,切换用户聊天时,聊天框滚动条保持在最底部
9,发送信息时滚动条保持在最底部
10,用户名和最新消息的展示,超出的会用省略号显示,用户名可以鼠标移上去显示完全
11,输入框中回车可以发送消息,并阻止换行。
12,输入框中ctrl+回车换行
13,换行和空格保留文本格式
14,websocket以及心跳重连机制模板

测试注意部分

可以发送信息,但是我是模拟的数据,没有链接后台。所以我这个页面你们复制后可以直接测试一下效果。
但是注意,发消息只能发给花间一壶酒这个用户,因为我是定死了的给他的数组内加新消息,其他的如果想试自己改一下方法。

使用了elementul,没有的下载一下

代码

<template><div style="height: 100%; width: 100%; background-color: #ededed"><div class="wrap"><!-- 头部 --><div class="titleBox"><imgsrc="https://img-qn-2.51miz.com/preview/element/00/01/27/97/E-1279706-3813EBF9.jpg!/quality/90/unsharp/true/compress/true/format/jpg/fh/320"alt=""class="head_portrait"style="margin-left: 20px; margin-right: 20px"/><span style="color: #fff">官方客服</span><!-- 在线状态弹框 --><el-popover placement="bottom" trigger="click"><div class="stateBox2" @click="uploadState(1)"><span class="state1"></span><span class="stateText">在线</span></div><div class="stateBox2" @click="uploadState(2)"><span class="state2"></span><span class="stateText">离线</span></div><div class="stateBox2" @click="uploadState(3)"><span class="state3"></span><span class="stateText">忙碌</span></div><div class="stateBox2" @click="uploadState(4)"><span class="state4"></span><span class="stateText">退出</span></div><div class="stateBox" slot="reference" v-if="state == 1"><span class="state1"></span><span class="stateText">在线</span></div><div class="stateBox" slot="reference" v-if="state == 2"><span class="state2"></span><span class="stateText">离线</span></div><div class="stateBox" slot="reference" v-if="state == 3"><span class="state3"></span><span class="stateText">忙碌</span></div></el-popover></div><!-- 底部 --><div class="infoBox"><!-- 左边用户列表 --><div class="userList"><div class="searchBox"><el-inputplaceholder="请输入内容"v-model="search"class="input-with-select"size="mini"@input="inquire"><iclass="el-icon-search el-input__icon"slot="suffix"@click="handleIconClick"></i></el-input><el-buttonicon="el-icon-plus"size="mini"type="primary"@click="dialogVisible = true"></el-button></div><div class="userListBox"><divv-for="(item, index) in userListData":key="index"@click="getAct(item, index)":class="item.id == act ? 'userFlexAct' : 'userFlex'"><div><img:src="item.url"alt="头像"class="head_portrait2"style="margin-left: 20px"/></div><div style="margin-right: 40px"><el-tooltip:content="item.username"placement="bottom"effect="light"><div style="color: #565656" class="userName">{{ item.username }}</div></el-tooltip><div class="userInfo">{{ item.info }}</div></div><div style="margin-right: 10px; font-size: 14px; color: #ccc">{{ item.timer }}</div></div></div></div><!-- 右边输入框和信息展示 --><div class="infoList"><!-- 信息 --><div class="infoTop" ref="scrollBox" id="box"><div:class="item.position == 'left' ? 'chatInfoLeft' : 'chatInfoRight'"v-for="(item, index) in userInfoList":key="index"><img :src="item.url" alt="头像" class="head_portrait2" /><div :class="item.position == 'left' ? 'chatLeft' : 'chatRight'"><div class="text" v-html="item.info"></div></div></div></div><!-- 输入框 --><div class="infoBottom"><div class="infoIcon"><i@click="extend('照片上传')"class="el-icon-picture-outline-round"></i><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><textareatype="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><!-- 搜索框边 + 号弹框 --><el-dialogtitle="选择需要添加的联系人":visible.sync="dialogVisible"width="30%":modal="false"><span>自定义页面,还没想好写什么功能</span><span slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="dialogVisible = false">确 定</el-button></span></el-dialog></div>
</template><script>
export default {data() {return {// 在线状态state: 1,//搜索用户search: "",//用户列表渲染数据userListData: [{id: 0,url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",},{id: 1,url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",},{id: 2,url: "https://img1.baidu.com/it/u=2029513305,2137933177&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=472",username: "王子变蛤蟆",info: "你好,在吗",timer: "2022/8/9",},{id: 3,url: "https://img1.baidu.com/it/u=1960292808,1761809160&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "皇太子",info: "给我看一下这个情况呢",timer: "2022/8/2",},{id: 4,url: "https://img2.baidu.com/it/u=3684117954,695988885&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400",username: "飞天小女警",info: "模拟数据发送一下呢",timer: "2022/8/4",},{id: 5,url: "https://img2.baidu.com/it/u=4122738859,2522601053&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "罗大大大",info: "在吗",timer: "2022/8/5",},{id: 6,url: "https://img0.baidu.com/it/u=661161858,172661768&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "王子花",info: "好的,我知道了",timer: "2022/8/9",},{id: 7,url: "https://img2.baidu.com/it/u=835899845,548435859&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "开盖有奖",info: "试试看吧,再发一次",timer: "2022/8/9",},{id: 8,url: "https://img0.baidu.com/it/u=4065107391,2142799144&fm=253&fmt=auto&app=138&f=JPEG?w=527&h=500",username: "日本大爆炸",info: "在吗",timer: "2022/8/5",},{id: 9,url: "https://img2.baidu.com/it/u=2860188096,638334621&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "秋天的第一杯奶茶",info: "好的,我知道了",timer: "2022/8/9",},{id: 10,url: "https://img0.baidu.com/it/u=1694074520,2517635995&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "潮汐",info: "试试看吧,再发一次",timer: "2022/8/9",},],//用户列表筛选数据userListDatas: [{id: 0,url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",},{id: 1,url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",},{id: 2,url: "https://img1.baidu.com/it/u=2029513305,2137933177&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=472",username: "王子变蛤蟆",info: "你好,在吗",timer: "2022/8/9",},{id: 3,url: "https://img1.baidu.com/it/u=1960292808,1761809160&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "皇太子",info: "给我看一下这个情况呢",timer: "2022/8/2",},{id: 4,url: "https://img2.baidu.com/it/u=3684117954,695988885&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400",username: "飞天小女警",info: "模拟数据发送一下呢",timer: "2022/8/4",},{id: 5,url: "https://img2.baidu.com/it/u=4122738859,2522601053&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "罗大大大",info: "在吗",timer: "2022/8/5",},{id: 6,url: "https://img0.baidu.com/it/u=661161858,172661768&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "王子花",info: "好的,我知道了",timer: "2022/8/9",},{id: 7,url: "https://img2.baidu.com/it/u=835899845,548435859&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "开盖有奖",info: "试试看吧,再发一次",timer: "2022/8/9",},{id: 8,url: "https://img0.baidu.com/it/u=4065107391,2142799144&fm=253&fmt=auto&app=138&f=JPEG?w=527&h=500",username: "日本大爆炸",info: "在吗",timer: "2022/8/5",},{id: 9,url: "https://img2.baidu.com/it/u=2860188096,638334621&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "秋天的第一杯奶茶",info: "好的,我知道了",timer: "2022/8/9",},{id: 10,url: "https://img0.baidu.com/it/u=1694074520,2517635995&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "潮汐",info: "试试看吧,再发一次",timer: "2022/8/9",},],//用户点击选中变色act: Number,// 加号弹框dialogVisible: false,//模拟花间一壶酒用户的历史信息userInfoList2: [{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",position: "left",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",position: "right",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",position: "right",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗在吗在吗在吗在吗在吗在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗",timer: "2022/8/1",position: "right",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗在吗在吗在吗在吗",timer: "2022/8/1",position: "right",},],//模拟超人不换内裤用户的历史信息userInfoList3: [{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "测试测试",timer: "2022/8/9",position: "left",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "测试",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "测试测试测试测试",timer: "2022/8/1",position: "right",},],//历史信息userInfoList: [],//输入框textarea: "",//滚动条距离顶部距离scrollTop: 0,//发送和输入显隐isshow:0};},methods: {//切换客服状态uploadState(state) {if (state !== 4) {this.state = state;} else {this.$confirm("是否退出登录?", "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {this.$message({type: "success",message: "退出成功!",});}).catch(() => {this.$message({type: "info",message: "已取消退出",});});}},//搜索iconhandleIconClick() {console.log(1);},//点击用户getAct(val, index) {this.isshow=1// 点击用户切换数据时先清除监听滚动事件,防止出现没有历史数据的用户,滚动条为0,会触发滚动事件this.$refs.scrollBox.removeEventListener("scroll", this.srTop);//点击变色this.act = val.id;//清空消息数组this.userInfoList = [];// 模拟一下点击用户出现历史记录的样子,实际开发中是axios请求后数组赋值然后调用setPageScrollToif (val.username == "花间一壶酒") {this.userInfoList = this.userInfoList2;// 直接调用不生效:因为你历史数据刚给,渲染的时候盒子高度还没有成型,所以直接调用拿不到,用个定时器让他在下一轮循环中调用,盒子就已经生成了this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})} else if (val.username == "超人不换内裤") {this.userInfoList = this.userInfoList3;// 直接调用不生效:因为你历史数据刚给,渲染的时候盒子高度还没有成型,所以直接调用拿不到,用个定时器让他在下一轮循环中调用,盒子就已经生成了this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})} else if (val.username == "王子变蛤蟆") {this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})}},// 模糊搜索用户inquire() {let fuzzy = this.search;if (fuzzy) {this.userListData = this.userListDatas.filter((item) => {return item.username.includes(fuzzy);});} else {this.userListData = this.userListDatas;}},//发送setUp() {console.log("发送内容:", this.textarea);this.userInfoList2.push({url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: this.textarea,timer: "2022/8/1",position: "right",});this.textarea = "";// 页面滚动到底部this.$nextTick(() => { // 一定要用nextTickthis.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";},//点击iconextend(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("已经到顶部了");}},},
};
</script><style scoped>
.wrap {height: 80%;width: 55%;background-color: #f2f2f2;margin: auto;transform: translateY(10%);box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);border-radius: 10px;
}
.titleBox {height: 10%;width: 100%;background-image: linear-gradient(to right, #1e76bc, #69a3d5);display: flex;align-items: center;border-top-right-radius: 10px;border-top-left-radius: 10px;
}
.infoBottom {height: 30%;display: flex;flex-direction: column;
}
/* 输入框 */
.infoInput {height: 58%;width: 100%;border: none;resize: none;padding: 10px;box-sizing: border-box;background-color: #f2f2f2;color: #434343;
}
.fasong {height: 30px;width: 80px;background-color: #e8e8e8;text-align: center;line-height: 30px;border-radius: 4px;color: #58df4d;margin-top: 1%;align-self: flex-end;margin-right: 20px;cursor: pointer;
}
.infoIcon {height: 40px;width: 100%;display: flex;align-items: center;
}
.infoIcon i {font-size: 24px;color: #676767;margin-left: 15px;cursor: pointer;
}
/* 头像 */
.head_portrait {width: 3rem;height: 3rem;border-radius: 50%;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}
.head_portrait2 {width: 3rem;height: 3rem;border-radius: 50%;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}
.stateBox {margin-left: 20px;padding: 1px 8px;background-color: #fff;border-radius: 10px;text-align: center;cursor: pointer;
}
.stateBox2 {margin-left: 20px;padding: 1px 8px;background-color: #fff;border-radius: 10px;text-align: center;cursor: pointer;
}
.stateBox2:hover {background-color: #dcdcdc;
}
/* 在线 */
.state1 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #8ee80e;
}
/* 离线 */
.state2 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #cacaca;
}
/* 忙碌 */
.state3 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #ff8c1e;
}
/* 退出登录 */
.state4 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #7e7e7e;
}
.stateText {font-size: 14px;margin-left: 5px;
}
/* 列表和信息 */
.infoBox {height: 90%;width: 100%;display: flex;
}
/* 用户列表大盒子 */
.userList {height: 100%;width: 300px;border-right: 1px solid #ccc;display: flex;flex-direction: column;
}
/* 用户列表 */
.userListBox {flex: 1;width: 100%;overflow: auto;
}
/* 信息外层盒子 */
.infoList {height: 100%;width: 72%;
}
/* 信息列表 */
.infoTop {height: 70%;width: 100%;border-bottom: 1px solid #ccc;padding: 10px;box-sizing: border-box;overflow: auto;
}
/* 对方发的信息样式 */
.chatInfoLeft {min-height: 70px;margin-left: 10px;margin-top: 10px;display: flex;
}
.chatLeft {margin-left: 15px;flex: 1;
}
.chatLeft .text {color: #434343;margin-top: 8px;background-color: #e3e3e3;display: inline-block;padding: 6px 10px;border-radius: 10px;max-width: 50%;/* 忽略多余的空白,只保留一个空白 */white-space: normal;/* 换行显示全部字符 */word-break: break-all;white-space: pre-wrap;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}/* 自己发的信息样式 */
.chatInfoRight {height: 70px;margin-left: 10px;margin-top: 10px;display: flex;flex-direction: row-reverse;
}
.chatRight {margin-right: 15px;flex: 1;/* 用align-items把元素靠右对齐 */display: flex;flex-direction: column;align-items: flex-end;
}
.chatRight .text {color: #434343;margin-top: 8px;background-color: #95ec69;display: inline-block;padding: 6px 10px;border-radius: 10px;max-width: 50%;/* 忽略多余的空白,只保留一个空白 */white-space: normal;/* 换行显示全部字符 */word-break: break-all;white-space: pre-wrap;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}.searchBox {padding: 4px 2px;border-bottom: 1px solid #ededed;
}
.input-with-select {width: 80%;margin-right: 2%;
}
/* 点击用户变色 */
.userFlexAct {display: flex;justify-content: space-between;align-items: center;height: 70px;border-bottom: 1px solid #e8e8e8;cursor: pointer;background-color: #e8e8e8;
}
/* 用户默认颜色 */
.userFlex {display: flex;justify-content: space-between;align-items: center;height: 70px;border-bottom: 1px solid #e8e8e8;cursor: pointer;
}
/* 用户名 */
.userName {width: 100px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;
}
/* 简略信息 */
.userInfo {width: 100px;font-size: 14px;color: #ccc;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;margin-top: 3px;
}/* 滚动条样式 */
::-webkit-scrollbar {width: 5px;height: 10px;
}
::-webkit-scrollbar-thumb {background-color: #dbd9d9;border-radius: 3px;
}
</style>

websocket模板(2022.10.14更新)

这里考虑到聊天功能都需要用到websocket链接,所以这里更新一下,加入一个websocket模板。方便大家复制后可以直接使用,这里包含了心跳重连机制,放心食用。可以直接复制去试一下。目前没有和页面搭配起来,因为我没有写后台,但是模板给到大家,相信大家看注释能知道如何搭配聊天使用。

websocket效果图

心跳重连机制,如果链接不上会一直重连,直到链接成功为止

心跳ping,后台返回ok

包含websocket的聊天室代码,聊天功能还是上面的代码,新增加了链接功能

<template><div style="height: 100%; width: 100%; background-color: #ededed"><div class="wrap"><!-- 头部 --><div class="titleBox"><imgsrc="https://img-qn-2.51miz.com/preview/element/00/01/27/97/E-1279706-3813EBF9.jpg!/quality/90/unsharp/true/compress/true/format/jpg/fh/320"alt=""class="head_portrait"style="margin-left: 20px; margin-right: 20px"/><span style="color: #fff">官方客服</span><!-- 在线状态弹框 --><el-popover placement="bottom" trigger="click"><div class="stateBox2" @click="uploadState(1)"><span class="state1"></span><span class="stateText">在线</span></div><div class="stateBox2" @click="uploadState(2)"><span class="state2"></span><span class="stateText">离线</span></div><div class="stateBox2" @click="uploadState(3)"><span class="state3"></span><span class="stateText">忙碌</span></div><div class="stateBox2" @click="uploadState(4)"><span class="state4"></span><span class="stateText">退出</span></div><div class="stateBox" slot="reference" v-if="state == 1"><span class="state1"></span><span class="stateText">在线</span></div><div class="stateBox" slot="reference" v-if="state == 2"><span class="state2"></span><span class="stateText">离线</span></div><div class="stateBox" slot="reference" v-if="state == 3"><span class="state3"></span><span class="stateText">忙碌</span></div></el-popover></div><!-- 底部 --><div class="infoBox"><!-- 左边用户列表 --><div class="userList"><div class="searchBox"><el-inputplaceholder="请输入内容"v-model="search"class="input-with-select"size="mini"@input="inquire"><iclass="el-icon-search el-input__icon"slot="suffix"@click="handleIconClick"></i></el-input><el-buttonicon="el-icon-plus"size="mini"type="primary"@click="dialogVisible = true"></el-button></div><div class="userListBox"><divv-for="(item, index) in userListData":key="index"@click="getAct(item, index)":class="item.id == act ? 'userFlexAct' : 'userFlex'"><div><img:src="item.url"alt="头像"class="head_portrait2"style="margin-left: 20px"/></div><div style="margin-right: 40px"><el-tooltip:content="item.username"placement="bottom"effect="light"><div style="color: #565656" class="userName">{{ item.username }}</div></el-tooltip><div class="userInfo">{{ item.info }}</div></div><div style="margin-right: 10px; font-size: 14px; color: #ccc">{{ item.timer }}</div></div></div></div><!-- 右边输入框和信息展示 --><div class="infoList"><!-- 信息 --><div class="infoTop" ref="scrollBox" id="box"><div:class="item.position == 'left' ? 'chatInfoLeft' : 'chatInfoRight'"v-for="(item, index) in userInfoList":key="index"><img :src="item.url" alt="头像" class="head_portrait2" /><div :class="item.position == 'left' ? 'chatLeft' : 'chatRight'"><div class="text" v-html="item.info"></div></div></div></div><!-- 输入框 --><div class="infoBottom"><div class="infoIcon"><i@click="extend('照片上传')"class="el-icon-picture-outline-round"></i><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><textareatype="textarea"class="infoInput"v-model="textarea"@keydown.enter.exact="handlePushKeyword($event)"@keyup.ctrl.enter="lineFeed":disabled="isshow == 1 ? false : true"/><divclass="fasong"@click="setUp"v-show="isshow == 1 ? true : false">发送</div></div></div></div></div><!-- 搜索框边 + 号弹框 --><el-dialogtitle="选择需要添加的联系人":visible.sync="dialogVisible"width="30%":modal="false"><span>自定义页面,还没想好写什么功能</span><span slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="dialogVisible = false">确 定</el-button></span></el-dialog></div>
</template><script>
export default {data() {return {//websocket部分path: "ws://localhost:8888", //后台的websocket地址,找后端要ws: null, //建立的连接lockReconnect: false, //是否真正建立连接timeout: 10 * 1000, //30秒一次心跳timeoutObj: null, //心跳心跳倒计时serverTimeoutObj: null, //心跳倒计时timeoutnum: null, //断开 重连倒计时// 在线状态state: 1,//搜索用户search: "",//用户列表渲染数据userListData: [{id: 0,url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",},{id: 1,url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",},{id: 2,url: "https://img1.baidu.com/it/u=2029513305,2137933177&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=472",username: "王子变蛤蟆",info: "你好,在吗",timer: "2022/8/9",},{id: 3,url: "https://img1.baidu.com/it/u=1960292808,1761809160&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "皇太子",info: "给我看一下这个情况呢",timer: "2022/8/2",},{id: 4,url: "https://img2.baidu.com/it/u=3684117954,695988885&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400",username: "飞天小女警",info: "模拟数据发送一下呢",timer: "2022/8/4",},{id: 5,url: "https://img2.baidu.com/it/u=4122738859,2522601053&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "罗大大大",info: "在吗",timer: "2022/8/5",},{id: 6,url: "https://img0.baidu.com/it/u=661161858,172661768&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "王子花",info: "好的,我知道了",timer: "2022/8/9",},{id: 7,url: "https://img2.baidu.com/it/u=835899845,548435859&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "开盖有奖",info: "试试看吧,再发一次",timer: "2022/8/9",},{id: 8,url: "https://img0.baidu.com/it/u=4065107391,2142799144&fm=253&fmt=auto&app=138&f=JPEG?w=527&h=500",username: "日本大爆炸",info: "在吗",timer: "2022/8/5",},{id: 9,url: "https://img2.baidu.com/it/u=2860188096,638334621&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "秋天的第一杯奶茶",info: "好的,我知道了",timer: "2022/8/9",},{id: 10,url: "https://img0.baidu.com/it/u=1694074520,2517635995&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "潮汐",info: "试试看吧,再发一次",timer: "2022/8/9",},],//用户列表筛选数据userListDatas: [{id: 0,url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",},{id: 1,url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",},{id: 2,url: "https://img1.baidu.com/it/u=2029513305,2137933177&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=472",username: "王子变蛤蟆",info: "你好,在吗",timer: "2022/8/9",},{id: 3,url: "https://img1.baidu.com/it/u=1960292808,1761809160&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "皇太子",info: "给我看一下这个情况呢",timer: "2022/8/2",},{id: 4,url: "https://img2.baidu.com/it/u=3684117954,695988885&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400",username: "飞天小女警",info: "模拟数据发送一下呢",timer: "2022/8/4",},{id: 5,url: "https://img2.baidu.com/it/u=4122738859,2522601053&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "罗大大大",info: "在吗",timer: "2022/8/5",},{id: 6,url: "https://img0.baidu.com/it/u=661161858,172661768&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "王子花",info: "好的,我知道了",timer: "2022/8/9",},{id: 7,url: "https://img2.baidu.com/it/u=835899845,548435859&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "开盖有奖",info: "试试看吧,再发一次",timer: "2022/8/9",},{id: 8,url: "https://img0.baidu.com/it/u=4065107391,2142799144&fm=253&fmt=auto&app=138&f=JPEG?w=527&h=500",username: "日本大爆炸",info: "在吗",timer: "2022/8/5",},{id: 9,url: "https://img2.baidu.com/it/u=2860188096,638334621&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "秋天的第一杯奶茶",info: "好的,我知道了",timer: "2022/8/9",},{id: 10,url: "https://img0.baidu.com/it/u=1694074520,2517635995&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "潮汐",info: "试试看吧,再发一次",timer: "2022/8/9",},],//用户点击选中变色act: Number,// 加号弹框dialogVisible: false,//模拟花间一壶酒用户的历史信息userInfoList2: [{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",position: "left",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",position: "right",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗",timer: "2022/8/1",position: "right",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗在吗在吗在吗在吗在吗在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗",timer: "2022/8/1",position: "right",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗在吗",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "在吗在吗在吗在吗在吗在吗",timer: "2022/8/1",position: "right",},],//模拟超人不换内裤用户的历史信息userInfoList3: [{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "测试测试",timer: "2022/8/9",position: "left",},{url: "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",username: "花间一壶酒",info: "测试",timer: "2022/8/9",position: "left",},{url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: "测试测试测试测试",timer: "2022/8/1",position: "right",},],//历史信息userInfoList: [],//输入框textarea: "",//滚动条距离顶部距离scrollTop: 0,//发送和输入显隐isshow: 0,};},created() {this.initWebpack();},beforeDestroy() {// 离开页面后关闭连接this.ws.close();// 清除时间clearTimeout(this.timeoutObj);clearTimeout(this.serverTimeoutObj);},methods: {//切换客服状态uploadState(state) {if (state !== 4) {this.state = state;} else {this.$confirm("是否退出登录?", "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {this.$message({type: "success",message: "退出成功!",});}).catch(() => {this.$message({type: "info",message: "已取消退出",});});}},//搜索iconhandleIconClick() {console.log(1);},//点击用户getAct(val, index) {this.isshow = 1;// 点击用户切换数据时先清除监听滚动事件,防止出现没有历史数据的用户,滚动条为0,会触发滚动事件this.$refs.scrollBox.removeEventListener("scroll", this.srTop);//点击变色this.act = val.id;//清空消息数组this.userInfoList = [];// 模拟一下点击用户出现历史记录的样子,实际开发中是axios请求后数组赋值然后调用setPageScrollToif (val.username == "花间一壶酒") {this.userInfoList = this.userInfoList2;// 直接调用不生效:因为你历史数据刚给,渲染的时候盒子高度还没有成型,所以直接调用拿不到,用个定时器让他在下一轮循环中调用,盒子就已经生成了this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})} else if (val.username == "超人不换内裤") {this.userInfoList = this.userInfoList3;// 直接调用不生效:因为你历史数据刚给,渲染的时候盒子高度还没有成型,所以直接调用拿不到,用个定时器让他在下一轮循环中调用,盒子就已经生成了this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})} else if (val.username == "王子变蛤蟆") {this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})}},// 模糊搜索用户inquire() {let fuzzy = this.search;if (fuzzy) {this.userListData = this.userListDatas.filter((item) => {return item.username.includes(fuzzy);});} else {this.userListData = this.userListDatas;}},//发送setUp() {console.log("发送内容:", this.textarea);this.userInfoList2.push({url: "https://img2.baidu.com/it/u=2859542338,3761174075&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500",username: "超人不换内裤",info: this.textarea,timer: "2022/8/1",position: "right",});this.textarea = "";// 页面滚动到底部this.$nextTick(() => { // 一定要用nextTickthis.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";},//点击iconextend(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("ping");} else {//如果检测readyState不等于1那也就代表不处在链接状态,那就是不正常的,那就调用重连方法self.reconnect();}//从新赋值一个超时计时器,这个定时器的作用:当你触发心跳的时候可能会出现一个情况,后台崩了,前台发了个心跳,没有回应,就不会触发onmessage方法//所以我们需要在这个心跳发送出去了后,再开一个定时器,用于监控心跳返回的时间,比如10秒,那么10秒内如果后台回我了,触发onmessage方法,自然就会把心跳时间和超时倒计时一起清空掉//也就不会触发这个关闭连接,但是如果10秒后还是没有收到回应,那么就会触发关闭连接,而关闭连接方法内又会触发重连方法,循环就走起来了。self.serverTimeoutObj = setTimeout(function () {//如果超时了就关闭连接self.ws.close();}, self.timeout);}, self.timeout);},//连接成功onopen() {console.log("连接成功");if (this.ws.readyState == 1) {//如果连接正常,给后天发送一个值,可以自定义,然后后台返回我们一个信息,我们接收到后会触发onmessage方法回调this.ws.send("链接上啦!!!");}this.start(); //链接成功后开启心跳},//接受后台信息回调onmessage(e) {/**这里写自己的业务逻辑代码**/console.log("收到后台信息:", e.data);this.reset(); //收到服务器信息,心跳重置},//关闭连接回调onclose(e) {console.log("连接关闭");this.reconnect(); //重连},//连接异常回调onerror(e) {console.log("出现错误");this.reconnect(); //重连},},
};
</script><style scoped>
.wrap {height: 80%;width: 55%;background-color: #f2f2f2;margin: auto;transform: translateY(10%);box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);border-radius: 10px;
}
.titleBox {height: 10%;width: 100%;background-image: linear-gradient(to right, #1e76bc, #69a3d5);display: flex;align-items: center;border-top-right-radius: 10px;border-top-left-radius: 10px;
}
.infoBottom {height: 30%;display: flex;flex-direction: column;
}
/* 输入框 */
.infoInput {height: 58%;width: 100%;border: none;resize: none;padding: 10px;box-sizing: border-box;background-color: #f2f2f2;color: #434343;
}
.fasong {height: 30px;width: 80px;background-color: #e8e8e8;text-align: center;line-height: 30px;border-radius: 4px;color: #58df4d;margin-top: 1%;align-self: flex-end;margin-right: 20px;cursor: pointer;
}
.infoIcon {height: 40px;width: 100%;display: flex;align-items: center;
}
.infoIcon i {font-size: 24px;color: #676767;margin-left: 15px;cursor: pointer;
}
/* 头像 */
.head_portrait {width: 3rem;height: 3rem;border-radius: 50%;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}
.head_portrait2 {width: 3rem;height: 3rem;border-radius: 50%;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}
.stateBox {margin-left: 20px;padding: 1px 8px;background-color: #fff;border-radius: 10px;text-align: center;cursor: pointer;
}
.stateBox2 {margin-left: 20px;padding: 1px 8px;background-color: #fff;border-radius: 10px;text-align: center;cursor: pointer;
}
.stateBox2:hover {background-color: #dcdcdc;
}
/* 在线 */
.state1 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #8ee80e;
}
/* 离线 */
.state2 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #cacaca;
}
/* 忙碌 */
.state3 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #ff8c1e;
}
/* 退出登录 */
.state4 {display: inline-block;height: 10px;width: 10px;border-radius: 50%;background-color: #7e7e7e;
}
.stateText {font-size: 14px;margin-left: 5px;
}
/* 列表和信息 */
.infoBox {height: 90%;width: 100%;display: flex;
}
/* 用户列表大盒子 */
.userList {height: 100%;width: 300px;border-right: 1px solid #ccc;display: flex;flex-direction: column;
}
/* 用户列表 */
.userListBox {flex: 1;width: 100%;overflow: auto;
}
/* 信息外层盒子 */
.infoList {height: 100%;width: 72%;
}
/* 信息列表 */
.infoTop {height: 70%;width: 100%;border-bottom: 1px solid #ccc;padding: 10px;box-sizing: border-box;overflow: auto;
}
/* 对方发的信息样式 */
.chatInfoLeft {min-height: 70px;margin-left: 10px;margin-top: 10px;display: flex;
}
.chatLeft {margin-left: 15px;flex: 1;
}
.chatLeft .text {color: #434343;margin-top: 8px;background-color: #e3e3e3;display: inline-block;padding: 6px 10px;border-radius: 10px;max-width: 50%;/* 忽略多余的空白,只保留一个空白 */white-space: normal;/* 换行显示全部字符 */word-break: break-all;white-space: pre-wrap;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}/* 自己发的信息样式 */
.chatInfoRight {height: 70px;margin-left: 10px;margin-top: 10px;display: flex;flex-direction: row-reverse;
}
.chatRight {margin-right: 15px;flex: 1;/* 用align-items把元素靠右对齐 */display: flex;flex-direction: column;align-items: flex-end;
}
.chatRight .text {color: #434343;margin-top: 8px;background-color: #95ec69;display: inline-block;padding: 6px 10px;border-radius: 10px;max-width: 50%;/* 忽略多余的空白,只保留一个空白 */white-space: normal;/* 换行显示全部字符 */word-break: break-all;white-space: pre-wrap;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
}.searchBox {padding: 4px 2px;border-bottom: 1px solid #ededed;
}
.input-with-select {width: 80%;margin-right: 2%;
}
/* 点击用户变色 */
.userFlexAct {display: flex;justify-content: space-between;align-items: center;height: 70px;border-bottom: 1px solid #e8e8e8;cursor: pointer;background-color: #e8e8e8;
}
/* 用户默认颜色 */
.userFlex {display: flex;justify-content: space-between;align-items: center;height: 70px;border-bottom: 1px solid #e8e8e8;cursor: pointer;
}
/* 用户名 */
.userName {width: 100px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;
}
/* 简略信息 */
.userInfo {width: 100px;font-size: 14px;color: #ccc;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;margin-top: 3px;
}/* 滚动条样式 */
::-webkit-scrollbar {width: 5px;height: 10px;
}
::-webkit-scrollbar-thumb {background-color: #dbd9d9;border-radius: 3px;
}
</style>

客服列表更新和后台发消息区分(2022.10.29更新)

最近完整的写了这个功能,又发现一个问题,就是后台发送的消息我存到信息列表内会出现,我在和A用户聊天时,B用户发消息,会显示在A的页面上,因为我是直接循环的信息列表。这不对,没有区分用户。

解决方法这里贴出来,大家写到这个功能的时候可以参考,我通过后台信息的id和当前选中用户的id对比,如果一致我才把信息加进去渲染,如果不一致就不加了,因为点击用户的时候会调用历史记录,所以不需要直接渲染
就是在websocket收到信息的方法内写

            onmessage(e) {let type = JSON.parse(e.data)if (type.type == 'reply') {//用户信息//收到信息判断信息的id和当前选中的用户是否一致,一致才把信息添加进去渲染  if (type.data.uid == this.user_Info.user_id) {this.userInfoList.push(type.data)console.log('后台信息',type.data);}//查询发来的信息id在客服的用户列表内是否有这个人,如果没有,从新拉取一下用户列表。-1代表找不到,找得到的会返回角标let uidIndex=this.findElem(this.userListData,'user_id',type.data.uid)if(uidIndex==-1){this.getUserList()//获取用户列表}this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})} else if (type.type == 'online') {//用户上下线状态this.online_user = 'online'this.online_type = type.data.onlinethis.online_uid = type.data.uid}this.reset(); //收到服务器信息,心跳重置},findElem(array, attr, val) {for (var i = 0; i < array.length; i++) {if (array[i][attr] == val) {return i; //返回当前索引值}}return -1;}

离开页面时断开链接自动重连解决办法

还有一个情况是这里通过离开前断开websocket链接,但是会有一种情况就是你链接退出去后并没有断开,因为我们这里断开连接的close方法内也做了心跳重连这就导致,我们退出页面时关闭了链接,但是同时又触发了重连方法,导致在外面的页面还在一直重复链接报错。
解决办法:
我们新增一个变量,比如叫A,默认为0。在进入页面时,created内把A设置为0,代表可以重连。在离开页面前先把A设置为1。代表不能重连然后我们在close方法内用if判断一下,如果A等于0就可以重连,A等于1就不重连,这样就可以完美解决页面内无限重连,页面外断开链接了。

消息发送失败重新发送功能逻辑

这个功能是常见功能,就是当你发送消息时如果发送不成功就会消息旁边出现一个转圈的图标,然后转圈一会如果还是发不出去就会显示一个红色感叹号,点击感叹号重新发送消息。
逻辑 简单描述,把html这个代码放到你想放的位置。里面两个i标签是图标,是elementul里面的图标,上面一个是转圈的加载图标,下面是红色的感叹号。
首先,外面大盒子通过你当前循环的这个信息中的uid判断和自己这个账号的uid是否一致,如果一致代表是我发的信息,如果不一致,代表是对方发的信息,对方发的信息就没必要去显示这个对吧。只有我发的信息失败了才会从发
然后我们判断chatShow是否等于2,2代表显示这个图标。默认就是显示的。因为前面有判断uid是我们的才会显示,两个条件满足才行。
这时候只有我们自己发送的消息才会显示盒子了。继续开始内部图标的判断,根据jzshow变量来判断,0代表不显示,1代表加载,2代表红色感叹号。
我们的逻辑是,发送消息的时候,我们先把jzshow改为1代表加载。同时我们开一个定时器三秒。如果三秒内没有关闭定时器,就会把jzshow改为2代表发送失败。红色感叹号
我们定时器从哪里关闭呢,就从websocket返回的值里来关闭。当你发送了消息后,后端会返回给你一个成功的信息,我们判断如果接受到了这个信息就把jzshow改为0,同时把定时器给清除掉。就完成了一个信息的发送
如果我们没有接收到后端发来的成功消息,或者发来的消息时异常错误等信息,我们就不会关闭定时器,那么三秒后就会触发jzshow变成2,就是红色感叹号,这时候可以点击感叹号触发resend重新发送方法。
这个方法内逻辑,先把你点击的那一条数据存到变量内,然后和发送的方法基本一样,从新发送一次websocket链接。如果链接返回成功消息就结束,如果没有返回就继续重新发送。
html

                     <div style="display: inline-block;"v-show="item.uid == wechatInfo.tag_uid&&chatShow==item.chatShow"><i class="el-icon-loading" v-show="jzshow==1"></i><i class="el-icon-warning" style="color:red" v-show="jzshow==2"@click="resend(item.msn)"></i></div>

data

             jzshow: 0, //加载图标iconjzshowtimer: null, //加载图标定时器chatShow: 2, //加载图标显示

methods

            //发送sendOut() {this.chatList.push({msn: this.inputValue,uid: this.wechatInfo.tag_uid,avatar: this.wechatInfo.headimgurl,nickname: this.wechatInfo.nickname,chatShow: 2})this.jzshow = 1let parms = {msn: this.inputValue,to_uid: this.kfInfo,type: 1,form_type: 'wechat',is_tourist: 0}//通过websocket发送信息到后台this.ws.send(JSON.stringify({type: "chat",data: parms}))this.jzshowtimer = setTimeout((res) => {this.jzshow = 2}, 3000);this.inputValue = '' //点击发送后清空输入框console.log('发送成功', this.inputValue);// 页面滚动到底部this.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})},// 重新发送resend(msn) {this.inputValue = msnthis.jzshow = 1let parms = {msn: this.inputValue,to_uid: this.kfInfo[0].kf_id,type: 1,form_type: 'wechat',is_tourist: 0}//通过websocket发送信息到后台this.ws.send(JSON.stringify({type: "chat",data: parms}))this.jzshowtimer = setTimeout((res) => {this.jzshow = 2}, 3000);this.inputValue = '' //点击发送后清空输入框},

websocket返回信息

            //接受后台信息回调onmessage(res) {let type = JSON.parse(res.data)//后台返回消息,通过type字段判断是不是别人发送给我的消息if (type.type == 'chat') {clearTimeout(this.jzshowtimer);this.chatShow = 1//this.chatList.push(type.data) //把消息添加到信息列表渲染this.jzshow = 0 //隐藏加载iconthis.$nextTick(() => { // 一定要用nextTickthis.setPageScrollTo();//页面滚动条距离顶部高度等于这个盒子的高度this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;})console.log("收到后台信息:", JSON.parse(res.data));} else if (type.type == 'reply') {this.chatList.push(type.data)}this.reset(); //收到服务器信息,心跳重置},

【PC端聊天功能模板】vue-elementul简单实现电脑端客服聊天功能,pc端聊天系统静态页面布局,配套websocket方案和心跳重连机制【详细注释,拿来即用】相关推荐

  1. 【小程序websocket前后端交互】uniapp写微信小程序聊天功能功能,websocket交互功能,心跳重连【详细注释,复制即用】

    前言 这几天在做的一个需求,就是要写一个小程序端的页面,用于跟客服聊天. 然后就用到了websocket技术,以前我做过网页版的,但是做小程序后发现网页版的逻辑放过来没问题,但是很多的方法和api是不 ...

  2. java在线网页客服聊天_管理员消息java 网站用户在线和客服聊天

    首先声明,我是一个菜鸟.一下文章中出现技术误导情况盖不负责 这是应用到项目中的一个例子. 实现原理是将信息存储到Application域里面.然后应用Struts2 Action 用json格式的数据 ...

  3. express-ws实现客服聊天功能

    express-ws实现webSocket通信 前言 最近要给自己写的项目中加一个客服聊天的功能.就是前端商城和后台管理系统通信.但是是两个不同的端口. 百度了一下就找到了webSocket. 什么是 ...

  4. ASP.NET SignalR 与LayIM配合,轻松实现网站客服聊天室(四) 添加表情、群聊功能...

    休息了两天,还是决定把这个尾巴给收了.本篇是最后一篇,也算是草草收尾吧.今天要加上表情功能和群聊.基本上就差不多了,其他功能,读者可以自行扩展或者优化.至于我写的代码方面,自己也没去重构.好的,我们开 ...

  5. php实现网站客服聊天/在线沟通功能

    php实现网站客服聊天/在线沟通功能 这是后台页面 这是访客页面 前后端数据交互格式 JSON 前端框架 VUE Element UI JavaScript 后端技术 mysql 5.6 php 5. ...

  6. uni-app 微信小程序客服聊天和发送页面卡片功能

    微信小程序客服聊天和发送页面卡片功能实现步骤如下: (一):首先登录微信公众平台 :https://mp.weixin.qq.com/ 登录成功后找到----->功能-----> 客服-- ...

  7. 融云api开发Java后台客服聊天功能(一)

    以下所有内容均为本人自己开发总结的经验,如有雷同,不胜荣幸! 最近公司在做一款app,app里面涉及到与客服聊天功能. 于是公司就接入了融云api聊天系统. app端(前端)聊天由专业的ios and ...

  8. java微信多客服_微信多客服聊天功能怎么实现?

    原标题:微信多客服聊天功能怎么实现? 朋友小赵是很早的一批代购,微信号上添加了不少顾客.随着业务不断扩展,口碑越来越好,客户数量越来越多.经常有顾客在微信上咨询,日子久了,小赵就有点吃不消了.于是小赵 ...

  9. 转载 ASP.NET SignalR 与LayIM配合,轻松实现网站客服聊天室(三) 激动人心的时刻到啦,实现1v1聊天...

    ASP.NET SignalR 与LayIM配合,轻松实现网站客服聊天室(三) 激动人心的时刻到啦,实现1v1聊天 看起来挺简单,细节还是很多的,好,接上一篇,我们已经成功连接singalR服务器了, ...

  10. 基于webScoket的在线客服聊天

    什么是webScoket WebSocket协议是基于TCP的一种新的网络协议.它实现了浏览器与服务器全双工(full-duplex)通信--允许服务器主动发送信息给客户端. 简单的说,WebSock ...

最新文章

  1. 第四范式送上2022虎年祝福
  2. 理解TCP为什么需要进行三次握手(白话)(转载)
  3. 关于TCP粘包的拙见
  4. 通过Repository Manager 1.3来管理戴尔驱动程序更新
  5. iPhone新机或全部采用OLED屏:日本JDI股价应声下跌
  6. builder forms oracle 函数如何使用_Oracle EBS 之 Forms开发 一
  7. hadoop学习视频
  8. GB2312转换为Unicode编码表
  9. M6(面试)-01-牛客网Java面试题集锦
  10. 点餐系统+小程序常见问题解决(2022年最新版)
  11. labview中DAQ采集多个数据通道(温度传感器)(TTL信号的曲轴位置传感器)总结
  12. mdk是什么意思_MDK是什么意思
  13. 收藏的书录,值得花时间去读的书
  14. 计算机软件工程考研考哪些专业,2022考研:软件工程考研考什么科目?
  15. 服务器看门狗芯片电路图,看门狗电路简介(低成本)
  16. BIM模型文件下载——施工场地部署模型
  17. 计算机系统之设备管理
  18. 2小时破解WIFI密码!WPS漏洞目前尚无解决方案
  19. AutoSAR系列讲解(入门篇)3.5-RTE对数据一致性的管理
  20. [收藏] 最受欢迎的ASP.NET的CMS下载

热门文章

  1. 数据库mongodb效率测试
  2. 以太网未识别的网络win10_Win10以太网未识别的网络怎么办
  3. WorkPlus协同办公系统的优势有哪些?
  4. 纵向时间线html,51个css时间轴
  5. 关于“wuauclt.exe”病毒的清理
  6. 致新浪科技频道的一封公开信
  7. 以《简单易懂》的语言带你搞懂无监督学习算法【附Python代码详解】机器学习系列之K-Means篇
  8. eNSP路由器启动不了
  9. 采样频率和带宽的关系_磁共振成像带宽
  10. 博客园申请开通博客时理由应该怎么写才能通过?