Compare commits
2 Commits
main
...
2.0/email-
| Author | SHA1 | Date |
|---|---|---|
|
|
31b854efab | 1 year ago |
|
|
ade510a701 | 1 year ago |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.8 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 19.9967V14.9967H10V19.9967H19V12.9967H5V19.9967H8ZM4 10.9967H20V7.9967H14V3.9967H10V7.9967H4V10.9967ZM3 20.9967V12.9967H2V6.9967C2 6.44442 2.44772 5.9967 3 5.9967H8V2.9967C8 2.44442 8.44772 1.9967 9 1.9967H15C15.5523 1.9967 16 2.44442 16 2.9967V5.9967H21C21.5523 5.9967 22 6.44442 22 6.9967V12.9967H21V20.9967C21 21.549 20.5523 21.9967 20 21.9967H4C3.44772 21.9967 3 21.549 3 20.9967Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 491 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8.58564 8.85449L3.63589 13.8042L8.83021 18.9985L9.99985 18.9978V18.9966H11.1714L14.9496 15.2184L8.58564 8.85449ZM9.99985 7.44027L16.3638 13.8042L19.1922 10.9758L12.8283 4.61185L9.99985 7.44027ZM13.9999 18.9966H20.9999V20.9966H11.9999L8.00229 20.9991L1.51457 14.5113C1.12405 14.1208 1.12405 13.4877 1.51457 13.0971L12.1212 2.49053C12.5117 2.1 13.1449 2.1 13.5354 2.49053L21.3136 10.2687C21.7041 10.6592 21.7041 11.2924 21.3136 11.6829L13.9999 18.9966Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 555 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 4H21V6H11V4ZM6 7V11H4V7H1L5 3L9 7H6ZM6 17H9L5 21L1 17H4V13H6V17ZM11 18H21V20H11V18ZM9 11H21V13H9V11Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 208 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4.9967V7.9967H19V4.9967H5ZM4 2.9967H20C20.5523 2.9967 21 3.44442 21 3.9967V8.9967C21 9.54899 20.5523 9.9967 20 9.9967H4C3.44772 9.9967 3 9.54899 3 8.9967V3.9967C3 3.44442 3.44772 2.9967 4 2.9967ZM6 11.9967H12C12.5523 11.9967 13 12.4444 13 12.9967V15.9967H14V21.9967H10V15.9967H11V13.9967H5C4.44772 13.9967 4 13.549 4 12.9967V10.9967H6V11.9967ZM17.7322 13.7289L19.5 11.9612L21.2678 13.7289C22.2441 14.7052 22.2441 16.2882 21.2678 17.2645C20.2915 18.2408 18.7085 18.2408 17.7322 17.2645C16.7559 16.2882 16.7559 14.7052 17.7322 13.7289Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 640 B |
@ -1,25 +0,0 @@
|
||||
import { fetchJSON, postForm, postJSON } from '@haina/utils-request'
|
||||
import { usingStorage } from '@/utils/usingStorage'
|
||||
|
||||
const WAI_SERVER_KEY = 'G-STR:WAI_SERVER'
|
||||
|
||||
const WAI_API_VER = '/api/v1'
|
||||
|
||||
export const postSendMsg = async (body) => {
|
||||
const { waiServer } = usingStorage(WAI_SERVER_KEY)
|
||||
// const { attaList = [], atta, content, ...bodyData } = body
|
||||
// const formData = new FormData()
|
||||
// Object.keys(bodyData).forEach(function (key) {
|
||||
// formData.append(key, bodyData[key])
|
||||
// })
|
||||
// attaList.forEach(function (item) {
|
||||
// formData.append('attachment', item)
|
||||
// })
|
||||
const { result } = await postJSON(`${waiServer}${WAI_API_VER}/messages/send`, body)
|
||||
return result
|
||||
}
|
||||
|
||||
export const fetchQRCode = (phone) => {
|
||||
const { waiServer } = usingStorage(WAI_SERVER_KEY)
|
||||
return fetchJSON(`${waiServer}${WAI_API_VER}/channels/qrcode`, { phone })
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
.logo {
|
||||
float: left;
|
||||
height: 60px;
|
||||
height: 68px;
|
||||
margin: 0 6px 0 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB |
@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 262 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.9999 18.9966H20.9999V20.9966H11.9999L8.00229 20.9991L1.51457 14.5113C1.12405 14.1208 1.12405 13.4877 1.51457 13.0971L12.1212 2.49053C12.5117 2.1 13.1449 2.1 13.5354 2.49053L21.3136 10.2687C21.7041 10.6592 21.7041 11.2924 21.3136 11.6829L13.9999 18.9966ZM15.6567 14.5113L19.1922 10.9758L12.8283 4.61185L9.29275 8.14738L15.6567 14.5113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 442 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8.58564 8.85449L3.63589 13.8042L8.83021 18.9985L9.99985 18.9978V18.9966H11.1714L14.9496 15.2184L8.58564 8.85449ZM9.99985 7.44027L16.3638 13.8042L19.1922 10.9758L12.8283 4.61185L9.99985 7.44027ZM13.9999 18.9966H20.9999V20.9966H11.9999L8.00229 20.9991L1.51457 14.5113C1.12405 14.1208 1.12405 13.4877 1.51457 13.0971L12.1212 2.49053C12.5117 2.1 13.1449 2.1 13.5354 2.49053L21.3136 10.2687C21.7041 10.6592 21.7041 11.2924 21.3136 11.6829L13.9999 18.9966Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 555 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12.6512 14.0654L11.6047 20H9.57389L10.9247 12.339L3.51465 4.92892L4.92886 3.51471L20.4852 19.0711L19.071 20.4853L12.6512 14.0654ZM11.7727 7.53009L12.0425 5.99999H10.2426L8.24257 3.99999H19.9999V5.99999H14.0733L13.4991 9.25652L11.7727 7.53009Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 347 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22 13.3414C21.3744 13.1203 20.7013 13 20 13C16.6863 13 14 15.6863 14 19C14 19.7013 14.1203 20.3744 14.3414 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V13.3414ZM12.0606 11.6829L5.64722 6.2377L4.35278 7.7623L12.0731 14.3171L19.6544 7.75616L18.3456 6.24384L12.0606 11.6829ZM21 18H24V20H21V23H19V20H16V18H19V15H21V18Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 460 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22 13H20V7.23792L12.0718 14.338L4 7.21594V19H14V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V13ZM4.51146 5L12.0619 11.662L19.501 5H4.51146ZM21 18H24V20H21V23H19V20H16V18H19V15H21V18Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 328 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22 13.3414C21.3744 13.1203 20.7013 13 20 13C16.6863 13 14 15.6863 14 19C14 19.7013 14.1203 20.3744 14.3414 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V13.3414ZM12.0606 11.6829L5.64722 6.2377L4.35278 7.7623L12.0731 14.3171L19.6544 7.75616L18.3456 6.24384L12.0606 11.6829ZM19 22L15.4645 18.4645L16.8787 17.0503L19 19.1716L22.5355 15.636L23.9497 17.0503L19 22Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 504 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22 14H20V7.23792L12.0718 14.338L4 7.21594V19H14V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V14ZM4.51146 5L12.0619 11.662L19.501 5H4.51146ZM19 22L15.4645 18.4645L16.8787 17.0503L19 19.1716L22.5355 15.636L23.9497 17.0503L19 22Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 372 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.8032 8.4928C19.4663 8.81764 20.2118 9 21 9C21.3425 9 21.6769 8.96557 22 8.89998V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H16.1C16.0344 3.32311 16 3.65753 16 4C16 5.23672 16.449 6.36857 17.1929 7.24142L12.0606 11.6829L5.64722 6.2377L4.35278 7.7623L12.0731 14.3171L18.8032 8.4928ZM21 7C19.3431 7 18 5.65685 18 4C18 2.34315 19.3431 1 21 1C22.6569 1 24 2.34315 24 4C24 5.65685 22.6569 7 21 7Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 539 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16.1 3C16.0344 3.32311 16 3.65753 16 4C16 4.34247 16.0344 4.67689 16.1 5H4.51146L12.0619 11.662L17.1098 7.14141C17.5363 7.66888 18.0679 8.10787 18.6728 8.42652L12.0718 14.338L4 7.21594V19H20V8.89998C20.3231 8.96557 20.6575 9 21 9C21.3425 9 21.6769 8.96557 22 8.89998V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H16.1ZM21 1C22.6569 1 24 2.34315 24 4C24 5.65685 22.6569 7 21 7C19.3431 7 18 5.65685 18 4C18 2.34315 19.3431 1 21 1Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 572 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4.9967V7.9967H19V4.9967H5ZM4 2.9967H20C20.5523 2.9967 21 3.44442 21 3.9967V8.9967C21 9.54899 20.5523 9.9967 20 9.9967H4C3.44772 9.9967 3 9.54899 3 8.9967V3.9967C3 3.44442 3.44772 2.9967 4 2.9967ZM6 11.9967H12C12.5523 11.9967 13 12.4444 13 12.9967V15.9967H14V21.9967H10V15.9967H11V13.9967H5C4.44772 13.9967 4 13.549 4 12.9967V10.9967H6V11.9967ZM17.7322 13.7289L19.5 11.9612L21.2678 13.7289C22.2441 14.7052 22.2441 16.2882 21.2678 17.2645C20.2915 18.2408 18.7085 18.2408 17.7322 17.2645C16.7559 16.2882 16.7559 14.7052 17.7322 13.7289Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 640 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 6V21H11V6H5V4H19V6H13Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 130 B |
@ -1,27 +0,0 @@
|
||||
import { LexicalCommand, createCommand, TextFormatType } from 'lexical';
|
||||
|
||||
export interface CopiedFormat {
|
||||
textFormatFlags: number; // 从node.getFormat()
|
||||
style: string; // 从node.getStyle()
|
||||
// todo: p 标签的样式
|
||||
}
|
||||
|
||||
export interface ActivateFormatPainterPayload {
|
||||
sticky: boolean;
|
||||
}
|
||||
|
||||
// activate the format painter and copy the current selection's format
|
||||
export const ACTIVATE_FORMAT_PAINTER_COMMAND: LexicalCommand<ActivateFormatPainterPayload> =
|
||||
createCommand('ACTIVATE_FORMAT_PAINTER_COMMAND');
|
||||
|
||||
// deactivate the format painter
|
||||
export const DEACTIVATE_FORMAT_PAINTER_COMMAND: LexicalCommand<void> =
|
||||
createCommand('DEACTIVATE_FORMAT_PAINTER_COMMAND');
|
||||
|
||||
// dispatched by the plugin to inform UI about state changes
|
||||
export interface FormatPainterState {
|
||||
isActive: boolean;
|
||||
isSticky: boolean;
|
||||
}
|
||||
export const FORMAT_PAINTER_STATE_UPDATE_COMMAND: LexicalCommand<FormatPainterState> =
|
||||
createCommand('FORMAT_PAINTER_STATE_UPDATE_COMMAND');
|
||||
@ -1,86 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor, COMMAND_PRIORITY_NORMAL } from 'lexical';
|
||||
import {
|
||||
ACTIVATE_FORMAT_PAINTER_COMMAND,
|
||||
DEACTIVATE_FORMAT_PAINTER_COMMAND,
|
||||
FORMAT_PAINTER_STATE_UPDATE_COMMAND,
|
||||
FormatPainterState,
|
||||
} from './FormatPainterCommands';
|
||||
|
||||
const PaintBrushIcon = () => <i className='format painter' />;
|
||||
|
||||
export function FormatPainterToolbarButton() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
const [canCopy, setCanCopy] = useState(false);
|
||||
|
||||
// 插件状态
|
||||
useEffect(() => {
|
||||
return editor.registerCommand<FormatPainterState>(
|
||||
FORMAT_PAINTER_STATE_UPDATE_COMMAND,
|
||||
(payload) => {
|
||||
setIsActive(payload.isActive);
|
||||
setIsSticky(payload.isSticky);
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
// 选区状态
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection) && !selection.isCollapsed()) {
|
||||
setCanCopy(true);
|
||||
} else {
|
||||
setCanCopy(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (isActive) {
|
||||
editor.dispatchCommand(DEACTIVATE_FORMAT_PAINTER_COMMAND, undefined);
|
||||
} else if (canCopy) {
|
||||
editor.dispatchCommand(ACTIVATE_FORMAT_PAINTER_COMMAND, { sticky: false });
|
||||
}
|
||||
// * !isActive and !canCopy 什么也不做
|
||||
};
|
||||
|
||||
// 双击 保持激活
|
||||
const handleDoubleClick = () => {
|
||||
if (isActive && isSticky) {
|
||||
editor.dispatchCommand(DEACTIVATE_FORMAT_PAINTER_COMMAND, undefined);
|
||||
} else if (canCopy) {
|
||||
editor.dispatchCommand(ACTIVATE_FORMAT_PAINTER_COMMAND, { sticky: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className={`toolbar-item spaced ${isActive ? 'active' : ''}`}
|
||||
// title={isActive ? (isSticky ? 'Format Painter (Sticky)' : 'Format Painter (Active)') : 'Format Painter'}
|
||||
title={'格式刷'}
|
||||
aria-label={isActive ? (isSticky ? 'Deactivate Format Painter (Sticky)' : 'Deactivate Format Painter (Active)') : 'Activate Format Painter'}
|
||||
disabled={!isActive && !canCopy}
|
||||
>
|
||||
<PaintBrushIcon />
|
||||
{/* <span style={{wordBreak: 'keep-all'}}>格式刷</span> */}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
{/* <button type='button'
|
||||
className={'toolbar-item spaced ' + (isActive ? 'active' : '')}
|
||||
aria-label='Format Painter'>
|
||||
<i className='format painter' />
|
||||
</button> */}
|
||||
export default FormatPainterToolbarButton;
|
||||
@ -1,267 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
TextFormatType,
|
||||
LexicalEditor,
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
} from 'lexical';
|
||||
import { $patchStyleText, } from '@lexical/selection';
|
||||
// $patchStyleText is more efficient for merging styles.
|
||||
|
||||
import {
|
||||
CopiedFormat,
|
||||
ACTIVATE_FORMAT_PAINTER_COMMAND,
|
||||
DEACTIVATE_FORMAT_PAINTER_COMMAND,
|
||||
FORMAT_PAINTER_STATE_UPDATE_COMMAND,
|
||||
ActivateFormatPainterPayload,
|
||||
FormatPainterState,
|
||||
} from './FormatPainterCommands';
|
||||
|
||||
// parse style string to object for $patchStyleText
|
||||
function parseStyleText(style: string): Record<string, string> {
|
||||
const styleObj: Record<string, string> = {};
|
||||
style.split(';').forEach((rule) => {
|
||||
const [key, value] = rule.split(':');
|
||||
if (key && value && key.trim() && value.trim()) {
|
||||
styleObj[key.trim()] = value.trim();
|
||||
}
|
||||
});
|
||||
return styleObj;
|
||||
}
|
||||
|
||||
// map format flags to TextFormatType
|
||||
const textFormatTypeMap: { flag: number; type: TextFormatType }[] = [
|
||||
{ flag: 1, type: 'bold' },
|
||||
{ flag: 2, type: 'italic' },
|
||||
{ flag: 4, type: 'strikethrough' },
|
||||
{ flag: 8, type: 'underline' },
|
||||
{ flag: 16, type: 'code' },
|
||||
{ flag: 32, type: 'subscript' },
|
||||
{ flag: 64, type: 'superscript' },
|
||||
];
|
||||
|
||||
export function FormatPainterPlugin(): null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
const [copiedFormat, setCopiedFormat] = useState<CopiedFormat | null>(null);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
|
||||
// 避免多次复制
|
||||
const isPickingUpRef = useRef(false);
|
||||
|
||||
const broadcastState = useCallback(() => {
|
||||
editor.dispatchCommand(FORMAT_PAINTER_STATE_UPDATE_COMMAND, {
|
||||
isActive,
|
||||
isSticky,
|
||||
});
|
||||
}, [editor, isActive, isSticky]);
|
||||
|
||||
// Update broadcast whenever state changes
|
||||
useEffect(() => {
|
||||
broadcastState();
|
||||
}, [isActive, isSticky, broadcastState]);
|
||||
|
||||
|
||||
// Activate Format Painter (Copy Format)
|
||||
useEffect(() => {
|
||||
return editor.registerCommand<ActivateFormatPainterPayload>(
|
||||
ACTIVATE_FORMAT_PAINTER_COMMAND,
|
||||
(payload) => {
|
||||
isPickingUpRef.current = true;
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection) && !selection.isCollapsed()) {
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
let formatToCopy: CopiedFormat | null = null;
|
||||
|
||||
if ($isTextNode(anchorNode)) {
|
||||
formatToCopy = {
|
||||
textFormatFlags: anchorNode.getFormat(),
|
||||
style: anchorNode.getStyle(),
|
||||
};
|
||||
} else {
|
||||
// ? todo: 从第一个字符获取格式
|
||||
const nodes = selection.getNodes();
|
||||
for (const node of nodes) {
|
||||
if ($isTextNode(node)) {
|
||||
formatToCopy = {
|
||||
textFormatFlags: node.getFormat(),
|
||||
style: node.getStyle(),
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (formatToCopy) {
|
||||
setCopiedFormat(formatToCopy);
|
||||
setIsActive(true);
|
||||
setIsSticky(payload.sticky);
|
||||
// console.log('Format Painter Activated. Sticky:', payload.sticky, 'Format:', formatToCopy);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 鼠标抬起
|
||||
setTimeout(() => { isPickingUpRef.current = false; }, 50);
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
// Deactivate Format Painter
|
||||
useEffect(() => {
|
||||
return editor.registerCommand<void>(
|
||||
DEACTIVATE_FORMAT_PAINTER_COMMAND,
|
||||
() => {
|
||||
if (!isActive) return false;
|
||||
setIsActive(false);
|
||||
setIsSticky(false);
|
||||
// 不保留
|
||||
setCopiedFormat(null);
|
||||
// console.log('Format Painter Deactivated.');
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
);
|
||||
}, [editor, isActive]);
|
||||
|
||||
|
||||
// 应用复制的格式
|
||||
const applyFormat = useCallback(() => {
|
||||
if (!isActive || !copiedFormat || isPickingUpRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection) && copiedFormat) {
|
||||
// console.log('copiedFormat:', copiedFormat, '\ntextFormatTypeMap:', textFormatTypeMap);
|
||||
// TextNode (bold, italic, ...)
|
||||
textFormatTypeMap.forEach(fmt => {
|
||||
if (copiedFormat.textFormatFlags & fmt.flag) {
|
||||
selection.formatText(fmt.type);
|
||||
} else {
|
||||
const currentSelection = $getSelection();
|
||||
if ($isRangeSelection(currentSelection)) {
|
||||
textFormatTypeMap.forEach(fmt => {
|
||||
const shouldHaveFormat = (copiedFormat.textFormatFlags & fmt.flag) > 0;
|
||||
if (currentSelection.hasFormat(fmt.type) !== shouldHaveFormat) {
|
||||
currentSelection.formatText(fmt.type);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// ensure applied
|
||||
let newSelection = $getSelection();
|
||||
if ($isRangeSelection(newSelection)) {
|
||||
textFormatTypeMap.forEach(fmt => {
|
||||
if (copiedFormat.textFormatFlags & fmt.flag) {
|
||||
if (!newSelection!.hasFormat(fmt.type)) newSelection!.formatText(fmt.type);
|
||||
} else {
|
||||
if (newSelection!.hasFormat(fmt.type)) newSelection!.formatText(fmt.type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// inline styles (font-family, color, font-size, ...)
|
||||
const stylesToApply = parseStyleText(copiedFormat.style);
|
||||
// console.log('inline style', stylesToApply);
|
||||
if (Object.keys(stylesToApply).length > 0) {
|
||||
newSelection = $getSelection();
|
||||
if ($isRangeSelection(newSelection)) {
|
||||
$patchStyleText(newSelection as any, stylesToApply);
|
||||
}
|
||||
} else {
|
||||
// 清除格式
|
||||
const selectedNodes = newSelection.getNodes();
|
||||
selectedNodes.forEach(node => {
|
||||
if ($isTextNode(node)) {
|
||||
if (node.getStyle() !== "") {
|
||||
node.setStyle(""); // 清除
|
||||
}
|
||||
}
|
||||
// todo: <p> node
|
||||
});
|
||||
}
|
||||
|
||||
// console.log('Format Applied. Sticky:', isSticky);
|
||||
|
||||
if (!isSticky) {
|
||||
setIsActive(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}, [editor, isActive, isSticky, copiedFormat]);
|
||||
|
||||
|
||||
// 鼠标抬起
|
||||
useEffect(() => {
|
||||
if (!isActive || !copiedFormat) return;
|
||||
|
||||
const editorElement = editor.getRootElement();
|
||||
if (!editorElement) return;
|
||||
|
||||
const handleMouseUp = (event: MouseEvent) => {
|
||||
if (isPickingUpRef.current) return;
|
||||
|
||||
if (editorElement.contains(event.target as Node)) {
|
||||
// todo: 改为在下一帧更新
|
||||
setTimeout(() => {
|
||||
const selection = editor.getEditorState().read($getSelection);
|
||||
if ($isRangeSelection(selection) && !selection.isCollapsed()) {
|
||||
applyFormat();
|
||||
} else if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
||||
// 折叠的选区, 也应用
|
||||
// applyFormat();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [editor, isActive, copiedFormat, applyFormat]);
|
||||
|
||||
|
||||
// 按 esc 键取消格式
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
editor.dispatchCommand(DEACTIVATE_FORMAT_PAINTER_COMMAND, undefined);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [editor, isActive]);
|
||||
|
||||
// 鼠标样式
|
||||
useEffect(() => {
|
||||
const editorElement = editor.getRootElement();
|
||||
if (editorElement) {
|
||||
editorElement.style.cursor = isActive ? 'copy' : 'auto';
|
||||
}
|
||||
return () => {
|
||||
if (editorElement) {
|
||||
editorElement.style.cursor = 'auto';
|
||||
}
|
||||
};
|
||||
}, [editor, isActive]);
|
||||
|
||||
return null;
|
||||
}
|
||||
export default FormatPainterPlugin;
|
||||
@ -1,77 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Popover, message, FloatButton, Button, Form, Input } from "antd";
|
||||
import { BugOutlined } from "@ant-design/icons";
|
||||
import useAuthStore from '@/stores/AuthStore'
|
||||
import { sendNotify } from "@/utils/pagespy";
|
||||
import { uploadPageSpyLog } from "@haina/utils-pagespy";
|
||||
|
||||
function LogUploader() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const hide = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const handleOpenChange = (newOpen) => {
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
const [loginUser] = useAuthStore((s) => [s.loginUser]);
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [formBug] = Form.useForm();
|
||||
|
||||
const popoverContent = (
|
||||
<Form
|
||||
layout={"vertical"}
|
||||
form={formBug}
|
||||
initialValues={{ problem: '' }}
|
||||
scrollToFirstError
|
||||
onFinish={async (values) => {
|
||||
const success = await uploadPageSpyLog();
|
||||
messageApi.success("Thanks for the feedback😊");
|
||||
if (success) {
|
||||
sendNotify(`${loginUser?.username}(${loginUser?.userIdStr})说:${values.problem}`);
|
||||
} else {
|
||||
sendNotify(`${loginUser?.username}(${loginUser?.userIdStr})上传日志失败`);
|
||||
}
|
||||
hide();
|
||||
formBug.setFieldsValue({problem: ''});
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="problem"
|
||||
label="Need help?"
|
||||
rules={[{ required: true, message: "Specify issue needing support." }]}
|
||||
>
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
color="cyan"
|
||||
variant="solid"
|
||||
block
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Popover
|
||||
content={popoverContent}
|
||||
trigger={["click"]}
|
||||
placement="topRight"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
fresh
|
||||
destroyOnHidden
|
||||
>
|
||||
<FloatButton icon={<BugOutlined />} tooltip={<div>上传日志给研发部</div>} />
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogUploader;
|
||||
@ -1,363 +0,0 @@
|
||||
import {
|
||||
WhatsAppOutlined,
|
||||
FileAddOutlined,
|
||||
MailOutlined,
|
||||
PhoneOutlined,
|
||||
UserOutlined,
|
||||
FieldNumberOutlined,
|
||||
CompassOutlined,
|
||||
CalendarOutlined,
|
||||
EditOutlined,
|
||||
CheckOutlined,
|
||||
CopyOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { App, Flex, Select, Tooltip, Divider, Typography, Skeleton, Checkbox, Drawer, Button, Empty, Form, Input } from 'antd'
|
||||
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions } from '@/stores/OrderStore'
|
||||
import { copy, isEmpty } from '@haina/utils-commons'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import useConversationStore from '@/stores/ConversationStore'
|
||||
import useAuthStore from '@/stores/AuthStore'
|
||||
const OrderProfile = ({ coliSN, ...props }) => {
|
||||
|
||||
const { notification, message } = App.useApp()
|
||||
const [formComment] = Form.useForm()
|
||||
const [formWhatsApp] = Form.useForm()
|
||||
const [formExtra] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [openOrderCommnet, setOpenOrderCommnet] = useState(false)
|
||||
const [openWhatsApp, setOpenWhatsApp] = useState(false)
|
||||
const [openExtra, setOpenExtra] = useState(false)
|
||||
|
||||
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions)
|
||||
orderLabelOptions.unshift({ value: 0, label: '未设置', disabled: true })
|
||||
|
||||
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions)
|
||||
const [orderDetail, customerDetail, fetchOrderDetail, setOrderPropValue,
|
||||
appendOrderComment, updateWhatsapp, updateExtraInfo, remindCheckList, updateRemindState] = useOrderStore((s) => [
|
||||
s.orderDetail,
|
||||
s.customerDetail,
|
||||
s.fetchOrderDetail,
|
||||
s.setOrderPropValue,
|
||||
s.appendOrderComment,
|
||||
s.updateWhatsapp,
|
||||
s.updateExtraInfo,
|
||||
s.remindCheckList,
|
||||
s.updateRemindState
|
||||
])
|
||||
|
||||
const loginUser = useAuthStore((state) => state.loginUser)
|
||||
const currentOrder = useConversationStore(useShallow((state) => state.currentConversation?.coli_sn || ''))
|
||||
const orderId = coliSN || currentOrder
|
||||
|
||||
useEffect(() => {
|
||||
if (orderId) {
|
||||
setLoading(true)
|
||||
fetchOrderDetail(orderId)
|
||||
.finally(() => setLoading(false))
|
||||
.catch((reason) => {
|
||||
notification.error({
|
||||
message: '查询出错',
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
}
|
||||
return () => {}
|
||||
}, [orderId])
|
||||
|
||||
const handleSetRemindState = async (checkedValue) => {
|
||||
|
||||
try {
|
||||
await updateRemindState(coliSN, checkedValue)
|
||||
message.success('设置成功')
|
||||
} catch (error) {
|
||||
notification.warning({ message: '设置失败', description: error.message, placement: 'top', duration: 60 })
|
||||
}
|
||||
}
|
||||
|
||||
const getCustomerName = () => {
|
||||
if (orderDetail.buytime > 0) return customerDetail.name + '(R' + orderDetail.buytime + ')'
|
||||
return customerDetail.name
|
||||
}
|
||||
|
||||
const getPlanStatus = () => {
|
||||
return orderDetail.DidPlan === 0 ? '未做计划' : '已做计划'
|
||||
}
|
||||
|
||||
const renderOrderDetail = () => {
|
||||
return (
|
||||
<>
|
||||
<Skeleton active loading={loading}>
|
||||
<Flex gap='small' vertical={true} justify='space-between' className='p-2'>
|
||||
<Typography.Text>
|
||||
<FieldNumberOutlined className='pr-1' />
|
||||
{orderDetail.order_no}
|
||||
<CopyOutlined onClick={() => {
|
||||
navigator.clipboard.writeText(orderDetail.order_no)
|
||||
message.success('已复制😀')
|
||||
}}/>
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<UserOutlined className=' pr-1' />
|
||||
{getCustomerName()}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<CompassOutlined className=' pr-1' />
|
||||
{orderDetail.MEI_Country}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<PhoneOutlined className=' pr-1' />
|
||||
{customerDetail.phone}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<MailOutlined className='pr-1' />
|
||||
{customerDetail.email}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<WhatsAppOutlined className='pr-1' />
|
||||
{isEmpty(customerDetail.whatsapp_phone_number) ? (
|
||||
<Button type='text' onClick={() => setOpenWhatsApp(true)} size='small'>
|
||||
设置 WhatsApp
|
||||
</Button>
|
||||
) : (
|
||||
<Link to={`/order/chat/${coliSN}`} state={{...orderDetail, coli_guest_WhatsApp: customerDetail.whatsapp_phone_number, }}>
|
||||
{customerDetail.whatsapp_phone_number}
|
||||
</Link>
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<Tooltip title='出发日期'>
|
||||
<CalendarOutlined className='pr-1' />
|
||||
{orderDetail.COLI_OrderStartDate}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<Tooltip title='计划状态'>
|
||||
<CheckOutlined className='pr-1' />
|
||||
{getPlanStatus()}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
<Divider orientation='left'>
|
||||
<Typography.Text strong>订单状态</Typography.Text>
|
||||
</Divider>
|
||||
<Flex gap='small' vertical={false} justify='space-between'>
|
||||
<Select
|
||||
className={`[&_.ant-select-selection-item]:text-gray-950`}
|
||||
key={'orderlabel'}
|
||||
size='small'
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
variant='underlined'
|
||||
onSelect={(value) => {
|
||||
setOrderPropValue(coliSN, 'orderlabel', value)
|
||||
.then(() => {
|
||||
message.success('设置成功')
|
||||
})
|
||||
.catch((reason) => {
|
||||
notification.error({
|
||||
message: '设置出错',
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
}}
|
||||
value={orderDetail.tags}
|
||||
options={orderLabelOptions}
|
||||
/>
|
||||
<Select
|
||||
className={`[&_.ant-select-selection-item]:text-gray-950`}
|
||||
key={'orderstatus'}
|
||||
size='small'
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
variant='underlined'
|
||||
onSelect={(value) => {
|
||||
setOrderPropValue(coliSN, 'orderstatus', value)
|
||||
.then(() => {
|
||||
message.success('设置成功')
|
||||
})
|
||||
.catch((reason) => {
|
||||
notification.error({
|
||||
message: '设置出错',
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
}}
|
||||
value={orderDetail.states}
|
||||
options={orderStatusOptions}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Divider orientation='left'>
|
||||
<Typography.Text strong>催信</Typography.Text>
|
||||
</Divider>
|
||||
<Checkbox.Group key='substatus' className='px-2' value={remindCheckList} options={remindStatusOptions} onChange={handleSetRemindState} />
|
||||
|
||||
<Divider orientation='left'>
|
||||
<Typography.Text strong>表单信息</Typography.Text>
|
||||
<Tooltip title='添加'>
|
||||
<FileAddOutlined
|
||||
className='pl-1'
|
||||
onClick={() => {
|
||||
setOpenOrderCommnet(true)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Divider>
|
||||
<p className='p-2 overflow-auto m-0 break-words whitespace-pre-wrap' dangerouslySetInnerHTML={{ __html: orderDetail.order_detail }}></p>
|
||||
|
||||
<Divider orientation='left'>
|
||||
<Typography.Text strong>特殊要求</Typography.Text>
|
||||
</Divider>
|
||||
<Typography.Text>{orderDetail.customer_request}</Typography.Text>
|
||||
<Divider orientation='left'>
|
||||
<Typography.Text strong>外联备注</Typography.Text>
|
||||
</Divider>
|
||||
<Typography.Text>{orderDetail.wl_memo}</Typography.Text>
|
||||
<Divider orientation='left'>
|
||||
<Typography.Text strong>附加信息</Typography.Text>
|
||||
<Tooltip title='修改'>
|
||||
<EditOutlined
|
||||
className='pl-1'
|
||||
onClick={() => {
|
||||
formExtra.setFieldsValue({ extra: orderDetail.COLI_Introduction })
|
||||
setOpenExtra(true)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Divider>
|
||||
<Typography.Text>{orderDetail.COLI_Introduction}</Typography.Text>
|
||||
</Skeleton>
|
||||
<Drawer title='添加表单信息' closable={{ 'aria-label': 'Close Button' }} onClose={() => setOpenOrderCommnet(false)} open={openOrderCommnet}>
|
||||
<Form
|
||||
layout={'vertical'}
|
||||
form={formComment}
|
||||
initialValues={{ comment: '' }}
|
||||
scrollToFirstError
|
||||
onFinish={(values) => {
|
||||
appendOrderComment(loginUser.userId, orderId, values.comment)
|
||||
.then(() => {
|
||||
notification.success({
|
||||
message: '温性提示',
|
||||
description: '添加表单信息成功',
|
||||
})
|
||||
setOpenOrderCommnet(false)
|
||||
formComment.setFieldsValue({ comment: '' })
|
||||
})
|
||||
.catch((reason) => {
|
||||
notification.error({
|
||||
message: '添加出错',
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
}}>
|
||||
<Form.Item name='comment' label='表单信息' rules={[{ required: true, message: '请输入表单信息' }]}>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
<Drawer title='设置 WhatsApp' closable={{ 'aria-label': 'Close Button' }} onClose={() => setOpenWhatsApp(false)} open={openWhatsApp}>
|
||||
<Form
|
||||
layout={'vertical'}
|
||||
form={formWhatsApp}
|
||||
initialValues={{ number: '' }}
|
||||
scrollToFirstError
|
||||
onFinish={(values) => {
|
||||
updateWhatsapp(orderId, values.number)
|
||||
.then(() => {
|
||||
notification.success({
|
||||
message: '温性提示',
|
||||
description: '设置 WhatsApp 成功',
|
||||
})
|
||||
setOpenWhatsApp(false)
|
||||
formWhatsApp.setFieldsValue({ number: '' })
|
||||
})
|
||||
.catch((reason) => {
|
||||
notification.error({
|
||||
message: '设置出错',
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
}}>
|
||||
<Form.Item name='number' label='WhatsApp' rules={[{ required: true, message: '请输入 WhatsApp 号码' }]}>
|
||||
<Input placeholder='国家代码+城市代码+电话号码' />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
<Drawer title='设置附加信息' closable={{ 'aria-label': 'Close Button' }} onClose={() => setOpenExtra(false)} open={openExtra}>
|
||||
<Form
|
||||
layout={'vertical'}
|
||||
form={formExtra}
|
||||
scrollToFirstError
|
||||
onFinish={(values) => {
|
||||
updateExtraInfo(orderId, values.extra)
|
||||
.then(() => {
|
||||
notification.success({
|
||||
message: '温性提示',
|
||||
description: '设置附加信息成功',
|
||||
})
|
||||
setOpenExtra(false)
|
||||
})
|
||||
.catch((reason) => {
|
||||
notification.error({
|
||||
message: '设置出错',
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
}}>
|
||||
<Form.Item name='extra' label='附加信息' rules={[{ required: true, message: '请输入附加信息' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderDefaultEmpty = () => {
|
||||
return (
|
||||
<Empty description={<span>没有订单关联</span>}>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
|
||||
if (orderId) {
|
||||
return renderOrderDetail()
|
||||
} else {
|
||||
return props.renderEmpty ? props.renderEmpty() : renderDefaultEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderProfile
|
||||
@ -1,41 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { fetchJSON, postForm } from '@haina/utils-request'
|
||||
import { HT3, EMAIL_HOST } from '@/config'
|
||||
import { isNotEmpty, prepareUrl } from '@haina/utils-commons'
|
||||
|
||||
export const useCustomerRelationStore = create((set, get) => ({
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
|
||||
tasksList: [],
|
||||
|
||||
fetchSearchTasks: async (data) => {
|
||||
set({ loading: true })
|
||||
const formData = new FormData()
|
||||
for (const key in data) {
|
||||
formData.append(key, data[key])
|
||||
}
|
||||
fetch(`${HT3}/customerrelation/search_tasks`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`)
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then((data) => {
|
||||
set({ tasksList: data })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Fetch error:', error)
|
||||
})
|
||||
.finally(() => {
|
||||
set({ loading: false })
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
export default useCustomerRelationStore
|
||||
@ -1,189 +0,0 @@
|
||||
import { getEmailDirAction, getMailboxCountAction, getRootMailboxDirAction, getEmailChangesChannel, EMAIL_CHANNEL_NAME } from '@/actions/EmailActions'
|
||||
import { buildTree, isEmpty, olog, sortArrayByOrder } from '@haina/utils-commons'
|
||||
import { readIndexDB, writeIndexDB, createIndexedDBStore, clean7DaysMailboxLog } from '@/utils/indexedDB';
|
||||
import { internalEventEmitter } from '@/utils/EventEmitterService';
|
||||
|
||||
/**
|
||||
* 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 }))
|
||||
},
|
||||
updateCurrentMailboxNestedDirs: (dirs) => {
|
||||
const { mailboxNestedDirsActive } = get()
|
||||
const _Map = new Map(mailboxNestedDirsActive.map((obj) => [obj.key, obj]))
|
||||
dirs.forEach((row) => {
|
||||
_Map.set(row.key, row)
|
||||
})
|
||||
// const _newValue = sortArrayByOrder(Array.from(_Map.values()), 'key', ['search-orders'])
|
||||
const _newValue = Array.from(_Map.values())
|
||||
|
||||
return set(() => ({ mailboxNestedDirsActive: _newValue }))
|
||||
},
|
||||
|
||||
mailboxActiveNode: {},
|
||||
setMailboxActiveNode: (node) => {
|
||||
return set(() => ({ mailboxActiveNode: node }))
|
||||
},
|
||||
|
||||
mailboxList: [],
|
||||
setMailboxList: (list) => {
|
||||
return set(() => ({ mailboxList: list }))
|
||||
},
|
||||
mailboxActiveMAI: 0,
|
||||
setMailboxActiveMAI: (mai) => {
|
||||
return set(() => ({ mailboxActiveMAI: mai }))
|
||||
},
|
||||
mailboxActiveCOLI: 0,
|
||||
setMailboxActiveCOLI: (coli) => {
|
||||
return set(() => ({ mailboxActiveCOLI: coli }))
|
||||
},
|
||||
|
||||
getOPIEmailDir: async (opi_sn = 0, userIdStr = '', refreshNow = false) => {
|
||||
// console.log('🌐requesting opi dir', opi_sn, typeof opi_sn)
|
||||
const { setMailboxNestedDirsActive, updateMailboxCount } = get()
|
||||
const readCache = await readIndexDB(Number(opi_sn), 'dirs', 'mailbox')
|
||||
// console.log(readCache);
|
||||
let isNeedRefresh = refreshNow
|
||||
if (!isEmpty(readCache)) {
|
||||
setMailboxNestedDirsActive(readCache?.tree || [])
|
||||
isNeedRefresh = refreshNow || Date.now() - readCache.treeTimestamp > 1 * 60 * 60 * 1000
|
||||
// isNeedRefresh = true; // test: 0
|
||||
}
|
||||
if (isEmpty(readCache) || isNeedRefresh) {
|
||||
// > {4} 更新
|
||||
const rootTree = await getRootMailboxDirAction({ opi_sn, userIdStr: String(userIdStr || opi_sn) })
|
||||
// console.log('empty', opi_sn, userIdStr, isEmpty(readCache), isNeedRefresh, rootTree);
|
||||
setMailboxNestedDirsActive(rootTree)
|
||||
} else {
|
||||
// 只更新数量
|
||||
updateMailboxCount({ opi_sn })
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新数量
|
||||
* @usage 1. 邮件列表页切换用户时
|
||||
* @usage 2. 收到新邮件推送时
|
||||
*
|
||||
*/
|
||||
updateMailboxCount: async ({ opi_sn }) => {
|
||||
// const { setMailboxNestedDirsActive } = get()
|
||||
await getMailboxCountAction({ opi_sn })
|
||||
// const readCache = await readIndexDB(Number(opi_sn), 'dirs', 'mailbox')
|
||||
// if (!isEmpty(readCache)) {
|
||||
// setMailboxNestedDirsActive(readCache?.tree || [])
|
||||
// }
|
||||
},
|
||||
|
||||
async initMailbox({ opi_sn, dei_sn, userIdStr }) {
|
||||
olog('Initialize Mailbox ---- ')
|
||||
const { currentMailboxOPI, setCurrentMailboxOPI, setCurrentMailboxDEI, getOPIEmailDir, setMailboxNestedDirsActive, } = get()
|
||||
createIndexedDBStore(['dirs', 'maillist', 'listrow', 'mailinfo', 'draft'], 'mailbox')
|
||||
setCurrentMailboxOPI(opi_sn)
|
||||
setCurrentMailboxDEI(dei_sn)
|
||||
getOPIEmailDir(opi_sn, userIdStr, true)
|
||||
|
||||
// --- Setup Internal Event Listener ---
|
||||
internalEventEmitter.on(EMAIL_CHANNEL_NAME, async (event) => {
|
||||
// console.log(`🔔Received internal event. `, event.detail)
|
||||
if (event.detail && event.detail.type === 'dirs') {
|
||||
const readCache = await readIndexDB(event.detail.key, 'dirs', 'mailbox')
|
||||
if (!isEmpty(readCache)) {
|
||||
setMailboxNestedDirsActive(readCache?.tree || [])
|
||||
}
|
||||
}
|
||||
})
|
||||
// --- Setup BroadcastChannel Listener ---
|
||||
const channel = getEmailChangesChannel()
|
||||
channel.addEventListener('message', async (event) => {
|
||||
// console.log(`📣Received channel event. `, event.data)
|
||||
if (event.data.type === 'dirs' && currentMailboxOPI === event.data.key) {
|
||||
const readCache = await readIndexDB(event.data.key, 'dirs', 'mailbox')
|
||||
if (!isEmpty(readCache)) {
|
||||
setMailboxNestedDirsActive(readCache?.tree || [])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
})
|
||||
export default emailSlice
|
||||
@ -1,21 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
const useUrlStore = create(devtools((set, get) => ({
|
||||
|
||||
drawerOpen: false,
|
||||
|
||||
openDrawer: () => {
|
||||
set(() => ({
|
||||
drawerOpen: true
|
||||
}))
|
||||
},
|
||||
|
||||
closeDrawer: () => {
|
||||
set(() => ({
|
||||
drawerOpen: false
|
||||
}))
|
||||
},
|
||||
|
||||
}), { name: 'urlStore' }))
|
||||
|
||||
export default useUrlStore
|
||||
@ -1,20 +0,0 @@
|
||||
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();
|
||||
@ -1,588 +0,0 @@
|
||||
import { isEmpty } from '@haina/utils-commons';
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 数据库版本
|
||||
* ! 每次涉及indexedDB的更新都要往上+1
|
||||
* @type {number}
|
||||
*/
|
||||
const INDEXED_DB_VERSION = 6;
|
||||
export const logWebsocket = (message, direction) => {
|
||||
var open = indexedDB.open('LogWebsocketData', INDEXED_DB_VERSION)
|
||||
open.onupgradeneeded = function () {
|
||||
var db = open.result
|
||||
// 数据库是否存在
|
||||
if (!db.objectStoreNames.contains('LogStore')) {
|
||||
var store = db.createObjectStore('LogStore', { keyPath: 'id', autoIncrement: true })
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false })
|
||||
} else {
|
||||
const logStore = open.transaction.objectStore('LogStore')
|
||||
if (!logStore.indexNames.contains('timestamp')) {
|
||||
logStore.createIndex('timestamp', 'timestamp', { unique: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
open.onsuccess = function () {
|
||||
var db = open.result
|
||||
var tx = db.transaction('LogStore', 'readwrite')
|
||||
var store = tx.objectStore('LogStore')
|
||||
store.put({ direction, message, _date: new Date().toLocaleString(), timestamp: Date.now() })
|
||||
tx.oncomplete = function () {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
};
|
||||
export const readWebsocketLog = (limit = 20) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let openRequest = indexedDB.open('LogWebsocketData')
|
||||
openRequest.onupgradeneeded = function () {
|
||||
var db = openRequest.result
|
||||
// 数据库是否存在
|
||||
if (!db.objectStoreNames.contains('LogStore')) {
|
||||
var store = db.createObjectStore('LogStore', { keyPath: 'id', autoIncrement: true })
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false })
|
||||
} else {
|
||||
const logStore = openRequest.transaction.objectStore('LogStore')
|
||||
if (!logStore.indexNames.contains('timestamp')) {
|
||||
logStore.createIndex('timestamp', 'timestamp', { unique: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
openRequest.onerror = function (e) {
|
||||
reject('Error opening database.')
|
||||
}
|
||||
openRequest.onsuccess = function (e) {
|
||||
let db = e.target.result
|
||||
// 数据库是否存在
|
||||
if (!db.objectStoreNames.contains('LogStore')) {
|
||||
resolve('Database does not exist.')
|
||||
return
|
||||
}
|
||||
let transaction = db.transaction('LogStore', 'readonly')
|
||||
let store = transaction.objectStore('LogStore')
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const clearWebsocketLog = () => {
|
||||
let openRequest = indexedDB.open('LogWebsocketData')
|
||||
openRequest.onerror = function (e) {}
|
||||
openRequest.onsuccess = function (e) {
|
||||
let db = e.target.result
|
||||
if (!db.objectStoreNames.contains('LogStore')) {
|
||||
return
|
||||
}
|
||||
let transaction = db.transaction('LogStore', 'readwrite')
|
||||
let store = transaction.objectStore('LogStore')
|
||||
// Clear the store
|
||||
let clearRequest = store.clear()
|
||||
clearRequest.onerror = function (e) {}
|
||||
clearRequest.onsuccess = function (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
export const createIndexedDBStore = (tables, database, keySet = {keyPath: 'key' }) => {
|
||||
var open = indexedDB.open(database, INDEXED_DB_VERSION)
|
||||
// console.trace('createIndexedDBStore');
|
||||
open.onupgradeneeded = function () {
|
||||
// console.log('createIndexedDBStore onupgradeneeded', database, )
|
||||
var db = open.result
|
||||
// 数据库是否存在
|
||||
for (const table of tables) {
|
||||
if (!db.objectStoreNames.contains(table)) {
|
||||
var store = db.createObjectStore(table, keySet)
|
||||
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)) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
open.onsuccess = function () {
|
||||
var db = open.result
|
||||
// 数据库是否存在
|
||||
if (!db.objectStoreNames.contains(table)) {
|
||||
console.warn(`writeIndexDB > Database does not exist.`, table);
|
||||
return
|
||||
}
|
||||
var tx = db.transaction(table, 'readwrite')
|
||||
var store = tx.objectStore(table)
|
||||
rows.forEach(row => {
|
||||
store.put({ ...row, _date: new Date().toLocaleString(), timestamp: Date.now() })
|
||||
});
|
||||
tx.oncomplete = function () {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads data from an IndexedDB object store.
|
||||
* It can read a single record by key, multiple records by an array of keys, or all records.
|
||||
*
|
||||
* @param {string|string[]|null} keys - The key(s) to read.
|
||||
* - If `string`: Reads a single record and returns the data object directly.
|
||||
* - If `string[]`: Reads multiple records and returns a Map of `rowkey` to `data` objects.
|
||||
* - If `null` or `undefined` or `empty string/array`: Reads all records and returns a Map of `rowkey` to `data` objects.
|
||||
* @param {string} table - The name of the IndexedDB object store (table).
|
||||
* @param {string} database - The name of the IndexedDB database.
|
||||
* @returns {Promise<any|Map<string, any>>} A promise that resolves with the data.
|
||||
* - Single key: Resolves with the data object or `undefined` if not found.
|
||||
* - Array of keys or All records: Resolves with a `Map` where keys are rowkeys and values are data objects.
|
||||
* The Map will be empty if no records are found.
|
||||
* - Rejects if there's an error opening the database or during the transaction.
|
||||
*/
|
||||
export const readIndexDB = (keys=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 store = openRequest.transaction.objectStore(table)
|
||||
if (!store.indexNames.contains('timestamp')) {
|
||||
store.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)) {
|
||||
console.warn(`readIndexDB > Database does not exist.`, table);
|
||||
resolve();
|
||||
return
|
||||
}
|
||||
let transaction = db.transaction(table, 'readonly')
|
||||
let store = transaction.objectStore(table)
|
||||
// read by key
|
||||
// Handle array of keys
|
||||
if (Array.isArray(keys) && keys.length > 0) {
|
||||
const promises = keys.map(key => {
|
||||
return new Promise((innerResolve) => {
|
||||
const getRequest = store.get(key);
|
||||
getRequest.onsuccess = (event) => {
|
||||
const result = event.target.result;
|
||||
if (result) {
|
||||
// console.log(`💾Found record with key ${key}:`, result);
|
||||
innerResolve([key, result]); // Resolve with [key, data] tuple
|
||||
} else {
|
||||
// console.log(`No record found with key ${key}.`);
|
||||
innerResolve(void 0); // Resolve with undefined for non-existent keys
|
||||
}
|
||||
};
|
||||
getRequest.onerror = (event) => {
|
||||
console.error(`Error getting record with key ${key}:`, event.target.error);
|
||||
innerResolve(undefined); // Resolve with undefined on error, or innerReject if you want to fail fast
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(promises)
|
||||
.then(results => {
|
||||
const resultMap = new Map();
|
||||
results.forEach(item => {
|
||||
if (item !== undefined) {
|
||||
resultMap.set(item[0], item[1]); // item[0] is key, item[1] is data
|
||||
}
|
||||
});
|
||||
resolve(resultMap);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during batch read:', error);
|
||||
reject(error); // Reject the main promise if Promise.all encounters an error
|
||||
});
|
||||
} else if (!isEmpty(keys)) { // Handle single key
|
||||
const getRequest = store.get(keys);
|
||||
getRequest.onsuccess = (event) => {
|
||||
const result = event.target.result;
|
||||
if (result) {
|
||||
// console.log(`💾Found record with key ${keys}:`, result);
|
||||
resolve(result);
|
||||
} else {
|
||||
// console.log(`No record found with key ${keys}.`);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
getRequest.onerror = (event) => {
|
||||
console.error(`Error getting record with key ${keys}:`, event.target.error);
|
||||
reject(event.target.error);
|
||||
};
|
||||
} else { // Handle read all
|
||||
const getAllRequest = store.getAll();
|
||||
getAllRequest.onsuccess = (event) => {
|
||||
const allData = event.target.result;
|
||||
const resultMap = new Map();
|
||||
if (allData && allData.length > 0) {
|
||||
allData.forEach(item => {
|
||||
resultMap.set(item.key, item);
|
||||
});
|
||||
// console.log(`💾Found all records:`, resultMap);
|
||||
resolve(resultMap);
|
||||
} else {
|
||||
// console.log(`No records found.`);
|
||||
resolve(resultMap); // Resolve with an empty Map if no records
|
||||
}
|
||||
};
|
||||
getAllRequest.onerror = (event) => {
|
||||
console.error(`Error getting all records:`, event.target.error);
|
||||
reject(event.target.error);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
};
|
||||
export const deleteIndexDBbyKey = (keys=null, table, database) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var open = indexedDB.open(database, INDEXED_DB_VERSION)
|
||||
open.onupgradeneeded = function () {
|
||||
// var db = open.result
|
||||
// // 数据库是否存在
|
||||
// if (!db.objectStoreNames.contains(table)) {
|
||||
// var store = db.createObjectStore(table, { keyPath: 'id', autoIncrement: true })
|
||||
// }
|
||||
}
|
||||
open.onsuccess = function (e) {
|
||||
let db = e.target.result
|
||||
// 数据库是否存在
|
||||
if (!db.objectStoreNames.contains(table)) {
|
||||
console.warn('deleteIndexDBbyKey > Database does not exist.', table)
|
||||
resolve();
|
||||
return
|
||||
}
|
||||
var tx = db.transaction(table, 'readwrite')
|
||||
var store = tx.objectStore(table)
|
||||
if (Array.isArray(keys) && keys.length > 0) {
|
||||
const promises = keys.map((key) => {
|
||||
return new Promise((innerResolve) => {
|
||||
const delRequest = store.delete(key)
|
||||
delRequest.onsuccess = (event) => {
|
||||
const result = event.target.result
|
||||
if (result) {
|
||||
innerResolve()
|
||||
} else {
|
||||
innerResolve(void 0) // Resolve with undefined for non-existent keys
|
||||
}
|
||||
}
|
||||
delRequest.onerror = (event) => {
|
||||
innerResolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
Promise.allSettled(promises)
|
||||
.then((results) => {
|
||||
resolve(results)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
} else if (!isEmpty(keys)) { // Handle single key
|
||||
const delRequest = store.delete(keys);
|
||||
delRequest.onsuccess = (event) => {
|
||||
const result = event.target.result;
|
||||
if (result) {
|
||||
resolve(result);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
delRequest.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
} else {
|
||||
// 删除所有
|
||||
let clearRequest = store.clear()
|
||||
clearRequest.onsuccess = function (e) {
|
||||
resolve(e.target.result)
|
||||
}
|
||||
clearRequest.onerror = function (e) {
|
||||
reject(e.target.error)
|
||||
}
|
||||
}
|
||||
tx.oncomplete = function () {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
function cleanOldData(database, storeNames=[], dateKey = 'timestamp', keySet = { keyPath: 'key' }) {
|
||||
createIndexedDBStore(storeNames, database, keySet);
|
||||
return function (daysToKeep = 7) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let deletedCount = 0
|
||||
const recordsToDelete = new Set()
|
||||
|
||||
let openRequest = indexedDB.open(database, INDEXED_DB_VERSION)
|
||||
openRequest.onupgradeneeded = function () {
|
||||
// console.log('----cleanOldData onupgradeneeded----')
|
||||
var db = openRequest.result
|
||||
storeNames.forEach(storeName => {
|
||||
// 数据库是否存在
|
||||
if (!db.objectStoreNames.contains(storeName)) {
|
||||
var store = db.createObjectStore(storeName, keySet)
|
||||
// var store = db.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true })
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false })
|
||||
} else {
|
||||
const logStore = openRequest.transaction.objectStore(storeName)
|
||||
if (!logStore.indexNames.contains('timestamp')) {
|
||||
logStore.createIndex('timestamp', 'timestamp', { unique: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
openRequest.onsuccess = function (e) {
|
||||
let db = e.target.result
|
||||
// 数据库是否存在
|
||||
// if (!db.objectStoreNames.contains(storeName)) {
|
||||
// resolve('Database does not exist.')
|
||||
// return
|
||||
// }
|
||||
|
||||
// Calculate the cutoff timestamp for "X days ago"
|
||||
const cutoffTimestamp = Date.now() - daysToKeep * 24 * 60 * 60 * 1000
|
||||
|
||||
const objectStoreNames = isEmpty(storeNames) ? db.objectStoreNames : storeNames
|
||||
|
||||
if (!isEmpty(objectStoreNames)) {
|
||||
const objectStores = Array.from(objectStoreNames).map((storeName) => db.transaction([storeName], 'readwrite').objectStore(storeName))
|
||||
|
||||
for (const objectStore of objectStores) {
|
||||
// Identify old data using the date index and primary key ID
|
||||
|
||||
if (!objectStore.indexNames.contains(`${dateKey}`)) {
|
||||
// Clear the store
|
||||
let clearRequest = objectStore.clear()
|
||||
console.log(`Cleanup complete. clear ${objectStore.name} records.`)
|
||||
resolve()
|
||||
clearRequest.onerror = function (e) {}
|
||||
clearRequest.onsuccess = function (e) {}
|
||||
return
|
||||
}
|
||||
// Get records older than 'daysToKeep' using the index
|
||||
const dateIndex = objectStore.index(`${dateKey}`)
|
||||
const dateRange = IDBKeyRange.upperBound(cutoffTimestamp, false) // Get keys < cutoffTimestamp (strictly older)
|
||||
|
||||
const dateCursorRequest = dateIndex.openCursor(dateRange)
|
||||
|
||||
dateCursorRequest.onsuccess = (event) => {
|
||||
const cursor = event.target.result
|
||||
if (cursor) {
|
||||
recordsToDelete.add(cursor.primaryKey) // Add the primary key of the record to the set
|
||||
cursor.continue()
|
||||
} else {
|
||||
const storeName = objectStore.name;
|
||||
// Delete identified data in a new transaction
|
||||
const deleteTransaction = db.transaction([storeName], 'readwrite')
|
||||
const deleteObjectStore = deleteTransaction.objectStore(storeName)
|
||||
|
||||
deleteTransaction.oncomplete = () => {
|
||||
console.log(`Cleanup complete. Deleted ${deletedCount} records in ${database}.${storeName}.`)
|
||||
resolve(deletedCount)
|
||||
}
|
||||
|
||||
deleteTransaction.onerror = (event) => {
|
||||
console.error('Deletion transaction error:', event.target.error)
|
||||
reject(event.target.error)
|
||||
}
|
||||
|
||||
// Convert Set to Array for forEach
|
||||
Array.from(recordsToDelete).forEach((key) => {
|
||||
const deleteRequest = deleteObjectStore.delete(key)
|
||||
deleteRequest.onsuccess = () => {
|
||||
deletedCount++
|
||||
}
|
||||
deleteRequest.onerror = (event) => {
|
||||
console.warn(`Failed to delete record with key ${key}:`, event.target.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dateCursorRequest.onerror = (event) => {
|
||||
console.error('Error opening date cursor for deletion:', event.target.error)
|
||||
reject(event.target.error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('cleanOldData: No data to delete.', database);
|
||||
}
|
||||
}
|
||||
openRequest.onerror = function (e) {
|
||||
reject('Error opening database:'+database, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', ['LogStore'], 'timestamp', { keyPath: 'id', autoIncrement: true });
|
||||
export const clean7DaysMailboxLog = cleanOldData('mailbox', ['dirs', 'maillist', 'listrow', 'mailinfo', 'draft']);
|
||||
|
||||
|
||||
/**
|
||||
* 缓存清除策略: 清理7天前的
|
||||
* - 每次进入
|
||||
* - 每天半夜
|
||||
*/
|
||||
|
||||
export const LAST_SCHEDULED_CLEANUP_DAY_KEY = 'lastScheduledCleanupDay'; // For tracking scheduling
|
||||
export const LAST_EXECUTED_CLEANUP_DAY_KEY = 'lastExecutedCleanupDay'; // For tracking actual execution
|
||||
let cleanupTimeoutId = null; // To store the ID of the setTimeout
|
||||
|
||||
/**
|
||||
* Determines if the cleanup needs to be scheduled for today.
|
||||
* This is based on when it was *last scheduled* to prevent re-scheduling
|
||||
* if the app was merely refreshed within the same day.
|
||||
* @returns {boolean} True if a new schedule for today is needed.
|
||||
*/
|
||||
function shouldScheduleForToday() {
|
||||
const lastScheduledDay = localStorage.getItem(LAST_SCHEDULED_CLEANUP_DAY_KEY);
|
||||
const today = new Date().toDateString(); // e.g., "Fri Jun 13 2025"
|
||||
|
||||
return !lastScheduledDay || lastScheduledDay !== today;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the cleanup was already *executed* today.
|
||||
* This is to prevent running the cleanup task multiple times in one day
|
||||
* if the app stays open past midnight or if it is refreshed.
|
||||
* @returns {boolean} True if the cleanup has not executed today.
|
||||
*/
|
||||
function hasCleanupExecutedToday() {
|
||||
const lastExecutedDay = localStorage.getItem(LAST_EXECUTED_CLEANUP_DAY_KEY);
|
||||
const today = new Date().toDateString();
|
||||
return lastExecutedDay === today;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the cleanup and updates the last execution timestamp.
|
||||
* This function is designed to be called via requestIdleCallback.
|
||||
*/
|
||||
export async function executeDailyCleanupTask() {
|
||||
// const lastExecutedDay = localStorage.getItem(LAST_EXECUTED_CLEANUP_DAY_KEY)
|
||||
const today = new Date().toDateString()
|
||||
|
||||
if (!hasCleanupExecutedToday()) {
|
||||
if ('requestIdleCallback' in window) {
|
||||
// console.log(`[${new Date().toLocaleTimeString()}] Scheduling cleanup via requestIdleCallback for execution.`);
|
||||
|
||||
requestIdleCallback(
|
||||
async (deadline) => {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] Running scheduled cleanup. Time remaining: ${deadline.timeRemaining().toFixed(2)}ms, Did timeout: ${deadline.didTimeout}`)
|
||||
try {
|
||||
await clean7DaysMailboxLog()
|
||||
await clean7DaysWebsocketLog()
|
||||
// Mark that cleanup was successfully executed for today
|
||||
localStorage.setItem(LAST_EXECUTED_CLEANUP_DAY_KEY, today)
|
||||
console.log('Daily cleanup marked as executed for today.')
|
||||
} catch (error) {
|
||||
console.error('Error during scheduled cleanup execution:', error)
|
||||
}
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
) // Give it up to 5 seconds to find idle time
|
||||
} else {
|
||||
console.warn('requestIdleCallback not supported. Executing cleanup directly (might cause jank).')
|
||||
// Fallback for very old browsers: run directly.
|
||||
try {
|
||||
await clean7DaysMailboxLog()
|
||||
await clean7DaysWebsocketLog()
|
||||
localStorage.setItem(LAST_EXECUTED_CLEANUP_DAY_KEY, today)
|
||||
console.log('Daily cleanup marked as executed for today (without rIC).')
|
||||
} catch (error) {
|
||||
console.error('Error during direct cleanup execution:', error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] Cleanup already executed today.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates or re-initiates the daily midnight cleanup scheduler.
|
||||
* This function calls itself recursively to set up the next day's schedule.
|
||||
*/
|
||||
export function setupDailyMidnightCleanupScheduler() {
|
||||
if (cleanupTimeoutId) {
|
||||
clearTimeout(cleanupTimeoutId)
|
||||
cleanupTimeoutId = null
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const midnight = new Date(now)
|
||||
|
||||
// Set to midnight (00:00:00)
|
||||
midnight.setDate(now.getDate() + 1)
|
||||
midnight.setHours(0, 0, 0, 0)
|
||||
|
||||
const msToMidnight = midnight.getTime() - now.getTime()
|
||||
|
||||
console.log(`[${new Date().toLocaleTimeString()}] Scheduling next daily cleanup at ${midnight.toLocaleTimeString()}, in ${msToMidnight / (1000 * 60 * 60)} hours.`)
|
||||
|
||||
// Set the timeout for the next midnight
|
||||
cleanupTimeoutId = setTimeout(async () => {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] Midnight trigger fired.`)
|
||||
if (!hasCleanupExecutedToday()) {
|
||||
await executeDailyCleanupTask()
|
||||
} else {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] Cleanup already executed today, skipping re-execution.`)
|
||||
}
|
||||
setupDailyMidnightCleanupScheduler()
|
||||
}, msToMidnight)
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
const persistObject = {}
|
||||
|
||||
/**
|
||||
* G-INT:USER_ID -> userId = 456
|
||||
* G-STR:LOGIN_TOKEN -> loginToken = 'E6779386E7D64DF0ADD0F97767E00D8B'
|
||||
* G-JSON:LOGIN_USER -> loginUser = { username: 'test-username' }
|
||||
*/
|
||||
export function usingStorage() {
|
||||
|
||||
const getStorage = () => {
|
||||
if (import.meta.env.DEV && window.localStorage) {
|
||||
return window.localStorage
|
||||
} else if (window.sessionStorage) {
|
||||
return window.sessionStorage
|
||||
} else {
|
||||
console.error('browser not support localStorage and sessionStorage.')
|
||||
}
|
||||
}
|
||||
|
||||
const setProperty = (key, value) => {
|
||||
const webStorage = getStorage()
|
||||
const typeAndKey = key.split(':')
|
||||
if (typeAndKey.length === 2) {
|
||||
const propName = camelCasedWords(typeAndKey[1])
|
||||
persistObject[propName] = value
|
||||
if (typeAndKey[0] === 'G-JSON') {
|
||||
webStorage.setItem(key, JSON.stringify(value))
|
||||
} else {
|
||||
webStorage.setItem(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// USER_ID -> userId
|
||||
const camelCasedWords = (string) => {
|
||||
if (typeof string !== 'string' || string.length === 0) {
|
||||
return string;
|
||||
}
|
||||
return string.split('_').map((word, index) => {
|
||||
if (index === 0) {
|
||||
return word.toLowerCase()
|
||||
} else {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||
}
|
||||
}).join('')
|
||||
}
|
||||
|
||||
if (Object.keys(persistObject).length == 0) {
|
||||
|
||||
const webStorage = getStorage()
|
||||
|
||||
for (let i = 0; i < webStorage.length; i++) {
|
||||
const key = webStorage.key(i)
|
||||
const typeAndKey = key.split(':')
|
||||
|
||||
if (typeAndKey.length === 2) {
|
||||
const value = webStorage.getItem(key)
|
||||
const propName = camelCasedWords(typeAndKey[1])
|
||||
if (typeAndKey[0] === 'G-INT') {
|
||||
persistObject[propName] = parseInt(value, 10)
|
||||
} else if (typeAndKey[0] === 'G-JSON') {
|
||||
try {
|
||||
persistObject[propName] = JSON.parse(value)
|
||||
} catch (e) {
|
||||
// 如果解析失败,保留原始字符串值
|
||||
persistObject[propName] = value
|
||||
console.error('解析 JSON 失败。')
|
||||
}
|
||||
} else {
|
||||
persistObject[propName] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...persistObject,
|
||||
setStorage: (key, value) => {
|
||||
setProperty(key, value)
|
||||
},
|
||||
clearStorage: () => {
|
||||
getStorage().clear()
|
||||
Object.assign(persistObject, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
import { POPUP_FEATURES } from '@/config'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
|
||||
const EmailContent = ({ id, content: MailContent, className='', ...props }) => {
|
||||
const [iframeHeight, setIframeHeight] = useState(5000) // Initial height
|
||||
const [content, setContent] = useState(MailContent)
|
||||
const iframeRef = useRef(null)
|
||||
const containerRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
setContent(MailContent)
|
||||
}, [MailContent])
|
||||
|
||||
const setIframeContent = (iframe, content) => {
|
||||
if (!iframe || !iframe.contentDocument) {
|
||||
console.error('Iframe not loaded or contentDocument is null')
|
||||
return
|
||||
}
|
||||
|
||||
const doc = iframe.contentDocument
|
||||
doc.open()
|
||||
// doc.write(content)
|
||||
doc.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/*overflow-y: hidden;*/
|
||||
width: 900px;
|
||||
max-width: 100%;
|
||||
}
|
||||
img {
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
img:not(a img){ cursor: pointer;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
doc.close()
|
||||
}
|
||||
|
||||
const calculateHeight = () => {
|
||||
try {
|
||||
if (iframeRef.current && iframeRef.current.contentDocument) {
|
||||
const doc = iframeRef.current.contentDocument
|
||||
const body = doc.body
|
||||
|
||||
if (body) {
|
||||
try {
|
||||
const links = doc.querySelectorAll('a')
|
||||
links.forEach((link) => {
|
||||
link.setAttribute('target', '_blank')
|
||||
})
|
||||
const imgs = doc.querySelectorAll('img:not(a img)')
|
||||
imgs.forEach((img) => {
|
||||
// open img in new tab
|
||||
img.addEventListener('click', (e) => {
|
||||
// e.preventDefault()
|
||||
img.style.cursor = 'pointer'
|
||||
window.open(img.src, img.src, POPUP_FEATURES)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
// console.error('Could not access iframe content due to Same-Origin Policy or other error:', e)
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const newHeight = Math.max(body.scrollHeight, body.offsetHeight, body.clientHeight)
|
||||
|
||||
// console.log('body.scrollHeight: ', body.scrollHeight)
|
||||
// console.log('body.offsetHeight: ', body.offsetHeight)
|
||||
// console.log('body.clientHeight: ', body.clientHeight)
|
||||
|
||||
const addMore = Math.max(Math.ceil(newHeight * 0.05), 120)
|
||||
// console.log('Calculated height:', newHeight, addMore)
|
||||
setIframeHeight(newHeight + addMore)
|
||||
})
|
||||
return
|
||||
} else {
|
||||
console.warn('iframe body is null or undefined')
|
||||
}
|
||||
} else {
|
||||
console.warn('iframeRef.current or contentDocument is null')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating height:', error)
|
||||
}
|
||||
// setIframeHeight(200)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleLoad = () => {
|
||||
calculateHeight()
|
||||
}
|
||||
|
||||
const currentIframe = iframeRef.current
|
||||
|
||||
if (currentIframe) {
|
||||
currentIframe.addEventListener('load', handleLoad)
|
||||
setIframeContent(currentIframe, content)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (currentIframe) {
|
||||
currentIframe.removeEventListener('load', handleLoad)
|
||||
}
|
||||
}
|
||||
}, [content])
|
||||
|
||||
// useEffect(() => {
|
||||
// if(iframeRef.current){
|
||||
// setIframeContent(iframeRef.current, content);
|
||||
// calculateHeight();
|
||||
// }
|
||||
// }, [content])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`space-y-4 w-full ${className}`}>
|
||||
<div className='w-full relative pt-2'>
|
||||
<iframe
|
||||
key={id}
|
||||
ref={iframeRef}
|
||||
height={iframeHeight}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: `${iframeHeight}px`,
|
||||
// border: '1px solid #e5e7eb',
|
||||
border: 'none',
|
||||
display: 'block',
|
||||
}}
|
||||
sandbox='allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms allow-top-navigation'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmailContent
|
||||
@ -1,344 +0,0 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
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, DeleteOutlined, ExpandOutlined } from '@ant-design/icons'
|
||||
import { EditIcon, MailCheckIcon, ReplyAllIcon, ReplyIcon, ResendIcon, ShareForwardIcon, SendPlaneFillIcon, InboxIcon } from '@/components/Icons'
|
||||
import { isEmpty, TagColorStyle } from '@haina/utils-commons'
|
||||
import EmailEditorPopup from '../Input/EmailEditorPopup'
|
||||
import DnDModal from '@/components/DnDModal'
|
||||
import useStyleStore from '@/stores/StyleStore'
|
||||
import { openPopup, useEmailDetail, } from '@/hooks/useEmail'
|
||||
import { EMAIL_ATTA_HOST, POPUP_FEATURES } from '@/config'
|
||||
import EmailBindFormModal from './EmailBind'
|
||||
import EmailContent from './EmailContent'
|
||||
|
||||
const extTypeMapped = {
|
||||
txt: { icon: FileTextOutlined },
|
||||
zip: { icon: FileZipOutlined, color: '#ffe78f' },
|
||||
pdf: { icon: FilePdfOutlined, color: '#ad0b00' },
|
||||
doc: { icon: FileWordOutlined, color: '#103f91' },
|
||||
docx: { icon: FileWordOutlined, color: '#103f91' },
|
||||
rtf: { icon: FileWordOutlined, color: '#103f91' },
|
||||
xls: { icon: FileExcelOutlined, color: '#0c7d0c' },
|
||||
xlsx: { icon: FileExcelOutlined, color: '#0c7d0c' },
|
||||
jpg: { icon: FileImageOutlined, color: '#1985ff' },
|
||||
jpeg: { icon: FileImageOutlined, color: '#1985ff' },
|
||||
png: { icon: FileImageOutlined, color: '#1985ff' },
|
||||
gif: { icon: FileGifOutlined, color: '#1985ff' },
|
||||
html: { icon: GlobalOutlined, color: '#1985ff' },
|
||||
htm: { icon: GlobalOutlined, color: '#1985ff' },
|
||||
default: { icon: FileOutlined }, // rtf bmp
|
||||
}
|
||||
|
||||
const FileTypeIcon = ({fileName}) => {
|
||||
const ext = fileName.split('.').pop() || 'default';
|
||||
const Com = extTypeMapped[ext]?.icon || FileOutlined;
|
||||
const color = extTypeMapped[ext]?.color || 'inherit';
|
||||
return <Com style={{ color: color }} size={36} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* @property {*} emailMsg - 邮件数据. { conversationid, actionId, order_opi, coli_sn, msgOrigin: { from, to, id, email: { subject, mai_sn, } } }
|
||||
*/
|
||||
const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, size, autoMark = false, ...props }) => {
|
||||
// console.log('emailDetail', emailMsg);
|
||||
const componentRef = useRef(null);
|
||||
const [compactBtn, setCompactBtn] = useState(size==='small');
|
||||
|
||||
useEffect(() => {
|
||||
if (componentRef.current) {
|
||||
// console.log(componentRef.current.getBoundingClientRect().width);
|
||||
setCompactBtn(componentRef.current.offsetWidth < 800);
|
||||
}
|
||||
return () => {}
|
||||
}, [])
|
||||
|
||||
// const NEW_EMAIL_CONFIG = useMemo(() => {
|
||||
// return ['new', 'edit', 'reply', 'replyall', 'forward'].reduce((a, action) => ({...a, [`${action}-${mailID || 0}`]: {
|
||||
// url: `/email/${action}/${mailID || 0}`,
|
||||
// name: `${action}-${mailID || 0}`,
|
||||
// features: POPUP_FEATURES,
|
||||
// }}), {});
|
||||
// }, [mailID]);
|
||||
|
||||
const { notification, message } = App.useApp()
|
||||
|
||||
const { conversationid, actionId, order_opi, coli_sn } = emailMsg
|
||||
const { mai_sn, id } = emailMsg.msgOrigin?.email || emailMsg.msgOrigin || {}
|
||||
// const mailID = mai_sn || id
|
||||
|
||||
// const [action, setAction] = useState('')
|
||||
|
||||
// const [openEmailEditor, setOpenEmailEditor] = useState(false)
|
||||
// const [fromEmail, setFromEmail] = useState('')
|
||||
// useEffect(() => {
|
||||
// setOpenEmailEditor(false)
|
||||
|
||||
// return () => {}
|
||||
// }, [mailID])
|
||||
|
||||
const onOpenEditor = (msgOrigin, action='reply') => {
|
||||
openPopup(`/email/${action}/${mailID || 0}`, `${action}-${mailID || 0}`)
|
||||
if (typeof props.onOpenEditor === 'function') {
|
||||
props.onOpenEditor(msgOrigin, action);
|
||||
} else {
|
||||
// const { from, to } = msgOrigin
|
||||
// setOpenEmailEditor(true)
|
||||
// setFromEmail(action === 'edit' ? from : to)
|
||||
// setAction(action)
|
||||
// // setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const { loading, mailData, orderDetail, postEmailResend } = useEmailDetail(mailID, null, false, autoMark)
|
||||
const [showBindBtn, setShowBindBtn] = useState(false)
|
||||
useEffect(() => {
|
||||
setShowBindBtn(mailID ? isEmpty(mailData.info?.MAI_COLI_SN) : false)
|
||||
return () => {}
|
||||
}, [mailID, mailData.info?.MAI_COLI_SN])
|
||||
|
||||
const handleView = async () => {
|
||||
openPopup(`/email/view/${mailID || 0}`, `view-${mailID || 0}`, { fullscreen: true })
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
if (isEmpty(mai_sn)) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await postEmailResend({ mai_sn, conversationid, actionId })
|
||||
// setOpen(false)
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: '请求失败',
|
||||
description: err.message,
|
||||
placement: 'top',
|
||||
duration: 3,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDel = async () => {
|
||||
if (isEmpty(mai_sn)) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
//
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: '请求失败',
|
||||
description: err.message,
|
||||
placement: 'top',
|
||||
duration: 3,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态, 显示操作
|
||||
* * 已保存: []
|
||||
* * 已发送: 回复, 转发
|
||||
* * 失败: 重发
|
||||
* todo: disabled 不显示
|
||||
*/
|
||||
const renderActionBtns = ({ className, ...props }) => {
|
||||
const { status } = mailData.info
|
||||
|
||||
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>)
|
||||
// }
|
||||
// 没有关联订单的邮件`绑定订单`
|
||||
if (showBindBtn) {
|
||||
btns.push(<EmailBindFormModal key={'bind'} onBoundSuccess={() => setShowBindBtn(false)} {...{ conversationid, mai_sn, showBindBtn }} />)
|
||||
btns.push(<Divider key='divider1' type='vertical' className='mx-0' />)
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'accepted':
|
||||
break
|
||||
case 'sent':
|
||||
case '': // 接收的邮件没有发送状态
|
||||
btns.push(
|
||||
<Tooltip key='reply-t' title='回复'>
|
||||
<Button key={'reply'} onClick={() => onOpenEditor(emailMsg.msgOrigin, 'reply')} size='small' type='text' icon={<ReplyIcon className='text-indigo-500' />}>
|
||||
{compactBtn ? '' : '回复'}
|
||||
</Button>
|
||||
</Tooltip>,
|
||||
)
|
||||
btns.push(
|
||||
<Tooltip key='replyall-t' title='回复全部'>
|
||||
<Button key={'replyall'} onClick={() => onOpenEditor(emailMsg.msgOrigin, 'replyall')} size='small' type='text' icon={<ReplyAllIcon className='text-indigo-500' />} >{compactBtn ? '' : '回复全部'}</Button>
|
||||
</Tooltip>,
|
||||
)
|
||||
btns.push(
|
||||
<Tooltip key='forward-t' title='转发'>
|
||||
<Button key={'forward'} onClick={() => onOpenEditor(emailMsg.msgOrigin, 'forward')} size='small' type='text' icon={<ShareForwardIcon className='text-primary' />}>{compactBtn ? '' : '转发'}</Button></Tooltip>,
|
||||
)
|
||||
break
|
||||
case 'failed':
|
||||
btns.push(
|
||||
// <Tooltip key='delete-t' title='删除'>
|
||||
// <Button key={'delete'} danger onClick={() => handleDel()} size='small' type='text' icon={<DeleteOutlined className='text-red-500' />}>{compactBtn ? '' : '删除'}</Button></Tooltip>,
|
||||
|
||||
<Tooltip key='resend-t' title='发送'>
|
||||
<Button key={'resend'} onClick={() => handleResend()} size='small' type='text' icon={<SendPlaneFillIcon className='text-orange-500' />}>{compactBtn ? '' : '发送'}</Button></Tooltip>,
|
||||
)
|
||||
btns.push(
|
||||
<Tooltip key='edit-t' title='编辑'>
|
||||
<Button
|
||||
key={'edit'}
|
||||
onClick={() => onOpenEditor({ ...(emailMsg.msgOrigin || {}), content: mailData.content }, 'edit')}
|
||||
size='small'
|
||||
type='text'
|
||||
icon={<EditIcon className='text-indigo-500' />}>{compactBtn ? '' : '编辑'}</Button></Tooltip>,
|
||||
)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
const showFullBtn = variant !== 'full'
|
||||
if (showFullBtn) {
|
||||
btns.push(<Divider key='divider2' type='vertical' className='mx-0' />);
|
||||
btns.push(
|
||||
<Button key={'view'} onClick={() => handleView()} size='small' type='text' icon={<ExpandOutlined className='text-indigo-500' />}>
|
||||
最大化
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className={`flex justify-end items-center gap-1 ${className || ''}`}>{btns}</div>
|
||||
}
|
||||
|
||||
const renderAttaList = ({ list }) => {
|
||||
return (
|
||||
<List
|
||||
dataSource={list}
|
||||
size='small'
|
||||
className='[&_.ant-list-item]:p-1 [&_.ant-list-item]:justify-start'
|
||||
renderItem={(atta) => (
|
||||
<List.Item>
|
||||
<FileTypeIcon fileName={atta.ATI_Name} />
|
||||
<Typography.Text ellipsis={{ tooltip: { title: atta.ATI_Name, placement: 'left' } }} className='text-inherit'>
|
||||
<span key={atta.ATI_SN} onClick={() => openPopup(`${EMAIL_ATTA_HOST}${encodeURIComponent(atta.ATI_ServerFile)}`, atta.ATI_SN)} size='small' className='text-blue-500 cursor-pointer'>
|
||||
{atta.ATI_Name}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
const variantCls = (variant) => {
|
||||
switch (variant) {
|
||||
case 'outline':
|
||||
return 'h-full border-y-0 border-x border-solid border-neutral-200'
|
||||
case 'full':
|
||||
return 'h-[calc(100dvh-16px)]'
|
||||
default:
|
||||
return 'h-full'
|
||||
}
|
||||
}
|
||||
|
||||
return mailID ? (
|
||||
<>
|
||||
<div ref={componentRef} className={`email-container flex flex-col gap-0 divide-y divide-neutral-200 divide-solid *:p-2 *:border-0 bg-white ${variantCls(variant)}`}>
|
||||
<div className=''>
|
||||
<div className='flex flex-wrap justify-between'>
|
||||
<span className={(mailData.info?.MAI_ReadState || 0) > 0 ? '' : ' font-bold '}>
|
||||
{loading ? <LoadingOutlined className='mr-1' /> : null}
|
||||
{mailData.info?.MAI_Subject || emailMsg?.msgOrigin?.subject}
|
||||
</span>
|
||||
{/* <ActionBtns key='actions' className={'ml-auto'} /> */}
|
||||
{renderActionBtns({ className: 'ml-auto'})}
|
||||
</div>
|
||||
<Divider className='my-2' />
|
||||
<div className={['flex flex-wrap justify-end', window.innerWidth < 800 ? 'flex-col' : 'flex-row '].join(' ')}>
|
||||
<div className=' flex-auto basis-0 flex flex-wrap gap-2 mb-2 items-center'>
|
||||
<Avatar className='' style={TagColorStyle(mailData.info?.MAI_From, true)}>
|
||||
{(mailData.info?.MAI_From || '').substring(0, 1)}
|
||||
</Avatar>
|
||||
<div className=' flex flex-col '>
|
||||
{/* <span className=' font-bold text-base'>{mailData.info?.fromName}</span> */}
|
||||
<span className='text-neutral-500 text-wrap break-words break-all '>{mailData.info?.MAI_From}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className=' ml-auto flex flex-col justify-start gap-1 items-end'>
|
||||
{/* <ActionBtns /> */}
|
||||
<div className='text-xs '>{mailData.info?.MAI_SendDate || emailMsg.localDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-sm'>
|
||||
<span className='text-neutral-500 pr-2 w-14 inline-block text-justify' style={{ textAlignLast: 'justify' }}>
|
||||
收件人
|
||||
</span>
|
||||
{mailData.info?.MAI_To}
|
||||
</div>
|
||||
{mailData.info?.MAI_CS && (
|
||||
<div className='text-sm'>
|
||||
<span className='text-neutral-500 pr-2 w-12 inline-block text-justify' style={{ textAlignLast: 'justify' }}>
|
||||
抄送
|
||||
</span>
|
||||
{mailData.info.MAI_CS}
|
||||
</div>
|
||||
)}
|
||||
{mailData.info?.bcc && (
|
||||
<div className='text-sm'>
|
||||
<span className='text-neutral-500 pr-2 w-12 inline-block text-justify' style={{ textAlignLast: 'justify' }}>
|
||||
密送
|
||||
</span>
|
||||
{mailData.info.bcc}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='overflow-auto'>
|
||||
<Flex className={` divide-gray-200 divide-solid gap-0 ${compactBtn === false ? 'divide-y-0 divide-x' : 'flex-col-reverse divide-x-0 '}`}>
|
||||
{mailData.info?.mailType === 'text/html' ? (
|
||||
<EmailContent content={mailData.content} id={mailID} key={mailID} />
|
||||
) : (
|
||||
<div className='mt-2 whitespace-pre-wrap flex-auto' dangerouslySetInnerHTML={{ __html: mailData.content }}></div>
|
||||
)}
|
||||
{mailData.AttachList.length > 0 && (
|
||||
<div className={`${compactBtn === false ? 'w-48' : 'border-b border-t-0'} overflow-hidden`}>
|
||||
{mailData.attachments.length > 0 && (
|
||||
<>
|
||||
<span> 附件 ({mailData.attachments.length})</span>
|
||||
{renderAttaList({ list: mailData.attachments || []})}
|
||||
</>
|
||||
)}
|
||||
{mailData.insideAttachments.length > 0 && <details>
|
||||
<summary>
|
||||
<span className='text-gray-500 italic'> 文内附件 ({mailData.insideAttachments.length}) 已在正文显示 </span><span className='cursor-pointer underline'>点击展开</span>
|
||||
</summary>
|
||||
{renderAttaList({ list: mailData.insideAttachments || []})}
|
||||
</details>}
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
{/* <EmailEditorPopup
|
||||
open={openEmailEditor}
|
||||
setOpen={setOpenEmailEditor}
|
||||
fromEmail={fromEmail}
|
||||
fromUser={mailData.info?.MAI_OPI_SN || order_opi}
|
||||
fromOrder={mailData.info?.MAI_COLI_SN || coli_sn}
|
||||
conversationid={conversationid}
|
||||
oid={orderDetail.order_no}
|
||||
customerDetail={orderDetail.customerDetail}
|
||||
// emailMsg={ReferEmailMsg}
|
||||
quoteid={mailID}
|
||||
// initial={{ ...initialPosition, ...initialSize }}
|
||||
mailData={void 0}
|
||||
action={action}
|
||||
key={`email-detail-inline-${action}_${mailID}`}
|
||||
/> */}
|
||||
</>
|
||||
) : (
|
||||
<Empty description={'未打开邮件'} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)
|
||||
}
|
||||
export default EmailDetailInline
|
||||
@ -1,145 +0,0 @@
|
||||
import { createContext, useEffect, useState, useRef, useMemo } from 'react'
|
||||
import { App, Button, Card, Empty, Flex, Select, Spin, Typography, Divider, Modal, List, Row, Col, Tag, Drawer, Input, Tooltip } from 'antd'
|
||||
import { CloseOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs'
|
||||
import { InboxIcon, SendPlaneFillIcon, ExpandIcon } from '@/components/Icons'
|
||||
import EmailDetailInline from './EmailDetailInline'
|
||||
import { debounce, isEmpty } from '@haina/utils-commons'
|
||||
import useConversationStore from '@/stores/ConversationStore';
|
||||
|
||||
const EmailListDrawer = ({ showExpandBtn=true, title, list: otherEmailList, currentConversationID, opi_sn, oid, emailItem: clickItem, onOpenEditor, ...props }) => {
|
||||
|
||||
const [, setEmailMsg] = useConversationStore((state) => [state.emailMsg, state.setEmailMsg]);
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedEmail, setSelectedEmail] = useState({})
|
||||
const searchInputRef = useRef(null)
|
||||
const [dataSource, setDataSource] = useState([])
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
setDataSource(otherEmailList)
|
||||
// setSelectedEmail({ MAI_SN: -1000 });
|
||||
|
||||
return () => {}
|
||||
}, [otherEmailList])
|
||||
|
||||
const onClearSearch = () => {
|
||||
setDataSource(otherEmailList)
|
||||
}
|
||||
const handleSearch = (value) => {
|
||||
if (isEmpty(value)) onClearSearch()
|
||||
const res = otherEmailList.filter((ele) => `${ele.MAI_Subject}${ele.SenderReceiver}`.toLowerCase().includes(value.toLowerCase()))
|
||||
setDataSource(res)
|
||||
}
|
||||
|
||||
const onClickEmailItem = (emailItem) => {
|
||||
const emailMsg = {
|
||||
conversationid: currentConversationID,
|
||||
order_opi: opi_sn,
|
||||
coli_sn: oid,
|
||||
id: emailItem.MAI_SN,
|
||||
MAI_SN: emailItem.MAI_SN,
|
||||
msgOrigin: {
|
||||
from: '',
|
||||
to: '',
|
||||
...(emailItem?.msgOrigin || {}),
|
||||
id: emailItem.MAI_SN,
|
||||
email: { mai_sn: emailItem.MAI_SN, subject: emailItem.MAI_Subject, id: emailItem.MAI_SN },
|
||||
subject: emailItem.MAI_Subject,
|
||||
},
|
||||
}
|
||||
console.log('emailItem', emailItem);
|
||||
setSelectedEmail(emailMsg)
|
||||
setEmailMsg(emailMsg);
|
||||
};
|
||||
|
||||
const [pageCurrent, setPageCurrent] = useState(1);
|
||||
const onChangePagination = (page, size) => {
|
||||
setPageCurrent(page)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(clickItem)) {
|
||||
const itemIndex = dataSource.findIndex((ele) => ele.MAI_SN === clickItem.MAI_SN);
|
||||
const page = Math.ceil((itemIndex+1) / 8) || 1;
|
||||
setPageCurrent(page);
|
||||
onClickEmailItem({...clickItem, ...dataSource[itemIndex]});
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
return () => {}
|
||||
}, [clickItem]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showExpandBtn ? <Button
|
||||
icon={<ExpandIcon />}
|
||||
type={'primary'}
|
||||
className='ml-2'
|
||||
ghost
|
||||
size='small'
|
||||
onClick={() => {
|
||||
setOpen(true)
|
||||
}}
|
||||
/> : null}
|
||||
<Drawer
|
||||
zIndex={3}
|
||||
mask={false}
|
||||
width={1000}
|
||||
styles={{ header: {} }}
|
||||
title={
|
||||
<>
|
||||
<Button icon={<CloseOutlined />} onClick={() => setOpen(false)} type='text' size='small' className='text-gray-500' />
|
||||
<b>{title || '邮件列表'}</b>
|
||||
<Input.Search
|
||||
className=''
|
||||
ref={searchInputRef}
|
||||
allowClear
|
||||
onClear={onClearSearch}
|
||||
onPressEnter={(e) => {
|
||||
handleSearch(e.target.value)
|
||||
return false
|
||||
}}
|
||||
onSearch={(v, e, { source }) => handleSearch(v)}
|
||||
placeholder={`输入: 标题/发件人, 回车搜索`}
|
||||
/>
|
||||
<List
|
||||
dataSource={dataSource}
|
||||
className='h-[30vh] overflow-y-auto'
|
||||
pagination={false}
|
||||
renderItem={(emailItem) => (
|
||||
<List.Item
|
||||
className={`hover:bg-stone-50 cursor-pointer !py-1 ${selectedEmail.MAI_SN === emailItem.MAI_SN ? 'bg-blue-100 font-bold ' : ''}`}
|
||||
onClick={() => onClickEmailItem(emailItem)}>
|
||||
<Flex vertical={false} wrap={false} className='w-full'>
|
||||
<div className='flex-auto ml-auto min-w-40 line-clamp-2'>
|
||||
{emailItem.Direction === '收' ? <InboxIcon className='text-indigo-500' /> : <SendPlaneFillIcon className='text-primary' />}
|
||||
{/* <Tooltip title={emailItem.MAI_Subject}> */}
|
||||
<Typography.Text>{emailItem.MAI_Subject}</Typography.Text>
|
||||
{/* </Tooltip> */}
|
||||
</div>
|
||||
<div className='ml-1 max-w-40'>
|
||||
<Typography.Text ellipsis={{ tooltip: emailItem.SenderReceiver }}>{emailItem.SenderReceiver.replaceAll('"', '')}</Typography.Text>
|
||||
</div>
|
||||
<div className='ml-1 max-w-20'>
|
||||
<Typography.Text ellipsis={{ tooltip: emailItem.MAI_SendDate }}>{dayjs(emailItem.MAI_SendDate).format('MM-DD HH:mm')}</Typography.Text>
|
||||
</div>
|
||||
</Flex>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
classNames={{ header: '!py-1 !px-2 [&_.ant-drawer-title]:font-normal [&_.ant-list-pagination]:m-0', body: '!p-1 [&_.ant-list-pagination]:ms-1' }}
|
||||
placement='right'
|
||||
closeIcon={null}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
open={open}>
|
||||
<EmailDetailInline {...{ mailID: selectedEmail.MAI_SN, emailMsg: selectedEmail, onOpenEditor }} />
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default EmailListDrawer
|
||||
@ -1,93 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { App, Button } from 'antd'
|
||||
import EmailEditorPopup from '../Input/EmailEditorPopup'
|
||||
import { useOrderStore } from '@/stores/OrderStore'
|
||||
import useAuthStore from '@/stores/AuthStore'
|
||||
import useConversationStore from '@/stores/ConversationStore'
|
||||
import { getEmailQuotationDraftAction } from '@/actions/EmailActions'
|
||||
import { isEmpty } from '@haina/utils-commons'
|
||||
|
||||
const EmailQuotation = ({ sfi_sn, ...props }) => {
|
||||
const {notification} = App.useApp()
|
||||
const currentConversation = useConversationStore((state) => state.currentConversation)
|
||||
const { userId, username, emailList, email } = useAuthStore((state) => state.loginUser)
|
||||
const [orderDetail, customerDetail] = useOrderStore((s) => [s.orderDetail, s.customerDetail])
|
||||
|
||||
const emailListOption = emailList?.map((ele) => ({ ...ele, label: ele.email, key: ele.email, value: ele.email })) || []
|
||||
const emailListAddrMapped = emailListOption?.reduce((r, v) => ({ ...r, [v.email]: v }), {})
|
||||
|
||||
const [pickEmail, setPickEmail] = useState({ key: email, email })
|
||||
|
||||
useEffect(() => {
|
||||
const order_opi = Number(orderDetail?.opi_sn || userId)
|
||||
|
||||
const find =
|
||||
emailListOption?.find((ele) => ele.opi_sn === order_opi && ele.default === true) ||
|
||||
emailListOption?.find((ele) => ele.opi_sn === order_opi && ele.backup === true) ||
|
||||
emailListOption?.find((ele) => ele.opi_sn === order_opi) ||
|
||||
emailListOption?.find((ele) => ele.default === true) ||
|
||||
emailListOption?.find((ele) => ele.backup === true) ||
|
||||
emailListOption[0]
|
||||
setPickEmail(find)
|
||||
return () => {}
|
||||
}, [orderDetail])
|
||||
|
||||
const [draftLoading, setDraftLoading] = useState(false)
|
||||
const [draft, setDraft] = useState({})
|
||||
const getEmailDraft = async ({ sfi_sn, coli_sn, lgc = 1 }) => {
|
||||
if (isEmpty(sfi_sn)) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
setDraftLoading(true)
|
||||
const data = await getEmailQuotationDraftAction({ sfi_sn, coli_sn, lgc })
|
||||
setDraft(data)
|
||||
setDraftLoading(false)
|
||||
} catch (err) {
|
||||
setDraftLoading(false)
|
||||
notification.error({
|
||||
message: '请求失败',
|
||||
description: err.message || '网络异常',
|
||||
placement: 'top',
|
||||
duration: 3,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const [editorOpen, setEditorOpen] = useState(false)
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button loading={draftLoading}
|
||||
type='link'
|
||||
key={'email-now'} size='small'
|
||||
onClick={async () => {
|
||||
await getEmailDraft({ sfi_sn, coli_sn: currentConversation.coli_sn })
|
||||
setEditorOpen(true)
|
||||
}}>
|
||||
邮件
|
||||
</Button>
|
||||
|
||||
{/* <EmailEditorPopup
|
||||
open={editorOpen}
|
||||
setOpen={setEditorOpen}
|
||||
fromEmail={pickEmail.key}
|
||||
fromUser={orderDetail.opi_sn}
|
||||
toEmail={currentConversation?.channels?.email || customerDetail?.email}
|
||||
fromOrder={currentConversation.coli_sn}
|
||||
oid={orderDetail?.order_no}
|
||||
conversationid={currentConversation.sn}
|
||||
// emailMsg={ReferEmailMsg}
|
||||
// quoteid={mailID}
|
||||
// initial={{ ...initialPosition, ...initialSize }}
|
||||
// mailData={mailData}
|
||||
draft={draft}
|
||||
// customerDetail={customerDetail}
|
||||
action={'new'}
|
||||
key={`email-quotation-new-popup_${currentConversation.sn}`}
|
||||
/> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default EmailQuotation
|
||||
@ -1,14 +0,0 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { Drawer } from 'antd'
|
||||
import SnippetList from '@/views/accounts/SnippetList'
|
||||
import useSnippetStore from '@/stores/SnippetStore'
|
||||
|
||||
const GenerateAutoDocDrawer = ({ ...props }) => {
|
||||
const [openSnippetDrawer, closeSnippetDrawer, snippetDrawerOpen] = useSnippetStore((state) => [state.openDrawer, state.closeDrawer, state.drawerOpen])
|
||||
return (
|
||||
<Drawer title='图文集' placement={'top'} size={'large'} onClose={() => closeSnippetDrawer()} open={snippetDrawerOpen}>
|
||||
<SnippetList></SnippetList>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
export default GenerateAutoDocDrawer
|
||||
@ -1,14 +0,0 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { Drawer } from 'antd'
|
||||
import { useOrderStore } from '@/stores/OrderStore'
|
||||
import GeneratePayment from '@/views/accounts/GeneratePayment'
|
||||
|
||||
const GeneratePaymentDrawer = ({ ...props }) => {
|
||||
const [openPaymentDrawer, closePaymentDrawer, paymentDrawerOpen] = useOrderStore((state) => [state.openDrawer, state.closeDrawer, state.drawerOpen])
|
||||
return (
|
||||
<Drawer title='支付链接' placement={'top'} size={'large'} onClose={() => closePaymentDrawer()} open={paymentDrawerOpen}>
|
||||
<GeneratePayment></GeneratePayment>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
export default GeneratePaymentDrawer
|
||||
@ -1,14 +0,0 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { Drawer } from 'antd'
|
||||
import useUrlStore from '@/stores/UrlStore'
|
||||
import ShorturlConversion from '@/views/accounts/ShorturlConversion'
|
||||
|
||||
const GenerateShorturlDrawer = ({ ...props }) => {
|
||||
const [openShorturlDrawer, closeShorturlDrawer, shorturlDrawerOpen] = useUrlStore((state) => [state.openDrawer, state.closeDrawer, state.drawerOpen])
|
||||
return (
|
||||
<Drawer title='短链接转换' placement={'top'} onClose={() => closeShorturlDrawer()} open={shorturlDrawerOpen}>
|
||||
<ShorturlConversion/>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
export default GenerateShorturlDrawer
|
||||