react客服
index.tsx import { useEffect, useRef, useState } from 'react'; import './index.less'; import { membersList, messageList } from '@/services/info/info-api'; import { Badge, message } from 'antd'; import { GbDivider } from '@/components'; import { LeftOutlined } from '@ant-design/icons'; import { useLocation } from 'react-router-dom'; import MainChatting from './main-chatting'; import FooterPage from './footer-page'; //客服聊天 export default function Info() { const [value, setValue] = useState(''); const [fileImg, setFileImg] = useState<string>(''); const useLocationTo = useLocation(); console.log('useLocationTo', useLocationTo); const [membersLeft, setMembersLeft] = useState<any[]>([]); const [messageRight, setMessageRight] = useState<any[]>([]); const [sendFunc, setSendFunc] = useState<any>(); const [dataList, setDataList] = useState<any>(); const mainRef = useRef<any>(); let ws: any = null; let list = localStorage.getItem('rodledata') ? JSON.parse(localStorage.getItem('rodledata') as string) : []; const userName = list?.loginName; const connectWebSocket = () => { if ('WebSocket' in window) { ws = new WebSocket(`ws://xxx/${userName}`); setSendFunc(ws); ws.onopen = () => { console.log('服务器已连接'); }; ws.onmessage = (msg: any) => { leftMembersList(); console.log('来自服务器端的数据:' + dataList?.msg, msg.data); //监听接受来自服务端的信息 }; ws.onclose = () => { console.log('服务器关闭'); }; } else { alert('当前浏览器不支持Websocket'); } }; const sendserver = (type: string) => { const socketMsg = { msg: type === 'text' ? value : fileImg, typeMsg: type, toUser: dataList?.senderName, showName: dataList?.showName, type: dataList?.type, fromUser: dataList?.userName, userId: dataList?.userId, senderId: dataList?.senderId, userUrl: list?.userAvatar // ? list?.userAvatar // : 'http://frphn2.chickfrp.com:10029/fisp-image/2023/07/18/341266221211459c96ec430bc01f2853/logos.png"' }; console.log('socketMsg', socketMsg); // if (value || fileImg) { sendFunc.send(JSON.stringify(socketMsg)); setValue(''); } else { message.success('请输入内容'); } }; const leftMembersList = async () => { const data: any = { username: userName, orderId: useLocationTo?.state?.orderId }; await membersList(data).then((res) => { console.log('fgggggggg', res); setMembersLeft(res.data); const messagedata: any = { socketMsg: { type: res?.data[0]?.type, toUser: res?.data[0]?.userName, fromUser: res?.data[0]?.senderName, msg: '', senderId: res?.data[0]?.senderId, userId: res?.data[0]?.userId, userUrl: res?.data[0]?.senderUrl } }; setDataList(res?.data[0]); console.log('res?.data[0]', res?.data[0]); console.log('df', res?.data[0]); if (res?.data?.length > 0) { getNowMessageList(messagedata); } connectWebSocket(); }); }; const handleRef = () => { mainRef.current?.scrollIntoView({ behavior: 'auto' }); }; useEffect(() => { handleRef(); }, [messageRight]); const getNowMessageList = async (data: any) => { await messageList(data).then((res: any) => { if (res.success) { console.log('右边信息', res.data); setMessageRight(res.data); } }); }; useEffect(() => { leftMembersList(); }, []); return ( <div className="in-wrap"> <div style={{ cursor: 'pointer' }} className={'edit-top'}> <LeftOutlined onClick={() => { window.history.back(); }} style={{ padding: '15px 5px 15px 15px' }} /> <p style={{ margin: 0, padding: '10px 5px 10px 0px', display: 'inline-block' }}>客服</p> <GbDivider /> </div> <div className="info-page"> <div className="info-right"> {membersLeft?.map((item, index) => { return ( <div className="info-right-header" key={index}> <Badge count={item.count}>{item.showName}</Badge> </div> ); })} <div className="info-right-main"> {messageRight?.map((i, v) => { return ( <div key={v} className="main-wrap" ref={mainRef} style={{ textAlign: userName === i.fromName ? 'right' : 'left' }} > {/* 聊天框 */} <MainChatting data={{ typeMsg: i.typeMsg, topsubtitle: `${i?.fromName} ${i?.createTime}`, userName: userName, fromName: i?.fromName, message: i?.message, userUrl: i.userUrl }} /> </div> ); })} </div> <div className="info-right-footer"> <FooterPage data={{ setValue: setValue, setFileImg: setFileImg, sendserver: sendserver, value: value }} ></FooterPage> </div> </div> </div> </div> ); }
main-chatting.tsx import React from 'react'; import textImg from '../../../assets/wjj2.png'; import { Image } from 'antd'; export default function MainChatting(props: { data: { typeMsg: string; topsubtitle: any; userName: string; fromName: string; message: string; userUrl: string; }; }) { const { data } = props; const handletype = (type: string, dri: string) => { if (type == 'text') { return ( <> {dri === 'right' ? ( <div className="info-img-right" style={{ marginLeft: 'auto' }}> {data?.message} </div> ) : ( <div className="info-img-left">{data?.message}</div> )} </> ); } else if (type == 'img') { return ( <> {dri === 'right' ? ( <div className="main-img-right" style={{ marginLeft: 'auto' }}> <Image src={data?.message} alt="" /> </div> ) : ( <div className="main-img-left"> <Image src={data?.message} alt="" /> </div> )} </> ); } else if (type == 'file') { return ( <> {dri === 'right' ? ( <div className="main-file-right" style={{ marginLeft: 'auto' }}> <span> { data?.message?.split('/').slice(-1)[ data?.message?.split('/').slice(-1).length - 1 ] } </span> <a target="_blank" href={data?.message} download> <img style={{ width: '25px', objectFit: 'contain', marginLeft: '10px' }} src={textImg} alt="" /> </a> </div> ) : ( <div className="main-file-left"> <span> { data?.message?.split('/').slice(-1)[ data?.message?.split('/').slice(-1).length - 1 ] } </span> <a target="_blank" href={data?.message} download> <img style={{ width: '25px', objectFit: 'contain', marginLeft: '10px' }} src={textImg} alt="" /> </a> </div> )} </> ); } }; return ( <div className="main-card"> <p>{data?.topsubtitle}</p> <div className="info-img"> {data?.userName === data?.fromName ? ( <> {handletype(data?.typeMsg, 'right')} <img style={{ marginLeft: '10px' }} src={data?.userUrl} alt="" /> </> ) : ( <> <img style={{ marginRight: '10px' }} src={data?.userUrl} alt="" /> {handletype(data?.typeMsg, 'left')} </> )} </div> </div> ); }
footer-page.tsx import { Webmsg } from '@/services/info/info-api'; import { Button, Popover, Upload, message } from 'antd'; import TextArea from 'antd/lib/input/TextArea'; import React, { useEffect, useState } from 'react'; const baseUrl = import.meta.env.VITE_APP_PROXY_URL; import type { UploadProps } from 'antd/es/upload/interface'; export default function FooterPage(props: { data: { setValue: any; setFileImg: any; sendserver: any; value: any; }; }) { const { data } = props; const [open, setOpen] = useState(false); const [look, setLook] = useState([]); const handleWebmsg = async () => { await Webmsg().then((res) => { console.log('表情', res?.data); setLook(res?.data); }); }; const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { console.log('Change:', e.target.value); data?.setValue(e.target.value); }; const sendImg = (remove: any, file: any) => { if (file?.type?.indexOf('image/') === 0) { data?.sendserver('img'); } else if (file?.type?.indexOf('application/') === 0) { data?.sendserver('file'); } remove(file); }; const cancelImg = (remove: any, file: any) => { console.log(remove); remove(file); }; const hide = (i: string) => { data?.setValue(i); setOpen(false); }; const handleOpenChange = (newOpen: boolean) => { setOpen(newOpen); }; const propsUpload: UploadProps = { maxCount: 1, name: 'file', action: `${baseUrl}electronic_seal/upload`, headers: { Authorization: unescape(localStorage.getItem('token') as string), 'User-Agent': navigator.userAgent }, onChange(info) { console.log('info', info); if (info.file.status !== 'uploading') { console.log('成功上传', info); data?.setFileImg(info?.file?.response?.data?.objectUrl); } if (info.file.status === 'done') { message.success('上传成功'); } else if (info.file.status === 'error') { message.error(`上传失败`); } } }; useEffect(() => { handleWebmsg(); }, []); return ( <> <div className="footer-top"> <Popover content={ <div style={{ maxWidth: '400px' }}> {look?.map((i: any, v: number) => { return ( <span onClick={() => hide(i)} style={{ padding: '10px', fontSize: '20px', cursor: 'pointer' }} key={v} > {i} </span> ); })} </div> } trigger="click" open={open} onOpenChange={handleOpenChange} > <img style={{ padding: '5px 0 0 5px', width: '40px', cursor: 'pointer' }} src="http://frphn1.chickfrp.com:43961/image/xiao.svg" alt="" /> </Popover> <div> <Upload itemRender={(originNode, file, fileList: object[], actions) => ( <div className="footer-imglist"> {originNode} <div style={{ textAlign: 'right' }}> <Button onClick={() => cancelImg(actions?.remove, file)} style={{ margin: '10px 10px 0 0' }} > 取消 </Button> <Button type="primary" onClick={() => sendImg(actions?.remove, file)}> 发送 </Button> </div> </div> )} {...propsUpload} > <img style={{ padding: '5px 0 0 5px', width: '40px', cursor: 'pointer' }} src="http://frphn1.chickfrp.com:43961/image/upload.svg" alt="" /> </Upload> </div> </div> <TextArea style={{ fontSize: '20px' }} onChange={onChange} value={data?.value} bordered={false} autoSize={{ minRows: 3, maxRows: 5 }} rows={3} className="textarea-input" /> <div className="btn"> <Button type="primary" onClick={() => data?.sendserver('text')}> 发送(s) </Button> </div> </> ); }
index.less .info-page { background-color: @white; height: calc(100% - 56px); display: flex; flex-wrap: nowrap; background-color: #f5f5f5; box-sizing: border-box; border: 1px solid rgb(218, 213, 213); } .info-left { overflow: auto; min-width: 200px; border: 1px solid rgb(218, 213, 213); } .info-right { display: flex; flex-direction: column; flex: 1; .info-right-header { padding: 10px; height: auto; cursor: pointer; border-bottom: 1px solid rgb(218, 213, 213); } .info-right-main { overflow: auto; .main-wrap { width: 100%; } flex: 1; .main-card { display: inline-block; margin: 10px; // padding: 10px; } } .info-right-footer { .textarea-input { background-color: #f5f5f5; } // height: 130px; border-top: 1px solid rgb(218, 213, 213); .btn { padding: 10px; text-align: right; } } } .main-card { img { width: 40px; height: 40px; } .info-img { display: flex; flex-wrap: nowrap; } .infoimg { padding: 10px; border-radius: 5px; font-size: 20px; word-wrap: break-word; border-radius: 5px; text-align: left; max-width: 300px; } .info-img-right { .infoimg(); background-color: #95ec69; } .info-img-left { .infoimg(); background-color: white; } .main-img-left, .main-img-right { padding: 10px; border-radius: 2px; padding: 10px; font-size: 20px; word-wrap: break-word; text-align: left; width: 100px; height: 100px; img { width: 100%; height: 100%; object-fit: contain; } } .main-file-left, .main-file-right { width: 200px; display: flex; align-items: center; span { margin-left: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } padding: 10px; border-radius: 5px; background-color: white; } } .in-wrap { height: calc(100% - 16px); background-color: white; .edit-top { font-size: 18px; color: @primary-color; } } .footer-top { display: flex; position: relative; .footer-imglist { padding: 10px; border-radius: 5px; width: 200px; height: auto; z-index: 1000; background-color: white; position: absolute; top: -100px; } }
聊天时间 const isYear = (timeValue: any) => { // 是否为今年 const dateyear = new Date(timeValue).getFullYear(); const toyear = new Date().getFullYear(); // console.log(dateyear, toyear) if (dateyear === toyear) { return true; } else { return false; } }; // 转化日期 如2018-7-6 -->(2018-07-06) const formatNumber = (n: any) => { n = n.toString(); return n[1] ? n : '0' + n; }; const getWeeken = (date: any) => { var weekArray = new Array('周日', '周一', '周二', '周三', '周四', '周五', '周六'); var week = weekArray[new Date(date).getDay()]; return week; }; const formatTime = (date: any) => { var t = getTimeArray(date); return ( [t[0], t[1], t[2]].map(formatNumber).join('-') + ' ' + [t[3], t[4], t[5]].map(formatNumber).join(':') ); }; const getTimeArray = (date: any) => { date = new Date(date); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); var hour = date.getHours(); var minute = date.getMinutes(); var second = date.getSeconds(); return [year, month, day, hour, minute, second].map(formatNumber); }; const isYestday = (timeValue: any) => { // 是否为昨天 const date = new Date(timeValue); const today = new Date(); if (date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth()) { if (today.getDate() - date.getDate() === 1) { return true; } else { return false; } } else { return false; } }; export const handlerMsgTime = (timeValue: any) => { timeValue = new Date(timeValue).getTime(); var timeNew = new Date().getTime(); // 当前时间 // console.log('传入的时间', timeValue, timeNew) var timeDiffer = timeNew - timeValue; // 与当前时间误差 // console.log('时间差', timeDiffer) var returnTime = ''; // if (timeDiffer <= 60000) { // // 一分钟内 // returnTime = "刚刚"; // } else if (timeDiffer > 60000 && timeDiffer < 3600000) { // // 1小时内 // returnTime = Math.floor(timeDiffer / 60000) + "分钟前"; // } else if ( // timeDiffer >= 3600000 && if (timeDiffer < 86400000 && isYestday(timeValue) === false) { // 今日 returnTime = formatTime(timeValue).substr(11, 5); } else if (timeDiffer > 3600000 && isYestday(timeValue) === true) { // 昨天 returnTime = '昨天 ' + formatTime(timeValue).substr(11, 5); } else if (timeDiffer > 86400000 && timeDiffer <= 518400000) { // 星期几 returnTime = getWeeken(timeValue) + ' ' + formatTime(timeValue).substr(11, 5); } else if ( timeDiffer > 86400000 && isYestday(timeValue) === false && isYear(timeValue) === true ) { // 今年 returnTime = formatTime(timeValue).substr(5, 11); } else if ( timeDiffer > 86400000 && isYestday(timeValue) === false && isYear(timeValue) === false ) { // 不属于今年 returnTime = formatTime(timeValue).substr(0, 16); } return returnTime; }; export const counttime = (v: number) => { if (v == 0) { return v; } else { return v - 1; } }; export const checkTime = (a: any, b: any, c: any) => { var timeOut = 5 * 60 * 1000; //设置超时时间 // 上一个 let syg: any = new Date(a); var compareTime = Date.parse(syg); let dq: any = new Date(b); var currentTime = Date.parse(dq); //对比时间 if (compareTime - currentTime < timeOut) { console.log('您已长时间未操作,即将退出登录 '); return null; } else { return c; } };
本文来自博客园,作者:zjxgdq,转载请注明原文链接:https://www.cnblogs.com/zjxzhj/p/17560518.html