|
|
|
@ -0,0 +1,750 @@
|
|
|
|
|
import { useEffect, useState, useRef, useCallback, useMemo } 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, Typography, } from 'antd'
|
|
|
|
|
import { UploadOutlined, LoadingOutlined, SaveOutlined, SendOutlined, CheckCircleOutlined, ExclamationCircleFilled } from '@ant-design/icons'
|
|
|
|
|
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, olog, omitEmpty } from '@/utils/commons'
|
|
|
|
|
import { writeIndexDB, readIndexDB, deleteIndexDBbyKey, } from '@/utils/indexedDB';
|
|
|
|
|
|
|
|
|
|
import '@/views/Conversations/Online/Input/EmailEditor.css'
|
|
|
|
|
import { deleteEmailAttachmentAction, parseHTMLString, postSendEmail, saveEmailDraftOrSendAction } from '@/actions/EmailActions'
|
|
|
|
|
import { sentMsgTypeMapped } from '@/channel/bubbleMsgUtils'
|
|
|
|
|
import { EmailBuilder, openPopup, useEmailDetail, useEmailSignature, useEmailTemplate } 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 { EMAIL_ATTA_HOST, POPUP_FEATURES } from '@/config';
|
|
|
|
|
import RoosterEditor from '@/components/RoosterEditor';
|
|
|
|
|
|
|
|
|
|
const {confirm} = Modal;
|
|
|
|
|
// 禁止上传的附件类型
|
|
|
|
|
// .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, '<')
|
|
|
|
|
.replace(/>/g, '>')} </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, '<')
|
|
|
|
|
.replace(/>/g, '>')}</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) => `${mailData.content}<br>`
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 独立窗口编辑器
|
|
|
|
|
*
|
|
|
|
|
* - 从销售平台进入: 自动复制 storage, 可读取loginUser
|
|
|
|
|
*
|
|
|
|
|
* ! 无状态管理
|
|
|
|
|
*/
|
|
|
|
|
const NewEmail = () => {
|
|
|
|
|
const pageParam = useParams();
|
|
|
|
|
const { templateKey } = pageParam
|
|
|
|
|
const editorKey = pageParam.action==='new' ? `new-0-${pageParam.oid}` : `${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 = useMemo(() => emailList?.map((ele) => ({ ...ele, label: ele.email, key: ele.email, value: ele.email })) || [], [emailList])
|
|
|
|
|
const emailListOPIMapped = useMemo(() => emailListOption?.reduce((r, v) => ({ ...r, [v.opi_sn]: v }), {}), [emailListOption]);
|
|
|
|
|
const emailListAddrMapped = useMemo(() => emailListOption?.reduce((r, v) => ({ ...r, [v.email]: v }), {}), [emailListOption])
|
|
|
|
|
const emailListMatMapped = useMemo(() => emailListOption?.reduce((r, v) => ({ ...r, [v.mat_sn]: v }), {}), [emailListOption])
|
|
|
|
|
// console.log('emailListMapped', emailListOption, emailListAddrMapped);
|
|
|
|
|
|
|
|
|
|
const [emailAccount, setEmailAccount] = useState({});
|
|
|
|
|
const [emailAccountOPI, setEmailAccountOPI] = useState(0);
|
|
|
|
|
|
|
|
|
|
const mai_sn = pageParam.quoteid // activeEdit.quoteid
|
|
|
|
|
const { loading: quoteLoading, mailData, orderDetail, postEmailSaveOrSend } = useEmailDetail(mai_sn, null, pageParam.oid)
|
|
|
|
|
const { loading: loadingTamplate, templateContent } = useEmailTemplate(templateKey, {coli_sn: pageParam.oid, opi_sn: orderDetail.opi_sn || mailData.info?.MAI_OPI_SN || 0, lgc: 1});
|
|
|
|
|
const initOPI = useMemo(() => emailAccountOPI || orderDetail.opi_sn || mailData.info?.MAI_OPI_SN || 0, [emailAccountOPI, mailData, orderDetail])
|
|
|
|
|
const { signature } = useEmailSignature(initOPI)
|
|
|
|
|
|
|
|
|
|
const [initialContent, setInitialContent] = useState('')
|
|
|
|
|
const [showQuoteContent, setShowQuoteContent] = useState(false)
|
|
|
|
|
const [quoteContent, setQuoteContent] = useState('')
|
|
|
|
|
|
|
|
|
|
// 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 [contentPrefix, setContentPrefix] = useState('')
|
|
|
|
|
const [localDraft, setLocalDraft] = useState();
|
|
|
|
|
|
|
|
|
|
// const readMailboxLocalCache = async () => {
|
|
|
|
|
// console.log('===============', 'readMailboxLocalCache')
|
|
|
|
|
// const readCache = await readIndexDB(editorKey, 'draft', 'mailbox')
|
|
|
|
|
// if (readCache) {
|
|
|
|
|
// const btn = (
|
|
|
|
|
// <Space>
|
|
|
|
|
// <Button type='link' size='small' onClick={() => notification.destroy()}>
|
|
|
|
|
// 关闭
|
|
|
|
|
// </Button>
|
|
|
|
|
// {/* <Button type="primary" size="small" onClick={() => notification.destroy()}>
|
|
|
|
|
// Confirm
|
|
|
|
|
// </Button> */}
|
|
|
|
|
// </Space>
|
|
|
|
|
// )
|
|
|
|
|
// // notification.open({
|
|
|
|
|
// // key: editorKey,
|
|
|
|
|
// // placement: 'top',
|
|
|
|
|
// // // message: '提示',
|
|
|
|
|
// // description: '已从本地缓存中读取邮件内容',
|
|
|
|
|
// // duration: 0,
|
|
|
|
|
// // actions: btn,
|
|
|
|
|
// // })
|
|
|
|
|
// setLocalDraft(readCache)
|
|
|
|
|
|
|
|
|
|
// if (!isEmpty(localDraft)) {
|
|
|
|
|
// const { htmlContent, ...draftFormsValues } = localDraft
|
|
|
|
|
|
|
|
|
|
// const _findMatOld = emailListMatMapped?.[draftFormsValues.mat_sn]
|
|
|
|
|
// const _from = draftFormsValues?.from || _findMatOld?.email || ''
|
|
|
|
|
|
|
|
|
|
// form.setFieldsValue(draftFormsValues)
|
|
|
|
|
// setNewFromEmail(_from)
|
|
|
|
|
// setEmailOPI(draftFormsValues.opi_sn)
|
|
|
|
|
// setEmailMat(draftFormsValues.mat_sn)
|
|
|
|
|
// setEmailOrder(draftFormsValues.coli_sn)
|
|
|
|
|
// requestAnimationFrame(() => {
|
|
|
|
|
// setInitialContent(htmlContent)
|
|
|
|
|
// })
|
|
|
|
|
|
|
|
|
|
// return false
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// useEffect(() => {
|
|
|
|
|
// readMailboxLocalCache()
|
|
|
|
|
// return () => {}
|
|
|
|
|
// }, [])
|
|
|
|
|
|
|
|
|
|
// 填充表单
|
|
|
|
|
// - 从原邮件内容
|
|
|
|
|
// - 从订单
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// console.log('useEffect 1---- \nform.setFieldsValue ');
|
|
|
|
|
if (isEmpty(mailData.content) && isEmpty(orderDetail.order_no)) {
|
|
|
|
|
// return () => {}
|
|
|
|
|
}
|
|
|
|
|
const docTitle = mailData.info?.MAI_Subject || 'New Email-';
|
|
|
|
|
document.title = docTitle
|
|
|
|
|
|
|
|
|
|
const { order_no } = orderDetail
|
|
|
|
|
// setContentPrefix(order_no ? `<p>Dear Mr./Ms. ${orderDetail.contact?.[0]?.name || ''}</p><p>Reference Number: ${order_no}</p>` : '')
|
|
|
|
|
const orderPrefix = order_no ? `<p>Dear Mr./Ms. ${orderDetail.contact?.[0]?.name || ''}</p><p>Reference Number: ${order_no}</p>` : ''
|
|
|
|
|
|
|
|
|
|
const { info } = mailData
|
|
|
|
|
const { ...templateFormValues } = templateContent;
|
|
|
|
|
|
|
|
|
|
const orderReceiver = orderDetail.contact?.[0]?.email || ''
|
|
|
|
|
|
|
|
|
|
const _findMatOld = emailListOPIMapped?.[orderDetail.opi_sn]
|
|
|
|
|
const orderSender = _findMatOld?.email || ''
|
|
|
|
|
|
|
|
|
|
const _findMatOldE = emailListMatMapped?.[info.MAI_MAT_SN]
|
|
|
|
|
const quotedMailSender = _findMatOldE?.email || ''
|
|
|
|
|
|
|
|
|
|
const sender = quotedMailSender || orderSender
|
|
|
|
|
const quotedMailSenderObj = emailAccount?.email || sender; // { key: sender, label: sender, value: sender }
|
|
|
|
|
const defaultMAT = emailListAddrMapped?.[sender]?.mat_sn || ''
|
|
|
|
|
|
|
|
|
|
const _form2 = {
|
|
|
|
|
coli_sn: Number(pageParam.oid) || info?.MAI_COLI_SN || '',
|
|
|
|
|
mat_sn: emailAccount?.mat_sn || info?.MAI_MAT_SN || defaultMAT,
|
|
|
|
|
opi_sn: emailAccount?.opi_sn || info?.MAI_OPI_SN || orderDetail.opi_sn || '',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let readyToInitialContent = '';
|
|
|
|
|
let _formValues = {};
|
|
|
|
|
|
|
|
|
|
// setShowCc(!isEmpty(mailData.info?.MAI_CS));
|
|
|
|
|
|
|
|
|
|
const signatureBody = generateMailContent({ content: signature })
|
|
|
|
|
|
|
|
|
|
// const preQuoteBody = generateQuoteContent(mailData)
|
|
|
|
|
|
|
|
|
|
// const _initialContent = isEmpty(mailData.info) ? signatureBody : signatureBody+preQuoteBody
|
|
|
|
|
|
|
|
|
|
// 排除草稿: `编辑`有id 的邮件
|
|
|
|
|
if (!isEmpty(mailData.info) && !['edit'].includes(pageParam.action)) {
|
|
|
|
|
readyToInitialContent = orderPrefix + signatureBody
|
|
|
|
|
}
|
|
|
|
|
switch (pageParam.action) {
|
|
|
|
|
case 'reply':
|
|
|
|
|
_formValues = {
|
|
|
|
|
from: quotedMailSenderObj,
|
|
|
|
|
to: info?.replyTo || orderReceiver,
|
|
|
|
|
cc: info?.MAI_CS || '',
|
|
|
|
|
// bcc: quote.bcc || '',
|
|
|
|
|
subject: `Re: ${info.MAI_Subject || ''}`,
|
|
|
|
|
..._form2
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
case 'replyall':
|
|
|
|
|
_formValues = {
|
|
|
|
|
from: quotedMailSenderObj,
|
|
|
|
|
to: info?.replyToAll || orderReceiver,
|
|
|
|
|
cc: info?.MAI_CS || '',
|
|
|
|
|
// bcc: quote.bcc || '',
|
|
|
|
|
subject: `Re: ${info.MAI_Subject || ''}`,
|
|
|
|
|
..._form2
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
case 'forward':
|
|
|
|
|
_formValues = {
|
|
|
|
|
from: quotedMailSenderObj,
|
|
|
|
|
subject: `Fw: ${info.MAI_Subject || ''}`,
|
|
|
|
|
// coli_sn: pageParam.oid,
|
|
|
|
|
..._form2
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
case 'edit':
|
|
|
|
|
_formValues = {
|
|
|
|
|
from: quotedMailSenderObj,
|
|
|
|
|
to: info?.MAI_To || '',
|
|
|
|
|
cc: info?.MAI_CS || '',
|
|
|
|
|
subject: `${info.MAI_Subject || ''}`,
|
|
|
|
|
id: pageParam.quoteid,
|
|
|
|
|
mai_sn: pageParam.quoteid,
|
|
|
|
|
..._form2
|
|
|
|
|
}
|
|
|
|
|
readyToInitialContent = '<br>'+ generateMailContent(mailData)
|
|
|
|
|
setFileList(mailData.attachments.map(ele => ({ uid: ele.ATI_SN, name: ele.ATI_Name, url: ele.ATI_ServerFile, fullPath: `${EMAIL_ATTA_HOST}${ele.ATI_ServerFile}` })))
|
|
|
|
|
break
|
|
|
|
|
case 'new':
|
|
|
|
|
_formValues = {
|
|
|
|
|
...templateFormValues,
|
|
|
|
|
from: quotedMailSenderObj,
|
|
|
|
|
to: orderReceiver || info?.MAI_To || '',
|
|
|
|
|
subject: `${info.MAI_Subject || templateFormValues.subject || ''}`,
|
|
|
|
|
..._form2,
|
|
|
|
|
}
|
|
|
|
|
readyToInitialContent = generateMailContent({ content: templateContent.bodycontent || readyToInitialContent || `<br>${signatureBody}` || '' })
|
|
|
|
|
setFileList(mailData.attachments.map(ele => ({ uid: ele.ATI_SN, name: ele.ATI_Name, url: ele.ATI_ServerFile, fullPath: `${EMAIL_ATTA_HOST}${ele.ATI_ServerFile}` })))
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
// olog('222', _formValues, pageParam.action)
|
|
|
|
|
form.setFieldsValue(_formValues) // todo: from
|
|
|
|
|
setInitialContent(readyToInitialContent);
|
|
|
|
|
|
|
|
|
|
return () => {}
|
|
|
|
|
}, [orderDetail.order_no, quoteLoading, loadingTamplate, emailAccount, signature])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// const readFromTemplate = () => {
|
|
|
|
|
// const { mailcontent, ...templateFormValues } = templateContent;
|
|
|
|
|
// if (mailcontent) {
|
|
|
|
|
// const _findMatOld = emailListOPIMapped?.[orderDetail.opi_sn]
|
|
|
|
|
// const _from = _findMatOld?.email || ''
|
|
|
|
|
// form.setFieldsValue({...templateFormValues, to: orderDetail?.contact?.[0]?.email || '', from1: { key: _from, value: _from, label: _from}});
|
|
|
|
|
// // setNewFromEmail(_from);
|
|
|
|
|
// // setEmailOPI(draftFormsValues.opi_sn)
|
|
|
|
|
// requestAnimationFrame(() => {
|
|
|
|
|
// setInitialContent(mailcontent);
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// readMailboxLocalCache()
|
|
|
|
|
if (loadingTamplate) {
|
|
|
|
|
notification.open({
|
|
|
|
|
key: editorKey,
|
|
|
|
|
placement: 'top',
|
|
|
|
|
// message: '提示',
|
|
|
|
|
description: '正在加载邮件模板...',
|
|
|
|
|
duration: 0,
|
|
|
|
|
icon: <LoadingOutlined />,
|
|
|
|
|
// actions: btn,
|
|
|
|
|
// closeIcon: null,
|
|
|
|
|
closable: false,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
notification.destroy(editorKey)
|
|
|
|
|
}
|
|
|
|
|
// readFromTemplate();
|
|
|
|
|
|
|
|
|
|
return () => {}
|
|
|
|
|
}, [loadingTamplate])
|
|
|
|
|
|
|
|
|
|
const handleSwitchEmail = (value) => {
|
|
|
|
|
// const { value } = labelValue
|
|
|
|
|
// setNewFromEmail(value)
|
|
|
|
|
const _findMat = emailListAddrMapped?.[value]
|
|
|
|
|
form.setFieldsValue({ mat_sn: _findMat?.mat_sn, opi_sn: _findMat?.opi_sn })
|
|
|
|
|
// console.log(_findMat, 'handleSwitchEmail')
|
|
|
|
|
setEmailAccount(_findMat)
|
|
|
|
|
setEmailAccountOPI(_findMat?.opi_sn)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
const { bodyText: abstract } = parseHTMLString(htmlContent, true);
|
|
|
|
|
// form.setFieldValue('abstract', getAbstract(textContent))
|
|
|
|
|
const formValues = omitEmpty(form.getFieldsValue());
|
|
|
|
|
if (!isEmpty(formValues)) {
|
|
|
|
|
debouncedSave({ ...form.getFieldsValue(), htmlContent, abstract, })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: async (file) => {
|
|
|
|
|
console.log('onRomove', file)
|
|
|
|
|
if (file.fullPath) {
|
|
|
|
|
try {
|
|
|
|
|
const x = await deleteEmailAttachmentAction([file.uid]);
|
|
|
|
|
message.success(`已删除 ${file.name}`, 2)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
|
|
|
|
notification.error({
|
|
|
|
|
key: editorKey,
|
|
|
|
|
message: '删除失败',
|
|
|
|
|
description: error.message,
|
|
|
|
|
placement: 'top',
|
|
|
|
|
duration: 3,
|
|
|
|
|
})
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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.fullPath) {
|
|
|
|
|
openPopup(file.fullPath, file.uid)
|
|
|
|
|
}
|
|
|
|
|
else 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 [sendLoading, setSendLoading] = useState(false)
|
|
|
|
|
|
|
|
|
|
const onHandleSaveOrSend = async (isDraft = false) => {
|
|
|
|
|
// console.log('onSend callback', '\nisRichText', isRichText);
|
|
|
|
|
// console.log(form.getFieldsValue());
|
|
|
|
|
const body = structuredClone(form.getFieldsValue())
|
|
|
|
|
body.attaList = fileList;
|
|
|
|
|
// console.log('body', body, '\n', fileList);
|
|
|
|
|
const values = await form.validateFields()
|
|
|
|
|
const preQuoteBody = !['edit', 'new'].includes(pageParam.action) && 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 || ''
|
|
|
|
|
body.bcc = values.mailtype || ''
|
|
|
|
|
setSendLoading(!isDraft)
|
|
|
|
|
notification.open({
|
|
|
|
|
key: editorKey,
|
|
|
|
|
placement: 'top',
|
|
|
|
|
// message: '提示',
|
|
|
|
|
description: '正在保存...',
|
|
|
|
|
duration: 0,
|
|
|
|
|
icon: <LoadingOutlined className='text-primary' />,
|
|
|
|
|
closable: false,
|
|
|
|
|
})
|
|
|
|
|
// body.externalID = stickToCid
|
|
|
|
|
// body.actionID = `${stickToCid}.${msgObj.id}`
|
|
|
|
|
body.contenttype = isRichText ? 'text/html' : 'text/plain'
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
|
// console.log('postSendEmail', body, '\n');
|
|
|
|
|
// return;
|
|
|
|
|
const mailSavedId = await postEmailSaveOrSend(body, isDraft)
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
mai_sn: mailSavedId,
|
|
|
|
|
id: mailSavedId,
|
|
|
|
|
})
|
|
|
|
|
// bubbleMsg.email.mai_sn = mailSavedId
|
|
|
|
|
|
|
|
|
|
// setSendLoading(false);
|
|
|
|
|
|
|
|
|
|
if (!isDraft) {
|
|
|
|
|
notification.success({
|
|
|
|
|
key: editorKey,
|
|
|
|
|
message: '成功',
|
|
|
|
|
description: isDraft ? '' : '窗口将自动关闭...',
|
|
|
|
|
placement: 'top',
|
|
|
|
|
duration: 2,
|
|
|
|
|
showProgress: true,
|
|
|
|
|
pauseOnHover: true,
|
|
|
|
|
onClose: () => {
|
|
|
|
|
deleteIndexDBbyKey(editorKey, 'draft', 'mailbox');
|
|
|
|
|
isDraft ? false : window.close();
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
} else { notification.destroy(editorKey) }
|
|
|
|
|
// setOpen(false)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
|
|
|
|
notification.error({
|
|
|
|
|
key: editorKey,
|
|
|
|
|
message: '邮件保存失败',
|
|
|
|
|
description: error.message,
|
|
|
|
|
placement: 'top',
|
|
|
|
|
duration: 3,
|
|
|
|
|
})
|
|
|
|
|
} finally {
|
|
|
|
|
setSendLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [openDrawerSnippet] = useSnippetStore((state) => [state.openDrawer])
|
|
|
|
|
|
|
|
|
|
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', 'mailbox')
|
|
|
|
|
})
|
|
|
|
|
}, 1500), // 1.5s
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (idleCallbackId.current && window.cancelIdleCallback) {
|
|
|
|
|
window.cancelIdleCallback(idleCallbackId.current)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [debouncedSave])
|
|
|
|
|
const onEditChange = (changedValues, allValues) => {
|
|
|
|
|
// console.log('onEditChange', changedValues, '\n', allValues)
|
|
|
|
|
if ('from' in changedValues) {
|
|
|
|
|
handleSwitchEmail(allValues.from);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<ConfigProvider theme={{ token: { colorPrimary: '#6366f1' } }}>
|
|
|
|
|
<Form
|
|
|
|
|
form={form}
|
|
|
|
|
onValuesChange={onEditChange}
|
|
|
|
|
// onFinishFailed={onFinishFailed}
|
|
|
|
|
preserve={false}
|
|
|
|
|
name={`email_max_form`}
|
|
|
|
|
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 }}
|
|
|
|
|
>
|
|
|
|
|
<div className='w-full flex flex-wrap gap-2 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' size='middle' onClick={onHandleSaveOrSend} loading={sendLoading} icon={<SendOutlined />}>
|
|
|
|
|
发送
|
|
|
|
|
</Button>
|
|
|
|
|
<Form.Item name={'from'} rules={[{ required: true, message: '请选择发件地址' }]}>
|
|
|
|
|
<Select
|
|
|
|
|
labelInValue={false}
|
|
|
|
|
options={emailListOption}
|
|
|
|
|
labelRender={(item) => `发件人: ${item?.label || '选择'}`}
|
|
|
|
|
variant={'borderless'}
|
|
|
|
|
placeholder='发件人: 选择'
|
|
|
|
|
className='[&_.ant-select-selection-item]:font-bold [&_.ant-select-selection-placeholder]:font-bold [&_.ant-select-selection-placeholder]:text-black'
|
|
|
|
|
classNames={{ popup: { root: 'min-w-60' } }}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
{/* <div className="ant-form-item-explain-error text-red-500" >请选择发件地址</div> */}
|
|
|
|
|
<div className='ml-auto'></div>
|
|
|
|
|
<span>{orderDetail.order_no}</span>
|
|
|
|
|
<span>{templateContent.mailtypeName}</span>
|
|
|
|
|
<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 onClick={() => onHandleSaveOrSend(true)} type='dashed' icon={<SaveOutlined />} size='small' className=''>
|
|
|
|
|
存草稿
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<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='抄 送' name={'cc'} hidden={!showCc} className='w-full pt-1'>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item label='密 送' name={'bcc'} hidden={!showBcc} className='w-full pt-1'>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item label='主 题' 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 [&_.ant-upload-list-item-name]:cursor-pointer'>
|
|
|
|
|
<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.Item name='id' hidden>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name='mai_sn' hidden>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name='mat_sn' hidden>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name='coli_sn' hidden>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name='opi_sn' hidden>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name='mailtype' hidden>
|
|
|
|
|
<Input />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
<RoosterEditor initialContent={'<p>Hello from <b>RoosterJs</b> in your <i>Ant Design</i> React app!</p>' }onChange={handleEditorChange} />
|
|
|
|
|
<LexicalEditor {...{ isRichText }} onChange={handleEditorChange} defaultValue={initialContent} />
|
|
|
|
|
{!isEmpty(Number(pageParam.quoteid)) && pageParam.action !== 'edit' && !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
|