Compare commits

..

No commits in common. 'main' and 'dev/2025a' have entirely different histories.

31
.gitignore vendored

@ -10,11 +10,11 @@ lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
distTest distTest
dev-dist dev-dist
tmp tmp
schema* schema*
.gitkeep .gitkeep
# Editor directories and files # Editor directories and files
@ -29,14 +29,13 @@ schema*
*.sw? *.sw?
/package-lock.json /package-lock.json
pnpm-lock.yaml
**/LexicalEditor0
**/LexicalEditor0
*.zip
*.zip
.env.*
.env.*
vonage-client*
vonage-client* **/test
**/test *.bak
*.bak

@ -1,18 +1,3 @@
## 查找使用邮件功能人数
select group_concat(opi_sn_value separator ''',''') as 'opi_list' from (
SELECT
SUBSTRING_INDEX(SUBSTRING_INDEX(request_uri, 'opi_sn=', -1), '&', 1) AS opi_sn_value,
COUNT(*) AS count
FROM log_message
WHERE request_uri LIKE '%/v3/dir_count%'
GROUP BY opi_sn_value
) a
select DISTINCT OPI_RealName from V_Operator_Info voi
where OPI2_OPI_SN in ('513','5130','466','621','404','622','383','609','510','512','582','586','633','0','415','639','641','640','577','654','601','602','599','535','568','496','648','691','690','525','540','626','162','634','487','585','594','628','611','624','674','637','522','676','606','631','451','551','489','583','495','503','719','698','644','605','587','588','589','509','552','526','227','501','515','581','216','575','484','687','679','370','580','490','617','618','619','261','600','603','604','114','579','481','387','629','354','492','632','414','660','574','79','486','663','391','584','482','252','264','265','376','453','649','651','650','210','212','343','565','143','591','590','328','476','593','514','576','595','536','543','564','178','528','541','625','119','571','598','573','332','413','155','330','627','550','742','612','444','360','519','421','146','553','441')
## 查找出掉线的 WhatsApp ## 查找出掉线的 WhatsApp
select * select *
from whatsapp_individual.connections from whatsapp_individual.connections

@ -1,7 +1,7 @@
{ {
"name": "global-sales", "name": "global-sales",
"private": true, "private": true,
"version": "1.6.7", "version": "1.4.10",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -10,46 +10,31 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.0.0",
"@dckj/react-better-modal": "^0.1.2", "@dckj/react-better-modal": "^0.1.2",
"@haina/utils-commons": "https://research.hainatravel.com/npm/utils-commons-0.1.2.tgz", "@lexical/react": "^0.20.0",
"@haina/utils-pagespy": "https://research.hainatravel.com/npm/utils-pagespy-0.1.2.tgz",
"@haina/utils-request": "https://research.hainatravel.com/npm/utils-request-0.1.2.tgz",
"@lexical/code": "^0.34.0",
"@lexical/hashtag": "^0.34.0",
"@lexical/html": "^0.34.0",
"@lexical/link": "^0.34.0",
"@lexical/list": "^0.34.0",
"@lexical/markdown": "^0.34.0",
"@lexical/react": "^0.34.0",
"@lexical/rich-text": "^0.34.0",
"@lexical/selection": "^0.34.0",
"@lexical/table": "^0.34.0",
"@lexical/utils": "^0.34.0",
"@vonage/client-sdk": "^2.0.0", "@vonage/client-sdk": "^2.0.0",
"antd": "^5.25.2", "antd": "^5.25.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dingtalk-jsapi": "^3.0.41", "dingtalk-jsapi": "^3.0.41",
"emoji-picker-react": "^4.12.0", "emoji-picker-react": "^4.12.0",
"lexical": "^0.34.0", "lexical": "^0.20.0",
"prismjs": "^1.30.0",
"prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-chat-elements": "^12.0.18", "react-chat-elements": "^12.0.17",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.30.1", "react-router-dom": "^6.30.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"workbox-window": "^7.3.0",
"zustand": "^4.5.7" "zustand": "^4.5.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.33.0", "eslint": "^8.45.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.14",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"vite": "^4.5.1", "vite": "^4.5.1",

Binary file not shown.

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="M11 4H21V6H11V4ZM6 7V11H4V7H1L5 3L9 7H6ZM6 17H9L5 21L1 17H4V13H6V17ZM11 18H21V20H11V18ZM9 11H21V13H9V11Z"></path></svg>

Before

Width:  |  Height:  |  Size: 208 B

@ -1,4 +1,4 @@
import { fetchJSON, postForm, } from '@haina/utils-request'; import { fetchJSON, postForm, } from '@/utils/request';
import { API_HOST } from '@/config'; import { API_HOST } from '@/config';
/** /**

@ -1,9 +1,9 @@
import { groupBy, isNotEmpty, pick, sortArrayByOrder, sortBy } from '@haina/utils-commons'; import { groupBy, isNotEmpty, pick, sortArrayByOrder, sortBy } from '@/utils/commons';
import { fetchJSON, postJSON, postForm } from '@haina/utils-request' import { fetchJSON, postJSON, postForm } from '@/utils/request'
import { parseRenderMessageList } from '@/channel/bubbleMsgUtils'; import { parseRenderMessageList } from '@/channel/bubbleMsgUtils';
import { API_HOST } from '@/config'; import { API_HOST } from '@/config';
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
/** /**
@ -11,9 +11,6 @@ import dayjs from 'dayjs';
*/ */
export const fetchTemplates = async (params) => { export const fetchTemplates = async (params) => {
const data = await fetchJSON(`${API_HOST}/listtemplates`, params); const data = await fetchJSON(`${API_HOST}/listtemplates`, params);
const leftPageCnt = Math.ceil( data?.result?.total/100 || 0)-1;
const leftData = await Promise.all(Array.from({ length: leftPageCnt }, (_, i) => fetchJSON(`${API_HOST}/listtemplates`, {...params, page: i+2})));
const leftItems = leftData.map(item => item.result.items).flat();
const topName = [ const topName = [
'agent_intro_with_update_v1', 'agent_intro_with_update_v1',
'online_inquiry_received', 'online_inquiry_received',
@ -23,22 +20,14 @@ export const fetchTemplates = async (params) => {
'travel_service_update_v2', 'travel_service_update_v2',
'travel_service_update_v1', 'travel_service_update_v1',
'order_updated_specialist_assigned_sharon', 'order_updated_specialist_assigned_sharon',
'travel_service_update_v3',
'first_message_for_not_reply', 'first_message_for_not_reply',
// 'free_style_3', // 'free_style_3',
// 'free_style_4', // 'free_style_4',
// CR
'notification_of_following_up_by_cr_v3','notification_of_following_up_by_cr_v1',
'notification_of_following_up_by_cr_v2',
'membership_activation_update_by_cr_v1', '45d_before_the_trip_referral_voucher_by_cr_v0',
'membership_activation_update_by_cr_v0',
'departure_reminder_by_cr_v5','departure_reminder_by_cr_v2',
]; ];
// shouwcase // shouwcase
const scNames = ['trip_planner_showcase', 'showcase_different', 'order_status_updated']; const scNames = ['trip_planner_showcase', 'showcase_different', 'order_status_updated'];
// 客运 // 客运
const crNames = [ const crNames = [
'notification_of_next_trip_planning',
// 'notification_of_following_up_by_cr_v3', // 'notification_of_following_up_by_cr_v3',
'notification_of_one_day_before_ending_the_trip_by_cr_v2', 'notification_of_one_day_before_ending_the_trip_by_cr_v2',
'one_day_after_payment_by_yuni', 'one_day_after_payment_by_yuni',
@ -47,14 +36,7 @@ export const fetchTemplates = async (params) => {
'one_day_before_ending_the_trip_contacted_by_yuni','one_day_before_ending_the_trip_first_time_by_yuni', 'one_day_before_ending_the_trip_contacted_by_yuni','one_day_before_ending_the_trip_first_time_by_yuni',
'post_booking_confirmation_welcome', 'post_booking_confirmation_welcome',
]; ];
const crNamesAuto = [ const crNamesOmit = [
'notification_of_status_changed', 'notification_of_next_trip_planning_by_cr_v2',
'notification_of_payment_received_3_asea_by_cr', 'one_day_after_payment_by_yuni',
'30_days_after_end_of_the_trip_asean_referral_voucher_by_cr',
'7notification_of_one_day_before_ending_the_trip_only_asean_by_cr_v7',
'notification_of_one_day_before_ending_the_trip_by_cr_v2',
];
const NamesOmit = [
'birthday_greetings_by_marketing','one_day_before_ending_the_trip_by_marketing', 'birthday_greetings_by_marketing','one_day_before_ending_the_trip_by_marketing',
'introduce_the_voucher_one_day_before_ending_the_trip_by_marketing', 'introduce_the_voucher_one_day_before_ending_the_trip_by_marketing',
'birthday_greetings_by_customer_relations_0', 'birthday_greetings_by_customer_relations_0',
@ -69,9 +51,8 @@ export const fetchTemplates = async (params) => {
'notification_of_account_updated_by_cr', 'notification_of_account_updated_by_cr',
'birthday_greetings_by_customer_relations', 'birthday_greetings_by_customer_relations',
'one_day_before_ending_the_trip_by_customer_relations', 'one_day_before_ending_the_trip_by_customer_relations',
'network_troubleshooting',
] ]
const canUseTemplates = (data?.result?.items || []).concat(leftItems) const canUseTemplates = (data?.result?.items || [])
.filter((_t) => _t.status === 'APPROVED' && !['say_hello_from_trip_advisor', 'free_style_7', 'free_style_1', 'free_style_2'].includes(_t.name)) .filter((_t) => _t.status === 'APPROVED' && !['say_hello_from_trip_advisor', 'free_style_7', 'free_style_1', 'free_style_2'].includes(_t.name))
.map((ele, i) => ({ .map((ele, i) => ({
...ele, ...ele,
@ -79,16 +60,8 @@ export const fetchTemplates = async (params) => {
components: groupBy(ele.components, (_c) => _c.type.toLowerCase()), components: groupBy(ele.components, (_c) => _c.type.toLowerCase()),
key: ele.name, key: ele.name,
// displayName: ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : templatesDisplayNameMap?.[ele.name] || ele.name, // displayName: ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : templatesDisplayNameMap?.[ele.name] || ele.name,
displayName: templatesDisplayNameMap?.[ele.name] || (ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated'] + `_${i}` : ele.name), displayName: templatesDisplayNameMap?.[ele.name] || (ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : ele.name),
displayLanguage: NamesOmit.includes(ele.name) displayLanguage: crNamesOmit.includes(ele.name) ? '客运-' : (crNames.includes(ele.name) || ele.name.includes('by_cr')) ? ele.language + '-客运' : scNames.includes(ele.name) ? ele.language + '-示例' : ele.language,
? '--'
: crNamesAuto.includes(ele.name)
? '客运Task'
: crNames.includes(ele.name) || ele.name.includes('by_cr')
? ele.language + '-客运'
: scNames.includes(ele.name)
? ele.language + '-示例'
: ele.language.slice(0, 2),
})) }))
const top2Name = topName.concat(canUseTemplates.filter(_t => _t.name.startsWith('order_updated')).map(_tem => _tem.name)); const top2Name = topName.concat(canUseTemplates.filter(_t => _t.name.startsWith('order_updated')).map(_tem => _tem.name));
@ -98,7 +71,7 @@ export const fetchTemplates = async (params) => {
const secondS = second.sort(sortBy('name')); const secondS = second.sort(sortBy('name'));
const raw = canUseTemplates.filter((_t) => !top2Name.includes(_t.name) && !_t.name.includes('free_style')); const raw = canUseTemplates.filter((_t) => !top2Name.includes(_t.name) && !_t.name.includes('free_style'));
// 剩下的排序 // 剩下的排序
const rawS = sortArrayByOrder(raw, 'name', [...crNames, ...scNames, ...NamesOmit ]); const rawS = sortArrayByOrder(raw, 'name', [...crNames, ...scNames, ...crNamesOmit ]);
return [...top, ...secondS, ...rawS]; return [...top, ...secondS, ...rawS];
}; };
/** /**

@ -1,6 +1,6 @@
import { fetchJSON, postForm, postJSON } from '@haina/utils-request'; import { fetchJSON, postForm, postJSON } from '@/utils/request';
import { API_HOST, API_HOST_V3, DATE_FORMAT, DATEEND_FORMAT, DATETIME_FORMAT, EMAIL_HOST, EMAIL_HOST_v3 } from '@/config'; import { API_HOST, API_HOST_V3, DATE_FORMAT, DATEEND_FORMAT, DATETIME_FORMAT, EMAIL_HOST } from '@/config';
import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, uniqWith } from '@haina/utils-commons'; import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, uniqWith } from '@/utils/commons';
import { readIndexDB, writeIndexDB } from '@/utils/indexedDB'; import { readIndexDB, writeIndexDB } from '@/utils/indexedDB';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { internalEventEmitter } from '@/utils/EventEmitterService'; import { internalEventEmitter } from '@/utils/EventEmitterService';
@ -81,16 +81,17 @@ export const postResendEmailAction = async (body) => {
const encodeEmailInfo = (info) => { const encodeEmailInfo = (info) => {
const encodeQuote = (str = '') => str.replace(/"/g, ''); //.replace(/</g,'&lt;').replace(/>/g,'&gt;') const encodeQuote = (str = '') => str.replace(/"/g, ''); //.replace(/</g,'&lt;').replace(/>/g,'&gt;')
const CSsClean = encodeQuote(info.MAI_CS.replace(';', ',')).split(','); const CSsClean = encodeQuote(info.MAI_CS).includes(',') ? encodeQuote(info.MAI_CS).split(',') : encodeQuote(info.MAI_CS).split(';');
const tosClean = (encodeQuote(info.MAI_To.replace(';', ',')).split(',')).map(e => e.trim()).filter(s => s); const tosClean = (encodeQuote(info.MAI_To).includes(',') ? encodeQuote(info.MAI_To).split(',') : encodeQuote(info.MAI_To).split(';')).concat(CSsClean).filter(s => s);
const replyTo = info.MAI_Direction === 1 ? info.MAI_To : info.MAI_From; const replyTo = info.MAI_Direction === 1 ? info.MAI_To : info.MAI_From;
const replyToAll = [].concat([info.MAI_From], tosClean); const replyToAll = (tosClean.length > 1) ?
(info.MAI_Direction === 1 ? tosClean.join(',') : [...tosClean, info.MAI_From].join(','))
: (info.MAI_Direction === 1 ? info.MAI_To : info.MAI_From)
return { return {
...info, ...info,
MAI_From: encodeQuote(info.MAI_From), MAI_From: encodeQuote(info.MAI_From),
MAI_To: encodeQuote(info.MAI_To), MAI_To: encodeQuote(info.MAI_To),
tos: [...new Set(tosClean)], tos: [...new Set(tosClean)],
ccs: [...new Set(CSsClean)],
replyToAll, replyToAll,
replyTo, replyTo,
} }
@ -207,7 +208,7 @@ export const getMailboxCountAction = async (params = { opi_sn: '' }, update = tr
const ret = errcode !== 0 ? { [`${params.opi_sn}`]: {} } : { [`${params.opi_sn}`]: result } const ret = errcode !== 0 ? { [`${params.opi_sn}`]: {} } : { [`${params.opi_sn}`]: result }
// 更新数量 // 更新数量
if (update !== false) { if (update !== false) {
const readCacheDir = (await readIndexDB(Number(params.opi_sn), 'dirs', 'mailbox')) || {}; const readCacheDir = await readIndexDB(Number(params.opi_sn), 'dirs', 'mailbox');
const mailboxDir = isEmpty(readCacheDir) ? [] : readCacheDir.tree.filter(node => node?._raw?.IsTrue === 1); const mailboxDir = isEmpty(readCacheDir) ? [] : readCacheDir.tree.filter(node => node?._raw?.IsTrue === 1);
const _MapDir = new Map(mailboxDir.map((obj) => [obj.key, obj])) const _MapDir = new Map(mailboxDir.map((obj) => [obj.key, obj]))
Object.keys(result).map(dirKey => { Object.keys(result).map(dirKey => {
@ -219,7 +220,7 @@ export const getMailboxCountAction = async (params = { opi_sn: '' }, update = tr
_MapRoot.set(row.key, row) _MapRoot.set(row.key, row)
}) })
const _newRoot = Array.from(_MapRoot.values()) const _newRoot = Array.from(_MapRoot.values())
writeIndexDB([{ ...readCacheDir, key: Number(params.opi_sn), tree: _newRoot }], 'dirs', 'mailbox') writeIndexDB([{ key: Number(params.opi_sn), tree: _newRoot }], 'dirs', 'mailbox')
notifyMailboxUpdate({ type: 'dirs', key: Number(params.opi_sn) }) notifyMailboxUpdate({ type: 'dirs', key: Number(params.opi_sn) })
} }
@ -337,7 +338,7 @@ export const getRootMailboxDirAction = async ({ opi_sn = 0, userIdStr = '' } = {
const mailBoxCount = await Promise.all(userIdStr.split(',').map(_opi => getMailboxCountAction({ opi_sn: _opi }, false))); const mailBoxCount = await Promise.all(userIdStr.split(',').map(_opi => getMailboxCountAction({ opi_sn: _opi }, false)));
const mailboxDirCountByOPI = mailBoxCount.reduce((a, c) => ({ ...a, ...c, }), {}) const mailboxDirCountByOPI = mailBoxCount.reduce((a, c) => ({ ...a, ...c, }), {})
const mailboxDirByOPI = mailboxDir.reduce((a, c) => ({ ...a, ...(Object.keys(c).reduce((a, opi) => ({...a, [opi]: c[`${opi}`].map((dir) => ({ ...dir, count: mailboxDirCountByOPI[opi][`${dir.key}`] })) }), {} )) }), {}) const mailboxDirByOPI = mailboxDir.reduce((a, c) => ({ ...a, ...(Object.keys(c).reduce((a, opi) => ({...a, [opi]: c[`${opi}`].map((dir) => ({ ...dir, count: mailboxDirCountByOPI[opi][`${dir.key}`] })) }), {} )) }), {})
const rootTree = Object.keys(stickyTree).map((opi) => ({ key: Number(opi), tree: [...stickyTree[opi], ...(mailboxDirByOPI?.[opi] || [])], treeTimestamp: Date.now() })) const rootTree = Object.keys(stickyTree).map((opi) => ({ key: Number(opi), tree: [...stickyTree[opi], ...(mailboxDirByOPI?.[opi] || [])] }))
writeIndexDB(rootTree, 'dirs', 'mailbox') writeIndexDB(rootTree, 'dirs', 'mailbox')
const _mapped = groupBy(rootTree, 'key') const _mapped = groupBy(rootTree, 'key')
return _mapped[opi_sn]?.[0]?.tree || [] return _mapped[opi_sn]?.[0]?.tree || []
@ -347,6 +348,7 @@ export const getRootMailboxDirAction = async ({ opi_sn = 0, userIdStr = '' } = {
* 获取邮件列表 * 获取邮件列表
* @usage 邮件目录下的邮件列表 * @usage 邮件目录下的邮件列表
* @usage 订单的邮件列表 * @usage 订单的邮件列表
* @usage 高级搜索
*/ */
export const queryEmailListAction = async ({ opi_sn = '', pagesize = 10, last_id = '', node = {}, } = {}) => { export const queryEmailListAction = async ({ opi_sn = '', pagesize = 10, last_id = '', node = {}, } = {}) => {
const _params = { const _params = {
@ -373,20 +375,6 @@ export const queryEmailListAction = async ({ opi_sn = '', pagesize = 10, last_id
return ret; return ret;
} }
export const searchEmailListAction = async ({opi_sn = '', mailboxtype = 'ALL', sender = '', receiver = '', subject = '', content=''}={}) => {
const formData = new FormData()
formData.append('opi_sn', opi_sn)
formData.append('mailboxtype', mailboxtype)
formData.append('sender', sender)
formData.append('receiver', receiver)
formData.append('subject', subject)
// formData.append('content', content)
const { errcode, result } = await postForm(`${API_HOST_V3}/mail_search`, formData)
const ret = errcode === 0 ? result : []
notifyMailboxUpdate({ type: 'maillist-search-result', query: [sender, receiver, subject].filter(s => s).join(' '), data: ret.map(ele => ({...ele, key: ele.MAI_SN, showFolder: true })) })
return ret;
}
const removeFromCurrentList = async (params) => { const removeFromCurrentList = async (params) => {
const readRow0 = await readIndexDB(params.mai_sn_list[0], 'listrow', 'mailbox') const readRow0 = await readIndexDB(params.mai_sn_list[0], 'listrow', 'mailbox')
const listKey = readRow0?.data?.listKey || '' const listKey = readRow0?.data?.listKey || ''
@ -454,7 +442,7 @@ export const getReminderEmailTemplateAction = async (params = { coli_sn: 0, lgc:
* @param {boolean} [isDraft=false] - Whether the email is a draft. * @param {boolean} [isDraft=false] - Whether the email is a draft.
*/ */
export const saveEmailDraftOrSendAction = async (body, isDraft = false) => { export const saveEmailDraftOrSendAction = async (body, isDraft = false) => {
const url = isDraft !== false ? `${API_HOST_V3}/email_draft_save` : `${EMAIL_HOST_v3}/sendmail`; const url = isDraft !== false ? `${API_HOST_V3}/email_draft_save` : `${EMAIL_HOST}/sendmail`;
const { attaList=[], atta, content, ...bodyData } = body; const { attaList=[], atta, content, ...bodyData } = body;
bodyData.ordertype = 227001; bodyData.ordertype = 227001;
const formData = new FormData(); const formData = new FormData();
@ -488,3 +476,7 @@ export const queryOPIOrderAction = async (params) => {
return errcode !== 0 ? [] : result return errcode !== 0 ? [] : result
}; };
export const queryInMailboxAction = async (params) => {
const { errcode, result } = await fetchJSON(`${API_HOST_V3}/mail_search`, params)
return errcode !== 0 ? [] : result
}

@ -1,4 +1,4 @@
import { fetchJSON, postForm, postJSON } from '@haina/utils-request' import { fetchJSON, postForm, postJSON } from '@/utils/request'
import { usingStorage } from '@/utils/usingStorage' import { usingStorage } from '@/utils/usingStorage'
const WAI_SERVER_KEY = 'G-STR:WAI_SERVER' const WAI_SERVER_KEY = 'G-STR:WAI_SERVER'

@ -1,6 +1,6 @@
.logo { .logo {
float: left; float: left;
height: 60px; height: 68px;
margin: 0 6px 0 0; margin: 0 6px 0 0;
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@ -1,4 +1,4 @@
import { cloneDeep, isEmpty, olog, fixTo2Decimals, pick, objectMapper } from "@haina/utils-commons"; import { cloneDeep, isEmpty, olog, fixTo2Decimals, pick, objectMapper } from "@/utils/commons";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
@ -58,27 +58,6 @@ export const WABAccounts = [
"requestedVerifiedName": "Customer Relation Specialist", "requestedVerifiedName": "Customer Relation Specialist",
"rejectionReason": "NONE" "rejectionReason": "NONE"
}, },
{
"id": "955633124303178",
"phoneNumber": "+85265210895",
"wabaId": "190290134156880",
"verifiedName": "Customer Relation Specialist at Highlights",
"qualityRating": "UNKNOWN",
"qualityUpdateEvent": "ONBOARDING",
"messagingLimit": "TIER_2K",
"whatsappBusinessManagerMessagingLimit": "TIER_2K",
"isOfficialBusinessAccount": false,
"codeVerificationStatus": "VERIFIED",
"status": "CONNECTED",
"displayPhoneNumber": "+852 6521 0895",
"nameStatus": "APPROVED",
"newName": "Customer Relation Specialist at Highlights",
"newNameStatus": "NONE",
"decision": "APPROVED",
"requestedVerifiedName": "Customer Relation Specialist at Highlights",
"rejectionReason": "NONE",
"isOnBizApp": false
},
]; ];
export const WABAccountsMapped = WABAccounts.reduce((a, c) => ({ ...a, [removeFirstPlus(c.phoneNumber)]: c, [c.phoneNumber]: c }), {}) export const WABAccountsMapped = WABAccounts.reduce((a, c) => ({ ...a, [removeFirstPlus(c.phoneNumber)]: c, [c.phoneNumber]: c }), {})
@ -87,8 +66,9 @@ export const replaceTemplateString = (str, replacements) => {
let keys = str.match(/{{(.*?)}}/g).map(key => key.replace(/{{|}}/g, '')); let keys = str.match(/{{(.*?)}}/g).map(key => key.replace(/{{|}}/g, ''));
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const replaceValue = replacements[i]; let replaceValue = replacements[i];
result = result.replaceAll(`{{${keys[i]}}}`, replaceValue); let template = new RegExp(`{{${keys[i]}}}`, 'g');
result = result.replace(template, replaceValue);
} }
return result; return result;
@ -139,7 +119,7 @@ const mediaMsg = {
...msg, ...msg,
actionId: msg.id, actionId: msg.id,
conversationid: msg.id.split('.')[0], conversationid: msg.id.split('.')[0],
data: { ...msg.data, status: { download: true, click: true, loading: 0 } }, data: { ...msg.data, status: { download: msg.data?.loading ? false : true, click: true, loading: msg.data.loading } },
...(msg.context ...(msg.context
? { ? {
reply: { reply: {
@ -508,7 +488,7 @@ const sessionMsgMapped = {
getMsg: (result) => { getMsg: (result) => {
// sessionItem 是数组 // sessionItem 是数组
return isEmpty(result?.sessionItem) return isEmpty(result?.sessionItem)
? [] ? null
: result.sessionItem.map((ele) => ({ : result.sessionItem.map((ele) => ({
...ele, ...ele,
customer_name: `${ele.whatsapp_name || ''}`.trim(), customer_name: `${ele.whatsapp_name || ''}`.trim(),
@ -712,12 +692,12 @@ export const whatsappMsgTypeMapped = {
unsupported: { unsupported: {
type: 'text', type: 'text',
data: (msg) => ({ id: msg.wamid, text: `[对方删除消息](${msg.wamid})`, dateString: `${dayjs(msg.sendTime).format('MM-DD HH:mm')} [ WhatsApp未提供消息内容 ] 客人删除消息/会话` }), data: (msg) => ({ id: msg.wamid, text: `[对方删除消息](${msg.wamid})`, dateString: `${dayjs(msg.sendTime).format('MM-DD HH:mm')} [ WhatsApp未提供消息内容 ] 客人删除消息/会话` }),
renderForReply: (msg) => ({ id: msg?.wamid || msg?.id || '', message: `[Message type unsupported](${msg.wamid})` }), renderForReply: (msg) => ({ id: msg?.wamid || msg.id, message: `[Message type unsupported](${msg.wamid})` }),
}, },
unresolvable: { unresolvable: {
type: 'text', type: 'text',
data: (msg) => ({ id: msg.wamid, text: `[无法解析](${msg.wamid})`, }), data: (msg) => ({ id: msg.wamid, text: `[无法解析](${msg.wamid})`, }),
renderForReply: (msg) => ({ id: msg?.wamid || msg?.id || '', message: `[无法解析](${msg.wamid})` }), renderForReply: (msg) => ({ id: msg?.wamid || msg.id, message: `[无法解析](${msg.wamid})` }),
}, },
reaction: { reaction: {
type: 'text', type: 'text',
@ -846,18 +826,15 @@ export const parseRenderMessageItem = (msg) => {
origin: msg.context, origin: msg.context,
}), }),
msg_source: msg?.msg_source || msg.type, msg_source: msg?.msg_source || msg.type,
whatsapp_msg_type: '',
waba: '',
wabaName: '',
...((msg.msg_source) === 'WABA' ? { ...((msg.msg_source) === 'WABA' ? {
whatsapp_msg_type: msg.type, whatsapp_msg_type: msg.type,
waba: msg.msg_direction === 'outbound' ? msg.from : msg.to, waba: msg.msg_direction === 'outbound' ? msg.from : msg.to,
wabaName: WABAccountsMapped[msg.msg_direction === 'outbound' ? msg.from : msg.to]?.verifiedName, wabaName: WABAccountsMapped[msg.msg_direction === 'outbound' ? msg.from : msg.to]?.verifiedName,
} : {}), } : {
...((msg.msg_source) === 'wai' ? { whatsapp_msg_type: '',
whatsapp_msg_type: msg.type, waba: '',
wabaName: '个人号', wabaName: '',
} : {}), }),
}; };
}; };
/** /**
@ -1094,120 +1071,3 @@ export const phoneNumberToWAID = (input) => {
} }
export const uploadProgressSimulate = () => fixTo2Decimals(Math.random() * (0.8 - 0.2) + 0.2); export const uploadProgressSimulate = () => fixTo2Decimals(Math.random() * (0.8 - 0.2) + 0.2);
// Parse text segments for URLs and numbers
const parseTextForMarkdown = (text) => {
// Find URLs and four-digit numbers
const urlRegex = /https?:\/\/[^\s]+/g;
const numberRegex = /\d{4,}/g;
const matches = [];
// Find all URLs
let match;
while ((match = urlRegex.exec(text)) !== null) {
matches.push({ start: match.index, end: match.index + match[0].length, type: 'url', content: match[0] });
}
// Find all 4+ digit numbers
numberRegex.lastIndex = 0; // Reset regex
while ((match = numberRegex.exec(text)) !== null) {
matches.push({ start: match.index, end: match.index + match[0].length, type: 'number', content: match[0] });
}
// Sort matches by position
matches.sort((a, b) => a.start - b.start);
// Remove overlapping matches (URLs take priority)
const filteredMatches = [];
for (const current of matches) {
const isOverlapping = filteredMatches.some(existing =>
(current.start >= existing.start && current.start < existing.end) ||
(current.end > existing.start && current.end <= existing.end)
);
if (!isOverlapping) {
filteredMatches.push(current);
}
}
// Split text into segments
const segments = [];
let currentIndex = 0;
for (const match of filteredMatches) {
if (currentIndex < match.start) {
const textContent = text.slice(currentIndex, match.start);
if (textContent) {
segments.push({ type: 'text', content: textContent });
}
}
segments.push(match);
currentIndex = match.end;
}
if (currentIndex < text.length) {
const textContent = text.slice(currentIndex);
if (textContent) {
segments.push({ type: 'text', content: textContent });
}
}
return segments.length > 0 ? segments : [{ type: 'text', content: text }];
};
// Parse markdown with nesting support, autolinks, and number recognition
export const parseSimpleMarkdown = (text) => {
const tokens = [];
let current = '';
let i = 0;
while (i < text.length) {
const char = text[i];
if (char === '*' || char === '_') {
// Save any accumulated text before processing markdown
if (current) {
tokens.push(...parseTextForMarkdown(current));
current = '';
}
// Find the closing marker
const marker = char;
let j = i + 1;
let content = '';
let found = false;
while (j < text.length) {
if (text[j] === marker) {
found = true;
break;
}
content += text[j];
j++;
}
if (found && content) {
// Recursively parse the content inside the markers
tokens.push({
type: marker === '*' ? 'bold' : 'italic',
content: parseSimpleMarkdown(content)
});
i = j + 1; // Skip past the closing marker
} else {
// If no closing marker found, treat as regular text
current += char;
i++;
}
} else {
current += char;
i++;
}
}
// Add any remaining text
if (current) {
tokens.push(...parseTextForMarkdown(current));
}
return tokens;
};

@ -2,7 +2,7 @@ import { createContext, useEffect, useState } from 'react';
import {} from 'antd'; import {} from 'antd';
import Modal from '@dckj/react-better-modal'; import Modal from '@dckj/react-better-modal';
import '@dckj/react-better-modal/dist/index.css'; import '@dckj/react-better-modal/dist/index.css';
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore'; import useStyleStore from '@/stores/StyleStore';
const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial = {}, title, footer=null, ...props }) => { const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial = {}, title, footer=null, ...props }) => {

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import LexicalEditor from '@/components/LexicalEditor' import LexicalEditor from '@/components/LexicalEditor'
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
/** /**
* 封装的编辑组件, 用于在Form.Item 中使用 * 封装的编辑组件, 用于在Form.Item 中使用

@ -51,7 +51,7 @@ import FormatPaintPlugin from './plugins/FormatPaint';
import { TextNode, $getRoot, $getSelection, $createParagraphNode } from 'lexical'; import { TextNode, $getRoot, $getSelection, $createParagraphNode } from 'lexical';
import { $generateHtmlFromNodes, $generateNodesFromDOM, } from '@lexical/html'; import { $generateHtmlFromNodes, $generateNodesFromDOM, } from '@lexical/html';
// import { } from '@lexical/clipboard'; // import { } from '@lexical/clipboard';
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
import {useSettings} from './context/SettingsContext'; import {useSettings} from './context/SettingsContext';
import './styles.css'; import './styles.css';

@ -89,19 +89,6 @@ const FONT_SIZE_OPTIONS = [
// ['48px', '48px'], // ['48px', '48px'],
]; ];
const LINE_SPACING_OPTIONS = [
['1', '1'],
['1.25', '1.25'],
['1.5', '1.5'],
['2', '2'],
['2.5', '2.5'],
['3', '3'],
// ['3.5', '3.5'],
// ['4', '4'],
// ['4.5', '4.5'],
// ['5', '5'],
];
const ELEMENT_FORMAT_OPTIONS = { const ELEMENT_FORMAT_OPTIONS = {
center: { icon: 'center-align', iconRTL: 'center-align', name: 'Center Align' }, center: { icon: 'center-align', iconRTL: 'center-align', name: 'Center Align' },
end: { icon: 'right-align', iconRTL: 'left-align', name: 'End Align' }, end: { icon: 'right-align', iconRTL: 'left-align', name: 'End Align' },
@ -547,26 +534,6 @@ function BlockOptionsDropdownList({ editor, blockType, toolbarRef, setShowBlockO
); );
} }
const FontDropDownMapped = {
'font-family': {
buttonAriaLabel: 'Formatting options for font family',
IconClassName: 'icon icon2 block-type font-family',
options: FONT_FAMILY_OPTIONS,
styleName: 'fontFamily',
},
'font-size': {
buttonAriaLabel: 'Formatting options for font size',
IconClassName: '',
options: FONT_SIZE_OPTIONS,
styleName: 'fontSize',
},
'line-height': {
buttonAriaLabel: 'Formatting options for line spacing',
IconClassName: 'icon icon2 line-height',
options: LINE_SPACING_OPTIONS,
styleName: 'lineHeight',
},
}
function FontDropDown({ editor, value, style, disabled = false }) { function FontDropDown({ editor, value, style, disabled = false }) {
const handleClick = useCallback( const handleClick = useCallback(
(option) => { (option) => {
@ -582,23 +549,26 @@ function FontDropDown({ editor, value, style, disabled = false }) {
[editor, style] [editor, style]
); );
const buttonIconClassName = FontDropDownMapped[style].IconClassName; const buttonAriaLabel = style === 'font-family' ? 'Formatting options for font family' : 'Formatting options for font size';
const buttonAriaLabel = FontDropDownMapped[style].buttonAriaLabel;
const dropdownOptions = FontDropDownMapped[style].options;
return ( return (
<DropDown disabled={disabled} buttonClassName={'toolbar-item ' + style} buttonLabel={value} buttonIconClassName={buttonIconClassName} buttonAriaLabel={buttonAriaLabel}> <DropDown
{dropdownOptions.map(([option, text]) => ( disabled={disabled}
buttonClassName={'toolbar-item ' + style}
buttonLabel={value}
buttonIconClassName={style === 'font-family' ? 'icon block-type font-family' : ''}
buttonAriaLabel={buttonAriaLabel}>
{(style === 'font-family' ? FONT_FAMILY_OPTIONS : FONT_SIZE_OPTIONS).map(([option, text]) => (
<DropDownItem <DropDownItem
className={`item font-m-${option.replace(/\s+/g, '_')} ${dropDownActiveClass(value === option)} ${style === 'font-size' ? 'fontsize-item' : ''}`} className={`item font-m-${option.replace(/\s+/g, '_')} ${dropDownActiveClass(value === option)} ${style === 'font-size' ? 'fontsize-item' : ''}`}
style={{ fontFamily: style === 'font-family' ? option : undefined, fontSize: style === 'font-size' ? option : undefined, }} style={{ fontFamily: style === 'font-family' ? option : undefined, fontSize: style === 'font-size' ? option : undefined }}
onClick={() => handleClick(option)} onClick={() => handleClick(option)}
key={option}> key={option}>
<span className='text'>{text}</span> <span className='text'>{text}</span>
</DropDownItem> </DropDownItem>
))} ))}
</DropDown> </DropDown>
) );
} }
function ElementFormatDropdown({ editor, value, isRTL, disabled = false }) { function ElementFormatDropdown({ editor, value, isRTL, disabled = false }) {
@ -703,7 +673,6 @@ export default function ToolbarPlugin() {
const [fontColor, setFontColor] = useState('#000'); const [fontColor, setFontColor] = useState('#000');
const [bgColor, setBgColor] = useState('#fff'); const [bgColor, setBgColor] = useState('#fff');
const [elementFormat, setElementFormat] = useState('left'); const [elementFormat, setElementFormat] = useState('left');
const [lineSpacing, setLineSpacing] = useState('1.5');
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null); const [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false); const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false);
@ -791,9 +760,6 @@ export default function ToolbarPlugin() {
setFontSize( setFontSize(
$getSelectionStyleValueForProperty(selection, 'font-size', '16px'), $getSelectionStyleValueForProperty(selection, 'font-size', '16px'),
); );
setLineSpacing(
$getSelectionStyleValueForProperty(selection, 'line-height', '1.5'),
);
let matchingParent; let matchingParent;
if ($isLinkNode(parent)) { if ($isLinkNode(parent)) {
// If node is a link, we need to fetch the parent paragraph node to set format // If node is a link, we need to fetch the parent paragraph node to set format
@ -930,12 +896,6 @@ export default function ToolbarPlugin() {
value={fontSize} value={fontSize}
editor={editor} editor={editor}
/> />
<FontDropDown
disabled={!isEditable}
style={'line-height'}
value={lineSpacing}
editor={editor}
/>
<Divider /> <Divider />
<button type='button' <button type='button'
onClick={() => { onClick={() => {

@ -87,7 +87,6 @@
outline: 0; outline: 0;
padding: 15px 10px; padding: 15px 10px;
caret-color: #444; caret-color: #444;
line-height: 1.5;
} }
.editor-pure-input { .editor-pure-input {
@ -416,7 +415,7 @@ pre::-webkit-scrollbar-thumb {
margin-right: 2px; margin-right: 2px;
} }
.toolbar button.toolbar-item i.format, .toolbar button.toolbar-item .icon2 { .toolbar button.toolbar-item i.format {
background-size: contain; background-size: contain;
display: inline-block; display: inline-block;
height: 18px; height: 18px;
@ -890,10 +889,6 @@ i.justify-align {
background-image: url(/images/icons/justify.svg); background-image: url(/images/icons/justify.svg);
} }
i.line-height, .icon.line-height {
background-image: url(/images/icons/line-height.svg);
}
.editor-container span.editor-image { .editor-container span.editor-image {
cursor: default; cursor: default;
display: inline-block; display: inline-block;

@ -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;

@ -9,50 +9,45 @@ import {
CalendarOutlined, CalendarOutlined,
EditOutlined, EditOutlined,
CheckOutlined, CheckOutlined,
CopyOutlined ReloadOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { App, Flex, Select, Tooltip, Divider, Typography, Skeleton, Checkbox, Drawer, Button, Empty, Form, Input } from 'antd' import { App, Flex, Select, Tooltip, Divider, Typography, Skeleton, Checkbox, Drawer, Button, Form, Input } from 'antd'
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions } from '@/stores/OrderStore' import { useOrderStore, fetchSetRemindStateAction, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions } from '@/stores/OrderStore'
import { copy, isEmpty } from '@haina/utils-commons' import { copy, isEmpty } from '@/utils/commons'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
const OrderProfile = ({ coliSN, ...props }) => { const OrderProfile = ({ coliSN, ...props }) => {
const navigate = useNavigate()
const { notification, message } = App.useApp() const { notification, message } = App.useApp()
const [formComment] = Form.useForm() const [formComment] = Form.useForm()
const [formWhatsApp] = Form.useForm()
const [formExtra] = Form.useForm()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [openOrderCommnet, setOpenOrderCommnet] = useState(false) const [openOrderCommnet, setOpenOrderCommnet] = useState(false)
const [openWhatsApp, setOpenWhatsApp] = useState(false)
const [openExtra, setOpenExtra] = useState(false)
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions) const orderLabelOptions = copy(OrderLabelDefaultOptions)
orderLabelOptions.unshift({ value: 0, label: '未设置', disabled: true }) orderLabelOptions.unshift({ value: 0, label: '未设置', disabled: true })
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions) const orderStatusOptions = copy(OrderStatusDefaultOptions)
const [orderDetail, customerDetail, fetchOrderDetail, setOrderPropValue, const [orderDetail, customerDetail, fetchOrderDetail, setOrderPropValue, appendOrderComment] = useOrderStore((s) => [
appendOrderComment, updateWhatsapp, updateExtraInfo, remindCheckList, updateRemindState] = useOrderStore((s) => [
s.orderDetail, s.orderDetail,
s.customerDetail, s.customerDetail,
s.fetchOrderDetail, s.fetchOrderDetail,
s.setOrderPropValue, s.setOrderPropValue,
s.appendOrderComment, s.appendOrderComment,
s.updateWhatsapp,
s.updateExtraInfo,
s.remindCheckList,
s.updateRemindState
]) ])
const loginUser = useAuthStore((state) => state.loginUser) const loginUser = useAuthStore((state) => state.loginUser)
const currentOrder = useConversationStore(useShallow((state) => state.currentConversation?.coli_sn || '')) const currentOrder = useConversationStore(useShallow((state) => state.currentConversation?.coli_sn || ''))
const orderId = coliSN || currentOrder const orderId = coliSN || currentOrder
const [orderRemindState, setOrderRemindState] = useState(orderDetail.remindstate)
useEffect(() => {
setOrderRemindState(orderDetail.remindstate);
}, [orderDetail.remindstate]);
useEffect(() => { useEffect(() => {
if (orderId) { if (orderId) {
setLoading(true) setLoading(true)
@ -71,12 +66,20 @@ const OrderProfile = ({ coliSN, ...props }) => {
}, [orderId]) }, [orderId])
const handleSetRemindState = async (checkedValue) => { const handleSetRemindState = async (checkedValue) => {
const state = checkedValue.filter((v) => v !== orderRemindState)
const oldState = orderRemindState
try { try {
await updateRemindState(coliSN, checkedValue) if (isEmpty(state)) {
setOrderRemindState(null)
} else {
setOrderRemindState(state[0])
}
await fetchSetRemindStateAction({ coli_sn: coliSN, remindstate: state })
message.success('设置成功') message.success('设置成功')
} catch (error) { } catch (error) {
notification.warning({ message: '设置失败', description: error.message, placement: 'top', duration: 60 }) notification.warning({ message: '设置失败', description: error.message, placement: 'top', duration: 60 })
setOrderRemindState(oldState)
} }
} }
@ -89,18 +92,13 @@ const OrderProfile = ({ coliSN, ...props }) => {
return orderDetail.DidPlan === 0 ? '未做计划' : '已做计划' return orderDetail.DidPlan === 0 ? '未做计划' : '已做计划'
} }
const renderOrderDetail = () => { return (
return ( <>
<>
<Skeleton active loading={loading}> <Skeleton active loading={loading}>
<Flex gap='small' vertical={true} justify='space-between' className='p-2'> <Flex gap='small' vertical={true} justify='space-between' className='p-2'>
<Typography.Text> <Typography.Text>
<FieldNumberOutlined className='pr-1' /> <FieldNumberOutlined className='pr-1' />
{orderDetail.order_no} {orderDetail.order_no}
<CopyOutlined onClick={() => {
navigator.clipboard.writeText(orderDetail.order_no)
message.success('已复制😀')
}}/>
</Typography.Text> </Typography.Text>
<Typography.Text> <Typography.Text>
<UserOutlined className=' pr-1' /> <UserOutlined className=' pr-1' />
@ -119,16 +117,10 @@ const OrderProfile = ({ coliSN, ...props }) => {
{customerDetail.email} {customerDetail.email}
</Typography.Text> </Typography.Text>
<Typography.Text> <Typography.Text>
<WhatsAppOutlined className='pr-1' /> <WhatsAppOutlined className=' pr-1' />
{isEmpty(customerDetail.whatsapp_phone_number) ? ( <Link to={`/order/chat/${coliSN}`} state={orderDetail}>
<Button type='text' onClick={() => setOpenWhatsApp(true)} size='small'> {customerDetail.whatsapp_phone_number}
设置 WhatsApp </Link>
</Button>
) : (
<Link to={`/order/chat/${coliSN}`} state={{...orderDetail, coli_guest_WhatsApp: customerDetail.whatsapp_phone_number, }}>
{customerDetail.whatsapp_phone_number}
</Link>
)}
</Typography.Text> </Typography.Text>
<Typography.Text> <Typography.Text>
<Tooltip title='出发日期'> <Tooltip title='出发日期'>
@ -202,7 +194,7 @@ const OrderProfile = ({ coliSN, ...props }) => {
<Divider orientation='left'> <Divider orientation='left'>
<Typography.Text strong>催信</Typography.Text> <Typography.Text strong>催信</Typography.Text>
</Divider> </Divider>
<Checkbox.Group key='substatus' className='px-2' value={remindCheckList} options={remindStatusOptions} onChange={handleSetRemindState} /> <Checkbox.Group key='substatus' className='px-2' value={[orderRemindState]} options={remindStatusOptions} onChange={handleSetRemindState} />
<Divider orientation='left'> <Divider orientation='left'>
<Typography.Text strong>表单信息</Typography.Text> <Typography.Text strong>表单信息</Typography.Text>
@ -223,19 +215,17 @@ const OrderProfile = ({ coliSN, ...props }) => {
<Typography.Text>{orderDetail.customer_request}</Typography.Text> <Typography.Text>{orderDetail.customer_request}</Typography.Text>
<Divider orientation='left'> <Divider orientation='left'>
<Typography.Text strong>外联备注</Typography.Text> <Typography.Text strong>外联备注</Typography.Text>
{/* <Tooltip title=''>
<EditOutlined className='pl-1' />
</Tooltip> */}
</Divider> </Divider>
<Typography.Text>{orderDetail.wl_memo}</Typography.Text> <Typography.Text>{orderDetail.wl_memo}</Typography.Text>
<Divider orientation='left'> <Divider orientation='left'>
<Typography.Text strong>附加信息</Typography.Text> <Typography.Text strong>附加信息</Typography.Text>
<Tooltip title='修改'> {/* <Tooltip title=''>
<EditOutlined <EditOutlined className='pl-1' />
className='pl-1' </Tooltip> */}
onClick={() => {
formExtra.setFieldsValue({ extra: orderDetail.COLI_Introduction })
setOpenExtra(true)
}}
/>
</Tooltip>
</Divider> </Divider>
<Typography.Text>{orderDetail.COLI_Introduction}</Typography.Text> <Typography.Text>{orderDetail.COLI_Introduction}</Typography.Text>
</Skeleton> </Skeleton>
@ -246,6 +236,7 @@ const OrderProfile = ({ coliSN, ...props }) => {
initialValues={{ comment: '' }} initialValues={{ comment: '' }}
scrollToFirstError scrollToFirstError
onFinish={(values) => { onFinish={(values) => {
console.log('Received values of form: ', values)
appendOrderComment(loginUser.userId, orderId, values.comment) appendOrderComment(loginUser.userId, orderId, values.comment)
.then(() => { .then(() => {
notification.success({ notification.success({
@ -274,90 +265,8 @@ const OrderProfile = ({ coliSN, ...props }) => {
</Form.Item> </Form.Item>
</Form> </Form>
</Drawer> </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 export default OrderProfile

@ -1,6 +1,6 @@
import React, { useMemo, useRef, useState } from 'react'; import React, { useMemo, useRef, useState } from 'react';
import { Select, Spin } from 'antd'; import { Select, Spin } from 'antd';
import { debounce } from '@haina/utils-commons'; import { debounce } from '@/utils/commons';
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) { function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const [fetching, setFetching] = useState(false); const [fetching, setFetching] = useState(false);

@ -6,20 +6,20 @@
// export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_144'; // prod: Slave // export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_144'; // prod: Slave
// debug: // debug:
// export const API_HOST = 'http://202.103.68.144:8888/v2'; export const API_HOST = 'http://202.103.68.144:8889/v2';
export const API_HOST_V3 = 'http://202.103.68.144:8889/v3';
// export const WS_URL = 'ws://202.103.68.144:8888'; // export const WS_URL = 'ws://202.103.68.144:8888';
// export const EMAIL_HOST = 'http://202.103.68.231:888/service-mail';
// export const WAI_HOST = 'http://47.83.248.120/api/v1'; // 香港服务器 // export const WAI_HOST = 'http://47.83.248.120/api/v1'; // 香港服务器
// export const WAI_HOST = 'http://47.254.53.81/api/v1'; // 美国服务器 export const WAI_HOST = 'http://47.254.53.81/api/v1'; // 美国服务器
// export const WAI_HOST = 'http://localhost:3031/api/v1'; // 美国服务器 // export const WAI_HOST = 'http://localhost:3031/api/v1'; // 美国服务器
export const WAI_SERVER_KEY = 'G-STR:WAI_SERVER' export const WAI_SERVER_KEY = 'G-STR:WAI_SERVER'
// prod:--------------------------------------------------------------------------------------------------
export const EMAIL_ATTA_HOST = 'https://p9axztuwd7x8a7.mycht.cn/attachment'; // 邮件附件 export const EMAIL_ATTA_HOST = 'https://p9axztuwd7x8a7.mycht.cn/attachment'; // 邮件附件
export const WAI_HOST = 'https://wai-server-qq4qmtq7wc9he4.mycht.cn/api/v1'; // prod:
// export const WAI_HOST = 'https://wai-server-qq4qmtq7wc9he4.mycht.cn/api/v1';
export const EMAIL_HOST = 'https://p9axztuwd7x8a7.mycht.cn/mail-server/service-mail'; export const EMAIL_HOST = 'https://p9axztuwd7x8a7.mycht.cn/mail-server/service-mail';
export const EMAIL_HOST_v3 = 'https://p9axztuwd7x8a7.mycht.cn/mail-server/service-mail'; // export const API_HOST = 'https://p9axztuwd7x8a7.mycht.cn/whatsapp_server/v2';
export const API_HOST = 'https://p9axztuwd7x8a7.mycht.cn/whatsapp_server/v2';
export const API_HOST_V3 = 'https://p9axztuwd7x8a7.mycht.cn/whatsapp_server/v3';
export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_server'; // prod: export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_server'; // prod:
export const VONAGE_URL = 'https://p9axztuwd7x8a7.mycht.cn/vonage-server'; // 语音和视频接口: export const VONAGE_URL = 'https://p9axztuwd7x8a7.mycht.cn/vonage-server'; // 语音和视频接口:
export const HT3 = process.env.NODE_ENV === 'production' ? 'https://p9axztuwd7x8a7.mycht.cn/ht3' : 'https://p9axztuwd7x8a7.mycht.cn/ht3'; export const HT3 = process.env.NODE_ENV === 'production' ? 'https://p9axztuwd7x8a7.mycht.cn/ht3' : 'https://p9axztuwd7x8a7.mycht.cn/ht3';
@ -38,9 +38,8 @@ export const DEFAULT_WABA = '+8617607730395';
const __BUILD_VERSION__ = `__BUILD_VERSION__`.replace(/"/g, '') const __BUILD_VERSION__ = `__BUILD_VERSION__`.replace(/"/g, '')
const __BUILD_DATE__ = `__BUILD_DATE__`; const __BUILD_DATE__ = `__BUILD_DATE__`;
const __GIT_HEAD__ = `__GIT_HEAD__`
export const BUILD_VERSION = import.meta.env.PROD ? __BUILD_VERSION__ : import.meta.env.MODE; export const BUILD_VERSION = process.env.NODE_ENV === 'production' ? __BUILD_VERSION__ : process.env.NODE_ENV;
export const BUILD_DATE = import.meta.env.PROD ? __BUILD_DATE__ : new Date().toLocaleString(); export const BUILD_DATE = process.env.NODE_ENV === 'production' ? __BUILD_DATE__ : new Date().toLocaleString();
export const GIT_HEAD = import.meta.env.PROD ? __GIT_HEAD__ : 'current';
export const POPUP_FEATURES = 'left=20,top=20,width=1000,height=800,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'; export const POPUP_FEATURES = 'left=20,top=20,width=1000,height=800,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no';

@ -1,7 +1,7 @@
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { fetchConversationsList, fetchOrderConversationsList, postNewOrEditConversationItem } from '@/actions/ConversationActions'; import { fetchConversationsList, fetchOrderConversationsList, postNewOrEditConversationItem } from '@/actions/ConversationActions';
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
const CHAT_ITEM_RECORD = { const CHAT_ITEM_RECORD = {
"sn": null, "sn": null,

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { isEmpty, objectMapper, olog, } from '@haina/utils-commons' import { isEmpty, objectMapper, olog, } from '@/utils/commons'
import { readIndexDB } from '@/utils/indexedDB' import { readIndexDB } from '@/utils/indexedDB'
import { getEmailDetailAction, postResendEmailAction, getSalesSignatureAction, getEmailOrderAction, queryEmailListAction, searchEmailListAction, getReminderEmailTemplateAction, saveEmailDraftOrSendAction, updateEmailAction, getEmailChangesChannel, EMAIL_CHANNEL_NAME } from '@/actions/EmailActions' import { getEmailDetailAction, postResendEmailAction, getSalesSignatureAction, getEmailOrderAction, queryEmailListAction, getReminderEmailTemplateAction, saveEmailDraftOrSendAction, updateEmailAction, getEmailChangesChannel, EMAIL_CHANNEL_NAME } from '@/actions/EmailActions'
import { App } from 'antd' import { App } from 'antd'
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { msgStatusRenderMapped } from '@/channel/bubbleMsgUtils'; import { msgStatusRenderMapped } from '@/channel/bubbleMsgUtils';
@ -132,12 +132,16 @@ export const useEmailDetail = (mai_sn=0, data={}, oid=0, markRead=false) => {
} }
const postEmailSaveOrSend = async (body, isDraft) => { const postEmailSaveOrSend = async (body, isDraft) => {
const { id: savedID } = await saveEmailDraftOrSendAction(body, isDraft) try {
setMaiSN(savedID) const { id: savedID } = await saveEmailDraftOrSendAction(body, isDraft)
if (isDraft) { setMaiSN(savedID)
refresh() if (isDraft) {
refresh()
}
return savedID
} catch (error) {
console.error(error);
} }
return savedID
}; };
return { loading, mailData, orderDetail, postEmailResend, postEmailSaveOrSend } return { loading, mailData, orderDetail, postEmailResend, postEmailSaveOrSend }
@ -167,7 +171,6 @@ export const useEmailList = (mailboxDirNode) => {
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [isFreshData, setIsFreshData] = useState(false) const [isFreshData, setIsFreshData] = useState(false)
const [refreshTrigger, setRefreshTrigger] = useState(0) const [refreshTrigger, setRefreshTrigger] = useState(0)
const [tempBreadcrumb, setTempBreadcrumb] = useState(null);
const refresh = useCallback(() => { const refresh = useCallback(() => {
setRefreshTrigger((prev) => prev + 1) setRefreshTrigger((prev) => prev + 1)
@ -175,7 +178,7 @@ export const useEmailList = (mailboxDirNode) => {
const { OPI_SN: opi_sn, COLI_SN, VKey, VParent, ApplyDate, OrderSourceType, IsTrue } = mailboxDirNode const { OPI_SN: opi_sn, COLI_SN, VKey, VParent, ApplyDate, OrderSourceType, IsTrue } = mailboxDirNode
const markAsUnread = useCallback( const markAsRead = useCallback(
async (sn_list) => { async (sn_list) => {
// 优化性能的话,需要更新 mailList 数据, // 优化性能的话,需要更新 mailList 数据,
// 但是更新 mailList 会造成页面全部刷新 // 但是更新 mailList 会造成页面全部刷新
@ -191,7 +194,7 @@ export const useEmailList = (mailboxDirNode) => {
await updateEmailAction({ await updateEmailAction({
opi_sn: opi_sn, opi_sn: opi_sn,
mai_sn_list: sn_list, mai_sn_list: sn_list,
set: { read: 0 }, set: { read: 1 },
}) })
}, },
[VKey], [VKey],
@ -239,7 +242,7 @@ export const useEmailList = (mailboxDirNode) => {
setIsFreshData(false) setIsFreshData(false)
return return
} }
setTempBreadcrumb(null)
setLoading(true) setLoading(true)
setError(null) setError(null)
setIsFreshData(false) setIsFreshData(false)
@ -268,19 +271,10 @@ export const useEmailList = (mailboxDirNode) => {
getMailList() getMailList()
// --- Setup Internal Event Listener --- // --- Setup Internal Event Listener ---
const handleInternalUpdate = (event) => { const handleInternalUpdate = (event) => {
// console.log(`🔔[useEmailList] Received internal event. `, event.detail) // console.log(`[useEmailList] Received internal event. `, event.detail)
if (isEmpty(event.detail)) { if (event.detail && event.detail.type === 'listrow') {
return false;
}
const { type, } = event.detail
if (type === 'listrow') {
loadMailListFromCache() loadMailListFromCache()
} }
if (type === 'maillist-search-result') {
const { data, query } = event.detail
setMailList(data)
setTempBreadcrumb([{title: '查找邮件:'+query, iconIndex: 'search'}]);
}
} }
internalEventEmitter.on(EMAIL_CHANNEL_NAME, handleInternalUpdate) internalEventEmitter.on(EMAIL_CHANNEL_NAME, handleInternalUpdate)
@ -288,20 +282,11 @@ export const useEmailList = (mailboxDirNode) => {
const channel = getEmailChangesChannel() const channel = getEmailChangesChannel()
const handleMessage = (event) => { const handleMessage = (event) => {
// console.log(`[useEmailList] Received channel event. `, event.data) // console.log(`[useEmailList] Received channel event. `, event.data)
if (isEmpty(event.data)) {
return false;
}
const { type, } = event.data
const cacheKey = isEmpty(COLI_SN) ? `dir-${VKey}` : `order-${VKey}` const cacheKey = isEmpty(COLI_SN) ? `dir-${VKey}` : `order-${VKey}`
if (type === 'listrow' && cacheKey === event.data.listKey) { if (event.data.type === 'listrow' && cacheKey === event.data.listKey) {
// cacheKey 不相同时, 不需要更新; 邮箱目录不相同 // cacheKey 不相同时, 不需要更新; 邮箱目录不相同
loadMailListFromCache(event.data) loadMailListFromCache(event.data)
} }
if (type === 'maillist-search-result') {
// 搜索的结果不需要更新所有页面
// const { data } = event.detail
// setMailList(data)
}
} }
channel.addEventListener('message', handleMessage) channel.addEventListener('message', handleMessage)
@ -312,7 +297,7 @@ export const useEmailList = (mailboxDirNode) => {
} }
}, [getMailList]) }, [getMailList])
return { loading, isFreshData, error, mailList, tempBreadcrumb, refresh, markAsUnread, markAsProcessed, markAsDeleted } return { loading, isFreshData, error, mailList, refresh, markAsRead, markAsProcessed, markAsDeleted }
} }
const orderMailTypes = new Map([ const orderMailTypes = new Map([
@ -373,14 +358,13 @@ const orderMailTypes = new Map([
['48055', 'GH海外PostSurvey'], ['48055', 'GH海外PostSurvey'],
]) ])
export const emailTemplates = [ export const emailTemplates = [
{ type: 'RemindOneWL', index: 1, key: '1@RemindOneWL', value: '1@RemindOneWL', label: '一催模版1鼓励客人回复和讨论行程' }, { type: 'RemindOneWL', index: 1, key: '1@RemindOneWL', value: '1@RemindOneWL', label: '一催模板一,询问客人是否收到报价信' },
{ type: 'RemindOneWL', index: 2, key: '2@RemindOneWL', value: '2@RemindOneWL', label: '一催模版2询问客人对于报价是否有疑问' }, { type: 'RemindOneWL', index: 2, key: '2@RemindOneWL', value: '2@RemindOneWL', label: '一催模板二,询问客人是否修改行程' },
{ type: 'divider' }, { type: 'divider' },
{ type: 'RemindTwoWL', index: 1, key: '1@RemindTwoWL', value: '1@RemindTwoWL', label: '二催模版1省钱的方式' }, { type: 'RemindTwoWL', index: 1, key: '1@RemindTwoWL', value: '1@RemindTwoWL', label: '二催模板一,询问客人对行程的看法' },
{ type: 'RemindTwoWL', index: 2, key: '2@RemindTwoWL', value: '2@RemindTwoWL', label: '二催模版2Why us' }, { type: 'RemindTwoWL', index: 2, key: '2@RemindTwoWL', value: '2@RemindTwoWL', label: '二催模板二,表达服务的意识' },
{ type: 'divider' }, { type: 'divider' },
{ type: 'RemindThreeWL', index: 1, key: '1@RemindThreeWL', value: '1@RemindThreeWL', label: '三催模版1再次强调服务' }, { type: 'RemindThreeWL', index: 1, key: '1@RemindThreeWL', value: '1@RemindThreeWL', label: '三催模板三,强调价格有效期' },
{ type: 'RemindThreeWL', index: 2, key: '2@RemindThreeWL', value: '2@RemindThreeWL', label: '三催模版2客人常见问题询问' },
]; ];
export const emailTemplateMap = emailTemplates.reduce((acc, cur) => { export const emailTemplateMap = emailTemplates.reduce((acc, cur) => {
if (cur.type === 'divider') { if (cur.type === 'divider') {
@ -432,12 +416,12 @@ export const useEmailTemplate = (templateKey, params) => {
} }
export const mailboxSystemDirs = [ export const mailboxSystemDirs = [
{ key: 1, value: 1, label: '收件箱' }, { key: 1, value: 1, label: '收件箱' },
{ key: 2, value: 2, label: '未读邮件' }, { key: 2, value: 2, label: '未读邮件' },
{ key: 3, value: 3, label: '已发邮件' }, { key: 3, value: 3, label: '已发邮件' },
{ key: 4, value: 4, label: '待发邮件' }, { key: 4, value: 4, label: '待发邮件' },
{ key: 5, value: 5, label: '草稿' }, { key: 5, value: 5, label: '草稿' },
{ key: 6, value: 6, label: '垃圾邮件' }, { key: 6, value: 6, label: '垃圾邮件' },
{ key: 7, value: 7, label: '已处理邮件' }, { key: 7, value: 7, label: '已处理邮件' },
] ]

@ -1,5 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { isEmpty } from '@haina/utils-commons' import { isEmpty } from '@/utils/commons'
import { App, notification } from 'antd' import { App, notification } from 'antd'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'

@ -1,124 +0,0 @@
import { useCallback } from 'react';
import useConversationStore from '@/stores/ConversationStore';
const useShortUrlChange = () => {
const setGlobalNotify = useConversationStore((state) => state.setGlobalNotify);
const apiPrefix = {
"japanhighlights.com": "https://www.japanhighlights.com/index.php",
"chinahighlights.com": "https://www.chinahighlights.com/guide-use.php",
"highlightstravel.com": "https://www.highlightstravel.com/index.php",
"asiahighlights.com": "https://www.asiahighlights.com/index.php",
"globalhighlights.com": "https://www.globalhighlights.com/index.php",
};
const fetchNowConversationsitems = async (base64Url, apiUrl) => {
try {
const formData = new FormData();
formData.append(' url', base64Url);
formData.append('type', 'info');
const response = await fetch(`${apiUrl}/apps/short_link/index/create`, {
method: 'POST',
body: formData,
});
const data = await response.json();
if (data[0].name == 'ok') {
return data[0].value;
}
return null;
} catch (error) {
console.error('获取短链接转换内容失败:', error);
return null;
}
};
const urlBase64 = (longUrl) => {
try {
const extracted2 = longUrl.match(/https:\/\/www\.([^\/]+)/)?.[1] || '';
const encoder = new TextEncoder();
const utf8Bytes = encoder.encode(longUrl);
const base64Url = btoa(String.fromCharCode(...utf8Bytes));
return { base64Url, extracted2 };
} catch (error) {
setGlobalNotify([{
key: Date.now().toString(),
title: '错误',
content: '转换失败请检查输入的URL是否正确',
type: 'error'
}]);
console.error('URL转换错误:', error);
return { base64Url: '', extracted2: '' };
}
};
const normalizeUrl = (longUrl) => {
try {
const hasProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(longUrl);
const url = new URL(hasProtocol ? longUrl : `http://${longUrl}`);
const params = Object.fromEntries(url.searchParams.entries());
return {
url: url.href,
valid: true,
origin: url.origin,
hostname: url.hostname,
pathname: url.pathname,
params
};
} catch (e) {
return { valid: false, error: "Invalid URL" };
}
};
const convertUrl = useCallback(async (longUrl) => {
const normalizedUrl = normalizeUrl(longUrl);
if (!normalizedUrl.valid || Object.keys(normalizedUrl?.params || {}).length === 0) {
setGlobalNotify([{
key: Date.now().toString(),
title: '错误',
content: '没有需要转换的长链接, 请重新输入',
type: 'warning'
}]);
return null;
}
const { base64Url, extracted2 } = urlBase64(normalizedUrl.url);
if (base64Url) {
const apiUrl = apiPrefix[extracted2] || apiPrefix["chinahighlights.com"];
const extracted1 = apiUrl.match(/^https?:\/\/[^\/]*\.com/)?.[0] || '';
const data = await fetchNowConversationsitems(base64Url, apiUrl);
if (data) {
const resultShortUrl = extracted1 + data.isl_link;
setGlobalNotify([{
key: Date.now().toString(),
title: '成功',
content: '转换成功!',
type: 'success'
}]);
return resultShortUrl;
} else {
setGlobalNotify([{
key: Date.now().toString(),
title: '错误',
content: '转换失败请检查输入的URL是否正确',
type: 'error'
}]);
return null;
}
} else {
setGlobalNotify([{
key: Date.now().toString(),
title: '错误',
content: 'URL格式不正确请输入完整的URL',
type: 'error'
}]);
return null;
}
}, [setGlobalNotify]);
return { convertUrl };
};
export default useShortUrlChange;

@ -27,7 +27,6 @@ import ChatAssign from '@/views/Conversations/ChatAssign'
import DingdingLogin from '@/views/dingding/Login' import DingdingLogin from '@/views/dingding/Login'
import DingdingQRCode from '@/views/dingding/QRCode' import DingdingQRCode from '@/views/dingding/QRCode'
import DingdingAuthCode from '@/views/dingding/AuthCode' import DingdingAuthCode from '@/views/dingding/AuthCode'
import LocalWhatsAppQRCode from '@/views/accounts/LocalWhatsAppQRCode'
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import '@/assets/index.css' import '@/assets/index.css'
@ -36,10 +35,7 @@ import NewEmail from '@/views/NewEmail'
import EmailDetailWindow from '@/views/EmailDetailWindow' import EmailDetailWindow from '@/views/EmailDetailWindow'
import { executeDailyCleanupTask, setupDailyMidnightCleanupScheduler } from '@/utils/indexedDB' import { executeDailyCleanupTask, setupDailyMidnightCleanupScheduler } from '@/utils/indexedDB'
import { appendRequestHeader } from '@haina/utils-request';
import { BUILD_VERSION } from '@/config'
appendRequestHeader('X-Web-Version', BUILD_VERSION);
useAuthStore.getState().loadUserSession() useAuthStore.getState().loadUserSession()
const isMobileApp = const isMobileApp =
@ -111,7 +107,6 @@ const router = createBrowserRouter([
{ path: 'dingding/callback', element: <DingdingCallback /> }, { path: 'dingding/callback', element: <DingdingCallback /> },
{ path: 'dingding/qr-code', element: <DingdingQRCode /> }, { path: 'dingding/qr-code', element: <DingdingQRCode /> },
{ path: 'dingding/auth-code', element: <DingdingAuthCode /> }, { path: 'dingding/auth-code', element: <DingdingAuthCode /> },
{ path: 'whatsapp/qr-code', element: <LocalWhatsAppQRCode /> },
], ],
}, },
]) ])

@ -1,4 +1,4 @@
import { clearAllCaches } from '@haina/utils-commons'; import { clearAllCaches } from '@/utils/commons';
import { BUILD_VERSION, BUILD_DATE } from '@/config' import { BUILD_VERSION, BUILD_DATE } from '@/config'
const MaintenancePage = () => { const MaintenancePage = () => {

@ -1,7 +1,7 @@
import { create } from 'zustand' import { create } from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
import { fetchJSON } from '@haina/utils-request' import { fetchJSON } from '@/utils/request'
import { isEmpty, isNotEmpty } from '@haina/utils-commons' import { isEmpty, isNotEmpty } from '@/utils/commons'
import { API_HOST, BUILD_VERSION } from '@/config' import { API_HOST, BUILD_VERSION } from '@/config'
import { usingStorage } from '@/utils/usingStorage'; import { usingStorage } from '@/utils/usingStorage';
@ -70,8 +70,9 @@ const useAuthStore = create(devtools((set, get) => ({
) )
if (json.errcode === 0 && isNotEmpty(json.result.opisn)) { if (json.errcode === 0 && isNotEmpty(json.result.opisn)) {
// TODO保存个人 WhatsApp 服务器地址
const waiServer = json.result.whatsappinfo.length > 0 ? json.result.whatsappinfo[0].wai_server : '' const waiServer = json.result.whatsappinfo.length > 0 ? json.result.whatsappinfo[0].wai_server : ''
setStorage(WAI_SERVER_KEY, waiServer) setStorage('G-STR:WAI_SERVER', waiServer)
set(() => ({ set(() => ({
loginUser: { loginUser: {
userId: json.result.opisn, userId: json.result.opisn,
@ -107,67 +108,6 @@ const useAuthStore = create(devtools((set, get) => ({
} }
}, },
parseLoginJson: (json) => {
const { setStorage } = usingStorage()
if (json.errcode === 0 && isNotEmpty(json.result.opisn)) {
const waiServer = json.result.whatsappinfo.length > 0 ? json.result.whatsappinfo[0].wai_server : ''
setStorage(WAI_SERVER_KEY, waiServer)
const parsedUser = {
userId: json.result.opisn,
userIdStr: json.result?.accountlist
.map((acc) => {
return acc.OPI_SN
})
.join(','),
emailList: json.result?.emaillist.map(item => {
return {
opi_sn: item.opi_sn,
mat_sn: item.mat_sn,
email: item.email,
default: item.Isdefaultemail == 1,
backup: item.Isbakemail == 1,
}
}),
whatsAppBusiness: json.result.whatsappinfo.length > 0 ? json.result.whatsappinfo[0].whatsapp_waba : '',
whatsAppNo: json.result.whatsappinfo.length > 0 ? json.result.whatsappinfo[0].whatsapp_wa : '',
accountName: json.result.opicode,
username: json.result.nick,
avatarUrl: json.result.avatarUrl,
mobile: '+' + json.result.stateCode + '-' + json.result.mobile,
email: json.result.email,
openId: json.result.openId,
accountList: json.result.accountlist,
}
return parsedUser
} else { return null }
},
// 钉钉免登后获取用户信息
loginByJSAuth: async (authCode) => {
const { saveUserSession, setLoginStatus, parseLoginJson } = get()
setLoginStatus(200)
const json = await fetchJSON(
'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/Getusers_auth_wa',
{ code: authCode },
)
const parsedUser = parseLoginJson(json)
if (parsedUser) {
set(() => ({
loginUser: parsedUser,
}))
saveUserSession()
setLoginStatus(302)
} else {
setLoginStatus(403)
}
},
getPrimaryEmail: () => { getPrimaryEmail: () => {
const { loginUser } = get() const { loginUser } = get()
@ -218,7 +158,13 @@ const useAuthStore = create(devtools((set, get) => ({
loadUserSession: () => { loadUserSession: () => {
let sessionData = window.sessionStorage.getItem('GLOBAL_SALES_LOGIN_USER') let sessionData = window.sessionStorage.getItem('GLOBAL_SALES_LOGIN_USER')
// if (import.meta.env.DEV) sessionData ='{"userId":"155","userIdStr":"155","emailList":[],"whatsAppBusiness":"+8617607730395","whatsAppNo":null,"username":"尹诚诚","avatarUrl":"https://static-legacy.dingtalk.com/media/lADPBE1XYG_HAcDNAgDNAgA_512_512.jpg","mobile":"+86-18507832160","email":"ycc@hainatravel.com","openId":"K8BNXMf8ESSr1DzLVUrX7wiEiE","accountList":[{"OPI_SN":155,"OPI_Code":"YCC","OPI_NameCN":"尹诚诚","OPI_DEI_SN":1,"OPI_NameEN":"Yin Chengcheng"}]}'
// if (window.location.hostname === '202.103.68.93' && window.location.port === '4173' && isEmpty(sessionData)) {
// sessionData = `{"userId":"383","userIdStr":"383,609","emailList":[{"opi_sn":383,"mat_sn":760,"email":"lyj@asiahighlights.com","default":false,"backup":false},{"opi_sn":383,"mat_sn":759,"email":"lyj@chinahighlights.com","default":true,"backup":false},{"opi_sn":383,"mat_sn":758,"email":"lyj@hainatravel.com","default":false,"backup":false}],"username":"廖一军","avatarUrl":"https://static-legacy.dingtalk.com/media/lALPBDDrhXr716HNAoDNAoA_640_640.png","mobile":"+86-18777396951","email":"lyj@hainatravel.com","whatsAppBusiness":"8617458471254","openId":"iioljiPmZ4RPoOYpkFiSn7IKAiEiE","accountList":[{"OPI_SN":383,"OPI_Code":"LYJ","OPI_NameCN":"廖一军","OPI_DEI_SN":7,"OPI_NameEN":"Jimmy Liow"},{"OPI_SN":609,"OPI_Code":"LYJAH","OPI_NameCN":"廖一军ah","OPI_DEI_SN":28,"OPI_NameEN":"Jimmy Liow"}]}`
// window.localStorage.setItem('GLOBAL_SALES_LOGIN_USER', sessionData)
// }
if (import.meta.env.DEV && isEmpty(sessionData)) { if (import.meta.env.DEV && isEmpty(sessionData)) {
sessionData = window.localStorage.getItem('GLOBAL_SALES_LOGIN_USER') sessionData = window.localStorage.getItem('GLOBAL_SALES_LOGIN_USER')
} }
@ -261,6 +207,26 @@ const useAuthStore = create(devtools((set, get) => ({
} }
}) })
}, },
sendNotify: async () => {
const { loginUser } = get()
const notifyUrl = 'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/SendMDMsgByDingRobotToGroup'
const params = {
groupid: 'cidFtzcIzNwNoiaGU9Q795CIg==',
msgTitle: '有人求助',
msgText: loginUser.username + '上传了销售平台' + BUILD_VERSION + '的日志'
};
return fetchJSON(notifyUrl, params)
.then(json => {
if (json.errcode === 0) {
console.info('发送通知成功')
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
}), { name: 'authStore' })) }), { name: 'authStore' }))
export default useAuthStore export default useAuthStore

@ -1,7 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { VonageClient, ClientConfig, ConfigRegion, LoggingLevel } from '@vonage/client-sdk' import { VonageClient, ClientConfig, ConfigRegion, LoggingLevel } from '@vonage/client-sdk'
import { fetchJSON } from "@haina/utils-request"; import { fetchJSON } from "@/utils/request";
import { prepareUrl, isNotEmpty, } from "@haina/utils-commons"; import { prepareUrl, isNotEmpty, } from "@/utils/commons";
import { VONAGE_URL, DATETIME_FORMAT } from "@/config"; import { VONAGE_URL, DATETIME_FORMAT } from "@/config";
import dayjs from "dayjs"; import dayjs from "dayjs";

@ -1,6 +1,6 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { RealTimeAPI } from '@/channel/realTimeAPI'; import { RealTimeAPI } from '@/channel/realTimeAPI';
import { olog, isEmpty, groupBy, sortArrayByOrder, pick, sortKeys, omit, sortObjectsByKeysMap, merge } from '@haina/utils-commons'; import { olog, isEmpty, groupBy, sortArrayByOrder, pick, sortKeys, omit, sortObjectsByKeysMap } from '@/utils/commons';
import { logWebsocket, clean7DaysWebsocketLog } from '@/utils/indexedDB' import { logWebsocket, clean7DaysWebsocketLog } from '@/utils/indexedDB'
import { receivedMsgTypeMapped, handleNotification } from '@/channel/bubbleMsgUtils'; import { receivedMsgTypeMapped, handleNotification } from '@/channel/bubbleMsgUtils';
import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions'; import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions';
@ -197,10 +197,10 @@ const websocketSlice = (set, get) => ({
// console.log('msgRender msgUpdate', msgRender, msgUpdate); // console.log('msgRender msgUpdate', msgRender, msgUpdate);
if (['email.updated', 'email.inbound.received',].includes(resultType)) { if (['email.updated', 'email.inbound.received',].includes(resultType)) {
updateMailboxCount({ opi_sn: msgObj.opi_sn }) updateMailboxCount({ opi_sn: msgObj.opi_sn })
// if (!isEmpty(msgRender)) { if (!isEmpty(msgRender)) {
// const msgNotify = receivedMsgTypeMapped[resultType].contentToNotify(msgObj); const msgNotify = receivedMsgTypeMapped[resultType].contentToNotify(msgObj);
// addGlobalNotify(msgNotify); addGlobalNotify(msgNotify);
// } }
return false; return false;
} }
if ([ if ([
@ -238,11 +238,9 @@ const websocketSlice = (set, get) => ({
// }, 60_000); // }, 60_000);
} }
// 会话表 更新 // 会话表 更新
if (['session.new', 'session.updated'].includes(resultType) if (['session.new', 'session.updated'].includes(resultType)) {
&& result.webhooksource !== 'email'
) {
const sessionList = receivedMsgTypeMapped[resultType].getMsg(result); const sessionList = receivedMsgTypeMapped[resultType].getMsg(result);
addToConversationList(sessionList || [], 'top') addToConversationList(sessionList, 'top');
} }
// console.log('handleMessage*******************'); // console.log('handleMessage*******************');
}, },
@ -278,17 +276,13 @@ const conversationSlice = (set, get) => ({
// 让当前会话显示在页面上 // 让当前会话显示在页面上
let _tmpCurrentMsgs = []; let _tmpCurrentMsgs = [];
if (currentConversation.sn) { if (currentConversation.sn) {
// _tmpCurrentMsgs = activeConversations[currentConversation.sn]; _tmpCurrentMsgs = activeConversations[currentConversation.sn];
} }
const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
const indexCurrent = currentConversation.sn ? conversationsList.findIndex(ele => Number(ele.sn) === Number(currentConversation.sn)) : -1; if (currentConversation.sn) {
if (indexCurrent !== -1) { const hasCurrent = Object.keys(conversationsMapped).findIndex(sn => Number(sn) === Number(currentConversation.sn)) !== -1;
const [currentElement] = conversationsList.splice(indexCurrent, 1); conversationsMapped[currentConversation.sn] = _tmpCurrentMsgs;
conversationsList.unshift(currentElement); // Add to top const _len = hasCurrent ? 0 : conversationsList.unshift(currentConversation);
// const hasCurrent = Object.keys(conversationsMapped).findIndex(sn => Number(sn) === Number(currentConversation.sn)) !== -1;
// conversationsMapped[currentConversation.sn] = _tmpCurrentMsgs;
// conversationsList.unshift(hasCurrent ? )
// hasCurrent ? 0 : conversationsList.unshift(currentConversation);
} }
const { topList, pageList } = sortConversationList(conversationsList); const { topList, pageList } = sortConversationList(conversationsList);
@ -324,11 +318,9 @@ const conversationSlice = (set, get) => ({
const needUpdateCurrent = -1 !== newList.findIndex(row => Number(row.sn) === Number(currentConversation.sn)); const needUpdateCurrent = -1 !== newList.findIndex(row => Number(row.sn) === Number(currentConversation.sn));
const updateCurrent = needUpdateCurrent ? { currentConversation: newList.find(row => Number(row.sn) === Number(currentConversation.sn)) } : {}; const updateCurrent = needUpdateCurrent ? { currentConversation: newList.find(row => Number(row.sn) === Number(currentConversation.sn)) } : {};
// 让当前会话显示在页面上 // 让当前会话显示在页面上
const indexCurrent = currentConversation.sn ? mergedList.findIndex(ele => Number(ele.sn) === Number(currentConversation.sn)) : -1; if (currentConversation.sn) {
if (indexCurrent !== -1 ) { const hasCurrent = -1 !== Object.keys(mergedListMsgs).findIndex(sn => Number(sn) === Number(currentConversation.sn));
const [currentElement] = mergedList.splice(indexCurrent, 1); hasCurrent ? 0 : mergedList.unshift(currentConversation);
mergedList.unshift(currentElement); // Add to top
// hasCurrent ? 0 : mergedList.unshift(currentConversation);
} }
const refreshTotalNotify = mergedList.reduce((r, c) => r+(c.unread_msg_count === UNREAD_MARK ? 0 : c.unread_msg_count), 0); const refreshTotalNotify = mergedList.reduce((r, c) => r+(c.unread_msg_count === UNREAD_MARK ? 0 : c.unread_msg_count), 0);

@ -1,8 +1,8 @@
import { create } from 'zustand' import { create } from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@haina/utils-request' import { fetchJSON, postForm } from '@/utils/request'
import { HT3, EMAIL_HOST } from '@/config' import { HT3, EMAIL_HOST } from '@/config'
import { isNotEmpty, prepareUrl } from '@haina/utils-commons' import { isNotEmpty, prepareUrl } from '@/utils/commons'
export const useCustomerRelationStore = create((set, get) => ({ export const useCustomerRelationStore = create((set, get) => ({
loading: false, loading: false,

@ -1,5 +1,5 @@
import { getEmailDirAction, getMailboxCountAction, getRootMailboxDirAction, getEmailChangesChannel, EMAIL_CHANNEL_NAME } from '@/actions/EmailActions' import { getEmailDirAction, getMailboxCountAction, getRootMailboxDirAction, getEmailChangesChannel, EMAIL_CHANNEL_NAME } from '@/actions/EmailActions'
import { buildTree, isEmpty, olog, sortArrayByOrder } from '@haina/utils-commons' import { buildTree, isEmpty, olog, sortArrayByOrder } from '@/utils/commons'
import { readIndexDB, writeIndexDB, createIndexedDBStore, clean7DaysMailboxLog } from '@/utils/indexedDB'; import { readIndexDB, writeIndexDB, createIndexedDBStore, clean7DaysMailboxLog } from '@/utils/indexedDB';
import { internalEventEmitter } from '@/utils/EventEmitterService'; import { internalEventEmitter } from '@/utils/EventEmitterService';
@ -124,7 +124,7 @@ const emailSlice = (set, get) => ({
let isNeedRefresh = refreshNow let isNeedRefresh = refreshNow
if (!isEmpty(readCache)) { if (!isEmpty(readCache)) {
setMailboxNestedDirsActive(readCache?.tree || []) setMailboxNestedDirsActive(readCache?.tree || [])
isNeedRefresh = refreshNow || Date.now() - readCache.treeTimestamp > 1 * 60 * 60 * 1000 isNeedRefresh = refreshNow || Date.now() - readCache.timestamp > 1 * 60 * 60 * 1000
// isNeedRefresh = true; // test: 0 // isNeedRefresh = true; // test: 0
} }
if (isEmpty(readCache) || isNeedRefresh) { if (isEmpty(readCache) || isNeedRefresh) {
@ -139,14 +139,9 @@ const emailSlice = (set, get) => ({
return false return false
}, },
/** // 更新数量
* 更新数量
* @usage 1. 邮件列表页切换用户时
* @usage 2. 收到新邮件推送时
*
*/
updateMailboxCount: async ({ opi_sn }) => { updateMailboxCount: async ({ opi_sn }) => {
// const { setMailboxNestedDirsActive } = get() const { setMailboxNestedDirsActive } = get()
await getMailboxCountAction({ opi_sn }) await getMailboxCountAction({ opi_sn })
// const readCache = await readIndexDB(Number(opi_sn), 'dirs', 'mailbox') // const readCache = await readIndexDB(Number(opi_sn), 'dirs', 'mailbox')
// if (!isEmpty(readCache)) { // if (!isEmpty(readCache)) {
@ -155,7 +150,7 @@ const emailSlice = (set, get) => ({
}, },
async initMailbox({ opi_sn, dei_sn, userIdStr }) { async initMailbox({ opi_sn, dei_sn, userIdStr }) {
olog('Initialize Mailbox ---- ') olog('initMailbox ---- ')
const { currentMailboxOPI, setCurrentMailboxOPI, setCurrentMailboxDEI, getOPIEmailDir, setMailboxNestedDirsActive, } = get() const { currentMailboxOPI, setCurrentMailboxOPI, setCurrentMailboxDEI, getOPIEmailDir, setMailboxNestedDirsActive, } = get()
createIndexedDBStore(['dirs', 'maillist', 'listrow', 'mailinfo', 'draft'], 'mailbox') createIndexedDBStore(['dirs', 'maillist', 'listrow', 'mailinfo', 'draft'], 'mailbox')
setCurrentMailboxOPI(opi_sn) setCurrentMailboxOPI(opi_sn)

@ -1,8 +1,8 @@
import { create } from 'zustand' import { create } from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm, postJSON } from '@haina/utils-request' import { fetchJSON, postForm } from '@/utils/request'
import { API_HOST, API_HOST_V3, EMAIL_HOST } from '@/config' import { API_HOST, EMAIL_HOST } from '@/config'
import { isEmpty, isNotEmpty, prepareUrl, uniqWith } from '@haina/utils-commons' import { isNotEmpty, prepareUrl, uniqWith } from '@/utils/commons'
const initialState = { const initialState = {
orderList: [], orderList: [],
@ -11,261 +11,213 @@ const initialState = {
lastQuotation: {}, lastQuotation: {},
quotationList: [], quotationList: [],
otherEmailList: [], otherEmailList: [],
remindCheckList: [], };
}
export const useOrderStore = create(devtools((set, get) => ({
export const useOrderStore = create(
devtools( ...initialState,
(set, get) => ({ drawerOpen: false,
...initialState,
drawerOpen: false, resetOrderStore: () => set(initialState),
resetOrderStore: () => set(initialState), openDrawer: () => {
set(() => ({
openDrawer: () => { drawerOpen: true
set(() => ({ }))
drawerOpen: true, },
}))
}, closeDrawer: () => {
set(() => ({
closeDrawer: () => { drawerOpen: false
set(() => ({ }))
drawerOpen: false, },
}))
}, fetchOrderList: async (formValues, loginUser) => {
let fetchOrderUrl = `${API_HOST}/getwlorder?opisn=${loginUser.userIdStr}&otype=${formValues.type}`
fetchOrderList: async (formValues, loginUser) => { const params = {};
let fetchOrderUrl = `${API_HOST}/getwlorder?opisn=${loginUser.userIdStr}&otype=${formValues.type}`
const params = {} if (formValues.type === 'advance') {
fetchOrderUrl = `${API_HOST}/getdvancedwlorder?opisn=${loginUser.userIdStr}`;
if (formValues.type === 'advance') { const { type, ...formParams } = formValues;
fetchOrderUrl = `${API_HOST}/getdvancedwlorder?opisn=${loginUser.userIdStr}` Object.assign(params, formParams)
const { type, ...formParams } = formValues }
Object.assign(params, formParams)
return fetchJSON(fetchOrderUrl, params)
.then(json => {
if (json.errcode === 0) {
const _result = json.result.map((order) => { return { ...order, key: order.COLI_ID } })
const _result_unique = uniqWith(_result, (a, b) => a.COLI_SN === b.COLI_SN)
set(() => ({
orderList: _result_unique,
}))
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
} }
})
return fetchJSON(fetchOrderUrl, params).then((json) => {
if (json.errcode === 0) { },
const _result = json.result.map((order) => {
return { ...order, key: order.COLI_ID } fetchOrderDetail: (colisn) => {
}) return fetchJSON(`${API_HOST}/getorderinfo`, { colisn })
const _result_unique = uniqWith(_result, (a, b) => a.COLI_SN === b.COLI_SN) .then(json => {
set(() => ({ if (json.errcode === 0 && json.result.length > 0) {
orderList: _result_unique, const orderResult = json.result[0]
})) set(() => ({
} else { orderDetail: {...orderResult, coli_sn: colisn },
throw new Error(json?.errmsg + ': ' + json.errcode) customerDetail: orderResult.contact.length > 0 ? orderResult.contact[0] : {},
} // lastQuotation: orderResult.quotes.length > 0 ? orderResult.quotes[0] : {},
}) // quotationList: orderResult.quotes,
}, }))
return {
fetchOrderDetail: (colisn) => { orderDetail: {...orderResult, coli_sn: colisn },
return fetchJSON(`${API_HOST}/getorderinfo`, { colisn }).then((json) => { customerDetail: orderResult.contact.length > 0 ? orderResult.contact[0] : {},
if (json.errcode === 0 && json.result.length > 0) { // lastQuotation: orderResult.quotes.length > 0 ? orderResult.quotes[0] : {},
const orderResult = json.result[0] // quotationList: orderResult.quotes,
set(() => ({
remindCheckList: transferRemind2Checklist(orderResult.remindstate),
orderDetail: { ...orderResult, coli_sn: colisn },
customerDetail: orderResult.contact.length > 0 ? orderResult.contact[0] : {},
}))
return {
orderDetail: { ...orderResult, coli_sn: colisn },
customerDetail: orderResult.contact.length > 0 ? orderResult.contact[0] : {},
}
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
} }
}) } else {
}, throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
appendOrderComment: async (opi_sn, coli_sn, comment) => {
const { fetchOrderDetail } = get()
const formData = new FormData()
formData.append('opi_sn', opi_sn)
formData.append('coli_sn', coli_sn)
formData.append('remark', comment)
return postForm(`${API_HOST}/remark_order`, formData)
.then(json => {
if (json.errcode === 0) {
return fetchOrderDetail(coli_sn)
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
generatePayment: async (formValues) => {
const formData = new FormData()
formData.append('descriptions', formValues.description)
formData.append('currency', formValues.currency)
formData.append('lgc', formValues.langauge)
formData.append('amount', formValues.amount)
formData.append('coli_id', formValues.orderNumber)
formData.append('ordertype', formValues.orderType)
formData.append('opisn', formValues.userId)
formData.append('paytype', 'SYT')
formData.append('wxzh', 'cht')
formData.append('fq', 0)
formData.append('onlyusa', 0)
formData.append('useyhm', 0)
return postForm(`${API_HOST}/generate_payment_links`, formData)
.then(json => {
if (json.errcode === 0) {
return json.result
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
fetchHistoryOrder: (userId, email, whatsappid='') => {
return fetchJSON(`${API_HOST}/query_guest_order`, { opisn: userId, whatsappid, email: email })
.then(json => {
if (json.errcode === 0) {
return json.result
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
updateRemindState: async (orderId, checkedValue) => { },
set(() => ({
remindCheckList: checkedValue, importEmailMessage: ({ orderId, orderNumber }) => {
})) return fetchJSON(`${API_HOST}/generate_email_msg`, { coli_sn: orderId, coli_id: orderNumber })
const finalState = { .then(json => {
'FirstRemind': checkedValue.includes('FirstRemind') ? 1 : 0, if (json.errcode === 0) {
'SecondRemind': checkedValue.includes('SecondRemind') ? 1 : 0, return json
'ThirdRemind': checkedValue.includes('ThirdRemind') ? 1 : 0, } else {
'important': checkedValue.includes('important') ? 1 : 0, throw new Error(json?.errmsg + ': ' + json.errcode)
'sendsurvey': checkedValue.includes('sendsurvey') ? 1 : 0,
} }
const { errcode, result } = await postJSON(`${API_HOST}/SetRemindState`, { coli_sn: orderId, remindstate: JSON.stringify(finalState)}) })
return errcode === 0 ? result : {} },
},
batchImportEmailMessage: () => {
appendOrderComment: async (opi_sn, coli_sn, comment) => { const { orderList } = get()
const { fetchOrderDetail } = get()
const formData = new FormData() const orderPromiseList = orderList.map(order => {
formData.append('opi_sn', opi_sn) return new Promise((resolve, reject) => {
formData.append('coli_sn', coli_sn) fetchJSON(`${API_HOST}/generate_email_msg`, { coli_sn: order.COLI_SN, coli_id: order.COLI_ID })
formData.append('remark', comment) .then(json => {
if (json.errcode === 0) {
return postForm(`${API_HOST}/remark_order`, formData).then((json) => { resolve(json)
if (json.errcode === 0) { } else {
return fetchOrderDetail(coli_sn) reject(json?.errmsg + ': ' + json.errcode)
} else { }
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
generatePayment: async (formValues) => {
const formData = new FormData()
formData.append('descriptions', formValues.description)
formData.append('currency', formValues.currency)
formData.append('lgc', formValues.langauge)
formData.append('amount', formValues.amount)
formData.append('coli_id', formValues.orderNumber)
formData.append('ordertype', formValues.orderType)
formData.append('opisn', formValues.userId)
formData.append('paytype', 'SYT')
formData.append('wxzh', 'cht')
formData.append('fq', 0)
formData.append('onlyusa', 0)
formData.append('useyhm', 0)
return postForm(`${API_HOST}/generate_payment_links`, formData).then((json) => {
if (json.errcode === 0) {
return json.result
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
fetchHistoryOrder: (userId, email, whatsappid = '') => {
return fetchJSON(`${API_HOST}/query_guest_order`, { opisn: userId, whatsappid, email: email }).then((json) => {
if (json.errcode === 0) {
return json.result
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
importEmailMessage: ({ orderId, orderNumber }) => {
return fetchJSON(`${API_HOST}/generate_email_msg`, { coli_sn: orderId, coli_id: orderNumber }).then((json) => {
if (json.errcode === 0) {
return json
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
batchImportEmailMessage: () => {
const { orderList } = get()
const orderPromiseList = orderList.map((order) => {
return new Promise((resolve, reject) => {
fetchJSON(`${API_HOST}/generate_email_msg`, { coli_sn: order.COLI_SN, coli_id: order.COLI_ID }).then((json) => {
if (json.errcode === 0) {
resolve(json)
} else {
reject(json?.errmsg + ': ' + json.errcode)
}
})
}) })
}) })
})
Promise.all(orderPromiseList).then((values) => {
console.log(values) Promise.all(orderPromiseList).then((values) => {
}) console.log(values);
}, })
},
fetchOtherEmail: (coli_sn) => {
return fetchJSON(`${EMAIL_HOST}/email_supplier`, { coli_sn }).then((json) => { fetchOtherEmail: (coli_sn) => {
if (json.errcode === 0) { return fetchJSON(`${EMAIL_HOST}/email_supplier`, { coli_sn })
set(() => ({ .then(json => {
otherEmailList: json.result.MailInfo ?? [], if (json.errcode === 0) {
})) set(() => ({
} else { otherEmailList: json.result.MailInfo ?? [],
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
updateWhatsapp: (coli_sn, number) => {
return postJSON(`${API_HOST_V3}/order_update`, {
coli_sn: coli_sn,
set: {
concat_whatsapp: number,
},
}).then((json) => {
if (json.errcode > 0) {
throw new Error(json?.errmsg + ': ' + json.errcode)
} else {
set((state) => ({
customerDetail: {
...state.customerDetail,
whatsapp_phone_number: number,
},
}))
}
})
},
updateExtraInfo: (coli_sn, extra) => {
const { orderDetail } = get()
return postJSON(`${API_HOST_V3}/order_update`, {
coli_sn: coli_sn,
set: {
extra_info: extra,
},
}).then((json) => {
if (json.errcode > 0) {
throw new Error(json?.errmsg + ': ' + json.errcode)
} else {
set((state) => ({
orderDetail: {
...state.orderDetail,
COLI_Introduction: extra,
},
}))
}
})
},
setOrderPropValue: async (colisn, propName, value) => {
if (propName === 'orderlabel') {
set((state) => ({
orderDetail: {
...state.orderDetail,
tags: value,
},
})) }))
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
} }
})
},
if (propName === 'orderstatus') { setOrderPropValue: async (colisn, propName, value) => {
set((state) => ({
orderDetail: { if (propName === 'orderlabel') {
...state.orderDetail, set((state) => ({
states: value, orderDetail: {
}, ...state.orderDetail,
})) tags: value
} }
}))
}
if (propName === 'orderstatus') {
set((state) => ({
orderDetail: {
...state.orderDetail,
states: value
}
}))
}
return fetchJSON(`${API_HOST}/setorderstatus`, { colisn, stype: propName, svalue: value }).then((json) => { return fetchJSON(`${API_HOST}/setorderstatus`, { colisn, stype: propName, svalue: value })
if (json.errcode > 0) { .then(json => {
throw new Error(json?.errmsg + ': ' + json.errcode) if (json.errcode > 0) {
} throw new Error(json?.errmsg + ': ' + json.errcode)
}) }
}, })
}), },
{ name: 'orderStore' },
), }), { name: 'orderStore' }))
)
export const OrderLabelDefaultOptions = [ export const OrderLabelDefaultOptions = [
{ value: 240003, label: '重点', emoji: '❣️' }, { value: 240003, label: '重点', emoji: '❣️' },
{ value: 240002, label: '次重点', emoji: '❗' }, { value: 240002, label: '次重点', emoji: '❗' },
{ value: 240001, label: '一般', emoji: '' }, { value: 240001, label: '一般', emoji: '' }
] ]
export const OrderLabelDefaultOptionsMapped = OrderLabelDefaultOptions.reduce((acc, cur) => { export const OrderLabelDefaultOptionsMapped = OrderLabelDefaultOptions.reduce((acc, cur) => {
return { ...acc, [String(cur.value)]: cur } return { ...acc, [String(cur.value)]: cur }
}, {}) }, {}) ;
export const OrderStatusDefaultOptions = [ export const OrderStatusDefaultOptions = [
{ value: 1, label: '新订单', emoji: '' }, { value: 1, label: '新订单', emoji: '' },
@ -274,8 +226,8 @@ export const OrderStatusDefaultOptions = [
{ value: 4, label: '等待付订金', emoji: '🛒' }, { value: 4, label: '等待付订金', emoji: '🛒' },
{ value: 5, label: '成行', emoji: '💰' }, { value: 5, label: '成行', emoji: '💰' },
{ value: 6, label: '丢失', emoji: '🎈' }, { value: 6, label: '丢失', emoji: '🎈' },
// { value: 7, label: '取消', emoji: '🚫' }, // 取消要顾问确认后才能执行操作,暂时到 HT 操作 { value: 7, label: '取消', emoji: '🚫' },
{ value: 8, label: '未报价', emoji: '' }, { value: 8, label: '未报价', emoji: '' }
] ]
export const OrderStatusDefaultOptionsMapped = OrderStatusDefaultOptions.reduce((acc, cur) => { export const OrderStatusDefaultOptionsMapped = OrderStatusDefaultOptions.reduce((acc, cur) => {
return { ...acc, [String(cur.value)]: cur } return { ...acc, [String(cur.value)]: cur }
@ -287,32 +239,27 @@ export const OrderStatusDefaultOptionsMapped = OrderStatusDefaultOptions.reduce(
export const RemindStateDefaultOptions = [ export const RemindStateDefaultOptions = [
{ value: '1', label: '一催' }, { value: '1', label: '一催' },
{ value: '2', label: '二催' }, { value: '2', label: '二催' },
{ value: '3', label: '三催' }, { value: '3', label: '三催' }
] ]
/** /**
* @useage 订单信息: 标记状态 * @useage 订单信息: 标记状态
*/ */
export const remindStatusOptions = [ export const remindStatusOptions = [
{ value: 'FirstRemind', label: '已发一催' }, { value: 1, label: '已发一催' },
{ value: 'SecondRemind', label: '已发二催' }, { value: 2, label: '已发二催' },
{ value: 'ThirdRemind', label: '已发三催' }, { value: 3, label: '已发三催' },
{ value: 'important', label: '重点团' }, { value: 'important', label: '重点团' },
{ value: 'sendsurvey', label: '已发 travel advisor survey' }, { value: 'sendsurvey', label: '已发 travel advisor survey' },
] ];
const transferRemind2Checklist = (remindstate) => {
const remindValueList = []
if (isEmpty(remindstate)) return remindValueList
Object.keys(remindstate).forEach(prop => {
if (remindstate[prop]) remindValueList.push(prop)
})
return remindValueList
}
export const remindStatusOptionsMapped = remindStatusOptions.reduce((acc, cur) => { export const remindStatusOptionsMapped = remindStatusOptions.reduce((acc, cur) => {
return { ...acc, [String(cur.value)]: cur } return { ...acc, [String(cur.value)]: cur }
}, {}) }, {});
/**
* @param {Object} params { coli_sn, remindstate }
*/
export const fetchSetRemindStateAction = async (params) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/SetRemindState`, params);
return errcode === 0 ? result : {};
};

@ -1,8 +1,8 @@
import { create } from 'zustand' import { create } from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@haina/utils-request' import { fetchJSON, postForm } from '@/utils/request'
import { API_HOST } from '@/config' import { API_HOST } from '@/config'
import { copy } from '@haina/utils-commons' import { copy } from '@/utils/commons'
const useSnippetStore = create(devtools((set, get) => ({ const useSnippetStore = create(devtools((set, get) => ({
@ -41,7 +41,7 @@ const useSnippetStore = create(devtools((set, get) => ({
const mapTypeList = json?.result?.type.map(item => { const mapTypeList = json?.result?.type.map(item => {
return { value: item.vsn, label: item.vname } return { value: item.vsn, label: item.vname }
}) })
const mapTypeAllList = structuredClone(mapTypeList); const mapTypeAllList = copy(mapTypeList);
mapTypeAllList.unshift({ value: '', label: '全部' }); mapTypeAllList.unshift({ value: '', label: '全部' });
set(() => ({ set(() => ({
ownerList: json?.result?.owner.map(item => { ownerList: json?.result?.owner.map(item => {

@ -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,4 +1,3 @@
console.warn('Warning: `commons.js` is deprecated and will be removed in next version.');
export function copy(obj) { export function copy(obj) {
return JSON.parse(JSON.stringify(obj)) return JSON.parse(JSON.stringify(obj))
} }
@ -189,8 +188,6 @@ export function merge(...objects) {
* 数组分组 * 数组分组
* - 相当于 lodash _.groupBy * - 相当于 lodash _.groupBy
* @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity * @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
* ECMAScript 2021 原生
* - Object.groupBy(items, callbackFn)
*/ */
export function groupBy(array = [], callback) { export function groupBy(array = [], callback) {
return array.reduce((groups, item) => { return array.reduce((groups, item) => {

@ -1,14 +1,8 @@
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from './commons';
/** /**
* *
*/ */
const INDEXED_DB_VERSION = 4;
/**
* 数据库版本
* ! 每次涉及indexedDB的更新都要往上+1
* @type {number}
*/
const INDEXED_DB_VERSION = 6;
export const logWebsocket = (message, direction) => { export const logWebsocket = (message, direction) => {
var open = indexedDB.open('LogWebsocketData', INDEXED_DB_VERSION) var open = indexedDB.open('LogWebsocketData', INDEXED_DB_VERSION)
open.onupgradeneeded = function () { open.onupgradeneeded = function () {
@ -107,16 +101,15 @@ export const clearWebsocketLog = () => {
} }
} }
export const createIndexedDBStore = (tables, database, keySet = {keyPath: 'key' }) => { export const createIndexedDBStore = (tables, database) => {
var open = indexedDB.open(database, INDEXED_DB_VERSION) var open = indexedDB.open(database, INDEXED_DB_VERSION)
// console.trace('createIndexedDBStore');
open.onupgradeneeded = function () { open.onupgradeneeded = function () {
// console.log('createIndexedDBStore onupgradeneeded', database, ) console.log('readIndexDB onupgradeneeded', database, )
var db = open.result var db = open.result
// 数据库是否存在 // 数据库是否存在
for (const table of tables) { for (const table of tables) {
if (!db.objectStoreNames.contains(table)) { if (!db.objectStoreNames.contains(table)) {
var store = db.createObjectStore(table, keySet) var store = db.createObjectStore(table, { keyPath: 'key' })
store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false })
} else { } else {
const objectStore = open.transaction.objectStore(table) const objectStore = open.transaction.objectStore(table)
@ -131,7 +124,7 @@ export const createIndexedDBStore = (tables, database, keySet = {keyPath: 'key'
export const writeIndexDB = (rows, table, database) => { export const writeIndexDB = (rows, table, database) => {
var open = indexedDB.open(database, INDEXED_DB_VERSION) var open = indexedDB.open(database, INDEXED_DB_VERSION)
open.onupgradeneeded = function () { open.onupgradeneeded = function () {
// console.log('readIndexDB onupgradeneeded', table, ) console.log('readIndexDB onupgradeneeded', table, )
var db = open.result var db = open.result
// 数据库是否存在 // 数据库是否存在
if (!db.objectStoreNames.contains(table)) { if (!db.objectStoreNames.contains(table)) {
@ -146,11 +139,6 @@ export const writeIndexDB = (rows, table, database) => {
} }
open.onsuccess = function () { open.onsuccess = function () {
var db = open.result 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 tx = db.transaction(table, 'readwrite')
var store = tx.objectStore(table) var store = tx.objectStore(table)
rows.forEach(row => { rows.forEach(row => {
@ -182,16 +170,16 @@ export const readIndexDB = (keys=null, table, database) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let openRequest = indexedDB.open(database) let openRequest = indexedDB.open(database)
openRequest.onupgradeneeded = function () { openRequest.onupgradeneeded = function () {
// console.log('readIndexDB onupgradeneeded', table, ) console.log('readIndexDB onupgradeneeded', table, )
var db = openRequest.result var db = openRequest.result
// 数据库是否存在 // 数据库是否存在
if (!db.objectStoreNames.contains(table)) { if (!db.objectStoreNames.contains(table)) {
var store = db.createObjectStore(table, { keyPath: 'key' }) var store = db.createObjectStore(table, { keyPath: 'key' })
store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false })
} else { } else {
const store = openRequest.transaction.objectStore(table) const logStore = openRequest.transaction.objectStore(table)
if (!store.indexNames.contains('timestamp')) { if (!logStore.indexNames.contains('timestamp')) {
store.createIndex('timestamp', 'timestamp', { unique: false }) logStore.createIndex('timestamp', 'timestamp', { unique: false })
} }
} }
} }
@ -203,8 +191,7 @@ export const readIndexDB = (keys=null, table, database) => {
let db = e.target.result let db = e.target.result
// 数据库是否存在 // 数据库是否存在
if (!db.objectStoreNames.contains(table)) { if (!db.objectStoreNames.contains(table)) {
console.warn(`readIndexDB > Database does not exist.`, table); resolve('Database does not exist.')
resolve();
return return
} }
let transaction = db.transaction(table, 'readonly') let transaction = db.transaction(table, 'readonly')
@ -221,7 +208,7 @@ export const readIndexDB = (keys=null, table, database) => {
// console.log(`💾Found record with key ${key}:`, result); // console.log(`💾Found record with key ${key}:`, result);
innerResolve([key, result]); // Resolve with [key, data] tuple innerResolve([key, result]); // Resolve with [key, data] tuple
} else { } else {
// console.log(`No record found with key ${key}.`); console.log(`No record found with key ${key}.`);
innerResolve(void 0); // Resolve with undefined for non-existent keys innerResolve(void 0); // Resolve with undefined for non-existent keys
} }
}; };
@ -254,7 +241,7 @@ export const readIndexDB = (keys=null, table, database) => {
// console.log(`💾Found record with key ${keys}:`, result); // console.log(`💾Found record with key ${keys}:`, result);
resolve(result); resolve(result);
} else { } else {
// console.log(`No record found with key ${keys}.`); console.log(`No record found with key ${keys}.`);
resolve(); resolve();
} }
}; };
@ -271,10 +258,10 @@ export const readIndexDB = (keys=null, table, database) => {
allData.forEach(item => { allData.forEach(item => {
resultMap.set(item.key, item); resultMap.set(item.key, item);
}); });
// console.log(`💾Found all records:`, resultMap); console.log(`💾Found all records:`, resultMap);
resolve(resultMap); resolve(resultMap);
} else { } else {
// console.log(`No records found.`); console.log(`No records found.`);
resolve(resultMap); // Resolve with an empty Map if no records resolve(resultMap); // Resolve with an empty Map if no records
} }
}; };
@ -301,8 +288,7 @@ export const deleteIndexDBbyKey = (keys=null, table, database) => {
let db = e.target.result let db = e.target.result
// 数据库是否存在 // 数据库是否存在
if (!db.objectStoreNames.contains(table)) { if (!db.objectStoreNames.contains(table)) {
console.warn('deleteIndexDBbyKey > Database does not exist.', table) resolve('Database does not exist.')
resolve();
return return
} }
var tx = db.transaction(table, 'readwrite') var tx = db.transaction(table, 'readwrite')
@ -361,8 +347,7 @@ export const deleteIndexDBbyKey = (keys=null, table, database) => {
}) })
}; };
function cleanOldData(database, storeNames=[], dateKey = 'timestamp', keySet = { keyPath: 'key' }) { function cleanOldData(database, storeNames=[], dateKey = 'timestamp') {
createIndexedDBStore(storeNames, database, keySet);
return function (daysToKeep = 7) { return function (daysToKeep = 7) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let deletedCount = 0 let deletedCount = 0
@ -370,13 +355,11 @@ function cleanOldData(database, storeNames=[], dateKey = 'timestamp', keySet = {
let openRequest = indexedDB.open(database, INDEXED_DB_VERSION) let openRequest = indexedDB.open(database, INDEXED_DB_VERSION)
openRequest.onupgradeneeded = function () { openRequest.onupgradeneeded = function () {
// console.log('----cleanOldData onupgradeneeded----')
var db = openRequest.result var db = openRequest.result
storeNames.forEach(storeName => { storeNames.forEach(storeName => {
// 数据库是否存在 // 数据库是否存在
if (!db.objectStoreNames.contains(storeName)) { if (!db.objectStoreNames.contains(storeName)) {
var store = db.createObjectStore(storeName, keySet) var store = db.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true })
// var store = db.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true })
store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false })
} else { } else {
const logStore = openRequest.transaction.objectStore(storeName) const logStore = openRequest.transaction.objectStore(storeName)
@ -459,8 +442,6 @@ function cleanOldData(database, storeNames=[], dateKey = 'timestamp', keySet = {
reject(event.target.error) reject(event.target.error)
} }
} }
} else {
console.warn('cleanOldData: No data to delete.', database);
} }
} }
openRequest.onerror = function (e) { openRequest.onerror = function (e) {
@ -470,8 +451,8 @@ function cleanOldData(database, storeNames=[], dateKey = 'timestamp', keySet = {
} }
} }
export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', ['LogStore'], 'timestamp', { keyPath: 'id', autoIncrement: true }); export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', ['LogStore']);
export const clean7DaysMailboxLog = cleanOldData('mailbox', ['dirs', 'maillist', 'listrow', 'mailinfo', 'draft']); export const clean7DaysMailboxLog = cleanOldData('mailbox');
/** /**
@ -586,3 +567,4 @@ export function setupDailyMidnightCleanupScheduler() {
setupDailyMidnightCleanupScheduler() setupDailyMidnightCleanupScheduler()
}, msToMidnight) }, msToMidnight)
} }

@ -1,125 +1,54 @@
import { loadScript } from '@haina/utils-commons' import { loadScript } from '@/utils/commons'
import { fetchJSON } from '@haina/utils-request'
import { readWebsocketLog } from '@/utils/indexedDB' import { readWebsocketLog } from '@/utils/indexedDB'
import { BUILD_VERSION, BUILD_DATE } from '@/config' import { BUILD_VERSION, BUILD_DATE } from '@/config'
/** export const loadPageSpy = (title) => {
* @deprecated if (import.meta.env.DEV || window.$pageSpy) return
*/
// export const loadPageSpy = (title) => { const PageSpyConfig = { api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true }
// if (import.meta.env.DEV || window.$pageSpy) return
const PageSpySrc = [
// const PageSpyConfig = { api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true } 'https://page-spy.mycht.cn/page-spy/index.min.js' + `?${BUILD_DATE}`,
'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js' + `?${BUILD_DATE}`,
// const PageSpySrc = [ 'https://page-spy.mycht.cn/plugin/rrweb/index.min.js' + `?${BUILD_DATE}`,
// 'https://page-spy.mycht.cn/page-spy/index.min.js' + `?${BUILD_DATE}`, ]
// 'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js' + `?${BUILD_DATE}`, Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
// 'https://page-spy.mycht.cn/plugin/rrweb/index.min.js' + `?${BUILD_DATE}`, // 注册插件
// ] window.$harbor = new DataHarborPlugin()
// Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => { window.$rrweb = new RRWebPlugin()
// // 注册插件 ;[window.$harbor, window.$rrweb].forEach((p) => {
// window.$harbor = new DataHarborPlugin() PageSpy.registerPlugin(p)
// window.$rrweb = new RRWebPlugin() })
// ;[window.$harbor, window.$rrweb].forEach((p) => { window.$pageSpy = new PageSpy(PageSpyConfig)
// PageSpy.registerPlugin(p)
// }) // PageSpy.registerPlugin(new DataHarborPlugin());
// window.$pageSpy = new PageSpy(PageSpyConfig) // PageSpy.registerPlugin(new RRWebPlugin());
// 实例化 PageSpy
// // PageSpy.registerPlugin(new DataHarborPlugin()); // window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true, });
// // PageSpy.registerPlugin(new RRWebPlugin()); console.log('[PageSpy]', window.$pageSpy.version)
// // 实例化 PageSpy // window.addEventListener('beforeunload', (e) => {
// // window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true, }); // e.preventDefault() // If you prevent default behavior in Mozilla Firefox
// console.log('[PageSpy]', window.$pageSpy.version) // e.returnValue = '' // Chrome requires returnValue to be set
// // window.addEventListener('beforeunload', (e) => {
// // e.preventDefault() // If you prevent default behavior in Mozilla Firefox // window.$harbor.upload({ clearCache: false, remark: '自动上传' }) // 上传日志 { clearCache: true, remark: '' }
// // e.returnValue = '' // Chrome requires returnValue to be set // })
window.onerror = async function (msg, url, lineNo, columnNo, error) {
// // window.$harbor.upload({ clearCache: false, remark: '自动上传' }) // 上传日志 { clearCache: true, remark: '' } await readWebsocketLog()
// // }) // 上传最近 3 分钟的日志
// window.onerror = async function (msg, url, lineNo, columnNo, error) { const now = Date.now()
// await readWebsocketLog() await window.$harbor.uploadPeriods({
// // 上传最近 3 分钟的日志 startTime: now - 3 * 60000,
// const now = Date.now() endTime: now,
// await window.$harbor.uploadPeriods({ remark: `\`onerror\`自动上传. ${msg}`,
// startTime: now - 3 * 60000, })
// endTime: now,
// remark: `\`onerror\`自动上传. ${msg}`,
// })
// }
// })
// }
/**
* @deprecated
*/
// export const uploadPageSpyLog = async () => {
// // window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
// // if (window.$pageSpy) {
// // await window.$harbor.upload() // 上传日志 { clearCache: true, remark: '' }
// // }
// if (import.meta.env.DEV) return true;
// if (window.$pageSpy) {
// try {
// await readWebsocketLog()
// // await window.$harbor.upload() // 上传日志 { clearCache: true, remark: '' }
// // 上传最近 1 小时的日志, 直接upload 所有日志: 413 Payload Too Large
// const now = Date.now();
// await window.$harbor.uploadPeriods({
// startTime: now - 60 * 60000,
// endTime: now,
// });
// return true;
// } catch (error) {
// return false;
// }
// } else {
// return false;
// }
// }
export const sendNotify = async (message) => {
const notifyUrl = 'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/SendMDMsgByDingRobotToGroup';
const params = {
groupid: 'cidFtzcIzNwNoiaGU9Q795CIg==',
msgTitle: '有人求助',
msgText: `${message}\\n\\nSales CRM (${BUILD_VERSION})`,
};
return fetchJSON(notifyUrl, params).then((json) => {
if (json.errcode === 0) {
console.info('发送通知成功');
} else {
throw new Error(json?.errmsg + ': ' + json.errcode);
} }
}); })
}; }
/** export const uploadPageSpyLog = async () => {
* @deprecated await readWebsocketLog()
*/ // window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
// const uploadLog = async () => { if (window.$pageSpy) {
// await readWebsocketLog() await window.$harbor.upload() // 上传日志 { clearCache: true, remark: '' }
// if (window.$pageSpy) { }
// // window.$pageSpy.triggerPlugins('onOfflineLog', 'upload') }
// try {
// // await window.$harbor.upload() // 上传日志 { clearCache: true, remark: '' }
// // 上传最近 1 小时的日志, 直接upload 所有日志: 413 Payload Too Large
// const now = Date.now()
// await window.$harbor.uploadPeriods({
// startTime: now - 60 * 60000,
// endTime: now,
// })
// messageApi.info('Success')
// // clearWebsocketLog()
// sendNotify()
// } catch (error) {
// messageApi.error('Failure')
// }
// } else {
// messageApi.error('Failure')
// }
// }

@ -1,10 +1,6 @@
import { BUILD_VERSION } from '@/config' import { BUILD_VERSION } from '@/config'
console.warn('Warning: `request.js` is deprecated and will be removed in next version.');
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
const customHeaders = [] const customHeaders = []
// 添加 HTTP Reuqest 自定义头部 // 添加 HTTP Reuqest 自定义头部

@ -6,37 +6,41 @@ import {
App as AntApp, App as AntApp,
ConfigProvider, ConfigProvider,
Empty, Empty,
Modal, FloatButton, Modal,
theme message,
FloatButton,
theme,
} from 'antd' } from 'antd'
import { CustomerServiceOutlined } from '@ant-design/icons' import { AudioOutlined, AudioTwoTone, BugOutlined, CustomerServiceOutlined } from '@ant-design/icons'
import zhLocale from 'antd/locale/zh_CN' import zhLocale from 'antd/locale/zh_CN'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import { useEffect } from 'react' import { useEffect } from 'react'
import { Outlet, useHref, useNavigate } from 'react-router-dom' import { Link, NavLink, Outlet, useHref, useNavigate } from 'react-router-dom'
import { appendRequestHeader } from '@haina/utils-request' import { appendRequestHeader } from '@/utils/request'
import { loadPageSpy } from '@haina/utils-pagespy' import { loadPageSpy } from '@/utils/pagespy'
import AppLogo from '@/assets/highlights_travel_300_300.png' import AppLogo from '@/assets/highlights_travel_300_300.png'
import '@/assets/App.css' import '@/assets/App.css'
import 'react-chat-elements/dist/main.css' import 'react-chat-elements/dist/main.css'
import EmailFetch from './Conversations/Online/Components/EmailFetch' import EmailFetch from './Conversations/Online/Components/EmailFetch'
import FetchEmailWorker from './../workers/fetchEmailWorker?worker&url' import FetchEmailWorker from './../workers/fetchEmailWorker?worker&url'
import { readWebsocketLog } from '@/utils/indexedDB'
import { useGlobalNotify } from '@/hooks/useGlobalNotify' import { useGlobalNotify } from '@/hooks/useGlobalNotify'
import GeneratePaymentDrawer from './Conversations/Online/Components/GeneratePaymentDrawer' import GeneratePaymentDrawer from './Conversations/Online/Components/GeneratePaymentDrawer'
import GenerateAutoDocDrawer from './Conversations/Online/Components/GenerateAutoDocDrawer' import GenerateAutoDocDrawer from './Conversations/Online/Components/GenerateAutoDocDrawer'
import GenerateShorturlDrawer from './Conversations/Online/Components/GenerateShorturlDrawer'
import LogUploader from '@/components/LogUploader'
import { BUILD_VERSION } from '@/config'
// const fetchEmailWorkerURL = new URL('/src/workers/fetchEmailWorker.js', import.meta.url);
const fetchEmailWorker = new Worker(FetchEmailWorker, { type: 'module' }); const fetchEmailWorker = new Worker(FetchEmailWorker, { type: 'module' });
function AuthApp() { function AuthApp() {
const navigate = useNavigate() const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
const { colorPrimary, borderRadius } = useThemeContext() const { colorPrimary, borderRadius } = useThemeContext()
const [loginUser] = useAuthStore((state) => [ const [loginUser, sendNotify] = useAuthStore((state) => [
state.loginUser state.loginUser, state.sendNotify
]) ])
const href = useHref() const href = useHref()
@ -57,7 +61,7 @@ function AuthApp() {
let _fetchEmailWorker; let _fetchEmailWorker;
if (loginUser.userId > 0) { if (loginUser.userId > 0) {
appendRequestHeader('X-User-Id', loginUser.userId) appendRequestHeader('X-User-Id', loginUser.userId)
loadPageSpy(loginUser.username + '(v' + BUILD_VERSION + ')', 'Sales CRM', true) loadPageSpy(loginUser.username)
connectWebsocket(loginUser.userId) connectWebsocket(loginUser.userId)
fetchInitialData(loginUser) fetchInitialData(loginUser)
@ -86,6 +90,29 @@ function AuthApp() {
return fetchEmailWorker; return fetchEmailWorker;
} }
const uploadLog = async () => {
await readWebsocketLog()
if (window.$pageSpy) {
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload')
try {
// await window.$harbor.upload() // { clearCache: true, remark: '' }
// 1 , upload : 413 Payload Too Large
const now = Date.now()
await window.$harbor.uploadPeriods({
startTime: now - 60 * 60000,
endTime: now,
})
messageApi.info('Success')
// clearWebsocketLog()
sendNotify()
} catch (error) {
messageApi.error('Failure')
}
} else {
messageApi.error('Failure')
}
}
// /p... // /p...
const needToLogin = loginUser.userId === -1 && href.indexOf('/p/') === -1 const needToLogin = loginUser.userId === -1 && href.indexOf('/p/') === -1
@ -134,9 +161,10 @@ function AuthApp() {
icon={<CustomerServiceOutlined />} icon={<CustomerServiceOutlined />}
> >
<EmailFetch /> <EmailFetch />
<LogUploader /> <FloatButton icon={<BugOutlined />} tooltip={<div>上传日志给研发部</div>} onClick={() => uploadLog()} />
<FloatButton.BackTop /> <FloatButton.BackTop />
</FloatButton.Group> </FloatButton.Group>
{contextHolder}
{needToLogin ? <>login...</> : <Outlet />} {needToLogin ? <>login...</> : <Outlet />}
<dialog id="about-dialog" className="border-0"> <dialog id="about-dialog" className="border-0">
<img className="logo" src={AppLogo} alt="logo" /> <img className="logo" src={AppLogo} alt="logo" />
@ -163,7 +191,6 @@ function AuthApp() {
</dialog> </dialog>
<GeneratePaymentDrawer /> <GeneratePaymentDrawer />
<GenerateAutoDocDrawer /> <GenerateAutoDocDrawer />
<GenerateShorturlDrawer />
</ErrorBoundary> </ErrorBoundary>
</AntApp> </AntApp>
</ConfigProvider> </ConfigProvider>

@ -2,7 +2,7 @@ import { useCallback, useState, useEffect } from "react";
import { Grid, Divider, Layout, Flex, Spin, Input, Col, Row, List, Typography, Alert } from "antd"; import { Grid, Divider, Layout, Flex, Spin, Input, Col, Row, List, Typography, Alert } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined } from "@ant-design/icons"; import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined } from "@ant-design/icons";
import { useParams, useHref, useNavigate } from "react-router-dom"; import { useParams, useHref, useNavigate } from "react-router-dom";
import { isEmpty } from "@haina/utils-commons"; import { isEmpty } from "@/utils/commons";
import callCenterStore from "@/stores/CallCenterStore"; import callCenterStore from "@/stores/CallCenterStore";
import useAuthStore from "@/stores/AuthStore"; import useAuthStore from "@/stores/AuthStore";

@ -6,7 +6,7 @@ import ConversationsList from './Conversations/History/ConversationsList';
import MessagesMatchList from './Conversations/History/MessagesMatchList'; import MessagesMatchList from './Conversations/History/MessagesMatchList';
import MessagesList from './Conversations/History/MessagesList'; import MessagesList from './Conversations/History/MessagesList';
import ImageAlbumPreview from './Conversations/History/ImageAlumPreview'; import ImageAlbumPreview from './Conversations/History/ImageAlumPreview';
import { flush, pick } from '@haina/utils-commons'; import { flush, pick } from '@/utils/commons';
import { fetchConversationsSearch, CONVERSATION_PAGE_SIZE } from '@/actions/ConversationActions'; import { fetchConversationsSearch, CONVERSATION_PAGE_SIZE } from '@/actions/ConversationActions';
import EmailDetail from './Conversations/Online/Components/EmailDetail'; import EmailDetail from './Conversations/Online/Components/EmailDetail';
import SupplierEmailDrawer from './Conversations/Online/Components/EmailListDrawer'; import SupplierEmailDrawer from './Conversations/Online/Components/EmailListDrawer';

@ -6,7 +6,7 @@ import ConversationsList from './Conversations/History/ConversationsList';
import MessagesMatchList from './Conversations/History/MessagesMatchList'; import MessagesMatchList from './Conversations/History/MessagesMatchList';
import MessagesList from './Conversations/History/MessagesList'; import MessagesList from './Conversations/History/MessagesList';
import ImageAlbumPreview from './Conversations/History/ImageAlumPreview'; import ImageAlbumPreview from './Conversations/History/ImageAlumPreview';
import { flush, pick } from '@haina/utils-commons'; import { flush, pick } from '@/utils/commons';
import { fetchConversationsSearch, fetchConversationsUnassigned } from '@/actions/ConversationActions'; import { fetchConversationsSearch, fetchConversationsUnassigned } from '@/actions/ConversationActions';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;

@ -1,36 +1,30 @@
import { useState } from 'react' import { useEffect, useState } from 'react';
import { Layout, Empty, Button } from 'antd' import { Layout, Spin, Button } from 'antd';
import { RightOutlined, LeftOutlined } from '@ant-design/icons' import { RightCircleOutlined, RightOutlined, ReloadOutlined, MenuFoldOutlined, MenuUnfoldOutlined, LeftOutlined } from '@ant-design/icons';
import MessagesHeader from './Conversations/Online/MessagesHeader' // import { useParams, useNavigate } from 'react-router-dom';
import MessagesWrapper from './Conversations/Online/MessagesWrapper' import MessagesHeader from './Conversations/Online/MessagesHeader';
import ConversationsList from './Conversations/Online/ConversationsList' import MessagesWrapper from './Conversations/Online/MessagesWrapper';
import ConversationsList from './Conversations/Online/ConversationsList';
import OrderProfile from '@/components/OrderProfile' import OrderProfile from '@/components/OrderProfile'
import useAuthStore from '@/stores/AuthStore' import ReplyWrapper from './Conversations/Online/ReplyWrapper';
import ReplyWrapper from './Conversations/Online/ReplyWrapper' import useConversationStore from '@/stores/ConversationStore';
import useConversationStore from '@/stores/ConversationStore' import { useShallow } from 'zustand/react/shallow';
import { useShallow } from 'zustand/react/shallow'
import ConversationBind from '@/views/Conversations/Online/ConversationBind' import './Conversations/Conversations.css';
import './Conversations/Conversations.css' import EmailEditorPopup from './Conversations/Online/Input/EmailEditorPopup';
import EmailEditorPopup from './Conversations/Online/Input/EmailEditorPopup'
const { Sider, Content, Header, Footer } = Layout const { Sider, Content, Header, Footer } = Layout;
/**
*
*/
const ChatWindow = () => { const ChatWindow = () => {
const [collapsedRight, setCollapsedRight] = useState(false)
const loginUser = useAuthStore((state) => state.loginUser) const [collapsedLeft, setCollapsedLeft] = useState(false);
const currentOrder = useConversationStore(useShallow((state) => state.currentConversation?.coli_sn || '')) const [collapsedRight, setCollapsedRight] = useState(true);
const currentConversationID = useConversationStore(useShallow((state) => state.currentConversation?.sn || ''))
const [updateCurrentConversation] = useConversationStore((state) => [state.updateCurrentConversation])
const renderEmptyOrder = () => {
return ( const currentOrder = useConversationStore(useShallow(state => state.currentConversation?.coli_sn || ""));
<Empty description={<span>没有订单关联</span>}>
<ConversationBind currentConversationID={currentConversationID} userId={loginUser.userId} onBoundSuccess={(coli_sn) => updateCurrentConversation({ coli_sn })} />
</Empty>
)
}
return ( return (
<> <>
@ -60,7 +54,7 @@ const ChatWindow = () => {
{/* <Button type='text' icon={<ReloadOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className='' title='最新消息记录' /> */} {/* <Button type='text' icon={<ReloadOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className='' title='最新消息记录' /> */}
<Button type='text' icon={collapsedRight ? <RightOutlined /> : <LeftOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className=' rounded-none rounded-r' /> <Button type='text' icon={collapsedRight ? <RightOutlined /> : <LeftOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className=' rounded-none rounded-r' />
</Header> </Header>
<Content className='flex-grow bg-whatsapp-bg relative'> <Content className="flex-grow bg-whatsapp-bg relative" >
<MessagesWrapper /> <MessagesWrapper />
</Content> </Content>
<Footer className='ant-layout-sider-light p-0'> <Footer className='ant-layout-sider-light p-0'>
@ -81,12 +75,12 @@ const ChatWindow = () => {
collapsedWidth={0} collapsedWidth={0}
trigger={null} trigger={null}
collapsed={collapsedRight}> collapsed={collapsedRight}>
<OrderProfile coliSN={currentOrder} renderEmpty={renderEmptyOrder} /> <OrderProfile coliSN={currentOrder} />
</Sider> </Sider>
</Layout> </Layout>
<EmailEditorPopup key='email-editor-online' /> <EmailEditorPopup key='email-editor-online' />
</> </>
) );
} };
export default ChatWindow export default ChatWindow;

@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { App } from 'antd'; import { App } from 'antd';
import { calcCacheSizes, clearAllCaches } from '@haina/utils-commons'; import { calcCacheSizes, clearAllCaches } from '@/utils/commons';
const ClearCache = (props) => { const ClearCache = (props) => {
const { message } = App.useApp(); const { message } = App.useApp();

@ -4,13 +4,12 @@ import { LoadingOutlined } from '@ant-design/icons';
import { MessageBox } from 'react-chat-elements'; import { MessageBox } from 'react-chat-elements';
import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions'; import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions';
import useFormStore from '@/stores/FormStore'; import useFormStore from '@/stores/FormStore';
import { isEmpty, stringToColour, groupBy, isNotEmpty, TagColorStyle } from '@haina/utils-commons'; import { isEmpty, stringToColour, groupBy, isNotEmpty, TagColorStyle } from '@/utils/commons';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import MergeConversationTo from './MergeConversationTo'; import MergeConversationTo from './MergeConversationTo';
import BubbleIM from '../Online/Components/BubbleIM'; import BubbleIM from '../Online/Components/BubbleIM';
import BubbleEmail from '../Online/Components/BubbleEmail'; import BubbleEmail from '../Online/Components/BubbleEmail';
import { ERROR_IMG, POPUP_FEATURES } from '@/config'; import { ERROR_IMG, POPUP_FEATURES } from '@/config';
import { parseSimpleMarkdown } from '@/channel/bubbleMsgUtils';
const BIG_PAGE_SIZE = MESSAGE_PAGE_SIZE * 20; const BIG_PAGE_SIZE = MESSAGE_PAGE_SIZE * 20;
const MessagesList = ({ ...listProps }) => { const MessagesList = ({ ...listProps }) => {
@ -149,27 +148,6 @@ const MessagesList = ({ ...listProps }) => {
setFocusMsg(id); setFocusMsg(id);
} }
}; };
// Render parsed tokens to React elements
const renderMDTokens = (tokens) => {
return tokens.map((token, index) => {
switch (token.type) {
case 'text':
return <span key={index}>{token.content}</span>;
case 'bold':
return <b key={index}>{renderMDTokens(token.content)}</b>;
case 'italic':
return <i key={index}>{renderMDTokens(token.content)}</i>;
case 'url':
return (
<a key={index} href={token.content} target='_blank' rel='noopener noreferrer' className=' underline text-sm'>
{token.content}
</a>
)
default:
return <span key={index}>{token.content}</span>;
}
});
};
const RenderText = memo(function renderText({ str, className, template }) { const RenderText = memo(function renderText({ str, className, template }) {
let headerObj, footerObj, buttonsArr; let headerObj, footerObj, buttonsArr;
@ -179,8 +157,23 @@ const MessagesList = ({ ...listProps }) => {
footerObj = componentsObj?.footer?.[0]; footerObj = componentsObj?.footer?.[0];
buttonsArr = componentsObj?.buttons?.reduce((r, c) => r.concat(c.buttons), []); buttonsArr = componentsObj?.buttons?.reduce((r, c) => r.concat(c.buttons), []);
} }
const parts = str.split(/(https?:\/\/[^\s()]+|\p{Emoji_Presentation})/gmu).filter((s) => s !== '');
const links = str.match(/https?:\/\/[^\s()]+/gi) || [];
const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || [];
const extraClass = isEmpty(emojis) ? '' : '';
const objArr = parts.reduce((prev, curr, index) => {
if (links.includes(curr)) {
prev.push({ type: 'link', key: curr });
} else if (emojis.includes(curr)) {
prev.push({ type: 'emoji', key: curr });
} else {
prev.push({ type: 'text', key: curr });
}
return prev;
}, []);
return ( return (
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} `} key={'msg-text'}> <span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass} `} key={'msg-text'}>
{headerObj ? ( {headerObj ? (
<div className='text-neutral-500 text-center'> <div className='text-neutral-500 text-center'>
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>} {'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
@ -192,7 +185,17 @@ const MessagesList = ({ ...listProps }) => {
)} )}
</div> </div>
) : null} ) : null}
{renderMDTokens(parseSimpleMarkdown(str))} {(objArr || []).map((part, index) => {
if (part.type === 'link') {
return (
<a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-sm'>
{part.key}
</a>
);
} else {
return part.key;
}
})}
</span> </span>
); );
}); });

@ -1,6 +1,6 @@
import { ChatItem } from 'react-chat-elements'; import { ChatItem } from 'react-chat-elements';
import useFormStore from '@/stores/FormStore'; import useFormStore from '@/stores/FormStore';
import { isNotEmpty } from '@haina/utils-commons'; import { isNotEmpty } from '@/utils/commons';
const MessagesMatchList = ({ ...props }) => { const MessagesMatchList = ({ ...props }) => {
const [formValues] = useFormStore((state) => [state.chatHistoryForm]); const [formValues] = useFormStore((state) => [state.chatHistoryForm]);

@ -2,7 +2,7 @@
import { memo } from 'react'; import { memo } from 'react';
import { Form, Flex, Input, Button, DatePicker, Select } from 'antd'; import { Form, Flex, Input, Button, DatePicker, Select } from 'antd';
import SearchInput from '@/components/SearchInput'; import SearchInput from '@/components/SearchInput';
import { isNotEmpty } from '@haina/utils-commons'; import { isNotEmpty } from '@/utils/commons';
import { fetchSalesAgentWithDD as fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions'; import { fetchSalesAgentWithDD as fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions';
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;

@ -2,7 +2,7 @@ import { memo } from 'react';
import { App } from 'antd'; import { App } from 'antd';
import { MailOutlined } from '@ant-design/icons'; import { MailOutlined } from '@ant-design/icons';
import { MessageBox } from 'react-chat-elements'; import { MessageBox } from 'react-chat-elements';
import { isEmpty, } from '@haina/utils-commons'; import { isEmpty, } from '@/utils/commons';
import { useEmailDetail, } from '@/hooks/useEmail'; import { useEmailDetail, } from '@/hooks/useEmail';
const BubbleEmail = ({ onOpenEditor, onOpenEmail, ...message }) => { const BubbleEmail = ({ onOpenEditor, onOpenEmail, ...message }) => {

@ -2,8 +2,7 @@ import { memo } from 'react';
import { App, Button, Image } from 'antd'; import { App, Button, Image } from 'antd';
import { ExportOutlined, CopyOutlined, PhoneOutlined } from '@ant-design/icons'; import { ExportOutlined, CopyOutlined, PhoneOutlined } from '@ant-design/icons';
import { MessageBox } from 'react-chat-elements'; import { MessageBox } from 'react-chat-elements';
import { groupBy, isEmpty, TagColorStyle } from '@haina/utils-commons'; import { groupBy, isEmpty, TagColorStyle } from '@/utils/commons';
import { parseSimpleMarkdown } from '@/channel/bubbleMsgUtils';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { ReplyIcon } from '@/components/Icons'; import { ReplyIcon } from '@/components/Icons';
@ -24,34 +23,6 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
setNewChatModalVisible(true); setNewChatModalVisible(true);
setNewChatFormValues((prev) => ({ ...prev, phone_number: wa_id, name: wa_name })); setNewChatFormValues((prev) => ({ ...prev, phone_number: wa_id, name: wa_name }));
}; };
// Render parsed tokens to React elements
const renderMDTokens = (tokens) => {
return tokens.map((token, index) => {
switch (token.type) {
case 'text':
return <span key={index}>{token.content}</span>
case 'bold':
return <b key={index}>{renderMDTokens(token.content)}</b>
case 'italic':
return <i key={index}>{renderMDTokens(token.content)}</i>
case 'url':
return (
<a key={index} href={token.content} target='_blank' rel='noopener noreferrer' className=' underline text-sm'>
{token.content}
</a>
)
case 'number':
return (
<a key={`${index}`} className='text-sm ' onClick={() => openNewChatModal({ wa_id: token.content, wa_name: token.content })}>
{token.content}
</a>
)
default:
return <span key={index}>{token.content}</span>
}
})
};
const RenderText = memo(function renderText({ str, className, template, message }) { const RenderText = memo(function renderText({ str, className, template, message }) {
let headerObj, footerObj, buttonsArr; let headerObj, footerObj, buttonsArr;
if (!isEmpty(template) && !isEmpty(template.components)) { if (!isEmpty(template) && !isEmpty(template.components)) {
@ -61,8 +32,26 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
buttonsArr = componentsObj?.button; // ?.reduce((r, c) => r.concat(c.buttons), []); buttonsArr = componentsObj?.button; // ?.reduce((r, c) => r.concat(c.buttons), []);
} }
const parts = str.split(/(https?:\/\/[^\s()]+|\p{Emoji_Presentation}|\d{4,})/gmu).filter((s) => s !== '');
const links = str.match(/https?:\/\/[^\s()]+/gi) || [];
const numbers = str.match(/\d{4,}/g) || [];
const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || [];
const extraClass = isEmpty(emojis) ? '' : '';
const objArr = parts.reduce((prev, curr, index) => {
if (links.includes(curr)) {
prev.push({ type: 'link', key: curr });
} else if (numbers.includes(curr)) {
prev.push({ type: 'number', key: curr });
} else if (emojis.includes(curr)) {
prev.push({ type: 'emoji', key: curr });
} else {
prev.push({ type: 'text', key: curr });
}
return prev;
}, []);
return ( return (
<div className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className}`} key={'msg-text'}> <span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`} key={'msg-text'}>
{headerObj ? ( {headerObj ? (
<div className='text-neutral-500 text-center'> <div className='text-neutral-500 text-center'>
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>} {'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
@ -74,7 +63,24 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
)} )}
</div> </div>
) : null} ) : null}
{renderMDTokens(parseSimpleMarkdown(str))} {(objArr || []).map((part, index) => {
if (part.type === 'link') {
return (
<a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-sm'>
{part.key}
</a>
)
} else if (part.type === 'number') {
return (
<a key={`${part.key}${index}`} className='text-sm' onClick={() => openNewChatModal({ wa_id: part.key, wa_name: part.key })}>
{part.key}
</a>
)
} else {
// if (part.type === 'emoji')
return part.key
}
})}
{footerObj ? <div className=' text-neutral-500'>{footerObj.text}</div> : null} {footerObj ? <div className=' text-neutral-500'>{footerObj.text}</div> : null}
{buttonsArr && buttonsArr.length > 0 ? ( {buttonsArr && buttonsArr.length > 0 ? (
<div className='flex flex-row gap-1'> <div className='flex flex-row gap-1'>
@ -95,7 +101,7 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
)} )}
</div> </div>
) : null} ) : null}
</div> </span>
) )
}); });
return ( return (

@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Button, Tag, Radio, Popover, Form, Space, Tooltip } from 'antd'; import { Button, Tag, Radio, Popover, Form, Space } from 'antd';
import { isEmpty, objectMapper, TagColorStyle } from '@haina/utils-commons'; import { isEmpty, objectMapper, TagColorStyle } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { OrderLabelDefaultOptions } from '@/stores/OrderStore'; import { OrderLabelDefaultOptions } from '@/stores/OrderStore';
import { FilterOutlined, FilterTwoTone } from '@ant-design/icons'; import { FilterIcon } from '@/components/Icons';
const otypes = [ const otypes = [
{ label: 'All', value: '', labelValue: '' }, { label: 'All', value: '', labelValue: '' },
@ -202,15 +202,17 @@ const ChatListFilter = ({ onFilterChange, activeList, ...props }) => {
</Form> </Form>
</> </>
}> }>
<Tooltip title='更多筛选' > <Button
<Button icon={
icon={ <FilterIcon
isEmpty(selectedTags) ? <FilterOutlined className='text-neutral-500' /> : <FilterTwoTone /> className={
} isEmpty(selectedTags) ? 'text-neutral-500' : 'text-blue-500'
type='text' }
size='middle' />
/> }
</Tooltip> type='text'
size='middle'
/>
</Popover> </Popover>
</div> </div>
</> </>

@ -5,7 +5,7 @@ import { CloseCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
import { fetchConversationItemClose, fetchConversationsSearch, fetchConversationItemUnread, fetchConversationItemTop, postConversationTags, deleteConversationTags, fetchCleanUnreadMsgCount } from '@/actions/ConversationActions'; import { fetchConversationItemClose, fetchConversationsSearch, fetchConversationItemUnread, fetchConversationItemTop, postConversationTags, deleteConversationTags, fetchCleanUnreadMsgCount } from '@/actions/ConversationActions';
import { ChatItem } from 'react-chat-elements'; import { ChatItem } from 'react-chat-elements';
// import ConversationsNewItem from './ConversationsNewItem'; // import ConversationsNewItem from './ConversationsNewItem';
import { flush, isEmpty, isNotEmpty, stringToColour, TagColorStyle } from '@haina/utils-commons'; import { flush, isEmpty, isNotEmpty, stringToColour, TagColorStyle } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore';
import ChannelLogo from './ChannelLogo'; import ChannelLogo from './ChannelLogo';

@ -1,8 +1,8 @@
import { useState } from 'react' import { useState } from 'react'
import { App, Modal, Button, Table, Form, Row, Col, Input, Checkbox } from 'antd' import { App, Modal, Button, Table, Form, Row, Col, Input, Checkbox } from 'antd'
import { ApiOutlined } from '@ant-design/icons' import { ApiOutlined } from '@ant-design/icons'
import { isEmpty, cloneDeep } from '@haina/utils-commons' import { isEmpty, cloneDeep } from '@/utils/commons'
import { fetchJSON } from '@haina/utils-request' import { fetchJSON } from '@/utils/request'
import AdvanceSearchForm from '../../../orders/AdvanceSearchForm' import AdvanceSearchForm from '../../../orders/AdvanceSearchForm'
import { API_HOST } from '@/config' import { API_HOST } from '@/config'
import dayjs from 'dayjs' import dayjs from 'dayjs'

@ -1,4 +1,3 @@
import { POPUP_FEATURES } from '@/config'
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
const EmailContent = ({ id, content: MailContent, className='', ...props }) => { const EmailContent = ({ id, content: MailContent, className='', ...props }) => {
@ -32,12 +31,11 @@ const EmailContent = ({ id, content: MailContent, className='', ...props }) => {
width: 900px; width: 900px;
max-width: 100%; max-width: 100%;
} }
img { img {
max-width: 90%; max-width: 90%;
height: auto; height: auto;
object-fit: contain; object-fit: contain;
} }
img:not(a img){ cursor: pointer;}
</style> </style>
</head> </head>
<body> <body>
@ -60,15 +58,6 @@ const EmailContent = ({ id, content: MailContent, className='', ...props }) => {
links.forEach((link) => { links.forEach((link) => {
link.setAttribute('target', '_blank') 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) { } catch (e) {
// console.error('Could not access iframe content due to Same-Origin Policy or other error:', e) // console.error('Could not access iframe content due to Same-Origin Policy or other error:', e)
} }
@ -124,7 +113,7 @@ const EmailContent = ({ id, content: MailContent, className='', ...props }) => {
return ( return (
<div ref={containerRef} className={`space-y-4 w-full ${className}`}> <div ref={containerRef} className={`space-y-4 w-full ${className}`}>
<div className='w-full relative pt-2'> <div className='w-full relative'>
<iframe <iframe
key={id} key={id}
ref={iframeRef} ref={iframeRef}
@ -136,7 +125,7 @@ const EmailContent = ({ id, content: MailContent, className='', ...props }) => {
border: 'none', border: 'none',
display: 'block', display: 'block',
}} }}
sandbox='allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms allow-top-navigation' sandbox='allow-scripts allow-same-origin allow-popups'
/> />
</div> </div>
</div> </div>

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { App, Button, Divider, Avatar } from 'antd' import { App, Button, Divider, Avatar } from 'antd'
import { LoadingOutlined, ApiOutlined } from '@ant-design/icons'; import { LoadingOutlined, ApiOutlined } from '@ant-design/icons';
import { EditIcon, ReplyIcon, ResendIcon, ShareForwardIcon } from '@/components/Icons' import { EditIcon, ReplyIcon, ResendIcon, ShareForwardIcon } from '@/components/Icons'
import { isEmpty, TagColorStyle } from '@haina/utils-commons' import { isEmpty, TagColorStyle } from '@/utils/commons'
import EmailEditorPopup from '../Input/EmailEditorPopup' import EmailEditorPopup from '../Input/EmailEditorPopup'
import DnDModal from '@/components/DnDModal' import DnDModal from '@/components/DnDModal'
import useStyleStore from '@/stores/StyleStore' import useStyleStore from '@/stores/StyleStore'

@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useMemo } from 'react'
import { App, Button, Divider, Avatar, List, Flex, Typography, Tooltip, Empty } from 'antd' import { App, Button, Divider, Avatar, List, Flex, Typography, Tooltip, Empty } from 'antd'
import { LoadingOutlined, ApiOutlined, FilePdfOutlined, FileOutlined, FileWordOutlined, FileExcelOutlined, FileJpgOutlined, FileImageOutlined, FileTextOutlined, FileGifOutlined, GlobalOutlined, FileZipOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons' 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 { EditIcon, MailCheckIcon, ReplyAllIcon, ReplyIcon, ResendIcon, ShareForwardIcon, SendPlaneFillIcon, InboxIcon } from '@/components/Icons'
import { isEmpty, TagColorStyle } from '@haina/utils-commons' import { isEmpty, TagColorStyle } from '@/utils/commons'
import EmailEditorPopup from '../Input/EmailEditorPopup' import EmailEditorPopup from '../Input/EmailEditorPopup'
import DnDModal from '@/components/DnDModal' import DnDModal from '@/components/DnDModal'
import useStyleStore from '@/stores/StyleStore' import useStyleStore from '@/stores/StyleStore'
@ -29,13 +29,6 @@ const extTypeMapped = {
default: { icon: FileOutlined }, // rtf bmp 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, } } } * @property {*} emailMsg - 邮件数据. { conversationid, actionId, order_opi, coli_sn, msgOrigin: { from, to, id, email: { subject, mai_sn, } } }
*/ */
@ -132,6 +125,12 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
}) })
} }
} }
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} />;
};
/** /**
* 根据状态, 显示操作 * 根据状态, 显示操作
@ -140,7 +139,7 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
* * 失败: 重发 * * 失败: 重发
* todo: disabled 不显示 * todo: disabled 不显示
*/ */
const renderActionBtns = ({ className, ...props }) => { const ActionBtns = ({ className, ...props }) => {
const { status } = mailData.info const { status } = mailData.info
let btns = [] let btns = []
@ -212,48 +211,27 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
return <div className={`flex justify-end items-center gap-1 ${className || ''}`}>{btns}</div> 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) => { const variantCls = (variant) => {
switch (variant) { switch (variant) {
case 'outline': case 'outline':
return 'h-full border-y-0 border-x border-solid border-neutral-200' return 'border-y-0 border-x border-solid border-neutral-200'
case 'full': case 'full':
return 'h-[calc(100dvh-16px)]' return 'h-[calc(100vh-16px)]'
default: default:
return 'h-full' return ''
} }
} }
return mailID ? ( 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 ref={componentRef} className={`email-container h-full flex flex-col gap-0 divide-y divide-neutral-200 divide-solid *:p-2 *:border-0 bg-white ${variantCls(variant)}`}>
<div className=''> <div className=''>
<div className='flex flex-wrap justify-between'> <div className='flex flex-wrap justify-between'>
<span className={(mailData.info?.MAI_ReadState || 0) > 0 ? '' : ' font-bold '}> <span className={(mailData.info?.MAI_ReadState || 0) > 0 ? '' : ' font-bold '}>
{loading ? <LoadingOutlined className='mr-1' /> : null} {loading ? <LoadingOutlined className='mr-1' /> : null}
{mailData.info?.MAI_Subject || emailMsg?.msgOrigin?.subject} {mailData.info?.MAI_Subject || emailMsg?.msgOrigin?.subject}
</span> </span>
{/* <ActionBtns key='actions' className={'ml-auto'} /> */} <ActionBtns key='actions' className={'ml-auto'} />
{renderActionBtns({ className: 'ml-auto'})}
</div> </div>
<Divider className='my-2' /> <Divider className='my-2' />
<div className={['flex flex-wrap justify-end', window.innerWidth < 800 ? 'flex-col' : 'flex-row '].join(' ')}> <div className={['flex flex-wrap justify-end', window.innerWidth < 800 ? 'flex-col' : 'flex-row '].join(' ')}>
@ -306,14 +284,42 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
{mailData.attachments.length > 0 && ( {mailData.attachments.length > 0 && (
<> <>
<span>&nbsp;附件 ({mailData.attachments.length})</span> <span>&nbsp;附件 ({mailData.attachments.length})</span>
{renderAttaList({ list: mailData.attachments || []})} <List
dataSource={mailData.attachments || []}
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}${atta.ATI_ServerFile}`, atta.ATI_SN)} size='small' className='text-blue-500 cursor-pointer'>
{atta.ATI_Name}
</span>
</Typography.Text>
</List.Item>
)}
/>
</> </>
)} )}
{mailData.insideAttachments.length > 0 && <details> {mailData.insideAttachments.length > 0 && <details>
<summary> <summary>
<span className='text-gray-500 italic'>&nbsp;文内附件 ({mailData.insideAttachments.length}) 已在正文显示&nbsp;</span><span className='cursor-pointer underline'>点击展开</span> <span className='text-gray-500 italic'>&nbsp;文内附件 ({mailData.insideAttachments.length}) 已在正文显示&nbsp;</span><span className='cursor-pointer underline'>点击展开</span>
</summary> </summary>
{renderAttaList({ list: mailData.insideAttachments || []})} <List
dataSource={mailData.insideAttachments || []}
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}${atta.ATI_ServerFile}`, atta.ATI_SN)} size='small' className='text-blue-500 cursor-pointer'>
{atta.ATI_Name}
</span>
</Typography.Text>
</List.Item>
)}
/>
</details>} </details>}
</div> </div>
)} )}

@ -4,7 +4,7 @@ import { CloseOutlined, DownloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { InboxIcon, SendPlaneFillIcon, ExpandIcon } from '@/components/Icons' import { InboxIcon, SendPlaneFillIcon, ExpandIcon } from '@/components/Icons'
import EmailDetailInline from './EmailDetailInline' import EmailDetailInline from './EmailDetailInline'
import { debounce, isEmpty } from '@haina/utils-commons' import { debounce, isEmpty } from '@/utils/commons'
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
const EmailListDrawer = ({ showExpandBtn=true, title, list: otherEmailList, currentConversationID, opi_sn, oid, emailItem: clickItem, onOpenEditor, ...props }) => { const EmailListDrawer = ({ showExpandBtn=true, title, list: otherEmailList, currentConversationID, opi_sn, oid, emailItem: clickItem, onOpenEditor, ...props }) => {

@ -5,7 +5,7 @@ import { useOrderStore } from '@/stores/OrderStore'
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { getEmailQuotationDraftAction } from '@/actions/EmailActions' import { getEmailQuotationDraftAction } from '@/actions/EmailActions'
import { isEmpty } from '@haina/utils-commons' import { isEmpty } from '@/utils/commons'
const EmailQuotation = ({ sfi_sn, ...props }) => { const EmailQuotation = ({ sfi_sn, ...props }) => {
const {notification} = App.useApp() const {notification} = App.useApp()

@ -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

@ -3,7 +3,7 @@ import { App, Button, Popover, Tabs, List, Image, Avatar, Card, Flex, Space,Typo
import { FileSearchOutlined, LoadingOutlined } from '@ant-design/icons'; import { FileSearchOutlined, LoadingOutlined } from '@ant-design/icons';
import { RotateLeftOutlined, RotateRightOutlined, ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons' import { RotateLeftOutlined, RotateRightOutlined, ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons'
import { InboxIcon, SendPlaneFillIcon } from '@/components/Icons'; import { InboxIcon, SendPlaneFillIcon } from '@/components/Icons';
import { groupBy, isEmpty, TagColorStyle as CalColorStyle } from '@haina/utils-commons'; import { groupBy, isEmpty, TagColorStyle as CalColorStyle } from '@/utils/commons';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import EmailDetail from './EmailDetail'; import EmailDetail from './EmailDetail';
import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions'; import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions';

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { App, Modal, Button, Table } from 'antd'; import { App, Modal, Button, Table } from 'antd';
import { isEmpty, cloneDeep } from '@haina/utils-commons'; import { isEmpty, cloneDeep } from '@/utils/commons';
import { fetchJSON } from '@haina/utils-request'; import { fetchJSON } from '@/utils/request';
import AdvanceSearchForm from './../../orders/AdvanceSearchForm'; import AdvanceSearchForm from './../../orders/AdvanceSearchForm';
import { API_HOST } from '@/config'; import { API_HOST } from '@/config';
import dayjs from 'dayjs'; import dayjs from 'dayjs';

@ -4,7 +4,7 @@ import { App, Input, Button, Empty, Tooltip, List } from 'antd';
import { PlusOutlined, LoadingOutlined, HistoryOutlined, FireOutlined,AudioTwoTone } from '@ant-design/icons'; import { PlusOutlined, LoadingOutlined, HistoryOutlined, FireOutlined,AudioTwoTone } from '@ant-design/icons';
import { fetchConversationsList, fetchOrderConversationsList, CONVERSATION_PAGE_SIZE } from '@/actions/ConversationActions'; import { fetchConversationsList, fetchOrderConversationsList, CONVERSATION_PAGE_SIZE } from '@/actions/ConversationActions';
import ConversationsNewItem from './ConversationsNewItem'; import ConversationsNewItem from './ConversationsNewItem';
import { debounce, flush, isEmpty, isNotEmpty, pick } from '@haina/utils-commons'; import { debounce, flush, isEmpty, isNotEmpty, pick } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore';
// import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from "@/stores/OrderStore"; // import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from "@/stores/OrderStore";
@ -336,6 +336,13 @@ const Conversations = () => {
<Button onClick={toggleClosedConversationsList} icon={activeList ? '🗂' : '📌'} type='text' /> <Button onClick={toggleClosedConversationsList} icon={activeList ? '🗂' : '📌'} type='text' />
</Tooltip> </Tooltip>
} }
{mobile && (
<AudioTwoTone className='px-3'
onClick={() => {
navigate(`/callcenter/call`)
}}
/>
)}
</div> </div>
<div className='flex-1 overflow-x-hidden overflow-y-auto relative'> <div className='flex-1 overflow-x-hidden overflow-y-auto relative'>
{/* {mobile && conversationsListLoading && dataSource.length === 0 ? ( */} {/* {mobile && conversationsListLoading && dataSource.length === 0 ? ( */}

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Form, Input, Modal } from 'antd'; import { Form, Input, Modal } from 'antd';
import { isEmpty, isNotEmpty, pick } from '@haina/utils-commons'; import { isEmpty, isNotEmpty, pick } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { phoneNumberToWAID } from '@/channel/bubbleMsgUtils'; import { phoneNumberToWAID } from '@/channel/bubbleMsgUtils';
import { useConversationNewItem } from '@/hooks/useConversation'; import { useConversationNewItem } from '@/hooks/useConversation';

@ -6,8 +6,7 @@ import InputMediaUpload from './MediaUpload'
import PaymentlinkBtn from './PaymentlinkBtn' import PaymentlinkBtn from './PaymentlinkBtn'
import SnippestBtn from './SnippestBtn' import SnippestBtn from './SnippestBtn'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { isEmpty } from '@haina/utils-commons' import { isEmpty } from '@/utils/commons'
import ShortlinkBtn from './ShortlinkBtn'
const ComposerTools = ({ channel, invokeSendUploadMessage, invokeSendMessage, invokeUploadFileMessage, inputEmoji, ...props }) => { const ComposerTools = ({ channel, invokeSendUploadMessage, invokeSendMessage, invokeUploadFileMessage, inputEmoji, ...props }) => {
const websocket = useConversationStore((state) => state.websocket) const websocket = useConversationStore((state) => state.websocket)
@ -26,7 +25,6 @@ const ComposerTools = ({ channel, invokeSendUploadMessage, invokeSendMessage, in
<PaymentlinkBtn /> <PaymentlinkBtn />
<SnippestBtn /> <SnippestBtn />
<ShortlinkBtn />
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} /> {/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />
<Button type='text' className='' icon={<AudioOutlined />} size={'middle'} /> */} <Button type='text' className='' icon={<AudioOutlined />} size={'middle'} /> */}

@ -4,11 +4,11 @@ import { DownOutlined, DollarOutlined, ExpandAltOutlined, ExpandOutlined, SendOu
import EmailEditorPopup from './EmailEditorPopup' import EmailEditorPopup from './EmailEditorPopup'
import useStyleStore from '@/stores/StyleStore' import useStyleStore from '@/stores/StyleStore'
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
// import { isEmpty, } from '@haina/utils-commons'; // import { isEmpty, } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { useOrderStore } from '@/stores/OrderStore' import { useOrderStore } from '@/stores/OrderStore'
import { EditIcon } from '@/components/Icons' import { EditIcon } from '@/components/Icons'
import { cloneDeep, isEmpty } from '@haina/utils-commons' import { cloneDeep, isEmpty } from '@/utils/commons'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { postSendEmail } from '@/actions/EmailActions' import { postSendEmail } from '@/actions/EmailActions'
import { sentMsgTypeMapped, } from '@/channel/bubbleMsgUtils'; import { sentMsgTypeMapped, } from '@/channel/bubbleMsgUtils';

@ -10,7 +10,7 @@ import useAuthStore from '@/stores/AuthStore';
import LexicalEditor from '@/components/LexicalEditor'; import LexicalEditor from '@/components/LexicalEditor';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { cloneDeep, debounce, isEmpty, } from '@haina/utils-commons'; import { cloneDeep, debounce, isEmpty, } from '@/utils/commons';
import { writeIndexDB } from '@/utils/indexedDB'; import { writeIndexDB } from '@/utils/indexedDB';
import './EmailEditor.css'; import './EmailEditor.css';
import { postSendEmail } from '@/actions/EmailActions'; import { postSendEmail } from '@/actions/EmailActions';

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { SendOutlined, CloseCircleOutlined, LoadingOutlined, FileOutlined } from '@ant-design/icons' import { SendOutlined, CloseCircleOutlined, LoadingOutlined, FileOutlined } from '@ant-design/icons'
import { isEmpty, } from '@haina/utils-commons'; import { isEmpty, } from '@/utils/commons';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { sentMsgTypeMapped, whatsappSupportFileTypes, uploadProgressSimulate, WABAccountsMapped } from '@/channel/bubbleMsgUtils'; import { sentMsgTypeMapped, whatsappSupportFileTypes, uploadProgressSimulate, WABAccountsMapped } from '@/channel/bubbleMsgUtils';
import { OSS_URL as aliOSSHost, DEFAULT_WABA } from '@/config'; import { OSS_URL as aliOSSHost, DEFAULT_WABA } from '@/config';

@ -4,7 +4,7 @@ import { FileAddOutlined } from '@ant-design/icons';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { API_HOST, OSS_URL as aliOSSHost } from '@/config'; import { API_HOST, OSS_URL as aliOSSHost } from '@/config';
import { whatsappSupportFileTypes, uploadProgressSimulate } from '@/channel/bubbleMsgUtils'; import { whatsappSupportFileTypes, uploadProgressSimulate } from '@/channel/bubbleMsgUtils';
import { isEmpty, sanitizeFilename } from '@haina/utils-commons'; import { isEmpty, sanitizeFilename } from '@/utils/commons';
// import useConversationStore from '@/stores/ConversationStore'; // import useConversationStore from '@/stores/ConversationStore';
const ImageUpload = ({ disabled, invokeUploadFileMessage, invokeSendUploadMessage }) => { const ImageUpload = ({ disabled, invokeUploadFileMessage, invokeSendUploadMessage }) => {

@ -1,24 +0,0 @@
import { createContext, useEffect, useState } from 'react'
import { Tooltip, Button } from 'antd'
import useUrlStore from '@/stores/UrlStore'
const ShortlinkBtn = ({ type, ...props }) => {
const [openShorturlDrawer] = useUrlStore((state) => [state.openDrawer])
return (
<>
<Tooltip title='短链接'>
{type === 'link' ? (
<Button type={'link'} onClick={() => openShorturlDrawer()}>
短链接
</Button>
) : (
<Button type='text' onClick={() => openShorturlDrawer()} size={'middle'} className='px-1'>
🔗
</Button>
)}
</Tooltip>
</>
)
}
export default ShortlinkBtn

@ -3,9 +3,9 @@ import { App, Popover, Flex, Button, List, Input, Tabs, Tag, Alert, Divider } fr
import { MessageOutlined, SendOutlined } from '@ant-design/icons'; import { MessageOutlined, SendOutlined } from '@ant-design/icons';
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { cloneDeep, flush, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, sortObjectsByKeysMap, TagColorStyle } from '@haina/utils-commons'; import { cloneDeep, flush, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, sortObjectsByKeysMap, TagColorStyle } from '@/utils/commons';
import { replaceTemplateString, whatsappTemplateBtnParamTypesMapped } from '@/channel/bubbleMsgUtils'; import { replaceTemplateString, whatsappTemplateBtnParamTypesMapped } from '@/channel/bubbleMsgUtils';
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore'; import useStyleStore from '@/stores/StyleStore';
const splitTemplate = (template) => { const splitTemplate = (template) => {
@ -27,7 +27,6 @@ const splitTemplate = (template) => {
// MARKETING // MARKETING
const templateCaterogyText = { 'UTILITY': '跟进', 'MARKETING': '营销' } const templateCaterogyText = { 'UTILITY': '跟进', 'MARKETING': '营销' }
const templateCaterogyTipText = { 'UTILITY': '触达率高', 'MARKETING': '' } const templateCaterogyTipText = { 'UTILITY': '触达率高', 'MARKETING': '' }
const templateCaterogyTipText2 = { 'UTILITY': '', 'MARKETING': '美国(+1)❌' }
const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, activeInput }) => { const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, activeInput }) => {
const currentConversation = useConversationStore((state) => state.currentConversation); const currentConversation = useConversationStore((state) => state.currentConversation);
@ -61,7 +60,7 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
<span key={ele.trim()} className=' text-wrap'> <span key={ele.trim()} className=' text-wrap'>
{ele.replace(/\n+/g, '\n')} {ele.replace(/\n+/g, '\n')}
</span> </span>
) : (ele.key.includes('free') || ele.key.includes('detail') || ele.key.includes('update') || ele.key.includes('info')) ? ( ) : ele.key.includes('free') || ele.key.includes('detail') ? (
<Input.TextArea <Input.TextArea
key={`${ele.key}_${i}`} key={`${ele.key}_${i}`}
rows={2} rows={2}
@ -71,8 +70,7 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
className={` w-11/12 `} className={` w-11/12 `}
size={'small'} size={'small'}
title={ele.key} title={ele.key}
// ${paramsVal[ele.key] || ele.key} placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格\n注意: 模板消息无法输入换行`}
placeholder={`按Tab键跳到下一个空格\n注意: 模板消息无法输入换行`}
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''} value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
// onPressEnter={() => handleSendTemplate(tempItem)} // onPressEnter={() => handleSendTemplate(tempItem)}
/> />
@ -165,11 +163,10 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
<span> <span>
{item.components.header?.[0]?.text || (item.displayName)} {item.components.header?.[0]?.text || (item.displayName)}
<Tag style={{ ...TagColorStyle(item.language.toUpperCase(), true) }} className='ml-1'> <Tag style={{ ...TagColorStyle(item.language.toUpperCase(), true) }} className='ml-1'>
{item.language.slice(-2).toUpperCase()} {item.language.toUpperCase()}
</Tag> </Tag>
{/* <Tag style={{...TagColorStyle(item.category.toUpperCase(), true)}}>{templateCaterogyText[item.category]}</Tag> */} {/* <Tag style={{...TagColorStyle(item.category.toUpperCase(), true)}}>{templateCaterogyText[item.category]}</Tag> */}
{templateCaterogyTipText[item.category] && <Tag style={{ ...TagColorStyle(item.category.toUpperCase(), true) }}>{templateCaterogyTipText[item.category]}</Tag>} {templateCaterogyTipText[item.category] && <Tag style={{ ...TagColorStyle(item.category.toUpperCase(), true) }}>{templateCaterogyTipText[item.category]}</Tag>}
{/* {templateCaterogyTipText2[item.category] && <Tag style={{ ...TagColorStyle(templateCaterogyTipText2[item.category].toUpperCase(), true) }}>{templateCaterogyTipText2[item.category]}</Tag>} */}
</span> </span>
<Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}> <Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
Send Send
@ -359,11 +356,9 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => {
<> <>
<Popover <Popover
overlayClassName={[mobile === false ? 'w-3/5' : 'w-full max-h-full'].join(' ')} overlayClassName={[mobile === false ? 'w-3/5' : 'w-full max-h-full'].join(' ')}
classNames={{root: [mobile === false ? 'w-3/5' : 'w-full max-h-full'].join(' ')}}
fresh fresh
forceRender forceRender
destroyTooltipOnHide={true} destroyTooltipOnHide={true}
destroyOnHidden={true}
title={ title={
<div className='flex justify-between mt-0 gap-4 items-center'> <div className='flex justify-between mt-0 gap-4 items-center'>
<Input.Search prefix={'💬'} <Input.Search prefix={'💬'}

@ -4,7 +4,7 @@ import { App, Button } from 'antd';
import { DownOutlined, LoadingOutlined } from '@ant-design/icons'; import { DownOutlined, LoadingOutlined } from '@ant-design/icons';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { groupBy, isEmpty, } from '@haina/utils-commons'; import { groupBy, isEmpty, } from '@/utils/commons';
import BubbleEmail from './Components/BubbleEmail'; import BubbleEmail from './Components/BubbleEmail';
import BubbleIM from './Components/BubbleIM'; import BubbleIM from './Components/BubbleIM';

@ -11,7 +11,7 @@ import ConversationNewItem from './ConversationsNewItem';
import EmailEditorPopup from './Input/EmailEditorPopup'; import EmailEditorPopup from './Input/EmailEditorPopup';
import EmailDetail from './Components/EmailDetail'; import EmailDetail from './Components/EmailDetail';
import { useOrderStore, } from "@/stores/OrderStore"; import { useOrderStore, } from "@/stores/OrderStore";
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore'; import useStyleStore from '@/stores/StyleStore';
import EmailListDrawer from './Components/EmailListDrawer'; import EmailListDrawer from './Components/EmailListDrawer';
import { POPUP_FEATURES } from '@/config'; import { POPUP_FEATURES } from '@/config';
@ -39,7 +39,6 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
const [longList, setLongList] = useState([]); const [longList, setLongList] = useState([]);
const [longListLoading, setLongListLoading] = useState(false); const [longListLoading, setLongListLoading] = useState(false);
const [shouldScrollBottom, setShouldScrollBottom] = useState(true); const [shouldScrollBottom, setShouldScrollBottom] = useState(true);
const [currentListLasttime, setCurrentListLasttime] = useState('');
const prevSN = useRef(currentConversationSN); const prevSN = useRef(currentConversationSN);
const prevColiSN = useRef(currentConversationColiSN); const prevColiSN = useRef(currentConversationColiSN);
@ -141,7 +140,7 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
conversationid: currentConversation.sn, conversationid: currentConversation.sn,
opisn: currentConversation.opi_sn, opisn: currentConversation.opi_sn,
whatsappid: currentConversation.whatsapp_phone_number, whatsappid: currentConversation.whatsapp_phone_number,
lasttime: currentListLasttime || '', lasttime: currentConversation?.lasttime || '',
coli_sn: currentConversation.coli_sn || '', coli_sn: currentConversation.coli_sn || '',
}) })
setLongListLoading(false); setLongListLoading(false);
@ -239,8 +238,6 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
const [emailList, setEmailList] = useState([]); const [emailList, setEmailList] = useState([]);
useEffect(() => { useEffect(() => {
const thisLastTime = longList.length > 0 ? longList[0].msgtime : '';
setCurrentListLasttime(thisLastTime);
const _emailList = longList const _emailList = longList
.filter((item) => item.msg_source === 'email') .filter((item) => item.msg_source === 'email')
.map((ele) => ({ .map((ele) => ({

@ -7,7 +7,7 @@ import { WABIcon } from '@/components/Icons';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import useStyleStore from '@/stores/StyleStore'; import useStyleStore from '@/stores/StyleStore';
import { isEmpty } from '@haina/utils-commons'; import { isEmpty } from '@/utils/commons';
import { DEFAULT_CHANNEL } from '@/config'; import { DEFAULT_CHANNEL } from '@/config';
import { WABAccounts, WABAccountsMapped } from '@/channel/bubbleMsgUtils'; import { WABAccounts, WABAccountsMapped } from '@/channel/bubbleMsgUtils';
import useAuthStore, { PERM_USE_WHATSAPP } from '@/stores/AuthStore'; import useAuthStore, { PERM_USE_WHATSAPP } from '@/stores/AuthStore';

@ -4,7 +4,7 @@ import { useEffect, useState, useRef, useCallback } from "react";
import { useNavigate, } from "react-router-dom"; import { useNavigate, } from "react-router-dom";
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { copy, isEmpty } from "@haina/utils-commons"; import { copy, isEmpty } from "@/utils/commons";
import { Conditional } from "@/components/Conditional"; import { Conditional } from "@/components/Conditional";
import useConversationStore from "@/stores/ConversationStore"; import useConversationStore from "@/stores/ConversationStore";
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions, fetchSetRemindStateAction, remindStatusOptionsMapped } from "@/stores/OrderStore"; import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions, fetchSetRemindStateAction, remindStatusOptionsMapped } from "@/stores/OrderStore";
@ -44,10 +44,10 @@ const CustomerProfile = ({ disabled }) => {
const [chatOrder, setChatOrder] = useState(currentOrder); const [chatOrder, setChatOrder] = useState(currentOrder);
const [orderRemindState, setOrderRemindState] = useState(orderDetail.remindstate); const [orderRemindState, setOrderRemindState] = useState(orderDetail.remindstate);
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions); const orderLabelOptions = copy(OrderLabelDefaultOptions);
orderLabelOptions.unshift({ value: 0, label: "未设置", disabled: true }); orderLabelOptions.unshift({ value: 0, label: "未设置", disabled: true });
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions); const orderStatusOptions = copy(OrderStatusDefaultOptions);
const getHistoryOrder = (email, whatsappid='') => { const getHistoryOrder = (email, whatsappid='') => {
return fetchHistoryOrder(loginUser.userId, email, whatsappid) return fetchHistoryOrder(loginUser.userId, email, whatsappid)

@ -3,7 +3,6 @@ import { Button, Flex, List, Popover } from 'antd'
import { useState } from 'react' import { useState } from 'react'
import EmailQuotation from '../Components/EmailQuotation' import EmailQuotation from '../Components/EmailQuotation'
// @Deprecated
const QuotesHistory = ((props) => { const QuotesHistory = ((props) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)

@ -1,7 +1,6 @@
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import useSnippetStore from '@/stores/SnippetStore' import useSnippetStore from '@/stores/SnippetStore'
import { useOrderStore } from '@/stores/OrderStore' import { useOrderStore } from '@/stores/OrderStore'
import useUrlStore from '@/stores/UrlStore'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { useThemeContext } from '@/stores/ThemeContext' import { useThemeContext } from '@/stores/ThemeContext'
import { DownOutlined } from '@ant-design/icons' import { DownOutlined } from '@ant-design/icons'
@ -23,12 +22,12 @@ import { useEffect, useState, useCallback, useRef } from 'react'
import { Link, NavLink, Outlet, useHref } from 'react-router-dom' import { Link, NavLink, Outlet, useHref } from 'react-router-dom'
import '@/assets/App.css' import '@/assets/App.css'
import AppLogo from '@/assets/highlights_travel_300_300_250613.png' import AppLogo from '@/assets/highlights_travel_300_300.png'
import 'react-chat-elements/dist/main.css' import 'react-chat-elements/dist/main.css'
import ReloadPrompt from './ReloadPrompt' import ReloadPrompt from './ReloadPrompt'
import ClearCache from './ClearCache' import ClearCache from './ClearCache'
import { BUILD_VERSION, GIT_HEAD } from '@/config' import { BUILD_VERSION, BUILD_DATE } from '@/config'
const { Header, Footer, Content } = Layout const { Header, Footer, Content } = Layout
const { Title } = Typography const { Title } = Typography
@ -60,23 +59,12 @@ function DesktopApp() {
state.closeDrawer, state.closeDrawer,
state.drawerOpen, state.drawerOpen,
]) ])
const [
openShorturlDrawer,
closeShorturlDrawer,
shorturlDrawerOpen,
] = useUrlStore((state) => [
state.openDrawer,
state.closeDrawer,
state.drawerOpen,
])
const onClick = ({ key }) => { const onClick = ({ key }) => {
if (key === 'snippet-list') { if (key === 'snippet-list') {
openSnippetDrawer() openSnippetDrawer()
} else if (key == 'generate-payment') { } else if (key == 'generate-payment') {
openPaymentDrawer() openPaymentDrawer()
} else if (key === 'shorturl-conversion') {
openShorturlDrawer()
} }
} }
@ -135,7 +123,7 @@ function DesktopApp() {
background: 'white', background: 'white',
}}> }}>
<Row gutter={{ md: 24 }} align='middle'> <Row gutter={{ md: 24 }} align='middle'>
<Col flex='240px'> <Col flex='220px'>
<NavLink to='/'> <NavLink to='/'>
<img src={AppLogo} className='logo' alt='App logo' /> <img src={AppLogo} className='logo' alt='App logo' />
</NavLink> </NavLink>
@ -195,15 +183,10 @@ function DesktopApp() {
label: <Link to='/account/profile'>个人资料</Link>, label: <Link to='/account/profile'>个人资料</Link>,
key: 'profile', key: 'profile',
}, },
{ type: 'divider' },
{ {
label: '支付链接', label: '支付链接',
key: 'generate-payment', key: 'generate-payment',
}, },
{
label: '短链接转换',
key: 'shorturl-conversion',
},
{ {
label: '图文集', label: '图文集',
key: 'snippet-list', key: 'snippet-list',
@ -248,7 +231,7 @@ function DesktopApp() {
</Content> </Content>
</Layout> </Layout>
<Footer> <Footer>
桂林海纳国际旅行社有限公司 Version: {BUILD_VERSION}({GIT_HEAD}) 桂林海纳国际旅行社有限公司 Version: {BUILD_VERSION}({BUILD_DATE})
</Footer> </Footer>
</Layout> </Layout>
) )

@ -9,7 +9,7 @@ import useAuthStore from '@/stores/AuthStore'
import LexicalEditor from '@/components/LexicalEditor' import LexicalEditor from '@/components/LexicalEditor'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { cloneDeep, debounce, isEmpty, olog, omitEmpty } from '@haina/utils-commons' import { cloneDeep, debounce, isEmpty, olog, omitEmpty } from '@/utils/commons'
import { writeIndexDB, readIndexDB, deleteIndexDBbyKey, } from '@/utils/indexedDB'; import { writeIndexDB, readIndexDB, deleteIndexDBbyKey, } from '@/utils/indexedDB';
import '@/views/Conversations/Online/Input/EmailEditor.css' import '@/views/Conversations/Online/Input/EmailEditor.css'
@ -46,19 +46,18 @@ const parseHTMLText = (html) => {
const parser = new DOMParser() const parser = new DOMParser()
const dom = parser.parseFromString(html, 'text/html') const dom = parser.parseFromString(html, 'text/html')
// Replace <br> and <p> with line breaks // Replace <br> and <p> with line breaks
// Array.from(dom.body.querySelectorAll('br, p')).forEach((el) => { Array.from(dom.body.querySelectorAll('br, p')).forEach((el) => {
// el.textContent = '<br>' + el.textContent el.textContent = '\n' + el.textContent
// }) })
// Replace <hr> with a line of dashes // Replace <hr> with a line of dashes
// Array.from(dom.body.querySelectorAll('hr')).forEach((el) => { Array.from(dom.body.querySelectorAll('hr')).forEach((el) => {
// el.innerHTML = '<p><hr>------------------------------------------------------------------</p>' el.textContent = '\n------------------------------------------------------------------\n'
// }) })
const line = '<p>------------------------------------------------------------------</p>' return dom.body.textContent || ''
return line+(dom.body.innerHTML || '')
} }
const generateQuoteContent = (mailData, isRichText = true) => { const generateQuoteContent = (mailData, isRichText = true) => {
const html = `<br><hr><blockquote><p class="font-sans"><b><strong >From: </strong></b><span >${(mailData.info?.MAI_From || '') const html = `<br><br><hr><p class="font-sans"><b><strong >From: </strong></b><span >${(mailData.info?.MAI_From || '')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;')} </span></p><p class="font-sans"><b><strong >Sent: </strong></b><span >${ .replace(/>/g, '&gt;')} </span></p><p class="font-sans"><b><strong >Sent: </strong></b><span >${
mailData.info?.MAI_SendDate || '' mailData.info?.MAI_SendDate || ''
@ -66,11 +65,11 @@ const generateQuoteContent = (mailData, isRichText = true) => {
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;')}</span></p><p class="font-sans"><b><strong >Subject: </strong></b><span >${mailData.info?.MAI_Subject || ''}</span></p><p>${ .replace(/>/g, '&gt;')}</span></p><p class="font-sans"><b><strong >Subject: </strong></b><span >${mailData.info?.MAI_Subject || ''}</span></p><p>${
mailData.info?.MAI_ContentType === 'text/html' ? mailData.content : mailData.content.replace(/\r\n/g, '<br>') mailData.info?.MAI_ContentType === 'text/html' ? mailData.content : mailData.content.replace(/\r\n/g, '<br>')
}</p></blockquote>` }</p>`
return isRichText ? html : parseHTMLText(html) return isRichText ? html : parseHTMLText(html)
} }
const generateMailContent = (mailData) => mailData.info?.MAI_ContentType === 'text/html' ? `${mailData.content}<br>` : `<p>${mailData.content.replace(/\r\n/g, '<br>')}</p>` const generateMailContent = (mailData) => `${mailData.content}<br>`
/** /**
* 独立窗口编辑器 * 独立窗口编辑器
@ -200,8 +199,6 @@ const NewEmail = () => {
mat_sn: emailAccount?.mat_sn || info?.MAI_MAT_SN || defaultMAT, mat_sn: emailAccount?.mat_sn || info?.MAI_MAT_SN || defaultMAT,
opi_sn: emailAccount?.opi_sn || info?.MAI_OPI_SN || orderDetail.opi_sn || '', opi_sn: emailAccount?.opi_sn || info?.MAI_OPI_SN || orderDetail.opi_sn || '',
} }
const originalContentType = info?.mailType === 'text/html';
setIsRichText(originalContentType)
let readyToInitialContent = ''; let readyToInitialContent = '';
let _formValues = {}; let _formValues = {};
@ -216,7 +213,7 @@ const NewEmail = () => {
// 稿: ``id // 稿: ``id
if (!isEmpty(mailData.info) && !['edit'].includes(pageParam.action)) { if (!isEmpty(mailData.info) && !['edit'].includes(pageParam.action)) {
readyToInitialContent = orderPrefix + '<br>' + signatureBody readyToInitialContent = orderPrefix + signatureBody
} }
switch (pageParam.action) { switch (pageParam.action) {
case 'reply': case 'reply':
@ -228,21 +225,17 @@ const NewEmail = () => {
subject: `Re: ${info.MAI_Subject || ''}`, subject: `Re: ${info.MAI_Subject || ''}`,
..._form2 ..._form2
} }
readyToInitialContent += generateQuoteContent(mailData, originalContentType)
break break
case 'replyall': { case 'replyall':
const tosNotMe = quotedMailSenderObj ? (info?.replyToAll || []).filter(addr => addr.indexOf(quotedMailSenderObj) === -1).join(', ') : (info?.replyToAll || []).join(', ');
_formValues = { _formValues = {
from: quotedMailSenderObj, from: quotedMailSenderObj,
to: isEmpty(info?.replyToAll) ? orderReceiver : tosNotMe, to: info?.replyToAll || orderReceiver,
cc: info?.MAI_CS || '', cc: info?.MAI_CS || '',
// bcc: quote.bcc || '', // bcc: quote.bcc || '',
subject: `Re: ${info.MAI_Subject || ''}`, subject: `Re: ${info.MAI_Subject || ''}`,
..._form2 ..._form2
} }
readyToInitialContent += generateQuoteContent(mailData, originalContentType)
break break
}
case 'forward': case 'forward':
_formValues = { _formValues = {
from: quotedMailSenderObj, from: quotedMailSenderObj,
@ -250,7 +243,6 @@ const NewEmail = () => {
// coli_sn: pageParam.oid, // coli_sn: pageParam.oid,
..._form2 ..._form2
} }
readyToInitialContent += generateQuoteContent(mailData, originalContentType)
break break
case 'edit': case 'edit':
_formValues = { _formValues = {
@ -262,7 +254,7 @@ const NewEmail = () => {
mai_sn: pageParam.quoteid, mai_sn: pageParam.quoteid,
..._form2 ..._form2
} }
readyToInitialContent = generateMailContent(mailData) readyToInitialContent = '<br>'+ generateMailContent(mailData)
setFileList(mailData.attachments.map(ele => ({ uid: ele.ATI_SN, name: ele.ATI_Name, url: ele.ATI_ServerFile, fullPath: `${EMAIL_ATTA_HOST}${ele.ATI_ServerFile}` }))) setFileList(mailData.attachments.map(ele => ({ uid: ele.ATI_SN, name: ele.ATI_Name, url: ele.ATI_ServerFile, fullPath: `${EMAIL_ATTA_HOST}${ele.ATI_ServerFile}` })))
break break
case 'new': case 'new':
@ -273,9 +265,8 @@ const NewEmail = () => {
subject: `${info.MAI_Subject || templateFormValues.subject || ''}`, subject: `${info.MAI_Subject || templateFormValues.subject || ''}`,
..._form2, ..._form2,
} }
readyToInitialContent = generateMailContent({ content: templateContent.bodycontent || readyToInitialContent || `<p></p><br>${signatureBody}` || '' }) readyToInitialContent = generateMailContent({ content: templateContent.bodycontent || readyToInitialContent || `<br>${signatureBody}` || '' })
// setFileList(mailData.attachments.map(ele => ({ uid: ele.ATI_SN, name: ele.ATI_Name, url: ele.ATI_ServerFile, fullPath: `${EMAIL_ATTA_HOST}${ele.ATI_ServerFile}` }))) setFileList(mailData.attachments.map(ele => ({ uid: ele.ATI_SN, name: ele.ATI_Name, url: ele.ATI_ServerFile, fullPath: `${EMAIL_ATTA_HOST}${ele.ATI_ServerFile}` })))
setIsRichText(true)
break break
default: default:
@ -350,14 +341,12 @@ const NewEmail = () => {
} }
const handleEditorChange = ({ editorStateJSON, htmlContent, textContent }) => { const handleEditorChange = ({ editorStateJSON, htmlContent, textContent }) => {
const _text = textContent.replace(/\r\n/g, '\n').replace(/\n{2,}/g, '\n') // console.log('textContent', textContent);
// console.log('textContent---\n', textContent, 'textContent');
// console.log('html', html); // console.log('html', html);
setHtmlContent(htmlContent) setHtmlContent(htmlContent)
setTextContent(_text) setTextContent(textContent)
form.setFieldValue('content', htmlContent) form.setFieldValue('content', htmlContent)
const abstract = _text; const { bodyText: abstract } = parseHTMLString(htmlContent, true);
// const { bodyText: abstract } = parseHTMLString(htmlContent, true);
// form.setFieldValue('abstract', getAbstract(textContent)) // form.setFieldValue('abstract', getAbstract(textContent))
const formValues = omitEmpty(form.getFieldsValue()); const formValues = omitEmpty(form.getFieldsValue());
if (!isEmpty(formValues)) { if (!isEmpty(formValues)) {
@ -507,12 +496,11 @@ const NewEmail = () => {
body.attaList = fileList; body.attaList = fileList;
// console.log('body', body, '\n', fileList); // console.log('body', body, '\n', fileList);
const values = await form.validateFields() const values = await form.validateFields()
// const preQuoteBody = !['edit', 'new'].includes(pageParam.action) && pageParam.quoteid ? (quoteContent ? quoteContent : generateQuoteContent(mailData, isRichText)) : '' const preQuoteBody = !['edit', 'new'].includes(pageParam.action) && pageParam.quoteid ? (quoteContent ? quoteContent : generateQuoteContent(mailData, isRichText)) : ''
body.mailcontent = isRichText ? EmailBuilder({ subject: values.subject, content: htmlContent }) : textContent body.mailcontent = isRichText ? EmailBuilder({ subject: values.subject, content: htmlContent + preQuoteBody }) : textContent + preQuoteBody
body.cc = values.cc || '' body.cc = values.cc || ''
body.bcc = values.bcc || '' body.bcc = values.bcc || ''
body.mailtype = values.mailtype || '' body.bcc = values.mailtype || ''
body.order_mail_type = values.mailtype || ''
setSendLoading(!isDraft) setSendLoading(!isDraft)
notification.open({ notification.open({
key: editorKey, key: editorKey,
@ -525,13 +513,10 @@ const NewEmail = () => {
}) })
// body.externalID = stickToCid // body.externalID = stickToCid
// body.actionID = `${stickToCid}.${msgObj.id}` // body.actionID = `${stickToCid}.${msgObj.id}`
body.actionID = `0.${uuid()}`
body.contenttype = isRichText ? 'text/html' : 'text/plain' body.contenttype = isRichText ? 'text/html' : 'text/plain'
try { try {
// console.log('postSendEmail', body, '\n'); // console.log('postSendEmail', body, '\n');
// console.log('🎈postSendEmail mailContent', body.mailcontent, '\n');
// throw new Error('test')
// return; // return;
const mailSavedId = await postEmailSaveOrSend(body, isDraft) const mailSavedId = await postEmailSaveOrSend(body, isDraft)
form.setFieldsValue({ form.setFieldsValue({
@ -577,12 +562,10 @@ const NewEmail = () => {
const idleCallbackId = useRef(null) const idleCallbackId = useRef(null)
const debouncedSave = useCallback( const debouncedSave = useCallback(
debounce((data) => { debounce((data) => {
if ('requestIdleCallback' in window) { idleCallbackId.current = window.requestIdleCallback(() => {
idleCallbackId.current = window.requestIdleCallback(() => { console.log('Saving data (idle, debounced):', data)
console.log('Saving data (idle, debounced):', data) writeIndexDB([{ ...data, key: editorKey }], 'draft', 'mailbox')
writeIndexDB([{ ...data, key: editorKey }], 'draft', 'mailbox') })
})
}
}, 1500), // 1.5s }, 1500), // 1.5s
[], [],
) )
@ -620,7 +603,7 @@ const NewEmail = () => {
// labelCol={{ span: 3 }} // labelCol={{ span: 3 }}
> >
<div className='w-full flex flex-wrap gap-2 justify-start items-center text-indigo-600 pb-1 mb-2 border-x-0 border-t-0 border-b border-solid border-neutral-200'> <div className='w-full flex flex-wrap gap-2 justify-start items-center text-indigo-600 pb-1 mb-2 border-x-0 border-t-0 border-b border-solid border-neutral-200'>
<Button type='primary' size='middle' onClick={() => onHandleSaveOrSend()} loading={sendLoading} icon={<SendOutlined />}> <Button type='primary' size='middle' onClick={onHandleSaveOrSend} loading={sendLoading} icon={<SendOutlined />}>
发送 发送
</Button> </Button>
<Form.Item name={'from'} rules={[{ required: true, message: '请选择发件地址' }]} > <Form.Item name={'from'} rules={[{ required: true, message: '请选择发件地址' }]} >
@ -646,7 +629,7 @@ const NewEmail = () => {
<Form.Item className='w-full'> <Form.Item className='w-full'>
<Space.Compact className='w-full'> <Space.Compact className='w-full'>
<Form.Item name={'to'} label='收件人' rules={[{ required: true }]} className='!flex-1'> <Form.Item name={'to'} label='收件人' rules={[{ required: true }]} className='!flex-1'>
<Input.TextArea rows={1} autoSize className='w-full' /> <Input className='w-full' />
</Form.Item> </Form.Item>
<Flex gap={4}> <Flex gap={4}>
{!showCc && ( {!showCc && (
@ -663,13 +646,13 @@ const NewEmail = () => {
</Space.Compact> </Space.Compact>
</Form.Item> </Form.Item>
<Form.Item label='抄&nbsp;&nbsp;&nbsp;&nbsp;送' name={'cc'} hidden={!showCc} className='w-full pt-1'> <Form.Item label='抄&nbsp;&nbsp;&nbsp;&nbsp;送' name={'cc'} hidden={!showCc} className='w-full pt-1'>
<Input.TextArea rows={1} autoSize /> <Input />
</Form.Item> </Form.Item>
<Form.Item label='密&nbsp;&nbsp;&nbsp;&nbsp;送' name={'bcc'} hidden={!showBcc} className='w-full pt-1'> <Form.Item label='密&nbsp;&nbsp;&nbsp;&nbsp;送' name={'bcc'} hidden={!showBcc} className='w-full pt-1'>
<Input.TextArea rows={1} autoSize /> <Input />
</Form.Item> </Form.Item>
<Form.Item label='主&nbsp;&nbsp;&nbsp;&nbsp;题' name={'subject'} rules={[{ required: true }]} className='w-full pt-1'> <Form.Item label='主&nbsp;&nbsp;&nbsp;&nbsp;题' name={'subject'} rules={[{ required: true }]} className='w-full pt-1'>
<Input.TextArea rows={1} autoSize /> <Input />
</Form.Item> </Form.Item>
<Form.Item name='atta' label='' className='w-full py-1 border-b-0' valuePropName='fileList' getValueFromEvent={normFile}> <Form.Item name='atta' label='' className='w-full py-1 border-b-0' valuePropName='fileList' getValueFromEvent={normFile}>
<Flex justify='space-between'> <Flex justify='space-between'>
@ -720,14 +703,14 @@ const NewEmail = () => {
</Form.Item> </Form.Item>
</Form> </Form>
<LexicalEditor {...{ isRichText }} onChange={handleEditorChange} defaultValue={initialContent} /> <LexicalEditor {...{ isRichText }} onChange={handleEditorChange} defaultValue={initialContent} />
{/* {!isEmpty(Number(pageParam.quoteid)) && pageParam.action!=='edit' && !showQuoteContent && ( {!isEmpty(Number(pageParam.quoteid)) && pageParam.action!=='edit' && !showQuoteContent && (
<div className='flex justify-start items-center ml-2'> <div className='flex justify-start items-center ml-2'>
<Button className='flex gap-2 ' type='link' onClick={() => { <Button className='flex gap-2 ' type='link' onClick={() => setShowQuoteContent(!showQuoteContent)}>
setShowQuoteContent(!showQuoteContent); 显示引用内容 {/*(不可更改)*/}
setInitialContent(pre => pre + generateQuoteContent(mailData))
}}>
显示引用内容
</Button> </Button>
{/* <Button className='flex gap-2 ' type='link' danger onClick={() => {setMergeQuote(false);setShowQuoteContent(false)}}>
删除引用内容
</Button> */}
</div> </div>
)} )}
{showQuoteContent && ( {showQuoteContent && (
@ -736,7 +719,7 @@ const NewEmail = () => {
className='border-0 outline-none cursor-text' className='border-0 outline-none cursor-text'
onBlur={(e) => setQuoteContent(`<blockquote>${e.target.innerHTML}</blockquote>`)} onBlur={(e) => setQuoteContent(`<blockquote>${e.target.innerHTML}</blockquote>`)}
dangerouslySetInnerHTML={{ __html: generateQuoteContent(mailData) }}></blockquote> dangerouslySetInnerHTML={{ __html: generateQuoteContent(mailData) }}></blockquote>
)} */} )}
</ConfigProvider> </ConfigProvider>
</> </>
) )

@ -1,5 +1,5 @@
// import './ReloadPrompt.css'; // import './ReloadPrompt.css';
import { clearAllCaches } from '@haina/utils-commons'; import { clearAllCaches } from '@/utils/commons';
import { useRegisterSW } from 'virtual:pwa-register/react'; import { useRegisterSW } from 'virtual:pwa-register/react';
// import { pwaInfo } from 'virtual:pwa-info'; // import { pwaInfo } from 'virtual:pwa-info';
@ -65,7 +65,7 @@ function ReloadPrompt({ force, ...props }) {
needRefresh ? updateServiceWorker(true) : forceReload() needRefresh ? updateServiceWorker(true) : forceReload()
}}> }}>
{force ? '系统更新' : `新版本发布了,点击👉马上更新`}{needRefresh && '🚀'} {force ? '系统更新' : `新版本发布了,点击👉马上更新`}{needRefresh && '🚀'}
</a>} type="info" showIcon={needRefresh} icon={needRefresh ? '🎉' : null} /> </a>} type="info" showIcon icon={'🎉'} />
)} )}
</> </>

@ -1,5 +1,5 @@
import '@/assets/App.css' import '@/assets/App.css'
import AppLogo from '@/assets/highlights_travel_300_300_250613.png' import AppLogo from '@/assets/highlights_travel_300_300.png'
import { useThemeContext } from '@/stores/ThemeContext' import { useThemeContext } from '@/stores/ThemeContext'
import { App as AntApp, Col, ConfigProvider, Empty, Layout, Row, Typography, theme } from 'antd' import { App as AntApp, Col, ConfigProvider, Empty, Layout, Row, Typography, theme } from 'antd'
import { NavLink, Outlet } from 'react-router-dom' import { NavLink, Outlet } from 'react-router-dom'

@ -9,12 +9,13 @@ import useStyleStore from '@/stores/StyleStore';
function GeneratePayment() { function GeneratePayment() {
const { message, notification } = App.useApp() const { notification } = App.useApp()
const [messageApi, contextHolder] = message.useMessage()
const [mobile, setMobile] = useStyleStore((state) => [state.mobile, state.setMobile]); const [mobile, setMobile] = useStyleStore((state) => [state.mobile, state.setMobile]);
const [generateForm] = Form.useForm() const [generateForm] = Form.useForm()
const [loginUser] = useAuthStore((s) => [s.loginUser]) const [getPrimaryEmail, loginUser] = useAuthStore((s) => [s.getPrimaryEmail, s.loginUser])
const [generatePayment, fetchOrderDetail] = useOrderStore((s) => [s.generatePayment, s.fetchOrderDetail]) const [generatePayment, fetchOrderDetail] = useOrderStore((s) => [s.generatePayment, s.fetchOrderDetail])
const currentOrder = useConversationStore((state) => state.currentConversation?.coli_sn || '') const currentOrder = useConversationStore((state) => state.currentConversation?.coli_sn || '')
@ -44,19 +45,24 @@ function GeneratePayment() {
const orderNumber = result.orderDetail.order_no const orderNumber = result.orderDetail.order_no
const travelAdvisor = loginUser.accountList.length > 0 ? (loginUser.accountList[0]?.OPI_NameEN || '') : '' const travelAdvisor = loginUser.accountList.length > 0 ? (loginUser.accountList[0]?.OPI_NameEN || '') : ''
generateForm.setFieldsValue({ generateForm.setFieldsValue({
// notifyEmail: getPrimaryEmail(),
orderNumber: orderNumber, orderNumber: orderNumber,
description: 'Tracking Code: ' + orderNumber + '\r\nTravel Advisor: ' + travelAdvisor + '\r\nContent: \r\n', description: 'Tracking Code: ' + orderNumber + '\r\nTravel Advisor: ' + travelAdvisor + '\r\nContent: \r\n',
langauge: 'en_HTravel', langauge: 'US',
orderType: '227001', orderType: '227001',
currency: 'USD', currency: 'USD',
amount: 1, amount: 1,
userId: loginUser.userId, userId: loginUser.userId,
}) })
}) })
// .finally(() => setLoading(false))
// .catch(() => {
// })
} else { } else {
generateForm.setFieldsValue({ generateForm.setFieldsValue({
// notifyEmail: getPrimaryEmail(),
description: 'Tracking Code: \r\nTravel Advisor: \r\nContent: \r\n', description: 'Tracking Code: \r\nTravel Advisor: \r\nContent: \r\n',
langauge: 'en_HTravel', langauge: 'US',
orderType: '227001', orderType: '227001',
currency: 'USD', currency: 'USD',
amount: 1, amount: 1,
@ -103,7 +109,6 @@ function GeneratePayment() {
]}> ]}>
<Select <Select
options={[ options={[
{ value: 'en_HTravel', label: '英语(Highlights)' },
{ value: 'US', label: '英语' }, { value: 'US', label: '英语' },
{ value: 'de_de', label: '德语' }, { value: 'de_de', label: '德语' },
{ value: 'fr_fr', label: '法语' }, { value: 'fr_fr', label: '法语' },
@ -131,7 +136,7 @@ function GeneratePayment() {
rules={[ rules={[
{ {
required: true, required: true,
message: '金额填', message: '金额填',
}, },
]}> ]}>
<InputNumber <InputNumber
@ -181,7 +186,7 @@ function GeneratePayment() {
</Tooltip> </Tooltip>
<span>支付按钮</span> <span>支付按钮</span>
</Flex> </Flex>
<HtmlPreview value={generatedObject.payhtml} loading={isHtmlLoading} onCopied={() => message.success('已复制😀')} /> <HtmlPreview value={generatedObject.payhtml} loading={isHtmlLoading} onCopied={() => messageApi.success('已复制')} />
<Flex gap='small'> <Flex gap='small'>
<Tooltip placement='topLeft' title='发送 WhatsApp 使用'> <Tooltip placement='topLeft' title='发送 WhatsApp 使用'>
<InfoCircleOutlined /> <InfoCircleOutlined />
@ -191,7 +196,7 @@ function GeneratePayment() {
size='small' size='small'
onClick={() => { onClick={() => {
navigator.clipboard.writeText(generatedObject.paylink) navigator.clipboard.writeText(generatedObject.paylink)
message.success('复制😀') messageApi.success('复制成功😀')
}}> }}>
复制链接 复制链接
</Button> </Button>
@ -199,6 +204,7 @@ function GeneratePayment() {
<Typography.Text>{generatedObject.paylink}</Typography.Text> <Typography.Text>{generatedObject.paylink}</Typography.Text>
</Flex> </Flex>
</div> </div>
{contextHolder}
</Flex> </Flex>
</> </>
) )

@ -1,6 +1,6 @@
import { Empty, Skeleton, Divider, Flex, Button } from 'antd' import { Empty, Skeleton, Divider, Flex, Button } from 'antd'
import { Conditional } from '@/components/Conditional' import { Conditional } from '@/components/Conditional'
import { isNotEmpty } from '@haina/utils-commons' import { isNotEmpty } from '@/utils/commons'
const HtmlPreview = (props) => { const HtmlPreview = (props) => {
const { loading = false, value, onEdit, onCopied, onDelete } = props const { loading = false, value, onEdit, onCopied, onDelete } = props

@ -1,24 +0,0 @@
import { useEffect, useCallback, useState } from 'react'
import { Typography, App, Flex, Input, QRCode } from 'antd'
import { ReloadOutlined, CheckCircleFilled, ExclamationCircleFilled } from '@ant-design/icons'
import useAuthStore from '@/stores/AuthStore'
import { fetchQRCode } from '@/actions/WaiAction'
import useConversationStore from '@/stores/ConversationStore'
import { isEmpty } from '@haina/utils-commons'
// QR Code WhatsApp Baileys
const LocalWhatsAppQRCode = () => {
const [qrCode, setQRCode] = useState('expired')
return (
<Flex justify='center' align='center' gap='middle' vertical>
<Typography.Title level={4}>WhatsApp Baileys</Typography.Title>
<QRCode size={264} value={qrCode} errorLevel='L' />
<Input style={{ width: 400, marginTop: 16 }} placeholder='QR Code' onChange={(e) => {
setQRCode(e.target.value)
}}/>
</Flex>
)
}
export default LocalWhatsAppQRCode

@ -6,7 +6,7 @@ import { Conditional } from '@/components/Conditional'
import { PERM_USE_WHATSAPP } from '@/stores/AuthStore' import { PERM_USE_WHATSAPP } from '@/stores/AuthStore'
import { usingStorage } from '@/utils/usingStorage'; import { usingStorage } from '@/utils/usingStorage';
import { isEmpty } from '@haina/utils-commons' import { isEmpty } from '@/utils/commons'
import { WAI_SERVER_KEY } from '@/config'; import { WAI_SERVER_KEY } from '@/config';
import WAIQRCode from './WAIQRCode'; import WAIQRCode from './WAIQRCode';
@ -88,14 +88,25 @@ function Profile() {
value: '+639454682947', value: '+639454682947',
label: 'GH 客运(+639454682947)', label: 'GH 客运(+639454682947)',
}, },
{
value: '+85265210895',
label: 'GH 客运 HK(+85265210895)',
},
{ {
value: '+8618174165365', value: '+8618174165365',
label: '国际事业部(+8618174165365)', label: '国际事业部(+8618174165365)',
}, },
{
value: 'GH 客服',
label: 'GH 客服(无)',
disabled: true,
},
{
value: 'CT 事业部',
label: 'CT 事业部(无)',
disabled: true,
},
{
value: '花梨鹰事业部',
label: '花梨鹰事业部(无)',
disabled: true,
},
]} ]}
/> />
</Form.Item> </Form.Item>

@ -1,68 +0,0 @@
import { Input, Button, Space, Typography } from 'antd'
import { LinkOutlined } from '@ant-design/icons'
import useShortUrlChange from '@/hooks/useShorturlchange'
import { useState } from 'react'
const { Title, Text, Paragraph } = Typography
function ShorturlConversion() {
const [longUrl, setLongUrl] = useState('')
const [shortUrl, setShortUrl] = useState('')
const { convertUrl } = useShortUrlChange()
const handleConvert = async () => {
const result = await convertUrl(longUrl);
if (result) {
setShortUrl(result);
}
};
return (
<Space direction='vertical' size='large' className='w-full'>
<div>
<Text strong style={{ fontSize: '18px' }}>长链接</Text>
<Input
placeholder="输入需要转换的长链接"
value={longUrl}
onChange={(e) => setLongUrl(e.target.value)}
onPressEnter={handleConvert}
prefix={<LinkOutlined />}
size="large"
/>
</div>
<Button
type='primary'
size='medium'
onClick={handleConvert}
icon={<LinkOutlined />}
>
转换
</Button>
{shortUrl && (
<div>
<Text strong style={{ fontSize: '18px' }}>转换后的短链接</Text>
<Paragraph
copyable={shortUrl ? {
text: shortUrl,
tooltips: ['点击复制', '已复制!'],
} : false}
style={{ fontSize: '17px' }}
>
<a
href={shortUrl}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: 'underline' }}
>
{shortUrl}
</a>
</Paragraph>
<Text type="secondary" style={{ fontSize: '14px', marginTop: '4px', display: 'block' }}>
说明使用短链接前可点击测试短链接是否可用
</Text>
</div>)}
</Space>
);
}
export default ShorturlConversion

@ -7,7 +7,7 @@ import useSnippetStore from '@/stores/SnippetStore'
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
function SnippetList() { function SnippetList() {
const { message, notification } = App.useApp() const [messageApi, contextHolder] = message.useMessage()
const [searchform] = Form.useForm() const [searchform] = Form.useForm()
const [snippetForm] = Form.useForm() const [snippetForm] = Form.useForm()
@ -32,6 +32,7 @@ function SnippetList() {
const [loginUser] = useAuthStore((state) => [state.loginUser]) const [loginUser] = useAuthStore((state) => [state.loginUser])
const { notification } = App.useApp()
const [isSnippetModalOpen, setSnippetModalOpen] = useState(false) const [isSnippetModalOpen, setSnippetModalOpen] = useState(false)
const [isHtmlLoading, setHtmlLoading] = useState(false) const [isHtmlLoading, setHtmlLoading] = useState(false)
@ -235,13 +236,14 @@ function SnippetList() {
value={currentSnippet.content} value={currentSnippet.content}
loading={isHtmlLoading} loading={isHtmlLoading}
onEdit={() => handelSnippetEdit()} onEdit={() => handelSnippetEdit()}
onCopied={() => message.success('已复制😀')} onCopied={() => messageApi.success('已复制')}
onDelete={() => handelSnippetDelete()} onDelete={() => handelSnippetDelete()}
/> />
</Col> </Col>
<div></div> <div></div>
</Row> </Row>
</Space> </Space>
{contextHolder}
</> </>
) )
} }

@ -4,7 +4,7 @@ import { ReloadOutlined, CheckCircleFilled, ExclamationCircleFilled } from '@ant
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import { fetchQRCode } from '@/actions/WaiAction' import { fetchQRCode } from '@/actions/WaiAction'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { isEmpty } from '@haina/utils-commons' import { isEmpty } from '@/utils/commons'
const connectionStateMappedQRCodeState = { const connectionStateMappedQRCodeState = {
open: 'scanned', open: 'scanned',

@ -4,7 +4,6 @@ import dayjs from 'dayjs'
import { ReadIcon, DeliverIcon, SentIcon, WaitingIcon, FailedIcon } from '@/components/Icons' import { ReadIcon, DeliverIcon, SentIcon, WaitingIcon, FailedIcon } from '@/components/Icons'
import { MessageTwoTone } from '@ant-design/icons' import { MessageTwoTone } from '@ant-design/icons'
import useCustomerRelationStore from '@/stores/CustomerRelationStore' import useCustomerRelationStore from '@/stores/CustomerRelationStore'
import CountryInfo2 from '@/assets/CountryInfo2.json'
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
const { Option } = Select const { Option } = Select
@ -19,12 +18,6 @@ const statusIconMap = {
default: <WaitingIcon title='waiting' />, default: <WaitingIcon title='waiting' />,
} }
// ID
const countryMap = CountryInfo2.reduce((map, item) => {
map[item.COI2_COI_SN] = item.COI2_Country;
return map;
}, {});
const Index = () => { const Index = () => {
const { loading, tasksList, fetchSearchTasks } = useCustomerRelationStore() const { loading, tasksList, fetchSearchTasks } = useCustomerRelationStore()
@ -149,9 +142,9 @@ const Index = () => {
const columns = [ const columns = [
{ title: '团号', dataIndex: 'crt_coli_id', key: 'crt_coli_id' }, { title: '团号', dataIndex: 'crt_coli_id', key: 'crt_coli_id' },
{ title: '客人', dataIndex: 'crt_mei_firstname', key: 'crt_mei_firstname', render: (text, record) => record.crt_mei_firstname + ' ' + record.crt_mei_lastname }, { title: '客人', dataIndex: 'crt_mei_firstname', key: 'crt_mei_firstname', render: (text, record) => record.crt_mei_firstname + ' ' + record.crt_mei_lastname },
{ title: '时区', dataIndex: 'crt_coi2_country', key: 'crt_coi2_country' }, { title: '国家', dataIndex: 'crt_coi2_country', key: 'crt_coi2_country' },
{ title: '国籍', dataIndex: 'crt_mei_country', key: 'crt_mei_country',render: (sn) => countryMap[sn] || sn }, // { title: '', dataIndex: 'ct_coi_code', key: 'ct_coi_code' },
{ title: '附加信息', dataIndex: 'crt_add_info', key: 'crt_add_info' },
{ {
title: '状态', title: '状态',
dataIndex: 'crt_status', dataIndex: 'crt_status',
@ -167,7 +160,6 @@ const Index = () => {
}, },
{ title: '发送时间', dataIndex: 'crt_send_datetime', key: 'crt_send_datetime', sorter: (a, b) => dayjs(a.crt_send_datetime).unix() - dayjs(b.crt_send_datetime).unix() }, { title: '发送时间', dataIndex: 'crt_send_datetime', key: 'crt_send_datetime', sorter: (a, b) => dayjs(a.crt_send_datetime).unix() - dayjs(b.crt_send_datetime).unix() },
{ title: '模板', dataIndex: 'crt_template', key: 'crt_template' }, { title: '模板', dataIndex: 'crt_template', key: 'crt_template' },
{ title: 'WhatsApp', dataIndex: 'crt_whatsapp', key: 'crt_whatsapp' }, { title: 'WhatsApp', dataIndex: 'crt_whatsapp', key: 'crt_whatsapp' },
{ {
title: '会话', title: '会话',
@ -177,7 +169,7 @@ const Index = () => {
if (text) { if (text) {
const icon = statusIconMap[record.msg_status] || statusIconMap['default'] const icon = statusIconMap[record.msg_status] || statusIconMap['default']
return ( return (
<Link to={`/order/chat/${record.crt_coli_sn}`} title={record.errors_code ? record.errors_code + '' + record.errors_title : ''} state={{ coli_guest_WhatsApp: record.crt_whatsapp, guest_email: record.crt_mei_maillist }}> <Link to={`/order/chat/${record.crt_coli_sn}`} title={record.errors_code ? record.errors_code + '' + record.errors_title : ''}>
查看会话 {icon} {record.msg_reply ? <MessageTwoTone title='已回复' /> : ''} 查看会话 {icon} {record.msg_reply ? <MessageTwoTone title='已回复' /> : ''}
</Link> </Link>
) )

@ -1,14 +1,11 @@
import { Flex, Result, Input, Button, Typography } from 'antd' import { Flex, Result, Input, Button } from 'antd'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import * as dd from 'dingtalk-jsapi' import * as dd from 'dingtalk-jsapi'
import useAuthStore from '@/stores/AuthStore'
// //
// https://open.dingtalk.com/document/orgapp/jsapi-request-auth-code // https://open.dingtalk.com/document/orgapp/jsapi-request-auth-code
function AuthCode() { function AuthCode() {
const loginByJSAuth = useAuthStore((state) => state.loginByJSAuth)
const loginStatus = useAuthStore((state) => state.loginStatus)
const [result, setResult] = useState('') const [result, setResult] = useState('')
const [clientValue, setClientValue] = useState('dingl3jyntkazyg4coxf') const [clientValue, setClientValue] = useState('dingl3jyntkazyg4coxf')
const handleRequest = () => { const handleRequest = () => {
@ -26,10 +23,6 @@ function AuthCode() {
}) })
} }
const handleLogin = () => {
loginByJSAuth(result)
}
useEffect(() => { useEffect(() => {
const dingTalkPlatForm = dd.env.platform const dingTalkPlatForm = dd.env.platform
setResult(dingTalkPlatForm) setResult(dingTalkPlatForm)
@ -42,10 +35,8 @@ function AuthCode() {
title={clientValue} title={clientValue}
subTitle={result} subTitle={result}
/> />
<Typography.Text>Login: {loginStatus}</Typography.Text>
<Input value={clientValue} onChange={e => setClientValue(e.currentTarget.value)} /> <Input value={clientValue} onChange={e => setClientValue(e.currentTarget.value)} />
<Button type='primary' onClick={() => handleRequest()}>请求</Button> <Button type='primary' onClick={() => handleRequest()}>请求</Button>
<Button onClick={() => handleLogin()}>登录</Button>
</Flex> </Flex>
) )
} }

@ -1,5 +1,5 @@
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import { isNotEmpty } from '@haina/utils-commons' import { isNotEmpty } from '@/utils/commons'
import { Flex, Result, Spin } from 'antd' import { Flex, Result, Spin } from 'antd'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
@ -13,7 +13,6 @@ function Callback() {
const login = useAuthStore((state) => state.login) const login = useAuthStore((state) => state.login)
const loginStatus = useAuthStore((state) => state.loginStatus) const loginStatus = useAuthStore((state) => state.loginStatus)
const loginByJSAuth = useAuthStore((state) => state.loginByJSAuth)
const urlSearch = new URLSearchParams(location.search) const urlSearch = new URLSearchParams(location.search)
const authCode = urlSearch.get('authCode') const authCode = urlSearch.get('authCode')
@ -22,10 +21,10 @@ function Callback() {
const originUrl = urlSearch.get('origin_url') const originUrl = urlSearch.get('origin_url')
useEffect (() => { useEffect (() => {
if (state === 'global-saels' && isNotEmpty(authCode)) { if (isNotEmpty(authCode) && state === 'global-saels') {
login(authCode) login(authCode)
} else if (state === 'jsapi-auth' && isNotEmpty(authCode)) { } else if (isNotEmpty(authCode) && state === 'jsapi-auth') {
loginByJSAuth(authCode) // loginByJSAuth()
} else { } else {
console.error('error: ' + error) console.error('error: ' + error)
} }
@ -82,4 +81,4 @@ function Callback() {
} }
} }
export default Callback export default Callback

@ -1,7 +1,7 @@
import { Flex, Result, Spin } from 'antd' import { Flex, Result, Spin } from 'antd'
import { useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { isNotEmpty } from '@haina/utils-commons' import { isNotEmpty } from '@/utils/commons'
import * as dd from 'dingtalk-jsapi' import * as dd from 'dingtalk-jsapi'
// //
@ -32,25 +32,25 @@ function Login() {
</Flex> </Flex>
) )
} }
window.location = redirectUrl
if (dingTalkPlatForm === 'notInDingTalk') { // if (dingTalkPlatForm === 'notInDingTalk') {
window.location = redirectUrl // window.location = redirectUrl
} else { // } else {
dd.requestAuthCode({ // dd.requestAuthCode({
clientId: 'dingl3jyntkazyg4coxf', // clientId: 'dingl3jyntkazyg4coxf',
corpId: 'ding48bce8fd3957c96b', // corpId: 'ding48bce8fd3957c96b',
success: (res) => { // success: (res) => {
const { code } = res // const { code } = res
navigate('/p/dingding/callback?origin_url='+originUrl+'&state=jsapi-auth&authCode=' + code, { // navigate('/p/dingding/callback?state=jsapi-auth&authCode=' + code, {
replace: true, // replace: true,
}) // })
}, // },
fail: (error) => { // fail: (error) => {
setErrorMsg(JSON.stringify(error)) // setErrorMsg(JSON.stringify(error))
}, // },
complete: () => {}, // complete: () => {},
}) // })
} // }
return ( return (
<Flex justify='center' align='center' gap='middle' vertical> <Flex justify='center' align='center' gap='middle' vertical>

@ -2,7 +2,7 @@ import { Layout, Button } from 'antd';
import MessagesHeader from '@/views/Conversations/Online/MessagesHeader'; import MessagesHeader from '@/views/Conversations/Online/MessagesHeader';
import MessagesWrapper from '@/views/Conversations/Online/MessagesWrapper'; import MessagesWrapper from '@/views/Conversations/Online/MessagesWrapper';
import InputComposer from '@/views/Conversations/Online/Input/InputComposer'; import InputComposer from '@/views/Conversations/Online/Input/InputComposer';
import { UnorderedListOutlined, MenuUnfoldOutlined, MenuFoldOutlined, LeftOutlined } from '@ant-design/icons'; import { UnorderedListOutlined, MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import ReplyWrapper from '../Conversations/Online/ReplyWrapper'; import ReplyWrapper from '../Conversations/Online/ReplyWrapper';
@ -14,7 +14,7 @@ function Chat() {
<> <>
<Layout className='h-full chatwindow-wrapper mobilechat-wrapper' style={{ maxHeight: 'calc(100vh - 20px)', height: 'calc(100vh - 20px)', minWidth: '360px' }}> <Layout className='h-full chatwindow-wrapper mobilechat-wrapper' style={{ maxHeight: 'calc(100vh - 20px)', height: 'calc(100vh - 20px)', minWidth: '360px' }}>
<Header className=' px-2 ant-layout-sider-light bg-white ant-card h-auto flex justify-between gap-1 items-center'> <Header className=' px-2 ant-layout-sider-light bg-white ant-card h-auto flex justify-between gap-1 items-center'>
<Button type='text' icon={<LeftOutlined />} onClick={() => navigate('/m/conversation')} className=' rounded-none rounded-l' /> <Button type='text' icon={<MenuFoldOutlined />} onClick={() => navigate('/m/conversation')} className=' rounded-none rounded-l' />
<MessagesHeader /> <MessagesHeader />
<Button type='text' icon={<MenuUnfoldOutlined />} onClick={() => navigate('/m/order')} className=' rounded-none rounded-r' /> <Button type='text' icon={<MenuUnfoldOutlined />} onClick={() => navigate('/m/order')} className=' rounded-none rounded-r' />
</Header> </Header>

@ -1,5 +1,5 @@
import { OrderLabelDefaultOptions, OrderStatusDefaultOptions, RemindStateDefaultOptions } from '@/stores/OrderStore'; import { OrderLabelDefaultOptions, OrderStatusDefaultOptions, RemindStateDefaultOptions } from '@/stores/OrderStore';
import { copy, objectMapper } from '@haina/utils-commons'; import { copy, objectMapper } from '@/utils/commons';
import { Button, Col, DatePicker, Form, Input, Row, Select } from 'antd'; import { Button, Col, DatePicker, Form, Input, Row, Select } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { memo } from 'react'; import { memo } from 'react';
@ -66,13 +66,13 @@ const AdvanceSearchForm = memo(function NoName({ initialValues, onSubmit, loadin
}, },
]; ];
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions); const orderLabelOptions = copy(OrderLabelDefaultOptions);
orderLabelOptions.unshift({ value: '', label: '全部' }); orderLabelOptions.unshift({ value: '', label: '全部' });
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions); const orderStatusOptions = copy(OrderStatusDefaultOptions);
orderStatusOptions.unshift({ value: '', label: '全部' }); orderStatusOptions.unshift({ value: '', label: '全部' });
const remindStateOptions = structuredClone(RemindStateDefaultOptions); const remindStateOptions = copy(RemindStateDefaultOptions);
remindStateOptions.unshift({ value: '', label: '全部' }); remindStateOptions.unshift({ value: '', label: '全部' });
const [form] = Form.useForm(); const [form] = Form.useForm();

@ -1,14 +1,13 @@
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import { pick } from '@haina/utils-commons' import { pick } from '@/utils/commons'
import { UnorderedListOutlined, LeftOutlined } from '@ant-design/icons' import { UnorderedListOutlined, LeftOutlined } from '@ant-design/icons'
import { Flex, Segmented, Tree, Typography, Layout, Splitter, Button, Tooltip, Badge } from 'antd' import { Flex, Segmented, Tree, Typography, Layout, Splitter, Button, Tooltip, Badge } from 'antd'
import { useEffect, useMemo, useState, useRef } from 'react' import { useEffect, useMemo, useState } from 'react'
import EmailDetailInline from '../Conversations/Online/Components/EmailDetailInline' import EmailDetailInline from '../Conversations/Online/Components/EmailDetailInline'
import OrderProfile from '@/components/OrderProfile' import OrderProfile from '@/components/OrderProfile'
import Mailbox from './components/MailBox' import Mailbox from './components/Mailbox'
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { MailboxDirIcon } from './components/MailboxDirIcon' import { MailboxDirIcon } from './components/MailboxDirIcon'
import { useVisibilityState } from '@/hooks/useVisibilityState'
const deptMap = new Map([ const deptMap = new Map([
['1', 'CH'], // CH ['1', 'CH'], // CH
@ -45,9 +44,7 @@ const deptMap = new Map([
function Follow() { function Follow() {
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(true)
const mailboxTreeRef = useRef(null);
const [treeHeight, setTreeHeight] = useState(500);
const [loginUser, isPermitted] = useAuthStore((state) => [state.loginUser, state.isPermitted]) const [loginUser, isPermitted] = useAuthStore((state) => [state.loginUser, state.isPermitted])
const { accountList } = loginUser const { accountList } = loginUser
@ -126,35 +123,10 @@ function Follow() {
useEffect(() => { useEffect(() => {
const first = currentMailboxDEI || accountDEI[0].value const first = currentMailboxDEI || accountDEI[0].value
const opi = accountListDEIMapped[first].OPI_SN const opi = accountListDEIMapped[first].OPI_SN
setExpandTree([...[`${opi}-today`, `${opi}-todo`, `search-orders` ]]) setExpandTree(prev => [...[`${opi}-today`, `${opi}-todo`, `search-orders`, mailboxActiveNode?.VParent ]])
return () => {} return () => {}
}, [currentMailboxDEI ]) }, [currentMailboxDEI, mailboxNestedDirsActive, mailboxActiveNode])
useEffect(() => {
if (mailboxActiveNode?.expand === true) {
setExpandTree([mailboxActiveNode.key])
}
return () => {}
}, [mailboxActiveNode])
useEffect(() => {
const targetRect = mailboxTreeRef.current?.getBoundingClientRect()
setTreeHeight(Math.floor(targetRect.height))
return () => {}
}, [])
const isVisible = useVisibilityState();
useEffect(() => {
// console.log('effect isVisible', isVisible);
if (isVisible && currentMailboxOPI) {
getOPIEmailDir(currentMailboxOPI, null, true)
}
return () => {}
}, [isVisible]);
return ( return (
<> <>
@ -162,14 +134,13 @@ function Follow() {
<Layout.Sider width='300' theme='light' style={{ height: 'calc(100vh - 166px)' }} className=' relative'> <Layout.Sider width='300' theme='light' style={{ height: 'calc(100vh - 166px)' }} className=' relative'>
<Flex justify='start' align='start' vertical className='h-full'> <Flex justify='start' align='start' vertical className='h-full'>
<Segmented className='w-full' block shape='round' options={accountDEI} value={currentMailboxDEI} onChange={handleSwitchAccount} /> <Segmented className='w-full' block shape='round' options={accountDEI} value={currentMailboxDEI} onChange={handleSwitchAccount} />
<div ref={mailboxTreeRef} className='overflow-y-auto flex-auto w-full [&_.ant-tree-switcher]:me-0 [&_.ant-tree-node-content-wrapper]:px-0 [&_.ant-tree-node-content-wrapper]:text-ellipsis [&_.ant-tree-node-content-wrapper]:overflow-hidden [&_.ant-tree-node-content-wrapper]:whitespace-nowrap'> <div className='overflow-y-auto flex-auto w-full [&_.ant-tree-switcher]:me-0 [&_.ant-tree-node-content-wrapper]:px-0 [&_.ant-tree-node-content-wrapper]:text-ellipsis [&_.ant-tree-node-content-wrapper]:overflow-hidden [&_.ant-tree-node-content-wrapper]:whitespace-nowrap'>
<Tree <Tree
className='[&_.ant-typography-ellipsis]:max-w-44 [&_.ant-typography-ellipsis]:min-w-36' className='[&_.ant-typography-ellipsis]:max-w-44 [&_.ant-typography-ellipsis]:min-w-36'
key='sticky-today' key='sticky-today'
blockNode blockNode
showIcon showIcon
showLine showLine
height={treeHeight}
autoExpandParent={true} autoExpandParent={true}
expandAction={'doubleClick'} expandAction={'doubleClick'}
onSelect={handleTreeSelectGetMails} onSelect={handleTreeSelectGetMails}
@ -182,7 +153,7 @@ function Follow() {
titleRender={(node) => ( titleRender={(node) => (
<Typography.Text ellipsis={{ tooltip: node.title }} className={`${node?._raw?.IsSuccess === 1 ? 'text-primary' : ''}`}> <Typography.Text ellipsis={{ tooltip: node.title }} className={`${node?._raw?.IsSuccess === 1 ? 'text-primary' : ''}`}>
{node.title} {node.title}
<Badge size={'small'} count={node.count} offset={[3, 0]} style={{backgroundColor: node?._raw?.ImageIndex===2 ? "" : "#1ba784", color1: '#1ba784'}} overflowCount={999} /> <Badge size={'small'} count={node.count} offset={[3, 0]} style={{backgroundColor: "#1ba784", color1: '#1ba784'}} overflowCount={999} />
</Typography.Text> </Typography.Text>
)} )}
/> />
@ -204,7 +175,7 @@ function Follow() {
<Button <Button
icon={collapsed ? <LeftOutlined /> : <UnorderedListOutlined />} icon={collapsed ? <LeftOutlined /> : <UnorderedListOutlined />}
onClick={() => setCollapsed(!collapsed)} onClick={() => setCollapsed(!collapsed)}
className={`absolute z-10 rounded-none ${collapsed ? 'right-1 top-20 rounded-l-xl' : 'right-8 top-20 rounded-l'}`} className={`absolute z-10 rounded-none ${collapsed ? 'right-1 top-36 rounded-l-xl' : 'right-8 top-20 rounded-l'}`}
size={collapsed ? 'small' : 'middle'} size={collapsed ? 'small' : 'middle'}
/> />
</Tooltip> </Tooltip>

@ -1,20 +1,48 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ReloadOutlined, RightOutlined, LeftOutlined, MailOutlined, DeleteOutlined, CloseCircleOutlined } from '@ant-design/icons' import { ReloadOutlined, ReadOutlined, RightOutlined, LeftOutlined, SearchOutlined, MailOutlined, DeleteOutlined } from '@ant-design/icons'
import { Flex, Button, Tooltip, List, Checkbox, Space, Breadcrumb, Skeleton } from 'antd' import { Flex, Button, Tooltip, List, Form, Row, Col, Input, Checkbox, DatePicker, Switch, Breadcrumb, Skeleton, Popconfirm } from 'antd'
import dayjs from 'dayjs'
import { useEmailList } from '@/hooks/useEmail' import { useEmailList } from '@/hooks/useEmail'
import { isEmpty } from '@haina/utils-commons' import { isEmpty } from '@/utils/commons'
import { MailboxDirIcon } from './MailboxDirIcon' import { MailboxDirIcon } from './MailboxDirIcon'
import { AttachmentIcon, MailCheckIcon } from '@/components/Icons' import { AttachmentIcon, MailCheckIcon, MailOpenIcon } from '@/components/Icons'
import NewEmailButton from './NewEmailButton' import NewEmailButton from './NewEmailButton'
import MailOrderSearchModal from './MailOrderSearchModal' import MailOrderSearchModal from './MailOrderSearchModal'
import MailListSearchModal from './MailListSearchModal'
const { RangePicker } = DatePicker
const PAGE_SIZE = 50 // const PAGE_SIZE = 50 //
const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => { const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
const DATE_RANGE_PRESETS = [
{
label: '本周',
value: [dayjs().startOf('w'), dayjs().endOf('w')],
},
{
label: '上周',
value: [dayjs().startOf('w').subtract(7, 'days'), dayjs().endOf('w').subtract(7, 'days')],
},
{
label: '本月',
value: [dayjs().startOf('M'), dayjs().endOf('M')],
},
{
label: '上月',
value: [dayjs().subtract(1, 'M').startOf('M'), dayjs().subtract(1, 'M').endOf('M')],
},
{
label: '前三月',
value: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
},
{
label: '本年',
value: [dayjs().startOf('y'), dayjs().endOf('y')],
},
]
const [form] = Form.useForm()
const [selectedItems, setSelectedItems] = useState([]) const [selectedItems, setSelectedItems] = useState([])
const { mailList, loading, error, tempBreadcrumb, refresh, markAsUnread, markAsProcessed, markAsDeleted, } = useEmailList(mailboxDir) const { mailList, loading, error, refresh, markAsRead, markAsProcessed, markAsDeleted } = useEmailList(mailboxDir)
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
current: 1, current: 1,
@ -41,96 +69,7 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
const getPagedData = (data, currentPage) => { const getPagedData = (data, currentPage) => {
const startIndex = (currentPage - 1) * PAGE_SIZE const startIndex = (currentPage - 1) * PAGE_SIZE
const endIndex = Math.min(startIndex + PAGE_SIZE, data.length) const endIndex = Math.min(startIndex + PAGE_SIZE, data.length)
const slicedData = data.slice(startIndex, endIndex) return data.slice(startIndex, endIndex)
const today = new Date();
today.setHours(0, 0, 0, 0);
//
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
// ()()
const currentDay = today.getDay(); // 0
const mondayOffset = currentDay === 0 ? -6 : 1 - currentDay; //
const weekStart = new Date(today);
weekStart.setDate(today.getDate() + mondayOffset);
weekStart.setHours(0, 0, 0, 0);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
weekEnd.setHours(0, 0, 0, 0);
const groups = {
today: [],
yesterday: [],
dayBeforeYesterday: [],
thisWeek: {}, //
lastWeek: [],
earlier: [],
};
//
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(weekStart.getDate() + i);
const weekday = weekdays[date.getDay()];
groups.thisWeek[weekday] = [];
}
slicedData.forEach((mail) => {
const mailDate = new Date(mail.SRDate);
mailDate.setHours(0, 0, 0, 0);
const diffTime = today - mailDate;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
//
const weekday = weekdays[mailDate.getDay()];
if (diffDays === 0) {
groups.today.push(mail);
} else if (diffDays === 1) {
groups.yesterday.push(mail);
} else if (diffDays === 2) {
groups.dayBeforeYesterday.push(mail);
} else if (mailDate >= weekStart && mailDate <= weekEnd && diffDays > 2) {
//
groups.thisWeek[weekday].push(mail);
} else if (diffDays <= 14) {
groups.lastWeek.push(mail);
} else {
groups.earlier.push(mail);
}
});
const groupedData = [];
if (groups.today.length > 0) {
groupedData.push({ title: '今天', data: groups.today });
}
if (groups.yesterday.length > 0) {
groupedData.push({ title: '昨天', data: groups.yesterday });
}
if (groups.dayBeforeYesterday.length > 0) {
groupedData.push({ title: '前天', data: groups.dayBeforeYesterday });
}
//
Object.entries(groups.thisWeek).forEach(([weekday, mails]) => {
if (mails.length > 0) {
groupedData.push({ title: weekday, data: mails });
}
});
if (groups.lastWeek.length > 0) {
groupedData.push({ title: '上周', data: groups.lastWeek });
}
if (groups.earlier.length > 0) {
groupedData.push({ title: '更早', data: groups.earlier });
}
return groupedData
} }
const prePage = () => { const prePage = () => {
@ -141,7 +80,6 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
current: newCurrent, current: newCurrent,
pagedList: getPagedData(mailList, newCurrent), pagedList: getPagedData(mailList, newCurrent),
})) }))
setSelectedItems([]);
} }
} }
@ -153,28 +91,25 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
current: newCurrent, current: newCurrent,
pagedList: getPagedData(mailList, newCurrent), pagedList: getPagedData(mailList, newCurrent),
})) }))
setSelectedItems([]);
} }
} }
const mailItemRender = (item) => { const mailItemRender = (item) => {
const isOrderNode = mailboxDir.COLI_SN > 0 const isOrderNode = mailboxDir.COLI_SN > 0
const orderNumber = isEmpty(item.MAI_COLI_ID) || isOrderNode ? '' : item.MAI_COLI_ID + ' - ' const orderNumber = isEmpty(item.MAI_COLI_ID) || isOrderNode ? '' : item.MAI_COLI_ID + ' - '
const folderName = (item.showFolder || isOrderNode) ? <span className='text-neutral-500 '>[{item.FDir}]&nbsp;&nbsp;</span> : '' const folderName = isOrderNode ? `[${item.FDir}]` : ''
const orderMailType = item.MAT_Name ? <span className='text-neutral-600 text-xs'>{item.MAT_Name}</span> : '' const orderMailType = <span className='text-blue-400 text-xs'>{item.MAT_Name}</span>
const countryName = isEmpty(item.CountryCN) ? '' : '[' + item.CountryCN + '] ' const countryName = isEmpty(item.CountryCN) ? '' : '[' + item.CountryCN + '] '
const mailStateClass = item.MOI_ReadState === 0 ? 'font-black text-emerald-600' : '' const mailStateClass = item.MOI_ReadState === 0 ? 'font-bold' : ''
const hasAtta = item.MAI_Attachment !== 0 ? <AttachmentIcon className='text-blue-500' /> : null const hasAtta = item.MAI_Attachment !== 0 ? <AttachmentIcon className='text-blue-500' /> : null
return ( return (
<li <li className={`flex border border-solid border-t-0 border-x-0 border-gray-200 hover:bg-neutral-50 active:bg-gray-200 p-2 ${props.currentActiveMailItem === item.MAI_SN ? 'bg-neutral-100' : ''}`}>
className={`flex border border-solid border-t-0 border-x-0 border-gray-200 hover:bg-neutral-50 active:bg-gray-200 p-2 ${props.currentActiveMailItem === item.MAI_SN ? 'bg-neutral-100' : ''}`}>
<div className=''> <div className=''>
<Checkbox <Checkbox
checked={selectedItems.some((i) => i.MAI_SN === item.MAI_SN)} checked={selectedItems.some((i) => i.MAI_SN === item.MAI_SN)}
onClick={(e) => { onClick={(e) => {
const isChecked = e.target.checked const isChecked = e.target.checked
const noCurrent = selectedItems.filter((i) => i.MAI_SN !== item.MAI_SN); const updatedSelection = isChecked ? [...selectedItems, item] : selectedItems.filter((item) => item.MAI_SN !== item.MAI_SN)
const updatedSelection = isChecked ? [...noCurrent, item] : noCurrent;
setSelectedItems(updatedSelection) setSelectedItems(updatedSelection)
}}></Checkbox> }}></Checkbox>
</div> </div>
@ -185,15 +120,11 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
}}> }}>
<Flex gap='small' vertical={true} justify='space-between' className='cursor-pointer'> <Flex gap='small' vertical={true} justify='space-between' className='cursor-pointer'>
<div> <div>
{folderName}{orderNumber} {orderNumber}
<span className={mailStateClass}>{item.MAI_Subject || '[无主题]'}</span> <span className={mailStateClass}>{item.MAI_Subject || '[无主题]'}</span>
{hasAtta} {hasAtta}
</div> </div>
<Flex gap='small' align='center' justify='flex-end' wrap className='text-neutral-500 text-wrap break-words break-all '> <span className='text-neutral-500 text-wrap break-words break-all '>{countryName + item.SenderReceiver + ' ' + item.SRDate}</span>
<span className='mr-auto'>{countryName + item.SenderReceiver}</span>
{orderMailType}
<span className=''>{item.SRDate}</span>
</Flex>
</Flex> </Flex>
</div> </div>
</li> </li>
@ -201,17 +132,17 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
} }
return ( return (
<div className='h-full flex flex-col gap-1 bg-white'> <>
<div className='bg-white h-auto px-1 flex gap-1 items-center'> <div className='bg-white h-auto px-1 flex gap-1 items-center'>
<Flex wrap gap='middle' justify={'center'} className='min-w-30 px-1'> <Flex wrap gap='middle' justify={'center'} className='min-w-30 px-1'>
<Tooltip title='全选'> <Tooltip title='全选'>
<Checkbox <Checkbox
indeterminate={selectedItems.length > 0 && selectedItems.length < Math.min(pagination.current * PAGE_SIZE, (pagination.total - ((pagination.current - 1) * PAGE_SIZE)))} indeterminate={selectedItems.length > 0 && selectedItems.length < pagination.pagedList.length}
checked={pagination.total === 0 ? false : pagination.pagedList.reduce((a, item) => a.concat(item.data), []).every((item) => selectedItems.some((selected) => selected.MAI_SN === item.MAI_SN))} checked={pagination.pagedList.length === 0 ? false : pagination.pagedList.every((item) => selectedItems.some((selected) => selected.MAI_SN === item.MAI_SN))}
onChange={(e) => { onChange={(e) => {
const isChecked = e.target.checked const isChecked = e.target.checked
if (isChecked) { if (isChecked) {
setSelectedItems(pagination.pagedList.reduce((a, item) => a.concat(item.data), [])) setSelectedItems((prev) => [...prev, ...pagination.pagedList])
} else { } else {
setSelectedItems([]) setSelectedItems([])
} }
@ -222,93 +153,113 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
</Tooltip> </Tooltip>
</Flex> </Flex>
<Flex wrap gap={8} > <Flex wrap gap={8}>
<NewEmailButton /> <NewEmailButton />
<Button <Button
size='small' size='small'
icon={<MailOutlined />} icon={<MailOpenIcon />}
onClick={() => { onClick={() => {
markAsUnread(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([])) markAsRead(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}> }}
未读 >已读</Button>
</Button> <Button
<Button size='small'
size='small' icon={<MailOutlined />}
icon={<MailCheckIcon />} onClick={() => {
onClick={() => { console.info('未读未实现')
}}
>未读</Button>
<Button
size='small'
icon={<MailCheckIcon />}
onClick={() => {
markAsProcessed(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([])) markAsProcessed(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}> }}
已处理 >已处理</Button>
</Button> <Button
<Button size='small' // danger
size='small' // danger icon={<DeleteOutlined />}
icon={<DeleteOutlined />} onClick={() => {
onClick={() => {
markAsDeleted(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([])) markAsDeleted(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}> }}
删除 >删除</Button>
</Button> <MailOrderSearchModal />
<MailOrderSearchModal />
<MailListSearchModal />
</Flex> </Flex>
</div> </div>
<Flex align='center' justify='space-between' wrap className='px-1 border-0 border-b border-solid border-neutral-200'> <div className='bg-white h-auto p-1 flex gap-1 items-center hidden'>
<Breadcrumb <Form
items={(tempBreadcrumb || props.breadcrumb).map((bc) => { form={form}
return { initialValues={{}}
title: ( // onFinish={handleSubmit}
<> >
<MailboxDirIcon type={bc?.iconIndex} /> <Row justify='start' gutter={16}>
<span>{bc.title}</span> <Col span={10}>
</> <Form.Item label='订单号' name='orderNumber'>
), <Input placeholder='订单号' allowClear />
} </Form.Item>
})} </Col>
/> <Col span={12}>
{tempBreadcrumb && (<Button type='text' icon={<CloseCircleOutlined />} onClick={() => refresh()} />)} <Form.Item label='日期' name='confirmDateRange'>
<Flex align='center' justify='space-between' className='ml-auto'> <RangePicker allowClear={true} inputReadOnly={true} presets={DATE_RANGE_PRESETS} />
<span>已选: {selectedItems.length} </span> </Form.Item>
<span> </Col>
{(pagination.current - 1) * PAGE_SIZE + 1}-{Math.min(pagination.current * PAGE_SIZE, pagination.total)} of {pagination.total} <Col span={1} offset={1}>
</span> <Button type='primary' htmlType='submit'>
<Button 搜索
icon={<LeftOutlined />} </Button>
type='text' </Col>
onClick={() => { </Row>
prePage() </Form>
}} </div>
iconPosition={'end'}></Button>
<Button
icon={<RightOutlined />}
type='text'
onClick={() => {
nextPage()
}}
iconPosition={'end'}></Button>
</Flex>
</Flex>
<div className='bg-white overflow-auto px-2' style={{ height1: 'calc(100vh - 198px)' }}> <div className='bg-white overflow-y-auto px-2' style={{ height: 'calc(100vh - 198px)' }}>
<Skeleton active loading={loading}> <Skeleton active loading={loading}>
<Space direction='vertical' size='middle' style={{ display: 'flex' }}> <List
{pagination.pagedList.map(item => { loading={loading}
return ( header={
<List <Flex align='center' justify='space-between' wrap >
key={item.title} <Breadcrumb
loading={loading} items={props.breadcrumb.map((bc) => {
className='flex flex-col h-full [&_.ant-list-items]:overflow-auto' return {
header={item.title} title: (
itemLayout='vertical' <>
pagination={false} <MailboxDirIcon type={bc?.iconIndex} />
dataSource={item.data} <span>{bc.title}</span>
renderItem={mailItemRender} </>
/> ),
) }
})} })}
</Space> />
<Flex align='center' justify='space-between' className='ml-auto' >
<span>已选: {selectedItems.length} </span>
<span>
{(pagination.current - 1) * PAGE_SIZE + 1}-{Math.min(pagination.current * PAGE_SIZE, pagination.total)} of {pagination.total}
</span>
<Button
icon={<LeftOutlined />}
type='text'
onClick={() => {
prePage()
}}
iconPosition={'end'}></Button>
<Button
icon={<RightOutlined />}
type='text'
onClick={() => {
nextPage()
}}
iconPosition={'end'}></Button>
</Flex>
</Flex>
}
itemLayout='vertical'
pagination={false}
dataSource={pagination.pagedList}
renderItem={mailItemRender}
/>
</Skeleton> </Skeleton>
</div> </div>
</div> </>
) )
} }

@ -1,75 +0,0 @@
import { useState } from 'react'
import { SearchOutlined } from '@ant-design/icons'
import { Button, Modal, Form, Input, Radio } from 'antd'
import { searchEmailListAction } from '@/actions/EmailActions'
import useConversationStore from '@/stores/ConversationStore'
const MailListSearchModal = ({ ...props }) => {
const [currentMailboxOPI] = useConversationStore((state) => [state.currentMailboxOPI])
const [openForm, setOpenForm] = useState(false)
const [formSearch] = Form.useForm()
const [loading, setLoading] = useState(false)
const onSubmitSearchMailList = async (values) => {
setLoading(true)
await searchEmailListAction({...values, opi_sn: currentMailboxOPI});
setLoading(false)
setOpenForm(false)
}
return (
<>
<Button key={'bound'} onClick={() => setOpenForm(true)} size='small' icon={<SearchOutlined className='' />}>
查找邮件
</Button>
<Modal
width={window.innerWidth < 700 ? '95%' : 960}
open={openForm}
cancelText='关闭'
okText='查找'
confirmLoading={loading}
okButtonProps={{ autoFocus: true, htmlType: 'submit', type: 'default' }}
onCancel={() => setOpenForm(false)}
footer={null}
destroyOnHidden
modalRender={(dom) => (
<Form
layout='vertical'
form={formSearch}
name='searchmaillist_form_in_modal'
initialValues={{ mailboxtype: '' }}
clearOnDestroy
onFinish={(values) => onSubmitSearchMailList(values)}
className='[&_.ant-form-item]:m-2'>
{dom}
</Form>
)}>
<Form.Item name='mailboxtype' label='邮箱文件夹'>
<Radio.Group
options={[
{ key: 'All', value: '', label: 'All' },
{ key: '1', value: '1', label: '收件箱' },
{ key: '2', value: '2', label: '未读邮件' },
{ key: '3', value: '3', label: '已发邮件' },
{ key: '7', value: '7', label: '已处理邮件' },
]}
optionType='button'
/>
</Form.Item>
<Form.Item name='sender' label='发件人'>
<Input />
</Form.Item>
<Form.Item name='receiver' label='收件人'>
<Input />
</Form.Item>
<Form.Item name='subject' label='主题'>
<Input />
</Form.Item>
<Button type='primary' htmlType='submit' loading={loading}>
查找
</Button>
</Modal>
</>
)
}
export default MailListSearchModal

@ -1,10 +1,11 @@
import { useState } from 'react' import { createContext, useEffect, useState } from 'react'
import { SearchOutlined } from '@ant-design/icons' import { ReloadOutlined, ReadOutlined, RightOutlined, LeftOutlined, SearchOutlined, MailOutlined } from '@ant-design/icons'
import { Button, Modal, Form, Input, Checkbox, Radio, DatePicker, Divider, Typography, Flex } from 'antd' import { Button, Modal, Form, Input, Checkbox, Select, Radio, DatePicker, Divider, Typography, Flex } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { getEmailDirAction, queryHTOrderListAction, } from '@/actions/EmailActions' import { getEmailDirAction, queryHTOrderListAction, queryInMailboxAction } from '@/actions/EmailActions'
import { isEmpty, objectMapper, pick } from '@haina/utils-commons' import { isEmpty, objectMapper, pick } from '@/utils/commons'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { mailboxSystemDirs } from '@/hooks/useEmail'
const MailOrderSearchModal = ({ ...props }) => { const MailOrderSearchModal = ({ ...props }) => {
const [currentMailboxOPI] = useConversationStore((state) => [state.currentMailboxOPI]) const [currentMailboxOPI] = useConversationStore((state) => [state.currentMailboxOPI])
@ -32,14 +33,12 @@ const MailOrderSearchModal = ({ ...props }) => {
const { coli_id, sourcetype, ...mailboxParams } = valuesToSub const { coli_id, sourcetype, ...mailboxParams } = valuesToSub
result = await getEmailDirAction({ ...mailboxParams, opi_sn: currentMailboxOPI }, false) result = await getEmailDirAction({ ...mailboxParams, opi_sn: currentMailboxOPI }, false)
updateCurrentMailboxNestedDirs(result[`${currentMailboxOPI}`]) updateCurrentMailboxNestedDirs(result[`${currentMailboxOPI}`])
setMailboxActiveNode({expand:true, key: -1, title: '1月', iconIndex: 1, _raw: { VKey: -1, COLI_SN: 0, IsTrue: 0 }})
} else { } else {
const htOrderParams = pick(valuesToSub, ['coli_id', 'sourcetype']) const htOrderParams = pick(valuesToSub, ['coli_id', 'sourcetype'])
result = await queryHTOrderListAction({ ...htOrderParams, opi_sn: currentMailboxOPI }) result = await queryHTOrderListAction({ ...htOrderParams, opi_sn: currentMailboxOPI })
const addToTree = { const addToTree = {
expand:true,
key: 'search-orders', key: 'search-orders',
title: '查找订单', title: '搜索结果',
iconIndex: 'search', iconIndex: 'search',
_raw: { COLI_SN: 0, IsTrue: 0 }, _raw: { COLI_SN: 0, IsTrue: 0 },
children: result.map((o) => ({ children: result.map((o) => ({
@ -47,7 +46,7 @@ const MailOrderSearchModal = ({ ...props }) => {
title: `${o.COLI_ID}`, title: `${o.COLI_ID}`,
iconIndex: 13, iconIndex: 13,
parent: 'search-orders', parent: 'search-orders',
parentTitle: '查找订单', parentTitle: '搜索结果',
parentIconIndex: 'search', parentIconIndex: 'search',
_raw: { ...o, VKey: o.COLI_SN, VName: o.COLI_ID, VParent: 'search-orders', IsTrue: 0, ApplyDate: '', OrderSourceType: htOrderParams.sourcetype, parent: 'search-orders' }, _raw: { ...o, VKey: o.COLI_SN, VName: o.COLI_ID, VParent: 'search-orders', IsTrue: 0, ApplyDate: '', OrderSourceType: htOrderParams.sourcetype, parent: 'search-orders' },
})), })),
@ -61,7 +60,7 @@ const MailOrderSearchModal = ({ ...props }) => {
return ( return (
<> <>
<Button key={'bound'} onClick={() => setOpen(true)} size='small' icon={<SearchOutlined className='' />}> <Button key={'bound'} onClick={() => setOpen(true)} size='small' icon={<SearchOutlined className='' />}>
查找订单 查找
</Button> </Button>
<Modal <Modal
width={window.innerWidth < 700 ? '95%' : 960} width={window.innerWidth < 700 ? '95%' : 960}

@ -2,7 +2,7 @@ import { useMemo } from 'react'
import { App, Dropdown } from 'antd' import { App, Dropdown } from 'antd'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { emailTemplates, openPopup } from '@/hooks/useEmail' import { emailTemplates, openPopup } from '@/hooks/useEmail'
import { isEmpty } from '@haina/utils-commons' import { isEmpty } from '@/utils/commons'
const NewEmailButton = ({ ...props }) => { const NewEmailButton = ({ ...props }) => {
const { notification } = App.useApp() const { notification } = App.useApp()

@ -5,10 +5,8 @@ import { VitePWA } from 'vite-plugin-pwa';
import packageJson from './package.json'; import packageJson from './package.json';
import dayjs from 'dayjs' import dayjs from 'dayjs'
import svgr from "vite-plugin-svgr"; import svgr from "vite-plugin-svgr";
import { execSync } from 'child_process';
const today = new dayjs().format('YYYY-MM-DD HH:mm:ss') const today = new dayjs().format('YYYY-MM-DD HH:mm:ss')
const gitHead = execSync('git rev-parse --short HEAD').toString().trim()
const buildDatePlugin = () => { const buildDatePlugin = () => {
return { return {
@ -161,7 +159,6 @@ export default defineConfig({
define: { define: {
__BUILD_DATE__: JSON.stringify(`${today}`), __BUILD_DATE__: JSON.stringify(`${today}`),
__BUILD_VERSION__: JSON.stringify(`${packageJson.version}`), __BUILD_VERSION__: JSON.stringify(`${packageJson.version}`),
__GIT_HEAD__: JSON.stringify(`${gitHead}`),
}, },
plugins: [ svgr(), react(), WindiCSS(), buildDatePlugin(), VitePWA(manifestForPWAPlugIn), ], plugins: [ svgr(), react(), WindiCSS(), buildDatePlugin(), VitePWA(manifestForPWAPlugIn), ],
server: { server: {
@ -186,7 +183,10 @@ export default defineConfig({
output: { output: {
entryFileNames: '[name]/build.[hash].js', entryFileNames: '[name]/build.[hash].js',
manualChunks(id) { manualChunks(id) {
if (id.includes('node_modules/')) { if (id.toLowerCase().includes('lexical')) {
return 'lexical';
}
if (id.includes('node_modules')) {
return 'vendor'; return 'vendor';
// return id.toString().split('node_modules/')[1].split('/')[0].toString(); // return id.toString().split('node_modules/')[1].split('/')[0].toString();
} }

@ -39,5 +39,3 @@ baileys_store_multi.json
baileys_auth_info_*/ baileys_auth_info_*/
baileys_auth_info/ baileys_auth_info/
temp/
dist

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save