实现一个联系客服对话框的前端部分
一直都想写一个对话框,正好公司买了一个,就照着外观自己也写一个,每次写都会碰到意想不到的情况,通过解决这些情况,就很好的了解和学习了js知识。
先给出效果图:
这一次主要是碰到了一个问题:极短时间内多次按Enter键触发”发送内容不能为空“的提示,提示也会多次触发渐隐效果,但实际上应该是出发一次,后来发现setTimeout()方法是有一个类似id的返回值(setInterval()方法也类似),可以用clearTimeout(id),将其停止。
同时,也测试了一下键盘事件的发生顺序和可以获得的内容。输入框触发事件的顺序是focus-keydown-input-keyup-change-blur,在keydown发生的时候,能获得keycode,但是不能获得value;在input发生的时候,能获得value;在keyup发生的时候,能获得keycode,同时也能获得value,就是利用这个,实现了Enter键发送消息,shift+Enter换行。
getElementByXX与querySelector(),querySelectorAll()还是有区别的,但是本文中getElementById()完全可以用后者替代。据我所知getElementById()性能上要比querySelector()快一些,而且getElementByXX是动态的查询,querySelectorAll()只能查询出HTML代码中的节点,不会考虑动态添加的。
另外,操作DOM生成提问和回复的js代码写的不好,容我在搜索学习下=。=至少目前看来,createElement()之后应该马上appendChild()到父节点;可能一次性用innerHTML加入内容会更好。(2017年5月26日更新:今天在搜索关于回流与重绘的问题时候,突然想到了以前DOM笔记里的有关DocumentFragment类型的内容,毫无疑问,利用DocumentFragment类型来处理这种动态节点的添加再好不过了)
下面给出代码:
HTML & JS
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>客服聊天</title> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> 7 <meta name="format-detection" content="telephone=no"> 8 <meta name="apple-mobile-web-app-capable" content="yes"> 9 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 10 <link rel="stylesheet" href="styles/style.css"> 11 <script src="http://www.weizoom.com/static/resources/js/jquery-1.7.1.min.js"></script> 12 </head> 13 <body> 14 <div class="dialogue-wrapper"> 15 <div id="btn_open" class="dialogue-support-btn"> 16 <i class="dialogue-support-icon"></i> 17 <i class="dialogue-support-line"></i> 18 <span class="dialogue-support-text">联系客服</span> 19 </div> 20 <div class="dialogue-main"> 21 <div class="dialogue-header"> 22 <i id="btn_close" class="dialogue-close">></i> 23 <div class="dialogue-service-info"> 24 <i class="dialogue-service-img">头像</i> 25 <div class="dialogue-service-title"> 26 <p class="dialogue-service-name">XX客服</p> 27 <p class="dialogue-service-detail">XX客服支持平台</p> 28 </div> 29 </div> 30 </div> 31 <div id="dialogue_contain" class="dialogue-contain"> 32 <p class="dialogue-service-contain"><span class="dialogue-text dialogue-service-text">您好,请提问</span></p> 33 <!-- <p class="dialogue-customer-contain"><span class="dialogue-text dialogue-customer-text">我有个问题</span></p> --> 34 </div> 35 <div class="dialogue-submit"> 36 <p id="dialogue_hint" class="dialogue-hint"><span class="dialogue-hint-icon">!</span><span class="dialogue-hint-text">发送内容不能为空</span></p> 37 <textarea id="dialogue_input" class="dialogue-input-text" placeholder="请输入您的问题,按Enter键提交(shift+Enter换行)"></textarea> 38 <div class="dialogue-input-tools"> 39 小工具预留位置 40 </div> 41 </div> 42 </div> 43 </div> 44 <script> 45 var doc = document; 46 // 模拟一些后端传输数据 47 var serviceData = { 48 'robot': { 49 'name': 'robot001', 50 'dialogue': ['模拟回复1', '模拟回复2', '模拟回复3'], 51 'welcome': '您好,robot001为您服务' 52 } 53 }; 54 55 var dialogueInput = doc.getElementById('dialogue_input'), 56 dialogueContain = doc.getElementById('dialogue_contain'), 57 dialogueHint = doc.getElementById('dialogue_hint'), 58 btnOpen = doc.getElementById('btn_open'), 59 btnClose = doc.getElementById('btn_close'), 60 timer, 61 timerId, 62 shiftKeyOn = false; // 辅助判断shift键是否按住 63 64 btnOpen.addEventListener('click', function(e) { 65 $('.dialogue-support-btn').css({'display': 'none'}); 66 $('.dialogue-main').css({'display': 'inline-block', 'height': '0'}); 67 $('.dialogue-main').animate({'height': '600px'}) 68 }) 69 70 btnClose.addEventListener('click', function(e) { 71 $('.dialogue-main').animate({'height': '0'}, function() { 72 $('.dialogue-main').css({'display': 'none'}); 73 $('.dialogue-support-btn').css({'display': 'inline-block'}); 74 }); 75 }) 76 77 dialogueInput.addEventListener('keydown', function(e) { 78 var e = e || window.event; 79 if (e.keyCode == 16) { 80 shiftKeyOn = true; 81 } 82 if (shiftKeyOn) { 83 return true; 84 } else if (e.keyCode == 13 && dialogueInput.value == '') { 85 // console.log('发送内容不能为空'); 86 // 多次触发只执行最后一次渐隐 87 setTimeout(function() { 88 fadeIn(dialogueHint); 89 clearTimeout(timerId) 90 timer = setTimeout(function() { 91 fadeOut(dialogueHint) 92 }, 2000); 93 }, 10); 94 timerId = timer; 95 return true; 96 } else if (e.keyCode == 13) { 97 var nodeP = doc.createElement('p'), 98 nodeSpan = doc.createElement('span'); 99 nodeP.classList.add('dialogue-customer-contain'); 100 nodeSpan.classList.add('dialogue-text', 'dialogue-customer-text'); 101 nodeSpan.innerHTML = dialogueInput.value; 102 nodeP.appendChild(nodeSpan); 103 dialogueContain.appendChild(nodeP); 104 dialogueContain.scrollTop = dialogueContain.scrollHeight; 105 submitCustomerText(dialogueInput.value); 106 } 107 }); 108 109 dialogueInput.addEventListener('keyup', function(e) { 110 var e = e || window.event; 111 if (e.keyCode == 16) { 112 shiftKeyOn = false; 113 return true; 114 } 115 if (!shiftKeyOn && e.keyCode == 13) { 116 dialogueInput.value = null; 117 } 118 }); 119 120 function submitCustomerText(text) { 121 console.log(text) 122 // code here 向后端发送text内容 123 124 // 模拟后端回复 125 var num = Math.random() * 10; 126 if (num <= 7) { 127 getServiceText(serviceData); 128 } 129 } 130 131 function getServiceText(data) { 132 var serviceText = data.robot.dialogue, 133 i = Math.floor(Math.random() * serviceText.length); 134 var nodeP = doc.createElement('p'), 135 nodeSpan = doc.createElement('span'); 136 nodeP.classList.add('dialogue-service-contain'); 137 nodeSpan.classList.add('dialogue-text', 'dialogue-service-text'); 138 nodeSpan.innerHTML = serviceText[i]; 139 nodeP.appendChild(nodeSpan); 140 dialogueContain.appendChild(nodeP); 141 dialogueContain.scrollTop = dialogueContain.scrollHeight; 142 } 143 144 // 渐隐 145 function fadeOut(obj) { 146 var n = 100; 147 var time = setInterval(function() { 148 if (n > 0) { 149 n -= 10; 150 obj.style.opacity = '0.' + n; 151 } else if (n <= 30) { 152 obj.style.opacity = '0'; 153 clearInterval(time); 154 } 155 }, 10); 156 return true; 157 } 158 159 // 渐显 160 function fadeIn(obj) { 161 var n = 30; 162 var time = setInterval(function() { 163 if (n < 90) { 164 n += 10; 165 obj.style.opacity = '0.' + n; 166 } else if (n >= 80) { 167 168 obj.style.opacity = '1'; 169 clearInterval(time); 170 } 171 }, 100); 172 return true; 173 } 174 </script> 175 </body> 176 </html>
CSS
1 @charset "utf-8"; 2 /*公共样式*/ 3 html{font-family:"Helvetica Neue",Helvetica,STHeiTi,sans-serif;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;} 4 body{-webkit-overflow-scrolling:touch;margin:0;} 5 ul{margin:0;padding:0;list-style:none;outline:none;} 6 dl,dd{margin:0;} 7 a{display:inline-block;margin:0;padding:0;text-decoration:none;background:transparent;outline:none;color:#000;} 8 a:link,a:visited,a:hover,a:active{text-decoration:none;color:currentColor;} 9 a,dt,dd{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;} 10 img{border:0;} 11 p{margin:0;} 12 input,button,select,textarea{margin:0;padding:0;border:0;outline:0;background-color:transparent;} 13 /*css reset*/ 14 body { 15 position: relative; 16 } 17 18 .dialogue-wrapper { 19 font-size: 14px; 20 color: #fff; 21 } 22 /*右侧点击按钮*/ 23 .dialogue-wrapper .dialogue-support-btn { 24 position: fixed; 25 display: inline-block; 26 top: 50%; 27 right: 0; 28 margin-top: -70px; 29 padding: 10px 0; 30 width: 40px; 31 height: 120px; 32 font-size: 16px; 33 font-weight: 500; 34 text-align: center; 35 cursor: pointer; 36 border-top-left-radius: 5px; 37 border-bottom-left-radius: 5px; 38 box-shadow: -1px 1px 5px rgba(0, 0, 0, .4); 39 background-color: #5d94f3; 40 } 41 42 .dialogue-wrapper .dialogue-support-btn .dialogue-support-icon { 43 position: relative; 44 display: inline-block; 45 margin-bottom: -2px; 46 width: 20px; 47 height: 16px; 48 border-radius: 4px; 49 background-color: #fff; 50 } 51 52 .dialogue-wrapper .dialogue-support-btn .dialogue-support-icon:before { 53 content: ''; 54 position: absolute; 55 left: 50%; 56 bottom: -6px; 57 margin-left: -3px; 58 width: 0; 59 height: 0; 60 border-left: 4px solid transparent; 61 border-right: 4px solid transparent; 62 border-top: 6px solid #fff; 63 } 64 65 .dialogue-wrapper .dialogue-support-btn .dialogue-support-line { 66 display: inline-block; 67 width: 100%; 68 height: 1px; 69 background-color: #ddd; 70 } 71 72 .dialogue-wrapper .dialogue-support-btn .dialogue-support-text { 73 padding: 5px 0; 74 letter-spacing: 4px; 75 writing-mode: vertical-rl; 76 -webkit-user-select: none; 77 } 78 79 /*底部客服对话框*/ 80 .dialogue-wrapper .dialogue-main { 81 position: fixed; 82 display: none; 83 right: 100px; 84 bottom: 10px; 85 width: 400px; 86 height: 600px; 87 border-radius: 4px; 88 box-shadow: 0 0 5px rgba(0, 0, 0, .4); 89 } 90 91 /*客服对话框头部*/ 92 .dialogue-wrapper .dialogue-main .dialogue-header { 93 position: relative; 94 padding: 10px; 95 height: 80px; 96 border-top-left-radius: 4px; 97 border-top-right-radius: 4px; 98 box-shadow: 0 0 5px rgba(0, 0, 0, .2); 99 background-color: #5d94f3; 100 } 101 102 .dialogue-wrapper .dialogue-main .dialogue-close { 103 position: absolute; 104 top: 10px; 105 right: 20px; 106 padding: 2px; 107 font-size: 22px; 108 transform: rotate(90deg); 109 cursor: pointer; 110 } 111 112 .dialogue-wrapper .dialogue-main .dialogue-service-info { 113 position: relative; 114 top: 50%; 115 margin-top: -20px; 116 height: 40px; 117 } 118 119 .dialogue-wrapper .dialogue-main .dialogue-service-img { 120 display: inline-block; 121 margin: 0 10px 0 20px; 122 width: 40px; 123 height: 40px; 124 text-align: center; 125 line-height: 40px; 126 vertical-align: middle; 127 color: #000; 128 border-radius: 50%; 129 box-shadow: 1px 1px 4px rgba(0, 0, 0, .2); 130 background-color: #fff; 131 } 132 133 .dialogue-wrapper .dialogue-main .dialogue-service-title { 134 display: inline-block; 135 vertical-align: middle; 136 } 137 138 .dialogue-wrapper .dialogue-main .dialogue-service-detail { 139 font-size: 12px; 140 } 141 142 /*客服对话框内容*/ 143 .dialogue-wrapper .dialogue-main .dialogue-contain { 144 overflow-y: auto; 145 padding: 10px; 146 height: 380px; 147 word-wrap: break-word; 148 background-color: #f9f9f9; 149 } 150 151 .dialogue-wrapper .dialogue-main .dialogue-text { 152 display: inline-block; 153 position: relative; 154 padding: 10px; 155 max-width: 120px; 156 white-space: pre-wrap; 157 border: 1px solid #09d07d; 158 border-radius: 4px; 159 background-color: #09d07d; 160 box-sizing: border-box; 161 } 162 163 .dialogue-wrapper .dialogue-main .dialogue-service-contain { 164 margin-bottom: 10px; 165 text-align: left; 166 } 167 168 .dialogue-wrapper .dialogue-main .dialogue-service-text { 169 margin-left: 20px; 170 } 171 172 .dialogue-wrapper .dialogue-main .dialogue-service-text:before { 173 content: ''; 174 position: absolute; 175 top: 50%; 176 left: -10px; 177 width: 0; 178 height: 0; 179 border-top: 6px solid transparent; 180 border-bottom: 6px solid transparent; 181 border-right: 10px solid #09d07d; 182 -webkit-transform: translate(0, -50%); 183 transform: translate(0, -50%); 184 } 185 186 .dialogue-wrapper .dialogue-main .dialogue-customer-contain { 187 margin-bottom: 10px; 188 text-align: right; 189 } 190 191 .dialogue-wrapper .dialogue-main .dialogue-customer-text { 192 margin-right: 20px; 193 } 194 195 .dialogue-wrapper .dialogue-main .dialogue-customer-text:after { 196 content: ''; 197 position: absolute; 198 top: 50%; 199 right: -10px; 200 width: 0; 201 height: 0; 202 border-top: 6px solid transparent; 203 border-bottom: 6px solid transparent; 204 border-left: 10px solid #09d07d; 205 -webkit-transform: translate(0, -50%); 206 transform: translate(0, -50%); 207 } 208 209 /*客服对话框底部与输入*/ 210 .dialogue-wrapper .dialogue-main .dialogue-submit { 211 position: relative; 212 padding: 10px; 213 height: 100px; 214 color: #000; 215 word-wrap: break-word; 216 border-top: 1px solid #ddd; 217 box-sizing: border-box; 218 } 219 220 /*空输入提示*/ 221 .dialogue-wrapper .dialogue-main .dialogue-hint { 222 position: absolute; 223 top: -15px; 224 left: 20px; 225 padding: 2px; 226 width: 140px; 227 height: 18px; 228 opacity: 0; 229 font-size: 12px; 230 text-align: center; 231 line-height: 18px; 232 border: 1px solid #ddd; 233 box-shadow: 1px 1px 4px rgba(0, 0, 0, .4); 234 background-color: #fff; 235 } 236 237 .dialogue-wrapper .dialogue-main .dialogue-hint-icon { 238 display: inline-block; 239 width: 18px; 240 height: 18px; 241 margin-right: 5px; 242 font-size: 14px; 243 font-style: italic; 244 font-weight: 700; 245 vertical-align: middle; 246 line-height: 18px; 247 color: #fff; 248 border-radius: 50%; 249 background-color: #5d94f3 250 } 251 252 .dialogue-wrapper .dialogue-main .dialogue-hint-text { 253 display: inline-block; 254 vertical-align: middle; 255 } 256 257 /*输入框*/ 258 .dialogue-wrapper .dialogue-submit .dialogue-input-text { 259 overflow-y: auto; 260 display: inline-block; 261 padding: 5px 10px; 262 width: 295px; 263 height: 70px; 264 vertical-align: middle; 265 white-space: pre-wrap; 266 word-wrap: break-word; 267 resize: none; 268 border-right: 1px solid #ddd; 269 box-sizing: border-box; 270 } 271 272 .dialogue-wrapper .dialogue-submit .dialogue-input-tools { 273 display: inline-block; 274 width: 80px; 275 height: 80px; 276 vertical-align: middle; 277 }
2017年6月1日补充:
利用端午好好夯实了一下js的对象、原型链、继承等知识。
代码中的渐隐、渐显完全可以扩展到Object的原型对象上:
1 // 渐隐 2 Object.prototype.iFadeOut = function() { 3 var n = 100, 4 that = this; 5 var time = setInterval(function() { 6 if (n > 0) { 7 n -= 10; 8 that.style.opacity = '0.' + n; 9 } else if (n <= 30) { 10 that.style.opacity = '0'; 11 clearInterval(time); 12 } 13 }, 10); 14 return true; 15 }
那么我们使用就看起来更为方便:node.iFadeOut()即可,例如dialogueHint.iFadeOut()。