From efae99e81eed2212b3ffd9d58a446ba56f73b3f2 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 6 Jun 2025 14:12:21 +0800 Subject: [PATCH] feat: useEmailList hooks --- src/actions/EmailActions.js | 34 +++- src/hooks/useEmail.js | 67 +++++++- src/stores/ConversationStore.js | 74 +-------- src/stores/EmailSlice.js | 134 ++++++++++++++++ src/utils/commons.js | 119 +++++++++++++- .../Online/Components/EmailDetailInline.jsx | 6 +- .../Online/Input/EmailEditorPopup.jsx | 2 +- src/views/NewEmail.jsx | 2 +- src/views/orders/Follow.jsx | 150 +++++++----------- src/views/orders/components/MailBox.jsx | 7 +- .../orders/components/MailboxDirIcon.jsx | 27 ++++ 11 files changed, 445 insertions(+), 177 deletions(-) create mode 100644 src/stores/EmailSlice.js create mode 100644 src/views/orders/components/MailboxDirIcon.jsx diff --git a/src/actions/EmailActions.js b/src/actions/EmailActions.js index 9a4ec36..451aba8 100644 --- a/src/actions/EmailActions.js +++ b/src/actions/EmailActions.js @@ -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(' { 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; } /** diff --git a/src/hooks/useEmail.js b/src/hooks/useEmail.js index 7cb142a..1c06f17 100644 --- a/src/hooks/useEmail.js +++ b/src/hooks/useEmail.js @@ -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 `${subject}${content}`; } + +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 } +} diff --git a/src/stores/ConversationStore.js b/src/stores/ConversationStore.js index 3c9b289..c6fc272 100644 --- a/src/stores/ConversationStore.js +++ b/src/stores/ConversationStore.js @@ -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); diff --git a/src/stores/EmailSlice.js b/src/stores/EmailSlice.js new file mode 100644 index 0000000..8cbec4f --- /dev/null +++ b/src/stores/EmailSlice.js @@ -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 diff --git a/src/utils/commons.js b/src/utils/commons.js index 44e7c2e..7c5f9c4 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -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'); - diff --git a/src/views/Conversations/Online/Components/EmailDetailInline.jsx b/src/views/Conversations/Online/Components/EmailDetailInline.jsx index 53358fb..dea7317 100644 --- a/src/views/Conversations/Online/Components/EmailDetailInline.jsx +++ b/src/views/Conversations/Online/Components/EmailDetailInline.jsx @@ -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 ?( <>
@@ -274,6 +274,8 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s key={`email-detail-inline-${action}_${mailID}`} /> */} + ) : ( + ) } export default EmailDetailInline diff --git a/src/views/Conversations/Online/Input/EmailEditorPopup.jsx b/src/views/Conversations/Online/Input/EmailEditorPopup.jsx index 6582c33..284aad2 100644 --- a/src/views/Conversations/Online/Input/EmailEditorPopup.jsx +++ b/src/views/Conversations/Online/Input/EmailEditorPopup.jsx @@ -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 [] diff --git a/src/views/NewEmail.jsx b/src/views/NewEmail.jsx index f7179b4..5658204 100644 --- a/src/views/NewEmail.jsx +++ b/src/views/NewEmail.jsx @@ -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 [], diff --git a/src/views/orders/Follow.jsx b/src/views/orders/Follow.jsx index 4ce5cc8..6a2a71a 100644 --- a/src/views/orders/Follow.jsx +++ b/src/views/orders/Follow.jsx @@ -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 = { // 1新订单;2未读消息;3需一催;4需二催;5需三催;6未处理邮件;入境提醒coli_ordertype=7,余款提醒coli_ordertype=8 @@ -96,14 +83,14 @@ function Follow() { key: ele.OPI_DEI_SN + '-today', getMails: false, icon: , - children: [], + children: [], COLI_SN: 0, }, { title: '待办任务', key: ele.OPI_DEI_SN + '-todo', getMails: false, icon: , - 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 - } - - 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 ? : 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 () => {} }, []) // 1新订单;2未读消息;3需一催;4需二催;5需三催;6未处理邮件;入境提醒coli_ordertype=7,余款提醒coli_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: , - 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: , _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: , - 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: ,_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() { - +
setExpandTree(expandedKeys)} expandedKeys={expandTree} defaultExpandedKeys={expandTree} - defaultSelectedKeys={['today']} - treeData={[...(stickyTree || []), ...mailboxDir]} + treeData={[...(stickyTree || []), ...mailboxNestedDirsActive]} // treeData={mergedTree} + icon={(node) => } titleRender={(node) => {node.title}} />
- - - setActiveEmailId(id)} /> - - - + + + setActiveEmailId(id)} /> + + - - + + { +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 ( <>
@@ -145,7 +148,7 @@ const MailBox = (props) => { itemLayout='vertical' size='large' pagination={false} - dataSource={props.dataSource} + dataSource={mailList} renderItem={(item) => (
  • diff --git a/src/views/orders/components/MailboxDirIcon.jsx b/src/views/orders/components/MailboxDirIcon.jsx new file mode 100644 index 0000000..d042e27 --- /dev/null +++ b/src/views/orders/components/MailboxDirIcon.jsx @@ -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 +} + +export default MailboxDirIcon