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
dist
dist-ssr
*.local
distTest
dev-dist
tmp
schema*
*.local
distTest
dev-dist
tmp
schema*
.gitkeep
# Editor directories and files
@ -29,14 +29,13 @@ schema*
*.sw?
/package-lock.json
pnpm-lock.yaml
**/LexicalEditor0
*.zip
.env.*
vonage-client*
**/test
*.bak
**/LexicalEditor0
*.zip
.env.*
vonage-client*
**/test
*.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
select *
from whatsapp_individual.connections

@ -1,7 +1,7 @@
{
"name": "global-sales",
"private": true,
"version": "1.6.7",
"version": "1.4.10",
"type": "module",
"scripts": {
"dev": "vite",
@ -10,46 +10,31 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@dckj/react-better-modal": "^0.1.2",
"@haina/utils-commons": "https://research.hainatravel.com/npm/utils-commons-0.1.2.tgz",
"@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",
"@lexical/react": "^0.20.0",
"@vonage/client-sdk": "^2.0.0",
"antd": "^5.25.2",
"dayjs": "^1.11.13",
"dingtalk-jsapi": "^3.0.41",
"emoji-picker-react": "^4.12.0",
"lexical": "^0.34.0",
"prismjs": "^1.30.0",
"prop-types": "^15.8.1",
"lexical": "^0.20.0",
"react": "^18.3.1",
"react-chat-elements": "^12.0.18",
"react-chat-elements": "^12.0.17",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1",
"rxjs": "^7.8.1",
"uuid": "^9.0.1",
"workbox-window": "^7.3.0",
"zustand": "^4.5.7"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"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",
"tailwindcss": "^3.4.15",
"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';
/**

@ -1,9 +1,9 @@
import { groupBy, isNotEmpty, pick, sortArrayByOrder, sortBy } from '@haina/utils-commons';
import { fetchJSON, postJSON, postForm } from '@haina/utils-request'
import { groupBy, isNotEmpty, pick, sortArrayByOrder, sortBy } from '@/utils/commons';
import { fetchJSON, postJSON, postForm } from '@/utils/request'
import { parseRenderMessageList } from '@/channel/bubbleMsgUtils';
import { API_HOST } from '@/config';
import { isEmpty } from '@haina/utils-commons';
import { isEmpty } from '@/utils/commons';
import dayjs from 'dayjs';
/**
@ -11,9 +11,6 @@ import dayjs from 'dayjs';
*/
export const fetchTemplates = async (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 = [
'agent_intro_with_update_v1',
'online_inquiry_received',
@ -23,22 +20,14 @@ export const fetchTemplates = async (params) => {
'travel_service_update_v2',
'travel_service_update_v1',
'order_updated_specialist_assigned_sharon',
'travel_service_update_v3',
'first_message_for_not_reply',
// 'free_style_3',
// '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
const scNames = ['trip_planner_showcase', 'showcase_different', 'order_status_updated'];
// 客运
const crNames = [
'notification_of_next_trip_planning',
// 'notification_of_following_up_by_cr_v3',
'notification_of_one_day_before_ending_the_trip_by_cr_v2',
'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',
'post_booking_confirmation_welcome',
];
const crNamesAuto = [
'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 = [
const crNamesOmit = [
'birthday_greetings_by_marketing','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',
@ -69,9 +51,8 @@ export const fetchTemplates = async (params) => {
'notification_of_account_updated_by_cr',
'birthday_greetings_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))
.map((ele, i) => ({
...ele,
@ -79,16 +60,8 @@ export const fetchTemplates = async (params) => {
components: groupBy(ele.components, (_c) => _c.type.toLowerCase()),
key: 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),
displayLanguage: NamesOmit.includes(ele.name)
? '--'
: 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),
displayName: templatesDisplayNameMap?.[ele.name] || (ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : 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,
}))
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 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];
};
/**

@ -1,6 +1,6 @@
import { fetchJSON, postForm, postJSON } from '@haina/utils-request';
import { API_HOST, API_HOST_V3, DATE_FORMAT, DATEEND_FORMAT, DATETIME_FORMAT, EMAIL_HOST, EMAIL_HOST_v3 } from '@/config';
import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, uniqWith } from '@haina/utils-commons';
import { fetchJSON, postForm, postJSON } from '@/utils/request';
import { API_HOST, API_HOST_V3, DATE_FORMAT, DATEEND_FORMAT, DATETIME_FORMAT, EMAIL_HOST } from '@/config';
import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, uniqWith } from '@/utils/commons';
import { readIndexDB, writeIndexDB } from '@/utils/indexedDB';
import dayjs from 'dayjs';
import { internalEventEmitter } from '@/utils/EventEmitterService';
@ -81,16 +81,17 @@ export const postResendEmailAction = async (body) => {
const encodeEmailInfo = (info) => {
const encodeQuote = (str = '') => str.replace(/"/g, ''); //.replace(/</g,'&lt;').replace(/>/g,'&gt;')
const CSsClean = encodeQuote(info.MAI_CS.replace(';', ',')).split(',');
const tosClean = (encodeQuote(info.MAI_To.replace(';', ',')).split(',')).map(e => e.trim()).filter(s => s);
const CSsClean = encodeQuote(info.MAI_CS).includes(',') ? encodeQuote(info.MAI_CS).split(',') : encodeQuote(info.MAI_CS).split(';');
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 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 {
...info,
MAI_From: encodeQuote(info.MAI_From),
MAI_To: encodeQuote(info.MAI_To),
tos: [...new Set(tosClean)],
ccs: [...new Set(CSsClean)],
replyToAll,
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 }
// 更新数量
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 _MapDir = new Map(mailboxDir.map((obj) => [obj.key, obj]))
Object.keys(result).map(dirKey => {
@ -219,7 +220,7 @@ export const getMailboxCountAction = async (params = { opi_sn: '' }, update = tr
_MapRoot.set(row.key, row)
})
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) })
}
@ -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 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 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')
const _mapped = groupBy(rootTree, 'key')
return _mapped[opi_sn]?.[0]?.tree || []
@ -347,6 +348,7 @@ export const getRootMailboxDirAction = async ({ opi_sn = 0, userIdStr = '' } = {
* 获取邮件列表
* @usage 邮件目录下的邮件列表
* @usage 订单的邮件列表
* @usage 高级搜索
*/
export const queryEmailListAction = async ({ opi_sn = '', pagesize = 10, last_id = '', node = {}, } = {}) => {
const _params = {
@ -373,20 +375,6 @@ export const queryEmailListAction = async ({ opi_sn = '', pagesize = 10, last_id
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 readRow0 = await readIndexDB(params.mai_sn_list[0], 'listrow', 'mailbox')
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.
*/
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;
bodyData.ordertype = 227001;
const formData = new FormData();
@ -488,3 +476,7 @@ export const queryOPIOrderAction = async (params) => {
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'
const WAI_SERVER_KEY = 'G-STR:WAI_SERVER'

@ -1,6 +1,6 @@
.logo {
float: left;
height: 60px;
height: 68px;
margin: 0 6px 0 0;
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 { v4 as uuid } from "uuid";
@ -58,27 +58,6 @@ export const WABAccounts = [
"requestedVerifiedName": "Customer Relation Specialist",
"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 }), {})
@ -87,8 +66,9 @@ export const replaceTemplateString = (str, replacements) => {
let keys = str.match(/{{(.*?)}}/g).map(key => key.replace(/{{|}}/g, ''));
for (let i = 0; i < keys.length; i++) {
const replaceValue = replacements[i];
result = result.replaceAll(`{{${keys[i]}}}`, replaceValue);
let replaceValue = replacements[i];
let template = new RegExp(`{{${keys[i]}}}`, 'g');
result = result.replace(template, replaceValue);
}
return result;
@ -139,7 +119,7 @@ const mediaMsg = {
...msg,
actionId: msg.id,
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
? {
reply: {
@ -508,7 +488,7 @@ const sessionMsgMapped = {
getMsg: (result) => {
// sessionItem 是数组
return isEmpty(result?.sessionItem)
? []
? null
: result.sessionItem.map((ele) => ({
...ele,
customer_name: `${ele.whatsapp_name || ''}`.trim(),
@ -712,12 +692,12 @@ export const whatsappMsgTypeMapped = {
unsupported: {
type: 'text',
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: {
type: 'text',
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: {
type: 'text',
@ -846,18 +826,15 @@ export const parseRenderMessageItem = (msg) => {
origin: msg.context,
}),
msg_source: msg?.msg_source || msg.type,
whatsapp_msg_type: '',
waba: '',
wabaName: '',
...((msg.msg_source) === 'WABA' ? {
whatsapp_msg_type: msg.type,
waba: msg.msg_direction === 'outbound' ? msg.from : msg.to,
wabaName: WABAccountsMapped[msg.msg_direction === 'outbound' ? msg.from : msg.to]?.verifiedName,
} : {}),
...((msg.msg_source) === 'wai' ? {
whatsapp_msg_type: msg.type,
wabaName: '个人号',
} : {}),
} : {
whatsapp_msg_type: '',
waba: '',
wabaName: '',
}),
};
};
/**
@ -1094,120 +1071,3 @@ export const phoneNumberToWAID = (input) => {
}
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 Modal from '@dckj/react-better-modal';
import '@dckj/react-better-modal/dist/index.css';
import { isEmpty } from '@haina/utils-commons';
import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore';
const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial = {}, title, footer=null, ...props }) => {

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

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

@ -89,19 +89,6 @@ const FONT_SIZE_OPTIONS = [
// ['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 = {
center: { icon: 'center-align', iconRTL: 'center-align', name: 'Center 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 }) {
const handleClick = useCallback(
(option) => {
@ -582,23 +549,26 @@ function FontDropDown({ editor, value, style, disabled = false }) {
[editor, style]
);
const buttonIconClassName = FontDropDownMapped[style].IconClassName;
const buttonAriaLabel = FontDropDownMapped[style].buttonAriaLabel;
const dropdownOptions = FontDropDownMapped[style].options;
const buttonAriaLabel = style === 'font-family' ? 'Formatting options for font family' : 'Formatting options for font size';
return (
<DropDown disabled={disabled} buttonClassName={'toolbar-item ' + style} buttonLabel={value} buttonIconClassName={buttonIconClassName} buttonAriaLabel={buttonAriaLabel}>
{dropdownOptions.map(([option, text]) => (
<DropDown
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
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)}
key={option}>
<span className='text'>{text}</span>
</DropDownItem>
))}
</DropDown>
)
);
}
function ElementFormatDropdown({ editor, value, isRTL, disabled = false }) {
@ -703,7 +673,6 @@ export default function ToolbarPlugin() {
const [fontColor, setFontColor] = useState('#000');
const [bgColor, setBgColor] = useState('#fff');
const [elementFormat, setElementFormat] = useState('left');
const [lineSpacing, setLineSpacing] = useState('1.5');
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false);
@ -791,9 +760,6 @@ export default function ToolbarPlugin() {
setFontSize(
$getSelectionStyleValueForProperty(selection, 'font-size', '16px'),
);
setLineSpacing(
$getSelectionStyleValueForProperty(selection, 'line-height', '1.5'),
);
let matchingParent;
if ($isLinkNode(parent)) {
// 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}
editor={editor}
/>
<FontDropDown
disabled={!isEditable}
style={'line-height'}
value={lineSpacing}
editor={editor}
/>
<Divider />
<button type='button'
onClick={() => {

@ -87,7 +87,6 @@
outline: 0;
padding: 15px 10px;
caret-color: #444;
line-height: 1.5;
}
.editor-pure-input {
@ -416,7 +415,7 @@ pre::-webkit-scrollbar-thumb {
margin-right: 2px;
}
.toolbar button.toolbar-item i.format, .toolbar button.toolbar-item .icon2 {
.toolbar button.toolbar-item i.format {
background-size: contain;
display: inline-block;
height: 18px;
@ -890,10 +889,6 @@ i.justify-align {
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 {
cursor: default;
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,
EditOutlined,
CheckOutlined,
CopyOutlined
ReloadOutlined,
} from '@ant-design/icons'
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { App, Flex, Select, Tooltip, Divider, Typography, Skeleton, Checkbox, Drawer, Button, Empty, Form, Input } from 'antd'
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions } from '@/stores/OrderStore'
import { copy, isEmpty } from '@haina/utils-commons'
import { Link, useNavigate } from 'react-router-dom'
import { App, Flex, Select, Tooltip, Divider, Typography, Skeleton, Checkbox, Drawer, Button, Form, Input } from 'antd'
import { useOrderStore, fetchSetRemindStateAction, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions } from '@/stores/OrderStore'
import { copy, isEmpty } from '@/utils/commons'
import { useShallow } from 'zustand/react/shallow'
import useConversationStore from '@/stores/ConversationStore'
import useAuthStore from '@/stores/AuthStore'
const OrderProfile = ({ coliSN, ...props }) => {
const navigate = useNavigate()
const { notification, message } = App.useApp()
const [formComment] = Form.useForm()
const [formWhatsApp] = Form.useForm()
const [formExtra] = Form.useForm()
const [loading, setLoading] = useState(false)
const [openOrderCommnet, setOpenOrderCommnet] = useState(false)
const [openWhatsApp, setOpenWhatsApp] = useState(false)
const [openExtra, setOpenExtra] = useState(false)
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions)
const orderLabelOptions = copy(OrderLabelDefaultOptions)
orderLabelOptions.unshift({ value: 0, label: '未设置', disabled: true })
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions)
const [orderDetail, customerDetail, fetchOrderDetail, setOrderPropValue,
appendOrderComment, updateWhatsapp, updateExtraInfo, remindCheckList, updateRemindState] = useOrderStore((s) => [
const orderStatusOptions = copy(OrderStatusDefaultOptions)
const [orderDetail, customerDetail, fetchOrderDetail, setOrderPropValue, appendOrderComment] = useOrderStore((s) => [
s.orderDetail,
s.customerDetail,
s.fetchOrderDetail,
s.setOrderPropValue,
s.appendOrderComment,
s.updateWhatsapp,
s.updateExtraInfo,
s.remindCheckList,
s.updateRemindState
])
const loginUser = useAuthStore((state) => state.loginUser)
const currentOrder = useConversationStore(useShallow((state) => state.currentConversation?.coli_sn || ''))
const orderId = coliSN || currentOrder
const [orderRemindState, setOrderRemindState] = useState(orderDetail.remindstate)
useEffect(() => {
setOrderRemindState(orderDetail.remindstate);
}, [orderDetail.remindstate]);
useEffect(() => {
if (orderId) {
setLoading(true)
@ -71,12 +66,20 @@ const OrderProfile = ({ coliSN, ...props }) => {
}, [orderId])
const handleSetRemindState = async (checkedValue) => {
const state = checkedValue.filter((v) => v !== orderRemindState)
const oldState = orderRemindState
try {
await updateRemindState(coliSN, checkedValue)
if (isEmpty(state)) {
setOrderRemindState(null)
} else {
setOrderRemindState(state[0])
}
await fetchSetRemindStateAction({ coli_sn: coliSN, remindstate: state })
message.success('设置成功')
} catch (error) {
notification.warning({ message: '设置失败', description: error.message, placement: 'top', duration: 60 })
setOrderRemindState(oldState)
}
}
@ -89,18 +92,13 @@ const OrderProfile = ({ coliSN, ...props }) => {
return orderDetail.DidPlan === 0 ? '未做计划' : '已做计划'
}
const renderOrderDetail = () => {
return (
<>
return (
<>
<Skeleton active loading={loading}>
<Flex gap='small' vertical={true} justify='space-between' className='p-2'>
<Typography.Text>
<FieldNumberOutlined className='pr-1' />
{orderDetail.order_no}
<CopyOutlined onClick={() => {
navigator.clipboard.writeText(orderDetail.order_no)
message.success('已复制😀')
}}/>
</Typography.Text>
<Typography.Text>
<UserOutlined className=' pr-1' />
@ -119,16 +117,10 @@ const OrderProfile = ({ coliSN, ...props }) => {
{customerDetail.email}
</Typography.Text>
<Typography.Text>
<WhatsAppOutlined className='pr-1' />
{isEmpty(customerDetail.whatsapp_phone_number) ? (
<Button type='text' onClick={() => setOpenWhatsApp(true)} size='small'>
设置 WhatsApp
</Button>
) : (
<Link to={`/order/chat/${coliSN}`} state={{...orderDetail, coli_guest_WhatsApp: customerDetail.whatsapp_phone_number, }}>
{customerDetail.whatsapp_phone_number}
</Link>
)}
<WhatsAppOutlined className=' pr-1' />
<Link to={`/order/chat/${coliSN}`} state={orderDetail}>
{customerDetail.whatsapp_phone_number}
</Link>
</Typography.Text>
<Typography.Text>
<Tooltip title='出发日期'>
@ -202,7 +194,7 @@ const OrderProfile = ({ coliSN, ...props }) => {
<Divider orientation='left'>
<Typography.Text strong>催信</Typography.Text>
</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'>
<Typography.Text strong>表单信息</Typography.Text>
@ -223,19 +215,17 @@ const OrderProfile = ({ coliSN, ...props }) => {
<Typography.Text>{orderDetail.customer_request}</Typography.Text>
<Divider orientation='left'>
<Typography.Text strong>外联备注</Typography.Text>
{/* <Tooltip title=''>
<EditOutlined className='pl-1' />
</Tooltip> */}
</Divider>
<Typography.Text>{orderDetail.wl_memo}</Typography.Text>
<Divider orientation='left'>
<Typography.Text strong>附加信息</Typography.Text>
<Tooltip title='修改'>
<EditOutlined
className='pl-1'
onClick={() => {
formExtra.setFieldsValue({ extra: orderDetail.COLI_Introduction })
setOpenExtra(true)
}}
/>
</Tooltip>
{/* <Tooltip title=''>
<EditOutlined className='pl-1' />
</Tooltip> */}
</Divider>
<Typography.Text>{orderDetail.COLI_Introduction}</Typography.Text>
</Skeleton>
@ -246,6 +236,7 @@ const OrderProfile = ({ coliSN, ...props }) => {
initialValues={{ comment: '' }}
scrollToFirstError
onFinish={(values) => {
console.log('Received values of form: ', values)
appendOrderComment(loginUser.userId, orderId, values.comment)
.then(() => {
notification.success({
@ -274,90 +265,8 @@ const OrderProfile = ({ coliSN, ...props }) => {
</Form.Item>
</Form>
</Drawer>
<Drawer title='设置 WhatsApp' closable={{ 'aria-label': 'Close Button' }} onClose={() => setOpenWhatsApp(false)} open={openWhatsApp}>
<Form
layout={'vertical'}
form={formWhatsApp}
initialValues={{ number: '' }}
scrollToFirstError
onFinish={(values) => {
updateWhatsapp(orderId, values.number)
.then(() => {
notification.success({
message: '温性提示',
description: '设置 WhatsApp 成功',
})
setOpenWhatsApp(false)
formWhatsApp.setFieldsValue({ number: '' })
})
.catch((reason) => {
notification.error({
message: '设置出错',
description: reason.message,
placement: 'top',
duration: 60,
})
})
}}>
<Form.Item name='number' label='WhatsApp' rules={[{ required: true, message: '请输入 WhatsApp 号码' }]}>
<Input placeholder='国家代码+城市代码+电话号码' />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit'>
提交
</Button>
</Form.Item>
</Form>
</Drawer>
<Drawer title='设置附加信息' closable={{ 'aria-label': 'Close Button' }} onClose={() => setOpenExtra(false)} open={openExtra}>
<Form
layout={'vertical'}
form={formExtra}
scrollToFirstError
onFinish={(values) => {
updateExtraInfo(orderId, values.extra)
.then(() => {
notification.success({
message: '温性提示',
description: '设置附加信息成功',
})
setOpenExtra(false)
})
.catch((reason) => {
notification.error({
message: '设置出错',
description: reason.message,
placement: 'top',
duration: 60,
})
})
}}>
<Form.Item name='extra' label='附加信息' rules={[{ required: true, message: '请输入附加信息' }]}>
<Input />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit'>
提交
</Button>
</Form.Item>
</Form>
</Drawer>
</>
)
}
const renderDefaultEmpty = () => {
return (
<Empty description={<span>没有订单关联</span>}>
</Empty>
)
}
if (orderId) {
return renderOrderDetail()
} else {
return props.renderEmpty ? props.renderEmpty() : renderDefaultEmpty()
}
)
}
export default OrderProfile

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

@ -6,20 +6,20 @@
// export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_144'; // prod: Slave
// 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 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.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_SERVER_KEY = 'G-STR:WAI_SERVER'
// prod:--------------------------------------------------------------------------------------------------
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_v3 = 'https://p9axztuwd7x8a7.mycht.cn/mail-server/service-mail';
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 API_HOST = 'https://p9axztuwd7x8a7.mycht.cn/whatsapp_server/v2';
export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_server'; // prod:
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';
@ -38,9 +38,8 @@ export const DEFAULT_WABA = '+8617607730395';
const __BUILD_VERSION__ = `__BUILD_VERSION__`.replace(/"/g, '')
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_DATE = import.meta.env.PROD ? __BUILD_DATE__ : new Date().toLocaleString();
export const GIT_HEAD = import.meta.env.PROD ? __GIT_HEAD__ : 'current';
export const BUILD_VERSION = process.env.NODE_ENV === 'production' ? __BUILD_VERSION__ : process.env.NODE_ENV;
export const BUILD_DATE = process.env.NODE_ENV === 'production' ? __BUILD_DATE__ : new Date().toLocaleString();
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 useConversationStore from '@/stores/ConversationStore';
import { fetchConversationsList, fetchOrderConversationsList, postNewOrEditConversationItem } from '@/actions/ConversationActions';
import { isEmpty } from '@haina/utils-commons';
import { isEmpty } from '@/utils/commons';
const CHAT_ITEM_RECORD = {
"sn": null,

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

@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { isEmpty } from '@haina/utils-commons'
import { isEmpty } from '@/utils/commons'
import { App, notification } from 'antd'
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 DingdingQRCode from '@/views/dingding/QRCode'
import DingdingAuthCode from '@/views/dingding/AuthCode'
import LocalWhatsAppQRCode from '@/views/accounts/LocalWhatsAppQRCode'
import useAuthStore from '@/stores/AuthStore'
import '@/assets/index.css'
@ -36,10 +35,7 @@ import NewEmail from '@/views/NewEmail'
import EmailDetailWindow from '@/views/EmailDetailWindow'
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()
const isMobileApp =
@ -111,7 +107,6 @@ const router = createBrowserRouter([
{ path: 'dingding/callback', element: <DingdingCallback /> },
{ path: 'dingding/qr-code', element: <DingdingQRCode /> },
{ 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'
const MaintenancePage = () => {

@ -1,7 +1,7 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON } from '@haina/utils-request'
import { isEmpty, isNotEmpty } from '@haina/utils-commons'
import { fetchJSON } from '@/utils/request'
import { isEmpty, isNotEmpty } from '@/utils/commons'
import { API_HOST, BUILD_VERSION } from '@/config'
import { usingStorage } from '@/utils/usingStorage';
@ -70,8 +70,9 @@ const useAuthStore = create(devtools((set, get) => ({
)
if (json.errcode === 0 && isNotEmpty(json.result.opisn)) {
// TODO保存个人 WhatsApp 服务器地址
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(() => ({
loginUser: {
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: () => {
const { loginUser } = get()
@ -218,7 +158,13 @@ const useAuthStore = create(devtools((set, get) => ({
loadUserSession: () => {
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)) {
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' }))
export default useAuthStore

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

@ -1,6 +1,6 @@
import { create } from 'zustand';
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 { receivedMsgTypeMapped, handleNotification } from '@/channel/bubbleMsgUtils';
import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions';
@ -197,10 +197,10 @@ const websocketSlice = (set, get) => ({
// console.log('msgRender msgUpdate', msgRender, msgUpdate);
if (['email.updated', 'email.inbound.received',].includes(resultType)) {
updateMailboxCount({ opi_sn: msgObj.opi_sn })
// if (!isEmpty(msgRender)) {
// const msgNotify = receivedMsgTypeMapped[resultType].contentToNotify(msgObj);
// addGlobalNotify(msgNotify);
// }
if (!isEmpty(msgRender)) {
const msgNotify = receivedMsgTypeMapped[resultType].contentToNotify(msgObj);
addGlobalNotify(msgNotify);
}
return false;
}
if ([
@ -238,11 +238,9 @@ const websocketSlice = (set, get) => ({
// }, 60_000);
}
// 会话表 更新
if (['session.new', 'session.updated'].includes(resultType)
&& result.webhooksource !== 'email'
) {
if (['session.new', 'session.updated'].includes(resultType)) {
const sessionList = receivedMsgTypeMapped[resultType].getMsg(result);
addToConversationList(sessionList || [], 'top')
addToConversationList(sessionList, 'top');
}
// console.log('handleMessage*******************');
},
@ -278,17 +276,13 @@ const conversationSlice = (set, get) => ({
// 让当前会话显示在页面上
let _tmpCurrentMsgs = [];
if (currentConversation.sn) {
// _tmpCurrentMsgs = activeConversations[currentConversation.sn];
_tmpCurrentMsgs = activeConversations[currentConversation.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 (indexCurrent !== -1) {
const [currentElement] = conversationsList.splice(indexCurrent, 1);
conversationsList.unshift(currentElement); // Add to top
// 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);
if (currentConversation.sn) {
const hasCurrent = Object.keys(conversationsMapped).findIndex(sn => Number(sn) === Number(currentConversation.sn)) !== -1;
conversationsMapped[currentConversation.sn] = _tmpCurrentMsgs;
const _len = hasCurrent ? 0 : conversationsList.unshift(currentConversation);
}
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 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 (indexCurrent !== -1 ) {
const [currentElement] = mergedList.splice(indexCurrent, 1);
mergedList.unshift(currentElement); // Add to top
// hasCurrent ? 0 : mergedList.unshift(currentConversation);
if (currentConversation.sn) {
const hasCurrent = -1 !== Object.keys(mergedListMsgs).findIndex(sn => Number(sn) === Number(currentConversation.sn));
hasCurrent ? 0 : mergedList.unshift(currentConversation);
}
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 { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@haina/utils-request'
import { fetchJSON, postForm } from '@/utils/request'
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) => ({
loading: false,

@ -1,5 +1,5 @@
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 { internalEventEmitter } from '@/utils/EventEmitterService';
@ -124,7 +124,7 @@ const emailSlice = (set, get) => ({
let isNeedRefresh = refreshNow
if (!isEmpty(readCache)) {
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
}
if (isEmpty(readCache) || isNeedRefresh) {
@ -139,14 +139,9 @@ const emailSlice = (set, get) => ({
return false
},
/**
* 更新数量
* @usage 1. 邮件列表页切换用户时
* @usage 2. 收到新邮件推送时
*
*/
// 更新数量
updateMailboxCount: async ({ opi_sn }) => {
// const { setMailboxNestedDirsActive } = get()
const { setMailboxNestedDirsActive } = get()
await getMailboxCountAction({ opi_sn })
// const readCache = await readIndexDB(Number(opi_sn), 'dirs', 'mailbox')
// if (!isEmpty(readCache)) {
@ -155,7 +150,7 @@ const emailSlice = (set, get) => ({
},
async initMailbox({ opi_sn, dei_sn, userIdStr }) {
olog('Initialize Mailbox ---- ')
olog('initMailbox ---- ')
const { currentMailboxOPI, setCurrentMailboxOPI, setCurrentMailboxDEI, getOPIEmailDir, setMailboxNestedDirsActive, } = get()
createIndexedDBStore(['dirs', 'maillist', 'listrow', 'mailinfo', 'draft'], 'mailbox')
setCurrentMailboxOPI(opi_sn)

@ -1,8 +1,8 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm, postJSON } from '@haina/utils-request'
import { API_HOST, API_HOST_V3, EMAIL_HOST } from '@/config'
import { isEmpty, isNotEmpty, prepareUrl, uniqWith } from '@haina/utils-commons'
import { fetchJSON, postForm } from '@/utils/request'
import { API_HOST, EMAIL_HOST } from '@/config'
import { isNotEmpty, prepareUrl, uniqWith } from '@/utils/commons'
const initialState = {
orderList: [],
@ -11,261 +11,213 @@ const initialState = {
lastQuotation: {},
quotationList: [],
otherEmailList: [],
remindCheckList: [],
}
export const useOrderStore = create(
devtools(
(set, get) => ({
...initialState,
drawerOpen: false,
resetOrderStore: () => set(initialState),
openDrawer: () => {
set(() => ({
drawerOpen: true,
}))
},
closeDrawer: () => {
set(() => ({
drawerOpen: false,
}))
},
fetchOrderList: async (formValues, loginUser) => {
let fetchOrderUrl = `${API_HOST}/getwlorder?opisn=${loginUser.userIdStr}&otype=${formValues.type}`
const params = {}
if (formValues.type === 'advance') {
fetchOrderUrl = `${API_HOST}/getdvancedwlorder?opisn=${loginUser.userIdStr}`
const { type, ...formParams } = formValues
Object.assign(params, formParams)
};
export const useOrderStore = create(devtools((set, get) => ({
...initialState,
drawerOpen: false,
resetOrderStore: () => set(initialState),
openDrawer: () => {
set(() => ({
drawerOpen: true
}))
},
closeDrawer: () => {
set(() => ({
drawerOpen: false
}))
},
fetchOrderList: async (formValues, loginUser) => {
let fetchOrderUrl = `${API_HOST}/getwlorder?opisn=${loginUser.userIdStr}&otype=${formValues.type}`
const params = {};
if (formValues.type === 'advance') {
fetchOrderUrl = `${API_HOST}/getdvancedwlorder?opisn=${loginUser.userIdStr}`;
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 }
})
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)
}
})
},
fetchOrderDetail: (colisn) => {
return fetchJSON(`${API_HOST}/getorderinfo`, { colisn }).then((json) => {
if (json.errcode === 0 && json.result.length > 0) {
const orderResult = json.result[0]
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)
})
},
fetchOrderDetail: (colisn) => {
return fetchJSON(`${API_HOST}/getorderinfo`, { colisn })
.then(json => {
if (json.errcode === 0 && json.result.length > 0) {
const orderResult = json.result[0]
set(() => ({
orderDetail: {...orderResult, coli_sn: colisn },
customerDetail: orderResult.contact.length > 0 ? orderResult.contact[0] : {},
// lastQuotation: orderResult.quotes.length > 0 ? orderResult.quotes[0] : {},
// quotationList: orderResult.quotes,
}))
return {
orderDetail: {...orderResult, coli_sn: colisn },
customerDetail: orderResult.contact.length > 0 ? orderResult.contact[0] : {},
// lastQuotation: orderResult.quotes.length > 0 ? orderResult.quotes[0] : {},
// quotationList: orderResult.quotes,
}
})
},
} 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,
}))
const finalState = {
'FirstRemind': checkedValue.includes('FirstRemind') ? 1 : 0,
'SecondRemind': checkedValue.includes('SecondRemind') ? 1 : 0,
'ThirdRemind': checkedValue.includes('ThirdRemind') ? 1 : 0,
'important': checkedValue.includes('important') ? 1 : 0,
'sendsurvey': checkedValue.includes('sendsurvey') ? 1 : 0,
},
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)
}
const { errcode, result } = await postJSON(`${API_HOST}/SetRemindState`, { coli_sn: orderId, remindstate: JSON.stringify(finalState)})
return errcode === 0 ? result : {}
},
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)
}
})
},
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)
}
})
})
},
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)
})
},
fetchOtherEmail: (coli_sn) => {
return fetchJSON(`${EMAIL_HOST}/email_supplier`, { coli_sn }).then((json) => {
if (json.errcode === 0) {
set(() => ({
otherEmailList: json.result.MailInfo ?? [],
}))
} else {
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,
},
})
})
Promise.all(orderPromiseList).then((values) => {
console.log(values);
})
},
fetchOtherEmail: (coli_sn) => {
return fetchJSON(`${EMAIL_HOST}/email_supplier`, { coli_sn })
.then(json => {
if (json.errcode === 0) {
set(() => ({
otherEmailList: json.result.MailInfo ?? [],
}))
} else {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
if (propName === 'orderstatus') {
set((state) => ({
orderDetail: {
...state.orderDetail,
states: value,
},
}))
setOrderPropValue: async (colisn, propName, value) => {
if (propName === 'orderlabel') {
set((state) => ({
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) => {
if (json.errcode > 0) {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
}),
{ name: 'orderStore' },
),
)
return fetchJSON(`${API_HOST}/setorderstatus`, { colisn, stype: propName, svalue: value })
.then(json => {
if (json.errcode > 0) {
throw new Error(json?.errmsg + ': ' + json.errcode)
}
})
},
}), { name: 'orderStore' }))
export const OrderLabelDefaultOptions = [
{ value: 240003, label: '重点', emoji: '❣️' },
{ value: 240002, label: '次重点', emoji: '❗' },
{ value: 240001, label: '一般', emoji: '' },
{ value: 240001, label: '一般', emoji: '' }
]
export const OrderLabelDefaultOptionsMapped = OrderLabelDefaultOptions.reduce((acc, cur) => {
return { ...acc, [String(cur.value)]: cur }
}, {})
}, {}) ;
export const OrderStatusDefaultOptions = [
{ value: 1, label: '新订单', emoji: '' },
@ -274,8 +226,8 @@ export const OrderStatusDefaultOptions = [
{ value: 4, label: '等待付订金', emoji: '🛒' },
{ value: 5, label: '成行', emoji: '💰' },
{ value: 6, label: '丢失', emoji: '🎈' },
// { value: 7, label: '取消', emoji: '🚫' }, // 取消要顾问确认后才能执行操作,暂时到 HT 操作
{ value: 8, label: '未报价', emoji: '' },
{ value: 7, label: '取消', emoji: '🚫' },
{ value: 8, label: '未报价', emoji: '' }
]
export const OrderStatusDefaultOptionsMapped = OrderStatusDefaultOptions.reduce((acc, cur) => {
return { ...acc, [String(cur.value)]: cur }
@ -287,32 +239,27 @@ export const OrderStatusDefaultOptionsMapped = OrderStatusDefaultOptions.reduce(
export const RemindStateDefaultOptions = [
{ value: '1', label: '一催' },
{ value: '2', label: '二催' },
{ value: '3', label: '三催' },
{ value: '3', label: '三催' }
]
/**
* @useage 订单信息: 标记状态
*/
export const remindStatusOptions = [
{ value: 'FirstRemind', label: '已发一催' },
{ value: 'SecondRemind', label: '已发二催' },
{ value: 'ThirdRemind', label: '已发三催' },
{ value: 1, label: '已发一催' },
{ value: 2, label: '已发二催' },
{ value: 3, label: '已发三催' },
{ value: 'important', label: '重点团' },
{ 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) => {
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 { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@haina/utils-request'
import { fetchJSON, postForm } from '@/utils/request'
import { API_HOST } from '@/config'
import { copy } from '@haina/utils-commons'
import { copy } from '@/utils/commons'
const useSnippetStore = create(devtools((set, get) => ({
@ -41,7 +41,7 @@ const useSnippetStore = create(devtools((set, get) => ({
const mapTypeList = json?.result?.type.map(item => {
return { value: item.vsn, label: item.vname }
})
const mapTypeAllList = structuredClone(mapTypeList);
const mapTypeAllList = copy(mapTypeList);
mapTypeAllList.unshift({ value: '', label: '全部' });
set(() => ({
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) {
return JSON.parse(JSON.stringify(obj))
}
@ -189,8 +188,6 @@ export function merge(...objects) {
* 数组分组
* - 相当于 lodash _.groupBy
* @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
* ECMAScript 2021 原生
* - Object.groupBy(items, callbackFn)
*/
export function groupBy(array = [], callback) {
return array.reduce((groups, item) => {

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

@ -1,125 +1,54 @@
import { loadScript } from '@haina/utils-commons'
import { fetchJSON } from '@haina/utils-request'
import { loadScript } from '@/utils/commons'
import { readWebsocketLog } from '@/utils/indexedDB'
import { BUILD_VERSION, BUILD_DATE } from '@/config'
/**
* @deprecated
*/
// export const loadPageSpy = (title) => {
// if (import.meta.env.DEV || window.$pageSpy) return
// const PageSpyConfig = { api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true }
// const PageSpySrc = [
// '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}`,
// 'https://page-spy.mycht.cn/plugin/rrweb/index.min.js' + `?${BUILD_DATE}`,
// ]
// Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
// // 注册插件
// window.$harbor = new DataHarborPlugin()
// window.$rrweb = new RRWebPlugin()
// ;[window.$harbor, window.$rrweb].forEach((p) => {
// PageSpy.registerPlugin(p)
// })
// window.$pageSpy = new PageSpy(PageSpyConfig)
// // PageSpy.registerPlugin(new DataHarborPlugin());
// // PageSpy.registerPlugin(new RRWebPlugin());
// // 实例化 PageSpy
// // window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true, });
// console.log('[PageSpy]', window.$pageSpy.version)
// // window.addEventListener('beforeunload', (e) => {
// // e.preventDefault() // If you prevent default behavior in Mozilla Firefox
// // e.returnValue = '' // Chrome requires returnValue to be set
// // window.$harbor.upload({ clearCache: false, remark: '自动上传' }) // 上传日志 { clearCache: true, remark: '' }
// // })
// window.onerror = async function (msg, url, lineNo, columnNo, error) {
// await readWebsocketLog()
// // 上传最近 3 分钟的日志
// const now = Date.now()
// await window.$harbor.uploadPeriods({
// 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 loadPageSpy = (title) => {
if (import.meta.env.DEV || window.$pageSpy) return
const PageSpyConfig = { api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true }
const PageSpySrc = [
'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}`,
'https://page-spy.mycht.cn/plugin/rrweb/index.min.js' + `?${BUILD_DATE}`,
]
Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
// 注册插件
window.$harbor = new DataHarborPlugin()
window.$rrweb = new RRWebPlugin()
;[window.$harbor, window.$rrweb].forEach((p) => {
PageSpy.registerPlugin(p)
})
window.$pageSpy = new PageSpy(PageSpyConfig)
// PageSpy.registerPlugin(new DataHarborPlugin());
// PageSpy.registerPlugin(new RRWebPlugin());
// 实例化 PageSpy
// window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: 'Sales CRM', title: title + '(v' + BUILD_VERSION + ')', autoRender: false, offline: true, });
console.log('[PageSpy]', window.$pageSpy.version)
// window.addEventListener('beforeunload', (e) => {
// e.preventDefault() // If you prevent default behavior in Mozilla Firefox
// e.returnValue = '' // Chrome requires returnValue to be set
// window.$harbor.upload({ clearCache: false, remark: '自动上传' }) // 上传日志 { clearCache: true, remark: '' }
// })
window.onerror = async function (msg, url, lineNo, columnNo, error) {
await readWebsocketLog()
// 上传最近 3 分钟的日志
const now = Date.now()
await window.$harbor.uploadPeriods({
startTime: now - 3 * 60000,
endTime: now,
remark: `\`onerror\`自动上传. ${msg}`,
})
}
});
};
/**
* @deprecated
*/
// 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')
// }
// }
})
}
export const uploadPageSpyLog = async () => {
await readWebsocketLog()
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (window.$pageSpy) {
await window.$harbor.upload() // 上传日志 { clearCache: true, remark: '' }
}
}

@ -1,10 +1,6 @@
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 = []
// 添加 HTTP Reuqest 自定义头部

@ -6,37 +6,41 @@ import {
App as AntApp,
ConfigProvider,
Empty,
Modal, FloatButton,
theme
Modal,
message,
FloatButton,
theme,
} 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 'dayjs/locale/zh-cn'
import { useEffect } from 'react'
import { Outlet, useHref, useNavigate } from 'react-router-dom'
import { appendRequestHeader } from '@haina/utils-request'
import { loadPageSpy } from '@haina/utils-pagespy'
import { Link, NavLink, Outlet, useHref, useNavigate } from 'react-router-dom'
import { appendRequestHeader } from '@/utils/request'
import { loadPageSpy } from '@/utils/pagespy'
import AppLogo from '@/assets/highlights_travel_300_300.png'
import '@/assets/App.css'
import 'react-chat-elements/dist/main.css'
import EmailFetch from './Conversations/Online/Components/EmailFetch'
import FetchEmailWorker from './../workers/fetchEmailWorker?worker&url'
import { readWebsocketLog } from '@/utils/indexedDB'
import { useGlobalNotify } from '@/hooks/useGlobalNotify'
import GeneratePaymentDrawer from './Conversations/Online/Components/GeneratePaymentDrawer'
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' });
function AuthApp() {
const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
const { colorPrimary, borderRadius } = useThemeContext()
const [loginUser] = useAuthStore((state) => [
state.loginUser
const [loginUser, sendNotify] = useAuthStore((state) => [
state.loginUser, state.sendNotify
])
const href = useHref()
@ -57,7 +61,7 @@ function AuthApp() {
let _fetchEmailWorker;
if (loginUser.userId > 0) {
appendRequestHeader('X-User-Id', loginUser.userId)
loadPageSpy(loginUser.username + '(v' + BUILD_VERSION + ')', 'Sales CRM', true)
loadPageSpy(loginUser.username)
connectWebsocket(loginUser.userId)
fetchInitialData(loginUser)
@ -86,6 +90,29 @@ function AuthApp() {
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...
const needToLogin = loginUser.userId === -1 && href.indexOf('/p/') === -1
@ -134,9 +161,10 @@ function AuthApp() {
icon={<CustomerServiceOutlined />}
>
<EmailFetch />
<LogUploader />
<FloatButton icon={<BugOutlined />} tooltip={<div>上传日志给研发部</div>} onClick={() => uploadLog()} />
<FloatButton.BackTop />
</FloatButton.Group>
{contextHolder}
{needToLogin ? <>login...</> : <Outlet />}
<dialog id="about-dialog" className="border-0">
<img className="logo" src={AppLogo} alt="logo" />
@ -163,7 +191,6 @@ function AuthApp() {
</dialog>
<GeneratePaymentDrawer />
<GenerateAutoDocDrawer />
<GenerateShorturlDrawer />
</ErrorBoundary>
</AntApp>
</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 { PhoneOutlined, CustomerServiceOutlined, AudioOutlined } from "@ant-design/icons";
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 useAuthStore from "@/stores/AuthStore";

@ -6,7 +6,7 @@ import ConversationsList from './Conversations/History/ConversationsList';
import MessagesMatchList from './Conversations/History/MessagesMatchList';
import MessagesList from './Conversations/History/MessagesList';
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 EmailDetail from './Conversations/Online/Components/EmailDetail';
import SupplierEmailDrawer from './Conversations/Online/Components/EmailListDrawer';

@ -6,7 +6,7 @@ import ConversationsList from './Conversations/History/ConversationsList';
import MessagesMatchList from './Conversations/History/MessagesMatchList';
import MessagesList from './Conversations/History/MessagesList';
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';
const { Sider, Content } = Layout;

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

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

@ -4,13 +4,12 @@ import { LoadingOutlined } from '@ant-design/icons';
import { MessageBox } from 'react-chat-elements';
import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions';
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 MergeConversationTo from './MergeConversationTo';
import BubbleIM from '../Online/Components/BubbleIM';
import BubbleEmail from '../Online/Components/BubbleEmail';
import { ERROR_IMG, POPUP_FEATURES } from '@/config';
import { parseSimpleMarkdown } from '@/channel/bubbleMsgUtils';
const BIG_PAGE_SIZE = MESSAGE_PAGE_SIZE * 20;
const MessagesList = ({ ...listProps }) => {
@ -149,27 +148,6 @@ const MessagesList = ({ ...listProps }) => {
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 }) {
let headerObj, footerObj, buttonsArr;
@ -179,8 +157,23 @@ const MessagesList = ({ ...listProps }) => {
footerObj = componentsObj?.footer?.[0];
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 (
<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 ? (
<div className='text-neutral-500 text-center'>
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
@ -192,7 +185,17 @@ const MessagesList = ({ ...listProps }) => {
)}
</div>
) : 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>
);
});

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

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

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

@ -2,8 +2,7 @@ import { memo } from 'react';
import { App, Button, Image } from 'antd';
import { ExportOutlined, CopyOutlined, PhoneOutlined } from '@ant-design/icons';
import { MessageBox } from 'react-chat-elements';
import { groupBy, isEmpty, TagColorStyle } from '@haina/utils-commons';
import { parseSimpleMarkdown } from '@/channel/bubbleMsgUtils';
import { groupBy, isEmpty, TagColorStyle } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
import { ReplyIcon } from '@/components/Icons';
@ -24,34 +23,6 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
setNewChatModalVisible(true);
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 }) {
let headerObj, footerObj, buttonsArr;
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), []);
}
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 (
<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 ? (
<div className='text-neutral-500 text-center'>
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
@ -74,7 +63,24 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
)}
</div>
) : 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}
{buttonsArr && buttonsArr.length > 0 ? (
<div className='flex flex-row gap-1'>
@ -95,7 +101,7 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
)}
</div>
) : null}
</div>
</span>
)
});
return (

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

@ -5,7 +5,7 @@ import { CloseCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
import { fetchConversationItemClose, fetchConversationsSearch, fetchConversationItemUnread, fetchConversationItemTop, postConversationTags, deleteConversationTags, fetchCleanUnreadMsgCount } from '@/actions/ConversationActions';
import { ChatItem } from 'react-chat-elements';
// 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 useAuthStore from '@/stores/AuthStore';
import ChannelLogo from './ChannelLogo';

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

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

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { App, Button, Divider, Avatar } from 'antd'
import { LoadingOutlined, ApiOutlined } from '@ant-design/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 DnDModal from '@/components/DnDModal'
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 { LoadingOutlined, ApiOutlined, FilePdfOutlined, FileOutlined, FileWordOutlined, FileExcelOutlined, FileJpgOutlined, FileImageOutlined, FileTextOutlined, FileGifOutlined, GlobalOutlined, FileZipOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons'
import { EditIcon, MailCheckIcon, ReplyAllIcon, ReplyIcon, ResendIcon, ShareForwardIcon, SendPlaneFillIcon, InboxIcon } from '@/components/Icons'
import { isEmpty, TagColorStyle } from '@haina/utils-commons'
import { isEmpty, TagColorStyle } from '@/utils/commons'
import EmailEditorPopup from '../Input/EmailEditorPopup'
import DnDModal from '@/components/DnDModal'
import useStyleStore from '@/stores/StyleStore'
@ -29,13 +29,6 @@ const extTypeMapped = {
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, } } }
*/
@ -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 不显示
*/
const renderActionBtns = ({ className, ...props }) => {
const ActionBtns = ({ className, ...props }) => {
const { status } = mailData.info
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>
}
const renderAttaList = ({ list }) => {
return (
<List
dataSource={list}
size='small'
className='[&_.ant-list-item]:p-1 [&_.ant-list-item]:justify-start'
renderItem={(atta) => (
<List.Item>
<FileTypeIcon fileName={atta.ATI_Name} />
<Typography.Text ellipsis={{ tooltip: { title: atta.ATI_Name, placement: 'left' } }} className='text-inherit'>
<span key={atta.ATI_SN} onClick={() => openPopup(`${EMAIL_ATTA_HOST}${encodeURIComponent(atta.ATI_ServerFile)}`, atta.ATI_SN)} size='small' className='text-blue-500 cursor-pointer'>
{atta.ATI_Name}
</span>
</Typography.Text>
</List.Item>
)}
/>
)
};
const variantCls = (variant) => {
switch (variant) {
case 'outline':
return 'h-full border-y-0 border-x border-solid border-neutral-200'
return 'border-y-0 border-x border-solid border-neutral-200'
case 'full':
return 'h-[calc(100dvh-16px)]'
return 'h-[calc(100vh-16px)]'
default:
return 'h-full'
return ''
}
}
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='flex flex-wrap justify-between'>
<span className={(mailData.info?.MAI_ReadState || 0) > 0 ? '' : ' font-bold '}>
{loading ? <LoadingOutlined className='mr-1' /> : null}
{mailData.info?.MAI_Subject || emailMsg?.msgOrigin?.subject}
</span>
{/* <ActionBtns key='actions' className={'ml-auto'} /> */}
{renderActionBtns({ className: 'ml-auto'})}
<ActionBtns key='actions' className={'ml-auto'} />
</div>
<Divider className='my-2' />
<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 && (
<>
<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>
<summary>
<span className='text-gray-500 italic'>&nbsp;文内附件 ({mailData.insideAttachments.length}) 已在正文显示&nbsp;</span><span className='cursor-pointer underline'>点击展开</span>
</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>}
</div>
)}

@ -4,7 +4,7 @@ import { CloseOutlined, DownloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'
import { InboxIcon, SendPlaneFillIcon, ExpandIcon } from '@/components/Icons'
import EmailDetailInline from './EmailDetailInline'
import { debounce, isEmpty } from '@haina/utils-commons'
import { debounce, isEmpty } from '@/utils/commons'
import useConversationStore from '@/stores/ConversationStore';
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 useConversationStore from '@/stores/ConversationStore'
import { getEmailQuotationDraftAction } from '@/actions/EmailActions'
import { isEmpty } from '@haina/utils-commons'
import { isEmpty } from '@/utils/commons'
const EmailQuotation = ({ sfi_sn, ...props }) => {
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 { RotateLeftOutlined, RotateRightOutlined, ZoomInOutlined, ZoomOutOutlined } from '@ant-design/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 EmailDetail from './EmailDetail';
import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions';

@ -1,7 +1,7 @@
import { useState } from 'react';
import { App, Modal, Button, Table } from 'antd';
import { isEmpty, cloneDeep } from '@haina/utils-commons';
import { fetchJSON } from '@haina/utils-request';
import { isEmpty, cloneDeep } from '@/utils/commons';
import { fetchJSON } from '@/utils/request';
import AdvanceSearchForm from './../../orders/AdvanceSearchForm';
import { API_HOST } from '@/config';
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 { fetchConversationsList, fetchOrderConversationsList, CONVERSATION_PAGE_SIZE } from '@/actions/ConversationActions';
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 useAuthStore from '@/stores/AuthStore';
// import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from "@/stores/OrderStore";
@ -336,6 +336,13 @@ const Conversations = () => {
<Button onClick={toggleClosedConversationsList} icon={activeList ? '🗂' : '📌'} type='text' />
</Tooltip>
}
{mobile && (
<AudioTwoTone className='px-3'
onClick={() => {
navigate(`/callcenter/call`)
}}
/>
)}
</div>
<div className='flex-1 overflow-x-hidden overflow-y-auto relative'>
{/* {mobile && conversationsListLoading && dataSource.length === 0 ? ( */}

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
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 { phoneNumberToWAID } from '@/channel/bubbleMsgUtils';
import { useConversationNewItem } from '@/hooks/useConversation';

@ -6,8 +6,7 @@ import InputMediaUpload from './MediaUpload'
import PaymentlinkBtn from './PaymentlinkBtn'
import SnippestBtn from './SnippestBtn'
import useConversationStore from '@/stores/ConversationStore'
import { isEmpty } from '@haina/utils-commons'
import ShortlinkBtn from './ShortlinkBtn'
import { isEmpty } from '@/utils/commons'
const ComposerTools = ({ channel, invokeSendUploadMessage, invokeSendMessage, invokeUploadFileMessage, inputEmoji, ...props }) => {
const websocket = useConversationStore((state) => state.websocket)
@ -26,7 +25,6 @@ const ComposerTools = ({ channel, invokeSendUploadMessage, invokeSendMessage, in
<PaymentlinkBtn />
<SnippestBtn />
<ShortlinkBtn />
{/* <Button type='text' className='' icon={<YoutubeOutlined />} 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 useStyleStore from '@/stores/StyleStore'
import useAuthStore from '@/stores/AuthStore'
// import { isEmpty, } from '@haina/utils-commons';
// import { isEmpty, } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore'
import { useOrderStore } from '@/stores/OrderStore'
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 { postSendEmail } from '@/actions/EmailActions'
import { sentMsgTypeMapped, } from '@/channel/bubbleMsgUtils';

@ -10,7 +10,7 @@ import useAuthStore from '@/stores/AuthStore';
import LexicalEditor from '@/components/LexicalEditor';
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 './EmailEditor.css';
import { postSendEmail } from '@/actions/EmailActions';

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore';
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 { sentMsgTypeMapped, whatsappSupportFileTypes, uploadProgressSimulate, WABAccountsMapped } from '@/channel/bubbleMsgUtils';
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 { API_HOST, OSS_URL as aliOSSHost } from '@/config';
import { whatsappSupportFileTypes, uploadProgressSimulate } from '@/channel/bubbleMsgUtils';
import { isEmpty, sanitizeFilename } from '@haina/utils-commons';
import { isEmpty, sanitizeFilename } from '@/utils/commons';
// import useConversationStore from '@/stores/ConversationStore';
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 useAuthStore from '@/stores/AuthStore';
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 { isEmpty } from '@haina/utils-commons';
import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore';
const splitTemplate = (template) => {
@ -27,7 +27,6 @@ const splitTemplate = (template) => {
// MARKETING
const templateCaterogyText = { 'UTILITY': '跟进', 'MARKETING': '营销' }
const templateCaterogyTipText = { 'UTILITY': '触达率高', 'MARKETING': '' }
const templateCaterogyTipText2 = { 'UTILITY': '', 'MARKETING': '美国(+1)❌' }
const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, activeInput }) => {
const currentConversation = useConversationStore((state) => state.currentConversation);
@ -61,7 +60,7 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
<span key={ele.trim()} className=' text-wrap'>
{ele.replace(/\n+/g, '\n')}
</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
key={`${ele.key}_${i}`}
rows={2}
@ -71,8 +70,7 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
className={` w-11/12 `}
size={'small'}
title={ele.key}
// ${paramsVal[ele.key] || ele.key}
placeholder={`按Tab键跳到下一个空格\n注意: 模板消息无法输入换行`}
placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格\n注意: 模板消息无法输入换行`}
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
// onPressEnter={() => handleSendTemplate(tempItem)}
/>
@ -165,11 +163,10 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
<span>
{item.components.header?.[0]?.text || (item.displayName)}
<Tag style={{ ...TagColorStyle(item.language.toUpperCase(), true) }} className='ml-1'>
{item.language.slice(-2).toUpperCase()}
{item.language.toUpperCase()}
</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>}
{/* {templateCaterogyTipText2[item.category] && <Tag style={{ ...TagColorStyle(templateCaterogyTipText2[item.category].toUpperCase(), true) }}>{templateCaterogyTipText2[item.category]}</Tag>} */}
</span>
<Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
Send
@ -359,11 +356,9 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => {
<>
<Popover
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
forceRender
destroyTooltipOnHide={true}
destroyOnHidden={true}
title={
<div className='flex justify-between mt-0 gap-4 items-center'>
<Input.Search prefix={'💬'}

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

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

@ -7,7 +7,7 @@ import { WABIcon } from '@/components/Icons';
import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
import useStyleStore from '@/stores/StyleStore';
import { isEmpty } from '@haina/utils-commons';
import { isEmpty } from '@/utils/commons';
import { DEFAULT_CHANNEL } from '@/config';
import { WABAccounts, WABAccountsMapped } from '@/channel/bubbleMsgUtils';
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 { useShallow } from 'zustand/react/shallow';
import { copy, isEmpty } from "@haina/utils-commons";
import { copy, isEmpty } from "@/utils/commons";
import { Conditional } from "@/components/Conditional";
import useConversationStore from "@/stores/ConversationStore";
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions, fetchSetRemindStateAction, remindStatusOptionsMapped } from "@/stores/OrderStore";
@ -44,10 +44,10 @@ const CustomerProfile = ({ disabled }) => {
const [chatOrder, setChatOrder] = useState(currentOrder);
const [orderRemindState, setOrderRemindState] = useState(orderDetail.remindstate);
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions);
const orderLabelOptions = copy(OrderLabelDefaultOptions);
orderLabelOptions.unshift({ value: 0, label: "未设置", disabled: true });
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions);
const orderStatusOptions = copy(OrderStatusDefaultOptions);
const getHistoryOrder = (email, whatsappid='') => {
return fetchHistoryOrder(loginUser.userId, email, whatsappid)

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

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

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

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

@ -1,5 +1,5 @@
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 { App as AntApp, Col, ConfigProvider, Empty, Layout, Row, Typography, theme } from 'antd'
import { NavLink, Outlet } from 'react-router-dom'

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

@ -1,6 +1,6 @@
import { Empty, Skeleton, Divider, Flex, Button } from 'antd'
import { Conditional } from '@/components/Conditional'
import { isNotEmpty } from '@haina/utils-commons'
import { isNotEmpty } from '@/utils/commons'
const HtmlPreview = (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 { usingStorage } from '@/utils/usingStorage';
import { isEmpty } from '@haina/utils-commons'
import { isEmpty } from '@/utils/commons'
import { WAI_SERVER_KEY } from '@/config';
import WAIQRCode from './WAIQRCode';
@ -88,14 +88,25 @@ function Profile() {
value: '+639454682947',
label: 'GH 客运(+639454682947)',
},
{
value: '+85265210895',
label: 'GH 客运 HK(+85265210895)',
},
{
value: '+8618174165365',
label: '国际事业部(+8618174165365)',
},
{
value: 'GH 客服',
label: 'GH 客服(无)',
disabled: true,
},
{
value: 'CT 事业部',
label: 'CT 事业部(无)',
disabled: true,
},
{
value: '花梨鹰事业部',
label: '花梨鹰事业部(无)',
disabled: true,
},
]}
/>
</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'
function SnippetList() {
const { message, notification } = App.useApp()
const [messageApi, contextHolder] = message.useMessage()
const [searchform] = Form.useForm()
const [snippetForm] = Form.useForm()
@ -32,6 +32,7 @@ function SnippetList() {
const [loginUser] = useAuthStore((state) => [state.loginUser])
const { notification } = App.useApp()
const [isSnippetModalOpen, setSnippetModalOpen] = useState(false)
const [isHtmlLoading, setHtmlLoading] = useState(false)
@ -235,13 +236,14 @@ function SnippetList() {
value={currentSnippet.content}
loading={isHtmlLoading}
onEdit={() => handelSnippetEdit()}
onCopied={() => message.success('已复制😀')}
onCopied={() => messageApi.success('已复制')}
onDelete={() => handelSnippetDelete()}
/>
</Col>
<div></div>
</Row>
</Space>
{contextHolder}
</>
)
}

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

@ -4,7 +4,6 @@ import dayjs from 'dayjs'
import { ReadIcon, DeliverIcon, SentIcon, WaitingIcon, FailedIcon } from '@/components/Icons'
import { MessageTwoTone } from '@ant-design/icons'
import useCustomerRelationStore from '@/stores/CustomerRelationStore'
import CountryInfo2 from '@/assets/CountryInfo2.json'
const { RangePicker } = DatePicker
const { Option } = Select
@ -19,12 +18,6 @@ const statusIconMap = {
default: <WaitingIcon title='waiting' />,
}
// ID
const countryMap = CountryInfo2.reduce((map, item) => {
map[item.COI2_COI_SN] = item.COI2_Country;
return map;
}, {});
const Index = () => {
const { loading, tasksList, fetchSearchTasks } = useCustomerRelationStore()
@ -149,9 +142,9 @@ const Index = () => {
const columns = [
{ 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_coi2_country', key: 'crt_coi2_country' },
{ title: '国籍', dataIndex: 'crt_mei_country', key: 'crt_mei_country',render: (sn) => countryMap[sn] || sn },
{ title: '附加信息', dataIndex: 'crt_add_info', key: 'crt_add_info' },
{ title: '国家', dataIndex: 'crt_coi2_country', key: 'crt_coi2_country' },
// { title: '', dataIndex: 'ct_coi_code', key: 'ct_coi_code' },
{
title: '状态',
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_template', key: 'crt_template' },
{ title: 'WhatsApp', dataIndex: 'crt_whatsapp', key: 'crt_whatsapp' },
{
title: '会话',
@ -177,7 +169,7 @@ const Index = () => {
if (text) {
const icon = statusIconMap[record.msg_status] || statusIconMap['default']
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='已回复' /> : ''}
</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 * as dd from 'dingtalk-jsapi'
import useAuthStore from '@/stores/AuthStore'
//
// https://open.dingtalk.com/document/orgapp/jsapi-request-auth-code
function AuthCode() {
const loginByJSAuth = useAuthStore((state) => state.loginByJSAuth)
const loginStatus = useAuthStore((state) => state.loginStatus)
const [result, setResult] = useState('')
const [clientValue, setClientValue] = useState('dingl3jyntkazyg4coxf')
const handleRequest = () => {
@ -26,10 +23,6 @@ function AuthCode() {
})
}
const handleLogin = () => {
loginByJSAuth(result)
}
useEffect(() => {
const dingTalkPlatForm = dd.env.platform
setResult(dingTalkPlatForm)
@ -42,10 +35,8 @@ function AuthCode() {
title={clientValue}
subTitle={result}
/>
<Typography.Text>Login: {loginStatus}</Typography.Text>
<Input value={clientValue} onChange={e => setClientValue(e.currentTarget.value)} />
<Button type='primary' onClick={() => handleRequest()}>请求</Button>
<Button onClick={() => handleLogin()}>登录</Button>
</Flex>
)
}

@ -1,5 +1,5 @@
import useAuthStore from '@/stores/AuthStore'
import { isNotEmpty } from '@haina/utils-commons'
import { isNotEmpty } from '@/utils/commons'
import { Flex, Result, Spin } from 'antd'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
@ -13,7 +13,6 @@ function Callback() {
const login = useAuthStore((state) => state.login)
const loginStatus = useAuthStore((state) => state.loginStatus)
const loginByJSAuth = useAuthStore((state) => state.loginByJSAuth)
const urlSearch = new URLSearchParams(location.search)
const authCode = urlSearch.get('authCode')
@ -22,10 +21,10 @@ function Callback() {
const originUrl = urlSearch.get('origin_url')
useEffect (() => {
if (state === 'global-saels' && isNotEmpty(authCode)) {
if (isNotEmpty(authCode) && state === 'global-saels') {
login(authCode)
} else if (state === 'jsapi-auth' && isNotEmpty(authCode)) {
loginByJSAuth(authCode)
} else if (isNotEmpty(authCode) && state === 'jsapi-auth') {
// loginByJSAuth()
} else {
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 { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { isNotEmpty } from '@haina/utils-commons'
import { isNotEmpty } from '@/utils/commons'
import * as dd from 'dingtalk-jsapi'
//
@ -32,25 +32,25 @@ function Login() {
</Flex>
)
}
if (dingTalkPlatForm === 'notInDingTalk') {
window.location = redirectUrl
} else {
dd.requestAuthCode({
clientId: 'dingl3jyntkazyg4coxf',
corpId: 'ding48bce8fd3957c96b',
success: (res) => {
const { code } = res
navigate('/p/dingding/callback?origin_url='+originUrl+'&state=jsapi-auth&authCode=' + code, {
replace: true,
})
},
fail: (error) => {
setErrorMsg(JSON.stringify(error))
},
complete: () => {},
})
}
window.location = redirectUrl
// if (dingTalkPlatForm === 'notInDingTalk') {
// window.location = redirectUrl
// } else {
// dd.requestAuthCode({
// clientId: 'dingl3jyntkazyg4coxf',
// corpId: 'ding48bce8fd3957c96b',
// success: (res) => {
// const { code } = res
// navigate('/p/dingding/callback?state=jsapi-auth&authCode=' + code, {
// replace: true,
// })
// },
// fail: (error) => {
// setErrorMsg(JSON.stringify(error))
// },
// complete: () => {},
// })
// }
return (
<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 MessagesWrapper from '@/views/Conversations/Online/MessagesWrapper';
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 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' }}>
<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 />
<Button type='text' icon={<MenuUnfoldOutlined />} onClick={() => navigate('/m/order')} className=' rounded-none rounded-r' />
</Header>

@ -1,5 +1,5 @@
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 dayjs from 'dayjs';
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: '全部' });
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions);
const orderStatusOptions = copy(OrderStatusDefaultOptions);
orderStatusOptions.unshift({ value: '', label: '全部' });
const remindStateOptions = structuredClone(RemindStateDefaultOptions);
const remindStateOptions = copy(RemindStateDefaultOptions);
remindStateOptions.unshift({ value: '', label: '全部' });
const [form] = Form.useForm();

@ -1,14 +1,13 @@
import useAuthStore from '@/stores/AuthStore'
import { pick } from '@haina/utils-commons'
import { pick } from '@/utils/commons'
import { UnorderedListOutlined, LeftOutlined } from '@ant-design/icons'
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 OrderProfile from '@/components/OrderProfile'
import Mailbox from './components/MailBox'
import Mailbox from './components/Mailbox'
import useConversationStore from '@/stores/ConversationStore';
import { MailboxDirIcon } from './components/MailboxDirIcon'
import { useVisibilityState } from '@/hooks/useVisibilityState'
const deptMap = new Map([
['1', 'CH'], // CH
@ -45,9 +44,7 @@ const deptMap = new Map([
function Follow() {
const [collapsed, setCollapsed] = useState(false)
const mailboxTreeRef = useRef(null);
const [treeHeight, setTreeHeight] = useState(500);
const [collapsed, setCollapsed] = useState(true)
const [loginUser, isPermitted] = useAuthStore((state) => [state.loginUser, state.isPermitted])
const { accountList } = loginUser
@ -126,35 +123,10 @@ function Follow() {
useEffect(() => {
const first = currentMailboxDEI || accountDEI[0].value
const opi = accountListDEIMapped[first].OPI_SN
setExpandTree([...[`${opi}-today`, `${opi}-todo`, `search-orders` ]])
setExpandTree(prev => [...[`${opi}-today`, `${opi}-todo`, `search-orders`, mailboxActiveNode?.VParent ]])
return () => {}
}, [currentMailboxDEI ])
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]);
}, [currentMailboxDEI, mailboxNestedDirsActive, mailboxActiveNode])
return (
<>
@ -162,14 +134,13 @@ function Follow() {
<Layout.Sider width='300' theme='light' style={{ height: 'calc(100vh - 166px)' }} className=' relative'>
<Flex justify='start' align='start' vertical className='h-full'>
<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
className='[&_.ant-typography-ellipsis]:max-w-44 [&_.ant-typography-ellipsis]:min-w-36'
key='sticky-today'
blockNode
showIcon
showLine
height={treeHeight}
autoExpandParent={true}
expandAction={'doubleClick'}
onSelect={handleTreeSelectGetMails}
@ -182,7 +153,7 @@ function Follow() {
titleRender={(node) => (
<Typography.Text ellipsis={{ tooltip: node.title }} className={`${node?._raw?.IsSuccess === 1 ? 'text-primary' : ''}`}>
{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>
)}
/>
@ -204,7 +175,7 @@ function Follow() {
<Button
icon={collapsed ? <LeftOutlined /> : <UnorderedListOutlined />}
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'}
/>
</Tooltip>

@ -1,20 +1,48 @@
import { useEffect, useState } from 'react'
import { ReloadOutlined, RightOutlined, LeftOutlined, MailOutlined, DeleteOutlined, CloseCircleOutlined } from '@ant-design/icons'
import { Flex, Button, Tooltip, List, Checkbox, Space, Breadcrumb, Skeleton } from 'antd'
import { ReloadOutlined, ReadOutlined, RightOutlined, LeftOutlined, SearchOutlined, MailOutlined, DeleteOutlined } from '@ant-design/icons'
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 { isEmpty } from '@haina/utils-commons'
import { isEmpty } from '@/utils/commons'
import { MailboxDirIcon } from './MailboxDirIcon'
import { AttachmentIcon, MailCheckIcon } from '@/components/Icons'
import { AttachmentIcon, MailCheckIcon, MailOpenIcon } from '@/components/Icons'
import NewEmailButton from './NewEmailButton'
import MailOrderSearchModal from './MailOrderSearchModal'
import MailListSearchModal from './MailListSearchModal'
const { RangePicker } = DatePicker
const PAGE_SIZE = 50 //
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 { mailList, loading, error, tempBreadcrumb, refresh, markAsUnread, markAsProcessed, markAsDeleted, } = useEmailList(mailboxDir)
const { mailList, loading, error, refresh, markAsRead, markAsProcessed, markAsDeleted } = useEmailList(mailboxDir)
const [pagination, setPagination] = useState({
current: 1,
@ -41,96 +69,7 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
const getPagedData = (data, currentPage) => {
const startIndex = (currentPage - 1) * PAGE_SIZE
const endIndex = Math.min(startIndex + PAGE_SIZE, data.length)
const slicedData = 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
return data.slice(startIndex, endIndex)
}
const prePage = () => {
@ -141,7 +80,6 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
current: newCurrent,
pagedList: getPagedData(mailList, newCurrent),
}))
setSelectedItems([]);
}
}
@ -153,28 +91,25 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
current: newCurrent,
pagedList: getPagedData(mailList, newCurrent),
}))
setSelectedItems([]);
}
}
const mailItemRender = (item) => {
const isOrderNode = mailboxDir.COLI_SN > 0
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 orderMailType = item.MAT_Name ? <span className='text-neutral-600 text-xs'>{item.MAT_Name}</span> : ''
const folderName = isOrderNode ? `[${item.FDir}]` : ''
const orderMailType = <span className='text-blue-400 text-xs'>{item.MAT_Name}</span>
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
return (
<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' : ''}`}>
<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' : ''}`}>
<div className=''>
<Checkbox
checked={selectedItems.some((i) => i.MAI_SN === item.MAI_SN)}
onClick={(e) => {
const isChecked = e.target.checked
const noCurrent = selectedItems.filter((i) => i.MAI_SN !== item.MAI_SN);
const updatedSelection = isChecked ? [...noCurrent, item] : noCurrent;
const updatedSelection = isChecked ? [...selectedItems, item] : selectedItems.filter((item) => item.MAI_SN !== item.MAI_SN)
setSelectedItems(updatedSelection)
}}></Checkbox>
</div>
@ -185,15 +120,11 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
}}>
<Flex gap='small' vertical={true} justify='space-between' className='cursor-pointer'>
<div>
{folderName}{orderNumber}
{orderNumber}
<span className={mailStateClass}>{item.MAI_Subject || '[无主题]'}</span>
{hasAtta}
</div>
<Flex gap='small' align='center' justify='flex-end' wrap className='text-neutral-500 text-wrap break-words break-all '>
<span className='mr-auto'>{countryName + item.SenderReceiver}</span>
{orderMailType}
<span className=''>{item.SRDate}</span>
</Flex>
<span className='text-neutral-500 text-wrap break-words break-all '>{countryName + item.SenderReceiver + ' ' + item.SRDate}</span>
</Flex>
</div>
</li>
@ -201,17 +132,17 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
}
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'>
<Flex wrap gap='middle' justify={'center'} className='min-w-30 px-1'>
<Tooltip title='全选'>
<Checkbox
indeterminate={selectedItems.length > 0 && selectedItems.length < Math.min(pagination.current * PAGE_SIZE, (pagination.total - ((pagination.current - 1) * PAGE_SIZE)))}
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))}
indeterminate={selectedItems.length > 0 && selectedItems.length < pagination.pagedList.length}
checked={pagination.pagedList.length === 0 ? false : pagination.pagedList.every((item) => selectedItems.some((selected) => selected.MAI_SN === item.MAI_SN))}
onChange={(e) => {
const isChecked = e.target.checked
if (isChecked) {
setSelectedItems(pagination.pagedList.reduce((a, item) => a.concat(item.data), []))
setSelectedItems((prev) => [...prev, ...pagination.pagedList])
} else {
setSelectedItems([])
}
@ -222,93 +153,113 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
</Tooltip>
</Flex>
<Flex wrap gap={8} >
<NewEmailButton />
<Button
size='small'
icon={<MailOutlined />}
onClick={() => {
markAsUnread(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}>
未读
</Button>
<Button
size='small'
icon={<MailCheckIcon />}
onClick={() => {
<Flex wrap gap={8}>
<NewEmailButton />
<Button
size='small'
icon={<MailOpenIcon />}
onClick={() => {
markAsRead(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}
>已读</Button>
<Button
size='small'
icon={<MailOutlined />}
onClick={() => {
console.info('未读未实现')
}}
>未读</Button>
<Button
size='small'
icon={<MailCheckIcon />}
onClick={() => {
markAsProcessed(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}>
已处理
</Button>
<Button
size='small' // danger
icon={<DeleteOutlined />}
onClick={() => {
}}
>已处理</Button>
<Button
size='small' // danger
icon={<DeleteOutlined />}
onClick={() => {
markAsDeleted(selectedItems.map((item) => item.MAI_SN)).then(() => setSelectedItems([]))
}}>
删除
</Button>
<MailOrderSearchModal />
<MailListSearchModal />
}}
>删除</Button>
<MailOrderSearchModal />
</Flex>
</div>
<Flex align='center' justify='space-between' wrap className='px-1 border-0 border-b border-solid border-neutral-200'>
<Breadcrumb
items={(tempBreadcrumb || props.breadcrumb).map((bc) => {
return {
title: (
<>
<MailboxDirIcon type={bc?.iconIndex} />
<span>{bc.title}</span>
</>
),
}
})}
/>
{tempBreadcrumb && (<Button type='text' icon={<CloseCircleOutlined />} onClick={() => refresh()} />)}
<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>
<div className='bg-white h-auto p-1 flex gap-1 items-center hidden'>
<Form
form={form}
initialValues={{}}
// onFinish={handleSubmit}
>
<Row justify='start' gutter={16}>
<Col span={10}>
<Form.Item label='订单号' name='orderNumber'>
<Input placeholder='订单号' allowClear />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='日期' name='confirmDateRange'>
<RangePicker allowClear={true} inputReadOnly={true} presets={DATE_RANGE_PRESETS} />
</Form.Item>
</Col>
<Col span={1} offset={1}>
<Button type='primary' htmlType='submit'>
搜索
</Button>
</Col>
</Row>
</Form>
</div>
<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}>
<Space direction='vertical' size='middle' style={{ display: 'flex' }}>
{pagination.pagedList.map(item => {
return (
<List
key={item.title}
loading={loading}
className='flex flex-col h-full [&_.ant-list-items]:overflow-auto'
header={item.title}
itemLayout='vertical'
pagination={false}
dataSource={item.data}
renderItem={mailItemRender}
/>
)
})}
</Space>
<List
loading={loading}
header={
<Flex align='center' justify='space-between' wrap >
<Breadcrumb
items={props.breadcrumb.map((bc) => {
return {
title: (
<>
<MailboxDirIcon type={bc?.iconIndex} />
<span>{bc.title}</span>
</>
),
}
})}
/>
<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>
</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 { SearchOutlined } from '@ant-design/icons'
import { Button, Modal, Form, Input, Checkbox, Radio, DatePicker, Divider, Typography, Flex } from 'antd'
import { createContext, useEffect, useState } from 'react'
import { ReloadOutlined, ReadOutlined, RightOutlined, LeftOutlined, SearchOutlined, MailOutlined } from '@ant-design/icons'
import { Button, Modal, Form, Input, Checkbox, Select, Radio, DatePicker, Divider, Typography, Flex } from 'antd'
import dayjs from 'dayjs'
import { getEmailDirAction, queryHTOrderListAction, } from '@/actions/EmailActions'
import { isEmpty, objectMapper, pick } from '@haina/utils-commons'
import { getEmailDirAction, queryHTOrderListAction, queryInMailboxAction } from '@/actions/EmailActions'
import { isEmpty, objectMapper, pick } from '@/utils/commons'
import useConversationStore from '@/stores/ConversationStore'
import { mailboxSystemDirs } from '@/hooks/useEmail'
const MailOrderSearchModal = ({ ...props }) => {
const [currentMailboxOPI] = useConversationStore((state) => [state.currentMailboxOPI])
@ -32,14 +33,12 @@ const MailOrderSearchModal = ({ ...props }) => {
const { coli_id, sourcetype, ...mailboxParams } = valuesToSub
result = await getEmailDirAction({ ...mailboxParams, opi_sn: currentMailboxOPI }, false)
updateCurrentMailboxNestedDirs(result[`${currentMailboxOPI}`])
setMailboxActiveNode({expand:true, key: -1, title: '1月', iconIndex: 1, _raw: { VKey: -1, COLI_SN: 0, IsTrue: 0 }})
} else {
const htOrderParams = pick(valuesToSub, ['coli_id', 'sourcetype'])
result = await queryHTOrderListAction({ ...htOrderParams, opi_sn: currentMailboxOPI })
const addToTree = {
expand:true,
key: 'search-orders',
title: '查找订单',
title: '搜索结果',
iconIndex: 'search',
_raw: { COLI_SN: 0, IsTrue: 0 },
children: result.map((o) => ({
@ -47,7 +46,7 @@ const MailOrderSearchModal = ({ ...props }) => {
title: `${o.COLI_ID}`,
iconIndex: 13,
parent: 'search-orders',
parentTitle: '查找订单',
parentTitle: '搜索结果',
parentIconIndex: 'search',
_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 (
<>
<Button key={'bound'} onClick={() => setOpen(true)} size='small' icon={<SearchOutlined className='' />}>
查找订单
查找
</Button>
<Modal
width={window.innerWidth < 700 ? '95%' : 960}

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

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

@ -39,5 +39,3 @@ baileys_store_multi.json
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