在线客服系统源码开发实战总结:需求分析及前端代码基本技术方案
在这个系列文章里,我尝试将自己开发唯一客服系统(gofly.v1kf.com)所涉及的经验和技术点进行梳理总结。
文章写作水平有限,有时候会表达不清楚,难免有所疏漏,欢迎批评指正
该系列将分成以下几个部分
一. 需求分析
二. 初步技术方案选型,验证
三. 数据库结构设计
四. WEB访客前端设计与开发
五. WEB客服端设计与开发
六. 客户端设计与开发
在这个系列的文章中,您将了解并学习到以下技术知识:
MySQL、VUE、WebSocket、Golang+Gin、UniApp 等
如果这些技术对您有用,还请您 推荐 一下本文章,谢谢!
什么是在线客服系统:
常见的用法是,点击立即咨询按钮,直接跳转到聊天窗口。或者是只需将系统生成的一段JavaScript代码嵌入网站页面,即可在网站上显示代表客服的浮动小图标,邀请框,点击按钮后在当前页面弹窗展示。
而客服端可以在WEB客服后台,查看网站正在沟通的实时在线访客、浏览轨迹等,能直接和网站访客进行在线即时交流,目的是提升客户满意度,及时解决客户的问题,进一步提升网站的销售额。
由此分析,在线客服系统大至分为三大块:1)访客端,2)客服端,3)客服移动端。但是仅仅分为这三大块是不够的,后面我们还将对每一块进行进一步的分析。
访客弹窗入口界面
访客端弹窗界面
前端界面是使用的elementui,是基于vue.js的UI框架。作为后端开发程序员,非常不习惯用node.js编译开发前端,所以我还是选择了使用cdn引入的形式去使用这个框架
弹窗效果是使用的layer.js进行的弹窗,点击图标,调用layer.js去iframe的形式加载了访客链接,这个访客链接就是下面直接打开时的效果
访客端直接打开的界面
此界面为响应式设计,综合运用了css3的媒体查询功能,在大屏幕和小屏幕都能适配展示,所以该访客界面是可以直接接入微信和APP中。
这个界面可以说的还是比较多的,后面我再去详细总结
客服端界面
客服端也是使用的elementUI框架,整体结构是iframe框出来的,然后点击不同的菜单加载URL展示出来
总体来说,项目是偏向后端风格的,偏传统的架构
下面是访客端界面的代码,就可以看出这个工作量有多大~~
<!DOCTYPE html> <head> <meta charset="utf-8"> <!--删除苹果默认的工具栏和菜单栏,默认为no显示工具栏和菜单栏。--> <meta name="apple-mobile-web-app-capable" content="yes"/> <!--QQ强制全屏--> <meta name="x5-fullscreen" content="true"> <!--UC强制全屏--> <meta name="fullscreen" content="yes"> <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" name="viewport" /> <title>{{.Title}}</title> <link rel="stylesheet" href="/static/cdn/element-ui/2.15.1/theme-chalk/index.min.css"> <script src="/static/cdn/vue/2.6.11/vue.min.js"></script> <script src="/static/cdn/element-ui/2.15.1/index.js"></script> <script src="/static/cdn/jquery/3.6.0/jquery.min.js"></script> <script src="/static/js/functions.js?v=0.6.9"></script> <link rel="stylesheet" href="/static/css/common.css?v=yuyfgfgfg" /> <link rel="stylesheet" href="/static/css/icono.min.css" /> <link rel="icon" href="/static/images/favicon.ico"> <style> .el-message-box{ width: auto; max-width: 100%; max-height: 100%; overflow: auto; } </style> </head> <body class="visitorBody"> <div id="app" class="chatCenter"> <template> <!--客服代码--> <div class="chatEntTitle" v-show="!isIframe"> <el-badge :type="onlineType" is-dot class="item"> <el-avatar class="chatEntTitleLogo" :size="35" :src="noticeAvatar"></el-avatar> </el-badge> <div> <div><{chatTitle}></div> <div class="entIntro" v-show="entIntroduce!=''"><{entIntroduce}></div> </div> </div> <div class="chatEntBox"> <!--公告栏--> <div v-show="visitorNotice!=''" class="visitorNotice" > <img src='/static/images/laba.svg'/> <span><{visitorNotice}></span> <img v-on:click="visitorNotice=''" src="/static/images/cha.png" class="visitorNoticeClose"/> </div> <!--//公告栏--> <div ref="chatVisitorPage" id="chatVisitorPage" class="chatContext chatVisitorPage" v-on:click="showIconBtns=false;showFaceIcon=false"> <div class="chatBox"> <div class="chatNotice" v-on:click="loadMoreMessages" v-show="showLoadMore"> <a class="chatNoticeContent"><{flyLang.moremessage}></a> </div> <el-row :gutter="2" v-for="v in msgList" v-bind:class="{'chatBoxMe': v.is_kefu==true}"> <div class="messageBox questionBox" v-if="v.type=='question'"> <div class="chatTime left" v-bind:class="{'chatTimeHide': v.show_time==false}"><span><{v.time}></span></div> <div class="left" v-if="v.is_kefu!=true" style="display: flex;"> <el-avatar style="margin-right:10px;flex-shrink: 0;" :size="36" :src="v.avator"></el-avatar> <div class="chatMsgContent"> <div class="chatUser" v-if="showKefuName!='off'"><{v.name}></div> <div class="chatContent chatContent2 replyContentBtn" v-html="v.content"></div> </div> </div> </div> <!--猜你想问--> <div class="cardBox" v-else-if="v.type=='card'"> <div class='visitorReplyTitle'><{flyLang.guess}><a><i class='el-icon-refresh-left'></i> <{flyLang.huanyihuan}></a></div> <div class="cardBoxContent" v-html="v.content"></div> </div> <!--//猜你想问--> <!--消息模板--> <div class="messageBox" v-else> <div class="chatTime left" v-bind:class="{'chatTimeHide': v.show_time==false}"><span><{v.time}></span></div> <div class="left" v-if="v.is_kefu!=true" style="display: flex;"> <el-avatar style="margin-right:10px;flex-shrink: 0;" :size="36" :src="v.avator"></el-avatar> <div class="chatMsgContent"> <div class="chatUser" v-if="showKefuName!='off'"><{v.name}></div> <div class="chatContent chatContent2 replyContentBtn" v-html="v.content"></div> </div> </div> <div class="kefuMe right" v-if="v.is_kefu==true" style="display: flex;justify-content: flex-end;"> <div> <div class="chatContent chatContent2 replyContentBtn" v-html="v.content"></div> <div class="chatReadStatus" v-show="VisitorReadStatus!='true'"><{v.read_status}></div> </div> <el-avatar v-if="VisitorShowAvator=='true'" style="margin-left:10px;flex-shrink: 0;" :size="36" :src="v.avator"></el-avatar> </div> <div class="clear"></div> </div> <!--//消息模板--> </el-row> </div> </div> <div class="chatBoxSend"> <div class="chatBoxSendMask" v-if="reconnectDialog"> <a @click="initConn" href="javascript:void(0);"><{flyLang.socketclose}></a> </div> <div class="hotQuestion" v-if="hotQuestion.length!=0"> <a class="slideInRightItem" v-for="item in hotQuestion" v-on:click="messageContent=item;chatToUser()"> <{item}> </a> </div> <!--进度条--> <div class="progressLine"> <el-progress :stroke-width="6" :percentage="percentage" v-show="percentage!=0" :text-inside="true"></el-progress> </div> <!--//进度条--> <div class="iconBtns visitorIconBox"> <el-tooltip :content="flyLang.emotions" placement="top"> <div class="icono-smile visitorIconBtns visitorFaceBtn" v-on:click="showFaceIcon==true?showFaceIcon=false:showFaceIcon=true"></div> </el-tooltip> <el-tooltip :content="flyLang.photo" placement="top"> <div v-show="VisitorUploadImgBtn!='true'" :title="flyLang.photo" class="el-icon-picture" id="uploadImg" v-on:click="uploadImg('/uploadimg')" style="font-size: 22px;"></div> </el-tooltip> <el-tooltip :content="flyLang.file" placement="top"> <div v-show="VisitorUploadFileBtn!='true'" :title="flyLang.file" class="el-icon-upload" id="uploadFile" v-on:click="uploadFile('/2/uploadFile')" style="font-size: 22px;"></div> </el-tooltip> <el-tooltip :content="flyLang.recoder" placement="top"> <div v-show="VisitorVoiceBtn!='true'" :title="flyLang.recoder" class="el-icon-microphone" v-on:click="audioDialog=true" style="font-size: 22px;"></div> </el-tooltip> <el-tooltip :content="flyLang.map" placement="top"> <div v-show="VisitorMapBtn!='true'" style="font-size: 22px;" class="el-icon-location" v-on:click="qqMap==true?qqMap=false:qqMap=true;"></div> </el-tooltip> <el-tooltip :content="flyLang.audio" placement="top"> <div class="el-icon-phone-outline" @click="callPhone()" style="font-size: 20px;"> </div> </el-tooltip> <el-tooltip :content="flyLang.video" placement="top"> <div class="el-icon-video-camera" @click="callPeer()" style="font-size: 22px;"> </div> </el-tooltip> <el-tooltip :content="flyLang.language" placement="top"> <div @click="flagsDialog='true'"> <img src="/static/images/lang.png" style="width: 20px;"/> </div> </el-tooltip> </div> <div class="faceBox visitorFaceBox" v-if="showFaceIcon"> <ul class="faceBoxList"> <li v-on:click="faceIconClick(i)" class="faceIcon" v-for="(v,i) in face" :title="v.name"><img :src=v.path></li> </ul> <div class="clear"></div> </div> <!--搜索建议--> <div class="searchList" v-show="searchList.length!=0"> <div v-on:click="messageContent=item.title;chatToUser();searchList=[]" class="searchItem" v-for="item in searchList" v-html="item.htmlTitle"></div> </div> <!--//搜索建议--> <div class="visitorEditor"> {{/* <div v-if="VisitorVoiceBtn!='true'" v-on:click="audioDialog==true?audioDialog=false:audioDialog=true" class="visitorEditorVoice visitorFaceBtn"></div>*/}} <el-input :placeholder="flyLang.textarea" show-word-limit :maxlength="VisitorMaxLength" :rows="2" type="textarea" resize="none" class="visitorEditorArea" @focus="scrollBottom;showIconBtns=false" @blur="scrollBottom;showIconBtns=false" v-model="messageContent" @keyup.native="inputNextText" v-on:keyup.enter.native="chatToUser"> </el-input> {{/* <div v-if="VisitorFaceBtn!='true'" :title="flyLang.emotions" v-on:click="showIconBtns==true?showIconBtns=false:showIconBtns=true" class="visitorEditorSmile visitorFaceBtn"></div>*/}} {{/* <div v-if="VisitorUploadImgBtn!='true'&&VisitorPlusBtn=='true'" class="icono-image visitorEditorImg" id="uploadImg" v-on:click="uploadImg('/uploadimg')"></div>*/}} {{/* <div v-if="VisitorPlusBtn!='true'" v-on:click="showIconBtns==true?showIconBtns=false:showIconBtns=true" v-show="messageContent==''" :title="flyLang.emotions" class="visitorEditorChoose"></div>*/}} </div> <el-button type="primary" size="mini" class="visitorEditorBtn" :disabled="sendDisabled||messageContent==''" v-on:click="chatToUser();showIconBtns=false"><{flyLang.sent}></el-button> <div class="footContact clear"> <a href="{{.CopyrightUrl}}" target="_blank">{{.CopyrightTxt}}</a> </div> </div> </div> <div class="chatArticle"> <div style="padding: 8px;"><img style="width: 100%" :src="entInfo.intro_pic" v-if="entInfo.intro_pic" :title="entInfo.username"/></div> <h3 class="hotQuestionTitle"> <img src="/static/images/fire.svg" class="fire"/><{flyLang.hotQuestionTitle}> </h3> <ul> <li v-on:click="messageContent=item;chatToUser()" class="chatArticleItem" v-for="item in topQuestionList"><a><{item}></a></li> </ul> </div> <div class="clear"></div> <!--//客服代码--> <audio id="chatMessageAudio"> <source id="chatMessageAudioSource" /> </audio> <audio id="chatMessageSendAudio"> <source id="chatMessageSendAudioSource" /> </audio> <!--图片预览--> <el-image style="display: none;" ref="preview" class="hideImgDiv" :src="imgPreviewSrc[0]" :preview-src-list="imgPreviewSrc" z-index="9999" ></el-image> <!--评价--> <el-dialog center :title="flyLang.visitorCommentTitle" :close-on-click-modal="false" width="90%" :visible.sync="comment" > <div class="commentBox"> <div style="line-height: 25px;"><{flyLang.commentDesc}></div> <el-rate v-model="commentScore" style="margin-bottom: 30px;"></el-rate> <el-input type="textarea" :rows="4" v-model="commentContent"> </el-input> {{/* <el-tooltip content="good" placement="top">*/}} {{/* <span class="icono-smile" v-on:click="sendComment('good');comment = false"></span>*/}} {{/* </el-tooltip>*/}} {{/* <el-tooltip content="normal" placement="top">*/}} {{/* <span class="icono-meh" v-on:click="sendComment('normal');comment = false"></span>*/}} {{/* </el-tooltip>*/}} {{/* <el-tooltip content="bad" placement="top">*/}} {{/* <span class="icono-frown" v-on:click="sendComment('bad');comment = false"></span>*/}} {{/* </el-tooltip>*/}} </div> <span slot="footer" class="dialog-footer"> <el-button type="primary" v-on:click="sendComment();comment = false"><{flyLang.sent}></el-button> </span> </el-dialog> <!--//评价--> <!--地图--> <iframe v-if="qqMap" style="position: fixed;top: 0;left: 0;z-index: 999999999" id="mapPage" width="100%" height="100%" frameborder=0 src="https://apis.map.qq.com/tools/locpicker?search=1&type=1&key= OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77&referer=kefu"> </iframe> <!--//地图--> <el-dialog :title="flyLang.leave" :visible.sync="allOffline" width="100%" top="0"> <el-input style="margin-bottom: 10px;" :placeholder="flyLang.email" v-model="visitorContact.email"></el-input> <el-input style="margin-bottom: 10px;" :placeholder="flyLang.wechat" v-model="visitorContact.weixin"></el-input> <el-input style="margin-bottom: 10px;" :placeholder="flyLang.realname" v-model="visitorContact.name"></el-input> <el-input :placeholder="flyLang.content" type="textarea" v-model="visitorContact.msg"></el-input> <span slot="footer" class="dialog-footer"> <el-button @click="sendEmailMsg"><{flyLang.sent}></el-button> <el-button @click="allOffline = false"><{flyLang.cancel}></el-button> </span> </el-dialog> <!--录音--> <el-dialog :visible.sync="audioDialog" width="100%" > <div class="dialogRecoder"> <el-progress :color="colors" type="dashboard" :format="recoderFormat" :stroke-width="10" :percentage="recoderSecond"></el-progress> <br/> <audio v-show="recorderEnd" controls ref="audio" muted="muted" src="" id="audio"></audio> <br/> <el-button id="start" @click="startRecoder($event)" size="small" type="primary"><{flyLang.start}></el-button> <el-button @click="stopRecoder($event)" size="small" type="warning"><{flyLang.stop}></el-button> <el-button @click="cancelRecoder()" size="small" type="danger"><{flyLang.cancel}></el-button> <el-button @click="sendRecoder()" size="small" type="success"><{flyLang.sent}></el-button> </div> </el-dialog> <!--//录音--> <!--切换语言--> <el-dialog :visible.sync="flagsDialog" width="90%" top="30px"> <el-button @click="selectLang('cn')" class="flagBtn" type="primary" plain>中文简体</el-button> <el-button @click="selectLang('tw')" class="flagBtn" type="primary" plain>中文繁体</el-button> <el-button @click="selectLang('en')" class="flagBtn" type="primary" plain>English</el-button> </el-dialog> <!--//切换语言--> <!--录音--> <!--视频--> <el-dialog center :close-on-click-modal="false" :visible="isCalling" width="90%" :show-close="false" @opened="initCallingDialog" > <video id="chatRtc" style="width: 100%;" controls autoplay></video> <video v-if="isVideo==true" id="chatLocalRtc" style="width: 100%;" controls muted></video> <span slot="footer" class="dialog-footer"> <el-button @click="callClose()" type="danger" size="mini">挂断</el-button> </span> </el-dialog> <!--//录音--> </template> </div> </body> <script src="/static/js/xss.js"></script> <script src="/static/js/reconnecting-websocket.min.js"></script> <script src="/static/js/recoder.js"></script> <script src="/static/js/audio.js"></script> <script> var KEFU_ID='{{.KEFU_ID}}'; var REFER=urlDecode('{{.Refer}}'); var REFER_URL=urlDecode('{{.ReferUrl}}'); var ENT_ID='{{.ENT_ID}}'; var IS_TRY='{{.IS_TRY}}'; var VISITOR_ID='{{.visitorId}}'; var VISITOR_NAME='{{.visitorName}}'; var ERR_MSG='{{.errMsg}}'; var AVATOR='{{.avator}}'; var LANG=checkLang(); var SHOW_KEFU_NAME='{{.ShowKefuName}}'; var EXTRA='{{.Extra}}'; </script> <script src="/static/js/chat-lang.js?v=dsdsdgf45hkjk"></script> <script src="/static/js/chat-config.js?v=0.5.1"></script> <script src="/static/js/peer.js"></script> <script> new Vue({ el: '#app', delimiters:["<{","}>"], data: { window:window, server:getWsBaseUrl()+"/ws_visitor", socket:null, msgList:[], imgPreviewSrc:[ ""], msgListNum:[], messageContent:"", chatTitle:KEFU_LANG[LANG]['connecting'], visitor:{}, face:emojiGifsMap(), showKfonline:false, socketClosed:false, focusSendConn:false, wsSocketClosed:true, timer:null, loadingTimer:null, sendDisabled:false, entLogo:"", entName:"", peer:null, peerjsId:"", kefuPeerId:"", loading:null, localStream:null, flyLang:KEFU_LANG[LANG], lang:LANG, textareaFocused:false, replys:[], noticeName:"", noticeAvatar:"", allOffline:false, visitorContact:{ email:"", weixin:"", name:"", msg:"", }, haveUnreadMessage:false, audioDialog:false, flagsDialog:false, recorder:null, recorderAudio:null, recordeTimer:null, recoderSecond:0, currentActiveTime:Date.now(), timeoutTimer:null, timeoutLongTime:20*60*1000,//20分钟没反应 allTimeouter:[], currentPage:1, showLoadMore:false, loadMoreDisable:false, websocketOpenNum:0,//websocket打开次数 websocketMaxOpenNum:10,//websocket最大打开次数 talkBtnText:"按住 说话", recorderEnd:false, isMobile:false, isIframe:false, onlineType:"success", reconnectDialog:false, showIconBtns:false, showFaceIcon:false, showKefuName:SHOW_KEFU_NAME, comment:false, qqMap:false, hotQuestion:[], topQuestionList:[], topQuestionCount:0, topQuestionPage:1, topQuestionPagesize:5, entIntroduce:"", robotSwitch:"", robotNoAnswer:"", visitorNotice:"", autoWelcome:"", searchList:[], VisitorVoiceBtn:'{{.VisitorVoiceBtn}}', VisitorMapBtn:'{{.VisitorMapBtn}}', VisitorCommentBtn:'{{.VisitorCommentBtn}}', VisitorFaceBtn:'{{.VisitorFaceBtn}}', VisitorReadStatus:'{{.VisitorReadStatus}}', VisitorPlusBtn:'{{.VisitorPlusBtn}}', VisitorUploadImgBtn:'{{.VisitorUploadImgBtn}}', VisitorUploadFileBtn:'{{.VisitorUploadFileBtn}}', VisitorMaxLength:'{{.VisitorMaxLength}}'==''?100:parseInt('{{.VisitorMaxLength}}'), VisitorShowAvator:'{{.VisitorShowAvator}}', VisitorWechatQrcodeUrl:'{{.VisitorWechatQrcodeUrl}}', percentage:0, visitorMaxNumLimit:false,//客服达到接待上限 visitorMaxNumNotice:"",//客服达到接待上限文案 visitorCookie:"", scanWechatQrcode:"", entConfig:{}, colors: [ {color: '#f56c6c', percentage: 20}, {color: '#e6a23c', percentage: 40}, {color: '#5cb87a', percentage: 60}, {color: '#1989fa', percentage: 80}, {color: '#6f7ad3', percentage: 100} ], commentScore:0, commentContent:"", isCalling:false, call:null, videoElement:null, canvasElement:null, isVideo:false, entInfo:{}, }, methods: { //初始化websocket initConn:function() { this.socket = new ReconnectingWebSocket(this.server+"?visitor_id="+this.visitor.visitor_id+"&to_id="+this.visitor.to_id);//创建Socket实例 this.socket.debug = true; this.socket.onmessage = this.OnMessage; this.socket.onopen = this.OnOpen; this.socket.onerror = this.OnError; this.socket.onclose = this.OnClose; this.ping(); }, OnOpen:function() { console.log("ws:onopen"); //限制最大打开次数 if(this.websocketOpenNum>=this.websocketMaxOpenNum){ this.chatTitle=KEFU_LANG[LANG]['refresh']; this.socket.close(); return; } this.websocketOpenNum++; this.chatTitle=this.noticeName; this.checkTimeout(); this.socketClosed=false; this.focusSendConn=false; this.wsSocketClosed=false; this.sendVisitorLogin(); this.getExtendInfo(); this.reconnectDialog=false; this.showTitle(KEFU_LANG[LANG]['connectok']); this.getNotice(); }, OnMessage:function(e) { console.log("ws:onmessage"); this.socketClosed=false; this.focusSendConn=false; const redata = JSON.parse(e.data); if (redata.type == "kfOnline") { let msg = redata.data if(this.showKfonline && this.visitor.to_id==msg.id){ return; } this.visitor.to_id=msg.id; this.chatTitle=msg.name+","+KEFU_LANG[LANG]['chating']; $(".chatBox").append("<div class=\"chatTime\">"+this.chatTitle+"</div>"); this.scrollBottom(); this.showKfonline=true; } if (redata.type == "transfer") { var kefuId = redata.data if(!kefuId){ return; } this.visitor.to_id=kefuId; } if (redata.type == "comment") { this.comment=true; } if (redata.type == "wechat_notice") { this.showTitle(KEFU_LANG[LANG]['wechatNotice']); } if (redata.type == "notice") { let msg = redata.data if(!msg){ return; } this.chatTitle=msg $(".chatBox").append("<div class=\"chatTime\">"+this.chatTitle+"</div>"); this.scrollBottom(); } if (redata.type == "accept") { var _this=this; let msg = redata.data; if(!msg||_this.localStream==null){ return; } // this.$confirm('请求与您通话?', '提示', { // confirmButtonText: '确定', // cancelButtonText: '取消', // type: 'warning' // }).then(() => { _this.kefuPeerId=msg; _this.isCalling=true; // }).catch(() => { // }); } if (redata.type == "callPhone") { this.callPhone(); } if (redata.type == "callVideo") { this.callPeer(); } if (redata.type == "refuse") { this.$message({ message: "已挂断", type: 'error' }); this.callClear(); return; } if (redata.type == "delete") { var msg = redata.data; for(var i=0;i<this.msgList.length;i++){ if(this.msgList[i].msg_id==msg.msg_id){ this.msgList.splice(i,1); break; } } } if (redata.type == "read") { var msg = redata.data; for(var i=0;i<this.msgList.length;i++){ this.msgList[i].read_status=KEFU_LANG[LANG]['read']; } } if (redata.type == "message") { let msg = redata.data //this.visitor.to_id=msg.id; var _this=this; var msgArr=msg.content.split("[br]"); for(var i in msgArr){ let content = {} content.avator = msg.avator; content.name = msg.name; content.content =replaceSpecialTag(msgArr[i]); content.is_kefu = false; content.time = shortTime(msg.time); content.is_reply=true; content.msg_id = msg.msg_id; this.msgList.push(content); setTimeout(function () { _this.scrollBottom(); },200); } // let content = {} // content.avator = msg.avator; // content.name = msg.name; // content.content =replaceSpecialTag(msg.content); // content.is_kefu = false; // content.time = msg.time; // content.msg_id = msg.msg_id; // this.msgList.push(content); notify(msg.name, { body: msg.content, icon: msg.avator },function(notification) { window.focus(); notification.close(); }); //this.scrollBottom(); flashTitle();//标题闪烁 //clearInterval(this.timer); this.cleanAllTimeout(); this.alertSound('/static/images/alert4.mp3');//提示音 this.haveUnreadMessage=true; } if (redata.type == "close") { this.showTitle(KEFU_LANG[LANG]['closemes']); this.scrollBottom(); this.socket.close(); //this.socketClosed=true; this.focusSendConn=true; this.reconnectDialog=true; } if (redata.type == "force_close") { this.showTitle(KEFU_LANG[LANG]['forceclosemes']); this.scrollBottom(); this.socket.close(); this.socketClosed=true; this.reconnectDialog=true; } if (redata.type == "auto_close") { this.showTitle(KEFU_LANG[LANG]['autoclosemes']); this.scrollBottom(); this.socket.close(); this.socketClosed=true; this.reconnectDialog=true; } if (redata.type == "change_id") { var openId = redata.data; setFakeCookie("visitor_"+ENT_ID,openId,this.visitorCookie); location.reload(); } window.parent.postMessage(redata,"*"); }, //发送给客户 chatToUser:function() { this.searchList=[]; if(this.sendDisabled){ return; } var messageContent=this.messageContent.trim("\r\n"); messageContent=messageContent.replace("\n",""); messageContent=messageContent.replace("\r\n",""); messageContent=filterXSS(messageContent); if(messageContent==""||messageContent=="\r\n"){ this.messageContent=""; return; } this.messageContent=""; this.currentActiveTime=Date.now(); if(this.socketClosed){ this.initConn(); // this.$message({ // message: '连接关闭!请重新打开页面', // type: 'warning' // }); //return; } this.sendDisabled=true; let _this=this; let content = {} content.avator=_this.visitor.avator; content.content = replaceSpecialTag(messageContent); content.name = _this.visitor.name; content.is_kefu = true; content.time = _this.getNowDate(); content.show_time=false; let mes = {}; mes.type = "visitor"; mes.content = messageContent; mes.from_id = this.visitor.visitor_id; mes.to_id = this.visitor.to_id; //机器人回答 if(this.robotSwitch=="true"){ this.sendDisabled=false; this.messageContent=""; //转接人工,没用处于排队状态 if(this.turnToMan && this.turnToMan.includes(mes.content)){ if(this.visitorMaxNumLimit){ this.showTitle(this.visitorMaxNumNotice); return; } this.initConn(); this.robotSwitch=""; }else{ content.read_status = KEFU_LANG[LANG]['read']; _this.msgList.push(content); _this.scrollBottom(); _this.sendAjax("/2/robotMessage","post",{ent_id:ENT_ID,content:messageContent},function(msg){ if(msg.content==""){ msg.content=_this.robotNoAnswer; } if(msg.content==""){ return; } let content = {} content.avator=msg.avator; content.content = replaceSpecialTag(msg.content); content.name = msg.username; content.is_kefu = false; content.read_status = KEFU_LANG[LANG]['read']; content.time = _this.getNowDate(); content.show_time=false; _this.msgList.push(content); _this.scrollBottom(); }); return; } } content.read_status = KEFU_LANG[LANG]['unread']; _this.msgList.push(content); _this.scrollBottom(); //发送人工消息 $.post("/2/message?lang="+getQuery("lang"),mes,function(res){ _this.sendDisabled=false; if(res.code!=200){ _this.$message({ message: res.msg, type: 'error' }); if(res.code==401){ setTimeout(function(){ window.location.reload(); },2000); } return; } var result=res.result if(result.isBlack){ _this.msgList.pop(); content.content=result.content; _this.msgList.push(content); } _this.messageContent = ""; _this.cleanAllTimeout(); _this.sendSound(); _this.sendDisabled=false; }); }, //正在输入 inputNextText:function(){ var _this=this; this.sendInputingStrNow(this.messageContent); //是否进行搜索 if(this.robotSwitch!="true"){ return; } this.sendAjax("/2/searchQuestion","get",{ent_id:ENT_ID,content:this.messageContent},function(res){ result=res.result; if(!result){ return; } for(key in result){ var str=result[key].title; str= str.replace(_this.messageContent,"<span>"+_this.messageContent+"</span>"); result[key].htmlTitle=str; } _this.searchList=result; }); }, sendInputingStrNow:function(str){ if(this.socketClosed||!this.socket||this.wsSocketClosed){ return; } var message = {} message.type = "inputing"; message.data = { from : this.visitor.visitor_id, to : this.visitor.to_id, content:str }; this.socket.send(JSON.stringify(message)); }, sendVisitorLogin:function(){ var _this=this; setTimeout(function(){ if(_this.socketClosed||!_this.socket||_this.wsSocketClosed){ return; } var message = {} message.type = "visitor_login"; message.data = { from : _this.visitor.visitor_id, to : _this.visitor.to_id, }; _this.socket.send(JSON.stringify(message)); }, 3000); }, OnClose:function(event) { console.log("ws:onclose",event); this.focusSendConn=true; this.wsSocketClosed=true; this.closeTimeoutTimer(); }, OnError:function(event) { console.log("ws:onerror",event); this.closeTimeoutTimer(); }, //获取当前用户信息 getUserInfo:function(){ var _this=this; var visitor_id=getFakeCookie("visitor_"+ENT_ID); var to_id=KEFU_ID; var extra=EXTRA; var url=getQuery("url"); var paramVisitorId=VISITOR_ID; if(paramVisitorId!=""){ visitor_id=paramVisitorId; } var visitorName=VISITOR_NAME; var avator=AVATOR; if(extra==""){ var ext={}; var refer=document.referrer?document.referrer:"-"; ext.refer=refer; ext.host=document.location.href; extra=utf8ToB64(JSON.stringify(ext)); }else{ try{ var jsonStr=b64ToUtf8(extra) var extJson=JSON.parse(jsonStr) if(extJson.refer){ if(REFER=="") REFER=extJson.refer; if(REFER_URL=="") REFER_URL=extJson.refer; } }catch (e) {} } if(REFER_URL==""){ REFER_URL=document.referrer; } if(REFER==""){ REFER=document.title; } //发送消息 $.ajax({ type: "post", url: "/visitor_login", data:{visitor_id:visitor_id, visitor_name:visitorName, avator:avator, refer:REFER, to_id:to_id, extra:extra, ent_id:ENT_ID, url:document.location.href, refer_url:REFER_URL, title:document.title }, error:function(res){ var data=JSON.parse(res.responseText); _this.$message({ message: data.msg, type: 'error' }); }, success: function(res) { if(res.code==40012){ _this.$message({ message: res.msg, type: 'error' }); _this.chatTitle=res.msg; _this.sendDisabled=true; return; } if(res.code==40016){ _this.$message({ message: KEFU_LANG[LANG]['freqLimit'], type: 'error' }); _this.chatTitle=KEFU_LANG[LANG]['freqLimit']; _this.sendDisabled=true; return; } _this.entInfo=res.kefu; _this.noticeName=res.kefu.username; _this.noticeAvatar=res.kefu.avatar; _this.robotNoAnswer=res.robotNoAnswer; _this.getTopQuestion(); //判断同时接待访客数 if(res.code==40018){ _this.visitorMaxNumLimit=true; _this.visitor=res.result; _this.robotSwitch="true"; _this.chatTitle=_this.noticeName; _this.turnToMan=res.turnToMan.split(","); var visitorMaxNumNotice=res.visitorMaxNumNotice; if(visitorMaxNumNotice==""){ visitorMaxNumNotice="当前有"+res.visitorMaxNum+"位访客正在咨询,请稍等一会再尝试 <a href='javascript:window.location.reload();'>刷新</a>"; } _this.visitorMaxNumNotice=visitorMaxNumNotice; _this.showTitle(visitorMaxNumNotice); _this.sendDisabled=true; return; } if(res.code!=200){ _this.$message({ message: res.msg, type: 'error' }); _this.chatTitle=res.msg; _this.sendDisabled=true; return; } if(res.alloffline){ _this.onlineType="danger"; }else{ _this.onlineType="success"; } if(KEFU_CONFIG.SHOW_OFFLINE_PAGE){ _this.allOffline=res.alloffline; } _this.sendDisabled=false; _this.visitor=res.result; _this.noticeName=res.kefu.username; _this.noticeAvatar=res.kefu.avatar; _this.entIntroduce=res.entIntroduce; _this.robotSwitch=res.robotSwitch; _this.turnToMan=res.turnToMan.split(","); _this.chatTitle=_this.noticeName; _this.visitorNotice=res.visitorNotice; _this.autoWelcome=res.autoWelcome; _this.visitorCookie=res.visitorCookie; _this.scanWechatQrcode=res.scanWechatQrcode; document.title=res.kefu.username; if(!getFakeCookie("visitor_"+ENT_ID)){ setFakeCookie("visitor_"+ENT_ID,_this.visitor.visitor_id,res.visitorCookie); } _this.loadMoreMessages(); _this.showWechatTip(); if(_this.robotSwitch!="true"){ _this.initConn(); } } }); }, //获取信息列表 getMesssagesByVisitorId:function(isAll){ let _this=this; $.ajax({ type:"get", url:"/2/messages?visitor_id="+this.visitor.visitor_id, success: function(data) { if(data.code==200 && data.result!=null&&data.result.length!=0){ let msgList=data.result; _this.msgList=[]; for(var i=0;i<msgList.length;i++){ let visitorMes=msgList[i]; let content = {} if(visitorMes["mes_type"]=="kefu"){ content.is_kefu = false; }else{ content.is_kefu = true; } content.avator = visitorMes["avator"]; content.name = visitorMes["name"]; content.content = replaceContent(visitorMes["content"]); content.time = visitorMes["time"]; _this.msgList.push(content); _this.scrollBottom(); } } if(data.code!=200){ _this.$message({ message: data.msg, type: 'error' }); _this.chatTitle=KEFU_LANG[LANG]['refresh']; } } }); }, //获取信息列表 sendEmailMsg:function(){ let _this=this; _this.visitorContact.ent_id=ENT_ID; $.ajax({ type:"post", url:"/ent/email_message", data:_this.visitorContact, success: function(data) { if(data.code!=200){ _this.$message({ message: data.msg, type: 'error' }); }else{ _this.allOffline=false; } } }); }, //滚动到底部 scrollBottom:function(){ var _this=this; //$('.chatVisitorPage').animate({scrollTop:'99999999999999'},"slow"); this.$nextTick(function(){ var container = _this.$el.querySelector(".chatVisitorPage"); container.scrollTop = 999999; //alert(1); //$('.chatVisitorPage').animate({scrollTop:'99999999999999'},'99999999'); // $('.chatVisitorPage').scrollTop(9999999999999999999); }); }, //软键盘问题 textareaFocus:function(){ // if(/Android|webOS|iPhone|iPad|BlackBerry/i.test(navigator.userAgent)) { // //$(".chatContext").css("margin-bottom","0"); // //$(".chatBoxSend").css("position","static"); // this.textareaFocused=true; // } this.scrollBottom(); }, textareaBlur:function(){ // if(this.textareaFocused&&/Android|webOS|iPhone|iPad|BlackBerry/i.test(navigator.userAgent)) { // var chatBoxSendObj=$(".chatBoxSend"); // var chatContextObj=$(".chatContext"); // if(this.textareaFocused&&chatBoxSendObj.css("position")!="fixed"){ // //chatContextObj.css("margin-bottom","105px"); // //chatBoxSendObj.css("position","fixed"); // this.textareaFocused=false; // } // // } this.scrollBottom(); }, sendReply:function(title){ var _this=this; let msg = {} msg.avator=_this.visitor.avator; msg.content = replaceContent(title); msg.name = _this.visitor.name; msg.is_kefu = true; msg.time = _this.getNowDate(); msg.show_time=false; _this.msgList.push(msg); _this.scrollBottom(); var mes = {}; mes.content = title; mes.from_id = this.visitor.visitor_id; mes.ent_id = ENT_ID; _this.sendAjax("/2/message_ask","post",mes,function(msg){ var msgArr=msg.content.split("[b]"); for(var i in msgArr){ let content = {} content.avator = msg.avator; content.name = msg.name; content.content =replaceSpecialTag(msgArr[i]); content.is_kefu = false; content.time = msg.time; content.is_reply=true; _this.msgList.push(content); _this.scrollBottom(); } _this.cleanAllTimeout(); _this.alertSound('/static/images/notification.mp3');//提示音 }); //this.chatToUser(); }, //获取日期 getNowDate : function() {// 获取日期 var d = new Date(new Date()); return d.getFullYear() + '-' + this.digit(d.getMonth() + 1) + '-' + this.digit(d.getDate()) + ' ' + this.digit(d.getHours()) + ':' + this.digit(d.getMinutes()) + ':' + this.digit(d.getSeconds()); }, //补齐数位 digit : function (num) { return num < 10 ? '0' + (num | 0) : num; }, setCache : function (key,obj){ if(navigator.cookieEnabled&&typeof window.localStorage !== 'undefined'){ localStorage.setItem(key, JSON.stringify(obj)); } },getCache : function (key){ if(navigator.cookieEnabled&&typeof window.localStorage !== 'undefined') { return JSON.parse(localStorage.getItem(key)); } }, setNoticeWelcome(list){ var _this=this; var msgs = list; var delaySecond=0; for(let i in msgs){ var msg=msgs[i]; if(msg.delay_second){ delaySecond+=msg.delay_second; }else{ delaySecond+=4; } var timer = setTimeout(function (msg) { msg.time=shortTime(getNowDate()); msg.content = replaceSpecialTag(msg.content); msg.name=_this.entConfig.robotName; _this.msgList.push(msg); _this.scrollBottom(); _this.alertSound('/static/images/notification.mp3'); var redata={ type:"message", data:msg } window.parent.postMessage(redata,"*"); },1000*delaySecond,msg); _this.allTimeouter.push(timer); } }, //获取自动欢迎语句 getNotice : function (){ var _this=this; var oldNotice=getFakeCookie("noticed_"+ENT_ID); if(oldNotice){ if(_this.autoWelcome=="on"){ return; } $.get("/2/notices?visitor_id="+this.visitor.visitor_id+"&ent_id="+ENT_ID+"&kefu_name="+this.visitor.to_id,function(res) { _this.entConfig=res.result.ent_config; if (res.result.welcome != null) { setFakeCookie("noticed_"+ENT_ID,res.result.welcome,7*3600*24); _this.setNoticeWelcome(res.result.welcome); } }); return; } $.get("/2/notices?is_record=1&visitor_id="+this.visitor.visitor_id+"&ent_id="+ENT_ID+"&kefu_name="+this.visitor.to_id,function(res) { _this.entConfig=res.result.ent_config; if (res.result.welcome != null) { setFakeCookie("noticed_"+ENT_ID,res.result.welcome,7*3600*24); _this.setNoticeWelcome(res.result.welcome); } }); }, initCss:function(){ var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?82938760e00806c6c57adee91f39aa5e"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); var _this=this; $(function () { //手机端的样式问题 // if(_this.isMobile){ // $(".chatVisitorPage").css("height","calc(100% - 155px)"); // } //展示表情 // var faces=placeFace(); // $.each(faceTitles, function (index, item) { // _this.face.push({"name":item,"path":faces[item]}); // }); // $(".visitorFaceBtn").click(function(e){ // var status=$('.faceBox').css("display"); // if(status=="block"){ // $('.faceBox').hide(); // }else{ // $('.faceBox').show(); // } // return false; // }); $("body").on("click",".replyContentBtn a",function() { var txt=$(this).text(); var href=$(this).attr("href"); if(href=="self"||!href){ _this.messageContent=txt; _this.chatToUser(); return false; } }); //var windheight = $(window).height(); $(window).resize(function(){ //var docheight = $(window).height(); /*唤起键盘时当前窗口高度*/ //_this.scrollBottom(); _this.visitorPageHeight(); // if(docheight < windheight){ /*当唤起键盘高度小于未唤起键盘高度时执行*/ // $(".chatBoxSend").css("position","static"); // }else{ // $(".chatBoxSend").css("position","fixed"); // } //_this.visitorPageHeight(); //document.getElementById('chatVisitorPage').style.height = (document.documentElement.clientHeight - 71) + 'px'; }); if(isMobile()){ _this.visitorPageHeight(); //document.getElementById('chatVisitorPage').style.height = (document.documentElement.clientHeight - 71) + 'px'; } //自动问答 $("body").on("click",".visitorReplyContent",function() { var txt=$(this).find("span").text(); _this.messageContent=txt; _this.chatToUser(); }); //自动问答换一换 $("body").on("click",".visitorReplyTitle a",function() { ++_this.topQuestionPage; if(_this.topQuestionPage>_this.topQuestionCount){ _this.topQuestionPage=1; } var result=pagination(_this.topQuestionPage,_this.topQuestionPagesize,_this.topQuestionList); _this.makeReplyItem(result,true); }); }); }, //心跳 ping:function(){ let _this=this; let mes = {} mes.type = "ping"; mes.data = "visitor:"+_this.visitor.visitor_id; setInterval(function () { if(_this.socket!=null&&!_this.wsSocketClosed){ _this.socket.send(JSON.stringify(mes)); } },10000); }, //初始化 init:function(){ var _this=this; _this.isMobile=isMobile(); this.initCss(); //已读消息 var ms= 1000*2; var lastClick = Date.now() - ms; $("body").mouseover(function(){ if(!_this.haveUnreadMessage){ return; } if (Date.now() - lastClick >= ms) { lastClick = Date.now(); //如果有未读消息,调用已读接口 _this.sendAjax("/2/messages_read","post",{"visitor_id":_this.visitor.visitor_id,"kefu":_this.visitor.to_id},function(data){ _this.haveUnreadMessage=false; }); } }); $('body').click(function(){ clearFlashTitle(); window.parent.postMessage({type:"focus"},"*"); //$('.faceBox').hide(); //剪贴板 try{ var selecter = window.getSelection().toString(); if (selecter != null && selecter.trim() != ""){ var str=selecter.trim(); _this.sendInputingStrNow(str); } } catch (err){ var selecter = document.selection.createRange(); var s = selecter.text; if (s != null && s.trim() != ""){ var str=s.trim(); _this.sendInputingStrNow(str); } } }); $("body").on("click",".chatImagePic",function() { var url=$(this).attr("data-src"); _this.imgPreviewSrc=[url]; _this.$refs.preview.clickHandler(); //new PinchZoom.default($(this)[0], {}); // _this.$alert("<img src='"+url+"'/>", "", { // dangerouslyUseHTMLString: true // }); return false; }); window.onfocus = function () { //_this.focusHandle(); } //判断当前是否在iframe中 if(self!=top){ _this.isIframe=true; } }, //表情点击事件 faceIconClick:function(index){ this.showFaceIcon=false; this.messageContent+="face"+this.face[index].name; }, //上传图片 uploadImg:function (url){ let _this=this; $('#uploadImg').after('<input type="file" accept="image/gif,image/jpeg,image/jpg,image/png" id="uploadImgFile" name="file" style="display:none" >'); $("#uploadImgFile").click(); $("#uploadImgFile").change(function (e) { var formData = new FormData(); var file = $("#uploadImgFile")[0].files[0]; formData.append("imgfile",file); //传给后台的file的key值是可以自己定义的 filter(file) && $.ajax({ url: url || '', type: "post", data: formData, contentType: false, processData: false, dataType: 'JSON', mimeType: "multipart/form-data", //添加自定义属性,监听上下文的进度 xhr: function() { //创建原生的ajax请求对象 var xhr = $.ajaxSettings.xhr(); //监听进度的一个事件 xhr.upload.onprogress = function(e) { console.log(e.total); //文件大小 console.log(e.loaded); //上传多少 var w = parseInt((e.loaded / e.total) * 100) console.log(w); _this.percentage=w; if(w>=100){ _this.percentage=0; } } return xhr }, success: function (res) { if(res.code!=200){ _this.$message({ message: res.msg, type: 'error' }); }else{ _this.$message({ message: KEFU_LANG[LANG]['uploadSuccess'], type: 'success' }); _this.messageContent+='img[' + res.result.path + ']'; _this.chatToUser(); setTimeout(function () { _this.scrollBottom(); },2000); } }, error: function (data) { console.log(data); _this.$message({ message: KEFU_LANG[LANG]['uploadFailed']+data.responseText, type: 'error' }); } }); }); }, //上传文件 uploadFile:function (url){ let _this=this; $('#uploadFile').after('<input type="file" id="uploadRealFile" name="file2" style="display:none" >'); $("#uploadRealFile").click(); $("#uploadRealFile").change(function (e) { var formData = new FormData(); var file = $("#uploadRealFile")[0].files[0]; formData.append("realfile",file); //传给后台的file的key值是可以自己定义的 console.log(formData); $.ajax({ url: url || '', type: "post", data: formData, contentType: false, processData: false, dataType: 'JSON', mimeType: "multipart/form-data", //添加自定义属性,监听上下文的进度 xhr: function() { //创建原生的ajax请求对象 var xhr = $.ajaxSettings.xhr(); //监听进度的一个事件 xhr.upload.onprogress = function(e) { console.log(e.total); //文件大小 console.log(e.loaded); //上传多少 var w = parseInt((e.loaded / e.total) * 100) console.log(w); _this.percentage=w; if(w>=100){ _this.percentage=0; } } return xhr }, success: function (res) { if(res.code!=200){ _this.$message({ message: res.msg, type: 'error' }); }else{ _this.$message({ message: KEFU_LANG[LANG]['uploadSuccess'], type: 'success' }); //_this.messageContent+='file[' + res.result.path + ']'; var data=JSON.stringify({ name:res.result.name, ext:res.result.ext, size:res.result.size, path:res.result.path, }) _this.messageContent+='mutiFile[' + data+ ']'; _this.chatToUser(); } }, error: function (data) { console.log(data); _this.$message({ message: KEFU_LANG[LANG]['uploadFailed']+data.responseText, type: 'error' }); } }); }); }, //粘贴上传图片 onPasteUpload:function(event){ let items = event.clipboardData && event.clipboardData.items; let file = null if (items && items.length) { // 检索剪切板items for (var i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { file = items[i].getAsFile() } } } if (!file) { return; } let _this=this; var formData = new FormData(); formData.append('imgfile', file); $.ajax({ url: '/uploadimg', type: "post", data: formData, contentType: false, processData: false, dataType: 'JSON', mimeType: "multipart/form-data", //添加自定义属性,监听上下文的进度 xhr: function() { //创建原生的ajax请求对象 var xhr = $.ajaxSettings.xhr(); //监听进度的一个事件 xhr.upload.onprogress = function(e) { console.log(e.total); //文件大小 console.log(e.loaded); //上传多少 var w = parseInt((e.loaded / e.total) * 100) console.log(w); _this.percentage=w; if(w>=100){ _this.percentage=0; } } return xhr }, success: function (res) { if(res.code!=200){ _this.$message({ message: res.msg, type: 'error' }); }else{ _this.$message({ message: KEFU_LANG[LANG]['uploadSuccess'], type: 'success' }); _this.messageContent+='img[' + res.result.path + ']'; _this.chatToUser(); setTimeout(function () { _this.scrollBottom(); },2000); } }, error: function (data) { console.log(data); _this.$message({ message: KEFU_LANG[LANG]['uploadFailed']+data.responseText, type: 'error' }); } }); }, //自动 getTopQuestion:function(){ var _this=this; $.get("/other/getTopQuestion?ent_id="+ENT_ID,function(res) { if(res.code!=200||!res.result){ return; } var hotQuestion=res.result.hotQuestion; if(hotQuestion!=""){ _this.hotQuestion=hotQuestion.split(","); } var questionList=res.result.questionList; if(questionList.length==0){ return; } _this.topQuestionList=questionList; _this.topQuestionCount=sumPage(_this.topQuestionPagesize,questionList); var result=pagination(1,_this.topQuestionPagesize,questionList); _this.makeReplyItem(result); }); }, makeReplyItem:function(result,isPage){ var _this=this; var msg={}; msg.type="card"; msg.avator = _this.noticeAvatar; msg.name = _this.noticeName; msg.show_time = true; msg.time = _this.getNowDate(); msg.content=""; var i=1; for(key in result){ msg.content+="<div class='visitorReplyContent'>"+i+". <span>"+result[key]+"</span></div>"; i++; } if(!isPage){ _this.msgList.push(msg); _this.scrollBottom(); }else{ $(".cardBoxContent").html(msg.content); } }, //自动 getAutoReply:function(){ var _this=this; $.get("/autoreply?ent_id="+ENT_ID,function(res) { if(res.code!=200 || res.result.length==0){ return; } var result=res.result; _this.replys.push(result); }); }, //提示音 alertSound:function(soundUrl){ var b = document.getElementById("chatMessageAudio"); if (b.canPlayType('audio/ogg; codecs="vorbis"')) { b.type= 'audio/mpeg'; b.src=soundUrl ; var p = b.play(); p && p.then(function () { }).catch(function (e) { }); } }, sendSound:function(){ var b = document.getElementById("chatMessageSendAudio"); if (b.canPlayType('audio/ogg; codecs="vorbis"')) { b.type= 'audio/mpeg'; b.src= '/static/images/sent.ogg'; var p = b.play(); p && p.then(function(){}).catch(function(e){}); } }, initPeerjs:function(){ var peer = new Peer(); this.peer=peer; var _this=this; peer.on('open', function(id) { console.log('My peer ID is: ' + id); _this.peerjsId=id; }); peer.on('close', function() { console.log('My peer close'); if(_this.loading!=null){ _this.loading.close(); } }); peer.on('disconnected', function() { console.log('My peer disconnected'); if(_this.loading!=null){ _this.loading.close(); } }); peer.on('error', function() { console.log('My peer error'); if(_this.loading!=null){ _this.loading.close(); } }); }, //打电话 callPhone:function(){ var _this=this; var media=(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); if(!media){ _this.$message({ type: 'error', message: "not support" }); return ; } var getUserMedia = media.bind(navigator); this.$confirm(this.flyLang.videoAudio, this.flyLang.tips, { confirmButtonText: this.flyLang.audio, cancelButtonText: this.flyLang.cancel, type: 'warning' }).then(() => { _this.messageContent="voice call..."; _this.chatToUser(); _this.loading = _this.$loading({ lock: true, text: _this.flyLang.connecting, spinner: 'el-icon-phone-outline', background: 'rgba(0, 0, 0, 0.7)' }); _this.initPeerjs();//初始化peerjs _this.loadingTimerTimeoutClose(); _this.isVideo=false; getUserMedia({video:false, audio: { noiseSuppression: true, echoCancellation: true, }}, function(stream) { _this.localStream=stream; _this.sendAjax("/2/callKefu","post",{"action":"callpeer",kefu_id:_this.visitor.to_id,visitor_id:_this.visitor.visitor_id},function(result){ }); }, function(err) { _this.$message({ type: 'error', message: err }); if(_this.loading) _this.loading.close(); }); }).catch(() => { }); }, callPeer:function(){ var _this=this; var media=(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); if(!media){ _this.$message({ type: 'error', message: "not support" }); return ; } var getUserMedia = media.bind(navigator); this.$confirm(this.flyLang.videoAudio, this.flyLang.tips, { confirmButtonText: this.flyLang.video, cancelButtonText: this.flyLang.cancel, type: 'warning' }).then(() => { _this.messageContent="video call..."; _this.chatToUser(); this.loading = this.$loading({ lock: true, text: _this.flyLang.connecting, spinner: 'el-icon-video-camera', background: 'rgba(0, 0, 0, 0.5)' }); this.loadingTimerTimeoutClose(); this.isVideo=true; //初始化peerjs this.initPeerjs(); getUserMedia({video:true, audio: { noiseSuppression: true, echoCancellation: true, }}, function(stream) { _this.localStream=stream; _this.sendAjax("/2/callKefu","post",{"action":"callpeer",kefu_id:_this.visitor.to_id,visitor_id:_this.visitor.visitor_id},function(result){ }); }, function(err) { _this.$message({ type: 'error', message: err }); if(_this.loading) _this.loading.close(); }); }).catch(() => { }); }, talkPeer:function(){ var _this=this; var canvas = this.canvasElement; if(this.loading!=null){ this.loading.close(); } clearTimeout(this.loadingTimer); this.$message({ message: '正在通话...', type: 'success' }); if(_this.kefuPeerId==""||_this.localStream==null){ return; } //本人摄像头 if(_this.isVideo){ var localVideo=document.querySelector("#chatLocalRtc"); localVideo.srcObject = _this.localStream; localVideo.autoplay=true; } //localVideo.autoplay = true; _this.call = _this.peer.call(_this.kefuPeerId, _this.localStream); _this.call.on('stream', function(remoteStream) { var remoteVideo = document.querySelector("#chatRtc"); remoteVideo.srcObject = remoteStream; remoteVideo.autoplay = true; }); _this.call.on('close', function() { console.log("call close"); _this.loading.close(); _this.callClear(); }); _this.call.on('error', function(err) { console.log(err); _this.callClear(); _this.loading.close(); }); // _this.$alert('正在通话,请保持页面..', '提示', { // confirmButtonText: '挂断', // callback: function(){ // _this.sendAjax("/2/callKefu","post",{"action":"callCancel",kefu_id:_this.visitor.to_id,visitor_id:_this.visitor.visitor_id},function(result){ // }); // if(call!=null){ // call.close(); // } // } // }); // 调用Vudio // var vudio = new Vudio(_this.localStream, canvas, { // accuracy: 256, // width: 800, // height: 100, // waveform: { // fadeSide: false, // maxHeight: 100, // verticalAlign: 'middle', // horizontalAlign: 'center', // color: '#2980b9' // } // }) // // vudio.dance() }, callClose(){ var _this=this; if(this.call==null) return; _this.sendAjax("/2/callKefu","post",{"action":"callCancel",kefu_id:_this.visitor.to_id,visitor_id:_this.visitor.visitor_id},function(result){ }); _this.callClear(); }, getExtendInfo:function(){ var _this=this; var extra=getQuery("extra"); if(extra==""){ return; } try{ var extraString=b64ToUtf8(extra); if(_this.getCache("extra")==extraString){ return; } var extra=JSON.parse(extraString); if (typeof extra=="string"){ extra=JSON.parse(extra); } for(var key in extra){ if(extra[key]==""){ extra[key]="无"; } if(key=="visitorProduct"){ _this.messageContent="product["+JSON.stringify(extra[key])+"]"; _this.chatToUser(); _this.setCache("extra",extraString); }; } }catch (e) { } }, sendAjax:function(url,method,params,callback){ let _this=this; $.ajax({ type: method, url: url, data:params, headers:{ "lang":getQuery("lang"), }, error:function(res){ var data=JSON.parse(res.responseText); console.log(data); if(data.code!=200){ _this.$message({ message: data.msg, type: 'error' }); } }, success: function(data) { if(data.code!=200){ _this.$message({ message: data.msg, type: 'error' }); }else if(data.result!=null){ callback(data.result); }else{ callback(data); } } }); }, showTitle:function(title){ //this.chatTitle=title; $(".chatBox").append("<div class=\"chatNotice\"><div class='chatNoticeContent'>"+title+"</div></div>"); this.scrollBottom(); }, //开始录音 startRecoder:function(e){ if(this.recorder){ this.recorder.destroy(); this.recorder=null; } var _this=this; Recorder.getPermission().then(function() { _this.recorder = new Recorder(); _this.recorderAudio = document.querySelector('#audio'); _this.recorder.start(); _this.recorder.onprogress = function (params) { _this.recoderSecond = parseInt(params.duration); } this.talkBtnText = "松开 结束"; }, function(error){ _this.$message({ message: error, type: 'error' }); return; }); e.preventDefault(); }, stopRecoder:function(e){ if(!this.recorder){ return; } var blob=this.recorder.getWAVBlob(); this.recorderAudio.src = URL.createObjectURL(blob); this.recorderAudio.controls = true; this.talkBtnText="按住 说话"; this.recorderEnd=true; e.preventDefault(); }, sendRecoder:function(){ if(!this.recorder){ return; } var blob=this.recorder.getWAVBlob(); var formdata = new FormData(); // form 表单 {key:value} formdata.append("realfile", blob); // form input type="file" var _this=this; this.loading = this.$loading({ lock: true, text: '正在发送', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); $.ajax({ url: "/2/uploadAudio", type: 'post', processData: false, contentType: false, data: formdata, dataType: 'JSON', mimeType: "multipart/form-data", success: function (res) { _this.loading.close(); if(res.code!=200){ _this.$message({ message: res.msg, type: 'error' }); }else{ _this.cancelRecoder(); _this.messageContent+='audio[' + res.result.path + ']'; _this.chatToUser(); } } }) }, cancelRecoder:function(){ this.audioDialog=false; if(!this.recorder){ return; } this.recorder.destroy(); this.recorder=null; this.recoderSecond=0; }, recoderFormat:function(percentage){ return percentage+"s"; }, openNewWindow:function(){ var features = "height=800px, width=960px, top=0, left=0, toolbar=no, menubar=no,scrollbars=no,resizable=no, location=no, status=no"; //设置新窗口的特性 var me = window.open(location.href, "newW", features); }, //超时关闭 checkTimeout:function(){ var _this=this; this.timeoutTimer=setInterval(function(){ if (Date.now() - _this.currentActiveTime >= _this.timeoutLongTime) { if(_this.VisitorCommentBtn!="true"){ _this.comment=true; } console.log("长时间无操作"); if(_this.socket!=null){ _this.reconnectDialog=true; _this.showTitle(KEFU_LANG[LANG]['autoclosemes']); _this.socket.close(); _this.socket=null; } } },55000); }, closeTimeoutTimer:function(){ clearInterval(this.timeoutTimer); }, cleanAllTimeout:function(){ for(var i in this.allTimeouter){ clearTimeout(this.allTimeouter[i]); } }, loadMoreMessages:function(){ var _this=this; var pagesize=5; // if(this.currentPage>1){ // this.replys=[]; // } if(_this.loadMoreDisable){ return; } var moreMessage=KEFU_LANG[LANG]['moremessage']; this.flyLang.moremessage=this.flyLang.loading; this.loadMoreDisable=true; var hasUnread=false; this.sendAjax("/2/messages_page","get",{pagesize:pagesize,ent_id:ENT_ID,page:this.currentPage,visitor_id:_this.visitor.visitor_id},function(result){ var len=result.list.length; if(result.list.length!=0){ if(len<pagesize){ _this.showLoadMore=false; }else{ _this.showLoadMore=true; } let msgList=result.list; for(var i=0;i<msgList.length;i++) { let visitorMes = msgList[i]; let content = {} if (visitorMes["mes_type"] == "kefu") { content.is_kefu = false; content.content = replaceSpecialTag(visitorMes["content"]); } else { content.is_kefu = true; content.content = replaceContent(visitorMes["content"]); } if (visitorMes["read_status"] == "read") { content.read_status = KEFU_LANG[LANG].read; } else { content.read_status = KEFU_LANG[LANG].unread; if(i==0){ hasUnread=true; _this.haveUnreadMessage=true; } } content.avator = visitorMes["avator"]; content.name = visitorMes["name"]; content.msg_id = visitorMes["msg_id"]; content.time = shortTime(visitorMes["time"]); _this.msgList.unshift(content); //_this.scrollBottom(); } }else{ _this.showLoadMore=false; } if(_this.currentPage==1){ _this.scrollBottom(); //_this.getAutoReply(); } _this.currentPage++; _this.flyLang.moremessage=moreMessage; _this.loadMoreDisable=false; }); }, //展示微信公众号带参二维码 showWechatTip:function(){ var _this=this; if(this.VisitorWechatQrcodeUrl==""||this.scanWechatQrcode!="true"){ return; } if(this.visitor.visitor_id.substr(0,2)=='wx'){ this.showTitle("微信访客用户已登录"); return; } var msg={}; msg.avator = _this.noticeAvatar; msg.name = _this.noticeName; msg.show_time = true; msg.time = _this.getNowDate(); var child = '<div class="wechatTip">'; child += '<img style="width:100px; margin:6px;" src="'+this.VisitorWechatQrcodeUrl+'?visitor_id=' + this.visitor.visitor_id + '&ent_id='+ENT_ID+'">'; child += '扫描或长按左侧二维码关注公众号。<br>可防止更换浏览器丢失消息、收不到回复。<br>并可接收回复通知</div>'; msg.content=child; _this.msgList.push(msg); }, sendComment:function(tagName){ var _this=this; if(!_this.commentScore){ this.$message({ message: _this.flyLang.invalidParam, type: 'error' }); return; } this.sendAjax("/2/comment","post",{ comment_score:_this.commentScore, comment_content:_this.commentContent, kefu_name:this.visitor.to_id, ent_id:ENT_ID, visitor_id:_this.visitor.visitor_id},function(result){}); }, //格式化时间 formatTime:function(time) { // var timeDate=new Date(time); // var timeStamp = Math.round(timeDate.getTime()/1000); // var nowTime=Math.round(new Date(new Date().toLocaleDateString()).getTime()/1000); // var timeDiff=timeStamp-nowTime; // if(timeDiff>=0){ // return dateFormat("H:M:S",timeDate); // //return beautifyTime(timeStamp,LANG); // }else{ // return dateFormat("Y-m-d H:M:S",timeDate); // } return time; }, getVersion:function(){ if(IS_TRY=="false"){ return; } this.$alert('当前为试用版本,请点击底部链接获取授权', '警告!', { confirmButtonText: '确定', }); }, selectLang:function(lang){ var url=changeURLPar(document.URL,"lang",lang); document.location.href=url; }, //focus事件处理 focusHandle(){ clearFlashTitle(); window.location.reload(); }, visitorPageHeight(){ //$("#chatVisitorPage").css("height","calc(100% - 121px)"); if(isWeiXin()){ $("body").css("height","100vh"); }else{ $("body").css("height",document.documentElement.clientHeight+"px"); } //document.getElementById('chatVisitorPage').style.height = (document.documentElement.clientHeight - 71) + 'px'; }, checkDomainAuth(){ return; var _this=this; $.get("/other/domainAuth",{},function(data){ if(data.code=="201"){ _this.$alert( KEFU_LANG[LANG]['authLimit'],'', { callback: function(){ window.location.reload(); } }); } }); }, initCallingDialog(){ this.canvasElement=$('#audioCanvas')[0]; this.videoElement=$('#chatRtc')[0]; this.talkPeer(); }, //loading关闭 loadingTimerTimeoutClose(){ var _this=this; if(this.loadingTimer){ clearTimeout(this.loadingTimer); this.loadingTimer=null; } this.loadingTimer=setTimeout(function(){ _this.loading.close(); _this.callClear(); }, 30000); }, callClear() { var _this=this; _this.isCalling=false; if(_this.loading){ _this.loading.close(); } if(_this.call!=null){ _this.call.close(); } if(_this.localStream){ var tracks=_this.localStream.getTracks(); for(var i=0;i<tracks.length;i++){ tracks[i].stop(); } _this.localStream=null; } } }, mounted:function() { var _this=this; document.addEventListener('paste', this.onPasteUpload); document.addEventListener('scroll',this.textareaBlur); window.addEventListener('message',function(e){ var msg=e.data; if(msg.module&&msg.module=="locationPicker"){ _this.qqMap=false; console.log('location', msg); var address={ "title":msg.poiaddress, "price":msg.poiname, "img":"/static/images/qqmap.png", "url":"https://apis.map.qq.com/tools/poimarker?type=0&marker=coord:"+msg.latlng.lat+","+msg.latlng.lng+"&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77&referer=myapp" }; _this.messageContent="product["+JSON.stringify(address)+"]"; _this.chatToUser(); } if(msg.type=="inputing_message"){ _this.sendInputingStrNow(msg.content); } if(msg.type=="send_message"){ _this.messageContent=msg.content; _this.chatToUser(); } }); //监听页面关闭 window.onbeforeunload = function(e) { _this.callClose(); }; }, created: function () { this.init(); this.getUserInfo(); this.checkDomainAuth(); //加载历史记录 //this.msgList=this.getHistory(); //滚动底部 //this.scrollBottom(); //获取欢迎 //this.initPeerjs(); //this.getVersion(); } }) </script> </html>