You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Global-sales/src/views/NewEmail.jsx

629 lines
26 KiB
JavaScript

import { useEffect, useState, useRef, useCallback } from 'react'
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { App, ConfigProvider, Button, Form, Input, Flex, Checkbox, Popconfirm, Select, Space, Upload, Divider, Modal, Tabs, Radio } from 'antd'
import { UploadOutlined, LoadingOutlined, SaveOutlined, SendOutlined, CheckCircleOutlined } from '@ant-design/icons'
import '@dckj/react-better-modal/dist/index.css'
import DnDModal from '@/components/DnDModal'
import useStyleStore from '@/stores/StyleStore'
// import useConversationStore from '@/stores/ConversationStore'
import useAuthStore from '@/stores/AuthStore'
import LexicalEditor from '@/components/LexicalEditor'
import { v4 as uuid } from 'uuid'
import { cloneDeep, debounce, isEmpty, writeIndexDB } from '@/utils/commons'
import '@/views/Conversations/Online/Input/EmailEditor.css'
import { postSendEmail } from '@/actions/EmailActions'
import { sentMsgTypeMapped } from '@/channel/bubbleMsgUtils'
import { EmailBuilder, useEmailDetail, useEmailSignature } from '@/hooks/useEmail'
import useSnippetStore from '@/stores/SnippetStore'
// import { useOrderStore } from '@/stores/OrderStore'
import PaymentlinkBtn from '@/views/Conversations/Online/Input/PaymentlinkBtn'
import { TextIcon } from '@/components/Icons';
import GenerateAutoDocDrawer from './Conversations/Online/Components/GenerateAutoDocDrawer';
import { POPUP_FEATURES } from '@/config';
// 禁止上传的附件类型
// .application, .exe, .app
const disallowedAttachmentTypes = [
'.ps1',
'.msi',
'application/x-msdownload',
'application/x-ms-dos-executable',
'application/x-ms-wmd',
'application/x-ms-wmz',
'application/x-ms-xbap',
'application/x-msaccess',
]
const getAbstract = (longtext) => {
const lines = longtext.split('\n')
const firstLine = lines[0]
const abstract = firstLine.substring(0, 20)
return abstract
}
const parseHTMLText = (html) => {
const parser = new DOMParser()
const dom = parser.parseFromString(html, 'text/html')
// Replace <br> and <p> with line breaks
Array.from(dom.body.querySelectorAll('br, p')).forEach((el) => {
el.textContent = '\n' + el.textContent
})
// Replace <hr> with a line of dashes
Array.from(dom.body.querySelectorAll('hr')).forEach((el) => {
el.textContent = '\n------------------------------------------------------------------\n'
})
return dom.body.textContent || ''
}
const generateQuoteContent = (mailData, isRichText = true) => {
const html = `<br><br><hr><p class="font-sans"><b><strong >From: </strong></b><span >${(mailData.info?.MAI_From || '')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')} </span></p><p class="font-sans"><b><strong >Sent: </strong></b><span >${
mailData.info?.MAI_SendDate || ''
}</span></p><p class="font-sans"><b><strong >To: </strong></b><span >${(mailData.info?.MAI_To || '')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')}</span></p><p class="font-sans"><b><strong >Subject: </strong></b><span >${mailData.info?.MAI_Subject || ''}</span></p><p>${
mailData.info?.MAI_ContentType === 'text/html' ? mailData.content : mailData.content.replace(/\r\n/g, '<br>')
}</p>`
return isRichText ? html : parseHTMLText(html)
}
const generateMailContent = (mailData) => `<br><br><p>${mailData.content}</p>`
/**
* ! 无状态管理
*/
const NewEmail = ({ ...props }) => {
const pageParam = useParams();
const editorKey = pageParam.action==='new' ? `new-${Date.now().toString(32)}` : `${pageParam.action}-${pageParam.quoteid}`
const { notification, message } = App.useApp()
const [form] = Form.useForm()
const [mobile] = useStyleStore((state) => [state.mobile])
const [userId, username, emailList] = useAuthStore((state) => [state.loginUser.userId, state.loginUser.username, state.loginUser.emailList])
const emailListOption = emailList?.map((ele) => ({ ...ele, label: ele.email, key: ele.email, value: ele.email })) || []
// const emailListMapped = emailListOption?.reduce((r, v) => ({ ...r, [v.opi_sn]: v }), {});
const emailListAddrMapped = emailListOption?.reduce((r, v) => ({ ...r, [v.email]: v }), {})
const emailListMatMapped = emailListOption?.reduce((r, v) => ({ ...r, [v.mat_sn]: v }), {})
// console.log('emailListMapped', emailListOption, emailListAddrMapped);
// const emailEdiorProps = useConversationStore((state) => state.emailEdiorProps)
// const [open, setOpen, closeEditor1, currentEditKey, setCurrentEditKey] = useConversationStore((state) => [
// state.editorOpen,
// state.setEditorOpen,
// state.closeEditor1,
// state.currentEditKey,
// state.setCurrentEditKey,
// ])
// const propsKeysArr = Array.from(emailEdiorProps.keys())
// const propsArr = Array.from(emailEdiorProps.values())
// const [activeEdit, setActiveEdit] = useState(emailEdiorProps.get(editorKey) || {})
// const { fromEmail, fromUser, fromOrder, oid, toEmail, conversationid, quoteid, initial = {}, mailData: _mailData, action = 'reply', draft = {}, receiverName, ...props } = emailEdiorProps.get(currentEditKey) || {};
const mai_sn = pageParam.quoteid // activeEdit.quoteid
const { loading: getLoading, mailData, orderDetail } = useEmailDetail(mai_sn)
const [newFromEmail, setNewFromEmail] = useState('')
const [newToEmail, setNewToEmail] = useState('')
const [emailOPI, setEmailOPI] = useState('')
const [emailOrder, setEmailOrder] = useState('')
const [emailOrderSN, setEmailOrderSN] = useState('')
const [emailMat, setEmailMat] = useState('')
const stateReset = () => {
setEmailOrder('')
setEmailOPI('')
setNewFromEmail('')
setNewToEmail('')
}
const [contentPrefix, setContentPrefix] = useState('')
// const [stickToCid, setStickToCid] = useState(pageParam.conversationid)
useEffect(() => {
console.log('useeffect\n', orderDetail.order_no);
// const propsObj = { ...activeEdit, mai: activeEdit.mailData?.info?.MAI_MAT_SN }
setContentPrefix(orderDetail.order_no ? `<p>Dear Mr./Ms. ${orderDetail.contact?.[0]?.name || ''}</p><p>Reference Number: ${orderDetail.order_no}</p>` : '')
return () => {}
}, [orderDetail.order_no])
const handleSwitchEmail = (labelValue) => {
const { value } = labelValue
setNewFromEmail(value)
const _findMat = emailListAddrMapped?.[value]
setEmailMat(_findMat?.mat_sn)
setEmailOPI(_findMat?.opi_sn)
}
const { signature } = useEmailSignature(emailOPI)
const [isRichText, setIsRichText] = useState(mobile === false)
// const [isRichText, setIsRichText] = useState(false); // 默认纯文本
const [htmlContent, setHtmlContent] = useState('')
const [textContent, setTextContent] = useState('')
const [showCc, setShowCc] = useState(true)
const [showBcc, setShowBcc] = useState(false)
const handleShowCc = () => {
setShowCc(true)
}
const handleShowBcc = () => {
setShowBcc(true)
}
const handleEditorChange = ({ editorStateJSON, htmlContent, textContent }) => {
// console.log('textContent', textContent);
// console.log('html', html);
setHtmlContent(htmlContent)
setTextContent(textContent)
form.setFieldValue('content', htmlContent)
// form.setFieldValue('abstract', getAbstract(textContent))
debouncedSave({ htmlContent, ...form.getFieldsValue() })
}
const [initialForm, setInitialForm] = useState({})
const [initialContent, setInitialContent] = useState('')
const [showQuoteContent, setShowQuoteContent] = useState(false)
const [mergeQuote, setMergeQuote] = useState(true)
const [quoteContent, setQuoteContent] = useState('')
const setPreFillInProperty = () => { };
useEffect(() => {
console.log('useEffect 2---- \nform.setFieldsValue ');
setTimeout(() => {
document.title = mailData.info?.MAI_Subject || 'New Email-'
}, 1500);
// 没有引用邮件
if (isEmpty(pageParam.quoteid)) {
// setEmailOrder(orderDetail.order_no)
// setEmailOPI(orderDetail.opi_sn)
// setNewFromEmail(activeEdit.fromEmail)
// setNewToEmail(activeEdit.toEmail)
// const _findMat = emailListAddrMapped?.[activeEdit.fromEmail]?.mat_sn
// setEmailMat(_findMat)
// if (open !== true) {
// form.resetFields()
// }
}
// 转发/回复时, 使用详情的账户信息
if (mailData.info?.MAI_MAT_SN) {
const reEmailO = mailData.info?.MAI_COLI_SN
const reEmailUser = mailData.info?.MAI_OPI_SN
const reEmailUserMat = mailData.info?.MAI_MAT_SN
setEmailOrder((prev) => reEmailO || prev ) // activeEdit.fromOrder
setEmailOPI((prev) => reEmailUser || prev)
setEmailMat((prev) => reEmailUserMat || prev)
const _findMatOld = emailListMatMapped?.[reEmailUserMat]
if (_findMatOld) {
setNewFromEmail(_findMatOld.email)
setEmailOPI(_findMatOld.opi_sn)
setEmailMat(_findMatOld.mat_sn)
}
}
setShowQuoteContent(false)
setMergeQuote(true)
setQuoteContent('')
if (isEmpty(pageParam.quoteid) && pageParam.action !== 'new') {
return () => {}
}
const { info } = mailData
// setShowCc(!isEmpty(mailData.info?.MAI_CS));
const signatureBody = generateMailContent({ content: signature })
// const preQuoteBody = generateQuoteContent(mailData)
// const _initialContent = isEmpty(mailData.info) ? signatureBody : signatureBody+preQuoteBody
if (!isEmpty(mailData.info) && pageParam.action !== 'edit') {
setInitialContent(contentPrefix + signatureBody)
}
const forwardValues = { from: newFromEmail, subject: `Fw: ${info.MAI_Subject || ''}` }
if (pageParam.action === 'reply') {
const _formValues = {
to: info?.replyTo || newFromEmail,
cc: info?.MAI_CS || '',
// bcc: quote.bcc || '',
subject: `Re: ${info.MAI_Subject || ''}`,
}
form.setFieldsValue(_formValues)
setInitialForm(_formValues)
} else if (pageParam.action === 'replyall') {
const _formValues = {
to: info?.replyToAll || newFromEmail,
cc: info?.MAI_CS || '',
// bcc: quote.bcc || '',
subject: `Re: ${info.MAI_Subject || ''}`,
}
form.setFieldsValue(_formValues)
setInitialForm(_formValues)
} else if (pageParam.action === 'forward') {
form.setFieldsValue(forwardValues)
setInitialForm(forwardValues)
} else if (pageParam.action === 'edit') {
const thisFormValues = {
to: info?.MAI_To || '',
cc: info?.MAI_CS || '',
subject: info?.MAI_Subject || '',
}
form.setFieldsValue(thisFormValues)
setInitialForm(thisFormValues)
const thisBody = generateMailContent(mailData)
// console.log('thisBody', thisBody);
setInitialContent(thisBody)
} else if (pageParam.action === 'new') {
const newEmail = { to: newToEmail, subject: pageParam.draft?.subject || '' }
form.setFieldsValue(newEmail)
setInitialForm(newEmail)
setInitialContent((pageParam.draft?.content || contentPrefix || '') + signatureBody)
}
return () => {}
}, [ mailData.info, signature, newToEmail])
const [openPlainTextConfirm, setOpenPlainTextConfirm] = useState(false)
const handlePlainTextOpenChange = ({ target }) => {
const { value: newChecked } = target
if (newChecked === true) {
setIsRichText(true)
setOpenPlainTextConfirm(false)
return
}
setOpenPlainTextConfirm(true)
}
const confirmPlainText = () => {
setIsRichText(false)
setOpenPlainTextConfirm(false)
}
// 附件:
// 1. ~直接上传返回地址~
// 2. 发送文件信息
const [fileList, setFileList] = useState([])
const handleChange = (info) => {
let newFileList = [...info.fileList]
// 2. Read from response and show file link
newFileList = newFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url
}
return file
})
setFileList(newFileList)
}
const normFile = (e) => {
// console.log('Upload event:', e);
if (Array.isArray(e)) {
return e
}
return e?.fileList
}
const uploadProps = {
// action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
// onChange: handleChange,
multiple: true,
fileList,
beforeUpload: (file) => {
// console.log('beforeUpload', file);
const lastDotIndex = file.name.lastIndexOf('.')
const extension = file.name.slice(lastDotIndex).toLocaleLowerCase()
if (disallowedAttachmentTypes.includes(file.type) || disallowedAttachmentTypes.includes(extension)) {
message.warning('不支持的文件格式: ' + extension)
return false
}
setFileList((prev) => [...prev, file])
return false // 阻止默认上传, 附件不上传阿里云
},
onRemove: (file) => {
const index = fileList.indexOf(file)
const newFileList = fileList.slice()
newFileList.splice(index, 1)
setFileList(newFileList)
},
onPreview: (file) => {
// console.log('pn preview', file);
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = (e) => {
if (file.size > 1.5 * 1024 * 1024) {
message.info('附件太大,无法预览')
// message.info('附件太大,无法预览, 请下载后查看')
// var downloadLink = document.createElement('a');
// downloadLink.href = e.target.result;
// downloadLink.download = file.name;
// downloadLink.click();
resolve(e.target.result)
return
}
var win = window.open('', file.uid, POPUP_FEATURES)
win.document.body.style.margin = '0'
if (file.type.startsWith('image/')) {
win.document.write("<img src='" + e.target.result + '\' style="max-width: 100%;" />')
} else if (file.type.startsWith('text/') || file.type === 'application/html' || file.type === 'application/xhtml+xml') {
var iframe = win.document.createElement('iframe')
iframe.srcdoc = e.target.result
iframe.style.width = '100%'
iframe.style.height = '100%'
iframe.style.border = 'none'
win.document.body.appendChild(iframe)
win.document.body.style.margin = '0'
} else if (file.type === 'application/pdf') {
// win.document.write("<iframe src='" + e.target.result + "' width='100%' height='100%' frameborder=\"0\"></iframe>");
win.document.write("<embed src='" + e.target.result + "' width='100%' height='100%' style=\"border:none\"></embed>")
win.document.body.style.margin = '0'
} else if (file.type.startsWith('audio/')) {
win.document.write("<audio controls src='" + e.target.result + '\' style="max-width: 100%;"></audio>')
} else if (file.type.startsWith('video/')) {
win.document.write("<video controls src='" + e.target.result + '\' style="max-width: 100%;"></video>')
} else {
win.document.write('<h2>Preview not available for this file type</h2>')
}
// win.document.write("<iframe src='" + dataURL + "' width='100%' height='100%' style=\"border:none\"></iframe>");
resolve(reader.result)
}
if (file.type.startsWith('text/') || file.type === 'application/html' || file.type === 'application/xhtml+xml') {
reader.readAsText(file)
} else {
reader.readAsDataURL(file)
}
// reader.readAsDataURL(file);
reader.onerror = (error) => reject(error)
})
},
}
/**
* 保存成功, 推一个气泡
* 再从异步通知更新消息发送状态
*/
// const sentOrReceivedNewMessage = useConversationStore((state) => state.sentOrReceivedNewMessage)
const invokeEmailMessage = (msgObj) => {
const msgObjMerge = {
sender: 'me',
senderName: 'me',
// to: currentConversation.whatsapp_phone_number,
date: new Date(),
status: 'waiting', // accepted
...msgObj,
// id: `${currentConversation.sn}.${msgObj.id}`,
// id: `${stickToCid}.${msgObj.id}`,
// conversationid: stickToCid,
msg_source: 'email',
}
// olog('invoke upload', msgObjMerge)
const contentToRender = sentMsgTypeMapped[msgObjMerge.type].contentToRender(msgObjMerge)
// console.log(contentToRender, 'contentToRender sendMessage------------------');
// sentOrReceivedNewMessage(contentToRender.conversationid, contentToRender)
}
const [sendLoading, setSendLoading] = useState(false)
const onHandleSend = async () => {
// console.log('onSend callback', '\nisRichText', isRichText);
// console.log(form.getFieldsValue());
const body = structuredClone(form.getFieldsValue())
body.from = newFromEmail
body.attaList = fileList
body.opi_sn = emailOPI
body.mat_sn = emailMat
body.coli_sn = emailOrder || ''
// console.log('body', body, '\n', emailOrder);
const values = await form.validateFields()
const preQuoteBody = pageParam.quoteid ? (quoteContent ? quoteContent : generateQuoteContent(mailData, isRichText)) : ''
body.mailcontent = isRichText ? EmailBuilder({ subject: values.subject, content: htmlContent + preQuoteBody }) : textContent + preQuoteBody
body.cc = values.cc || ''
body.bcc = values.bcc || ''
const msgObj = {
type: 'email',
id: uuid(),
from: body.from,
to: values.to,
cc: values.cc || '',
bcc: values.bcc || '',
subject: values.subject,
content: body.mailcontent,
email: {
subject: values.subject,
content: body.mailcontent,
},
coli_id: orderDetail.order_no || (emailOrder ? `{${emailOrder}}` : ''),
}
setSendLoading(true)
// body.externalID = stickToCid
// body.actionID = `${stickToCid}.${msgObj.id}`
body.contenttype = isRichText ? 'text/html' : 'text/plain'
try {
const bubbleMsg = cloneDeep(msgObj)
// bubbleMsg.id = `${stickToCid}.${msgObj.id}`
// bubbleMsg.email.mai_sn = '';
bubbleMsg.content = undefined
// invokeEmailMessage(bubbleMsg);
// console.log('postSendEmail', body, '\n', msgObj);
// return;
const result = await postSendEmail(body)
const mailSavedId = result.id || ''
bubbleMsg.email.mai_sn = mailSavedId
// console.log('invokeEmailMessage', bubbleMsg);
invokeEmailMessage(bubbleMsg)
// setSendLoading(false);
// setOpen(false)
} catch (error) {
notification.error({
message: '邮件保存失败',
description: error.message,
placement: 'top',
duration: 3,
})
} finally {
setSendLoading(false)
}
}
const [openDrawerSnippet] = useSnippetStore((state) => [state.openDrawer])
// const [openPaymentDrawer] = useOrderStore((state) => [state.openDrawer])
const [bakData, setBakData] = useState({})
const idleCallbackId = useRef(null)
const debouncedSave = useCallback(
debounce((data) => {
idleCallbackId.current = window.requestIdleCallback(() => {
console.log('Saving data (idle, debounced):', data)
writeIndexDB({ ...data, key: editorKey }, 'draft', 'EmailEditor')
})
}, 1500), // 1.5s
[],
)
const onEditChange = (changedValues, allValues) => {
console.log('onEditChange', changedValues, '\n', allValues)
// const { name, value } = e.target;
// setBakData(prevData => ({ ...prevData, [name]: value }));
// debouncedSave(bakData);
}
useEffect(() => {
return () => {
if (idleCallbackId.current && window.cancelIdleCallback) {
window.cancelIdleCallback(idleCallbackId.current)
}
}
}, [debouncedSave])
return (
<>
<ConfigProvider theme={{ token: { colorPrimary: '#6366f1' } }}>
<div className='w-full flex gap-4 justify-start items-center text-indigo-600 pb-1 mb-2 border-x-0 border-t-0 border-b border-solid border-neutral-200'>
<Button type='primary' onClick={onHandleSend} loading={sendLoading} icon={<SendOutlined />}>
发送
</Button>
<Select labelInValue options={emailListOption} value={{ key: newFromEmail, value: newFromEmail, label: newFromEmail }} onChange={handleSwitchEmail} labelRender={item => `发件人: ${item.label || '选择'}`} variant={'borderless'} className='[&_.ant-select-selection-item]:font-bold' />
<div className='ml-auto'></div>
<Popconfirm trigger1={['hover', 'click']}
description='切换内容为纯文本格式将丢失信件和签名的格式, 确定使用纯文本?'
onConfirm={confirmPlainText}
open={openPlainTextConfirm}
onCancel={() => setOpenPlainTextConfirm(false)}>
{/* <Checkbox checked={!isRichText} onChange={handlePlainTextOpenChange}>
纯文本
</Checkbox> */}
{/* <Button type='link' size='small' icon={<TextIcon />} className=' ' >纯文本</Button> */}
<Radio.Group options={[{label: '纯文本', value: false}, {label: '富文本', value: true}]} optionType="button" buttonStyle="solid" onChange={handlePlainTextOpenChange} value={isRichText} size='small' />
</Popconfirm>
<Button type='dashed' icon={<SaveOutlined />} size='small' className='' >存草稿</Button>
</div>
<Form
form={form}
onValuesChange={onEditChange}
preserve={false}
name={`email_max_form-${Date.now().toString(32)}`}
size='small'
layout={'inline'}
variant={'borderless'}
// initialValues={{}}
// onFinish={() => {}}
className='email-editor-wrapper *:mb-2 *:border-b *:border-t-0 *:border-x-0 *:border-indigo-100 *:border-solid '
requiredMark={false}
// labelCol={{ span: 3 }}
>
<Form.Item className='w-full'>
<Space.Compact className='w-full'>
<Form.Item name={'to'} label='收件人' rules={[{ required: true }]} className='!flex-1'>
<Input className='w-full' />
</Form.Item>
<Flex gap={4}>
{!showCc && (
<Button type='text' onClick={handleShowCc}>
抄送
</Button>
)}
{!showBcc && (
<Button type='text' hidden={showBcc} onClick={handleShowBcc}>
密送
</Button>
)}
</Flex>
</Space.Compact>
</Form.Item>
<Form.Item label='抄&nbsp;&nbsp;&nbsp;&nbsp;送' name={'cc'} hidden={!showCc} className='w-full pt-1'>
<Input />
</Form.Item>
<Form.Item label='密&nbsp;&nbsp;&nbsp;&nbsp;送' name={'bcc'} hidden={!showBcc} className='w-full pt-1'>
<Input />
</Form.Item>
<Form.Item label='主&nbsp;&nbsp;&nbsp;&nbsp;题' name={'subject'} rules={[{ required: true }]} className='w-full pt-1'>
<Input />
</Form.Item>
<Form.Item name='atta' label='' className='w-full py-1 border-b-0' valuePropName='fileList' getValueFromEvent={normFile}>
<Flex justify='space-between'>
<Upload {...uploadProps} name='file' className='w-full'>
<Button icon={<UploadOutlined />}>附件</Button>
</Upload>
<Flex align={'center'} className='absolute right-0'>
<Divider type='vertical' />
<Button type={'link'} onClick={() => openDrawerSnippet()}>
图文集
</Button>
<PaymentlinkBtn type={'link'} />
{/* 更多工具 */}
{/* <Popover
content={
<div className='flex flex-col gap-2'>
<Button type={'link'}>??</Button>
</div>
}
trigger='click'
><MoreOutlined /></Popover> */}
</Flex>
</Flex>
</Form.Item>
<Form.Item name='content' hidden>
<Input />
</Form.Item>
<Form.Item name='abstract' hidden>
<Input />
</Form.Item>
</Form>
<LexicalEditor {...{ isRichText }} onChange={handleEditorChange} defaultValue={initialContent} />
{pageParam.quoteid && !showQuoteContent && (
<div className='flex justify-start items-center ml-2'>
<Button className='flex gap-2 ' type='link' onClick={() => setShowQuoteContent(!showQuoteContent)}>
显示引用内容 {/*(不可更改)*/}
</Button>
{/* <Button className='flex gap-2 ' type='link' danger onClick={() => {setMergeQuote(false);setShowQuoteContent(false)}}>
删除引用内容
</Button> */}
</div>
)}
{showQuoteContent && (
<blockquote
// contentEditable
className='border-0 outline-none cursor-text'
onBlur={(e) => setQuoteContent(`<blockquote>${e.target.innerHTML}</blockquote>`)}
dangerouslySetInnerHTML={{ __html: generateQuoteContent(mailData) }}></blockquote>
)}
</ConfigProvider>
</>
)
}
export default NewEmail