feat: useEmailList hooks

dev/ckeditor
Lei OT 4 months ago
parent 1d513ec038
commit efae99e81e

@ -1,6 +1,6 @@
import { fetchJSON, postForm, postJSON } from '@/utils/request';
import { API_HOST, DATE_FORMAT, DATEEND_FORMAT, DATETIME_FORMAT, EMAIL_HOST } from '@/config';
import { buildTree, isEmpty, objectMapper } from '@/utils/commons';
import { buildTree, isEmpty, objectMapper, omitEmpty, readIndexDB, uniqWith, writeIndexDB } from '@/utils/commons';
import dayjs from 'dayjs';
const parseHTMLString = (html, needText = false) => {
@ -80,6 +80,12 @@ const encodeEmailInfo = (info) => {
* @param {object} { mai_sn }
*/
export const getEmailDetailAction = async (params) => {
// const cacheKey = params.mai_sn;
// const readCache = await readIndexDB(cacheKey, 'mailinfo', 'mailbox');
// if (!isEmpty(readCache)) { // todo: 除了草稿
// return readCache.data;
// }
const { result } = await fetchJSON(`${EMAIL_HOST}/getmail`, params);
let mailType = result.MailInfo?.[0]?.MAI_ContentType || '';
mailType = mailType === '' && (result.MailContent||'').includes('<html') ? 'text/html' : mailType;
@ -94,12 +100,14 @@ export const getEmailDetailAction = async (params) => {
const attachments = (isEmpty(result?.AttachList) ? [] : result.AttachList).filter(ele => isEmpty(ele.ATI_ContentID));
return {
const ret = {
info: { ...encodeEmailInfo(result.MailInfo?.[0] || {}), mailType },
content: mailType === 'text/html' ? html : result.MailContent || '',
abstract: bodyText || result.MailContent || '',
attachments,
}
// writeIndexDB([{key: cacheKey, data: ret}], 'mailinfo', 'mailbox')
return ret;
}
export const getEmailOrderAction = async ({ colisn }) => {
@ -154,10 +162,26 @@ export const getEmailDirAction = async (params = { opi_sn: '' }) => {
* @usage 订单的邮件列表
* @usage 高级搜索
*/
export const queryEmailListAction = async ({ opi_sn= '', pagesize= 10, last_id= '', query={}, order= {} }={}) => {
const _params = { vkey: 0, vparent: 0, order_source_type: 0, mai_senddate1: dayjs().startOf('year').format(DATE_FORMAT), mai_senddate2: dayjs().format(DATEEND_FORMAT), ...order, ...query, opi_sn }
export const queryEmailListAction = async ({ opi_sn = '', pagesize = 10, last_id = '', node = {}, } = {}) => {
const _params = {
vkey: 0,
vparent: 0,
order_source_type: 0,
mai_senddate1: dayjs().subtract(1, 'year').startOf('year').format(DATE_FORMAT),
mai_senddate2: dayjs().format(DATEEND_FORMAT),
...omitEmpty({
...node,
opi_sn,
}),
}
_params.mai_senddate1 = dayjs(_params.mai_senddate1).format(DATE_FORMAT)
const cacheKey = isEmpty(_params.coli_sn) ? `dir-${node.vkey}` : `order-${node.vkey}`;
const { errcode, result } = await fetchJSON(`http://202.103.68.144:8889/v3/mail_list`, _params)
return errcode === 0 ? result : []
const ret = errcode === 0 ? result : []
if (!isEmpty(ret)) {
writeIndexDB([{key: cacheKey, data: ret}], 'maillist', 'mailbox')
}
return ret;
}
/**

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react'
import { isEmpty } from '@/utils/commons'
import { getEmailDetailAction, postResendEmailAction, getSalesSignatureAction, getEmailOrderAction } from '@/actions/EmailActions'
import { isEmpty, readIndexDB, } from '@/utils/commons'
import { getEmailDetailAction, postResendEmailAction, getSalesSignatureAction, getEmailOrderAction, queryEmailListAction } from '@/actions/EmailActions'
import { App } from 'antd'
import useConversationStore from '@/stores/ConversationStore';
import { msgStatusRenderMapped } from '@/channel/bubbleMsgUtils';
@ -116,3 +116,66 @@ export const useEmailDetail = (mai_sn, data) => {
export const EmailBuilder = ({subject, content}) => {
return `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html lang="en"><head><title>${subject}</title><meta http-equiv="Content-Type" content="text/html charset=UTF-8" /><meta content="width=device-width" name="viewport"><meta charset="UTF-8"><meta content="IE=edge" http-equiv="X-UA-Compatible"><meta content="width=device-width" name="viewport"><meta charset="UTF-8"><meta content="IE=edge" http-equiv="X-UA-Compatible"><meta content="telephone=no,address=no,email=no,date=no,url=no" name="format-detection"><meta content="light" name="color-scheme"><meta content="light" name="supported-color-schemes"><style id="font">body#highlights-email{ font-family: Verdana, sans-serif;} table{ border-collapse: collapse; border-spacing: 0;} </style></head><body id="highlights-email" style="margin: 0 auto; padding: 0; width: 900px; background-color: #fcfcfc;">${content}</body></html>`;
}
export const useEmailList = (mailboxDirNode) => {
const [loading, setLoading] = useState(false)
const [mailList, setMailList] = useState([])
const [error, setError] = useState(null)
const [isFreshData, setIsFreshData] = useState(false)
const { OPI_SN: opi_sn, COLI_SN, VKey, VParent, ApplyDate, OrderSourceType, IsTrue } = mailboxDirNode
const getMailList = useCallback(async () => {
console.log('getMailList', mailboxDirNode)
if (!opi_sn || !VKey || (!IsTrue && !COLI_SN)) {
setMailList([])
setLoading(false)
setError(null)
setIsFreshData(false)
return
}
setLoading(true)
setError(null)
setIsFreshData(false)
const cacheKey = isEmpty(COLI_SN) ? `dir-${VKey}` : `order-${VKey}`
const readCache = await readIndexDB(cacheKey, 'maillist', 'mailbox')
if (!isEmpty(readCache)) {
const _x = readCache.data.map((ele) => ({
...ele,
key: ele.MAI_SN,
title: ele.MAI_Subject,
description: ele.SenderReceiver,
mailDate: ele.SRDate,
orderNo: ele.MAI_COLI_ID || '',
country: ele.CountryCN || '',
}))
setMailList(_x)
}
try {
const nodeParam = { coli_sn: COLI_SN, order_source_type: OrderSourceType, vkey: VKey, vparent: VParent, mai_senddate1: ApplyDate }
const x = await queryEmailListAction({ opi_sn, node: nodeParam })
// 配合List的结构
const _x = x.map((ele) => ({
...ele,
key: ele.MAI_SN,
title: ele.MAI_Subject,
description: ele.SenderReceiver,
mailDate: ele.SRDate,
orderNo: ele.MAI_COLI_ID || '',
country: ele.CountryCN || '',
}))
setMailList(_x)
} catch (networkError) {
setError(new Error(`Failed to get mail list: ${networkError.message}.`))
} finally {
setLoading(false)
}
}, [VKey])
useEffect(() => {
getMailList()
}, [getMailList])
return { loading, isFreshData, error, mailList }
}

@ -1,11 +1,12 @@
import { create } from 'zustand';
import { RealTimeAPI } from '@/channel/realTimeAPI';
import { olog, isEmpty, groupBy, sortArrayByOrder, logWebsocket, pick, sortKeys, omit, sortObjectsByKeysMap, clean7DaysWebsocketLog } from '@/utils/commons';
import { olog, isEmpty, groupBy, sortArrayByOrder, logWebsocket, pick, sortKeys, omit, sortObjectsByKeysMap, clean7DaysWebsocketLog, createIndexedDBStore } from '@/utils/commons';
import { receivedMsgTypeMapped, handleNotification } from '@/channel/bubbleMsgUtils';
import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions';
import { devtools } from 'zustand/middleware';
import { WS_URL, DATETIME_FORMAT } from '@/config';
import dayjs from 'dayjs';
import EmailSlice from './EmailSlice';
const replaceObjectsByKey = (arr1, arr2, key) => {
const map2 = new Map(arr2.map(ele => [ele[key], ele]));
@ -552,70 +553,6 @@ const messageSlice = (set, get) => ({
},
});
/**
* Email
*/
const emailSlice = (set, get) => ({
emailMsg: { id: -1, conversationid: '', actionId: '', order_opi: '', coli_sn: '', msgOrigin: {} },
setEmailMsg: (emailMsg) => {
const { editorOpen } = get();
return editorOpen ? false : set({ emailMsg }); // 已经打开的不更新
},
detailPopupOpen: false,
setDetailOpen: (v) => set({ detailPopupOpen: v }),
openDetail: () => set(() => ({ detailPopupOpen: true })),
closeDetail: () => set(() => ({ detailPopupOpen: false })),
editorOpen: false,
setEditorOpen: (v) => set({ editorOpen: v }),
openEditor: () => set(() => ({ editorOpen: true })),
closeEditor: () => set(() => ({ editorOpen: false })),
// EmailEditorPopup 组件的 props
// @property {string} fromEmail - 发件人邮箱
// @property {string} fromUser - 发件人用户
// @property {string} fromOrder - 发件订单
// @property {string} toEmail - 收件人邮箱
// @property {string} conversationid - 会话ID
// @property {string} quoteid - 引用邮件ID
// @property {object} draft - 草稿
// @property {string} action - reply / forward / new / edit
// @property {string} oid - coli_sn
// @property {object} mailData - 邮件内容
// @property {string} receiverName - 收件人称呼
emailEdiorProps: new Map(),
setEditorProps: (v) => {
const { emailEdiorProps } = get();
const uniqueKey = v.quoteid || Date.now().toString(32);
const currentEditValue = {...v, key: `${v.action}-${uniqueKey}`};
const news = new Map(emailEdiorProps).set(currentEditValue.key, currentEditValue);
for (const [key, value] of news.entries()) {
console.log(value);
}
return set((state) => ({ emailEdiorProps: news, currentEditKey: currentEditValue.key, currentEditValue }))
// return set((state) => ({ emailEdiorProps: { ...state.emailEdiorProps, ...v } }))
},
closeEditor1: (key) => {
const { emailEdiorProps } = get();
const newProps = new Map(emailEdiorProps);
newProps.delete(key);
return set(() => ({ emailEdiorProps: newProps }))
},
clearEditor: () => {
return set(() => ({ emailEdiorProps: new Map() }))
},
currentEditKey: '',
setCurrentEditKey: (key) => {
const { emailEdiorProps, setCurrentEditValue } = get();
const value = emailEdiorProps.get(key);
setCurrentEditValue(value);
return set(() => ({ currentEditKey: key }))
},
currentEditValue: {},
setCurrentEditValue: (v) => {
return set(() => ({ currentEditValue: v }))
}
})
export const useConversationStore = create(
devtools((set, get) => ({
...initialConversationState,
@ -628,7 +565,7 @@ export const useConversationStore = create(
...tagsSlice(set, get),
...filterSlice(set, get),
...globalNotifySlice(set, get),
...emailSlice(set, get),
...EmailSlice(set, get),
...waiSlice(set, get),
// state actions
@ -637,7 +574,10 @@ export const useConversationStore = create(
// side effects
fetchInitialData: async ({userId, whatsAppBusiness, ...loginUser}) => {
const { addToConversationList, setTemplates, setInitial, setTags } = get();
const { addToConversationList, setTemplates, setInitial, setTags,
initMailbox } = get();
initMailbox({ userId, dei_sn: loginUser.accountList[0].OPI_DEI_SN, opi_sn: loginUser.accountList[0].OPI_SN })
const conversationsList = await fetchConversationsList({ opisn: userId });
addToConversationList(conversationsList);

@ -0,0 +1,134 @@
import { getEmailDirAction } from '@/actions/EmailActions'
import { buildTree, isEmpty, readIndexDB, writeIndexDB, createIndexedDBStore } from '@/utils/commons'
/**
* Email
*/
const emailSlice = (set, get) => ({
emailMsg: { id: -1, conversationid: '', actionId: '', order_opi: '', coli_sn: '', msgOrigin: {} },
setEmailMsg: (emailMsg) => {
const { editorOpen } = get()
return editorOpen ? false : set({ emailMsg }) // 已经打开的不更新
},
detailPopupOpen: false,
setDetailOpen: (v) => set({ detailPopupOpen: v }),
openDetail: () => set(() => ({ detailPopupOpen: true })),
closeDetail: () => set(() => ({ detailPopupOpen: false })),
editorOpen: false,
setEditorOpen: (v) => set({ editorOpen: v }),
openEditor: () => set(() => ({ editorOpen: true })),
closeEditor: () => set(() => ({ editorOpen: false })),
// EmailEditorPopup 组件的 props
// @property {string} fromEmail - 发件人邮箱
// @property {string} fromUser - 发件人用户
// @property {string} fromOrder - 发件订单
// @property {string} toEmail - 收件人邮箱
// @property {string} conversationid - 会话ID
// @property {string} quoteid - 引用邮件ID
// @property {object} draft - 草稿
// @property {string} action - reply / forward / new / edit
// @property {string} oid - coli_sn
// @property {object} mailData - 邮件内容
// @property {string} receiverName - 收件人称呼
emailEdiorProps: new Map(),
setEditorProps: (v) => {
const { emailEdiorProps } = get()
const uniqueKey = v.quoteid || Date.now().toString(32)
const currentEditValue = { ...v, key: `${v.action}-${uniqueKey}` }
const news = new Map(emailEdiorProps).set(currentEditValue.key, currentEditValue)
for (const [key, value] of news.entries()) {
console.log(value)
}
return set((state) => ({ emailEdiorProps: news, currentEditKey: currentEditValue.key, currentEditValue }))
// return set((state) => ({ emailEdiorProps: { ...state.emailEdiorProps, ...v } }))
},
closeEditor1: (key) => {
const { emailEdiorProps } = get()
const newProps = new Map(emailEdiorProps)
newProps.delete(key)
return set(() => ({ emailEdiorProps: newProps }))
},
clearEditor: () => {
return set(() => ({ emailEdiorProps: new Map() }))
},
currentEditKey: '',
setCurrentEditKey: (key) => {
const { emailEdiorProps, setCurrentEditValue } = get()
const value = emailEdiorProps.get(key)
setCurrentEditValue(value)
return set(() => ({ currentEditKey: key }))
},
currentEditValue: {},
setCurrentEditValue: (v) => {
return set(() => ({ currentEditValue: v }))
},
// mailboxNestedDirs: new Map(),
// setMailboxNestedDirs: (opi, dirs) => {
// const { mailboxNestedDirs } = get()
// const news = mailboxNestedDirs.set(opi, dirs)
// return set(() => ({ mailboxNestedDirs: news }))
// },
currentMailboxDEI: 0,
setCurrentMailboxDEI: (id) => {
return set(() => ({ currentMailboxDEI: id }))
},
currentMailboxOPI: 0,
setCurrentMailboxOPI: (id) => {
return set(() => ({ currentMailboxOPI: id }))
},
mailboxNestedDirsActive: [],
setMailboxNestedDirsActive: (dir) => {
return set(() => ({ mailboxNestedDirsActive: dir }))
},
mailboxActiveNode: {},
setMailboxActiveNode: (node) => {
return set(() => ({ mailboxActiveNode: node }))
},
mailboxList: [],
setMailboxList: (list) => {
return set(() => ({ mailboxList: list }))
},
mailboxActiveMAI: 0,
setMailboxActiveMAI: (mai) => {
return set(() => ({ mailboxActiveMAI: mai }))
},
getOPIEmailDir: async (opi_sn = 0) => {
// console.log('🌐requesting opi dir', opi_sn, typeof opi_sn)
const { setMailboxNestedDirsActive } = get()
const readCache = await readIndexDB(Number(opi_sn), 'dirs', 'mailbox')
// console.log(readCache);
// setMailboxNestedDirs(Number(opi_sn), readCache.tree)
let isNeedRefresh = false
if (!isEmpty(readCache)) {
setMailboxNestedDirsActive(readCache?.tree || [])
isNeedRefresh = Date.now() - readCache.timestamp > 4 * 60 * 60 * 1000
} else if (isEmpty(readCache) || isNeedRefresh) {
// > {4} 更新
const x = await getEmailDirAction({ opi_sn })
const mailboxSort = x //.sort(sortBy('MDR_Order'));
let tree = buildTree(mailboxSort, { key: 'VKey', parent: 'VParent', name: 'VName', iconIndex: 'ImageIndex', rootKeys: [1], ignoreKeys: [-227001, -227002] })
tree = tree.filter((ele) => ele.key !== 1)
writeIndexDB([{ key: Number(opi_sn), tree }], 'dirs', 'mailbox')
// setMailboxNestedDirs(Number(opi_sn), tree)
setMailboxNestedDirsActive(tree)
}
return false
},
async initMailbox({ opi_sn, dei_sn, userId }) {
const { setCurrentMailboxOPI, setCurrentMailboxDEI, getOPIEmailDir } = get()
createIndexedDBStore(['dirs', 'maillist', 'mailinfo'], 'mailbox')
setCurrentMailboxOPI(opi_sn)
setCurrentMailboxDEI(dei_sn)
getOPIEmailDir(userId)
},
})
export default emailSlice

@ -226,6 +226,15 @@ export function omit(object, keysToOmit) {
return Object.fromEntries(Object.entries(object).filter(([key]) => !keysToOmit.includes(key)));
}
/**
* 去除无效的值: undefined, null, '', []
* * 只删除 null undefined: flush 方法;
*/
export const omitEmpty = _object => {
Object.keys(_object).forEach(key => (_object[key] == null || _object[key] === '' || _object[key].length === 0) && delete _object[key]);
return _object;
};
/**
* 深拷贝
*/
@ -606,12 +615,14 @@ export const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findInde
* @param {string|null} parent - The key of the parent node, or null if it's a root.
* @returns {object} A plain JavaScript object representing the tree node.
*/
function createTreeNode(key, name, parent = null, _raw={}) {
function createTreeNode(key, name, parent = null, keyMap={}, _raw={}) {
return {
key: key,
title: name,
parent: parent,
parentTitle: '',
icon: _raw?.icon,
iconIndex: _raw?.[keyMap.iconIndex],
_raw: _raw,
children: []
};
@ -629,7 +640,7 @@ export const buildTree = (list, keyMap={ rootKeys: [], ignoreKeys: [] }) => {
const treeRoots = []
list.forEach((item) => {
const node = createTreeNode(item[keyMap.key], item[keyMap.name], item[keyMap.parent], item)
const node = createTreeNode(item[keyMap.key], item[keyMap.name], item[keyMap.parent], keyMap, item)
nodeMap.set(item[keyMap.key], node)
})
@ -642,12 +653,15 @@ export const buildTree = (list, keyMap={ rootKeys: [], ignoreKeys: [] }) => {
const parentNode = nodeMap.get(item[keyMap.parent])
if (keyMap.ignoreKeys.includes(item[keyMap.parent])) {
const grandParentNode = nodeMap.get(parentNode.parent);
node.rawParent = node.parent;
node.parent = parentNode.parent;
node.parentTitle = parentNode.title;
grandParentNode.children.push(node)
} else if (keyMap.ignoreKeys.includes(item[keyMap.key])) {
//
}
else if (parentNode) {
node.parentTitle = parentNode.title;
parentNode.children.push(node)
} else {
console.warn(`Parent with key '${item[keyMap.parent]}' not found for node '${item[keyMap.key]}'. This node will be treated as a root.`)
@ -761,9 +775,30 @@ export const clearWebsocketLog = () => {
}
}
export const writeIndexDB = (row, table, database) => {
export const createIndexedDBStore = (tables, database) => {
var open = indexedDB.open(database, INDEXED_DB_VERSION)
open.onupgradeneeded = function () {
console.log('readIndexDB onupgradeneeded', database, )
var db = open.result
// 数据库是否存在
for (const table of tables) {
if (!db.objectStoreNames.contains(table)) {
var store = db.createObjectStore(table, { keyPath: 'key' })
store.createIndex('timestamp', 'timestamp', { unique: false })
} else {
const objectStore = open.transaction.objectStore(table)
if (!objectStore.indexNames.contains('timestamp')) {
objectStore.createIndex('timestamp', 'timestamp', { unique: false })
}
}
}
}
};
export const writeIndexDB = (rows, table, database) => {
var open = indexedDB.open(database, INDEXED_DB_VERSION)
open.onupgradeneeded = function () {
console.log('readIndexDB onupgradeneeded', table, )
var db = open.result
// 数据库是否存在
if (!db.objectStoreNames.contains(table)) {
@ -780,12 +815,87 @@ export const writeIndexDB = (row, table, database) => {
var db = open.result
var tx = db.transaction(table, 'readwrite')
var store = tx.objectStore(table)
store.put({ ...row, _date: new Date().toLocaleString(), timestamp: Date.now() })
rows.forEach(row => {
store.put({ ...row, _date: new Date().toLocaleString(), timestamp: Date.now() })
});
tx.oncomplete = function () {
db.close()
}
}
};
export const readIndexDB = (key=null, table, database) => {
return new Promise((resolve, reject) => {
let openRequest = indexedDB.open(database)
openRequest.onupgradeneeded = function () {
console.log('readIndexDB onupgradeneeded', table, )
var db = openRequest.result
// 数据库是否存在
if (!db.objectStoreNames.contains(table)) {
var store = db.createObjectStore(table, { keyPath: 'key' })
store.createIndex('timestamp', 'timestamp', { unique: false })
} else {
const logStore = openRequest.transaction.objectStore(table)
if (!logStore.indexNames.contains('timestamp')) {
logStore.createIndex('timestamp', 'timestamp', { unique: false })
}
}
}
openRequest.onerror = function (e) {
console.error(`Error opening database.`, table, e)
reject('Error opening database.')
}
openRequest.onsuccess = function (e) {
let db = e.target.result
// 数据库是否存在
if (!db.objectStoreNames.contains(table)) {
resolve('Database does not exist.')
return
}
let transaction = db.transaction(table, 'readonly')
let store = transaction.objectStore(table)
// read by key
const getRequest = isEmpty(key) ? store.all() : store.get(key);
getRequest.onsuccess = (event) => {
const result = event.target.result
if (result) {
console.log(`Found record with key ${key}:`, result)
resolve(result)
} else {
console.log(`No record found with key ${key}.`)
resolve();
}
}
getRequest.onerror = (event) => {
console.error(`Error getting record with key ${key}:`, event.target.error)
}
// const request = store.openCursor(null, 'prev'); // 从后往前
// const results = [];
// let count = 0;
// request.onerror = function (e) {
// reject('Error getting records.')
// }
// request.onsuccess = function (e) {
// const cursor = e.target.result
// if (cursor) {
// if (count < limit) {
// results.unshift(cursor.value)
// count++
// cursor.continue()
// } else {
// console.log(JSON.stringify(results))
// resolve(results)
// }
// } else {
// console.log(JSON.stringify(results))
// resolve(results)
// }
// }
}
})
};
export const deleteIndexDBbyKey = (key, table, database) => {
var open = indexedDB.open(database, INDEXED_DB_VERSION)
open.onupgradeneeded = function () {
@ -902,4 +1012,3 @@ function cleanOldData(database, storeName, dateKey = 'timestamp') {
}
export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', 'LogStore');

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import { App, Button, Divider, Avatar, List, Flex, Typography, Tooltip } from 'antd'
import { App, Button, Divider, Avatar, List, Flex, Typography, Tooltip, Empty } from 'antd'
import { LoadingOutlined, ApiOutlined, FilePdfOutlined, FileOutlined, FileWordOutlined, FileExcelOutlined, FileJpgOutlined, FileImageOutlined, FileTextOutlined, FileGifOutlined, GlobalOutlined, FileZipOutlined } from '@ant-design/icons'
import { EditIcon, MailCheckIcon, ReplyAllIcon, ReplyIcon, ResendIcon, ShareForwardIcon, SendPlaneFillIcon, InboxIcon } from '@/components/Icons'
import { isEmpty, TagColorStyle } from '@/utils/commons'
@ -178,7 +178,7 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
}
}
return (
return mailID ?(
<>
<div ref={componentRef} className={`email-container h-full flex flex-col gap-0 divide-y divide-neutral-200 divide-solid *:p-2 *:border-0 bg-white ${variantCls(variant)}`} >
<div className=''>
@ -274,6 +274,8 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
key={`email-detail-inline-${action}_${mailID}`}
/> */}
</>
) : (
<Empty description={'未打开邮件'} image={Empty.PRESENTED_IMAGE_SIMPLE} />
)
}
export default EmailDetailInline

@ -483,7 +483,7 @@ const EmailEditorPopup = () => {
debounce((data) => {
idleCallbackId.current = window.requestIdleCallback(() => {
console.log('Saving data (idle, debounced):', data);
if (currentEditKey) writeIndexDB({ ...data, key: currentEditKey }, 'draft', 'EmailEditor')
if (currentEditKey) writeIndexDB([{ ...data, key: currentEditKey }], 'draft', 'EmailEditor')
});
}, 1500), // 1.5s
[]

@ -489,7 +489,7 @@ const NewEmail = ({ ...props }) => {
debounce((data) => {
idleCallbackId.current = window.requestIdleCallback(() => {
console.log('Saving data (idle, debounced):', data)
writeIndexDB({ ...data, key: editorKey }, 'draft', 'EmailEditor')
writeIndexDB([{ ...data, key: editorKey }], 'draft', 'EmailEditor')
})
}, 1500), // 1.5s
[],

@ -1,9 +1,9 @@
import useAuthStore from '@/stores/AuthStore'
import useFormStore from '@/stores/FormStore'
import { useOrderStore } from '@/stores/OrderStore'
import { isEmpty, groupBy, buildTree } from '@/utils/commons'
import { isEmpty, groupBy, buildTree, readIndexDB, writeIndexDB, pick } from '@/utils/commons'
import { StarTwoTone, CalendarTwoTone, FolderOutlined, DeleteOutlined, ClockCircleOutlined, FormOutlined, DatabaseOutlined } from '@ant-design/icons'
import { Flex, Drawer, Radio, Divider, Segmented, Tree, Typography, Checkbox, Layout, Row, Col } from 'antd'
import { Flex, Drawer, Radio, Divider, Segmented, Tree, Typography, Checkbox, Layout, Row, Col, Empty, Splitter } from 'antd'
import { useEffect, useMemo, useState } from 'react'
import { InboxIcon, MailUnreadIcon, SendPlaneFillIcon } from '@/components/Icons'
import { useShallow } from 'zustand/react/shallow'
@ -11,23 +11,10 @@ import EmailDetailInline from '../Conversations/Online/Components/EmailDetailInl
import { getEmailDirAction, queryEmailListAction } from '@/actions/EmailActions'
import OrderProfile from '@/components/OrderProfile'
import Mailbox from './components/Mailbox'
const EmailDirTypeIcons = {
0: { component: FolderOutlined, color: '#ffe78f', className: 'text-blue-500' },
1: { component: FolderOutlined, color: '#ffe78f', className: 'text-blue-500' },
3: { component: InboxIcon, color: '', className: 'text-indigo-500' },
17: { component: InboxIcon, color: '', className: 'text-indigo-500' },
11: { component: MailUnreadIcon, color: '', className: 'text-indigo-500' },
4: { component: SendPlaneFillIcon, color: '', className: 'text-primary' },
2: { component: ClockCircleOutlined, color: '', className: 'text-yellow-500' },
5: { component: FormOutlined, color: '', className: 'text-blue-500' },
7: { component: DeleteOutlined, color: '', className: 'text-red-500' },
// '3': { component: MailCheckIcon, color: '', className: 'text-yellow-600' },
12: { component: DatabaseOutlined, color: '', className: 'text-blue-600' },
13: { component: () => null, color: '', className: '' },
14: { component: () => '❗', color: '', className: '' }, // 240002 /
15: { component: () => '❣️', color: '', className: '' }, // 240003 /
}
import useConversationStore from '@/stores/ConversationStore';
import dayjs from 'dayjs'
import { DATE_FORMAT, DATETIME_FORMAT } from '@/config'
import { MailboxDirIcon } from './components/MailboxDirIcon'
const todoTypes = {
// 123456coli_ordertype=7coli_ordertype=8
@ -96,14 +83,14 @@ function Follow() {
key: ele.OPI_DEI_SN + '-today',
getMails: false,
icon: <StarTwoTone />,
children: [],
children: [], COLI_SN: 0,
},
{
title: '待办任务',
key: ele.OPI_DEI_SN + '-todo',
getMails: false,
icon: <CalendarTwoTone />,
children: [],
children: [], COLI_SN: 0,
},
],
}),
@ -111,80 +98,45 @@ function Follow() {
)
}, [accountList])
const [activeEmailId, setActiveEmailId] = useState(0)
const [getOPIEmailDir] = useConversationStore(state => [state.getOPIEmailDir]);
const [currentMailboxDEI, setCurrentMailboxDEI, mailboxNestedDirsActive] = useConversationStore(state => [state.currentMailboxDEI, state.setCurrentMailboxDEI, state.mailboxNestedDirsActive]);
const [currentMailboxOPI, setCurrentMailboxOPI] = useConversationStore(state => [state.currentMailboxOPI, state.setCurrentMailboxOPI]);
const [mailboxActiveNode, setMailboxActiveNode] = useConversationStore(state => [state.mailboxActiveNode, state.setMailboxActiveNode]);
const [activeEmailId, setActiveEmailId] = useConversationStore(state => [state.mailboxActiveMAI, state.setMailboxActiveMAI]);
// const [activeEmailId, setActiveEmailId] = useState(0)
const [activeAccount, setActiveAccount] = useState(accountDEI[0].value)
const [mailboxDir, setMailboxDir] = useState([])
const [deiStickyTree, setDeiStickyTree] = useState({})
const [stickyTree, setStickyTree] = useState([])
const [mergedTree, setMergedTree] = useState([]);
const [expandTree, setExpandTree] = useState([])
const [mailList, setMailList] = useState([]);
const DirTypeIcon = ({ type }) => {
const Icon = EmailDirTypeIcons[type || '0']?.component || EmailDirTypeIcons['0'].component
const className = EmailDirTypeIcons[type || '0']?.className || EmailDirTypeIcons['0'].className
return <Icon className={className} />
}
const getOPIEmailDir = async (opi_sn = 0) => {
console.log('🌐requesting opi dir', opi_sn)
const x = await getEmailDirAction({opi_sn})
const mailboxSort = x //.sort(sortBy('MDR_Order'));
const dirs = mailboxSort.map((ele) => {
return { ...ele, icon: ele.ImageIndex !== void 0 ? <DirTypeIcon type={ele.ImageIndex} /> : false }
})
let tree = buildTree(dirs, { key: 'VKey', parent: 'VParent', name: 'VName', rootKeys: [1], ignoreKeys: [-227001, -227002] })
tree = tree.filter((ele) => ele.key !== 1)
// console.log('tree', tree)
setMailboxDir(tree)
setMergedTree([...stickyTree, ...tree]);
// const level1 = []; // tree.filter((ele) => !isEmpty(ele.children)).map((ele) => ele.key)
// setExpandTree((pre) => [...pre, ...level1])
}
const getMailList = async ({ query, order }) => {
const opi_sn = accountListDEIMapped[activeAccount].OPI_SN || 404
const x = await queryEmailListAction({ opi_sn, query, order })
const _x = x.map(ele => ({...ele, key: ele.MAI_SN, title: ele.MAI_Subject, description: ele.SenderReceiver, mailDate: ele.SRDate, orderNo: ele.MAI_COLI_ID || '', country: ele.CountryCN || ''}));
setMailList(_x);
}
const handleSwitchAccount = (value) => {
setActiveAccount(value)
setActiveEmailId(0);
setCurrentMailboxDEI(value)
setStickyTree(deiStickyTree[value] || [])
setExpandTree([`${value}-today`, `${value}-todo`])
const opi = accountListDEIMapped[value].OPI_SN
getOPIEmailDir(opi)
setCurrentMailboxOPI(opi);
}
const handleTreeSelectGetMails = (selectedKeys, { node }) => {
console.info('selectedTreeKeys: ', selectedKeys, node)
if (node?.COLI_SN || node?._raw?.COLI_SN) {
// ;
// get order mails
// console.log('get order mails', { order: { coli_sn: node?.COLI_SN || node?._raw?.COLI_SN, order_source_type: node?._raw?.OrderSourceType || 227001, vkey: node.key } })
const coli_sn = node?.COLI_SN || node?._raw?.COLI_SN;
getMailList({ order: { coli_sn: coli_sn, order_source_type: node?._raw?.OrderSourceType || 227001, vkey: coli_sn, vparent: node?.parent || -1, mai_senddate1: node?._raw?.ApplyDate || '' } })
} else if ([-227001, -227002].includes(node.key) || [-227001, -227002].includes(node.parent) || node?.getMails === false) {
// nothing, expand only
console.log('nothing')
} else {
// get mail list
console.log('get mail list')
getMailList({ query: { vkey: selectedKeys[0], vparent: node.parent } })
}
// console.info('selectedTreeKeys: ', node)
const treeNode = pick(node, ['key', 'parent', 'getMails', 'title', 'parentTitle', ]);
setMailboxActiveNode({...treeNode, ...node._raw, OPI_SN: currentMailboxOPI});
// const { COLI_SN, VKey, VParent, ApplyDate, OrderSourceType, IsTrue } = node?._raw || {}
setActiveEmailId(0);
}
useEffect(() => {
fetchOrderList({ type: 'today' }, loginUser)
getOPIEmailDir(accountList[0].OPI_SN)
return () => {}
}, [])
// 123456coli_ordertype=7coli_ordertype=8
useEffect(() => {
console.log('orderList render', currentMailboxDEI, currentMailboxOPI)
const byDEI = groupBy(orderList, 'OPI_DEI_SN')
// console.log(byDEI, 'byDEI')
const byState = Object.keys(byDEI).reduce((acc, key) => {
@ -199,15 +151,27 @@ function Follow() {
title: '今日任务',
key: key + '-today',
getMails: false,
icon: <StarTwoTone />,
children: (sticky[0] || []).map((o) => ({ key: `today-${o.COLI_SN}`, title: `(${todoTypes[o.coli_ordertype] || o.COLI_State}) ${o.COLI_ID}`, _raw: {...o, ApplyDate: '', OrderSourceType: 227001,parent: -1, }})),
icon: <StarTwoTone />, _raw: {COLI_SN: 0},
children: (sticky[0] || []).map((o) => ({
key: `today-${o.COLI_SN}`,
title: `(${todoTypes[o.coli_ordertype] || o.COLI_State}) ${o.COLI_ID}`,
parent: key + '-today',
parentTitle: '今日任务',
_raw: { ...o, VKey: o.COLI_SN, VName: o.COLI_ID, VParent:-1, IsTrue: 0, ApplyDate: '', OrderSourceType: 227001, parent: -1 },
})),
},
{
title: '待办任务',
key: key + '-todo',
getMails: false,
icon: <CalendarTwoTone />,
children: (sticky[1] || []).map((o) => ({ key: `todo-${o.COLI_SN}`, title: `(${todoTypes[o.coli_ordertype] || o.COLI_State}) ${o.COLI_ID}`, _raw: {...o, ApplyDate: '', OrderSourceType: 227001,parent: -1, } })),
icon: <CalendarTwoTone />,_raw: {COLI_SN: 0},
children: (sticky[1] || []).map((o) => ({
key: `todo-${o.COLI_SN}`,
title: `(${todoTypes[o.coli_ordertype] || o.COLI_State}) ${o.COLI_ID}`,
parent: key + '-todo',
parentTitle: '待办任务',
_raw: { ...o, VKey: o.COLI_SN, VName: o.COLI_ID, VParent:-1, IsTrue: 0, ApplyDate: '', OrderSourceType: 227001, parent: -1 },
})),
},
]
// { key, title: deiName, children: sticky[0] };
@ -215,7 +179,7 @@ function Follow() {
}, defaultStickyTree)
// console.log('tree 0', byState);
setDeiStickyTree(byState)
const first = accountDEI[0].value
const first = currentMailboxDEI || accountDEI[0].value
setExpandTree([`${first}-today`, `${first}-todo`])
setStickyTree(byState[first] || [])
@ -227,42 +191,44 @@ function Follow() {
<Layout>
<Layout.Sider width='300' theme='light' style={{ height: 'calc(100vh - 166px)' }}>
<Flex justify='start' align='start' vertical className='h-full'>
<Segmented className='w-full' block shape='round' options={accountDEI} value={activeAccount} onChange={handleSwitchAccount} />
<Segmented className='w-full' block shape='round' options={accountDEI} value={currentMailboxDEI} onChange={handleSwitchAccount} />
<div className='overflow-y-auto flex-auto w-full [&_.ant-tree-switcher]:me-0 [&_.ant-tree-node-content-wrapper]:px-0 [&_.ant-tree-node-content-wrapper]:text-ellipsis [&_.ant-tree-node-content-wrapper]:overflow-hidden [&_.ant-tree-node-content-wrapper]:whitespace-nowrap'>
<Tree
key='sticky-today'
blockNode
showIcon
showLine expandAction={'doubleClick'}
showLine
expandAction={'doubleClick'}
onSelect={handleTreeSelectGetMails}
onExpand={(expandedKeys) => setExpandTree(expandedKeys)}
expandedKeys={expandTree}
defaultExpandedKeys={expandTree}
defaultSelectedKeys={['today']}
treeData={[...(stickyTree || []), ...mailboxDir]}
treeData={[...(stickyTree || []), ...mailboxNestedDirsActive]}
// treeData={mergedTree}
icon={(node) => <MailboxDirIcon type={node?.iconIndex} />}
titleRender={(node) => <Typography.Text ellipsis={{ tooltip: node.title }}>{node.title}</Typography.Text>}
/>
</div>
</Flex>
</Layout.Sider>
<Layout.Content style={{ maxHeight: 'calc(100vh - 166px)', height: 'calc(100vh - 166px)', minWidth: '360px' }}>
<Row>
<Col className='bg-white' span={14}>
<Mailbox dataSource={mailList} selectedIds={[activeEmailId]} onMailClick={id => setActiveEmailId(id)} />
</Col>
<Col span={10} style={{ height: 'calc(100vh - 166px)' }}>
<Splitter>
<Splitter.Panel defaultSize='40%' min={580} max='70%'>
<Mailbox mailboxDir={mailboxActiveNode} onMailItemClick={(id) => setActiveEmailId(id)} />
</Splitter.Panel>
<Splitter.Panel>
<EmailDetailInline mailID={activeEmailId || 0} emailMsg={{}} variant={'outline'} size={'small'} />
</Col>
</Row>
</Splitter.Panel>
</Splitter>
</Layout.Content>
<Layout.Sider
zeroWidthTriggerStyle={{ top: '30px' }}
width='280' theme='light' className='overflow-y-auto'
width='280'
theme='light'
className='overflow-y-auto'
style={{
height: 'calc(100vh - 166px)'
height: 'calc(100vh - 166px)',
}}
collapsible
collapsed={collapsed}

@ -2,9 +2,10 @@ import { useCallback, useEffect, useState } from 'react'
import { ReloadOutlined, ReadOutlined, CheckSquareOutlined, StarOutlined, RightOutlined, LeftOutlined } from '@ant-design/icons'
import { Flex, Button, Tooltip, List, Form, Row, Col, Drawer, Dropdown, Input, Checkbox, DatePicker, Select, Breadcrumb } from 'antd'
import dayjs from 'dayjs'
import { useEmailList } from '@/hooks/useEmail';
const { RangePicker } = DatePicker
const MailBox = (props) => {
const MailBox = ({ mailboxDir, onMailItemClick, ...props}) => {
const DATE_RANGE_PRESETS = [
{
label: '本周',
@ -34,6 +35,8 @@ const MailBox = (props) => {
const [openOrder, setOpenOrder] = useState(false)
const [form] = Form.useForm()
const { mailList, isLoading, error } = useEmailList(mailboxDir);
return (
<>
<div className='bg-white h-auto px-1 flex gap-1 items-center'>
@ -145,7 +148,7 @@ const MailBox = (props) => {
itemLayout='vertical'
size='large'
pagination={false}
dataSource={props.dataSource}
dataSource={mailList}
renderItem={(item) => (
<li className='flex border border-solid border-t-0 border-x-0 border-gray-200 hover:bg-neutral-50 p-2'>
<div className=''>

@ -0,0 +1,27 @@
import { StarTwoTone, CalendarTwoTone, FolderOutlined, DeleteOutlined, ClockCircleOutlined, FormOutlined, DatabaseOutlined } from '@ant-design/icons'
import { InboxIcon, MailUnreadIcon, SendPlaneFillIcon } from '@/components/Icons'
const EmailDirTypeIcons = {
0: { component: FolderOutlined, color: '#ffe78f', className: 'text-blue-500' },
1: { component: FolderOutlined, color: '#ffe78f', className: 'text-blue-500' },
3: { component: InboxIcon, color: '', className: 'text-indigo-500' },
17: { component: InboxIcon, color: '', className: 'text-indigo-500' },
11: { component: MailUnreadIcon, color: '', className: 'text-indigo-500' },
4: { component: SendPlaneFillIcon, color: '', className: 'text-primary' },
2: { component: ClockCircleOutlined, color: '', className: 'text-yellow-500' },
5: { component: FormOutlined, color: '', className: 'text-blue-500' },
7: { component: DeleteOutlined, color: '', className: 'text-red-500' },
// '3': { component: MailCheckIcon, color: '', className: 'text-yellow-600' },
12: { component: DatabaseOutlined, color: '', className: 'text-blue-600' },
13: { component: () => null, color: '', className: '' },
14: { component: () => '❗', color: '', className: '' }, // 240002 /
15: { component: () => '❣️', color: '', className: '' }, // 240003 /
}
export const MailboxDirIcon = ({ type }) => {
const Icon = EmailDirTypeIcons[type || '13']?.component || EmailDirTypeIcons['13'].component
const className = EmailDirTypeIcons[type || '13']?.className || EmailDirTypeIcons['13'].className
return <Icon className={className} />
}
export default MailboxDirIcon
Loading…
Cancel
Save