index.js 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197
  1. define(['io', 'moment', 'moment/locale/zh-cn'], function (io, Moment) {
  2. // 表情数据
  3. const emojiList = [
  4. { "name": "[笑掉牙]", "file": "xiaodiaoya.png" },
  5. { "name": "[可爱]", "file": "keai.png" },
  6. { "name": "[冷酷]", "file": "lengku.png" },
  7. { "name": "[闭嘴]", "file": "bizui.png" },
  8. { "name": "[生气]", "file": "shengqi.png" },
  9. { "name": "[惊恐]", "file": "jingkong.png" },
  10. { "name": "[瞌睡]", "file": "keshui.png" },
  11. { "name": "[大笑]", "file": "daxiao.png" },
  12. { "name": "[爱心]", "file": "aixin.png" },
  13. { "name": "[坏笑]", "file": "huaixiao.png" },
  14. { "name": "[飞吻]", "file": "feiwen.png" },
  15. { "name": "[疑问]", "file": "yiwen.png" },
  16. { "name": "[开心]", "file": "kaixin.png" },
  17. { "name": "[发呆]", "file": "fadai.png" },
  18. { "name": "[流泪]", "file": "liulei.png" },
  19. { "name": "[汗颜]", "file": "hanyan.png" },
  20. { "name": "[惊悚]", "file": "jingshu.png" },
  21. { "name": "[困~]", "file": "kun.png" },
  22. { "name": "[心碎]", "file": "xinsui.png" },
  23. { "name": "[天使]", "file": "tianshi.png" },
  24. { "name": "[晕]", "file": "yun.png" },
  25. { "name": "[啊]", "file": "a.png" },
  26. { "name": "[愤怒]", "file": "fennu.png" },
  27. { "name": "[睡着]", "file": "shuizhuo.png" },
  28. { "name": "[面无表情]", "file": "mianwubiaoqing.png" },
  29. { "name": "[难过]", "file": "nanguo.png" },
  30. { "name": "[犯困]", "file": "fankun.png" },
  31. { "name": "[好吃]", "file": "haochi.png" },
  32. { "name": "[呕吐]", "file": "outu.png" },
  33. { "name": "[龇牙]", "file": "ziya.png" },
  34. { "name": "[懵比]", "file": "mengbi.png" },
  35. { "name": "[白眼]", "file": "baiyan.png" },
  36. { "name": "[饿死]", "file": "esi.png" },
  37. { "name": "[凶]", "file": "xiong.png" },
  38. { "name": "[感冒]", "file": "ganmao.png" },
  39. { "name": "[流汗]", "file": "liuhan.png" },
  40. { "name": "[笑哭]", "file": "xiaoku.png" },
  41. { "name": "[流口水]", "file": "liukoushui.png" },
  42. { "name": "[尴尬]", "file": "ganga.png" },
  43. { "name": "[惊讶]", "file": "jingya.png" },
  44. { "name": "[大惊]", "file": "dajing.png" },
  45. { "name": "[不好意思]", "file": "buhaoyisi.png" },
  46. { "name": "[大闹]", "file": "danao.png" },
  47. { "name": "[不可思议]", "file": "bukesiyi.png" },
  48. { "name": "[爱你]", "file": "aini.png" },
  49. { "name": "[红心]", "file": "hongxin.png" },
  50. { "name": "[点赞]", "file": "dianzan.png" },
  51. { "name": "[恶魔]", "file": "emo.png" }
  52. ]
  53. // 通知消息
  54. function audioPlay(type) {
  55. let ex = new Audio(`/assets/addons/shopro/img/chat/${type}.mp3`);
  56. if (ex) {
  57. ex.play();
  58. }
  59. }
  60. // 客服错误通知
  61. function callBackNotice(res, flag = false) {
  62. flag &&
  63. res.msg &&
  64. ElementPlus.ElNotification({
  65. title: 'socket',
  66. message: `客服错误:${res.msg}`,
  67. showClose: true,
  68. type: res.code == 1 ? 'success' : 'warning',
  69. duration: 3000,
  70. });
  71. }
  72. const IS_DEBUG = true; // DEBUG开关
  73. const debug = (...args) => {
  74. if (IS_DEBUG) {
  75. console.info('%c%s', 'color: blue; background: yellow; font-size: 11px;', ...args);
  76. }
  77. return;
  78. };
  79. // 重新连接尝试次数
  80. const reconnectionAttempts = 5; //次
  81. // 重新连接间隔时间
  82. const reconnectionDelay = 10; // 秒
  83. const SaChat = {
  84. template: `#saChatTemplate`,
  85. props: {},
  86. setup(props) {
  87. const { ref, reactive, computed, getCurrentInstance, nextTick } = Vue
  88. const { proxy } = getCurrentInstance();
  89. const chat = {
  90. state: reactive({
  91. config: {}, // 配置信息
  92. chatService: null, // 示例
  93. sessionList: [], // 会话列表
  94. customerServicesList: [], // 客服列表
  95. sessionType: 'ing', // 列表状态 ing=会话中|waiting=排队中| history=历史
  96. customerOnLineList: [], // 会话中
  97. customerWaitingList: [], // 排队中
  98. customerHistoryList: [], // 历史
  99. chatList: [], // 会话信息
  100. commonWords: [], // 常用语列表
  101. currentCustomerService: {}, // 当前客服信息
  102. customerServiceIdentityList: [], // 客服身份列表
  103. currentCustomer: null, // 当前顾客信息
  104. historyPagination: {
  105. page: 0,
  106. list_rows: 10,
  107. last_id: 0,
  108. lastPage: 0,
  109. loadStatus: 'loadmore', //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
  110. },
  111. connection: {
  112. status: '',
  113. attempts: 0,
  114. reconnectionAttempts,
  115. delay: 0,
  116. showTip: true,
  117. isFlag: false,
  118. }, // 连接状态
  119. isSendSucces: 0, // 是否发送成功 -1=发送中|0=发送成功|1发送失败
  120. notificationType: '', // 站内信类型
  121. notificationTypeList: [], // 站内信类型列表
  122. notificationList: [], // 站内信列表
  123. }),
  124. // 打开客服弹窗
  125. open_chat() {
  126. chat.state.currentCustomer = chat.state.customerOnLineList[0];
  127. chat.state.customerOnLineList.length && chat.socket_change_customer_list({
  128. session: chat.state.sessionType,
  129. })
  130. },
  131. // 初始化chat配置请求
  132. async chatInit() {
  133. Fast.api.ajax({
  134. url: 'shopro/chat/index/init',
  135. type: 'GET',
  136. }, function (ret, res) {
  137. chat.state.config = res.data;
  138. chat.socket_init(chat.state.config.chat_domain);
  139. return false
  140. }, function (ret, res) { })
  141. },
  142. // 1.初始化socket
  143. async socket_init(connectionUrl, options) {
  144. chat.state.connection.status = 'connecting';
  145. chat.state.connection.isFlag = true
  146. // 连接
  147. try {
  148. chat.state.chatService = io(connectionUrl, {
  149. reconnection: true, // 默认 true 是否断线重连
  150. reconnectionAttempts: reconnectionAttempts, // 默认无限次 断线尝试次数
  151. reconnectionDelay: reconnectionDelay * 1000, // 默认 1000,进行下一次重连的间隔。
  152. reconnectionDelayMax: reconnectionDelay * 1000, // 默认 5000, 重新连接等待的最长时间 默认 5000
  153. randomizationFactor: 0.5, // 默认 0.5 [0-1],随机重连延迟时间
  154. timeout: 20000, // 默认 20s
  155. transports: ['websocket', 'polling'], // websocket | polling,
  156. ...options,
  157. });
  158. } catch (error) {
  159. debug('Socket连接错误', error);
  160. }
  161. // 连接成功
  162. chat.state.chatService.on('connect', (res) => {
  163. debug('connect', res);
  164. chat.state.connection.status = 'connect';
  165. chat.socket_connection();
  166. });
  167. // 监听会话
  168. chat.state.chatService.on('message', (res) => {
  169. debug('message', res);
  170. if (res.code == 1) {
  171. audioPlay('chat');
  172. const { message, sender } = res.data;
  173. if (chat.state.currentCustomer && chat.state.currentCustomer?.session_id == sender.session_id) {
  174. // 如果是当前会话,数字角标不增加,chatList,push
  175. chat.state.chatList.push(message);
  176. scrollBottom();
  177. } else {
  178. // 非当前会话,顾客角标+1
  179. const index = chat.state.customerOnLineList.findIndex(
  180. (item) => item.session_id == sender.session_id,
  181. );
  182. if (~index) chat.state.customerOnLineList[index].unread_num += 1;
  183. }
  184. }
  185. });
  186. // 用户上线
  187. chat.state.chatService.on('customer_online', (res) => {
  188. res.code == 1 && chat.socket_change_customer_state(res.data);
  189. });
  190. // 用户下线
  191. chat.state.chatService.on('customer_offline', (res) => {
  192. res.code == 1 && chat.socket_change_customer_state(res.data);
  193. });
  194. // 用户被接入
  195. chat.state.chatService.on('customer_accessed', (res) => {
  196. if (res.code == 1) {
  197. const { session_id, chat_user } = res.data;
  198. const index = chat.state.customerWaitingList.findIndex((item) => item.session_id == session_id);
  199. index >= 0 && chat.state.customerWaitingList.splice(index, 1);
  200. if (chat.state.sessionType == 'waiting') {
  201. chat.state.currentCustomer =
  202. chat.state.customerWaitingList.length >= 1
  203. ? chat.state.customerWaitingList[0]
  204. : chat.socket_reset();
  205. }
  206. }
  207. });
  208. // 新用户接入
  209. chat.state.chatService.on('customer_access', (res) => {
  210. if (res.code == 1) {
  211. const { session_id, chat_user } = res.data;
  212. const index = chat.state.customerOnLineList.findIndex((item) => item.session_id == session_id);
  213. // 如果用户存在,修改会话中用户状态
  214. if (index >= 0) {
  215. chat.state.customerOnLineList[index].status = chat_user.status;
  216. } else {
  217. // 如果用户不存在
  218. chat.state.customerOnLineList.unshift(chat_user);
  219. // 如果当前会话列表有且只有一个新增会话
  220. if (chat.state.customerOnLineList.length > 0 && chat.state.sessionType == 'ing') {
  221. chat.state.currentCustomer = chat_user;
  222. chat.socket_customer_history();
  223. }
  224. }
  225. callBackNotice(res);
  226. }
  227. });
  228. // 更新客服列表customer_service_update
  229. chat.state.chatService.on('customer_service_update', (res) => {
  230. if (res.code == 1) {
  231. chat.state.customerServicesList = res.data.customer_services.map((item) => ({
  232. label: item.name,
  233. value: item.id,
  234. }));
  235. }
  236. });
  237. // 新顾客等待,更新等待列表
  238. chat.state.chatService.on('customer_waiting', (res) => {
  239. if (res.code == 1) {
  240. chat.state.customerWaitingList = res.data.waitings;
  241. audioPlay('chat');
  242. if (chat.state.sessionType == 'waiting') {
  243. chat.state.sessionList = res.data.waitings;
  244. chat.state.currentCustomer = chat.state.sessionList.length
  245. ? res.data.waitings[0]
  246. : chat.socket_reset();
  247. }
  248. }
  249. });
  250. // 消息通知
  251. chat.state.chatService.on('notification', (res) => {
  252. if (res.code == 1) {
  253. audioPlay('notice');
  254. if (chat.state.notificationType == res.data.notification_type) {
  255. chat.state.notificationList.unshift(res.data);
  256. } else {
  257. chat.state.notificationTypeList.map((item) => {
  258. if (item.value == res.data.notification_type) {
  259. item.unread_num += 1;
  260. }
  261. });
  262. }
  263. }
  264. });
  265. // 自定义错误
  266. chat.state.chatService.on('custom_error', (error) => {
  267. callBackNotice(error, true);
  268. });
  269. chat.state.chatService.on('error', (error) => {
  270. chat.state.connection.status = 'error';
  271. debug('error', error);
  272. scrollBottom();
  273. });
  274. // 连接失败
  275. chat.state.chatService.on('connect_error', (error) => {
  276. debug('connect_error:', error);
  277. scrollBottom();
  278. });
  279. // 连接超时
  280. chat.state.chatService.on('connect_timeout', (error) => {
  281. debug('connect_timeout:', error);
  282. });
  283. // 断开连接
  284. chat.state.chatService.on('disconnect', (error) => {
  285. debug('disconnect:', error);
  286. });
  287. // 服务重启重连上reconnect
  288. chat.state.chatService.on('reconnect', (error) => {
  289. debug('disconnect:', error);
  290. });
  291. // 尝试重连
  292. chat.state.chatService.on('reconnect_attempt', (counter) => {
  293. debug('reconnect_attempt', counter);
  294. chat.state.connection.status = 'reconnect_attempt';
  295. });
  296. // 重新连接中
  297. chat.state.chatService.on('reconnecting', (counter) => {
  298. debug('reconnecting', counter);
  299. chat.state.connection.status = 'reconnecting';
  300. chat.state.connection.attempts = counter;
  301. });
  302. // 重连失败
  303. chat.state.chatService.on('reconnect_error', (error) => {
  304. debug('reconnect_error', error);
  305. if (chat.state.connection.attempts >= chat.state.connection.reconnectionAttempts) {
  306. return;
  307. }
  308. chat.state.connection.status = 'reconnect_error';
  309. // 设置倒计时
  310. chat.state.connection.delay = reconnectionDelay;
  311. const timer = setInterval(() => {
  312. chat.state.connection.delay -= 1;
  313. if (chat.state.connection.delay <= 1) {
  314. clearInterval(timer);
  315. }
  316. }, 1000);
  317. });
  318. // 重连失败 达到最大重试次数
  319. chat.state.chatService.on('reconnect_failed', () => {
  320. debug('reconnect_failed');
  321. chat.state.connection.status = 'reconnect_failed';
  322. chat.state.isSendSucces = 1;
  323. chat.state.connection.isFlag = false
  324. });
  325. // 与服务器连接失败
  326. chat.state.chatService.on('connect_failed', (error) => {
  327. debug('connect_failed', error);
  328. });
  329. },
  330. // socket 连接
  331. socket_connection(token) {
  332. chat.state.chatService.emit(
  333. 'connection',
  334. {
  335. auth: 'admin',
  336. token: chat.state.config.token,
  337. },
  338. (res) => {
  339. debug('socket_connection:', res);
  340. if (res.code == 1) {
  341. chat.socket_check_identify();
  342. }
  343. },
  344. );
  345. },
  346. // 检测是否是客服 获取客服身份
  347. socket_check_identify() {
  348. chat.state.chatService.emit('check_identify', {}, (res) => {
  349. debug('socket_check_identify:', res);
  350. if (res.code == 1) {
  351. chat.socket_customer_login(res.data.customer_services[0]);
  352. chat.state.customerServiceIdentityList = res.data.customer_services.map((item) => ({
  353. label: item.name,
  354. value: item.id,
  355. room_id: item.room_id,
  356. }));
  357. }
  358. });
  359. },
  360. // 客服登录
  361. socket_customer_login(data) {
  362. chat.state.chatService.emit(
  363. 'customer_service_login',
  364. {
  365. room_id: data?.room_id || 'admin',
  366. },
  367. (res) => {
  368. debug('customer_service_login:', res);
  369. if (res.code == 1) {
  370. chat.state.currentCustomerService = res.data.customer_service;
  371. chat.state.commonWords = res.data.common_words;
  372. chat.socket_customer_init();
  373. }
  374. callBackNotice(res);
  375. },
  376. );
  377. },
  378. // 初始化客服
  379. socket_customer_init() {
  380. chat.state.chatService.emit('customer_service_init', {}, (res) => {
  381. if (res.code == 1) {
  382. chat.state.customerOnLineList = res.data.onlines;
  383. chat.state.customerWaitingList = res.data.waitings;
  384. if (chat.state.customerWaitingList.length > 0) {
  385. // audioPlay('chat');
  386. }
  387. chat.state.customerHistoryList = res.data.histories;
  388. chat.state.sessionList = res.data.onlines;
  389. // 当前顾客选中
  390. if (chat.state.sessionList.length) {
  391. if (!currentSessionTypeIndexs.hasOwnProperty(chat.state.sessionType)) {
  392. currentSessionTypeIndexs[chat.state.sessionType] = 0;
  393. }
  394. }
  395. chat.socket_change_customer_list({
  396. session: chat.state.sessionType,
  397. });
  398. }
  399. });
  400. },
  401. // 切换客服身份
  402. socket_change_customer_identity(data) {
  403. // 退出客服
  404. chat.socket_logout_customer();
  405. // 登录客服
  406. let info = chat.state.customerServiceIdentityList.filter((item) => data == item.value);
  407. chat.socket_customer_login(info[0]);
  408. // 初始化客服
  409. },
  410. // 切换会话列表
  411. socket_change_customer_list(data) {
  412. chat.socket_reset();
  413. chat.state.sessionType = data.session;
  414. switch (data.session) {
  415. case 'ing':
  416. chat.state.sessionList = chat.state.customerOnLineList;
  417. break;
  418. case 'waiting':
  419. chat.state.sessionList = chat.state.customerWaitingList;
  420. break;
  421. case 'history':
  422. chat.state.sessionList = chat.state.customerHistoryList;
  423. break;
  424. default:
  425. break;
  426. }
  427. if (!data.type) {
  428. // 默认显示第一个人的会话信息
  429. chat.state.sessionList.length && chat.socket_change_customer_info(data.index || 0);
  430. }
  431. },
  432. // 修改顾客信息,获取历史记录
  433. socket_change_customer_info(index = 0) {
  434. chat.socket_reset();
  435. chat.state.currentCustomer = chat.state.sessionList[index];
  436. if (chat.state.sessionList[index]) {
  437. chat.state.sessionList[index].unread_num = 0;
  438. }
  439. chat.socket_customer_history();
  440. },
  441. // 重置
  442. socket_reset() {
  443. chat.state.currentCustomer = null;
  444. chat.state.chatList = [];
  445. chat.state.historyPagination = {
  446. page: 0,
  447. list_rows: 10,
  448. last_id: 0,
  449. totalPage: 0,
  450. loadStatus: 'loadmore',
  451. };
  452. },
  453. // 退出客服
  454. socket_logout_customer() {
  455. debug('socket_logout_start');
  456. chat.state.chatService.emit('customer_service_logout', {}, (res) => {
  457. debug('socket_logout_res:', res);
  458. });
  459. },
  460. // 获取用户历史消息
  461. socket_customer_history() {
  462. if (!chat.state.currentCustomer) return;
  463. chat.state.historyPagination.loadStatus = 'loading';
  464. chat.state.historyPagination.page += 1;
  465. chat.state.chatService.emit(
  466. 'messages',
  467. {
  468. session_id: chat.state.currentCustomer?.session_id || 0,
  469. ...chat.state.historyPagination,
  470. },
  471. (res) => {
  472. if (res.code == 1) {
  473. chat.state.historyPagination.total = res.data.messages.total;
  474. chat.state.historyPagination.lastPage = res.data.messages.last_page;
  475. chat.state.historyPagination.page = res.data.messages.current_page;
  476. if (res.data.messages.current_page == 1) {
  477. chat.state.chatList = [];
  478. }
  479. res.data.messages.data.forEach((item) => {
  480. chat.state.chatList.unshift(item);
  481. });
  482. chat.state.historyPagination.loadStatus =
  483. chat.state.historyPagination.page < chat.state.historyPagination.lastPage
  484. ? 'loadmore'
  485. : 'nomore';
  486. chat.state.historyPagination.page == 1 && scrollBottom();
  487. if (chat.state.historyPagination.last_id == 0) {
  488. chat.state.historyPagination.last_id = res.data.messages.data.length
  489. ? res.data.messages.data[0].id
  490. : 0;
  491. }
  492. }
  493. },
  494. );
  495. },
  496. // 修改顾客状态
  497. socket_change_customer_state(data) {
  498. const { session_id, chat_user } = data;
  499. let waitingIndex = -1,
  500. onlineIndex = -1,
  501. historyIndex = -1;
  502. if (chat.state.customerWaitingList.length) {
  503. waitingIndex = chat.state.customerWaitingList.findIndex((item) => item.session_id == session_id);
  504. if (waitingIndex >= 0) {
  505. chat.state.customerWaitingList[waitingIndex].status = chat_user.status;
  506. }
  507. }
  508. if (chat.state.customerOnLineList.length) {
  509. onlineIndex = chat.state.customerOnLineList.findIndex((item) => item.session_id == session_id);
  510. if (onlineIndex >= 0) {
  511. chat.state.customerOnLineList[onlineIndex].status = chat_user.status;
  512. }
  513. }
  514. if (chat.state.customerHistoryList.length) {
  515. historyIndex = chat.state.customerHistoryList.findIndex((item) => item.session_id == session_id);
  516. if (historyIndex >= 0) {
  517. chat.state.customerHistoryList[historyIndex].status = chat_user.status;
  518. }
  519. }
  520. },
  521. // 发送消息
  522. socket_send(data) {
  523. // 给会话列表
  524. chat.state.isSendSucces = -1;
  525. chat.state.chatList.push(data);
  526. // 给socket
  527. chat.state.chatService.emit(
  528. 'message',
  529. {
  530. session_id: chat.state.currentCustomer?.session_id || 0,
  531. message: {
  532. message: data.message,
  533. message_type: data.message_type,
  534. },
  535. },
  536. (res) => {
  537. chat.state.isSendSucces = res.error;
  538. },
  539. );
  540. },
  541. // 断开,删除顾客
  542. socket_change_customer(session_id, index, sessionType, is_del_record) {
  543. if (sessionType == 'ing') {
  544. chat.state.chatService.emit('break_customer', { session_id }, (res) => {
  545. if (res.code == 1) {
  546. chat.state.customerOnLineList.splice(index, 1);
  547. chat.socket_reset();
  548. }
  549. callBackNotice(res);
  550. });
  551. }
  552. if (sessionType == 'history') {
  553. chat.state.chatService.emit('del_customer', { session_id, is_del_record }, (res) => {
  554. if (res.code == 1) {
  555. chat.state.customerHistoryList.splice(index, 1);
  556. chat.socket_reset();
  557. }
  558. callBackNotice(res);
  559. });
  560. }
  561. },
  562. // 切换客服状态
  563. socket_change_customer_service_status(status) {
  564. console.log(chat.state, 'chat.state')
  565. // return
  566. switch (status) {
  567. case 'online':
  568. chat.state.chatService.emit('customer_service_online', {}, (res) => {
  569. changeBack(res);
  570. });
  571. break;
  572. case 'busy':
  573. chat.state.chatService.emit('customer_service_busy', {}, (res) => {
  574. changeBack(res);
  575. });
  576. chat.state.sessionList = chat.state.customerWaitingList;
  577. break;
  578. case 'offline':
  579. chat.state.chatService.emit('customer_service_offline', {}, (res) => {
  580. changeBack(res);
  581. });
  582. chat.state.sessionList = chat.state.customerHistoryList;
  583. break;
  584. default:
  585. break;
  586. }
  587. // 切换回调
  588. function changeBack(res) {
  589. if (res.code == 1) chat.state.currentCustomerService = res.data.customer_service;
  590. callBackNotice(res);
  591. }
  592. },
  593. // access 接入客户,会触发监听接入,被接入
  594. socket_customer_access(index) {
  595. chat.state.chatService.emit(
  596. 'access',
  597. { session_id: chat.state.currentCustomer?.session_id },
  598. (res) => { },
  599. );
  600. },
  601. // transfer 转接顾客
  602. socket_transfer(customer_service_id) {
  603. chat.state.chatService.emit(
  604. 'transfer',
  605. {
  606. session_id: chat.state.currentCustomer?.session_id,
  607. customer_service_id,
  608. },
  609. (res) => {
  610. callBackNotice(res);
  611. if (res.code == 1) {
  612. const index = chat.state.customerOnLineList.findIndex(
  613. (item) => item.session_id == chat.state.currentCustomer?.session_id,
  614. );
  615. chat.state.customerOnLineList.splice(index, 1);
  616. chat.socket_reset();
  617. }
  618. },
  619. );
  620. },
  621. }
  622. const state = reactive({})
  623. // 打开客服
  624. const showChat = ref(false)
  625. function onShowChat() {
  626. showChat.value = !showChat.value;
  627. // 初始化聊天
  628. if (chat.state.connection.status != 'connect') {
  629. if (!chat.state.connection.isFlag) {
  630. chat.chatInit();
  631. }
  632. chat.open_chat();
  633. }
  634. }
  635. // 客服是否有未读消息
  636. const isChatUnreadNum = computed(() => {
  637. return chat.state.customerOnLineList.reduce((pre, cur) => {
  638. return pre + cur?.unread_num;
  639. }, chat.state.customerWaitingList.length);
  640. })
  641. // 修改客服状态
  642. const customerServiceStatus = {
  643. online: '在线',
  644. offline: '离线',
  645. busy: '忙碌',
  646. // disconnect: 'sa-duankailianjie',
  647. }
  648. function onChangeCustomerServiceStatus(status) {
  649. if (chat.state.currentCustomerService.status == status) return;
  650. chat.socket_change_customer_service_status(status);
  651. };
  652. // 切换客服身份
  653. function onChangeCustomerServiceIdentity(e) {
  654. chat.socket_change_customer_identity(e);
  655. };
  656. // 修改会话类型
  657. const sessionTypeList = {
  658. ing: {
  659. label: '会话中',
  660. left: '2px'
  661. },
  662. waiting: {
  663. label: '排队中',
  664. left: '52px'
  665. },
  666. history: {
  667. label: '历史',
  668. left: '102px'
  669. }
  670. }
  671. function onChangeSessionType(session, type = '') {
  672. if (chat.state.sessionType == session) return;
  673. if (!currentSessionTypeIndexs.hasOwnProperty(session)) {
  674. currentSessionTypeIndexs[session] = 0;
  675. }
  676. chat.socket_change_customer_list({
  677. session,
  678. type,
  679. index: currentSessionTypeIndexs[session],
  680. });
  681. }
  682. // 操作当前顾客
  683. const currentSessionTypeIndexs = reactive({});
  684. function onChangeCurrentSessionTypeIndex(index) {
  685. if (currentSessionTypeIndexs[chat.state.sessionType] == index) return;
  686. currentSessionTypeIndexs[chat.state.sessionType] = index;
  687. chat.socket_change_customer_info(index);
  688. };
  689. // 删除会话中顾客
  690. function onDeleteSession(session_id, index, sessionType) {
  691. chat.socket_change_customer(session_id, index, sessionType);
  692. }
  693. // 操作历史顾客
  694. const historyDeletePopover = reactive({
  695. flag: {},
  696. is_del_record: 0,
  697. });
  698. function onCancelHistoryDeletePopover(index) {
  699. historyDeletePopover.flag[index] = false;
  700. historyDeletePopover.is_del_record = 0;
  701. }
  702. function onConfirmHistoryDeletePopover(session_id, index, sessionType) {
  703. chat.socket_change_customer(
  704. session_id,
  705. index,
  706. sessionType,
  707. historyDeletePopover.is_del_record,
  708. );
  709. onCancelHistoryDeletePopover(index);
  710. }
  711. // 获取除了自己的客服列表
  712. const avaliableCustomerServicesList = computed(() => {
  713. return chat.state.customerServicesList.filter((item) => {
  714. return item.value != chat.state.currentCustomerService.id;
  715. });
  716. })
  717. // 转接客服
  718. const transferCustomer = ref(null);
  719. function onTransferCommand(val) {
  720. transferCustomer.value = val
  721. }
  722. function onTransferCustomer() {
  723. chat.socket_transfer(transferCustomer.value);
  724. };
  725. // 立即接入
  726. function onAccessCustomer() {
  727. chat.socket_customer_access();
  728. // 获取历史记录
  729. onChangeSessionType('ing', 'access');
  730. }
  731. // 加载更多
  732. function onLoadMore() {
  733. chat.state.historyPagination.page < chat.state.historyPagination.lastPage &&
  734. chat.socket_customer_history();
  735. };
  736. const showTime = (item, index) => {
  737. if (chat.state.chatList[index + 1]) {
  738. let dateString = Moment(chat.state.chatList[index + 1].createtime * 1000).fromNow();
  739. if (dateString == Moment(item.createtime * 1000).fromNow()) {
  740. return false;
  741. } else {
  742. dateString = Moment(item.createtime * 1000).fromNow();
  743. return true;
  744. }
  745. }
  746. return false;
  747. };
  748. // 格式化时间
  749. const formatTime = (time) => {
  750. let diffTime = Moment().unix() - time;
  751. if (diffTime > 28 * 24 * 60) {
  752. return Moment(time * 1000).format('MM/DD HH:mm');
  753. }
  754. if (diffTime > 360 * 28 * 24 * 60) {
  755. return Moment(time * 1000).format('YYYY/MM/DD HH:mm');
  756. }
  757. return Moment(time * 1000).fromNow();
  758. };
  759. function replaceEmoji(data) {
  760. let newData = data;
  761. if (typeof newData != 'object') {
  762. let reg = /\[(.+?)\]/g; // [] 中括号
  763. let zhEmojiName = newData.match(reg);
  764. if (zhEmojiName) {
  765. zhEmojiName.forEach((item) => {
  766. let emojiFile = selEmojiFile(item);
  767. newData = newData.replace(
  768. item,
  769. `<img class="message-emoji" src="${Fast.api.cdnurl(`/assets/addons/shopro/img/chat/emoji/${emojiFile}`)}" />`,
  770. );
  771. });
  772. }
  773. }
  774. return newData;
  775. }
  776. function selEmojiFile(name) {
  777. for (let index in emojiList) {
  778. if (emojiList[index].name == name) {
  779. return emojiList[index].file;
  780. }
  781. }
  782. return false;
  783. }
  784. const messageInput = ref('');
  785. function getMessageInput(e) {
  786. if (
  787. e.target.innerHTML.replace(/&nbsp;|\s/g, '') &&
  788. e.target.innerHTML.replace(/&nbsp;|\s/g, '').indexOf('<br>') != 0
  789. ) {
  790. messageInput.value = e.target;
  791. } else {
  792. messageInput.value = '';
  793. }
  794. };
  795. // 获取焦点
  796. function getMessageInputFocus() {
  797. if (window.getSelection) {
  798. let chatInput = proxy.$refs.messageInputRef;
  799. chatInput.focus();
  800. let range = window.getSelection();
  801. range.selectAllChildren(chatInput);
  802. range.collapseToEnd();
  803. } else if (document.selection) {
  804. let range = document.selection.createRange();
  805. range.moveToElementText(chatInput);
  806. range.collapse(false);
  807. range.select();
  808. }
  809. };
  810. // ctrl + enter 换行 ,enter发送
  811. function onKeyDown(e) {
  812. if (e.ctrlKey && e.keyCode == 13) {
  813. document.execCommand('insertHTML', false, '<br></br>');
  814. } else if (e.keyCode == 13) {
  815. // 阻止默认enter
  816. e.preventDefault();
  817. onSendMessage();
  818. return false;
  819. }
  820. }
  821. function onSendMessage() {
  822. if (!messageInput.value || !chat.state.currentCustomer) return;
  823. let res = '';
  824. let elemArr = Array.from(messageInput.value.childNodes);
  825. elemArr.forEach((child, index) => {
  826. if (child.nodeName == '#text') {
  827. res += child.nodeValue;
  828. if (elemArr[index + 1]?.nodeName == 'IMG' && elemArr[index + 1]?.name != 'emoji') {
  829. const data = {
  830. sender_identify: 'customer_service',
  831. message_type: 'text',
  832. message: res,
  833. createtime: Moment().unix(),
  834. };
  835. chat.socket_send(data);
  836. scrollBottom();
  837. res = '';
  838. }
  839. } else if (child.nodeName == 'BR') {
  840. res += '<br/>';
  841. } else if (child.nodeName == 'IMG') {
  842. if (child.name != 'emoji') {
  843. let srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i;
  844. let src = child.outerHTML.match(srcReg);
  845. const data = {
  846. sender_identify: 'customer_service',
  847. message_type: 'image',
  848. message: {
  849. url: Fast.api.cdnurl(src[1].replace(/http:\/\/[^\/]*/, ''), true),
  850. path: src[1].replace(/http:\/\/[^\/]*/, ''),
  851. },
  852. createtime: Moment().unix(),
  853. };
  854. chat.socket_send(data);
  855. scrollBottom();
  856. } else {
  857. res += child.outerHTML;
  858. }
  859. } else if (child.nodeName == 'DIV') {
  860. res += `<div style="width:200px; white-space: nowrap;">${child.outerHTML}</div>`;
  861. }
  862. });
  863. if (res) {
  864. const data = {
  865. sender_identify: 'customer_service',
  866. message_type: 'text',
  867. message: res,
  868. createtime: Moment().unix(),
  869. };
  870. chat.socket_send(data);
  871. }
  872. messageInput.value = '';
  873. proxy.$refs.messageInputRef.innerHTML = '';
  874. scrollBottom();
  875. }
  876. function onSelectToolbar(message_type, message) {
  877. !messageInput.value && getMessageInputFocus();
  878. let obj
  879. switch (message_type) {
  880. case 'emoji':
  881. let img = `<img src="${Fast.api.cdnurl('/assets/addons/shopro/img/chat/emoji/' + message.file, true)}" name="emoji" style="object-fit: cover;vertical-align:bottom; display: inline-block;width:20px !important;height:20px;margin:2px">`;
  882. document.execCommand('insertHTML', false, img);
  883. break;
  884. case 'text':
  885. obj = {
  886. sender_identify: 'customer_service',
  887. message_type,
  888. message,
  889. createtime: Moment().unix(),
  890. };
  891. chat.socket_send(obj);
  892. scrollBottom();
  893. break;
  894. case 'image':
  895. Fast.api.open(`general/attachment/select`, "选择", {
  896. callback: function (data) {
  897. obj = {
  898. sender_identify: 'customer_service',
  899. message_type,
  900. message: data.url,
  901. createtime: Moment().unix(),
  902. }
  903. chat.socket_send(obj);
  904. scrollBottom();
  905. }
  906. });
  907. break;
  908. case 'goods':
  909. Fast.api.open(`shopro/goods/goods/select`, "选择商品", {
  910. callback(data) {
  911. obj = {
  912. sender_identify: 'customer_service',
  913. message_type,
  914. message: {
  915. id: data.id,
  916. title: data.title,
  917. image: data.image,
  918. price: data.price,
  919. stock: data.stock,
  920. },
  921. createtime: Moment().unix(),
  922. };
  923. chat.socket_send(obj);
  924. scrollBottom();
  925. }
  926. })
  927. break;
  928. default:
  929. break;
  930. }
  931. }
  932. function onOpenGoodsDetail(id) {
  933. Fast.api.open(`shopro/goods/goods/add?type=edit&id=${id}`, "商品详情")
  934. }
  935. function onOpenOrderDetail(id) {
  936. Fast.api.open(`shopro/order/order/detail?id=${id}`, "订单详情")
  937. }
  938. function scrollBottom() {
  939. chat.state.connection.status == 'connect' &&
  940. nextTick(() => {
  941. console.log(proxy.$refs.chatScrollRef, '000')
  942. console.log(proxy.$refs.chatScrollRef['wrap$'].childNodes[0].offsetHeight, 1)
  943. console.log(proxy.$refs.chatScrollRef['wrap$'].offsetHeight, 2)
  944. let scrollTop = proxy.$refs.chatScrollRef['wrap$'].childNodes[0].offsetHeight - proxy.$refs.chatScrollRef['wrap$'].offsetHeight + 20
  945. console.log(scrollTop, 'scrollTop')
  946. proxy.$refs.chatScrollRef.setScrollTop(scrollTop);
  947. });
  948. }
  949. // 消息通知
  950. const showNotification = ref(false)
  951. function onShowNotification() {
  952. showNotification.value = true
  953. chat.state.notificationList = [];
  954. getNotificationType()
  955. }
  956. // 消息通知-是否有未读数据
  957. const isNotificationUnreadNum = computed(() => {
  958. return chat.state.notificationTypeList.reduce((pre, cur) => {
  959. return pre + cur.unread_num;
  960. }, 0);
  961. })
  962. // 消息通知-类型
  963. function getNotificationType() {
  964. Fast.api.ajax({
  965. url: 'shopro/notification/notification/notificationType',
  966. type: 'GET',
  967. }, function (ret, res) {
  968. chat.state.notificationTypeList = res.data.notification_type;
  969. chat.state.notificationType = res.data.notification_type[0].value
  970. // 请求列表
  971. pagination.page = 1;
  972. getNotificationList()
  973. return false
  974. }, function (ret, res) { })
  975. };
  976. // 消息通知-切换类型
  977. function onChangeNotificationType(type) {
  978. chat.state.notificationType = type
  979. chat.state.notificationList = [];
  980. pagination.page = 1;
  981. getNotificationList();
  982. };
  983. // 消息通知-列表
  984. const pagination = reactive({
  985. page: 0,
  986. lastPage: 0,
  987. loadStatus: 'loadmore', //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
  988. });
  989. function getNotificationList() {
  990. pagination.loadStatus = 'loading';
  991. Fast.api.ajax({
  992. url: 'shopro/notification/notification',
  993. type: 'GET',
  994. data: {
  995. search: JSON.stringify({ notification_type: chat.state.notificationType }),
  996. page: pagination.page,
  997. },
  998. }, function (ret, res) {
  999. if (pagination.page == 1) {
  1000. chat.state.notificationList = []
  1001. }
  1002. res.data.data.forEach((item) => {
  1003. chat.state.notificationList.push(item);
  1004. });
  1005. pagination.page = res.data.current_page;
  1006. pagination.lastPage = res.data.last_page;
  1007. pagination.loadStatus = pagination.page < pagination.lastPage ? 'loadmore' : 'nomore';
  1008. // 请求之后 把未读改为0
  1009. chat.state.notificationTypeList.map((item) => {
  1010. if (item.value == chat.state.notificationType) {
  1011. item.unread_num = 0;
  1012. }
  1013. });
  1014. return false
  1015. }, function (ret, res) { })
  1016. };
  1017. function onLoadMoreNotification() {
  1018. if (pagination.page < pagination.lastPage) {
  1019. pagination.page += 1;
  1020. getNotificationList();
  1021. }
  1022. };
  1023. // 消息通知-标记为已读消息
  1024. function onReadNotification(id, index) {
  1025. Fast.api.ajax({
  1026. url: `shopro/notification/notification/read/id/${id}`,
  1027. type: 'POST',
  1028. }, function (ret, res) {
  1029. chat.state.notificationList[index] = res.data;
  1030. return false
  1031. }, function (ret, res) { })
  1032. }
  1033. // 消息通知-清空已读消息
  1034. function onClearNotification() {
  1035. Fast.api.ajax({
  1036. url: 'shopro/notification/notification/delete',
  1037. type: 'DELETE',
  1038. }, function (ret, res) {
  1039. pagination.page = 1;
  1040. chat.state.notificationList = []
  1041. getNotificationList();
  1042. return false
  1043. }, function (ret, res) { })
  1044. };
  1045. return {
  1046. Fast,
  1047. emojiList,
  1048. chat,
  1049. state,
  1050. showChat,
  1051. onShowChat,
  1052. isChatUnreadNum,
  1053. customerServiceStatus,
  1054. onChangeCustomerServiceStatus,
  1055. onChangeCustomerServiceIdentity,
  1056. sessionTypeList,
  1057. onChangeSessionType,
  1058. currentSessionTypeIndexs,
  1059. onChangeCurrentSessionTypeIndex,
  1060. onDeleteSession,
  1061. historyDeletePopover,
  1062. onCancelHistoryDeletePopover,
  1063. onConfirmHistoryDeletePopover,
  1064. avaliableCustomerServicesList,
  1065. transferCustomer,
  1066. onTransferCommand,
  1067. onTransferCustomer,
  1068. onAccessCustomer,
  1069. onLoadMore,
  1070. showTime,
  1071. formatTime,
  1072. replaceEmoji,
  1073. selEmojiFile,
  1074. messageInput,
  1075. getMessageInput,
  1076. getMessageInputFocus,
  1077. onKeyDown,
  1078. onSendMessage,
  1079. onSelectToolbar,
  1080. onOpenGoodsDetail,
  1081. onOpenOrderDetail,
  1082. scrollBottom,
  1083. showNotification,
  1084. onShowNotification,
  1085. isNotificationUnreadNum,
  1086. onChangeNotificationType,
  1087. pagination,
  1088. onLoadMoreNotification,
  1089. onReadNotification,
  1090. onClearNotification,
  1091. loadingMap: {
  1092. loadmore: {
  1093. title: '查看更多',
  1094. icon: 'el-icon-arrow-left',
  1095. },
  1096. nomore: {
  1097. title: '没有更多了',
  1098. icon: '',
  1099. },
  1100. loading: {
  1101. title: '加载中... ',
  1102. icon: 'el-icon-loading',
  1103. },
  1104. },
  1105. }
  1106. }
  1107. }
  1108. return SaChat
  1109. })