除了第三篇中的弹窗消息外,本次计划在弹窗消息的基础上实现点对点的聊天窗口,即可以在线聊天,效果如图:
即在“即时消息” 页面,新增“聊天窗口”的按钮,点击相应的人员对应的此按钮后,即可打开一个临时的点对点聊天窗口,实现类似于微信或QQ的临时对话交流。
最终测试效果已经部署到本人个人的服务器,有兴趣的可以去测试和体验一下,体验地址:
怎知后台管理系统https://www.zenknow.cn/
- 体验账号1:test1 密码:123456 (公告发起者账号)
- 体验账号2:test2 密码:123456 (流程审核者账号)
5.1 功能策划
本次聊天窗口和弹窗功能的改造,需要满足以下功能:
- 发送消息时,如果接收消息的用户已经打开聊天窗口,则直接在窗口显示消息,不弹窗
- 发送消息时,如果接受消息的用户未打开聊天窗口,则以弹窗形式提醒
- 从聊天窗口发送消息时,发送者的消息一方面显示在本窗口右侧,同时发送到接收者
- 打开不同接收者的聊天窗口,只显示是对应的与该人员的聊天记录
- 记录以用户本地浏览器缓存的形式保存,服务器只转发,不存储
5.2 后端初步改造
5.2.1 消息对象改造
原弹窗发送消息只传递了fromUser(发送者)、msg(消息)两个信息,为了实现对话框功能还需要传递用户头像、用户id等字段,故对原弹窗消息后端的对象字段改造如下:
SysUserOnline和UserOnlineDTO中分别增加:
/**
* 用户ID
*/
private String userId;
/**
* 头像
*/
private String avatar;
5.2.2 后端消息处理改造
后端收到消息后,给users发正式消息时,携带上userId和avatar
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
log.info("发送文本消息");
// 获得客户端传来的消息
JSONObject json = JSONUtil.parseObj(message.getPayload());
if(json != null) {
List<String> users = (List<String>) json.get("users");
String msg = (String) json.get("msg");
String fromNick = (String) json.get("fromNick");
String fromAvatar = (String) json.get("fromAvatar");
String fromUserId = String.valueOf( json.get("fromUserId"));
if(fromNick == null){
fromNick ="后台系统";
}
if(users.isEmpty()){
//向发送者反馈发送信息
JSONObject sendResult = new JSONObject();
sendResult.set("fromUser", "系统发送反馈");
sendResult.set("msg", "消息发送失败,您未勾选接收人");
if(session != null) {
session.sendMessage(new TextMessage(sendResult.toString()));
}
} else {
for(String user : users) {
WebSocketSession userSession = WsSessionManager.get(user);
System.out.println(userSession);
if (userSession != null) {
//向消息者发送消息
JSONObject jsonObject = new JSONObject();
jsonObject.set("fromUser", fromNick);
jsonObject.set("msg", msg);
jsonObject.set("fromAvatar", fromAvatar);
jsonObject.set("fromUserId", fromUserId);
userSession.sendMessage(new TextMessage(jsonObject.toString()));
//向发送者反馈发送信息
JSONObject sendResult = new JSONObject();
sendResult.set("fromUser", "系统发送反馈");
sendResult.set("msg", "消息发送成功");
if(session != null) {
session.sendMessage(new TextMessage(sendResult.toString()));
}
} else {
//向发送者反馈发送信息
JSONObject sendResult = new JSONObject();
sendResult.set("fromUser", "系统发送反馈");
sendResult.set("msg", "消息发送失败,可能是对方已经下线,请刷新用户列表");
if(session != null) {
session.sendMessage(new TextMessage(sendResult.toString()));
}
}
}
}
}
}
5.3 前端改造
5.3.1 前端界面改造
本次聊天对话框使用JWchat插件,具体使用文档可以参考其官网文档:
https://codegi.gitee.io/jwchatdoc/component/chatIndex.html
这次是直接在对话框中使用JWchat-index插件,并将右侧和左侧隐藏,只保留聊天窗口。其中列表中首先增加一列按钮:
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="big"
type="text"
@click="openChatDialog(scope.row.userNick,scope.row.userId,scope.row.avatar,scope.row.deptName)"
>聊天窗口</el-button>
</template>
</el-table-column>
对话框及聊天窗口:
<el-dialog title="聊天窗口" :visible.sync="openChat" width="800px" append-to-body @close="closeChat">
<div style="display: flex; align-items: center; justify-content: center;">
<JwChat-index
:taleList="taleList"
:showRightBox="false"
:config="config"
width="750px"
scrollType="scroll"
@enter="bindEnter"
v-model="inputMsg"
:toolConfig="tool" />
</div>
</el-dialog>
5.3.2 相关配置数据
在data中配置JWchat-index
//配置对话框头部的头像和名字部门的
config:{
img: '',
name: '',
dept: '',
},
//聊天窗口中输入位置的信息
inputMsg:"",
//配置聊天窗口输入上方的工具栏的
tool:{
// file img video 现在只配置了三个图标
show: ['file', 'img'],
callback: this.toolEvent
},
//以下三个变量为了和导航栏的弹窗消息能够共享,需改造为vuex存储为全局变量,见下节
//用于存储当前打开的窗口的聊天对象的昵称
dialogActiveUserNick:"",
//用于存储当前打开的窗口的聊天对象的id
dialogActiveUserId:"",
//用于监听是否发来了新信息
dialogDetailChange:false,
//聊天记录
taleList:[
{
"date": "",
"text": { "text": "" },
"mine": false,
"name": "",
"img": "i"
}
],
//是否打开聊天窗口,由新增的列表中的按钮触发
openChat:false,
//要发送的整体信息,新增fromAvatar、fromUserId
usersMessage: {
fromNick:"",
fromAvatar:"",
fromUserId:"",
users:[],
msg:""
},
其中,dialogActiveUserNick等三个变量是用来监听对话框聊天对象信息的,为了与Navbar.vue中的弹窗消息能够通讯,需要设置为共享形式,此处仅为占位。将这三个信息设置为vuex:
在store的moudles中新建chat.js
const chat = {
state: {
chatDialogActiveUserNick:"",
chatDialogActiveUserId:"",
chatCurGetInfoChanged:false,
},
mutations: {
SET_CHAT_DIA_ACT_USER_NICK: (state, chatDialogActiveUserNick) => {
state.chatDialogActiveUserNick = chatDialogActiveUserNick
},
SET_CHAT_DIA_ACT_USER_ID: (state, chatDialogActiveUserId) => {
state.chatDialogActiveUserId = chatDialogActiveUserId
},
SET_CHAT_CUR_GETINFO_CHANGED: (state, chatCurGetInfoChanged) => {
state.chatCurGetInfoChanged = chatCurGetInfoChanged
},
},
actions: {},
}
export default chat
在getters中设置:
const getters = {
....
chatDialogActiveUserNick:state => state.chat.chatDialogActiveUserNick,
chatDialogActiveUserId:state => state.chat.chatDialogActiveUserId,
chatCurGetInfoChanged:state => state.chat.chatCurGetInfoChanged,
}
export default getters
store中的index中添加:
....
import chat from './modules/chat'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
...
chat
},
getters
})
export default store
5.3.3 针对界面对应的方法
created() {
this.getList();
//此处为当窗口整体关闭时,防止store中的共享变量状态未能及时清空所做的操作
window.addEventListener('beforeunload', e => this.closeChat());
window.addEventListener('unload', e => this.this.closeChat());
},
此处为当窗口整体关闭时,防止store中的共享变量状态未能及时清空所做的操作
destroyed() {
window.removeEventListener('beforeunload', e => this.this.closeChat());
window.removeEventListener('unload', e => this.this.closeChat());
},
computed: {
//引入store中共享的表明对话框聊天对象的信息的变量,以便于watch监听
...mapGetters([
'chatCurGetInfoChanged',
'chatDialogActiveUserNick',
]),
},
watch:{
//用于监听store中共享的,表明新增了消息的变量,此变量在true和false之间轮转变化
chatCurGetInfoChanged:{
handler(newVal,oldVal){
var curTaleList = localStorage.getItem(store.getters.chatDialogActiveUserNick) == null ? JSON.stringify([]) : localStorage.getItem(store.getters.chatDialogActiveUserNick) ;
this.taleList = JSON.parse(curTaleList);
},
immediate:true
},
},
methods: {
//外部群发弹窗的按钮动作,增加了
//1.发送者的userID
//2.发送者的头像avatar
send() {
if (store.getters.ws) {
this.usersMessage.fromNick = store.getters.nick;
this.usersMessage.fromUserId = store.getters.userId;
this.usersMessage.fromAvatar = store.getters.avatar;
this.usersMessage.users = this.toUsers;
this.usersMessage.msg = this.message;
console.log(JSON.stringify(this.usersMessage));
store.getters.ws.send(JSON.stringify(this.usersMessage));
} else {
alert("未连接到服务器");
}
},
//打开聊天窗口时的方法,主要执行了:
//1.设置头部的聊天对象的头像、名字、部门
//2.显示对话框
//3.设置STORE中的聊天对象共享变量为聊天对象昵称和id
//4.从本地缓存中读取此聊天对象对应的聊天记录
openChatDialog(userNick,userId,userAvatar,userDept){
this.config.img = userAvatar;
this.config.dept = userDept;
this.config.name = userNick;
this.openChat=true;
this.$store.commit('SET_CHAT_DIA_ACT_USER_NICK',userNick);
this.$store.commit('SET_CHAT_DIA_ACT_USER_ID',userId);
var curTaleList = localStorage.getItem(store.getters.chatDialogActiveUserNick) == null ? JSON.stringify([]) : localStorage.getItem(store.getters.chatDialogActiveUserNick) ;
this.taleList = JSON.parse(curTaleList);
},
//关闭临时对话框的方法,主要执行了:
//1.隐藏对话框
//2.将Store中共享的,表明聊天对象的变量信息重置为空
//3.将聊天记录重置为空
closeChat() {
this.openChat=false;
this.$store.commit('SET_CHAT_DIA_ACT_USER_NICK',"");
this.$store.commit('SET_CHAT_DIA_ACT_USER_ID',"");
this.taleList = [];
},
//在聊天窗口中点击发送按钮后的方法,主要执行了
//1.在聊天窗口的聊天记录中添加mine为true的自己发送的消息,并存储到本地缓存
//2.向服务器端请求发送给聊天对象发送的消息
bindEnter (e) {
const msg = this.inputMsg
if (!msg) return;
var nowDate = moment(new Date().getTime()).format("YYYY/MM/DD HH:mm:ss");
const msgObj = {
"date": nowDate,
"text": { "text": msg },
"mine": true,
"name": store.getters.nick,
"img": store.getters.avatar
}
this.taleList.push(msgObj)
var curTaleList = localStorage.getItem(store.getters.chatDialogActiveUserNick) == null ? JSON.stringify([]) : localStorage.getItem(store.getters.chatDialogActiveUserNick) ;
var getCurTaleList = JSON.parse(curTaleList);
getCurTaleList.push(msgObj);
localStorage.setItem(store.getters.chatDialogActiveUserNick,JSON.stringify(getCurTaleList));
if (store.getters.ws) {
this.usersMessage.fromNick = store.getters.nick;
this.usersMessage.fromUserId = store.getters.userId;
this.usersMessage.fromAvatar = store.getters.avatar;
this.usersMessage.users = [store.getters.chatDialogActiveUserId];
this.usersMessage.msg = msg;
store.getters.ws.send(JSON.stringify(this.usersMessage));
} else {
alert("未连接到服务器");
}
},
},
5.3.4 导航栏收到消息时的操作,Navbar.vue
this.ws.onmessage = function (event) {
console.log("收到消息!!!");
//self.text_content = event.data + "\n";
// 判断是推动预警消息的时候
var fromUser = JSON.parse(event.data).fromUser;
var msg = JSON.parse(event.data).msg;
// 预警消息包含预警id的时候
if(fromUser == "系统发送反馈"){
if(msg.includes("成功")){
self.$modal.msgSuccess(msg);
} else {
self.$modal.msgError(msg);
}
} else if(fromUser == "流程中心") {
Notification.info({
title: "来自" + fromUser + "的消息 " + moment(new Date().getTime()).format(
"HH:mm:ss"
),
dangerouslyUseHTMLString: true,
message: msg,
duration: 3000,
position: "bottom-right",
onClick: function () {
self.flowDetail(); //自定义回调,message为传的参数
},
});
} else {
//如果发送来的消息,对应的对话框窗口是打开的,那么
//1.消息存储到聊天对象的本地缓存中
//2.设置store中共享的变量,表明有新增消息
if(store.getters.chatDialogActiveUserNick==fromUser){
var fromAvatar = JSON.parse(event.data).fromAvatar;
var fromUserId = JSON.parse(event.data).fromUserId;
var nowDate = moment(new Date().getTime()).format("YYYY/MM/DD HH:mm:ss");
var taleListOne = {
"date":nowDate,
"text":{"text":msg},
"mine":false,
"name":fromUser,
"img":fromAvatar
};
var getCurTaleList = localStorage.getItem(fromUser) == null ? JSON.stringify([]) : localStorage.getItem(fromUser);
var curTaleList = JSON.parse(getCurTaleList);
curTaleList.push(taleListOne);
localStorage.setItem(fromUser,JSON.stringify(curTaleList));
var changed = store.getters.chatCurGetInfoChanged ? false : true
console.log("changed:" + changed);
store.commit("SET_CHAT_CUR_GETINFO_CHANGED",changed);
} else {
//如果发送来的消息,对应的对话框窗口是关闭的,那么
//1.消息存储到聊天对象的本地缓存中
//2.生成弹窗消息显示
Notification.info({
title: "来自" + fromUser + "的消息 " + moment(new Date().getTime()).format(
"HH:mm:ss"
),
dangerouslyUseHTMLString: true,
message: msg,
duration: 3000,
position: "bottom-right",
onClick: function () {//预留跳转
//self.warnDetailByWarnid(messageBody.warnId); //自定义回调,message为传的参数
// 点击跳转的页面
},
});
var fromAvatar = JSON.parse(event.data).fromAvatar;
var fromUserId = JSON.parse(event.data).fromUserId;
var nowDate = moment(new Date().getTime()).format("YYYY/MM/DD HH:mm:ss");
var taleListOne = {
"date":nowDate,
"text":{"text":msg},
"mine":false,
"name":fromUser,
"img":fromAvatar
};
var getCurTaleList = localStorage.getItem(fromUser) == null ? JSON.stringify([]) : localStorage.getItem(fromUser);
var curTaleList = JSON.parse(getCurTaleList);
curTaleList.push(taleListOne);
localStorage.setItem(fromUser,JSON.stringify(curTaleList));
}
}
};
5.4 最终效果
最终测试效果已经部署到本人个人的服务器,有兴趣的可以去测试和体验一下,体验地址:
怎知后台管理系统https://www.zenknow.cn/
- 体验账号1:test1 密码:123456 (公告发起者账号)
- 体验账号2:test2 密码:123456 (流程审核者账号)