在这个系列文章里,我尝试将自己开发唯一客服系统(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("chatTime\">"+this.chatTitle+"");
- 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("chatTime\">"+this.chatTitle+"");
- 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,""+_this.messageContent+"");
- 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==""){
- }
- _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("
", "", { - // 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('');
- $("#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('');
- $("#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+=""+i+". "+result[key]+"";
- 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("chatNotice\">"+title+"");
- 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 = '';
- child += '
+this.VisitorWechatQrcodeUrl+'?visitor_id=' + this.visitor.visitor_id + '&ent_id='+ENT_ID+'">'; - child += '扫描或长按左侧二维码关注公众号。
可防止更换浏览器丢失消息、收不到回复。
并可接收回复通知'; - 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>
