实现方案:
环信没有临时聊天人员历史记录接口,所以实现的方案是:
1.监听消息接收;2.将受到的消息存到缓存中,同时判断此用户是否之前聊天过,如果没聊过的话,把他的用户信息也存到缓存中;3.展示的东西都是从缓存中获取;4.新消息提醒是在监听到消息后,给用户信息里设置一个状态值,当点击刚用户查看历史记录时,再改变这个状态值
实现效果:
1.准备工作
npm引入sdk及 strophe.js(坑:刚开始下载的最新版的sdk,但是下载下来总是少文件src,挣扎半天,换了1.8.3版本的sdk好了,可能跟我项目的哪些东西的版本有不兼容的吧)
(1)cnpm i easemob-websdk@1.8.3 --save
(2)cnpm i strophe.js@1.2.16 --save
(3)下载webim.config.js: https://gitee.com/weimingye/web-im/blob/master/demo/javascript/dist/webim.config.js#
2.修改sdk的connection.js文件(注意新增代码的位置,放在11行声明变量的后边)
代码:
//新增代码***********start var Strophe = require('../../strophe.js/dist/strophe.js').Strophe; var meStrophe = require('../../strophe.js/dist/strophe.js'); $iq = meStrophe.$iq; $build = meStrophe.$build; $msg = meStrophe.$msg; $pres = meStrophe.$pres; //新增代码***********end
3.修改strophe.js
代码:
//新增代码***********start setJid: function (jid) { this.jid = jid; this.authzid = Strophe.getBareJidFromJid(this.jid); this.authcid = Strophe.getNodeFromJid(this.jid); }, getJid: function () { return this.jid; }, //新增代码***********end
4.修改webim.config.js(首行、最后一行,把自己公司的appkey换上去)
5.main.js新增配置
//环信start require('./assets/webim.config.js') let WebIM = require('easemob-websdk') Vue.prototype.$webim = WebIM const conn = new WebIM.connection({ isMultiLoginSessions: WebIM.config.isMultiLoginSessions, https: typeof WebIM.config.https === 'boolean' ? WebIM.config.https : location.protocol === 'https:', url: WebIM.config.xmppURL, heartBeatWait: WebIM.config.heartBeatWait, autoReconnectNumMax: WebIM.config.autoReconnectNumMax, autoReconnectInterval: WebIM.config.autoReconnectInterval, apiUrl: WebIM.config.apiURL, isAutoLogin: true }) const options = { apiUrl: WebIM.config.apiURL, user: '',//用户名 pwd: '',//密码 appKey: WebIM.config.appkey, success:function (res) { console.log('链接服务器正常') }, error:function (err) { alert(err) } } Vue.prototype.$imconn = conn Vue.prototype.$imoption = options //环信end
6.聊天中的表情编码对应文件
emoji.js
module.exports = { path: "../../static/faces", obj: { "[):]": "ee_1.png", "[:D]": "ee_2.png", "[;)]": "ee_3.png", "[:-o]": "ee_4.png", "[:p]": "ee_5.png", "[(H)]": "ee_6.png", "[:@]": "ee_7.png", "[:s]": "ee_8.png", "[:$]": "ee_9.png", "[:(]": "ee_10.png", "[:'(]": "ee_11.png", "[:|]": "ee_18.png", "[(a)]": "ee_13.png", "[8o|]": "ee_14.png", "[8-|]": "ee_15.png", "[+o(]": "ee_16.png", "[<o)]": "ee_12.png", "[|-)]": "ee_17.png", "[*-)]": "ee_19.png", "[:-#]": "ee_20.png", "[:-*]": "ee_22.png", "[^o)]": "ee_21.png", "[8-)]": "ee_23.png", "[(|)]": "ee_24.png", "[(u)]": "ee_25.png", "[(S)]": "ee_26.png", "[(*)]": "ee_27.png", "[(#)]": "ee_28.png", "[(R)]": "ee_29.png", "[({)]": "ee_30.png", "[(})]": "ee_31.png", "[(k)]": "ee_32.png", "[(F)]": "ee_33.png", "[(W)]": "ee_34.png", "[(D)]": "ee_35.png" } };
index.js(暂时未用到)
const _WIDTH = window.screen.availWidth > 350 ? 350 : window.screen.availWidth; export default { // whether auto check media query and dispatch by redux or not ? reduxMatchMedia: true, // map of media query breakpoints dimensionMap: { xs: "480px", sm: "768px", md: "992px", lg: "1200px", xl: "1600px" }, name: "Web IM", logo: "", SIDER_COL_BREAK: "sm", // md SIDER_COL_WIDTH: 80, SIDER_WIDTH: 350, RIGHT_SIDER_WIDTH: _WIDTH, // imgType: { // gif: 1, // bmp: 1, // jpg: 1, // png: 1, // }, PAGE_NUM: 20 };
7.图片文件存放在static文件夹下,图片大致都差不多,网上应该可以查到
8.表情组件
<template> <span> <el-popover ref="popover5" placement="top-start" width="360" v-model="showModal"> <img v-for="(v,i) in emojiList" :src="require(`../../../static/faces/${v}`)" :key="i" @click="selectEmoji(i)" class="img-style" /> </el-popover> <i class="icon iconfont icon-face" @click="showModal = !showModal"></i> </span> </template> <script> import emoji from "../../config/emoji"; export default { data() { return { emojiList: emoji.obj, currentEmoji: "", showModal: false }; }, methods: { selectEmoji(e) { let value = (this.inpMessage || "") + e; this.$data.showModal = false; this.$emit("selectEmoji", value); } }, props: { inpMessage: String } }; </script> <style scoped> /deep/ .el-popover.el-popper { 360px; position: absolute; bottom: 20%; left: 260px; transform-origin: center bottom; z-index: 2001; } .img-style { 22px; margin: 5px; cursor: pointer; } .img-style:hover { background-color: aquamarine; } </style>
8.页面中使用
<template> <div> <div class="headerTitle">{{title}}</div> <div class="chatBox"> <!-- 临时聊天用户列表--> <div class="chatList" ref="chatList"> <div class="chatList_title">近期联系人</div> <div> <div v-if="chatList && chatList.length>0" v-for="(item,index) in chatList" :key="index" class="chatList_item" :class="{'active':index == currentIndex}" @click="selectFriend(item.userPhone,index)"> <img :src="item.userPic" :onerror="defaultImgs" style="min- 40px;"/> <div>{{item.userPhone}} {{item.userName}}</div> <span v-if="item.active"></span> </div> </div> </div> <!-- 聊天内容--> <div class="content" ref="chatContent"> <!-- 聊天历史记录--> <div class="historyList" id="historyList"> <div v-for="(item,index) in historyMessage" :key="index"> <div class="item_time">{{item.time}}</div> <div class="item_message" :class="{'currentUser':item.from == username_from}"> <!-- 展示图片--> <img v-if="item.type == 'img'" :src="item.message" style="max- 30%"/> <!--解析文本或表情--> <p v-if="item.type =='txt'" style="user-select: text" v-html="renderTxt(item.message)" /> </div> </div> </div> <!--发送聊天--> <div class="chatInput"> <div style="padding: 4px 10px;"> <!-- 表情组件 --> <ChatEmoji v-on:selectEmoji="selectEmoji" :inpMessage="message" /> <!-- 上传图片 --> <div class="upImgBox"> <i class="icon iconfont icon-tupian"></i> <input type="file" class="sendImg" ref="sendImg" @change="sendPrivateUrlImg"> </div> </div> <!-- 输入框--> <el-input ref="txtDom" type="textarea" :autosize="{ minRows: 4, maxRows: 8}" placeholder="请输入内容" resize="none" v-model="message" @keyup.enter.native="sendPrivateText"> </el-input> <el-button type="primary" size="small" @click="sendPrivateText" style="float: right;margin-right: 20px;">提交</el-button> </div> </div> </div> </div> </template> <script> import ChatEmoji from "../../components/chatEmoji/index.vue";//表情组件 import emoji from "../../config/emoji"; export default { name: "index", components: { ChatEmoji }, data(){ return { title: '互动消息', type: 'contact', currentUserpwd: '123456', username_from: '15263819410',//当前登录的用户(发送人手机号) username_to: '',//当前聊天的用户名(接收人手机号) message:'',//当前发送的信息 historyMessage: [],//聊天历史记录 showEmoji: false,//是否展示表情组件 chatList:[],//临时聊天用户列表 currentIndex: undefined,//左侧当前激活index defaultImgs: 'this.src = "' + require('../../assets/img/userLogo.png') + '"', } }, created() { this.login(); this.updateChatList();//获取临时聊天用户列表 }, mounted(){ this.$refs.chatContent.style.height = this.getClientHeight() - 50 + 'px'; this.$refs.chatList.style.height = this.getClientHeight() - 50 + 'px'; }, methods: { //登录环信账户 login(){ var _this = this; this.$imoption.user = this.username_from; this.$imoption.pwd = this.currentUserpwd; this.$imoption.success = (res)=>{ // console.log('登录成功回调',res); } this.$imconn.open(this.$imoption); this.$imconn.listen({ onOpened: function (message) { // console.log('用户已上线'); }, onClosed: function (message) { this.$message.info('用户已下线') }, //收到表情消息 onEmojiMessage: function (message) { // //把聊天对象名message.from存储到storage中 _this.updateChatList(message); //把新接收的信息更新存储到storage中 _this.updateStorage(message); _this.selectFriend(message.from); }, //收到图片消息 onPictureMessage: (message) => { //把聊天对象名message.from存储到storage中 _this.updateChatList(message); //把新接收的信息更新存储到storage中 _this.updateStorage(message); _this.selectFriend(message.from); }, //收到文本消息 onTextMessage: function (message) { //把聊天对象名message.from存储到storage中 _this.updateChatList(message); //把新接收的信息更新存储到storage中 _this.updateStorage(message); // 更新历史记录到页面 _this.selectFriend(message.from); }, //收到音频消息 onAudioMessage: function ( message ) { var options = { url: message.url }; options.onFileDownloadComplete = function ( response ) { //音频下载成功,需要将response转换成blob,使用objectURL作为audio标签的src即可播放。 var objectURL = WebIM.utils.parseDownloadResponse.call(Demo.conn, response); // console.log('音频',objectURL) }; options.onFileDownloadError = function () { //音频下载失败 }; //通知服务器将音频转为mp3 options.headers = { 'Accept': 'audio/mp3' }; WebIM.utils.download.call(conn, options); }, //收到视频消息 onVideoMessage: function (message) { var node = document.getElementById('privateVideo'); var option = { url: message.url, headers: { 'Accept': 'audio/mp4' }, onFileDownloadComplete: function (response) { var objectURL = WebIM.utils.parseDownloadResponse.call(conn, response); // console.log('视频消息',objectURL); node.src = objectURL; }, onFileDownloadError: function () { // console.log('File down load error.') } }; WebIM.utils.download.call(conn, option); }, }) }, // 获取好友列表(项目没用到) // getFriends(){ // var newarry = []; // this.friends = newarry; // this.$imconn.getRoster({ // success: function (roster) { // console.log('获取好友列表1',roster) // roster.forEach((item,index)=>{ // if(item.subscription === 'both' || item.subscription === 'to'){ // newarry.push(item); // console.log('好友列表',newarry, item.name); // } // }); // }, // error: function (error) { // console.log('error',error) // } // }) // }, //发送文本消息 sendPrivateText(){ if(this.message == ''){ this.$message.error('不能发送空白信息'); return false; } if(this.username_to == ''){ this.$message.error('请选择发送的好友'); return false; } var id = this.$imconn.getUniqueId();// 生成本地消息id var msg = new WebIM.message('txt',id); var sendTime = this.getNowTime(); var username_from = this.username_from; var message = this.message; var _this = this; msg.set({ msg: message, // 消息内容 to: _this.username_to, // 接收消息对象(用户id) roomType: false, success: function (id, serverMsgId) { _this.historyMessage.push({ from: username_from, message: message, time:sendTime, type:'txt' }) window.localStorage.setItem(_this.username_to,JSON.stringify(_this.historyMessage)); var lusername = window.localStorage.getItem(_this.username_to); lusername = JSON.parse(lusername); _this.message = ''; _this.setScroll(); }, fail: function(e){ // console.log("Send private text error"); } }); this.$imconn.send(msg.body); }, // 发送图片消息 sendPrivateUrlImg(context, payload) { let _this = this; if(_this.username_to == ''){ _this.$message.error('请选择发送的好友'); return false; } let file = WebIM.utils.getFileUrl(_this.$refs.sendImg); // 将图片转化为二进制文件 var id = this.$imconn.getUniqueId(); // 生成本地消息id var msg = new WebIM.message('img', id); // 创建图片消息 // console.log('图片消息',msg); msg.set({ apiUrl: WebIM.config.apiURL, file: file, to: this.username_to,// 接收消息对象 roomType: false, chatType: 'singleChat', timestamp:new Date().getTime(), onFileUploadError: function(error){ // console.log("图片上传失败", error); }, onFileUploadComplete: function(file){ //更新发送的图片到缓存中 _this.historyMessage.push({ from: _this.username_from, message: `${file.uri}/${file.entities[0].uuid}`, time:_this.getNowTime(), type:'img' }) window.localStorage.setItem(_this.username_to,JSON.stringify(_this.historyMessage)); var lusername = window.localStorage.getItem(_this.username_to); lusername = JSON.parse(lusername); this.setScroll(); }, success: function(){ // console.log("图片发送成功"); } }); this.$imconn.send(msg.body); }, // 选择表情 selectEmoji (code) { this.showEmoji = false this.message = code; this.$refs.txtDom.focus(); }, //返回表情 customEmoji(value) { return `<img src="http://39.100.159.29/faces/${value}" style="20px"/>`; // return '<img src="http://localhost:9527/static/img/logo.063d13ac.png"/>' 本地图片访问不到,原因未知 }, //解析接收的消息 renderTxt(txt = "") { let rnTxt = []; let match = null; const regex = /([.*?])/g; let start = 0; let index = 0; while ((match = regex.exec(txt))) { index = match.index; if (index > start) { rnTxt.push(txt.substring(start, index)); } if (match[1] in emoji.obj) { const v = emoji.obj[match[1]]; rnTxt.push(this.customEmoji(v)); } else { rnTxt.push(match[1]); } start = index + match[1].length; } rnTxt.push(txt.substring(start, txt.length)); return rnTxt.toString().replace(/,/g, ""); }, // 获取发送和接收消息的时间 getNowTime(){ let now = new Date(); return now.getFullYear() + '-' + now.getMonth()+1 + '-' + now.getDate() + ' ' + (now.getHours()<10 ? '0' + now.getHours() : now.getHours()) + ':' + (now.getMinutes()<10 ? '0' + now.getMinutes() : now.getMinutes()) + ':' + (now.getSeconds()<10 ? '0' + now.getSeconds() : now.getSeconds()); }, // 选择好友发送消息 selectFriend(item,index){ if(index != undefined){this.currentIndex = index}; var chatList = JSON.parse(window.localStorage.getItem('chatList')); this.username_to = item; //获取当前聊天对象历史记录 var historyMessage = window.localStorage.getItem(item); historyMessage = historyMessage ? JSON.parse(historyMessage) : []; var emojiArr = []; for(var i=0;i<historyMessage.length;i++){ var item = historyMessage[i].message; if(typeof(item) === 'object'){ if(item[0].type == 'emoji'){ var emojiItem = {}; var url = item[0].data; item.data = url; } } } this.historyMessage = historyMessage; //清除消息提示 console.log('chatList',window.localStorage.getItem('chatList')); for(var i=0;i<chatList.length;i++){ console.log('username',this.username_to) if(this.username_to == chatList[i].userPhone){ chatList[i].active = false; } } window.localStorage.setItem('chatList',JSON.stringify(chatList)); this.chatList = chatList; this.setScroll(); }, // 更新聊天历史记录缓存数据 updateStorage(message){ let sendTimeWZ = message.time ? new Date(message.time) : new Date(); let sendTime = sendTimeWZ.getFullYear() + '-' + sendTimeWZ.getMonth()+1 + '-' + sendTimeWZ.getDate() + ' ' + (sendTimeWZ.getHours()<10 ? '0' + sendTimeWZ.getHours() : sendTimeWZ.getHours()) + ':' + (sendTimeWZ.getMinutes()<10 ? '0' + sendTimeWZ.getMinutes() : sendTimeWZ.getMinutes()) + ':' + (sendTimeWZ.getSeconds()<10 ? '0' + sendTimeWZ.getSeconds() : sendTimeWZ.getSeconds()); //判断接收的消息是什么类型 let type = ''; if(message.url){ type = 'img'; }else{ type = 'txt'; } //定义新接收到的信息 let message_new = { from: message.from, message: message.url ? message.url :message.data, time: sendTime, type: type } var historyMessage = window.localStorage.getItem(message.from); historyMessage = historyMessage ? JSON.parse(historyMessage) : []; historyMessage.push(message_new); window.localStorage.removeItem(message.from); window.localStorage.setItem(message.from,JSON.stringify(historyMessage)); this.setScroll(); }, // 判断数组对象中是否有某个属性值 findElem(chatList,userPhone,formId){ for(var i=0;i<chatList.length;i++){ if(chatList[i].userPhone == formId){ return i } } return -1; }, //获取可视区域高度 getClientHeight(){ var clientHeight=0; if(document.body.clientHeight&&document.documentElement.clientHeight) { var clientHeight = (document.body.clientHeight<document.documentElement.clientHeight)?document.body.clientHeight:document.documentElement.clientHeight; } else { var clientHeight = (document.body.clientHeight>document.documentElement.clientHeight)?document.body.clientHeight:document.documentElement.clientHeight; } return clientHeight; }, // 更新临时聊天用户列表 updateChatList(message){ var chatList_str = window.localStorage.getItem('chatList'); var chatList = chatList_str ? JSON.parse(chatList_str) : []; if(message){ //用户信息之前不存在 if(this.findElem(chatList,'userPhone',message.from) == (-1)){ var chatUser_new = { userName: message.ext.userName, userPic: message.ext.userPic, userPhone: message.from, active: false } chatList.push(chatUser_new); } // 新消息提示 for(var i=0;i<chatList.length;i++){ if(message.from == chatList[i].userPhone){ chatList[i].active = true; } } } window.localStorage.setItem('chatList',JSON.stringify(chatList)); this.chatList = chatList; }, // 设置默认滚动到页面最底部 setScroll(){ let msg = document.getElementById('historyList') // 获取对象 if(msg){ this.$nextTick(() => { msg.scrollTop = msg.scrollHeight // 滚动高度 }) } } } } </script> <style scoped lang="scss"> .headerTitle{ color: #757373; 100%; height: 50px; line-height: 50px; padding-left: 10px; border-bottom: 1px solid #ddd; position: fixed; top: 50px; left: 210px; z-index: 100; background-color: #f9fafa; } #app .hideSidebar { .headerTitle { left: 54px; } } #app .mobile.hideSidebar { .headerTitle { left: 0; } } .chatBox { display: flex; justify-content: flex-start; align-items: flex-start; } .chatList { 20%; height: 100%; border-right: 1px solid #ddd; display: inline-block; font-size: 14px; padding-top: 50px; .chatList_title { 100%; height: 40px; line-height: 40px; padding: 0 20px; font-weight: 600; border-bottom: 1px solid #ddd; background-color: #f9fafa; } .chatList_item { 100%; height: 60px; padding: 0 20px; border-bottom: 1px solid #ddd; display: flex; justify-content: flex-start; align-items: center; cursor: pointer; position: relative; span { 10px; height: 10px; border-radius: 50%; background-color: red; position: absolute; right: 14px; top: 10px; } &.active { background-color: #0178EF; color: #fff; } img { 40px; height: 40px; border-radius: 50%; margin-right: 10px; } } } .content { 80%; display: inline-block; padding: 44px 0 20px; .historyList { 100%; height: 80%; padding: 20px 20px 0; overflow-y: scroll; .item_time { text-align: center; margin: 10px 0; } .item_message { text-align: left; margin-bottom: 20px; &.currentUser { text-align: right; p { display: inline-block; background-color: #00b7ee; border: 1px solid #00b7ee; color: #fff; } } p { display: inline-block; border: 1px solid #ddd; border-radius: 4px; padding: 4px 8px; } } } } /*表情聊天对话框*/ .chatInput { border-top: 1px solid #ddd; /deep/ .el-textarea__inner:hover { border: none; } /deep/ .el-textarea__inner { border: none; } /deep/ .icon { cursor: pointer; } /*发送图片*/ .upImgBox { display: inline-block; position: relative; 16px; height: 17px; margin: 0 4px; cursor: pointer; /deep/ .iconfont { position: absolute; left: 0; top: 0; cursor: pointer; } .sendImg { position: absolute; left: 0; top: 0; 16px; height: 17px; opacity: 0; } } } </style>