feat: 数据更新: 广播; 事件; `已读`, `已处理` 不刷新请求, 仅更新缓存

dev/ckeditor
Lei OT 4 months ago
parent 10e6b56446
commit 5f657b2618

@ -3,6 +3,7 @@ import { API_HOST, API_HOST_V3, DATE_FORMAT, DATEEND_FORMAT, DATETIME_FORMAT, EM
import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, uniqWith } from '@/utils/commons';
import { readIndexDB, writeIndexDB } from '@/utils/indexedDB';
import dayjs from 'dayjs';
import { internalEventEmitter } from '@/utils/EventEmitterService';
export const parseHTMLString = (html, needText = false) => {
const parser = new DOMParser()
@ -152,13 +153,13 @@ export const fetchEmailBindOrderAction = async (params) => {
const todoTypes = {
// 1新订单2未读消息3需一催4需二催5需三催6未处理邮件入境提醒coli_ordertype=7余款提醒coli_ordertype=8
// 1新订单2WhatsApp未读消息3需一催4需二催5需三催6未处理邮件入境提醒coli_ordertype=7余款提醒coli_ordertype=8
1: '新订单',
2: '未读',
2: '未读WhatsApp',
3: '一催',
4: '二催',
5: '三催',
6: '未处理',
6: '老邮件',
7: '入境提醒',
8: '余款提醒',
}
@ -213,7 +214,7 @@ export const getTodoOrdersAction = async (params) => {
const orderList = errcode === 0 ? _result_unique : []
const byOPI = groupBy(orderList, 'OPI_SN')
const byState = Object.keys(byOPI).reduce((acc, key) => {
const sticky = groupBy(byOPI[key], (ele) => ([1, 2, 6].includes(ele.coli_ordertype) ? 0 : [3, 4, 5].includes(ele.coli_ordertype) ? 1 : 2))
const sticky = groupBy(byOPI[key], (ele) => ([1, 6].includes(ele.coli_ordertype) ? 0 : [2, 3, 4, 5].includes(ele.coli_ordertype) ? 1 : 2))
const treeNode = [
{
key: key + '-today',
@ -315,17 +316,56 @@ export const queryEmailListAction = async ({ opi_sn = '', pagesize = 10, last_id
if (!isEmpty(ret)) {
const listids = [...new Set(ret.map(ele => ele.MAI_SN))];
writeIndexDB([{key: cacheKey, data: listids}], 'maillist', 'mailbox')
writeIndexDB(ret.map(ele => ({ data: ele, key: ele.MAI_SN})), 'listrow', 'mailbox')
writeIndexDB(ret.map(ele => ({ data: {...ele, listKey: cacheKey }, key: ele.MAI_SN})), 'listrow', 'mailbox')
}
return ret;
}
export const EMAIL_CHANNEL_NAME = 'mailbox_changes';
let emailChangesChannel = null;
export function getEmailChangesChannel() {
if (!emailChangesChannel) {
emailChangesChannel = new BroadcastChannel(EMAIL_CHANNEL_NAME)
}
return emailChangesChannel
}
const updateEmailKeyMap = { read: 'MOI_ReadState' };
const updateEmailKeyFun = {
read: async (params) => {
const readCache = await readIndexDB(params.mai_sn_list, 'listrow', 'mailbox')
const updateField = Object.keys(params.set).reduce((a, c) => ({ ...a, [updateEmailKeyMap[c]]: params.set[c] }), {})
writeIndexDB(
params.mai_sn_list.map((ele) => ({ data: { ...(readCache.get(ele).data || {}), ...updateField }, key: ele })),
'listrow',
'mailbox',
)
const listKey = readCache.get(params.mai_sn_list[0])?.data?.listKey || '';
// 通知邮件列表数据更新
const notificationPayload = { type: 'listrow', listKey, affectKeys: params.mai_sn_list }
// - 多个tab
const channel = getEmailChangesChannel()
channel.postMessage(notificationPayload)
// - 当前tab
// console.log(`[EmailDetail] Emitted internal`, EMAIL_CHANNEL_NAME, notificationPayload);
internalEventEmitter.emit(EMAIL_CHANNEL_NAME, notificationPayload);
},
}
/**
* 更新邮件属性
*/
export const updateEmailAction = async (params = { opi_sn: 0, mai_sn_list: [], set: {} }) => {
if (isEmpty(params.mai_sn_list)) {
throw new Error('没有需要更新的邮件');
}
const { errcode, result } = await postJSON(`${API_HOST_V3}/mail_update`, params)
return errcode === 0 ? {} : result
if (errcode === 0 ) {
for (const [key, value] of Object.entries(params.set)) {
const updateFun = updateEmailKeyFun[key] || (() => {});
updateFun(params)
}
}
return errcode === 0 ? result : {}
}
/**

@ -1,11 +1,12 @@
import { useState, useEffect, useCallback } from 'react'
import { isEmpty, objectMapper, olog, } from '@/utils/commons'
import { readIndexDB } from '@/utils/indexedDB'
import { getEmailDetailAction, postResendEmailAction, getSalesSignatureAction, getEmailOrderAction, queryEmailListAction, getEmailTemplateAction, saveEmailDraftOrSendAction, updateEmailAction } from '@/actions/EmailActions'
import { getEmailDetailAction, postResendEmailAction, getSalesSignatureAction, getEmailOrderAction, queryEmailListAction, getEmailTemplateAction, saveEmailDraftOrSendAction, updateEmailAction, getEmailChangesChannel, EMAIL_CHANNEL_NAME } from '@/actions/EmailActions'
import { App } from 'antd'
import useConversationStore from '@/stores/ConversationStore';
import { msgStatusRenderMapped } from '@/channel/bubbleMsgUtils';
import { POPUP_FEATURES } from '@/config';
import { internalEventEmitter } from '@/utils/EventEmitterService';
export const useEmailSignature = (opi_sn) => {
@ -41,7 +42,7 @@ export const useEmailSignature = (opi_sn) => {
* - If `number`: 直接传递, 直接获取订单详情
* - If `false`: 不需要获取订单信息
*/
export const useEmailDetail = (mai_sn, data={}, oid=0) => {
export const useEmailDetail = (mai_sn=0, data={}, oid=0) => {
const {notification} = App.useApp()
const [loading, setLoading] = useState(false)
const [mailData, setMailData] = useState({ loading, info: { MAI_COLI_SN: 0 }, content: '', attachments: [], AttachList: [] })
@ -128,7 +129,9 @@ export const useEmailDetail = (mai_sn, data={}, oid=0) => {
try {
const { id: savedID } = await saveEmailDraftOrSendAction(body, isDraft)
setMaiSN(savedID)
refresh()
if (isDraft) {
refresh()
}
return savedID
} catch (error) {
console.error(error);
@ -151,31 +154,46 @@ export const useEmailList = (mailboxDirNode) => {
const [mailList, setMailList] = useState([])
const [error, setError] = useState(null)
const [isFreshData, setIsFreshData] = useState(false)
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [refreshTrigger, setRefreshTrigger] = useState(0)
const refresh = useCallback(() => {
setRefreshTrigger(prev => prev + 1);
}, []);
setRefreshTrigger((prev) => prev + 1)
}, [])
const { OPI_SN: opi_sn, COLI_SN, VKey, VParent, ApplyDate, OrderSourceType, IsTrue } = mailboxDirNode
const markAsRead = useCallback((sn_list) => {
updateEmailAction({
opi_sn: opi_sn,
mai_sn_list: sn_list,
set: { read: 1}
});
refresh()
}, []);
const markAsRead = useCallback(
async (sn_list) => {
await updateEmailAction({
opi_sn: opi_sn,
mai_sn_list: sn_list,
set: { read: 1 },
})
},
[VKey],
)
const markAsProcessed = useCallback(
async (sn_list) => {
await updateEmailAction({
opi_sn: opi_sn,
mai_sn_list: sn_list,
set: { processed: 1 },
})
},
[VKey],
)
const markAsProcessed = useCallback((sn_list) => {
updateEmailAction({
opi_sn: opi_sn,
mai_sn_list: sn_list,
set: { processed: 1}
});
refresh()
}, []);
const loadMailListFromCache = useCallback(async (payload) => {
const cacheKey = isEmpty(COLI_SN) ? `dir-${VKey}` : `order-${VKey}`
const readCacheIDList = await readIndexDB(cacheKey, 'maillist', 'mailbox')
if (!isEmpty(readCacheIDList)) {
const readCacheListRowsMap = await readIndexDB(readCacheIDList.data, 'listrow', 'mailbox')
const _x = readCacheIDList.data.map((ele) => readCacheListRowsMap.get(ele).data || {})
setMailList(_x)
setLoading(false)
}
}, [VKey])
const getMailList = useCallback(async () => {
console.log('getMailList', mailboxDirNode)
@ -191,15 +209,11 @@ export const useEmailList = (mailboxDirNode) => {
setError(null)
setIsFreshData(false)
const cacheKey = isEmpty(COLI_SN) ? `dir-${VKey}` : `order-${VKey}`
const readCacheIDList = await readIndexDB(cacheKey, 'maillist', 'mailbox')
if (!isEmpty(readCacheIDList)) {
const readCacheListRowsMap = await readIndexDB(readCacheIDList.data, 'listrow', 'mailbox')
const _x = readCacheIDList.data.map((ele) => readCacheListRowsMap.get(ele).data || {})
setMailList(_x)
setLoading(false)
}
// const cacheKey = isEmpty(COLI_SN) ? `dir-${VKey}` : `order-${VKey}`
try {
// 1. 先从缓存读取
await loadMailListFromCache();
// 2. 从接口获取最新的列表
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的结构
@ -217,6 +231,32 @@ export const useEmailList = (mailboxDirNode) => {
useEffect(() => {
getMailList()
// --- Setup Internal Event Listener ---
const handleInternalUpdate = (event) => {
console.log(`[useEmailList] Received internal event. `, event.detail)
if (event.detail && event.detail.type === 'listrow') {
loadMailListFromCache()
}
}
internalEventEmitter.on(EMAIL_CHANNEL_NAME, handleInternalUpdate)
// --- Setup BroadcastChannel Listener ---
const channel = getEmailChangesChannel()
const handleMessage = (event) => {
console.log(`[useEmailList] Received channel event. `, event.data)
const cacheKey = isEmpty(COLI_SN) ? `dir-${VKey}` : `order-${VKey}`
if (event.data.type === 'listrow' && cacheKey === event.data.listKey) {
// cacheKey 不相同时, 不需要更新; 邮箱目录不相同
loadMailListFromCache(event.data)
}
}
channel.addEventListener('message', handleMessage)
// Cleanup
return () => {
internalEventEmitter.off(EMAIL_CHANNEL_NAME, handleInternalUpdate)
channel.removeEventListener('message', handleMessage)
}
}, [getMailList])
return { loading, isFreshData, error, mailList, refresh, markAsRead, markAsProcessed }

@ -118,7 +118,7 @@ const emailSlice = (set, get) => ({
if (isEmpty(readCache) || isNeedRefresh) {
// > {4} 更新
const rootTree = await getRootMailboxDirAction({ opi_sn, userIdStr })
console.log('empty', opi_sn, userIdStr, isEmpty(readCache), isNeedRefresh, rootTree);
// console.log('empty', opi_sn, userIdStr, isEmpty(readCache), isNeedRefresh, rootTree);
setMailboxNestedDirsActive(rootTree)
}
return false

@ -0,0 +1,20 @@
class EventEmitterService extends EventTarget {
constructor() {
super();
// console.log('Internal EventEmitterService created.'); // For debugging
}
emit(eventName, detail) {
this.dispatchEvent(new CustomEvent(eventName, { detail }));
}
on(eventName, handler) {
this.addEventListener(eventName, handler);
}
off(eventName, handler) {
this.removeEventListener(eventName, handler);
}
}
export const internalEventEmitter = new EventEmitterService();

@ -138,10 +138,10 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
let btns = []
const showDoneBtn = mailData.info?.MAI_Direction !== 1 ? true : false
if (showDoneBtn) {
// btns.push(<Button type='text' key={'set-done'} onClick={() => { alert('todo')}} icon={<MailCheckIcon className={'text-yellow-600'} />} size='small'></Button>)
}
// const showDoneBtn = mailData.info?.MAI_Direction !== 1 ? true : false
// if (showDoneBtn) {
// btns.push(<Button type='text' key={'set-done'} onClick={() => { alert('todo')}} icon={<MailCheckIcon className={'text-yellow-600'} />} size='small'></Button>)
// }
// ``
if (showBindBtn) {
btns.push(<EmailBindFormModal key={'bind'} onBoundSuccess={() => setShowBindBtn(false)} {...{ conversationid, mai_sn, showBindBtn }} />)

@ -165,7 +165,7 @@ const MailBox = ({ mailboxDir, onMailItemClick, onSelect, ...props }) => {
<Button shape='circle' type='text' size='small' icon={<ReadOutlined />}
onClick={() => {
console.info('markAsRead: ', selectedItems.map((item) => item.MAI_SN))
markAsRead(selectedItems.map((item) => item.MAI_SN))
markAsRead(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}
/>
</Tooltip>
@ -173,7 +173,7 @@ const MailBox = ({ mailboxDir, onMailItemClick, onSelect, ...props }) => {
<Button shape='circle' type='text' size='small' icon={<MailCheckIcon />}
onClick={() => {
console.info('markAsProcessed: ', selectedItems.map((item) => item.MAI_SN))
markAsProcessed(selectedItems.map((item) => item.MAI_SN))
markAsProcessed(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}} />
</Tooltip>
<Tooltip title='刷新'>

Loading…
Cancel
Save