socket.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. "use strict";
  2. const common_vendor = require("../../common/vendor.js");
  3. const sheep_index = require("../../sheep/index.js");
  4. function useChatWebSocket(socketConfig) {
  5. let SocketIo = null;
  6. const state = common_vendor.reactive({
  7. chatDotNum: 0,
  8. //总状态红点
  9. chatList: [],
  10. //会话信息
  11. customerUserInfo: {},
  12. //用户信息
  13. customerServerInfo: {
  14. //客服信息
  15. title: "连接中...",
  16. state: "connecting",
  17. avatar: null,
  18. nickname: ""
  19. },
  20. socketState: {
  21. isConnect: true,
  22. //是否连接成功
  23. isConnecting: false,
  24. //重连中,不允许新的socket开启。
  25. tip: ""
  26. },
  27. chatHistoryPagination: {
  28. page: 0,
  29. //当前页
  30. list_rows: 10,
  31. //每页条数
  32. last_id: 0,
  33. //最后条ID
  34. lastPage: 0,
  35. //总共多少页
  36. loadStatus: "loadmore"
  37. //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
  38. },
  39. templateChatList: [],
  40. //猜你想问
  41. chatConfig: {},
  42. // 配置信息
  43. isSendSucces: -1
  44. // 是否发送成功 -1=发送中|0=发送成功|1发送失败
  45. });
  46. const socketInit = (config, callBack) => {
  47. state.chatConfig = config;
  48. if (SocketIo && SocketIo.connected)
  49. return;
  50. if (state.socketState.isConnecting)
  51. return;
  52. SocketIo = common_vendor.io(config.chat_domain, {
  53. reconnection: true,
  54. // 默认 true 是否断线重连
  55. reconnectionAttempts: 5,
  56. // 默认无限次 断线尝试次数
  57. reconnectionDelay: 1e3,
  58. // 默认 1000,进行下一次重连的间隔。
  59. reconnectionDelayMax: 5e3,
  60. // 默认 5000, 重新连接等待的最长时间 默认 5000
  61. randomizationFactor: 0.5,
  62. // 默认 0.5 [0-1],随机重连延迟时间
  63. timeout: 2e4,
  64. // 默认 20s
  65. transports: ["websocket", "polling"],
  66. // websocket | polling,
  67. ...config
  68. });
  69. SocketIo.on("connect", async (res) => {
  70. socketReset(callBack);
  71. console.log("socket:connect");
  72. });
  73. SocketIo.on("message", (res) => {
  74. if (res.error === 0) {
  75. res.data;
  76. state.chatList.push(formatMessage(res.data.message));
  77. callBack && callBack();
  78. }
  79. });
  80. SocketIo.on("customer_service_access", (res) => {
  81. if (res.error === 0) {
  82. editCustomerServerInfo({
  83. title: res.data.customer_service.name,
  84. state: "online",
  85. avatar: res.data.customer_service.avatar
  86. });
  87. state.chatList.push(formatMessage(res.data.message));
  88. }
  89. });
  90. SocketIo.on("waiting_queue", (res) => {
  91. if (res.error === 0) {
  92. editCustomerServerInfo({
  93. title: res.data.title,
  94. state: "waiting",
  95. avatar: ""
  96. });
  97. }
  98. });
  99. SocketIo.on("no_customer_service", (res) => {
  100. if (res.error === 0) {
  101. editCustomerServerInfo({
  102. title: "暂无客服在线...",
  103. state: "waiting",
  104. avatar: ""
  105. });
  106. }
  107. state.chatList.push(formatMessage(res.data.message));
  108. });
  109. SocketIo.on("customer_service_online", (res) => {
  110. if (res.error === 0) {
  111. editCustomerServerInfo({
  112. title: res.data.customer_service.name,
  113. state: "online",
  114. avatar: res.data.customer_service.avatar
  115. });
  116. }
  117. });
  118. SocketIo.on("customer_service_offline", (res) => {
  119. if (res.error === 0) {
  120. editCustomerServerInfo({
  121. title: res.data.customer_service.name,
  122. state: "offline",
  123. avatar: res.data.customer_service.avatar
  124. });
  125. }
  126. });
  127. SocketIo.on("customer_service_busy", (res) => {
  128. if (res.error === 0) {
  129. editCustomerServerInfo({
  130. title: res.data.customer_service.name,
  131. state: "busy",
  132. avatar: res.data.customer_service.avatar
  133. });
  134. }
  135. });
  136. SocketIo.on("customer_service_break", (res) => {
  137. if (res.error === 0) {
  138. editCustomerServerInfo({
  139. title: "客服服务结束",
  140. state: "offline",
  141. avatar: ""
  142. });
  143. state.socketState.isConnect = false;
  144. state.socketState.tip = "当前服务已结束";
  145. }
  146. state.chatList.push(formatMessage(res.data.message));
  147. });
  148. SocketIo.on("custom_error", (error) => {
  149. editCustomerServerInfo({
  150. title: error.msg,
  151. state: "offline",
  152. avatar: ""
  153. });
  154. console.log("custom_error:", error);
  155. });
  156. SocketIo.on("error", (error) => {
  157. console.log("error:", error);
  158. });
  159. SocketIo.on("connect_error", (error) => {
  160. console.log("connect_error");
  161. });
  162. SocketIo.on("connect_timeout", (error) => {
  163. console.log(error, "connect_timeout");
  164. });
  165. SocketIo.on("disconnect", (error) => {
  166. console.log(error, "disconnect");
  167. });
  168. SocketIo.on("reconnect", (error) => {
  169. console.log(error, "reconnect");
  170. });
  171. SocketIo.on("reconnect_attempt", (error) => {
  172. state.socketState.isConnect = false;
  173. state.socketState.isConnecting = true;
  174. editCustomerServerInfo({
  175. title: `重连中,第${error}次尝试...`,
  176. state: "waiting",
  177. avatar: ""
  178. });
  179. console.log(error, "reconnect_attempt");
  180. });
  181. SocketIo.on("reconnecting", (error) => {
  182. console.log(error, "reconnecting");
  183. });
  184. SocketIo.on("reconnect_error", (error) => {
  185. console.log("reconnect_error");
  186. });
  187. SocketIo.on("reconnect_failed", (error) => {
  188. state.socketState.isConnecting = false;
  189. editCustomerServerInfo({
  190. title: `重连失败,请刷新重试~`,
  191. state: "waiting",
  192. avatar: ""
  193. });
  194. console.log(error, "reconnect_failed");
  195. state.isSendSucces = 1;
  196. });
  197. };
  198. const socketReset = (callBack) => {
  199. state.chatList = [];
  200. state.chatHistoryList = [];
  201. state.chatHistoryPagination = {
  202. page: 0,
  203. per_page: 10,
  204. last_id: 0,
  205. totalPage: 0
  206. };
  207. socketConnection(callBack);
  208. };
  209. const socketClose = () => {
  210. SocketIo.emit("customer_logout", {}, (res) => {
  211. console.log("socket:退出", res);
  212. });
  213. };
  214. const socketTest = () => {
  215. SocketIo.emit("test", {}, (res) => {
  216. console.log("test:test", res);
  217. });
  218. };
  219. const socketSendMsg = (data, sendMsgCallBack) => {
  220. state.isSendSucces = -1;
  221. state.chatList.push(data);
  222. sendMsgCallBack && sendMsgCallBack();
  223. SocketIo.emit(
  224. "message",
  225. {
  226. message: formatInput(data),
  227. ...data.customData
  228. },
  229. (res) => {
  230. state.isSendSucces = res.error;
  231. }
  232. );
  233. };
  234. const socketConnection = (callBack) => {
  235. SocketIo.emit(
  236. "connection",
  237. {
  238. auth: "user",
  239. token: common_vendor.index.getStorageSync("socketUserToken") || "",
  240. session_id: common_vendor.index.getStorageSync("socketSessionId") || ""
  241. },
  242. (res) => {
  243. if (res.error === 0) {
  244. socketCustomerLogin(callBack);
  245. common_vendor.index.setStorageSync("socketSessionId", res.data.session_id);
  246. state.customerUserInfo = res.data.chat_user;
  247. state.socketState.isConnect = true;
  248. } else {
  249. editCustomerServerInfo({
  250. title: `服务器异常!`,
  251. state: "waiting",
  252. avatar: ""
  253. });
  254. state.socketState.isConnect = false;
  255. }
  256. }
  257. );
  258. };
  259. const getUserToken = async (id) => {
  260. const res = await chat.unifiedToken();
  261. if (res.error === 0) {
  262. common_vendor.index.setStorageSync("socketUserToken", res.data.token);
  263. }
  264. return res;
  265. };
  266. const socketCustomerLogin = (callBack) => {
  267. SocketIo.emit(
  268. "customer_login",
  269. {
  270. room_id: state.chatConfig.room_id
  271. },
  272. (res) => {
  273. state.templateChatList = res.data.questions.length ? res.data.questions : [];
  274. state.chatList.push({
  275. from: "customer_service",
  276. // 用户customer右 | 顾客customer_service左 | 系统system中间
  277. mode: "template",
  278. // goods,order,image,text,system
  279. date: (/* @__PURE__ */ new Date()).getTime(),
  280. //时间
  281. content: {
  282. //内容
  283. list: state.templateChatList
  284. }
  285. });
  286. res.error === 0 && socketHistoryList(callBack);
  287. }
  288. );
  289. };
  290. const socketHistoryList = (historyCallBack) => {
  291. state.chatHistoryPagination.loadStatus = "loading";
  292. state.chatHistoryPagination.page += 1;
  293. SocketIo.emit("messages", state.chatHistoryPagination, (res) => {
  294. if (res.error === 0) {
  295. state.chatHistoryPagination.total = res.data.messages.total;
  296. state.chatHistoryPagination.lastPage = res.data.messages.last_page;
  297. state.chatHistoryPagination.page = res.data.messages.current_page;
  298. res.data.messages.data.forEach((item) => {
  299. item.message_type && state.chatList.unshift(formatMessage(item));
  300. });
  301. state.chatHistoryPagination.loadStatus = state.chatHistoryPagination.page < state.chatHistoryPagination.lastPage ? "loadmore" : "nomore";
  302. if (state.chatHistoryPagination.last_id == 0) {
  303. state.chatHistoryPagination.last_id = res.data.messages.data.length ? res.data.messages.data[0].id : 0;
  304. }
  305. state.chatHistoryPagination.page === 1 && historyCallBack && historyCallBack();
  306. }
  307. });
  308. };
  309. const editCustomerServerInfo = (data) => {
  310. state.customerServerInfo = {
  311. ...state.customerServerInfo,
  312. ...data
  313. };
  314. };
  315. const showTime = (item, index) => {
  316. if (common_vendor.unref(state.chatList)[index + 1]) {
  317. let dateString = common_vendor.dayjs(common_vendor.unref(state.chatList)[index + 1].date).fromNow();
  318. if (dateString === common_vendor.dayjs(common_vendor.unref(item).date).fromNow()) {
  319. return false;
  320. } else {
  321. dateString = common_vendor.dayjs(common_vendor.unref(item).date).fromNow();
  322. return true;
  323. }
  324. }
  325. return false;
  326. };
  327. const formatTime = (time) => {
  328. let diffTime = (/* @__PURE__ */ new Date()).getTime() - time;
  329. if (diffTime > 28 * 24 * 60 * 1e3) {
  330. return common_vendor.dayjs(time).format("MM/DD HH:mm");
  331. }
  332. if (diffTime > 360 * 28 * 24 * 60 * 1e3) {
  333. return common_vendor.dayjs(time).format("YYYY/MM/DD HH:mm");
  334. }
  335. return common_vendor.dayjs(time).fromNow();
  336. };
  337. const getFocus = (virtualNode) => {
  338. if (window.getSelection) {
  339. let chatInput2 = common_vendor.unref(virtualNode);
  340. chatInput2.focus();
  341. let range = window.getSelection();
  342. range.selectAllChildren(chatInput2);
  343. range.collapseToEnd();
  344. } else if (document.selection) {
  345. let range = document.selection.createRange();
  346. range.moveToElementText(chatInput);
  347. range.collapse(false);
  348. range.select();
  349. }
  350. };
  351. const upload = (name, file) => {
  352. return new Promise((resolve, reject) => {
  353. let data = new FormData();
  354. data.append("file", file, name);
  355. data.append("group", "chat");
  356. ajax({
  357. url: "/upload",
  358. method: "post",
  359. headers: {
  360. "Content-Type": "multipart/form-data"
  361. },
  362. data,
  363. success: function(res) {
  364. resolve(res);
  365. },
  366. error: function(err) {
  367. reject(err);
  368. }
  369. });
  370. });
  371. };
  372. const onPaste = async (e) => {
  373. let paste = e.clipboardData || window.clipboardData;
  374. let filesArr = Array.from(paste.files);
  375. filesArr.forEach(async (child) => {
  376. if (child && child.type.includes("image")) {
  377. e.preventDefault();
  378. let file = child;
  379. const img = await readImg(file);
  380. const blob = await compressImg(img, file.type);
  381. const { data } = await upload(file.name, blob);
  382. let image = `<img class="full-url" src='${data.fullurl}'>`;
  383. document.execCommand("insertHTML", false, image);
  384. } else {
  385. document.execCommand("insertHTML", false, paste.getData("text"));
  386. }
  387. });
  388. };
  389. const onDrop = async (e) => {
  390. e.preventDefault();
  391. let filesArr = Array.from(e.dataTransfer.files);
  392. filesArr.forEach(async (child) => {
  393. if (child && child.type.includes("image")) {
  394. let file = child;
  395. const img = await readImg(file);
  396. const blob = await compressImg(img, file.type);
  397. const { data } = await upload(file.name, blob);
  398. let image = `<img class="full-url" src='${data.fullurl}' >`;
  399. document.execCommand("insertHTML", false, image);
  400. } else {
  401. ElMessage({
  402. message: "禁止拖拽非图片资源",
  403. type: "warning"
  404. });
  405. }
  406. });
  407. };
  408. const formatChatInput = (virtualNode, formatInputCallBack) => {
  409. let res = "";
  410. let elemArr = Array.from(virtualNode.childNodes);
  411. elemArr.forEach((child, index) => {
  412. if (child.nodeName === "#text") {
  413. res += child.nodeValue;
  414. if (
  415. //文本节点的后面是图片,并且不是emoji,分开发送。输入框中的图片和文本表情分开。
  416. elemArr[index + 1] && elemArr[index + 1].nodeName === "IMG" && elemArr[index + 1] && elemArr[index + 1].name !== "emoji"
  417. ) {
  418. const data = {
  419. from: "customer",
  420. mode: "text",
  421. date: (/* @__PURE__ */ new Date()).getTime(),
  422. content: {
  423. text: filterXSS(res)
  424. }
  425. };
  426. formatInputCallBack && formatInputCallBack(data);
  427. res = "";
  428. }
  429. } else if (child.nodeName === "BR") {
  430. res += "<br/>";
  431. } else if (child.nodeName === "IMG") {
  432. if (child.name !== "emoji") {
  433. let srcReg = /src=[\'\']?([^\'\']*)[\'\']?/i;
  434. let src = child.outerHTML.match(srcReg);
  435. const data = {
  436. from: "customer",
  437. mode: "image",
  438. date: (/* @__PURE__ */ new Date()).getTime(),
  439. content: {
  440. url: src[1],
  441. path: src[1].replace(/http:\/\/[^\/]*/, "")
  442. }
  443. };
  444. formatInputCallBack && formatInputCallBack(data);
  445. } else {
  446. res += child.outerHTML;
  447. }
  448. } else if (child.nodeName === "DIV") {
  449. res += `<div style='width:200px; white-space: nowrap;'>${child.outerHTML}</div>`;
  450. }
  451. });
  452. if (res) {
  453. const data = {
  454. from: "customer",
  455. mode: "text",
  456. date: (/* @__PURE__ */ new Date()).getTime(),
  457. content: {
  458. text: filterXSS(res)
  459. }
  460. };
  461. formatInputCallBack && formatInputCallBack(data);
  462. }
  463. common_vendor.unref(virtualNode).innerHTML = "";
  464. };
  465. const callBackNotice = (res) => {
  466. ElNotification({
  467. title: "socket",
  468. message: res.msg,
  469. showClose: true,
  470. type: res.error === 0 ? "success" : "warning",
  471. duration: 1200
  472. });
  473. };
  474. const formatInput = (message) => {
  475. let obj = {};
  476. switch (message.mode) {
  477. case "text":
  478. obj = {
  479. message_type: "text",
  480. message: message.content.text
  481. };
  482. break;
  483. case "image":
  484. obj = {
  485. message_type: "image",
  486. message: message.content.path
  487. };
  488. break;
  489. case "goods":
  490. obj = {
  491. message_type: "goods",
  492. message: message.content.item
  493. };
  494. break;
  495. case "order":
  496. obj = {
  497. message_type: "order",
  498. message: message.content.item
  499. };
  500. break;
  501. }
  502. return obj;
  503. };
  504. const formatMessage = (message) => {
  505. let obj = {};
  506. switch (message.message_type) {
  507. case "system":
  508. obj = {
  509. from: "system",
  510. // 用户customer左 | 顾客customer_service右 | 系统system中间
  511. mode: "system",
  512. // goods,order,image,text,system
  513. date: message.create_time * 1e3,
  514. //时间
  515. content: {
  516. //内容
  517. text: message.message
  518. }
  519. };
  520. break;
  521. case "text":
  522. obj = {
  523. from: message.sender_identify,
  524. mode: message.message_type,
  525. date: message.create_time * 1e3,
  526. //时间
  527. sender: message.sender,
  528. content: {
  529. text: message.message,
  530. messageId: message.id
  531. }
  532. };
  533. break;
  534. case "image":
  535. obj = {
  536. from: message.sender_identify,
  537. mode: message.message_type,
  538. date: message.create_time * 1e3,
  539. //时间
  540. sender: message.sender,
  541. content: {
  542. url: sheep_index.sheep.$url.cdn(message.message),
  543. messageId: message.id
  544. }
  545. };
  546. break;
  547. case "goods":
  548. obj = {
  549. from: message.sender_identify,
  550. mode: message.message_type,
  551. date: message.create_time * 1e3,
  552. //时间
  553. sender: message.sender,
  554. content: {
  555. item: message.message,
  556. messageId: message.id
  557. }
  558. };
  559. break;
  560. case "order":
  561. obj = {
  562. from: message.sender_identify,
  563. mode: message.message_type,
  564. date: message.create_time * 1e3,
  565. //时间
  566. sender: message.sender,
  567. content: {
  568. item: message.message,
  569. messageId: message.id
  570. }
  571. };
  572. break;
  573. }
  574. return obj;
  575. };
  576. const readImg = (file) => {
  577. return new Promise((resolve, reject) => {
  578. const img = new Image();
  579. const reader = new FileReader();
  580. reader.onload = function(e) {
  581. img.src = e.target.result;
  582. };
  583. reader.onerror = function(e) {
  584. reject(e);
  585. };
  586. reader.readAsDataURL(file);
  587. img.onload = function() {
  588. resolve(img);
  589. };
  590. img.onerror = function(e) {
  591. reject(e);
  592. };
  593. });
  594. };
  595. const compressImg = (img, type = "image/jpeg", mx = 1e3, mh = 1e3, quality = 1) => {
  596. return new Promise((resolve, reject) => {
  597. const canvas = document.createElement("canvas");
  598. const context = canvas.getContext("2d");
  599. const { width: originWidth, height: originHeight } = img;
  600. const maxWidth = mx;
  601. const maxHeight = mh;
  602. let targetWidth = originWidth;
  603. let targetHeight = originHeight;
  604. if (originWidth > maxWidth || originHeight > maxHeight) {
  605. if (originWidth / originHeight > 1) {
  606. targetWidth = maxWidth;
  607. targetHeight = Math.round(maxWidth * (originHeight / originWidth));
  608. } else {
  609. targetHeight = maxHeight;
  610. targetWidth = Math.round(maxHeight * (originWidth / originHeight));
  611. }
  612. }
  613. canvas.width = targetWidth;
  614. canvas.height = targetHeight;
  615. context.clearRect(0, 0, targetWidth, targetHeight);
  616. context.drawImage(img, 0, 0, targetWidth, targetHeight);
  617. canvas.toBlob(
  618. function(blob) {
  619. resolve(blob);
  620. },
  621. type,
  622. quality
  623. );
  624. });
  625. };
  626. return {
  627. compressImg,
  628. readImg,
  629. formatMessage,
  630. formatInput,
  631. callBackNotice,
  632. socketInit,
  633. socketSendMsg,
  634. socketClose,
  635. socketHistoryList,
  636. getFocus,
  637. formatChatInput,
  638. onDrop,
  639. onPaste,
  640. upload,
  641. getUserToken,
  642. state,
  643. socketTest,
  644. showTime,
  645. formatTime
  646. };
  647. }
  648. exports.useChatWebSocket = useChatWebSocket;