前言:公司这几天要实现一个实时通讯的功能,不同账号登陆进行聊天,我研究了2天,还有一点问题,但差不多了,不多说了,看一下怎么实现吧☺
1.第一步,咱们先得弄出来了样式,了解一下大概的流程是什么样的,我之前在网上看到一个大神写的,我找不到了,没法艾特了,先说声抱歉😊,如果有幸被你看到,私信我,我艾特出来
2.第二步,咱们先了解一下流程,我这只有前端代码哈,流程大概就是,你用这个账号发了消息,给后端,后端根据你当前登录账号的id和你发个对方的id进行处理,然后用webSocket转给另一个账号,前端在webSocket接收消息的接口中,把消息拿出来处理完,显示在窗口上,
下面是发送给后端的数据格式:(调取webSocket发送后,你还得在前端push进去一遍,要不然得点击账号才会出你发的数据,看看最下面的代码)
这是接收的格式:用position的左右来判断接收方和发送方
3.第三步,有一个消息提示,在左边菜单栏,有新消息进来,会有红点提示,直接让后端返回两个字段,一个有最新消息出来,一个是position,一起判断
4.我说的不好,你可以把下面代码直接拿去,研究研究就懂了,哈哈哈哈,给你们看效果:
:1.html部分
<el-dialog
v-dialogDrag
custom-class="no-header-dialog"
:visible.sync="dialogVisibleto"
:show-close="false"
width="70%"
>
<div style="height: 600px; width: 100%; background-color: #ededed">
<div class="wrap">
<!-- 头部 -->
<div class="titleBox">
<img
:src="imglogin"
alt=""
class="head_portrait"
style="margin-left: 20px; margin-right: 20px"
/>
<span style="color: #fff">{{ username }}</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 style="margin-left: 81%;">
<img
src="../assets/叉号 .png"
style="width: 15px;"
@click="showtow()"
alt=""
>
</div>
</div>
<!-- 底部 -->
<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>
<el-button
icon="el-icon-plus"
size="mini"
type="primary"
@click="dialogVisible = true"
></el-button>
</div>
<div class="userListBox">
<div
v-for="(item, index) in userListData"
:key="index"
@click="getAct(item, index)"
:class="item.userId == act ? 'userFlexAct' : 'userFlex'"
style="position: relative;"
>
<div>
<div
class="redborder"
v-show="item.status===0&&item.position==='right'"
></div>
<img
:src="item.url"
alt="头像"
class="head_portrait2"
style="margin-left: 20px"
/>
</div>
<div style="margin-right: 40px">
<el-tooltip
:content="item.nick"
placement="bottom"
effect="light"
>
<div
style="color: #565656"
class="userName"
>
{{ item.nick }}
</div>
</el-tooltip>
<div class="userInfo">{{ item.msg }}</div>
</div>
<div style="margin-right: 10px; font-size: 14px; color: #ccc">
{{ item.updateTime }}
</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.msg"
></div>
</div>
<div style="color: #ccc;font-size: 12px;line-height: 50px;">{{item.updateTime1}}</div>
</div>
</div>
<!-- 输入框 -->
<div class="infoBottom">
<div class="infoIcon">
<el-upload
class="upload-demo"
action="/api/user-msg/imageUpload2"
:on-remove="handleRemove"
:on-success="handsuccess"
:show-file-list="false"
:file-list="fileList"
list-type="picture">
<i
@click="extend('照片上传')"
class="el-icon-picture-outline-round"
></i>
<!-- <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> -->
</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>
<!-- 搜索框边 + 号弹框 -->
<el-dialog
title="选择需要添加的联系人"
: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>
</el-dialog>
2.css部分:
.wrap {
height: 100%;
width: 100%;
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;
}
/* 未读的红色提示 */
.redborder {
width: 10px;
height: 10px;
background: red;
border-radius: 10px;
position: absolute;
left: 58px;
}
/* 图片上传 */
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
3.script部分:
return里面:
dialogVisibleto: false, //即时通信
// 在线状态
state: 1,
//搜索用户
search: "",
//用户列表渲染数据
userListData: [],
//用户列表筛选数据
userListDatas: [],
//用户点击选中变色
act: Number,
// 加号弹框
dialogVisible: false,
//历史信息
userInfoList: [],
//输入框
textarea: "",
//滚动条距离顶部距离
scrollTop: 0,
//发送和输入显隐
isshow: 0,
//点击用户的id
userId: null,
//WebSocket
wsConn: {},
//wx连接
ws: {},
limitConnect: 0,
wsClose: 0, //0是没手动关闭,1是手动关闭
wsData: {},
事件:
created() {
this.init();
},
methods: {
//即时通信按钮
timely() {
this.getlist();
this.dialogVisibleto = true;
//清空变色
this.act = Number;
//清空消息数组
this.userInfoList = [];
},
//退出即时通信
showtow() {
this.dialogVisibleto = false;
},
//即时通讯
init() {
let userid = localStorage.getItem("userInfo");
//ws启用初始化
let vm = this;
vm.wsClose = 0;
try {
vm.wsConn = new WebSocket("ws://192.168.1.103:9092/notice/" + userid);
} catch (error) {
reconnect();
}
this.ws = vm.wsConn;
// 获取连接状态
// console.log('ws连接状态:' + ws.readyState);
//监听是否连接成功
vm.wsConn.onopen = function () {
/* console.log('ws连接状态:' + this.ws.readyState); */
// console.log("ws连接状态:");
/* this.limitConnect = 0;
vm.data=JSON.stringify(vm.data) */
// console.log("44",vm.data)
//连接成功则发送一个数据
// wsConn.send(vm.data);
};
// 接听服务器发回的信息并处理展示
vm.wsConn.onmessage = function (data) {
vm.wsData = JSON.parse(data.data);
vm.wxDataService(vm.wsData);
};
// 监听连接关闭事件
vm.wsConn.onclose = function () {
console.log("连接关闭");
if (vm.wsClose == 0) {
console.log("重新连接");
vm.reconnect();
}
};
// 监听并处理error事件
vm.wsConn.onerror = function (error) {
console.log(error);
};
},
reconnect() {
//ws重新连接
/* this.limitConnect++;
console.log("重连第" + this.limitConnect + "次"); */
this.init();
},
wxDataService(data) {
if (data.msgType == "即时通讯") {
if (this.userId === data.reciveId) {
this.userInfoList.push({
id: null,
msg: data.msg,
nick: null,
position: data.position,
reciveId: data.reciveId,
updateTime: data.updateTime,
url: data.url,
userId: data.userId,
});
setTimeout((res) => {
this.getlist();
}, 1000);
this.$nextTick(() => {
// 一定要用nextTick
this.setPageScrollTo();
//页面滚动条距离顶部高度等于这个盒子的高度
this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;
});
} else {
setTimeout((res) => {
this.getlist();
}, 1000);
}
}
},
//切换客服状态
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: "已取消退出",
});
});
}
},
//搜索icon
handleIconClick() {
console.log(1);
},
//点击用户
getAct(val, index) {
this.userId = val.userId;
this.isshow = 1;
// 点击用户切换数据时先清除监听滚动事件,防止出现没有历史数据的用户,滚动条为0,会触发滚动事件
this.$refs.scrollBox.removeEventListener("scroll", this.srTop);
//点击变色
this.act = val.userId;
//清空消息数组
this.userInfoList = [];
let data = {
userId: this.userId,
loginUserId: localStorage.getItem("userInfo"),
};
getMsg(data).then((res) => {
if (res.code == 20000) {
this.userInfoList = res.data;
this.$nextTick(() => {
// 一定要用nextTick
this.setPageScrollTo();
//页面滚动条距离顶部高度等于这个盒子的高度
this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;
});
}
});
//更改状态已读
let dataone = {
id: val.userId,
loginUserId: localStorage.getItem("userInfo"),
};
getsendup(dataone).then((res) => {
if (res.code == 20000) {
this.getlist();
}
});
},
// 模糊搜索用户
inquire() {
let fuzzy = this.search;
if (fuzzy) {
this.userListData = this.userListDatas.filter((item) => {
return item.nick.includes(fuzzy);
});
} else {
this.userListData = this.userListDatas;
}
},
//发送
setUp() {
if (this.textarea != "") {
let data = {
type: 1,
msg: {
data: this.textarea,
userId: this.userId,
loginUserId: localStorage.getItem("userInfo"),
},
};
data = JSON.stringify(data);
this.ws.send(data);
this.userInfoList.push({
id: null,
msg: this.textarea,
nick: null,
position: "right",
reciveId: localStorage.getItem("userInfo"),
updateTime: new Date(),
url: this.imglogin,
userId: this.userId,
});
this.textarea = "";
//更改状态已读
let dataone = {
id: this.userId,
loginUserId: localStorage.getItem("userInfo"),
};
getsendupone(dataone).then((res) => {});
this.getlist();
// 页面滚动到底部
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) {
},
//滚动条默认滚动到最底部
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("已经到顶部了"); */
}
},
upload(){
this.dialogVisibleoplod=true;
},
handleAvatarSuccess(res, file) {
console.log(res)
this.imglogin = URL.createObjectURL(file.raw);
console.log(this.imageUrl)
},
beforeAvatarUpload(file) {
const isJPG = file.type === "image/jpeg";
const isLt2M = file.size / 1024 / 1024 < 10;
if (!isJPG) {
this.$message.error("上传头像图片只能是 JPG 格式!");
}
if (!isLt2M) {
this.$message.error("上传头像图片大小不能超过 2MB!");
}
let formData = new FormData();
formData.append("file", file);
formData.append("id", this.keyid);
getsendupoade(formData).then(res=>{
if(res.code==20000){
this.dialogVisibleoplod=false
}
})
return isJPG && isLt2M;
},
//消息上传图片
handleRemove(file, fileList) {
this.$message({
type: "success",
message: "删除成功!",
});
},
handsuccess(response, file, fileList) {
if(this.userId==null){
this.$message({
type: "error",
message: "请选择发送的对象!",
});
}else{
let data = {
type: 1,
msg: {
data: file.raw,
userId: this.userId,
loginUserId: localStorage.getItem("userInfo"),
},
};
data = JSON.stringify(data);
this.ws.send(data);
console.log(response, file, fileList)
let formData = new FormData();
formData.append("file", file.raw);
formData.append("userId", this.userId);
formData.append("reciveId", this.keyid);
getsendupoadeone(formData).then(res=>{
})
}
},
},