import { reactive, ref, unref } from 'vue'; import sheep from '@/sheep'; // import chat from '@/sheep/api/chat'; import dayjs from 'dayjs'; import io from '@hyoga/uni-socket.io'; export function useChatWebSocket(socketConfig) { let SocketIo = null; // chat状态数据 const state = reactive({ chatDotNum: 0, //总状态红点 chatList: [], //会话信息 customerUserInfo: {}, //用户信息 customerServerInfo: { //客服信息 title: '连接中...', state: 'connecting', avatar: null, nickname: '', }, socketState: { isConnect: true, //是否连接成功 isConnecting: false, //重连中,不允许新的socket开启。 tip: '', }, chatHistoryPagination: { page: 0, //当前页 list_rows: 10, //每页条数 last_id: 0, //最后条ID lastPage: 0, //总共多少页 loadStatus: 'loadmore', //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态 }, templateChatList: [], //猜你想问 chatConfig: {}, // 配置信息 isSendSucces: -1, // 是否发送成功 -1=发送中|0=发送成功|1发送失败 }); /** * 连接初始化 * @param {Object} config - 配置信息 * @param {Function} callBack -回调函数,有新消息接入,保持底部 */ const socketInit = (config, callBack) => { state.chatConfig = config; if (SocketIo && SocketIo.connected) return; // 如果socket已经连接,返回false if (state.socketState.isConnecting) return; // 重连中,返回false // 启动初始化 SocketIo = io(config.chat_domain, { reconnection: true, // 默认 true 是否断线重连 reconnectionAttempts: 5, // 默认无限次 断线尝试次数 reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。 reconnectionDelayMax: 5000, // 默认 5000, 重新连接等待的最长时间 默认 5000 randomizationFactor: 0.5, // 默认 0.5 [0-1],随机重连延迟时间 timeout: 20000, // 默认 20s transports: ['websocket', 'polling'], // websocket | polling, ...config, }); // 监听连接 SocketIo.on('connect', async (res) => { socketReset(callBack); // socket连接 // 用户登录 // 顾客登录 console.log('socket:connect'); }); // 监听消息 SocketIo.on('message', (res) => { if (res.error === 0) { const { message, sender } = res.data; state.chatList.push(formatMessage(res.data.message)); // 告诉父级页面 // window.parent.postMessage({ // chatDotNum: ++state.chatDotNum // }) callBack && callBack(); } }); // 监听客服接入成功 SocketIo.on('customer_service_access', (res) => { if (res.error === 0) { editCustomerServerInfo({ title: res.data.customer_service.name, state: 'online', avatar: res.data.customer_service.avatar, }); state.chatList.push(formatMessage(res.data.message)); // callBack && callBack() } }); // 监听排队等待 SocketIo.on('waiting_queue', (res) => { if (res.error === 0) { editCustomerServerInfo({ title: res.data.title, state: 'waiting', avatar: '', }); // callBack && callBack() } }); // 监听没有客服在线 SocketIo.on('no_customer_service', (res) => { if (res.error === 0) { editCustomerServerInfo({ title: '暂无客服在线...', state: 'waiting', avatar: '', }); } state.chatList.push(formatMessage(res.data.message)); // callBack && callBack() }); // 监听客服上线 SocketIo.on('customer_service_online', (res) => { if (res.error === 0) { editCustomerServerInfo({ title: res.data.customer_service.name, state: 'online', avatar: res.data.customer_service.avatar, }); } }); // 监听客服下线 SocketIo.on('customer_service_offline', (res) => { if (res.error === 0) { editCustomerServerInfo({ title: res.data.customer_service.name, state: 'offline', avatar: res.data.customer_service.avatar, }); } }); // 监听客服忙碌 SocketIo.on('customer_service_busy', (res) => { if (res.error === 0) { editCustomerServerInfo({ title: res.data.customer_service.name, state: 'busy', avatar: res.data.customer_service.avatar, }); } }); // 监听客服断开链接 SocketIo.on('customer_service_break', (res) => { if (res.error === 0) { editCustomerServerInfo({ title: '客服服务结束', state: 'offline', avatar: '', }); state.socketState.isConnect = false; state.socketState.tip = '当前服务已结束'; } state.chatList.push(formatMessage(res.data.message)); // callBack && callBack() }); // 监听自定义错误 custom_error SocketIo.on('custom_error', (error) => { editCustomerServerInfo({ title: error.msg, state: 'offline', avatar: '', }); console.log('custom_error:', error); }); // 监听错误 error SocketIo.on('error', (error) => { console.log('error:', error); }); // 重连失败 connect_error SocketIo.on('connect_error', (error) => { console.log('connect_error'); }); // 连接上,但无反应 connect_timeout SocketIo.on('connect_timeout', (error) => { console.log(error, 'connect_timeout'); }); // 服务进程销毁 disconnect SocketIo.on('disconnect', (error) => { console.log(error, 'disconnect'); }); // 服务重启重连上reconnect SocketIo.on('reconnect', (error) => { console.log(error, 'reconnect'); }); // 开始重连reconnect_attempt SocketIo.on('reconnect_attempt', (error) => { state.socketState.isConnect = false; state.socketState.isConnecting = true; editCustomerServerInfo({ title: `重连中,第${error}次尝试...`, state: 'waiting', avatar: '', }); console.log(error, 'reconnect_attempt'); }); // 重新连接中reconnecting SocketIo.on('reconnecting', (error) => { console.log(error, 'reconnecting'); }); // 重新连接错误reconnect_error SocketIo.on('reconnect_error', (error) => { console.log('reconnect_error'); }); // 重新连接失败reconnect_failed SocketIo.on('reconnect_failed', (error) => { state.socketState.isConnecting = false; editCustomerServerInfo({ title: `重连失败,请刷新重试~`, state: 'waiting', avatar: '', }); console.log(error, 'reconnect_failed'); // setTimeout(() => { state.isSendSucces = 1; // }, 500) }); }; // 重置socket const socketReset = (callBack) => { state.chatList = []; state.chatHistoryList = []; state.chatHistoryPagination = { page: 0, per_page: 10, last_id: 0, totalPage: 0, }; socketConnection(callBack); // 连接 }; // 退出连接 const socketClose = () => { SocketIo.emit('customer_logout', {}, (res) => { console.log('socket:退出', res); }); }; // 测试事件 const socketTest = () => { SocketIo.emit('test', {}, (res) => { console.log('test:test', res); }); }; // 发送消息 const socketSendMsg = (data, sendMsgCallBack) => { state.isSendSucces = -1; state.chatList.push(data); sendMsgCallBack && sendMsgCallBack(); SocketIo.emit( 'message', { message: formatInput(data), ...data.customData, }, (res) => { // setTimeout(() => { state.isSendSucces = res.error; // }, 500) // console.log(res, 'socket:send'); // sendMsgCallBack && sendMsgCallBack() }, ); }; // 连接socket,存入sessionId const socketConnection = (callBack) => { SocketIo.emit( 'connection', { auth: 'user', token: uni.getStorageSync('socketUserToken') || '', session_id: uni.getStorageSync('socketSessionId') || '', }, (res) => { if (res.error === 0) { socketCustomerLogin(callBack); uni.setStorageSync('socketSessionId', res.data.session_id); // uni.getStorageSync('socketUserToken') && socketLogin(uni.getStorageSync( // 'socketUserToken')) // 如果有用户token,绑定 state.customerUserInfo = res.data.chat_user; state.socketState.isConnect = true; } else { editCustomerServerInfo({ title: `服务器异常!`, state: 'waiting', avatar: '', }); state.socketState.isConnect = false; } }, ); }; // 用户id,获取token const getUserToken = async (id) => { const res = await chat.unifiedToken(); if (res.error === 0) { uni.setStorageSync('socketUserToken', res.data.token); // SocketIo && SocketIo.connected && socketLogin(res.data.token) } return res; }; // 用户登录 const socketLogin = (token) => { SocketIo.emit( 'login', { token: token, }, (res) => { console.log(res, 'socket:login'); state.customerUserInfo = res.data.chat_user; }, ); }; // 顾客登录 const socketCustomerLogin = (callBack) => { SocketIo.emit( 'customer_login', { room_id: state.chatConfig.room_id, }, (res) => { state.templateChatList = res.data.questions.length ? res.data.questions : []; state.chatList.push({ from: 'customer_service', // 用户customer右 | 顾客customer_service左 | 系统system中间 mode: 'template', // goods,order,image,text,system date: new Date().getTime(), //时间 content: { //内容 list: state.templateChatList, }, }); res.error === 0 && socketHistoryList(callBack); }, ); }; // 获取历史消息 const socketHistoryList = (historyCallBack) => { state.chatHistoryPagination.loadStatus = 'loading'; state.chatHistoryPagination.page += 1; SocketIo.emit('messages', state.chatHistoryPagination, (res) => { if (res.error === 0) { state.chatHistoryPagination.total = res.data.messages.total; state.chatHistoryPagination.lastPage = res.data.messages.last_page; state.chatHistoryPagination.page = res.data.messages.current_page; res.data.messages.data.forEach((item) => { item.message_type && state.chatList.unshift(formatMessage(item)); }); state.chatHistoryPagination.loadStatus = state.chatHistoryPagination.page < state.chatHistoryPagination.lastPage ? 'loadmore' : 'nomore'; if (state.chatHistoryPagination.last_id == 0) { state.chatHistoryPagination.last_id = res.data.messages.data.length ? res.data.messages.data[0].id : 0; } state.chatHistoryPagination.page === 1 && historyCallBack && historyCallBack(); } // 历史记录之后,猜你想问 // state.chatList.push({ // from: 'customer_service', // 用户customer右 | 顾客customer_service左 | 系统system中间 // mode: 'template', // goods,order,image,text,system // date: new Date().getTime(), //时间 // content: { //内容 // list: state.templateChatList // } // }) }); }; // 修改客服信息 const editCustomerServerInfo = (data) => { state.customerServerInfo = { ...state.customerServerInfo, ...data, }; }; /** * ================ * 工具函数 ↓ * =============== */ /** * 是否显示时间 * @param {*} item - 数据 * @param {*} index - 索引 */ const showTime = (item, index) => { if (unref(state.chatList)[index + 1]) { let dateString = dayjs(unref(state.chatList)[index + 1].date).fromNow(); if (dateString === dayjs(unref(item).date).fromNow()) { return false; } else { dateString = dayjs(unref(item).date).fromNow(); return true; } } return false; }; /** * 格式化时间 * @param {*} time - 时间戳 */ const formatTime = (time) => { let diffTime = new Date().getTime() - time; if (diffTime > 28 * 24 * 60 * 1000) { return dayjs(time).format('MM/DD HH:mm'); } if (diffTime > 360 * 28 * 24 * 60 * 1000) { return dayjs(time).format('YYYY/MM/DD HH:mm'); } return dayjs(time).fromNow(); }; /** * 获取焦点 * @param {*} virtualNode - 节点信息 ref */ const getFocus = (virtualNode) => { if (window.getSelection) { let chatInput = unref(virtualNode); chatInput.focus(); let range = window.getSelection(); range.selectAllChildren(chatInput); range.collapseToEnd(); } else if (document.selection) { let range = document.selection.createRange(); range.moveToElementText(chatInput); range.collapse(false); range.select(); } }; /** * 文件上传 * @param {Blob} file -文件数据流 * @return {path,fullPath} */ const upload = (name, file) => { return new Promise((resolve, reject) => { let data = new FormData(); data.append('file', file, name); data.append('group', 'chat'); ajax({ url: '/upload', method: 'post', headers: { 'Content-Type': 'multipart/form-data', }, data, success: function (res) { resolve(res); }, error: function (err) { reject(err); }, }); }); }; /** * 粘贴到输入框 * @param {*} e - 粘贴内容 * @param {*} uploadHttp - 上传图片地址 */ const onPaste = async (e) => { let paste = e.clipboardData || window.clipboardData; let filesArr = Array.from(paste.files); filesArr.forEach(async (child) => { if (child && child.type.includes('image')) { e.preventDefault(); //阻止默认 let file = child; const img = await readImg(file); const blob = await compressImg(img, file.type); const { data } = await upload(file.name, blob); let image = ``; document.execCommand('insertHTML', false, image); } else { document.execCommand('insertHTML', false, paste.getData('text')); } }); }; /** * 拖拽到输入框 * @param {*} e - 粘贴内容 * @param {*} uploadHttp - 上传图片地址 */ const onDrop = async (e) => { e.preventDefault(); //阻止默认 let filesArr = Array.from(e.dataTransfer.files); filesArr.forEach(async (child) => { if (child && child.type.includes('image')) { let file = child; const img = await readImg(file); const blob = await compressImg(img, file.type); const { data } = await upload(file.name, blob); let image = ``; document.execCommand('insertHTML', false, image); } else { ElMessage({ message: '禁止拖拽非图片资源', type: 'warning', }); } }); }; /** * 解析富文本输入框内容 * @param {*} virtualNode -节点信息 * @param {Function} formatInputCallBack - cb 回调 */ const formatChatInput = (virtualNode, formatInputCallBack) => { let res = ''; let elemArr = Array.from(virtualNode.childNodes); elemArr.forEach((child, index) => { if (child.nodeName === '#text') { //如果为文本节点 res += child.nodeValue; if ( //文本节点的后面是图片,并且不是emoji,分开发送。输入框中的图片和文本表情分开。 elemArr[index + 1] && elemArr[index + 1].nodeName === 'IMG' && elemArr[index + 1] && elemArr[index + 1].name !== 'emoji' ) { const data = { from: 'customer', mode: 'text', date: new Date().getTime(), content: { text: filterXSS(res), }, }; formatInputCallBack && formatInputCallBack(data); res = ''; } } else if (child.nodeName === 'BR') { res += '
'; } else if (child.nodeName === 'IMG') { // 有emjio 和 一般图片 // 图片解析后直接发送,不跟文字表情一组 if (child.name !== 'emoji') { let srcReg = /src=[\'\']?([^\'\']*)[\'\']?/i; let src = child.outerHTML.match(srcReg); const data = { from: 'customer', mode: 'image', date: new Date().getTime(), content: { url: src[1], path: src[1].replace(/http:\/\/[^\/]*/, ''), }, }; formatInputCallBack && formatInputCallBack(data); } else { // 非表情图片跟文字一起发送 res += child.outerHTML; } } else if (child.nodeName === 'DIV') { res += `
${child.outerHTML}
`; } }); if (res) { const data = { from: 'customer', mode: 'text', date: new Date().getTime(), content: { text: filterXSS(res), }, }; formatInputCallBack && formatInputCallBack(data); } unref(virtualNode).innerHTML = ''; }; /** * 状态回调 * @param {*} res -接口返回数据 */ const callBackNotice = (res) => { ElNotification({ title: 'socket', message: res.msg, showClose: true, type: res.error === 0 ? 'success' : 'warning', duration: 1200, }); }; /** * 格式化发送信息 * @param {Object} message * @returns obj - 消息对象 */ const formatInput = (message) => { let obj = {}; switch (message.mode) { case 'text': obj = { message_type: 'text', message: message.content.text, }; break; case 'image': obj = { message_type: 'image', message: message.content.path, }; break; case 'goods': obj = { message_type: 'goods', message: message.content.item, }; break; case 'order': obj = { message_type: 'order', message: message.content.item, }; break; default: break; } return obj; }; /** * 格式化接收信息 * @param {*} message * @returns obj - 消息对象 */ const formatMessage = (message) => { let obj = {}; switch (message.message_type) { case 'system': obj = { from: 'system', // 用户customer左 | 顾客customer_service右 | 系统system中间 mode: 'system', // goods,order,image,text,system date: message.create_time * 1000, //时间 content: { //内容 text: message.message, }, }; break; case 'text': obj = { from: message.sender_identify, mode: message.message_type, date: message.create_time * 1000, //时间 sender: message.sender, content: { text: message.message, messageId: message.id, }, }; break; case 'image': obj = { from: message.sender_identify, mode: message.message_type, date: message.create_time * 1000, //时间 sender: message.sender, content: { url: sheep.$url.cdn(message.message), messageId: message.id, }, }; break; case 'goods': obj = { from: message.sender_identify, mode: message.message_type, date: message.create_time * 1000, //时间 sender: message.sender, content: { item: message.message, messageId: message.id, }, }; break; case 'order': obj = { from: message.sender_identify, mode: message.message_type, date: message.create_time * 1000, //时间 sender: message.sender, content: { item: message.message, messageId: message.id, }, }; break; default: break; } return obj; }; /** * file 转换为 img * @param {*} file - file 文件 * @returns img - img标签 */ const readImg = (file) => { return new Promise((resolve, reject) => { const img = new Image(); const reader = new FileReader(); reader.onload = function (e) { img.src = e.target.result; }; reader.onerror = function (e) { reject(e); }; reader.readAsDataURL(file); img.onload = function () { resolve(img); }; img.onerror = function (e) { reject(e); }; }); }; /** * 压缩图片 *@param img -被压缩的img对象 * @param type -压缩后转换的文件类型 * @param mx -触发压缩的图片最大宽度限制 * @param mh -触发压缩的图片最大高度限制 * @returns blob - 文件流 */ const compressImg = (img, type = 'image/jpeg', mx = 1000, mh = 1000, quality = 1) => { return new Promise((resolve, reject) => { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const { width: originWidth, height: originHeight } = img; // 最大尺寸限制 const maxWidth = mx; const maxHeight = mh; // 目标尺寸 let targetWidth = originWidth; let targetHeight = originHeight; if (originWidth > maxWidth || originHeight > maxHeight) { if (originWidth / originHeight > 1) { // 宽图片 targetWidth = maxWidth; targetHeight = Math.round(maxWidth * (originHeight / originWidth)); } else { // 高图片 targetHeight = maxHeight; targetWidth = Math.round(maxHeight * (originWidth / originHeight)); } } canvas.width = targetWidth; canvas.height = targetHeight; context.clearRect(0, 0, targetWidth, targetHeight); // 图片绘制 context.drawImage(img, 0, 0, targetWidth, targetHeight); canvas.toBlob( function (blob) { resolve(blob); }, type, quality, ); }); }; return { compressImg, readImg, formatMessage, formatInput, callBackNotice, socketInit, socketSendMsg, socketClose, socketHistoryList, getFocus, formatChatInput, onDrop, onPaste, upload, getUserToken, state, socketTest, showTime, formatTime, }; }