diff --git a/config/config.UAT.ts b/config/config.UAT.ts index 22b07c1..de9d1a0 100644 --- a/config/config.UAT.ts +++ b/config/config.UAT.ts @@ -19,5 +19,16 @@ export default defineConfig({ //询价查看报价跳转地址 REACT_APP_XUNJIA_REDIRECT: 'http://10.0.204.215:8280/provider_uat', + + //智慧客服ws地址 + REACT_APP_CUSTOMERSERVICE_WS_REDIRECT: 'ws://10.242.31.158:18022/api/api/biz-customer-service', + + //智慧客服文档中心查看图片地址 + REACT_APP_CUSTOMERSERVICE_DOC_REDIRECT: 'http://cos.gz-tst.cos.tg.unicom.local/349553515466:mall/', + + //智慧客服加密公钥私钥ciphercode + REACT_APP_CUSTOMERSERVICE_PUBLICKEY : '0428D625CEEB71CE823BD7D78DFEE7B122F2DA5C4D21E32253AD684D0FE21810394A799639C0CDFBFEB535A1DFD6A366A637E582CE0B1466A5FE7858841135DE6B', + REACT_APP_CUSTOMERSERVICE_PRIVATEKEY : '4F7144028D4DCF88FA50F0E2B3FFDDCF63BBE17D1700537DCE037687D3AA3DA7', + REACT_APP_CUSTOMERSERVICE_CIPHERCODE : 1, }, }); diff --git a/config/config.dev.ts b/config/config.dev.ts index 971d818..574135e 100644 --- a/config/config.dev.ts +++ b/config/config.dev.ts @@ -19,5 +19,16 @@ export default defineConfig({ //询价查看报价跳转地址 REACT_APP_XUNJIA_REDIRECT: 'http://10.0.204.215:8080/provider_dev', + + //智慧客服ws地址 + REACT_APP_CUSTOMERSERVICE_WS_REDIRECT: 'ws://10.242.37.148:18022/api/api/biz-customer-service', + + //智慧客服文档中心查看图片地址 + REACT_APP_CUSTOMERSERVICE_DOC_REDIRECT: 'http://cos.gz-tst.cos.tg.unicom.local/349553515466:mall/', + + //智慧客服加密公钥私钥ciphercode + REACT_APP_CUSTOMERSERVICE_PUBLICKEY : '0428D625CEEB71CE823BD7D78DFEE7B122F2DA5C4D21E32253AD684D0FE21810394A799639C0CDFBFEB535A1DFD6A366A637E582CE0B1466A5FE7858841135DE6B', + REACT_APP_CUSTOMERSERVICE_PRIVATEKEY : '4F7144028D4DCF88FA50F0E2B3FFDDCF63BBE17D1700537DCE037687D3AA3DA7', + REACT_APP_CUSTOMERSERVICE_CIPHERCODE : 1, }, }); diff --git a/config/config.prod.ts b/config/config.prod.ts index 99e7610..c81cdac 100644 --- a/config/config.prod.ts +++ b/config/config.prod.ts @@ -19,5 +19,16 @@ export default defineConfig({ //询价查看报价跳转地址 REACT_APP_XUNJIA_REDIRECT: 'https://60.10.26.178/provider', + + //智慧客服ws地址 + REACT_APP_CUSTOMERSERVICE_WS_REDIRECT: 'ws://uscm.unicom.local:18022/api/api/biz-customer-service', + + //智慧客服文档中心查看图片地址 + REACT_APP_CUSTOMERSERVICE_DOC_REDIRECT: 'http://cos.xx-pbc.cos.tg.unicom.local/349553515466:mall/', + + //智慧客服加密公钥私钥ciphercode + REACT_APP_CUSTOMERSERVICE_PUBLICKEY : '0428D625CEEB71CE823BD7D78DFEE7B122F2DA5C4D21E32253AD684D0FE21810394A799639C0CDFBFEB535A1DFD6A366A637E582CE0B1466A5FE7858841135DE6B', + REACT_APP_CUSTOMERSERVICE_PRIVATEKEY : '4F7144028D4DCF88FA50F0E2B3FFDDCF63BBE17D1700537DCE037687D3AA3DA7', + REACT_APP_CUSTOMERSERVICE_CIPHERCODE : 1, }, }); diff --git a/config/config.sim.ts b/config/config.sim.ts index bce3024..9117142 100644 --- a/config/config.sim.ts +++ b/config/config.sim.ts @@ -19,5 +19,16 @@ export default defineConfig({ //询价查看报价跳转地址 REACT_APP_XUNJIA_REDIRECT: 'http://10.0.204.215:8080/provider', + + //智慧客服ws地址 + REACT_APP_CUSTOMERSERVICE_WS_REDIRECT: 'ws://10.242.31.54:18022/api/api/biz-customer-service', + + //智慧客服文档中心查看图片地址 + REACT_APP_CUSTOMERSERVICE_DOC_REDIRECT: 'http://cos.xx-pbc.cos.tg.unicom.local/349553515466:mall/', + + //智慧客服加密公钥私钥ciphercode + REACT_APP_CUSTOMERSERVICE_PUBLICKEY : '0428D625CEEB71CE823BD7D78DFEE7B122F2DA5C4D21E32253AD684D0FE21810394A799639C0CDFBFEB535A1DFD6A366A637E582CE0B1466A5FE7858841135DE6B', + REACT_APP_CUSTOMERSERVICE_PRIVATEKEY : '4F7144028D4DCF88FA50F0E2B3FFDDCF63BBE17D1700537DCE037687D3AA3DA7', + REACT_APP_CUSTOMERSERVICE_CIPHERCODE : 1, }, }); diff --git a/src/assets/img/cs_client.png b/src/assets/img/cs_client.png new file mode 100644 index 0000000..97cb5d7 Binary files /dev/null and b/src/assets/img/cs_client.png differ diff --git a/src/assets/img/cs_user.png b/src/assets/img/cs_user.png new file mode 100644 index 0000000..abb7ffe Binary files /dev/null and b/src/assets/img/cs_user.png differ diff --git a/src/customerServiceHelpers/constants/http.js b/src/customerServiceHelpers/constants/http.js new file mode 100644 index 0000000..bfff3b2 --- /dev/null +++ b/src/customerServiceHelpers/constants/http.js @@ -0,0 +1,25 @@ +// HTTP状态码映射(可补充) +export const HTTP_STATUS_CODE_MAP = { + 200: '服务请求成功!', + 201: '新建或修改数据成功!', + 202: '请求已进入后台排队中!', + 204: '删除数据成功!', + 400: '发出的请求有错误,未能执行相关操作!', + 401: '无权限访问,请重新登录!', + 403: '已得到授权,但访问被禁止!', + 404: '请求失败,未找到相关资源!', + 406: '请求格式不可得!', + 410: '请求的资源已被永久删除!', + 422: '请求过程中出现验证错误!', + 500: '服务器发生错误!', + 502: '网关错误!', + 503: '服务不可用,服务器暂时过载或正在维护!', + 504: '网关超时!', + }; + + // 携带着请求体的请求方法 + export const WITH_REQUEST_BODY_METHODS = ['post', 'put', 'patch']; + + // 请求后端接口失败时对应的几种code值(与后端协定) + export const RESPONSE_FAILED_CODES = [400, 500]; + \ No newline at end of file diff --git a/src/customerServiceHelpers/request.js b/src/customerServiceHelpers/request.js new file mode 100644 index 0000000..cf9cba8 --- /dev/null +++ b/src/customerServiceHelpers/request.js @@ -0,0 +1,110 @@ +import axios from 'axios'; +import { WITH_REQUEST_BODY_METHODS } from './constants/http'; + + +function paddingBaseURL(config) { + return Object.assign(config); +} + +/** + * 封装通用的axios公共方法 (需要结合业务逻辑来进一步完善) + * @param {Object} config - 非通用性(自定义)的一些axios请求配置 + * @returns {Promise} + */ +const $axios = function (config) { + + let defaultConfig = paddingBaseURL(config); + + if (WITH_REQUEST_BODY_METHODS.includes(config.method)) { + if (!config.data && config.params) { + defaultConfig.data = config.params; + defaultConfig.params = undefined; + } + } + + return new Promise((resolve, reject) => { + axios(defaultConfig) + .then(result => { + const { data } = result; + resolve(data); + }) + .catch(err => { + reject(err); + }); + }); +}; + +export function $get(url, options) { + return $axios({ ...options, url, method: 'get' }); +} + +export function $post(url, options) { + return $axios({ ...options, url, method: 'post' }); +} + +export function $put(url, options) { + return $axios({ ...options, url, method: 'put' }); +} + +export function $patch(url, options) { + return $axios({ ...options, url, method: 'patch' }); +} + +export function $delete(url, options) { + return $axios({ ...options, url, method: 'delete' }); +} + +export function $head(url, options) { + return $axios({ ...options, url, method: 'head' }); +} + +export function $options(url, options) { + return $axios({ ...options, url, method: 'options' }); +} + + +/** + * 下载专用请求 + * @param {downloadUrl} 接口路径 params:接口参数 + * @returns {Promise} + */ +export function $download(downloadUrl, params) { + return new Promise((resolve, reject) => { + axios( + { + method: "post", + url: downloadUrl, + data: params, + responseType: "blob", + } + ).then(result => { + resolve(result); + }).catch(err => { + reject(err); + }); + }); +} +/** + * 上传附件 + * @param {uploadUrl} 接口路径 params:接口参数 + * @params formData封装的数据 + * @returns {Promise} + */ + export function $upload(uploadUrl, params) { + return new Promise((resolve, reject) => { + fetch(uploadUrl, { + method: 'POST', + body: params.params, + headers:{ + 'Authorization':sessionStorage.getItem('token')?'Bearer '+sessionStorage.getItem('token'):null + } + }) + .then(response => { + return response.json() + }).then(json => { + resolve(json) + }).catch(error => { + reject(error) + }) + }) +} \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/Chat/Chat.jsx b/src/pages/customerservice/support/conversation/components/Chat/Chat.jsx new file mode 100644 index 0000000..4ccf1a6 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/Chat/Chat.jsx @@ -0,0 +1,53 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ChatHeader from '../ChatHeader/ChatHeader' +import ChatInput from '../ChatInput/ChatInput' +import ChatRecordList from '../ChatRecordList/ChatRecordList' +import ScrollWrapper from '../ScrollWrapper/ScrollWrapper' +import styles from './style.module.css' + +const WrappedChatRecordList = ScrollWrapper(ChatRecordList) + +const Chat =((props) => { + + const {style, contact, closeWin, hideWin, chatList, listHeight, textHeight, onSend, onImage, changeHeight, setPostion } = props + + return ( +
+ + + onSend(msgData)} + onImage={onImage} + changeHeight={changeHeight} + setPostion={setPostion} + /> +
+ ) +}) +Chat.propTypes = { + onSend: PropTypes.func.isRequired, + me: PropTypes.object.isRequired, + contact: PropTypes.object, + style: PropTypes.object.isRequired, + chatList:PropTypes.array, + onImage: PropTypes.func, + changeHeight: PropTypes.func.isRequired, + setPostion: PropTypes.func.isRequired, + closeWin: PropTypes.func.isRequired, + hideWin: PropTypes.func.isRequired, + isModalVisible:PropTypes.bool, + chatUI:PropTypes.any, + listHeight: PropTypes.number, + textHeight:PropTypes.number, + supplierNo:PropTypes.any, +} + +export default Chat; \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/Chat/style.module.css b/src/pages/customerservice/support/conversation/components/Chat/style.module.css new file mode 100644 index 0000000..ddd2b3c --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/Chat/style.module.css @@ -0,0 +1,9 @@ +.customer_service_content { + background-color: rgb(245,245,245); + display: flex; + flex-direction: column; + overflow: hidden; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + border: 1px solid rgb(245,245,245); +} \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/ChatHeader/ChatHeader.jsx b/src/pages/customerservice/support/conversation/components/ChatHeader/ChatHeader.jsx new file mode 100644 index 0000000..217a695 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatHeader/ChatHeader.jsx @@ -0,0 +1,53 @@ +import React from 'react' +import style from './style.module.css' +import PropTypes from 'prop-types'; +import { Button, Tooltip, Popconfirm } from 'antd'; +import { CloseOutlined, MinusOutlined } from '@ant-design/icons'; +import { mouseOverHandle, mouseMoveHandle, mouseDrag,} from '../../utils/utils' + +export default function ChatHeader(props) { + return ( +
{ + if(e.target.id=='chatHeader'){ + mouseOverHandle(e,props.chatUI) + } + }} + onMouseMove={(e)=>{ + if(e.target.id=='chatHeader'){ + mouseMoveHandle(e,props.chatUI) + } + }} + onMouseDown={(e)=>{ + if(e.target.id=='chatHeader'){ + mouseDrag(e,150,props.chatUI,null,null) + } + }} + > + +
+ 中国联通供应链智慧客服 + 客户至上 产品至上 +
+
+ +
+
+ ) +} + +ChatHeader.propTypes = { + data: PropTypes.object, + closeWin: PropTypes.func, + hideWin: PropTypes.func, + chatUI: PropTypes.any, +} diff --git a/src/pages/customerservice/support/conversation/components/ChatHeader/style.module.css b/src/pages/customerservice/support/conversation/components/ChatHeader/style.module.css new file mode 100644 index 0000000..fba1ccb --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatHeader/style.module.css @@ -0,0 +1,42 @@ +.customer_service_content { + border-top-left-radius: 10px; + border-top-right-radius: 10px; + display: flex; + align-items: center; + padding: 10px; + -moz-user-select: none; + -khtml-user-select: none; + user-select: none; + background-color: rgb(235, 34, 32); + color: white; + -border-bottom: 1.5px solid rgb(226,226,226); +} +.customer_service_content .avatar { + width: 40px; + height: 40px; + background-image: url('../../image/server-avatar.jpg'); + background-size: cover; + -background-color: aqua; + + border-radius: 50%; + overflow: hidden; +} +.customer_service_content .desc_area { + display: flex; + flex-direction: column; + padding-left: 10px; +} +.customer_service_content .btn{ + background-color: rgb(235, 34, 32); + position: absolute; + right: 5px; + top:10px +} +.customer_service_content .name { + font-size: 14px; + -color: #333; +} +.customer_service_content .sologan { + font-size: 12px; + -color: #999; +} diff --git a/src/pages/customerservice/support/conversation/components/ChatInput/ChatInput.jsx b/src/pages/customerservice/support/conversation/components/ChatInput/ChatInput.jsx new file mode 100644 index 0000000..b8ae6e3 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatInput/ChatInput.jsx @@ -0,0 +1,565 @@ +import React, { + useState, + useRef, + useEffect, +} from 'react' +import PropTypes from 'prop-types'; +import style from './style.module.css' +//import {doEncrypt} from '@/utils/utils' +import ChatToolBar from '../ChatToolsBar/ChatToolBar' +import { mouseOverHandle, mouseMoveHandle, mouseDrag, utf16toEntities, closeHTML, imageHandleUtil, doEncrypt} from '../../utils/utils' +import { customerserviceAPI as API} from '@/services/customerservice'; +import { Spin, Modal, message } from 'antd'; +import { useLocation } from 'react-router-dom' +//import {docUp} from '@config/host' +import lodash from 'lodash'; +import moment from "moment"; + +const ChatInput = ((props) => { + const { + me, + onSend = () => {}, + height, + showPic, + changeHeight, + setPostion, + isModalVisible, + supplierNo, + conversationId, + isSend, + sendMsg, + cancelSend, + } = props + + const innerRef = useRef(); + const [msg, setMsg] = useState([]) + const [isShift, setIsShift] = useState(false) + const [isCtrl, seIsCtrl] = useState(false) + const [isAlt, setIsAlt] = useState(false) + const [isAllowSend, setIsAllowSend] = useState(false) + const [cachedRange, setCachedRange] = useState(null) + const [loading, setLoading] = useState(false) + const [uploadFile, setUploadFile] = useState({}) + const [isSendFile, setIsSendFile] = useState(false) //发送文件之前确认 + + const location = useLocation() + + //imgInputSize:输入框图片尺寸 + //imgMsgSize:聊天界面图片尺寸 + //imgPreviewSize:预览图片尺寸 + const imgInputSize = 100 + const imgMsgSize = 150 + const imgPreviewSize = 400 + + + //递归给图片resize + const nextSrc = (srcArr, sig, msgArr)=>{ + var img=new Image(); + img.src=srcArr[sig].src; + + img.onload=function(){ + //递归处理消息数组 + for(let i = 0; i0 + setMsg(msgArr) + setIsAllowSend(isAllowSend) + } + } + + } + + //输入框内容重新组合,图片单独发送形成聊天记录 + const reformMsg = ()=>{ + let msgArr= [] + var div = innerRef.current + var imgs = div.getElementsByTagName('img') + var allElements = div.getElementsByTagName('*') + var judge_div = document.getElementById('judgeNull') + + //输入框内容去除样式 + if(allElements && allElements.length>0){ + for(var i=0; i0){ + for(var j=0; j]*src=['"]([^'"]+)[^>]+?>/) + if(img){ + var findImg = currentVal.indexOf('0 + setMsg(msgArr) + setIsAllowSend(isAllowSend) + } + } + const onChange = () => { + reformMsg() + } + // 图片blob转file + const getFile = (blob)=>{ + var timestamp=new Date().getTime() + var type = blob.type.split('/')[1] + var suffix = suffix == ''?'jpeg': type + let file = new File([blob], timestamp+"."+suffix,{ type: blob.type }) + const formData = new FormData(); + formData.append('multipartFiles', file); + return formData + } + //文档中心:图片上传 + const imgUpload = (blob, msgData)=>{ + API.fileUpload({ + multipartFiles: getFile(blob), + objectType:'img_public' + }).then(res => { + if (res && res.data && res.data.length>0 && res.data[0].sysStorageVO && res.data[0].sysStorageVO.filePath!=undefined && res.data[0].sysStorageVO.filePath!=null&& res.data[0].sysStorageVO.filePath!='') { + msgData.message.content.success = res.success + let vo = res.data[0].sysStorageVO + //文档中心返回的filePath重写content + msgData.message.content.content = `${REACT_APP_CUSTOMERSERVICE_DOC_REDIRECT}`+vo.filePath + msgData.message.fileId = vo.fileId + sendToCs(msgData) + }else{ + console.log(res && res.message) + } + }) + } + //文档中心:文件上传(除图片以外) + const fileUpload = (file, msgData)=>{ + const formData = new FormData(); + formData.append('multipartFiles', file); + API.fileUpload({ + multipartFiles: formData, + objectType:'file' + }).then(res => { + if (res && res.data && res.data.length>0 && res.data[0].sysStorageVO && res.data[0].sysStorageVO.filePath!=undefined && res.data[0].sysStorageVO.filePath!=null&& res.data[0].sysStorageVO.filePath!='') { + let vo = res.data[0].sysStorageVO + //文档中心返回的filePath重写content + msgData.message.content.content = `${REACT_APP_CUSTOMERSERVICE_DOC_REDIRECT}`+vo.filePath + vo.filePath + msgData.message.content.fileId = vo.fileId + msgData.message.content.success = res.success + sendToCs(msgData) + + }else{ + console.log(res && res.message) + } + }) + } + + //发送消息给客服-websocket + const sendToCs = (msg)=>{ + let params = { + "clientNo": msg.user.id, + "contentType": msg.message.type=="text"?1:msg.message.type=="image"?2:msg.message.type=="file"?3:'', + "custType": location.pathname.indexOf('/product-detail')!=-1? 2 : 1, + "message": msg.message.type=="image" || msg.message.type=="file" ?JSON.stringify(msg.message.content):msg.message.content, + //"questionType": location.pathname.indexOf('/home')!=-1? 3 : 1, + "questionType": 2, + "supplierNo": supplierNo + } + API.sendToCs(params).then(res => { + if (res && !res.data) { + console.log(res && res.message) + } + + }) + } + + //发送消息 + const sendHandle = () => { + if (!isAllowSend && !isSend && !sendMsg) { + return + } + let msgList = [] + let newmsg = sendMsg && sendMsg.length>0 ?sendMsg : lodash.cloneDeep(msg); + newmsg.map((item)=>{ + const randomNum = Math.floor(Math.random() * 1000) + //const date = dayjs().unix() + const date = new Date() + const encryption = doEncrypt(`${date}${randomNum}`) //有无更快的加密方法,可以加密图片?这个加密方法加密图片太慢! + //console.log('加密生成完毕') + let msgData = null + if(item.type=='text'){ + msgData = { + _id: encryption, + date: date, + user: me, + message: { + type: 'text', + content: item.content, + }, + } + sendToCs(msgData) + msgList.push(msgData) + }else if(item.type=='image'){ + let content = { + content: item.content, + realWidth: item.realWidth, + realHeight:item.realHeight, + msgWidth: item.msgWidth, + msgHeight:item.msgHeight, + previewidth: item.previewidth, + previewHeight:item.previewHeight, + success:false, + fileId:'', + } + msgData = { + _id: encryption, + date: date, + user: me, + message: { + type: 'image', + content: content + }, + } + msgList.push(msgData) + + //调用文档中心存储图片 + //消息图片src、消息(文档中心返回后修改消息中的content)、上传消息图片、从文档中心下载 + imageHandleUtil.uploadImage(msgData, imgUpload) + } + }) + onSend(msgList) + resetText() + innerRef.current.focus() + } + //发送文件 + const sendFileHandle = (msg) => { + let msgList = [] + + const randomNum = Math.floor(Math.random() * 1000) + //const date = dayjs().unix() + const date = new Date() + const encryption = doEncrypt(`${date}${randomNum}`) //有无更快的加密方法,可以加密图片?这个加密方法加密图片太慢! + //console.log('加密生成完毕') + const msgData = { + _id: encryption, + date: date, + user: me, + message: { + type: msg.type, + content: {content:msg.content, success:false, fileId:'', name:msg.name, size:msg.size}, + }, + } + msgList.push(msgData) + onSend(msgList) + fileUpload(msg.file,msgData) + } +//设置cachedRange +const resetCachedRange = (range) =>{ + setCachedRange(range) +} +//设置loading +const resetLoadling = ()=>{ + setLoading(false) + } +const resetText = () => { + setMsg([]) + innerRef.current.innerText='' + setIsAllowSend(false) +} + +const keyDownHandle = (e) => { + if (e.keyCode === 16) { + setIsShift(true) + } + if (e.keyCode === 17) { + seIsCtrl(true) + } + if(e.keyCode === 18) { + setIsAlt(true) + } + if ((e.nativeEvent.keyCode === 13 || e.keyCode === 13) && ((!isCtrl && !isShift && !isAlt)||(!e.ctrlKey && !e.shiftKey &&!e.altKey))) { + e.preventDefault() + sendHandle() + } +} + +const keyUpHandle = (e) => { + window.getSelection().rangeCount>0 && setCachedRange(window.getSelection().getRangeAt(0)) + + if(e.nativeEvent.keyCode === 18 || e.keyCode === 18){ + setIsAlt(false) + } + if (e.nativeEvent.keyCode === 16 || e.keyCode === 16){ + setIsShift(false) + } + if (e.nativeEvent.keyCode === 17 || e.keyCode === 17){ + seIsCtrl(false) + } +} + +const mouseUpHandle = () => { + window.getSelection().rangeCount>0 && setCachedRange(window.getSelection().getRangeAt(0)) +} + +const mouseDownHandle= ()=>{ + +} +//取特定字符串之间的字符串 +const str_substr = (start, end, str)=> { + let temp = str.split(start, 2); + let content = temp[1].split(end, 2); + return content[0]; +} +//粘贴操作 + const pasteHandle = (e) =>{ + setLoading(true) + var clipboardData = e.clipboardData; + var txt = clipboardData.getData('text/html') || clipboardData.getData('text/plain') + if(txt){ + var imgReg = /|\/>)/gi; + //匹配src属性 + //var srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i; + var arr = txt.match(imgReg); + if((arr && arr.length>0) || txt.indexOf('')!=-1){ + let newtxt = str_substr('','',txt) + txt = newtxt + } + //去除 style + let styleReg = "style=\"(.*?)\"" + let styleArr = txt.match(styleReg) + if(styleArr && styleArr.length>0) txt = txt.replace(styleArr[0],"") + + var el = document.createElement('span') + el.innerHTML=txt + var allImgs = el.getElementsByTagName('img') + + if(allImgs && allImgs.length>0){ + window.getSelection().deleteFromDocument() //如果有选中内容,粘贴时替换 + let range = cachedRange + if(!cachedRange && innerRef.current){ + innerRef.current.focus() + range = window.getSelection().getRangeAt(0) + } + if(range) { + let params={srcArr:allImgs, sig:0, el, range, reformMsg, showPic, resetCachedRange, resetLoadling, imgInputSize, imgMsgSize, imgPreviewSize} + imageHandleUtil.pasteImages(params) + } + } + } + }else if(clipboardData){//如果不是Files就直接粘贴,Files要先处理后再粘贴 + e.preventDefault() + var items = e.clipboardData.items; + let item=null; + if(items && items.length) { + for(var i=0; i { + let range = cachedRange + if(!cachedRange){ + innerRef.current.focus() + range = window.getSelection().getRangeAt(0) + } + if (range && innerRef.current) { + var obj = document.createElement('span') + obj.innerHTML=emoji + range.insertNode(obj) + setCachedRange(range) + range.collapse(false) + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + reformMsg() + } + } + //插入图片 + const onImageHandle = (image)=>{ + setLoading(true) + window.getSelection().deleteFromDocument() //如果有选中内容,粘贴时替换 + let range = cachedRange + if(!cachedRange && innerRef.current){ + innerRef.current.focus() + range = window.getSelection().getRangeAt(0) + } + //image:文档中心返回的图片路径 + //range:插入图片位置 + //reformMsg:插入后,处理input里的内容,为信息发送做准备(去除文本样式、每张图片作为单条消息发送) + //showPic:点击图片,预览事件 + //resetCachedRange:cachedRange赋值 + //resetLoadling:loading=false + if(range){ + let params={image, range, reformMsg, showPic, resetCachedRange, resetLoadling:null, imgInputSize, imgMsgSize, imgPreviewSize} + imageHandleUtil.insertImageHandle(params) + } + } + //文件发送 + const sendFile = ()=>{ + let file = uploadFile + var reader = new FileReader(); + reader.readAsDataURL(file); + reader.onloadend = function (e) { + let isLt50M = file.size / 1024 + sendFileHandle({type:'file',content:e.target.result, name:file.name,size:isLt50M>=1024?(isLt50M/1024).toFixed(1)+"M":(isLt50M).toFixed(1)+"K", file:file}) + }; + } + //文件发送前确认 + const onFileHandle = (file)=>{ + setUploadFile(file) + setIsSendFile(true) + } + //评价 + const onRateHandle = (score)=>{ + // const conversationId = '1506204072738394114' + if(conversationId==''){ + message.warn('您还没有发起咨询,无需评价') + }else{ + API.estimate({id: conversationId, remarkScore: score*2, remarkTime: moment(new Date()).format('yyyy-MM-DD HH:mm:ss')}).then(res=>{ + if (res && res.success) { + message.success('感谢您的评价!') + }else{ + message.warn(res && res.message) + } + }) + } + } + useEffect(()=>{ + if(isModalVisible){ + resetText() + innerRef.current.focus() + } + },[isModalVisible]) + + useEffect(()=>{ + if(isSend){ + sendHandle() + } + },[isSend]) + return ( + +
{ + if(e.target.id=='chatInput'){ + mouseOverHandle(e,props.chatUI) + } + }} + onMouseMove={(e)=>{ + if(e.target.id=='chatInput'){ + mouseMoveHandle(e,props.chatUI) + } + }} + onMouseDown={(e)=>{ + if(e.target.id=='chatInput'){ + mouseDrag(e,150,props.chatUI,changeHeight,setPostion) + } + }} + > + +
{e.preventDefault()}} + className={style.input_area} + contentEditable + onKeyUp={keyUpHandle} + onKeyDown={keyDownHandle} + onInput={onChange} + onMouseUp={mouseUpHandle} + onMouseDown={mouseDownHandle} + onPaste={pasteHandle} + onMouseMove={(e)=>{ + //防止cursor显示异常 + mouseMoveHandle(e,props.chatUI) + }} + ref={innerRef} + id='chatInputArea' + > +
+ +
+ +
+
+
+ {isSendFile && + {setIsSendFile(false);sendFile()}} onCancel={()=>setIsSendFile(false)} > +

您确定要上传文件么?【确定】后,文件将无法撤回!

+
+ } +
+ ) +}) +ChatInput.propTypes = { + me:PropTypes.object.isRequired, + onSend:PropTypes.func.isRequired, + height:PropTypes.number.isRequired, + showPic:PropTypes.func.isRequired, + changeHeight:PropTypes.func.isRequired, + setPostion:PropTypes.func.isRequired, + isModalVisible:PropTypes.bool, + style:PropTypes.any, + chatUI:PropTypes.any, + supplierNo:PropTypes.any, +} +export default ChatInput; \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/ChatInput/style.module.css b/src/pages/customerservice/support/conversation/components/ChatInput/style.module.css new file mode 100644 index 0000000..2d3e84c --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatInput/style.module.css @@ -0,0 +1,54 @@ +.customer_service_content { + background-color: white; + box-sizing: border-box; + width: 100%; + height: 100%; + min-height: 150px; + display: flex; + flex-direction: column; + /* box-shadow: 0px -5px 10px -5px rgba(0, 0, 0, 0.2); */ +} +.customer_service_content .input_area { + height: 100%; + overflow-y:auto; + border: none; + outline: none; + padding: 10px; + margin-right: 10px; + font-size: 13px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + letter-spacing: 2px; +} +.customer_service_content .input_area::placeholder { + color: #999; +} +.customer_service_content .but_area { + align-self: flex-end; + margin-bottom: 10px; + margin-right: 20px; +} +.customer_service_content .but:hover{ + cursor: pointer; +} +.customer_service_content .but { + padding: 3px 20px; + border: none; + outline: none; + -background-color: #2ba245; + background-color: rgb(235, 34, 32); + color: white; + border-radius: 3px; +} +.customer_service_content .but:active { + background-color: rgb(235, 34, 32); +} +.customer_service_content .but:disabled { + background-color: #ddd; + cursor:auto; +} +.customer_service_content .bar_block{ + width: 8px; + /* border-radius: 4px; */ + box-sizing: border-box; + background-color: white +} \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/ChatRecordList/ChatRecordList.jsx b/src/pages/customerservice/support/conversation/components/ChatRecordList/ChatRecordList.jsx new file mode 100644 index 0000000..8e91dbb --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatRecordList/ChatRecordList.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import style from './style.module.css' +import MsgItem from '../MsgItem/MsgItem' +import PropTypes from 'prop-types'; + +const ChatRecordList = (props) => { + let { data, me, showPic} = props + return ( +
+ {/* +
+ +
+ */} + {data.map((bubble) => ( + + ))} +
+ ) +} +ChatRecordList.propTypes = { + loading: PropTypes.bool, + showPic: PropTypes.func, + contact: PropTypes.object, + data:PropTypes.array, + me:PropTypes.any, +} +export default ChatRecordList + diff --git a/src/pages/customerservice/support/conversation/components/ChatRecordList/style.module.css b/src/pages/customerservice/support/conversation/components/ChatRecordList/style.module.css new file mode 100644 index 0000000..ba10780 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatRecordList/style.module.css @@ -0,0 +1,29 @@ +.customer_service_content_list_area { + flex: 1; + overflow: auto; + -ms-overflow-style: none; + overflow: -moz-scrollbars-none; + padding: 10px; + background-color: rgb(245,245,245) +} +.customer_service_content_list_area .load_more { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 100px; + text-align: center; + color: #999; + background-color: #f1f1f1; + font-size: 12px; + margin: 0 auto; + border: none; + outline: none; + border-radius: 10px; + -moz-user-select: none; + -khtml-user-select: none; + user-select: none; +} +.customer_service_content_list_area .load_more:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/ChatToolsBar/ChatToolBar.jsx b/src/pages/customerservice/support/conversation/components/ChatToolsBar/ChatToolBar.jsx new file mode 100644 index 0000000..8c9ffae --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatToolsBar/ChatToolBar.jsx @@ -0,0 +1,34 @@ +import React from 'react' +import PropTypes from 'prop-types' +import style from './style.module.css' +import EmojiPopover from '../EmojiPopover/EmojiPopover' +import ImgPopover from '../ImgPopover/ImgPopover' +import FilePopover from '../FilePopover/FilePopover' +import RatePopover from '../RatePopover/RatePopover' + +export default function ChatToolBar(props) { + const { tools = [], + onEmojiSelect, + onImage, + onFile, + onRate, + } = props + return ( +
+ {typeof onEmojiSelect === 'function' && ( + + )} + {typeof onImage === 'function' && } + {typeof onFile === 'function' && } + {typeof onRate === 'function' && } + {tools.map((tool) => tool)} +
+ ) +} + +ChatToolBar.propTypes = { + tools: PropTypes.array, + onEmojiSelect: PropTypes.func, + onImage: PropTypes.func, + onFile:PropTypes.func, +} diff --git a/src/pages/customerservice/support/conversation/components/ChatToolsBar/style.module.css b/src/pages/customerservice/support/conversation/components/ChatToolsBar/style.module.css new file mode 100644 index 0000000..3b95871 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ChatToolsBar/style.module.css @@ -0,0 +1,8 @@ +.customer_service_content { + height: 30px; + display: flex; + align-items: center; + padding: 0px 10px; + margin-top: 8px; + margin-right: 10px; +} \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/EmojiPopover/EmojiPopover.jsx b/src/pages/customerservice/support/conversation/components/EmojiPopover/EmojiPopover.jsx new file mode 100644 index 0000000..56e8429 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/EmojiPopover/EmojiPopover.jsx @@ -0,0 +1,141 @@ +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import style from './style.module.css' +import { cns } from '../../utils/toClass' + +const emojiList = [ + '😀', + '😃', + '😄', + '😁', + '😆', + '😅', + '😂', + '🤣', + '😊', + '😇', + '🙂', + '🙃', + '😉', + '😌', + '😍', + '🥰', + '😘', + '😗', + '😙', + '😚', + '😋', + '😛', + '😝', + '😜', + '🤪', + '🤨', + '🧐', + '🤓', + '😎', + '🤩', + '🥳', + '😏', + '😒', + '😞', + '😔', + '😟', + '😕', + '🙁', + '😣', + '😖', + '😫', + '😩', + '🥺', + '😢', + '😭', + '😤', + '😠', + '😡', + '🤬', + '🤯', + '😳', + '🥵', + '🥶', + '😱', + '😨', + '😰', + '😥', + '😓', + '🤗', + '🤔', + '🤭', + '🤫', + '🤥', + '😶', + '😐', + '😑', + '😬', + '🙄', + '😯', + '😦', + '😧', + '😮', + '😲', + '🥱', + '😴', + '🤤', + '😪', + '😵', + '🤐', + '🥴', + '🤢', + '🤮', + '🤧', + '😷', + '🤒', + '🤕', +] + +export default function EmojiPopover(props) { + const {onSelect} = props + const [visible, setVisible] = useState(false) + + const switchEmojiModal = (vis) => { + setVisible(vis) + } + + const iconClickHandle = (emoji) => { + onSelect(emoji) + } + + useEffect(() => { + addEventListener('click', (e) => { + if (e.target.getAttribute('datatype') === 'emoji') { + switchEmojiModal(true) + } else { + switchEmojiModal(false) + } + }) + }, []) + + return ( +
+
+ {emojiList.map((emoji) => ( + + {emoji} + + ))} +
+
+
+ ) +} + +EmojiPopover.propTypes = { + onSelect: PropTypes.func.isRequired, +} diff --git a/src/pages/customerservice/support/conversation/components/EmojiPopover/style.module.css b/src/pages/customerservice/support/conversation/components/EmojiPopover/style.module.css new file mode 100644 index 0000000..301bb93 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/EmojiPopover/style.module.css @@ -0,0 +1,55 @@ +.customer_service_content { + position: relative; +} +.customer_service_content .emoji_wrapper { + position: absolute; + bottom: -90px; + left: 30px; + display: flex; + flex-wrap: wrap; + width: 288px; + background-color: white; + border: solid 1px #aaaaaa; + border-radius: 5px; + padding: 3px; +} +.customer_service_content .emoji_wrapper::before { + content: ''; + border: solid 8px transparent; + border-right: solid 8px #aaaaaa; + position: absolute; + top: 50%; + left: -16px; +} +.customer_service_content .emoji_wrapper::after { + content: ''; + border: solid 8px transparent; + border-right: solid 8px white; + position: absolute; + top: 50%; + left: -14.5px; +} +.customer_service_content .emoji_item { + box-sizing: border-box; + padding: 0px 7px 0px 3px; +} +.customer_service_content .emoji_item:hover { + background-color: #f1f1f1; + border-radius: 50%; + cursor: pointer; +} +.customer_service_content .tool_icon { + height: 20px; + width: 20px; + background-size: 100%; + background-repeat: no-repeat; + background-position: center; + margin-right: 10px; +} +.customer_service_content .emoji { + background-image: url('../../image/emoji_black.png'); +} +.customer_service_content .emoji:hover { + cursor: pointer; + background-image: url('../../image/emoji_light.png'); +} diff --git a/src/pages/customerservice/support/conversation/components/FilePopover/FilePopover.jsx b/src/pages/customerservice/support/conversation/components/FilePopover/FilePopover.jsx new file mode 100644 index 0000000..dc66605 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/FilePopover/FilePopover.jsx @@ -0,0 +1,69 @@ +import React, {useRef } from 'react' +import PropTypes from 'prop-types' +import style from './style.module.css' +import { message, } from 'antd'; + +export default function FilePopover(props) { + const { onFile } = props + const fileSelector = useRef() + + const isUpload = (file) => { + /* + let name = file.name; + let type; + if (name.lastIndexOf(".") > 0) { + type = name + .substr(name.lastIndexOf(".") + 1, name.length) + .toLocaleLowerCase(); + } + let isType = ["pdf", "png", "jpg", "jpeg"].indexOf(type) === -1; + if (isType) { + message.error("上传格式错误"); + }*/ + const isType = false + const isLt50M = file.size / 1024 / 1024 < 50; + if (!isLt50M) { + message.error("文件大小不能超过50MB"); + } + const bool = !isType && isLt50M; + return bool + } + + const beforeUpload = (file) => { + let bool = isUpload(file) + if (bool) { + onFile(file) + } + return false; + }; + const selectImg = () => { + if (fileSelector.current) { + fileSelector.current.value='' + fileSelector.current.click() + } + } + + const fileHandle = (event) => { + const files = event.target.files + //console.log(files[0]) + if(files && files[0] && (files[0].type.indexOf('application/')!=-1 || files[0].type.indexOf('text/plain')!=-1 )){ + beforeUpload(files[0]) + }else{ + message.error('上传文件格式不支持') + } + } + return ( +
+ +
+ ) +} + +FilePopover.propTypes = { + onFile: PropTypes.func.isRequired, +} diff --git a/src/pages/customerservice/support/conversation/components/FilePopover/style.module.css b/src/pages/customerservice/support/conversation/components/FilePopover/style.module.css new file mode 100644 index 0000000..e9d5bca --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/FilePopover/style.module.css @@ -0,0 +1,16 @@ +.customer_service_content { + width: 20px; + height: 20px; + margin-right: 10px; + background-image: url('../../image/file_black.png'); + background-size: 100%; + background-position: center; + background-repeat: no-repeat; +} +.customer_service_content:hover { + background-image: url('../../image/file_light.png'); + cursor: pointer; +} +.customer_service_content > input { + display: none; +} diff --git a/src/pages/customerservice/support/conversation/components/ImgPopover/ImgPopover.jsx b/src/pages/customerservice/support/conversation/components/ImgPopover/ImgPopover.jsx new file mode 100644 index 0000000..64f6821 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ImgPopover/ImgPopover.jsx @@ -0,0 +1,86 @@ +import React, {useRef } from 'react' +import PropTypes from 'prop-types' +import style from './style.module.css' +import { message } from "antd"; +//import { customerserviceAPI as API} from '@/services/customerservice'; + +export default function ImgPopover(props) { + const { onImage } = props + const fileSelector = useRef() + + const isUpload = (file) => { + /* + let name = file.name; + let type; + if (name.lastIndexOf(".") > 0) { + type = name + .substr(name.lastIndexOf(".") + 1, name.length) + .toLocaleLowerCase(); + } + let isType = ["pdf", "png", "jpg", "jpeg"].indexOf(type) === -1; + if (isType) { + message.error("上传格式错误"); + }*/ + const isType = false + const isLt50M = file.size / 1024 / 1024 < 50; + if (!isLt50M) { + message.error("文件大小不能超过50MB"); + } + const bool = !isType && isLt50M; + return bool + } + + const beforeUpload = (file) => { + let bool = isUpload(file) + if (bool) { + onImage(file) + /* + const formData = new FormData(); + console.log(file.getOriginalFilename()) + formData.append('multipartFiles', file); + API.fileUpload({ + multipartFiles: formData + }).then(res => { + if (res && res.data && res.data.length>0 && res.data[0].sysStorageVO && res.data[0].sysStorageVO.filePath!=undefined && res.data[0].sysStorageVO.filePath!=null&& res.data[0].sysStorageVO.filePath!='') { + console.log(res) + console.log(res.data) + + //文档中心返回的filePath重写content + + }else{ + console.log(res && res.message) + } + })*/ + } + return false; + }; + const selectImg = () => { + if (fileSelector.current) { + fileSelector.current.value='' + fileSelector.current.click() + } + } + + const fileHandle = (event) => { + const files = event.target.files + if(files && files[0] && files[0].type.indexOf('image/')!=-1){ + beforeUpload(files[0]) + }else{ + message.error("上传格式错误"); + } + } + return ( +
+ +
+ ) +} + +ImgPopover.propTypes = { + onImage: PropTypes.func.isRequired, +} diff --git a/src/pages/customerservice/support/conversation/components/ImgPopover/style.module.css b/src/pages/customerservice/support/conversation/components/ImgPopover/style.module.css new file mode 100644 index 0000000..7c95726 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ImgPopover/style.module.css @@ -0,0 +1,16 @@ +.customer_service_content { + width: 20px; + height: 20px; + margin-right: 10px; + background-image: url('../../image/pic_black.png'); + background-size: 100%; + background-position: center; + background-repeat: no-repeat; +} +.customer_service_content:hover { + background-image: url('../../image/pic_light.png'); + cursor: pointer; +} +.customer_service_content > input { + display: none; +} diff --git a/src/pages/customerservice/support/conversation/components/ImgPreview/ImgPreview.jsx b/src/pages/customerservice/support/conversation/components/ImgPreview/ImgPreview.jsx new file mode 100644 index 0000000..94ca9e6 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ImgPreview/ImgPreview.jsx @@ -0,0 +1,56 @@ +import React, { useRef, useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import {handleElement } from '../../utils/utils' +import { Button,} from 'antd'; +import { CloseOutlined, BorderOutlined, MinusOutlined } from '@ant-design/icons'; +import style from './style.module.css' + +export default function ImgPreview(props) { + const { isImgPreviewVisible, imageParams, closePreview, maxSizePreview, normalSizePreview,} = props + const imgRef = useRef() + const [scales,setScales] = useState(1) + //const refs = useRef() + + const changeScales = (a) =>{ + setScales(a) + } + useEffect(()=>{ + if(isImgPreviewVisible) { + var html=document.querySelector("html"); + var body=document.querySelector("body"); + html.style.overflow="hidden"; + body.style.overflow="hidden"; + } + + },[isImgPreviewVisible]) + + return ( + <> +
+
+
+ handleElement.handleStopDrag(e)} + onMouseDown={(e) => { e.persist(); handleElement.handleDrag(e,imgRef) }} + onContextMenu={(e)=>e.preventDefault()} + onWheel={(e)=>handleElement.handleZoom(e, imgRef, scales, changeScales)} + /> +
+ + ) +} + +ImgPreview.propTypes = { + isImgPreviewVisible:PropTypes.bool, + closePreview:PropTypes.func, + maxSizePreview:PropTypes.func, + normalSizePreview:PropTypes.func, + imageParams:PropTypes.any, +} diff --git a/src/pages/customerservice/support/conversation/components/ImgPreview/style.module.css b/src/pages/customerservice/support/conversation/components/ImgPreview/style.module.css new file mode 100644 index 0000000..87998db --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ImgPreview/style.module.css @@ -0,0 +1,6 @@ +.customer_service_btn{ + height: 20px; + background-color: rgb(245, 245, 245); + position: relative; + align-self: flex-end; +} diff --git a/src/pages/customerservice/support/conversation/components/MsgBubble/MsgBubble.jsx b/src/pages/customerservice/support/conversation/components/MsgBubble/MsgBubble.jsx new file mode 100644 index 0000000..916545c --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/MsgBubble/MsgBubble.jsx @@ -0,0 +1,100 @@ +import React from 'react' +import PropTypes from 'prop-types' +import style from './style.module.css' +import { cns } from '../../utils/toClass' +import {parseBase64ToBlob,downloadFile} from '../../utils/utils' +import { customerserviceAPI as API} from '@/services/customerservice'; +import { message, Tooltip } from "antd"; +//import { downloadFile } from "@utils/utils"; + +export default function MsgBubble(props) { + const { data, isMe, showPic} = props + + const onDownload = (fileId) => { + API.getSecretKey({fileId: fileId}).then(res => { + if(res.success==true){ + API.getDownload({ fileId: fileId, documentSecretKey: res.data }).then(buffer => { + const dotIndex = data.content.name.lastIndexOf('.') + const ext = data.content.name.substr(dotIndex) + const fileName = data.content.name.substring(0, dotIndex) + downloadFile(buffer, fileName, ext) + }) + }else{ + message.warning(res.message) + } + }) + } + const openFile = ()=>{ + const element = document.createElement("a"); + if(data.content.content.indexOf('base64')!=-1){ + var arr = data.content.content.split(',') //分割为数组,分割到第一个逗号 + let mime = arr[0].match(/:(.*?);/)[1] //获取分割后的base64前缀中的类型 + let myBlob = parseBase64ToBlob(arr[1], mime); + element.href = URL.createObjectURL(myBlob); + element.download = data.content.name; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + document.body.removeChild(element); + }else{ + if(data.content.fileId!=''){ + onDownload(data.content.fileId) + } + } + } + const renderContent = (message) => { + switch (message.type) { + case 'text': + return( +
{e.preventDefault()}} dangerouslySetInnerHTML={{ __html: message.content }} /> + ) + case 'image': + //console.log(message.content.content) + return ( +
{e.preventDefault()}}> + { + let src = message.content.content + let realWidth = message.content.realWidth + let realHeight = message.content.realHeight + let msgWidth = message.content.msgWidth + let msgHeight = message.content.msgHeight + let previewidth = message.content.previewidth + let previewHeight = message.content.previewHeight + let param = { + src, realWidth, realHeight, msgWidth, msgHeight, previewidth, previewHeight + } + showPic(param) + }} /> +
+ ) + case 'file': + return( + +
{e.preventDefault()}} className={style.customer_service_uploadfile} onClick={openFile}> +
{message.content.name}
+
{message.content.size}
+
+
+
+ ) + default: + break + } + + } + return ( +
+ {renderContent(data)} +
+ ) +} + +MsgBubble.propTypes = { + data: PropTypes.object.isRequired, + isMe: PropTypes.bool.isRequired, + showPic: PropTypes.func.isRequired +} diff --git a/src/pages/customerservice/support/conversation/components/MsgBubble/style.module.css b/src/pages/customerservice/support/conversation/components/MsgBubble/style.module.css new file mode 100644 index 0000000..5cd47b2 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/MsgBubble/style.module.css @@ -0,0 +1,83 @@ +.customer_service_noselect{ + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -khtml-user-select: none; /* Konqueror */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + not supported by any browser */ +} +.customer_service_text_content { + background-color: rgb(216,216,216); + color: #333; + padding: 8px; + border-radius: 3px; + max-width: 100%; + word-wrap: break-word; + word-break: break-all; + font-size: 13px; + font-family:sans-serif; + letter-spacing: 2px; +} +.customer_service_uploadfile{ + border: 1px solid #CCC; + background-color: white; + width: 200px; + height: 120px; + display: flex; + flex-direction: column; + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -khtml-user-select: none; /* Konqueror */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + not supported by any browser */ +} +.customer_service_uploadfile:hover{ + cursor: pointer; +} +.customer_service_filename { + margin-left: 10px; + margin-top: 5px; +} +.customer_service_filesize { + margin-left: 10px; + margin-top: 5px; + color: rgb(153,153,153); +} +.customer_service_ellipsis { + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + display:-webkit-box; + -webkit-box-orient:vertical; + -webkit-line-clamp:2; + -white-space: nowrap; +} +.customer_service_uploadicon{ + align-self: flex-end; + margin-right: 10px; + width: 40px; + height: 40px; + background-image: url('../../image/uploadFile.png'); + background-size: cover; +} +.customer_service_arrow { + position: relative; +} +.customer_service_arrow::after { + content: ''; + position: absolute; + width: 8px; + height: 8px; + background-color: rgb(216,216,216); + transform: rotate(45deg); + top: 8px; +} +.customer_service_arrow_left::after { + left: -4px; +} +.customer_service_arrow_right::after { + right: -4px; +} \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/MsgItem/MsgItem.jsx b/src/pages/customerservice/support/conversation/components/MsgItem/MsgItem.jsx new file mode 100644 index 0000000..6c1f0ed --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/MsgItem/MsgItem.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import PropTypes from 'prop-types' +import style from './style.module.css' +import MsgBubble from '../MsgBubble/MsgBubble' +//import dayjs from 'dayjs' +import moment from 'moment'; + + +export default function MsgItem(props) { + const { data, me ,showPic } = props + const isMe = data.user.id === me.id + + const isValidDate= (date)=>{ + return date instanceof Date && !isNaN(date.getTime()) + } + + return ( +
+
+ +
+
+
+ {data.user.name} + + {/*moment(data.date).format('MM-DD HH:mm:ss')*/} + {isValidDate(new Date(data.date))?moment(data.date).format('MM-DD HH:mm:ss'):data.date} + +
+ {} +
+
+ ) +} + +MsgItem.propTypes = { + data: PropTypes.object.isRequired, + me: PropTypes.object.isRequired, + showPic: PropTypes.func.isRequired +} diff --git a/src/pages/customerservice/support/conversation/components/MsgItem/style.module.css b/src/pages/customerservice/support/conversation/components/MsgItem/style.module.css new file mode 100644 index 0000000..3c82dde --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/MsgItem/style.module.css @@ -0,0 +1,39 @@ +@import '../../style/common.css'; + +.customer_service_content { + padding-top: 20px; + display: flex; +} +.customer_service_content .avatar { + min-width: 35px; + width: 35px; + padding-top: 14px; + -moz-user-select: none; + -khtml-user-select: none; + user-select: none; +} +.customer_service_content .avatar > img { + width: 100%; + height: auto; + border-radius: 50%; + overflow: hidden; +} +.customer_service_content .text_area { + flex: 1; + padding: 0px 10px; + display: flex; + flex-direction: column; + overflow: hidden; + max-width: 60%; +} +.customer_service_content .comment_area { + font-size: 12px; + color: #999; + padding-bottom: 4px; + -moz-user-select: none; + -khtml-user-select: none; + user-select: none; +} +.customer_service_content .date_text { + margin-left: 10px; +} diff --git a/src/pages/customerservice/support/conversation/components/RatePopover/RatePopover.jsx b/src/pages/customerservice/support/conversation/components/RatePopover/RatePopover.jsx new file mode 100644 index 0000000..b10429e --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/RatePopover/RatePopover.jsx @@ -0,0 +1,39 @@ +import React, {useRef, useState } from 'react' +import PropTypes from 'prop-types' +import style from './style.module.css' +import { HeartFilled } from '@ant-design/icons'; +import { Rate, Button, Modal, Tooltip } from 'antd'; +export default function RatePopover(props) { + + const { onRate } = props + const fileSelector = useRef() + const [score, setScore] = useState(0) + const [rateModal, setRateModal] = useState(false) + + return ( + <> +
setRateModal(true)} className={style.customer_service_btn}/> + {rateModal && + { + setRateModal(false) + }} + footer={[ + , + ]} + > + { + setScore(value) + }}/> + + } + + ) +} +RatePopover.propTypes = { + onRate: PropTypes.func.isRequired, +} diff --git a/src/pages/customerservice/support/conversation/components/RatePopover/style.module.css b/src/pages/customerservice/support/conversation/components/RatePopover/style.module.css new file mode 100644 index 0000000..8a2b54c --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/RatePopover/style.module.css @@ -0,0 +1,28 @@ +.customer_service_btn { + width: 12px; + height: 12px; + line-height: 12px; + margin-left: 5px; + position: relative; + background-color: #b30000; + transform: rotate(45deg); +} +.customer_service_btn::before, +.customer_service_btn::after { + position: absolute; + content: ''; + background-color: #b30000; + top: 0px; + left: -50%; + width: 100%; + height: 100%; + border-radius: 50%; +} + +.customer_service_btn::after { + left: 0; + top: -50%; +} +.customer_service_btn:hover{ + cursor: pointer; +} \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/ScrollWrapper/ScrollWrapper.jsx b/src/pages/customerservice/support/conversation/components/ScrollWrapper/ScrollWrapper.jsx new file mode 100644 index 0000000..5d5c05d --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ScrollWrapper/ScrollWrapper.jsx @@ -0,0 +1,143 @@ +import React, { + useEffect, + useLayoutEffect, + useRef, + useState, + useMemo +} from 'react' +import { cns } from '../../utils/toClass' +import styles from './style.module.css' +import PropTypes from 'prop-types'; + +const ScrollWrapper = (Comp) => (props) => { + + //var data = this.props.data + const scrollView = useRef() + const thumb = useRef() + + const [viewPortH, setViewPortH] = useState(1) + const [scrollH, setScrollH] = useState(1) + const [scrollT, setScrollT] = useState(0) + const [scrollR, setScrollR] = useState(1) + // const [isReset, setIsReset] = useState(false) + const [isPressing, setIsPressing] = useState(false) + const [showScrollBar, setShowScrollBar] = useState(false) + + const [shadowStyle, setShadowStyle] = useState('') + +let { data, viewH, scrollToBottom, switchFlag, style, isMove, isModalVisible} = this? this.props : props + //let data = props.data + const scrollHandle = () => { + if (scrollView.current) { + setScrollT(scrollView.current.scrollTop) + } + } + + const mouseUpHandle = () => { + setIsPressing(false) + } + + const mouseDownHandle = () => { + setIsPressing(true) + } + + const mouseMovingHandle = (e) => { + if (isPressing !== true) return + if (scrollT < 0) { + setScrollT(0) + } else if (scrollT > scrollH - viewPortH) { + setScrollT(scrollH - viewPortH) + } else { + setScrollT((preScrollT) => preScrollT + e.nativeEvent.movementY / scrollR) + } + } + + const thumbHeight = () => viewPortH * scrollR + + const transH = () => scrollT * scrollR + + useEffect(() => { + addEventListener('mouseup', mouseUpHandle) + + return () => { + removeEventListener('mouseup', mouseUpHandle) + } + }, []) + + useEffect(() => { + if(isModalVisible){ + if (scrollView.current) { + setViewPortH(scrollView.current.offsetHeight) + setScrollH(scrollView.current.scrollHeight) + } + } + }, [data, isModalVisible, viewH]) + + useLayoutEffect(() => { + if (scrollView.current) { + scrollView.current.scrollTop = scrollT + } + + if (showScrollBar !== true) return + + if (scrollT <= 0) { + setShadowStyle(styles.shadow_bottom) + } else if (scrollT >= scrollH - viewPortH) { + setShadowStyle(styles.shadow_top) + } else { + setShadowStyle(styles.shadow_vertical) + } + }, [scrollT]) + + useEffect(() => { + const sr = scrollH==0? 0: (viewPortH / scrollH) + setScrollR(sr) + if (viewPortH < scrollH) { + setShowScrollBar(true) + } else { + setShowScrollBar(false) + } + }, [scrollH, viewPortH]) + + useEffect(() => { + if (scrollToBottom) { + setScrollT(scrollH - viewPortH) + }else{ + setScrollT(0) + } + }, [scrollR, switchFlag]) + + return ( +
+
+ {useMemo(() => , [data])} +
+ +
+ ) +}; + +ScrollWrapper.propTypes = { + data: PropTypes.object, +}; +export default ScrollWrapper; \ No newline at end of file diff --git a/src/pages/customerservice/support/conversation/components/ScrollWrapper/style.module.css b/src/pages/customerservice/support/conversation/components/ScrollWrapper/style.module.css new file mode 100644 index 0000000..bdcf6e5 --- /dev/null +++ b/src/pages/customerservice/support/conversation/components/ScrollWrapper/style.module.css @@ -0,0 +1,49 @@ +@import '../../style/common.css'; + +.customer_service_content_wrapper_content { + display: flex; + box-sizing: border-box; + border-top: solid 1px rgb(226, 226, 226); + border-bottom: solid 1px rgb(226, 226, 226); +} +.customer_service_content_wrapper_content .list_block { + flex: auto; + overflow: auto; + overflow-x: hidden; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ + box-sizing: border-box; +} +.customer_service_content_wrapper_content .list_block::-webkit-scrollbar { + display: none; /* Chrome Safari */ +} +.customer_service_content_wrapper_content .scroll_bar_block { + width: 8px; + /* border-radius: 4px; */ + overflow: hidden; + transition: all 0.3s; + box-sizing: border-box; + background-color: rgb(231,229,229); +} +.customer_service_content_wrapper_content .scroll_bar_block:hover { + background-color: #eee; +} +.customer_service_content_wrapper_content .scroll_thumb { + display: block; + width: 100%; + background-color: #aaa; + border-radius: 4px; +} +.customer_service_content_wrapper_content .scroll_thumb:hover { + cursor: pointer; +} +.customer_service_content_wrapper_content .shadow_top { + box-shadow: inset 0px 5px 10px -5px rgba(0, 0, 0, 0.2); +} +.customer_service_content_wrapper_content .shadow_bottom { + box-shadow: inset 0px -5px 10px -5px rgba(0, 0, 0, 0.2); +} +.customer_service_content_wrapper_content .shadow_vertical { + box-shadow: inset 0px 5px 10px -5px rgba(0, 0, 0, 0.2), + inset 0px -5px 10px -5px rgba(0, 0, 0, 0.2); +} diff --git a/src/pages/customerservice/support/conversation/image/emoji_black.png b/src/pages/customerservice/support/conversation/image/emoji_black.png new file mode 100644 index 0000000..6095106 Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/emoji_black.png differ diff --git a/src/pages/customerservice/support/conversation/image/emoji_light.png b/src/pages/customerservice/support/conversation/image/emoji_light.png new file mode 100644 index 0000000..ebc0d2c Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/emoji_light.png differ diff --git a/src/pages/customerservice/support/conversation/image/file_black.png b/src/pages/customerservice/support/conversation/image/file_black.png new file mode 100644 index 0000000..ac827b1 Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/file_black.png differ diff --git a/src/pages/customerservice/support/conversation/image/file_light.png b/src/pages/customerservice/support/conversation/image/file_light.png new file mode 100644 index 0000000..204df98 Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/file_light.png differ diff --git a/src/pages/customerservice/support/conversation/image/pic_black.png b/src/pages/customerservice/support/conversation/image/pic_black.png new file mode 100644 index 0000000..e14669b Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/pic_black.png differ diff --git a/src/pages/customerservice/support/conversation/image/pic_light.png b/src/pages/customerservice/support/conversation/image/pic_light.png new file mode 100644 index 0000000..a3f3016 Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/pic_light.png differ diff --git a/src/pages/customerservice/support/conversation/image/server-avatar.jpg b/src/pages/customerservice/support/conversation/image/server-avatar.jpg new file mode 100644 index 0000000..abb7ffe Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/server-avatar.jpg differ diff --git a/src/pages/customerservice/support/conversation/image/uploadFile.png b/src/pages/customerservice/support/conversation/image/uploadFile.png new file mode 100644 index 0000000..bd61399 Binary files /dev/null and b/src/pages/customerservice/support/conversation/image/uploadFile.png differ diff --git a/src/pages/customerservice/support/conversation/index.jsx b/src/pages/customerservice/support/conversation/index.jsx new file mode 100644 index 0000000..2bd5a70 --- /dev/null +++ b/src/pages/customerservice/support/conversation/index.jsx @@ -0,0 +1,372 @@ +import React, { useState, useEffect, useRef } from 'react' +import Chat from './components/Chat/Chat' +import ImgPreview from './components/ImgPreview/ImgPreview' +import {entitiestoUtf16, handleElement } from './utils/utils' +//import { checToken } from '@/services/login' +//import { bizServiceCustomService } from "@/config/host"; +import user from '@/assets/img/cs_user.png' +import client from '@/assets/img/cs_client.png' +import { customerserviceAPI as API} from '@/services/customerservice'; +import { message, Modal, Table, Button, Popconfirm } from 'antd'; + +const conversation = (props) => { + const [msgList, setMsgList] = useState([]) + const [scrollToBottom, setScrollToBottom] = useState(true) + const [switchFlag,setSwitchFlag] = useState(false) + const [viewH,setViewH] = useState(0) + const [curuser,setCuruser] = useState({}) + const [wsMsg, setWsMsg] = useState({}) + const [wsObj, setWsObj] = useState(null) + const [conversationId, setConversationId] = useState('') + const [isImgPreviewVisible, setIsImgPreviewVisible] = useState(false) //在线客服/图片预览 + const [imageParams, setImageParams] = useState({}) //图片属性 + const [show, setShow] = useState(false) + const [projectData, setProjectData] = useState([]) + const [showLoading, setShowLoading] = useState(false) + const [isSend, setIsSend] = useState(false) + const [sendMsg, setSendMsg] = useState([]) + const [agentNumber, setAgentNumber] = useState('') + //const [angle, setAngle] = useState(0) + const [pagination, setPagination] = useState({ + showQuickJumper: true, + showSizeChanger: false, + current: 1, + pageSize: 10, + total: 0, + }); + const refs = useRef() + const chatUI = document.getElementById('chatUI') + const record = document.getElementById('section') + const chatInput = document.getElementById('chatInput') + const listHeight = 290 + const textHeight = 150 + const wid = 600 + const hei = 500 + + let {isModalVisible, isCloseWs, closeWin, hideWin, msgAlert, supplierNumber, staffId, staffName, optionsModal, projectModal, closeOptionsModal, closeProjectModal, openWin, openProjectWin} = props + const onMsgRender = ()=>{ + let serverAvatar = user + let data = wsMsg + if(data && data.clientNo!=undefined){ + console.log('监听渲染') + let type = data.contentType==1?'text':data.contentType==2?'image':data.contentType==3?'file':'text' + let content={} + setConversationId(data.conversationId) + if(type=='text'){ + content = data.contentType==1?entitiestoUtf16(data.message) : data.message + }else if(type =='image'){ + let newcontent = data.message.indexOf('{')!= -1 ? JSON.parse(data.message):{} + content={ + content: newcontent.content, + realWidth: newcontent.realWidth, + realHeight:newcontent.realHeight, + msgWidth: newcontent.msgWidth, + msgHeight:newcontent.msgHeight, + previewidth: newcontent.previewidth, + previewHeight:newcontent.previewHeight, + success:false, + fileId:newcontent.fileId, + } + }else if(type =='file'){ + let newcontent = data.message.indexOf('{')!= -1 ? JSON.parse(data.message):{} + content={ + content: newcontent.content, + name: newcontent.name, + size:newcontent.size, + success:false, + fileId:newcontent.fileId, + } + } + let msgData={ + _id: Math.floor(Math.random() * 1000)+new Date(), + date: new Date(), + user: { + id: data.serverNo, + avatar: serverAvatar, + name: '供应链客服', + }, + message: { type: type, content: content }, + } + setMsgList([...msgList, msgData]) + msgAlert && msgAlert() + } + } + /*websocket*/ + const websocketInit = (id)=>{ + //const bizServiceCustomService = 'http://10.242.31.158:8100/api/biz-customer-service' + var addr = `${REACT_APP_CUSTOMERSERVICE_WS_REDIRECT}` + '/websocketClient/' + let no = agentNumber!=''?agentNumber: supplierNumber + var ws = new WebSocket(addr+id+'/'+no); + // 建立 web socket 连接成功触发事件 + ws.onopen = function () { + console.log("Connection open ..."); + //记录ws + setShow(true) //连接建立后,才可以显示弹窗、收发消息 + if(sendMsg && sendMsg.length>0) setIsSend(true) //连接建立后,sendMsg有值才可以发及时消息 + setWsObj(ws) + }; + + // 接收服务端数据时触发事件 + ws.onmessage = function (res) { + if(res.data){ + let data = JSON.parse(res.data) + console.log('onmessage') + console.log(data) + setWsMsg(data) + } + }; + + // 断开 web socket 连接成功触发事件 + ws.onclose = function () { + console.log("连接已关闭..."); + setWsObj(null) + }; + + } + //获取当前登录者token + const getToken = ()=>{ + //登录人的信息的关键字 顺序是固定的。 + let avatar = client + //当前用户 + setCuruser({ + id: staffId, + name: staffName, + avatar: avatar, + }) + API.check({ + clientNo: staffId, + supplierNo:agentNumber!=''?agentNumber: supplierNumber + }).then(res => { + if (res && res.success) { + websocketInit(staffId) + }else{ + closeWin && closeWin() + message.error(res && '已建立会话,无需重复建立') + } + }) + } +const onChange = (pagination) => { + setShowLoading(true) + getProject({ + pageNum: pagination.current, + pageSize: pagination.pageSize, + }); +} +const getProject = (params)=>{ + API.list({supplierNumber:supplierNumber}).then((res)=>{ + setShowLoading(false) + if(res && res.success && res.data){ + let data = res.data + setProjectData(data) + setPagination({ + showQuickJumper: true, + showSizeChanger: false, + pageSize: params.pageSize, + current: params.pageNum, + total: res.data.total, + showTotal: total => `共 ${total} 条`, + }) + }else{ + message.warn(res && res.message) + } + }) +} + useEffect(() => { + if(isModalVisible){ + //恢复初始值 + chatUI.style.width = wid + chatUI.style.height = hei + chatUI.style.left = (document.documentElement.clientWidth - wid)*0.5 +"px" + chatUI.style.top = (document.documentElement.clientHeight - hei)*0.5 +"px" + record.style.height = listHeight + "px" + chatInput.style.height = textHeight + "px" + }else{ + if(isCloseWs) setMsgList([]) + } + //获取当前用户token + if(isModalVisible && !isCloseWs && wsObj==null) getToken() + }, [isModalVisible]) + + useEffect(() => { + if(projectModal && supplierNumber!='EMPTY'){ + //获取项目 + setShowLoading(true) + getProject({ + pageNum: 1, + pageSize: 10, + }) + } + }, [projectModal]) + + useEffect(() => { + //关闭弹窗,清空消息列表 + if(isCloseWs && wsObj!=null) { + setShow(false) + wsObj.close() + } + }, [isCloseWs]) + + useEffect(() => { + //关闭弹窗,断开ws连接 + //防止ws还没open,就关闭弹窗了。加一次判断:建立好连接后,只要弹窗已关闭,就断开。 + if(isCloseWs && wsObj!=null) { + wsObj.close() + } + }, [wsObj]) + + useEffect(() => { + //onmessage时,触发,必须放到useEffect,直接写到onmessage里不渲染 + if(wsMsg && wsMsg.clientNo!=undefined) onMsgRender() + }, [wsMsg]) + + const columns=[ + { title: '项目名称', dataIndex: 'projectName', align: 'center', ellipsis:true, width:'40%'}, + { title: '代理公司', dataIndex: 'agentName', align: 'center', ellipsis:true, width:'40%'}, + { + title: '操作', align: 'center', width: '150px', + render: (text, record) => { + // let params = { + // "clientNo": curuser.id, + // "contentType":1, + // "custType": 1, + // "message": '项目名称:'+record.projectName+',项目编号:'+record.projectNumber+'代理公司:'+record.agentName+',代理公司编号:'+record.agentNumber, + // //"questionType": location.pathname.indexOf('/home')!=-1? 3 : 1, + // "questionType": 2, + // "supplierNo": supplierNumber + // } + setAgentNumber(record.agentNumber) + setSendMsg([{ + type:'text', + content:'项目名称:'+record.projectName+'
项目编号:'+record.projectNumber+'
项目状态:'+record.projectStatus + } + ]) + closeProjectModal && closeProjectModal() + openWin && openWin() + }} + okText="确定" + cancelText="取消" + >
+ }, + ] + return ( + <> +
+ {setIsSend(false); setSendMsg([])}} + chatUI={chatUI} + textHeight={textHeight} + listHeight={listHeight} + supplierNo={agentNumber!=''?agentNumber: supplierNumber} + conversationId={conversationId} + closeWin={()=>{ + closeWin && closeWin() + }} + hideWin={()=>{ + hideWin && hideWin() + }} + setPostion={()=>{ + setScrollToBottom(true) + setSwitchFlag(!switchFlag) + }} + changeHeight={(h)=>{ + setViewH(h) + }} + onSend={(msg) =>{ + setScrollToBottom(true) + setMsgList([...msgList, ...msg]) + setIsSend(false) + setSendMsg([]) + }} + showPic={(imageParams)=>{ + setImageParams(imageParams) + setIsImgPreviewVisible(true) + }} + style={{ + width: wid, + height: hei, + }} + /> +
+ {isImgPreviewVisible &&
{if(e.target.id=='blankArea' || e.target.id=='popUpImage'){e.persist(); handleElement.handleDrag(e,refs)} }}> + { + var html=document.querySelector("html") + var body=document.querySelector("body") + html.style.overflow="visible" + body.style.overflow="visible" + setIsImgPreviewVisible(false) + }} + maxSizePreview={()=>{ + refs.current.style.width=document.documentElement.clientWidth + "px" + refs.current.style.height=document.documentElement.clientHeight + "px" + refs.current.style.top= '50%' + refs.current.style.left= '50%' + refs.current.style.transform='translate(-50%,-50%)' + }} + normalSizePreview={()=>{ + refs.current.style.width= wid + "px" + refs.current.style.height= hei + "px" + refs.current.style.top= '50%' + refs.current.style.left= '50%' + refs.current.style.transform='translate(-50%,-50%)' + }} + /> +
} + {optionsModal && isCloseWs && + { + closeOptionsModal && closeOptionsModal() + }}> +

openWin && openWin()}>点击咨询平台客服(平台操作相关问题)

+

openWin && openWin()}>点击咨询智慧安全公司客服(iPASS及电子签章相关问题)

+

openProjectWin && openProjectWin()}>点击咨询代理机构客服(项目内容相关问题)

+
+ } + {projectModal && + { + closeWin && closeWin() + }}> + {supplierNumber=='EMPTY'?

没有要咨询的项目

: record['projectNumber']} + loading={showLoading} + pagination={pagination} + onChange={onChange} + />} + + } + + ) +} +export default conversation diff --git a/src/pages/customerservice/support/conversation/style/common.css b/src/pages/customerservice/support/conversation/style/common.css new file mode 100644 index 0000000..fb6a08b --- /dev/null +++ b/src/pages/customerservice/support/conversation/style/common.css @@ -0,0 +1,21 @@ +/* flex */ +.flex { + display: flex; +} +.flex_1 { + flex: auto; +} +.jus_cen { + justify-content: center; +} +.ali_cen { + align-items: center; +} + +/* position */ +.pos_a { + position: absolute; +} +.pos_r { + position: relative; +} diff --git a/src/pages/customerservice/support/conversation/utils/toClass.js b/src/pages/customerservice/support/conversation/utils/toClass.js new file mode 100644 index 0000000..1e0ebdc --- /dev/null +++ b/src/pages/customerservice/support/conversation/utils/toClass.js @@ -0,0 +1,3 @@ +export function cns(classes = []) { + return classes.join(' ') +} diff --git a/src/pages/customerservice/support/conversation/utils/utils.jsx b/src/pages/customerservice/support/conversation/utils/utils.jsx new file mode 100644 index 0000000..8a8e701 --- /dev/null +++ b/src/pages/customerservice/support/conversation/utils/utils.jsx @@ -0,0 +1,537 @@ +const sm2 = require('sm-crypto').sm2 + +export function AutoResizeImage(maxWidth,maxHeight,objImg) { + var img = new Image(); + img.src = objImg.src; + var hRatio; + var wRatio; + var Ratio = 1; + var w = img.width; + var h = img.height; + wRatio = maxWidth / w; + hRatio = maxHeight / h; + if (maxWidth ==0 && maxHeight==0){ + Ratio = 1; + }else if (maxWidth==0){// + if (hRatio<1) Ratio = hRatio; + }else if (maxHeight==0){ + if (wRatio<1) Ratio = wRatio; + }else if (wRatio<1 || hRatio<1){ + Ratio = (wRatio<=hRatio?wRatio:hRatio); + } + if (Ratio<1){ + w = w * Ratio; + h = h * Ratio; + } + objImg.style.height = h+"px"; + objImg.style.width = w+"px"; +} +export function GetResizeImg(maxsize,obj) { + var img = new Image(); + img.src = obj.src; + var hRatio; + var wRatio; + var Ratio = 1; + var w = img.width; + var h = img.height; + var maxWidth; + var maxHeight; + + if(w>=h){ + maxWidth = maxsize + maxHeight = 0 + + }else{ + maxWidth = 0 + maxHeight = maxsize + } + + wRatio = maxWidth / w; + hRatio = maxHeight / h; + + if (maxWidth ==0 && maxHeight==0){ + Ratio = 1; + }else if (maxWidth==0){// + if (hRatio<1) Ratio = hRatio; + }else if (maxHeight==0){ + if (wRatio<1) Ratio = wRatio; + }else if (wRatio<1 || hRatio<1){ + Ratio = (wRatio<=hRatio?wRatio:hRatio); + } + /* + wRatio = maxWidth / w; + hRatio = maxHeight / h; + if (maxWidth ==0 && maxHeight==0){ + Ratio = 1; + }else if (maxWidth==0){// + if (hRatio<1) Ratio = hRatio; + }else if (maxHeight==0){ + if (wRatio<1) Ratio = wRatio; + }else if (wRatio<1 || hRatio<1){ + Ratio = (wRatio<=hRatio?wRatio:hRatio); + } + */ + if (Ratio<1){ + w = w * Ratio; + h = h * Ratio; + } + + return {"width":w,"height":h} +} +export function GetNewsize(maxsize,width,height) { + var hRatio; + var wRatio; + var Ratio = 1; + var w = width; + var h = height; + + var maxWidth; + var maxHeight; + + if(w>=h){ + maxWidth = maxsize + maxHeight = 0 + + }else{ + maxWidth = 0 + maxHeight = maxsize + } + + wRatio = maxWidth / w; + hRatio = maxHeight / h; + if (maxWidth ==0 && maxHeight==0){ + Ratio = 1; + }else if (maxWidth==0){// + if (hRatio<1) Ratio = hRatio; + }else if (maxHeight==0){ + if (wRatio<1) Ratio = wRatio; + }else if (wRatio<1 || hRatio<1){ + Ratio = (wRatio<=hRatio?wRatio:hRatio); + } + if (Ratio<1){ + w = w * Ratio; + h = h * Ratio; + } + return {"width":w,"height":h} +} +/*over、move、拖拽*/ +export function mouseOverHandle(e,chatUI){ + let clickBox = e.target + var direction = 0; + + var mouseDownX = e.clientX; + var mouseDownY = e.clientY; + var clickBoxTop = chatUI.offsetHeight - clickBox.offsetHeight + chatUI.offsetTop + var clickBoxBottom = clickBoxTop + 60 + var clickBoxLeft = chatUI.offsetLeft; + var clickBoxRight = chatUI.offsetWidth + clickBoxLeft + + if(clickBox.id=="chatInput" ){ + e=e||event; //兼容ie和其他浏览器的写法 + + if ( mouseDownY < clickBoxTop + 5){ + direction = 'top'; + } + + if(clickBox.id=="chatHeader" ){ + e=e||event; //兼容ie和其他浏览器的写法 + if ((mouseDownY > clickBoxTop && mouseDownY < clickBoxBottom) || (mouseDownX > clickBoxLeft && mouseDownX < clickBoxRight)){ + direction = 'move'; + } + } + + if(direction!=0){ + if(direction=="left" || direction=="right"){ + clickBox.style.cursor='w-resize' + }else if(direction=="top" || direction=="bottom"){ + clickBox.style.cursor='s-resize' + }else{ + clickBox.style.cursor="auto" + } + }else if(direction == "move"){ + clickBox.style.cursor="move" + }else{ + clickBox.style.cursor="auto" + } + } + +} +export function mouseMoveHandle(e,chatUI){ + let clickBox = e.target + var direction = 0; + clickBox.style.cursor="auto" + + var mouseDownX = e.clientX; + var mouseDownY = e.clientY; + var clickBoxTop = chatUI.offsetHeight - clickBox.offsetHeight + chatUI.offsetTop + var clickBoxBottom = clickBoxTop + 60 + var clickBoxLeft = chatUI.offsetLeft; + var clickBoxRight = chatUI.offsetWidth + clickBoxLeft + + if(clickBox.id=="chatInput" ){ + e=e||event; //兼容ie和其他浏览器的写法 + + if ( mouseDownY < clickBoxTop + 5){ + direction = 'top'; + } + } + if(clickBox.id=="chatHeader" ){ + e=e||event; //兼容ie和其他浏览器的写法 + if ((mouseDownY > clickBoxTop && mouseDownY < clickBoxBottom) || (mouseDownX > clickBoxLeft && mouseDownX < clickBoxRight)){ + direction = 'move'; + } + } + + if(direction!=0){ + if(direction=="left" || direction=="right"){ + clickBox.style.cursor='w-resize' + }else if(direction=="top" || direction=="bottom"){ + clickBox.style.cursor='s-resize' + }else if(direction == "move"){ + clickBox.style.cursor="move" + }else { + clickBox.style.cursor="auto" + } + }else{ + clickBox.style.cursor="auto" + } +} +export function mouseDrag(e,selfHeight,chatUI,changeHeight,setPostion){ + + let clickBox = e.target + var direction = 0; + let count = 0 + + var mouseDownX = e.clientX; + var mouseDownY = e.clientY; + var clickBoxTop = chatUI.offsetHeight - clickBox.offsetHeight + chatUI.offsetTop + var clickBoxBottom = clickBoxTop + 60 + var clickBoxLeft = chatUI.offsetLeft; + var clickBoxRight = chatUI.offsetWidth + clickBoxLeft + var ll = chatUI.offsetLeft + var tt = chatUI.offsetTop + let record=null; + let topHeight=0; + + var maxLeft = document.documentElement.clientWidth - chatUI.offsetWidth + var maxTop = document.documentElement.clientHeight - chatUI.offsetHeight + + if(clickBox.id=="chatInput" ){ + e=e||event; //兼容ie和其他浏览器的写法 + var clickBoxHeight = clickBox.offsetHeight; + var input_maxHeight = chatUI.offsetHeight - 60 - 40 + var record_maxHeight = chatUI.offsetHeight - 60 - selfHeight + record = document.getElementById('section') + topHeight = record.offsetHeight + + if ( mouseDownY < clickBoxTop + 10){ + direction = 'top'; + } + } + if(clickBox.id=="chatHeader" ){ + e=e||event; //兼容ie和其他浏览器的写法 + if ((mouseDownY > clickBoxTop && mouseDownY < clickBoxBottom) || (mouseDownX > clickBoxLeft && mouseDownX < clickBoxRight)){ + direction = 'move'; + } + } + document.onmousemove = function(e) { + e = e || event; //是要是使用原生js给我们提供的e回调参数,这存储了很多有用的信息 + + var yy = e.clientY; + var xx = e.clientX; + + if (clickBox.id=="chatInput" && direction==='top'){ + if(count==0 && setPostion && setPostion!=undefined) { + setPostion() + count++ + } + record.style.height = Math.min(record_maxHeight,(topHeight - mouseDownY + yy) < 40 ? 40:(topHeight - mouseDownY + yy)) + 'px'; + clickBox.style.height = Math.min(input_maxHeight, (clickBoxHeight + mouseDownY - yy) < selfHeight? selfHeight: (clickBoxHeight + mouseDownY - yy)) + 'px'; + if(changeHeight && changeHeight!=undefined) changeHeight(Math.min(record_maxHeight,(topHeight - mouseDownY + yy) < 20 ? 20:(topHeight - mouseDownY + yy))) + + } + if (clickBox.id=="chatHeader" && direction==='move'){ + + var l = xx - mouseDownX + ll + var t = yy - mouseDownY + tt + + chatUI.style.left = l <0 ? 0 : Math.min(l,maxLeft) +"px" + chatUI.style.top = t < 0 ? 0 : Math.min(t,maxTop)+"px" + + chatUI.style.transform='' + //chatUI.style.top = t < 0 ? 0 : Math.min(t,maxTop) + } + //return false; //这里为了避免抖动 + }; + document.onmouseup = function(e) { + document.onmousemove = null; + document.onmouseup = null; + e.target.style.cursor='auto' + }; + + if (e.preventDefault){ + e.preventDefault(); + } +} +/*base64转blob*/ +export function previewHtmlByBase64 (base64Str, fileType) { + if (base64Str) { + const myBlob = parseBase64ToBlob(base64Str, fileType) + return myBlob + } + } +export function parseBase64ToBlob (data, fileType) { + let bstr = window.atob(data) + let n = bstr.length + const u8arr = new Uint8Array(n) + while (n--) { + u8arr[n] = bstr.charCodeAt(n) + } + return new Blob([u8arr], { type: fileType }) +} +// 表情解码 +export function entitiestoUtf16 (strObj) { + const patt = /&#\d+;/g; + const arr = strObj.match(patt) || []; + + let H; + let L; + let code; + + for (let i = 0; i < arr.length; i += 1) { + code = arr[i]; + code = code.replace('&#', '').replace(';', ''); + // 高位 + H = Math.floor((code - 0x10000) / 0x400) + 0xD800; + // 低位 + L = ((code - 0x10000) % 0x400) + 0xDC00; + code = `&#${code};`; + const s = String.fromCharCode(H, L); + strObj = strObj.replace(code, s); + } + return strObj; + } +// 表情转义保存数据库 +export function utf16toEntities (str){ +const patt = /[\ud800-\udbff][\udc00-\udfff]/g; // 检测utf16字符正则 +str = str.replace(patt, (char) => { + let H; + let L; + let code; + let s; + + if (char.length === 2) { + H = char.charCodeAt(0); // 取出高位 + L = char.charCodeAt(1); // 取出低位 + code = (H - 0xD800) * 0x400 + 0x10000 + L - 0xDC00; // 转换算法 + s = `&#${code};`; + } else { + s = char; + } + + return s; +}); + +return str; +} +//补齐html标签 +export function closeHTML(str){ + var arrTags=["span","font","b","u","i","h1","h2","h3","h4","h5","h6","p","li","ul","table","div"]; + for(var i=0;i]+|)\\>","ig"); + var arrMatch=str.match(re); + if(arrMatch!=null) intOpen=arrMatch.length; + re=new RegExp("\\<\\/"+arrTags[i]+"\\>","ig"); + arrMatch=str.match(re); + if(arrMatch!=null) intClose=arrMatch.length; + for(var j=0;j"; + } + } + return str; +} +/*图片处理*/ +export const imageHandleUtil = { + //插入图片 + insertImage(img, range, reformMsg, resetCachedRange,resetLoadling){ + range.insertNode(img) + resetCachedRange && resetCachedRange(range) + range.collapse(false) + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + reformMsg && reformMsg() + resetLoadling && resetLoadling() + }, + //图片调整大小、增加onclick事件 + changeImage(img, showPic, imgInputSize, imgMsgSize, imgPreviewSize){ + let w = img.width + let h =img.height + let inputSize = GetNewsize(imgInputSize,w,h) + let msgSize = GetNewsize(imgMsgSize,w,h) + let previewSize = GetNewsize(imgPreviewSize,w,h) + + img.style.width=inputSize.width+"px" + img.style.height=inputSize.height+"px" + + let realWidth = img.realWidth = w+"px" + let realHeight = img.realHeight = h+"px" + let msgWidth = img.msgWidth = msgSize.width+"px" + let msgHeight = img.msgHeight = msgSize.height+"px" + let previewidth = img.previewidth = previewSize.width+"px" + let previewHeight = img.previewHeight = previewSize.height+"px" + + img.onclick = function() { + let src = img.src + let param = { + src, realWidth, realHeight, msgWidth, msgHeight, previewidth, previewHeight + } + showPic(param); + } + }, + //粘贴图片 + pasteImages (params){ + let {srcArr, sig, el, range, reformMsg, showPic, resetCachedRange, resetLoadling, imgInputSize, imgMsgSize, imgPreviewSize} = params + var img=new Image(); + img.src=srcArr[sig].src + img.onload=function(){ + //console.log(srcArr[sig]) + //if(!srcArr[sig].style.width || srcArr[sig].style.width==undefined || srcArr[sig].style.width==''){ + if(srcArr[sig].src.indexOf('base64')!=-1){ + var dataarr = img.src.split(',') //分割为数组,分割到第一个逗号 + let mime = dataarr[0].match(/:(.*?);/)[1] //获取分割后的base64前缀中的类型 + let myBlob = parseBase64ToBlob(dataarr[1], mime); + srcArr[sig].src = URL.createObjectURL(myBlob); + } + imageHandleUtil.changeImage(srcArr[sig], showPic, imgInputSize, imgMsgSize, imgPreviewSize) + //} + if(sig{ + imageHandleUtil.changeImage(img, showPic, imgInputSize, imgMsgSize, imgPreviewSize) + imageHandleUtil.insertImage(img, range, reformMsg, resetCachedRange, resetLoadling) + } + } + }, + // canvas转dataURL:canvas对象、转换格式、图像品质 + canvasToDataURL(canvas, format, quality){ + return canvas.toDataURL(format||'image/*', quality||1.0); + }, + // DataURL转Blob对象 + dataURLToBlob(dataurl){ + var arr = dataurl.split(','); + var mime = arr[0].match(/:(.*?);/)[1]; + let blob = parseBase64ToBlob(arr[1], mime); + return blob + }, + // image转canvas:图片地址 + imageToCanvas(src, cb){ + var canvas = document.createElement('CANVAS'); + var ctx = canvas.getContext('2d'); + var img = new Image(); + + img.onload = function (){ + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + cb(canvas); + }; + img.setAttribute("crossOrigin",'anonymous') + img.src = src + }, + // image转Blob + imageToBlob(msgData, imgUpload){ + imageHandleUtil.imageToCanvas(msgData.message.content.content, function (canvas){ + let blob = imageHandleUtil.dataURLToBlob(imageHandleUtil.canvasToDataURL(canvas)); + imgUpload && imgUpload(blob, msgData) + }) + }, + uploadImage(msgData, imgUpload){ + imageHandleUtil.imageToBlob(msgData, imgUpload) + }, +} + +//拖动组件 +export const handleElement = { + handleDrag(e,imgRef){ + const o = imgRef.current + const s = o.style + const p = 'onmousemove' + //在jsx中需要用e.persist()此方法会从池中移除合成事件,允许用户代码保留对事件的引用,否则clientX会是null + let x = e.clientX - o.offsetLeft; + let y = e.clientY - o.offsetTop; + document[p] = function (e) { + s.left = e.clientX - x + 'px'; + s.top = e.clientY - y + 'px'; + } + + document.onmouseup = function () { + document[p] = null + } + }, + handleZoom(e, imgRef, scales, changeScales){ + let { style } = imgRef.current + if (e.nativeEvent.deltaY <= 0 && scales < 5 ) { + let a = scales+0.05 + style.transform = 'translate(-50%,-50%) scale('+a+')' + changeScales && changeScales(a) + } else if (e.nativeEvent.deltaY > 0) { + if(scales>0.5){ + let a = scales-0.05 + style.transform = 'translate(-50%,-50%) scale('+a+') ' + changeScales && changeScales(a) + } + } + }, + + handleStopDrag(e){ + e.preventDefault() + }, +} +export function downloadFile(data, name, suffix) { + if (window.navigator && window.navigator.msSaveBlob) { + window.navigator.msSaveBlob(new Blob([data]),name + (suffix ? ('.' + suffix) : '')); + }else{ + const url = window.URL.createObjectURL(new Blob([data])) + const link = document.createElement('a') + link.style.display = 'none' + link.href = url + // const fileName = parseTime(new Date()) + '-' + name + (suffix ? ('.' + suffix) : '') + const fileName = name + (suffix ? ('.' + suffix) : '') + link.setAttribute('download', fileName) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + } +//加密 +export function doEncrypt(value) { + let encryptData = sm2.doEncrypt(value, `${REACT_APP_CUSTOMERSERVICE_PUBLICKEY}`, `${REACT_APP_CUSTOMERSERVICE_CIPHERCODE}`) + return '04' + encryptData; + } + //解密 + export function doDecrypt(value) { + let newValue = value.slice(2, value.length); + let decryptData = sm2.doDecrypt(newValue, `${REACT_APP_CUSTOMERSERVICE_PRIVATEKEY}`, `${REACT_APP_CUSTOMERSERVICE_CIPHERCODE}`) // 解密结果 + return decryptData; + } \ No newline at end of file diff --git a/src/services/customerservice.js b/src/services/customerservice.js new file mode 100644 index 0000000..e65295f --- /dev/null +++ b/src/services/customerservice.js @@ -0,0 +1,60 @@ +import { $post, $get } from '@/customerServiceHelpers/request'; +import request from '@/utils/request'; + +export const customerserviceAPI = { + //发送消息 + sendToCs(params) { + return request('/api/api/biz-customer-service/msg/sendToCs',{ + method:'POST', + data:{ + ...params, + } + }); + }, + //上传附件 + fileUpload(params){ + return $post('/api/doc/v1.0/files/upload?appCode=upload_cs&objectId=22&objectType='+params.objectType, { + params: params.multipartFiles, + headers: { + 'Content-Type': 'multipart/form-data' + }, + }); + }, + //下载附件 + getSecretKey(params) { + const downloadService = '/api/doc/api/data-service-document-center' + return $get(`${downloadService}` + '/outer/v1.0/files/getSecretKey', { params }); + }, + /** + * + * @param {object} params + * + * 物流订单详情附件下载 + */ + getDownload(params) { + const downloadService = '/api/doc/api/data-service-document-center' + return $post(`${downloadService}` + '/outer/v1.0/files/getDownloadBody', { params, responseType: 'arraybuffer' }); + }, + check(params) { + return request('/api/api/biz-customer-service/conversation/check',{ + method:'POST', + data:{ + ...params, + } + }); + }, + //评价 + estimate(params) { + return request('/api/api/biz-customer-service/outer/v1.0/eshopConversation/updateConversationById',{ + method:'POST', + data:{ + ...params, + } + }); + }, + //获取咨询项目 + list(params) { + let method = "get"; + return request('/api/biz-service-ebtp-tender/v1/participateProject/list?supplierNumber=' + params.supplierNumber, { method: method }) + }, +} \ No newline at end of file