模块概览
基于 Vue 2 + Element UI + WebSocket 的企业级即时通讯模块,支持单聊、群聊、审批消息推送、翻译等功能。
聊天核心流程
1. 页面初始化
kotlin
mounted() {
// 从通讯录跳转时,store 中存有 receiverUserId
const receiverUserId = this.$store.getters.receiverUserId;
if (receiverUserId) {
this.handleSendMessage({ friendId: receiverUserId });
} else {
this.initData(); // 加载会话列表 + 自动选中首个会话
}
}
initData() 做了三件事:
- 请求
/api/q5-system/chatMessageBox/list获取会话列表 - 填充左侧
winBarConfig.list - 自动选中第一个会话,调用
openMsg()加载消息并建立 WebSocket
2. 发送文本消息
javascript
bindEnter(res) {
let msg = this.inputMsg;
// 清除富文本编辑器的 HTML 标签
msg = msg.replace(/</?p>/g, '');
msg = msg.replace(/</?br\s*/?>/g, '');
// 1. 本地列表先追加(即时显示)
this.list.push({
date: this.dateFormat(new Date(), 'yyyy-MM-dd hh:mm:ss'),
text: { text: msg },
mine: true,
name: this.uInfo.name,
img: this.uInfo.avatar,
});
// 2. 通过 WebSocket 发送
this.ws.send(JSON.stringify({
receiverUserId: this.currentItem.receiverUserId,
receiverGroupId: this.currentItem.receiverGroupId,
content: msg,
type: "text",
fileName: "",
}));
}
3. 接收消息
kotlin
this.ws.onmessage = (event) => {
let ele = JSON.parse(event.data);
// 只处理当前打开的会话
if (ele.boxId == this.currentItem.id) {
this.list.push({
date: this.dateFormat(new Date(ele.date), 'yyyy-MM-dd hh:mm:ss'),
text: { text: ele.content },
mine: this.uInfo.id == ele.userId,
name: ele.name,
img: ele.img,
messageId: ele.messageId,
translate: ele.translate,
boxId: ele.boxId,
});
// 标记已读
getHttpData('/api/q5-system/chatUserMessage/read', 'get', {
ids: ele.messageId
});
}
// 刷新左侧会话栏(更新未读数)
this.initWinBar();
};
4. WebSocket 连接
javascript
// 双重认证:Bearer token + Basic clientId:clientSecret
let token = "bearer " + getToken();
let authorization = `Basic ${Base64.encode(`${clientId}:${clientSecret}`)}`;
this.ws = new WebSocket(
`wss://q5.qh16688.com/q5-system/websocket/${language}/${userId}`,
[],
{ headers: { 'Authorization': authorization, 'Blade-Auth': token } }
);
// 页面销毁时优雅关闭
beforeDestroy() {
if (this.ws) {
this.ws.close(1000, 'page destroy');
}
}
5. 文件/图片/视频发送
ini
async handleHttpRequest(file, type) {
// 1. FormData 上传文件
const formData = new FormData();
formData.append("file", file);
const res = await putFile(formData);
const { link, originalName } = res.data.data;
// 2. 根据类型组装不同的 HTML 内容
let content = '';
if (type == 'file') {
content = "<i class='el-icon-document-checked' data-src='" + link + "'/>";
// subLink 用于点击下载
}
if (type == 'img') {
content = "<img data-src='" + link + "'/>";
}
if (type == 'video') {
content = "<video data-src='" + link + "' controls='controls' />";
}
// 3. 本地追加 + WebSocket 发送
this.list.push({ text: { text: content }, mine: true, ... });
this.ws.send(JSON.stringify({
receiverUserId: this.currentItem.receiverUserId,
content: type == 'file' ? link : content,
type: type,
fileName: originalName || '',
}));
}
文件大小限制 50MB,工具栏配置 show: ["file", "img", "video"]。
6. 历史消息加载
kotlin
// 首次打开会话
openMsg(item) {
this.page.current = 1;
getHttpData('/api/q5-system/chatMessage/page', 'get', {
boxId: item.id, current: 1, size: 10
}).then(res => {
// records 倒序排列(最新在前),reverse 回时间正序
let records = res.data.data.records.slice().reverse();
records.forEach(ele => {
this.list.push({
date: ele.date,
text: { text: ele.content, subLink: ... },
mine: this.uInfo.id == ele.userId,
type: ele.type,
});
});
});
}
// 上拉加载更多
loadMoreList(done) {
this.page.current++;
if (this.page.current <= this.pages) {
getHttpData('/api/q5-system/chatMessage/page', 'get', {
boxId: this.currentItem.id, ...this.page
}).then(res => {
let records = res.data.data.records.slice().reverse();
// 拼接到列表头部
this.list = records.concat(this.list);
});
} else {
done(); // 没有更多了
}
}
7. 会话栏切换与关闭
kotlin
bindWinBar(play) {
switch (play.type) {
case 'winBar':
// 切换会话:更新 config + 加载消息
this.config = { ...this.config, name: play.data.name, img: play.data.img, ... };
this.openMsg(play.data);
break;
case 'close':
// 关闭会话:隐藏(不是删除)
getHttpData('/api/q5-system/chatMessageBox/submit', 'post', {
id: play.data.id, isDeleted: 1
});
break;
}
}
扩展功能
通讯录 -> 发起聊天
kotlin
// 通过 $addressBook 插件打开通讯录弹窗
this.$addressBook((row) => {
// 选中联系人后,创建聊天会话
getHttpData('/api/q5-system/chatMessageBox/submit', 'post', {
receiverUserId: row.friendId
}).then(res => {
this.initData(res.data.data); // 跳转到新会话
});
});
群聊管理
javascript
// 创建群聊
createGroup() {
this.$groupDialog((checkedKeys) => {
let userList = checkedKeys.map(id => ({ userId: id }));
getHttpData('/api/q5-system/chatGroup/submit', 'post', { userList });
});
}
// 编辑群聊(改名/加人/踢人/解散/退出)
editGroup() {
this.$groupChat((list) => {
this.getGroupList(); // 刷新群成员
this.initWinBar(); // 刷新会话栏
}, this.rightConfig.list, this.config);
}
系统消息 -> 发送到聊天
javascript
handleSystemMsg() {
this.$systemMsg((res) => {
// 选中审批通知后,以 bill 类型发送
let msgObj = {
text: { text: JSON.stringify(res) },
mine: true,
type: 'bill', // 审批单据消息
};
this.list.push(msgObj);
this.ws.send(JSON.stringify({
receiverUserId: this.currentItem.receiverUserId,
content: JSON.stringify(res),
type: "bill",
}));
});
}
消息翻译
less
// 自动翻译开关(工具栏 checkbox)
toolEvent('translate', flag) {
getHttpData('/api/q5-system/chatMessageBox/autoTranslate', 'get', {
id: this.currentItem.id,
flag: flag
});
}
// 单条翻译(chatList 组件内)
getHttpData('/api/q5-system/i18n/translate', 'get', {
content: msgText,
language: 'en',
boxId: boxId,
messageId: messageId
});
消息撤回与删除
javascript
// 只能撤回自己的消息
retractMsg() {
getHttpData('/api/q5-system/chatUserMessage/retract', 'post', { id: messageId });
}
deleteMsg() {
getHttpData('/api/q5-system/chatUserMessage/delete', 'post', { id: messageId });
}
技术要点
对话框插件模式
所有弹窗采用 Vue.extend + $mount + Vue.prototype 模式,无需在模板中声明组件:
javascript
const Constructor = Vue.extend(DialogComponent);
const instance = new Constructor();
instance.$mount(document.createElement('div'));
document.body.appendChild(instance.$el);
Vue.prototype.$dialogName = function(callback) { instance.open(callback); };
// 调用
this.$addressBook((row) => { /* 选中联系人 */ });
this.$systemMsg((msg) => { /* 发送系统消息 */ });
this.$groupDialog((keys) => { /* 创建群聊 */ });
this.$groupChat(() => { /* 管理群聊 */ });
滚动与未读
- 基于 BetterScroll 封装,支持下拉加载历史消息
- 未读消息计数:判断消息是否在视窗内超过 90%
- 新消息提醒:标题栏闪烁 + 浏览器 Notification
消息类型
| type | 渲染方式 | 说明 |
|---|---|---|
text |
纯文本/HTML | 支持 <img> <video> <audio> 标签 |
file |
图标 + 下载链接 | 点击触发浏览器下载 |
bill |
审批单据卡片 | JSON 解析为 title:content 列表,点击跳转单据页 |
approval |
审批消息 | 显示制单人/提交时间,点击跳转审批详情 |