Chat 聊天模块功能总结

模块概览

基于 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 审批消息 显示制单人/提交时间,点击跳转审批详情
相关推荐
子非鱼a1 小时前
【WEB】EasySSTI
java·开发语言·前端
小羊没烦恼!1 小时前
jQuery1.5的改进细节
java·服务器·开发语言·前端·c#
独孤九剑打醒他1 小时前
【原创开源·修订版】源-栅-漏-栅-源横向双栅MOS:从“被误解的短路”到“电流路径多值逻辑与顶层供电架构”
前端·嵌入式硬件·架构·开源·硬件工程
创世虚拟世界2 小时前
我做了一个 3D 虚拟世界基底,基于这个再做 3D 虚拟世界或者元宇宙,会事半功倍
javascript·人工智能·3d·gitee·虚拟现实
阿狗童鞋2 小时前
React实战指南
前端·react.js·前端框架
v:ychya20182 小时前
独立站外贸建站公司怎么选?建站就要建营销型网站
前端·php
春涧草茶2 小时前
慢就是快12-12手动抛出异常
java·linux·前端
三小河2 小时前
从 Markdown 到 Generative UI:AI 如何从“生成答案”进化到“生成界面”?
前端·人工智能·后端
请为小H留灯2 小时前
Windows 本地前端项目一键运行指南:Volta+Node+pnpm + 阿里镜像完整初始化流程
前端·pnpm·node·js