/* global gDomains */ /* yoho客服 im: 首页 @author acgpiano */ 'use strict'; import appBridge from 'yoho-app'; import {time} from './time'; import {api} from './store'; import {RatingView, LeaveMSGView, OrderListView } from './view'; import tip from 'plugin/tip'; import dialog from 'plugin/dialog'; window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60); }; }()); let qs = require('yoho-qs'); let socket = require('./socket-chat'), socketConf = require('./socket-config'); let cmEntity = socketConf.conversationMessage; // 配置 const msgTypeMap = { 1: 'text', 2: 'picture', 10: 'order' }; let userName = $('#js-uname').val(); let encryptedUid = cmEntity.encryptedUid = $('#js-eid').val() || 0; let userAvatar = cmEntity.userHead = socketConf.defaultUserHead; let imgSrc = $('#js-avatar').val(); function checkUserAvatarValid(src, success) { let imgDOM = new Image(); imgDOM.src = src; imgDOM.style.display = 'none'; imgDOM.onload = function() { success(src); document.body.removeChild(this); }; imgDOM.onerror = function() { document.body.removeChild(this); }; document.body.appendChild(imgDOM); } // window.checkUserAvatarValid = checkUserAvatarValid; checkUserAvatarValid(imgSrc, src => { userAvatar = cmEntity.userHead = src; }); let isAndroid = /YohoBuy-android/i.test(navigator.userAgent); // 历史消息分页 let msgHistory = { conversationId: '', // 会话id endTime: null, curCount: 0, totalCount: null }; cmEntity.userName = userName; let chat = { $chat: null, $chatWin: null, $netTip: null, unFinshMSGs: {}, // 没有发送出去消息 $ratingView: $('#chat-comment'), canEvalute: true, canManualService: true, canLeaveMSG: false, messageT: require('service/chat/msg.hbs'), // 初始化websocket init: function() { this.$chat = $('#chat-main-wrap'); this.$header = this.$chat.find('.chat-header'); this.$chatWin = this.$chat.find('#chat-window'); this.$netTip = this.$chat.find('.connection-failed'); this.$historyLoader = this.$chat.find('#chat-history-loader'); // 组件 this.leaveMSGView = new LeaveMSGView('#leave-msg'); this.orderListView = new OrderListView('#order-list'); this.ratingView = new RatingView('#chat-comment', cmEntity); const self = this; cmEntity.encryptedUid = encryptedUid; self.fetchHistoryMsg().always(function() { self.$chatWin.append(time(Date.now()).show()); self.connect(); if (msgHistory.curCount < msgHistory.totalCount) { // has more history self.$historyLoader .show() .on('click', function() { if (self.$historyLoader.hasClass('chat-history-loading')) { return; } self.$historyLoader.toggleClass('chat-history-loading', true); self.fetchHistoryMsg() .done(function(hasMore) { if (!hasMore) { self.$historyLoader.remove(); } }) .always(function() { self.$historyLoader.toggleClass('chat-history-loading', false); }); }); } }); this.bindEvents(); }, bootSocket: function() { const self = this; let actions = { onMessage: function(event) { let received = JSON.parse(event.data); // update 会话id cmEntity.conversationId = received.newConversationId > 0 ? received.newConversationId : received.conversationId; // 处理消息 self.handleReceiveMSG(received); }, // onClose: function() { // self._sysInfo('连接已断开'); // }, onOpen: $.noop, sendFailCallback: function() { self._sysInfo('<p>连接断开,请尝试<span class="blue">重连</span></p>') .one('touchend', function() { self.connect(); }); }, }; let config = $.extend(socketConf, actions); socket.init(config); }, /** * method: 绑定事件 */ bindEvents: function() { let self = this; let $dialog; this.$chat .on('click.Queue.quit', '[data-trigger=quit-queue]', function() { dialog.showDialog({ dialogText: '确认结束排队吗?', hasFooter: { leftBtnText: '继续等待', rightBtnText: '结束排队' } }, function() { cmEntity.type = socketConf.recType.QUIT_QUEUE; socket.send(JSON.stringify(cmEntity)); dialog.hideDialog(); }); $dialog = $('.dialog-wrapper .dialog-box'); $dialog.css({ background: 'hsla(100, 100%, 100%, 1)' }); }) .on('click.Chat.end', '[data-trigger=end-chat]', function() { dialog.showDialog({ dialogText: '确认结束本次服务吗?', hasFooter: { leftBtnText: '继续咨询', rightBtnText: '结束服务' } }, function() { cmEntity.type = socketConf.recType.USER_END_CHAT; socket.send(JSON.stringify(cmEntity)); dialog.hideDialog(); }); $dialog = $('.dialog-wrapper .dialog-box'); $dialog.css({ background: 'hsla(100, 100%, 100%, 1)' }); }) .on('click.Chat.end', '[data-action=re-connect]', function() { self.connect(); }) .on('click.Rating.toggle', '[data-trigger=rating]', function() { if (self.canEvalute) { self.ratingView.toggle(); } else { tip.show('您已评价,请勿重复评价'); } }) .on('click.leaveMSG', '[data-trigger=leave-msg]', function() { self.canLeaveMSG && self.leaveMSGView.trigger('show.LeaveMSGView'); }) .on('click.orderList', '[data-trigger=order-list]', function() { let orderType = $(this).data('type') || ''; self.orderListView.trigger('show.OderListView', [orderType]); }) .on('click.chat.switchServer', '[data-action=change-human]', function() { self.canManualService && self.switchService('human'); }) .on('focus.chat.sendText', '.text-in', function() { self.toggleMenu(false); window.requestAnimFrame(() => { $('#chat-footer')[0].scrollIntoView(); }); }) .on('click', '#chat-window', function() { self.toggleMenu(false); }) .on('focus', '.text-in', function() { self.chatWinScrollToBottom(); return false; }) .on('keydown.chat.sendText', '.text-in', function(event) { event.stopPropagation(); window.requestAnimFrame(() => { $('#chat-footer')[0].scrollIntoView(); }); if (event.which === 13) { let val = $.trim(event.target.value); if (!val) { return; } let content = { avatar: userAvatar, data: { content: val } }; self.sendMSG(content); event.target.value = ''; } }); this.orderListView .on('selectOrder.OrderListView', function(event, data) { let msg = { type: 'order', data, style: 'send-msg', }; self.sendMSG(msg); self.$chatWin[0].scrollTop = self.$chatWin[0].scrollHeight; }); this.leaveMSGView .on('save.LeaveMSGView', function(event, data) { self._sysInfo(data); }); this.$ratingView.on('click', '.submit', function() { self.ratingView.post({ encryptedUid, conversationId: cmEntity.conversationId, }); }).on('rating-success', function(e, data) { const state = (window.socket || {}).readyState; self._sysInfo(`您对我们的服务评价为:${data}`); self.canEvalute = false; cmEntity.type = socketConf.recType.EVALUTE_SUCCESS; if (state === WebSocket.OPEN) { socket.send(JSON.stringify(cmEntity)); } }); window.addEventListener('online', function() { self.$netTip.toggleClass('hide', true); self.connect(); }); window.addEventListener('offline', function() { self.$chat.toggleClass('online', false); self.$netTip.toggleClass('hide', false); self.toggleMenu(false); // self.disconnect(); }); // app 不支持打开链接 if (isAndroid || appBridge.isApp) { this.$chatWin.on('click', '.link', function(event) { event.preventDefault(); // tip.show('抱歉,暂不支持,长按复制到手机浏览器打开'); return false; }); } }, /** * 当连接时 */ connect() { cmEntity.type = 1; this.bootSocket(); this.canEvalute = true; this.switchService('robot'); }, /** * 当断开时 */ disconnect(content) { let self = this; self.renderHeader('offline'); this.canLeaveMSG = true; this.$chat.toggleClass('online', false); this.toggleMenu(false); this._sysInfo(`<p>${content}<span class="blue">连接客服</span></p>`) .one('touchend', function() { self.connect(); }); }, /** * method: 在聊天窗口上, 画出信息 * @param object msg * { * from: str //来源 ["customer", "robot", "employee", "system"] * type: str //消息类型 see swtich msg.type * data: object //消息数据 * } * * @param {boolean} noDraw 默认false 渲染并发送 * * @return {jquery} $elem * */ sendMSG: function(msg, noDraw = false) { const self = this; if (!msg || !msg.data) { return; } let data = msg.data; let uuid = Date.now(); let arr; msg.type = msg.type || 'text'; msg.from = msg.from || 'customer'; data.avatar = userAvatar; msg.uuid = uuid; cmEntity.userHead = msg.avatar = userAvatar; cmEntity.uuid = uuid; cmEntity.type = socketConf.recType.CU_SEND; switch (msg.type) { case 'order': arr = [ '单号:', data.orderCode, '金额: ', data.cost, '下单时间: ', data.createTime, '状态: ', data.orderStatus ]; cmEntity.chatMessage.type = 10; cmEntity.chatMessage.content = arr; break; case 'picture': cmEntity.chatMessage.type = 2; cmEntity.chatMessage.content = data.content; break; case 'text': default: cmEntity.chatMessage.type = 1; cmEntity.chatMessage.content = data.content; } if (!noDraw) { let $msg = this._drawMSG(msg); self.wrapMSG(cmEntity, $msg) .trigger('send.sendEvent'); } else { socket.send(JSON.stringify(cmEntity)); } return this; }, /** * 消息发送的生命周期 * 1. send Event * 2. sucess Event * 3. fail Event * @param {object} entity * @param {jquery} $msg * @param {function} sendAction * @return $msg */ wrapMSG(entity, $msg, sendAction) { // eslint-disable-line let self = this; let uuid = entity.uuid; let msgStr = JSON.stringify(entity); this.unFinshMSGs[uuid] = $msg; $msg.data('msg', msgStr); $msg .on('fail.sendEvent', function() { $msg.removeClass('send-loading') .addClass('send-fail') .one('click', () => { $msg.trigger('send.sendEvent'); }); }) .on('success.sendEvent', function(event, rec) { clearTimeout($msg.data('timeoutId')); if (rec.type === 1) { $msg.find('.msg-content').html(rec.chatMessage.newContent); } $msg.removeClass('send-loading send-fail') .off('sendEvent') .removeData(); }) .on('send.sendEvent', function() { if (navigator.onLine) { $msg.removeClass('send-fail') .addClass('send-loading'); $msg.data('timeoutId', setTimeout(() => { if (self.unFinshMSGs[uuid]) { $msg.trigger('fail.sendEvent'); } }, 5000)); socket.send($msg.data('msg')); } else { $msg.trigger('fail.sendEvent'); } }); return $msg; }, /** * 处理 conversationMessage, 生成 渲染用的数据 */ buildViewData: function(cm) { let viewData = {}; let chatMessage = cm.chatMessage; let allTypes = socketConf.recType; // 设置默认用户头像 cm.userHead = userAvatar; switch (cm.type) { case allTypes.CU_SEND: viewData.from = 'customer'; viewData.avatar = cm.userHead; break; case allTypes.CS_SEND: viewData.from = 'employee'; viewData.avatar = cm.csHead || socketConf.employeHead; break; case allTypes.ROBOT_SEND: viewData.from = 'rebot'; viewData.avatar = socketConf.rebotUserHead; break; default: return null; } function emojiHanlder(content) { let $div = $('<div></div>').html(content); $div.find('img[yohotype=emo]') .attr('src', function(i, val) { return window.STATIC_RESOURCE_PATH + '/img/service/emoji/' + val; }); return $div.html(); } switch (chatMessage.type) { case 1: viewData.type = msgTypeMap[1]; viewData.data = { content: emojiHanlder(chatMessage.newContent) || chatMessage.content }; break; case 2: viewData.type = msgTypeMap[2]; viewData.data = { content: chatMessage.content }; break; case 10: chatMessage.content = JSON.parse(chatMessage.content); viewData.type = msgTypeMap[10]; viewData.data = { orderCode: chatMessage.content[1], cost: chatMessage.content[3], createTime: chatMessage.content[5], orderStatus: chatMessage.content[7], }; break; default: return null; } return viewData; }, /** * 处理 接收到的信息 */ handleReceiveMSG: function(rec) { let recType = rec.type, chatMessage = rec.chatMessage, msgType = chatMessage.type, allTypes = socketConf.recType, uuid = rec.uuid; if (this.unFinshMSGs[uuid]) { let $msg = this.unFinshMSGs[uuid]; $msg.trigger('success.sendEvent', rec); } // let uuid = rec.uuid; let viewData; this.canManualService = true; this.canLeaveMSG = false; // 服务状态: 离线 if ( recType === allTypes.OFFLINE || recType === allTypes.OP_LEAVE || (recType === allTypes.MANUAL_SERVICE && msgType === 0) ) { this.$chat.toggleClass('online', false); } switch (recType) { // 客服消息 case allTypes.CS_SEND: case allTypes.ROBOT_SEND: viewData = this.buildViewData(rec); viewData && this._drawMSG(viewData); break; // 系统消息 // ------------------------------------------ // 用户进入 case allTypes.ENTER: this.renderHeader('robot'); chatMessage.newContent && this._sysInfo(chatMessage.newContent); break; case allTypes.CS_CHATTING: if (chatMessage.type === 5) { // 重复登陆 this.canManualService = false; this._sysInfo(chatMessage.newContent); } break; case allTypes.BREAK_TIME: this._sysInfo(chatMessage.content); break; case allTypes.QUIT_QUEUE: this.renderHeader('robot'); this._sysInfo(chatMessage.content); break; // 客服邀请评价 case allTypes.EVAL_INVITE: this._sysInfo('<p data-trigger="rating">请对我们的服务进行<span class="blue">评价</span></p>'); this.ratingView.toggle(true); break; // 客服进入 case allTypes.CS_ENTER: this.$chat.toggleClass('online', true); this._sysInfo('客服小YO正在为您服务'); break; case allTypes.OP_LEAVE: case allTypes.OFFLINE: this.disconnect(chatMessage.content); break; case allTypes.TRANSFER: break; case allTypes.MANUAL_SERVICE: this._manualState(chatMessage.type, rec); break; case allTypes.IN_QUNEUE: this._sysInfo(chatMessage.content); break; case allTypes.CS_CHANGE_STATE: if (msgType === 5) { // 重复登陆 this._sysInfo(chatMessage.content); } break; default: break; } return this; }, // -------------------------------------消息状态 处理------------------------------------------------ _disconnectState(state, cmEntity) { // eslint-disable-line }, /** * cmEntity.type = 2, 处理人工客服消息 * @param {int} state cmEntity.message.type * @param {object} cmEntity cmEntity */ _manualState(state, cmEntity) { // eslint-disable-line const self = this; const $chat = self.$chat; const sysInfo = self._sysInfo.bind(this); const chatMessage = cmEntity.chatMessage; function whetherLeaveMsg(cm) { const canLeave = cm.isLeaveMessage === 2; const append = canLeave ? '您可以选择<span class="blue" data-trigger="leave-msg">留言</span>' : ''; const reg = /[,|,]$/g; chatMessage.content = canLeave ? chatMessage.content : (chatMessage.content = chatMessage.content.replace(reg, '')); canLeave && (self.canLeaveMSG = true); sysInfo(`${chatMessage.content || ''}${append}`); } function noService() { self.renderHeader('robot'); $chat.append(time(Date.now()).show()); } // state: 0 没有人工客服 function noEmploye() { self.renderHeader('robot'); $chat.append(time(Date.now()).show()); whetherLeaveMsg(cmEntity); } // state 1: 排队中 function inQueue() { self.renderHeader('queue'); whetherLeaveMsg(cmEntity); } // state 2: 人工客服进入 function linkSuccess() { self.renderHeader('human'); $chat .toggleClass('online', true) .append(time(Date.now()).show()); sysInfo(chatMessage.content); } switch (state) { case 0: noEmploye(); break; case 1: inQueue(); break; case 2: case 3: linkSuccess(); break; default: noService(); break; } }, // -------------------------------------消息状态 处理 end------------------------------------------------ /** * 对话消息 * @param 【object|array] viewData 订单消息模版数据 * @param {function} cusAction 自定义处理函数 * function ($html) { * // $html 模版渲染出来的jquery * // this ----> chat * } */ _drawMSG: function(viewData, cusAction = null) { let chatWin = this.$chatWin[0]; if (viewData.type === 'picture') { viewData.data.content = viewData.data.content.replace(/^http:/, ''); } let $html = $(this.messageT(viewData)); this.checkTime(); if (cusAction) { cusAction.apply(this, [$html]); } else { $html.appendTo(this.$chatWin); chatWin.scrollTop = chatWin.scrollHeight; } return $html; }, /** * 系统消息 */ _sysInfo: function(msg) { let $chatWin = this.$chatWin; let chatWin = $chatWin[0]; if (msg) { msg = `<div class="sysinfo change-mm"> <div class="leave-msg">${msg}</div> </div>`; } else { msg = `<div class="sysinfo leave-msg-wraper"> <div class="leave-msg">您可以选择<a href="javascript:;" data-trigger="leave-msg">留言</a>,客服会以短信形式回复您</div> </div>`; } let $msg = $(msg); this.checkTime(); $msg.appendTo($chatWin); chatWin.scrollTop = chatWin.scrollHeight; return $msg; }, checkTime: $.noop, // 获取10条历史记录 fetchHistoryMsg: function() { const self = this; if (msgHistory.totalCount !== null && msgHistory.curCount >= msgHistory.totalCount) { return $.Deferred().resolve(false); // eslint-disable-line } return api.fetchHistory(msgHistory.endTime) .done(function(result) { if (result && result.code === 401) { window.location.href = '//m.yohobuy.com/signin.html?refer=' + window.location.href; } if (!result || result.code !== 200 || !result.data) { return false; } result = result.data; msgHistory.totalCount = result.totalCount; let records = result.records || []; // records.reverse(); let oldFister = self.$chatWin.find('.msg-wrap:first')[0]; let grap = oldFister ? oldFister.offsetTop : 0; let $docFragment = $(document.createDocumentFragment()); records.forEach((chatMessage, index) => { let nextChat = records[index + 1]; let data = self.buildViewData(chatMessage); //eslint-disable-line if (data) { let conversationId = chatMessage.conversationId; let timstamp = Math.ceil(chatMessage.sendTime / 1000); // 原来是微妙 转换成毫秒 let $html = self._drawMSG(data); $docFragment.prepend($html); if (index === 0 && !msgHistory.conversationId) { msgHistory.conversationId = conversationId; } if ( nextChat && (nextChat.conversationId !== msgHistory.conversationId) ) { $docFragment.prepend(time(timstamp).show()); msgHistory.conversationId = nextChat.conversationId; } } }); self.$historyLoader.after($docFragment); oldFister && self.$chatWin.scrollTop(oldFister.offsetTop - grap); if (records.length) { msgHistory.endTime = records[records.length - 1].sendTime; msgHistory.curCount += records.length; } return msgHistory.curCount < msgHistory.total; }) .fail($.noop); }, switchService: function(type) { this.renderHeader(`switch-${type}`); if (type === 'human') { cmEntity.type = socketConf.recType.MANUAL_SERVICE; socket.send(JSON.stringify(cmEntity)); } }, /** * method: 渲染头部 * @param {string} type ["robot", "human"] */ renderHeader: function(type) { let header = { robot: { title: '智能小YO', right: '<span data-action="change-human">人工客服</span>' }, queue: { title: '<i class="chat-status"></i><span class="js-service-txt">队列中...</span>', right: '<span data-trigger="quit-queue">结束排队</span>' }, human: { title: '<i class="chat-status"></i><span class="js-service-txt">YOHO客服</span>', right: '<span data-trigger="end-chat">结束服务</span>' }, offline: { title: '<i class="chat-status"></i><span class="js-service-txt">YOHO客服</span>', right: '<span data-action="re-connect">重新连接</span>' }, 'switch-robot': { title: '<i class="chat-status"></i><span class="js-service-txt">正在连接...</span>', right: '<span></span>' }, 'switch-human': { title: '<i class="chat-status"></i><span class="js-service-txt">连接人工...</span>', right: '' } }; header = header[type]; this.$header .find('.title').html(header.title).end() .find('.header-right').html(header.right); return this; }, toggleMenu: function(willShow) { if (willShow === void 0) { willShow = !this.$chat.hasClass('menu-open'); } this.$chat.toggleClass('menu-open', willShow); this.chatWinScrollToBottom(); // let chatWin = this.$chatWin[0]; // let footerH = $('#chat-footer .menu').outerHeight() || 0; // if (willShow) { // chatWin.scrollTop += footerH; // } else { // chatWin.scrollTop -= footerH; // } }, chatWinScrollToBottom: function() { let chatWin = chat.$chatWin[0]; window.requestAnimationFrame(() => { chatWin.scrollTop = chatWin.scrollHeight; }); } }; chat.init(); // 触发菜单 $('.menu-trigger').on('click', function() { chat.toggleMenu(); // let $menu = $('.menu'); // $menu.toggle(); // resizeFooter(); }); let $upload = $('.upload-img'); $upload.on('change', function() { let input = event.target; let files = input.files; let formData = new FormData(); if (!files[0]) { return; } formData.append('files[]', files[0]); let msg = { type: 'picture', from: 'customer', avatar: userAvatar, data: { content: '' } }; let $elem = chat._drawMSG(msg); let _action = function() { $elem.removeClass('send-fail').addClass('send-loading'); $.ajax({ type: 'POST', url: `${gDomains.imCs}/fileManage/uploadFile`, data: formData, processData: false, // 告诉jQuery不要去处理发送的数据 contentType: false }) .done(function(res) { if (res.code === 200) { $elem.removeClass('send-loading') .find('.chat-image').attr('src', res.data.filePath) .on('load', function() { chat.chatWinScrollToBottom(); }); msg.data.content = res.data.filePath; chat.sendMSG(msg, true); // only send msg, no draw msg; } }) .fail(function() { chat._sysInfo('图片上传失败,请稍后再试'); $elem .removeClass('send-loading') .addClass('send-fail') .one('click', _action); }) .always(function() { input.value = ''; chat.chatWinScrollToBottom(); }); }; _action(); }); // 图片放大 $('#chat-window').on('click', '.chat-image', function() { let img = $(this), fake = $('.fake-img-wrap'); fake.find('img').attr('src', img.attr('src')); fake.fadeIn(); fake.on('click', function() { fake.fadeOut(); }); }); function tellAppSuccess() { window.setTimeout(() => { appBridge.invokeMethod('im.page_success', {url: location.href}); }, 1000); window.addEventListener('pageshow', tellAppSuccess); } function setChannelColor(channel) { let dict = { 1: 'boys', 2: 'girls', 3: 'kids', 4: 'lifestyle' }; $(document.body) .removeClass('boys girls kids lifestyle') .addClass(dict[channel]); } if (appBridge.isApp && /YohoBuy-iOS/i.test(navigator.userAgent)) { document.body.classList.add('app-ios'); } if (isAndroid) { $('#js-back').removeAttr('href').on('click', function(e) { e.preventDefault(); window.im.goBack(); }); setChannelColor(qs.yh_channel); } else if (appBridge.isApp) { $('#js-back').removeAttr('href').on('click', function(e) { e.preventDefault(); appBridge.invokeMethod('go.back', {}); }); tellAppSuccess(); setChannelColor(qs.yh_channel); } window.channelChanged = function(channel) { setChannelColor(channel); }; window.$ = $; window.chat = chat; window.cmEntity = cmEntity; window.time = time;