在這個系列文章里,我嘗試將自己開發唯一客服系統(gofly.v1kf.com)所涉及的經驗和技術點進行梳理總結。 文章寫作水平有限,有時候會表達不清楚,難免有所疏漏,歡迎批評指正 該系列將分成以下幾個部分 一. 需求分析 二. 初步技術方案選型,驗證 三. 資料庫結構設計 四. WEB訪客前端設計與 ...
在這個系列文章里,我嘗試將自己開發唯一客服系統(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.s