feat: 邮箱文件夹

dev/ckeditor
Lei OT 4 months ago
parent 6bc5faa3f8
commit ab3c763238

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22 20V7L20 3H4L2 7.00353V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20ZM4 9H20V19H4V9ZM5.236 5H18.764L19.764 7H4.237L5.236 5ZM15 11H9V13H15V11Z"></path></svg>

After

Width:  |  Height:  |  Size: 262 B

@ -16,6 +16,7 @@ import MailDownloadLineSVG from '@/assets/icons/mail-download-line.svg?react';
import MailAddLineSVG from '@/assets/icons/mail-add-line.svg?react';
import MailCheckSVG from '@/assets/icons/mail-check-line.svg?react';
import MailUnreadSVG from '@/assets/icons/mail-unread-line.svg?react';
import MailArchiveSVG from '@/assets/icons/archive-2-line.svg?react';
import TextSVG from '@/assets/icons/text.svg?react';
@ -34,6 +35,7 @@ export const MailDownloadIcon = (props) => <Icon component={MailDownloadLineSVG}
export const MailAddloadIcon = (props) => <Icon component={MailAddLineSVG} {...props} />;
export const MailCheckIcon = (props) => <Icon component={MailCheckSVG} {...props} />;
export const MailUnreadIcon = (props) => <Icon component={MailUnreadSVG} {...props} />;
export const MailArchiveIcon = (props) => <Icon component={MailArchiveSVG} {...props} />;
export const TextIcon = (props) => <Icon component={TextSVG} {...props} />;

@ -2,7 +2,7 @@ import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@/utils/request'
import { API_HOST, EMAIL_HOST } from '@/config'
import { isNotEmpty, prepareUrl } from '@/utils/commons'
import { isNotEmpty, prepareUrl, uniqWith } from '@/utils/commons'
const initialState = {
orderList: [],
@ -45,8 +45,10 @@ export const useOrderStore = create(devtools((set, get) => ({
return fetchJSON(fetchOrderUrl, params)
.then(json => {
if (json.errcode === 0) {
const _result = json.result.map((order) => { return { ...order, key: order.COLI_ID } })
const _result_unique = uniqWith(_result, (a, b) => a.COLI_SN === b.COLI_SN)
set(() => ({
orderList: json.result.map((order) => { return { ...order, key: order.COLI_ID } }),
orderList: _result_unique,
}))
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)

@ -592,6 +592,66 @@ export const TagColorStyle = (tag, outerStyle = false) => {
return { color: `${color}`, ...outerStyleObj };
};
// 数组去掉重复
export function unique(arr) {
const x = new Set(arr);
return [...x];
}
export const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findIndex((step) => fn(element, step)) === index);
/**
* Creates a new tree node object.
* @param {string} key - The unique identifier for the node.
* @param {string} name - The display name of the node.
* @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={}) {
return {
key: key,
title: name,
parent: parent,
icon: _raw?.icon,
_raw: _raw,
children: []
};
}
/**
* Builds a tree structure from a flat list of nodes.
* @returns {Array<object>} An array of root tree nodes.
*/
export const buildTree = (list, keyMap={}) => {
if (!list || list.length === 0) {
return []
}
const nodeMap = new Map()
const treeRoots = []
list.forEach((item) => {
const node = createTreeNode(item[keyMap.key], item[keyMap.name], item[keyMap.parent], item)
nodeMap.set(item[keyMap.key], node)
})
list.forEach((item) => {
const node = nodeMap.get(item[keyMap.key])
if (item[keyMap.parent] === keyMap.rootKey || item[keyMap.parent] === 0 || item[keyMap.parent] === 1 || item[keyMap.parent] === null || item[keyMap.parent] === undefined) {
// This is a root node
treeRoots.push(node)
} else {
const parentNode = nodeMap.get(item[keyMap.parent])
if (parentNode) {
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.`)
treeRoots.push(node)
}
}
})
return treeRoots
}
/**
*
*/
@ -835,3 +895,4 @@ function cleanOldData(database, storeName, dateKey = 'timestamp') {
}
export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', 'LogStore');

@ -1,88 +1,44 @@
import { Conditional } from '@/components/Conditional'
import useAuthStore from '@/stores/AuthStore'
import { PERM_IMPORT_EMAIL } from '@/stores/AuthStore'
import useFormStore from '@/stores/FormStore'
import { useOrderStore } from '@/stores/OrderStore'
import { copy, isNotEmpty, isEmpty, groupBy, cloneDeep, sortBy, sortArrayByOrder } from '@/utils/commons'
import {
WhatsAppOutlined,
ImportOutlined,
FileAddOutlined,
StarTwoTone,
StarOutlined,
MailOutlined,
PhoneOutlined,
UserOutlined,
FieldNumberOutlined,
SaveOutlined,
PlusOutlined,
SearchOutlined,
ReloadOutlined,
ReadOutlined,
CompassOutlined,
CheckSquareTwoTone,
CarryOutTwoTone,
CheckSquareOutlined,
MailTwoTone,
BookTwoTone,
RightSquareTwoTone,
SwitcherTwoTone,
FolderTwoTone,
CalendarTwoTone,
CalendarOutlined,
HeartTwoTone,
MoneyCollectTwoTone, FolderOutlined, DeleteOutlined,
SendOutlined,
ClockCircleOutlined,
FormOutlined,
} from '@ant-design/icons'
import {
App,
Badge,
Empty,
Flex,
Button,
Drawer,
Space,
Radio,
Table,
Tabs,
Divider,
Tag,
Tooltip,
List,
Dropdown,
Segmented,
Tree,
Typography,
Input,
Descriptions,
Checkbox,
Layout,
Row,
Col,
} from 'antd'
import dayjs from 'dayjs'
import { useCallback, useEffect, useState } from 'react'
import { InboxIcon, MailCheckIcon, MailUnreadIcon, SendPlaneFillIcon } from '@/components/Icons'
import { Link } from 'react-router-dom'
import { isEmpty, groupBy, buildTree } 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 { useEffect, useMemo, useState } from 'react'
import { InboxIcon, MailUnreadIcon, SendPlaneFillIcon } from '@/components/Icons'
import { useShallow } from 'zustand/react/shallow'
import { UNREAD_MARK } from '@/actions/ConversationActions'
import AdvanceSearchForm from './AdvanceSearchForm'
import EmailDetailInline from '../Conversations/Online/Components/EmailDetailInline'
import { getEmailDirAction } from '@/actions/EmailActions'
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: InboxIcon, color: '', className: 'text-indigo-500' },
'2': { component: MailUnreadIcon, color: '', className: 'text-indigo-500' },
'3': { component: SendPlaneFillIcon, color: '', className: 'text-primary' },
'4': { component: ClockCircleOutlined, color: '', className: 'text-yellow-500' },
'5': { component: FormOutlined, color: '', className: 'text-blue-500' },
'6': { component: DeleteOutlined, color: '', className: 'text-red-500' },
'7': { component: MailCheckIcon, color: '', className: 'text-yellow-600' },
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 /
}
const todoTypes = {
// 123456coli_ordertype=7coli_ordertype=8
1: '新订单',
2: '未读',
3: '一催',
4: '二催',
5: '三催',
6: '未处理',
7: '入境提醒',
8: '余款提醒',
}
const deptMap = new Map([
@ -118,8 +74,6 @@ const deptMap = new Map([
['35', 'newsletter营销'],
])
function OrderGroupTable({ formValues }) {}
function Follow() {
const orderList = useOrderStore((state) => state.orderList)
const fetchOrderList = useOrderStore((state) => state.fetchOrderList)
@ -127,118 +81,187 @@ function Follow() {
const [openOrder, setOpenOrder] = useState(false)
const [collapsed, setCollapsed] = useState(false)
const [formValues, setFormValues] = useFormStore(useShallow((state) => [state.orderFollowForm, state.setOrderFollowForm]))
const [advanceChecked, toggleAdvance] = useFormStore(useShallow((state) => [state.orderFollowAdvanceChecked, state.setOrderFollowAdvanceChecked]))
const batchImportEmailMessage = useOrderStore((state) => state.batchImportEmailMessage)
const [loginUser, isPermitted] = useAuthStore((state) => [state.loginUser, state.isPermitted])
const handleImportEmail = useCallback(() => {
batchImportEmailMessage()
}, [])
const handleSubmit = useCallback((values) => {
setFormValues({ ...values, type: 'advance' })
}, [])
const { accountList } = loginUser
const accountListDEIMapped = useMemo(() => accountList.reduce((a, c) => ({ ...a, [c.OPI_DEI_SN]: c }), {}), [accountList])
const accountDEI = useMemo(() => {
return accountList.map((ele) => ({ key: ele.OPI_DEI_SN, value: ele.OPI_DEI_SN, label: deptMap.get(`${ele.OPI_DEI_SN}`) }))
}, [accountList])
const defaultStickyTree = useMemo(() => {
return accountList.reduce(
(a, ele) => ({
...a,
[ele.OPI_DEI_SN]: [
{
title: '今日任务',
key: ele.OPI_DEI_SN + '-today',
getMails: false,
icon: <StarTwoTone />,
children: [],
},
{
title: '待办任务',
key: ele.OPI_DEI_SN + '-todo',
getMails: false,
icon: <CalendarTwoTone />,
children: [],
},
],
}),
{},
)
}, [accountList])
const [activeEmailId, setActiveEmailId] = useState(0)
const [mailboxDir, setMailboxDir] = useState([]);
const [mailboxDir, setMailboxDir] = useState([])
const DirTypeIcon = ({ type }) => {
const Icon = EmailDirTypeIcons[type || '0'].component
const className = EmailDirTypeIcons[type || '0'].className
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) => {
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 mailboxSort = x //.sort(sortBy('MDR_Order'));
const dirs = mailboxSort.map((ele) => {
return { ...ele, key: ele.MDR_SN, title: ele.MDR_Name, icon: <DirTypeIcon type={ele.MDR_Type} /> }
return { ...ele, icon: ele.ImageIndex !== void 0 ? <DirTypeIcon type={ele.ImageIndex} /> : false }
})
setMailboxDir(dirs)
let tree = buildTree(dirs, { key: 'VKey', parent: 'VParent', name: 'VName', rootKey: 1 })
tree = tree.filter((ele) => ele.key !== 1)
// console.log(tree)
setMailboxDir(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
const x = await queryEmailListAction({ opi_sn, query, order })
}
const [deiStickyTree, setDeiStickyTree] = useState({})
const [stickyTree, setStickyTree] = useState([])
const [expandTree, setExpandTree] = useState([])
const [activeAccount, setActiveAccount] = useState()
const handleSwitchAccount = (value) => {
setActiveAccount(value)
setStickyTree(deiStickyTree[value] || [])
setExpandTree([`${value}-today`, `${value}-todo`])
const opi = accountListDEIMapped[value].OPI_SN
getOPIEmailDir(opi)
}
const handleTreeSelectGetMails = (selectedKeys, { node }) => {
// console.info('selectedKeys: ', 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 } })
getMailList({ order: { coli_sn: node?.COLI_SN || node?._raw?.COLI_SN, order_source_type: node?._raw?.OrderSourceType || 227001, vkey: node.key } })
} 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] } })
}
}
const [stickyTreeData, setStickyTreeData] = useState([{ title: '今日任务', key: 'today' }, { title: '待办任务', key: 'todo' }]);
const [deiStickyTree, setDeiStickyTree] = useState({});
useEffect(() => {
fetchOrderList({ type: 'today' }, loginUser)
getOPIEmailDir();
getOPIEmailDir()
return () => {}
}, [])
// 123456coli_ordertype=7coli_ordertype=8
useEffect(() => {
const byDEI = groupBy(orderList, 'OPI_DEI_SN');
const byDEI = groupBy(orderList, 'OPI_DEI_SN')
// console.log(byDEI, 'byDEI')
const byState = Object.keys(byDEI).reduce((acc, key) => {
// const stickyIndex0 = byDEI[key].filter(ele => [1, 2, 6].includes(ele.COLI_StateCode))
// const stickyIndex1 = byDEI[key].filter(ele => ![1, 2, 6].includes(ele.COLI_StateCode))
const sticky = groupBy(byDEI[key], ele => [1, 2, 6].includes(ele.COLI_StateCode) ? 0 : 1);
const sticky = groupBy(byDEI[key], (ele) => ([1, 2, 6].includes(ele.coli_ordertype) ? 0 : 1))
// const sticky = groupBy(byDEI[key], ele => [1, 2, 6].includes(ele.COLI_StateCode) ? 'today' : 'todo');
// console.log(sticky, ';;;;');
const deiName = deptMap.get(`${key}`);
const treeNode = [{ title: '今日任务', key: 'today', icon: <StarTwoTone />,children: (sticky[0]||[]).map(o => ({...o, key: o.COLI_SN, title: `(${o.COLI_State})${o.COLI_ID}`})) }, { title: '待办任务', key: 'todo', icon: <CalendarTwoTone />,children: (sticky[1] || []).map(o => ({...o, key: o.COLI_SN, title: `(${o.COLI_State})${o.COLI_ID}`})) }];
// const deiName = deptMap.get(`${key}`);
const treeNode = [
{
title: '今日任务',
key: key + '-today',
getMails: false,
icon: <StarTwoTone />,
children: (sticky[0] || []).map((o) => ({ ...o, key: o.COLI_SN, title: `(${todoTypes[o.coli_ordertype] || o.COLI_State}) ${o.COLI_ID}` })),
},
{
title: '待办任务',
key: key + '-todo',
getMails: false,
icon: <CalendarTwoTone />,
children: (sticky[1] || []).map((o) => ({ ...o, key: o.COLI_SN, title: `(${todoTypes[o.coli_ordertype] || o.COLI_State}) ${o.COLI_ID}` })),
},
]
// { key, title: deiName, children: sticky[0] };
return {...acc, [key]: treeNode};
}, {});
setDeiStickyTree(byState);
console.log(byState, 'byState')
return { ...acc, [key]: treeNode }
}, defaultStickyTree)
setDeiStickyTree(byState)
const first = accountDEI[0].value
setExpandTree([`${first}-today`, `${first}-todo`])
setStickyTree(byState[first] || [])
return () => {
}
return () => {}
}, [orderList])
return (
<>
<Layout>
<Layout.Sider
width='300'
style={{
backgroundColor: '#fff',
}}>
<Flex justify='start' align='start' vertical>
<Segmented className='w-full' block shape='round' options={['AH', 'CH', 'GH']} />
<Tree blockNode
showIcon
showLine
onSelect={(selectedKeys, e) => {
console.info('selectedKeys: ', selectedKeys)
}}
defaultExpandedKeys={['0-0-today-task', '0-1-todo-list']}
defaultSelectedKeys={['0-0-0']}
treeData={[...deiStickyTree['28']||deiStickyTree['30']||[], ...mailboxDir]
}
/>
<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} />
<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
onSelect={handleTreeSelectGetMails}
onExpand={(expandedKeys) => setExpandTree(expandedKeys)}
expandedKeys={expandTree}
defaultExpandedKeys={expandTree}
defaultSelectedKeys={['today']}
treeData={[...(stickyTree || []), ...mailboxDir]}
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 />
{/* },
onClick={() => console.info('item: ', item)}
title={<a href={item.href} onClick={() => setSubject(item.title)}>{item.title}</a>} */}
<Mailbox />
</Col>
<Col span={10} style={{ height: 'calc(100vh - 166px)' }}>
<EmailDetailInline mailID={activeEmailId || 5291957} emailMsg={{}} variant={'outline'} size={'small'} />
</Col>
</Row>
<Col span={10} style={{ height: 'calc(100vh - 166px)' }}>
<EmailDetailInline mailID={activeEmailId || 5291957} emailMsg={{}} variant={'outline'} size={'small'} />
</Col>
</Row>
</Layout.Content>
<Layout.Sider zeroWidthTriggerStyle={{top: '30px'}} width='280' style={{
backgroundColor: '#fff',
}} collapsible collapsed={collapsed} onCollapse={value => setCollapsed(value)} collapsedWidth={0} reverseArrow={true}>
<OrderProfile/>
</Layout.Sider>
<Layout.Sider
zeroWidthTriggerStyle={{ top: '30px' }}
width='280'
style={{
backgroundColor: '#fff',
}}
collapsible
collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)}
collapsedWidth={0}
reverseArrow={true}>
<OrderProfile />
</Layout.Sider>
</Layout>
<Drawer

Loading…
Cancel
Save