Compare commits

...

146 Commits
v1.5.6 ... main

Author SHA1 Message Date
Lei OT 517d2deed2 1.6.7 2 days ago
Lei OT 7193b62821 perf: 获取更多模板 2 days ago
LiaoYijun 98aec9b037 1.6.6 1 month ago
Lei OT a3ef048865 + 客运新的商业号 1 month ago
LiaoYijun ea00e7d2fd 1.6.5 1 month ago
LiaoYijun c600958384 feat: 增加客运香港号码 1 month ago
LiaoYijun 576532ebe6 1.6.4 1 month ago
LiaoYijun 7769929d00 fix:马币不支持跨境付款 1 month ago
Lei OT f1eb47c17f 1.6.3 2 months ago
Lei OT a9c4c71921 perf: 模板显示 2 months ago
Lei OT 4afa544641 邮件发送: actionID 2 months ago
Lei OT 78f10715e7 1.6.2 3 months ago
Lei OT 144abacf90 perf: 模板提示 3 months ago
Lei OT 96a050afa9 perf: 会话页面: 短链接按钮 3 months ago
Lei OT 552bcf8356 1.6.1 3 months ago
Lei OT aceeb135c2 fix: 移动端: 钉钉免登授权后跳转 3 months ago
Lei OT 3777fabe71 perf: 移动端 3 months ago
Lei OT b260909210 perf: 优化提示 3 months ago
Lei OT 406f96e8e1 Merge remote-tracking branch 'origin/main' 3 months ago
ybc 226ea99ed8 优化短链接转换2 3 months ago
Lei OT ef69a4764c 删除移动端的语音通话入口 3 months ago
Lei OT c45e8f09cb perf: 消息模板 3 months ago
Lei OT 89e6560b15 Merge remote-tracking branch 'origin/main' 3 months ago
ybc 4746f8c4eb 优化短链接转换 3 months ago
Lei OT a21856e9d2 1.6.0 3 months ago
ybc cf98a38559 继续改进短链接转换2 3 months ago
ybc cb09c9d819 Merge branch 'main' of github.com:hainatravel-it/global-sales 3 months ago
ybc a7509400f6 继续改进短链接转换 3 months ago
Lei OT 5512701f8b perf: 消息模板排序 3 months ago
Lei OT 02ef6b4d86 Merge remote-tracking branch 'origin/main' 3 months ago
Lei OT ad93c35912 perf: 模板消息参数替换; 客运模板排序 3 months ago
ybc e947c78a73 改进短链接转换 3 months ago
ybc 0c43d84631 短链接转换 3 months ago
LiaoYijun db0cf324ba perf: 废弃模块增加 Warning 3 months ago
Lei OT 07f9b0a19d WAI: 正式的 haina-npm 地址 3 months ago
Lei OT d943af09f7 正式的 haina-npm 地址 3 months ago
Lei OT deb33d43c6 refactor(前端): `copy` --> structuredClone` 4 months ago
Lei OT 27d12a765d refactor(前端): `@haina/utils-request` 4 months ago
Lei OT a88f861b13 refactor(前端): `@haina/utils-commons` 4 months ago
Lei OT d720795ec4 refactor(前端): `@haina/utils-pagespy` 4 months ago
Lei OT 2bb45fb16a refactor(WAI): `urils/commons` --> `@haina/utils-commons` 4 months ago
LiaoYijun 65cf370a5d 1.5.27 4 months ago
LiaoYijun 424bfb5c63 perf: paypal增加马币币种 4 months ago
LiaoYijun 17a57fc6a4 doc: baileys 6.7.21 4 months ago
LiaoYijun e4c33adbc9 perf: 解决 WA 过期,升级 baileys 6.7.21 4 months ago
Lei OT b8d7597004 perf(前端): 多媒体文件不要loading 5 months ago
LiaoYijun 3912e93428 1.5.26 5 months ago
Lei OT 6d74c8f99c style: 邮件目录: 待发邮件 6 months ago
Lei OT 9c0ac172df fix: 订单列表: 全选 6 months ago
Lei OT 846725d7aa Merge remote-tracking branch 'origin/main' 6 months ago
Lei OT 278e420483 perf(WAI): update事件的 from to 不要覆盖 6 months ago
LiaoYijun 97f47c75c4 1.5.25 6 months ago
Lei OT 8503e85b72 perf(WAI): update事件的 from to 不要覆盖 6 months ago
Lei OT a88ea053e0 perf(WAI): update事件的 from to 不要覆盖 6 months ago
Lei OT 49f7186167 perf: 邮件列表: 订单节点: 显示邮件文件夹 6 months ago
Lei OT 06aee58133 perf(WAI): 获取已保存的`from` `to` 6 months ago
LiaoYijun 0eebc7bfb3 perf: 支付链接默认使用Highlights Travel账号 6 months ago
LiaoYijun 0438be50f6 1.5.24 6 months ago
LiaoYijun a3a831f668 feat:收件箱按时间分组 6 months ago
LiaoYijun a67fd186c1 1.5.23 6 months ago
LiaoYijun e2b06c729c perf:未读邮件变色、加深 6 months ago
Lei OT ec20df7dc4 perf(WAI): 引用为空的不发送context对象 6 months ago
LiaoYijun 5c1147f57e 🎨refactor: 删除控制台调试信息 7 months ago
LiaoYijun 65d5758148 perf:删除 makeInMemoryStore,解决发群消息 7 months ago
LiaoYijun 704110ac70 1.5.22 7 months ago
LiaoYijun ed19f2ff3d perf: baileys⬆️6.7.19 7 months ago
Lei OT 5317703635 fix: WAI 收到文件类型消息, 预览 7 months ago
LiaoYijun 127c193257 feat: Caching Group Metadata 7 months ago
Lei OT c151444856 fix: 客户运营: 跳转会话获取会话失败 7 months ago
Lei OT 52897120f2 Merge remote-tracking branch 'origin/main' 7 months ago
Lei OT 0a99743758 perf(WAI): document 消息的webhook数据 7 months ago
LiaoYijun b109b51e01 🐛fix: document 消息增加 filename 7 months ago
Lei OT 408b9666cf perf(WAI): document 消息的webhook数据 7 months ago
LiaoYijun f5a0a58a19 feat:增加documnetMessagee、audioMessage消息解析 7 months ago
LiaoYijun aa1ef98b8f perf:官方最新版本号:1027426813 7 months ago
LiaoYijun fc7afb8f29 feat: 增加文档消息解析 7 months ago
Lei OT 12feaa7780 perf(WAI): 解析有引用的消息 7 months ago
Lei OT 44b9d930dc perf(WAI): 消息状态异步返回; 补全字段 7 months ago
LiaoYijun b97c8e449c perf:WAI 拷贝前清理目录 7 months ago
LiaoYijun a0ea74f81f 🎨refactor:WhatsApp 使用 macOS('SAFARI') 7 months ago
LiaoYijun aed3a7bf8c 🎨refactor:messageUpdate 统一创建 Message 7 months ago
Lei OT f51452c919 feat(WAI): WhatsApp离线接口 offline, 调用实例方法 7 months ago
Lei OT b7a1dffe96 perf(WAI): 消息状态异步返回; 7 months ago
LiaoYijun b1694270e4 feat: WA 增加editedMessage、reactionMessage消息解析 7 months ago
LiaoYijun a87433f045 1.5.21 7 months ago
LiaoYijun 9d396c5eb0 📝docs: 更新 Bailys 参考文档 7 months ago
Lei OT 5873cc947e fix: 邮件详情: iframe 内的链接, 新页面打开, 允许不继承iframe的沙盒限制 7 months ago
Lei OT 79a5187cd2 Merge remote-tracking branch 'origin/main' 7 months ago
LiaoYijun b58c84daa4 🐛fix:金额必填 7 months ago
Lei OT d1124f43bf perf(WAI): 消息状态异步返回; 合并保存所有事件的原文 7 months ago
LiaoYijun c40c742994 feat:个人WhasApp服务端NodeJS打包脚本 7 months ago
LiaoYijun a1c659dfb3 1.5.20 7 months ago
LiaoYijun 05e2a3a473 Revert "feat: 订单信息增加复制订单号"
This reverts commit 89431e9ec3.
7 months ago
LiaoYijun 89431e9ec3 feat: 订单信息增加复制订单号 7 months ago
LiaoYijun 56369cf856 feat:订单信息增加复制订单号 7 months ago
LiaoYijun 3dc5ab72da feat:完成商业号模板消息解析 7 months ago
LiaoYijun 1525af2415 feat:完成商业号模板消息解释 7 months ago
LiaoYijun e3fc505d41 feat:增加 WhatsApp Baileys 本地调试 7 months ago
LiaoYijun 21b86f656a 1.5.19 7 months ago
LiaoYijun f767619f5b 1.5.18 7 months ago
Lei OT a16dc962a8 Merge remote-tracking branch 'origin/main' 7 months ago
Lei OT 7879387fbc fix: indexDB 更新表 7 months ago
LiaoYijun dead850380 1.5.17 7 months ago
Lei OT 7920a5a939 fix: indexDB 消息表 7 months ago
LiaoYijun d83c1ad3fd 1.5.16 8 months ago
LiaoYijun e35c699751 🐛fix:催信状态初始值 8 months ago
LiaoYijun 98f0b22e1e perf: 催信状态使用 zustand、action 管理 8 months ago
LiaoYijun 02f573eae7 🐛fix:remindStatusOptions.SecondRemind 8 months ago
LiaoYijun 81ab201706 🐛🐛fix: props.SecondRemind 8 months ago
LiaoYijun 3b6907aa6f 🐛fix: SecondRemind 8 months ago
LiaoYijun f6c8fc1df7 🎨refactor:修改二催字段名 8 months ago
LiaoYijun 354ec5337f 🎨refactor: 催信支持多选设置 8 months ago
LiaoYijun 2f51a1c40a 1.5.15 8 months ago
LiaoYijun 5a0ec3c66b 🐛fix: 参数名错误 8 months ago
LiaoYijun 1a3d0add75 1.5.14 8 months ago
LiaoYijun a169a81eef 🐛fix:loginByJSAuth error 8 months ago
LiaoYijun 3f1cedd64e 1.5.13 8 months ago
LiaoYijun de96b0a8d1 feat:移动端使用免登授权码 8 months ago
Lei OT b691b584a5 # 8 months ago
Lei OT afa424fc05 fix: 判断设备是否支持 `requestIdleCallback` 8 months ago
LiaoYijun 9b1588b70c 1.5.12 8 months ago
Lei OT 01a0569bca Merge remote-tracking branch 'origin/main' 8 months ago
Lei OT 9ac4df74a6 fix: 偶发的indexedDB 未创建 8 months ago
LiaoYijun 25f59d613d feat: 完成钉钉免登测试代码,验证通过后再正式使用 8 months ago
LiaoYijun 12c45471fe 1.5.11 8 months ago
LiaoYijun 2111373630 🔒build:Vite Git Head 8 months ago
Lei OT a5684dd418 perf: 编辑器: 行间距 8 months ago
Ycc 4fd740dc6a 显示国籍中文和显示额外信息
修复引用大小写问题
8 months ago
LiaoYijun 228b6e4aec perf: 使用 App.messge 替换 messageApi 8 months ago
LiaoYijun 4c3b0fe420 perf: 使用 App.messge 替换 messageApi 8 months ago
LiaoYijun c5f959d043 fix:useMessage() error 8 months ago
LiaoYijun 8aa8434be3 perf:增加常量,删除无效代码 8 months ago
LiaoYijun d0ed4adaa0 feat: 把幽灵依赖转为显式,以便使用 PNPM 8 months ago
Lei OT 09f422339e 1.5.10 8 months ago
Lei OT fdd876db40 fix: 附件地址 8 months ago
Lei OT 561c25e8a1 perf: 更新催信模板 8 months ago
LiaoYijun 29f29bcf38 perf: 增加等出 WA 实例 8 months ago
LiaoYijun 3ee4230af8 feat:WA 实例增加 stop 方法 8 months ago
LiaoYijun cebabfcc92 1.5.9 8 months ago
Lei OT 0077f1f210 fix: 在线聊天: 加载更多消息记录 8 months ago
Lei OT 8c89ef3e80 perf: WhatsApp消息 支持加粗, 斜体 8 months ago
LiaoYijun be84e99d9e 1.5.8 8 months ago
LiaoYijun 73167db47a perf: 订单跟踪、在线聊天默认展开订单信息 8 months ago
LiaoYijun 67694da2ce 1.5.7 8 months ago
LiaoYijun 8557402e53 feat: 上传日志前可输入描述文字 8 months ago
LiaoYijun 0dac008996 fix:在线聊天显示关联订单 9 months ago

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,13 +29,14 @@ schema*
*.sw?
/package-lock.json
**/LexicalEditor0
*.zip
.env.*
vonage-client*
**/test
*.bak
pnpm-lock.yaml
**/LexicalEditor0
*.zip
.env.*
vonage-client*
**/test
*.bak

@ -1,7 +1,7 @@
{
"name": "global-sales",
"private": true,
"version": "1.5.6",
"version": "1.6.7",
"type": "module",
"scripts": {
"dev": "vite",
@ -10,31 +10,46 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@dckj/react-better-modal": "^0.1.2",
"@lexical/react": "^0.20.0",
"@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",
"@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.20.0",
"lexical": "^0.34.0",
"prismjs": "^1.30.0",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-chat-elements": "^12.0.17",
"react-chat-elements": "^12.0.18",
"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": "^8.45.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.14",
"eslint": "^9.33.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"vite": "^4.5.1",

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 208 B

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

@ -1,9 +1,9 @@
import { groupBy, isNotEmpty, pick, sortArrayByOrder, sortBy } from '@/utils/commons';
import { fetchJSON, postJSON, postForm } from '@/utils/request'
import { groupBy, isNotEmpty, pick, sortArrayByOrder, sortBy } from '@haina/utils-commons';
import { fetchJSON, postJSON, postForm } from '@haina/utils-request'
import { parseRenderMessageList } from '@/channel/bubbleMsgUtils';
import { API_HOST } from '@/config';
import { isEmpty } from '@/utils/commons';
import { isEmpty } from '@haina/utils-commons';
import dayjs from 'dayjs';
/**
@ -11,6 +11,9 @@ 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',
@ -20,9 +23,16 @@ 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'];
@ -37,7 +47,14 @@ 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 crNamesOmit = [
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 = [
'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',
@ -52,8 +69,9 @@ 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 || [])
const canUseTemplates = (data?.result?.items || []).concat(leftItems)
.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,
@ -61,8 +79,16 @@ 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: crNamesOmit.includes(ele.name) ? '客运-' : (crNames.includes(ele.name) || ele.name.includes('by_cr')) ? ele.language + '-客运' : scNames.includes(ele.name) ? ele.language + '-示例' : ele.language,
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),
}))
const top2Name = topName.concat(canUseTemplates.filter(_t => _t.name.startsWith('order_updated')).map(_tem => _tem.name));
@ -72,7 +98,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, ...crNamesOmit ]);
const rawS = sortArrayByOrder(raw, 'name', [...crNames, ...scNames, ...NamesOmit ]);
return [...top, ...secondS, ...rawS];
};
/**

@ -1,6 +1,6 @@
import { fetchJSON, postForm, postJSON } from '@/utils/request';
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 '@/utils/commons';
import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, uniqWith } from '@haina/utils-commons';
import { readIndexDB, writeIndexDB } from '@/utils/indexedDB';
import dayjs from 'dayjs';
import { internalEventEmitter } from '@/utils/EventEmitterService';

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

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
import { cloneDeep, isEmpty, olog, fixTo2Decimals, pick, objectMapper } from "@/utils/commons";
import { cloneDeep, isEmpty, olog, fixTo2Decimals, pick, objectMapper } from "@haina/utils-commons";
import dayjs from "dayjs";
import { v4 as uuid } from "uuid";
@ -58,6 +58,27 @@ 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 }), {})
@ -66,9 +87,8 @@ export const replaceTemplateString = (str, replacements) => {
let keys = str.match(/{{(.*?)}}/g).map(key => key.replace(/{{|}}/g, ''));
for (let i = 0; i < keys.length; i++) {
let replaceValue = replacements[i];
let template = new RegExp(`{{${keys[i]}}}`, 'g');
result = result.replace(template, replaceValue);
const replaceValue = replacements[i];
result = result.replaceAll(`{{${keys[i]}}}`, replaceValue);
}
return result;
@ -119,7 +139,7 @@ const mediaMsg = {
...msg,
actionId: msg.id,
conversationid: msg.id.split('.')[0],
data: { ...msg.data, status: { download: msg.data?.loading ? false : true, click: true, loading: msg.data.loading } },
data: { ...msg.data, status: { download: true, click: true, loading: 0 } },
...(msg.context
? {
reply: {
@ -692,12 +712,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',
@ -826,15 +846,18 @@ 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,
} : {
whatsapp_msg_type: '',
waba: '',
wabaName: '',
}),
} : {}),
...((msg.msg_source) === 'wai' ? {
whatsapp_msg_type: msg.type,
wabaName: '个人号',
} : {}),
};
};
/**
@ -1071,3 +1094,120 @@ 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 '@/utils/commons';
import { isEmpty } from '@haina/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 '@/utils/commons';
import { isEmpty } from '@haina/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 '@/utils/commons';
import { isEmpty } from '@haina/utils-commons';
import {useSettings} from './context/SettingsContext';
import './styles.css';

@ -89,6 +89,19 @@ 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' },
@ -534,6 +547,26 @@ 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) => {
@ -549,26 +582,23 @@ function FontDropDown({ editor, value, style, disabled = false }) {
[editor, style]
);
const buttonAriaLabel = style === 'font-family' ? 'Formatting options for font family' : 'Formatting options for font size';
const buttonIconClassName = FontDropDownMapped[style].IconClassName;
const buttonAriaLabel = FontDropDownMapped[style].buttonAriaLabel;
const dropdownOptions = FontDropDownMapped[style].options;
return (
<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]) => (
<DropDown disabled={disabled} buttonClassName={'toolbar-item ' + style} buttonLabel={value} buttonIconClassName={buttonIconClassName} buttonAriaLabel={buttonAriaLabel}>
{dropdownOptions.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 }) {
@ -673,6 +703,7 @@ 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);
@ -760,6 +791,9 @@ 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
@ -896,6 +930,12 @@ export default function ToolbarPlugin() {
value={fontSize}
editor={editor}
/>
<FontDropDown
disabled={!isEditable}
style={'line-height'}
value={lineSpacing}
editor={editor}
/>
<Divider />
<button type='button'
onClick={() => {

@ -87,6 +87,7 @@
outline: 0;
padding: 15px 10px;
caret-color: #444;
line-height: 1.5;
}
.editor-pure-input {
@ -415,7 +416,7 @@ pre::-webkit-scrollbar-thumb {
margin-right: 2px;
}
.toolbar button.toolbar-item i.format {
.toolbar button.toolbar-item i.format, .toolbar button.toolbar-item .icon2 {
background-size: contain;
display: inline-block;
height: 18px;
@ -889,6 +890,10 @@ 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;

@ -0,0 +1,77 @@
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,18 +9,19 @@ import {
CalendarOutlined,
EditOutlined,
CheckOutlined,
ReloadOutlined,
CopyOutlined
} from '@ant-design/icons'
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { App, Flex, Select, Tooltip, Divider, Typography, Skeleton, Checkbox, Drawer, Button, Form, Input } from 'antd'
import { useOrderStore, fetchSetRemindStateAction, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions } from '@/stores/OrderStore'
import { copy, isEmpty } from '@/utils/commons'
import { App, Flex, Select, Tooltip, Divider, Typography, Skeleton, Checkbox, Drawer, Button, Empty, Form, Input } from 'antd'
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions, remindStatusOptions } from '@/stores/OrderStore'
import { copy, isEmpty } from '@haina/utils-commons'
import { useShallow } from 'zustand/react/shallow'
import useConversationStore from '@/stores/ConversationStore'
import useAuthStore from '@/stores/AuthStore'
const OrderProfile = ({ coliSN, ...props }) => {
const { notification, message } = App.useApp()
const [formComment] = Form.useForm()
const [formWhatsApp] = Form.useForm()
@ -31,11 +32,12 @@ const OrderProfile = ({ coliSN, ...props }) => {
const [openWhatsApp, setOpenWhatsApp] = useState(false)
const [openExtra, setOpenExtra] = useState(false)
const orderLabelOptions = copy(OrderLabelDefaultOptions)
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions)
orderLabelOptions.unshift({ value: 0, label: '未设置', disabled: true })
const orderStatusOptions = copy(OrderStatusDefaultOptions)
const [orderDetail, customerDetail, fetchOrderDetail, setOrderPropValue, appendOrderComment, updateWhatsapp, updateExtraInfo] = useOrderStore((s) => [
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions)
const [orderDetail, customerDetail, fetchOrderDetail, setOrderPropValue,
appendOrderComment, updateWhatsapp, updateExtraInfo, remindCheckList, updateRemindState] = useOrderStore((s) => [
s.orderDetail,
s.customerDetail,
s.fetchOrderDetail,
@ -43,16 +45,14 @@ const OrderProfile = ({ coliSN, ...props }) => {
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,20 +71,12 @@ const OrderProfile = ({ coliSN, ...props }) => {
}, [orderId])
const handleSetRemindState = async (checkedValue) => {
const state = checkedValue.filter((v) => v !== orderRemindState)
const oldState = orderRemindState
try {
if (isEmpty(state)) {
setOrderRemindState(null)
} else {
setOrderRemindState(state[0])
}
await fetchSetRemindStateAction({ coli_sn: coliSN, remindstate: state })
await updateRemindState(coliSN, checkedValue)
message.success('设置成功')
} catch (error) {
notification.warning({ message: '设置失败', description: error.message, placement: 'top', duration: 60 })
setOrderRemindState(oldState)
}
}
@ -97,13 +89,18 @@ const OrderProfile = ({ coliSN, ...props }) => {
return orderDetail.DidPlan === 0 ? '未做计划' : '已做计划'
}
return (
<>
const renderOrderDetail = () => {
return (
<>
<Skeleton active loading={loading}>
<Flex gap='small' vertical={true} justify='space-between' className='p-2'>
<Typography.Text>
<FieldNumberOutlined className='pr-1' />
{orderDetail.order_no}
<CopyOutlined onClick={() => {
navigator.clipboard.writeText(orderDetail.order_no)
message.success('已复制😀')
}}/>
</Typography.Text>
<Typography.Text>
<UserOutlined className=' pr-1' />
@ -205,7 +202,7 @@ const OrderProfile = ({ coliSN, ...props }) => {
<Divider orientation='left'>
<Typography.Text strong>催信</Typography.Text>
</Divider>
<Checkbox.Group key='substatus' className='px-2' value={[orderRemindState]} options={remindStatusOptions} onChange={handleSetRemindState} />
<Checkbox.Group key='substatus' className='px-2' value={remindCheckList} options={remindStatusOptions} onChange={handleSetRemindState} />
<Divider orientation='left'>
<Typography.Text strong>表单信息</Typography.Text>
@ -226,12 +223,8 @@ 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='修改'>
@ -350,7 +343,21 @@ const OrderProfile = ({ coliSN, ...props }) => {
</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 '@/utils/commons';
import { debounce } from '@haina/utils-commons';
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const [fetching, setFetching] = useState(false);

@ -6,7 +6,7 @@
// export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_144'; // prod: Slave
// debug:
// export const API_HOST = 'http://202.103.68.144:8889/v2';
// export const API_HOST = 'http://202.103.68.144:8888/v2';
// export const WS_URL = 'ws://202.103.68.144:8888';
// export const WAI_HOST = 'http://47.83.248.120/api/v1'; // 香港服务器
// export const WAI_HOST = 'http://47.254.53.81/api/v1'; // 美国服务器
@ -38,8 +38,9 @@ export const DEFAULT_WABA = '+8617607730395';
const __BUILD_VERSION__ = `__BUILD_VERSION__`.replace(/"/g, '')
const __BUILD_DATE__ = `__BUILD_DATE__`;
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();
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 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 '@/utils/commons';
import { isEmpty } from '@haina/utils-commons';
const CHAT_ITEM_RECORD = {
"sn": null,

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { isEmpty, objectMapper, olog, } from '@/utils/commons'
import { isEmpty, objectMapper, olog, } from '@haina/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 { App } from 'antd'
@ -373,13 +373,14 @@ const orderMailTypes = new Map([
['48055', 'GH海外PostSurvey'],
])
export const emailTemplates = [
{ type: 'RemindOneWL', index: 1, key: '1@RemindOneWL', value: '1@RemindOneWL', label: '一催模板一,询问客人是否收到报价信' },
{ type: 'RemindOneWL', index: 2, key: '2@RemindOneWL', value: '2@RemindOneWL', label: '一催模板二,询问客人是否修改行程' },
{ 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: 'divider' },
{ type: 'RemindTwoWL', index: 1, key: '1@RemindTwoWL', value: '1@RemindTwoWL', label: '二催模板一,询问客人对行程的看法' },
{ type: 'RemindTwoWL', index: 2, key: '2@RemindTwoWL', value: '2@RemindTwoWL', label: '二催模板二,表达服务的意识' },
{ 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: 'divider' },
{ type: 'RemindThreeWL', index: 1, key: '1@RemindThreeWL', value: '1@RemindThreeWL', label: '三催模板三,强调价格有效期' },
{ type: 'RemindThreeWL', index: 1, key: '1@RemindThreeWL', value: '1@RemindThreeWL', label: '三催模版1再次强调服务' },
{ type: 'RemindThreeWL', index: 2, key: '2@RemindThreeWL', value: '2@RemindThreeWL', label: '三催模版2客人常见问题询问' },
];
export const emailTemplateMap = emailTemplates.reduce((acc, cur) => {
if (cur.type === 'divider') {

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

@ -0,0 +1,124 @@
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,6 +27,7 @@ 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'
@ -35,7 +36,10 @@ 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 =
@ -107,6 +111,7 @@ 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 '@/utils/commons';
import { clearAllCaches } from '@haina/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 '@/utils/request'
import { isEmpty, isNotEmpty } from '@/utils/commons'
import { fetchJSON } from '@haina/utils-request'
import { isEmpty, isNotEmpty } from '@haina/utils-commons'
import { API_HOST, BUILD_VERSION } from '@/config'
import { usingStorage } from '@/utils/usingStorage';
@ -70,9 +70,8 @@ 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('G-STR:WAI_SERVER', waiServer)
setStorage(WAI_SERVER_KEY, waiServer)
set(() => ({
loginUser: {
userId: json.result.opisn,
@ -108,6 +107,67 @@ 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()
@ -158,13 +218,7 @@ 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')
}
@ -207,26 +261,6 @@ 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 "@/utils/request";
import { prepareUrl, isNotEmpty, } from "@/utils/commons";
import { fetchJSON } from "@haina/utils-request";
import { prepareUrl, isNotEmpty, } from "@haina/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 '@/utils/commons';
import { olog, isEmpty, groupBy, sortArrayByOrder, pick, sortKeys, omit, sortObjectsByKeysMap, merge } from '@haina/utils-commons';
import { logWebsocket, clean7DaysWebsocketLog } from '@/utils/indexedDB'
import { receivedMsgTypeMapped, handleNotification } from '@/channel/bubbleMsgUtils';
import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions';

@ -1,8 +1,8 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@/utils/request'
import { fetchJSON, postForm } from '@haina/utils-request'
import { HT3, EMAIL_HOST } from '@/config'
import { isNotEmpty, prepareUrl } from '@/utils/commons'
import { isNotEmpty, prepareUrl } from '@haina/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 '@/utils/commons'
import { buildTree, isEmpty, olog, sortArrayByOrder } from '@haina/utils-commons'
import { readIndexDB, writeIndexDB, createIndexedDBStore, clean7DaysMailboxLog } from '@/utils/indexedDB';
import { internalEventEmitter } from '@/utils/EventEmitterService';

@ -1,8 +1,8 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm, postJSON } from '@/utils/request'
import { fetchJSON, postForm, postJSON } from '@haina/utils-request'
import { API_HOST, API_HOST_V3, EMAIL_HOST } from '@/config'
import { isNotEmpty, prepareUrl, uniqWith } from '@/utils/commons'
import { isEmpty, isNotEmpty, prepareUrl, uniqWith } from '@haina/utils-commons'
const initialState = {
orderList: [],
@ -11,6 +11,7 @@ const initialState = {
lastQuotation: {},
quotationList: [],
otherEmailList: [],
remindCheckList: [],
}
export const useOrderStore = create(
@ -62,17 +63,15 @@ export const useOrderStore = create(
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] : {},
// 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)
@ -80,6 +79,21 @@ export const useOrderStore = create(
})
},
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,
}
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()
@ -280,20 +294,25 @@ export const RemindStateDefaultOptions = [
* @useage 订单信息: 标记状态
*/
export const remindStatusOptions = [
{ value: 1, label: '已发一催' },
{ value: 2, label: '已发二催' },
{ value: 3, label: '已发三催' },
{ value: 'FirstRemind', label: '已发一催' },
{ value: 'SecondRemind', label: '已发二催' },
{ value: 'ThirdRemind', 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 '@/utils/request'
import { fetchJSON, postForm } from '@haina/utils-request'
import { API_HOST } from '@/config'
import { copy } from '@/utils/commons'
import { copy } from '@haina/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 = copy(mapTypeList);
const mapTypeAllList = structuredClone(mapTypeList);
mapTypeAllList.unshift({ value: '', label: '全部' });
set(() => ({
ownerList: json?.result?.owner.map(item => {

@ -0,0 +1,21 @@
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,3 +1,4 @@
console.warn('Warning: `commons.js` is deprecated and will be removed in next version.');
export function copy(obj) {
return JSON.parse(JSON.stringify(obj))
}
@ -188,6 +189,8 @@ 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,4 +1,4 @@
import { isEmpty } from './commons';
import { isEmpty } from '@haina/utils-commons';
/**
*
*/
@ -8,7 +8,7 @@ import { isEmpty } from './commons';
* ! 每次涉及indexedDB的更新都要往上+1
* @type {number}
*/
const INDEXED_DB_VERSION = 5;
const INDEXED_DB_VERSION = 6;
export const logWebsocket = (message, direction) => {
var open = indexedDB.open('LogWebsocketData', INDEXED_DB_VERSION)
open.onupgradeneeded = function () {
@ -107,15 +107,16 @@ export const clearWebsocketLog = () => {
}
}
export const createIndexedDBStore = (tables, database) => {
export const createIndexedDBStore = (tables, database, keySet = {keyPath: 'key' }) => {
var open = indexedDB.open(database, INDEXED_DB_VERSION)
// console.trace('createIndexedDBStore');
open.onupgradeneeded = function () {
// console.log('createIndexedDBStore onupgradeneeded', database, )
var db = open.result
// 数据库是否存在
for (const table of tables) {
if (!db.objectStoreNames.contains(table)) {
var store = db.createObjectStore(table, { keyPath: 'key' })
var store = db.createObjectStore(table, keySet)
store.createIndex('timestamp', 'timestamp', { unique: false })
} else {
const objectStore = open.transaction.objectStore(table)
@ -188,9 +189,9 @@ export const readIndexDB = (keys=null, table, database) => {
var store = db.createObjectStore(table, { keyPath: 'key' })
store.createIndex('timestamp', 'timestamp', { unique: false })
} else {
const logStore = openRequest.transaction.objectStore(table)
if (!logStore.indexNames.contains('timestamp')) {
logStore.createIndex('timestamp', 'timestamp', { unique: false })
const store = openRequest.transaction.objectStore(table)
if (!store.indexNames.contains('timestamp')) {
store.createIndex('timestamp', 'timestamp', { unique: false })
}
}
}
@ -361,6 +362,7 @@ export const deleteIndexDBbyKey = (keys=null, table, database) => {
};
function cleanOldData(database, storeNames=[], dateKey = 'timestamp', keySet = { keyPath: 'key' }) {
createIndexedDBStore(storeNames, database, keySet);
return function (daysToKeep = 7) {
return new Promise((resolve, reject) => {
let deletedCount = 0

@ -1,54 +1,125 @@
import { loadScript } from '@/utils/commons'
import { loadScript } from '@haina/utils-commons'
import { fetchJSON } from '@haina/utils-request'
import { readWebsocketLog } from '@/utils/indexedDB'
import { BUILD_VERSION, BUILD_DATE } from '@/config'
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 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 uploadPageSpyLog = async () => {
await readWebsocketLog()
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (window.$pageSpy) {
await window.$harbor.upload() // 上传日志 { clearCache: true, remark: '' }
}
}
});
};
/**
* @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')
// }
// }

@ -1,6 +1,10 @@
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,41 +6,37 @@ import {
App as AntApp,
ConfigProvider,
Empty,
Modal,
message,
FloatButton,
theme,
Modal, FloatButton,
theme
} from 'antd'
import { AudioOutlined, AudioTwoTone, BugOutlined, CustomerServiceOutlined } from '@ant-design/icons'
import { CustomerServiceOutlined } from '@ant-design/icons'
import zhLocale from 'antd/locale/zh_CN'
import 'dayjs/locale/zh-cn'
import { useEffect } from 'react'
import { Link, NavLink, Outlet, useHref, useNavigate } from 'react-router-dom'
import { appendRequestHeader } from '@/utils/request'
import { loadPageSpy } from '@/utils/pagespy'
import { Outlet, useHref, useNavigate } from 'react-router-dom'
import { appendRequestHeader } from '@haina/utils-request'
import { loadPageSpy } from '@haina/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, sendNotify] = useAuthStore((state) => [
state.loginUser, state.sendNotify
const [loginUser] = useAuthStore((state) => [
state.loginUser
])
const href = useHref()
@ -61,7 +57,7 @@ function AuthApp() {
let _fetchEmailWorker;
if (loginUser.userId > 0) {
appendRequestHeader('X-User-Id', loginUser.userId)
loadPageSpy(loginUser.username)
loadPageSpy(loginUser.username + '(v' + BUILD_VERSION + ')', 'Sales CRM', true)
connectWebsocket(loginUser.userId)
fetchInitialData(loginUser)
@ -90,29 +86,6 @@ 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
@ -161,10 +134,9 @@ function AuthApp() {
icon={<CustomerServiceOutlined />}
>
<EmailFetch />
<FloatButton icon={<BugOutlined />} tooltip={<div>上传日志给研发部</div>} onClick={() => uploadLog()} />
<LogUploader />
<FloatButton.BackTop />
</FloatButton.Group>
{contextHolder}
{needToLogin ? <>login...</> : <Outlet />}
<dialog id="about-dialog" className="border-0">
<img className="logo" src={AppLogo} alt="logo" />
@ -191,6 +163,7 @@ 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 "@/utils/commons";
import { isEmpty } from "@haina/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 '@/utils/commons';
import { flush, pick } from '@haina/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 '@/utils/commons';
import { flush, pick } from '@haina/utils-commons';
import { fetchConversationsSearch, fetchConversationsUnassigned } from '@/actions/ConversationActions';
const { Sider, Content } = Layout;

@ -1,30 +1,36 @@
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 { 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 OrderProfile from '@/components/OrderProfile'
import ReplyWrapper from './Conversations/Online/ReplyWrapper';
import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
import useAuthStore from '@/stores/AuthStore'
import ReplyWrapper from './Conversations/Online/ReplyWrapper'
import useConversationStore from '@/stores/ConversationStore'
import { useShallow } from 'zustand/react/shallow'
import './Conversations/Conversations.css';
import EmailEditorPopup from './Conversations/Online/Input/EmailEditorPopup';
import ConversationBind from '@/views/Conversations/Online/ConversationBind'
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 [collapsedLeft, setCollapsedLeft] = useState(false);
const [collapsedRight, setCollapsedRight] = useState(true);
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 currentOrder = useConversationStore(useShallow(state => state.currentConversation?.coli_sn || ""));
const renderEmptyOrder = () => {
return (
<Empty description={<span>没有订单关联</span>}>
<ConversationBind currentConversationID={currentConversationID} userId={loginUser.userId} onBoundSuccess={(coli_sn) => updateCurrentConversation({ coli_sn })} />
</Empty>
)
}
return (
<>
@ -54,7 +60,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'>
@ -75,12 +81,12 @@ const ChatWindow = () => {
collapsedWidth={0}
trigger={null}
collapsed={collapsedRight}>
<OrderProfile coliSN={currentOrder} />
<OrderProfile coliSN={currentOrder} renderEmpty={renderEmptyOrder} />
</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 '@/utils/commons';
import { calcCacheSizes, clearAllCaches } from '@haina/utils-commons';
const ClearCache = (props) => {
const { message } = App.useApp();

@ -4,12 +4,13 @@ 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 '@/utils/commons';
import { isEmpty, stringToColour, groupBy, isNotEmpty, TagColorStyle } from '@haina/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 }) => {
@ -148,6 +149,27 @@ 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;
@ -157,23 +179,8 @@ 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} ${extraClass} `} key={'msg-text'}>
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} `} key={'msg-text'}>
{headerObj ? (
<div className='text-neutral-500 text-center'>
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
@ -185,17 +192,7 @@ const MessagesList = ({ ...listProps }) => {
)}
</div>
) : null}
{(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;
}
})}
{renderMDTokens(parseSimpleMarkdown(str))}
</span>
);
});

@ -1,6 +1,6 @@
import { ChatItem } from 'react-chat-elements';
import useFormStore from '@/stores/FormStore';
import { isNotEmpty } from '@/utils/commons';
import { isNotEmpty } from '@haina/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 '@/utils/commons';
import { isNotEmpty } from '@haina/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 '@/utils/commons';
import { isEmpty, } from '@haina/utils-commons';
import { useEmailDetail, } from '@/hooks/useEmail';
const BubbleEmail = ({ onOpenEditor, onOpenEmail, ...message }) => {

@ -2,7 +2,8 @@ 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 '@/utils/commons';
import { groupBy, isEmpty, TagColorStyle } from '@haina/utils-commons';
import { parseSimpleMarkdown } from '@/channel/bubbleMsgUtils';
import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
import { ReplyIcon } from '@/components/Icons';
@ -23,6 +24,34 @@ 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)) {
@ -32,26 +61,8 @@ 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 (
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`} key={'msg-text'}>
<div className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className}`} key={'msg-text'}>
{headerObj ? (
<div className='text-neutral-500 text-center'>
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
@ -63,24 +74,7 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
)}
</div>
) : null}
{(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
}
})}
{renderMDTokens(parseSimpleMarkdown(str))}
{footerObj ? <div className=' text-neutral-500'>{footerObj.text}</div> : null}
{buttonsArr && buttonsArr.length > 0 ? (
<div className='flex flex-row gap-1'>
@ -101,7 +95,7 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
)}
</div>
) : null}
</span>
</div>
)
});
return (

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Button, Tag, Radio, Popover, Form, Space, Tooltip } from 'antd';
import { isEmpty, objectMapper, TagColorStyle } from '@/utils/commons';
import { isEmpty, objectMapper, TagColorStyle } from '@haina/utils-commons';
import useConversationStore from '@/stores/ConversationStore';
import { OrderLabelDefaultOptions } from '@/stores/OrderStore';
import { FilterOutlined, FilterTwoTone } from '@ant-design/icons';

@ -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 '@/utils/commons';
import { flush, isEmpty, isNotEmpty, stringToColour, TagColorStyle } from '@haina/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 '@/utils/commons'
import { fetchJSON } from '@/utils/request'
import { isEmpty, cloneDeep } from '@haina/utils-commons'
import { fetchJSON } from '@haina/utils-request'
import AdvanceSearchForm from '../../../orders/AdvanceSearchForm'
import { API_HOST } from '@/config'
import dayjs from 'dayjs'

@ -136,7 +136,7 @@ const EmailContent = ({ id, content: MailContent, className='', ...props }) => {
border: 'none',
display: 'block',
}}
sandbox='allow-scripts allow-same-origin allow-popups'
sandbox='allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms allow-top-navigation'
/>
</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 '@/utils/commons'
import { isEmpty, TagColorStyle } from '@haina/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 '@/utils/commons'
import { isEmpty, TagColorStyle } from '@haina/utils-commons'
import EmailEditorPopup from '../Input/EmailEditorPopup'
import DnDModal from '@/components/DnDModal'
import useStyleStore from '@/stores/StyleStore'
@ -222,7 +222,7 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s
<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'>
<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>

@ -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 '@/utils/commons'
import { debounce, isEmpty } from '@haina/utils-commons'
import useConversationStore from '@/stores/ConversationStore';
const EmailListDrawer = ({ showExpandBtn=true, title, list: otherEmailList, currentConversationID, opi_sn, oid, emailItem: clickItem, onOpenEditor, ...props }) => {

@ -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 '@/utils/commons'
import { isEmpty } from '@haina/utils-commons'
const EmailQuotation = ({ sfi_sn, ...props }) => {
const {notification} = App.useApp()

@ -0,0 +1,14 @@
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 '@/utils/commons';
import { groupBy, isEmpty, TagColorStyle as CalColorStyle } from '@haina/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 '@/utils/commons';
import { fetchJSON } from '@/utils/request';
import { isEmpty, cloneDeep } from '@haina/utils-commons';
import { fetchJSON } from '@haina/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 '@/utils/commons';
import { debounce, flush, isEmpty, isNotEmpty, pick } from '@haina/utils-commons';
import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore';
// import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from "@/stores/OrderStore";
@ -336,13 +336,6 @@ 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 '@/utils/commons';
import { isEmpty, isNotEmpty, pick } from '@haina/utils-commons';
import useConversationStore from '@/stores/ConversationStore';
import { phoneNumberToWAID } from '@/channel/bubbleMsgUtils';
import { useConversationNewItem } from '@/hooks/useConversation';

@ -6,7 +6,8 @@ import InputMediaUpload from './MediaUpload'
import PaymentlinkBtn from './PaymentlinkBtn'
import SnippestBtn from './SnippestBtn'
import useConversationStore from '@/stores/ConversationStore'
import { isEmpty } from '@/utils/commons'
import { isEmpty } from '@haina/utils-commons'
import ShortlinkBtn from './ShortlinkBtn'
const ComposerTools = ({ channel, invokeSendUploadMessage, invokeSendMessage, invokeUploadFileMessage, inputEmoji, ...props }) => {
const websocket = useConversationStore((state) => state.websocket)
@ -25,6 +26,7 @@ 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 '@/utils/commons';
// import { isEmpty, } from '@haina/utils-commons';
import useConversationStore from '@/stores/ConversationStore'
import { useOrderStore } from '@/stores/OrderStore'
import { EditIcon } from '@/components/Icons'
import { cloneDeep, isEmpty } from '@/utils/commons'
import { cloneDeep, isEmpty } from '@haina/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 '@/utils/commons';
import { cloneDeep, debounce, isEmpty, } from '@haina/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 '@/utils/commons';
import { isEmpty, } from '@haina/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 '@/utils/commons';
import { isEmpty, sanitizeFilename } from '@haina/utils-commons';
// import useConversationStore from '@/stores/ConversationStore';
const ImageUpload = ({ disabled, invokeUploadFileMessage, invokeSendUploadMessage }) => {

@ -0,0 +1,24 @@
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 '@/utils/commons';
import { cloneDeep, flush, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, sortObjectsByKeysMap, TagColorStyle } from '@haina/utils-commons';
import { replaceTemplateString, whatsappTemplateBtnParamTypesMapped } from '@/channel/bubbleMsgUtils';
import { isEmpty } from '@/utils/commons';
import { isEmpty } from '@haina/utils-commons';
import useStyleStore from '@/stores/StyleStore';
const splitTemplate = (template) => {
@ -27,6 +27,7 @@ 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);
@ -60,7 +61,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('free') || ele.key.includes('detail') || ele.key.includes('update') || ele.key.includes('info')) ? (
<Input.TextArea
key={`${ele.key}_${i}`}
rows={2}
@ -70,7 +71,8 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
className={` w-11/12 `}
size={'small'}
title={ele.key}
placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格\n注意: 模板消息无法输入换行`}
// ${paramsVal[ele.key] || ele.key}
placeholder={`按Tab键跳到下一个空格\n注意: 模板消息无法输入换行`}
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
// onPressEnter={() => handleSendTemplate(tempItem)}
/>
@ -163,10 +165,11 @@ 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.toUpperCase()}
{item.language.slice(-2).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
@ -356,9 +359,11 @@ 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 '@/utils/commons';
import { groupBy, isEmpty, } from '@haina/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 '@/utils/commons';
import { isEmpty } from '@haina/utils-commons';
import useStyleStore from '@/stores/StyleStore';
import EmailListDrawer from './Components/EmailListDrawer';
import { POPUP_FEATURES } from '@/config';
@ -39,6 +39,7 @@ 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);
@ -140,7 +141,7 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
conversationid: currentConversation.sn,
opisn: currentConversation.opi_sn,
whatsappid: currentConversation.whatsapp_phone_number,
lasttime: currentConversation?.lasttime || '',
lasttime: currentListLasttime || '',
coli_sn: currentConversation.coli_sn || '',
})
setLongListLoading(false);
@ -238,6 +239,8 @@ 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 '@/utils/commons';
import { isEmpty } from '@haina/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 "@/utils/commons";
import { copy, isEmpty } from "@haina/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 = copy(OrderLabelDefaultOptions);
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions);
orderLabelOptions.unshift({ value: 0, label: "未设置", disabled: true });
const orderStatusOptions = copy(OrderStatusDefaultOptions);
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions);
const getHistoryOrder = (email, whatsappid='') => {
return fetchHistoryOrder(loginUser.userId, email, whatsappid)

@ -1,6 +1,7 @@
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'
@ -27,7 +28,7 @@ import 'react-chat-elements/dist/main.css'
import ReloadPrompt from './ReloadPrompt'
import ClearCache from './ClearCache'
import { BUILD_VERSION, BUILD_DATE } from '@/config'
import { BUILD_VERSION, GIT_HEAD } from '@/config'
const { Header, Footer, Content } = Layout
const { Title } = Typography
@ -59,12 +60,23 @@ 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()
}
}
@ -183,10 +195,15 @@ function DesktopApp() {
label: <Link to='/account/profile'>个人资料</Link>,
key: 'profile',
},
{ type: 'divider' },
{
label: '支付链接',
key: 'generate-payment',
},
{
label: '短链接转换',
key: 'shorturl-conversion',
},
{
label: '图文集',
key: 'snippet-list',
@ -231,7 +248,7 @@ function DesktopApp() {
</Content>
</Layout>
<Footer>
桂林海纳国际旅行社有限公司 Version: {BUILD_VERSION}({BUILD_DATE})
桂林海纳国际旅行社有限公司 Version: {BUILD_VERSION}({GIT_HEAD})
</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 '@/utils/commons'
import { cloneDeep, debounce, isEmpty, olog, omitEmpty } from '@haina/utils-commons'
import { writeIndexDB, readIndexDB, deleteIndexDBbyKey, } from '@/utils/indexedDB';
import '@/views/Conversations/Online/Input/EmailEditor.css'
@ -525,6 +525,7 @@ const NewEmail = () => {
})
// body.externalID = stickToCid
// body.actionID = `${stickToCid}.${msgObj.id}`
body.actionID = `0.${uuid()}`
body.contenttype = isRichText ? 'text/html' : 'text/plain'
try {
@ -576,10 +577,12 @@ const NewEmail = () => {
const idleCallbackId = useRef(null)
const debouncedSave = useCallback(
debounce((data) => {
idleCallbackId.current = window.requestIdleCallback(() => {
console.log('Saving data (idle, debounced):', data)
writeIndexDB([{ ...data, key: editorKey }], 'draft', 'mailbox')
})
if ('requestIdleCallback' in window) {
idleCallbackId.current = window.requestIdleCallback(() => {
console.log('Saving data (idle, debounced):', data)
writeIndexDB([{ ...data, key: editorKey }], 'draft', 'mailbox')
})
}
}, 1500), // 1.5s
[],
)

@ -1,5 +1,5 @@
// import './ReloadPrompt.css';
import { clearAllCaches } from '@/utils/commons';
import { clearAllCaches } from '@haina/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 icon={'🎉'} />
</a>} type="info" showIcon={needRefresh} icon={needRefresh ? '🎉' : null} />
)}
</>

@ -9,13 +9,12 @@ import useStyleStore from '@/stores/StyleStore';
function GeneratePayment() {
const { notification } = App.useApp()
const [messageApi, contextHolder] = message.useMessage()
const { message, notification } = App.useApp()
const [mobile, setMobile] = useStyleStore((state) => [state.mobile, state.setMobile]);
const [generateForm] = Form.useForm()
const [getPrimaryEmail, loginUser] = useAuthStore((s) => [s.getPrimaryEmail, s.loginUser])
const [loginUser] = useAuthStore((s) => [s.loginUser])
const [generatePayment, fetchOrderDetail] = useOrderStore((s) => [s.generatePayment, s.fetchOrderDetail])
const currentOrder = useConversationStore((state) => state.currentConversation?.coli_sn || '')
@ -45,24 +44,19 @@ 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: 'US',
langauge: 'en_HTravel',
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: 'US',
langauge: 'en_HTravel',
orderType: '227001',
currency: 'USD',
amount: 1,
@ -109,8 +103,8 @@ function GeneratePayment() {
]}>
<Select
options={[
{ value: 'US', label: '英语' },
{ value: 'en_HTravel', label: '英语(Highlights)' },
{ value: 'US', label: '英语' },
{ value: 'de_de', label: '德语' },
{ value: 'fr_fr', label: '法语' },
{ value: 'es_es', label: '西语' },
@ -137,7 +131,7 @@ function GeneratePayment() {
rules={[
{
required: true,
message: '金额填',
message: '金额填',
},
]}>
<InputNumber
@ -187,7 +181,7 @@ function GeneratePayment() {
</Tooltip>
<span>支付按钮</span>
</Flex>
<HtmlPreview value={generatedObject.payhtml} loading={isHtmlLoading} onCopied={() => messageApi.success('已复制')} />
<HtmlPreview value={generatedObject.payhtml} loading={isHtmlLoading} onCopied={() => message.success('已复制😀')} />
<Flex gap='small'>
<Tooltip placement='topLeft' title='发送 WhatsApp 使用'>
<InfoCircleOutlined />
@ -197,7 +191,7 @@ function GeneratePayment() {
size='small'
onClick={() => {
navigator.clipboard.writeText(generatedObject.paylink)
messageApi.success('复制成功😀')
message.success('复制😀')
}}>
复制链接
</Button>
@ -205,7 +199,6 @@ 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 '@/utils/commons'
import { isNotEmpty } from '@haina/utils-commons'
const HtmlPreview = (props) => {
const { loading = false, value, onEdit, onCopied, onDelete } = props

@ -0,0 +1,24 @@
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 '@/utils/commons'
import { isEmpty } from '@haina/utils-commons'
import { WAI_SERVER_KEY } from '@/config';
import WAIQRCode from './WAIQRCode';
@ -89,23 +89,12 @@ function Profile() {
label: 'GH 客运(+639454682947)',
},
{
value: '+8618174165365',
label: '国际事业部(+8618174165365)',
},
{
value: 'GH 客服',
label: 'GH 客服(无)',
disabled: true,
value: '+85265210895',
label: 'GH 客运 HK(+85265210895)',
},
{
value: 'CT 事业部',
label: 'CT 事业部(无)',
disabled: true,
},
{
value: '花梨鹰事业部',
label: '花梨鹰事业部(无)',
disabled: true,
value: '+8618174165365',
label: '国际事业部(+8618174165365)',
},
]}
/>

@ -0,0 +1,68 @@
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 [messageApi, contextHolder] = message.useMessage()
const { message, notification } = App.useApp()
const [searchform] = Form.useForm()
const [snippetForm] = Form.useForm()
@ -32,7 +32,6 @@ function SnippetList() {
const [loginUser] = useAuthStore((state) => [state.loginUser])
const { notification } = App.useApp()
const [isSnippetModalOpen, setSnippetModalOpen] = useState(false)
const [isHtmlLoading, setHtmlLoading] = useState(false)
@ -236,14 +235,13 @@ function SnippetList() {
value={currentSnippet.content}
loading={isHtmlLoading}
onEdit={() => handelSnippetEdit()}
onCopied={() => messageApi.success('已复制')}
onCopied={() => message.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 '@/utils/commons'
import { isEmpty } from '@haina/utils-commons'
const connectionStateMappedQRCodeState = {
open: 'scanned',

@ -4,6 +4,7 @@ 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
@ -18,6 +19,12 @@ 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()
@ -143,8 +150,8 @@ const Index = () => {
{ 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' },
{ 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_status',
@ -160,6 +167,7 @@ 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: '会话',
@ -169,7 +177,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 : ''}>
<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 }}>
查看会话 {icon} {record.msg_reply ? <MessageTwoTone title='已回复' /> : ''}
</Link>
)

@ -1,11 +1,14 @@
import { Flex, Result, Input, Button } from 'antd'
import { Flex, Result, Input, Button, Typography } 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 = () => {
@ -23,6 +26,10 @@ function AuthCode() {
})
}
const handleLogin = () => {
loginByJSAuth(result)
}
useEffect(() => {
const dingTalkPlatForm = dd.env.platform
setResult(dingTalkPlatForm)
@ -35,8 +42,10 @@ 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 '@/utils/commons'
import { isNotEmpty } from '@haina/utils-commons'
import { Flex, Result, Spin } from 'antd'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
@ -13,6 +13,7 @@ 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')
@ -21,10 +22,10 @@ function Callback() {
const originUrl = urlSearch.get('origin_url')
useEffect (() => {
if (isNotEmpty(authCode) && state === 'global-saels') {
if (state === 'global-saels' && isNotEmpty(authCode)) {
login(authCode)
} else if (isNotEmpty(authCode) && state === 'jsapi-auth') {
// loginByJSAuth()
} else if (state === 'jsapi-auth' && isNotEmpty(authCode)) {
loginByJSAuth(authCode)
} else {
console.error('error: ' + error)
}
@ -81,4 +82,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 '@/utils/commons'
import { isNotEmpty } from '@haina/utils-commons'
import * as dd from 'dingtalk-jsapi'
//
@ -32,25 +32,25 @@ function Login() {
</Flex>
)
}
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: () => {},
// })
// }
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: () => {},
})
}
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 } from '@ant-design/icons';
import { UnorderedListOutlined, MenuUnfoldOutlined, MenuFoldOutlined, LeftOutlined } 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={<MenuFoldOutlined />} onClick={() => navigate('/m/conversation')} className=' rounded-none rounded-l' />
<Button type='text' icon={<LeftOutlined />} 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 '@/utils/commons';
import { copy, objectMapper } from '@haina/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 = copy(OrderLabelDefaultOptions);
const orderLabelOptions = structuredClone(OrderLabelDefaultOptions);
orderLabelOptions.unshift({ value: '', label: '全部' });
const orderStatusOptions = copy(OrderStatusDefaultOptions);
const orderStatusOptions = structuredClone(OrderStatusDefaultOptions);
orderStatusOptions.unshift({ value: '', label: '全部' });
const remindStateOptions = copy(RemindStateDefaultOptions);
const remindStateOptions = structuredClone(RemindStateDefaultOptions);
remindStateOptions.unshift({ value: '', label: '全部' });
const [form] = Form.useForm();

@ -1,11 +1,11 @@
import useAuthStore from '@/stores/AuthStore'
import { pick } from '@/utils/commons'
import { pick } from '@haina/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 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'
@ -45,7 +45,7 @@ const deptMap = new Map([
function Follow() {
const [collapsed, setCollapsed] = useState(true)
const [collapsed, setCollapsed] = useState(false)
const mailboxTreeRef = useRef(null);
const [treeHeight, setTreeHeight] = useState(500);
@ -182,7 +182,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: "#1ba784", color1: '#1ba784'}} overflowCount={999} />
<Badge size={'small'} count={node.count} offset={[3, 0]} style={{backgroundColor: node?._raw?.ImageIndex===2 ? "" : "#1ba784", color1: '#1ba784'}} overflowCount={999} />
</Typography.Text>
)}
/>

@ -1,15 +1,14 @@
import { useEffect, useState } from 'react'
import { ReloadOutlined, ReadOutlined, RightOutlined, LeftOutlined, SearchOutlined, MailOutlined, DeleteOutlined, CloseOutlined, CloseCircleTwoTone, CloseCircleOutlined } from '@ant-design/icons'
import { Flex, Button, Tooltip, List, Form, Row, Col, Input, Checkbox, DatePicker, Switch, Breadcrumb, Skeleton, Popconfirm } from 'antd'
import { ReloadOutlined, RightOutlined, LeftOutlined, MailOutlined, DeleteOutlined, CloseCircleOutlined } from '@ant-design/icons'
import { Flex, Button, Tooltip, List, Checkbox, Space, Breadcrumb, Skeleton } from 'antd'
import { useEmailList } from '@/hooks/useEmail'
import { isEmpty } from '@/utils/commons'
import { isEmpty } from '@haina/utils-commons'
import { MailboxDirIcon } from './MailboxDirIcon'
import { AttachmentIcon, MailCheckIcon, MailOpenIcon } from '@/components/Icons'
import { AttachmentIcon, MailCheckIcon } from '@/components/Icons'
import NewEmailButton from './NewEmailButton'
import MailOrderSearchModal from './MailOrderSearchModal'
import MailListSearchModal from './MailListSearchModal'
const PAGE_SIZE = 50 //
const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
@ -42,7 +41,96 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
const getPagedData = (data, currentPage) => {
const startIndex = (currentPage - 1) * PAGE_SIZE
const endIndex = Math.min(startIndex + PAGE_SIZE, data.length)
return data.slice(startIndex, endIndex)
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
}
const prePage = () => {
@ -53,6 +141,7 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
current: newCurrent,
pagedList: getPagedData(mailList, newCurrent),
}))
setSelectedItems([]);
}
}
@ -64,16 +153,17 @@ 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) ? `[${item.FDir}] ` : ''
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 countryName = isEmpty(item.CountryCN) ? '' : '[' + item.CountryCN + '] '
const mailStateClass = item.MOI_ReadState === 0 ? 'font-bold' : ''
const mailStateClass = item.MOI_ReadState === 0 ? 'font-black text-emerald-600' : ''
const hasAtta = item.MAI_Attachment !== 0 ? <AttachmentIcon className='text-blue-500' /> : null
return (
<li
@ -83,7 +173,8 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
checked={selectedItems.some((i) => i.MAI_SN === item.MAI_SN)}
onClick={(e) => {
const isChecked = e.target.checked
const updatedSelection = isChecked ? [...selectedItems, item] : selectedItems.filter((item) => item.MAI_SN !== item.MAI_SN)
const noCurrent = selectedItems.filter((i) => i.MAI_SN !== item.MAI_SN);
const updatedSelection = isChecked ? [...noCurrent, item] : noCurrent;
setSelectedItems(updatedSelection)
}}></Checkbox>
</div>
@ -115,12 +206,12 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
<Flex wrap gap='middle' justify={'center'} className='min-w-30 px-1'>
<Tooltip title='全选'>
<Checkbox
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))}
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))}
onChange={(e) => {
const isChecked = e.target.checked
if (isChecked) {
setSelectedItems((prev) => [...prev, ...pagination.pagedList])
setSelectedItems(pagination.pagedList.reduce((a, item) => a.concat(item.data), []))
} else {
setSelectedItems([])
}
@ -174,7 +265,7 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
}
})}
/>
{tempBreadcrumb && (<Button type="text" icon={<CloseCircleOutlined />} onClick={() => refresh()} />)}
{tempBreadcrumb && (<Button type='text' icon={<CloseCircleOutlined />} onClick={() => refresh()} />)}
<Flex align='center' justify='space-between' className='ml-auto'>
<span>已选: {selectedItems.length} </span>
<span>
@ -199,15 +290,22 @@ const MailBox = ({ mailboxDir, onMailItemClick, ...props }) => {
<div className='bg-white overflow-auto px-2' style={{ height1: 'calc(100vh - 198px)' }}>
<Skeleton active loading={loading}>
<List
loading={loading}
className='flex flex-col h-full [&_.ant-list-items]:overflow-auto'
header={null}
itemLayout='vertical'
pagination={false}
dataSource={pagination.pagedList}
renderItem={mailItemRender}
/>
<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>
</Skeleton>
</div>
</div>

@ -3,7 +3,7 @@ import { SearchOutlined } from '@ant-design/icons'
import { Button, Modal, Form, Input, Checkbox, Radio, DatePicker, Divider, Typography, Flex } from 'antd'
import dayjs from 'dayjs'
import { getEmailDirAction, queryHTOrderListAction, } from '@/actions/EmailActions'
import { isEmpty, objectMapper, pick } from '@/utils/commons'
import { isEmpty, objectMapper, pick } from '@haina/utils-commons'
import useConversationStore from '@/stores/ConversationStore'
const MailOrderSearchModal = ({ ...props }) => {

@ -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 '@/utils/commons'
import { isEmpty } from '@haina/utils-commons'
const NewEmailButton = ({ ...props }) => {
const { notification } = App.useApp()

@ -5,8 +5,10 @@ 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 {
@ -159,6 +161,7 @@ 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: {

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

@ -1,3 +0,0 @@
# koa-template
Koa boilerplate template for create-koa-application.

@ -4,7 +4,7 @@ const { sessionStore } = require('../../core'); // Import from core/index.js
const { createWhatsApp } = require('../../core/baileys'); // Import from core/index.js
const { getConnection } = require('../../services/connections.service');
const { upsert: upsertAgentSession } = require('../../services/agent_sessions.service');
const { isEmpty } = require('../../utils/commons.util');
const { isEmpty } = require('@haina/utils-commons');
const { getUserLogger } = require('../../utils/logger.util');
const { domain } = require('../../config').server;
const waEmitter = require('../../core/emitter');
@ -44,7 +44,7 @@ const offline = async ctx => {
ctx.assert(existsSession, 400, `WhatsApp ${phone} 已离线`);
try {
getUserLogger(phone).info(`WhatsApp ${phone} 准备离线`);
// existsSession.stop(); // todo:
existsSession.stop();
// sessionStore.removeSession(existsSession.channelId);
waEmitter.emit('connection:close', { phone, whatsAppNo: phone, status: 'offline', channelId: existsSession.channelId });
return ''; // { wsToSend, ret: 'Message sent successfully' };

@ -0,0 +1,47 @@
const fs = require('fs-extra');
const path = require('path');
const copyItems = [
'api',
'config',
'core',
'helper',
'middleware',
'models',
'services',
'utils',
'index.js',
'server.js'
];
// 目标目录dist
const distPath = path.resolve(__dirname, 'dist');
async function copyFiles() {
try {
// 确保dist目录存在
await fs.ensureDir(distPath);
// 清空dist目录中的所有内容
await fs.emptyDir(distPath);
console.log('🧹 ', distPath);
// 遍历并复制每一项
for (const item of copyItems) {
const srcPath = path.resolve(__dirname, item);
const destPath = path.join(distPath, path.basename(item));
if (await fs.pathExists(srcPath)) {
await fs.copy(srcPath, destPath, { overwrite: true });
console.log(`${item}${destPath}`);
} else {
console.warn(`⚠️ ${srcPath}`);
}
}
console.log('🎉 Done');
} catch (err) {
console.error('❌ ', err);
process.exit(1);
}
}
copyFiles();

@ -66,8 +66,12 @@ const getFileExtension = mimeType => {
return '.bmp';
case 'image/webp':
return '.webp';
case 'audio/mpeg':
return '.mp3';
case 'audio/ogg; codecs=opus':
return '.opus';
default:
return 'unknown';
return '.unknown';
}
}

@ -2,15 +2,14 @@ const {
makeWASocket,
Browsers,
DisconnectReason,
fetchLatestBaileysVersion,
makeCacheableSignalKeyStore,
makeInMemoryStore,
fetchLatestWaWebVersion,
getMessageFromStore,
useMultiFileAuthState,
downloadMediaMessage,
isJidNewsletter, isJidGroup, isJidBroadcast, isJidStatusBroadcast
} = require('@whiskeysockets/baileys');
const fs = require('fs');
const path = require('path');
const { writeFile } = require('fs/promises');
const waEmitter = require('../emitter');
const serverConfig = require('../../config').server;
@ -20,208 +19,297 @@ const generateId = require('../../utils/generateId.util');
const NodeCache = require('node-cache');
const P = require('pino');
// https://baileys.whiskeysockets.io/
// Ref:
// https://github.com/WhiskeySockets/Baileys/blob/v6.7.19/README.md
// https://github.com/WhiskeySockets/Baileys/blob/v6.7.19/Example/example.ts
const createWhatsApp = async phone => {
let qrCode = null;
const channelId = generateId();
const whatsAppNo = phone;
// 缓存 msgId-externalId过期时间为 5 分钟
const externalIdCache = new NodeCache({ stdTTL: 60*5 });
// 缓存群信息,过期时间为 24 小时
const groupSubjectCache = new NodeCache({ stdTTL: 60*60*24 });
// 缓存群信息,过期时间为 5 分钟
const groupCache = new NodeCache({stdTTL: 5 * 60, useClones: false})
const logger = P({ timestamp: () => `,"time":"${new Date().toJSON()}"` }, P.destination('./logs/wa-logs-' + phone + '.txt'));
logger.level = 'trace';
logger.level = 'warn';
const msgRetryCounterCache = new NodeCache();
const storeFilename = './baileys_auth_info/baileys_store_' + phone + '.json'
const store = makeInMemoryStore({ logger });
store?.readFromFile(storeFilename);
// save every 10s
setInterval(() => {
store?.writeToFile(storeFilename);
}, 10_000);
const authStateFolder = './baileys_auth_info/' + phone;
const { state, saveCreds } = await useMultiFileAuthState(authStateFolder);
// fetch latest version of WA Web
// const { version, isLatest } = await fetchLatestBaileysVersion();
const { version, isLatest, } = { version: [2, 3000, 1025091846], isLatest: false };
const waVersion = version.join('.') + ', ' + (isLatest ? 'latest' : 'out');
// https://git.../WhiskeySockets/.../src/Utils/generics.ts
// const fetchLatestWaWebVersion, fetchLatestWaWebVersion
// https://web.whatsapp.com/sw.js, client_revision: 1031072708
const { version, isLatest } = await fetchLatestWaWebVersion();
const whatsAppVersion = version;//[2, 3000, 1031072708];
const stop = () => {
fs.rm(authStateFolder, { recursive: true, force: true }, (err) => {
if (err) {
console.error(`Error deleting authStateFolder directory: ${err.message}`);
} else {
console.log('Successfully deleted authStateFolder directory: ', authStateFolder);
}
});
waEmitter.emit('request.' + whatsAppNo + '.stop', {});
};
const start = () => {
const waSocket = makeWASocket({
version,
version: whatsAppVersion,
logger,
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
// https://github.com/WhiskeySockets/Baileys/blob/31bc8ab/src/Utils/generics.ts#L21
// https://github.com/WhiskeySockets/Baileys/blob/31bc8ab4e2c825c0d774875701ed07e20d05bdb6/WAProto/WAProto.proto
browser: Browsers.macOS('SAFARI'),//Browsers.macOS('SAFARI'),//Browsers.ubuntu('IOS_PHONE'),//Browsers.baileys('WEAR_OS'),//
cachedGroupMetadata: async (jid) => groupCache.get(jid),
getMessage: async (key) => await getMessageFromStore(key),
// https://github.com/WhiskeySockets/Baileys/blob/master/src/Utils/generics.ts
// https://github.com/WhiskeySockets/Baileys/blob/master/WAProto/WAProto.proto
// Browsers.macOS('SAFARI'), Browsers.ubuntu('IOS_PHONE'), Browsers.baileys('WEAR_OS'),
browser: Browsers.macOS('SAFARI'),
msgRetryCounterCache,
generateHighQualityLinkPreview: false,
syncFullHistory: false,
markOnlineOnConnect: false // Receive Notifications in Whatsapp App
});
store?.bind(waSocket.ev);
const getGroupSubject = async jid => {
const cachedMatadata = groupCache.get(jid);
if (cachedMatadata === undefined) {
const groupMetadata = await waSocket.groupMetadata(jid);
groupCache.set(jid, groupMetadata);
return groupMetadata.subject;
} else {
return cachedMatadata.subject;
}
};
const buildStandardMessage = async msg => {
// 如果是群发(xxx@broadcast)participant 是发送人,不然则是 remoteJid
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
const externalId = externalIdCache.get(msg.key.id);
const isPersonal = isJidPersonal(msg.key.remoteJid);
const conversationType = isPersonal ? 'individual' : 'group';
const isGroup = isJidGroup(msg.key.remoteJid);
const groupSubject = isGroup ? await getGroupSubject(msg.key.remoteJid) : '';
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
const msgTimestamp = msg.messageTimestamp === undefined ? new Date().getTime() / 1000 : msg.messageTimestamp;
const originalStatus = msg.status || msg.update?.status;
const msgStatus = originalStatus === undefined ? '' : formatStatus(originalStatus);
return {
id: msg.key.id,
externalId,
status: msgStatus,
direction: msgDirection,
from: msgFrom,
to: msgTo,
conversation: {
type: conversationType,
name: groupSubject,
},
customerProfile: {
id: isGroup ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid),
// 商业号使用 verifiedBizName个人使用 pushName
name: msg.verifiedBizName || msg.pushName,
},
whatsAppNo,
fromMe: msg.key.fromMe,
updateTime: formatTimestamp(msgTimestamp),
}
};
const saveMediaFile = async (original, fileName) => {
const mediaBuffer = await downloadMediaMessage(
original, 'buffer', {}, { logger, reuploadRequest: waSocket.updateMediaMessage, },
);
await writeFile(fileName, mediaBuffer);
};
const parseTextMessage = original => {
const text = original.message?.conversation;
return {
type: 'text',
text: {
body: text,
},
};
};
const parseExtendedTextMessage = original => {
const text = original.message?.extendedTextMessage?.text;
return {
type: 'text',
text: {
body: text,
},
context: {
from: decodeJid(original.message?.extendedTextMessage?.contextInfo?.participant),
id: original.message?.extendedTextMessage?.contextInfo?.stanzaId,
},
};
};
const parseReactionMessage = original => {
const text = original.message?.reactionMessage?.text;
const context = original.message?.reactionMessage?.key?.id;
return {
type: 'reaction',
reaction: {
message_id: context,
emoji: text,
},
};
};
const parseEditedMessage = original => {
const text = original.message?.protocolMessage?.editedMessage?.conversation;
return {
type: 'text',
text: {
body: text,
},
};
};
const parseImageMessage = async original => {
const imageMessage = original.message.imageMessage;
const fileExtension = getFileExtension(imageMessage.mimetype);
const imageFilename = './temp/image_' + whatsAppNo + '_' + original.key.id + fileExtension;
await saveMediaFile(original, imageFilename);
return {
type: 'image',
image: {
mimetype: imageMessage.mimetype,
sha256: uint8ArrayToBase64(imageMessage.fileSha256),
caption: imageMessage.caption,
filePath: imageFilename,
link_original: imageMessage.url,
},
};
};
const parseAudioMessage = async original => {
const audioMessage = original.message.audioMessage;
const fileExtension = getFileExtension(audioMessage.mimetype);
const audioFilename = './temp/audio_' + whatsAppNo + '_' + original.key.id + fileExtension;
await saveMediaFile(original, audioFilename);
return {
type: 'audio',
audio: {
mimetype: audioMessage.mimetype,
sha256: uint8ArrayToBase64(audioMessage.fileSha256),
filePath: audioFilename,
link_original: audioMessage.url,
},
};
};
const parseDocumentMessage = async original => {
const documentMessage = original.message.documentMessage || original.message.documentWithCaptionMessage.message.documentMessage;
const documentFilename = './temp/file_' + whatsAppNo + '_' + original.key.id + '_' + documentMessage.fileName;
await saveMediaFile(original, documentFilename);
return {
type: 'document',
document: {
filename: documentMessage.fileName,
mimetype: documentMessage.mimetype,
sha256: uint8ArrayToBase64(documentMessage.fileSha256),
caption: documentMessage.caption,
filePath: documentFilename,
link_original: documentMessage.url,
},
};
};
const parseTemplateMessage = original => {
if (original.message.templateMessage.hydratedTemplate) {
const text = original.message.templateMessage?.hydratedTemplate?.hydratedContentText;
return {
type: 'text',
text: {
body: text,
},
}
} else if (original.message.templateMessage.interactiveMessageTemplate) {
const text = original.message.templateMessage.interactiveMessageTemplate?.body?.text;
return {
type: 'text',
text: {
body: text,
},
}
}
};
const getMessageParser = (original) => {
if (original.message?.conversation) return parseTextMessage;
if (original.message?.extendedTextMessage) return parseExtendedTextMessage;
if (original.message?.reactionMessage) return parseReactionMessage;
if (original.message?.protocolMessage?.editedMessage) return parseEditedMessage;
if (original.message?.imageMessage) return parseImageMessage;
if (original.message?.documentMessage || original.message?.documentWithCaptionMessage) return parseDocumentMessage;
if (original.message?.audioMessage) return parseAudioMessage;
if (original.message?.templateMessage) return parseTemplateMessage;
return null;
};
const handleMessagesUpsert = async upsert => {
console.info('messages.upsert: ', JSON.stringify(upsert, undefined, 2));
const msgEventSource = serverConfig.name + '.messages.upsert.' + upsert.type;
if (upsert.type === 'notify') {
for (const msg of upsert.messages) {
// 没有类型的消息,先忽略
if (!msg.message) {
console.info('!msg.message, ignored.');
continue;
}
if (isJidStatusBroadcast(msg.key.remoteJid)) {
console.info('isJidStatusBroadcast, ignored.');
continue;
}
if (isJidNewsletter(msg.key.remoteJid)) {
console.info('isJidNewsletter, ignored.');
continue;
}
const messageType = Object.keys(msg.message)[0];
// 如果是群发(xxx@broadcast)participant 是发送人,不然则是 remoteJid
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
const externalId = externalIdCache.get(msg.key.id);
const isPersonal = isJidPersonal(msg.key.remoteJid);
const conversationType = isPersonal ? 'individual' : 'group';
const isGroup = isJidGroup(msg.key.remoteJid);
let groupSubject = groupSubjectCache.get(msg.key.remoteJid);
if (isGroup && groupSubject === undefined) {
const groupMetadata = await waSocket.groupMetadata(msg.key.remoteJid);
groupSubject = groupMetadata.subject;
groupSubjectCache.set(msg.key.remoteJid, groupMetadata.subject)
}
for (const msg of upsert.messages) {
const emitEventName = msg.key.fromMe ? 'message:updated' : 'message:received';
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
const msgStatus = msg.status === undefined ? '' : formatStatus(msg.status);
if (msg.message?.conversation || msg.message?.extendedTextMessage?.text) {
const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text;
waEmitter.emit(emitEventName, {
id: msg.key.id,
externalId,
status: msgStatus,
direction: msgDirection,
from: msgFrom,
to: msgTo,
type: 'text',
text: {
body: text,
},
conversation: {
type: conversationType,
name: groupSubject,
},
customerProfile: {
id: decodeJid(msg.key.participant),
name: msg.pushName,
},
whatsAppNo,
fromMe: msg.key.fromMe,
eventSource: serverConfig.name + '.messages.upsert.notify',
updateTime: formatTimestamp(msg.messageTimestamp),
});
} else if (messageType === 'imageMessage') {
const imageMessage = msg.message.imageMessage;
const fileExtension = getFileExtension(imageMessage.mimetype);
const imageBuffer = await downloadMediaMessage(
msg, 'buffer', {}, { logger, reuploadRequest: waSocket.updateMediaMessage, },
);
const imageFilename = './temp/image_' + whatsAppNo + '_' + msg.key.id + fileExtension;
await writeFile(imageFilename, imageBuffer);
waEmitter.emit(emitEventName, {
id: msg.key.id,
externalId,
status: msgStatus,
direction: msgDirection,
from: msgFrom,
to: msgTo,
type: 'image',
image: {
mimetype: imageMessage.mimetype,
sha256: uint8ArrayToBase64(imageMessage.fileSha256),
caption: imageMessage.caption,
filePath: imageFilename,
link_original: imageMessage.url,
},
conversation: {
type: conversationType,
name: groupSubject,
},
customerProfile: {
id: decodeJid(msg.key.participant),
name: msg.pushName,
},
whatsAppNo,
fromMe: msg.key.fromMe,
eventSource: serverConfig.name + '.messages.upsert.notify',
updateTime: formatTimestamp(msg.messageTimestamp),
});
}
// 没有类型的消息,先忽略
if (!msg.message) {
continue;
}
if (isJidStatusBroadcast(msg.key.remoteJid)) {
continue;
}
} else if (upsert.type === 'append') {
for (const msg of upsert.messages) {
if (msg.message?.conversation || msg.message?.extendedTextMessage?.text) {
const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text;
// 如果是群发(xxx@broadcast)participant 是发送人,不然则是 remoteJid
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
const externalId = externalIdCache.get(msg.key.id);
const isPersonal = isJidPersonal(msg.key.remoteJid);
const conversationType = isPersonal ? 'individual' : 'group';
const emitEventName = msg.key.fromMe ? 'message:updated' : 'message:received';
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
const msgStatus = msg.status === undefined ? '' : formatStatus(msg.status);
waEmitter.emit(emitEventName, {
id: msg.key.id,
externalId,
status: msgStatus,
direction: msgDirection,
from: msgFrom,
to: msgTo,
type: 'text',
text: {
body: text,
},
conversation: {
type: conversationType,
},
customerProfile: {
id: decodeJid(msg.participant),
name: msg.pushName,
},
whatsAppNo,
fromMe: msg.key.fromMe,
eventSource: serverConfig.name + '.messages.upsert.append',
updateTime: formatTimestamp(msg.messageTimestamp),
});
}
if (isJidNewsletter(msg.key.remoteJid)) {
continue;
}
const standardMessage = await buildStandardMessage(msg)
const messageParser = getMessageParser(msg);
if (messageParser) {
const parsedMessage = await messageParser(msg);
const mergedMessage = Object.assign({}, standardMessage, parsedMessage, {
eventSource: msgEventSource
});
console.info('upsert.mergedMessage: ', mergedMessage);
const emitEventName = msg.key.fromMe ? 'message:updated' : 'message:received';
waEmitter.emit(emitEventName, mergedMessage);
} else {
console.info('不支持该消息类型:', msg);
}
}
}
const handleMessagesUpdate = async messageUpdate => {
console.info('messages.update: ', JSON.stringify(messageUpdate, undefined, 2));
const msgEventSource = serverConfig.name + '.messages.updated'
for (const msg of messageUpdate) {
@ -230,36 +318,14 @@ const createWhatsApp = async phone => {
if (ignore) continue;
// 如果是群发(xxx@broadcast)participant 是发送人,不然则是 remoteJid
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
const externalId = externalIdCache.get(msg.key.id);
const isPersonal = isJidPersonal(msg.key.remoteJid);
const conversationType = isPersonal ? 'individual' : 'group';
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
const msgStatus = formatStatus(msg.update.status);
waEmitter.emit('message:updated', {
id: msg.key.id,
externalId,
status: msgStatus,
direction: msgDirection,
from: msgFrom,
to: msgTo,
conversation: {
type: conversationType,
},
customerProfile: {
id: decodeJid(msg.key.participant),
name: msg.pushName,
},
whatsAppNo,
fromMe: msg.key.fromMe,
eventSource: serverConfig.name + '.messages.updated',
updateTime: formatTimestamp(new Date().getTime() / 1000),
const standardMessage = await buildStandardMessage(msg)
const mergedMessage = Object.assign({}, standardMessage, {
eventSource: msgEventSource
});
console.info('updated.mergedMessage: ', mergedMessage);
//
waEmitter.emit('message:updated', mergedMessage);
}
}
@ -290,13 +356,22 @@ const createWhatsApp = async phone => {
});
}
const stopHandler = () => {
waSocket.ev.off('messages.upsert', handleMessagesUpsert);
waSocket.ev.off('messages.update', handleMessagesUpdate);
waSocket.ev.off('creds.update', handleCredsUpdate);
waSocket.logout(() => '实例已停止');
waEmitter.off('request.' + whatsAppNo + '.send.message', sendMessageHandler);
}
waSocket.ev.on('connection.update', async update => {
const { connection, lastDisconnect, qr } = update;
if (connection === 'close') {
waEmitter.off('request.' + whatsAppNo + '.send.message', sendMessageHandler);
if((lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut) {
waEmitter.off('request.' + whatsAppNo + '.stop', stopHandler);
if ((lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut) {
start();
} else {
// logout 异步删除验证目录
@ -313,12 +388,12 @@ const createWhatsApp = async phone => {
});
}
} else if (connection === 'open') {
waEmitter.on('request.' + whatsAppNo + '.send.message', sendMessageHandler);
waEmitter.on('request.' + whatsAppNo + '.stop', stopHandler);
waEmitter.emit('connection:open', {
status: 'open', whatsAppNo, channelId,
eventSource: serverConfig.name + '.connection.update.open',
});
waEmitter.on('request.' + whatsAppNo + '.send.message', sendMessageHandler);
} else if (qr !== undefined) {
// WebSocket 创建成功等待扫码,如果没有扫码会更新 qr
// 第一次一分钟,后面是 20 秒更新一次
@ -341,15 +416,24 @@ const createWhatsApp = async phone => {
waSocket.ev.on('creds.update', handleCredsUpdate);
waSocket.ev.on('messages.upsert', handleMessagesUpsert);
waSocket.ev.on('messages.update', handleMessagesUpdate);
waSocket.ev.on('groups.update', async ([event]) => {
const metadata = await waSocket.groupMetadata(event.id);
groupCache.set(event.id, metadata);
})
waSocket.ev.on('group-participants.update', async (event) => {
const metadata = await waSocket.groupMetadata(event.id)
groupCache.set(event.id, metadata);
})
};
return {
createTimestamp: Date.now(),
status: 'offline',
version: waVersion,
version: whatsAppVersion.join('.') + ', maybe',
channelId: channelId,
phone: phone,
start,
stop,
};
};

@ -1,164 +0,0 @@
const {
makeWASocket,
Browsers,
DisconnectReason,
fetchLatestBaileysVersion,
makeCacheableSignalKeyStore,
makeInMemoryStore,
useMultiFileAuthState,
delay,
downloadMediaMessage,
isJidUser
} = require('@whiskeysockets/baileys');
const { formatPhoneNumber, parsePhoneNumber, formatStatus, formatTimestamp } = require('./helper');
const NodeCache = require('node-cache');
const P = require('pino');
const logger = P({ timestamp: () => `,"time":"${new Date().toJSON()}"` }, P.destination('./wa-logs.txt'))
logger.level = 'trace'
// external map to store retry counts of messages when decryption/encryption fails
// keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts
const msgRetryCounterCache = new NodeCache()
// the store maintains the data of the WA connection in memory
// can be written out to a file & read from it
const store = makeInMemoryStore({ logger })
store?.readFromFile('./baileys_store_multi.json')
// save every 10s
setInterval(() => {
store?.writeToFile('./baileys_store_multi.json')
}, 10_000)
// start a connection
const startSock = async() => {
const { state, saveCreds } = await useMultiFileAuthState('baileys_auth_info')
// fetch latest version of WA Web
const { version, isLatest } = await fetchLatestBaileysVersion()
console.log(`using WA v${version.join('.')}, isLatest: ${isLatest}`)
const sock = makeWASocket({
version,
logger,
auth: {
creds: state.creds,
/** caching makes the store faster to send/recv messages */
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
msgRetryCounterCache,
generateHighQualityLinkPreview: true,
})
store?.bind(sock.ev)
const sendMessageWTyping = async(msg, jid) => {
await sock.presenceSubscribe(jid)
await delay(500)
await sock.sendPresenceUpdate('composing', jid)
await delay(2000)
await sock.sendPresenceUpdate('paused', jid)
await sock.sendMessage(jid, msg)
}
return new Promise((resolve) => {
// the process function lets you process all events that just occurred
// efficiently in a batch
sock.ev.process(
// events is a map for event name => event data
async(events) => {
// something about the connection changed
// maybe it closed, or we received all offline message or connection opened
if(events['connection.update']) {
const update = events['connection.update']
const { connection, qr, lastDisconnect } = update
if(connection === 'close') {
// reconnect if not logged out
if((lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut) {
startSock()
} else {
console.log('Connection closed. You are logged out.')
}
}
if (connection === 'open') {
console.log('Connection open.')
// setInterval(async() => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 10; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
sendMessageWTyping({ text: result + "-local.Connection open 发送:" + new Date().toString() }, '8613317835586@s.whatsapp.net')
// }, 1000*60*5)
setInterval(async() => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 10; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
sendMessageWTyping({ text: result + "-setInterval(5min) 发送:" + new Date().toString() }, '8613317835586@s.whatsapp.net')
}, 1000*60*5)
}
if (qr !== undefined) {
resolve(qr);
}
console.log('connection update', update)
}
if(events['creds.update']) {
await saveCreds()
}
// received a new message
if(events['messages.upsert']) {
const upsert = events['messages.upsert']
console.log('recv messages ', JSON.stringify(upsert, undefined, 2))
if(upsert.type === 'notify') {
for (const msg of upsert.messages) {
if (msg.message?.conversation || msg.message?.extendedTextMessage?.text) {
const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text
if (text == "requestPlaceholder" && !upsert.requestId) {
const messageId = await sock.requestPlaceholderResend(msg.key)
console.log('requested placeholder resync, id=', messageId)
} else if (upsert.requestId) {
console.log('Message received from phone, id=', upsert.requestId, msg)
}
}
}
}
}
}
)
})
//return sock
}
async function go() {
const qrCode = await startSock();
console.info('qr code: ', qrCode);
}
go();

@ -1,100 +0,0 @@
function generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const start = () => {
setInterval(() => {
const randomString = generateRandomString(10);
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Accept", "*/*");
myHeaders.append("Host", "wai-server-01-qq4qmtq7wc9he4.chinahighlights.cn");
myHeaders.append("Connection", "keep-alive");
var raw = JSON.stringify({
"from": "8618777396951",
"to": "8613557032060",
"content": randomString + "-setInterval(2min) 发送:" + new Date().toString()
});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch("http://wai-server-01-qq4qmtq7wc9he4.chinahighlights.cn/api/v1/channels/send", requestOptions)
.then(rsp => rsp.json())
.then(json => console.info('8613557032060: ', json))
.catch(ex => console.error(ex));
}, 1000*60*2);
//
setInterval(() => {
const randomString = generateRandomString(10);
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Accept", "*/*");
myHeaders.append("Host", "wai-server-01-qq4qmtq7wc9he4.chinahighlights.cn");
myHeaders.append("Connection", "keep-alive");
var raw = JSON.stringify({
"from": "8618777396951",
"to": "8613317835586",
"content": randomString + "-setInterval(3min) 发送:" + new Date().toString()
});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch("http://wai-server-01-qq4qmtq7wc9he4.chinahighlights.cn/api/v1/channels/send", requestOptions)
.then(rsp => rsp.json())
.then(json => console.info('8613317835586: ', json))
.catch(ex => console.error(ex));
}, 1000*60*3);
//
setInterval(() => {
const randomString = generateRandomString(10);
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Accept", "*/*");
myHeaders.append("Host", "wai-server-01-qq4qmtq7wc9he4.chinahighlights.cn");
myHeaders.append("Connection", "keep-alive");
var raw = JSON.stringify({
"from": "8618777396951",
"to": "8617607735120",
"content": randomString + "-setInterval(5min) 发送:" + new Date().toString()
});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch("http://wai-server-01-qq4qmtq7wc9he4.chinahighlights.cn/api/v1/channels/send", requestOptions)
.then(rsp => rsp.json())
.then(json => console.info('8613317835586: ', json))
.catch(ex => console.error(ex));
}, 1000*60*5);
}
start();

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

Loading…
Cancel
Save