feat: 多渠道回复框

dev/email
Lei OT 10 months ago
parent 0a90396b6a
commit 03e437a075

@ -12,14 +12,17 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@lexical/react": "^0.17.1",
"@vonage/client-sdk": "^1.6.0", "@vonage/client-sdk": "^1.6.0",
"antd": "^5.14.0", "antd": "^5.14.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"emoji-picker-react": "^4.8.0", "emoji-picker-react": "^4.8.0",
"lexical": "^0.17.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-chat-elements": "^12.0.11", "react-chat-elements": "^12.0.11",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-quill": "^2.0.0", "react-quill": "^2.0.0",
"react-router-dom": "^6.21.1", "react-router-dom": "^6.21.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",

@ -0,0 +1 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M16.065 29.045h-.005a13.27 13.27 0 01-6.74-1.836l-.484-.287-5.012 1.31 1.338-4.865-.315-.498a13.102 13.102 0 01-2.025-7.014C2.825 8.588 8.766 2.676 16.071 2.676a13.185 13.185 0 019.362 3.866 13.068 13.068 0 013.875 9.324c-.003 7.267-5.943 13.18-13.243 13.18zM27.336 4.65A15.868 15.868 0 0016.066 0C7.281-.002.135 7.111.131 15.853a15.771 15.771 0 002.127 7.927l-2.26 8.217 8.446-2.205a15.982 15.982 0 007.614 1.93h.006c8.781 0 15.93-7.114 15.933-15.856a15.724 15.724 0 00-4.663-11.219z" fill="#25D366"/><path d="M10.273 23.549c-.18-.105-.356-.197-.356-.769.004-2.836.009-9.394 0-11.82-.005-1.527-.209-2.515 1.14-2.515 3.65 0 8.983-.677 10.225 2.31 1.253 3.02-.774 4.483-1.219 5.26 3.042.842 3.208 7.593-3.293 7.593-1.391 0-3.348.005-5.615.01-.533 0-.77 0-.882-.07zm2.816-2.475h3.301c1.406-.004 2.657-.649 2.625-2.031-.023-1.3-.9-1.729-2.12-1.848-1.154.014-2.48.014-3.806.014v3.865zm0-6.476c2.443-.033 3.385.095 4.72-.234.918-.512 1.317-2.42.005-3.064-.909-.45-3.608-.297-4.725-.252v3.55z" fill="#25D366"/></svg>

@ -0,0 +1,33 @@
<?xml version='1.0' encoding='UTF-8'?>
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(0, 0)">
<g transform="matrix(1.3809472322464, 0, 0, 1.3809472322464, 0, 0)">
<g transform="translate(0, 0)">
<g transform="matrix(0.827589750289917, 0, 0, 0.827589750289917, 0, 0)">
<g class="prefix__icon" transform="translate(-5.3347, -5.3347)">
<g transform="matrix(1.20832681655884, 0, 0, 1.20832681655884, 0, 0)">
<g transform="matrix(0.03125, 0, 0, 0.03125, 0, 0)">
<path d="M513.554, 814.327L513.43, 814.327A307.518 307.518 0 0 1 357.235, 771.778L346.024, 765.14L229.888, 795.471L260.89, 682.761L253.599, 671.214A303.616 303.616 0 0 1 206.671, 508.681C206.742, 340.286 344.399, 203.299 513.677, 203.299A305.54 305.54 0 0 1 730.624, 292.864A302.822 302.822 0 0 1 820.418, 508.928C820.348, 677.323 682.708, 814.328 513.554, 814.328zM774.727, 249.01C705.007, 179.554 612.281, 141.26 513.554, 141.242C310.024, 141.242 144.419, 306.072 144.331, 508.647A365.462 365.462 0 0 0 193.624, 692.349L141.241, 882.759L336.967, 831.666A370.353 370.353 0 0 0 513.395, 876.386L513.554, 876.386C717.03, 876.386 882.67, 711.54 882.759, 508.946C882.794, 410.784 844.429, 318.447 774.709, 248.992z" fill="#25D366" />
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-2.67028807954262E-07, -8.39233399219097E-07)">
<g transform="matrix(1.3809472322464, 0, 0, 1.3809472322464, 0, 0)">
<g transform="translate(1.33514403977131E-07, 1.71661376668908E-07)">
<g transform="matrix(0.827589750289917, 0, 0, 0.827589750289917, 0, 0)">
<g class="prefix__icon" transform="translate(-5.33470045776367, -5.33470049591064)">
<g transform="matrix(1.20832681655884, 0, 0, 1.20832681655884, 0, 0)">
<g transform="matrix(0.03125, 0, 0, 0.03125, 0, 0)">
<path d="M379.339, 686.963C375.155, 684.526 371.076, 682.408 371.076, 669.149C371.182, 603.436 371.288, 451.46 371.076, 395.264C370.97, 359.865 366.239, 336.967 397.506, 336.967C482.074, 336.967 605.661, 321.289 634.439, 390.497C663.464, 460.465 616.501, 494.38 606.19, 512.388C676.67, 531.898 680.536, 688.34 529.885, 688.34C497.646, 688.34 452.308, 688.446 399.766, 688.552C387.407, 688.552 381.934, 688.552 379.339, 686.962zM444.575, 629.619L521.075, 629.619C553.649, 629.513 582.656, 614.577 581.897, 582.55C581.367, 552.448 561.064, 542.49 532.797, 539.736C506.033, 540.054 475.313, 540.054 444.575, 540.054L444.575, 629.619zM444.575, 479.549C501.195, 478.791 523.017, 481.757 553.966, 474.129C575.241, 462.265 584.474, 418.057 554.072, 403.121C533.01, 392.721 470.475, 396.235 444.575, 397.294L444.575, 479.532z" fill="#25D366" />
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

@ -1,13 +1,13 @@
import Icon from '@ant-design/icons'; import Icon from '@ant-design/icons';
const WABSvg = () => ( const WABSvg = () => (
<svg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='32' height='32'> <svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' width='16' height='16'>
<path <path
d='M513.554 814.327h-.124a307.518 307.518 0 01-156.195-42.549l-11.211-6.638-116.136 30.331 31.002-112.71-7.291-11.547a303.616 303.616 0 01-46.928-162.533c.071-168.395 137.728-305.382 307.006-305.382a305.54 305.54 0 01216.947 89.565 302.822 302.822 0 0189.794 216.064c-.07 168.395-137.71 305.4-306.864 305.4zM774.727 249.01c-69.72-69.456-162.446-107.75-261.173-107.768-203.53 0-369.135 164.83-369.223 367.405a365.462 365.462 0 0049.293 183.702l-52.383 190.41 195.726-51.093a370.353 370.353 0 00176.428 44.72h.159c203.476 0 369.116-164.846 369.205-367.44.035-98.162-38.33-190.499-108.05-259.954z' d='M16.065 29.045h-.005a13.27 13.27 0 01-6.74-1.836l-.484-.287-5.012 1.31 1.338-4.865-.315-.498a13.102 13.102 0 01-2.025-7.014C2.825 8.588 8.766 2.676 16.071 2.676a13.185 13.185 0 019.362 3.866 13.068 13.068 0 013.875 9.324c-.003 7.267-5.943 13.18-13.243 13.18zM27.336 4.65A15.868 15.868 0 0016.066 0C7.281-.002.135 7.111.131 15.853a15.771 15.771 0 002.127 7.927l-2.26 8.217 8.446-2.205a15.982 15.982 0 007.614 1.93h.006c8.781 0 15.93-7.114 15.933-15.856a15.724 15.724 0 00-4.663-11.219z'
fill='#25D366' fill='#2ba84a'
/> />
<path <path
d='M379.339 686.963c-4.184-2.437-8.263-4.555-8.263-17.814.106-65.713.212-217.689 0-273.885-.106-35.399-4.837-58.297 26.43-58.297 84.568 0 208.155-15.678 236.933 53.53 29.025 69.968-17.938 103.883-28.249 121.891 70.48 19.51 74.346 175.952-76.305 175.952-32.239 0-77.577.106-130.119.212-12.359 0-17.832 0-20.427-1.59zm65.236-57.344h76.5c32.574-.106 61.581-15.042 60.822-47.069-.53-30.102-20.833-40.06-49.1-42.814-26.764.318-57.484.318-88.222.318v89.565zm0-150.07c56.62-.758 78.442 2.208 109.391-5.42 21.275-11.864 30.508-56.072.106-71.008-21.062-10.4-83.597-6.886-109.497-5.827v82.238z' d='M10.273 23.549c-.18-.105-.356-.197-.356-.769.004-2.836.009-9.394 0-11.82-.005-1.527-.209-2.515 1.14-2.515 3.65 0 8.983-.677 10.225 2.31 1.253 3.02-.774 4.483-1.219 5.26 3.042.842 3.208 7.593-3.293 7.593-1.391 0-3.348.005-5.615.01-.533 0-.77 0-.882-.07zm2.816-2.475h3.301c1.406-.004 2.657-.649 2.625-2.031-.023-1.3-.9-1.729-2.12-1.848-1.154.014-2.48.014-3.806.014v3.865zm0-6.476c2.443-.033 3.385.095 4.72-.234.918-.512 1.317-2.42.005-3.064-.909-.45-3.608-.297-4.725-.252v3.55z'
fill='#25D366' fill='#2ba84a'
/> />
</svg> </svg>
); );

@ -4,11 +4,12 @@ import { RightCircleOutlined, RightOutlined, ReloadOutlined, MenuFoldOutlined, M
// import { useParams, useNavigate } from 'react-router-dom'; // import { useParams, useNavigate } from 'react-router-dom';
import MessagesHeader from './Conversations/Online/MessagesHeader'; import MessagesHeader from './Conversations/Online/MessagesHeader';
import MessagesWrapper from './Conversations/Online/MessagesWrapper'; import MessagesWrapper from './Conversations/Online/MessagesWrapper';
import InputComposer from './Conversations/Online/InputComposer'; import InputComposer from './Conversations/Online/Input/InputComposer';
import ConversationsList from './Conversations/Online/ConversationsList'; import ConversationsList from './Conversations/Online/ConversationsList';
import CustomerProfile from './Conversations/Online/order/CustomerProfile'; import CustomerProfile from './Conversations/Online/order/CustomerProfile';
// import { useAuthContext } from '@/stores/AuthContext'; // import { useAuthContext } from '@/stores/AuthContext';
// import useConversationStore from '@/stores/ConversationStore'; // import useConversationStore from '@/stores/ConversationStore';
import ReplyWrapper from './Conversations/Online/ReplyWrapper';
import './Conversations/Conversations.css'; import './Conversations/Conversations.css';
@ -54,7 +55,9 @@ const ChatWindow = () => {
<MessagesWrapper /> <MessagesWrapper />
</Content> </Content>
<Footer className='ant-layout-sider-light p-0'> <Footer className='ant-layout-sider-light p-0'>
<InputComposer /> <ReplyWrapper />
{/* <hr />
<InputComposer /> */}
</Footer> </Footer>
</Layout> </Layout>
</Content> </Content>

@ -236,6 +236,17 @@
animation-duration: 2s; animation-duration: 2s;
} }
.chatwindow-wrapper .reply-wrapper .ant-tabs-nav {
margin-top: 0;
margin-bottom: 0;
}
.chatwindow-wrapper .reply-wrapper .ant-tabs-card >.ant-tabs-nav .ant-tabs-tab-active, .ant-tabs-card >div>.ant-tabs-nav .ant-tabs-tab-active{
background: #e5e7eb;
/* background: #f3f4f6; */
border-top: none;
border: 1px solid #a7f3d0;
border-top: none;
}
/** /**
* Mobile chat ------------------------------------------------------------------------------------ * Mobile chat ------------------------------------------------------------------------------------

@ -0,0 +1,88 @@
import { createContext, useEffect, useState, useRef } from 'react';
import { Button, Flex, Modal } from 'antd';
import Draggable from 'react-draggable';
import 'react-quill/dist/quill.snow.css';
const EmailEditor = ({ mobile }) => {
const [open, setOpen] = useState(false);
const [dragDisabled, setDragDisabled] = useState(true);
const [bounds, setBounds] = useState({
left: 0,
top: 0,
bottom: 0,
right: 0,
});
const draggleRef = useRef(null);
const onStart = (_event, uiData) => {
const { clientWidth, clientHeight } = window.document.documentElement;
const targetRect = draggleRef.current?.getBoundingClientRect();
if (!targetRect) {
return;
}
setBounds({
left: -targetRect.left + uiData.x,
right: clientWidth - (targetRect.right - uiData.x),
top: -targetRect.top + uiData.y,
bottom: clientHeight - (targetRect.bottom - uiData.y),
});
};
const [useAddr, setUseAddr] = useState('');
const openEditor = (email_addr) => {
setOpen(true);
setUseAddr(email_addr);
};
return (
<>
<Button type='primary' className='bg-violet-500 shadow shadow-violet-300 hover:!bg-violet-400 active:bg-violet-400 focus:bg-violet-400' onClick={() => openEditor('lyt@hainatravel.com')}>
lyt@hainatravel.com
</Button>
<Modal
title={
<div
style={{
width: '100%',
cursor: 'move',
}}
onMouseOver={() => {
if (dragDisabled) {
setDragDisabled(false);
}
}}
onMouseOut={() => {
setDragDisabled(true);
}}
// fix eslintjsx-a11y/mouse-events-have-key-events
// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/mouse-events-have-key-events.md
onFocus={() => {}}
onBlur={() => {}}
// end
>
写邮件: {useAddr}
</div>
}
open={open}
width={mobile === undefined ? '75%' : '100%'}
footer={false}
onCancel={() => setOpen(false)}
destroyOnClose
modalRender={(modal) => (
<Draggable disabled={dragDisabled} bounds={bounds} nodeRef={draggleRef} onStart={(event, uiData) => onStart(event, uiData)}>
<div ref={draggleRef}>{modal}</div>
</Draggable>
)}>
</Modal>
</>
);
};
const EmailComposer = ({ ...props }) => {
return (
<Flex gap={8} className='p-2 bg-gray-200 rounded rounded-b-none border-gray-300 border-solid border border-b-0 border-x-0'>
<EmailEditor />
</Flex>
);
};
export default EmailComposer;

@ -20,15 +20,15 @@ import {
import { isEmpty, } from '@/utils/commons'; import { isEmpty, } from '@/utils/commons';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { sentMsgTypeMapped, whatsappSupportFileTypes, uploadProgressSimulate } from '@/channel/whatsappUtils'; import { sentMsgTypeMapped, whatsappSupportFileTypes, uploadProgressSimulate } from '@/channel/whatsappUtils';
import InputTemplate from './Input/Template'; import InputTemplate from './Template';
import InputEmoji from './Input/Emoji'; import InputEmoji from './Emoji';
import InputMediaUpload from './Input/MediaUpload'; import InputMediaUpload from './MediaUpload';
import { OSS_URL as aliOSSHost } from '@/config'; import { OSS_URL as aliOSSHost } from '@/config';
import { postUploadFileItem } from '@/actions/CommonActions'; import { postUploadFileItem } from '@/actions/CommonActions';
import ExpireTimeClock from './ExpireTimeClock'; import ExpireTimeClock from '../ExpireTimeClock';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const InputComposer = ({ mobile }) => { const InputComposer = ({ mobile, isWABA }) => {
const userId = useAuthStore((state) => state.loginUser.userId); const userId = useAuthStore((state) => state.loginUser.userId);
const websocket = useConversationStore((state) => state.websocket); const websocket = useConversationStore((state) => state.websocket);
const websocketOpened = useConversationStore((state) => state.websocketOpened); const websocketOpened = useConversationStore((state) => state.websocketOpened);
@ -271,7 +271,7 @@ const InputComposer = ({ mobile }) => {
placeholder={ placeholder={
!talkabled !talkabled
? '请先选择会话' ? '请先选择会话'
: !textabled0 : (!textabled0 && isWABA)
? '会话已超24h不活跃. 请发送打招呼消息激活对话💬.' ? '会话已超24h不活跃. 请发送打招呼消息激活对话💬.'
: mobile === undefined : mobile === undefined
? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送' ? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'
@ -290,9 +290,9 @@ const InputComposer = ({ mobile }) => {
}} }}
autoSize={{ minRows: 2, maxRows: 6 }} autoSize={{ minRows: 2, maxRows: 6 }}
/> />
<Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'> <Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b-0'>
<Flex gap={4} className='*:text-primary *:rounded-none items-center'> <Flex gap={4} className='*:text-primary *:rounded-none items-center'>
<InputTemplate key='templates' disabled={!talkabled} invokeSendMessage={invokeSendMessage} {...{ mobile }} /> {isWABA && <InputTemplate key='templates' disabled={!talkabled} invokeSendMessage={invokeSendMessage} {...{ mobile }} />}
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} {...{ mobile }} /> <InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} {...{ mobile }} />
<InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} /> <InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} />
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} /> {/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />

@ -0,0 +1,20 @@
import { Children, createContext, useEffect, useState } from 'react';
import { Tabs } from 'antd';
import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons';
import InputComposer from './Input/InputComposer';
import EmailComposer from './Input/EmailComposer';
import { WABIcon } from './../../../components/Icons';
const ReplyWrapper = ({ ...props }) => {
const replyTypes = [
{ key: 'WABA', label: 'WABA-Global Highlights', icon: <WABIcon />, children: <InputComposer isWABA /> },
{ key: 'Email', label: 'Email', icon: <MailOutlined className='text-violet-500' />, children: <EmailComposer /> },
{ key: 'WA', label: 'WhatsApp', icon: <WhatsAppOutlined className='text-whatsapp' />, children: <InputComposer /> },
];
return (
<div className='reply-wrapper rounded rounded-b-none emoji bg-white'>
<Tabs type={'card'} size={'small'} tabPosition={'bottom'} className='bg-white *:m-0 ' items={replyTypes} />
</div>
);
};
export default ReplyWrapper;

@ -1,7 +1,7 @@
import { Layout, Button } from 'antd'; import { Layout, Button } from 'antd';
import MessagesHeader from '@/views/Conversations/Online/MessagesHeader'; import MessagesHeader from '@/views/Conversations/Online/MessagesHeader';
import MessagesWrapper from '@/views/Conversations/Online/MessagesWrapper'; import MessagesWrapper from '@/views/Conversations/Online/MessagesWrapper';
import InputComposer from '@/views/Conversations/Online/InputComposer'; import InputComposer from '@/views/Conversations/Online/Input/InputComposer';
import { UnorderedListOutlined, MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons'; import { UnorderedListOutlined, MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';

Loading…
Cancel
Save