Compare commits

...

261 Commits

Author SHA1 Message Date
Lei OT 021fb429ce perf: 销售进度: 表格优化 5 days ago
ZJYHX a9f45e334d perf:客运-老客户:订单数占比、毛利占比值保留小数 1 month ago
ZJYHX 488b33ae50 perf:客运-老客户:增加订单数占比、毛利占比 1 month ago
Lei OT 6dbf9cfb56 2.11.6 1 month ago
ZJYHX 108becd2cd perf:客服-目的地:增加国籍统计 1 month ago
Lei OT 73e1dcdb4c Revert "perf: 数据透视: + 预定的出发日期"
This reverts commit 16b08311fa.
1 month ago
Lei OT baae505831 perf: 老客户-分析: 移动端显示 1 month ago
Lei OT 16b08311fa perf: 数据透视: + 预定的出发日期 1 month ago
Lei OT e0e7c3c84f 2.11.5 2 months ago
ZJYHX 87f24c40fa fix:东道主项目:加载动画 2 months ago
ZJYHX bed6b81753 feat:东道主项目:单团明细 2 months ago
ZJYHX 171dd7514a fix:错误id命名 2 months ago
Lei OT a0ea6507a3 feat: 老客户-分析 2 months ago
Lei OT 40cd81f9a7 2.11.4 2 months ago
Lei OT 876216458e perf: 数据透视: 增加日期 2 months ago
Lei OT 8a0f919a42 fix: 2 months ago
Lei OT 020c9fc3e9 Merge remote-tracking branch 'origin/main' 2 months ago
Lei OT b2e539a4d2 perf: 导出表格: render 显示不正确; 酒店/三峡导出 2 months ago
ZJYHX fee1185406 feat:东道主项目页 2 months ago
Lei OT e297d36256 perf: 老客户: 默认参数 2 months ago
Lei OT 667574ec34 perf: 销售-老客户: 默认参数 2 months ago
Lei OT 185db1ec80 2.11.3 2 months ago
Lei OT 6dc82a0a01 perf: 销售-老客户: + 对比 2 months ago
Lei OT 4724b13a2d fix: 客服-目的地接团: 明细 2 months ago
Lei OT f532f138ff perf: 三峡,酒店: 合计 2 months ago
Lei OT 3b51ae8d08 2.11.2 2 months ago
Lei OT a6f7884e6e perf: 三峡: 常用的地接社列表; fix: 地接社 参数 2 months ago
Lei OT 928e27913d perf: 三峡: 人数 2 months ago
Lei OT 94363df0fc 2.11.1 2 months ago
Lei OT 621d3a2446 Merge remote-tracking branch 'origin/main' 2 months ago
ZJYHX 8f12fc8747 fix:老客户:修复数据量大时加载状态不提前消失 2 months ago
Lei OT 397c886aea perf: 三峡: 预订类型 2 months ago
ZJYHX 459db55453 fix:老客户:修复数据量大时加载状态不提前消失 2 months ago
ZJYHX 6317c24a75 perf:老客户:添加总计对比折线;修改折线颜色 2 months ago
Lei OT c9e13385dc 2.11.0 2 months ago
Lei OT a02fa37f17 fear: CRM统计: 个人看板; 违规明细 2 months ago
Lei OT ccbd0be1cd Merge remote-tracking branch 'origin/main' 2 months ago
Lei OT c7d3cf5746 fix: 三峡游船, 酒店 参数 2 months ago
ZJYHX 23b1d2c81b fix:老客户:切换页面折线颜色变换 2 months ago
Lei OT 7c08e8bfe6 Merge branch 'feature/hotel-cruise' 2 months ago
Lei OT 1e46cf6e63 feat: 客服: 三峡游船: 人数 2 months ago
Lei OT 17cedf14d9 feat: 客服: 酒店 2 months ago
ZJYHX 2fae92fa2a perf:老客户:增加折线虚实颜色 2 months ago
Lei OT 08aa01e33c feat: 客服: 三峡游船 2 months ago
ZJYHX 17a85122b1 fix:老客户:折线显示 2 months ago
ZJYHX 1b86ab1192 perf:老客户:增加对比折线 2 months ago
ZJYHX be31c3983d perf:老客户:添加数据对比、对比折线 2 months ago
ZJYHX 5551b2e362 perf:客服:添加目的地接数据对比 3 months ago
Lei OT 7cfd79a0d2 Merge branch 'main' into feature/hotel-cruise 3 months ago
Lei OT dcb185be5a Merge remote-tracking branch 'origin/main' 3 months ago
ZJYHX c3547538fd perf:客服:增加对比 3 months ago
Lei OT cb598ecba1 perf: CRM统计: tips 3 months ago
Lei OT a32943fffb Merge branch 'main' into feature/hotel-cruise
# Conflicts:
#	src/stores/CustomerServices.js
3 months ago
Lei OT a7b24d206d perf: 过程指标: tips 3 months ago
Lei OT f8eef38944 站点来源: + JH 3 months ago
Lei OT 89d151f388 2.10.7 3 months ago
Lei OT 6ea43a58e2 perf: 老客户: 订单走势 3 months ago
Lei OT 12e99bb816 2.10.6 3 months ago
Lei OT d29dab5824 feat: 数据透视: 增加 人天消费; 酒店星级; perf: 毛利范围 3 months ago
Lei OT f1e0c1200b 2.10.5 3 months ago
Lei OT 6f640bac4d perf: 客服: 增加按抵达日期统计; 3 months ago
Lei OT e822b9bb3a feat: 统计: 饮食要求、兴趣爱好、年龄段 3 months ago
Lei OT ac11babdab perf: 客服: 增加按抵达日期统计; 汇总按整团统计 3 months ago
Lei OT 04431635b5 2.10.4 4 months ago
Lei OT 80d4050527 perf: 老客户统计增加2列: 上次走团, 上次小组 4 months ago
Lei OT 6d6a7d98ee fix: CRM-统计: 过程指标 4 months ago
Lei OT b583bfcd2f 2.10.3 4 months ago
Lei OT 539f57e16d perf: CRM-统计: 结果指标: 增加日期选择; 修改样式 4 months ago
Lei OT e5ff51ae63 feat: CRM-统计: 过程指标 4 months ago
Lei OT cc3d0c19ad 2.10.2 4 months ago
Lei OT 00e22a6f08 conf: 来源配置: GH站外渠道 (中国), GH站外渠道 (海外) 4 months ago
Lei OT 02c09c9821 perf: 统计分布 4 months ago
Lei OT bd67f6ca4e 2.10.1 4 months ago
Lei OT 04d1cc2b39 perf: GH例会数据, 2025模板: 销售: 年走团毛利 4 months ago
Lei OT d1ee551a4e todo: 三峡, 酒店 4 months ago
Lei OT 4531ce1469 perf: CRM-统计: 导航 4 months ago
Lei OT d2696d8b01 2.10.0 4 months ago
Lei OT 557e4ac104 Merge branch 'feature/crm'
# Conflicts:
#	src/stores/Index.js
4 months ago
Lei OT 9a4c693c9e perf: GH例会数据, 2025模板: 销售: 年走团毛利 4 months ago
Lei OT 2be734a591 feat: CRM-统计数据 4 months ago
Lei OT 88e09d0bc9 feat: GH例会数据, 2025模板: 客服: 不显示好评个数, 因数据获取不准确 5 months ago
Lei OT ff4852475c 2.9.0 5 months ago
Lei OT a0b71dba79 feat: GH例会数据, 2025模板 5 months ago
Lei OT b2ce88e8aa 2.8.4 5 months ago
Lei OT 20e237f75d perf: 修改GH例会模板的文字 5 months ago
Lei OT b4a2b02709 perf: 数据透视: 增加查看订单明细, 导出 5 months ago
Lei OT 3890c4700f 2.8.3 7 months ago
Lei OT cb211ec207 perf: 客运-老客户: 增加条件`门票` 7 months ago
Lei OT 83e570bb49 feat: 数据透视: 增加毛利范围 1, 1.5, 2, 3, 4 8 months ago
Lei OT 545ad25d05 2.8.2 8 months ago
Lei OT 496fbc1ebd feat: 数据透视: 毛利范围[2W] 8 months ago
Lei OT e53f241d10 perf: 老客户: 提示同时勾选的数据, 存在重复计数 9 months ago
Lei OT 93766a28b6 perf: 客服: 小组多选 11 months ago
Lei OT b5115a8d1e 2.8.1 11 months ago
Lei OT 322ce021e6 perf: 搜索组件: 站点来源: +GH 国际各个站点 11 months ago
Lei OT 55bd0576a8 2.8.0 12 months ago
Lei OT 4792291025 perf: 导出; 解决汇总行空; 表头文字 12 months ago
Lei OT f2a937cc81 feat: 订单统计: 增加`站外渠道` 12 months ago
Lei OT d05d15ae09 feat: 订单统计: 客运标记 12 months ago
Lei OT 4743a66408 2.7.9 1 year ago
Lei OT 49b1657046 perf: 老客户/推荐-销售业绩: 导出, 可选总表和展开表 1 year ago
Lei OT 71b0cce66a 2.7.8 1 year ago
Lei OT 9e3bb33043 perf: 老客户/推荐-销售业绩: 合并账户 1 year ago
Lei OT c2317d40a7 导出的老客户推荐的值 1 year ago
Lei OT 046b7e2988 显示团天数和人数 1 year ago
Lei OT d18c59bb50 2.7.7 1 year ago
Lei OT ab79cdc289 feat: 老客户/推荐-销售业绩 1 year ago
Lei OT 345a2b88d8 2.7.6 1 year ago
Lei OT 530e558812 perf: 数据透视: +顾问 1 year ago
Lei OT ebdb7d41b4 2.7.5 1 year ago
Lei OT 3c9c9ede8c perf: GH例会: 顾问数据: 年走团 1 year ago
Lei OT 536a8b7f86 perf: GH例会: 顾问数据: 周/年成交 1 year ago
Lei OT 216bd9c0e6 perf: GH例会: 客服数据: 好评总数, AH, GH 1 year ago
Lei OT 55d205aaaa 2.7.4 1 year ago
Lei OT 0e76cc1f08 perf: GH例会: 客服数据: AH, GH 1 year ago
Lei OT 854c4a5ebc perf: GH例会: 顾问数据 1 year ago
Lei OT 5f55529f68 perf: GH例会: 客服数据: 中国 1 year ago
Lei OT 8eb539abb2 conf: 调整pagespy日志文件大小 1 year ago
Lei OT e42f1f1b6c fix: 登录后 init PageSpy 1 year ago
Lei OT f92f3f53bb 2.7.3 1 year ago
Lei OT 1025d321ef perf: GH例会: 顾问成交; 年数据用1.1-12.31; CH顾问成交用总览 1 year ago
Lei OT 00f0c1a7b1 chore: PageSpy 1 year ago
Lei OT a7280a79b9 feat: GH例会: 顾问成交; 客服; todo: 年数据用1.1-12.31; 顾问成交用总览/产品接口 1 year ago
Lei OT 512b4699ba 2.7.2 1 year ago
Lei OT 5dcdcd6345 perf: 销售业绩数据: 筛选顾问 1 year ago
Lei OT ac82432e67 perf: 销售业绩数据: 筛选顾问 1 year ago
Lei OT 35d2731560 perf: 销售业绩数据: 名称排序 1 year ago
Lei OT 513b7c7c1c 2.7.1 1 year ago
Lei OT 401d5720d8 perf: GH例会数据: 规则调整: 印度 1 year ago
Lei OT e4a63f38f6 perf: GH例会数据: 规则调整: 老客户: 仅To C 客户 1 year ago
Lei OT 1a2ee6b637 2.7.0 1 year ago
Lei OT 81ee6594c0 feat: GH 市场例会数据 1 year ago
Jimmy Liow fe29cb3d1c 企业微信接口已经不再续费,无法调用联系人和消息。下线。 1 year ago
Lei OT 8017b9c2c8 fix: 临时订单数量: 查询站点多选 1 year ago
Lei OT 5e98893ef0 2.6.5 1 year ago
Lei OT f8820d0eb5 perf: 数据透视: 增加 老客户; 老客户推荐 1 year ago
Lei OT 1bc85a8b38 perf: 查询日期范围的格式 1 year ago
YCC 16fb5b3dfc 显示出发日期 1 year ago
Lei OT a777eff3bd 2.6.4 1 year ago
Lei OT 766411cf16 perf(数据透视): 汇总表增加: 成团周期, 预定周期, 团天数 1 year ago
Lei OT 956303d9e5 fix: 导出 1 year ago
Lei OT 606c1e9983 common utils 1 year ago
Lei OT 9718c023a0 2.6.3 2 years ago
Lei OT 8fc609c1c0 fix: 数据透视: 目的地国籍 2 years ago
Lei OT d272cf6806 fix: 导出 2 years ago
Lei OT b4f869453a perf: 服务状态和403 区分 2 years ago
Lei OT d0d0cb897f style: 地图: 红色 2 years ago
Lei OT 08b19ee3a9 2.6.2 2 years ago
Lei OT 3f62a59648 conf: 测试地址: 144 2 years ago
Lei OT 8484beba64 feat: 数据透视: +人均天计算 2 years ago
Lei OT 528a950b22 perf: 看板: 增加目的地国籍地图 2 years ago
Lei OT 3ba2b44daa perf: 文旅局服务人数: 解决小数点问题 2 years ago
Lei OT 8caf1a5fb8 2.6.1 2 years ago
Lei OT 18dcfe3759 perf: 文旅局服务人数 2 years ago
Lei OT d80ecf398b perf: 文旅局服务人数 2 years ago
YCC 43780b5fc2 删除虚拟控制台,防止页面卡死 2 years ago
Lei OT 805804ee88 文旅局服务人数 2 years ago
Lei OT 151dcff1a8 feat: 文旅局服务人数 2 years ago
Lei OT ea7048e41b Merge branch 'main' into feature/person-num
# Conflicts:
#	src/App.jsx
2 years ago
Lei OT 9695335b30 数据透视: 目的地 2 years ago
Lei OT 5dafa48ab3 perf: 数据透视: +目的地国籍. 途径的计算会重复 2 years ago
Lei OT e59c1e11bd 2.4.5 2 years ago
Lei OT 95e799cdae perf: 客运订单: +页面类型; 不显示在华 2 years ago
Lei OT d141e0fa7d perf: 优化导航 2 years ago
Lei OT 609b6761b3 perf: 统计分布: 目的地/国籍等, 只要前30 2 years ago
Lei OT a48bbf35f3 perf: 移动端 2 years ago
Lei OT 6eea280a1d 2.4.4 2 years ago
Lei OT cfb4a5f5f7 perf: 移动端折叠导航 2 years ago
Lei OT ad91ce73a0 2.4.3 2 years ago
Lei OT f39ed273c3 perf: 销售业绩: 合并相同名字的账户 2 years ago
Lei OT c8e66c12d8 perf: 统计分布: 目的地/国籍等, 分开请求; 显示预定团数 2 years ago
Lei OT 95ec1fa822 fix: 年度对比: 折线中断 2 years ago
Lei OT c17f8e8f85 fix: 手机端内核过低报错. 69.0.3497.100 2 years ago
Lei OT e2423c1b85 conf: build.js 2 years ago
Lei OT f67fd5da31 fix: 销售进度: 小组的KPI曲线 2 years ago
Lei OT d90a2cd90d 2.4.2 2 years ago
Lei OT a7ecec79d6 权限 2 years ago
Lei OT 96b401b8b9 2.4.2-beta.0 2 years ago
Lei OT 8847d03fc1 test: 权限 2 years ago
Lei OT c7e16af0aa style: 年度对比 2 years ago
Lei OT 77f9e67c08 2.4.1 2 years ago
Lei OT 72dd9ef590 style: 年度对比 2 years ago
Lei OT 3f35bbfade perf: 业绩KPI折现图的图例 2 years ago
Lei OT fd57a7c3bd 年度对比 2 years ago
Lei OT 9e87278cef feat: 看板: 年度对比; 切换groupType 2 years ago
Lei OT 6ea675f453 feat: 看板: 总额年份对比 2 years ago
Lei OT 7926ce3efc 2.3.5 2 years ago
Lei OT 03655b5e5d style: 折线图: 不要平滑 2 years ago
Lei OT 0d3b170f01 perf: 数据透视: 增加数据项显示 2 years ago
Lei OT aa452b2f26 perf: 客服>地接: 隐藏提交日期; fix: 导出 2 years ago
Lei OT 40c8f37ff8 2.3.4 2 years ago
Lei OT edbd4f0dd6 fix: 移动成交: 不查询"确认日期" 2 years ago
Lei OT 01748ecb71 fix: 客运页面: 查询的日期类型的赋值 2 years ago
Lei OT 30d185eb3c 2.3.3 2 years ago
Lei OT c27690cdd1 style: 权限申请指引 2 years ago
Lei OT 5068f3409b feat: 综合看板: 去年总数据对比 2 years ago
Lei OT 230670e43a perf: 统计分布: 图表的图例 2 years ago
Lei OT c9c7638cf8 perf: 统计分布: 导出 2 years ago
Lei OT 7a0b38c8d0 fix: pivot页面的key 2 years ago
Lei OT bfc953a211 Merge branch 'feature/pivot'
# Conflicts:
#	src/components/Data.jsx
#	src/views/DataPivot.jsx
2 years ago
Lei OT 24a85d9b38 feat: 业绩透视 2 years ago
Lei OT 168f63e2d9 perf: 订单透视: 增加: 渠道类型,线路 2 years ago
Lei OT f6e6dc4c98 2.3.2 2 years ago
Lei OT 13fad7dfc5 fix: 导出 2 years ago
Lei OT 8228e07209 perf: 预设; 导出 2 years ago
Lei OT 42f4cbd134 perf: 销售进度: 导出列表; 小组多选 2 years ago
Lei OT 1aa6bfce18 优化权限申请引导 2 years ago
Lei OT e2ae31d339 perf: 权限申请的引导 2 years ago
Lei OT eb686fae86 数据透视 2 years ago
Lei OT 06d1eaa1c9 conf: 删除包 2 years ago
Lei OT 6a7c0a2064 Merge branch 'feature/pivot' 2 years ago
Lei OT cfeecf4af5 perf: 透视计算速度 2 years ago
Lei OT c878167dc9 perf: 销售>业绩数据: 排序 2 years ago
Lei OT 494dde5276 feat: 透视图表
透视表字段操作
2 years ago
Lei OT 46e6dfcf1e style: 搜索组件吸顶 2 years ago
Lei OT 7eb57a0ffd feat: 权限申请链接 2 years ago
Lei OT 27d0f48315 style: table, annual 2 years ago
Lei OT 9593e62573 fix: search input label filter 2 years ago
Lei OT 51ae4c9cea perf: 订单>仪表盘-移动订单: 搜索组件 2 years ago
Lei OT 40b07c503e 优化移动端等 2 years ago
Lei OT f5dc044469 perf: 客服>*: 搜索组件 2 years ago
Lei OT e0f55ddeb8 perf: 信用卡账单: 搜索组件 2 years ago
Lei OT a9c21a54e6 perf: 移动端表格调整 2 years ago
Lei OT e9bccf450d perf: 订单>仪表盘: 移动端调整 2 years ago
Lei OT 2a85a989df fix: 来源多选的默认值 2 years ago
Lei OT ede377dcca perf: 订单>子维度: 搜索多选 2 years ago
Lei OT 150b42418f perf: 订单>子维度: 表格导出 2 years ago
Lei OT e34f769723 perf: 销售>业绩数据: 来源多选 2 years ago
Lei OT 67723c55eb fix: 市场>订单: 产品类型 2 years ago
Lei OT 43750214be perf: 市场>订单数据: 小组多选 2 years ago
Lei OT 31aa3da60b conf: 编译时间,加点浮动 2 years ago
Lei OT 633935b5c8 fix: 部分页面水印无覆盖 2 years ago
Lei OT b669943f07 2.2.0 2 years ago
Lei OT e8e4efe796 perf: 应用搜索组件: 客运, 订单 2 years ago
Lei OT 077b3b3108 perf: 销售>业绩数据. 应用搜索组件 2 years ago
Lei OT fee2a327f8 perf: 市场>订单数据. 应用搜索组件 2 years ago
Lei OT e19733caa9 perf: 增加登录身份水印 2 years ago
Lei OT 9d1a08f92a perf: 年度总额的进度条样式; KPI设置的搜索组件初始化 2 years ago
Lei OT 5f68801a3b add compile date time 2 years ago
Lei OT de6feb85f5 perf: 销售业绩: 小组多选 2 years ago
Lei OT 619d5c25ed 2.1.1 2 years ago
Lei OT 1a864c00a8 Merge remote-tracking branch 'origin/main' 2 years ago
Lei OT 662e5ac2ae conf: 调整路由权限 2 years ago
YCC 23de7481bd 默认权限设置为空 2 years ago
Lei OT 21e70b9dfb style: 首页: TOP条形图: 负数红色 2 years ago
Lei OT 34080fce8b fix: 首页: 走势两个Y轴不一致 2 years ago
Lei OT 34bb49f487 feat: 首页: 地图显示国籍业绩 2 years ago
Lei OT 84722e0d0a style: 2 years ago
Lei OT 7af50f0e9e feat: 财务>文旅局服务人数 2 years ago
Lei OT 1c2c7c28c6 fix: 首页: TOP: 成交率 2 years ago
Lei OT 432d679517 perf: 调整统计分布的图表 2 years ago
Lei OT 479531c82c fix: 首页: TOP: 成交率 2 years ago
Lei OT f3b6b1ed5a KPI 设置: 计算累计 2 years ago
Lei OT 3e3e7f1485 顾问: 销售进度: 实际完成=传统+商务, 对比KPI 2 years ago
Lei OT 2942a61c8e 顾问: 销售进度: 显示全年的KPI 2 years ago
Lei OT c3194869fd 顾问: 销售进度: 图表 2 years ago
Lei OT 64898b7533 perf: 数据项组件: +人数 2 years ago
Lei OT b6df3c17f7 conf: 页面权限: KPI; 统计分布; 2 years ago
Lei OT 9d2d04cdc5 feat: 统计分布: +客人国籍 2 years ago
Lei OT 3198226430 首页TOP: 增加商务订单数据 2 years ago
Lei OT a225c1bbc4 统计分布的图表 2 years ago

@ -1 +1 @@
npm run build npm run build

@ -0,0 +1,7 @@
process.env.REACT_APP_BUILD_TIME = new Date().getTime()+(5*60*1000);
require('child_process').execSync(
'react-scripts build',
{ stdio: 'inherit' }
);

33
package-lock.json generated

@ -1,17 +1,18 @@
{ {
"name": "haina-dashboard", "name": "haina-dashboard",
"version": "0.1.0", "version": "2.11.6",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "haina-dashboard", "name": "haina-dashboard",
"version": "0.1.0", "version": "2.11.6",
"dependencies": { "dependencies": {
"@ant-design/charts": "^1.4.2", "@ant-design/charts": "^1.4.2",
"@ant-design/pro-components": "^2.6.16", "@ant-design/pro-components": "^2.6.16",
"antd": "^4.22.6", "antd": "^4.22.6",
"dingtalk-jsapi": "^3.0.9", "dingtalk-jsapi": "^3.0.9",
"insert-css": "^2.0.0",
"mobx": "^6.6.1", "mobx": "^6.6.1",
"mobx-react": "^7.5.2", "mobx-react": "^7.5.2",
"react": "^18.2.0", "react": "^18.2.0",
@ -7283,9 +7284,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001458", "version": "1.0.30001553",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz",
"integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==" "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A=="
}, },
"node_modules/case-sensitive-paths-webpack-plugin": { "node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0", "version": "2.4.0",
@ -11538,7 +11539,7 @@
}, },
"node_modules/insert-css": { "node_modules/insert-css": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/insert-css/-/insert-css-2.0.0.tgz", "resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz",
"integrity": "sha512-xGq5ISgcUP5cvGkS2MMFLtPDBtrtQPSFfC6gA6U8wHKqfjTIMZLZNxOItQnoSjdOzlXOLU/yD32RKC4SvjNbtA==" "integrity": "sha512-xGq5ISgcUP5cvGkS2MMFLtPDBtrtQPSFfC6gA6U8wHKqfjTIMZLZNxOItQnoSjdOzlXOLU/yD32RKC4SvjNbtA=="
}, },
"node_modules/internal-slot": { "node_modules/internal-slot": {
@ -19362,9 +19363,9 @@
} }
}, },
"node_modules/stylis": { "node_modules/stylis": {
"version": "4.1.1", "version": "4.2.0",
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==" "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
}, },
"node_modules/supercluster": { "node_modules/supercluster": {
"version": "7.1.5", "version": "7.1.5",
@ -26806,9 +26807,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001458", "version": "1.0.30001553",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz",
"integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==" "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A=="
}, },
"case-sensitive-paths-webpack-plugin": { "case-sensitive-paths-webpack-plugin": {
"version": "2.4.0", "version": "2.4.0",
@ -29963,7 +29964,7 @@
}, },
"insert-css": { "insert-css": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/insert-css/-/insert-css-2.0.0.tgz", "resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz",
"integrity": "sha512-xGq5ISgcUP5cvGkS2MMFLtPDBtrtQPSFfC6gA6U8wHKqfjTIMZLZNxOItQnoSjdOzlXOLU/yD32RKC4SvjNbtA==" "integrity": "sha512-xGq5ISgcUP5cvGkS2MMFLtPDBtrtQPSFfC6gA6U8wHKqfjTIMZLZNxOItQnoSjdOzlXOLU/yD32RKC4SvjNbtA=="
}, },
"internal-slot": { "internal-slot": {
@ -35567,9 +35568,9 @@
} }
}, },
"stylis": { "stylis": {
"version": "4.1.1", "version": "4.2.0",
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==" "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
}, },
"supercluster": { "supercluster": {
"version": "7.1.5", "version": "7.1.5",

@ -1,12 +1,13 @@
{ {
"name": "haina-dashboard", "name": "haina-dashboard",
"version": "0.1.0", "version": "2.11.6",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/charts": "^1.4.2", "@ant-design/charts": "^1.4.2",
"@ant-design/pro-components": "^2.6.16", "@ant-design/pro-components": "^2.6.16",
"antd": "^4.22.6", "antd": "^4.22.6",
"dingtalk-jsapi": "^3.0.9", "dingtalk-jsapi": "^3.0.9",
"insert-css": "^2.0.0",
"mobx": "^6.6.1", "mobx": "^6.6.1",
"mobx-react": "^7.5.2", "mobx-react": "^7.5.2",
"react": "^18.2.0", "react": "^18.2.0",
@ -18,7 +19,7 @@
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "node build.js",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint": "eslint ./src", "lint": "eslint ./src",

@ -25,10 +25,20 @@
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Hainatravel Dashboard</title> <title>Hainatravel Dashboard</title>
<script crossorigin="anonymous" src="https://page-spy.mycht.cn/page-spy/index.min.js"></script>
<script crossorigin="anonymous" src="https://page-spy.mycht.cn/plugin/data-harbor/index.min.js"></script>
<script crossorigin="anonymous" src="https://page-spy.mycht.cn/plugin/rrweb/index.min.js"></script>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script defer>
window.initPageSpy = function () {
PageSpy.registerPlugin(new DataHarborPlugin({ maximum: 2 * 1024 * 1024, }));
window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: 'Dashboard', title: window.__spytitle, autoRender: false, });
// console.log(window.$pageSpy.address.substring(0, 4)); // device ID
}
</script>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.

@ -32,5 +32,12 @@
padding: 0; padding: 0;
} }
.p-s1{ .p-s1{
padding: .5em; padding: .5rem!important;
}
.sticky-top{
margin: -16px -8px .5em -8px;
position: sticky;
top: 0;
z-index: 100;
} }

@ -1,6 +1,6 @@
import './App.css'; import './App.css';
import React, { useContext } from 'react'; import React, { useContext, useState } from 'react';
import { import Icon, {
HomeOutlined, HomeOutlined,
TeamOutlined, TeamOutlined,
DashboardOutlined, DashboardOutlined,
@ -10,9 +10,14 @@ import {
DollarOutlined, DollarOutlined,
AreaChartOutlined, AreaChartOutlined,
WechatOutlined, WechatOutlined,
UserOutlined, FlagOutlined, PieChartOutlined, BarChartOutlined UserOutlined,
FlagOutlined,
PieChartOutlined,
BarChartOutlined,
CoffeeOutlined, DesktopOutlined,
WhatsAppOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Layout, Menu, Image, Badge } from 'antd'; import { Layout, Menu, Image, Badge, Button } from 'antd';
import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom'; import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
import Home from './views/Home'; import Home from './views/Home';
import Dashboard from './views/Dashboard'; import Dashboard from './views/Dashboard';
@ -32,20 +37,52 @@ import Credit_card_bill from './views/Credit_card_bill';
import Sale from './views/Sale'; import Sale from './views/Sale';
import Sale_sub from './views/Sale_sub'; import Sale_sub from './views/Sale_sub';
import Sale_KPI from './views/Sale_KPI'; import Sale_KPI from './views/Sale_KPI';
import Logo from './logo.png'; // import Logo from './logo.png';
import { stores_Context } from './config';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import ExchangeRate from './charts/ExchangeRate'; import ExchangeRate from './charts/ExchangeRate';
import KPI from './views/KPI'; import KPI from './views/KPI';
import Distribution from './views/Distribution'; import Distribution from './views/Distribution';
import Detail from './views/Detail'; import Detail from './views/Detail';
import ServicePersonNum from './views/ServicePersonNum';
import DataPivot from './views/DataPivot';
import Welcome from './views/Welcome';
import Meeting2024GH from './views/Meeting2024-GH';
import Meeting2025GH from './views/Meeting2025-GH';
import SalesCustomerCareRegular from './views/SalesCustomerCareRegular';
import { stores_Context, APP_VERSION } from './config';
import { WaterMark } from '@ant-design/pro-components';
import CooperationIcon from './components/icons/CooperationIcon';
import OPDashboard from './views/sales-crm/Dashboard';
import OPProcess from './views/sales-crm/Process';
import OPRisk from './views/sales-crm/Risk';
import Cruise from './views/Cruise';
import Hotel from './views/Hotel';
import HostCaseCount from './views/HostCaseCount';
const App = () => { const App = () => {
const { Content, Footer, Sider } = Layout; const { Content, Footer, Sider, } = Layout;
const { auth_store } = useContext(stores_Context); const { auth_store, date_picker_store } = useContext(stores_Context);
const [collapsed, setCollapsed] = useState(false);
const menu_items = [ const menu_items = [
{ key: 1, label: <NavLink to="/">主页</NavLink>, icon: <HomeOutlined /> }, { key: 1, label: <NavLink to="/">欢迎</NavLink>, icon: <CoffeeOutlined /> },
{ key: 'annual', label: <NavLink to="/annual">综合年度</NavLink>, icon: <DashboardOutlined /> },
{
key: 'mixed',
label: '报告',
icon: <DesktopOutlined />,
children: [
{
key: 'meeting-2025-GH',
label: <NavLink to="/orders/meeting-2025-GH">GH例会数据-2025</NavLink>, // GH-2024
},
{
key: 'meeting-2024-GH',
label: <NavLink to="/orders/meeting-2024-GH">GH区域数据</NavLink>, // GH-2024
},
]
},
{ {
key: 2, key: 2,
label: '市场', label: '市场',
@ -54,12 +91,16 @@ const App = () => {
{ {
key: 21, key: 21,
label: <NavLink to="/orders">订单数据</NavLink>, label: <NavLink to="/orders">订单数据</NavLink>,
icon: <FileProtectOutlined />, // icon: <FileProtectOutlined />,
}, },
{ {
key: 22, key: 22,
label: <NavLink to="/dashboard">仪表盘</NavLink>, label: <NavLink to="/dashboard">仪表盘</NavLink>,
icon: <DashboardOutlined />, // icon: <DashboardOutlined />,
},
{
key: 'orders-pivot',
label: <NavLink to="/orders/pivot">数据透视</NavLink>,
}, },
], ],
}, },
@ -67,7 +108,12 @@ const App = () => {
key: 5, key: 5,
label: '销售', label: '销售',
icon: <SnippetsTwoTone />, icon: <SnippetsTwoTone />,
children: [{ key: 51, label: <NavLink to="/sale">业绩数据</NavLink> },{ key: 52, label: <NavLink to="/sale_kpi">销售进度</NavLink> }], children: [
{ key: 51, label: <NavLink to="/sale">业绩数据</NavLink> },
{ key: 52, label: <NavLink to="/sale_kpi">销售进度</NavLink> },
{ key: 'distribution', label: <NavLink to="/distribution">统计分布</NavLink> },
{ key: 'trade-pivot', label: <NavLink to="/trade/pivot">数据透视</NavLink> },
],
}, },
{ {
key: 3, key: 3,
@ -82,18 +128,11 @@ const App = () => {
key: 32, key: 32,
label: <NavLink to="/customer_care_regular">老客户</NavLink>, label: <NavLink to="/customer_care_regular">老客户</NavLink>,
}, },
{ key: 'customer_care_regular_sales', label: <NavLink to="/customer_care_regular_sales">老客户-分析</NavLink> },
{ {
key: 33, key: 33,
label: <NavLink to="/customer_care_inchina">在华客户</NavLink>, label: <NavLink to="/customer_care_inchina">在华客户</NavLink>,
}, },
{
key: 34,
label: <NavLink to="/wechat_session">微信会话存档</NavLink>,
},
{
key: 35,
label: <NavLink to="/whatsapp_session">WhatsApp会话存档</NavLink>,
},
], ],
}, },
{ {
@ -106,12 +145,13 @@ const App = () => {
label: <NavLink to="/credit_card_bill">信用卡账单</NavLink>, label: <NavLink to="/credit_card_bill">信用卡账单</NavLink>,
}, },
{ key: 42, label: <NavLink to="/exchange_rate">汇率</NavLink> }, { key: 42, label: <NavLink to="/exchange_rate">汇率</NavLink> },
{ key: 'service_person_num', label: <NavLink to="/service_person_num">服务人数</NavLink> },
], ],
}, },
{ {
key: 6, key: 6,
label: '客服', label: '客服',
icon: <WechatOutlined />, icon: <CooperationIcon/>,
children: [ children: [
{ {
key: 61, key: 61,
@ -121,10 +161,23 @@ const App = () => {
key: 62, key: 62,
label: <NavLink to="/destination/group/count">目的地接团</NavLink>, label: <NavLink to="/destination/group/count">目的地接团</NavLink>,
}, },
{ key: 'cruise', label: <NavLink to="/cruise">三峡游船</NavLink> },
{ key: 'hotel', label: <NavLink to="/hotel">酒店</NavLink> },
{ key: 'hostcase', label: <NavLink to="/hostcase/count">东道主项目</NavLink> },
],
},
{
key: 'crm',
label: '销售平台',
icon: <WhatsAppOutlined />,
children: [
// { key: 'xx', type: 'divider' },
{ key: 'op_dashboard', label: <NavLink to="/op_dashboard">结果</NavLink> },
{ key: 'op_process', label: <NavLink to="/op_process">过程</NavLink> },
// { key: 'op_risk', label: <NavLink to="/op_risk"></NavLink> },
], ],
}, },
{ key: 'kpi', label: <NavLink to="/kpi">目标</NavLink>, icon: <FlagOutlined /> }, { key: 'kpi', label: <NavLink to="/kpi">目标配置</NavLink>, icon: <FlagOutlined /> },
{ key: 'distribution', label: <NavLink to="/distribution">统计分布</NavLink>, icon: <PieChartOutlined /> },
// { // {
// key: 'detail', // key: 'detail',
// label: ( // label: (
@ -136,85 +189,122 @@ const App = () => {
// }, // },
]; ];
const callDebug = () => {
// const vConsole = new window.VConsole({ theme: 'dark' });
// auth_store.get_auth();
// window.$pageSpy.render();
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
window.$pageSpy.triggerPlugins('onOfflineLog', 'download');
};
return ( return (
<BrowserRouter> <BrowserRouter>
<Layout <WaterMark content={['HT统计']} gapY={100} gapX={100} rotate={-20} offsetLeft={150} offsetTop={150} fontColor="rgba(0,0,0,.06)" zIndex={20}>
hasSider <WaterMark content={[auth_store.user.name, auth_store.user.userid.slice(-4)]} gapY={100} gapX={100} rotate={-20} fontColor="rgba(0,0,0,.08)" zIndex={20}>
style={{ <Layout
minHeight: '100vh', hasSider
}}
>
<Sider
collapsible={true}
breakpoint="lg"
style={{
overflow: 'auto',
height: '100vh',
position: 'sticky',
left: 0,
top: 0,
bottom: 0,
}}
>
<Image src={Logo} preview={false} />
<Menu
theme="dark"
defaultSelectedKeys={['1']}
defaultOpenKeys={['2', '5']}
mode="inline"
items={menu_items}
/>
</Sider>
<Layout className="site-layout">
<Content
style={{
padding: 16,
minHeight: 480,
}}
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/kpi" element={<KPI />} />
<Route path="/distribution" element={<Distribution />} />
<Route path="/detail" element={<Detail />} />
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'marketing']} />}>
<Route path="/orders" element={<Orders />} />
<Route path="/orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<Orders_sub />} />
<Route path="/dashboard" element={<Dashboard />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'customer_care']} />}>
<Route path="/customer_care_inchina" element={<Customer_care_inchina />} />
<Route path="/customer_care_regular" element={<Customer_care_regular />} />
<Route path="/customer_care_potential" element={<Customer_care_potential />} />
<Route path="/whatsapp_session" element={<WhatsApp_session />} />
<Route path="/wechat_session" element={<Wechat_session />} />
<Route path="/agent/group/count" element={<AgentGroupCount />} />
<Route path="/destination/group/count" element={<DestinationGroupCount />} />
<Route path="/agent/:agentId/group/list" element={<AgentGroupList />} />
<Route path="/destination/:destinationId/group/list" element={<DestinationGroupList />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'financial']} />}>
<Route path="/credit_card_bill" element={<Credit_card_bill />} />
<Route path="/exchange_rate" element={<ExchangeRate />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'sale']} />}>
<Route path="/sale" element={<Sale />} />
<Route path="/sale_sub/:type_sub" element={<Sale_sub />} />
<Route path="/sale_kpi" element={<Sale_KPI />} />
</Route>
</Routes>
</Content>
<Footer
style={{ style={{
textAlign: 'center', minHeight: '100vh',
}} }}
> >
<UserOutlined /> {auth_store.user.name} ({auth_store.user.userid}) <Sider
<br /> collapsible={true}
Hainatravel Dashboard ©2022 Created by IT breakpoint="lg"
</Footer> collapsedWidth="0"
</Layout> collapsed={collapsed}
</Layout> style={{
// overflow: 'auto',
height: '100vh',
position: 'sticky',
left: 0,
top: 0,
bottom: 0,
zIndex: 999,
}}
zeroWidthTriggerStyle={collapsed ? { zIndex: 90, bottom: '64px', top: '3px', left: 'unset', right: '-34px' } : { bottom: '64px', top: '3px', left: 'unset', right: 0 }}
onBreakpoint={(broken) => {
date_picker_store.setSiderBroken(broken);
}}
onCollapse={(collapsed, type) => {
setCollapsed(collapsed);
}}
>
{/* <Image src={Logo} preview={false} /> */}
<Menu theme="dark" defaultSelectedKeys={['1']} defaultOpenKeys={[]} mode="inline" items={menu_items} onClick={() => date_picker_store.siderBroken ? setCollapsed(!collapsed) : false} />
</Sider>
<Layout className="site-layout">
<Content
style={{
padding: 16,
minHeight: 480,
}}
>
<Routes>
<Route path="/" element={<Welcome />} />
<Route path="/detail" element={<Detail />} />
<Route element={<ProtectedRoute auth={['admin', 'director_bu']} />}>
<Route path="/annual" element={<Home />} />
<Route path="/kpi" element={<KPI />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'marketing']} />}>
<Route path="/orders" element={<Orders />} />
<Route path="/orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<Orders_sub />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/orders/meeting-2024-GH" element={<Meeting2024GH />} />
<Route path="/orders/meeting-2025-GH" element={<Meeting2025GH />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'customer_care']} />}>
<Route path="/customer_care_inchina" element={<Customer_care_inchina />} />
<Route path="/customer_care_regular" element={<Customer_care_regular />} />
<Route path="/customer_care_regular_sales" element={<SalesCustomerCareRegular />} />
<Route path="/customer_care_potential" element={<Customer_care_potential />} />
<Route path="/whatsapp_session" element={<WhatsApp_session />} />
<Route path="/wechat_session" element={<Wechat_session />} />
<Route path="/agent/group/count" element={<AgentGroupCount />} />
<Route path="/destination/group/count" element={<DestinationGroupCount />} />
<Route path="/agent/:agentId/group/list" element={<AgentGroupList />} />
<Route path="/destination/:destinationId/group/list" element={<DestinationGroupList />} />
<Route path="/cruise" element={<Cruise />} />
<Route path="/hotel" element={<Hotel />} />
<Route path="/hostcase/count" element={<HostCaseCount />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'financial']} />}>
<Route path="/credit_card_bill" element={<Credit_card_bill />} />
<Route path="/exchange_rate" element={<ExchangeRate />} />
<Route path="/service_person_num" element={<ServicePersonNum />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'sale']} />}>
<Route path="/sale" element={<Sale />} />
<Route path="/sale_sub/:type_sub" element={<Sale_sub />} />
<Route path="/sale_kpi" element={<Sale_KPI />} />
<Route path="/distribution" element={<Distribution />} />
<Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/op_dashboard" element={<OPDashboard />} />
<Route path="/op_process" element={<OPProcess />} />
<Route path="/op_risk" element={<OPRisk />} />
<Route path="/op_risk/sales/:opisn" element={<OPRisk />} />
</Route>
</Routes>
</Content>
<Footer
style={{
textAlign: 'center',
}}
>
<UserOutlined /> {auth_store.user.name} ({auth_store.user.userid})
<br />
Hainatravel Dashboard{' '}
<span>
v<span>{APP_VERSION}</span>{' '}
</span>{' '}
©2022 <span > Created by IT</span>
{window.$pageSpy && <Button type='link' onClick={callDebug}>上传/下载debug日志({window.$pageSpy.address.substring(0, 4)})</Button>}
</Footer>
</Layout>
</Layout>
</WaterMark>
</WaterMark>
</BrowserRouter> </BrowserRouter>
); );
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

@ -1,19 +1,9 @@
import React, {useContext, useEffect} from 'react'; import React, {useContext, useEffect} from 'react';
import {Row, Col, Button, Divider, Table, Space, Radio, Tooltip} from 'antd'; import {Row, Col, Divider, Table} from 'antd';
import {
ContainerOutlined,
SearchOutlined,
} from '@ant-design/icons';
import {stores_Context} from '../config'; import {stores_Context} from '../config';
import {Line} from "@ant-design/charts";
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import DatePickerCharts from '../components/search/DatePickerCharts';
import {NavLink, useParams} from "react-router-dom";
import * as comm from "../utils/commons";
import * as config from "../config";
import SiteSelect from "../components/search/SiteSelect";
import GroupSelect from "../components/search/GroupSelect";
import {utils, writeFileXLSX} from "xlsx"; import {utils, writeFileXLSX} from "xlsx";
import SearchForm from './../components/search/SearchForm';
const Customer_care_inchina = () => { const Customer_care_inchina = () => {
@ -27,38 +17,35 @@ const Customer_care_inchina = () => {
return ( return (
<div> <div>
<Row> <Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col span={8}> <Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...inchina_data.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'inchina_data');
customer_store.inchina_customer_order();
customer_store.inchina_customer_order(true);
}}
/>
</Col>
</Row>
<Row gutter={16} >
<Col span={24}>
<h2>在华客人</h2> <h2>在华客人</h2>
</Col> </Col>
<Col span={15}>
<Row>
<Col span={6}>
<SiteSelect store={inchina_data} show_all={true}/>
</Col>
<Col span={18}> <GroupSelect store={inchina_data}/></Col>
<Col span={24}> <Space>
<DatePickerCharts hide_vs={true}/>
<Radio.Group value={inchina_data.date_type}
onChange={inchina_data.onChange_datetype}>
<Radio value="applyDate">提交日期</Radio>
<Radio value="startDate">出发日期</Radio>
<Radio value="ConfirmDate">确认日期</Radio>
</Radio.Group>
<Button type="primary" icon={<SearchOutlined/>} loading={inchina_data.loading}
onClick={() => {
inchina_data.inchina_customer_order();
inchina_data.inchina_customer_order(true);
}}>统计</Button>
</Space>
</Col>
</Row>
</Col>
<Col span={1}></Col>
<Col span={24}> <Col span={24}>
<Table dataSource={inchina_data.data} columns={ <Table dataSource={inchina_data.data} loading={inchina_data.loading} columns={
[ [
{ {
title: '统计条目', title: '统计条目',
@ -101,7 +88,7 @@ const Customer_care_inchina = () => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx").getElementsByTagName('table')[0]); const wb = utils.table_to_book(document.getElementById("table_to_xlsx").getElementsByTagName('table')[0]);
writeFileXLSX(wb, "在华客人.xlsx"); writeFileXLSX(wb, "在华客人.xlsx");
}}>导出excel</a></Divider> }}>导出excel</a></Divider>
<Table id="table_to_xlsx" pagination={false} dataSource={inchina_data.data_detail} columns={ <Table id="table_to_xlsx" pagination={false} loading={inchina_data.loading} dataSource={inchina_data.data_detail} scroll={{x: 1200 }} columns={
[ [
{ {
title: '订单号', title: '订单号',

@ -1,19 +1,9 @@
import React, {useContext, useEffect} from 'react'; import React, {useContext, useEffect} from 'react';
import {Row, Col, Button, Divider, Table, Space, Radio, Tooltip} from 'antd'; import {Row, Col, Divider, Table} from 'antd';
import {
ContainerOutlined,
SearchOutlined,
} from '@ant-design/icons';
import {stores_Context} from '../config'; import {stores_Context} from '../config';
import {Line} from "@ant-design/charts";
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import DatePickerCharts from '../components/search/DatePickerCharts';
import {NavLink, useParams} from "react-router-dom";
import * as comm from "../utils/commons";
import * as config from "../config";
import SiteSelect from "../components/search/SiteSelect";
import GroupSelect from "../components/search/GroupSelect";
import {utils, writeFileXLSX} from "xlsx"; import {utils, writeFileXLSX} from "xlsx";
import SearchForm from './../components/search/SearchForm';
const Customer_care_potential = () => { const Customer_care_potential = () => {
@ -21,43 +11,41 @@ const Customer_care_potential = () => {
const potential_data = customer_store.potential_data; const potential_data = customer_store.potential_data;
useEffect(() => { useEffect(() => {
}, []); }, []);
return ( return (
<div> <div>
<Row> <Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col span={8}> <Col className="gutter-row" span={24}>
<SearchForm className={date_picker_store.siderBroken ? "" : "sticky-top"}
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...potential_data.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: true, mode: 'multiple' },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'potential_data');
customer_store.potential_customer_order();
customer_store.potential_customer_order(true);
}}
/>
</Col>
</Row>
<Row gutter={16} >
<Col span={24}>
<h2>潜力客户</h2> <h2>潜力客户</h2>
</Col> </Col>
<Col span={15}> {/* <Col span={1}></Col> */}
<Row>
<Col span={6}>
<SiteSelect store={potential_data} show_all={true}/>
</Col>
<Col span={18}> <GroupSelect store={potential_data}/></Col>
<Col span={24}> <Space>
<DatePickerCharts hide_vs={true}/>
<Radio.Group value={potential_data.date_type}
onChange={potential_data.onChange_datetype}>
<Radio value="applyDate">预定日期</Radio>
<Radio value="startDate">出发日期</Radio>
<Radio value="ConfirmDate">确认日期</Radio>
</Radio.Group>
<Button type="primary" icon={<SearchOutlined/>} loading={potential_data.loading}
onClick={() => {
potential_data.potential_customer_order();
potential_data.potential_customer_order(true);
}}>统计</Button>
</Space>
</Col>
</Row>
</Col>
<Col span={1}></Col>
<Col span={24}> <Col span={24}>
<Table dataSource={potential_data.data} columns={ <Table dataSource={potential_data.data} loading={potential_data.loading} columns={
[ [
{ {
title: '订单数', title: '订单数',
@ -95,7 +83,7 @@ const Customer_care_potential = () => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx").getElementsByTagName('table')[0]); const wb = utils.table_to_book(document.getElementById("table_to_xlsx").getElementsByTagName('table')[0]);
writeFileXLSX(wb, "潜力客户.xlsx"); writeFileXLSX(wb, "潜力客户.xlsx");
}}>导出excel</a></Divider> }}>导出excel</a></Divider>
<Table id="table_to_xlsx" pagination={false} dataSource={potential_data.data_detail} columns={ <Table id="table_to_xlsx" pagination={false} loading={potential_data.loading} dataSource={potential_data.data_detail} scroll={{x: 1200 }} columns={
[ [
{ {
title: '订单号', title: '订单号',
@ -165,9 +153,9 @@ const Customer_care_potential = () => {
key: 'SourceType', key: 'SourceType',
}, },
{ {
title: '在华', title: '页面类型',
dataIndex: 'ZH', dataIndex: 'COLI_LineClass',
key: 'ZH', key: 'COLI_LineClass',
}, },
] ]
} size="small" } size="small"

@ -1,189 +1,249 @@
import React, {useContext, useEffect} from 'react'; import React, { useContext, useState } from 'react';
import {Row, Col, Button, Divider, Table, Space, Radio, Tooltip} from 'antd'; import { Row, Col, Divider, Table, Tooltip } from 'antd';
import {read, utils, writeFileXLSX} from 'xlsx'; import { InfoCircleOutlined } from '@ant-design/icons';
import { import { utils, writeFileXLSX } from 'xlsx';
ContainerOutlined, import { stores_Context } from '../config';
SearchOutlined, import { observer } from 'mobx-react';
} from '@ant-design/icons'; import SearchForm from './../components/search/SearchForm';
import {stores_Context} from '../config'; import LineWithAvg from '../components/LineWithAvg';
import {Line} from "@ant-design/charts"; import { flow } from 'mobx';
import {observer} from 'mobx-react';
import DatePickerCharts from '../components/search/DatePickerCharts';
import {NavLink, useParams} from "react-router-dom";
import * as comm from "../utils/commons";
import * as config from "../config";
import SiteSelect from "../components/search/SiteSelect";
import GroupSelect from "../components/search/GroupSelect";
const Customer_care_regular = () => { const Customer_care_regular = () => {
const { orders_store, date_picker_store, customer_store } = useContext(stores_Context);
const regular_data = customer_store.regular_data;
const {orders_store, date_picker_store, customer_store} = useContext(stores_Context); // useEffect(() => {}, []);
const regular_data = customer_store.regular_data;
useEffect(() => { return (
<div>
<Row gutter={16} className={date_picker_store.siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...regular_data.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates', 'IncludeTickets'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
},
}}
onSubmit={async (_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'regular_data');
regular_data.data_compare=[];
if (obj.DateDiff1 && obj.DateDiff2){
regular_data.isCompareLine=true;
regular_data.showCompareSum=true;
await customer_store.regular_customer_order();
customer_store.regular_customer_order(false,true);
customer_store.regular_customer_order(true,false,true);
customer_store.regular_customer_order(true,true,true);
}
else{
regular_data.isCompareLine=false;
regular_data.showCompareSum=false;
customer_store.regular_customer_order();
customer_store.regular_customer_order(true);
}
}}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<h2>老客户</h2>
</Col>
<Col span={24}>
<Table
dataSource={regular_data.data}
loading={regular_data.loading}
columns={[
{
title: '统计条目',
dataIndex: 'ItemName',
key: 'ItemName',
},
{
title: () => (
<>
订单数{' '}
<Tooltip key='total_data_tips_title' title="总订单: 当同时勾选老客户和推荐时, 将重复计数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'OrderNum',
key: 'OrderNum',
render: (text, record, index) => (
<>
<span>{text}</span>&nbsp;&nbsp;
{<Tooltip key='total_data_tips' title={regular_data.total_data_tips}>
{index === 0 && regular_data.total_data_tips!=='' && <InfoCircleOutlined className='ant-tag-gold' />}
</Tooltip>}
</>
),
},
{
title: '订单数占比',
dataIndex: 'OrderRate',
key: 'OrderRate',
render: (text) => typeof text === 'number'?<span>{parseFloat((text * 100).toFixed(2))}%</span>:text,
},
{
title: '成行数',
dataIndex: 'SUCOrderNum',
key: 'SUCOrderNum',
},
{
title: '成行率',
dataIndex: 'SUCRate',
key: 'SUCRate',
render: (text) => typeof text === 'number'?<span>{Math.round(text * 100)}%</span>:text,
},
{
title: '毛利',
dataIndex: 'ML',
key: 'ML',
},
{
title: '毛利占比',
dataIndex: 'OrderMLRate',
key: 'OrderMLRate',
render: (text) => typeof text === 'number'?<span>{parseFloat((text * 100).toFixed(2))}%</span>:text,
},
{
title: '人数(含成人+儿童)',
dataIndex: 'PersonNum',
key: 'PersonNum',
},
]}
size="small"
pagination={false}
rowKey={(record) => record.ItemName}
/>
</Col>
}, []); <Col span={24}>
<LineWithAvg dataSource={regular_data.pivotData} loading={regular_data.detail_loading} xField={regular_data.pivotX} yField={regular_data.pivotY}
seriesField='_ylabel' showCompareSum={regular_data.showCompareSum} solidLineTime={regular_data.solidLineTime} solidLineCompareTime={regular_data.solidLineCompareTime}
return ( solidLineDash={regular_data.solidLineDash} isCompareLine={regular_data.isCompareLine}/>
<div> </Col>
<Row>
<Col span={8}>
<h2>老客户</h2>
</Col>
<Col span={15}>
<Row>
<Col span={6}>
<SiteSelect store={regular_data} show_all={true}/>
</Col>
<Col span={18}> <GroupSelect store={regular_data}/></Col>
<Col span={24}> <Space>
<DatePickerCharts hide_vs={true}/>
<Radio.Group value={regular_data.date_type}
onChange={regular_data.onChange_datetype}>
<Radio value="applyDate">预定日期</Radio>
<Radio value="startDate">出发日期</Radio>
<Radio value="ConfirmDate">确认日期</Radio>
</Radio.Group>
<Button type="primary" icon={<SearchOutlined/>} loading={regular_data.loading}
onClick={() => {
regular_data.regular_customer_order();
regular_data.regular_customer_order(true);
}}>统计</Button>
</Space>
</Col>
</Row>
</Col>
<Col span={1}></Col>
<Col span={24}>
<Table dataSource={regular_data.data} columns={
[
{
title: '统计条目',
dataIndex: 'ItemName',
key: 'ItemName',
},
{
title: '订单数',
dataIndex: 'OrderNum',
key: 'OrderNum',
},
{
title: '成行数',
dataIndex: 'SUCOrderNum',
key: 'SUCOrderNum',
},
{
title: '成行率',
dataIndex: 'SUCRate',
key: 'SUCRate',
render: (text, record) => <span>{Math.round(text * 100)}%</span>
},
{
title: '毛利',
dataIndex: 'ML',
key: 'ML',
},
{
title: '人数(含成人+儿童)',
dataIndex: 'PersonNum',
key: 'PersonNum',
},
]
} size="small" pagination={false} rowKey={record => record.ItemName}
/>
</Col>
<Col span={24}>
<Divider orientation="right" plain><a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx").getElementsByTagName('table')[0]);
writeFileXLSX(wb, "老客户.xlsx");
}}>导出excel</a></Divider>
<Table id="table_to_xlsx" pagination={false} dataSource={regular_data.data_detail} columns={
[
{
title: '订单号',
dataIndex: 'COLI_ID',
key: 'COLI_ID',
},
{
title: '预定日期',
dataIndex: 'COLI_ApplyDate',
key: 'COLI_ApplyDate',
},
{
title: '订单状态',
dataIndex: 'OrderState',
key: 'OrderState',
render: (text, record) => <span>{text == 1 ? '成行' : '未成行'}</span>,
sorter: (a, b) => b.OrderState - a.OrderState,
},
{
title: '毛利',
dataIndex: 'ML',
key: 'ML',
},
{
title: '人数',
dataIndex: 'PersonNum',
key: 'PersonNum',
},
{
title: '天数',
dataIndex: 'COLI_Days',
key: 'COLI_Days',
},
{
title: '人天数',
dataIndex: 'CGI_PersonDays',
key: 'CGI_PersonDays',
},
{
title: '走团日期',
dataIndex: 'COLI_OrderStartDate',
key: 'COLI_OrderStartDate',
},
{
title: '小组',
dataIndex: 'Department',
key: 'Department',
},
{
title: '老客户',
dataIndex: 'COLI_IsOld',
key: 'COLI_IsOld',
},
{
title: '老客户推荐',
dataIndex: 'COLI_IsCusCommend',
key: 'COLI_IsCusCommend',
},
{
title: '网站',
dataIndex: 'COLI_WebCode',
key: 'COLI_WebCode',
},
{
title: '来源',
dataIndex: 'SourceType',
key: 'SourceType',
},
{
title: '在华',
dataIndex: 'ZH',
key: 'ZH',
},
]
} size="small"
rowKey={record => record.COLI_ID}
/>
</Col>
</Row>
</div>
);
<Col span={24}>
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById('table_to_xlsx').getElementsByTagName('table')[0]);
writeFileXLSX(wb, '老客户.xlsx');
}}
>
导出excel
</a>
</Divider>
<Table
id="table_to_xlsx"
pagination={false}
loading={regular_data.detail_loading}
dataSource={regular_data.data_detail}
scroll={{ x: 1200 }}
columns={[
{
title: '订单号',
dataIndex: 'COLI_ID',
key: 'COLI_ID',
},
{
title: '预定日期',
dataIndex: 'COLI_ApplyDate',
key: 'COLI_ApplyDate',
},
{
title: '订单状态',width: '4rem',
dataIndex: 'OrderState',
key: 'OrderState',
render: (text, record) => <span>{text == 1 ? '成行' : '未成行'}</span>,
sorter: (a, b) => b.OrderState - a.OrderState,
},
{
title: '毛利',
dataIndex: 'ML',
key: 'ML',
},
{
title: '人数',
dataIndex: 'PersonNum',
key: 'PersonNum',
},
{
title: '天数',
dataIndex: 'COLI_Days',
key: 'COLI_Days',
},
{
title: '人天数',
dataIndex: 'CGI_PersonDays',
key: 'CGI_PersonDays',
},
{
title: '走团日期',
dataIndex: 'COLI_OrderStartDate',
key: 'COLI_OrderStartDate',
},
{
title: '小组',
dataIndex: 'Department',
key: 'Department',
},
{
title: '老客户',
dataIndex: 'COLI_IsOld',
key: 'COLI_IsOld',
},
{
title: '老客户推荐',
dataIndex: 'COLI_IsCusCommend',
key: 'COLI_IsCusCommend',
},
{
title: '网站',
dataIndex: 'COLI_WebCode',
key: 'COLI_WebCode',
},
{
title: '来源',
dataIndex: 'SourceType',
key: 'SourceType',
},
{
title: '页面类型',
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
{ title: '上次 订单号', dataIndex: 'coli_id_Last', key: 'coli_id_Last', width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
{ title: '上次 走团日期', dataIndex: 'COLI_OrderStartDate_Last', key: 'COLI_OrderStartDate_Last',width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
{ title: '上次 小组', dataIndex: 'Department_Last', key: 'Department_Last',width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
]}
size="small"
rowKey={(record) => record.COLI_ID}
/>
</Col>
</Row>
</div>
);
}; };
export default observer(Customer_care_regular); export default observer(Customer_care_regular);

@ -1,41 +1,45 @@
import React, {Component} from 'react'; import React, { Component } from 'react';
import {Table, Button, Space, Radio} from 'antd'; import { Table, } from 'antd';
import {SearchOutlined} from '@ant-design/icons'; import SearchForm from './../components/search/SearchForm';
import GroupSelect from '../components/search/GroupSelect'; import { stores_Context } from '../config';
import DatePickerCharts from "../components/search/DatePickerCharts"; import { observer } from 'mobx-react';
import {stores_Context} from "../config";
import {observer} from "mobx-react";
class MobileDeal extends Component { class MobileDeal extends Component {
static contextType = stores_Context; static contextType = stores_Context;
constructor(props) { constructor(props) {
super(props); super(props);
} }
render() { render() {
const {dashboard_store} = this.context; const { dashboard_store, date_picker_store } = this.context;
const mobile_data = dashboard_store.mobile_data; const mobile_data = dashboard_store.mobile_data;
return ( return (
<div> <div>
<h2>移动成交</h2> <h2>移动成交</h2>
<GroupSelect store={mobile_data}/> <SearchForm
<Space size="large"> defaultValue={{
<DatePickerCharts hide_vs={true}/> initialValue: {
...date_picker_store.formValues,
<Radio.Group value={mobile_data.date_type} onChange={mobile_data.onChange_datetype}> ...mobile_data.mobileSearchValues,
<Radio value="applyDate">预定日期</Radio> },
<Radio value="startDate">出发日期</Radio> shows: ['DateType', 'DepartmentList', 'dates'],
</Radio.Group> fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple', col: 24 },
<Button type="primary" icon={<SearchOutlined/>} loading={mobile_data.loading} onClick={() => { WebCode: { show_all: true },
mobile_data.asyncFetch(); dates: { hide_vs: true, col: 12 },
}}>统计</Button> DateType: { col: 6, disabledKeys: ['confirmDate'] },
</Space> },
<Table dataSource={mobile_data.data} columns={mobile_data.columns} pagination={false} size="small"/> }}
</div> onSubmit={(_err, obj, form, str) => {
); dashboard_store.setMobileSearchValues(obj, form);
} mobile_data.asyncFetch();
}}
/>
<Table dataSource={mobile_data.data} columns={mobile_data.columns} pagination={false} size="small" />
</div>
);
}
} }
export default observer(MobileDeal); export default observer(MobileDeal);

@ -1,16 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Row, Col, Button, Tabs, Table, Space } from 'antd'; import { Button, Space } from 'antd';
import { import {
ContainerOutlined,
CarryOutOutlined,
SmileOutlined,
TagsOutlined,
GlobalOutlined,
SearchOutlined, SearchOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { Line } from '@ant-design/charts'; import { Line } from '@ant-design/charts';
import SiteSelect from '../components/search/SiteSelect';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import DatePickerCharts from '../components/search/DatePickerCharts'; import DatePickerCharts from '../components/search/DatePickerCharts';
import DateGroupRadio from '../components/DateGroupRadio'; import DateGroupRadio from '../components/DateGroupRadio';
@ -40,7 +34,7 @@ class Orders extends Component {
// xAxis: { // xAxis: {
// type: 'timeCat', // type: 'timeCat',
// }, // },
smooth: true, // smooth: true,
legend: { legend: {
position: 'right-top', position: 'right-top',
title: { title: {

@ -70,7 +70,7 @@ class OrdersTempTable extends Component {
<div> <div>
<h2>临时订单数量</h2> <h2>临时订单数量</h2>
<SiteSelect store={ordersTemp_data}/> <SiteSelect store={ordersTemp_data} mode={'multiple'} />
<Space><DatePickerCharts hide_vs={true}/> <Space><DatePickerCharts hide_vs={true}/>
<Button type="primary" icon={<SearchOutlined/>} loading={ordersTemp_data.loading} <Button type="primary" icon={<SearchOutlined/>} loading={ordersTemp_data.loading}
onClick={() => { onClick={() => {

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Bullet } from '@ant-design/plots'; import { Bullet } from '@ant-design/plots';
import { sortBy, merge } from '../utils/commons'; import { sortBy, merge, isEmpty } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht'; import { dataFieldAlias } from '../libs/ht';
export default observer((props) => { export default observer((props) => {
@ -9,37 +9,61 @@ export default observer((props) => {
// , number -> array // , number -> array
const dataParser = (origin) => { const dataParser = (origin) => {
const { measureField, rangeField, targetField } = extProps; const { measureField, rangeField, targetField } = extProps;
const measureFieldArrKey = `${measureField}_arr`;
const maxKPI = Math.max(...(origin || []).map((ele) => (ele?.[targetField] || 0))); const maxKPI = Math.max(...(origin || []).map((ele) => (ele?.[targetField] || 0)));
const maxValue = Math.max(...(origin || []).map((ele) => ele[measureField])); const maxValue = Math.max(...(origin || []).map((ele) => ele[measureField]));
const _max = Math.max(maxKPI, maxValue); const _max = Math.max(maxKPI, maxValue);
const minValue = Math.min(...(origin || []).map((ele) => ele[measureField]));
const _min = Math.ceil(Math.min(0, minValue));
const sortData = origin.sort(sortBy(measureField)).slice(-itemLength); const sortData = origin.sort(sortBy(measureField)).slice(-itemLength);
// //
const _parseData = sortData?.map((ele) => ({ ...ele, [rangeField]: [0, Math.ceil(_max / 0.9)], [measureField]: [ele[measureField]], [targetField]: (ele?.[targetField] || 0) })); const _parseData = sortData?.map((ele) => ({ ...ele,
return _parseData; [rangeField]: [_min, Math.ceil(_max / 0.9)],
// [measureField]: [ele[measureField]],
[measureField]: ele[measureFieldArrKey] || [ele[measureField]],
[targetField]: (ele?.[targetField] || 0)
}));
return { _parseData, _max, _min };
}; };
const dataMapped = dataSource.reduce((r, v) => ({...r, [v.groupsLabel]: v}), {});
const ifMergeTB = isEmpty(dataSource) ? false : !isEmpty(dataSource[0]?.[`${extProps.measureField}_arr`]);
const [parseData, setParseData] = useState([]); const [parseData, setParseData] = useState([]);
const [maxV, setMaxV] = useState(0);
const [minV, setMinV] = useState(0);
useEffect(() => { useEffect(() => {
setParseData(dataParser(dataSource)); const _pdata = dataParser(dataSource);
setParseData(_pdata._parseData);
setMaxV(_pdata._max);
setMinV(_pdata._min);
return () => {}; return () => {};
}, [extProps.measureField, dataSource]); }, [extProps.measureField, dataSource]);
const config = merge({ const config = merge({
color: { color: {
range: [ '#FFF3E1', '#FFF3E1'], range: [].concat((minV < 0 ? ['#ffe4e4'] : []), [ '#FFF3E1', '#FFF3E1']),
// range: [ '#FFF3E1', '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0',
measure: ['#5B8FF9', '#61ddaa'], measure: ['#5B8FF9', '#61ddaa'],
target: '#FF9845', target: '#FF9845',
}, },
bulletStyle: {
measure: (item, ...r) => {
if (item[extProps.measureField] < 0) {
return {
fill: '#F4664A',
};
}
},
},
label: { label: {
target: false, target: false,
measure: { measure: {
position: extProps?.layout === 'vertical' ? 'top' : 'right', position: extProps?.layout === 'vertical' ? 'top' : 'right',
// style: { // formatter: (v, ...d) => {
// fill: '#063CAA', // return dataFieldAlias[extProps.measureField]?.formatter(v[extProps.measureField]) || v;
// }, // },
formatter: (v) => { content: (item, ...r) => {
return dataFieldAlias[extProps.measureField]?.formatter(v[extProps.measureField]) || v; const val = isEmpty(dataMapped) ? 0 : dataMapped[item.groupsLabel][extProps.measureField];
return String(item?.mKey || '_0').endsWith('_0') ? '' : dataFieldAlias[extProps.measureField]?.formatter(val) || val;
} }
}, },
}, },
@ -57,8 +81,8 @@ export default observer((props) => {
position: 'bottom', position: 'bottom',
items: [ items: [
{ {
value: '实际', value: ifMergeTB ? '传统' : '实际',
name: '实际', name: ifMergeTB ? '传统' : '实际',
marker: { marker: {
symbol: 'square', symbol: 'square',
style: { style: {
@ -67,6 +91,17 @@ export default observer((props) => {
}, },
}, },
}, },
...(ifMergeTB ? [{
value: '商务',
name: '商务',
marker: {
symbol: 'square',
style: {
fill: '#61ddaa',
r: 5,
},
},
}] : []),
{ {
value: '目标', value: '目标',
name: '目标', name: '目标',
@ -84,12 +119,28 @@ export default observer((props) => {
tooltip: { tooltip: {
customItems: (originalItems) => { customItems: (originalItems) => {
// process originalItems, // process originalItems,
const _items = originalItems.map((ele) => ({ const measureIndex = originalItems[0].name.split('_');
...ele, const measureKey = measureIndex[0];
value: dataFieldAlias[ele.name]?.formatter(Number(ele.value)), const measureName = measureKey.endsWith('KPIvalue') ? '' : measureIndex[1] === 0 ? '传统' : '商务';
name: dataFieldAlias[ele.name]?.alias || ele.name, const measureFieldArrKey = `${measureKey}_arr`;
})); const kpiKey = dataFieldAlias[measureKey]?.nestkey?.v;
return _items; const mItems = (measureName && !String(measureKey).toLowerCase().endsWith('rates'))
? originalItems.reduce((r, ele) => {
const _itemMeasures = dataMapped[ele.title][measureFieldArrKey];
r.push({ ...ele, color: '#5B8FF9', value: dataFieldAlias[measureKey]?.formatter(Number(_itemMeasures[0])), name: '传统' });
r.push({ ...ele, color: '#61ddaa', value: dataFieldAlias[measureKey]?.formatter(Number(_itemMeasures[1])), name: '商务' });
const _itemMeasureKPIv = dataMapped[ele.title][kpiKey];
if (kpiKey && _itemMeasureKPIv) {
r.push({ ...ele, color: '#FF9845', value: dataFieldAlias[measureKey]?.formatter(Number(_itemMeasureKPIv)), name: dataFieldAlias[kpiKey].label });
}
return r;
}, [])
: originalItems.map((ele) => ({
...ele,
value: dataFieldAlias[measureKey]?.formatter(Number(ele.value)),
name: dataFieldAlias[measureKey]?.alias || measureKey,
}));
return mItems;
}, },
}, },
}, extProps); }, extProps);

@ -35,6 +35,12 @@ export default observer((props) => {
// xField: 'value', // xField: 'value',
// yField: 'year', // yField: 'year',
// seriesField: 'type', // seriesField: 'type',
xAxis: {
label: {
autoHide: false,
autoRotate: true,
},
},
label: { label: {
// label // label
position: 'middle', position: 'middle',
@ -62,5 +68,6 @@ export default observer((props) => {
}, },
annotations: [...annotationsLine], annotations: [...annotationsLine],
}, extProps); }, extProps);
// console.log(config);
return <Column {...config} data={dataSource} />; return <Column {...config} data={dataSource} />;
}); });

@ -1,5 +1,8 @@
import { Tag } from 'antd'; import React, { useState, useEffect } from "react";
import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons'; import { Tag, Button, message } from 'antd';
import { CaretUpOutlined, CaretDownOutlined, DownloadOutlined } from '@ant-design/icons';
import { utils, writeFile } from "xlsx";
import { isEmpty, getNestedValue } from "../utils/commons";
/** /**
* @property diffPercent * @property diffPercent
@ -24,3 +27,73 @@ export const VSTag = (props) => {
</span> </span>
); );
}; };
/**
* 导出表格数据存为xlsx
* @property label 文件名字
* @property columns 表格列
* @property dataSource 表格数据
* @property btnTxt 按钮文字
*/
export const TableExportBtn = ({label, columns, dataSource, btnTxt, ...props}) => {
const output_name = `${label}`;
const [columnsMap, setColumnsMap] = useState([]);
const [summaryRow, setSummaryRow] = useState({});
useEffect(() => {
const r1 = columns.reduce((r, v) => ({
...r,
...(v.children ? v.children.reduce((rc, vc, ci) => ({
...rc,
...(vc?.titleX ? {[`${v?.titleX || v.title},${vc.titleX}`]: vc.titleX } : {[(v?.titleX || v.title) + (vc.key || ci || '')]: `${vc?.titleX || vc?.title || ''}`}),
}), {}) : {})
}), {});
const flatCols = columns.flatMap((v, k) =>
v.children ? v.children.map((vc, ci) => ({ ...vc, title: `${v?.titleX || v.title}` + (vc?.titleX ? `,${vc.titleX}` : (vc.key || ci || '')) })) : {...v, title: `${v?.titleX || v.title}`}
);
// .filter((c) => c.dataIndex)
// !['string', 'number'].includes(typeof vc.title) ? `${v?.titleX || v.title}` : `${v?.titleX || v.title}-${vc.title || ''}`
;
setColumnsMap(flatCols);
// console.log('flatCols', flatCols);
setSummaryRow(r1);
// console.log('summaryRow', r1);
return () => {};
}, [columns]);
const onExport = () => {
if (isEmpty(dataSource)) {
message.warning('无结果.');
return false;
}
const data = dataSource.map((item) => {
const itemMapped = columnsMap.reduce((sv, kset) => {
const export_val = typeof kset?.dataExport === 'function' ? kset.dataExport('', item) : null;
const render_val = typeof kset?.render === 'function' ? kset.render('', item) : null;
const data_val = kset?.dataIndex ? (Array.isArray(kset.dataIndex) ? getNestedValue(item, kset.dataIndex) : item[kset.dataIndex]) : undefined;
const x_val = item[`${kset.dataIndex}_X`];
// const _title = kset.title.replace('-[object Object]', '');
const v = { [kset.title]: x_val || export_val || data_val || render_val };
return { ...sv, ...v };
}, {});
return itemMapped;
});
const ws = utils.json_to_sheet([].concat(isEmpty(summaryRow) ? [] : [summaryRow], data), { header: columnsMap.filter((r) => r.dataIndex).map((r) => r.title) });
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, 'sheet');
writeFile(wb, `${output_name}.xlsx`);
};
return (
<Button {...props}
type="link"
icon={<DownloadOutlined />}
size="small"
disabled={false}
onClick={onExport}
>
{btnTxt || '导出excel'}
</Button>
);
};

@ -13,7 +13,7 @@ export default observer((props) => {
const defaultVal = dataFieldOptions[0].value; const defaultVal = dataFieldOptions[0].value;
const Component = () => const Component = () =>
dataFieldOptions.length < 6 ? ( dataFieldOptions.length < 8 ? (
<Radio.Group options={dataFieldOptions} optionType="button" onChange={(e) => handleChange(e.target.value)} defaultValue={defaultVal} {...extProps} /> <Radio.Group options={dataFieldOptions} optionType="button" onChange={(e) => handleChange(e.target.value)} defaultValue={defaultVal} {...extProps} />
) : ( ) : (
<Select showSearch options={dataFieldOptions} onChange={handleChange} defaultValue={defaultVal} {...extProps} /> <Select showSearch options={dataFieldOptions} onChange={handleChange} defaultValue={defaultVal} {...extProps} />

@ -1,5 +1,5 @@
import moment from 'moment'; import moment from 'moment';
import { groupBy } from '../../utils/commons'; import { fixTo2Decimals, groupBy } from '../../utils/commons';
export const datePartOptions = [ export const datePartOptions = [
{ label: '日', value: 'day' }, { label: '日', value: 'day' },
@ -83,7 +83,7 @@ export const parseDateType = (data, dateType = 'day', { dateKey, valueKey, serie
const dateRange = [min, max]; const dateRange = [min, max];
const summaryVal = everySeries[key].reduce((rows, row) => rows + row[valueKey], 0); const summaryVal = everySeries[key].reduce((rows, row) => rows + row[valueKey], 0);
const retValue = _f === 'sum' ? summaryVal : _calcF(summaryVal, everySeries[key].length); const retValue = _f === 'sum' ? summaryVal : _calcF(summaryVal, everySeries[key].length);
a.push({ groupKey: key, value: retValue, dateKey: dateRangeStr, dateRange, containDate, [seriesKey]: _seriesKey, [dateKey]: _dateKey }); a.push({ groupKey: key, value: fixTo2Decimals(retValue), dateKey: dateRangeStr, dateRange, containDate, [seriesKey]: _seriesKey, [dateKey]: _dateKey });
return a; return a;
}, []); }, []);
const avgDiv = [...new Set(dateArr)].length; const avgDiv = [...new Set(dateArr)].length;
@ -108,7 +108,7 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
const _data2 = data2 ? dataRaw[data2] : []; const _data2 = data2 ? dataRaw[data2] : [];
const parse1 = parseDateType(_data1, dateGroup, fieldMapper); const parse1 = parseDateType(_data1, dateGroup, fieldMapper);
const parseData1 = parse1.data.map((ele) => ({ const parseData1 = parse1.data.map((ele) => ({
[fieldMapper.dateKey]: ele[fieldMapper.dateKey], [fieldMapper.dateKey]: ele[fieldMapper.dateKey] === 'Invalid date' ? '空日期' : ele[fieldMapper.dateKey],
[fieldMapper.valueKey]: ele.value, [fieldMapper.valueKey]: ele.value,
[fieldMapper.seriesKey]: ele[fieldMapper.seriesKey], [fieldMapper.seriesKey]: ele[fieldMapper.seriesKey],
groups: _data1[0].groups, groups: _data1[0].groups,
@ -118,7 +118,7 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
})); }));
const parse2 = parseDateType(_data2, dateGroup, fieldMapper); const parse2 = parseDateType(_data2, dateGroup, fieldMapper);
const parseData2 = parse2.data.map((ele) => ({ const parseData2 = parse2.data.map((ele) => ({
[fieldMapper.dateKey]: ele[fieldMapper.dateKey], [fieldMapper.dateKey]: ele[fieldMapper.dateKey] === 'Invalid date' ? '空日期' : ele[fieldMapper.dateKey],
// [fieldMapper.dateKey]: ele.groupKey, // [fieldMapper.dateKey]: ele.groupKey,
[fieldMapper.valueKey]: ele.value, [fieldMapper.valueKey]: ele.value,
[fieldMapper.seriesKey]: ele[fieldMapper.seriesKey], [fieldMapper.seriesKey]: ele[fieldMapper.seriesKey],
@ -151,8 +151,15 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
[fieldMapper.dateKey]: keyMapped[ele[fieldMapper.dateKey]], [fieldMapper.dateKey]: keyMapped[ele[fieldMapper.dateKey]],
dateKey: ele.dateKey, dateKey: ele.dateKey,
})); }));
const retData = [].concat(parseData1, reindexData2 ).map(ele => ({...ele, [fieldMapper.dateKey]: data1KeyMappedStr[ele[fieldMapper.dateKey]] || data2KeyMappedStr[ele[fieldMapper.dateKey]]})); const retData = [].concat(parseData1, reindexData2 ).map(ele => ({...ele,
[fieldMapper.dateKey]: data1KeyMappedStr[ele[fieldMapper.dateKey]] === 'Invalid date' ? '空日期' : (data1KeyMappedStr[ele[fieldMapper.dateKey]] || data2KeyMappedStr[ele[fieldMapper.dateKey]])}));
const avg1 = parse1.avgVal; const avg1 = parse1.avgVal;
// console.log('callback', dateGroup, retData, data1KeyMappedStr, data2KeyMappedStr); // console.log('callback','\ndateGroup', dateGroup,
cb(dateGroup, retData, avg1); // '\nretData', retData,
// '\navg1', avg1,
// '\nparse2', parse2.avgVal,
// '\ndata1KeyMappedStr', data1KeyMappedStr,
// '\ndata2KeyMappedStr', data2KeyMappedStr
// );
cb(dateGroup, retData, avg1, parse2.avgVal);
}; };

@ -30,7 +30,7 @@ export default observer((props) => {
offset: '-50%', offset: '-50%',
autoRotate: false, autoRotate: false,
// content: '{value}', // content: '{value}',
content: ({ percent }) => `${fixTo2Decimals(percent * 100)}%`, content: ({ percent }) => percent > 0.02 ? `${fixTo2Decimals(percent * 100)}%`: '',
style: { style: {
textAlign: 'center', textAlign: 'center',
fontSize: 14, fontSize: 14,

@ -0,0 +1,240 @@
import React, { useEffect, useState } from 'react';
import { Row, Col, Spin } from 'antd';
import { Line } from '@ant-design/plots';
import { observer } from 'mobx-react';
import { dataFieldAlias } from '../libs/ht';
import DateGroupRadio from '../components/DateGroupRadio';
import { cloneDeep, groupBy, sortBy } from '../utils/commons';
export default observer((props) => {
const { dataSource: rawData, showAVG, showCompareSum, loading, solidLineTime,
solidLineDash, isCompareLine,solidLineCompareTime, ...config } = props;
const { xField, yField, yFieldAlias, seriesField } = config;
const [dataBeforeXChange, setDataBeforeXChange] = useState([]);
const [dataSource, setDataSource] = useState([]);
const [sumSeries, setSumSeries] = useState([]);
const line_config = {
// data: dataSource,
padding: 'auto',
xField,
yField,
seriesField,
// seriesField: 'rowLabel',
// xAxis: {
// type: 'timeCat',
// },
color: isCompareLine?['#17f485', '#1890ff','#17f485', '#1890ff',"#181414","#181414"]:undefined,
point: {
size: 4,
shape: "cicle",
},
lineStyle: (datum) => {
return {
stroke: isCompareLine?datum._ylabel.includes("总计")?"#181414":datum._ylabel.includes(solidLineDash) ? '#1890ff':'#17f485' : undefined, //
lineDash: isCompareLine?datum._ylabel.includes(solidLineTime) ? undefined:[4, 4] : undefined, // 线
};
},
yAxis: {
min: 0,
maxTickInterval: 5,
},
meta: {
...cloneDeep(dataFieldAlias),
},
// smooth: true,
label: {}, //
legend: {
position: 'right-top',
// title: {
// text: ' ' + dataSource.reduce((a, b) => a + b.SumOrder, 0),
// },
itemMarginBottom: 12, //
},
tooltip: {
customItems: (originalItems) => {
return originalItems
.map((ele) => ({ ...ele, valueR: ele.data[yField] }))
.sort(sortBy('valueR'))
.reverse();
},
},
};
const [lineConfig, setLineConfig] = useState(cloneDeep(line_config));
useEffect(() => {
resetX();
return () => {};
}, [rawData]);
useEffect(() => {
setLineConfig(cloneDeep(line_config));
return () => {};
}, [isCompareLine,solidLineTime]);
useEffect(() => {
if (lineChartX === 'day') {
setDataBeforeXChange(dataSource);
}
return () => {};
}, [dataSource]);
//
const [lineChartX, setLineChartX] = useState('day');
const orderCountDataMapper = { data1: 'data1', data2: undefined };
const orderCountDataFieldMapper = { 'dateKey': xField, 'valueKey': yField, 'seriesKey': seriesField, _f: 'sum' };
const resetX = () => {
setLineChartX('day');
setDataSource(rawData);
setDataBeforeXChange(rawData);
// ``线, ``线
const byDays = groupBy(rawData, xField);
const sumY = rawData.reduce((a, b) => a + b[yField], 0);
// const avgVal = Math.round(sumY / (Object.keys(byDays).length));
// const avgLine = [
// { type: 'text', position: ['start', avgVal], content: avgVal, offsetX: -15, style: { fill: '#F4664A', textBaseline: 'bottom' } },
// { type: 'line', start: [-10, avgVal], end: ['max', avgVal], style: { stroke: '#F4664A', lineDash: [2, 2] } },
// ];
// setLineConfig({ ...lineConfig, yField, xField, annotations: avgLine });
setLineConfig({ ...lineConfig, yField, xField,});
if (showCompareSum) {
const _sumLine = Object.keys(byDays).reduce((r, _d) => {
const summaryVal = byDays[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
const summaryCompareVal = byDays[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineCompareTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
r.push({ ...byDays[_d][0], [yField]: summaryVal, [seriesField]: solidLineTime+'总计' });
r.push({ ...byDays[_d][0], [yField]: summaryCompareVal, [seriesField]: solidLineCompareTime+'总计' });
return r;
}, []);
setSumSeries(_sumLine);
}
else{
const _sumLine = Object.keys(byDays).reduce((r, _d) => {
const summaryVal = byDays[_d].reduce((rows, row) => rows + row[yField], 0);
r.push({ ...byDays[_d][0], [yField]: summaryVal, [seriesField]: '总计' });
return r;
}, []);
// console.log(_sumLine.map((ele) => ele[yField]));
setSumSeries(_sumLine);
}
};
const onChangeXDateFieldGroup = (value, data, avg1) => {
// console.log(value, data, avg1);
const _sumLine = [];
const { xField, yField, seriesField } = lineConfig;
const groupByDate = data.reduce((r, v) => {
(r[v[xField]] || (r[v[xField]] = [])).push(v);
return r;
}, {});
// console.log(groupByDate);
const _data = Object.keys(groupByDate).reduce((r, _d) => {
if (showCompareSum) {
const summaryVal = groupByDate[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
const summaryCompareVal = groupByDate[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineCompareTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
_sumLine.push({ ...groupByDate[_d][0], [yField]: summaryVal, [seriesField]: solidLineTime+'总计' });
_sumLine.push({ ...groupByDate[_d][0], [yField]: summaryCompareVal, [seriesField]: solidLineCompareTime+'总计' });
}
else{
const summaryVal = groupByDate[_d].reduce((rows, row) => rows + row[yField], 0);
_sumLine.push({ ...groupByDate[_d][0], [yField]: summaryVal, [seriesField]: '总计' });
}
const xAxisGroup = groupByDate[_d].reduce((a, v) => {
(a[v[seriesField]] || (a[v[seriesField]] = [])).push(v);
return a;
}, {});
// console.log(xAxisGroup);
Object.keys(xAxisGroup).map((_group) => {
const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row[yField], 0);
r.push({ ...xAxisGroup[_group][0], [yField]: summaryVal, });
return _group;
});
return r;
}, []);
// const _sum = Object.values(groupBy(_data, 'dateGroup')).reduce((ac, b) => ({...b, [yField]: 0}), {});
// console.log(xField, avg1);
// console.log('date source=====', _data);
setLineChartX(value);
setDataSource(_data);
setSumSeries(_sumLine);
// setAvgLine1(avg1);
// const avg1Int = Math.round(avg1);
// const mergedConfig = { ...lineConfig,
// annotations: [
// { type: 'text', position: ['start', avg1Int], content: avg1Int, offsetX: -15, style: { fill: '#F4664A', textBaseline: 'bottom' } },
// { type: 'line', start: [-10, avg1Int], end: ['max', avg1Int], style: { stroke: '#F4664A', lineDash: [2, 2] } },
// ],
// };
// console.log(mergedConfig);
// setLineConfig(mergedConfig);
setLineConfig(cloneDeep(line_config));
};
return (
<section>
<Row gutter={16} justify={'space-between'} className="mb-1">
<Col flex={'auto'}>
<h3>
走势: <span style={{ fontSize: 'smaller' }}>{dataFieldAlias[lineConfig.yField].label}</span>
</h3>
</Col>
<Col style={{ textAlign: 'right' }} align={'end'}>
<DateGroupRadio
visible={true}
dataRaw={{ data1: dataBeforeXChange }}
onChange={onChangeXDateFieldGroup}
value={lineChartX}
dataMapper={orderCountDataMapper}
fieldMapper={orderCountDataFieldMapper}
/>
</Col>
</Row>
<Spin spinning={loading}>
<Line {...lineConfig} data={[].concat(dataSource, sumSeries)} />
</Spin>
</section>
);
});

@ -1,60 +1,103 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Line } from '@ant-design/plots'; import { Line } from '@ant-design/plots';
import { merge, isEmpty, groupBy } from '../utils/commons'; import { merge, isEmpty, groupBy, sortBy } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht'; import { dataFieldAlias } from '../libs/ht';
const uniqueByKey = (array, key, pickLast) => {
const seen = new Map();
const isPickLast = pickLast === true;
return array.filter(item => {
const k = item[key];
const storedItem = seen.get(k);
if(storedItem) {
if(isPickLast) {
seen.set(k, item); // update with last item
}
return false;
}
seen.set(k, item);
return true;
});
};
export default observer((props) => { export default observer((props) => {
const { dataSource, ...config } = props; const { dataSource, showKPI, ...config } = props;
const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v; const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v;
const seriesData = groupBy(dataSource, ele => ele[config.seriesField]); const seriesData = groupBy(dataSource, ele => ele[config.seriesField]);
const pickKey4KPI = Object.keys(seriesData)[0]; const splitData = showKPI ? dataSource.reduce((r, v) => {
const KPIData = (seriesData?.[pickKey4KPI] || []).reduce((r, v) => { r.push(v);
if ( ! isEmpty(v[kpiKey])) { // 线 : #F4664A if ( ! isEmpty(v[kpiKey])) { // 线, 线
r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: dataFieldAlias[kpiKey].label, extraLine: true,}); r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: `${v[config.seriesField]} ${dataFieldAlias[kpiKey].label}`, extraLine: true,});
} }
return r; return r;
}, []); }, []).sort(sortBy(config.xField)) : (dataSource.slice()).sort(sortBy(config.xField));
const dataColors = ['#598cf3', '#69deae', '#FAAD14']; const dataColors = [
const colorSets = Object.keys(seriesData).sort().reduce((obj, k, i) => ({...obj, [k]: dataColors[i]}), {}); "#5D7092","#F6BD16","#6F5EF9","#6DC8EC","#945FB9","#FF9845","#1E9493",
const mergeLineConfig = merge({ "#FF99C3","#FF6B3B","#626681","#FFC100","#9FB40F","#76523B","#DAD5B5",
// color: ['#598cf3', '#69deae', '#F4664A', '#FAAD14'], "#0E8E89","#E19348","#F383A2","#247FEA","#5B8FF9","#5AD8A6",
color: (item) => { ];
const thisSeries = item[config.seriesField]; const colorSets = Object.keys(seriesData)
return thisSeries.includes('目标') ? '#F4664A' : colorSets[thisSeries]; .sort()
}, .filter((ele) => !ele.includes(' '))
lineStyle: (data) => { .reduce((obj, k, i) => ({ ...obj, [k]: dataColors[i] || dataColors[i % 20] }), {});
// console.log(data); // console.log('colorSets', colorSets);
if (data[config.seriesField].includes('目标')) { const mergeLineConfig = merge(
{
color: (item) => {
const thisSeries = item[config.seriesField]?.split(' ')?.[0];
return colorSets[thisSeries];
},
lineStyle: (data) => {
if (data[config.seriesField].includes('目标')) {
return {
lineDash: [8, 20],
opacity: 0.5,
};
}
if (data[config.seriesField].includes('@')) {
return {
lineDash: [4, 8],
opacity: 0.6,
lineWidth: 1.5,
};
}
return { return {
lineDash: [4, 4], opacity: 1,
opacity: 0.5,
}; };
} },
legend: {
return { custom: true,
opacity: 1, items: Object.keys(seriesData)
}; .map((ele) => ({
id: ele,
name: ele,
value: ele,
marker: {
symbol: ele.includes(' ') ? 'hyphen' : 'circle',
style: { fill: colorSets[ele], stroke: colorSets[ele?.split(' ')?.[0]] || '#5B8FF9', r: 3, lineWidth: 2, color: colorSets[ele] },
},
}))
.sort(sortBy('name')),
},
tooltip: {
// title: dataFieldAlias[config.yField]?.alias,
showTitle: true,
customItems: (items) => items.sort(sortBy('name')).map((ele) => ({ ...ele, title: `${ele.title} ${dataFieldAlias[config.yField]?.alias}` })),
},
// annotations: [
// // 0
// {
// type: 'regionFilter',
// start: ['min', 0],
// end: ['max', 0],
// color: '#F4664A',
// },
// {
// type: 'text',
// position: ['min', 0],
// content: '0',
// offsetY: -4,
// style: {
// textBaseline: 'bottom',
// },
// },
// {
// type: 'line',
// start: ['min', 0],
// end: ['max', 0],
// style: {
// stroke: '#F4664A',
// lineDash: [2, 2],
// },
// },
// ],
}, },
}, config); config
return <Line {...mergeLineConfig} data={[...dataSource, ...KPIData]} />; );
return <Line {...mergeLineConfig} data={splitData} />;
}); });

@ -0,0 +1,136 @@
import { useContext, useState, useEffect, useMemo } from 'react';
import { observer } from 'mobx-react';
import { ChoroplethMap } from '@ant-design/maps';
import { dataFieldAlias } from '../libs/ht';
import { cloneDeep } from '../utils/commons';
export default observer((props) => {
const { dataSource, sourceField, valueField, containerNode, ...extConfig } = props;
const [mdataSource, setMdataSource] = useState([]);
useEffect(() => {
const dataMapped = (cloneDeep(dataSource) || []).reduce((r, v) => ({...r,
[(v[sourceField] || '_').replace('(待删除)', '')]: v
}), {});
if (dataMapped?.['中国']) {
dataMapped['中国'][sourceField] = '中华人民共和国';
}
setMdataSource(Object.values(dataMapped));
return () => {};
}, [dataSource, valueField]);
const config = {
container: containerNode || '#topC',
map: {
// type: 'amap',
type: 'mapbox',
// style: 'blank',
center: [120.19382669582967, 30.258134],
zoom: 2,
pitch: 0,
// scrollZoom: false,
// dragPan: false,
// zoomEnable: false,
// token: 'd78b5ba25a4699a1cb567b7a933e630b', // amap
},
// geoArea: {
// url: 'https://gw.alipayobjects.com/os/alisis/geo-data-v0.1.2/choropleth-data',
// type: 'topojson',
// },
source: {
data: mdataSource.filter((ele) => ele[sourceField] && ele[valueField] !== 0 ),
joinBy: {
geoField: 'name',
sourceField: sourceField || 'name',
},
},
autoFit: true,
color: {
field: valueField || 'value',
value: [
'#820C1B',
'#a31022',
'#ac2738',
'#b53f4e',
'#be5764',
'#c76f7a',
'#d18790',
'#da9fa6',
'#e3b7bc',
'#eccfd2',
'#f5e7e8',
'#fde7ea',
// '#8a1313',
// '#ad1818',
// '#b52f2f',
// '#bd4646',
// '#c55d5d',
// '#cd7474',
// '#d68b8b',
// '#dea2a2',
// '#e6b9b9',
// '#eed0d0',
// '#f6e7e7',
// '#fde7ea',
// '#001D70',
// '#0047A5',
// '#1A4397',
// '#2555B7',
// '#3165D1',
// '#3D76DD',
// '#467BE8',
// '#6296FE',
// '#7EA6F9',
// '#98B7F7',
// '#BDD0F8',
// '#DDE6F7',
// '#F2F5FC'
].reverse(),
scale: { type: 'quantile' },
},
viewLevel: {
// level: 'country',
// adcode: '100000',
// granularity: 'province',
level: 'world',
adcode: 'all',
granularity: 'country',
},
chinaBorder: false,
style: {
opacity: 1,
stroke: '#fff',
lineWidth: 0.6,
lineOpacity: 1,
},
state: {
active: {
stroke: 'yellow',
lineWidth: 0.6,
// lineOpacity: 0.8,
},
},
label: {
visible: true,
field: 'name',
style: {
fill: '#000',
opacity: 0.8,
fontSize: 10,
stroke: '#fff',
strokeWidth: 1.5,
textAllowOverlap: false,
padding: [8, 8],
},
},
tooltip: {
items: ['name', { field: valueField, alias: dataFieldAlias[valueField].alias, customValue: (v) => dataFieldAlias[valueField].formatter(v) }],
},
zoom: false,
legend: false,
};
return <ChoroplethMap {...config} />;
});

@ -0,0 +1,265 @@
import { observer } from 'mobx-react';
import { message } from 'antd';
import { Mix, getCanvasPattern, } from '@ant-design/plots';
import { merge, isEmpty, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
const COLOR_SETS = [
"#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const COLOR_SETS2 = [
"#5B8FF9",
"#61DDAA",
"#65789B",
];
/**
* 订单数, 团数: 柱形图
* 成交率: 折线图
*/
export default observer((props) => {
const { dataSource, summaryData: areaData, ...config } = props;
const { xField, yFields, colFields, lineFields, seriesField, tooltip, ...extConfig } = config;
const diffData0 = dataSource.reduce((r, row) => {
r.push({ ...row, yField: row[colFields[0]], yGroup: dataFieldAlias[colFields[0]].alias });
r.push({ ...row, yField: row[colFields[1]], yGroup: dataFieldAlias[colFields[1]].alias });
return r;
}, []);
const diffData1 = dataSource.reduce((r, row) => {
r.push({ ...row, yField: row[lineFields[1]], yGroup: dataFieldAlias[lineFields[1]].alias });
return r;
}, []);
const calcAxis = isEmpty(diffData0) ? 300 : (Math.max(...diffData0.map(ele => ele.yField))) * 3;
// const calcAxisC = isEmpty(diffData0) ? 300 : (Math.max(...diffDataPercent.map(ele => ele.yField))) * 3;
const diffLine = [
// {
// type: 'text',
// position: ['start', 0],
// content: `, `,
// offsetX: -15,
// style: {
// fill: COLOR_SETS[0],
// textBaseline: 'bottom',
// },
// },
// {
// type: 'line',
// start: [-10, 0],
// end: ['max', 0],
// style: {
// stroke: COLOR_SETS[0],
// // lineDash: [2, 2],
// lineWidth: 0.5,
// },
// },
];
const pattern = (datum, color) => {
return getCanvasPattern({
type: String(datum.yGroup).includes(' ') ? 'line' : '',
cfg: {
backgroundColor: color,
},
});
};
const MixConfig = {
appendPadding: 15,
height: 400,
syncViewPadding: true,
tooltip: {
shared: true,
// customItems: (originalItems) => {
// // process originalItems,
// const items = originalItems.map((ele) => ({ ...ele, name: ele.data?.extraLine ? ele.name : `${ele.name} ${dataFieldAlias[yField]?.alias || yField}` }));
// return items;
// },
},
legend: {
position: 'top',
layout: 'horizontal',
custom: true,
items: [
...['团数', '订单数'].map((ele, ei) => ({
name: `${ele}`,
value: `${ele}`,
marker: {
symbol: 'square',
style: {
fill: COLOR_SETS2[ei],
r: 5,
},
},
})),
...['', '成行率'].map((ele, ei) => ({ // '',
name: `${ele}`,
value: `${ele}`,
marker: {
symbol: 'hyphen',
style: {
stroke: COLOR_SETS[ei],
r: 5,
lineWidth: 2
},
},
})),
],
},
// event: (chart, e) => {
// console.log('mix', chart, e);
// if (e.type === 'click') {
// props.itemClick(e);
// }
// },
onReady: (plot) => {
plot.on('plot:click', (...args) => {
// message.info(', ');
});
plot.on('element:click', (e) => {
const {
data: { data },
} = e;
// console.log('plot element', data);
props.itemClick(data);
});
// axis-label
plot.on('axis-label:click', (e, ...args) => {
const { text } = e.target.attrs;
// console.log(text);
props.itemClick({ [xField]: text });
});
},
plots: [
{
type: 'column',
options: {
data: diffData0,
isGroup: true,
xField,
yField: 'yField',
seriesField: 'yGroup',
// xAxis: false,
meta: merge({
...cloneDeep(dataFieldAlias),
}),
// color: '#b32b19',
// color: '#f58269',
legend: false, // {},
// smooth: true,
yAxis: {
type: 'linear',
tickCount: 4,
min: 0,
max: calcAxis,
title: { text: '团数', autoRotate: false, position: 'end' },
},
xAxis: {
label: {
autoHide: false,
autoRotate: true,
},
},
label: false,
color: COLOR_SETS2,
pattern,
},
},
{
type: 'line',
options: {
data: diffData1,
isGroup: true,
xField,
yField: 'yField',
seriesField: 'yGroup',
xAxis: false,
legend: false, // {},
meta: merge(
{
...cloneDeep(dataFieldAlias),
},
{ yField: dataFieldAlias[lineFields[1]] }
),
// color: '#1AAF8B',
color: COLOR_SETS[1],
// smooth: true,
point: {
size: 4,
shape: 'cicle',
},
yAxis: {
type: 'linear',
// tickCount: 4,
min: 0,
position: 'right',
line: null,
grid: null,
title: { text: dataFieldAlias[lineFields[1]].label, autoRotate: false, position: 'end' },
},
label: {
style: {
fontWeight: 700,
stroke: '#fff',
lineWidth: 1,
},
},
lineStyle: (datum) => {
if (String(datum.yGroup).includes(' ')) {
return {
lineDash: [4, 4],
opacity: 0.75,
};
}
return {
opacity: 1,
};
},
},
},
// {
// type: 'column',
// options: {
// data: diffData2,
// xField,
// yField: 'yField',
// seriesField: 'yGroup',
// columnWidthRatio: 0.28,
// meta: {
// // yField: {
// // formatter: (v) => `${v}%`,
// // },
// },
// isGroup: true,
// xAxis: false,
// yAxis: {
// line: null,
// grid: null,
// label: false,
// position: 'left',
// // min: -calcAxisC,
// // max: calcAxisC/4,
// min: -3000,
// max: 250,
// tickCount: 4,
// },
// legend: false, // {},
// color: COLOR_SETS,
// // annotations: diffLine,
// minColumnWidth: 5,
// maxColumnWidth: 5,
// // ()
// dodgePadding: 1,
// // ()
// // intervalPadding: 20,
// },
// },
],
};
return <Mix {...MixConfig} />;
});

@ -3,26 +3,6 @@ import { Mix } from '@ant-design/plots';
import { merge, isEmpty, groupBy, cloneDeep } from '../utils/commons'; import { merge, isEmpty, groupBy, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht'; import { dataFieldAlias } from '../libs/ht';
const uniqueByKey = (array, key, pickLast) => {
const seen = new Map();
const isPickLast = pickLast === true;
return array.filter((item) => {
const k = item[key];
const storedItem = seen.get(k);
if (storedItem) {
if (isPickLast) {
seen.set(k, item); // update with last item
}
return false;
}
seen.set(k, item);
return true;
});
};
export default observer((props) => { export default observer((props) => {
const { dataSource, summaryData: areaData, ...config } = props; const { dataSource, summaryData: areaData, ...config } = props;
const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v; const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v;
@ -51,11 +31,17 @@ export default observer((props) => {
shape: 'cicle', shape: 'cicle',
}, },
xAxis: false, xAxis: false,
yAxis: false, yAxis: {
line: null,
grid: null,
label: false,
position: 'left',
min: 0,
},
meta: { meta: {
[yField]: { [yField]: {
sync: true, sync: true,
} },
}, },
// color: ['#598cf3', '#69deae', '#F4664A', '#FAAD14'], // color: ['#598cf3', '#69deae', '#F4664A', '#FAAD14'],
color: (item) => { color: (item) => {
@ -98,23 +84,52 @@ export default observer((props) => {
yField, yField,
seriesField, seriesField,
xAxis: false, xAxis: false,
yAxis: {
// line: null,
// grid: null,
// label: false,
position: 'left',
min: 0,
},
meta: merge( meta: merge(
{ {
...cloneDeep(dataFieldAlias), ...cloneDeep(dataFieldAlias),
}, },
{ [xField]: { sync: true }, [yField]: { sync: true } } { [xField]: { sync: true }, [yField]: { sync: true } }
), ),
// color: '#b32b19',
color: '#f58269', color: '#f58269',
smooth: true, // color: (datum) => {
areaStyle: () => { // console.log('color', datum, String(datum[seriesField]).includes(''));
return { // return String(datum[seriesField]).includes('') ? '#f7a593' : '#f58269';
fill: 'l(270) 0:#ffffff 0.25:#f8e8e7 0.5:#fac9bd 0.75:#f7a593', // }, // '#f58269',
}; // smooth: true,
line: {
size: 1,
style: (datum) => {
return String(datum[seriesField]).includes('对比')
? {
// lineWidth: 0.1,
lineDash: [4, 5],
stroke: '#f7a593',
}
: {
stroke: '#f58269',
};
},
}, },
label: { areaStyle: (datum) => {
offsetY: -8, // console.log('areaStyle', datum);
return String(datum[seriesField]).includes(' ')
? {
fill: 'l(270) 0:#ffffff 0.25:#f8e8e7 0.5:#fac9bd 0.75:#fac9bd',
}
: {
fill: 'l(270) 0:#ffffff 0.25:#f8e8e7 0.5:#fac9bd 0.75:#f7a593',
// lineWidth: 0.1,
// lineOpacity: 0.5,
};
}, },
label: (datum) => ({ offsetY: -8 }),
annotations: areaData.map((d) => { annotations: areaData.map((d) => {
return { return {
type: 'dataMarker', type: 'dataMarker',
@ -122,7 +137,7 @@ export default observer((props) => {
point: { point: {
style: { style: {
stroke: '#F4664A', stroke: '#F4664A',
lineWidth: 1.5, lineWidth: 0.5,
}, },
}, },
}; };

@ -0,0 +1,272 @@
import { observer } from 'mobx-react';
import { Mix, getCanvasPattern } from '@ant-design/plots';
import { merge, isEmpty, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
const COLOR_SETS = [
"#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const COLOR_SETS2 = [
"#5B8FF9",
"#61DDAA",
"#65789B",];
/**
* 当期数据; 同比; 环比
*/
export default observer((props) => {
const { dataSource, summaryData: areaData, ...config } = props;
const { xField, yFields, seriesField, tooltip, ...extConfig } = config;
const diffData0 = dataSource.reduce((r, row) => {
r.push({ ...row, yField: row[yFields[0]], yGroup: dataFieldAlias[yFields[0]].alias });
r.push({ ...row, yField: row.resultToQ[yFields[0]], yGroup: dataFieldAlias[yFields[0]].alias + ' 上个时间段' });
r.push({ ...row, yField: row.resultToY[yFields[0]], yGroup: dataFieldAlias[yFields[0]].alias + ' 去年同期' });
return r;
}, []);
const diffData1 = dataSource.reduce((r, row) => {
r.push({ ...row, yField: row[yFields[1]], yGroup: dataFieldAlias[yFields[1]].alias });
r.push({ ...row, yField: row.resultToQ[yFields[1]], yGroup: dataFieldAlias[yFields[1]].alias + ' 上个时间段' });
r.push({ ...row, yField: row.resultToY[yFields[1]], yGroup: dataFieldAlias[yFields[1]].alias + ' 去年同期' });
return r;
}, []);
const diffDataPercent = dataSource.reduce((r, row) => {
const _key0Q = `${yFields[0]}ToQ`;
const _key0Y = `${yFields[0]}ToY`;
r.push({ ...row, yField: row[_key0Q], yGroup: dataFieldAlias[yFields[0]].alias + ' 环比' });
r.push({ ...row, yField: row[_key0Y], yGroup: dataFieldAlias[yFields[0]].alias + ' 同比' });
const _key1Q = `${yFields[1]}ToQ`;
const _key1Y = `${yFields[1]}ToY`;
r.push({ ...row, yField: row[_key1Q], yGroup: dataFieldAlias[yFields[1]].alias + ' 环比' });
r.push({ ...row, yField: row[_key1Y], yGroup: dataFieldAlias[yFields[1]].alias + ' 同比' });
return r;
}, []);
const calcAxis = isEmpty(diffData0) ? 300 : (Math.max(...diffData0.map(ele => ele.yField))) * 3;
const calcAxisC = isEmpty(diffData0) ? 300 : (Math.max(...diffDataPercent.map(ele => ele.yField))) * 3;
const diffLine = [
{
type: 'text',
position: ['start', 0],
content: `同比, 环比`,
offsetX: -15,
style: {
fill: COLOR_SETS[0],
textBaseline: 'bottom',
},
},
{
type: 'line',
start: [-10, 0],
end: ['max', 0],
style: {
stroke: COLOR_SETS[0],
// lineDash: [2, 2],
lineWidth: 0.5,
},
},
];
const pattern = (datum, color) => {
return getCanvasPattern({
type: String(datum.yGroup).includes(' ') ? 'line' : '',
cfg: {
backgroundColor: color,
},
});
};
const MixConfig = {
appendPadding: 15,
height: 600,
syncViewPadding: true,
tooltip: {
shared: true,
// customItems: (originalItems) => {
// // process originalItems,
// const items = originalItems.map((ele) => ({ ...ele, name: ele.data?.extraLine ? ele.name : `${ele.name} ${dataFieldAlias[yField]?.alias || yField}` }));
// return items;
// },
},
legend: {
position: 'top',
layout: 'horizontal',
custom: true,
items: [
...['当期', '上期', '去年同期'].map((ele, ei) => ({
name: `${ele} 团数`,
value: `${ele} 团数`,
marker: {
symbol: 'square',
style: {
fill: COLOR_SETS2[ei],
r: 5,
},
},
})),
...['当期', '上期', '去年同期'].map((ele, ei) => ({
name: `${ele} 业绩`,
value: `${ele} 业绩`,
marker: {
symbol: 'hyphen',
style: {
stroke: COLOR_SETS2[ei],
r: 5,
lineWidth: 2
},
},
})),
...['环比', '同比'].map((ele, ei) => ({
name: `团数 ${ele}`,
value: `团数 ${ele}`,
marker: {
symbol: 'square',
style: {
fill: COLOR_SETS[ei],
r: 5,
},
},
})),
...['环比', '同比'].map((ele, ei) => ({
name: `业绩 ${ele}`,
value: `业绩 ${ele}`,
marker: {
symbol: 'square',
style: {
fill: COLOR_SETS[ei+2],
r: 5,
},
},
})),
],
},
plots: [
{
type: 'column',
options: {
data: diffData0,
isGroup: true,
xField,
yField: 'yField',
seriesField: 'yGroup',
// xAxis: false,
meta: merge({
...cloneDeep(dataFieldAlias),
}),
// color: '#b32b19',
// color: '#f58269',
legend: false, // {},
// smooth: true,
yAxis: {
type: 'linear',
tickCount: 4,
min: 0,
max: calcAxis,
title: { text: '团数', autoRotate: false, position: 'end' },
},
xAxis: {
label: {
autoHide: false,
autoRotate: true,
},
},
label: false,
color: COLOR_SETS2,
pattern,
},
},
{
type: 'line',
options: {
data: diffData1,
isGroup: true,
xField,
yField: 'yField',
seriesField: 'yGroup',
xAxis: false,
legend: false, // {},
meta: merge(
{
...cloneDeep(dataFieldAlias),
},
{ yField: dataFieldAlias[yFields[1]] }
),
// color: '#1AAF8B',
// smooth: true,
point: {
size: 4,
shape: 'cicle',
},
yAxis: {
type: 'linear',
// tickCount: 4,
min: 0,
position: 'right',
line: null,
grid: null,
title: { text: '业绩', autoRotate: false, position: 'end' },
},
label: {
style: {
fontWeight: 700,
stroke: '#fff',
lineWidth: 1,
},
},
lineStyle: (datum) => {
if (String(datum.yGroup).includes(' ')) {
return {
lineDash: [4, 4],
opacity: 0.75,
};
}
return {
opacity: 1,
};
},
},
},
{
type: 'column',
options: {
data: diffDataPercent,
xField,
yField: 'yField',
seriesField: 'yGroup',
columnWidthRatio: 0.28,
meta: {
yField: {
formatter: (v) => `${v}%`,
},
},
isGroup: true,
xAxis: false,
yAxis: {
line: null,
grid: null,
label: false,
position: 'left',
// min: -calcAxisC,
// max: calcAxisC/4,
min: -3000,
max: 250,
tickCount: 4,
},
legend: false, // {},
color: COLOR_SETS,
annotations: diffLine,
minColumnWidth: 5,
maxColumnWidth: 5,
// ()
dodgePadding: 1,
// ()
// intervalPadding: 20,
},
},
],
};
return <Mix {...MixConfig} />;
});

@ -5,23 +5,24 @@ import { RingProgress, Progress, Bullet } from '@ant-design/plots';
import RcResizeObserver from 'rc-resize-observer'; import RcResizeObserver from 'rc-resize-observer';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import { Table } from 'antd'; import { Table, Space } from 'antd';
const { Statistic, Divider } = StatisticCard; const { Statistic, Divider } = StatisticCard;
export default observer((props) => { export default observer((props) => {
const { icon, traditional, biz, kpiVal, originVal, ...extProps } = props; const { icon, traditional, biz, kpiVal, originVal, diff, ...extProps } = props;
const ValueIcon = props.icon; const ValueIcon = props.icon;
const valueStyle = { color: (props?.VSrate || -1) < 0 ? '#3f8600' : '#cf1322' }; const valueStyle = { color: '#3f8600' };
const VSIcon = () => ((props?.VSrate || -1) < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />); // const valueStyle = { color: (props?.VSrate || -1) < 0 ? '#3f8600' : '#cf1322' };
// const VSIcon = () => ((props?.VSrate || -1) < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />);
// console.log(props, ';;;;'); // console.log(props, ';;;;');
const [responsive, setResponsive] = useState(false); const [responsive, setResponsive] = useState(false);
const showMulti = traditional.value && biz.value; const showMulti = traditional.value && biz.value;
const rangeMax = Math.max(originVal, kpiVal);
const bulletData = [ const bulletData = [
{ {
title: '', title: '',
// ranges: [0, kpiVal || (traditional.value + biz.value + 100 )], ranges: [0, Math.ceil(rangeMax / 0.95)], // ,
ranges: [0, Math.ceil(originVal * 1.1)],
measures: [traditional.value, biz.value], measures: [traditional.value, biz.value],
target: kpiVal || 0, target: kpiVal || 0,
}, },
@ -70,9 +71,16 @@ export default observer((props) => {
valueStyle, valueStyle,
...extProps, ...extProps,
value: props.valueSuffix ? `${props.value} ${props.valueSuffix}` : props.value, value: props.valueSuffix ? `${props.value} ${props.valueSuffix}` : props.value,
prefix: <ValueIcon twoToneColor="#89B67F" />, prefix: <ValueIcon twoToneColor={"#89B67F"} />,
description: diff ? (
<Space>
<Statistic title={diff.label} value={` ${diff.value}`} />
{diff.VSrate && <Statistic title="" value={`${diff.VSrate}%`} trend={diff.VSrate > 0 ? 'up' : 'down'} />}
</Space>
) : null,
}} }}
chart={showMulti ? <Bullet data={bulletData} {...bulletConfig} layout={'horizontal'} />: false} chart={showMulti ? <Bullet data={bulletData} {...bulletConfig} layout={'horizontal'} />: false}
footer={null}
/> />
</StatisticCard.Group> </StatisticCard.Group>
</RcResizeObserver> </RcResizeObserver>

@ -0,0 +1,9 @@
import Icon, {} from '@ant-design/icons';
const CooperationSvg = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.8611 2.39057C12.8495 1.73163 14.1336 1.71797 15.1358 2.35573L19.291 4.99994H20.9998C21.5521 4.99994 21.9998 5.44766 21.9998 5.99994V14.9999C21.9998 15.5522 21.5521 15.9999 20.9998 15.9999H19.4801C19.5396 16.9472 19.0933 17.9102 18.1955 18.4489L13.1021 21.505C12.4591 21.8907 11.6609 21.8817 11.0314 21.4974C10.3311 22.1167 9.2531 22.1849 8.47104 21.5704L3.33028 17.5312C2.56387 16.9291 2.37006 15.9003 2.76579 15.0847C2.28248 14.7057 2 14.1254 2 13.5109V6C2 5.44772 2.44772 5 3 5H7.94693L11.8611 2.39057ZM4.17264 13.6452L4.86467 13.0397C6.09488 11.9632 7.96042 12.0698 9.06001 13.2794L11.7622 16.2518C12.6317 17.2083 12.7903 18.6135 12.1579 19.739L17.1665 16.7339C17.4479 16.5651 17.5497 16.2276 17.4448 15.9433L13.0177 9.74551C12.769 9.39736 12.3264 9.24598 11.9166 9.36892L9.43135 10.1145C8.37425 10.4316 7.22838 10.1427 6.44799 9.36235L6.15522 9.06958C5.58721 8.50157 5.44032 7.69318 5.67935 7H4V13.5109L4.17264 13.6452ZM14.0621 4.04306C13.728 3.83047 13.3 3.83502 12.9705 4.05467L7.56943 7.65537L7.8622 7.94814C8.12233 8.20827 8.50429 8.30456 8.85666 8.19885L11.3419 7.45327C12.5713 7.08445 13.8992 7.53859 14.6452 8.58303L18.5144 13.9999H19.9998V6.99994H19.291C18.9106 6.99994 18.5381 6.89148 18.2172 6.68727L14.0621 4.04306ZM6.18168 14.5448L4.56593 15.9586L9.70669 19.9978L10.4106 18.7659C10.6256 18.3897 10.5738 17.9178 10.2823 17.5971L7.58013 14.6247C7.2136 14.2215 6.59175 14.186 6.18168 14.5448Z"></path></svg>
);
const CooperationIcon = (props) => <Icon component={CooperationSvg} {...props} />;
export default CooperationIcon;

@ -3,12 +3,11 @@ import { observer } from 'mobx-react';
import { stores_Context } from './../../config'; import { stores_Context } from './../../config';
import { Typography, Row, Col, Tabs, } from 'antd'; import { Typography, Row, Col, Tabs, } from 'antd';
import SearchForm from './../search/SearchForm'; import SearchForm from './../search/SearchForm';
import { bu, KPIObjects, KPISubjects } from './../../libs/ht'; import { KPIObjects } from './../../libs/ht';
import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt, merge } from './../../utils/commons'; import { merge, pick } from './../../utils/commons';
import ProfitTable from './SubjectTable/Profit'; import ProfitTable from './SubjectTable/Profit';
import Count from './SubjectTable/Count'; import Count from './SubjectTable/Count';
import Rates from './SubjectTable/Rates'; import Rates from './SubjectTable/Rates';
import { toJS } from 'mobx';
const Todo = (props) => { const Todo = (props) => {
return <h2>TODO</h2>; return <h2>TODO</h2>;
@ -32,7 +31,7 @@ export default observer((props) => {
const { sort, initialValue, hides, shows, fieldProps: _fieldProps } = { const { sort, initialValue, hides, shows, fieldProps: _fieldProps } = {
sort: '', sort: '',
// initialValue: '', // initialValue: '',
initialValue: searchFormStore.formValues, initialValue: pick(searchFormStore.formValues, ['DateType', 'year']),
fieldProps: {}, fieldProps: {},
hides: [], hides: [],
shows: ['DateType', 'years'], shows: ['DateType', 'years'],

@ -36,7 +36,7 @@ export default observer((props) => {
const PercentInput = useMemo( const PercentInput = useMemo(
() => () =>
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
({ value, onChange, record, ...extProps }) => { ({ value, onChange, record, month, ...extProps }) => {
// // eslint-disable-next-line react-hooks/rules-of-hooks // // eslint-disable-next-line react-hooks/rules-of-hooks
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value);
const handleInputChange = (e) => { const handleInputChange = (e) => {
@ -44,10 +44,11 @@ export default observer((props) => {
onChange?.(e.target.value); onChange?.(e.target.value);
}; };
const calcV = inputValue ? numberConvert10K(fixToInt((Number(record?.yearValue) * inputValue) / 100)) : 0; const calcV = inputValue ? numberConvert10K(fixToInt((Number(record?.yearValue) * inputValue) / 100)) : 0;
const sumUtil = new Array(month).fill(1).reduce((r, v, i) => r + fixToInt((Number(record?.yearValue || 0) * record[`M${i + 1}Percent`]) / 100), 0); //
return ( return (
<Space direction={'vertical'}> <Space direction={'vertical'}>
<Input key={'input'} suffix="%" type={'number'} value={inputValue} onChange={handleInputChange} step={0.1} /> <Input key={'input'} suffix="%" type={'number'} value={inputValue} onChange={handleInputChange} step={0.1} />
<Text type={'secondary'}>{calcV}</Text> <Text type={'secondary'}>{calcV}<Text italic type={'secondary'}>, {numberConvert10K(sumUtil)}</Text> </Text>
</Space> </Space>
); );
}, },
@ -83,6 +84,7 @@ export default observer((props) => {
dataIndex: 'object_id', dataIndex: 'object_id',
editable: false, editable: false,
width: '5em', width: '5em',
fixed: 'left',
render: (_, r) => r.object_name, render: (_, r) => r.object_name,
}, },
{ {
@ -91,6 +93,7 @@ export default observer((props) => {
valueType: 'digit', valueType: 'digit',
fieldProps: { style: { width: '100%' }, step: 10000 * 100 }, fieldProps: { style: { width: '100%' }, step: 10000 * 100 },
width: '6em', width: '6em',
fixed: 'left',
formItemProps: { formItemProps: {
style: { width: '100%' }, style: { width: '100%' },
}, },

@ -35,7 +35,7 @@ class DataTypeSelect extends Component {
{...extProps} {...extProps}
> >
{dateTypes.map((ele) => ( {dateTypes.map((ele) => (
<Select.Option key={ele.key} value={ele.key}> <Select.Option key={ele.key} value={ele.key} disabled={extProps.disabledkeys.includes(ele.key)}>
{ele.label} {ele.label}
</Select.Option> </Select.Option>
))} ))}

@ -28,7 +28,7 @@ class GroupSelect extends Component {
}} }}
labelInValue={false} labelInValue={false}
maxTagCount={1} maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`} maxTagPlaceholder={(omittedValues) => `+${omittedValues.length}...`}
allowClear={_mode != null} allowClear={_mode != null}
{...extProps} {...extProps}
> >

@ -0,0 +1,40 @@
import React from 'react';
import { Select } from 'antd';
import { observer } from 'mobx-react';
import { HotelStars as options } from '../../libs/ht';
const HotelStars = (props) => {
const { mode, value, onChange, show_all, ...extProps } = props;
const _show_all = ['tags', 'multiple'].includes(mode) ? false : show_all;
return (
<div>
<Select
mode={mode || null}
allowClear
style={{ width: '100%' }}
placeholder="星级"
value={value || undefined}
onChange={(value) => {
if (typeof onChange === 'function') {
onChange(value);
}
}}
labelInValue={false}
{...extProps}
options={options}
>
{_show_all ? (
<Select.Option key="-1" value="ALL">
ALL
</Select.Option>
) : (
''
)}
</Select>
</div>
);
};
/**
* 酒店星级
*/
export default observer(HotelStars);

@ -1,7 +1,5 @@
import React from 'react'; import React from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
import querystring from 'querystring';
// import * as oMapper from 'object-mapper';
import { fetchJSON } from './../../utils/request'; import { fetchJSON } from './../../utils/request';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { isEmpty, merge, objectMapper } from './../../utils/commons'; import { isEmpty, merge, objectMapper } from './../../utils/commons';
@ -19,7 +17,6 @@ function curl(opts, callback) {
currentValue = opts.value; currentValue = opts.value;
function fake() { function fake() {
// console.log(currentValue, opts.value);
if (currentValue === opts.value && opts.value === '空') { if (currentValue === opts.value && opts.value === '空') {
const _p = [{ 'key': '0', 'label': '空' }]; const _p = [{ 'key': '0', 'label': '空' }];
return callback(_p); return callback(_p);
@ -29,11 +26,12 @@ function curl(opts, callback) {
// code: 'utf-8', // code: 'utf-8',
// q: opts.value, // q: opts.value,
// }).toString(); // }).toString();
const resultkey = opts.resultkey || 'result';
fetchJSON(`${opts.url}`, param) fetchJSON(`${opts.url}`, param)
.then(d => { .then(d => {
if (currentValue === opts.value) { if (currentValue === opts.value) {
const result = objectMapper(d.result, opts.map) || []; const result = objectMapper(d[resultkey], opts.map) || [];
callback(result); callback(result);
} }
}); });
@ -65,7 +63,7 @@ class SearchInput extends React.Component {
if (this.props.autoGet === true) { if (this.props.autoGet === true) {
const { map, resultkey, dependenciesFun } = this.props; const { map, resultkey, dependenciesFun } = this.props;
const param = typeof dependenciesFun === 'function' ? dependenciesFun() : {}; const param = typeof dependenciesFun === 'function' ? dependenciesFun() : {};
const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {}); const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: typeof map[v] === 'string' ? { key: map[v] } : (map[v] || []).map(vi => ({ key: vi})) }), {});
curl({ value: '', url: this.props.url || '', map: mapKey, resultkey, param }, (data) => curl({ value: '', url: this.props.url || '', map: mapKey, resultkey, param }, (data) =>
this.setState({ data, autoData: data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : '')) this.setState({ data, autoData: data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : ''))
); );
@ -90,8 +88,10 @@ class SearchInput extends React.Component {
} }
const { map, resultkey, dependenciesFun } = this.props; const { map, resultkey, dependenciesFun } = this.props;
const param = typeof dependenciesFun === 'function' ? dependenciesFun() : {}; const param = typeof dependenciesFun === 'function' ? dependenciesFun() : {};
const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {}); // const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {});
if (value || !isEmpty(param)) { const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: typeof map[v] === 'string' ? { key: map[v] } : (map[v] || []).map(vi => ({ key: vi})) }), {});
if ((value && this.state.data.length === 0) || !isEmpty(param)) {
curl({ value, url: this.props.url || '', map: mapKey, resultkey, param }, (data) => curl({ value, url: this.props.url || '', map: mapKey, resultkey, param }, (data) =>
this.setState({ data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : '')) this.setState({ data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : ''))
); );
@ -104,6 +104,10 @@ class SearchInput extends React.Component {
this.setState({ value }, () => this.props.onChange(value, option)); this.setState({ value }, () => this.props.onChange(value, option));
}; };
handleFilter = (value, option) => {
return String(option?.children || option?.label || option?.value || '').toLowerCase().includes(value.toLowerCase());
};
render() { render() {
const options = this.state.data.map(d => <Option key={d.key} extradata={d.options}>{d.label}</Option>); const options = this.state.data.map(d => <Option key={d.key} extradata={d.options}>{d.label}</Option>);
const { onSearchAfter, defaultOptions, autoGet, dependenciesFun, ...props } = this.props; const { onSearchAfter, defaultOptions, autoGet, dependenciesFun, ...props } = this.props;
@ -117,13 +121,15 @@ class SearchInput extends React.Component {
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
defaultActiveFirstOption={false} defaultActiveFirstOption={false}
showArrow={false} showArrow={false}
filterOption={false} filterOption={this.handleFilter}
onSearch={this.handleSearch} onSearch={this.handleSearch}
onChange={this.handleChange} onChange={this.handleChange}
onFocus={() => this.handleSearch('')} onFocus={() => this.handleSearch('')}
notFoundContent={null} notFoundContent={null}
allowClear={true} allowClear={true}
onClear={this.handleClear} onClear={this.handleClear}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => `+${omittedValues.length}`}
> >
{options} {options}
</Select> </Select>

@ -3,7 +3,7 @@ import { toJS } from 'mobx';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT, stores_Context } from './../../config'; import { DATE_FORMAT, SMALL_DATETIME_FORMAT, stores_Context } from './../../config';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { Form, Row, Col, Select, Button, Space, DatePicker } from 'antd'; import { Form, Row, Col, Select, Button, Space, DatePicker, Input, InputNumber } from 'antd';
import moment from 'moment'; import moment from 'moment';
// import locale from 'antd/es/date-picker/locale/zh_CN'; // import locale from 'antd/es/date-picker/locale/zh_CN';
import BusinessSelect from './BusinessSelect'; import BusinessSelect from './BusinessSelect';
@ -14,9 +14,11 @@ import DateTypeSelect from './DataTypeSelect';
import DatePickerCharts from './DatePickerCharts'; import DatePickerCharts from './DatePickerCharts';
import YearPickerCharts from './YearPickerCharts'; import YearPickerCharts from './YearPickerCharts';
import SearchInput from './Input'; import SearchInput from './Input';
import { objectMapper, at, empty } from './../../utils/commons'; import { objectMapper, at, empty, isEmpty } from './../../utils/commons';
import { departureDateTypes } from './../../libs/ht';
import './search.css'; import './search.css';
import HotelStarSelect from './HotelStarSelect';
const EditableContext = createContext(); const EditableContext = createContext();
const Option = Select.Option; const Option = Select.Option;
@ -52,6 +54,11 @@ export default observer((props) => {
transform: (value) => value?.key || '', transform: (value) => value?.key || '',
default: '', default: '',
}, },
'departureDateType': {
key: 'DateType',
transform: (value) => value?.key || '',
default: '',
},
'HTBusinessUnits': { 'HTBusinessUnits': {
key: 'HTBusinessUnits', key: 'HTBusinessUnits',
transform: (value) => { transform: (value) => {
@ -62,21 +69,21 @@ export default observer((props) => {
'businessUnits': { 'businessUnits': {
key: 'businessUnits', key: 'businessUnits',
transform: (value) => { transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : ''; return isEmpty(value) ? 'ALL': Array.isArray(value) ? value.map((ele) => ele.value).join(',') : value ? value.value : '';
}, },
default: '', default: '',
}, },
'DepartmentList': { 'DepartmentList': {
key: 'DepartmentList', key: 'DepartmentList',
transform: (value) => { transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : ''; return isEmpty(value) ? 'ALL': Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : '';
}, },
default: '', default: '',
}, },
'WebCode': { 'WebCode': {
key: 'WebCode', key: 'WebCode',
transform: (value) => { transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : ''; return isEmpty(value) ? 'ALL': Array.isArray(value) ? value.map((ele) => ele.key).filter(ele => ele !== 'ALL').join(',') : value ? value.key : '';
}, },
default: '', default: '',
}, },
@ -87,7 +94,13 @@ export default observer((props) => {
}, },
'operator': { 'operator': {
key: 'operator', key: 'operator',
transform: (value) => value?.key || '', // transform: (value) => value?.key || '',
transform: (value) => Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '',
default: '',
},
'date': {
key: 'date',
transform: (value) => (value ? moment(value).format(SMALL_DATETIME_FORMAT) : undefined),
default: '', default: '',
}, },
'applyDate': [ 'applyDate': [
@ -148,9 +161,54 @@ export default observer((props) => {
transform: (value) => value?.key || '', transform: (value) => value?.key || '',
default: '', default: '',
}, },
'billtype': {
key: 'billtype',
transform: (value) => isEmpty(value) ? 'ALL' : value?.key || '',
default: '',
},
'agency': {
key: 'agency',
transform: (value) => value?.key || '',
default: '',
},
'countryArea': {
key: 'countryArea',
transform: (value) => value?.value || value?.key || '',
default: '',
},
'orderStatus': {
key: 'orderStatus',
transform: (value) => value?.value || value?.key || '',
default: '',
},
'cruiseDirection': {
key: 'cruiseDirection',
transform: (value) => value?.value || '',
default: '',
},
'cruiseBookType': {
key: 'cruiseBookType',
transform: (value) => value?.value || '',
default: '',
},
'hotelBookType': {
key: 'hotelBookType',
transform: (value) => value?.value || '',
default: '',
},
'hotelRecommandRate': {
key: 'hotelRecommandRate',
transform: (value) => value?.value || '',
default: '',
},
'hotelStar': {
key: 'hotelStar',
transform: (value) => value?.value || '',
default: '',
},
}; };
let dest = {}; let dest = {};
const { applyDate, applyDate2, year, yearDiff, dates, months, ...omittedValue } = values; const { departureDateType, applyDate, applyDate2, year, yearDiff, dates, months, date, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) }; dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) { for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) { if (Object.prototype.hasOwnProperty.call(dest, key)) {
@ -187,21 +245,21 @@ export default observer((props) => {
}; };
const onValuesChange = (...args) => { const onValuesChange = (...args) => {
const [changedValues, allValues] = args; const [changedValues, allValues] = args;
// console.log('form onValuesChange', Object.keys(changedValues), args);
const dest = formValuesMapper(allValues); const dest = formValuesMapper(allValues);
searchFormStore.setFormValues(allValues); searchFormStore.setFormValues(allValues);
searchFormStore.setFormValuesToSub(dest); searchFormStore.setFormValuesToSub(dest);
// console.log('form onValuesChange', Object.keys(changedValues), args);
}; };
return ( return (
// layout="inline" // layout="inline"
<Form form={form} name="advanced_search" className="orders-search-form" onFinish={onFinish} onValuesChange={onValuesChange}> <Form form={form} name="advanced_search" className="orders-search-form" onFinish={onFinish} onValuesChange={onValuesChange}>
<EditableContext.Provider value={form}> <EditableContext.Provider value={form}>
<Row gutter={10} style={{ background: '#f9fafa', margin: '0px 0px 10px 0px', padding: '16px 8px', boxShadow: '0px 0px 3px 0px rgba(0,0,0,0.15)' }}> <Row gutter={10} style={{ background: '#f9fafa', margin: '0px 0px 10px 0px', padding: '16px 8px 0 8px', boxShadow: '0px 0px 3px 0px rgba(0,0,0,0.15)' }}>
{getFields({ sort, initialValue, hides, shows, fieldProps, form })} {getFields({ sort, initialValue, hides, shows, fieldProps, form })}
{/* 'textAlign': 'right' */} {/* 'textAlign': 'right' */}
<Col flex="1 0 120px" style={{ padding: '0px 5px' }}> <Col flex="1 0 90px" style={{ padding: '0px 5px', display: 'flex', justifyContent: 'flex-end', alignItems: 'flex-start' }}>
<Space align="center"> <Space align="center">
<Button size={'middle'} type="primary" icon={<SearchOutlined />} htmlType="submit"> <Button size={'middle'} type="primary" icon={<SearchOutlined />} htmlType="submit">
{confirmText || '统计'} {confirmText || '统计'}
@ -224,22 +282,53 @@ function getFields(props) {
const layoutProps = { const layoutProps = {
gutter: { xs: 8, sm: 8, lg: 16 }, gutter: { xs: 8, sm: 8, lg: 16 },
lg: { span: 4 }, lg: { span: 4 },
md: { span: 8 },
sm: { span: 12 }, sm: { span: 12 },
xs: { span: 24 }, xs: { span: 24 },
}; };
const item = (name, sort = 0, render, col) => { const item = (name, sort = 0, render, col) => {
const customCol = col || 4; const customCol = col || 4;
const mdCol = customCol * 2;
return { return {
'key': '', 'key': '',
sort, sort,
name, name,
render, render,
'hide': false, 'hide': false,
'col': { lg: { span: customCol } }, 'col': { lg: { span: customCol }, md: { span: mdCol < 8 ? 10 : mdCol}, flex: mdCol < 8 ? "1 0" : "" },
}; };
}; };
let baseChildren = []; let baseChildren = [];
baseChildren = [ baseChildren = [
item(
'keyword', // {...fieldComProps.keyword}
99,
<Form.Item name="keyword" {...fieldProps.keyword}>
<Input allowClear {...fieldProps.keyword} />
</Form.Item>,
fieldProps?.keyword?.col || 6
),
item(
'agency',
99,
<Form.Item name={'agency'}>
<SearchInput autoGet url="/service-web/QueryData/GetVEIName" map={{ 'CAV_VEI_SN': 'key', 'VEI2_CompanyBN': 'label' }} resultkey={'result1'} placeholder="所有地接社" {...fieldProps.agency} />
</Form.Item>
),
item(
'billtype',
99,
<Form.Item name={'billtype'}>
<SearchInput
autoGet
url="/service-web/QueryData/GetCreditCardBillType"
map={{ 'cb_billtype': ['key', 'label'] }}
// map={{ 'cb_billtype': 'key' }}
resultkey={'billtype'}
placeholder="所有账单类型"
/>
</Form.Item>
),
item( item(
'HTBusinessUnits', 'HTBusinessUnits',
99, 99,
@ -257,16 +346,22 @@ function getFields(props) {
item( item(
'DepartmentList', 'DepartmentList',
99, 99,
<Form.Item name={`DepartmentList`} initialValue={at(props, 'initialValue.DepartmentList')[0] || (fieldProps?.DepartmentList?.show_all ? { key: 'ALL', label: '所有小组' } : undefined)}> <Form.Item
name={`DepartmentList`}
initialValue={at(props, 'initialValue.DepartmentList')[0] || (fieldProps?.DepartmentList?.show_all ? { key: 'ALL', label: '所有小组' } : undefined)}
rules={[{ required: true, message: '选择小组' }]}
>
<GroupSelect {...fieldProps.DepartmentList} labelInValue={true} /> <GroupSelect {...fieldProps.DepartmentList} labelInValue={true} />
</Form.Item> </Form.Item>,
fieldProps?.DepartmentList?.col
), ),
item( item(
'WebCode', 'WebCode',
99, 99,
<Form.Item name={`WebCode`} initialValue={at(props, 'initialValue.WebCode')[0] || (fieldProps?.WebCode?.show_all ? { key: 'ALL', label: '所有来源' } : undefined)}> <Form.Item name={`WebCode`} initialValue={at(props, 'initialValue.WebCode')[0] || (fieldProps?.WebCode?.show_all ? { key: 'ALL', label: '所有来源' } : undefined)}>
<SiteSelect {...fieldProps.WebCode} labelInValue={true} /> <SiteSelect {...fieldProps.WebCode} labelInValue={true} />
</Form.Item> </Form.Item>,
fieldProps?.WebCode?.col
), ),
item( item(
'IncludeTickets', 'IncludeTickets',
@ -282,16 +377,70 @@ function getFields(props) {
</Option> </Option>
</Select> </Select>
</Form.Item>, </Form.Item>,
2 3
),
item(
'countryArea',
99,
<Form.Item name={`countryArea`} initialValue={at(props, 'initialValue.countryArea')[0] || (fieldProps?.countryArea?.show_all ? { key: 'all', label: '国内外' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="国内外" labelInValue allowClear={fieldProps?.countryArea?.show_all || false}>
{fieldProps?.countryArea?.show_all && (
<Option key="all" value="" disabled>
国内外
</Option>
)}
<Option key="china" value="china">
国内
</Option>
<Option key="foreign" value="foreign">
国外
</Option>
</Select>
</Form.Item>,
3
),
item(
'orderStatus',
99,
<Form.Item name={`orderStatus`} initialValue={at(props, 'initialValue.orderStatus')[0] || (fieldProps?.orderStatus?.show_all ? { key: '-1', label: '成行状态' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="成行状态" labelInValue allowClear>
{fieldProps?.orderStatus?.show_all && (
<Select.Option key="-1" value="-1" disabled>
成行状态
</Select.Option>
)}
<Select.Option key="已成行" value="1">
已成行
</Select.Option>
<Select.Option key="未成行" value="0">
未成行
</Select.Option>
</Select>
</Form.Item>,
3
), ),
// //
item( item(
'DateType', 'DateType',
99, 99,
<Form.Item name={`DateType`} initialValue={at(props, 'initialValue.DateType')[0] || { key: 'applyDate', label: '提交日期' }}> <Form.Item name={`DateType`} initialValue={at(props, 'initialValue.DateType')[0] || { key: 'applyDate', label: '提交日期' }}>
<DateTypeSelect labelInValue={true} /> <DateTypeSelect labelInValue={true} disabledkeys={fieldProps?.DateType?.disabledKeys || []} />
</Form.Item>,
fieldProps?.DateType?.col || 3
),
item(
'departureDateType',
99,
<Form.Item name={`departureDateType`} initialValue={at(props, 'initialValue.departureDateType')[0] || { key: 'departureDate', label: '抵达日期' }}>
<Select labelInValue={true} style={{ width: '100%' }} placeholder="选择日期类型">
{departureDateTypes.map((ele) => (
<Select.Option key={ele.key} value={ele.key} disabled={fieldProps?.departureDateType?.disabledKeys.includes(ele.key)}>
{ele.label}
</Select.Option>
))}
</Select>
</Form.Item>, </Form.Item>,
2 fieldProps?.departureDateType?.col || 3
), ),
item( item(
'years', 'years',
@ -300,7 +449,7 @@ function getFields(props) {
{/* <DatePicker picker="year" placeholder='年份' /> */} {/* <DatePicker picker="year" placeholder='年份' /> */}
<YearPickerCharts {...fieldProps.years} /> <YearPickerCharts {...fieldProps.years} />
</Form.Item>, </Form.Item>,
2 3
), ),
item( item(
'months', 'months',
@ -308,7 +457,7 @@ function getFields(props) {
<Form.Item> <Form.Item>
<DatePicker picker="month" placeholder="月份" /> <DatePicker picker="month" placeholder="月份" />
</Form.Item>, </Form.Item>,
2 3
), ),
item( item(
'dates', 'dates',
@ -316,19 +465,40 @@ function getFields(props) {
<Form.Item> <Form.Item>
<DatePickerCharts isform={true} {...fieldProps.dates} form={form} /> <DatePickerCharts isform={true} {...fieldProps.dates} form={form} />
</Form.Item>, </Form.Item>,
midCol fieldProps?.dates?.col || midCol
),
item(
'date',
99,
<Form.Item name={`date`} initialValue={at(props, 'initialValue.date')[0]}>
<DatePicker picker="date" placeholder="日期" />
</Form.Item>,
fieldProps?.date?.col || midCol
), ),
item( item(
'operator', 'operator',
99, 99,
<Form.Item name={'operator'} dependencies={['DepartmentList']} > <Form.Item name={'operator'} dependencies={['DepartmentList']}>
<SearchInput <SearchInput
{...fieldProps.operator}
autoGet autoGet
url="/service-Analyse2/GetOperatorInfo" url="/service-Analyse2/GetOperatorInfo"
map={{ 'op_id': 'key', 'cn_name': 'label' }} map={{ 'op_id': 'key', 'cn_name': 'label' }}
resultkey={'result'} resultkey={'result'}
placeholder="输入搜索顾问: 中/英名字" placeholder="输入搜索顾问: 中/英名字"
dependenciesFun={() => ({ dept_id: (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', ''), ...(fieldProps?.operator?.param || {}) })} dependenciesFun={() => {
let dependenciesValue = '';
if (Array.isArray(form.getFieldValue('DepartmentList'))) {
dependenciesValue = form
.getFieldValue('DepartmentList')
.map((e) => e.value)
.join(',')
.replace('ALL', '');
} else {
dependenciesValue = (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', '');
}
return { dept_id: dependenciesValue, ...(fieldProps?.operator?.param || {}) };
}}
/> />
</Form.Item> </Form.Item>
), ),
@ -353,6 +523,128 @@ function getFields(props) {
<SearchInput autoGet url="/service-Analyse2/GetGlobalDestinationInfo" map={{ 'c_id': 'key', 'cn_name': 'label' }} resultkey={'result'} placeholder="输入搜索城市: 中/英名字" /> <SearchInput autoGet url="/service-Analyse2/GetGlobalDestinationInfo" map={{ 'c_id': 'key', 'cn_name': 'label' }} resultkey={'result'} placeholder="输入搜索城市: 中/英名字" />
</Form.Item> </Form.Item>
), ),
item(
'cruiseDirection',
99,
<Form.Item name={`cruiseDirection`} initialValue={at(props, 'initialValue.cruiseDirection')[0] || undefined}>
<Select style={{ width: '100%' }} placeholder="上下水" labelInValue allowClear>
{fieldProps?.cruiseDirection?.show_all && (
<Option key="all" value="" disabled>
上下水
</Option>
)}
<Option key="1" value="1">
上水
</Option>
<Option key="2" value="2">
下水
</Option>
{/* <Option key="long" value="long">
长线
</Option> */}
</Select>
</Form.Item>,
3
),
item(
'cruiseBookType',
99,
<Form.Item name={`cruiseBookType`} initialValue={at(props, 'initialValue.cruiseBookType')[0] || (fieldProps?.cruiseBookType?.show_all ? { key: '-1', label: '预定类型' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="预定类型" labelInValue allowClear>
{fieldProps?.cruiseBookType?.show_all && (
<Option key="-1" value="-1" disabled>
预定类型
</Option>
)}
<Option key="1" value="1">
单订三峡
</Option>
<Option key="0" value="0">
含行程
</Option>
</Select>
</Form.Item>,
3
),
item(
'roomsRange',
99,
<Form.Item noStyle>
<Input.Group compact>
<Form.Item name={'RoomNumStart'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center' }} placeholder="房间数" />
</Form.Item>
<Input style={{ width: 30, borderLeft: 0, borderRight: 0, pointerEvents: 'none', }} placeholder="~" disabled />
<Form.Item name={'RoomNumEnd'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center', borderLeft: 0 }} placeholder="房间数" />
</Form.Item>
</Input.Group>
</Form.Item>,
fieldProps?.roomsRange?.col || 4
),
item(
'personRange',
99,
<Form.Item noStyle>
<Input.Group compact>
<Form.Item name={'PersonNumStart'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center' }} placeholder="人数" />
</Form.Item>
<Input style={{ width: 30, borderLeft: 0, borderRight: 0, pointerEvents: 'none', }} placeholder="~" disabled />
<Form.Item name={'PersonNumEnd'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center', borderLeft: 0 }} placeholder="人数" />
</Form.Item>
</Input.Group>
</Form.Item>,
fieldProps?.personRange?.col || 4
),
item(
'hotelBookType',
99,
<Form.Item name={`hotelBookType`} initialValue={at(props, 'initialValue.hotelBookType')[0] || (fieldProps?.hotelBookType?.show_all ? { key: 'all', label: '预定类型' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="预定类型" labelInValue allowClear={fieldProps?.hotelBookType?.show_all || false}>
{fieldProps?.hotelBookType?.show_all && (
<Option key="all" value="" disabled>
预定类型
</Option>
)}
<Option key="1" value="1">
代订
</Option>
<Option key="0" value="0">
自订
</Option>
</Select>
</Form.Item>,
3
),
item(
'hotelRecommandRate',
99,
<Form.Item name={`hotelRecommandRate`} initialValue={at(props, 'initialValue.hotelRecommandRate')[0] || (fieldProps?.hotelRecommandRate?.show_all ? { key: 'all', label: '推荐等级' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="推荐等级" labelInValue allowClear={fieldProps?.hotelRecommandRate?.show_all || false}>
{fieldProps?.hotelRecommandRate?.show_all && (
<Option key="all" value="" disabled>
推荐等级
</Option>
)}
<Option key="1" value="1">
主推
</Option>
<Option key="0" value="0">
非主推
</Option>
</Select>
</Form.Item>,
3
),
item(
'hotelStar',
99,
<Form.Item name={`hotelStar`} initialValue={at(props, 'initialValue.hotelStar')[0] || undefined}>
<HotelStarSelect {...fieldProps.hotelStar} labelInValue={true} />
</Form.Item>
),
]; ];
baseChildren = baseChildren baseChildren = baseChildren
.map((x) => { .map((x) => {

@ -13,13 +13,17 @@ class SiteSelect extends Component {
const { store, mode, value, onChange, show_all, ...extProps } = this.props; const { store, mode, value, onChange, show_all, ...extProps } = this.props;
const _mode = mode || store?.group_select_mode || null; const _mode = mode || store?.group_select_mode || null;
const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all; const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all;
const __value = ['tags', 'multiple'].includes(_mode) ? (value?.constructor === Object ? [value] : value) : undefined;
const _value = !['tags', 'multiple'].includes(_mode)
? value || store?.webcode || undefined
: (__value || store?.webcode || []).filter((item) => String(item?.value || item.key).toLowerCase() !== 'all');
return ( return (
<div> <div>
<Select <Select
mode={_mode} mode={_mode}
style={{width: '100%'}} style={{width: '100%'}}
placeholder="选择来源" placeholder="所有来源"
defaultValue={value || store?.webcode || undefined } value={_value}
onChange={(value) => { onChange={(value) => {
if (typeof onChange === 'function') { if (typeof onChange === 'function') {
onChange(value); onChange(value);
@ -28,12 +32,14 @@ class SiteSelect extends Component {
}} }}
labelInValue={false} labelInValue={false}
maxTagCount={1} maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`} maxTagPlaceholder={(omittedValues) => ` +${omittedValues.length}...`}
allowClear={_mode != null} allowClear={_mode != null}
dropdownStyle={{height: '400px'}}
listHeight={400}
{...extProps} {...extProps}
> >
{_show_all===true ? <Select.Option key="1" value="ALL">所有来源</Select.Option> : ''} {_show_all===true ? <Select.Option key="ALL" value="ALL">所有来源</Select.Option> : ''}
{sites.map(ele => <Select.Option key={ele.key} value={ele.code}>{ele.label}</Select.Option>)} {sites.map(ele => <Select.Option key={ele.code} value={ele.code}>{ele.label}</Select.Option>)}
</Select> </Select>
</div> </div>
); );

@ -51,7 +51,7 @@ class DatePickerCharts extends Component {
locale={locale} locale={locale}
placeholder={"对比 Year"} placeholder={"对比 Year"}
onChange={(value) => { onChange={(value) => {
const fullYear = [value.clone().set('month', 0).set('date', 1), value.clone().set('month', 11).set('date', 31)]; const fullYear = value ? [value.clone().set('month', 0).set('date', 1), value.clone().set('month', 11).set('date', 31)] : undefined;
if (typeof this.props.onChange === 'function') { if (typeof this.props.onChange === 'function') {
this.props.onChange(fullYear); this.props.onChange(fullYear);
} }

@ -1,7 +1,9 @@
import React from "react"; import React from "react";
import packageInfo from './../package.json';
export const APP_VERSION = packageInfo.version;
export const stores_Context = React.createContext(); export const stores_Context = React.createContext();
export const DATE_FORMAT = "YYYY-MM-DD"; export const DATE_FORMAT = "YYYY-MM-DD";
export const SMALL_DATETIME_FORMAT = 'YYYY-MM-DD 23:59:00'; export const SMALL_DATETIME_FORMAT = 'YYYY-MM-DD 23:59:00';
export const DATETIME_FORMAT = 'YYYY-MM-DD 23:59:59'; export const DATETIME_FORMAT = 'YYYY-MM-DD 23:59:59';
export const HT_HOST = process.env.NODE_ENV === "production" ? "https://p9axztuwd7x8a7.mycht.cn" : "http://202.103.68.100:890"; export const HT_HOST = process.env.NODE_ENV === "production" ? "https://p9axztuwd7x8a7.mycht.cn" : "http://202.103.68.144:890";

@ -1,4 +1,5 @@
import { fixTo4Decimals, fixTo1Decimals } from "../utils/commons"; import moment from 'moment';
import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush, fixTo2Decimals, isEmpty } from '../utils/commons';
/** /**
* 事业部 * 事业部
@ -57,6 +58,7 @@ export const groups = [
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] }, { value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
]; ];
export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {}); export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {});
export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
export const leafGroup = groups.slice(3); export const leafGroup = groups.slice(3);
export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
/** /**
@ -65,44 +67,59 @@ export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
export const sites = [ export const sites = [
{ value: '2', key: '2', label: 'CHT', code: 'CHT' }, { value: '2', key: '2', label: 'CHT', code: 'CHT' },
{ value: '8', key: '8', label: 'AH', code: 'AH' }, { value: '8', key: '8', label: 'AH', code: 'AH' },
{ value: '186', key: '186', label: 'JH', code: 'JH' },
{ value: '163', key: '163', label: 'GH', code: 'GH' }, { value: '163', key: '163', label: 'GH', code: 'GH' },
{ value: '184', key: '184', label: 'GH站外渠道 (中国)', code: 'ZWQD' },
{ value: '185', key: '185', label: 'GH站外渠道 (海外)', code: 'GH_ZWQD_HW' },
{ value: '28', key: '28', label: '客运中国', code: 'GHKYZG' }, { value: '28', key: '28', label: '客运中国', code: 'GHKYZG' },
{ value: '7', key: '7', label: '客运海外', code: 'GHKYHW' }, { value: '7', key: '7', label: '客运海外', code: 'GHKYHW' },
{ value: '172', key: '172', label: 'GHToB 海外', code: 'GHTOBHW' }, { value: '172', key: '172', label: 'GHToB 海外', code: 'GHTOBHW' },
{ value: '176', key: '176', label: 'GHToB 中国', code: 'GHTOBZG' }, { value: '176', key: '176', label: 'GHToB 中国', code: 'GHTOBZG' },
{ value: '11,12,20,21,10,18', key: '11,12,20,21,10,18', label: '国际(入境)', code: 'JP,VAC,IT,GM,RU,VC' }, { value: '11,12,20,21,10,18', key: '11,12,20,21,10,18', label: '国际(入境)', code: 'JP,VAC,IT,GM,RU,VC' },
{ value: '122,200,211,100,188', key: '122,200,211,100,188', label: '国际(海外)', code: 'VACHW,ITHW,GMHW,RUHW,VCHW' }, { value: '122,200,211,100,188', key: '122,200,211,100,188', label: '国际(海外)', code: 'VACHW,ITHW,GMHW,RUHW,VCHW' },
{ value: '178,179,180,181,182,183', key: '178,179,180,181,182,183', label: '国际GH', code: 'gh_gm,gh_jp,gh_vc,gh_vac,gh_ru,gh_it'},
{ value: '11', key: '11', label: '日语', code: 'JP' }, { value: '11', key: '11', label: '日语', code: 'JP' },
{ value: '179', key: '179', label: 'GH-日语', code: 'gh_jp'}, // www.globalhighlights.jp
{ value: '12', key: '12', label: '西语', code: 'VAC' }, { value: '12', key: '12', label: '西语', code: 'VAC' },
{ value: '122', key: '122', label: '西语海外', code: 'VACHW' }, { value: '122', key: '122', label: '西语海外', code: 'VACHW' },
{ value: '181', key: '181', label: 'GH-西语', code: 'gh_vac'}, // www.globalhighlights.es
{ value: '20', key: '20', label: '意大利', code: 'IT' }, { value: '20', key: '20', label: '意大利', code: 'IT' },
{ value: '200', key: '200', label: '意大利海外', code: 'ITHW' }, { value: '200', key: '200', label: '意大利海外', code: 'ITHW' },
{ value: '183', key: '183', label: 'GH-意语', code: 'gh_it'}, // www.globalhighlights.it
{ value: '21', key: '21', label: '德语', code: 'GM' }, { value: '21', key: '21', label: '德语', code: 'GM' },
{ value: '211', key: '211', label: '德语海外', code: 'GMHW' }, { value: '211', key: '211', label: '德语海外', code: 'GMHW' },
{ value: '178', key: '178', label: 'GH-德语', code: 'gh_gm'}, // wwww.globalhighlights.de
{ value: '10', key: '10', label: '俄语', code: 'RU' }, { value: '10', key: '10', label: '俄语', code: 'RU' },
{ value: '100', key: '100', label: '俄语海外', code: 'RUHW' }, { value: '100', key: '100', label: '俄语海外', code: 'RUHW' },
{ value: '182', key: '182', label: 'GH-俄语', code: 'gh_ru'}, // www.globalhighlights.ru
{ value: '18', key: '18', label: '法语', code: 'VC' }, { value: '18', key: '18', label: '法语', code: 'VC' },
{ value: '188', key: '188', label: '法语海外', code: 'VCHW' }, { value: '188', key: '188', label: '法语海外', code: 'VCHW' },
{ value: '180', key: '180', label: 'GH-法语', code: 'gh_vc'}, // www.globalhighlights.fr
{ value: '16', key: '16', label: 'CT', code: 'CT' }, { value: '16', key: '16', label: 'CT', code: 'CT' },
{ value: '30', key: '30', label: 'TP', code: 'trippest' }, { value: '30', key: '30', label: 'TP', code: 'trippest' },
{ value: '31', key: '31', label: '花梨鹰', code: 'HLY' }, { value: '31', key: '31', label: '花梨鹰', code: 'HLY' },
]; ];
export const sitesMappedByCode = sites.reduce((a, c) => ({ ...a, [String(c.code)]: { ...c, key: c.code, value: c.code } }), {});
export const dateTypes = [ export const dateTypes = [
{ key: 'applyDate', value: 'applyDate', label: '提交日期' }, { key: 'applyDate', value: 'applyDate', label: '提交日期' },
{ key: 'ConfirmDate', value: 'ConfirmDate', label: '确认日期' }, { key: 'confirmDate', value: 'confirmDate', label: '确认日期' },
{ key: 'startDate', value: 'startDate', label: '走团日期' }, { key: 'startDate', value: 'startDate', label: '走团日期' },
]; ];
export const departureDateTypes = [
...dateTypes,
{ key: 'departureDate', value: 'departureDate', label: '抵达日期' },
];
/** /**
* 结果字段 * 结果字段
*/ */
export const dataFieldOptions = [ export const dataFieldOptions = [
{ label: '营收', value: 'transactions', formatter: (v) => `${fixTo1Decimals(v / 10000)}`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } }, { label: '营收', value: 'transactions', formatter: (v) => `${fixTo1Decimals((v || 0) / 10000)}`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } },
{ label: '毛利', value: 'SumML', formatter: (v) => `${fixTo1Decimals(v / 10000)}`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } }, { label: '毛利', value: 'SumML', formatter: (v) => `${fixTo1Decimals((v || 0) / 10000)}`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } },
{ label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } }, { label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } },
{ label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } }, { label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } },
{ label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, { label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } },
// { label: '人数', value: 'CJPersonNum', formatter: (v) => v }, { label: '人数', value: 'SumPersonNum', formatter: (v) => v, nestkey: {} },
// todo: more... // todo: more...
]; ];
/** /**
@ -112,7 +129,7 @@ export const dataFieldAlias = dataFieldOptions.reduce(
(a, c) => ({ (a, c) => ({
...a, ...a,
[c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) }, [c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) },
[c.nestkey.v]: { ...c, value: c.nestkey.v, alias: `${c.label}目标`, label: `${c.label}目标`, formatter: (v) => c.formatter(v) }, [c.nestkey.v]: { ...c, value: c.nestkey.v, alias: `${c.label}目标`, label: `${c.label}目标`, formatter: (v) => c.formatter(v), nestkey: { o: c.value } },
}), }),
{} {}
); );
@ -121,11 +138,7 @@ export const dataFieldAlias = dataFieldOptions.reduce(
* KPI对象 * KPI对象
*/ */
export const KPIObjects = [ export const KPIObjects = [
{ key: 'overview', value: 'overview', label: '海纳', data: [ { key: 'overview', value: 'overview', label: '海纳', data: [{ key: 'ALL', value: 'ALL', label: '海纳' }, ...overviewGroup] },
{ key: 'ALL', value: 'ALL', label: '海纳' },
...overviewGroup
]
},
{ {
key: 'bizarea', key: 'bizarea',
value: 'bizarea', value: 'bizarea',
@ -171,3 +184,469 @@ export const KPISubjects = [
// { key: 'reply_eff_wa', value: 'reply_eff_wa', label: 'WA回复效率'}, // { key: 'reply_eff_wa', value: 'reply_eff_wa', label: 'WA回复效率'},
// { key: 'sum_person_num', value: 'sum_person_num', label: '人数' }, // { key: 'sum_person_num', value: 'sum_person_num', label: '人数' },
]; ];
export const HotelStars = [
{ key: '1', value: '1', label: '五星' },
{ key: '2', value: '2', label: '四星' },
{ key: '3', value: '3', label: '三星' },
{ key: '4', value: '4', label: '二星' },
{ key: '8', value: '8', label: '准五星 ' },
{ key: '9', value: '9', label: '准四星' },
{ key: '10', value: '10', label: '客栈' },
{ key: '11', value: '11', label: '公寓' },
{ key: '12', value: '12', label: '四合院酒店' },
{ key: '13', value: '13', label: '豪华五星' },
];
export const CruiseAgency = [
{ key: '14193', value: '14193', label: '长江海外游轮旅游有限公司' },
{ key: '1067', value: '1067', label: '湖北东方皇家游船公司(待删除)' },
{ key: '70', value: '70', label: '湖北皇家长江旅游船有限公司' },
{ key: '159', value: '159', label: '文嘉船务' },
{ key: '77', value: '77', label: '武汉扬子江' },
{ key: '1337', value: '1337', label: '重庆薇灿' },
{ key: '4378', value: '4378', label: '重庆新世纪国旅' },
{ key: '-1', value: '-1', label: '自订' },
];
/**
* 计算指标值的分段区间
* @param {number} value
* @returns
*/
const calcPPPriceRange = (value) => {
if (value < 0) {
return '--';
}
const step = 30; // step = 30 USD
const start = Math.floor(value / step) * step;
const end = start + step;
if (value >= 301) {
return `≥301`;
}
return `${start === 0 ? start : (start+1)}-${end}`;
};
function calculateRangeScale(data, numScales = 36) {
if (!data || data.length === 0 || numScales <= 0) {
return [];
}
const sortedData = [...data].sort((a, b) => a - b);
const min = sortedData[0];
const max = sortedData[sortedData.length - 1];
if (min === max) {
return [roundToNice(min), roundToNice(min)];
}
const scales = [roundToNice(min)];
const scaleSize = sortedData.length / numScales;
for (let i = 1; i < numScales; i++) {
const index = Math.floor(i * scaleSize);
scales.push(roundToNice(sortedData[Math.min(index, sortedData.length - 1)]));
}
scales.push(roundToNice(max));
return [...new Set(scales)];
}
function roundToNice(value) {
if (value === 0) {
return 0;
}
const magnitude = Math.pow(10, Math.floor(Math.log10(Math.abs(value))));
const normalized = value / magnitude;
let rounded;
if (normalized < 1.5) {
rounded = Math.floor(normalized);
} else if (normalized < 3) {
rounded = Math.floor(normalized * 2) / 2; // round to 0.5
} else if (normalized < 5) {
rounded = Math.floor(normalized/2) * 2; // round to 2
} else if (normalized < 7.5) {
rounded = Math.floor(normalized / 5) * 5;
} else {
// rounded = Math.floor(normalized / 5) * 5;
rounded = Math.floor(normalized / 10) * 10;
}
return rounded * magnitude;
};
const findRange = (value, scale) => {
if (value < scale[0]) {
return `0-${scale[0]}`; // `Value ${value} is below the scale range.`;
}
for (let i = 1; i < scale.length; i++) {
if (value >= scale[i - 1] && value < scale[i]) {
return `${scale[i - 1]}-${scale[i]}`; // `Value ${value} is in the range [${scale[i - 1]}, ${scale[i]})`;
}
}
if (value >= scale[scale.length - 1]) {
return `${scale[scale.length - 1]}`; // `Value ${value} is in the range [${scale[scale.length - 1]}, Infinity)`;
}
};
const SumML_range = [1, 1.5, 2, 3, 4].map(v => v * 10000);
/**
* 数据透视计算
* @param {object[]} data
* @param {any[]} groupbyKeys
* @returns
*/
export const pivotBy = (_data, [rows, columns, date]) => {
console.time('pivot3----');
console.log('pivotBy', [rows, columns, date]);
const groupbyKeys = flush([].concat(rows, columns, [date]));
// if (groupbyKeys.includes('PPPriceRange')) {
// }
// 补充计算的字段
const RTXF_WB_values = cloneDeep(_data).map(ele => ele.RTXF_WB); // 人天消费
// const max_RTXF_WB = Math.max(...RTXF_WB_values);
const RTXF_WB_range = calculateRangeScale(RTXF_WB_values);
let data = cloneDeep(_data).map(ele => {
ele.startYearMonth = ele.startDate ? moment(ele.startDate).format('YYYY-MM') : '';
ele.startMonth = ele.startDate ? moment(ele.startDate).format('MM') : '';
ele.applyYearMonth = ele.applyDate ? moment(ele.applyDate).format('YYYY-MM') : '';
ele.applyMonth = ele.applyDate ? moment(ele.applyDate).format('MM') : '';
ele.PPPrice = (Number(ele.orderState) === 1 && ele.tourdays && ele.personNum) ? fixToInt(ele.quotePrice / ele.tourdays / ele.personNum) : -1; // 报价: 人均天
ele.PPPriceRange = calcPPPriceRange(ele.PPPrice);
ele.RTXF_WB_range = findRange(ele.RTXF_WB, RTXF_WB_range);
ele.IsOld_txt = ele.IsOld === '1' ? '老客户' : '否';
ele.isCusCommend_txt = ele.isCusCommend === '1' ? '老客户推荐' : '否';
const hasOld = (ele.IsOld === '1' || ele.isCusCommend === '1') ? 1 : 0;
ele.hasOld = hasOld;
ele.hasOld_txt = hasOld === 1 ? '老客户(推荐)' : '';
// ele.SumML_ctxt1 = ele.ML > 10000 ? '1W+' : '1W-';
// ele.SumML_ctxt1_5 = ele.ML > 15000 ? '1.5W+' : '1.5W-';
// ele.SumML_ctxt2 = ele.ML > 20000 ? '2W+' : '2W-';
// ele.SumML_ctxt3 = ele.ML > 30000 ? '3W+' : '3W-';
// ele.SumML_ctxt4 = ele.ML > 40000 ? '4W+' : '4W-';
ele.SumML_ctxt = findRange(ele.ML, SumML_range);
return ele;
});
// 数组的字段值, 拆分处理
if (groupbyKeys.includes('destinationCountry_AsJOSN')) {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinationCountry_AsJOSN) ? [] : v.destinationCountry_AsJOSN;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinationCountry_AsJOSN: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
return rv;
}, []);
r = r.concat(xv);
return r;
}, []);
}
if (groupbyKeys.includes('destinations_AsJOSN')) {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinations_AsJOSN) ? [] : v.destinations_AsJOSN;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinations_AsJOSN: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
return rv;
}, []);
r = r.concat(xv);
return r;
}, []);
}
const getKeys = (keys) => keys.map((keyField) => [...new Set(data.map((f) => f[keyField]))]);
const [rowsKeys, columnsKeys, dateKeys] = [getKeys(rows), getKeys(columns), [getKeys([date])[0].filter(s => s)]];
// console.log('rowsKeys', rowsKeys, 'columnsKeys', columnsKeys, 'dateKeys', dateKeys);
const calcTradeFields = (dataObj, keepKeys = [], seriesKey = '') => {
const outerKeys = [];
const _keepKeys = [...keepKeys, seriesKey];
const DataGroupByKeys = {};
Object.keys(dataObj).forEach((colKey) => {
const _len = dataObj[colKey].length;
const _rowKey = dataObj[colKey].map((v) => v.key).join('_');
outerKeys.push(_rowKey);
const initialData = {
...pick(dataObj[colKey][0], _keepKeys),
...(keepKeys.length === 0
? { rowLabel: '总' }
: {
rowLabel: cloneDeep(keepKeys)
// .slice(0, -1)
.map((_k) => dataObj[colKey][0][_k])
.join('»'),
}),
_label: colKey || '(空)',
key: _rowKey,
_a:dataObj[colKey].map((v) => `${v.PPPrice}: ${v.PPPriceRange}`).join(', '),
_b:dataObj[colKey].map((v) => `${v.orderState}: ${v.quotePrice}/ ${v.tourdays}/ ${v.personNum}`).join(', '),
SumOrder: _len,
ResumeOrder: 0,
ResumeConfirmOrder: 0,
SumPersonNum: 0,
ConfirmPersonNum: 0,
ConfirmOrder: 0,
transactions: 0,
SumML: 0,
SumML_txt: '',
quotePrice: 0,
tourdays: 0,
applyDays: 0,
confirmDays: 0,
SingleML: 0,
OrderValue: 0,
PPPrice: 0,
AvgPPPrice: 0,
confirmTourdays: 0,
PPPriceRange: '',
unitPPPriceRange: '',
};
const calculatedData = dataObj[colKey].reduce((r, v) => {
r.SumPersonNum += v.personNum;
r.ConfirmPersonNum += Number(v.orderState) === 1 ? v.personNum : 0;
r.ConfirmOrder += Number(v.orderState) === 1 ? 1 : 0;
r.ResumeOrder += v.hasOld === 1 ? 1 : 0;
r.ResumeConfirmOrder += Number(v.orderState) === 1 && v.hasOld === 1 ? 1 : 0;
r.transactions += v.transactions;
r.SumML += Number(v.orderState) === 1 ? v.ML : 0;
r.quotePrice += Number(v.orderState) === 1 ? v.quotePrice : 0;
r.tourdays += v.tourdays;
r.applyDays += v.applyDays;
r.confirmDays += v.confirmDays;
r.PPPrice += Number(v.orderState) === 1 ? v.PPPrice : 0;
r.confirmTourdays += Number(v.orderState) === 1 ? v.tourdays : 0;
return r;
}, initialData);
// Calculations
calculatedData.tourdays = Math.ceil(calculatedData.tourdays / _len);
calculatedData.confirmTourdays = calculatedData.ConfirmOrder > 0 ? Math.ceil(calculatedData.confirmTourdays / calculatedData.ConfirmOrder) : '0';
calculatedData.applyDays = Math.ceil(calculatedData.applyDays / _len);
calculatedData.confirmDays = Math.ceil(calculatedData.confirmDays / _len);
const _rowCalc = {
ConfirmRates: calculatedData.ConfirmOrder ? fixTo4Decimals(calculatedData.ConfirmOrder / calculatedData.SumOrder*100) : 0,
OrderValue: calculatedData.SumOrder ? fixToInt(calculatedData.SumML / calculatedData.SumOrder) : 0,
SingleML: calculatedData.ConfirmOrder ? fixToInt(calculatedData.SumML / calculatedData.ConfirmOrder) : 0,
AvgPPPrice: calculatedData.ConfirmOrder ? fixToInt(calculatedData.PPPrice / calculatedData.ConfirmOrder) : -1,
unitPPPrice:
calculatedData.confirmTourdays && calculatedData.ConfirmPersonNum ? fixToInt(calculatedData.quotePrice / calculatedData.confirmTourdays / calculatedData.ConfirmPersonNum) : -1,
};
// Formatter
calculatedData.transactions = fixTo2Decimals(calculatedData.transactions);
calculatedData.SumML = fixTo2Decimals(calculatedData.SumML);
calculatedData.SumML_txt = dataFieldAlias.SumML.formatter(calculatedData.SumML);
calculatedData.quotePrice = fixTo2Decimals(calculatedData.quotePrice);
calculatedData.ConfirmRates_txt = dataFieldAlias.ConfirmRates.formatter(_rowCalc.ConfirmRates);
// calculatedData.SingleML = fixTo2Decimals(calculatedData.SingleML);
calculatedData.PPPrice = fixTo2Decimals(calculatedData.PPPrice);
calculatedData.PPPriceRange = calcPPPriceRange(_rowCalc.AvgPPPrice);
calculatedData.unitPPPriceRange = calcPPPriceRange(_rowCalc.unitPPPrice);
DataGroupByKeys[colKey] = { ...calculatedData, ..._rowCalc };
});
return { groupByKeys: DataGroupByKeys, key: outerKeys.join('_'), keys: outerKeys.join('_').split('_') };
};
const groupData = groupBy(data, (row) => groupbyKeys.map((kk) => `${row[kk]}`).join('=@='));
const rowsNcolumnsItems = calcTradeFields(groupData, [...rows, ...columns], date);
const pivotResult = Object.values(rowsNcolumnsItems.groupByKeys);
const transposeData = (keys, dataProp, [dataKey, colKeys]=[]) =>
Object.keys(dataProp)
.map((rowKey) => {
const rowLabel = keys.length === 0 ? '总' : keys.map(ekey => dataProp[rowKey][0][ekey]).join('»');
const _colKey = dataKey || 'dataKey';
const _colData = groupBy(dataProp[rowKey], (crow) => (colKeys || keys).map((kk) => `${crow[kk]}`).join('=@='));
const _columnsObj = calcTradeFields(_colData);
return { ...pick(dataProp[rowKey][0], keys), [_colKey]: _columnsObj.groupByKeys, key: _columnsObj.key, keys: _columnsObj.keys, rowLabel };
})
.map((everyR) => {
const _colKey = dataKey || 'dataKey';
const allColumns = Object.values(everyR[_colKey]).reduce((r, c) => r.concat([c]), []);
const summaryCalc = [
'ConfirmOrder',
'SumOrder',
'ResumeOrder', 'ResumeConfirmOrder',
'SumML',
'transactions',
'SumPersonNum',
'ConfirmPersonNum',
'quotePrice',
'tourdays',
'applyDays',
'confirmDays',
'PPPrice', 'AvgPPPrice',
'confirmTourdays',
].reduce((r, skey) => ({ ...r,
[skey]: allColumns.reduce((a, c) => (fixTo2Decimals(a + c[skey])), 0),
[`${skey}_arr`]: allColumns.reduce((a, c) => (a.concat(c[skey])), []),
}),everyR);
summaryCalc.tourdays = Math.ceil(summaryCalc.tourdays / allColumns.length);
summaryCalc.confirmTourdays = summaryCalc.ConfirmOrder > 0 ? Math.ceil(summaryCalc.confirmTourdays / summaryCalc.ConfirmOrder) : '0';
summaryCalc.applyDays = Math.ceil(summaryCalc.applyDays / allColumns.length);
summaryCalc.confirmDays = Math.ceil(summaryCalc.confirmDays / allColumns.length);
summaryCalc.ConfirmRates = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.ConfirmOrder / summaryCalc.SumOrder*100) : 0;
summaryCalc.ResumeConfirmRates = summaryCalc.ResumeConfirmOrder ? fixTo2Decimals(summaryCalc.ResumeConfirmOrder / summaryCalc.ResumeOrder*100) : 0;
summaryCalc.OrderValue = summaryCalc.SumOrder ? fixToInt(summaryCalc.SumML / summaryCalc.SumOrder) : 0;
summaryCalc.SingleML = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.SumML / summaryCalc.ConfirmOrder) : 0;
summaryCalc.AvgPPPrice = Math.ceil(summaryCalc.AvgPPPrice / allColumns.length);
summaryCalc.PPPriceRange = calcPPPriceRange(summaryCalc.AvgPPPrice);
summaryCalc.unitPPPrice = summaryCalc.confirmTourdays && summaryCalc.ConfirmPersonNum ? fixToInt(summaryCalc.quotePrice / summaryCalc.confirmTourdays / summaryCalc.ConfirmPersonNum) : -1;
summaryCalc.unitPPPriceRange = calcPPPriceRange(summaryCalc.unitPPPrice);
summaryCalc.SumML_txt = dataFieldAlias.SumML.formatter(summaryCalc.SumML);
summaryCalc.ConfirmRates_txt = dataFieldAlias.ConfirmRates.formatter(summaryCalc.ConfirmRates);
return { ...everyR, ...summaryCalc };
});
const rowsData = groupBy(data, (row) => rows.map((kk) => `${row[kk]}`).join('=@='));
const summaryRows = transposeData(rows, rowsData, ['columns', columns]);
const columnsData = groupBy(data, (row) => columns.map((kk) => `${row[kk]}`).join('=@='));
const summaryColumns = transposeData(columns, columnsData, ['rows', rows]);
const rowsMixcolumns = flush([].concat(rows, columns));
const rowsMixcolumnsData = groupBy(data, (row) => rowsMixcolumns.map((kk) => `${row[kk]}`).join('=@='));
const summaryMix = transposeData(rowsMixcolumns, rowsMixcolumnsData);
console.timeEnd('pivot3----');
return { data: pivotResult, columnValues: [rowsKeys, columnsKeys, dateKeys], summaryRows, summaryColumns, pivotKeys: groupbyKeys, summaryMix };
};
// todo: 优化 pivotBy 速度
export const pivotBy3 = (data, [rows, columns, date]) => {
console.log('pivotBy', [rows, columns, date]);
console.time('pivot2');
// const rowKeys = new Set(data.map(row => row[rows[0]]));
const rowKeys = rows.map((keyField) => {
const keyu = new Set(data.map((f) => f[keyField]));
return keyu;
});
const colKeys = new Set(data.map(row => row[columns[0]]));
const dateKeys = new Set(data.map(row => row[date]));
const aggregatedData = {};
data.forEach(row => {
const rowKey = row[rows[0]] ?? '__total';
const colKey = row[columns[0]] ?? '__total';
const dateKey = row[date];
if (!aggregatedData[rowKey]) {
aggregatedData[rowKey] = {};
}
// if (!aggregatedData[rowKey][colKey]) {
// aggregatedData[rowKey][colKey] = {};
// }
if (!aggregatedData[rowKey][colKey]) {
aggregatedData[rowKey][colKey] = {
SumOrder: 0,
// other aggregated fields
SumPersonNum: 0,
ConfirmOrder: 0,
transactions: 0,
SumML: 0,
// ...
quotePrice: 0,
tourdays: 0,
applyDays: 0,
confirmDays: 0,
};
}
aggregatedData[rowKey][colKey].SumOrder++;
aggregatedData[rowKey][colKey].SumPersonNum += row.personNum;
aggregatedData[rowKey][colKey].ConfirmOrder += Number(row.orderState === 1);
aggregatedData[rowKey][colKey].transactions += row.transactions;
aggregatedData[rowKey][colKey].SumML += row.ML;
// aggregate other fields
});
const summarizedData = [];
// Generate summary rows
for (const rowKey of rowKeys) {
const rowAggregations = {
SumOrder: 0,
// other aggregated fields
SumPersonNum: 0,
ConfirmOrder: 0,
transactions: 0,
SumML: 0,
// ...
quotePrice: 0,
tourdays: 0,
applyDays: 0,
confirmDays: 0,
};
// Calculate aggregates over colKey
for (const colKey in aggregatedData[rowKey]) {
rowAggregations.SumOrder += aggregatedData[rowKey][colKey].SumOrder;
rowAggregations.SumPersonNum += aggregatedData[rowKey][colKey].SumPersonNum;
rowAggregations.ConfirmOrder += aggregatedData[rowKey][colKey].ConfirmOrder;
rowAggregations.transactions += aggregatedData[rowKey][colKey].transactions;
rowAggregations.SumML += aggregatedData[rowKey][colKey].SumML;
// ...aggregate all other fields
}
const row = {
[rows[0]]: rowKey,
...rowAggregations
};
summarizedData.push(row);
}
// Generate summary columns
for (const colKey of colKeys) {
const colAggregations = {
SumOrder: 0,
// other aggregated fields
SumPersonNum: 0,
ConfirmOrder: 0,
transactions: 0,
SumML: 0,
// ...
quotePrice: 0,
tourdays: 0,
applyDays: 0,
confirmDays: 0,
};
// Calculate aggregates over rowKey
for (const rowKey in aggregatedData) {
if (aggregatedData[rowKey][colKey]) {
colAggregations.SumOrder += aggregatedData[rowKey][colKey].SumOrder;
colAggregations.SumPersonNum += aggregatedData[rowKey][colKey].SumPersonNum;
colAggregations.ConfirmOrder += aggregatedData[rowKey][colKey].ConfirmOrder;
colAggregations.transactions += aggregatedData[rowKey][colKey].transactions;
colAggregations.SumML += aggregatedData[rowKey][colKey].SumML;
// ...aggregate all other fields
}
}
const col = {
[columns[0]]: colKey,
...colAggregations
};
summarizedData.push(col);
}
console.timeEnd('pivot2');
console.log('pivot2 ddd', aggregatedData);
return {
data: [], // aggregatedData,
columnValues: [rowKeys, colKeys, dateKeys],
summaryRows: summarizedData.filter(r => r[rows[0]]),
summaryColumns: summarizedData.filter(c => c[columns[0]])
};
};

@ -0,0 +1,27 @@
{
"get|/inbound_person_num/test": {
"errcode": 0,
"errmsg": "",
"data": null,
"loading": null,
"resultTotal": {
"orgz": "@integer(10,99)",
"orgzPDays": "@integer(10,99)",
"hosts": "@integer(10,99)",
"hostsPDays": "@integer(10,99)",
"IndividualService": "@integer(10,99)",
"groupsKey": "0",
"groupsLabel": "总"
},
"result|10": [
{
"orgz": "@integer(10,99)",
"orgzPDays": "@integer(10,99)",
"hosts": "@integer(10,99)",
"hostsPDays": "@integer(10,99)",
"groupsKey": "@id",
"groupsLabel": "@region"
}
]
}
}

@ -1,59 +1,61 @@
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from 'mobx';
import * as dd from "dingtalk-jsapi"; import * as dd from 'dingtalk-jsapi';
import * as config from "../config"; import * as config from '../config';
// 权限管理 // 权限管理
class AuthStore { class AuthStore {
constructor(rootStore) { constructor(rootStore) {
this.rootStore = rootStore; this.rootStore = rootStore;
makeAutoObservable(this); makeAutoObservable(this);
if (process.env.NODE_ENV == "production") { if (process.env.NODE_ENV === 'production') {
this.get_auth(); // 放到钉钉环境才能开启 this.get_auth(); // 放到钉钉环境才能开启
} }
} }
auth = ["admin"]; // 开发时候用,正式环境留空 auth = process.env.NODE_ENV === 'production' ? [] : ['admin']; // 开发时候用,正式环境留空
user = { name: "loading", userid: "..." }; // 开发时候用,正式环境留空 user = { name: 'loading', userid: '...' }; // 开发时候用,正式环境留空
has_permission(requireds) { has_permission(requireds) {
if (Object.keys(requireds).length == 0) { if (Object.keys(requireds).length === 0) {
return true; return true;
} }
const has_permission = requireds.filter(item => this.auth.includes(item)); const has_permission = requireds.filter((item) => this.auth.includes(item));
if (Object.keys(has_permission).length !== 0) { if (Object.keys(has_permission).length !== 0) {
return true; return true;
} }
return false; return false;
} }
// 请求权限 // 请求权限
get_auth() { get_auth() {
const _this = this; const _this = this;
const CORPID = "ding48bce8fd3957c96b"; // 企业的id const CORPID = 'ding48bce8fd3957c96b'; // 企业的id
dd.runtime.permission.requestAuthCode({ dd.runtime.permission.requestAuthCode({
corpId: CORPID, corpId: CORPID,
onSuccess: function (res) { onSuccess: function (res) {
console.log(res); console.log(res);
const code = res.code; const code = res.code;
const url = "/dingtalk/dingtalkwork/Getusers_auth?code=" + code; const url = '/dingtalk/dingtalkwork/Getusers_auth?code=' + code;
// 请求获取HT接口获取用户权限和用户信息 // 请求获取HT接口获取用户权限和用户信息
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then((response) => response.json())
.then(json => { .then((json) => {
runInAction(() => { runInAction(() => {
_this.user = json.result; _this.user = json.result;
_this.auth = json.result.authlist; _this.auth = json.result.authlist;
}); window.__spytitle = json.result.name;
}) window.initPageSpy();
.catch(error => { });
console.log("fetch data failed", error); })
}); .catch((error) => {
}, console.log('fetch data failed', error);
onFail: function (err) { });
console.log(err); },
}, onFail: function (err) {
}); console.log(err);
} },
});
}
} }
export default AuthStore; export default AuthStore;

@ -3,7 +3,7 @@ import moment from "moment";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import * as config from "../config"; import * as config from "../config";
import * as req from '../utils/request'; import * as req from '../utils/request';
import { prepareUrl } from '../utils/commons'; import { groupBy, prepareUrl, isEmpty, show_vs_tag, formatPercent, percentToDecimal } from '../utils/commons';
class CustomerServices { class CustomerServices {
@ -13,7 +13,7 @@ class CustomerServices {
this.endDate = moment().endOf('week').subtract(7, 'days'); this.endDate = moment().endOf('week').subtract(7, 'days');
this.startDateString = this.startDate.format(config.DATE_FORMAT); this.startDateString = this.startDate.format(config.DATE_FORMAT);
this.endDateString = this.endDate.format(config.DATE_FORMAT) + '%2023:59'; this.endDateString = this.endDate.format(config.DATE_FORMAT) + '%2023:59';
this.dateType = 'startDate'; this.dateType = 'departureDate';
this.inProgress = false; this.inProgress = false;
this.selectedAgent = ''; this.selectedAgent = '';
this.selectedTeam = ''; this.selectedTeam = '';
@ -43,8 +43,8 @@ class CustomerServices {
.append('DateType', this.dateType) .append('DateType', this.dateType)
.append('Date1', this.startDateString) .append('Date1', this.startDateString)
.append('Date2', this.endDateString) .append('Date2', this.endDateString)
.append('OldDate1', this.startDateString) .append('OldDate1', this.startDateDiffString)
.append('OldDate2', this.endDateString) .append('OldDate2', this.endDateDiffString)
.append('VEI_SN', this.selectedAgent) .append('VEI_SN', this.selectedAgent)
.append('DepList', this.selectedTeam) .append('DepList', this.selectedTeam)
.append('Country', this.selectedCountry) .append('Country', this.selectedCountry)
@ -53,110 +53,272 @@ class CustomerServices {
.then(json => { .then(json => {
if (json.errcode === 0) { if (json.errcode === 0) {
runInAction(() => { runInAction(() => {
this.agentGroupList = json.result1; if (isEmpty(json.result2)){
const total1 = json.total1; const splitTotalList = groupBy(json.result1, row => row.EOI_ObjSN === -1 ? '0' : '1');
this.agentGroupListColumns = [ this.agentGroupList = splitTotalList['1'];
{ const total1 = splitTotalList['0']?.[0] || {}; // json.total1;
title: '地接社名称', this.agentGroupListColumns = [
dataIndex: 'VendorName', {
children: [{ title: '地接社名称',
// title: this.startDate.format(config.DATE_FORMAT) + '~' + this.endDate.format(config.DATE_FORMAT), dataIndex: 'VendorName',
dataIndex: 'VendorName', fixed: 'left',
render: (text, record) => <NavLink to={`/agent/${record.EOI_ObjSN}/group/list`}>{record.VendorName}</NavLink> children: [{
} // title: this.startDate.format(config.DATE_FORMAT) + '~' + this.endDate.format(config.DATE_FORMAT),
] dataIndex: 'VendorName',
}, fixed: 'left',
{ render: (text, record) => <NavLink to={`/agent/${record.EOI_ObjSN}/group/list`}>{record.VendorName}</NavLink>
title: '团数', }
dataIndex: 'GroupCount', ]
sorter: (a, b) => a.GroupCount - b.GroupCount, },
children: [{ {
title: total1.GroupCount, title: '团数',
dataIndex: 'GroupCount' dataIndex: 'GroupCount',
} sorter: (a, b) => a.GroupCount - b.GroupCount,
] children: [{
}, title: total1.GroupCount,
{ dataIndex: 'GroupCount'
title: '人数', }
dataIndex: 'PersonNum', ]
sorter: (a, b) => a.PersonNum - b.PersonNum, },
children: [{ {
title: total1.PersonNum, title: '人数',
dataIndex: 'PersonNum' dataIndex: 'PersonNum',
} sorter: (a, b) => a.PersonNum - b.PersonNum,
] children: [{
}, title: total1.PersonNum,
{ dataIndex: 'PersonNum'
title: '团天数', }
dataIndex: 'GroupDays', ]
sorter: (a, b) => a.GroupDays - b.GroupDays, },
children: [{ {
title: total1.GroupDays, title: '团天数',
dataIndex: 'GroupDays' dataIndex: 'GroupDays',
} sorter: (a, b) => a.GroupDays - b.GroupDays,
] children: [{
}, title: total1.GroupDays,
{ dataIndex: 'GroupDays'
title: '交易额', }
dataIndex: 'totalcost', ]
sorter: (a, b) => a.totalcost - b.totalcost, },
children: [{ {
title: total1.totalcost, title: '交易额',
dataIndex: 'totalcost' dataIndex: 'totalcost',
} sorter: (a, b) => a.totalcost - b.totalcost,
] children: [{
}, title: total1.totalcost,
{ dataIndex: 'totalcost'
title: '前勤分', }
dataIndex: 'qianqin', ]
sorter: (a, b) => a.qianqin - b.qianqin, },
children: [{ {
title: total1.qianqin, title: '前勤分',
dataIndex: 'qianqin' dataIndex: 'qianqin',
} sorter: (a, b) => a.qianqin - b.qianqin,
] children: [{
}, title: total1.qianqin,
{ dataIndex: 'qianqin'
title: '好评数', }
dataIndex: 'GoodCount', ]
sorter: (a, b) => a.GoodCount - b.GoodCount, },
children: [{ {
title: total1.GoodCount, title: '好评数',
dataIndex: 'GoodCount' dataIndex: 'GoodCount',
} sorter: (a, b) => a.GoodCount - b.GoodCount,
] children: [{
}, title: total1.GoodCount,
{ dataIndex: 'GoodCount'
title: '好评率', }
dataIndex: 'GoodRate', ]
sorter: (a, b) => parseInt(a.GoodRate) - parseInt(b.GoodRate), },
children: [{ {
title: total1.GoodRate, title: '好评率',
dataIndex: 'GoodRate' dataIndex: 'GoodRate',
} sorter: (a, b) => parseInt(a.GoodRate) - parseInt(b.GoodRate),
] children: [{
}, title: total1.GoodRate,
{ dataIndex: 'GoodRate'
title: '差评数', }
dataIndex: 'BadCount', ]
sorter: (a, b) => a.BadCount - b.BadCount, },
children: [{ {
title: total1.BadCount, title: '差评数',
dataIndex: 'BadCount' dataIndex: 'BadCount',
} sorter: (a, b) => a.BadCount - b.BadCount,
] children: [{
}, title: total1.BadCount,
{ dataIndex: 'BadCount'
title: '差评率', }
dataIndex: 'BadRate', ]
sorter: (a, b) => parseInt(a.BadRate) - parseInt(b.BadRate), },
children: [{ {
title: total1.BadRate, title: '差评率',
dataIndex: 'BadRate' dataIndex: 'BadRate',
sorter: (a, b) => parseInt(a.BadRate) - parseInt(b.BadRate),
children: [{
title: total1.BadRate,
dataIndex: 'BadRate'
}
]
}
];
}
else{
const splitTotalList1 = groupBy(json.result1, row => row.EOI_ObjSN === -1 ? '0' : '1');
const splitTotalList2 = groupBy(json.result2, row => row.EOI_ObjSN === -1 ? '0' : '1');
const result = [];
for (const item1 of splitTotalList1['1']) {
for (const item2 of splitTotalList2['1']) {
if (item1.EOI_ObjSN === item2.EOI_ObjSN) {
const goodRate1 = percentToDecimal(item1.GoodRate);
const goodRate2 = percentToDecimal(item2.GoodRate);
const badRate1 = percentToDecimal(item1.BadRate);
const badRate2 = percentToDecimal(item2.BadRate);
result.push({
EOI_ObjSN: item1.EOI_ObjSN,
VendorName: item1.VendorName,
GroupCount: show_vs_tag(formatPercent((item1.GroupCount-item2.GroupCount)/(item2.GroupCount===0?1:item2.GroupCount)),
item1.GroupCount-item2.GroupCount,item1.GroupCount,item2.GroupCount),
PersonNum: show_vs_tag(formatPercent((item1.PersonNum-item2.PersonNum)/(item2.PersonNum===0?1:item2.PersonNum)),
item1.PersonNum-item2.PersonNum,item1.PersonNum,item2.PersonNum),
GroupDays: show_vs_tag(formatPercent((item1.GroupDays-item2.GroupDays)/(item2.GroupDays===0?1:item2.GroupDays)),
item1.GroupDays-item2.GroupDays,item1.GroupDays,item2.GroupDays),
totalcost: show_vs_tag(formatPercent((item1.totalcost-item2.totalcost)/(item2.totalcost===0?1:item2.totalcost)),
(item1.totalcost-item2.totalcost).toFixed(2),item1.totalcost,item2.totalcost),
GoodCount: show_vs_tag(formatPercent((item1.GoodCount-item2.GoodCount)/(item2.GoodCount===0?1:item2.GoodCount)),
item1.GoodCount-item2.GoodCount,item1.GoodCount,item2.GoodCount),
GoodRate: show_vs_tag(formatPercent((goodRate1-goodRate2)/(goodRate2===0?1:goodRate2)),
formatPercent(goodRate1-goodRate2),item1.GoodRate,item2.GoodRate),
BadCount: show_vs_tag(formatPercent((item1.BadCount-item2.BadCount)/(item2.BadCount===0?1:item2.BadCount)),
item1.BadCount-item2.BadCount,item1.BadCount,item2.BadCount),
BadRate: show_vs_tag(formatPercent((badRate1-badRate2)/(badRate2===0?1:badRate2)),
formatPercent(badRate1-badRate2),item1.BadRate,item2.BadRate),
qianqin: show_vs_tag(formatPercent((item1.qianqin-item2.qianqin)/(item2.qianqin===0?1:item2.qianqin)),
(item1.qianqin-item2.qianqin).toFixed(2),item1.qianqin,item2.qianqin),
key:item1.key,
});
} }
] }
} }
]; this.agentGroupList = result;
const total1 = splitTotalList1['0']?.[0] || {};
const total2 = splitTotalList2['0']?.[0] || {};
this.agentGroupListColumns = [
{
title: '地接社名称',
dataIndex: 'VendorName',
fixed: 'left',
children: [{
// title: this.startDate.format(config.DATE_FORMAT) + '~' + this.endDate.format(config.DATE_FORMAT),
dataIndex: 'VendorName',
fixed: 'left',
render: (text, record) => <NavLink to={`/agent/${record.EOI_ObjSN}/group/list`}>{record.VendorName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: show_vs_tag(formatPercent((total1.GroupCount-total2.GroupCount)/total2.GroupCount),total1.GroupCount-total2.GroupCount,total1.GroupCount,total2.GroupCount),
titleX: [total1.GroupCount, total2.GroupCount].join(' vs '),
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: show_vs_tag(formatPercent((total1.PersonNum-total2.PersonNum)/total2.PersonNum),total1.PersonNum-total2.PersonNum,total1.PersonNum,total2.PersonNum),
titleX: [total1.PersonNum, total2.PersonNum].join(' vs '),
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: show_vs_tag(formatPercent((total1.GroupDays-total2.GroupDays)/total2.GroupDays),total1.GroupDays-total2.GroupDays,total1.GroupDays,total2.GroupDays),
titleX: [total1.GroupDays, total2.GroupDays].join(' vs '),
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'totalcost',
sorter: (a, b) => a.totalcost - b.totalcost,
children: [{
title: show_vs_tag(formatPercent((total1.totalcost-total2.totalcost)/total2.totalcost),(total1.totalcost-total2.totalcost).toFixed(2),total1.totalcost,total2.totalcost),
titleX: [total1.totalcost, total2.totalcost].join(' vs '),
dataIndex: 'totalcost'
}
]
},
{
title: '前勤分',
dataIndex: 'qianqin',
sorter: (a, b) => a.qianqin - b.qianqin,
children: [{
title: show_vs_tag(formatPercent((total1.qianqin-total2.qianqin)/total2.qianqin),(total1.qianqin-total2.qianqin).toFixed(2),total1.qianqin,total2.qianqin),
titleX: [total1.qianqin, total2.qianqin].join(' vs '),
dataIndex: 'qianqin'
}
]
},
{
title: '好评数',
dataIndex: 'GoodCount',
sorter: (a, b) => a.GoodCount - b.GoodCount,
children: [{
title: show_vs_tag(formatPercent((total1.GoodCount-total2.GoodCount)/total2.GoodCount),total1.GoodCount-total2.GoodCount,total1.GoodCount,total2.GoodCount),
titleX: [total1.GoodCount, total2.GoodCount].join(' vs '),
dataIndex: 'GoodCount'
}
]
},
{
title: '好评率',
dataIndex: 'GoodRate',
sorter: (a, b) => parseInt(a.GoodRate) - parseInt(b.GoodRate),
children: [{
title: show_vs_tag(formatPercent((percentToDecimal(total1.GoodRate)-percentToDecimal(total2.GoodRate))/percentToDecimal(total2.GoodRate)),
formatPercent(percentToDecimal(total1.GoodRate)-percentToDecimal(total2.GoodRate)),total1.GoodRate,total2.GoodRate),
titleX: [total1.GoodRate, total2.GoodRate].join(' vs '),
dataIndex: 'GoodRate'
}
]
},
{
title: '差评数',
dataIndex: 'BadCount',
sorter: (a, b) => a.BadCount - b.BadCount,
children: [{
title: show_vs_tag(formatPercent((total1.BadCount-total2.BadCount)/total2.BadCount),total1.BadCount-total2.BadCount,total1.BadCount,total2.BadCount),
titleX: [total1.BadCount, total2.BadCount].join(' vs '),
dataIndex: 'BadCount'
}
]
},
{
title: '差评率',
dataIndex: 'BadRate',
sorter: (a, b) => parseInt(a.BadRate) - parseInt(b.BadRate),
children: [{
title: show_vs_tag(formatPercent((percentToDecimal(total1.BadRate)-percentToDecimal(total2.BadRate))/percentToDecimal(total2.GoodRate)),
formatPercent(percentToDecimal(total1.BadRate)-percentToDecimal(total2.BadRate)),total1.BadRate,total2.BadRate),
titleX: [total1.BadRate, total2.BadRate].join(' vs '),
dataIndex: 'BadRate'
}
]
}
];
}
}); });
} }
}) })
@ -275,9 +437,10 @@ class CustomerServices {
}); });
} }
fetchDestinationGroupCount() { fetchDistGroupInfoByCountry(destinationId) {
this.inProgress = true; this.nationality_count_data.loading = true;
const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetdistGroupInfoAll') const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetDistGroupInfoALLByCountry')
.append('city', destinationId)
.append('DateType', this.dateType) .append('DateType', this.dateType)
.append('Date1', this.startDateString) .append('Date1', this.startDateString)
.append('Date2', this.endDateString) .append('Date2', this.endDateString)
@ -288,18 +451,19 @@ class CustomerServices {
.append('OrderStatus', this.selectedOrderStatus) .append('OrderStatus', this.selectedOrderStatus)
.build(); .build();
req.fetchJSON(fetchUrl) req.fetchJSON(fetchUrl)
.then(json => { .then((json) => {
if (json.errcode === 0) { if (json.errcode === 0) {
runInAction(() => { runInAction(() => {
this.destinationGroupCount = json.result1; const splitTotalList = groupBy(json.result1, row => row.COLD_ServiceCity === -1 ? '0' : '1');
const total1 = json.total1; this.nationality_count_data.destinationGroupByCountryList = splitTotalList['1'];
this.destinationGroupCountColumns = [ const total1 = splitTotalList['0']?.[0] || {};
this.nationality_count_data.destinationGroupByCountryListColumns =[
{ {
title: '城市', title: '国籍',
dataIndex: 'COLD_ServiceCityName', dataIndex: 'COLD_ServiceCityName',
children: [{ children: [{
dataIndex: 'COLD_ServiceCityName', title: total1.COLD_ServiceCityName,
render: (text, record) => <NavLink to={`/destination/${record.COLD_ServiceCity}/group/list`}>{record.COLD_ServiceCityName}</NavLink> dataIndex: 'COLD_ServiceCityName'
} }
] ]
}, },
@ -338,7 +502,7 @@ class CustomerServices {
dataIndex: 'TotalCost', dataIndex: 'TotalCost',
sorter: (a, b) => a.TotalCost - b.TotalCost, sorter: (a, b) => a.TotalCost - b.TotalCost,
children: [{ children: [{
title: total1.totalcost, title: total1.TotalCost,
dataIndex: 'TotalCost' dataIndex: 'TotalCost'
} }
] ]
@ -348,13 +512,200 @@ class CustomerServices {
dataIndex: 'TotalPrice', dataIndex: 'TotalPrice',
sorter: (a, b) => a.TotalPrice - b.TotalPrice, sorter: (a, b) => a.TotalPrice - b.TotalPrice,
children: [{ children: [{
title: total1.totalprice, title: total1.TotalPrice,
dataIndex: 'TotalPrice' dataIndex: 'TotalPrice'
} }
] ]
} }
]; ];
}); });
}
})
.then(() => {
this.nationality_count_data.loading = false;
});
}
fetchDestinationGroupCount() {
this.inProgress = true;
const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetdistGroupInfoAll')
.append('DateType', this.dateType)
.append('Date1', this.startDateString)
.append('Date2', this.endDateString)
.append('OldDate1', this.startDateDiffString)
.append('OldDate2', this.endDateDiffString)
.append('DepList', this.selectedTeam)
.append('Country', this.selectedCountry)
.append('OrderStatus', this.selectedOrderStatus)
.build();
req.fetchJSON(fetchUrl)
.then(json => {
if (json.errcode === 0) {
runInAction(() => {
if (isEmpty(json.result2)){
const splitTotalList = groupBy(json.result1, row => row.COLD_ServiceCity === -1 ? '0' : '1');
this.agentGroupList = splitTotalList['1'];
const total1 = splitTotalList['0']?.[0] || {}; // json.total1;
this.destinationGroupCount = splitTotalList['1'];
this.destinationGroupCountColumns = [
{
title: '城市',
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
children: [{
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
render: (text, record) => <NavLink to={`/destination/${record.COLD_ServiceCity}/group/list`}>{record.COLD_ServiceCityName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: total1.GroupCount,
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: total1.PersonNum,
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: total1.GroupDays,
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'TotalCost',
sorter: (a, b) => a.TotalCost - b.TotalCost,
children: [{
title: total1.TotalCost,
dataIndex: 'TotalCost'
}
]
},
{
title: '报价',
dataIndex: 'TotalPrice',
sorter: (a, b) => a.TotalPrice - b.TotalPrice,
children: [{
title: total1.TotalPrice,
dataIndex: 'TotalPrice'
}
]
}
];
}
else{
const splitTotalList1 = groupBy(json.result1, row => row.COLD_ServiceCity === -1 ? '0' : '1');
const splitTotalList2 = groupBy(json.result2, row => row.COLD_ServiceCity === -1 ? '0' : '1');
const result = [];
for (const item1 of splitTotalList1['1']) {
for (const item2 of splitTotalList2['1']) {
if (item1.COLD_ServiceCity === item2.COLD_ServiceCity) {
result.push({
COLD_ServiceCity: item1.COLD_ServiceCity,
COLD_ServiceCityName: item1.COLD_ServiceCityName,
GroupCount: show_vs_tag(formatPercent((item1.GroupCount-item2.GroupCount)/(item2.GroupCount===0?1:item2.GroupCount)),
item1.GroupCount-item2.GroupCount,item1.GroupCount,item2.GroupCount),
PersonNum: show_vs_tag(formatPercent((item1.PersonNum-item2.PersonNum)/(item2.PersonNum===0?1:item2.PersonNum)),
item1.PersonNum-item2.PersonNum,item1.PersonNum,item2.PersonNum),
GroupDays: show_vs_tag(formatPercent((item1.GroupDays-item2.GroupDays)/(item2.GroupDays===0?1:item2.GroupDays)),
item1.GroupDays-item2.GroupDays,item1.GroupDays,item2.GroupDays),
TotalCost: show_vs_tag(formatPercent((item1.TotalCost-item2.TotalCost)/(item2.TotalCost===0?1:item2.TotalCost)),
(item1.TotalCost-item2.TotalCost).toFixed(2),item1.TotalCost,item2.TotalCost),
TotalPrice: show_vs_tag(formatPercent((item1.TotalPrice-item2.TotalPrice)/(item2.TotalPrice===0?1:item2.TotalPrice)),
item1.TotalPrice-item2.TotalPrice,item1.TotalPrice,item2.TotalPrice),
key:item1.key,
});
}
}
}
this.destinationGroupCount = result;
const total1 = splitTotalList1['0']?.[0] || {};
const total2 = splitTotalList2['0']?.[0] || {};
this.destinationGroupCountColumns = [
{
title: '城市',
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
children: [{
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
render: (text, record) => <NavLink to={`/destination/${record.COLD_ServiceCity}/group/list`}>{record.COLD_ServiceCityName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: show_vs_tag(formatPercent((total1.GroupCount-total2.GroupCount)/total2.GroupCount),total1.GroupCount-total2.GroupCount,total1.GroupCount,total2.GroupCount),
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: show_vs_tag(formatPercent((total1.PersonNum-total2.PersonNum)/total2.PersonNum),total1.PersonNum-total2.PersonNum,total1.PersonNum,total2.PersonNum),
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: show_vs_tag(formatPercent((total1.GroupDays-total2.GroupDays)/total2.GroupDays),total1.GroupDays-total2.GroupDays,total1.GroupDays,total2.GroupDays),
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'TotalCost',
sorter: (a, b) => a.TotalCost - b.TotalCost,
children: [{
title: show_vs_tag(formatPercent((total1.TotalCost-total2.TotalCost)/total2.TotalCost),
(total1.TotalCost-total2.TotalCost).toFixed(2),total1.TotalCost,total2.TotalCost),
dataIndex: 'TotalCost'
}
]
},
{
title: '报价',
dataIndex: 'TotalPrice',
sorter: (a, b) => a.TotalPrice - b.TotalPrice,
children: [{
title: show_vs_tag(formatPercent((total1.TotalPrice-total2.TotalPrice)/total2.TotalPrice),
(total1.TotalPrice-total2.TotalPrice).toFixed(2),total1.TotalPrice,total2.TotalPrice),
dataIndex: 'TotalPrice'
}
]
}
];
}
});
} }
}) })
.then(() => { .then(() => {
@ -365,8 +716,6 @@ class CustomerServices {
fetchGroupListByDestinationId(destinationId) { fetchGroupListByDestinationId(destinationId) {
this.inProgress = true; this.inProgress = true;
this.destinationName = '...'; this.destinationName = '...';
this.destinationGroupList = [];
this.destinationGroupListColumns = [];
const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetdistGroupInfo') const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetdistGroupInfo')
.append('city', destinationId) .append('city', destinationId)
.append('DateType', this.dateType) .append('DateType', this.dateType)
@ -376,6 +725,7 @@ class CustomerServices {
.append('OldDate2', this.endDateString) .append('OldDate2', this.endDateString)
.append('DepList', this.selectedTeam) .append('DepList', this.selectedTeam)
.append('Country', this.selectedCountry) .append('Country', this.selectedCountry)
.append('OrderStatus', this.selectedOrderStatus)
.build(); .build();
req.fetchJSON(fetchUrl) req.fetchJSON(fetchUrl)
.then(json => { .then(json => {
@ -453,6 +803,23 @@ class CustomerServices {
}); });
} }
searchValues = {
DateType: { key: 'departureDate', label: '抵达日期'},
// departureDateType: { key: 'departureDate', label: '抵达日期'},
};
setSearchValues(obj, values) {
this.dateType = obj.DateType;
this.selectedAgent = obj.agency;
this.startDateString = obj.Date1;
this.endDateString = obj.Date2;
this.startDateDiffString = obj.DateDiff1;
this.endDateDiffString = obj.DateDiff2;
this.selectedCountry = obj.countryArea;
this.selectedTeam = (obj.DepartmentList || '').replace('ALL', '');
this.selectedOrderStatus = obj.orderStatus;
}
selectDateRange(startDate, endDate) { selectDateRange(startDate, endDate) {
this.startDate = startDate; this.startDate = startDate;
this.endDate = endDate; this.endDate = endDate;
@ -492,7 +859,7 @@ class CustomerServices {
inProgress; inProgress;
agentList = []; agentList = [];
groupList = []; groupList = [];
groupListColumns = []; groupListColumns = [];
@ -500,6 +867,15 @@ class CustomerServices {
destinationGroupCount = []; destinationGroupCount = [];
destinationGroupCountColumns =[]; destinationGroupCountColumns =[];
destinationGroupList = [];
destinationGroupListColumns = [];
// 国籍统计
nationality_count_data = {
loading: false,
destinationGroupByCountryList:[],
destinationGroupByCountryListColumns:[]
};
agentGroupList = [{ agentGroupList = [{
EOI_ObjSN: 1, EOI_ObjSN: 1,
VendorName: '---', VendorName: '---',
@ -518,6 +894,7 @@ class CustomerServices {
{ {
title: '地接社名称', title: '地接社名称',
dataIndex: 'VendorName', dataIndex: 'VendorName',
fixed: 'left',
render: (text, record) => { render: (text, record) => {
if (record.EOI_ObjSN === -1) { if (record.EOI_ObjSN === -1) {
return text; return text;
@ -569,4 +946,4 @@ class CustomerServices {
]; ];
} }
export default CustomerServices; export default CustomerServices;

@ -1,6 +1,17 @@
import {makeAutoObservable, runInAction} from "mobx"; import {makeAutoObservable, runInAction} from "mobx";
import { fetchJSON } from '../utils/request';
import * as config from "../config"; import * as config from "../config";
import { groupsMappedByKey, sitesMappedByCode, pivotBy } from './../libs/ht';
import { sortBy, show_vs_tag, formatPercent, groupBy, isEmpty, uniqWith, formatPercentToFloat } from "../utils/commons";
import moment from 'moment';
/**
* 用于透视的数据
*/
const getDetailData = async (param) => {
const json = await fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
return json.errcode === 0 ? json.result : [];
};
class CustomerStore { class CustomerStore {
@ -16,16 +27,16 @@ class CustomerStore {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-tourdesign/PotentialCusOrder'; let url = '/service-tourdesign/PotentialCusOrder';
url += '?Website=' + this.potential_data.webcode.toString() + '&DEI_SNList=' + this.potential_data.groups.toString(); url += '?Website=' + this.potential_data.webcode.toString() + '&DEI_SNList=' + this.potential_data.groups.toString();
if (this.potential_data.date_type == 'applyDate') { if (String(this.potential_data.date_type).toLowerCase() === 'applydate') {
url += '&ApplydateCheck=1&EntrancedateCheck=0&ConfirmDateCheck=0'; url += '&ApplydateCheck=1&EntrancedateCheck=0&ConfirmDateCheck=0';
} else if(this.potential_data.date_type == 'ConfirmDate'){ } else if(String(this.potential_data.date_type).toLowerCase() === 'confirmdate'){
url += '&ApplydateCheck=0&EntrancedateCheck=0&ConfirmDateCheck=1'; url += '&ApplydateCheck=0&EntrancedateCheck=0&ConfirmDateCheck=1';
}else { }else {
url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0'; url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0';
} }
url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (get_detail) { if (get_detail) {
url += '&IsDetail=1'; url += '&IsDetail=1';
} else { } else {
@ -79,47 +90,186 @@ class CustomerStore {
potential_customer_order: this.potential_customer_order.bind(this), potential_customer_order: this.potential_customer_order.bind(this),
onChange_show_detail_table: this.onChange_show_detail_table.bind(this), onChange_show_detail_table: this.onChange_show_detail_table.bind(this),
handleChange_webcode: this.handleChange_webcode.bind(this), handleChange_webcode: this.handleChange_webcode.bind(this),
searchValues: {
DepartmentList: ['1', '2', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: ['GHKYZG'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
},
}; };
// 潜力客户 end // 潜力客户 end
// 老客户 beign // 老客户 beign
regular_customer_order(get_detail = false) { // isCompare对比数据boolean isCompareRender对比折线boolean
regular_customer_order(get_detail = false, isCompare = false, isCompareRender=false) {
let pivotByOrder = 'SumOrder';
let pivotByDate = 'applyDate';
this.regular_data.loading = true; this.regular_data.loading = true;
this.regular_data.detail_loading = get_detail;
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-tourdesign/RegularCusOrder'; let url = '/service-tourdesign/RegularCusOrder';
url += '?Website=' + this.regular_data.webcode.toString() + '&DEI_SNList=' + this.regular_data.groups.toString(); url += '?Website=' + this.regular_data.webcode.toString() + '&DEI_SNList=' + this.regular_data.groups.toString();
if (this.regular_data.date_type == 'applyDate') { if (String(this.regular_data.date_type).toLowerCase() === 'applydate') {
url += '&ApplydateCheck=1&EntrancedateCheck=0&ConfirmDateCheck=0'; url += '&ApplydateCheck=1&EntrancedateCheck=0&ConfirmDateCheck=0';
} else if(this.regular_data.date_type == 'ConfirmDate'){ } else if(String(this.regular_data.date_type).toLowerCase() === 'confirmdate'){
url += '&ApplydateCheck=0&EntrancedateCheck=0&ConfirmDateCheck=1'; url += '&ApplydateCheck=0&EntrancedateCheck=0&ConfirmDateCheck=1';
pivotByOrder = 'ConfirmOrder';
pivotByDate = 'confirmDate';
}else { }else {
url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0'; url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0';
pivotByOrder = 'ConfirmOrder';
pivotByDate = 'startDate';
}
if (isCompare){
url += '&ApplydateStart=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
url += '&EntrancedateStart=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
url += '&ConfirmdateStart=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
}
else{
url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
} }
url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59';
url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59';
url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59';
if (get_detail) { if (get_detail) {
url += '&IsDetail=1'; url += '&IsDetail=1';
} else { } else {
url += '&IsDetail=0'; url += '&IsDetail=0';
} }
url += `&IncludeTickets=${this.regular_data.include_tickets}`;
return new Promise((resolve, reject) => {
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
runInAction(() => { runInAction(() => {
if (get_detail) { if (get_detail) {
this.regular_data.data_detail = json; if (!isCompare){
this.regular_data.data_detail = json;
}
if (isCompareRender){
this.regular_data.solidLineTime=date_picker_store.start_date.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
this.regular_data.solidLineCompareTime=date_picker_store.start_date_cp.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
const dump_l = (json || []).filter(ele => ele.COLI_IsOld !== '' && ele.COLI_IsCusCommend !== '').length;
this.regular_data.total_data_tips = dump_l > 0 ? `包含 ${dump_l} 条同时勾选的数据` : '';
/** 使用明细数据画图 */
const data_detail = (json || []).map((ele) => ({
...ele,
key: ele.COLI_ID,
orderState: ele.OrderState,
applyDate: moment(ele.COLI_ApplyDate).format('YYYY-MM-DD'),
startDate: ele.COLI_OrderStartDate,
confirmDate: moment(ele.COLI_ConfirmDate).format('YYYY-MM-DD'),
}));
const { data: IsOldData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsOld === '是'), [['COLI_IsOld', ], [], pivotByDate]);
const { data: isCusCommendData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsCusCommend === '是'), [['COLI_IsCusCommend', ], [], pivotByDate]);
// console.log('IsOldData====', IsOldData, '\nisCusCommend', isCusCommendData);
// 合并成两个系列
const seriesData = [].concat(IsOldData.map(ele => ({...ele, _ylabel: '老客户'})), isCusCommendData.map(ele => ({...ele, _ylabel: '老客户推荐'})),).sort(sortBy(pivotByDate));
const seriesNewData = seriesData.map(item => {
if (isCompare){
return {
...item,
_ylabel: date_picker_store.start_date_cp.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT) + item._ylabel
};
}
else{
return {
...item,
_ylabel: date_picker_store.start_date.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT) + item._ylabel
};
}
});
// console.log('seriesData====', seriesNewData);
if (this.regular_data.data_compare.length===0){
this.regular_data.data_compare=seriesNewData;
}
else{
let seriesCompareData = [];
const fistCompareDetail = this.regular_data.data_compare;
if (fistCompareDetail.length>seriesNewData.length){
seriesCompareData = fistCompareDetail;
for (let i = 0; i < seriesNewData.length; i++) {
seriesNewData[i][pivotByDate] = fistCompareDetail[i][pivotByDate];
}
seriesCompareData.push(...seriesNewData);
}
else{
seriesCompareData=seriesNewData;
for (let i = 0; i < fistCompareDetail.length; i++) {
fistCompareDetail[i][pivotByDate] = seriesNewData[i][pivotByDate];
}
seriesCompareData.push(...fistCompareDetail);
}
this.regular_data.detail_loading = false;
this.regular_data.pivotData = seriesCompareData; // { IsOldData, isCusCommendData, };
this.regular_data.pivotY = pivotByOrder;
this.regular_data.pivotX = pivotByDate;
}
}
else{
this.regular_data.detail_loading = false;
const dump_l = (json || []).filter(ele => ele.COLI_IsOld !== '' && ele.COLI_IsCusCommend !== '').length;
this.regular_data.total_data_tips = dump_l > 0 ? `包含 ${dump_l} 条同时勾选的数据` : '';
/** 使用明细数据画图 */
const data_detail = (json || []).map((ele) => ({
...ele,
key: ele.COLI_ID,
orderState: ele.OrderState,
applyDate: moment(ele.COLI_ApplyDate).format('YYYY-MM-DD'),
startDate: ele.COLI_OrderStartDate,
confirmDate: moment(ele.COLI_ConfirmDate).format('YYYY-MM-DD'),
}));
const { data: IsOldData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsOld === '是'), [['COLI_IsOld', ], [], pivotByDate]);
const { data: isCusCommendData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsCusCommend === '是'), [['COLI_IsCusCommend', ], [], pivotByDate]);
// console.log('IsOldData====', IsOldData, '\nisCusCommend', isCusCommendData);
// 合并成两个系列
const seriesData = [].concat(IsOldData.map(ele => ({...ele, _ylabel: '老客户'})), isCusCommendData.map(ele => ({...ele, _ylabel: '老客户推荐'})),).sort(sortBy(pivotByDate));
this.regular_data.pivotData = seriesData; // { IsOldData, isCusCommendData, };
this.regular_data.pivotY = pivotByOrder;
this.regular_data.pivotX = pivotByDate;
}
} else { } else {
this.regular_data.data = json; if (isCompare){
const result = [];
const firstCompareData = this.regular_data.data;
for (const item1 of firstCompareData) {
for (const item2 of json) {
if (item1.ItemName === item2.ItemName) {
result.push({
ItemName: item1.ItemName,
OrderNum: show_vs_tag(formatPercent((item1.OrderNum-item2.OrderNum)/(item2.OrderNum===0?1:item2.OrderNum)),
item1.OrderNum-item2.OrderNum,item1.OrderNum,item2.OrderNum),
SUCOrderNum: show_vs_tag(formatPercent((item1.SUCOrderNum-item2.SUCOrderNum)/(item2.SUCOrderNum===0?1:item2.SUCOrderNum)),
item1.SUCOrderNum-item2.SUCOrderNum,item1.SUCOrderNum,item2.SUCOrderNum),
OrderRate: show_vs_tag(formatPercent((item1.OrderRate-item2.OrderRate)/item2.OrderRate),
formatPercentToFloat(item1.OrderRate-item2.OrderRate),formatPercentToFloat(item1.OrderRate),formatPercentToFloat(item2.OrderRate)),
SUCRate: show_vs_tag(formatPercent((item1.SUCRate-item2.SUCRate)/(item2.SUCRate===0?1:item2.SUCRate)),
formatPercent(item1.SUCRate-item2.SUCRate),formatPercent(item1.SUCRate),formatPercent(item2.SUCRate)),
ML: show_vs_tag(formatPercent((item1.ML-item2.ML)/(item2.ML===0?1:item2.ML)),
(item1.ML-item2.ML).toFixed(2),item1.ML,item2.ML),
OrderMLRate: show_vs_tag(formatPercent((item1.OrderMLRate-item2.OrderMLRate)/item2.OrderMLRate),
formatPercentToFloat(item1.OrderMLRate-item2.OrderMLRate),formatPercentToFloat(item1.OrderMLRate),formatPercentToFloat(item2.OrderMLRate)),
PersonNum: show_vs_tag(formatPercent((item1.PersonNum-item2.PersonNum)/(item2.PersonNum===0?1:item2.PersonNum)),
item1.PersonNum-item2.PersonNum,item1.PersonNum,item2.PersonNum),
});
}
}
};
this.regular_data.data = result;
}
else{
this.regular_data.data = json;
}
} }
this.regular_data.loading = false; this.regular_data.loading = false;
resolve();
}); });
}) })
.catch((error) => { .catch((error) => {
this.regular_data.loading = false; this.regular_data.loading = false;
console.log('fetch data failed', error); console.log('fetch data failed', error);
}); });
});
} }
handleChange_webcode_regular = (value) => { handleChange_webcode_regular = (value) => {
@ -140,18 +290,38 @@ class CustomerStore {
regular_data = { regular_data = {
loading: false, loading: false,
detail_loading: false,
data: [], data: [],
data_detail: [], data_detail: [],
data_compare: [],
showCompareSum:false,
solidLineTime:'',
solidLineCompareTime:'',
solidLineDash:'老客户推荐',
isCompareLine:false,
total_data_tips: '',
webcode: 'ALL', webcode: 'ALL',
site_select_mode: 'multiple',// 站点是否多选 site_select_mode: 'multiple',// 站点是否多选
group_select_mode: 'multiple',// 是否多选分组 group_select_mode: 'multiple',// 是否多选分组
groups: ['1', '2', '28', '7'], groups: ['1', '2', '28', '7'],
date_type: 'applyDate', date_type: 'applyDate',
include_tickets: 0,
group_handleChange: this.handleChange_group_select_regular.bind(this), group_handleChange: this.handleChange_group_select_regular.bind(this),
onChange_datetype: this.onChange_datetype_regular.bind(this), onChange_datetype: this.onChange_datetype_regular.bind(this),
regular_customer_order: this.regular_customer_order.bind(this), regular_customer_order: this.regular_customer_order.bind(this),
onChange_show_detail_table: this.onChange_show_detail_table_regular.bind(this), onChange_show_detail_table: this.onChange_show_detail_table_regular.bind(this),
handleChange_webcode: this.handleChange_webcode_regular.bind(this), handleChange_webcode: this.handleChange_webcode_regular.bind(this),
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: ['CHT','AH','JH','GH','ZWQD','GH_ZWQD_HW','GHKYZG','GHKYHW'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
IncludeTickets: { key: '0', label: '不含门票' },
},
pivotData: [],
pivotY: 'SumOrder',
pivotX: 'applyDate',
}; };
// 老客户 end // 老客户 end
@ -162,9 +332,9 @@ class CustomerStore {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-tourdesign/RegularCusInChinaOrder'; let url = '/service-tourdesign/RegularCusInChinaOrder';
url += '?Website=' + this.inchina_data.webcode.toString() + '&DEI_SNList=' + this.inchina_data.groups.toString(); url += '?Website=' + this.inchina_data.webcode.toString() + '&DEI_SNList=' + this.inchina_data.groups.toString();
if (this.inchina_data.date_type == 'applyDate') { if (String(this.inchina_data.date_type).toLowerCase() === 'applydate') {
url += '&ApplydateCheck=1&EntrancedateCheck=0&ConfirmDateCheck=0'; url += '&ApplydateCheck=1&EntrancedateCheck=0&ConfirmDateCheck=0';
} else if(this.inchina_data.date_type == 'ConfirmDate'){ } else if(String(this.inchina_data.date_type).toLowerCase() === 'confirmdate'){
url += '&ApplydateCheck=0&EntrancedateCheck=0&ConfirmDateCheck=1'; url += '&ApplydateCheck=0&EntrancedateCheck=0&ConfirmDateCheck=1';
}else { }else {
url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0'; url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0';
@ -174,9 +344,9 @@ class CustomerStore {
} else { } else {
url += '&IsDetail=0'; url += '&IsDetail=0';
} }
url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
@ -225,9 +395,198 @@ class CustomerStore {
inchina_customer_order: this.inchina_customer_order.bind(this), inchina_customer_order: this.inchina_customer_order.bind(this),
onChange_show_detail_table: this.onChange_show_detail_table_inchina.bind(this), onChange_show_detail_table: this.onChange_show_detail_table_inchina.bind(this),
handleChange_webcode: this.handleChange_webcode_inchina.bind(this), handleChange_webcode: this.handleChange_webcode_inchina.bind(this),
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: undefined, // ['ALL'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
},
}; };
// 在华客人 end // 在华客人 end
// 东道主项目 begin
host_case_data = {
loading: false,
summaryData: [], // 汇总数据
groupData: [], // 小组数据
counselorData: [], // 顾问数据
singleDetailData:[], // 单团详细数据
group_select_mode: 'multiple',
groups: ['1', '2', '28', '7'],
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
},
};
getHostCaseData(groupBy) {
this.host_case_data.loading = true;
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-Analyse2/DDZCount';
url += '?DEI_SN=' + this.host_case_data.groups.toString();
url += '&ArriveDateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ArriveDateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&GroupBy=' + groupBy;
fetch(config.HT_HOST + url)
.then((response) => response.json())
.then((json) => {
runInAction(() => {
switch(groupBy){
case "1":
this.host_case_data.summaryData = json.result?json.result:[];
break;
case "2":
this.host_case_data.counselorData = json.result?json.result:[];
break;
case "3":
this.host_case_data.groupData = json.result?json.result:[];
break;
case "4":
this.host_case_data.singleDetailData = json.result?json.result:[];
this.host_case_data.loading = false;
break;
}
});
})
.catch((error) => {
this.host_case_data.loading = false;
console.log('fetch data failed', error);
});
};
// 东道主项目 end
// 销售-老客户
sales_regular_data = {
loading: false,
data: [],
mergedData: [],
rawData: [],
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: ['CHT','AH','JH','GH','ZWQD','GH_ZWQD_HW','GHKYZG','GHKYHW'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
IncludeTickets: { key: '0', label: '不含门票' },
},
pivotData: {
operatorName: { loading: false, data: [], rawData: [], mergedData: [], filterColValues: [] },
country: { loading: false, data: [], rawData: [], mergedData: [], filterColValues: [] },
},
};
get_sales_regular_data_vs = async (param, pivotRow = 'operatorName') => {
this.sales_regular_data.pivotData[pivotRow].loading = true;
const hasCompare = !isEmpty(param.DateDiff1);
const [result1, result2] = await Promise.all([
this.get_sales_regular_data(param, pivotRow),
hasCompare ? this.get_sales_regular_data({...param, Date1: param.DateDiff1, Date2: param.DateDiff2}, pivotRow) : { mergeDataBySales: [], mergeDataBySalesAccount: [], filterHasOld: []},
]);
const allTypes = ['老客户', '老客户推荐'];
// 独立的账户
const allSales = Array.from(new Set([...result1.mergeDataBySales.map(row=>row[pivotRow]), ...result2.mergeDataBySales.map(row=>row[pivotRow])]));
const sales1 = groupBy(result1.mergeDataBySales, pivotRow);
const sales2 = groupBy(result2.mergeDataBySales, pivotRow);
const mergeDataBySales = allSales.reduce((r, sale) => {
const _default = { [pivotRow]: sale, rowLabel: sales1?.[sale]?.rowLabel || sales2?.[sale]?.rowLabel, children: sales1?.[sale]?.children || [], key: sale};
const operatorRow = {...(sales1?.[sale]?.[0] || _default), vsData: sales2?.[sale]?.[0] || {}};
// 展开的两项: '老客户', '老客户推荐'
const series1Children = sales1?.[sale]?.[0]?.children || [];
const series2Children = sales2?.[sale]?.[0]?.children || [];
const children = allTypes.reduce((r, type) => {
const _default = { [pivotRow]: type, rowLabel: type, key: type};
const _typeRow = series1Children.find(sc => sc[pivotRow] === type) || _default;
const _typeVSRow = series2Children.find(sc => sc[pivotRow] === type) || {};
return r.concat({..._typeRow, vsData: _typeVSRow});
}, []);
operatorRow.children = children;
return r.concat(operatorRow);
}, []);
// 合并顾问的账户
let mergeDataBySalesAccount = [];
if (pivotRow === 'operatorName') {
const allSalesMerged = Array.from(new Set([...result1.mergeDataBySalesAccount.map(row=>row.operatorName), ...result2.mergeDataBySalesAccount.map(row=>row.operatorName)]));
const salesM1 = groupBy(result1.mergeDataBySalesAccount, 'operatorName');
const salesM2 = groupBy(result2.mergeDataBySalesAccount, 'operatorName');
mergeDataBySalesAccount = allSalesMerged.reduce((r, sale) => {
const _default = { operatorName: sale, rowLabel: salesM1?.[sale]?.rowLabel || salesM2?.[sale]?.rowLabel, children: salesM1?.[sale]?.children || [], key: sale};
const operatorRow = {...(salesM1?.[sale]?.[0] || _default), vsData: salesM2?.[sale]?.[0] || {}};
// 展开的两项: '老客户', '老客户推荐'
const series1Children = salesM1?.[sale]?.[0]?.children || [];
const series2Children = salesM2?.[sale]?.[0]?.children || [];
const children = allTypes.reduce((r, type) => {
const _default = { operatorName: type, rowLabel: type, key: type};
const _typeRow = series1Children.find(sc => sc.operatorName === type) || _default;
const _typeVSRow = series2Children.find(sc => sc.operatorName === type) || {};
return r.concat({..._typeRow, vsData: _typeVSRow});
}, []);
operatorRow.children = children;
return r.concat(operatorRow);
}, []);
}
const filterColValues = uniqWith(
mergeDataBySales.map((rr) => ({ text: rr[pivotRow], value: rr[pivotRow] })),
(a, b) => JSON.stringify(a) === JSON.stringify(b)
).sort((a, b) => a.text.localeCompare(b.text, 'zh-CN'));
this.sales_regular_data.pivotData[pivotRow].loading = false;
this.sales_regular_data.pivotData[pivotRow].rawData = [].concat(result1.filterHasOld, result2.filterHasOld);
this.sales_regular_data.pivotData[pivotRow].data = mergeDataBySales;
this.sales_regular_data.pivotData[pivotRow].mergedData = isEmpty(mergeDataBySalesAccount) ? mergeDataBySales : mergeDataBySalesAccount;
this.sales_regular_data.pivotData[pivotRow].filterColValues = filterColValues;
};
get_sales_regular_data = async (param, pivotRow = 'operatorName') => {
const seriesKey = `${param.Date1}${param.Date2}`;
const rawData = await getDetailData({...param, });
const filterHasOld = rawData.filter(ele => (ele.IsOld === '1' || ele.isCusCommend === '1')).map(e => ({
...e,
seriesKey,
operatorNameB: e.operatorName.replace(/\([^)]*\)/gi, '').toLowerCase(),
}));
const { data: hasOldData, } = pivotBy(filterHasOld, [['hasOld', pivotRow], [], []]);
const { data: IsOldData, } = pivotBy(filterHasOld.filter(ele => ele.IsOld === '1'), [[pivotRow, 'IsOld_txt', ], [], []]);
const { data: isCusCommendData, } = pivotBy(filterHasOld.filter(ele => ele.isCusCommend === '1'), [[pivotRow, 'isCusCommend_txt', ], [], []]);
// console.log('IsOldData====', IsOldData, '\nisCusCommend', isCusCommendData);
// console.log('data====', rawData, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns, '\nsummaryMix', summaryMix, '\nhasOld',filterHasOld);
const mergeDataBySales = hasOldData.map((ele) => ({
...ele,
seriesKey,
children: [].concat(
IsOldData.filter((ele1) => ele1[pivotRow] === ele[pivotRow]).map(o => ({...o, [pivotRow]: o.IsOld_txt, key: o.rowLabel, seriesKey,})),
isCusCommendData.filter((ele2) => ele2[pivotRow] === ele[pivotRow]).map(o => ({...o, [pivotRow]: o.isCusCommend_txt, key: o.rowLabel, seriesKey,}))
),
}));
// 合并顾问的账户
let mergeDataBySalesAccount = [];
if (pivotRow === 'operatorName') {
const { data: hasOldDataSales, } = pivotBy(filterHasOld, [['hasOld', 'operatorNameB'], [], []]);
const { data: IsOldDataSales, } = pivotBy(filterHasOld.filter(ele => ele.IsOld === '1'), [['operatorNameB', 'IsOld_txt', ], [], []]);
const { data: isCusCommendDataSales, } = pivotBy(filterHasOld.filter(ele => ele.isCusCommend === '1'), [['operatorNameB', 'isCusCommend_txt', ], [], []]);
mergeDataBySalesAccount = hasOldDataSales.map((ele) => ({
...ele,
operatorName: ele.operatorNameB,
seriesKey,
children: [].concat(
IsOldDataSales.filter((ele1) => ele1.operatorNameB === ele.operatorNameB).map(o => ({...o, operatorName: o.IsOld_txt, key: o.rowLabel, seriesKey,})),
isCusCommendDataSales.filter((ele2) => ele2.operatorNameB === ele.operatorNameB).map(o => ({...o, operatorName: o.isCusCommend_txt, key: o.rowLabel, seriesKey,}))
),
}));
}
// console.log('IsOldDataSales====', IsOldDataSales, '\nisCusCommendDataSales', isCusCommendDataSales);
return { mergeDataBySales, mergeDataBySalesAccount, filterHasOld };
};
setSearchValues(obj, values, target) {
this[target].groups = obj.DepartmentList;
this[target].webcode = obj.WebCode;
this[target].include_tickets = obj.IncludeTickets;
this[target].date_type = obj.DateType;
}
} }

@ -1,6 +1,7 @@
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from "mobx";
import * as config from "../config"; import * as config from "../config";
import { resultDataCb } from '../components/DateGroupRadio/date'; import { resultDataCb } from '../components/DateGroupRadio/date';
import { groupsMappedByKey } from './../libs/ht';
class DashboardStore { class DashboardStore {
constructor(rootStore) { constructor(rootStore) {
@ -33,7 +34,7 @@ class DashboardStore {
this.orders_data.loading = true; this.orders_data.loading = true;
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-web/QueryData/GetOrderCount"; let url = "/service-web/QueryData/GetOrderCount";
url += "?WebCode=all&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "?WebCode=all&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
@ -78,7 +79,7 @@ class DashboardStore {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-baseinfo/QueryWebData?type=orders_temp&db=1"; let url = "/service-baseinfo/QueryWebData?type=orders_temp&db=1";
const website_code = "'" + this.ordersTemp_data.webcode.join("','") + "'"; const website_code = "'" + this.ordersTemp_data.webcode.join("','") + "'";
url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
@ -99,7 +100,7 @@ class DashboardStore {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-baseinfo/QueryWebData?type=orders_temp_detail&db=1"; let url = "/service-baseinfo/QueryWebData?type=orders_temp_detail&db=1";
const website_code = "'" + this.ordersTemp_data.webcode.join("','") + "'"; const website_code = "'" + this.ordersTemp_data.webcode.join("','") + "'";
url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
@ -138,8 +139,8 @@ class DashboardStore {
} else { } else {
url += "&ApplydateCheck=0&OrderStartdateCheck=1"; url += "&ApplydateCheck=0&OrderStartdateCheck=1";
} }
url += "&ApplydateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplydateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&ApplydateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplydateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += "&OrderStartdateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&OrderStartdateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&OrderStartdateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&OrderStartdateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
@ -197,7 +198,17 @@ class DashboardStore {
group_handleChange: this.handleChange_group_select.bind(this), group_handleChange: this.handleChange_group_select.bind(this),
onChange_datetype: this.onChange_datetype.bind(this), onChange_datetype: this.onChange_datetype.bind(this),
asyncFetch: this.get_CountYDOrder.bind(this), asyncFetch: this.get_CountYDOrder.bind(this),
mobileSearchValues: {
'DateType': { key: 'applyDate', value: 'applyDate', label: '预定日期'},
'DepartmentList': ["1", "2", "28", "7", "8", "9", "11", "12", "20", "21", "18"].map(ele => groupsMappedByKey[ele] ),
}
}; };
setMobileSearchValues(obj, values) {
this.mobile_data.date_type = obj.DateType;
this.mobile_data.groups = obj.DepartmentList;
}
// 移动成交 end // 移动成交 end
// 汇率变化 begin // 汇率变化 begin
@ -211,7 +222,7 @@ class DashboardStore {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
this.exchangeRate_data.loading = true; this.exchangeRate_data.loading = true;
let url = "/service-web/QueryData/GetCurrency?Currency=ALL"; let url = "/service-web/QueryData/GetCurrency?Currency=ALL";
url += "&Currdate1=" + Currdate1_start + "&Currdate2=" + Currdate1_end + "%2023:59:59"; url += "&Currdate1=" + Currdate1_start + "&Currdate2=" + Currdate1_end + "%2023:59:00";
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {

@ -0,0 +1,51 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import { fetchJSON } from '../utils/request';
import { isEmpty, sortBy, pick, merge, fixTo2Decimals, groupBy, sortKeys, fixToInt, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from './../libs/ht';
class Trade {
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
/**
* 明细
*/
getDetailData = async (param, page) => {
this.detailData[page] = { loading: true, dataSource: [], originData: [] };
const json = await fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
if (json.errcode === 0) {
runInAction(() => {
this.detailData[page].loading = false;
this.detailData[page].dataSource = json.result;
this.detailData[page].originData = json.result;
});
}
return json.result;
};
setSearchValues(body) {
this.searchValues = body;
}
timeLineKey = 'week';
setTimeLineKey(v) {
this.timeLineKey = v;
}
resetData = () => {
this.detailData = {
orders: { loading: false, dataSource: [], originData: [] },
trade: { loading: false, dataSource: [], originData: [] },
};
};
searchValues = {};
detailData = {
orders: { loading: false, dataSource: [], originData: [] },
trade: { loading: false, dataSource: [], originData: [] },
};
}
export default Trade;

@ -1,5 +1,6 @@
import {makeAutoObservable} from "mobx"; import {makeAutoObservable} from "mobx";
import moment from "moment"; import moment from "moment";
import { DATE_FORMAT, DATETIME_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
/** /**
* 管理搜索组件的状态 * 管理搜索组件的状态
*/ */
@ -45,7 +46,7 @@ class DatePickerStore {
'DepartmentList': { 'key': 'ALL', 'label': '所有小组' }, 'DepartmentList': { 'key': 'ALL', 'label': '所有小组' },
'WebCode': { 'key': 'ALL', 'label': '所有来源' }, 'WebCode': { 'key': 'ALL', 'label': '所有来源' },
'IncludeTickets': { 'key': '1', 'label': '含门票' }, 'IncludeTickets': { 'key': '1', 'label': '含门票' },
'DateType': { 'key': 'ConfirmDate', 'label': '确认日期' }, 'DateType': { 'key': 'confirmDate', 'label': '确认日期' },
'year': this.start_date, 'year': this.start_date,
// 'months': [moment(), moment()], // 'months': [moment(), moment()],
'dates': [this.start_date, this.end_date], 'dates': [this.start_date, this.end_date],
@ -55,9 +56,9 @@ class DatePickerStore {
DepartmentList: 'ALL', DepartmentList: 'ALL',
WebCode: 'ALL', WebCode: 'ALL',
IncludeTickets: '1', IncludeTickets: '1',
DateType: 'ConfirmDate', DateType: 'confirmDate',
Date1: this.start_date.format('YYYY-MM-DD'), Date1: this.start_date.format(DATE_FORMAT),
Date2: this.end_date.format('YYYY-MM-DD 23:59:59'), Date2: this.end_date.format(SMALL_DATETIME_FORMAT),
}; };
setFormValues(data){ setFormValues(data){
@ -67,6 +68,11 @@ class DatePickerStore {
setFormValuesToSub(data){ setFormValuesToSub(data){
this.formValuesToSub = data; this.formValuesToSub = data;
} }
siderBroken = false;
setSiderBroken(broken){
this.siderBroken = broken;
}
} }
export default DatePickerStore; export default DatePickerStore;

@ -1,18 +1,19 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx'; import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request'; import * as req from '../utils/request';
import { DATE_FORMAT } from './../config'; import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment'; import moment from 'moment';
import { isEmpty, pick, sortBy, fixTo2Decimals, cloneDeep, unique } from '../utils/commons'; import { isEmpty, pick, sortBy, fixTo2Decimals, cloneDeep, unique } from '../utils/commons';
const modelMapper = { const modelMapper = {
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays', keySort: true }, 'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays', keySort: true, dynamicsX: false },
'PML': { url: '/service-Analyse2/GetTradeApartByPML', keySort: true }, 'PML': { url: '/service-Analyse2/GetTradeApartByPML', keySort: true, dynamicsX: false },
'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays', keySort: true }, 'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays', keySort: true, dynamicsX: false },
'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays', keySort: true }, 'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays', keySort: true, dynamicsX: false },
'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum', keySort: true }, 'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum', keySort: true, dynamicsX: false },
'destination': { url: '/service-Analyse2/GetTradeApartByDestination', keySort: false }, 'destination': { url: '/service-Analyse2/GetTradeApartByDestination', keySort: false, dynamicsX: true, },
'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination', keySort: false }, 'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination', keySort: false, dynamicsX: true, },
'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry', keySort: false }, 'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry', keySort: false, dynamicsX: true, },
'guestCountry': { url: '/service-Analyse2/GetTradeApartByGuestCountry', keySort: false, dynamicsX: true, },
}; };
class Distribution { class Distribution {
constructor(appStore) { constructor(appStore) {
@ -23,7 +24,7 @@ class Distribution {
/** /**
* 各个类型的分布 * 各个类型的分布
*/ */
getApartData = async (param) => { getApartData = async (param, getDiff = undefined) => {
this.pageLoading = true; this.pageLoading = true;
const mkey = this.curTab; const mkey = this.curTab;
this[mkey] = { loading: true, dataSource: [] }; this[mkey] = { loading: true, dataSource: [] };
@ -35,25 +36,41 @@ class Distribution {
const [DateToQ1, DateToQ2] = [moment(param.Date1).subtract(moment(param.Date2).diff(param.Date1, 'days') + 1, 'days'), moment(param.Date1).subtract(1, 'days')]; const [DateToQ1, DateToQ2] = [moment(param.Date1).subtract(moment(param.Date2).diff(param.Date1, 'days') + 1, 'days'), moment(param.Date1).subtract(1, 'days')];
// 同比的参数: 去年同期 // 同比的参数: 去年同期
const [DateToY1, DateToY2] = [moment(param.Date1).subtract(1, 'year'), moment(param.Date2).subtract(1, 'year')]; const [DateToY1, DateToY2] = [moment(param.Date1).subtract(1, 'year'), moment(param.Date2).subtract(1, 'year')];
param.DateToY1 = DateToY1.format(DATE_FORMAT); const dynamicsX = getDiff ?? modelMapper[mkey].dynamicsX;
param.DateToY2 = DateToY2.format(`${DATE_FORMAT} 23:59:59`); if (getDiff === undefined) {
param.DateToQ1 = DateToQ1.format(DATE_FORMAT); param.DateToY1 = DateToY1.format(DATE_FORMAT);
param.DateToQ2 = DateToQ2.format(`${DATE_FORMAT} 23:59:59`); param.DateToY2 = DateToY2.format(SMALL_DATETIME_FORMAT);
const json = await req.fetchJSON(modelMapper[mkey].url, param); param.DateToQ1 = DateToQ1.format(DATE_FORMAT);
param.DateToQ2 = DateToQ2.format(SMALL_DATETIME_FORMAT);
}
const json = dynamicsX === false ? await req.fetchJSON(modelMapper[mkey].url, param) : await this.getApartDataStep(param);
if (json.errcode === 0) { if (json.errcode === 0) {
const dataLength = json.result.length; const dataLength = json.result.length;
const pickResult = dataLength > 20 ? json.result.slice(0, 30) : json.result; const dataSource = calcDiff({ result: json.result, resultToY: json.resultToY, resultToQ: json.resultToQ }, modelMapper[mkey].keySort);
const dataSource = calcDiff({ result: pickResult, resultToY: json.resultToY, resultToQ: json.resultToQ }, modelMapper[mkey].keySort);
runInAction(() => { runInAction(() => {
this[mkey].loading = false; this[mkey].loading = false;
this[mkey].originData = json.result; this[mkey].originData = json.result;
this[mkey].dataSource = dataSource; this[mkey].dataSource = dataLength > 20 ? dataSource.slice(0,30) : dataSource;
this.pageLoading = false; this.pageLoading = false;
}); });
} }
return this[mkey]; return this[mkey];
}; };
getApartDataStep = async (param) => {
const mkey = this.curTab;
this[mkey] = { loading: true, dataSource: [] };
const xParam = cloneDeep(param);
delete xParam.DateToY1;
delete xParam.DateToY2;
delete xParam.DateToQ1;
delete xParam.DateToQ2;
const { result, ...jsonY } = await req.fetchJSON(modelMapper[mkey].url, { ...xParam });
const { result: resultToY } = await req.fetchJSON(modelMapper[mkey].url, { ...xParam, Date1: param.DateToY1, Date2: param.DateToY2 });
const { result: resultToQ } = await req.fetchJSON(modelMapper[mkey].url, { ...xParam, Date1: param.DateToQ1, Date2: param.DateToQ2 });
return { ...jsonY, result, resultToY, resultToQ };
};
/** /**
* 明细 * 明细
*/ */
@ -68,7 +85,7 @@ class Distribution {
this.scatterDays = daysData; this.scatterDays = daysData;
}); });
} }
return this.detailData; return json.result;
}; };
resetData = () => { resetData = () => {
@ -83,6 +100,7 @@ class Distribution {
this.destination = { loading: false, dataSource: [] }; this.destination = { loading: false, dataSource: [] };
this.GlobalDestination = { loading: false, dataSource: [] }; this.GlobalDestination = { loading: false, dataSource: [] };
this.destinationCountry = { loading: false, dataSource: [] }; this.destinationCountry = { loading: false, dataSource: [] };
this.guestCountry = { loading: false, dataSource: [] };
}; };
curTab = 'tourDays'; curTab = 'tourDays';
@ -114,6 +132,7 @@ class Distribution {
destination = { loading: false, dataSource: [] }; destination = { loading: false, dataSource: [] };
GlobalDestination = { loading: false, dataSource: [] }; GlobalDestination = { loading: false, dataSource: [] };
destinationCountry = { loading: false, dataSource: [] }; destinationCountry = { loading: false, dataSource: [] };
guestCountry = { loading: false, dataSource: [] };
} }
/** /**
@ -150,6 +169,17 @@ const calcDiff = ({ result, resultToY, resultToQ }, keySort) => {
: 0, : 0,
ConfirmOrderDiffY: resultMapped[row.key].ConfirmOrder - resultToYMapped[row.key].ConfirmOrder, ConfirmOrderDiffY: resultMapped[row.key].ConfirmOrder - resultToYMapped[row.key].ConfirmOrder,
ConfirmOrderDiffQ: resultMapped[row.key].ConfirmOrder - resultToQMapped[row.key].ConfirmOrder, ConfirmOrderDiffQ: resultMapped[row.key].ConfirmOrder - resultToQMapped[row.key].ConfirmOrder,
SumOrderY: resultToYMapped?.[row.key]?.SumOrder || 0,
SumOrderToY: resultToYMapped?.[row.key]?.SumOrder
? fixTo2Decimals(((resultMapped[row.key].SumOrder - resultToYMapped[row.key].SumOrder) / resultToYMapped[row.key].SumOrder) * 100)
: 0,
SumOrderQ: resultToQMapped?.[row.key]?.SumOrder || 0,
SumOrderToQ: resultToQMapped?.[row.key]?.SumOrder
? fixTo2Decimals(((resultMapped[row.key].SumOrder - resultToQMapped[row.key].SumOrder) / resultToQMapped[row.key].SumOrder) * 100)
: 0,
SumOrderDiffY: resultMapped[row.key].SumOrder - resultToYMapped[row.key].SumOrder,
SumOrderDiffQ: resultMapped[row.key].SumOrder - resultToQMapped[row.key].SumOrder,
}; };
return { ...resultMapped[row.key], ...diff, resultToY: resultToYMapped[row.key], resultToQ: resultToQMapped[row.key] }; return { ...resultMapped[row.key], ...diff, resultToY: resultToYMapped[row.key], resultToQ: resultToQMapped[row.key] };
}); });

@ -1,7 +1,7 @@
import {makeAutoObservable, runInAction} from "mobx"; import {makeAutoObservable, runInAction} from "mobx";
import * as dd from 'dingtalk-jsapi';
import * as config from "../config"; import * as config from "../config";
import * as comm from '../utils/commons'; import * as comm from '../utils/commons';
import { fetchJSON } from '../utils/request';
// 财务管理 // 财务管理
@ -85,14 +85,20 @@ class FinancialStore {
this.credit_card_data.groups = value; this.credit_card_data.groups = value;
}; };
setSearchValues(obj, values) {
this.credit_card_data.business_units = obj.businessUnits;
// this.credit_card_data.groups = obj.businessUnits;
this.bill_type_data.bill_types = obj.billtype;
}
// 请求信用卡账单 // 请求信用卡账单
get_credit_card_bills() { get_credit_card_bills() {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-web/QueryData/GetCreditCardBills'; let url = '/service-web/QueryData/GetCreditCardBills';
url += `?business_unit=${this.credit_card_data.business_units.toString()}&groups=${this.credit_card_data.groups.toString()}&billtype=${this.bill_type_data.bill_types.toString()}`; url += `?business_unit=${this.credit_card_data.business_units}&groups=${this.credit_card_data.groups.toString()}&billtype=${this.bill_type_data.bill_types}`;
url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) { if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then((response) => response.json()) .then((response) => response.json())
@ -110,10 +116,10 @@ class FinancialStore {
get_credit_card_bills_by_type() { get_credit_card_bills_by_type() {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-web/QueryData/GetCreditCardBillsByType'; let url = '/service-web/QueryData/GetCreditCardBillsByType';
url += `?business_unit=${this.credit_card_data.business_units.toString()}&groups=${this.credit_card_data.groups.toString()}`; url += `?business_unit=${this.credit_card_data.business_units}&groups=${this.credit_card_data.groups.toString()}`;
url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) { if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + '%2023:59:59'; url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then((response) => response.json()) .then((response) => response.json())
@ -126,6 +132,107 @@ class FinancialStore {
console.log('fetch data failed', error); console.log('fetch data failed', error);
}); });
} }
/**
* 服务人数页面 ----
*/
serviceModelMapper = {
'inbound': { url: '/service-Analyse2/inbound_person_num', keySort: true, dynamicsX: false },
'outbound': { url: '/service-Analyse2/outbound_person_num', keySort: true, dynamicsX: false },
'domestic': { url: '/service-Analyse2/domestic_person_num', keySort: true, dynamicsX: false },
};
servicePersonNum = { curTab: 'inbound', loading: false,
'inbound': { loading: false, dataSource: [], rawData: [], NoEmptyData: [], },
'outbound': { loading: false, dataSource: [], rawData: [], NoEmptyData: [], },
'domestic': { loading: false, dataSource: [], rawData: [], NoEmptyData: [], },
};
setCurTab(v) {
this.servicePersonNum.curTab = v;
}
resetPersonNumData = () => {
this.servicePersonNum.inbound = { loading: false, dataSource: [], rawData: [], NoEmptyData: [], };
this.servicePersonNum.outbound = { loading: false, dataSource: [], rawData: [], NoEmptyData: [], };
this.servicePersonNum.domestic = { loading: false, dataSource: [], rawData: [], NoEmptyData: [], };
};
setPersonNumTableDataSource = (notnull) => {
const mkey = this.servicePersonNum.curTab;
if (comm.isEmpty(this.servicePersonNum[mkey].dataSource)) {
return false;
}
this.servicePersonNum[mkey].dataSource = (notnull === false ? this.servicePersonNum[mkey].rawData : this.servicePersonNum[mkey].NoEmptyData);
};
handleRows = (json) => {
const mkey = this.servicePersonNum.curTab;
const sumFun = (_data, key='sum') => ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({ ...r, [skey]: _data.reduce((a, c) => a + (c[skey] || 0), 0) }), {
groupsKey: key, // `sumResult`,
groupsLabel: '合计',
});
const percentRow = (row, sumResult) => {
return ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({ ...r, [`${skey}Percent`]: sumResult[skey] ? comm.fixTo4Decimals(row[skey] / sumResult[skey]) : 0 }), row);
};
const sumResult = sumFun(json.result, 'sumResult');
const emptyRows = json.result.filter(ele => ele.groupsLabel === '' || ele.groupsLabel === '未知');
const sumEmptyResult = sumFun(emptyRows, 'sumEmptyResult');
const NotEmptyRows = json.result.filter(ele => ele.groupsLabel !== '' && ele.groupsLabel !== '未知');
const sumNotEmptyRows = sumFun(NotEmptyRows, 'sumNotEmptyRows');
const result = NotEmptyRows.map((row) => ({...row, ...percentRow(row, sumNotEmptyRows)})).sort(comm.sortBy('orgz')).reverse().map((row) => {
const newData = ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce(
(r, skey) => ({ ...r, [skey]: row[`${skey}Percent`] ? row[skey] + comm.fixToInt(sumEmptyResult[skey] * row[`${skey}Percent`]) : row[skey] }),
row
);
return newData;
});
const sumAfter = sumFun(result, 'sumAfter');
const diffSum = ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({ ...r, [skey]: sumResult[skey]-sumAfter[skey]}), {});
const result0 = ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({...r, [skey]: (result?.[0]?.[skey] || 0)+(diffSum?.[skey] || 0)}), {});
result[0] = { ...result[0], ...result0 };
const totalRow = Object.keys(json.resultTotal).length > 1 ? { ...json.resultTotal, groupsKey: `total${mkey}` } : (mkey === 'domestic' ? sumResult : { groupsKey: 'empty'});
const IndividualServiceRow = {orgz: json.resultTotal.IndividualService, groupsKey: `individualService${mkey}`, groupsLabel: '单项服务人数', };
return {
sumResult,
result,
rawData: [].concat([IndividualServiceRow, totalRow], json.result.sort(comm.sortBy('orgz')).reverse()),
NoEmptyData: [].concat([IndividualServiceRow, totalRow], result),
};
};
/**
* 获取服务人数
*/
async getPersonNum(queryData) {
const mkey = this.servicePersonNum.curTab;
const url = this.serviceModelMapper[mkey].url;
this.servicePersonNum.loading = true;
this.servicePersonNum[this.servicePersonNum.curTab].loading = true;
const json = await fetchJSON(url, {...queryData, DateType: 'startDate'});
const { rawData, NoEmptyData } = this.handleRows(json);
if (json.errcode === 0) {
runInAction(() => {
this.servicePersonNum.loading = false;
this.servicePersonNum[this.servicePersonNum.curTab].loading = false;
this.servicePersonNum[this.servicePersonNum.curTab].dataSource = rawData;
this.servicePersonNum[this.servicePersonNum.curTab].rawData = rawData;
this.servicePersonNum[this.servicePersonNum.curTab].NoEmptyData = NoEmptyData;
});
}
return json;
}
/**
* ---- end 服务人数页面
*/
} }

@ -0,0 +1,200 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import { fetchJSON } from '../utils/request';
import { isEmpty, sortDescBy, objectMapper, groupBy, pick, unique, cloneDeep, omit, fixTo2Decimals } from '../utils/commons';
import { groupsMappedByCode, dataFieldAlias } from './../libs/ht';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment';
const fetchHotelData = async (param) => {
const defaultParam = {
DEI_SN: '',
City: '',
OrderState: '',
BookingType: '-1',
RecommendedLevel: '-1',
Star: '-1',
ArriveDateCheck: '0',
ArriveDateStart: '',
ArriveDateEnd: '',
ConfirmDateCheck: '0',
ConfirmDateStart: '',
ConfirmDateEnd: '',
Compare: '0',
CompareDateStart: '',
CompareDateEnd: '',
Area: '-1',
};
const json = await fetchJSON('/service-Analyse2/HotelReservation', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result || [] : [];
};
const fetchCruiseData = async (param) => {
const defaultParam = {
DEI_SN: '',
OrderState: '', // 0: 不成行 1: 成行
ArriveDateStart: '',
ArriveDateEnd: '',
Compare: '',
CompareDateStart: '',
CompareDateEnd: '',
BookingType: '', // 0: 非单订三峡1: 单订三峡
ProductName: '',
Direction: '', // 1: 上水 2: 下水
VEI_SN: '-1', // 只要列出常用游船供应商选择
RoomNumStart: '0',
RoomNumEnd: '',
PersonNumStart: '0',
PersonNumEnd: '',
Country: '-1',
};
const json = await fetchJSON('/service-Analyse2/CruiseReservation', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result || [] : [];
};
const paramKeyMapped = {
'DateType': [
{ key: 'ArriveDateCheck', transform: (val) => (val === 'startDate' ? '1' : '0') },
{ key: 'ConfirmDateCheck', transform: (val) => (val === 'confirmDate' ? '1' : '0') },
// { key: 'ApplyDateCheck', transform: (val) => (val === 'applyDate' ? '1' : '0') },
],
'DepartmentList': { key: 'DEI_SN' },
'orderStatus': { key: 'OrderState' },
'Date1': [{ key: 'ArriveDateStart' }, { key: 'ConfirmDateStart' }],
'Date2': [{ key: 'ArriveDateEnd' }, { key: 'ConfirmDateEnd' }],
'DateDiff1': { key: 'CompareDateStart' },
'DateDiff2': { key: 'CompareDateEnd' },
'keyword': { key: 'ProductName' },
'agency': { key: 'VEI_SN' },
'cruiseDirection': { key: 'Direction' },
'cruiseBookType': { key: 'BookingType' },
'hotelStar': { key: 'Star' },
'hotelRecommandRate': { key: 'RecommendedLevel' },
'hotelBookType': { key: 'BookingType' },
'country': { key: 'Country' },
'countryArea': { key: 'Area', transform: (val) => (val === 'china' ? '1' : val === 'foreign' ? '0' : '-1') },
};
class HotelCruise {
constructor(appStore) {
this.appStore = appStore;
makeAutoObservable(this);
}
async getCruiseData(param = {}) {
this.cruise.loading = true;
this.cruise.dataSource = [];
const _queryParam = objectMapper(param, paramKeyMapped);
const queryParam = omit({ ...this.searchValuesToSub, ..._queryParam }, Object.keys(paramKeyMapped));
queryParam.Compare = isEmpty(param.DateDiff1) ? '' : '1';
const res = await fetchCruiseData(queryParam);
const resCP =
queryParam.Compare === ''
? res
: (res || []).map((ele) => ({
...ele,
// 计算 增长率 = (当前值 - 上次值) / 上次值 * 100
TotalNumPercent: ele.CPTotalNum ? fixTo2Decimals(((ele.TotalNum - ele.CPTotalNum) / ele.CPTotalNum) * 100) : '-',
TotalPersonNumPercent: ele.CPTotalPersonNum ? fixTo2Decimals(((ele.TotalPersonNum - ele.CPTotalPersonNum) / ele.CPTotalPersonNum) * 100) : '-',
TotalProfitPercent: ele.CPTotalProfit ? fixTo2Decimals(((ele.TotalProfit - ele.CPTotalProfit) / ele.CPTotalProfit) * 100) : '-',
}));
const summaryRow = ['TotalNum', 'TotalPersonNum', 'TotalProfit', 'CPTotalNum', 'CPTotalPersonNum', 'CPTotalProfit'].reduce(
(r, skey) => ({
...r,
[skey]: resCP.reduce((a, c) => a + c[skey], 0),
}),
{ ProductName: '合计' }
);
const summaryDelta = ['TotalNum', 'TotalPersonNum', 'TotalProfit'].reduce(
(r, skey) => ({
...r,
[`${skey}Percent`]: queryParam.Compare === '' ? null : fixTo2Decimals(((summaryRow[skey] - summaryRow[`CP${skey}`]) / summaryRow[`CP${skey}`]) * 100),
}),
{}
);
runInAction(() => {
this.cruise.loading = false;
this.cruise.dataSource = resCP;
this.cruise.summaryRow = { ...summaryRow, ...summaryDelta };
});
return this.cruise;
}
async getHotelData(param = {}) {
this.hotel.loading = true;
this.hotel.dataSource = [];
const _queryParam = objectMapper(param, paramKeyMapped);
const queryParam = omit({ ...this.searchValuesToSub, ..._queryParam }, Object.keys(paramKeyMapped));
queryParam.Compare = isEmpty(param.DateDiff1) ? '0' : '1';
const _res = await fetchHotelData(queryParam);
const res = (_res || []).map((ele) => ({ ...ele, RecommendRate_100: fixTo2Decimals(ele.RecommendRate * 100) + '%' }));
const resCP =
queryParam.Compare === '0'
? res
: (res || []).map((ele) => ({
...ele,
TotalNumPercent: ele.CPTotalNum ? fixTo2Decimals(((ele.TotalNum - ele.CPTotalNum) / ele.CPTotalNum) * 100) : '-',
RecomendNumPercent: ele.CPRecomendNum ? fixTo2Decimals(((ele.RecomendNum - ele.CPRecomendNum) / ele.CPRecomendNum) * 100) : '-',
RecommendRateDelta: fixTo2Decimals((ele.RecommendRate - (ele.CPRecommendRate || 0)) * 100),
CPRecommendRate_100: fixTo2Decimals(ele.CPRecommendRate * 100) + '%',
}));
const summaryRow = ['TotalNum', 'RecomendNum', ].reduce(
(r, skey) => ({
...r,
[skey]: resCP.reduce((a, c) => a + c[skey], 0),
[`CP${skey}`]: resCP.reduce((a, c) => a + c[`CP${skey}`], 0),
}),
{ CityName: '合计' }
);
summaryRow.RecommendRate = fixTo2Decimals(summaryRow.RecomendNum/summaryRow.TotalNum);
summaryRow.RecommendRate_100 = fixTo2Decimals(summaryRow.RecommendRate * 100) + '%';
summaryRow.CPRecommendRate = fixTo2Decimals(summaryRow.CPRecomendNum/summaryRow.CPTotalNum);
summaryRow.CPRecommendRate_100 = fixTo2Decimals(summaryRow.CPRecommendRate * 100) + '%';
const summaryDelta = ['TotalNum', 'RecomendNum', ].reduce(
(r, skey) => ({
...r,
[`${skey}Percent`]: queryParam.Compare === '0' ? null : fixTo2Decimals(((summaryRow[skey] - summaryRow[`CP${skey}`]) / summaryRow[`CP${skey}`]) * 100),
}),
{}
);
summaryDelta.RecommendRateDelta = queryParam.Compare === '0' ? undefined : fixTo2Decimals((summaryRow.RecommendRate - (summaryRow.CPRecommendRate || 0)) * 100);
runInAction(() => {
this.hotel.loading = false;
this.hotel.dataSource = resCP;
this.hotel.summaryRow = { ...summaryRow, ...summaryDelta };
});
}
searchValues = {
date: moment(),
DateType: { key: 'confirmDate', label: '确认日期' },
WebCode: { key: '', label: '所有来源' },
// IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: [{ key: '', label: '所有小组' }],
operator: '-1',
opisn: '-1',
};
searchValuesToSub = {};
setSearchValues(obj, values) {
this.searchValues = { ...this.searchValues, ...values };
this.searchValuesToSub = obj;
}
cruise = { loading: false, dataSource: [], summaryRow: {} };
hotel = { loading: false, dataSource: [], summaryRow: {} };
resetData = () => {
this.results.loading = false;
for (const key of Object.keys(this.results)) {
if (key !== 'loading') {
this.results[key] = [];
}
}
for (const key of Object.keys(this.hotel)) {
if (key !== 'loading') {
this.hotel[key] = [];
}
}
};
}
export default HotelCruise;

@ -14,6 +14,11 @@ import TradeStore from "./Trade";
import KPI from "./KPI"; import KPI from "./KPI";
import DictData from "./DictData"; import DictData from "./DictData";
import Distribution from "./Distribution"; import Distribution from "./Distribution";
import DataPivot from './DataPivot';
import MeetingData from './MeetingData2024';
import MeetingData2025 from './MeetingData2025';
import SalesCRMData from './SalesCRMData';
import HotelCruise from './HotelCruise';
class Index { class Index {
constructor() { constructor() {
this.dashboard_store = new DashboardStore(this); this.dashboard_store = new DashboardStore(this);
@ -31,6 +36,11 @@ class Index {
this.KPIStore = new KPI(this); this.KPIStore = new KPI(this);
this.DictDataStore = new DictData(this); this.DictDataStore = new DictData(this);
this.DistributionStore = new Distribution(this); this.DistributionStore = new Distribution(this);
this.DataPivotStore = new DataPivot(this);
this.MeetingDataStore = new MeetingData(this);
this.MeetingData2025Store = new MeetingData2025(this);
this.SalesCRMDataStore = new SalesCRMData(this);
this.HotelCruiseStore = new HotelCruise(this);
makeAutoObservable(this); makeAutoObservable(this);
} }

@ -0,0 +1,355 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON } from '../utils/request';
import { objectMapper, pick, price_to_number, } from '../utils/commons';
import { pivotBy } from '../libs/ht';
import moment from "moment";
import { DATE_FORMAT, DATETIME_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
/**
* 用于透视的数据
*/
const getDetailData = async (param) => {
const json = await fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
return json.errcode === 0 ? json.result : [];
};
/**
*
*/
const getOrderCountByType = async (param) => {
const paramBody = objectMapper(param, {
WebCode: 'WebCode',
OrderType: 'OrderType',
IncludeTickets: 'IncludeTickets',
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'COLI_ApplyDate1',
Date2: 'COLI_ApplyDate2',
});
const url = '/service-web/QueryData/GetOrderCountByType';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getAgentGroupInfoALL = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'OldDate1',
Date2: 'OldDate2',
});
const url = '/service-web/QueryData/GetAgentGroupInfoALL';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getDepartmentOrderMLByType = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
OrderType: 'OrderType', // 总览, 产品类型
});
const url = '/service-web/QueryData/GetDepartmentOrderMLByType';
const json = await fetchJSON(url, paramBody);
const { result1 } = json.errcode === 0 ? json : { result1: [] };
const total1 = ['COLI_CJCount', 'COLI_ML2',].reduce(
(r, col) => ({
...r,
[col]: result1.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
return { total1, result1 };
};
const GHproductTypeListSetting = {
ja: ['日本', '东亚跨国'],
se: ['东南亚跨国', '泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const GHCountryListSetting = {
ja: ['日本', ],
se: ['泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝', '新加坡', '马来西亚', '菲律宾'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const rowItem = (filterData) => {
const { data: dataByLineClass, summaryMix: summaryByLineClass } = pivotBy(filterData, [['COLI_LineClass'], [], []]);
const LineClass_Origin = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const LineClass_PPC = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const { data: dataByWebCode, summaryMix: summaryByWebCode } = pivotBy(filterData, [['WebCode'], [], []]);
const toB = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const external = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf("站外渠道") !== -1).reduce((r, c) => r + c.SumOrder, 0);
const filterIsOldC = filterData.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') === -1);
const { data: dataByIsOld, summaryMix: summaryByIsOld } = pivotBy(filterIsOldC, [['IsOld', 'isCusCommend'], [], []]);
const isOld1 = dataByIsOld.filter((ele) => ele.rowLabel.indexOf('1') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const total = LineClass_Origin + LineClass_PPC + toB + isOld1 + external;
return { LineClass_Origin, LineClass_PPC, toB, external, isOld1, total };
};
// 日本+: 日本+东亚跨国
const dataJA = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.ja;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 东南亚+: 东南亚跨国+泰国+越南+印尼+水灯节线路
const dataSE = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.se;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 印度+: 印度+次大陆跨国+尼泊尔+不丹+斯里兰卡
const dataIN = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.in;
const exceptProduct = ['印度尼西亚'];
const filterData = rawData
.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData
.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 其他GH
const dataGHOther = (rawData, yearData) => {
const exceptProduct = Object.values(GHproductTypeListSetting).reduce((r, c) => r.concat(c), []);
const filterData = rawData.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
const dataSales = (tKey, rawData, yearData, yearData2) => {
const targetList = GHCountryListSetting[tKey];
const tIndex = Object.keys(GHCountryListSetting).indexOf(tKey);
const exceptTargetList = Object.keys(GHCountryListSetting).reduce((r, c, i) => r.concat(i < tIndex ? GHCountryListSetting[c] : []), []);
// console.log(tIndex, tKey, 'exceptTargetList', exceptTargetList, 'targetList', targetList);
const filterRaw1 = rawData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log(tKey, 'filterRaw1', filterRaw1);
const filterDataC = filterRaw1.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataT = tKey === 'se' ? filterRaw1.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterData = filterDataC.concat(filterDataT);
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterRaw2 = yearData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearC = filterRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearT = tKey === 'se' ? filterRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear = filterDataYearC.concat(filterDataYearT);
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log(tKey, filterDataYear.map(ee => ee.destinationCountry_AsJOSN), filterDataYear.map(ee => ee.productType), filterDataYear);
const filterDataYearRaw2 = yearData2.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2C = filterDataYearRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2T = tKey === 'se' ? filterDataYearRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear2 = filterDataYear2C.concat(filterDataYear2T);
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
const cols = ['YJLY', 'CJCount'].reduce((r, key) => ({ ...r, [key]: filterData.reduce((a, c) => a + price_to_number(c[key]), 0) }), {});
// console.log(tKey, filterData, filterDataYear, filterDataYear2);
return { ...cols, GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
const dataSalesGHOther = (rawData, yearData, yearData2) => {
const exceptContry = Object.values(GHCountryListSetting).reduce((r, c) => r.concat(c), []);
// console.log('exceptContry', exceptContry);
// console.log('OOoo rawData', rawData.map(e => e.destinationCountry_AsJOSN));
const filterData = rawData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log('OOoo', filterData.map(e => e.destinationCountry_AsJOSN), filterData.map(e => e.productType));
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterDataYear = yearData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('OOoo year', filterDataYear.map(e => e.destinationCountry_AsJOSN), filterDataYear.map(e => e.productType));
const filterDataYear2 = yearData2
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('Oo', filterDataYear2.map(e => e.destinationCountry_AsJOSN), filterDataYear2);
// console.log('Oo row', rowYearData2);
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
return { GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
class MeetingData {
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
searchValues = {
DateType: { key: 'applyDate', value: 'applyDate', label: '提交日期' },
};
setSearchValues(body) {
this.searchValues = body;
}
GHTableData = [];
GHTableLoading = false;
/**
* 获取市场订单数据 ---------------------------------------------------------------------------------------------------
*/
dataGHOrder = async (param) => {
// console.log('dataGH', param);
this.GHTableLoading = true;
const defaultParam = { DateType: 'applyDate' };
// 本周
const CHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '1', 'WebCode': 'All' });
const exceptCHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '28,33', 'WebCode': 'All' });
const yearStart = moment().startOf("year").format(DATE_FORMAT);
/** 截至今年 - 行 */
const { ordercountTotal1: CHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1', 'WebCode': 'All', OrderType: 'LineClass' });
const { ordercount1: exceptCHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '28,33', 'WebCode': 'All', OrderType: 'Product' });
/** 截至今年 - 列 */
const { ordercount1: ColLineClassDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All', OrderType: 'LineClass' });
const { ordercountTotal1: ColToBDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'GHTOBHW,GHTOBZG', OrderType: 'LineClass' });
const { ordercountTotal1: ColExternalDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'ZWQD', OrderType: 'LineClass' });
// 老客户
const yearDetail = await getDetailData({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All' });
const { isOld1: isOld1Year } = rowItem(yearDetail);
const colYearRow = {
LineClass_Origin: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.OrderCount, 0),
LineClass_PPC: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.OrderCount, 0),
toB: ColToBDataYear.OrderCount,
isOld1: isOld1Year,
external: ColExternalDataYear.OrderCount,
};
const rows = [
{ key: 'ch', label: '中国', ...rowItem(CHData), rowYear: CHDataYear.OrderCount },
{ key: 'ja', label: '日本+', ...dataJA(exceptCHData, exceptCHDataYear) },
{ key: 'se', label: '东南亚+', ...dataSE(exceptCHData, exceptCHDataYear) },
{ key: 'in', label: '印度+', ...dataIN(exceptCHData, exceptCHDataYear) },
{ key: 'other', label: '其他GH', ...dataGHOther(exceptCHData, exceptCHDataYear) },
];
const columnsSum = ['LineClass_Origin', 'LineClass_PPC', 'toB', 'external', 'isOld1', 'total', 'rowYear'].reduce(
(r, col) => ({
...r,
[col]: rows.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
rows.push({ key: 'columnSum', label: '合计', ...columnsSum });
rows.push({ key: 'colYearRow', label: '截至', ...colYearRow });
runInAction(() => {
this.GHTableData = rows;
this.GHTableLoading = false;
});
};
GHSalesTableData = [];
GHSalesLoading = false;
/**
* 获取GH销售数据 ---------------------------------------------------------------------------------------------------
*/
dataGHSales = async (param) => {
this.GHSalesLoading = true;
const salesParam = { ...param, DateType: 'confirmDate', 'WebCode': 'All', };
// console.log(param);
const { total1: CHSalesDataWeek } = await getDepartmentOrderMLByType({...salesParam, DepartmentList: '1', OrderType:'ALL'});
const exceptCHSalesDataWeek = await getDetailData({ ...salesParam, 'DepartmentList': '28,33' });
const GHDataWeekConfirm = exceptCHSalesDataWeek.filter((ele) => Number(ele.orderState) === 1); // 成交的
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const yearEnd = moment().endOf("year").format(SMALL_DATETIME_FORMAT);
/** 截至今年 - 成交 */
const { total1: CHDataYear } = await getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '1', OrderType:'ALL' });
const GHDataYear = await getDetailData({ ...salesParam, Date1: yearStart, 'DepartmentList': '28,33' });
const GHDataYearConfirm = GHDataYear.filter((ele) => Number(ele.orderState) === 1); // 成交的
/** 截至今年 - 走团 */
const { total1: CHStartDataYear } = await getDepartmentOrderMLByType({...salesParam, Date1: yearStart,Date2:yearEnd,DepartmentList: '1', OrderType:'ALL',DateType: 'startDate' });
const GHStartDataYear0 = await getDetailData({ ...salesParam, DateType: 'startDate', 'DepartmentList': '28,33', Date1: yearStart,Date2:yearEnd, });
const GHStartDataYear = GHStartDataYear0.filter((ele) => Number(ele.orderState) === 1); // 成交的
const rows = [
{
key: 'ch',
label: '中国',
YJLY: price_to_number(CHSalesDataWeek.COLI_ML2),
CJCount: (CHSalesDataWeek.COLI_CJCount),
rowYear: { YJLY: price_to_number(CHDataYear.COLI_ML2), CJCount: CHDataYear.COLI_CJCount, YJLY2: price_to_number(CHStartDataYear.COLI_ML2) },
},
{ key: 'ja', label: '日本+', ...dataSales('ja', GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
{ key: 'se', label: '东南亚+', ...dataSales('se', GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
{ key: 'in', label: '印度+', ...dataSales('in', GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
{ key: 'other', label: '其他GH', ...dataSalesGHOther(GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
];
const columnsSum = ['CJCount', 'YJLY'].reduce((r, col) => ({ ...r, [col]: rows.reduce((rr, row) => rr + row[col], 0) }), {});
const allYearData = rows.map(row => row.rowYear);
const rowYear = ['CJCount', 'YJLY', 'CJCount2', 'YJLY2', ].reduce((r, col) => ({ ...r, [col]: allYearData.reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
rows.push({ key: 'columnSum', label: '合计', ...columnsSum, rowYear });
// console.log(rows, allYearData, rowYear);
runInAction(() => {
this.GHSalesTableData = rows;
this.GHSalesLoading = false;
});
};
GHServiceTableData = [];
GHServiceLoading = false;
/**
* 获取GH服务数据 ---------------------------------------------------------------------------------------------------
*/
dataGHService = async (param) => {
this.GHServiceLoading = true;
const serviceParam = { ...param, DateType: 'startDate', 'WebCode': 'All' };
// 走团数
const { ordercountTotal1: { OrderCount: GroupCount } } = await getOrderCountByType({ ...serviceParam, 'DepartmentList': '1', OrderType: 'Form' });
const exceptCHDataWeek = await getDetailData({ ...serviceParam, 'DepartmentList': '28,33' });
// 走团数 - 年
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const { ordercountTotal1: { OrderCount: GroupCountYear } } = await getOrderCountByType({ ...serviceParam, 'DepartmentList': '1', OrderType: 'Form', Date1: yearStart, });
const exceptCHDataYear = await getDetailData({ ...serviceParam, Date1: yearStart, 'DepartmentList': '28,33' });
// 好评数
const { total1: { GoodCount } } = await getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '1', });
const { total1: { GoodCount: GoodCountYear } } = await getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '1', });
const { total1: { GoodCount: GHGoodCountWeek } } = await getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '28,33', });
const { total1: { GoodCount: GHGoodCountYear } } = await getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '28,33', });
const rows = [
{ key: 'ch', label: '中国', ...{GoodCount, GroupCount}, rowYear: { GroupCount: GroupCountYear, GoodCount: GoodCountYear } },
{ key: 'ja', label: '日本+', ...dataSales('ja', exceptCHDataWeek, exceptCHDataYear, []) },
{ key: 'se', label: '东南亚+', ...dataSales('se', exceptCHDataWeek, exceptCHDataYear, []) },
{ key: 'in', label: '印度+', ...dataSales('in', exceptCHDataWeek, exceptCHDataYear, []) },
{ key: 'other', label: '其他GH', ...dataSalesGHOther(exceptCHDataWeek, exceptCHDataYear, []) },
];
const GHRowWeek = { GoodCount: GHGoodCountWeek, GroupCount: 0 };
const columnsSum = ['GoodCount', 'GroupCount'].reduce((r, col) => ({ ...r, [col]: [...rows, GHRowWeek].reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
const allYearData = rows.map((row) => row.rowYear).concat([{ GoodCount: GHGoodCountYear, GroupCount: 0 }]);
// console.log(allYearData);
const rowYear = ['GoodCount', 'GroupCount', ].reduce((r, col) => ({ ...r, [col]: allYearData.reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
rows.push({ key: 'columnSum', label: '合计', ...columnsSum, rowYear });
// console.log(rows);
runInAction(() => {
this.GHServiceTableData = rows;
this.GHServiceLoading = false;
});
};
}
export default MeetingData;

@ -0,0 +1,479 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON } from '../utils/request';
import { objectMapper, pick, price_to_number, } from '../utils/commons';
import { pivotBy } from './../libs/ht';
import moment from "moment";
import { DATE_FORMAT, DATETIME_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
/**
* 用于透视的数据
*/
const getDetailData = async (param) => {
const json = await fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
return json.errcode === 0 ? json.result : [];
};
/**
*
*/
const getOrderCountByType = async (param) => {
const paramBody = objectMapper(param, {
WebCode: 'WebCode',
OrderType: 'OrderType',
IncludeTickets: 'IncludeTickets',
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'COLI_ApplyDate1',
Date2: 'COLI_ApplyDate2',
});
const url = '/service-web/QueryData/GetOrderCountByType';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getAgentGroupInfoALL = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'OldDate1',
Date2: 'OldDate2',
});
const url = '/service-web/QueryData/GetAgentGroupInfoALL';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getDepartmentOrderMLByType = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
OrderType: 'OrderType', // 总览, 产品类型
});
const url = '/service-web/QueryData/GetDepartmentOrderMLByType';
const json = await fetchJSON(url, paramBody);
const { result1 } = json.errcode === 0 ? json : { result1: [] };
const total1 = ['COLI_CJCount', 'COLI_ML2',].reduce(
(r, col) => ({
...r,
[col]: result1.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
return { total1, result1 };
};
const GHproductTypeListSetting = {
ja: ['日本', '东亚跨国'],
se: ['东南亚跨国', '泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const GHCountryListSetting = {
ja: ['日本', ],
se: ['泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝', '新加坡', '马来西亚', '菲律宾'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const rowItem = (filterData) => {
const { data: dataByLineClass, summaryMix: summaryByLineClass } = pivotBy(filterData, [['COLI_LineClass'], [], []]);
const LineClass_Origin = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const LineClass_PPC = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const { data: dataByWebCode, summaryMix: summaryByWebCode } = pivotBy(filterData, [['WebCode'], [], []]);
const toB = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const external = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf("站外渠道") !== -1).reduce((r, c) => r + c.SumOrder, 0);
const filterIsOldC = filterData.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') === -1);
const { data: dataByIsOld, summaryMix: summaryByIsOld } = pivotBy(filterIsOldC, [['IsOld', 'isCusCommend'], [], []]);
const isOld1 = dataByIsOld.filter((ele) => ele.rowLabel.indexOf('1') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const total = LineClass_Origin + LineClass_PPC + toB + isOld1 + external;
return { LineClass_Origin, LineClass_PPC, toB, external, isOld1, total };
};
// 日本+: 日本+东亚跨国
const dataJA = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.ja;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 东南亚+: 东南亚跨国+泰国+越南+印尼+水灯节线路
const dataSE = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.se;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 印度+: 印度+次大陆跨国+尼泊尔+不丹+斯里兰卡
const dataIN = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.in;
const exceptProduct = ['印度尼西亚'];
const filterData = rawData
.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData
.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 其他GH
const dataGHOther = (rawData, yearData) => {
const exceptProduct = Object.values(GHproductTypeListSetting).reduce((r, c) => r.concat(c), []);
const filterData = rawData.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
const dataSales = (tKey, rawData, yearData, yearData2) => {
const targetList = GHCountryListSetting[tKey];
const tIndex = Object.keys(GHCountryListSetting).indexOf(tKey);
const exceptTargetList = Object.keys(GHCountryListSetting).reduce((r, c, i) => r.concat(i < tIndex ? GHCountryListSetting[c] : []), []);
// console.log(tIndex, tKey, 'exceptTargetList', exceptTargetList, 'targetList', targetList);
const filterRaw1 = rawData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log(tKey, 'filterRaw1', filterRaw1);
const filterDataC = filterRaw1.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataT = tKey === 'se' ? filterRaw1.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterData = filterDataC.concat(filterDataT);
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterRaw2 = yearData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearC = filterRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearT = tKey === 'se' ? filterRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear = filterDataYearC.concat(filterDataYearT);
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log(tKey, filterDataYear.map(ee => ee.destinationCountry_AsJOSN), filterDataYear.map(ee => ee.productType), filterDataYear);
const filterDataYearRaw2 = yearData2.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2C = filterDataYearRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2T = tKey === 'se' ? filterDataYearRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear2 = filterDataYear2C.concat(filterDataYear2T);
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
const cols = ['YJLY', 'CJCount'].reduce((r, key) => ({ ...r, [key]: filterData.reduce((a, c) => a + price_to_number(c[key]), 0) }), {});
// console.log(tKey, filterData, filterDataYear, filterDataYear2);
return { ...cols, GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
const dataSalesGHOther = (rawData, yearData, yearData2) => {
const exceptContry = Object.values(GHCountryListSetting).reduce((r, c) => r.concat(c), []);
// console.log('exceptContry', exceptContry);
// console.log('OOoo rawData', rawData.map(e => e.destinationCountry_AsJOSN));
const filterData = rawData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log('OOoo', filterData.map(e => e.destinationCountry_AsJOSN), filterData.map(e => e.productType));
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterDataYear = yearData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('OOoo year', filterDataYear.map(e => e.destinationCountry_AsJOSN), filterDataYear.map(e => e.productType));
const filterDataYear2 = yearData2
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('Oo', filterDataYear2.map(e => e.destinationCountry_AsJOSN), filterDataYear2);
// console.log('Oo row', rowYearData2);
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
return { GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
class MeetingData {
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
searchValues = {
DateType: { key: 'applyDate', value: 'applyDate', label: '提交日期' },
};
setSearchValues(body) {
this.searchValues = body;
}
GHTableData = [];
GHTableLoading = false;
/**
* 获取市场订单数据 ---------------------------------------------------------------------------------------------------
*/
dataGHOrder = async (param) => {
// console.log('dataGH', param);
this.GHTableLoading = true;
const defaultParam = { DateType: 'applyDate' };
// 本周
const CHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '1', 'WebCode': 'All' });
const exceptCHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '28,33', 'WebCode': 'All' });
const yearStart = moment().startOf("year").format(DATE_FORMAT);
/** 截至今年 - 行 */
const [
{ ordercountTotal1: CHDataYear },
{ ordercount1: exceptCHDataYear },
] = await Promise.all([
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1', 'WebCode': 'All', OrderType: 'LineClass' }),
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '28,33', 'WebCode': 'All', OrderType: 'Product' }),
]);
// const { ordercountTotal1: CHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1', 'WebCode': 'All', OrderType: 'LineClass' });
// const { ordercount1: exceptCHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '28,33', 'WebCode': 'All', OrderType: 'Product' });
/** 截至今年 - 列 */
const [
{ ordercount1: ColLineClassDataYear },
{ ordercountTotal1: ColToBDataYear },
{ ordercountTotal1: ColExternalDataYear },
] = await Promise.all([
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All', OrderType: 'LineClass' }),
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'GHTOBHW,GHTOBZG', OrderType: 'LineClass'}),
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'ZWQD', OrderType: 'LineClass'}),
]);
// const { ordercount1: ColLineClassDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All', OrderType: 'LineClass' });
// const { ordercountTotal1: ColToBDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'GHTOBHW,GHTOBZG', OrderType: 'LineClass' });
// const { ordercountTotal1: ColExternalDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'ZWQD', OrderType: 'LineClass' });
// 老客户
const yearDetail = await getDetailData({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All' });
const { isOld1: isOld1Year } = rowItem(yearDetail);
const colYearRow = {
LineClass_Origin: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.OrderCount, 0),
LineClass_PPC: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.OrderCount, 0),
toB: ColToBDataYear.OrderCount,
isOld1: isOld1Year,
external: ColExternalDataYear.OrderCount,
};
const rows = [
{ key: 'ch', label: '中国', ...rowItem(CHData), rowYear: CHDataYear.OrderCount },
{ key: 'ja', label: '日本+', ...dataJA(exceptCHData, exceptCHDataYear) },
{ key: 'se', label: '东南亚+', ...dataSE(exceptCHData, exceptCHDataYear) },
{ key: 'in', label: '印度+', ...dataIN(exceptCHData, exceptCHDataYear) },
{ key: 'other', label: '其他GH', ...dataGHOther(exceptCHData, exceptCHDataYear) },
];
const columnsSum = ['LineClass_Origin', 'LineClass_PPC', 'toB', 'external', 'isOld1', 'total', 'rowYear'].reduce(
(r, col) => ({
...r,
[col]: rows.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
rows.push({ key: 'columnSum', label: '合计', ...columnsSum });
rows.push({ key: 'colYearRow', label: '截至', ...colYearRow });
runInAction(() => {
this.GHTableData = rows;
this.GHTableLoading = false;
});
};
GHSalesTableData = [];
GHSalesLoading = false;
/**
* 获取GH销售数据 ---------------------------------------------------------------------------------------------------
*/
dataGHSales = async (param) => {
this.GHSalesLoading = true;
const salesParam = { ...param, DateType: 'confirmDate', WebCode: 'CHT,AH,GH,GHKYZG,GHKYHW,ZWQD', OrderType:'ALL', }; // WebCode: 不含分销
const partnerParam = { WebCode: 'GHTOBHW,GHTOBZG' };
const [
{ total1: CHSalesDataTotal },
{ total1: CHPartnerSalesData },
{ total1: AHSalesDataTotal },
{ total1: AHpartnerSalesData },
{ total1: GHSalesDataTotal },
{ total1: GHpartnerSalesData },
] = await Promise.all([
getDepartmentOrderMLByType({...salesParam, DepartmentList: '1,2', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '1,2', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '28', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '28', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '33', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '33', ...partnerParam }),
]);
// 不含分销 = 总额 - 分销
const CHSalesData = {'COLI_CJCount': CHSalesDataTotal.COLI_CJCount-CHPartnerSalesData.COLI_CJCount, 'COLI_ML2': CHSalesDataTotal.COLI_ML2-CHPartnerSalesData.COLI_ML2};
const AHSalesData = {'COLI_CJCount': AHSalesDataTotal.COLI_CJCount-AHpartnerSalesData.COLI_CJCount, 'COLI_ML2': AHSalesDataTotal.COLI_ML2-AHpartnerSalesData.COLI_ML2};
const GHSalesData = {'COLI_CJCount': GHSalesDataTotal.COLI_CJCount-GHpartnerSalesData.COLI_CJCount, 'COLI_ML2': GHSalesDataTotal.COLI_ML2-GHpartnerSalesData.COLI_ML2};
const partnerSalesData = {'COLI_CJCount': CHPartnerSalesData.COLI_CJCount+AHpartnerSalesData.COLI_CJCount, 'COLI_ML2': CHPartnerSalesData.COLI_ML2+AHpartnerSalesData.COLI_ML2};
const totalSalesData = {
'COLI_CJCount': CHSalesDataTotal.COLI_CJCount + AHSalesDataTotal.COLI_CJCount + GHSalesDataTotal.COLI_CJCount,
'COLI_ML2': CHSalesDataTotal.COLI_ML2 + AHSalesDataTotal.COLI_ML2 + GHSalesDataTotal.COLI_ML2,
};
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const yearEnd = moment().endOf("year").format(SMALL_DATETIME_FORMAT);
/** 截至今年 - 成交 */
const [
{ total1: CHSalesYearTotal },
{ total1: CHPartnerSalesYear },
{ total1: AHSalesYearTotal },
{ total1: AHpartnerSalesYear },
{ total1: GHSalesYearTotal },
{ total1: GHpartnerSalesYear },
] = await Promise.all([
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '1,2', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '1,2', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '28', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '28', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '33', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '33', ...partnerParam }),
]);
const CHDataYear = {'COLI_CJCount': CHSalesYearTotal.COLI_CJCount-CHPartnerSalesYear.COLI_CJCount, 'COLI_ML2': CHSalesYearTotal.COLI_ML2-CHPartnerSalesYear.COLI_ML2};
const AHDataYear = {'COLI_CJCount': AHSalesYearTotal.COLI_CJCount-AHpartnerSalesYear.COLI_CJCount, 'COLI_ML2': AHSalesYearTotal.COLI_ML2-AHpartnerSalesYear.COLI_ML2};
const GHDataYear = {'COLI_CJCount': GHSalesYearTotal.COLI_CJCount-GHpartnerSalesYear.COLI_CJCount, 'COLI_ML2': GHSalesYearTotal.COLI_ML2-GHpartnerSalesYear.COLI_ML2};
const partnerDataYear = {'COLI_CJCount': CHPartnerSalesYear.COLI_CJCount+AHpartnerSalesYear.COLI_CJCount, 'COLI_ML2': CHPartnerSalesYear.COLI_ML2+AHpartnerSalesYear.COLI_ML2};
const totalDataYear = {
'COLI_CJCount': CHSalesYearTotal.COLI_CJCount + AHSalesYearTotal.COLI_CJCount + GHSalesYearTotal.COLI_CJCount,
'COLI_ML2': CHSalesYearTotal.COLI_ML2 + AHSalesYearTotal.COLI_ML2 + GHSalesYearTotal.COLI_ML2,
};
/** 截至今年 - 走团 */
const [
{ total1: CHStartDataYearTotal },
{ total1: CHPartnerStartDataYear },
{ total1: AHStartDataYearTotal },
{ total1: AHpartnerStartDataYear },
{ total1: GHStartDataYearTotal },
{ total1: GHpartnerStartDataYear },
] = await Promise.all([
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '1,2', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '1,2', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '28', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '28', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '33', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '33', ...partnerParam }),
]);
const CHStartDataYear = {'COLI_CJCount': CHStartDataYearTotal.COLI_CJCount-CHPartnerStartDataYear.COLI_CJCount, 'COLI_ML2': CHStartDataYearTotal.COLI_ML2-CHPartnerStartDataYear.COLI_ML2};
const AHStartDataYear = {'COLI_CJCount': AHStartDataYearTotal.COLI_CJCount-AHpartnerStartDataYear.COLI_CJCount, 'COLI_ML2': AHStartDataYearTotal.COLI_ML2-AHpartnerStartDataYear.COLI_ML2};
const GHStartDataYear = {'COLI_CJCount': GHStartDataYearTotal.COLI_CJCount-GHpartnerStartDataYear.COLI_CJCount, 'COLI_ML2': GHStartDataYearTotal.COLI_ML2-GHpartnerStartDataYear.COLI_ML2};
const partnerStartDataYear = {'COLI_CJCount': CHPartnerStartDataYear.COLI_CJCount+AHpartnerStartDataYear.COLI_CJCount, 'COLI_ML2': CHPartnerStartDataYear.COLI_ML2+AHpartnerStartDataYear.COLI_ML2};
const totalStartDataYear = {
'COLI_CJCount': CHStartDataYearTotal.COLI_CJCount + AHStartDataYearTotal.COLI_CJCount + GHStartDataYearTotal.COLI_CJCount,
'COLI_ML2': CHStartDataYearTotal.COLI_ML2 + AHStartDataYearTotal.COLI_ML2 + GHStartDataYearTotal.COLI_ML2,
};
const rows = [
{
key: 'ch',
label: 'CH(不含分销)',
YJLY: price_to_number(CHSalesData.COLI_ML2),
CJCount: (CHSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(CHDataYear.COLI_ML2), CJCount: CHDataYear.COLI_CJCount, YJLY2: price_to_number(CHStartDataYear.COLI_ML2) },
},
{
key: 'ah',
label: 'AH(不含分销)',
YJLY: price_to_number(AHSalesData.COLI_ML2),
CJCount: (AHSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(AHDataYear.COLI_ML2), CJCount: AHDataYear.COLI_CJCount, YJLY2: price_to_number(AHStartDataYear.COLI_ML2) },
},
{
key: 'partner',
label: '分销',
YJLY: price_to_number(partnerSalesData.COLI_ML2),
CJCount: (partnerSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(partnerDataYear.COLI_ML2), CJCount: partnerDataYear.COLI_CJCount, YJLY2: price_to_number(partnerStartDataYear.COLI_ML2) },
},
{
key: 'gh',
label: 'GH(不含分销)',
YJLY: price_to_number(GHSalesData.COLI_ML2),
CJCount: (GHSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(GHDataYear.COLI_ML2), CJCount: GHDataYear.COLI_CJCount, YJLY2: price_to_number(GHStartDataYear.COLI_ML2) },
},
{
key: 'total',
label: '合计',
YJLY: price_to_number(totalSalesData.COLI_ML2),
CJCount: (totalSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(totalDataYear.COLI_ML2), CJCount: totalDataYear.COLI_CJCount, YJLY2: price_to_number(totalStartDataYear.COLI_ML2) },
},
];
runInAction(() => {
this.GHSalesTableData = rows;
this.GHSalesLoading = false;
});
};
GHServiceTableData = [];
GHServiceLoading = false;
/**
* 获取GH服务数据 ---------------------------------------------------------------------------------------------------
*/
dataGHService = async (param) => {
this.GHServiceLoading = true;
const serviceParam = { ...param, DateType: 'startDate', 'WebCode': 'All' };
// 走团数
const [
{ ordercountTotal1: { OrderCount: CHGroupCount } },
{ ordercountTotal1: { OrderCount: AHGroupCount } },
{ ordercountTotal1: { OrderCount: GHGroupCount } },
] = await Promise.all([
getOrderCountByType({ ...serviceParam, 'DepartmentList': '1,2', OrderType: 'Form'}),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '28', OrderType: 'Form'}),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '33', OrderType: 'Form'}),
]);
// 走团数 - 年
// * 走团: 整团数, 而非各地接社/目的地的总和
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const [
{ ordercountTotal1: { OrderCount: CHGroupCountYear } },
{ ordercountTotal1: { OrderCount: AHGroupCountYear } },
{ ordercountTotal1: { OrderCount: GHGroupCountYear } },
] = await Promise.all([
getOrderCountByType({ ...serviceParam, 'DepartmentList': '1,2', OrderType: 'Form', Date1: yearStart, }),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '28', OrderType: 'Form', Date1: yearStart, }),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '33', OrderType: 'Form', Date1: yearStart, }),
]);
// 好评数
const [
{ total1: { GoodCount: CHGoodCount } },
{ total1: { GoodCount: AHGoodCount } },
{ total1: { GoodCount: GHGoodCount } },
] = await Promise.all([
getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '1,2', }),
getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '28', }),
getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '33', }),
]);
// 好评数 - 年
const [
{ total1: { GoodCount: CHGoodCountYear, } },
{ total1: { GoodCount: AHGoodCountYear, } },
{ total1: { GoodCount: GHGoodCountYear, } },
] = await Promise.all([
getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '1,2', }),
getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '28', }),
getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '33', }),
]);
const rows = [
{ key: 'ch', label: 'CH', GoodCount: CHGoodCount, GroupCount: CHGroupCount, rowYear: { GroupCount: CHGroupCountYear, GoodCount: CHGoodCountYear } },
{ key: 'ah', label: 'AH', GoodCount: AHGoodCount, GroupCount: AHGroupCount, rowYear: { GroupCount: AHGroupCountYear, GoodCount: AHGoodCountYear } },
{ key: 'Gh', label: 'GH', GoodCount: GHGoodCount, GroupCount: GHGroupCount, rowYear: { GroupCount: GHGroupCountYear, GoodCount: GHGoodCountYear } },
];
// const GHRowWeek = { GoodCount: GHGoodCountWeek, GroupCount: 0 };
// const columnsSum = ['GoodCount', 'GroupCount'].reduce((r, col) => ({ ...r, [col]: [...rows, GHRowWeek].reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
// const allYearData = rows.map((row) => row.rowYear).concat([{ GoodCount: GHGoodCountYear, GroupCount: 0 }]);
// // console.log(allYearData);
// const rowYear = ['GoodCount', 'GroupCount', ].reduce((r, col) => ({ ...r, [col]: allYearData.reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
// rows.push({ key: 'columnSum', label: '合计', ...columnsSum, rowYear });
// console.log(rows);
runInAction(() => {
this.GHServiceTableData = rows;
this.GHServiceLoading = false;
});
};
}
export default MeetingData;

@ -73,6 +73,21 @@ class OrdersStore {
this.include_tickets = value; this.include_tickets = value;
}; };
searchValues = {
DateType: { key: 'applyDate', label: '提交日期'},
WebCode: { key: 'All', label: '所有来源'},
IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: groupsMappedByCode.GH,
};
setSearchValues(obj, values) {
this.groups = obj.DepartmentList;
this.webcode = obj.WebCode;
this.include_tickets = obj.IncludeTickets;
this.date_type = obj.DateType;
this.searchValues = values;
}
// 切换标签页 // 切换标签页
onChange_Tabs_sub(ordertype, ordertype_sub, active_key) { onChange_Tabs_sub(ordertype, ordertype_sub, active_key) {
this.active_tab_key_sub = active_key; this.active_tab_key_sub = active_key;
@ -212,9 +227,9 @@ class OrdersStore {
let url = "/service-web/QueryData/GetOrderCount"; let url = "/service-web/QueryData/GetOrderCount";
url += `?OrderType=${ordertype}&OrderType_val=${ordertype_sub}&IncludeTickets=${this.include_tickets}`; url += `?OrderType=${ordertype}&OrderType_val=${ordertype_sub}&IncludeTickets=${this.include_tickets}`;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`; url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
url += "&WebCode=" + this.webcode + "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&WebCode=" + this.webcode + "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) { if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
@ -241,10 +256,10 @@ class OrdersStore {
const date_picker_store = this.rootStore.date_picker_store; const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-web/QueryData/GetOrderCount"; // ?WebCode=cht&COLI_ApplyDate1=2022-08-01&COLI_ApplyDate2=2022-08-31&COLI_ApplyDateold1=2021-08-01&COLI_ApplyDateold2=2021-08-31'; let url = "/service-web/QueryData/GetOrderCount"; // ?WebCode=cht&COLI_ApplyDate1=2022-08-01&COLI_ApplyDate2=2022-08-31&COLI_ApplyDateold1=2021-08-01&COLI_ApplyDateold2=2021-08-31';
url += "?WebCode=" + this.webcode + "&IncludeTickets=" + this.include_tickets; url += "?WebCode=" + this.webcode + "&IncludeTickets=" + this.include_tickets;
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT) ;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`; url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) { if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT) ;
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
@ -269,9 +284,9 @@ class OrdersStore {
let url = "/service-web/QueryData/GetOrderCountByType"; let url = "/service-web/QueryData/GetOrderCountByType";
url += "?WebCode=" + this.webcode + "&OrderType=" + order_type + "&IncludeTickets=" + this.include_tickets; url += "?WebCode=" + this.webcode + "&OrderType=" + order_type + "&IncludeTickets=" + this.include_tickets;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`; url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) { if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())
@ -296,9 +311,9 @@ class OrdersStore {
} }
url += `?WebCode=${this.webcode}&OrderType=${ordertype}&OrderType_val=${ordertype_sub}&SubOrderType=${sub_type}` + "&IncludeTickets=" + this.include_tickets; url += `?WebCode=${this.webcode}&OrderType=${ordertype}&OrderType_val=${ordertype_sub}&SubOrderType=${sub_type}` + "&IncludeTickets=" + this.include_tickets;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`; url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) { if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59"; url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
.then(response => response.json()) .then(response => response.json())

@ -5,6 +5,8 @@ import * as comm from '../utils/commons';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { groupsMappedByCode, dataFieldAlias } from './../libs/ht'; import { groupsMappedByCode, dataFieldAlias } from './../libs/ht';
import * as req from '../utils/request'; import * as req from '../utils/request';
import { parseKPI } from './KPI';
import { parseMergeItem } from './Trade';
// 销售数据 // 销售数据
class SaleStore { class SaleStore {
@ -30,11 +32,40 @@ class SaleStore {
type_data_sub = []; // 类型的子维度数据 type_data_sub = []; // 类型的子维度数据
date_title = 'date_title'; // 日期段,只用于显示,防止日期选择控件的变化导致页面刷新 date_title = 'date_title'; // 日期段,只用于显示,防止日期选择控件的变化导致页面刷新
salesTrade = { groupType: 'dept', loading: false, operator: [], dept: [], overview: [] }; searchValues = {
DateType: { key: 'confirmDate', label: '确认日期'},
WebCode: { key: 'All', label: '所有来源'},
IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: [groupsMappedByCode.GH],
operator: '',
};
setSearchValues(obj, values) {
this.groups = obj.DepartmentList;
this.webcode = obj.WebCode;
this.include_tickets = obj.IncludeTickets;
this.date_type = obj.DateType;
this.searchValues.operator = obj.operator;
}
salesTrade = {
groupType: 'dept', loading: false, tableDataSource: [],
operator: [], dept: [], overview: [],
operatorMapped: {}, pickSales: [], pickSalesData: [],
};
setGroupType(v) { setGroupType(v) {
this.salesTrade.groupType = v; this.salesTrade.groupType = v;
} }
setPickSales = (v) => {
this.salesTrade.pickSales = v;
setTimeout(() => {
const data = v.reduce((r, ele) => r.concat(this.salesTrade.operatorMapped[ele.key]), []);
this.salesTrade.pickSalesData = data;
}, 500);
};
// 是否包含门票 // 是否包含门票
handleChange_include_tickets = (value) => { handleChange_include_tickets = (value) => {
this.include_tickets = value; this.include_tickets = value;
@ -53,6 +84,7 @@ class SaleStore {
// 切换标签页 // 切换标签页
onChange_Tabs(active_key) { onChange_Tabs(active_key) {
this.active_tab_key = active_key; this.active_tab_key = active_key;
this.type_data = {};
} }
// 下单日期或者出发日期 // 下单日期或者出发日期
@ -81,9 +113,9 @@ class SaleStore {
this.date_title = `${date1_start}~${date1_end}`; this.date_title = `${date1_start}~${date1_end}`;
let url = '/service-web/QueryData/GetDepartmentOrderML'; let url = '/service-web/QueryData/GetDepartmentOrderML';
url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`; url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:59`; url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:00`;
if (date2_start && date2_end) { if (date2_start && date2_end) {
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:59`; url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:00`;
this.date_title += ` ${date2_start}~${date2_end}`; this.date_title += ` ${date2_start}~${date2_end}`;
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
@ -123,9 +155,9 @@ class SaleStore {
this.date_title = `${date1_start}~${date1_end}`; this.date_title = `${date1_start}~${date1_end}`;
let url = '/service-web/QueryData/GetDepartmentOrderMLByType'; let url = '/service-web/QueryData/GetDepartmentOrderMLByType';
url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&OrderType=${this.active_tab_key}&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`; url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&OrderType=${this.active_tab_key}&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:59`; url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:00`;
if (date2_start && date2_end) { if (date2_start && date2_end) {
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:59`; url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:00`;
this.date_title += ` ${date2_start}~${date2_end}`; this.date_title += ` ${date2_start}~${date2_end}`;
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
@ -135,35 +167,42 @@ class SaleStore {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
if (false) { if (false) {
} else { } else {
const result1 = comm.isEmpty(this.searchValues.operator) ? json.result1 : json.result1.filter(row => (this.searchValues.operator.split(',')).includes(String(row.OPI_SN)));
const result2 = comm.isEmpty(this.searchValues.operator) ? json.result2 : json.result2.filter(row => (this.searchValues.operator.split(',')).includes(String(row.OPI_SN)));
const allOPI1 = comm.uniqWith(result1.map(rr => ({ text: rr.OPI_Name, value: rr.OPI_SN })), (a, b) => JSON.stringify(a) === JSON.stringify(b)).sort((a, b) => a.text.localeCompare(b.text));
if (this.active_tab_key === 'All') { if (this.active_tab_key === 'All') {
result.columns = [ result.columns = [
{ {
title: '', title: '账户名',
children: [ children: [
{ {
title: '', title: '',
dataIndex: 'OPI_Name', dataIndex: 'OPI_Name',
}, },
], ],
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.OPI_SN === value,
filterSearch: true,
}, },
{ {
title: '毛利', title: '毛利',
children: [{ title: json.result1.reduce((a, b) => a + comm.price_to_number(b.COLI_ML), 0), dataIndex: 'COLI_ML' }], children: [{ title: result1.reduce((a, b) => a + comm.price_to_number(b.COLI_ML), 0), dataIndex: 'COLI_ML' }],
sorter: (a, b) => comm.price_to_number(b.COLI_ML) - comm.price_to_number(a.COLI_ML), sorter: (a, b) => comm.price_to_number(b.COLI_ML) - comm.price_to_number(a.COLI_ML),
}, },
{ {
title: '成行率', title: '成行率',
children: [{ title: comm.formatPercent(json.result1.reduce((a, b) => a + b.COLI_CJCount, 0) / json.result1.reduce((a, b) => a + b.COLI_OrderCount, 0)), dataIndex: 'COLI_CJrate' }], children: [{ title: comm.formatPercent(result1.reduce((a, b) => a + b.COLI_CJCount, 0) / result1.reduce((a, b) => a + b.COLI_OrderCount, 0)), dataIndex: 'COLI_CJrate' }],
sorter: (a, b) => parseInt(b.COLI_CJrate) - parseInt(a.COLI_CJrate), sorter: (a, b) => parseInt(b.COLI_CJrate) - parseInt(a.COLI_CJrate),
}, },
{ {
title: '成团数', title: '成团数',
children: [{ title: json.result1.reduce((a, b) => a + b.COLI_CJCount, 0), dataIndex: 'COLI_CJCount' }], children: [{ title: result1.reduce((a, b) => a + b.COLI_CJCount, 0), dataIndex: 'COLI_CJCount' }],
sorter: (a, b) => b.COLI_CJCount - a.COLI_CJCount, sorter: (a, b) => b.COLI_CJCount - a.COLI_CJCount,
}, },
{ {
title: '订单数', title: '订单数',
children: [{ title: json.result1.reduce((a, b) => a + b.COLI_OrderCount, 0), dataIndex: 'COLI_OrderCount' }], children: [{ title: result1.reduce((a, b) => a + b.COLI_OrderCount, 0), dataIndex: 'COLI_OrderCount' }],
sorter: (a, b) => b.COLI_OrderCount - a.COLI_OrderCount, sorter: (a, b) => b.COLI_OrderCount - a.COLI_OrderCount,
}, },
{ {
@ -183,7 +222,7 @@ class SaleStore {
}, },
]; ];
result.dataSource = json.result1; result.dataSource = result1;
} else if (this.active_tab_key === 'ResponseRateByWL') { } else if (this.active_tab_key === 'ResponseRateByWL') {
result.columns = [ result.columns = [
{ {
@ -194,49 +233,55 @@ class SaleStore {
dataIndex: 'OPI_Name', dataIndex: 'OPI_Name',
}, },
], ],
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.OPI_SN === value,
filterSearch: true,
}, },
{ {
title: '报价次数', title: '报价次数',
children: [{ title: json.result1.reduce((a, b) => a + b.PriceTime, 0), dataIndex: 'PriceTime' }], children: [{ title: result1.reduce((a, b) => a + b.PriceTime, 0), dataIndex: 'PriceTime' }],
sorter: (a, b) => b.PriceTime - a.PriceTime, sorter: (a, b) => b.PriceTime - a.PriceTime,
}, },
{ {
title: '邮件发送次数', title: '邮件发送次数',
children: [{ title: json.result1.reduce((a, b) => a + b.mailSendTime, 0), dataIndex: 'mailSendTime' }], children: [{ title: result1.reduce((a, b) => a + b.mailSendTime, 0), dataIndex: 'mailSendTime' }],
sorter: (a, b) => b.mailSendTime - a.mailSendTime, sorter: (a, b) => b.mailSendTime - a.mailSendTime,
}, },
{ {
title: 'WhatsApp客人会话次数', title: 'WhatsApp客人会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WhatsAppGuestChatCount, 0), dataIndex: 'WhatsAppGuestChatCount' }], children: [{ title: result1.reduce((a, b) => a + b.WhatsAppGuestChatCount, 0), dataIndex: 'WhatsAppGuestChatCount' }],
sorter: (a, b) => b.WhatsAppGuestChatCount - a.WhatsAppGuestChatCount, sorter: (a, b) => b.WhatsAppGuestChatCount - a.WhatsAppGuestChatCount,
}, },
{ {
title: 'WhatsApp外联会话次数', title: 'WhatsApp外联会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WhatsAppWLChatCount, 0), dataIndex: 'WhatsAppWLChatCount' }], children: [{ title: result1.reduce((a, b) => a + b.WhatsAppWLChatCount, 0), dataIndex: 'WhatsAppWLChatCount' }],
sorter: (a, b) => b.WhatsAppWLChatCount - a.WhatsAppWLChatCount, sorter: (a, b) => b.WhatsAppWLChatCount - a.WhatsAppWLChatCount,
}, },
{ {
title: 'WhatsApp新增客户数', title: 'WhatsApp新增客户数',
children: [{ title: json.result1.reduce((a, b) => a + b.WhatsAppNewGuestCount, 0), dataIndex: 'WhatsAppNewGuestCount' }], children: [{ title: result1.reduce((a, b) => a + b.WhatsAppNewGuestCount, 0), dataIndex: 'WhatsAppNewGuestCount' }],
sorter: (a, b) => b.WhatsAppNewGuestCount - a.WhatsAppNewGuestCount, sorter: (a, b) => b.WhatsAppNewGuestCount - a.WhatsAppNewGuestCount,
}, },
{ {
title: '微信客人会话次数', title: '微信客人会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WXGuestChatCount, 0), dataIndex: 'WXGuestChatCount' }], children: [{ title: result1.reduce((a, b) => a + b.WXGuestChatCount, 0), dataIndex: 'WXGuestChatCount' }],
sorter: (a, b) => b.WXGuestChatCount - a.WXGuestChatCount, sorter: (a, b) => b.WXGuestChatCount - a.WXGuestChatCount,
}, },
{ {
title: '微信外联会话次数', title: '微信外联会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WXWLChatCount, 0), dataIndex: 'WXWLChatCount' }], children: [{ title: result1.reduce((a, b) => a + b.WXWLChatCount, 0), dataIndex: 'WXWLChatCount' }],
sorter: (a, b) => b.WXWLChatCount - a.WXWLChatCount, sorter: (a, b) => b.WXWLChatCount - a.WXWLChatCount,
}, },
{ {
title: '微信新增客户数', title: '微信新增客户数',
children: [{ title: json.result1.reduce((a, b) => a + b.WXNewGuestCount, 0), dataIndex: 'WXNewGuestCount' }], children: [{ title: result1.reduce((a, b) => a + b.WXNewGuestCount, 0), dataIndex: 'WXNewGuestCount' }],
sorter: (a, b) => b.WXNewGuestCount - a.WXNewGuestCount, sorter: (a, b) => b.WXNewGuestCount - a.WXNewGuestCount,
}, },
]; ];
result.dataSource = json.result1; result.dataSource = result1
.sort(comm.sortBy('PriceTime'))
.reverse();
} else if (this.active_tab_key === 'ResponseRateWhatsApp') { } else if (this.active_tab_key === 'ResponseRateWhatsApp') {
result.columns = [ result.columns = [
{ {
@ -247,6 +292,10 @@ class SaleStore {
dataIndex: 'OPI_Name', dataIndex: 'OPI_Name',
}, },
], ],
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.OPI_SN === value,
filterSearch: true,
}, },
{ {
title: '首次回复率', title: '首次回复率',
@ -312,11 +361,13 @@ class SaleStore {
sorter: (a, b) => b.COLI_ConfirmTimeAVG - a.COLI_ConfirmTimeAVG, sorter: (a, b) => b.COLI_ConfirmTimeAVG - a.COLI_ConfirmTimeAVG,
}, },
]; ];
result.dataSource = json.result1; result.dataSource = result1
.sort(comm.sortBy('COLI_ConfirmTimeAVG'))
.reverse();
} else { } else {
const diffDateFlagYes = !comm.isEmpty(date_moment.start_date_cp); const diffDateFlagYes = !comm.isEmpty(date_moment.start_date_cp);
// if (this.active_tab_key == "Country") // if (this.active_tab_key == "Country")
const mergeDiffData = calcDiff({result1: json.result1, result2: json.result2}); const mergeDiffData = calcDiff({result1, result2});
// 获取类型的项目,去掉重复,作为列名 // 获取类型的项目,去掉重复,作为列名
const type_name_arr = []; const type_name_arr = [];
mergeDiffData.map((item) => { mergeDiffData.map((item) => {
@ -333,7 +384,7 @@ class SaleStore {
const total_data_value = items.length ? items.reduce((a, b) => a + b.COLI_YJLY, 0) : ''; // 记录累加 const total_data_value = items.length ? items.reduce((a, b) => a + b.COLI_YJLY, 0) : ''; // 记录累加
const total_data_value_diff = items.length ? items.reduce((a, b) => a + b.COLI_YJLY2, 0) : ''; // 记录累加 const total_data_value_diff = items.length ? items.reduce((a, b) => a + b.COLI_YJLY2, 0) : ''; // 记录累加
if (comm.empty(type_data[op_sn])) { if (comm.empty(type_data[op_sn])) {
type_data[op_sn] = [{ key: item.OPI_SN }, { T_name: item.OPI_Name }, { T_total: total_data_value }, { V_total: total_data_value_diff }]; type_data[op_sn] = [{ key: item.OPI_SN }, { T_name: item.OPI_Name }, { T_OPI: item.OPI_SN }, { T_total: total_data_value }, { V_total: total_data_value_diff }];
} }
const _diff = comm.objectMapper(comm.pick(item, ['COLI_YJLY_diff', 'COLI_YJLY_vs', 'COLI_YJLY2']), { const _diff = comm.objectMapper(comm.pick(item, ['COLI_YJLY_diff', 'COLI_YJLY_vs', 'COLI_YJLY2']), {
COLI_YJLY2: { key: `v_${item.SubTypeSN}` }, COLI_YJLY2: { key: `v_${item.SubTypeSN}` },
@ -365,7 +416,12 @@ class SaleStore {
totalDiff.diff = (totalDiff.val-totalDiff.diffVal); totalDiff.diff = (totalDiff.val-totalDiff.diffVal);
totalDiff.vs = comm.fixTo2Decimals(((totalDiff.val-totalDiff.diffVal)/totalDiff.diffVal)*100)+'%'; totalDiff.vs = comm.fixTo2Decimals(((totalDiff.val-totalDiff.diffVal)/totalDiff.diffVal)*100)+'%';
result.columns.push( result.columns.push(
{ title: '顾问', children: [{ title: '', dataIndex: 'T_name', render: (text, record) => <NavLink to={`/sale_sub/${this.active_tab_key}`}>{text}</NavLink> }] }, { title: '顾问', children: [{ title: '', dataIndex: 'T_name', render: (text, record) => <NavLink to={`/sale_sub/${this.active_tab_key}`}>{text}</NavLink> }],
sorter: (a, b) => (a?.T_name || '').localeCompare(b.T_name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.T_OPI === value,
filterSearch: true,
},
{ {
title: '合计', title: '合计',
children: [ children: [
@ -390,13 +446,17 @@ class SaleStore {
vs: comm.fixTo2Decimals(((total_data_value-total_data_value_diff)/total_data_value_diff)*100)+'%', vs: comm.fixTo2Decimals(((total_data_value-total_data_value_diff)/total_data_value_diff)*100)+'%',
}; };
result.columns.push({ result.columns.push({
title: item.SubTypeName, title: item.SubTypeName, _val: total_data_value,
children: [{ title: diffDateFlagYes ? comm.show_vs_tag(columnDiff.vs, columnDiff.diff, total_data_value, total_data_value_diff) : total_data_value, dataIndex: data_index }], children: [{ title: diffDateFlagYes ? comm.show_vs_tag(columnDiff.vs, columnDiff.diff, total_data_value, total_data_value_diff) : total_data_value, dataIndex: data_index }],
sorter: (a, b) => b[`TV_${item.SubTypeSN}`] - a[`TV_${item.SubTypeSN}`], sorter: (a, b) => b[`TV_${item.SubTypeSN}`] - a[`TV_${item.SubTypeSN}`],
}); });
return item; return item;
}); });
result.dataSource = type_data_arr; result.columns = [].concat(
comm.cloneDeep(result.columns).slice(0, 2),
comm.cloneDeep(result.columns).slice(2).sort(comm.sortBy('_val')).reverse()
);
result.dataSource = type_data_arr.sort(comm.sortBy('T_total')).reverse();
} }
} }
runInAction(() => { runInAction(() => {
@ -420,9 +480,9 @@ class SaleStore {
const date2_end = comm.empty(date_moment.end_date_cp) ? '' : date_moment.end_date_cp.format(config.DATE_FORMAT); const date2_end = comm.empty(date_moment.end_date_cp) ? '' : date_moment.end_date_cp.format(config.DATE_FORMAT);
let url = '/service-web/QueryData/GetDepartmentOrderMLByType_sub'; let url = '/service-web/QueryData/GetDepartmentOrderMLByType_sub';
url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&subType=${type_sub}&subTypeVal=-1&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`; url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&subType=${type_sub}&subTypeVal=-1&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:59`; url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:00`;
if (date2_start && date2_end) { if (date2_start && date2_end) {
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:59`; url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:00`;
this.date_title += ` ${date2_start}~${date2_end}`; this.date_title += ` ${date2_start}~${date2_end}`;
} }
fetch(config.HT_HOST + url) fetch(config.HT_HOST + url)
@ -436,6 +496,7 @@ class SaleStore {
{ {
title: '', title: '',
dataIndex: 'OPI_Name', dataIndex: 'OPI_Name',
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
}, },
{ {
title: '毛利', title: '毛利',
@ -496,36 +557,123 @@ class SaleStore {
}); });
} }
setTableDataSource = (merge = false) => {
if (comm.isEmpty(this.salesTrade[this.salesTrade.groupType]) || comm.isEmpty(this.salesTrade.operator)) {
return false;
}
this.salesTrade.tableDataSource = [].concat(this.salesTrade[this.salesTrade.groupType], merge === false ? this.salesTrade.operator : this.salesTrade.operatorAccount);
};
async fetchOperatorTradeData(groupType, queryData) { async fetchOperatorTradeData(groupType, queryData) {
this.salesTrade.loading = true; this.salesTrade.loading = true;
const param1 = Object.assign(queryData, {groupType, groupDateType: 'year' }); const param1 = Object.assign({}, queryData, {groupType, groupDateType: 'year' });
const yearData = await this.fetchTradeData(param1); const yearData = await this.fetchTradeDataAll(param1);
const yData = parseSaleData(yearData, ['groupsKey', 'groupDateType']);
const param2 = Object.assign(queryData, {groupType, groupDateType: 'month' }); const param2 = Object.assign({}, queryData, {groupType, groupDateType: 'month' });
const monthData = await this.fetchTradeData(param2); const monthData = await this.fetchTradeDataAll(param2);
const mData = parseSaleData(monthData, ['groupsKey', 'groupDateType']);
const { salesTradeDataMapped, accountMergeYearMonth } = await this.handleYearMonthData(groupType, queryData, yearData, monthData);
runInAction(() => {
this.salesTrade.loading = false;
this.salesTrade[groupType] = Object.values(salesTradeDataMapped).sort(comm.sortBy('yearML')).reverse();
this.salesTrade[`${groupType}Account`] = accountMergeYearMonth.sort(comm.sortBy('yearML')).reverse();
this.salesTrade[`${groupType}Mapped`] = Object.values(salesTradeDataMapped).reduce((r, v) => ({...r, [v.groupsKey]: Object.values(v.mData)}), {});
});
}
handleYearMonthData = async (groupType, queryData, yearData, monthData) => {
const { mergeRows: yearMergeRows, mergeLabelsRows: yearMergeLabelsRows } = yearData.result1;
const { mergeRows: monthMergeRows, mergeLabelsRows: monthMergeLabelsRows } = monthData.result1;
const yData = parseSaleData(yearMergeRows, ['groupsKey', 'groupDateType']);
const mData = parseSaleData(monthMergeRows, ['groupsKey', 'groupDateType']);
const mergeYearMonth = Object.keys(yData).map(ykey => ({ const mergeYearMonth = Object.keys(yData).map(ykey => ({
...yData[ykey], ...yData[ykey],
mData: mData[ykey].data, mData: mData[ykey].data,
yData: Object.values(yData[ykey].data)[0], yData: Object.values(yData[ykey].data)[0],
data: undefined, data: undefined,
yearML: Object.values(yData[ykey].data)[0]?.SumML || 0, // 整理排序用 yearML: Object.values(yData[ykey].data)[0]?.SumML || 0, // 整理排序用
})).sort(comm.sortBy('yearML')).reverse(); }));
runInAction(() => {
this.salesTrade.loading = false; const accountYData = parseSaleData(yearMergeLabelsRows, ['groupsKey', 'groupDateType']);
this.salesTrade[groupType] = mergeYearMonth; const accountMData = parseSaleData(monthMergeLabelsRows, ['groupsKey', 'groupDateType']);
const accountMergeYearMonth = Object.keys(accountYData).map(ykey => ({
...accountYData[ykey],
mData: accountMData[ykey].data,
yData: Object.values(accountYData[ykey].data)[0],
data: undefined,
yearML: Object.values(accountYData[ykey].data)[0]?.SumML || 0, // 整理排序用
}));
const accountSalesTradeDataMapped = accountMergeYearMonth.reduce((ro, vo) => ({...ro, [vo.groupsKey]: vo}), {});
const kpiObjects = mergeYearMonth.map(v => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel }));
const kpiData = await this.getKPISettings(groupType, queryData, kpiObjects);
const salesTradeDataMapped = mergeYearMonth.reduce((ro, vo) => ({...ro, [vo.groupsKey]: vo}), {});
kpiData.map(ele => {
ele.kpiData.map((km) => {
const padM = String(km.monthIndex).padStart(2, '0');
const _monthMLKPI = { [dataFieldAlias.SumML.nestkey.v]: km.value, 'groupsKey': ele.object_id, 'groupsLabel': ele.object_name, 'groupDateVal': `${km.yearIndex}-${padM}-01`, };
if (comm.isEmpty(salesTradeDataMapped[ele.object_id])) {
return true;
}
if (comm.isEmpty(salesTradeDataMapped[ele.object_id].mData[`month_${padM}`])) {
salesTradeDataMapped[ele.object_id].mData[`month_${padM}`] = {};
}
// set
Object.assign(salesTradeDataMapped[ele.object_id].mData[`month_${padM}`], _monthMLKPI);
return km;
});
return ele;
}); });
}
return { salesTradeDataMapped, accountSalesTradeDataMapped, accountMergeYearMonth };
};
async getKPISettings(curObject, queryData, objects) {
const getkpiParam = comm.objectMapper(queryData, {
DateType: { key: 'date_type' },
Date1: { key: 'start_date' },
Date2: { key: 'end_date' },
HTBusinessUnits: { key: 'object_id' },
DepartmentList: { key: 'object_id' },
});
Object.assign(getkpiParam, { object: curObject, subject: 'sum_profit' });
getkpiParam.object_id = curObject === 'overview' ? '' : objects.map((ele) => ele.key).join(',');
const json = await req.fetchJSON('/service-Analyse2/getkpi', getkpiParam);
if (json.errcode === 0) {
const yearData = parseKPI(json.result, ['subject', 'object_id']);
const year = moment(queryData.Date1).year();
return yearData?.[year]?.sum_profit || [];
}
return [];
};
/** /**
* 获取业绩数据 * 获取业绩数据
*/ */
async fetchTradeData(queryData) { async fetchTradeData(queryData) {
const json = await req.fetchJSON('/service-Analyse2/GetTradeProcess', queryData); const json = await req.fetchJSON('/service-Analyse2/GetTradeProcess', queryData);
if (json.errcode === 0) { return json;
return json.result1; }
}
return []; /**
* 获取业绩数据: 商务订单
*/
async fetchTradeDataBiz(queryData) {
const json = await req.fetchJSON('/service-Analyse2/GetTradeProcess_biz', queryData);
return json;
}
/**
* 合并传统和商务
*/
async fetchTradeDataAll(queryData) {
const traditional = await this.fetchTradeData(queryData);
const biz = await this.fetchTradeDataBiz(queryData);
const rr = parseMergeItem({ traditional, biz });
return rr;
} }
} }

@ -0,0 +1,210 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON } from '../utils/request';
import { isEmpty, sortDescBy, groupBy, pick, unique } from '../utils/commons';
import { groupsMappedByCode } from './../libs/ht';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment';
const fetchResultsData = async (param) => {
const defaultParam = {
WebCode: 'All',
DepartmentList: '',
opisn: -1,
Date1: '',
Date2: '',
groupType: '',
groupDateType: '',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_results', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result : [];
};
const fetchProcessData = async (param) => {
const defaultParam = {
WebCode: 'All',
DepartmentList: '',
opisn: -1,
Date1: '',
Date2: '',
groupType: '',
groupDateType: '',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_process', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result : [];
};
const fetchRiskDetailData = async (param) => {
const defaultParam = {
opisn: -1,
DateType: '',
WebCode: 'All',
DepartmentList: '',
Date1: '',
Date2: '',
IncludeTickets: '1',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_process_detail', {...defaultParam, ...param});
return json.errcode === 0 ? json.result : [];
};
class SalesCRMData {
constructor(appStore) {
this.appStore = appStore;
makeAutoObservable(this);
}
async get90n180Data(param = {}) {
const retProps = param?.retLabel || '';
let retKey = param.groupDateType === '' ? (param.groupType === 'overview' ? 'dataSource' : 'details') : 'byDate';
retKey = param.opisn ? `operator_${param.opisn}`: retKey;
if (param.opisn ) {
if (!isEmpty(this.results.details)) {
const _this_opi_row = this.results.details.filter(ele => ele.groupsKey === param.opisn);
this.results[retKey] = _this_opi_row;
return;
}
if (!isEmpty(this.results[retKey])) {
return;
}
}
this.results.loading = true;
const date90=this.searchValues.date90;
const date180=this.searchValues.date180;
const [result90, result180] = await Promise.all([
fetchResultsData({ ...this.searchValuesToSub, ...date90, ...param }),
fetchResultsData({ ...this.searchValuesToSub, ...date180, ...param }),
]);
const _90O = groupBy(result90, 'groupsKey');
const _180O = groupBy(result180, 'groupsKey');
const result2 = unique(Object.keys(_90O).concat(Object.keys(_180O))).map((key) => {
return {
...pick(_90O[key]?.[0] || _180O[key][0], ['groupsKey', 'groupsLabel', 'groupType']),
...(retProps && retKey === 'dataSource' ? { groupsLabel: retProps, retProps } : { retProps }),
key: `${param.groupType}-${key}`,
result90: _90O[key]?.[0] || {},
result180: _180O[key]?.[0] || {},
};
});
// console.log(result2, '+++++ +++', retKey);
// console.log(this.results[retKey]?.length);
runInAction(() => {
this.results.loading = false;
this.results[retKey] = [].concat((this.results[retKey] || []), result2);
});
return this.results;
}
async getResultData(param = {}) {
let retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
retKey = param.opisn ? `operator_byDate_${param.opisn}`: retKey;
if (!isEmpty(this.results[retKey])) {
return;
}
this.results.loading = true;
this.results[retKey] = [];
const res = await fetchResultsData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.results.loading = false;
this.results[retKey] = retKey === 'byOperator' ? res.filter(ele => ele.SumML > 0).sort(sortDescBy('SumML')) : res;
});
return this.results;
};
async getProcessData(param = {}) {
// const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
let retKey = param.groupDateType === '' ? (param.groupType !== 'operator' ? 'dataSource' : 'details') : 'byDate';
retKey = param.opisn ? `operator_${param.opisn}`: retKey;
if (param.opisn) {
if (!isEmpty(this.process.details)) {
const _this_opi_row = this.process.details.filter(ele => ele.groupsKey === param.opisn);
this.process[retKey] = _this_opi_row;
return;
}
if (!isEmpty(this.process[retKey])) {
return;
}
}
this.process.loading = true;
this.process[retKey] = [];
const res = await fetchProcessData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.process.loading = false;
this.process[retKey] = [].concat(this.process[retKey], res);
});
}
async getRiskDetailData(param = {}) {
this.risk.loading = true;
this.risk.dataSource = [];
const res = await fetchRiskDetailData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.risk.loading = false;
this.risk.dataSource = res;
this.risk.byLostType = groupBy(res, 'lost_type');
});
}
searchValues = {
date: moment(),
Date1: moment().startOf("week").subtract(7, "days"),
Date2: moment().endOf("week").subtract(7, "days"),
DateType: { key: 'applyDate', label: '提交日期'},
WebCode: { key: 'All', label: '所有来源' },
// IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: [groupsMappedByCode.GH],
operator: '-1',
opisn: '-1',
date90: {
Date1: moment().subtract(90, 'days').format(DATE_FORMAT),
Date2: moment().subtract(30, 'days').format(SMALL_DATETIME_FORMAT),
},
date180: {
Date1: moment().subtract(180, 'days').format(DATE_FORMAT),
Date2: moment().subtract(50, 'days').format(SMALL_DATETIME_FORMAT),
}
};
searchValuesToSub = {
date: moment().format(DATE_FORMAT),
Date1: moment().startOf("week").subtract(7, "days").format(DATE_FORMAT),
Date2: moment().endOf("week").subtract(7, "days").format(SMALL_DATETIME_FORMAT),
DateType: 'applyDate',
DepartmentList: groupsMappedByCode.GH.value,
WebCode: 'All',
operator: '-1',
opisn: '-1',
};
setSearchValues(obj, values) {
this.searchValues = { ...this.searchValues, ...values };
if (values.date) {
this.searchValues.date90 = {
Date1: (values.date.clone()).subtract(90, 'days').format(DATE_FORMAT),
Date2: (values.date.clone()).subtract(30, 'days').format(SMALL_DATETIME_FORMAT),
};
this.searchValues.date180 = {
Date1: (values.date.clone()).subtract(180, 'days').format(DATE_FORMAT),
Date2: (values.date.clone()).subtract(50, 'days').format(SMALL_DATETIME_FORMAT),
};
}
this.searchValuesToSub = {...this.searchValuesToSub, ...obj};
}
results = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
process = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
risk = { loading: false, dataSource: [], byLostType: {}, };
resetData = (rootKey = '') => {
if (rootKey === '') {
return false;
}
this[rootKey].loading = false;
for (const key of Object.keys(this[rootKey])) {
if (key !== 'loading') {
this[rootKey][key] = [];
}
}
};
}
export default SalesCRMData;

@ -1,7 +1,9 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx'; import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request'; import * as req from '../utils/request';
import { isEmpty, sortBy, pick, merge, fixTo2Decimals, groupBy, sortKeys, fixToInt } from '../utils/commons'; import moment from 'moment';
import { isEmpty, sortBy, pick, merge, fixTo2Decimals, groupBy, sortKeys, fixToInt, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from './../libs/ht'; import { dataFieldAlias } from './../libs/ht';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
class Trade { class Trade {
constructor(rootStore) { constructor(rootStore) {
@ -14,19 +16,27 @@ class Trade {
*/ */
async fetchSummaryData(queryData) { async fetchSummaryData(queryData) {
this.summaryData.loading = true; this.summaryData.loading = true;
queryData.groupType = queryData?.groupType || 'overview'; const curQueryData = cloneDeep(queryData);
queryData.groupDateType = 'year'; curQueryData.groupType = curQueryData?.groupType || 'overview';
const multiData = await this.fetchTradeDataAll(queryData); curQueryData.groupDateType = 'year';
if (isEmpty(curQueryData.DateDiff1)) {
curQueryData.DateDiff1 = moment(curQueryData.Date1).subtract(1, 'year').format(DATE_FORMAT);
curQueryData.DateDiff2 = moment(curQueryData.Date2).subtract(1, 'year').format(SMALL_DATETIME_FORMAT);
}
const multiData = await this.fetchTradeDataAll((curQueryData));
const { summary, traditional, biz } = multiData.result1; const { summary, traditional, biz } = multiData.result1;
// console.log(JSON.stringify(summary), 'mmmmmmmmmmm'); const { summary: summary2, traditional: traditional2, biz: biz2 } = multiData.result2;
// console.log(JSON.stringify(summary), 'mmmmmmmmmmm', multiData);
const summaryData = { const summaryData = {
loading: false, loading: false,
dataSource: [ dataSource: [
{ {
title: '成团', title: '成团',
col: 6,
value: summary?.[0]?.ConfirmOrder, value: summary?.[0]?.ConfirmOrder,
originVal: (summary?.[0]?.ConfirmOrder || 0), originVal: summary?.[0]?.ConfirmOrder || 0,
valueSuffix: summary?.[0]?.ConfirmRates ? ` / ${summary?.[0]?.ConfirmRates} %` : undefined, valueSuffix: undefined,
// valueSuffix: summary?.[0]?.ConfirmRates ? ` / ${summary?.[0]?.ConfirmRates} %` : undefined,
// VSrate: summary?.[0]?.ConfirmOrderrate, // VSrate: summary?.[0]?.ConfirmOrderrate,
KPIrate: summary?.[0]?.[dataFieldAlias.ConfirmOrder.nestkey.p], KPIrate: summary?.[0]?.[dataFieldAlias.ConfirmOrder.nestkey.p],
// hasKPI: !isEmpty(summary?.[0]?.[dataFieldAlias.ConfirmOrder.nestkey.p]), // hasKPI: !isEmpty(summary?.[0]?.[dataFieldAlias.ConfirmOrder.nestkey.p]),
@ -35,39 +45,83 @@ class Trade {
kpiVal: summary?.[0]?.[dataFieldAlias.ConfirmOrder.nestkey.v], kpiVal: summary?.[0]?.[dataFieldAlias.ConfirmOrder.nestkey.v],
traditional: { title: '传统', value: traditional?.[0]?.ConfirmOrder }, traditional: { title: '传统', value: traditional?.[0]?.ConfirmOrder },
biz: { title: '商务', value: biz?.[0]?.ConfirmOrder }, biz: { title: '商务', value: biz?.[0]?.ConfirmOrder },
...(summary2?.[0]
? {
diff: {
label: summary2[0]?.groupDateVal || '对比',
value: summary2[0]?.ConfirmOrder || 0,
VSrate: summary2[0]?.ConfirmOrder ? fixTo2Decimals(((summary[0]?.ConfirmOrder - summary2[0]?.ConfirmOrder) / summary2[0]?.ConfirmOrder) * 100) : null,
},
}
: {}),
}, },
{ {
title: '毛利', title: '毛利',
originVal: (summary?.[0]?.SumML || 0), col: 8,
value: dataFieldAlias.SumML.formatter(summary?.[0]?.SumML || 0) + '=' + dataFieldAlias.SumML.formatter((traditional?.[0]?.SumML || 0)) + '+' + dataFieldAlias.SumML.formatter((biz?.[0]?.SumML || 0)), originVal: summary?.[0]?.SumML || 0,
value:
dataFieldAlias.SumML.formatter(summary?.[0]?.SumML || 0) +
'=' +
dataFieldAlias.SumML.formatter(traditional?.[0]?.SumML || 0) +
'+' +
dataFieldAlias.SumML.formatter(biz?.[0]?.SumML || 0),
KPIrate: summary?.[0]?.[dataFieldAlias.SumML.nestkey.p], KPIrate: summary?.[0]?.[dataFieldAlias.SumML.nestkey.p],
hasKPI: false, hasKPI: false,
childrenVisible: true, childrenVisible: true,
kpiVal: summary?.[0]?.[dataFieldAlias.SumML.nestkey.v], kpiVal: summary?.[0]?.[dataFieldAlias.SumML.nestkey.v],
traditional: { title: '传统', value: (traditional?.[0]?.SumML || 0) }, traditional: { title: '传统', value: traditional?.[0]?.SumML || 0 },
biz: { title: '商务', value: (biz?.[0]?.SumML || 0) }, biz: { title: '商务', value: biz?.[0]?.SumML || 0 },
...(summary2?.[0]
? {
diff: {
label: summary2[0]?.groupDateVal || '对比',
value: dataFieldAlias.SumML.formatter(summary2[0]?.SumML || 0),
VSrate: summary2[0]?.SumML ? fixTo2Decimals(((summary?.[0]?.SumML - summary2[0]?.SumML) / summary2[0]?.SumML) * 100) : null,
},
}
: {}),
}, },
{ {
title: '完成率', title: '完成率',
originVal: (summary?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0), col: 5,
originVal: summary?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0,
value: `${summary?.[0]?.[dataFieldAlias.SumML.nestkey.p] || ''}%`, value: `${summary?.[0]?.[dataFieldAlias.SumML.nestkey.p] || ''}%`,
hasKPI: false, hasKPI: false,
childrenVisible: false, childrenVisible: false,
kpiVal: 0 , // summary?.[0]?.[dataFieldAlias.SumML.nestkey.p], kpiVal: 0, // summary?.[0]?.[dataFieldAlias.SumML.nestkey.p],
traditional: { title: '传统', value: traditional?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0, }, traditional: { title: '传统', value: traditional?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0 },
biz: { title: '商务', value: biz?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0, }, biz: { title: '商务', value: biz?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0 },
...(summary2?.[0]
? {
diff: {
label: summary2[0]?.groupDateVal || '对比',
value: `${summary2[0]?.[dataFieldAlias.SumML.nestkey.p] || '-'}%`,
VSrate: null,
},
}
: {}),
}, },
{ {
title: '人数', title: '人数',
originVal: (summary?.[0]?.SumPersonNum || 0), col: 5,
originVal: summary?.[0]?.SumPersonNum || 0,
value: summary?.[0]?.SumPersonNum, value: summary?.[0]?.SumPersonNum,
// VSrate: summary?.[0]?.SumPersonNumrate, // VSrate: summary?.[0]?.SumPersonNumrate,
// KPIrate: summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.p], // KPIrate: summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.p],
hasKPI: false, // // !isEmpty(summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.p]),, hasKPI: false, // // !isEmpty(summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.p]),,
childrenVisible: true, childrenVisible: true,
// kpiVal: summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.v], // kpiVal: summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.v],
traditional: { title: '传统', value: traditional?.[0]?.SumPersonNum, }, traditional: { title: '传统', value: traditional?.[0]?.SumPersonNum },
biz: { title: '商务', value: biz?.[0]?.SumPersonNum, }, biz: { title: '商务', value: biz?.[0]?.SumPersonNum },
...(summary2?.[0]
? {
diff: {
label: summary2[0]?.groupDateVal || '对比',
value: summary2[0]?.SumPersonNum || '',
VSrate: summary2[0]?.SumPersonNum ? fixTo2Decimals(((summary?.[0]?.SumPersonNum - summary2[0]?.SumPersonNum) / summary2[0]?.SumPersonNum) * 100) : null,
},
}
: {}),
}, },
], ],
}; };
@ -86,16 +140,60 @@ class Trade {
queryData = Object.assign({}, this.searchPayloadHome, queryData); // queryData || this.searchPayloadHome; queryData = Object.assign({}, this.searchPayloadHome, queryData); // queryData || this.searchPayloadHome;
queryData.groupType = queryData?.groupType || 'overview'; queryData.groupType = queryData?.groupType || 'overview';
Object.assign(queryData, { groupDateType: this.timeLineKey }); Object.assign(queryData, { groupDateType: this.timeLineKey });
const multiData = await this.fetchTradeDataAll(queryData); const multiData = await this.fetchTradeDataAll(cloneDeep(queryData));
const { traditional, biz } = multiData.result1; const { traditional, biz, summaryRows: summaryRows1, } = multiData.result1;
// const { summaryRows: summaryRows2, mergeRows: mergeRows2 } = multiData.result2;
// console.log(biz, 'mmmmmmmm', queryData, multiData); // console.log(biz, 'mmmmmmmm', queryData, multiData);
const mergeData = [].concat(traditional, biz); const mergeData = [].concat(traditional, biz);
const dateKeyData = groupBy(mergeData, ele => ele.groupDateVal); const dateKeyData = groupBy(mergeData, ele => ele.groupDateVal);
const sortByDateKey = Object.values(sortKeys(dateKeyData)).reduce( (a, b) => a.concat(b), []); const sortByDateKey = Object.values(sortKeys(dateKeyData)).reduce( (a, b) => a.concat(b), []);
runInAction(() => { runInAction(() => {
this.timeData.loading = false; this.timeData.loading = false;
this.timeData.dataSource = sortByDateKey; this.timeData.dataSource = sortByDateKey;
this.timeData.origin = multiData.result1; this.timeData.origin = { summaryRows: summaryRows1 || [] }; // multiData.result1;
});
}
/**
* 有对比的时间轴
*/
async fetchTradeDataDiffByDate(queryData = {}) {
this.timeDiffData.loading = true;
queryData = Object.assign({}, this.searchPayloadHome, queryData); // queryData || this.searchPayloadHome;
queryData.groupType = queryData?.groupType || 'overview';
Object.assign(queryData, { groupDateType: this.timeLineKey });
const multiData = await this.fetchTradeDataAll(cloneDeep(queryData));
const { mergeRows: mergeRows1 } = multiData.result1;
const { mergeRows: mergeRows2 } = multiData.result2;
// console.log(biz, 'mmmmmmmm', queryData, multiData);
// 为了图表的X轴一致
const allDateKey1 = [...new Set(mergeRows1.reduce((rv, vk) => {
rv.push(vk.groupDateVal);
return rv;
}, []))].sort();
const allDateKey2 = [...new Set(mergeRows2.reduce((rv, vk) => {
rv.push(vk.groupDateVal);
return rv;
}, []))].sort();
const allLabelDateKeyMapped = {
...allDateKey2.reduce((obj, k, i) => ({...obj, [k]: allDateKey1[i] || `_${k}`}), {})
};
const mergeKeyDateRows = [].concat(
mergeRows1 || [],
(mergeRows2 || []).map((row, ri) => {
return {
...row,
groupsLabel: `${row.groupsLabel} @${moment(queryData.DateDiff1).year()}`,
groupDateVal: allLabelDateKeyMapped[row.groupDateVal],
rawGroupDateVal: row.groupDateVal,
};
})
).sort(sortBy('groupDateVal'));
runInAction(() => {
this.timeDiffData.loading = false;
this.timeDiffData.dataSource = mergeKeyDateRows;
}); });
} }
@ -170,17 +268,17 @@ class Trade {
/** /**
* TOP * TOP
*/ */
fetchTradeDataByType(orderType, queryData) { async fetchTradeDataByType(orderType, queryData) {
this.topData[orderType] = { loading: true, dataSource: [], originData: [] }; this.topData[orderType] = { loading: true, dataSource: [], originData: [] };
Object.assign(queryData, { groupType: orderType, groupDateType: 'year' }); Object.assign(queryData, { groupType: orderType, groupDateType: 'year' });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) { const multiData = await this.fetchTradeDataAll(cloneDeep(queryData));
runInAction(() => { const { summary } = multiData.result1;
this.topData[orderType].loading = false; // console.log(orderType, summary, 'mmmmmmmm', queryData, multiData);
this.topData[orderType].dataSource = json.result1; runInAction(() => {
this.getTargetsRes(orderType); this.topData[orderType].loading = false;
}); this.topData[orderType].dataSource = summary;
} this.getTargetsRes(orderType);
}); });
} }
@ -212,48 +310,7 @@ class Trade {
async fetchTradeDataAll(queryData) { async fetchTradeDataAll(queryData) {
const traditional = await this.fetchTradeData(queryData); const traditional = await this.fetchTradeData(queryData);
const biz = await this.fetchTradeDataBiz(queryData); const biz = await this.fetchTradeDataBiz(queryData);
const rr = ['result1', 'result2'].reduce((res, resKey) => { const rr = parseMergeItem({ traditional, biz });
const mergeItem = {
traditional: traditional[resKey].map(ele => ({...ele, groupsLabel: `传统 `})), // ${ele.groupsLabel}
biz: biz[resKey].map(ele => ({...ele, groupsLabel: `商务 `})), // ${ele.groupsLabel}
};
const mergeRes = [].concat(traditional[resKey], biz[resKey]);
const groupsData = mergeRes.reduce((r, v) => {
if (v.groupsLabel ) {
(r[v.groupsLabel] || (r[v.groupsLabel] = [])).push(v);
}
return r;
}, {});
const ByDate = sortKeys(groupBy(mergeRes, ele => ele.groupDateVal));
const summaryRows = Object.keys(ByDate).map(dateVal => {
const sumFields = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r, [skey]: ByDate[dateVal].reduce((a, c) => a + c[skey], 0) }),
ByDate[dateVal]?.[0] || {}
);
const pickFields = pick(ByDate[dateVal], Object.keys(ByDate[dateVal]).filter(_k => _k.endsWith('KPIvalue') ));
return {...sumFields, ...pickFields};
}, {}).map(row => ({...row,
ConfirmRates: row.SumOrder ? fixTo2Decimals((row.ConfirmOrder/row.SumOrder)*100) : 0,
MLKPIrates: row.MLKPIvalue ? fixTo2Decimals((row.SumML/row.MLKPIvalue)*100) : 0,
OrderValue: row.SumOrder ? fixToInt((row.SumML/row.SumOrder)) : 0,
groupsLabel: ``, // ${row.groupsLabel}
ConfirmOrderKPIrates: row.ConfirmOrderKPIvalue ? fixTo2Decimals((row.ConfirmOrder/row.ConfirmOrderKPIvalue)*100) : 0,
OrderKPIrates: row.OrderKPIvalue ? fixTo2Decimals((row.SumOrder/row.OrderKPIvalue)*100) : 0,
// ConfirmRatesKPIrates: 0, // todo:
}));
const summary = Object.keys(groupsData).map(groupsKey => {
return ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r, [skey]: groupsData[groupsKey].reduce((a, c) => a + c[skey], 0) }),
groupsData[groupsKey]?.[0] || {}
);
}).map(row => ({...row,
ConfirmRates: row.SumOrder ? fixTo2Decimals((row.ConfirmOrder/row.SumOrder)*100) : 0,
MLKPIrates: row.MLKPIvalue ? fixTo2Decimals((row.SumML/row.MLKPIvalue)*100) : 0,
OrderValue: row.SumOrder ? fixToInt((row.SumML/row.SumOrder)) : 0,
groupsLabel: ``, // ${row.groupsLabel}
}));
return Object.assign(res, { [resKey]: Object.assign({}, mergeItem, { summary, summaryRows }) });
}, {});
return rr; return rr;
} }
@ -281,18 +338,26 @@ class Trade {
this.targetTableProps.dataSource = [].concat(Object.values(finalTargetData.targetGuest), Object.values(finalTargetData.targetCountry)); // [finalTargetData.targetTotal], // todo: 总数是重复的 this.targetTableProps.dataSource = [].concat(Object.values(finalTargetData.targetGuest), Object.values(finalTargetData.targetCountry)); // [finalTargetData.targetTotal], // todo: 总数是重复的
}; };
setStateSearch(body) { searchValues = {};
setSearch(body, form) {
this.searchPayloadHome = body; this.searchPayloadHome = body;
this.searchValues = form;
} }
timeLineKey = 'week'; timeLineKey = 'month';
setTimeLineKey(v) { setTimeLineKey(v) {
this.timeLineKey = v; this.timeLineKey = v;
} }
groupKey = 'overview';
setGroupKey(v) {
this.groupKey = v;
}
resetData = () => { resetData = () => {
this.summaryData = { loading: false, dataSource: [], kpi: {}, }; this.summaryData = { loading: false, dataSource: [], kpi: {}, };
this.timeData = { loading: false, dataSource: [], origin: {} }; this.timeData = { loading: false, dataSource: [], origin: {}, diff: {} };
this.timeDiffData = { loading: false, dataSource: [], origin: {}, };
this.BuData = { loading: false, dataSource: [] }; this.BuData = { loading: false, dataSource: [] };
this.sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] }; this.sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
this.topData = {}; this.topData = {};
@ -302,7 +367,8 @@ class Trade {
searchPayloadHome = {}; searchPayloadHome = {};
summaryData = { loading: false, dataSource: [], kpi: {}, }; summaryData = { loading: false, dataSource: [], kpi: {}, };
timeData = { loading: false, dataSource: [], origin: {} }; timeData = { loading: false, dataSource: [], origin: {}, diff: {} };
timeDiffData = { loading: false, dataSource: [], origin: {}, };
BuData = { loading: false, dataSource: [] }; BuData = { loading: false, dataSource: [] };
sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] }; sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
topData = {}; topData = {};
@ -313,7 +379,111 @@ class Trade {
{key: 'ConfirmOrder', title: '成行数', dataIndex: 'ConfirmOrder'}, {key: 'ConfirmOrder', title: '成行数', dataIndex: 'ConfirmOrder'},
{key: 'ConfirmRates', title: '成行率', dataIndex: 'ConfirmRates'}, {key: 'ConfirmRates', title: '成行率', dataIndex: 'ConfirmRates'},
{key: 'SumML', title: '毛利', dataIndex: 'SumML', render: (v) => dataFieldAlias.SumML.formatter(v)}, {key: 'SumML', title: '毛利', dataIndex: 'SumML', render: (v) => dataFieldAlias.SumML.formatter(v)},
{key: 'transactions', title: '营收', dataIndex: 'transactions', render: (v) => dataFieldAlias.transactions.formatter(v)},
], dataSource: [] }; ], dataSource: [] };
} }
export const parseMergeItem = ({traditional, biz}) => {
return ['result1', 'result2'].reduce((res, resKey) => {
const mergeItem = {
traditional: traditional[resKey].map(ele => ({...ele, groupsLabel: `传统 `})), // ${ele.groupsLabel}
biz: biz[resKey].map(ele => ({...ele, groupsLabel: `商务 `})), // ${ele.groupsLabel}
};
const mergeRes = [].concat(traditional[resKey], biz[resKey]);
const groupsData = mergeRes.reduce((r, v) => {
if (v.groupsKey ) {
const groupsKeyLower = v.groupsKey.toLowerCase();
(r[groupsKeyLower] || (r[groupsKeyLower] = [])).push(v);
}
return r;
}, {});
const ByDate = sortKeys(groupBy(mergeRes, ele => ele.groupDateVal));
// 按日期汇总
const summaryRows = Object.keys(ByDate).map(dateVal => {
const sumFields = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r, [skey]: ByDate[dateVal].reduce((a, c) => a + c[skey], 0) }),
ByDate[dateVal]?.[0] || {}
);
const pickFields = pick(ByDate[dateVal], Object.keys(ByDate[dateVal]).filter(_k => _k.endsWith('KPIvalue') ));
return {...sumFields, ...pickFields};
}, {}).map(row => ({...row,
ConfirmRates: row.SumOrder ? fixTo2Decimals((row.ConfirmOrder/row.SumOrder)*100) : 0,
MLKPIrates: row.MLKPIvalue ? fixTo2Decimals((row.SumML/row.MLKPIvalue)*100) : 0,
OrderValue: row.SumOrder ? fixToInt((row.SumML/row.SumOrder)) : 0,
// groupsLabel: `总 `, // ${row.groupsLabel}
ConfirmOrderKPIrates: row.ConfirmOrderKPIvalue ? fixTo2Decimals((row.ConfirmOrder/row.ConfirmOrderKPIvalue)*100) : 0,
OrderKPIrates: row.OrderKPIvalue ? fixTo2Decimals((row.SumOrder/row.OrderKPIvalue)*100) : 0,
// ConfirmRatesKPIrates: 0, // todo:
}));
// 按对象汇总
const TMapped = traditional[resKey].reduce((r, v) => ({...r, [(v.groupsKey.toLowerCase())]: v}), {});
const BMapped = biz[resKey].reduce((r, v) => ({...r, [(v.groupsKey.toLowerCase())]: v}), {});
const summary = Object.keys(groupsData).map(groupsKey => {
const _groupsKey = groupsKey; // groupsData[groupsKey]?.[0]?.groupsKey || '';
return ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r,
[skey]: groupsData[groupsKey].reduce((a, c) => a + c[skey], 0),
// [`${skey}_arr`]: groupsData[groupsKey].map(sv => sv[skey]),
[`${skey}_arr`]: [ _groupsKey ? TMapped?.[_groupsKey]?.[skey] || 0 : 0, _groupsKey ? BMapped?.[_groupsKey]?.[skey] || 0 : 0,],
}),
groupsData[groupsKey]?.[0] || {}
);
}).map(row => ({...row,
ConfirmRates: row.SumOrder ? fixTo2Decimals((row.ConfirmOrder/row.SumOrder)*100) : 0,
MLKPIrates: row.MLKPIvalue ? fixTo2Decimals((row.SumML/row.MLKPIvalue)*100) : 0,
OrderValue: row.SumOrder ? fixToInt((row.SumML/row.SumOrder)) : 0,
// groupsLabel: `总 `, // ${row.groupsLabel}
}));
// 按每一行
// const ByDate = sortKeys(groupBy(mergeRes, ele => ele.groupDateVal));
const allRowsKeysData = groupBy(mergeRes, ele => `${ele.groupsKey.toLowerCase()}@${ele.groupDateVal}`);
const mergeRows = Object.keys(allRowsKeysData).map(rkey => {
const _groupsKey = (allRowsKeysData[rkey]?.[0]?.groupsKey || '').toLowerCase();
const sumFields = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r,
[skey]: allRowsKeysData[rkey].reduce((a, c) => a + c[skey], 0),
[`${skey}_arr`]: [ _groupsKey ? TMapped?.[_groupsKey]?.[skey] || 0 : 0, _groupsKey ? BMapped?.[_groupsKey]?.[skey] || 0 : 0,],
}),
allRowsKeysData[rkey]?.[0] || {}
);
const pickFields = pick(allRowsKeysData[rkey], Object.keys(allRowsKeysData[rkey]).filter(_k => _k.endsWith('KPIvalue') ));
return {...sumFields, ...pickFields};
}, {}).map(row => ({...row,
ConfirmRates: row.SumOrder ? fixTo2Decimals((row.ConfirmOrder/row.SumOrder)*100) : 0,
MLKPIrates: row.MLKPIvalue ? fixTo2Decimals((row.SumML/row.MLKPIvalue)*100) : 0,
OrderValue: row.SumOrder ? fixToInt((row.SumML/row.SumOrder)) : 0,
// groupsLabel: `总 `, // ${row.groupsLabel}
ConfirmOrderKPIrates: row.ConfirmOrderKPIvalue ? fixTo2Decimals((row.ConfirmOrder/row.ConfirmOrderKPIvalue)*100) : 0,
OrderKPIrates: row.OrderKPIvalue ? fixTo2Decimals((row.SumOrder/row.OrderKPIvalue)*100) : 0,
// ConfirmRatesKPIrates: 0, // todo:
}));
// 合并账户
const allRowsLabelsData = groupBy(mergeRes, ele => `${ele.groupsLabel.replace(/\([^)]*\)/gi, '').toLowerCase()}@${ele.groupDateVal}`);
const mergeLabelsRows = Object.keys(allRowsLabelsData).map(rkey => {
const _groupsLabel = rkey.split('@')[0].toLowerCase();
const _groupsKey = (allRowsLabelsData[rkey]?.[0]?.groupsKey || '').toLowerCase();
const sumFields = [
'ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum',
'ConfirmOrderKPIvalue', 'OrderKPIvalue', 'MLKPIvalue',
].reduce(
(r, skey) => ({ ...r,
[skey]: allRowsLabelsData[rkey].reduce((a, c) => a + c[skey], 0),
// [`${skey}_arr`]: [ _groupsKey ? TMapped?.[_groupsKey]?.[skey] || 0 : 0, _groupsKey ? BMapped?.[_groupsKey]?.[skey] || 0 : 0,],
}),
allRowsLabelsData[rkey]?.[0] || {}
);
const pickFields = pick(allRowsLabelsData[rkey], Object.keys(allRowsLabelsData[rkey]).filter(_k => _k.endsWith('KPIvalue') ));
return {...pickFields, ...sumFields, groupsLabel: _groupsLabel, groupsKey: _groupsLabel };
}, {}).map(row => ({...row,
ConfirmRates: row.SumOrder ? fixTo2Decimals((row.ConfirmOrder/row.SumOrder)*100) : 0,
MLKPIrates: row.MLKPIvalue ? fixTo2Decimals((row.SumML/row.MLKPIvalue)*100) : 0,
OrderValue: row.SumOrder ? fixToInt((row.SumML/row.SumOrder)) : 0,
ConfirmOrderKPIrates: row.ConfirmOrderKPIvalue ? fixTo2Decimals((row.ConfirmOrder/row.ConfirmOrderKPIvalue)*100) : 0,
OrderKPIrates: row.OrderKPIvalue ? fixTo2Decimals((row.SumOrder/row.OrderKPIvalue)*100) : 0,
// ConfirmRatesKPIrates: 0, // todo:
}));
return Object.assign(res, { [resKey]: Object.assign({}, mergeItem, { mergeRows, summary, summaryRows, mergeLabelsRows }) });
}, {});
};
export default Trade; export default Trade;

@ -20,6 +20,19 @@ if (!String.prototype.padStart) {
}; };
} }
if (!Object.fromEntries) {
Object.fromEntries = function(entries) {
const obj = {};
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
if (entry && entry.length === 2) {
obj[entry[0]] = entry[1];
}
}
return obj;
};
}
export function copy(obj) { export function copy(obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
@ -59,6 +72,14 @@ export function formatPercent(number) {
return Math.round(number * 100) + "%"; return Math.round(number * 100) + "%";
} }
export function formatPercentToFloat(number) {
return parseFloat((number * 100).toFixed(2)) + "%";
}
export function percentToDecimal(number) {
return parseFloat(number) / 100;
}
export function formatDate(date) { export function formatDate(date) {
if (isEmpty(date)) { if (isEmpty(date)) {
return "NaN"; return "NaN";
@ -243,7 +264,7 @@ export function unique(arr) {
const x = new Set(arr); const x = new Set(arr);
return [...x]; return [...x];
} }
export const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findIndex((step) => fn(element, step)) === index);
export function getWeek(date) { export function getWeek(date) {
// 参数时间戳 // 参数时间戳
const week = moment(date).day(); const week = moment(date).day();
@ -277,17 +298,36 @@ export function set_array_index(result) {
} }
/** /**
* 排序 * 数组排序
*/ */
export const sortBy = (key) => { export const sortBy = (key) => {
return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0); return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0);
}; };
export const sortDescBy = (key) => {
return (a, b) => (a[key] < b[key]) ? 1 : ((b[key] < a[key]) ? -1 : 0);
};
/**
* Object排序keys
*/
export const sortKeys = (obj) => export const sortKeys = (obj) =>
Object.keys(obj) Object.keys(obj)
.sort() .sort()
.reduce((a, k2) => ({...a, [k2]: obj[k2]}), {}); .reduce((a, k2) => ({...a, [k2]: obj[k2]}), {});
/**
* 数组排序, 给定排序数组
* @param {array} items 需要排序的数组
* @param {array} keyName 排序的key
* @param {array} keyOrder 给定排序
* @returns
*/
export const sortArrayByOrder = (items, keyName, keyOrder) => {
return items.sort((a, b) => {
return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]);
});
};
/** /**
* 合并Object, 递归地 * 合并Object, 递归地
*/ */
@ -328,7 +368,7 @@ export function merge(...objects) {
*/ */
export function groupBy(array, callback) { export function groupBy(array, callback) {
return array.reduce((groups, item) => { return array.reduce((groups, item) => {
const key = callback(item); const key = typeof callback === 'function' ? callback(item) : item[callback];
if (!groups[key]) { if (!groups[key]) {
groups[key] = []; groups[key] = [];
@ -353,6 +393,20 @@ export function pick(object, keys) {
}, {}); }, {});
} }
/**
* 返回对象的副本经过筛选以省略指定的键
* @param {*} object
* @param {string[]} keysToOmit
* @returns
*/
export function omit(object, keysToOmit) {
return Object.fromEntries(
Object.entries(object).filter(
([key]) => !keysToOmit.includes(key)
)
);
}
/** /**
* 深拷贝 * 深拷贝
*/ */
@ -439,6 +493,7 @@ export function objectMapper(input, keyMap) {
if (map) { if (map) {
let value = input[key]; let value = input[key];
if (map.transform) value = map.transform(value); if (map.transform) value = map.transform(value);
if (typeof map === 'string') mappedObj[map] = value;
mappedObj[map.key || key] = value; mappedObj[map.key || key] = value;
} }
} }
@ -513,3 +568,30 @@ export function flush(collection) {
export const numberFormatter = (number) => { export const numberFormatter = (number) => {
return new Intl.NumberFormat().format(number); return new Intl.NumberFormat().format(number);
}; };
/**
* @example
* const obj = { a: { b: 'c' } };
* const keyArr = ['a', 'b'];
* getNestedValue(obj, keyArr); // Returns: 'c'
*/
export const getNestedValue = (obj, keyArr) => {
return keyArr.reduce((acc, curr) => {
return acc && acc.hasOwnProperty(curr) ? acc[curr] : undefined;
// return acc && acc[curr];
}, obj);
};
/**
* 计算笛卡尔积
*/
export const cartesianProductArray = (arr, sep = '_', index = 0, prefix = '') => {
let result = [];
if(index === arr.length){
return [prefix];
}
arr[index].forEach(item => {
result = result.concat(cartesianProductArray(arr, sep, index+1, prefix ? `${prefix}${sep}${item}` : `${item}`));
});
return result;
};

@ -1,142 +1,56 @@
import React, { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { Row, Col, Typography, Space, DatePicker, Button, Select, Table, Divider } from 'antd'; import { Row, Col, Typography, Space, Table, Divider } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import * as config from '../config';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn';
import moment from 'moment';
import zhCNlocale from 'antd/es/date-picker/locale/zh_CN';
import { utils, writeFileXLSX } from 'xlsx'; import { utils, writeFileXLSX } from 'xlsx';
import GroupSelect from './../components/search/GroupSelect'; import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const AgentGroupCount = () => { const AgentGroupCount = () => {
const { customerServicesStore } = useContext(stores_Context); const { customerServicesStore, date_picker_store } = useContext(stores_Context);
const agentList = customerServicesStore.agentList;
const agentGroupList = customerServicesStore.agentGroupList; const agentGroupList = customerServicesStore.agentGroupList;
const agentGroupListColumns = customerServicesStore.agentGroupListColumns; const agentGroupListColumns = customerServicesStore.agentGroupListColumns;
const { startDate, endDate, dateType, inProgress } = customerServicesStore; const { inProgress } = customerServicesStore;
useEffect(() => { useEffect(() => {
customerServicesStore.fetchAllAgent(); customerServicesStore.fetchAllAgent();
}, []); }, []);
const handleSearchClick = () => {
customerServicesStore.fetchAgentGroupCount();
};
const renderAgentItem = (agent) => {
return (
<Select.Option key={agent.CAV_VEI_SN} value={agent.CAV_VEI_SN}>
{agent.VEI2_CompanyBN}
</Select.Option>
);
};
return ( return (
<> <>
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{ sm: 16, lg: 32 }} justify="end"> <Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col md={24} lg={8} xxl={8}> <Col className="gutter-row" span={24}>
<Select <SearchForm
value={customerServicesStore.selectedAgent} defaultValue={{
style={{ width: '95%' }} initialValue: {
showSearch ...date_picker_store.formValues,
onSearch={(value) => { ...customerServicesStore.searchValues,
console.log('search:', value); },
}} shows: ['agency', 'departureDateType', 'DepartmentList', 'countryArea', 'dates'],
filterOption={(input, option) => { fieldProps: {
return option.children.indexOf(input) > -1; DepartmentList: { show_all: true, mode: 'multiple' },
}} WebCode: { show_all: false, mode: 'multiple' },
onChange={(value) => customerServicesStore.selectAgent(value)} years: { hide_vs: true },
> departureDateType: { disabledKeys: ['applyDate'] },
<Select.Option key="0" value=""> },
所有地接社
</Select.Option>
{agentList.map(renderAgentItem)}
</Select>
</Col>
<Col md={24} lg={8} xxl={8}>
<GroupSelect value={customerServicesStore.selectedTeam}
onChange={(value) => customerServicesStore.selectTeam(value)}
style={{ width: '95%' }} show_all={true}
/>
</Col>
<Col md={24} lg={8} xxl={8}>
<Select
value={customerServicesStore.selectedCountry}
style={{ width: '95%' }}
onChange={(value) => customerServicesStore.selectCountry(value)}
>
<Select.Option key="ALL" value="">
所有国家
</Select.Option>
<Select.Option key="china" value="china">
国内
</Select.Option>
<Select.Option key="foreign" value="foreign">
国外
</Select.Option>
</Select>
</Col>
<Col md={24} lg={8} xxl={8}>
<Select
value={dateType}
style={{ width: '95%' }}
onChange={(value) => customerServicesStore.selectDateType(value)}
>
<Select.Option key="startDate" value="startDate">
走团日期
</Select.Option>
<Select.Option key="ConfirmDate" value="ConfirmDate">
成团日期
</Select.Option>
</Select>
</Col>
<Col md={24} lg={8} xxl={8}>
<DatePicker.RangePicker
format={config.DATE_FORMAT}
locale={zhCNlocale}
allowClear={false}
value={[startDate, endDate]}
onChange={(dates) => {
customerServicesStore.selectDateRange(dates[0], dates[1]);
}} }}
ranges={{ onSubmit={(_err, obj, form) => {
'本周': [moment().startOf('week'), moment().endOf('week')], customerServicesStore.setSearchValues(obj, form);
'上周': [moment().startOf('week').subtract(7, 'days'), moment().endOf('week').subtract(7, 'days')], customerServicesStore.fetchAgentGroupCount();
'本月': [moment().startOf('month'), moment().endOf('month')],
'上个月': [
moment().subtract(1, 'months').startOf('month'),
moment(new Date()).subtract(1, 'months').endOf('month'),
],
'近30天': [moment().subtract(30, 'days'), moment()],
'近三个月': [moment().subtract(2, 'month').startOf('month'), moment().endOf('month')],
'今年': [moment().startOf('year').subtract(1, 'month'), moment().endOf('year').subtract(1, 'month')],
'去年': [
moment().subtract(1, 'year').startOf('year').subtract(1, 'month'),
moment().subtract(1, 'year').endOf('year').subtract(1, 'month'),
],
}} }}
/> />
</Col> </Col>
<Col md={24} lg={8} xxl={8}>
<Button
type="primary"
icon={<SearchOutlined />}
loading={inProgress}
onClick={() => {
handleSearchClick();
}}
>
统计
</Button>
</Col>
</Row> </Row>
<Row> <Row>
<Col span={24}> <Col span={24}>
<Typography.Title level={3}>地接社团信息</Typography.Title> <Typography.Title level={3}>地接社团信息</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={'地接社团信息'} {...{ columns: agentGroupListColumns, dataSource: agentGroupList }} />
</Divider>
<Table <Table
sticky
id="agentGroupList" id="agentGroupList"
dataSource={agentGroupList} dataSource={agentGroupList}
columns={agentGroupListColumns} columns={agentGroupListColumns}
@ -144,20 +58,8 @@ const AgentGroupCount = () => {
rowKey={(record) => record.key} rowKey={(record) => record.key}
loading={inProgress} loading={inProgress}
pagination={false} pagination={false}
scroll={{ x: '100%' }} scroll={{ x: 1000 }}
/> />
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(
document.getElementById('agentGroupList').getElementsByTagName('table')[0]
);
writeFileXLSX(wb, '地接社团信息.xlsx');
}}
>
导出excel
</a>
</Divider>
</Col> </Col>
</Row> </Row>
</Space> </Space>

@ -1,111 +1,72 @@
import React, {useContext, useEffect} from 'react'; import { useContext, useEffect } from 'react';
import {Row, Col, Typography, Space, DatePicker, Button, Select, Table, List} from 'antd'; import { Row, Col, Typography, Space, Table, List } from 'antd';
import { import { stores_Context } from '../config';
SearchOutlined, import { observer } from 'mobx-react';
} from '@ant-design/icons'; import { NavLink, useParams } from 'react-router-dom';
import {stores_Context} from '../config';
import * as config from "../config";
import {observer} from 'mobx-react';
import { NavLink, useParams } from "react-router-dom";
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn';
import moment from "moment"; import SearchForm from './../components/search/SearchForm';
import zhCNlocale from 'antd/es/date-picker/locale/zh_CN';
import GroupSelect from './../components/search/GroupSelect';
const AgentGroupList = () => { const AgentGroupList = () => {
const { agentId } = useParams();
const {agentId} = useParams(); const { customerServicesStore, date_picker_store } = useContext(stores_Context);
const { customerServicesStore } = useContext(stores_Context);
useEffect(() => { useEffect(() => {
customerServicesStore.fetchGroupListByAgentId(agentId); customerServicesStore.fetchGroupListByAgentId(agentId);
}, []); }, []);
const groupList = customerServicesStore.groupList; const groupList = customerServicesStore.groupList;
const groupListColumns = customerServicesStore.groupListColumns; const groupListColumns = customerServicesStore.groupListColumns;
const {startDate, endDate, dateType, inProgress} = customerServicesStore; const { startDate, endDate, dateType, inProgress } = customerServicesStore;
return ( return (
<> <>
<Space direction="vertical" style={{width: '100%'}}> <Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{md: 24}} justify="end"> <Row gutter={{ md: 24 }} justify="end">
<Col span={8}> <Col span={24}>
<NavLink to={'/agent/group/count'}>返回</NavLink> <NavLink to={'/agent/group/count'}>返回</NavLink>
</Col> </Col>
<Col span={4}> <Col className="gutter-row" span={24}>
<GroupSelect value={customerServicesStore.selectedTeam} <SearchForm
onChange={(value) => customerServicesStore.selectTeam(value)} defaultValue={{
style={{ width: '95%' }} show_all={true} initialValue: {
...date_picker_store.formValues,
...customerServicesStore.searchValues,
},
shows: ['departureDateType', 'DepartmentList', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
departureDateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form, str) => {
customerServicesStore.setSearchValues(obj, form);
customerServicesStore.fetchGroupListByAgentId(agentId);
}}
/> />
</Col> </Col>
<Col span={4}>
<Select value={dateType} style={{ width: "95%" }} onChange={(value) => customerServicesStore.selectDateType(value)}>
<Select.Option key="1" value="startDate">
走团日期
</Select.Option>
<Select.Option key="2" value="ConfirmDate">
成团日期
</Select.Option>
</Select>
</Col>
<Col span={4}>
<DatePicker.RangePicker
format={config.DATE_FORMAT} locale={zhCNlocale}
allowClear={false}
value={[startDate, endDate]}
onChange={(dates) => {customerServicesStore.selectDateRange(dates[0], dates[1]);}}
ranges={{
'本周': [moment().startOf('week'), moment().endOf('week')],
'上周': [moment().startOf('week').subtract(7, 'days'), moment().endOf('week').subtract(7, 'days')],
'本月': [moment().startOf('month'), moment().endOf('month')],
'上个月': [moment().subtract(1, 'months').startOf('month'), moment(new Date()).subtract(1, 'months').endOf('month')],
'近30天': [moment().subtract(30, 'days'), moment()],
'近三个月': [moment().subtract(2, 'month').startOf('month'), moment().endOf('month')],
'今年': [moment().startOf('year').subtract(1, 'month'), moment().endOf('year').subtract(1, 'month')],
'去年': [moment().subtract(1, 'year').startOf('year').subtract(1, 'month'), moment().subtract(1, 'year').endOf('year').subtract(1, 'month')],
}}
/>
</Col>
<Col span={4}>
<Button
type="primary"
icon={<SearchOutlined />}
loading={inProgress}
onClick={() => {
customerServicesStore.fetchGroupListByAgentId(agentId);
}}>
统计
</Button>
</Col>
</Row> </Row>
<Row> <Row>
<Col span={24}> <Col span={24}>
<Typography.Title level={3}>{customerServicesStore.agentCompany}</Typography.Title> <Typography.Title level={3}>{customerServicesStore.agentCompany}</Typography.Title>
<Table <Table
sticky
dataSource={groupList} dataSource={groupList}
columns={groupListColumns} columns={groupListColumns}
size="small" size="small"
rowKey={(record) => record.key} rowKey={(record) => record.key}
loading={inProgress} loading={inProgress}
pagination={false} pagination={false}
scroll={{ x: "100%" }} scroll={{ x: 1000 }}
expandable={{ expandable={{
expandedRowRender: (record) => ( expandedRowRender: (record) => (
<List itemLayout="horizontal">
<List
itemLayout="horizontal"
>
<List.Item> <List.Item>
<List.Item.Meta <List.Item.Meta title="经过城市" description={record.PassCity} />
title='经过城市'
description={record.PassCity}
/>
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Item.Meta <List.Item.Meta title="评论内容" description={record.ECI_Content} />
title='评论内容'
description={record.ECI_Content}
/>
</List.Item> </List.Item>
</List> </List>
), ),
@ -113,9 +74,9 @@ const AgentGroupList = () => {
/> />
</Col> </Col>
</Row> </Row>
</Space> </Space>
</> </>
); );
}; };
export default observer(AgentGroupList); export default observer(AgentGroupList);

@ -9,6 +9,7 @@ import BillTypeSelect from "../components/search/BillTypeSelect";
import GroupSelect from "../components/search/GroupSelect"; import GroupSelect from "../components/search/GroupSelect";
import Business_unit from "../components/search/BusinessSelect"; import Business_unit from "../components/search/BusinessSelect";
import DatePickerCharts from "../components/search/DatePickerCharts"; import DatePickerCharts from "../components/search/DatePickerCharts";
import SearchForm from './../components/search/SearchForm';
import { Line, Pie } from "@ant-design/charts"; import { Line, Pie } from "@ant-design/charts";
import * as comm from "../utils/commons"; import * as comm from "../utils/commons";
import * as config from "../config"; import * as config from "../config";
@ -266,36 +267,32 @@ const Credit_card_bill = () => {
return ( return (
<div> <div>
<Row> <Row gutter={16} style={{ margin: '-16px -8px', position: 'relative', top: 0, zIndex: 10 }}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
// ...sale_store.searchValues,
},
shows: ['billtype', 'businessUnits', 'dates', ],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
financial_store.setSearchValues(obj, form);
financial_store.get_credit_card_bills();
financial_store.get_credit_card_bills_by_type();
financial_store.set_bill_filtered(false);
}}
/>
</Col>
<Col span={11}> <Col span={11}>
<h2>信用卡账单</h2> <h2>信用卡账单</h2>
</Col> </Col>
<Col span={10}>
<Row>
<Col span={12}>
<BillTypeSelect store={bill_type_data} show_all={true} />
<Business_unit store={credit_card_data} show_all={true} />
</Col>
<Col span={12}>
<DatePickerCharts />
</Col>
</Row>
</Col>
<Col span={1}></Col>
<Col span={2}>
<Button
type="primary"
icon={<SearchOutlined />}
size="large"
loading={credit_card_data.loading}
onClick={() => {
financial_store.get_credit_card_bills();
financial_store.get_credit_card_bills_by_type();
financial_store.set_bill_filtered(false);
}}>
统计
</Button>
</Col>
</Row> </Row>
<Row> <Row>
@ -315,7 +312,7 @@ const Credit_card_bill = () => {
</span> </span>
} }
key="summarized_data"> key="summarized_data">
<Table id="table_by_type" dataSource={credit_card_bills_by_type.dataSource} columns={credit_card_bills_by_type.columns} pagination={false} size="small" /> <Table id="table_by_type" dataSource={credit_card_bills_by_type.dataSource} columns={credit_card_bills_by_type.columns} pagination={false} size="small" scroll={{x: 600}} />
<Divider orientation="right"> <Divider orientation="right">
<a <a
onClick={() => { onClick={() => {
@ -334,7 +331,7 @@ const Credit_card_bill = () => {
</span> </span>
} }
key="detail_data"> key="detail_data">
<Table id="table_by_detail" dataSource={credit_card_bills.dataSource} columns={credit_card_bills.columns} pagination={false} onChange={credit_card_data.set_table_handleChange} size="small" /> <Table id="table_by_detail" dataSource={credit_card_bills.dataSource} columns={credit_card_bills.columns} pagination={false} onChange={credit_card_data.set_table_handleChange} size="small" scroll={{x: 800}} />
<Divider orientation="right"> <Divider orientation="right">
<a <a
onClick={() => { onClick={() => {

@ -0,0 +1,181 @@
import React, { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Row, Col, Table, Space, Typography, Divider } from 'antd';
import SearchForm from './../components/search/SearchForm';
import { VSTag, TableExportBtn } from './../components/Data';
import { CruiseAgency } from './../libs/ht';
const { Text } = Typography;
export default observer((props) => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const { HotelCruiseStore, date_picker_store } = useContext(stores_Context);
const { loading, dataSource, summaryRow } = HotelCruiseStore.cruise;
const { formValues, siderBroken } = searchFormStore;
const tableSorter = (a, b, colName) => a[colName] - b[colName];
const tableExportDataRow = (col1, col2) => [col1, col2].filter((r) => r).join(' VS ');
const tableProps = {
size: 'small',
bordered: true,
pagination: false,
columns: [
{ title: '产品',
sorter: (a, b) => a.ProductName.localeCompare(b.ProductName, 'zh-CN'),children: [{ title: summaryRow.ProductName, dataIndex: 'ProductName', key: 'ProductName' }] },
{
title: '房间数',
sorter: (a, b) => tableSorter(a, b, 'TotalNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalNum}
{summaryRow.TotalNumPercent ? <Text type="secondary"> VS {summaryRow.CPTotalNum}</Text> : null}
</Text>
{summaryRow.TotalNumPercent && <VSTag diffPercent={summaryRow.TotalNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalNum, summaryRow.CPTotalNum),
dataExport: (v, r) => tableExportDataRow(r.TotalNum, r.CPTotalNum),
dataIndex: 'TotalNum',
key: 'TotalNum',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalNum ? <Text type="secondary"> VS {r.CPTotalNum}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '人数',
sorter: (a, b) => tableSorter(a, b, 'TotalPersonNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalPersonNum}
{summaryRow.TotalPersonNumPercent ? <Text type="secondary"> VS {summaryRow.CPTotalPersonNum}</Text> : null}
</Text>
{summaryRow.TotalPersonNumPercent && <VSTag diffPercent={summaryRow.TotalPersonNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalPersonNum, summaryRow.CPTotalPersonNum),
dataExport: (v, r) => tableExportDataRow(r.TotalPersonNum, r.CPTotalPersonNum),
dataIndex: 'TotalPersonNum',
key: 'TotalPersonNum',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalPersonNum ? <Text type="secondary"> VS {r.CPTotalPersonNum}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalPersonNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '总利润',
sorter: (a, b) => tableSorter(a, b, 'TotalProfit'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalProfit}
{summaryRow.TotalProfitPercent ? <Text type="secondary"> VS {summaryRow.CPTotalProfit}</Text> : null}
</Text>
{summaryRow.TotalProfitPercent && <VSTag diffPercent={summaryRow.TotalProfitPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalProfit, summaryRow.CPTotalProfit),
dataExport: (v, r) => tableExportDataRow(r.TotalProfit, r.CPTotalProfit),
dataIndex: 'TotalProfit',
key: 'TotalProfit',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalProfit ? <Text type="secondary"> VS {r.CPTotalProfit}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalProfitPercent} />}
</Space>
</>
),
},
],
},
// { title: '', dataIndex: '', key: '' },
// { title: '', dataIndex: '', key: '' },
// { title: '', dataIndex: '', key: '' },
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...HotelCruiseStore.searchValues,
},
// 'countryArea',
shows: [
'DepartmentList',
'orderStatus',
'dates',
'keyword',
'cruiseDirection',
'agency',
'cruiseBookType',
'country', // 'roomsRange', 'personRange'
],
sort: { keyword: 101, agency: 110, cruiseDirection: 102, country: 104 },
fieldProps: {
keyword: { placeholder: '产品名', col: 4 },
DepartmentList: { show_all: true, mode: 'multiple' },
orderStatus: { show_all: true },
cruiseDirection: { show_all: true },
agency: { defaultOptions: CruiseAgency, autoGet: false },
// years: { hide_vs: false },
},
}}
onSubmit={(_err, obj, form) => {
HotelCruiseStore.setSearchValues(obj, form);
HotelCruiseStore.getCruiseData(obj);
}}
/>
</Col>
</Row>
<section>
<Divider orientation="right" >
<TableExportBtn label={'游船'} {...{ columns: tableProps.columns, dataSource }} />
</Divider>
<Table {...tableProps} {...{ loading, dataSource }} rowKey={(record) => record.ProductName} />
</section>
</>
);
});

@ -22,11 +22,24 @@ class Dashboard extends Component {
<Col span={24}> <Col span={24}>
<ExchangeRate /> <ExchangeRate />
</Col> </Col>
<Col span={12}>
<Col {...{
gutter: { xs: 8, sm: 8, md: 16, lg: 16 },
lg: { span: 12 },
md: { span: 24 },
sm: { span: 24 },
xs: { span: 24 },
}}>
<OrdersTempTable /> <OrdersTempTable />
</Col> </Col>
<Col span={12}> <Col {...{
gutter: { xs: 8, sm: 8, md: 16, lg: 16 },
lg: { span: 12 },
md: { span: 24 },
sm: { span: 24 },
xs: { span: 24 },
}}>
<MobileDeal /> <MobileDeal />
</Col> </Col>
</Row> </Row>

@ -0,0 +1,736 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { useParams, useLocation } from 'react-router-dom';
import { stores_Context } from '../config';
import { Row, Col, Spin, Table, Select, Typography, Card, Button, Space, Divider, Alert } from 'antd';
import { cloneDeep, groupBy, isEmpty, omit, pick, sortBy, unique, cartesianProductArray } from '../utils/commons';
import { dataFieldAlias, pivotBy } from '../libs/ht';
import SearchForm from '../components/search/SearchForm';
import { Line } from '@ant-design/plots';
import DateGroupRadio from '../components/DateGroupRadio';
import { TableExportBtn } from './../components/Data';
const { Text } = Typography;
const filterFields = [
{ key: 'SourceType', label: '来源类型' },
{ key: 'productType', label: '产品类型' },
{ key: 'country', label: '国籍' },
{ key: 'CLI_NO', label: '线路' },
// { key: 'destination', label: '' },
{ key: 'COLI_LineClass', label: '页面渠道' },
{ key: 'guestGroupType', label: '客群类别' },
{ key: 'travelMotivation', label: '出行目的' },
{ key: 'startMonth', label: '出行日期-月份' },
{ key: 'startYearMonth', label: '出行日期-年月' },
{ key: 'applyMonth', label: '预订日期-月份' },
{ key: 'applyYearMonth', label: '预订日期-年月' },
{ key: 'operatorName', label: '顾问' },
{ key: 'WebCode', label: '来源站点' },
{ key: 'IsOld_txt', label: '是否老客户' },
{ key: 'isCusCommend_txt', label: '是否老客户推荐' },
{ key: 'hasOld_txt', label: '老客户(推荐)' },
{ key: 'HotelStar', label: '酒店星级' },
{ key: 'destinationCountry_AsJOSN', label: '目的地国籍' },
{ key: 'destinations_AsJOSN', label: '目的地城市' },
{ key: 'RTXF_WB_range', label: '人天消费(外币)' },
{ key: 'PPPriceRange', label: '人均天/单(外币)' },
// { key: 'unitPPPriceRange', label: '()' },
// { key: 'SumML_ctxt1', label: '[1W]' },
// { key: 'SumML_ctxt1_5', label: '[1.5W]' },
// { key: 'SumML_ctxt2', label: '[2W]' },
// { key: 'SumML_ctxt3', label: '[3W]' },
// { key: 'SumML_ctxt4', label: '[4W]' },
{ key: 'SumML_ctxt', label: '毛利' },
];
const filterFieldsMapped = filterFields.reduce((r, v) => ({ ...r, [v.key]: v }), {});
/** 预设的选项, 只有行 */
const quickOptions = [
{ label: ' 来源站点 ', fields: [['WebCode'], []] },
{ label: '[ 产品×客群 ]', fields: [['productType', 'guestGroupType'], []] },
{ label: '[ 国籍×客群 ]', fields: [['country', 'guestGroupType'], []] },
{ label: '[ 客群×目的地国籍 ]', fields: [['guestGroupType', 'destinationCountry_AsJOSN'], []] },
// { label: '[ × ]×[ ]', fields: [['country', 'guestGroupType'], []] },
];
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
const pageSetting = {
orders: {
xField: 'applyDate',
yField: 'SumOrder',
tableColumns: [
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em' },
// { key: 'ConfirmOrder', title: '', dataIndex: 'ConfirmOrder', width: '5em' },
// { key: 'ConfirmPersonNum', title: '', dataIndex: 'ConfirmPersonNum', width: '5em' },
// { key: 'ConfirmRates', title: '', dataIndex: 'ConfirmRates_txt', width: '5em' },
// { key: 'SumML', title: '', dataIndex: 'SumML_txt', width: '5em' },
],
childrenColumns: [
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em' },
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em' },
{ key: 'SumML', title: '毛利', dataIndex: 'SumML', width: '5em' }, // SumML_txt
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em' },
{ key: 'tourdays', title: '团天数', dataIndex: 'tourdays', width: '5em' },
{ key: 'confirmTourdays', title: '✅团天数', dataIndex: 'confirmTourdays', width: '5em' },
{ key: 'SumPersonNum', title: '人数', dataIndex: 'SumPersonNum', width: '5em' },
{ key: 'ConfirmPersonNum', title: '✅人数', dataIndex: 'ConfirmPersonNum', width: '5em' },
],
searchInitial: { DateType: { key: 'applyDate', value: 'applyDate', label: '提交日期' } },
},
trade: {
xField: 'confirmDate',
yField: 'SumML',
yFieldAlias: 'SumML_txt',
tableColumns: [
{ key: 'SumML', title: '毛利', dataIndex: 'SumML', width: '5em' }, // SumML_txt
],
childrenColumns: [
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em' },
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em' },
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em' },
// { key: 'ResumeOrder', title: '', dataIndex: 'ResumeOrder', width: '5em', render: (_, r) => `${r.ResumeConfirmOrder}/${r.ResumeOrder}` },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em' },
{ key: 'OrderValue', title: '单个订单价值', dataIndex: 'OrderValue', width: '5em' },
{ key: 'PPPriceRange', title: '人均天(外币)', dataIndex: 'PPPriceRange', width: '5em' },
{ key: 'AvgPPPrice', title: '人均天/单(外币)', dataIndex: 'AvgPPPrice', width: '5em' },
// { key: 'unitPPPrice', title: '()', dataIndex: 'unitPPPrice', width: '5em' },
// { key: 'unitPPPriceRange', title: '()', dataIndex: 'unitPPPriceRange', width: '5em' },
{ key: 'confirmDays', title: '成团周期(天)', dataIndex: 'confirmDays', width: '5em' },
{ key: 'applyDays', title: '预定周期(天)', dataIndex: 'applyDays', width: '5em' },
{ key: 'tourdays', title: '团天数', dataIndex: 'tourdays', width: '5em' },
],
searchInitial: { DateType: { key: 'confirmDate', value: 'confirmDate', label: '确认日期' } },
},
};
export default observer((props) => {
const { page } = useParams();
const { pathname } = useLocation();
const { date_picker_store: searchFormStore, orders_store, DataPivotStore } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { originData } = DataPivotStore.detailData[page];
const { xField: defaultDateType, yField: defaultValKey, yFieldAlias, tableColumns, childrenColumns, searchInitial } = pageSetting[page];
const [curXfield, setCurXfield] = useState(defaultDateType);
const [loading, setLoading] = useState(false);
const [rawData, setRawData] = useState(originData || []);
const [dataBeforePick, setDataBeforePick] = useState([]);
const [dataBeforeXChange, setDataBeforeXChange] = useState([]);
const [dataSource, setDataSource] = useState([]);
// const [dataSourceMapped, setDataSourceMapped] = useState({});
const [pivotRow, setPivotRow] = useState({});
const [pivotRowDataSource, setPivotRowDataSource] = useState([]);
const [pivotDataSource, setPivotDataSource] = useState([]);
const [pivotTableDataSource, setPivotTableDataSource] = useState([]);
const [pivotTableColumnSummary, setPivotTableColumnSummary] = useState({}); // ,
const [pivotDateColumns, setPivotDateColumns] = useState([[], []]);
const [pivotDateColumnsValues, setPivotDateColumnsValues] = useState([[], []]);
const [showPassCountryTips, setShowPassCountryTips] = useState(false);
useEffect(() => {
calcDataByDate();
resetX();
resetItemFilter();
return () => {};
}, [pivotDateColumns]);
useEffect(() => {
if (lineChartX === 'day') {
setDataBeforeXChange(dataSource);
}
return () => {};
}, [dataSource]);
useEffect(() => {
setCurXfield(formValuesToSub.DateType);
setLineConfig({...lineConfig, xField: formValuesToSub.DateType});
return () => {};
}, [formValues]);
const detailRefresh = async (obj) => {
setLoading(true);
DataPivotStore.getDetailData({
...(obj || formValuesToSub),
}, page).then((resData) => {
setLoading(false);
setRawData(resData);
calcDataByDate(resData);
resetX();
resetItemFilter();
});
};
/**
* 走势的数据
* 汇总
*/
const calcDataByDate = (_rawData) => {
// console.log(';;;;;', pivotDateColumns);
const { data, columnValues, summaryRows, summaryColumns, pivotKeys, summaryMix } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, [curXfield]));
// console.log('data====', data, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns, '\nsummaryMix', summaryMix);
setShowPassCountryTips(pivotKeys.includes('destinationCountry_AsJOSN'));
setDataBeforePick(data.sort(sortBy(curXfield)));
// 线,
// const sortMixData = cloneDeep(summaryMix).sort(sortBy(defaultValKey)).reverse();
// setPivotDataSource(sortMixData);
// const sortRank = sortMixData.map(ele => ele.rowLabel);
// 线
setDataSource(data.sort(sortBy(curXfield)));
//
// .map(ele => ({...ele, rowLabel: `${sortRank.indexOf(ele.rowLabel)+1}).${ele.rowLabel}` }))
//
const sortRowData = cloneDeep(summaryRows).sort(sortBy(defaultValKey)).reverse();
setPivotTableDataSource(sortRowData);
setPivotRow({});
setPivotRowDataSource([]);
//
const _col1 = pivotDateColumns[1][0] || '';
const _sortByDateOrVal = (_col1.includes('Month') || _col1.includes('Date')) ? _col1 : defaultValKey;
let sortColData = summaryColumns.sort(sortBy(_sortByDateOrVal));
sortColData = _sortByDateOrVal === defaultValKey ? sortColData.reverse() : sortColData;
const colDataMapped = isEmpty(pivotDateColumns[1]) ? sortColData[0] : sortColData.reduce((r, v) => ({...r, [v[pivotDateColumns[1][0]]]: v}), {});
setPivotTableColumnSummary(colDataMapped);
//
const _r = (pivotDateColumns[0].map(eleR => unique(sortRowData.map(ele => ele[eleR]))));
const _c = (pivotDateColumns[1].map(eleC => unique(sortColData.map(ele => ele[eleC]))));
// console.log('_r', _r, '_c', _c);
setPivotDateColumnsValues([_r, _c, columnValues[2]]);
};
const line_config = {
// data: dataSource,
padding: 'auto',
xField: curXfield,
yField: defaultValKey,
seriesField: 'rowLabel',
// xAxis: {
// type: 'timeCat',
// },
yAxis: {
min: 0,
maxTickInterval: 5,
},
meta: {
...cloneDeep(dataFieldAlias),
},
// smooth: true,
label: {}, //
legend: {
position: 'right-top',
// title: {
// text: ' ' + dataSource.reduce((a, b) => a + b.SumOrder, 0),
// },
itemMarginBottom: 12, //
},
tooltip: {
customItems: (originalItems) => {
return originalItems.map(ele => ({...ele, valueR: ele.data[defaultValKey]})).sort(sortBy('valueR')).reverse();
},
},
};
const [lineConfig, setLineConfig] = useState(cloneDeep(line_config));
//
// const [leftFields, setLeftFields] = useState(filterFields);
const [rightFields, setRightFields] = useState(filterFields);
const [rowFields, setRowFields] = useState([]);
const [columnFields, setColumnFields] = useState([]);
const [rowSelection, setRowSelection] = useState([]);
const [columnSelection, setColumnSelection] = useState();
//
const quickOpt = (i) => {
const { fields: pivotFields } = quickOptions[i];
const [row, col] = pivotFields;
setRowSelection(Object.values(pick(filterFieldsMapped, row)));
!isEmpty(col) ? setColumnSelection(filterFieldsMapped[col[0]]) : setColumnSelection([]);
setRowFields(row);
setColumnFields(col);
resetItemFilter();
setPivotDateColumns(pivotFields);
};
const resetFields = () => {
setRowFields([]);
setColumnFields([]);
setRowSelection([]);
setColumnSelection([]);
resetItemFilter();
setPivotDateColumns([[], []]);
};
const handleRowsPick = (v) => {
setRowSelection(v);
const pickKeys = v.map((ele) => ele.key);
setRowFields(pickKeys);
// const leftFieldsMapped = leftFields.reduce((r, v) => ({ ...r, [v.key]: v }), {});
const _left = omit(filterFieldsMapped, pickKeys);
setRightFields(isEmpty(v) ? filterFields : Object.values(_left));
// setPivotDateColumns([].concat(pickKeys, columnFields));
setPivotDateColumns([pickKeys, columnFields]);
resetItemFilter();
};
const handleColsPick = (val) => {
setColumnSelection(val);
const pickKeys = isEmpty(val) ? [] : Array.isArray(val) ? val.map((ele) => ele.key) : [val.key];
setColumnFields(pickKeys);
const afterLeft = Object.values(omit(filterFieldsMapped, rowFields));
setRightFields(afterLeft); //
// const rightFieldsMapped = rightFields.reduce((r, v) => ({ ...r, [v.key]: v }), {});
// const _left = omit(rightFieldsMapped, pickKeys);
// setRightFields(isEmpty(val) ? afterLeft : Object.values(_left)); //
// setPivotDateColumns([].concat(rowFields, pickKeys));
setPivotDateColumns([rowFields, pickKeys]);
resetItemFilter();
};
//
const [rowsItemValues, setRowsItemValues] = useState();
const [columnsItemValues, setColumnsItemValues] = useState();
const [rowsFilter, setRowsFilter] = useState();
const [columnsFilter, setColumnsFilter] = useState();
const resetItemFilter = () => {
setRowsItemValues(null);
setColumnsItemValues(null);
setRowsFilter(null);
setColumnsFilter(null);
};
const handleFieldsItemPick = (v, columnsIndex, columnsName, actionSeries) => {
const _curFilter = { [columnsName]: actionSeries === 'row' ? v : [v] };
// console.log('handleFieldsItemPick', v, columnsIndex, columnsName, actionSeries, _curFilter);
const _rowsF = actionSeries === 'row' ? { ...rowsFilter, ..._curFilter } : rowsFilter;
const _columnsF = actionSeries === 'col' ? { ...columnsFilter, ..._curFilter } : columnsFilter;
actionSeries === 'row' ? setRowsFilter(_rowsF) : setColumnsFilter(_columnsF);
const currentFilterMerge = {
rows: _rowsF || {},
columns: _columnsF || {},
};
setRowsItemValues(currentFilterMerge.rows);
setColumnsItemValues(currentFilterMerge.columns);
const rowsFilterFields = Object.keys(currentFilterMerge.rows).filter((ele) => currentFilterMerge.rows[ele].length);
const dataMappedByRows = groupBy(dataBeforePick, (row) => rowsFilterFields.map((kk) => `${row[kk]}`).join('=@='));
const rowsFilterKey = isEmpty(rowsFilterFields)
? []
: cartesianProductArray(
Object.values(currentFilterMerge.rows)
.map((kv) => kv.map((kf) => kf.key))
.filter((s) => s.length),
'=@='
);
const afterRowsFilter = isEmpty(rowsFilterFields) ? dataBeforePick : rowsFilterKey.reduce((r, _key) => r.concat(dataMappedByRows[_key]), []);
const columnsFilterFields = Object.keys(currentFilterMerge.columns).filter((ele) => currentFilterMerge.rows[ele].length);
const allFilterValues = [].concat(
rowsFilterFields.reduce((r, v) => r.concat(currentFilterMerge.rows[v]), []),
columnsFilterFields.reduce((r, v) => r.concat(currentFilterMerge.columns[v]), [])
);
const dataMapped = groupBy(afterRowsFilter, (row) => row[columnsName]);
const pickData = isEmpty(v) ? afterRowsFilter : Array.isArray(v) ? v.reduce((r, v) => r.concat(dataMapped[v.value]), []) : dataMapped[v.value];
setDataSource(allFilterValues.length === 0 ? dataBeforePick : pickData.sort(sortBy(curXfield)));
resetX();
};
//
const [lineChartX, setLineChartX] = useState('day');
const [avgLine1, setAvgLine1] = useState(0);
const orderCountDataMapper = { data1: 'data1', data2: undefined };
const orderCountDataFieldMapper = { 'dateKey': curXfield, 'valueKey': defaultValKey, 'seriesKey': 'rowLabel', _f: 'sum' };
const resetX = () => {
setLineChartX('day');
setAvgLine1(0);
};
const onChangeXDateFieldGroup = (value, data, avg1) => {
const { xField, yField, seriesField } = lineConfig;
const groupByDate = data.reduce((r, v) => {
(r[v[xField]] || (r[v[xField]] = [])).push(v);
return r;
}, {});
const _data = Object.keys(groupByDate).reduce((r, _d) => {
const xAxisGroup = groupByDate[_d].reduce((a, v) => {
(a[v[seriesField]] || (a[v[seriesField]] = [])).push(v);
return a;
}, {});
Object.keys(xAxisGroup).map((_group) => {
const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row[yField], 0);
r.push({ ...xAxisGroup[_group][0], [yField]: summaryVal });
return _group;
});
return r;
}, []);
// .map((row) => ({ [xField]: row[xField], [yField]: row[yField], [seriesField]: row[seriesField], rowX: row.dateRange[0] }));
setLineChartX(value);
setDataSource(_data);
setAvgLine1(avg1);
};
const targetTableProps = {
loading: false,
// sticky: true,
scroll: { x: 1000, y: 400 },
pagination: false,
columns: [
...pivotDateColumns[0].map((ele) => ({ key: ele, title: filterFieldsMapped[ele].label, dataIndex: ele, width: '6em', fixed: 'left' })),
...(isEmpty(pivotDateColumns[1]) ? [].concat(cloneDeep(tableColumns), childrenColumns) : tableColumns),
...pivotDateColumns[1].map((ele) => ({
key: ele,
title: filterFieldsMapped[ele].label,
align: 'left',
className: 'p-s1',
children: cloneDeep(pivotDateColumnsValues[1][0] || []).map((col) => ({
key: col,
title: `${col || '(空)'}: ${pivotTableColumnSummary[col]?.[defaultValKey]}`,
dataIndex: ['columns', col, defaultValKey || yFieldAlias],
width: '6em',
})),
})),
],
};
const detailsTableProps = {
loading: false,
// sticky: true,
scroll: { x: 1000, y: 400 },
pagination: false,
columns: [
{
title: '订单号',
dataIndex: 'o_id',
key: 'o_id',
},
{
title: '来源站点',
dataIndex: 'WebCode',
key: 'WebCode',
},
{
title: '页面渠道',
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
{
title: '来源类型',
dataIndex: 'SourceType',
key: 'SourceType',
},
{
title: '客群类别',
dataIndex: 'guestGroupType',
key: 'guestGroupType',
},
{
title: '成行',
dataIndex: 'orderState',
key: 'orderState',
render: (text, record) => <span>{text === '1' ? '是' : '否'}</span>,
sorter: (a, b) => b.orderState - a.orderState,
},
// {
// title: "(//)",
// dataIndex: "COLI_PersonNum",
// key: "COLI_PersonNum",
// render: (text, record) => (
// <span>
// {record.COLI_PersonNum}/{record.COLI_ChildNum}/{record.COLI_BabyNum}
// </span>
// ),
// },
{
title: '预计利润',
dataIndex: 'ML',
key: 'ML',
},
{
title: '预定时间',
dataIndex: 'applyDate',
key: 'applyDate',
},
{
title: '出发日期',
dataIndex: 'startDate',
key: 'startDate',
},
],
};
return (
<div key={pathname}>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...orders_store.searchValues,
...searchInitial,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'], // 'country'
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
WebCode: { show_all: false, mode: 'multiple', col: 5 },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
detailRefresh(obj);
}}
/>
</Col>
</Row>
{/* extra={<Button type="link">重置</Button>} */}
<Card
size={'small'}
title={'透视选项'}
extra={
<Row gutter={3}>
<Col flex={'0 0 auto'}>
<Text type={'secondary'}>预设:</Text>
</Col>
{quickOptions.map((ele, elei) => (
<Col key={ele.label}>
<Button key={ele.label} onClick={() => quickOpt(elei)} type={'primary'} ghost size={'small'}>
{ele.label}
</Button>
</Col>
))}
<Col>
<Button key={'reset-quick'} onClick={resetFields} type={'primary'} ghost size={'small'}>
重置
</Button>
</Col>
</Row>
}
>
<Row gutter={16}>
<Col span={24}>
{/* todo: 拖拽的操作 */}
{/* <div className='p-s1' style={{border: '1px solid #d9d9d9 '}}>
{filterFields.map((tag) => (
<Tag key={tag.key} checked={selectedTags.indexOf(tag.key) > -1} onChange={(checked) => handleChange(tag.key, checked)} color={'orange'}>
{tag.label}
</Tag>
))}
</div> */}
</Col>
{/* 行: */}
<Col md={12} sm={24} xs={24}>
<Row gutter={8} align={'middle'}>
<Col flex={'4em'} align={'end'}>
<Text strong>: </Text>
</Col>
<Col flex={'auto'}>
<Select
labelInValue
mode={'multiple'}
style={{ width: '100%' }}
placeholder={`选择`}
onChange={(v) => handleRowsPick(v)}
value={rowSelection}
maxTagCount={2}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={true}
>
{filterFields.map((ele) => (
<Select.Option key={ele.key} value={ele.key}>
{ele.label}
</Select.Option>
))}
</Select>
</Col>
<Col span={24}>
{rowFields.length > 0
? cloneDeep(pivotDateColumnsValues)[0]
// .slice(0, rowFields.length)
.map((_colArr, _colIndex) => (
<Row gutter={8} key={filterFieldsMapped[pivotDateColumns[0][_colIndex]]?.key || _colIndex}>
<Col flex={'5em'} align={'end'}>
<Text strong>{filterFieldsMapped[pivotDateColumns[0][_colIndex]]?.label}: </Text>
</Col>
<Col flex={'auto'}>
<Select
size={'small'}
labelInValue
// mode={_colIndex === 0 ? 'multiple' : null}
mode={'multiple'}
style={{ width: '100%' }}
placeholder={`选择`}
onChange={(v) => handleFieldsItemPick(v, _colIndex, pivotDateColumns[0][_colIndex], 'row')}
value={rowsItemValues?.[pivotDateColumns[0][_colIndex]]}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={true}
>
{pivotDateColumnsValues[0][_colIndex].map((ele) => (
<Select.Option key={ele} value={ele}>
{ele}
</Select.Option>
))}
</Select>
</Col>
</Row>
))
: null}
</Col>
</Row>
</Col>
{/* 列: */}
<Col md={12} sm={24} xs={24} style={{ borderLeft: '1px solid #d9d9d9' }}>
<Row gutter={8} align={'middle'}>
<Col flex={'4em'} align={'end'}>
<Text strong>: </Text>
</Col>
<Col flex={'auto'}>
<Select
labelInValue
// mode={'multiple'}
style={{ width: '100%' }}
placeholder={`选择`}
onChange={(v) => handleColsPick(v)}
value={columnSelection}
maxTagCount={2}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={true}
>
{rightFields.map((ele) => (
<Select.Option key={ele.key} value={ele.key}>
{ele.label}
</Select.Option>
))}
</Select>
</Col>
<Col span={24}>
<Row gutter={16} align={'bottom'} className="mt-1">
<Col span={24}>{showPassCountryTips && <Alert message="途径的国家将会重复计算" type="warning" showIcon />}</Col>
</Row>
{/* {columnFields.length > 0
? cloneDeep(pivotDateColumnsValues)
.slice(rowFields.length)
.map((_colArr, _colIndex) => (
<>
<Row gutter={8} key={_colArr.join('_')}>
<Col flex={'5em'} align={'end'}>
<Text strong>{filterFieldsMapped[pivotDateColumns[_colIndex + rowFields.length]]?.label || _colIndex + rowFields.length}: </Text>
</Col>
<Col flex={'auto'}>
<Select
size={'small'}
labelInValue
mode={_colIndex + rowFields.length === 0 ? 'multiple' : null}
style={{ width: '100%' }}
placeholder={`选择`}
onChange={(v) => handleFieldsItemPick(v, _colIndex + rowFields.length, pivotDateColumns[_colIndex + rowFields.length], 'col')}
// value={sale_store.salesTrade.pickSales}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={true}
>
{pivotDateColumnsValues[_colIndex + rowFields.length].map((ele) => (
<Select.Option key={ele} value={ele}>
{ele}
</Select.Option>
))}
</Select>
</Col>
</Row>
</>
))
: null} */}
</Col>
</Row>
</Col>
</Row>
</Card>
<section>
<Row gutter={16} justify={'space-between'} className="mb-1">
<Col flex={'auto'}>
<h3>
走势: <span style={{ fontSize: 'smaller' }}>{dataFieldAlias[lineConfig.yField].label}</span>
</h3>
</Col>
<Col style={{ textAlign: 'right' }} align={'end'}>
<DateGroupRadio
visible={true}
dataRaw={{ data1: dataBeforeXChange }}
onChange={onChangeXDateFieldGroup}
value={lineChartX}
dataMapper={orderCountDataMapper}
fieldMapper={orderCountDataFieldMapper}
/>
</Col>
</Row>
<Spin spinning={loading}>
<Line {...lineConfig} data={dataSource} />
</Spin>
</section>
<section>
<Spin spinning={loading}>
<h3>
透视汇总表: <span style={{ fontSize: 'smaller' }}>{dataFieldAlias[lineConfig.yField].label}</span>
</h3>
<Divider orientation="right">
<TableExportBtn
label={'pivot-' + pivotDateColumns[0].map((ele, ri) => `${filterFieldsMapped[ele].label}`).join('×')}
{...{ columns: targetTableProps.columns, dataSource: pivotTableDataSource }}
/>
</Divider>
<Table
{...targetTableProps}
dataSource={pivotTableDataSource}
components={{ body: { cell: TdCell } }}
onRow={(record) => {
return {
//
onClick: (event) => {
setPivotRow(record);
const thisDetail = rawData.filter((ele) => {
return record.keys.includes(String(ele.key));
});
setPivotRowDataSource(thisDetail);
},
};
}}
/>
</Spin>
</section>
<section>
<Spin spinning={loading}>
<h3>
点击上方表格行查看单行数据的订单明细:{' '}
<span style={{ fontSize: 'smaller' }}>{pivotDateColumns[0].map((ele, ri) => `${filterFieldsMapped[ele].label}: ${pivotRow[pivotDateColumns[0][ri]] || ''}`).join('; ')}</span>
</h3>
<Divider orientation="right">
<TableExportBtn
label={'pivot-' + pivotDateColumns[0].map((ele, ri) => `${pivotRow[pivotDateColumns[0][ri]] || ''}`).join('-')}
{...{ columns: detailsTableProps.columns, dataSource: pivotRowDataSource }}
/>
</Divider>
<Table {...detailsTableProps} dataSource={pivotRowDataSource} components={{ body: { cell: TdCell } }} />
</Spin>
</section>
</div>
);
});

@ -1,109 +1,54 @@
import React, {useContext, useEffect} from 'react'; import { useContext, useEffect } from 'react';
import { Row, Col, Typography, Space, DatePicker, Button, Select, Table, Divider } from 'antd'; import { Row, Col, Typography, Space, Table, Divider } from 'antd';
import {
SearchOutlined,
} from '@ant-design/icons';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import * as config from "../config";
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn';
import moment from "moment"; import { utils, writeFileXLSX } from 'xlsx';
import zhCNlocale from 'antd/es/date-picker/locale/zh_CN'; import SearchForm from './../components/search/SearchForm';
import { utils, writeFileXLSX } from "xlsx"; import { TableExportBtn } from './../components/Data';
import GroupSelect from './../components/search/GroupSelect';
const DestinationGroupCount = () => { const DestinationGroupCount = () => {
const { customerServicesStore, date_picker_store } = useContext(stores_Context);
const {customerServicesStore} = useContext(stores_Context);
const destinationGroupCount = customerServicesStore.destinationGroupCount; const destinationGroupCount = customerServicesStore.destinationGroupCount;
const destinationGroupCountColumns = customerServicesStore.destinationGroupCountColumns; const destinationGroupCountColumns = customerServicesStore.destinationGroupCountColumns;
const {startDate, endDate, dateType, inProgress} = customerServicesStore; const { startDate, endDate, dateType, inProgress } = customerServicesStore;
useEffect(() => {
customerServicesStore.selectCountry('china');
}, []);
const handleSearchClick = () => {
customerServicesStore.fetchDestinationGroupCount();
};
return ( return (
<> <>
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{ sm: 16, lg: 32 }} justify="end"> <Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col md={24} lg={8} xxl={8}> <Col className="gutter-row" span={24}>
<GroupSelect value={customerServicesStore.selectedTeam} <SearchForm
onChange={(value) => customerServicesStore.selectTeam(value)} defaultValue={{
style={{ width: '95%' }} show_all={true} initialValue: {
/> ...date_picker_store.formValues,
</Col> countryArea: { key: 'china', label: '国内' },
<Col md={24} lg={8} xxl={8}> ...customerServicesStore.searchValues,
<Select value={customerServicesStore.selectedCountry} style={{ width: "95%" }} onChange={(value) => customerServicesStore.selectCountry(value)}> },
<Select.Option key="china" value="china"> shows: ['departureDateType', 'DepartmentList', 'countryArea', 'orderStatus', 'dates'],
国内 fieldProps: {
</Select.Option> DepartmentList: { show_all: true, mode: 'multiple' },
<Select.Option key="foreign" value="foreign"> orderStatus: { show_all: true },
国外 countryArea: { show_all: false },
</Select.Option> years: { hide_vs: true },
</Select> departureDateType: { disabledKeys: ['applyDate'] },
</Col> },
<Col md={24} lg={8} xxl={8}> }}
<Select value={customerServicesStore.selectedOrderStatus} style={{ width: "95%" }} onChange={(value) => customerServicesStore.selectStatus(value)}> onSubmit={(_err, obj, form) => {
<Select.Option key="所有" value="-1"> customerServicesStore.setSearchValues(obj, form);
所有 customerServicesStore.fetchDestinationGroupCount();
</Select.Option>
<Select.Option key="已成行" value="1">
已成行
</Select.Option>
<Select.Option key="未成行" value="0">
未成行
</Select.Option>
</Select>
</Col>
<Col md={24} lg={8} xxl={8}>
<Select value={dateType} style={{ width: "95%" }} onChange={(value) => customerServicesStore.selectDateType(value)}>
<Select.Option key="startDate" value="startDate">
走团日期
</Select.Option>
<Select.Option key="ConfirmDate" value="ConfirmDate">
成团日期
</Select.Option>
</Select>
</Col>
<Col md={24} lg={8} xxl={8}>
<DatePicker.RangePicker
format={config.DATE_FORMAT} locale={zhCNlocale}
allowClear={false}
value={[startDate, endDate]}
onChange={(dates) => { customerServicesStore.selectDateRange(dates[0], dates[1]); }}
ranges={{
'本周': [moment().startOf('week'), moment().endOf('week')],
'上周': [moment().startOf('week').subtract(7, 'days'), moment().endOf('week').subtract(7, 'days')],
'本月': [moment().startOf('month'), moment().endOf('month')],
'上个月': [moment().subtract(1, 'months').startOf('month'), moment(new Date()).subtract(1, 'months').endOf('month')],
'近30天': [moment().subtract(30, 'days'), moment()],
'近三个月': [moment().subtract(2, 'month').startOf('month'), moment().endOf('month')],
'今年': [moment().startOf('year').subtract(1, 'month'), moment().endOf('year').subtract(1, 'month')],
'去年': [moment().subtract(1, 'year').startOf('year').subtract(1, 'month'), moment().subtract(1, 'year').endOf('year').subtract(1, 'month')],
}} }}
/> />
</Col> </Col>
<Col md={24} lg={8} xxl={8}>
<Button
type="primary"
icon={<SearchOutlined />}
loading={inProgress}
onClick={() => {
handleSearchClick();
}}>
统计
</Button>
</Col>
</Row> </Row>
<Row> <Row>
<Col span={24}> <Col span={24}>
<Typography.Title level={3}>目的地团信息</Typography.Title> <Typography.Title level={3}>目的地团信息</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={'目的地团信息'} {...{ columns: destinationGroupCountColumns, dataSource: destinationGroupCount }} />
</Divider>
<Table <Table
sticky
id="destinationGroupCount" id="destinationGroupCount"
dataSource={destinationGroupCount} dataSource={destinationGroupCount}
columns={destinationGroupCountColumns} columns={destinationGroupCountColumns}
@ -111,17 +56,8 @@ const DestinationGroupCount = () => {
rowKey={(record) => record.key} rowKey={(record) => record.key}
loading={inProgress} loading={inProgress}
pagination={false} pagination={false}
scroll={{ x: "100%" }} scroll={{ x: 1000 }}
/> />
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("destinationGroupCount").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "目的地团信息.xlsx");
}}>
导出excel
</a>
</Divider>
</Col> </Col>
</Row> </Row>
</Space> </Space>

@ -1,112 +1,97 @@
import React, {useContext, useEffect} from 'react'; import { useContext, useEffect } from 'react';
import {Row, Col, Typography, Space, DatePicker, Button, Select, Table, List} from 'antd'; import { Row, Col, Space, Table, List, Typography, Divider } from 'antd';
import { import { stores_Context } from '../config';
SearchOutlined, import { observer } from 'mobx-react';
} from '@ant-design/icons'; import { NavLink, useParams } from 'react-router-dom';
import {stores_Context} from '../config';
import * as config from "../config";
import {observer} from 'mobx-react';
import { NavLink, useParams } from "react-router-dom";
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn';
import moment from "moment"; import SearchForm from './../components/search/SearchForm';
import zhCNlocale from 'antd/es/date-picker/locale/zh_CN'; import { TableExportBtn } from './../components/Data';
import GroupSelect from './../components/search/GroupSelect';
const DestinationGroupList = () => { const DestinationGroupList = () => {
const { destinationId } = useParams();
const {destinationId} = useParams(); const { customerServicesStore, date_picker_store } = useContext(stores_Context);
const { customerServicesStore } = useContext(stores_Context);
useEffect(() => { useEffect(() => {
customerServicesStore.fetchGroupListByDestinationId(destinationId); customerServicesStore.fetchDistGroupInfoByCountry(destinationId);
}, []); customerServicesStore.fetchGroupListByDestinationId(destinationId);
}, []);
const destinationGroupList = customerServicesStore.destinationGroupList; const destinationGroupList = customerServicesStore.destinationGroupList;
const destinationGroupListColumns = customerServicesStore.destinationGroupListColumns; const destinationGroupListColumns = customerServicesStore.destinationGroupListColumns;
const {startDate, endDate, dateType, inProgress} = customerServicesStore; const nationality_count_data = customerServicesStore.nationality_count_data;
const { inProgress } = customerServicesStore;
return ( return (
<> <>
<Space direction="vertical" style={{width: '100%'}}> <Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{md: 24}} justify="end"> <Row gutter={{ md: 24 }} justify="end">
<Col span={8}> <Col span={24}>
<NavLink to={'/destination/group/count'}>返回</NavLink> <NavLink to={'/destination/group/count'}>返回</NavLink>
</Col> </Col>
<Col span={4}> <Col className="gutter-row" span={24}>
<SearchForm
<GroupSelect value={customerServicesStore.selectedTeam} defaultValue={{
onChange={(value) => customerServicesStore.selectTeam(value)} initialValue: {
style={{ width: '95%' }} show_all={true} ...date_picker_store.formValues,
countryArea: { key: 'china', label: '国内' },
...customerServicesStore.searchValues,
},
shows: ['departureDateType', 'DepartmentList', 'dates', 'orderStatus'],
fieldProps: {
DepartmentList: { show_all: true },
orderStatus: { show_all: true },
countryArea: { show_all: false },
dates: { hide_vs: true },
departureDateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form) => {
customerServicesStore.setSearchValues(obj, form);
customerServicesStore.fetchGroupListByDestinationId(destinationId);
customerServicesStore.fetchDistGroupInfoByCountry(destinationId);
customerServicesStore.fetchDestinationGroupCount();
}}
/> />
</Col> </Col>
<Col span={4}>
<Select value={dateType} style={{ width: "95%" }} onChange={(value) => customerServicesStore.selectDateType(value)}>
<Select.Option key="1" value="startDate">
走团日期
</Select.Option>
<Select.Option key="2" value="ConfirmDate">
成团日期
</Select.Option>
</Select>
</Col>
<Col span={4}>
<DatePicker.RangePicker
format={config.DATE_FORMAT} locale={zhCNlocale}
allowClear={false}
value={[startDate, endDate]}
onChange={(dates) => {customerServicesStore.selectDateRange(dates[0], dates[1]);}}
ranges={{
'本周': [moment().startOf('week'), moment().endOf('week')],
'上周': [moment().startOf('week').subtract(7, 'days'), moment().endOf('week').subtract(7, 'days')],
'本月': [moment().startOf('month'), moment().endOf('month')],
'上个月': [moment().subtract(1, 'months').startOf('month'), moment(new Date()).subtract(1, 'months').endOf('month')],
'近30天': [moment().subtract(30, 'days'), moment()],
'近三个月': [moment().subtract(2, 'month').startOf('month'), moment().endOf('month')],
'今年': [moment().startOf('year').subtract(1, 'month'), moment().endOf('year').subtract(1, 'month')],
'去年': [moment().subtract(1, 'year').startOf('year').subtract(1, 'month'), moment().subtract(1, 'year').endOf('year').subtract(1, 'month')],
}}
/>
</Col>
<Col span={4}>
<Button
type="primary"
icon={<SearchOutlined />}
loading={inProgress}
onClick={() => {
customerServicesStore.fetchGroupListByDestinationId(destinationId);
customerServicesStore.fetchDestinationGroupCount();
}}>
统计
</Button>
</Col>
</Row> </Row>
<Row> <Row>
<Col span={24}> <Col span={24}>
<Typography.Title level={3}>国籍统计</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={'国籍统计'} {...{ columns: nationality_count_data.destinationGroupByCountryListColumns, dataSource: nationality_count_data.destinationGroupByCountryList }} />
</Divider>
<Table <Table
sticky
id="destinationGroupByCountry"
dataSource={nationality_count_data.destinationGroupByCountryList}
columns={nationality_count_data.destinationGroupByCountryListColumns}
size="small"
rowKey={(record) => record.key}
loading={nationality_count_data.loading}
pagination={false}
scroll={{ x: 1000 }}
/>
</Col>
</Row>
<Row>
<Col span={24}>
<Table
sticky
dataSource={destinationGroupList} dataSource={destinationGroupList}
columns={destinationGroupListColumns} columns={destinationGroupListColumns}
size="small" size="small"
rowKey={(record) => record.key} rowKey={(record) => record.key}
loading={inProgress} loading={inProgress}
pagination={false} pagination={false}
scroll={{ x: "100%" }} scroll={{ x: 600 }}
expandable={{ expandable={{
expandedRowRender: (record) => ( expandedRowRender: (record) => (
<List itemLayout="horizontal">
<List
itemLayout="horizontal"
>
<List.Item> <List.Item>
<List.Item.Meta <List.Item.Meta title="经过城市" description={record.PassCity} />
title='经过城市'
description={record.PassCity}
/>
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Item.Meta <List.Item.Meta title="地接社" description={record.VendorList} />
title='地接社'
description={record.VendorList}
/>
</List.Item> </List.Item>
</List> </List>
), ),
@ -114,9 +99,9 @@ const DestinationGroupList = () => {
/> />
</Col> </Col>
</Row> </Row>
</Space> </Space>
</> </>
); );
}; };
export default observer(DestinationGroupList); export default observer(DestinationGroupList);

@ -1,12 +1,13 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { Row, Col, Spin, Tabs, Table, Space, Typography } from 'antd'; import { Row, Col, Spin, Tabs, Table, Space, Typography, Divider } from 'antd';
import { RingProgress } from '@ant-design/plots'; import { RingProgress } from '@ant-design/plots';
import SearchForm from './../components/search/SearchForm'; import SearchForm from './../components/search/SearchForm';
import { empty } from '../utils/commons'; import { empty } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht'; import { dataFieldAlias } from '../libs/ht';
import { VSTag } from './../components/Data'; import { VSTag, TableExportBtn } from './../components/Data';
import MixYnQ from './../components/MixYnQ';
import './kpi.css'; import './kpi.css';
@ -18,14 +19,22 @@ const apartOptions = [
{ key: 'ConfirmDays', value: 'ConfirmDays', label: '成团周期' }, { key: 'ConfirmDays', value: 'ConfirmDays', label: '成团周期' },
{ key: 'ApplyDays', value: 'ApplyDays', label: '预定周期' }, { key: 'ApplyDays', value: 'ApplyDays', label: '预定周期' },
{ key: 'PersonNum', value: 'PersonNum', label: '人等' }, { key: 'PersonNum', value: 'PersonNum', label: '人等' },
{ key: 'destination', value: 'destination', label: '国内目的地' }, { key: 'destination', value: 'destination', label: '国内目的地', },
{ key: 'GlobalDestination', value: 'GlobalDestination', label: '海外目的地' }, { key: 'GlobalDestination', value: 'GlobalDestination', label: '海外目的地', },
{ key: 'destinationCountry', value: 'GlobalDestination', label: '目的地国籍' }, { key: 'destinationCountry', value: 'destinationCountry', label: '目的地国籍', },
{ key: 'guestCountry', value: 'guestCountry', label: '客人国籍', },
]; ];
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
export default observer(() => { export default observer(() => {
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context); const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore; const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { curTab, dateStringQ, dateStringY } = DistributionStore; const { curTab, dateStringQ, dateStringY } = DistributionStore;
const pageRefresh = (obj) => { const pageRefresh = (obj) => {
@ -34,12 +43,6 @@ export default observer(() => {
}); });
}; };
useEffect(() => {
if (empty(DistributionStore[curTab].dataSource)) {
pageRefresh();
}
}, [curTab]);
useEffect(() => { useEffect(() => {
DistributionStore.setFormDates(formValuesToSub); DistributionStore.setFormDates(formValuesToSub);
DistributionStore.resetData(); DistributionStore.resetData();
@ -48,6 +51,9 @@ export default observer(() => {
const onTabsChange = (tab) => { const onTabsChange = (tab) => {
DistributionStore.setCurTab(tab); DistributionStore.setCurTab(tab);
if (empty(DistributionStore[tab].dataSource)) {
pageRefresh();
}
}; };
const RingProgressConfig = { const RingProgressConfig = {
height: 60, height: 60,
@ -71,27 +77,38 @@ export default observer(() => {
innerRadius: 0.90, innerRadius: 0.90,
}; };
const columns = [ const columns = [
{ title: '', dataIndex: 'label' }, { title: '#', dataIndex: 'label' },
{
title: '预定',
dataIndex: 'SumOrder',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>{v}</Text>
<span>
<span>同比: </span> <VSTag diffPercent={r.SumOrderToY} diffData={r.SumOrderDiffY} />
</span>
<span>
<span>环比: </span> <VSTag diffPercent={r.SumOrderToQ} diffData={r.SumOrderDiffQ} />
</span>
</Space>
</>
),
},
{ {
title: '团数', title: '团数',
dataIndex: 'ConfirmOrder', dataIndex: 'ConfirmOrder',
render: (v, r) => ( render: (v, r) => (
<> <>
<Row align={'middle'}>
<Col flex={"100px"}>
<Text strong>{v}</Text>
</Col>
<Col flex={'auto'}>
<Space direction={'vertical'}> <Space direction={'vertical'}>
<span> <Text strong>{v}</Text>
<span>
<span>同比: </span> <VSTag diffPercent={r.ConfirmOrderToY} diffData={r.ConfirmOrderDiffY} /> <span>同比: </span> <VSTag diffPercent={r.ConfirmOrderToY} diffData={r.ConfirmOrderDiffY} />
</span> </span>
<span> <span>
<span>环比: </span> <VSTag diffPercent={r.ConfirmOrderToQ} diffData={r.ConfirmOrderDiffQ} /> <span>环比: </span> <VSTag diffPercent={r.ConfirmOrderToQ} diffData={r.ConfirmOrderDiffQ} />
</span> </span>
</Space> </Space>
</Col>
</Row>
</> </>
), ),
}, },
@ -100,21 +117,15 @@ export default observer(() => {
dataIndex: 'SumML', dataIndex: 'SumML',
render: (v, r) => ( render: (v, r) => (
<> <>
<Row align={'middle'}>
<Col flex={"150px"}>
<Text strong>{dataFieldAlias.SumML.formatter(v)}</Text>
</Col>
<Col flex={'auto'}>
<Space direction={'vertical'}> <Space direction={'vertical'}>
<span> <Text strong>{dataFieldAlias.SumML.formatter(v)}</Text>
<span>
<span>同比: </span> <VSTag diffPercent={r.SumMLToY} diffData={dataFieldAlias.SumML.formatter(r.SumMLDiffY)} /> <span>同比: </span> <VSTag diffPercent={r.SumMLToY} diffData={dataFieldAlias.SumML.formatter(r.SumMLDiffY)} />
</span> </span>
<span> <span>
<span>环比: </span> <VSTag diffPercent={r.SumMLToQ} diffData={dataFieldAlias.SumML.formatter(r.SumMLDiffQ)} /> <span>环比: </span> <VSTag diffPercent={r.SumMLToQ} diffData={dataFieldAlias.SumML.formatter(r.SumMLDiffQ)} />
</span> </span>
</Space> </Space>
</Col>
</Row>
</> </>
), ),
}, },
@ -130,44 +141,55 @@ export default observer(() => {
}, },
{ {
title: () => <><div>去年同期</div><div>{dateStringY}</div></>, title: () => <><div>去年同期</div><div>{dateStringY}</div></>,
titleX: ['去年同期', dateStringY], //
align: 'center', align: 'center',
children: [ children: [
{ {
title: '团数占比', title: '团数占比',
titleX: '团数占比',
width: 90, width: 90,
dataIndex: 'ConfirmOrderPercent', dataIndex: ['resultToY', 'ConfirmOrderPercent'], // 'ConfirmOrderPercent',
render: (v, r) => r.resultToY.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.ConfirmOrderPercent / 100} /> : '-', render: (v, r) => r.resultToY.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.ConfirmOrderPercent / 100} /> : '-',
}, },
{ {
title: '业绩占比', title: '业绩占比',
titleX: '业绩占比',
width: 90, width: 90,
dataIndex: 'SumMLPercent', dataIndex: ['resultToY', 'SumMLPercent'], // 'SumMLPercent',
render: (v, r) => r.resultToY.SumMLPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.SumMLPercent / 100} /> : '-', render: (v, r) => r.resultToY.SumMLPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.SumMLPercent / 100} /> : '-',
}, },
], ],
}, },
{ {
title: () => <><div>上个时间段</div><div>{dateStringQ}</div></>, title: () => <><div>上个时间段</div><div>{dateStringQ}</div></>,
titleX: ['上个时间段', dateStringY], //
align: 'center', align: 'center',
children: [ children: [
{ {
title: '团数占比', title: '团数占比',
titleX: '团数占比',
width: 90, width: 90,
dataIndex: 'ConfirmOrderPercent', dataIndex: ['resultToQ', 'ConfirmOrderPercent'], // 'ConfirmOrderPercent',
render: (v, r) => r.resultToQ.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.ConfirmOrderPercent / 100} /> : '-', render: (v, r) => r.resultToQ.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.ConfirmOrderPercent / 100} /> : '-',
}, },
{ {
title: '业绩占比', title: '业绩占比',
titleX: '业绩占比',
width: 90, width: 90,
dataIndex: 'SumMLPercent', dataIndex: ['resultToQ', 'SumMLPercent'], // 'SumMLPercent',
render: (v, r) => r.resultToQ.SumMLPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.SumMLPercent / 100} /> : '-', render: (v, r) => r.resultToQ.SumMLPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.SumMLPercent / 100} /> : '-',
}, },
], ],
}, },
]; ];
const chartsConfig = {
xField: 'label',
yFields: ['ConfirmOrder','SumML'],
seriesField: null,
};
return ( return (
<> <>
<Row gutter={16} style={{ margin: '-16px -8px' }}> <Row gutter={16} className={siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}> <Col className="gutter-row" span={24}>
<SearchForm <SearchForm
defaultValue={{ defaultValue={{
@ -197,8 +219,13 @@ export default observer(() => {
...ele, ...ele,
children: ( children: (
<Spin spinning={DistributionStore.pageLoading}> <Spin spinning={DistributionStore.pageLoading}>
<MixYnQ {...chartsConfig} dataSource={DistributionStore[curTab].dataSource} />
<Divider orientation="right" plain>
<TableExportBtn label={`统计分布-${ele.label}`} {...{ columns, dataSource: DistributionStore[curTab].dataSource }} />
</Divider>
<Table <Table
id="table_to_xlsx_sale" id="table_to_xlsx_sale"
components={{ body: { cell: TdCell } }}
dataSource={DistributionStore[curTab].dataSource} dataSource={DistributionStore[curTab].dataSource}
columns={columns} columns={columns}
size="small" size="small"

@ -1,30 +1,36 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Row, Col, Spin, Space, Radio, Table } from 'antd'; import { Row, Col, Spin, Space, Radio, Table, Button, } from 'antd';
import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone } from '@ant-design/icons'; import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone } from '@ant-design/icons';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { useNavigate } from 'react-router-dom';
import StatisticCard from '../components/StatisticCard';
import StatisticCard2 from '../components/StatisticCard2'; import StatisticCard2 from '../components/StatisticCard2';
import Bullet from '../components/BulletWithSort'; import Bullet from '../components/BulletWithSort';
import Waterfall from '../components/Waterfall'; import Waterfall from '../components/Waterfall';
import LineWithKPI from '../components/LineWithKPI';
import MixTBWithKPI from './../components/MixTBWithKPI'; import MixTBWithKPI from './../components/MixTBWithKPI';
import Donut from './../components/Donut'; import Donut from './../components/Donut';
import MapCountry from './../components/MapCountry';
import LineWithKPI from '../components/LineWithKPI';
import DataFieldRadio from '../components/DataFieldRadio'; import DataFieldRadio from '../components/DataFieldRadio';
import { datePartOptions } from './../components/DateGroupRadio/date'; import { datePartOptions } from './../components/DateGroupRadio/date';
import SearchForm from './../components/search/SearchForm'; import SearchForm from './../components/search/SearchForm';
import { empty, cloneDeep, isEmpty } from './../utils/commons'; import { empty, cloneDeep, isEmpty } from './../utils/commons';
import { dataFieldAlias } from './../libs/ht'; import { dataFieldAlias } from './../libs/ht';
import { Line } from '@ant-design/charts';
import './home.css'; import './home.css';
const topSeries = [ const topSeries = [
{ key: 'dept', label: '小组', graphVisible: true }, { key: 'dept', value: 'dept', label: '小组', graphVisible: true },
{ key: 'operator', label: '顾问', graphVisible: true }, { key: 'operator', value: 'operator', label: '顾问', graphVisible: true },
{ key: 'country', label: '国籍', graphVisible: true }, { key: 'destination', value: 'destination', label: '目的地', graphVisible: true },
{ key: 'GuestGroupType', label: '客群类别', graphVisible: false }, { key: 'GuestGroupType', value: 'GuestGroupType', label: '客群类别', graphVisible: false },
{ key: 'destination', label: '目的地', graphVisible: true }, { key: 'country', value: 'country', label: '国籍', graphVisible: true },
{ key: 'webcode', value: 'webcode', label: '站点', graphVisible: false },
{ key: 'bizarea', value: 'bizarea', label: '国境', graphVisible: false },
{ key: 'destinationcountry', value: 'destinationcountry', label: '目的地国籍', graphVisible: true },
];
const allGroupTypes = [
{ key: 'overview', value: 'overview', label: '总额' },
...topSeries,
]; ];
// const iconSets = [CheckCircleTwoTone, <MoneyCollectTwoTone />, <FlagTwoTone />, <ClockCircleTwoTone />, <DashboardTwoTone />,<SmileTwoTone />,]; // const iconSets = [CheckCircleTwoTone, <MoneyCollectTwoTone />, <FlagTwoTone />, <ClockCircleTwoTone />, <DashboardTwoTone />,<SmileTwoTone />,];
@ -32,17 +38,27 @@ const iconSets = [CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwo
export default observer(() => { export default observer(() => {
// const navigate = useNavigate(); // const navigate = useNavigate();
const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context); const { TradeStore, date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { sideData, summaryData, BuData, topData, timeData, timeLineKey, targetTableProps } = TradeStore; const { searchValues, sideData, summaryData, BuData, topData, timeData, timeLineKey, targetTableProps, timeDiffData, groupKey } = TradeStore;
const { formValues } = searchFormStore; const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { curTab, dateStringQ, dateStringY } = DistributionStore;
useEffect(() => { useEffect(() => {
if (empty(summaryData.dataSource)) { DistributionStore.setCurTab('destinationCountry');
// pageRefresh(); // if (empty(summaryData.dataSource)) {
} // // pageRefresh();
// }
return () => {}; return () => {};
}, []); }, []);
const getDestinationCountry = (obj) => {
DistributionStore.setCurTab('destinationCountry');
DistributionStore.getApartData({
...(obj || formValuesToSub),
}, false);
};
const [topSeriesSet, setTopSeriesSet] = useState(topSeries); const [topSeriesSet, setTopSeriesSet] = useState(topSeries);
const [overviewFlag, setOverviewFlag] = useState(true); const [overviewFlag, setOverviewFlag] = useState(true);
const [groupTypeVal, setGroupTypeVal] = useState('overview'); const [groupTypeVal, setGroupTypeVal] = useState('overview');
@ -51,10 +67,12 @@ export default observer(() => {
const groupType = _overviewFlag ? 'overview' : 'dept'; const groupType = _overviewFlag ? 'overview' : 'dept';
queryData.groupType = groupType; queryData.groupType = groupType;
setGroupTypeVal(groupType); setGroupTypeVal(groupType);
setDiffGroupKey('overview');
TradeStore.resetData(); TradeStore.resetData();
TradeStore.fetchSummaryData(Object.assign({}, queryData, { groupType })); TradeStore.fetchSummaryData(Object.assign({}, queryData, { groupType }));
TradeStore.fetchTradeDataByDate(queryData); TradeStore.fetchTradeDataByDate(queryData);
// TradeStore.fetchTradeDataByBU(queryData); TradeStore.fetchTradeDataDiffByDate(queryData);
// // TradeStore.fetchTradeDataByBU(queryData);
TradeStore.fetchTradeDataByMonth(queryData); TradeStore.fetchTradeDataByMonth(queryData);
const topSeriesF = _overviewFlag ? topSeries : topSeries.filter((ele) => ele.key !== 'dept'); const topSeriesF = _overviewFlag ? topSeries : topSeries.filter((ele) => ele.key !== 'dept');
setTopSeriesSet(topSeriesF); setTopSeriesSet(topSeriesF);
@ -136,7 +154,6 @@ export default observer(() => {
xAxis: { xAxis: {
type: 'cat', type: 'cat',
}, },
smooth: true,
point: { point: {
size: 4, size: 4,
shape: 'cicle', shape: 'cicle',
@ -161,35 +178,49 @@ export default observer(() => {
TradeStore.setTimeLineKey(value); TradeStore.setTimeLineKey(value);
if (!isEmpty(TradeStore.searchPayloadHome)) { if (!isEmpty(TradeStore.searchPayloadHome)) {
TradeStore.fetchTradeDataByDate({ groupType: groupTypeVal }); TradeStore.fetchTradeDataByDate({ groupType: groupTypeVal });
TradeStore.fetchTradeDataDiffByDate({ groupType: diffGroupKey });
}
};
const [diffGroupKey, setDiffGroupKey] = useState(groupKey);
const handleChangeDiffType = ({ target: { value } }) => {
// console.log('diffGroupKey', diffGroupKey, value);
setDiffGroupKey(value);
TradeStore.setGroupKey(value);
if (!isEmpty(TradeStore.searchPayloadHome)) {
TradeStore.fetchTradeDataDiffByDate({ groupType: value });
} }
}; };
const [showDiff, setShowDiff] = useState(false);
return ( return (
<> <>
<Row gutter={16} style={{ margin: '-16px -8px', position: 'sticky', top: 0, zIndex: 10 }}> <Row gutter={16} className={siderBroken ? "" : "sticky-top"}>
{/* style={{ margin: '-16px -8px', padding: 0 }} */}
<Col className="gutter-row" span={24}> <Col className="gutter-row" span={24}>
<SearchForm <SearchForm
defaultValue={{ defaultValue={{
initialValue: { initialValue: {
...formValues, ...formValues,
...searchValues,
}, },
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'], shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'],
fieldProps: { fieldProps: {
DepartmentList: { show_all: true }, DepartmentList: { show_all: true },
WebCode: { show_all: true }, WebCode: { show_all: true },
years: { hide_vs: true }, years: { hide_vs: false },
}, },
}} }}
onSubmit={(_err, obj, form, str) => { onSubmit={(_err, obj, form, str) => {
TradeStore.setStateSearch(obj); TradeStore.setSearch(obj, form);
pageRefresh(obj); pageRefresh(obj);
getDestinationCountry(obj);
}} }}
/> />
</Col> </Col>
</Row> </Row>
<section> <section>
<Space> <Space>
<h2>年度业绩=传统+商务</h2> <h2>
年度业绩<span style={{ fontSize: 'small' }}> =传统+商务</span>
</h2>
</Space> </Space>
<Spin spinning={summaryData.loading}> <Spin spinning={summaryData.loading}>
<Row gutter={layoutProps.gutter}> <Row gutter={layoutProps.gutter}>
@ -199,7 +230,7 @@ export default observer(() => {
</Col> </Col>
))} */} ))} */}
{summaryData.dataSource.map((item, i) => ( {summaryData.dataSource.map((item, i) => (
<Col {...layoutProps} key={item.title}> <Col {...layoutProps} key={item.title} lg={{ span: item?.col || layoutProps.lg.span }}>
<StatisticCard2 {...item} showProgress={item.hasKPI} icon={iconSets[i]} /> <StatisticCard2 {...item} showProgress={item.hasKPI} icon={iconSets[i]} />
</Col> </Col>
))} ))}
@ -207,22 +238,44 @@ export default observer(() => {
</Spin> </Spin>
</section> </section>
<section> <section>
<Space gutter={16} size={'large'}> <Row gutter={16}>
<h3>走势</h3> <Col flex={'4em'}><h3>{showDiff === false ? '走势' : '对比'}</h3></Col>
<DataFieldRadio value={timeDataField} onChange={handleChangetimeDataField} /> <Col ><DataFieldRadio value={timeDataField} onChange={handleChangetimeDataField} /></Col>
<Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} /> <Col ><Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} /></Col>
</Space> <Col >{searchValues.yearDiff && (
<Spin spinning={timeData.loading}> <Button type="primary" ghost size={'small'} onClick={() => setShowDiff(!showDiff)}>
{/* <LineWithKPI dataSource={timeData.dataSource} {...lineConfig} /> */} {showDiff === false ? '显示对比' : '返回本期走势'}
<MixTBWithKPI dataSource={timeData.dataSource} summaryData={timeData.origin?.summaryRows || []} {...lineConfig} /> </Button>
</Spin> )}</Col>
</Row>
{showDiff === false ? (
<Spin spinning={timeData.loading}>
<MixTBWithKPI dataSource={timeData.dataSource} summaryData={timeData.origin?.summaryRows || []} {...lineConfig} />
</Spin>
) : (
<Spin spinning={timeDiffData.loading}>
<Row gutter={16}>
<Col flex={'6em'}><h3>分类对比</h3></Col>
<Col ><Radio.Group options={allGroupTypes} optionType="button" onChange={handleChangeDiffType} value={diffGroupKey} /></Col>
</Row>
<LineWithKPI
dataSource={timeDiffData.dataSource}
showKPI={false}
{...lineConfig}
{...{ appendPadding: 10, legend: { position: 'right-top', title: { text: '虚线: 对比年份' } }, point: false }}
/>
</Spin>
)}
</section> </section>
<section> <section>
<h3>市场 (仅传统订单) </h3> <h3>市场 (仅传统订单) </h3>
<Spin spinning={BuData.loading}> <Spin spinning={sideData.loading}>
<Row gutter={layoutProps3.gutter}> <Row gutter={layoutProps3.gutter}>
<Col {...layoutProps3}> <Col {...layoutProps3}>
<><Donut {...{angleField: 'SumML', colorField: 'groupsLabel'}} title={formValues.DepartmentList?.label} dataSource={sideData.yearData} /></> <>
<Donut {...{ angleField: 'SumML', colorField: 'groupsLabel' }} title={formValues.DepartmentList?.label} dataSource={sideData.yearData} />
</>
{/* {overviewFlag ? ( {/* {overviewFlag ? (
<> <>
<Bullet {...BUConfig} dataSource={BuData?.dataSource || []} /> <Bullet {...BUConfig} dataSource={BuData?.dataSource || []} />
@ -232,34 +285,34 @@ export default observer(() => {
<><Donut {...{angleField: 'SumML', colorField: 'groupsLabel'}} title={formValues.DepartmentList?.label} dataSource={sideData.yearData} /></> <><Donut {...{angleField: 'SumML', colorField: 'groupsLabel'}} title={formValues.DepartmentList?.label} dataSource={sideData.yearData} /></>
)} */} )} */}
</Col> </Col>
{Object.keys(sideData.dataSource).sort().map((key) => ( {Object.keys(sideData.dataSource)
<Col {...layoutProps3} key={key}> .sort()
<Waterfall key={key} {...WaterfallConfig} title={key} dataSource={sideData.dataSource[key]} line={summaryData.kpi} /> .map((key) => (
<h3 style={{ textAlign: 'center' }}>{`${key}每月业绩`}</h3> <Col {...layoutProps3} key={key}>
</Col> <Waterfall key={key} {...WaterfallConfig} title={key} dataSource={sideData.dataSource[key]} line={summaryData.kpi} />
))} <h3 style={{ textAlign: 'center' }}>{`${key}每月业绩`}</h3>
</Col>
))}
</Row> </Row>
</Spin> </Spin>
</section> </section>
<section> <section>
<h3> <h3>
英语区目标客户 (仅传统订单) 英语区目标客户
<Spin spinning={topData?.GuestGroupType?.loading || false}> <Spin spinning={topData?.GuestGroupType?.loading || false}>
<Table {...targetTableProps} pagination={false} /> <Table {...targetTableProps} pagination={false} rowKey={'groupsLabel'} />
</Spin> </Spin>
</h3> </h3>
</section> </section>
<section> <section>
<Space> <Row gutter={16}>
<h3>TOP</h3> <Col flex={'4em'}><h3>TOP</h3></Col>
<div> <Col ><DataFieldRadio value={valueKey} onChange={handleChangeValueKey} /></Col>
<DataFieldRadio value={valueKey} onChange={handleChangeValueKey} /> </Row>
</div> <Row gutter={layoutProps3.gutter}>
</Space>
<Row gutter={layoutProps.gutter}>
{topSeriesSet.map((item) => {topSeriesSet.map((item) =>
item.graphVisible ? ( item.graphVisible ? (
<Col {...layoutProps} key={item.key}> <Col {...layoutProps3} key={item.key}>
<Spin spinning={topData[item.key]?.loading || false}> <Spin spinning={topData[item.key]?.loading || false}>
<h3 style={{ textAlign: 'center' }}>{item.label}</h3> <h3 style={{ textAlign: 'center' }}>{item.label}</h3>
<Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} itemLength={10} key={item.key} /> <Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} itemLength={10} key={item.key} />
@ -267,6 +320,31 @@ export default observer(() => {
</Col> </Col>
) : null ) : null
)} )}
<Col key={'mapG'} span={22}>
<hr />
<h3>来源国籍分布</h3>
<Spin spinning={topData?.country?.loading || false}>
<div id="topC" style={{ height: '700px' }}>
<MapCountry sourceField={'groupsLabel'} valueField={BUConfig.measureField} dataSource={topData?.country?.dataSource || []} containerNode='#topC' />
</div>
</Spin>
</Col>
<Col key={'mapG-r'} span={1}></Col>
<Col key={'mapDestinationCountry'} span={22}>
<hr />
<h3>目的地国籍分布</h3>
{/* <Spin spinning={DistributionStore.pageLoading || false}>
<div id="mapDestinationCountry" style={{ height: '700px' }}>
<MapCountry sourceField={'label'} valueField={BUConfig.measureField} dataSource={DistributionStore.destinationCountry.originData || []} containerNode='#mapDestinationCountry' />
</div>
</Spin> */}
<Spin spinning={topData?.destinationcountry?.loading || false}>
<div id="mapDestinationCountry" style={{ height: '700px' }}>
<MapCountry sourceField={'groupsLabel'} valueField={BUConfig.measureField} dataSource={topData?.destinationcountry?.dataSource || []} containerNode='#mapDestinationCountry' />
</div>
</Spin>
</Col>
<Col key={'mapGc-r'} span={1}></Col>
</Row> </Row>
</section> </section>
</> </>

@ -0,0 +1,147 @@
import { useContext } from 'react';
import { Row, Col, Typography, Space, Table, Divider } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import 'moment/locale/zh-cn';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn } from '../components/Data';
import * as comm from '../utils/commons';
const HostCaseCount = () => {
const { customer_store, date_picker_store } = useContext(stores_Context);
const host_case_data = customer_store.host_case_data;
const columnsList = [
{
title: '团数',
dataIndex: 'TotalGroupNum',
key: 'TotalGroupNum',
sorter: (a, b) => parseInt(a.TotalGroupNum) - parseInt(b.TotalGroupNum),
},
{
title: '人数',
dataIndex: 'TotalPersonNum',
key: 'TotalPersonNum',
sorter: (a, b) => parseInt(a.TotalPersonNum) - parseInt(b.TotalPersonNum),
},
{
title: '计费团天数',
dataIndex: 'TotalDays',
key: 'TotalDays',
sorter: (a, b) => parseInt(a.TotalDays) - parseInt(b.TotalDays),
},
{
title: '交易额',
dataIndex: 'TotalPrice',
key: 'TotalPrice',
sorter: (a, b) => parseInt(a.TotalPrice) - parseInt(b.TotalPrice),
},
];
//
const getFiltersData=(filterData,filterKey)=>{
return comm.uniqWith(filterData.map(rr => ({ text: rr[filterKey], value: rr[filterKey] })),
(a, b) => JSON.stringify(a) === JSON.stringify(b)).sort((a, b) => a.text.localeCompare(b.text));
};
//
const allOPIGroup = getFiltersData(host_case_data.summaryData,"GroupBy");
//
const allOPIConsultant = getFiltersData(host_case_data.counselorData,"GroupBy");
//
const allOPIDetailConsultant = getFiltersData(host_case_data.singleDetailData,"OPI_Name");
const summaryColumnsList = [{
title: '',
dataIndex: 'GroupBy',
key: 'GroupBy',
}, ...columnsList];
const groupColumnsList = [{
title: '组名',
dataIndex: 'GroupBy',
key: 'GroupBy',
filters: allOPIGroup,
onFilter: (value, record) => record.GroupBy === value,
filterSearch: true,
}, ...columnsList];
const counselorColumnsList = [{
title: '顾问名',
dataIndex: 'GroupBy',
key: 'GroupBy',
filters: allOPIConsultant,
onFilter: (value, record) => record.GroupBy === value,
filterSearch: true,
}, ...columnsList];
const singleDetailColumnsList = [{
title: '团名',
dataIndex: 'GroupBy',
key: 'GroupBy',
},
{
title: '顾问名',
dataIndex: 'OPI_Name',
key: 'OPI_Name',
filters: allOPIDetailConsultant,
onFilter: (value, record) => record.OPI_Name === value,
filterSearch: true,
},
...columnsList.slice(1)];
const renderRow=(rowColumns,rowDataSource,title)=>{
return(
<Row>
<Col span={24}>
<Typography.Title level={3}>{title}</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={title} {...{ columns: rowColumns, dataSource: rowDataSource }} />
</Divider>
<Table
sticky
id={`${rowColumns}`}
dataSource={rowDataSource}
columns={rowColumns}
size="small"
rowKey={(record) => record.key}
loading={host_case_data.loading}
pagination={false}
scroll={{ x: 1000 }}
/>
</Col>
</Row>
);
};
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...host_case_data.searchValues,
},
shows: ['DepartmentList', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form) => {
customer_store.setSearchValues(obj, form,"host_case_data");
customer_store.getHostCaseData("1");
customer_store.getHostCaseData("2");
customer_store.getHostCaseData("3");
customer_store.getHostCaseData("4");
}}
/>
</Col>
</Row>
{renderRow(summaryColumnsList,host_case_data.summaryData,'东道主项目汇总')}
{renderRow(groupColumnsList,host_case_data.groupData,'东道主项目小组统计')}
{renderRow(counselorColumnsList,host_case_data.counselorData,'东道主项目顾问统计')}
{renderRow(singleDetailColumnsList,host_case_data.singleDetailData,'单团明细')}
</Space>
</>
);
};
export default observer(HostCaseCount);

@ -0,0 +1,171 @@
import React, { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Row, Col, Table, Space, Typography, Divider } from 'antd';
import SearchForm from './../components/search/SearchForm';
import { VSTag, TableExportBtn } from './../components/Data';
const { Text } = Typography;
export default observer((props) => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const { date_picker_store, HotelCruiseStore } = useContext(stores_Context);
const { loading, dataSource, summaryRow } = HotelCruiseStore.hotel;
const { formValues, siderBroken } = searchFormStore;
const tableSorter = (a, b, colName) => a[colName] - b[colName];
const tableExportDataRow = (col1, col2) => [col1, col2].filter((r) => r).join(' VS ');
const tableProps = {
size: 'small',
bordered: true,
pagination: false,
columns: [
{
title: '目的地',
sorter: (a, b) => a.CityName.localeCompare(b.CityName, 'zh-CN'),
children: [{ title: summaryRow.CityName, dataIndex: 'CityName', key: 'CityName' }],
},
{
title: '总间夜',
sorter: (a, b) => tableSorter(a, b, 'TotalNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalNum}
{summaryRow.TotalNumPercent ? <Text type="secondary"> VS {summaryRow.CPTotalNum}</Text> : null}
</Text>
{summaryRow.TotalNumPercent && <VSTag diffPercent={summaryRow.TotalNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalNum, summaryRow.CPTotalNum),
dataIndex: 'TotalNum',
key: 'TotalNum',
dataExport: (v, r) => tableExportDataRow(r.TotalNum, r.CPTotalNum),
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalNum ? <Text type="secondary"> VS {r.CPTotalNum}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '主推',
sorter: (a, b) => tableSorter(a, b, 'RecomendNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.RecomendNum}
{summaryRow.RecomendNumPercent ? <Text type="secondary"> VS {summaryRow.CPRecomendNum}</Text> : null}
</Text>
{summaryRow.RecomendNumPercent && <VSTag diffPercent={summaryRow.RecomendNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.RecomendNum, summaryRow.CPRecomendNum),
dataIndex: 'RecomendNum',
key: 'RecomendNum',
dataExport: (v, r) => tableExportDataRow(r.RecomendNum, r.CPRecomendNum),
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPRecomendNum ? <Text type="secondary"> VS {r.CPRecomendNum}</Text> : null}
</Text>
{r.CPRecomendNum && <VSTag diffPercent={r.RecomendNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '使用比例',
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.RecommendRate_100}
{summaryRow.RecommendRateDelta ? <Text type="secondary"> VS {summaryRow.CPRecommendRate_100}</Text> : null}
</Text>
{summaryRow.RecommendRateDelta && <VSTag diffPercent={summaryRow.RecommendRateDelta} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.RecommendRate_100, summaryRow.CPRecommendRate_100),
dataIndex: 'RecommendRate_100',
key: 'RecommendRate_100',
dataExport: (v, r) => tableExportDataRow(r.RecommendRate_100, r.CPRecommendRate_100),
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.RecommendRateDelta !== undefined && <Text type="secondary"> VS {r.CPRecommendRate_100}</Text>}
</Text>
{r.RecommendRateDelta !== undefined && <VSTag diffPercent={r.RecommendRateDelta} />}
</Space>
</>
),
},
],
},
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...HotelCruiseStore.searchValues,
},
// 'countryArea', 'DateType', 'dates', 'hotelRecommandRate',
shows: ['DepartmentList', 'countryArea', 'orderStatus', 'hotelBookType', 'hotelStar', 'DateType', 'dates'],
sort: { DateType: 101, dates: 102 },
fieldProps: {
DepartmentList: { show_all: true, mode: 'multiple' },
countryArea: { show_all: true },
orderStatus: { show_all: true },
hotelBookType: { show_all: true },
hotelRecommandRate: { show_all: true },
// years: { hide_vs: false },
DateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form) => {
HotelCruiseStore.setSearchValues(obj, form);
HotelCruiseStore.getHotelData(obj);
}}
/>
</Col>
</Row>
<section>
<Divider orientation="right" >
<TableExportBtn label={'酒店'} {...{ columns: tableProps.columns, dataSource }} />
</Divider>
<Table {...tableProps} bordered {...{ loading, dataSource }} rowKey={(record) => record.CityName} />
</section>
</>
);
});

@ -0,0 +1,133 @@
import { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
import { fixTo2Decimals } from './../utils/commons';
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals(number / (1000 * scale)) + '';
};
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
export default observer((props) => {
const { date_picker_store: searchFormStore, MeetingDataStore } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const dataRefresh = async (obj) => {
MeetingDataStore.dataGHOrder({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHSales({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHService({ ...(obj || formValuesToSub) });
};
const targetTableProps = {
loading: MeetingDataStore.GHLoading,
// sticky: true,
scroll: { x: 1000, y: 400 },
pagination: false,
components: { body: { cell: TdCell } },
orderColumns: [
{ key: 'label', title: '市场:', dataIndex: 'label', width: 150 },
{
key: 'LineClass_Origin',
titleX: '网站',
title: () => (
<>
网站{' '}
<Tooltip title="网前自然订单">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'LineClass_Origin',
},
{
key: 'external',
titleX: '站外渠道',
title: () => (
<>
站外渠道{' '}
<Tooltip title="Facebook, Pinterest, Youtube, Instagram">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'external',
},
{ key: 'LineClass_PPC', title: 'PPC', dataIndex: 'LineClass_PPC' },
{ key: 'toB', title: 'To B', dataIndex: 'toB' },
{ key: 'isOld1', title: 'C老客户', dataIndex: 'isOld1' },
{ key: 'total', title: '合计', dataIndex: 'total' },
{ key: 'rowYear', title: '截至年订单数', dataIndex: 'rowYear' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
],
salesColumns: [
{ key: 'label', title: '顾问:', dataIndex: 'label', width: 150 },
{ key: 'CJCount', title: '成交个数', dataIndex: 'CJCount' },
{ key: 'YJLY', title: '成交毛利(万)', dataIndex: 'YJLY', render: (text) => numberConvert10K(text) },
{ key: 'CJCount1', title: '年成交个数', dataIndex: ['rowYear', 'CJCount'] },
{ key: 'YJLY1', title: '年成交毛利(万)', dataIndex: ['rowYear', 'YJLY'], render: (text) => numberConvert10K(text) },
{ key: 'YJLY2', title: '年走团毛利(万)', dataIndex: ['rowYear', 'YJLY2'], render: (text) => numberConvert10K(text) },
],
serviceColumns: [
{ key: 'label', title: '客服:', dataIndex: 'label', width: 150 },
{ key: 'GroupCount', title: '走团个数', dataIndex: 'GroupCount' },
{ key: 'GoodCount', title: '好评个数', dataIndex: 'GoodCount' },
{ key: 'GroupCount1', title: '年走团个数', dataIndex: ['rowYear', 'GroupCount'] },
{ key: 'GoodCount2', title: '年好评个数', dataIndex: ['rowYear', 'GoodCount'] },
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...MeetingDataStore.searchValues,
// ...searchInitial,
},
shows: ['IncludeTickets', 'dates'], // 'country'
fieldProps: {
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
MeetingDataStore.setSearchValues(form);
dataRefresh(obj);
}}
/>
</Col>
</Row>
<Divider orientation="right" plain>
<span>GH: 市场</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 市场`} {...{ columns: targetTableProps.orderColumns, dataSource: MeetingDataStore.GHTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHTable'} dataSource={MeetingDataStore.GHTableData} columns={targetTableProps.orderColumns} loading={MeetingDataStore.GHTableLoading} />
<Divider orientation="right" plain>
<span>GH: 顾问成交</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 顾问成交`} {...{ columns: targetTableProps.salesColumns, dataSource: MeetingDataStore.GHSalesTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHSales'} dataSource={MeetingDataStore.GHSalesTableData} columns={targetTableProps.salesColumns} loading={MeetingDataStore.GHSalesLoading} />
<Divider orientation="right" plain>
<span>GH: 客服</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 客服`} {...{ columns: targetTableProps.serviceColumns, dataSource: MeetingDataStore.GHServiceTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHService'} dataSource={MeetingDataStore.GHServiceTableData} columns={targetTableProps.serviceColumns} loading={MeetingDataStore.GHServiceLoading} />
</>
);
});

@ -0,0 +1,134 @@
import { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
import { fixTo2Decimals } from './../utils/commons';
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals(number / (1000 * scale)) + '';
};
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
export default observer((props) => {
const { date_picker_store: searchFormStore, MeetingData2025Store: MeetingDataStore } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const dataRefresh = async (obj) => {
MeetingDataStore.dataGHOrder({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHSales({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHService({ ...(obj || formValuesToSub) });
};
const targetTableProps = {
loading: MeetingDataStore.GHLoading,
// sticky: true,
scroll: { x: 1000, y: 400 },
pagination: false,
components: { body: { cell: TdCell } },
orderColumns: [
{ key: 'label', title: '市场:', dataIndex: 'label', width: 150 },
{
key: 'LineClass_Origin',
titleX: '网站',
title: () => (
<>
网站{' '}
<Tooltip title="网前自然订单">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'LineClass_Origin',
},
{
key: 'external',
titleX: '站外渠道',
title: () => (
<>
站外渠道{' '}
<Tooltip title="Facebook, Pinterest, Youtube, Instagram">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'external',
},
{ key: 'LineClass_PPC', title: 'PPC', dataIndex: 'LineClass_PPC' },
{ key: 'toB', title: 'To B', dataIndex: 'toB' },
{ key: 'isOld1', title: 'C老客户', dataIndex: 'isOld1' },
{ key: 'total', title: '合计', dataIndex: 'total' },
{ key: 'rowYear', title: '截至年订单数', dataIndex: 'rowYear' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
],
salesColumns: [
{ key: 'label', title: '顾问:', dataIndex: 'label', width: 150 },
{ key: 'CJCount', title: '成交个数', dataIndex: 'CJCount' },
{ key: 'YJLY', title: '成交毛利(万)', dataIndex: 'YJLY', render: (text) => numberConvert10K(text) },
{ key: 'CJCount1', title: '年成交个数', dataIndex: ['rowYear', 'CJCount'] },
{ key: 'YJLY1', title: '年成交毛利(万)', dataIndex: ['rowYear', 'YJLY'], render: (text) => numberConvert10K(text) },
{ key: 'YJLY2', title: '年走团毛利(万)', dataIndex: ['rowYear', 'YJLY2'], render: (text) => numberConvert10K(text) },
],
serviceColumns: [
{ key: 'label', title: '客服:', dataIndex: 'label', width: 150 },
{ key: 'GroupCount', title: '走团个数', dataIndex: 'GroupCount' },
// { key: 'GoodCount', title: '', dataIndex: 'GoodCount' },
{ key: 'GroupCount1', title: '年走团个数', dataIndex: ['rowYear', 'GroupCount'] },
// { key: 'GoodCount2', title: '', dataIndex: ['rowYear', 'GoodCount'] },
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...MeetingDataStore.searchValues,
// ...searchInitial,
},
shows: ['IncludeTickets', 'dates'], // 'country'
fieldProps: {
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
MeetingDataStore.setSearchValues(form);
dataRefresh(obj);
}}
/>
</Col>
</Row>
<h2>GH例会数据 2025</h2>
<Divider orientation="left" plain>
<span>GH: 市场</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 市场`} {...{ columns: targetTableProps.orderColumns, dataSource: MeetingDataStore.GHTableData }} style={{ marginLeft: 'auto' }} />
</Divider>
<Table {...targetTableProps} key={'GHTable'} dataSource={MeetingDataStore.GHTableData} columns={targetTableProps.orderColumns} loading={MeetingDataStore.GHTableLoading} />
<Divider orientation="left" plain>
<span>GH: 顾问成交</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 顾问成交`} {...{ columns: targetTableProps.salesColumns, dataSource: MeetingDataStore.GHSalesTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHSales'} dataSource={MeetingDataStore.GHSalesTableData} columns={targetTableProps.salesColumns} loading={MeetingDataStore.GHSalesLoading} />
<Divider orientation="left" plain>
<span>GH: 客服</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 客服`} {...{ columns: targetTableProps.serviceColumns, dataSource: MeetingDataStore.GHServiceTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHService'} dataSource={MeetingDataStore.GHServiceTableData} columns={targetTableProps.serviceColumns} loading={MeetingDataStore.GHServiceLoading} />
</>
);
});

@ -1,18 +1,17 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Row, Col, Button, Tabs, Table, Divider, Select, Radio } from "antd"; import { Row, Col, Tabs, Table, Divider, Spin } from "antd";
import { ContainerOutlined, CarryOutOutlined, BlockOutlined, SmileOutlined, TagsOutlined, GlobalOutlined, SearchOutlined, FullscreenOutlined, DingtalkOutlined } from "@ant-design/icons"; import { ContainerOutlined, BlockOutlined, SmileOutlined, TagsOutlined, GlobalOutlined, FullscreenOutlined, DingtalkOutlined, CarryOutOutlined, CoffeeOutlined, ClockCircleOutlined, HeartOutlined, IdcardOutlined, ContactsOutlined } from "@ant-design/icons";
import { stores_Context } from "../config"; import { stores_Context } from "../config";
import { Line, Pie } from "@ant-design/charts"; import { Line, Pie } from "@ant-design/charts";
import SiteSelect from "../components/search/SiteSelect";
import GroupSelect from "../components/search/GroupSelect";
import DataTypeSelect from "../components/search/DataTypeSelect";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import DatePickerCharts from "../components/search/DatePickerCharts";
import * as config from "../config"; import * as config from "../config";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import * as comm from "../utils/commons"; import * as comm from "../utils/commons";
import { utils, writeFileXLSX } from "xlsx"; import { utils, writeFileXLSX } from "xlsx";
import DateGroupRadio from '../components/DateGroupRadio'; import DateGroupRadio from '../components/DateGroupRadio';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
class Orders extends Component { class Orders extends Component {
static contextType = stores_Context; static contextType = stores_Context;
@ -29,81 +28,92 @@ class Orders extends Component {
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) { if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
// //
result.columns = [ result.columns = [
{ {
title: "", title: '#',
children: [ fixed: 'left',
{ children: [
title: ( {
<span> title: (
<div> <span>
{date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)} <div>
</div> {date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
<div> </div>
{date_picker_store.start_date_cp.format(config.DATE_FORMAT)}~{date_picker_store.end_date_cp.format(config.DATE_FORMAT)} <div>
</div> {date_picker_store.start_date_cp.format(config.DATE_FORMAT)}~{date_picker_store.end_date_cp.format(config.DATE_FORMAT)}
</span> </div>
), </span>
dataIndex: "OrderType", ),
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>, titleX: `${date_picker_store.start_date.format(config.DATE_FORMAT)}~${date_picker_store.end_date.format(config.DATE_FORMAT)} vs ${date_picker_store.start_date_cp.format(
}, config.DATE_FORMAT
], )}~${date_picker_store.end_date_cp.format(config.DATE_FORMAT)}`,
}, dataIndex: 'OrderType',
{ fixed: 'left',
title: "数量", render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>,
children: [ },
{ ],
title: comm.show_vs_tag(ordercountTotal1.OrderCount_vs, ordercountTotal1.OrderCount_diff, ordercountTotal1.OrderCount, ordercountTotal2.OrderCount), },
dataIndex: "OrderCount", {
}, title: '数量',
], children: [
}, {
{ title: comm.show_vs_tag(ordercountTotal1.OrderCount_vs, ordercountTotal1.OrderCount_diff, ordercountTotal1.OrderCount, ordercountTotal2.OrderCount),
title: "成交数", titleX: [ordercountTotal1.OrderCount, ordercountTotal2.OrderCount].join(' vs '),
children: [ dataIndex: 'OrderCount',
{ },
title: comm.show_vs_tag(ordercountTotal1.CJCount_vs, ordercountTotal1.CJCount_diff, ordercountTotal1.CJCount, ordercountTotal2.CJCount), ],
dataIndex: "CJCount", },
}, {
], title: '成交数',
}, children: [
{ {
title: "成交人数", title: comm.show_vs_tag(ordercountTotal1.CJCount_vs, ordercountTotal1.CJCount_diff, ordercountTotal1.CJCount, ordercountTotal2.CJCount),
children: [ titleX: [ordercountTotal1.CJCount, ordercountTotal2.CJCount].join(' vs '),
{ dataIndex: 'CJCount',
title: comm.show_vs_tag(ordercountTotal1.CJPersonNum_vs, ordercountTotal1.CJPersonNum_diff, ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum), },
dataIndex: "CJPersonNum", ],
}, },
], {
}, title: '成交人数',
{ children: [
title: "成交率", {
children: [ title: comm.show_vs_tag(ordercountTotal1.CJPersonNum_vs, ordercountTotal1.CJPersonNum_diff, ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum),
{ titleX: [ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum].join(' vs '),
title: comm.show_vs_tag(ordercountTotal1.CJrate_vs, ordercountTotal1.CJrate_diff, ordercountTotal1.CJrate, ordercountTotal2.CJrate), dataIndex: 'CJPersonNum',
dataIndex: "CJrate", },
}, ],
], },
}, {
{ title: '成交率',
title: "成交毛利(预计)", children: [
children: [ {
{ title: comm.show_vs_tag(ordercountTotal1.CJrate_vs, ordercountTotal1.CJrate_diff, ordercountTotal1.CJrate, ordercountTotal2.CJrate),
title: comm.show_vs_tag(ordercountTotal1.YJLY_vs, ordercountTotal1.YJLY_diff, ordercountTotal1.YJLY, ordercountTotal2.YJLY), titleX: [ordercountTotal1.CJrate, ordercountTotal2.CJrate].join(' vs '),
dataIndex: "YJLY", dataIndex: 'CJrate',
}, },
], ],
}, },
{
title: '成交毛利(预计)',
children: [
{
title: comm.show_vs_tag(ordercountTotal1.YJLY_vs, ordercountTotal1.YJLY_diff, ordercountTotal1.YJLY, ordercountTotal2.YJLY),
titleX: [ordercountTotal1.YJLY, ordercountTotal2.YJLY].join(' vs '),
dataIndex: 'YJLY',
},
],
},
{ {
title: "单个订单价值", title: '单个订单价值',
children: [ children: [
{ {
title: comm.show_vs_tag(ordercountTotal1.Ordervalue_vs, ordercountTotal1.Ordervalue_diff, ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue), title: comm.show_vs_tag(ordercountTotal1.Ordervalue_vs, ordercountTotal1.Ordervalue_diff, ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue),
dataIndex: "Ordervalue", titleX: [ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue].join(' vs '),
}, dataIndex: 'Ordervalue',
], },
}, ],
]; },
];
// 1.OrderType 2.OrderType // 1.OrderType 2.OrderType
let has_same_type = false; // 12 let has_same_type = false; // 12
for (const item of data.ordercount1) { for (const item of data.ordercount1) {
@ -117,11 +127,17 @@ class Orders extends Component {
OrderType: item.OrderType, OrderType: item.OrderType,
OrderTypeSN: item.OrderTypeSN, OrderTypeSN: item.OrderTypeSN,
OrderCount: comm.show_vs_tag(item.OrderCount_vs, item.OrderCount_diff, item.OrderCount, item2.OrderCount), OrderCount: comm.show_vs_tag(item.OrderCount_vs, item.OrderCount_diff, item.OrderCount, item2.OrderCount),
OrderCount_X: ([item.OrderCount, item2.OrderCount].join(' vs ')),
CJCount: comm.show_vs_tag(item.CJCount_vs, item.CJCount_diff, item.CJCount, item2.CJCount), CJCount: comm.show_vs_tag(item.CJCount_vs, item.CJCount_diff, item.CJCount, item2.CJCount),
CJCount_X: ([item.CJCount, item2.CJCount].join(' vs ')),
CJPersonNum: comm.show_vs_tag(item.CJPersonNum_vs, item.CJPersonNum_diff, item.CJPersonNum, item2.CJPersonNum), CJPersonNum: comm.show_vs_tag(item.CJPersonNum_vs, item.CJPersonNum_diff, item.CJPersonNum, item2.CJPersonNum),
CJPersonNum_X: ([item.CJPersonNum, item2.CJPersonNum].join(' vs ')),
CJrate: comm.show_vs_tag(item.CJrate_vs, item.CJrate_diff, item.CJrate, item2.CJrate), CJrate: comm.show_vs_tag(item.CJrate_vs, item.CJrate_diff, item.CJrate, item2.CJrate),
CJrate_X: ([item.CJrate, item2.CJrate].join(' vs ')),
YJLY: comm.show_vs_tag(item.YJLY_vs, item.YJLY_diff, item.YJLY, item2.YJLY), YJLY: comm.show_vs_tag(item.YJLY_vs, item.YJLY_diff, item.YJLY, item2.YJLY),
YJLY_X: ([item.YJLY, item2.YJLY].join(' vs ')),
Ordervalue: comm.show_vs_tag(item.Ordervalue_vs, item.Ordervalue_diff, item.Ordervalue, item2.Ordervalue), Ordervalue: comm.show_vs_tag(item.Ordervalue_vs, item.Ordervalue_diff, item.Ordervalue, item2.Ordervalue),
Ordervalue_X: ([item.Ordervalue, item2.Ordervalue].join(' vs ')),
}); });
} }
} }
@ -132,11 +148,17 @@ class Orders extends Component {
OrderType: item.OrderType, OrderType: item.OrderType,
OrderTypeSN: item.OrderTypeSN, OrderTypeSN: item.OrderTypeSN,
OrderCount: comm.show_vs_tag(comm.formatPercent(item.OrderCount), item.OrderCount, item.OrderCount, 0), OrderCount: comm.show_vs_tag(comm.formatPercent(item.OrderCount), item.OrderCount, item.OrderCount, 0),
OrderCount_X: ([item.OrderCount, 0].join(' vs ')),
CJCount: comm.show_vs_tag(comm.formatPercent(item.CJCount), item.CJCount, item.CJCount, 0), CJCount: comm.show_vs_tag(comm.formatPercent(item.CJCount), item.CJCount, item.CJCount, 0),
CJCount_X: ([item.CJCount, 0].join(' vs ')),
CJPersonNum: comm.show_vs_tag(comm.formatPercent(item.CJPersonNum), item.CJPersonNum, item.CJPersonNum, 0), CJPersonNum: comm.show_vs_tag(comm.formatPercent(item.CJPersonNum), item.CJPersonNum, item.CJPersonNum, 0),
CJPersonNum_X: ([item.CJPersonNum, 0].join(' vs ')),
CJrate: comm.show_vs_tag(item.CJrate, item.CJrate, item.CJrate, 0), CJrate: comm.show_vs_tag(item.CJrate, item.CJrate, item.CJrate, 0),
CJrate_X: ([item.CJrate, 0].join(' vs ')),
YJLY: comm.show_vs_tag(comm.formatPercent(item.YJLY), item.YJLY, item.YJLY, 0), YJLY: comm.show_vs_tag(comm.formatPercent(item.YJLY), item.YJLY, item.YJLY, 0),
YJLY_X: ([item.YJLY, 0].join(' vs ')),
Ordervalue: comm.show_vs_tag(comm.formatPercent(item.Ordervalue), item.Ordervalue, item.Ordervalue, 0), Ordervalue: comm.show_vs_tag(comm.formatPercent(item.Ordervalue), item.Ordervalue, item.Ordervalue, 0),
Ordervalue_X: ([item.Ordervalue, 0].join(' vs ')),
}); });
} }
} }
@ -154,18 +176,25 @@ class Orders extends Component {
OrderType: item2.OrderType, OrderType: item2.OrderType,
OrderTypeSN: item2.OrderTypeSN, OrderTypeSN: item2.OrderTypeSN,
OrderCount: comm.show_vs_tag(comm.formatPercent(-item2.OrderCount), -item2.OrderCount, 0, item2.OrderCount), OrderCount: comm.show_vs_tag(comm.formatPercent(-item2.OrderCount), -item2.OrderCount, 0, item2.OrderCount),
OrderCount_X: ([ 0, item2.OrderCount].join(' vs ')),
CJCount: comm.show_vs_tag(comm.formatPercent(-item2.CJCount), -item2.CJCount, 0, item2.CJCount), CJCount: comm.show_vs_tag(comm.formatPercent(-item2.CJCount), -item2.CJCount, 0, item2.CJCount),
CJCount_X: ([ 0, item2.CJCount].join(' vs ')),
CJPersonNum: comm.show_vs_tag(comm.formatPercent(-item2.CJPersonNum), -item2.CJPersonNum, 0, item2.CJPersonNum), CJPersonNum: comm.show_vs_tag(comm.formatPercent(-item2.CJPersonNum), -item2.CJPersonNum, 0, item2.CJPersonNum),
CJPersonNum_X: ([0, item2.CJPersonNum].join(' vs ')),
CJrate: comm.show_vs_tag(-item2.CJrate, -item2.CJrate, 0, item2.CJrate), CJrate: comm.show_vs_tag(-item2.CJrate, -item2.CJrate, 0, item2.CJrate),
CJrate_X: ([ 0, item2.CJrate].join(' vs ')),
YJLY: comm.show_vs_tag(comm.formatPercent(-item2.YJLY), -item2.YJLY, 0, item2.YJLY), YJLY: comm.show_vs_tag(comm.formatPercent(-item2.YJLY), -item2.YJLY, 0, item2.YJLY),
YJLY_X: ([0, item2.YJLY].join(' vs ')),
Ordervalue: comm.show_vs_tag(comm.formatPercent(-item2.Ordervalue), -item2.Ordervalue, 0, item2.Ordervalue), Ordervalue: comm.show_vs_tag(comm.formatPercent(-item2.Ordervalue), -item2.Ordervalue, 0, item2.Ordervalue),
Ordervalue_X: ([ 0, item2.Ordervalue].join(' vs ')),
}); });
} }
} }
} else { } else {
result.columns = [ result.columns = [
{ {
title: "", title: "#",
fixed: 'left',
children: [ children: [
{ {
title: ( title: (
@ -175,7 +204,9 @@ class Orders extends Component {
</div> </div>
</span> </span>
), ),
dataIndex: "OrderType", titleX: `${date_picker_store.start_date.format(config.DATE_FORMAT)}~${date_picker_store.end_date.format(config.DATE_FORMAT)}`,
fixed: 'left',
dataIndex: "OrderType",
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>, render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>,
}, },
], ],
@ -219,7 +250,7 @@ class Orders extends Component {
} }
render() { render() {
const { orders_store } = this.context; const { orders_store, date_picker_store } = this.context;
const table_data = orders_store.orderCountData_Form ? this.format_data(orders_store.orderCountData_Form) : []; const table_data = orders_store.orderCountData_Form ? this.format_data(orders_store.orderCountData_Form) : [];
const data_source = orders_store.orderCountData ? orders_store.orderCountData : []; const data_source = orders_store.orderCountData ? orders_store.orderCountData : [];
const avg_line_y = Math.round(orders_store.avgLine1); const avg_line_y = Math.round(orders_store.avgLine1);
@ -287,7 +318,7 @@ class Orders extends Component {
return ret; return ret;
}, },
}, },
smooth: true, // smooth: true,
}; };
const pie_config = { const pie_config = {
appendPadding: 10, appendPadding: 10,
@ -310,55 +341,44 @@ class Orders extends Component {
], ],
}; };
return ( const tableProps = {
<div> dataSource: table_data.dataSource,
<Row gutter={{ sm: 16, lg: 32 }}> columns: table_data.columns,
<Col md={24} lg={12} xxl={14}></Col> size: 'small',
<Col md={24} lg={12} xxl={10}> pagination: false,
<Row> scroll: { x: (100*(table_data.columns.length)) },
<Col md={24} lg={8} xxl={8}> loading: orders_store.loading,
<GroupSelect store={orders_store} /> };
</Col>
<Col md={24} lg={8} xxl={8}> return (
<SiteSelect store={orders_store} show_all={true} /> <div>
</Col> <Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col md={24} lg={8} xxl={8}> <Col className="gutter-row" span={24}>
<Select style={{ width: "100%" }} placeholder="是否含门票" value={orders_store.include_tickets} onChange={orders_store.handleChange_include_tickets}> <SearchForm
<Select.Option key="1" value="1"> defaultValue={{
含门票 initialValue: {
</Select.Option> ...date_picker_store.formValues,
<Select.Option key="0" value="0"> ...orders_store.searchValues,
不含门票 },
</Select.Option> shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
</Select> fieldProps: {
</Col> DepartmentList: { show_all: false, mode: 'multiple' },
</Row> WebCode: { show_all: false, mode: 'multiple' },
<Row> years: { hide_vs: true },
<Col md={24} lg={8} xxl={8}> },
<DataTypeSelect store={orders_store} /> }}
</Col> onSubmit={(_err, obj, form, str) => {
<Col md={24} lg={12} xxl={12}> orders_store.setSearchValues(obj, form);
<DatePickerCharts /> orders_store.getOrderCount();
</Col> orders_store.onChange_Tabs(orders_store.active_tab_key);
<Col md={24} lg={4} xxl={4} className='align_right'> }}
<Button />
type="primary" </Col>
icon={<SearchOutlined />} </Row>
loading={orders_store.loading} <Row gutter={[16, { sm: 16, lg: 32 }]}>
onClick={() => { <Col span={24} style={{ textAlign: 'right' }}>
orders_store.getOrderCount();
orders_store.onChange_Tabs(orders_store.active_tab_key);
}}>
统计
</Button>
</Col>
</Row>
</Col>
</Row>
<Row gutter={[16, { sm: 16, lg: 32 }]} >
<Col span={24} style={{textAlign: 'right'}}>
<DateGroupRadio <DateGroupRadio
visible={data_source.length!==0} visible={data_source.length !== 0}
dataRaw={orders_store.orderCountDataRaw} dataRaw={orders_store.orderCountDataRaw}
onChange={orders_store.onChangeDateGroup} onChange={orders_store.onChangeDateGroup}
value={orders_store.lineChartXGroup} value={orders_store.lineChartXGroup}
@ -366,177 +386,126 @@ class Orders extends Component {
fieldMapper={orders_store.orderCountDataFieldMapper} fieldMapper={orders_store.orderCountDataFieldMapper}
/> />
</Col> </Col>
<Col span={24}> <Col span={24}>
<Line {...config} /> <Spin spinning={orders_store.loading}>
</Col> <Line {...config} />
</Spin>
</Col>
<Col span={24}> <Col span={24}>
<Tabs activeKey={orders_store.active_tab_key} onChange={active_key => orders_store.onChange_Tabs(active_key)}> <Tabs
<Tabs.TabPane activeKey={orders_store.active_tab_key}
tab={ onChange={(active_key) => orders_store.onChange_Tabs(active_key)}
<span> items={[
<ContainerOutlined /> {
来源类型 key: 'Form',
</span> label: (
} <span>
key="Form"> <ContainerOutlined />
<Table id="table_to_xlsx_form" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} /> 来源类型
<Divider orientation="right" plain> </span>
<a ),
onClick={() => { },
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_form").getElementsByTagName("table")[0]); {
writeFileXLSX(wb, "来源类型.xlsx"); key: 'Product',
}}> label: (
导出excel <span>
</a> <CarryOutOutlined />
</Divider> 产品类型
</Tabs.TabPane> </span>),
<Tabs.TabPane },
tab={ {
<span> key: 'Country',
<CarryOutOutlined /> label: (
产品类型 <span>
</span> <SmileOutlined />
} 国籍
key="Product"> </span>
<Table id="table_to_xlsx_product" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} /> ),
<Divider orientation="right" plain> },
<a {
onClick={() => { key: 'line',
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_product").getElementsByTagName("table")[0]); label: (
writeFileXLSX(wb, "产品类型.xlsx"); <span>
}}> <TagsOutlined />
导出excel 线路
</a> </span>
</Divider> ),
</Tabs.TabPane> },
<Tabs.TabPane {
tab={ key: 'city',
<span> label: (
<SmileOutlined /> <span>
国籍 <GlobalOutlined />
</span> 目的地
} </span>
key="Country"> ),
<Table id="table_to_xlsx_country" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} /> },
<Divider orientation="right" plain> {
<a key: 'LineClass',
onClick={() => { label: (
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_country").getElementsByTagName("table")[0]); <span>
writeFileXLSX(wb, "国籍.xlsx"); <BlockOutlined />
}}> 页面类型
导出excel </span>
</a> ),
</Divider> },
</Tabs.TabPane> {
<Tabs.TabPane key: 'GuestGroupType',
tab={ label: (
<span> <span>
<TagsOutlined /> <FullscreenOutlined />
线路 客群类别
</span> </span>
} ),
key="line"> },
<Table id="table_to_xlsx_line" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} /> {
<Divider orientation="right" plain> key: 'TravelMotivation',
<a label: (
onClick={() => { <span>
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_line").getElementsByTagName("table")[0]); <DingtalkOutlined />
writeFileXLSX(wb, "线路.xlsx"); 出行动机
}}> </span>
导出excel ),
</a> },
</Divider> {
</Tabs.TabPane> key: 'ToB',
<Tabs.TabPane label: (
tab={ <span>
<span> <ContactsOutlined />
<GlobalOutlined /> 客运类别
目的地 </span>
</span> ),
} },
key="city"> {key: 'FoodRequirement',label: (<span><CoffeeOutlined />饮食要求</span>),},
<Table id="table_to_xlsx_city" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} /> {key: 'hobbies',label: (<span><HeartOutlined/>兴趣爱好</span>),},
<Divider orientation="right" plain> {key: 'ages',label: (<span><IdcardOutlined/>年龄段</span>),},
<a ].map((ele) => {
onClick={() => { return {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_city").getElementsByTagName("table")[0]); ...ele,
writeFileXLSX(wb, "目的地.xlsx"); children: (
}}> <>
导出excel <Table sticky id={`table_to_xlsx_${ele.key}`} {...tableProps} />
</a> <Divider orientation="right" plain>
</Divider> <TableExportBtn label={ele.key} {...{ columns: tableProps.columns, dataSource: tableProps.dataSource }} />
</Tabs.TabPane> </Divider>
<Tabs.TabPane </>
tab={ ),
<span> };
<BlockOutlined /> })}
页面类型 />
</span> <Row>
} <Col sm={24} lg={12}>
key="LineClass"> <Pie {...pie_config} data={pie_data} />
<Table id="table_to_xlsx_LineClass" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} /> </Col>
<Divider orientation="right" plain> <Col sm={24} lg={12}>
<a <Pie {...pie_config} data={pie_data2} />
onClick={() => { </Col>
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_LineClass").getElementsByTagName("table")[0]); </Row>
writeFileXLSX(wb, "页面类型.xlsx"); </Col>
}}> </Row>
导出excel </div>
</a> );
</Divider>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<FullscreenOutlined />
客群类别
</span>
}
key="GuestGroupType">
<Table id="table_to_xlsx_GuestGroupType" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} />
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_GuestGroupType").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "客群类别.xlsx");
}}>
导出excel
</a>
</Divider>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<DingtalkOutlined />
出行动机
</span>
}
key="TravelMotivation">
<Table id="table_to_xlsx_TravelMotivation" dataSource={table_data.dataSource} columns={table_data.columns} size="small" pagination={false} scroll={{ x: "100%" }} />
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_TravelMotivation").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "出行动机.xlsx");
}}>
导出excel
</a>
</Divider>
</Tabs.TabPane>
</Tabs>
<Row>
<Col sm={24} lg={12}>
<Pie {...pie_config} data={pie_data} />
</Col>
<Col sm={24} lg={12}>
<Pie {...pie_config} data={pie_data2} />
</Col>
</Row>
</Col>
</Row>
</div>
);
} }
} }

@ -1,18 +1,16 @@
import React, { useContext, useEffect } from "react"; import React, { useContext, useEffect, useState } from "react";
import { Row, Col, Button, Tabs, Table, Select, Divider } from "antd"; import { Row, Col, Tabs, Table, Divider } from "antd";
import { ContainerOutlined, SearchOutlined } from "@ant-design/icons"; import { ContainerOutlined } from "@ant-design/icons";
import { stores_Context } from "../config"; import { stores_Context } from "../config";
import { Line } from "@ant-design/charts"; import { Line } from "@ant-design/charts";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import DatePickerCharts from "../components/search/DatePickerCharts";
import SiteSelect from "../components/search/SiteSelect";
import GroupSelect from "../components/search/GroupSelect";
import DataTypeSelect from "../components/search/DataTypeSelect";
import { NavLink, useParams } from "react-router-dom"; import { NavLink, useParams } from "react-router-dom";
import * as comm from "../utils/commons"; import * as comm from "../utils/commons";
import * as config from "../config"; import * as config from "../config";
import { utils, writeFileXLSX } from "xlsx"; import { utils, writeFileXLSX } from "xlsx";
import DateGroupRadio from '../components/DateGroupRadio'; import DateGroupRadio from '../components/DateGroupRadio';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const Orders_sub = () => { const Orders_sub = () => {
const { ordertype, ordertype_sub, ordertype_title } = useParams(); const { ordertype, ordertype_sub, ordertype_title } = useParams();
@ -151,8 +149,8 @@ const Orders_sub = () => {
}, },
{ {
title: "出发日期", title: "出发日期",
dataIndex: "COLI_OrderStartDate", dataIndex: "CGI_ArriveDate",
key: "COLI_OrderStartDate", key: "CGI_ArriveDate",
}, },
{ {
title: "客人需求", title: "客人需求",
@ -178,173 +176,152 @@ const Orders_sub = () => {
const table_data_p = format_data(orders_store.orderCountData_Form_sub.ordercount1); const table_data_p = format_data(orders_store.orderCountData_Form_sub.ordercount1);
const table_data2_p = format_data(orders_store.orderCountData_Form_sub.ordercount2); const table_data2_p = format_data(orders_store.orderCountData_Form_sub.ordercount2);
return (
<div>
<Row gutter={{ sm: 16, lg: 32 }}>
<Col md={24} lg={12} xxl={14}>
<NavLink to={`/orders`}>返回</NavLink>
</Col>
<Col md={24} lg={12} xxl={10}>
<Row>
<Col md={24} lg={8} xxl={8}>
<GroupSelect store={orders_store} />
</Col>
<Col md={24} lg={8} xxl={8}>
<SiteSelect store={orders_store} show_all={true} />
</Col>
<Col md={24} lg={8} xxl={8}>
<Select style={{ width: "100%" }} placeholder="是否含门票" value={orders_store.include_tickets} onChange={orders_store.handleChange_include_tickets}>
<Select.Option key="1" value="1">
含门票
</Select.Option>
<Select.Option key="0" value="0">
不含门票
</Select.Option>
</Select>
</Col>
</Row>
<Row>
<Col md={24} lg={8} xxl={8}>
<DataTypeSelect store={orders_store} />
</Col>
<Col md={24} lg={12} xxl={12}>
<DatePickerCharts />
</Col>
<Col md={24} lg={4} xxl={4} className="align_right">
<Button
type="primary"
icon={<SearchOutlined />}
loading={orders_store.loading}
onClick={() => {
orders_store.getOrderCount_type(ordertype, ordertype_sub);
orders_store.getOrderCountByType_sub(ordertype, ordertype_sub, orders_store.active_tab_key_sub);
}}>
统计
</Button>
</Col>
</Row>
</Col>
</Row>
<Row gutter={[16, { xs: 8, sm: 16, md: 24, lg: 32 }]}> const tab_items = [
<Col span={24} style={{textAlign: 'right'}}> {
<DateGroupRadio key: 'detail', label: <span><ContainerOutlined />订单内容</span>, title: '订单内容',
visible={data_source.length!==0} children: (<Row>
dataRaw={orders_store.orderCountDataRaw_type} <Col span={24}>
onChange={orders_store.onChangeDateGroupSub} {date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
value={orders_store.orderCount_type_dateRadio.lineChartXGroup} <Table
dataMapper={orders_store.orderCount_type_dateRadio.orderCountDataMapper} id="table_to_xlsx_form"
fieldMapper={orders_store.orderCount_type_dateRadio.orderCountDataFieldMapper} dataSource={table_data.dataSource}
/> columns={table_data.columns}
</Col> size="small"
<Col className="gutter-row" span={24}> pagination={false}
<Line {...line} /> rowKey={record => record.key}
</Col> expandable={{
expandedRowRender: record => (
<pre>
<Divider orientation="left" plain>
客户需求
</Divider>
{record.COLI_CustomerRequest}
<Divider orientation="left" plain>
订单内容
</Divider>
{record.COLI_OrderDetailText}
</pre>
),
}}
/>
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_form").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "订单列表.xlsx");
}}>
导出excel
</a>
</Divider>
</Col>
<Col span={24}>
{date_picker_store.start_date_cp ? date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "~" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) : ""}
<Table
dataSource={table_data2.dataSource}
columns={table_data2.columns}
size="small"
rowKey={record => record.key}
expandable={{
expandedRowRender: record => <pre>{record.COLI_OrderDetailText}</pre>,
}}
/>
</Col>
</Row>),
},
{ key: 'page', label: <span><ContainerOutlined />访问路径</span>, title: '访问路径',children: (<Row>
<Col span={24}>
{date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
<Table dataSource={table_data_p.dataSource} rowKey={record => record.key} columns={table_data_p.columns} size="small" />
</Col>
<Col className="gutter-row" span={24}> <Col span={24}>
<Tabs activeKey={orders_store.active_tab_key_sub} onChange={active_key => orders_store.onChange_Tabs_sub(ordertype, ordertype_sub, active_key)}> {date_picker_store.start_date_cp ? date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "~" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) : ""}
<Tabs.TabPane <Table dataSource={table_data2_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" />
tab={ </Col>
<span> </Row>)},
<ContainerOutlined /> { key: 'page_cxstate1', label: <span><ContainerOutlined />访问路径成行</span>, title: '访问路径(成行)',children: (<Row>
订单内容 <Col span={24}>
</span> {date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
} <Table dataSource={table_data_p.dataSource} rowKey={record => record.key} columns={table_data_p.columns} size="small" />
key="detail"> </Col>
<Row>
<Col span={24}>
{date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
<Table
id="table_to_xlsx_form"
dataSource={table_data.dataSource}
columns={table_data.columns}
size="small"
pagination={false}
rowKey={record => record.key}
expandable={{
expandedRowRender: record => (
<pre>
<Divider orientation="left" plain>
客户需求
</Divider>
{record.COLI_CustomerRequest}
<Divider orientation="left" plain>
订单内容
</Divider>
{record.COLI_OrderDetailText}
</pre>
),
}}
/>
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_form").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "订单列表.xlsx");
}}>
导出excel
</a>
</Divider>
</Col>
<Col span={24}> <Col span={24}>
{date_picker_store.start_date_cp ? date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "~" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) : ""} {date_picker_store.start_date_cp ? date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "~" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) : ""}
<Table <Table dataSource={table_data2_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" />
dataSource={table_data2.dataSource} </Col>
columns={table_data2.columns} </Row>)},
size="small" ];
rowKey={record => record.key}
expandable={{ const [propsForExport, setPropsForExport] = useState(tab_items[0]);
expandedRowRender: record => <pre>{record.COLI_OrderDetailText}</pre>, const tabItemsMapped = tab_items.reduce((r, v) => ({...r, [v.key]: v}), {});
}} const onTabsChange = (active_key) => {
/> setPropsForExport(tabItemsMapped[active_key]);
</Col> orders_store.onChange_Tabs_sub(ordertype, ordertype_sub, active_key);
</Row> };
</Tabs.TabPane> return (
<Tabs.TabPane <div>
tab={ <Row gutter={{ sm: 16, lg: 32 }} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<span> <Col md={24} lg={12} xxl={14}>
<ContainerOutlined /> <NavLink to={`/orders`}>返回</NavLink>
访问路径 </Col>
</span> <Col className="gutter-row" span={24}>
} <SearchForm
key="page"> defaultValue={{
<Row> initialValue: {
<Col span={24}> ...date_picker_store.formValues,
{date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)} },
<Table dataSource={table_data_p.dataSource} rowKey={record => record.key} columns={table_data_p.columns} size="small" /> shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
</Col> fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
// dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
orders_store.setSearchValues(obj, form);
orders_store.getOrderCount_type(ordertype, ordertype_sub);
orders_store.getOrderCountByType_sub(ordertype, ordertype_sub, orders_store.active_tab_key_sub);
}}
/>
</Col>
</Row>
<Col span={24}> <Row gutter={[16, { xs: 8, sm: 16, md: 24, lg: 32 }]}>
{date_picker_store.start_date_cp ? date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "~" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) : ""} <Col span={24} style={{ textAlign: 'right' }}>
<Table dataSource={table_data2_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" /> <DateGroupRadio
</Col> visible={data_source.length !== 0}
</Row> dataRaw={orders_store.orderCountDataRaw_type}
</Tabs.TabPane> onChange={orders_store.onChangeDateGroupSub}
<Tabs.TabPane value={orders_store.orderCount_type_dateRadio.lineChartXGroup}
tab={ dataMapper={orders_store.orderCount_type_dateRadio.orderCountDataMapper}
<span> fieldMapper={orders_store.orderCount_type_dateRadio.orderCountDataFieldMapper}
<ContainerOutlined /> />
访问路径成行 </Col>
</span> <Col className="gutter-row" span={24}>
} <Line {...line} />
key="page_cxstate1"> </Col>
<Row>
<Col span={24}>
{date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
<Table dataSource={table_data_p.dataSource} rowKey={record => record.key} columns={table_data_p.columns} size="small" />
</Col>
<Col span={24}> <Col className="gutter-row" span={24}>
{date_picker_store.start_date_cp ? date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "~" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) : ""} <Tabs
<Table dataSource={table_data2_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" /> activeKey={orders_store.active_tab_key_sub}
</Col> onChange={onTabsChange}
</Row> tabBarExtraContent={{
</Tabs.TabPane> right: (
</Tabs> <TableExportBtn
</Col> label={propsForExport.title}
</Row> {...(orders_store.active_tab_key_sub === 'detail'
</div> ? { columns: table_data.columns, dataSource: table_data.dataSource }
); : { columns: table_data_p.columns, dataSource: table_data_p.dataSource })}
/>
),
}}
items={tab_items}
/>
</Col>
</Row>
</div>
);
}; };
export default observer(Orders_sub); export default observer(Orders_sub);

@ -1,36 +1,87 @@
import React, {useContext, useEffect} from 'react'; import { useContext } from 'react';
import {Row, Col, Button, Tabs, Spin, Result, Space} from 'antd'; import { Button, Result, message, Typography, Image } from 'antd';
import { import { stores_Context } from '../config';
ContainerOutlined, import { observer } from 'mobx-react';
SearchOutlined, import { Outlet, useLocation } from 'react-router-dom';
} from '@ant-design/icons'; import authExample from './../auth-apply.png';
import {stores_Context} from '../config';
import {Line} from "@ant-design/charts";
import {observer} from 'mobx-react';
import DatePickerCharts from '../components/search/DatePickerCharts';
import {NavLink, useParams,Outlet, useOutlet, useLocation, useNavigate} from "react-router-dom";
import * as comm from "../utils/commons";
import * as config from "../config";
const ProtectedRoute = ({auth}) => { const { Text } = Typography;
const {auth_store} = useContext(stores_Context);
if (auth_store.has_permission(auth)) { const ProtectedRoute = ({ auth }) => {
return <Outlet/>; const { auth_store } = useContext(stores_Context);
}
return ( if (auth_store.has_permission(auth)) {
<div> return <Outlet />;
<Result }
status="403"
title="403 权限不足"
subTitle={"试着联系一下技术,所需权限: "+auth.toString()}
extra=''
/>
</div>
);
const authApplyLink =
// eslint-disable-next-line max-len
'dingtalk://dingtalkclient/action/openapp?app_id=-4&container_type=work_platform&corpid=ding48bce8fd3957c96b&ddtab=true&redirect_type=jump&redirect_url=https%3A%2F%2Faflow.dingtalk.com%2Fdingtalk%2Fmobile%2Fhomepage.htm%3Fback_control%3Dfalse%26backcontrol%3Dfalse%26corpid%3Dding48bce8fd3957c96b%26dd_progress%3Dfalse%26dd_share%3Dfalse%26ddtab%3Dtrue%26showmenu%3Dfalse%23%2Fcustom%3Fpcredirect%3Dself%26processCode%3DPROC-C0C2E970-1E4E-44CF-A389-C7D3F10A7885';
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
message.success('已复制到剪贴板');
});
};
const { pathname } = useLocation();
const applyInfo = `授权账户: ${auth_store.user.name}(${auth_store.user.userid})\n申请权限: ${auth[auth.length - 1].toString()}\n请求页面: ${pathname}`;
return (
<div>
{/* '试着联系一下技术,所需权限: ' + auth.toString() */}
<Result
status={auth_store.user.name === 'loading' ? '500' : '403'}
title={auth_store.user.name === 'loading' ? '无服务' : '403 权限不足'}
// title="403 "
subTitle={
auth_store.user.name !== 'loading' ? (
<>
<div style={{ width: 300, textAlign: 'left', margin: 'auto auto' }}>
<div>
<Text type={'danger'} strong>
申请步骤:
</Text>
</div>
<ol style={{ padding: '0 1rem' }}>
<li>
复制以下信息
{/* <Button type="link" onClick={() => copyToClipboard(applyInfo)}>
复制
</Button> */}
</li>
{/* <li> */}
<pre className={'p-s1'} style={{ border: '1px solid rgba(0,0,0,.15)', borderRadius: 4 }}>
{applyInfo}
</pre>
{/* </li> */}
<li>
<Button type="link" href={authApplyLink}>
点击打开 &raquo;OA审批 &raquo;
</Button>
<ul>
<li>输入申请信息(姓名等)</li>
<li>
选择开通权限: <Text type={'warning'}>HT系统分析</Text>
</li>
<li>
填入上述复制的信息到 <Text type={'warning'}>权限内容</Text>
</li>
</ul>
</li>
</ol>
<div>
<Text type={'secondary'} strong>
示例:
</Text>
</div>
<Image alt="example" src={authExample} preview={false} />
</div>
</>
) : null
}
/>
</div>
);
}; };
export default observer(ProtectedRoute); export default observer(ProtectedRoute);

@ -1,17 +1,13 @@
import React, { useContext, useEffect } from 'react'; import React, { useContext } from 'react';
import { Row, Col, Button, Tabs, Table, Divider, Radio, Select } from 'antd'; import { Row, Col, Tabs, Table, Divider, Spin } from 'antd';
import { ContainerOutlined, SearchOutlined, UserSwitchOutlined } from '@ant-design/icons'; import { ContainerOutlined, UserSwitchOutlined } from '@ant-design/icons';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { Column, Pie, Treemap } from '@ant-design/charts'; import { Column, Pie } from '@ant-design/charts';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import DatePickerCharts from '../components/search/DatePickerCharts';
import DataTypeSelect from '../components/search/DataTypeSelect';
import { NavLink, useParams } from 'react-router-dom';
import * as comm from '../utils/commons'; import * as comm from '../utils/commons';
import * as config from '../config';
import SiteSelect from '../components/search/SiteSelect';
import GroupSelect from '../components/search/GroupSelect';
import { utils, writeFileXLSX } from 'xlsx'; import { utils, writeFileXLSX } from 'xlsx';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const Sale = () => { const Sale = () => {
const { sale_store, date_picker_store } = useContext(stores_Context); const { sale_store, date_picker_store } = useContext(stores_Context);
@ -52,6 +48,10 @@ const Sale = () => {
...config_data, ...config_data,
...{ ...{
//seriesField: "OPI_Name",// //seriesField: "OPI_Name",//
columnWidthRatio: 0.28,
// dodgePadding: 1,
// minColumnWidth: 5,
// maxColumnWidth: 15,
label: { label: {
position: 'top', position: 'top',
}, },
@ -204,51 +204,35 @@ const Sale = () => {
), ),
}, },
]; ];
const tableColumns = type_data.columns.map((ele, i) =>
i === 0 ? { ...ele, fixed: 'left', children: (ele?.children || []).map((ele_child, i_child) => (i_child === 0 ? { ...ele_child, fixed: 'left' } : { ...ele_child })) } : { ...ele }
);
return ( return (
<div> <div>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}> <Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col md={24} lg={12} xxl={14}></Col> <Col className="gutter-row" span={24}>
<Col md={24} lg={12} xxl={10}> <SearchForm
<Row> defaultValue={{
<Col md={24} lg={8} xxl={8}> initialValue: {
<GroupSelect store={sale_store} /> ...date_picker_store.formValues,
</Col> ...sale_store.searchValues,
<Col md={24} lg={8} xxl={8}> },
<SiteSelect store={sale_store} show_all={true} /> shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
</Col> fieldProps: {
<Col md={24} lg={8} xxl={8}> DepartmentList: { show_all: false, mode: 'multiple' },
<Select style={{ width: '100%' }} placeholder="是否含门票" value={sale_store.include_tickets} onChange={sale_store.handleChange_include_tickets}> WebCode: { show_all: false, mode: 'multiple' },
<Select.Option key="1" value="1"> years: { hide_vs: true },
含门票 operator: { mode: 'multiple'},
</Select.Option> },
<Select.Option key="0" value="0"> }}
不含门票 onSubmit={(_err, obj, form, str) => {
</Select.Option> sale_store.setSearchValues(obj, form);
</Select> sale_store.get_department_order_ml_by_type(date_picker_store);
</Col> }}
</Row> />
<Row> </Col>
<Col md={24} lg={8} xxl={8}> </Row>
<DataTypeSelect store={sale_store} />
</Col>
<Col md={24} lg={12} xxl={12}>
<DatePickerCharts />
</Col>
<Col md={24} lg={4} xxl={4} className="align_right">
<Button
type="primary"
icon={<SearchOutlined />}
loading={sale_store.loading}
onClick={() => {
// sale_store.get_department_order_ml(date_picker_store);
sale_store.get_department_order_ml_by_type(date_picker_store);
}}
>
统计
</Button>
</Col>
</Row>
{/* <Row> {/* <Row>
<Col md={24} lg={12} xxl={12}> <Col md={24} lg={12} xxl={12}>
@ -284,12 +268,11 @@ const Sale = () => {
</Select> </Select>
</Col> </Col>
</Row> */} </Row> */}
</Col>
</Row>
<Row> <Row>
<Col className="gutter-row" md={24}> <Col className="gutter-row" md={24}>
<Column {...column_config} /> <Spin spinning={sale_store.loading_table} >
<Column {...column_config} /></Spin>
</Col> </Col>
<Col className="gutter-row" md={24}> <Col className="gutter-row" md={24}>
@ -303,25 +286,18 @@ const Sale = () => {
></Tabs> ></Tabs>
<Row> <Row>
<Col span={24}> <Col span={24}>
<Table <Table sticky
id="table_to_xlsx_sale" id="table_to_xlsx_sale"
dataSource={type_data.dataSource} dataSource={type_data.dataSource}
columns={type_data.columns} columns={tableColumns}
size="small" size="small"
rowKey={(record) => record.key} rowKey={(record) => record.key}
loading={sale_store.loading_table} loading={sale_store.loading_table}
pagination={false} pagination={false}
scroll={{ x: '100%' }} scroll={{ x: (100*(tableColumns.length)) }}
/> />
<Divider orientation="right" plain> <Divider orientation="right" plain>
<a <TableExportBtn label={'sale'} {...{ columns: tableColumns, dataSource: type_data.dataSource }} />
onClick={() => {
const wb = utils.table_to_book(document.getElementById('table_to_xlsx_sale').getElementsByTagName('table')[0]);
writeFileXLSX(wb, 'sale.xlsx');
}}
>
导出excel
</a>
</Divider> </Divider>
</Col> </Col>
<Col sm={24} lg={12}> <Col sm={24} lg={12}>

@ -1,31 +1,38 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useState } from 'react';
import { Row, Col, Button, Tabs, Table, Divider, Radio, Select, Space, Typography, Progress } from 'antd'; import { Row, Col, Table, Select, Space, Typography, Progress, Spin, Divider, Button, Switch } from 'antd';
import { ContainerOutlined, SearchOutlined, UserSwitchOutlined } from '@ant-design/icons';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { NavLink, useParams } from 'react-router-dom';
import * as comm from '../utils/commons'; import * as comm from '../utils/commons';
import * as config from '../config';
import { utils, writeFileXLSX } from 'xlsx';
import SearchForm from './../components/search/SearchForm'; import SearchForm from './../components/search/SearchForm';
import { dataFieldAlias } from '../libs/ht'; import { dataFieldAlias, overviewGroup } from '../libs/ht';
import Donut from './../components/Donut';
import LineWithKPI from '../components/LineWithKPI';
import { TableExportBtn } from './../components/Data';
const { Text } = Typography; const { Text } = Typography;
const overviewGroupKeys = overviewGroup.map(item => item.key);
const Sale_KPI = () => { const Sale_KPI = () => {
const { sale_store, date_picker_store: searchFormStore } = useContext(stores_Context); const { sale_store, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues } = searchFormStore; const { formValues, siderBroken } = searchFormStore;
const { groupType, loading, operator } = sale_store.salesTrade; const { groupType, loading, operator, tableDataSource: dataSource } = sale_store.salesTrade;
const dataSource = [].concat(sale_store.salesTrade[groupType], operator); const yearData = sale_store.salesTrade[groupType].reduce((r, ele) => r.concat(Object.values(ele.mData)), []);
const operatorObjects = operator.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel }));
const pageRefresh = (queryData) => { const pageRefresh = async (queryData) => {
const overviewFlag = queryData.DepartmentList.toLowerCase() === 'all' || queryData.DepartmentList.toLowerCase().includes(','); const overviewFlag = queryData.DepartmentList.toLowerCase() === 'all'
|| overviewGroupKeys.includes(queryData.DepartmentList.toLowerCase()); // queryData.DepartmentList.toLowerCase().includes(',');
const _groupType = overviewFlag ? 'overview' : 'dept'; const _groupType = overviewFlag ? 'overview' : 'dept';
sale_store.setGroupType(_groupType); sale_store.setGroupType(_groupType);
sale_store.fetchOperatorTradeData(_groupType, { ...queryData, groupDateType: 'year' }); await sale_store.fetchOperatorTradeData(_groupType, { ...queryData, groupDateType: 'year' });
sale_store.fetchOperatorTradeData('operator', { ...queryData, groupDateType: 'year' }); await sale_store.fetchOperatorTradeData('operator', { ...queryData, groupDateType: 'year' });
sale_store.setPickSales([]);
sale_store.setTableDataSource(false);
setIfmerge(false);
}; };
const [ifmerge, setIfmerge] = useState(false);
const monthCol = new Array(12).fill(1).map((_, index) => { const monthCol = new Array(12).fill(1).map((_, index) => {
return { return {
title: `${index + 1}`, title: `${index + 1}`,
@ -34,11 +41,13 @@ const Sale_KPI = () => {
width: '7.5em', width: '7.5em',
render: (_, row) => ( render: (_, row) => (
<Space direction={'vertical'}> <Space direction={'vertical'}>
{/* 目标 */}
<div> <div>
<Text italic type={'secondary'}> <Text italic type={'secondary'}>
{dataFieldAlias.SumML.formatter(row.mData[`month_${String(index + 1).padStart(2, '0')}`]?.MLKPIvalue || 0)} {dataFieldAlias.SumML.formatter(row.mData[`month_${String(index + 1).padStart(2, '0')}`]?.MLKPIvalue || 0)}
</Text> </Text>
</div> </div>
{/* 完成 */}
<div>{dataFieldAlias.SumML.formatter(row.mData[`month_${String(index + 1).padStart(2, '0')}`]?.SumML || 0)}</div> <div>{dataFieldAlias.SumML.formatter(row.mData[`month_${String(index + 1).padStart(2, '0')}`]?.SumML || 0)}</div>
{row.mData[`month_${String(index + 1).padStart(2, '0')}`]?.MLKPIrates || 0 ? ( {row.mData[`month_${String(index + 1).padStart(2, '0')}`]?.MLKPIrates || 0 ? (
<Progress <Progress
@ -66,11 +75,13 @@ const Sale_KPI = () => {
dataIndex: 'groupsLabel', dataIndex: 'groupsLabel',
editable: false, editable: false,
width: '7.5em', width: '7.5em',
fixed: 'left',
}, },
{ {
title: '年度', title: '年度',
dataIndex: 'yearValue', dataIndex: 'yearValue',
width: '10em', width: '10em',
fixed: 'left',
render: (_, row) => ( render: (_, row) => (
<Space direction={'vertical'}> <Space direction={'vertical'}>
<div> <div>
@ -98,44 +109,152 @@ const Sale_KPI = () => {
}, },
...monthCol, ...monthCol,
]; ];
const columnsForExport = [
{
title: `#`,
dataIndex: 'groupsLabel',
editable: false,
width: '7.5em',
fixed: 'left',
},
{
title: '--',
dataIndex: 'rowLabel',
width: '10em',
fixed: 'left',
},
{
title: '年度',
dataIndex: 'yearML',
width: '10em',
fixed: 'left',
},
...new Array(12).fill(1).map((_, index) => {
return {
title: `${index + 1}`,
dataIndex: ['mData', `month_${String(index + 1).padStart(2, '0')}`], // , 'SumML'
valueType: 'digit',
width: '7.5em',
};
}),
];
const dataForExport = dataSource.reduce((r, ele) => {
const targetRow = {
groupsLabel: ele.groupsLabel,
rowLabel: '目标',
yearML: ele.yData.MLKPIvalue,
mData: Object.values(ele.mData).reduce((rt, et) => ({ ...rt, [`month_${et.dateVal}`]: et.MLKPIvalue }), {}),
};
const valRow = { groupsLabel: ele.groupsLabel, rowLabel: '完成', yearML: ele.yData.SumML, mData: Object.values(ele.mData).reduce((rt, et) => ({ ...rt, [`month_${et.dateVal}`]: et.SumML }), {}) };
const processRow = {
groupsLabel: ele.groupsLabel,
rowLabel: '进度(%)',
yearML: ele.yData.MLKPIrates,
mData: Object.values(ele.mData).reduce((rt, et) => ({ ...rt, [`month_${et.dateVal}`]: et.MLKPIrates }), {}),
};
r.push(targetRow, valRow, processRow);
return r;
}, []);
const lineConfig = { appendPadding: 10, xField: 'groupDateVal', yField: 'SumML', seriesField: 'groupsLabel', isGroup: true, smooth: false, meta: comm.cloneDeep(dataFieldAlias), };
return ( return (
<div> <div>
<Row gutter={16} style={{ margin: '-16px -8px' }}> <Row gutter={16} className={siderBroken ? "" : "sticky-top"}>
<Col md={24} lg={24} xxl={24}> <Col md={24} lg={24} xxl={24}>
<SearchForm <SearchForm
defaultValue={{ defaultValue={{
initialValue: { initialValue: {
...formValues, ...formValues,
...sale_store.searchValues,
}, },
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'], shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'],
fieldProps: { fieldProps: {
DepartmentList: { show_all: true }, DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
WebCode: { show_all: true }, WebCode: { show_all: false, mode: 'multiple', col: 5 },
years: { hide_vs: true }, years: { hide_vs: true },
}, },
}} }}
onSubmit={(_err, obj, form, str) => { onSubmit={(_err, obj, form, str) => {
sale_store.setSearchValues(obj, form);
pageRefresh(obj); pageRefresh(obj);
}} }}
/> />
</Col> </Col>
</Row> </Row>
<Spin spinning={loading}>
<h2 style={{ marginTop: '.5em' }}>年度业绩组成和走势</h2>
<Row>
<Col className="gutter-row" md={8} sm={24} xs={24}>
<Donut
{...{ angleField: 'SumML', colorField: 'groupsLabel', label1: { style: { color: '#000000' }, type: 'spider', content: '{name}\n{percentage}' }, legend: false, label2: false }}
title={formValues.DepartmentList?.label}
dataSource={operator.map((row) => ({ ...row, SumML: row.yearML }))}
/>
</Col>
<Col className="gutter-row" md={16} sm={24} xs={24}>
<LineWithKPI dataSource={yearData} showKPI={true} {...lineConfig} {...{ legend: false }} />
</Col>
<Col className="gutter-row" md={24}>
<Row gutter={16}>
<Col flex={'12em'}><h2>顾问业绩走势</h2></Col>
<Col flex={'auto'}>
<Select
labelInValue
mode={'multiple'}
style={{ width: '100%' }}
placeholder={'选择顾问'}
onChange={sale_store.setPickSales}
value={sale_store.salesTrade.pickSales}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={true}
>
{operatorObjects.map((ele) => (
<Select.Option key={ele.key} value={ele.value}>
{ele.label}
</Select.Option>
))}
</Select>
</Col>
</Row>
</Col>
<Col className="gutter-row" span={24}>
<LineWithKPI dataSource={sale_store.salesTrade.pickSalesData} showKPI={true} {...lineConfig} />
</Col>
</Row>
<Row> <Row>
<Col className="gutter-row" md={24}> <Col className="gutter-row" md={24}>
<Table <Divider orientation="right">
key={`salesTradeTable`} {dataSource.length > 0 && (
loading={loading} <Switch
columns={columns} unCheckedChildren="各账户"
rowKey="groupsKey" checkedChildren="合并"
scroll={{ key={'openOrMerge'}
x: 1000, checked={ifmerge}
}} onChange={(e) => {
dataSource={dataSource} sale_store.setTableDataSource(e);
pagination={false} setIfmerge(e);
/> }}
</Col> />
</Row> )}
<TableExportBtn label={'sales kpi'} {...{ columns: columnsForExport, dataSource: dataForExport }} />
</Divider>
<Table
sticky={{ offsetHeader: 64 }}
key={`salesTradeTable`}
loading={loading}
columns={columns}
rowKey="groupsKey"
scroll={{
x: 1000,
}}
dataSource={dataSource}
pagination={false}
/>
</Col>
</Row>
</Spin>
</div> </div>
); );
}; };

@ -0,0 +1,202 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Switch, Space, Tabs } from 'antd';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn, VSTag } from './../components/Data';
import { fixTo2Decimals, isEmpty } from './../utils/commons';
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
const SalesCustomerCareRegular = (props) => {
const { date_picker_store: searchFormStore, customer_store } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { pivotData: pageData } = customer_store.sales_regular_data;
const pivotOptions = [{key: 'operatorName', label: '顾问'}, {key: 'country', label: '国家'}];
const [pivotRow, setPivotRow] = useState('operatorName');
const onTabsChange = (key) => {
setPivotRow(key);
};
const [dataSource, setDataSource] = useState([]);
const [ifmerge, setIfmerge] = useState(false);
const [dataForExport, setDataForExport] = useState([]);
const [dataForExportS, setDataForExportS] = useState([]);
useEffect(() => {
if ( ! ifmerge) {
setDataSource(pageData[pivotRow].data);
setDataForExport(
pageData[pivotRow].data.reduce(
(r, c) =>
r.concat(
[{ ...c, children: undefined }],
c.children
.reduce((rc, ele) => rc.concat([{ ...ele, [pivotRow]: ele.rowLabel }], [{ ...ele.vsData, [pivotRow]: ele.vsData.rowLabel, vsData: {} }]), [])
.filter((ele) => ele.SumOrder !== undefined)
),
[]
)
);
setDataForExportS(pageData[pivotRow].data.reduce((r, c) => r.concat([{...c, children: undefined}], [{ ...c.vsData, vsData: {} }]), []).filter((ele) => ele.SumOrder !== undefined));
} else {
setDataSource(pageData[pivotRow].mergedData);
setDataForExport(
pageData[pivotRow].mergedData.reduce(
(r, c) =>
r.concat(
[{ ...c, children: undefined }],
c.children.reduce((rc, ele) => rc.concat([{ ...ele, operatorName: ele.rowLabel }], [{ ...ele.vsData, operatorName: ele.vsData.rowLabel, vsData: {} }]), [])
.filter((ele) => ele.SumOrder !== undefined)
),
[]
)
);
setDataForExportS(pageData[pivotRow].mergedData.reduce((r, c) => r.concat([{...c, children: undefined}], [{ ...c.vsData, vsData: {} }]), []).filter((ele) => ele.SumOrder !== undefined));
}
return () => {};
}, [ifmerge, pageData[pivotRow].data]);
const rowColumns = [
{ title: '日期区间', dataIndex: 'seriesKey', key: 'seriesKey' },
{ title: '顾问', dataIndex: 'operatorName', key: 'operatorName' },
{ title: '订单号', dataIndex: 'o_id', key: 'o_id' },
{ title: '预定日期', dataIndex: 'applyDate', key: 'applyDate' },
{ title: '订单状态', key: 'orderState', render: (_, r) => r.orderState === '1' ? '成行' : '', },
{ title: '毛利', dataIndex: 'ML', key: 'ML' },
{ title: '人数', dataIndex: 'personNum', key: 'personNum' },
{ title: '天数', dataIndex: 'tourdays', key: 'tourdays' },
// { title: '', dataIndex: 'CGI_PersonDays', key: 'CGI_PersonDays' },
// { title: '', dataIndex: 'COLI_OrderStartDate', key: 'COLI_OrderStartDate' },
{ title: '小组', dataIndex: 'dept', key: 'dept' },
{ title: '老客户', key: 'IsOld', render: (_, r) => r.IsOld === '1' ? '是' : '' },
{ title: '老客户推荐', key: 'IsCusCommend', render: (_, r) => r.isCusCommend === '1' ? '是' : '' },
{ title: '网站', dataIndex: 'WebCode', key: 'WebCode' },
{ title: '来源', dataIndex: 'SourceType', key: 'SourceType' },
{ title: '页面类型', dataIndex: 'COLI_LineClass', key: 'COLI_LineClass' },
];
const calcDelta = (r, key) => !isEmpty(Number(r.vsData[key])) ? fixTo2Decimals((Number(r[key] || 0) - Number(r.vsData[key]))/Number(r.vsData[key]) *100) : null;
const renderVS = (v, r, key) => {
const delta = calcDelta(r, key);
return <>
<Space direction={'vertical'}>
<span>
{v || 0}
{r.vsData[key] ? <span type="secondary"> VS {r.vsData[key]}</span> : null}
</span>
{delta && <VSTag diffPercent={delta} />}
</Space>
</>;
};
const columns = [
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em', render: (v, r) => renderVS(v, r, 'SumOrder') },
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmOrder') },
{ key: 'ConfirmPersonNum', title: '✔人数(SUM)', dataIndex: 'ConfirmPersonNum', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmPersonNum') },
{ key: 'confirmTourdays', title: '✔团天数(AVG)', dataIndex: 'confirmTourdays', width: '5em', render: (v, r) => renderVS(v, r, 'confirmTourdays') },
{ key: 'SumML', title: '预计毛利', dataIndex: 'SumML', width: '5em', render: (v, r) => renderVS(v, r, 'SumML') }, // SumML_txt
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmRates') },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em', render: (v, r) => renderVS(v, r, 'SingleML') },
];
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...customer_store.sales_regular_data.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates', 'IncludeTickets'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: false },
},
}}
onSubmit={(_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'sales_regular_data');
customer_store.get_sales_regular_data_vs(obj, pivotRow);
}}
/>
</Col>
</Row>
<Tabs
type={'card'}
activeKey={pivotRow}
onChange={onTabsChange}
items={pivotOptions.map((ele) => {
return {
...ele,
children: (
<>
{/* <h2>{ele.label}-老客户, 含推荐</h2> */}
<>
<Divider orientation={'right'} style={{backgroundColor: '#fff', margin: 0, padding: '10px 0'}} >
{pageData[pivotRow].data.length > 0 && pivotRow === 'operatorName' && (
<Switch
unCheckedChildren="各账户"
checkedChildren="合并"
key={'openOrMerge'}
checked={ifmerge}
onChange={(e) => {
setIfmerge(e);
}}
/>
)}
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出明细"
label={`${formValuesToSub.Date1}-老客户-明细`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...rowColumns], dataSource: pageData[pivotRow].rawData }}
/>
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出下表-展开"
label={`${formValuesToSub.Date1}-${ele.label}.老客户`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExport }}
/>
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出下表-总"
label={`${formValuesToSub.Date1}-${ele.label}.老客户`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExportS }}
/>
</Divider>
</>
<Table
sticky
dataSource={dataSource}
loading={pageData[ele.key].loading}
columns={[
{
key: ele.key,
title: ele.label,
dataIndex: ele.key,
width: '6em',
filters: pageData[ele.key].filterColValues,
onFilter: (value, record) => value.includes(record[ele.key]),
filterSearch: true,
},
...columns,
]}
pagination={false}
/>
</>
),
};
})}
/>
</>
);
};
export default observer(SalesCustomerCareRegular);

@ -0,0 +1,123 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Spin, Table, Row, Col, Tabs, Switch } from 'antd';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
import { empty } from '../utils/commons';
import './kpi.css';
const apartOptions = [
{ key: 'inbound', value: 'inbound', label: '入境' },
{ key: 'outbound', value: 'outbound', label: '出境' },
{ key: 'domestic', value: 'domestic', label: '国内' },
];
const apartOptionsMapped = apartOptions.reduce((r, v) => ({...r, [v.value]: v}), {});
export default observer((props) => {
const { financial_store: financialStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { servicePersonNum } = financialStore;
const { curTab } = servicePersonNum;
const [ifNull, setIfNull] = useState(false);
useEffect(() => {
// DistributionStore.setFormDates(formValuesToSub);
financialStore.resetPersonNumData();
return () => {};
}, [formValuesToSub]);
const pageRefresh = (queryData = formValuesToSub) => {
// console.log(queryData, 'qqqq');
financialStore.getPersonNum(queryData);
financialStore.setPersonNumTableDataSource(false);
setIfNull(false);
};
const columns = [
{ title: apartOptionsMapped[curTab].label, dataIndex: 'groupsLabel', children: [{ title: `${formValuesToSub.Date1}~${formValuesToSub.Date2.substring(0, 10)}`, dataIndex: 'groupsLabel' }] },
{
title: '人次数',
children: [{ title: '组织', dataIndex: 'orgz' }, ...(['inbound', 'domestic'].includes(curTab) ? [{ title: '接待', dataIndex: 'hosts' }] : [])],
},
{
title: '人天',
children: [{ title: '组织', dataIndex: 'orgzPDays' }, ...(['inbound', 'domestic'].includes(curTab) ? [{ title: '接待', dataIndex: 'hostsPDays' }] : [])],
},
];
return (
<>
<Row gutter={16} style={{ margin: '-16px -8px', position: 'sticky', top: 0, zIndex: 10 }}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
DateType: { key: 'startDate', value: 'startDate', label: '走团日期' },
},
shows: ['dates'],
fieldProps: {
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Tabs
activeKey={curTab}
onChange={(v) => {
financialStore.setCurTab(v);
if (empty(servicePersonNum[v].dataSource)) {
pageRefresh();
}
}}
tabBarExtraContent={{
right: (
<>
<Switch
unCheckedChildren="原数据"
checkedChildren="去除空"
key={'ifNull'}
checked={ifNull}
onChange={(e) => {
financialStore.setPersonNumTableDataSource(e);
setIfNull(e);
}}
/>
<TableExportBtn label={'服务人数_'+apartOptionsMapped[curTab].label} {...{ columns, dataSource: servicePersonNum[curTab].dataSource }} />
</>
),
}}
type="card"
items={apartOptions.map((ele) => {
return {
...ele,
children: (
<Spin spinning={servicePersonNum.loading}>
<Table
id="table_to_xlsx_sale"
dataSource={servicePersonNum[curTab].dataSource}
rowKey="groupsKey"
columns={columns}
size="small"
loading={servicePersonNum[curTab].loading}
pagination={false}
scroll={{ x: '100%' }}
/>
</Spin>
),
};
})}
/>
</section>
</>
);
});

@ -0,0 +1,19 @@
import { observer } from 'mobx-react';
import moment from 'moment';
import { APP_VERSION } from '../config';
import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined } from '@ant-design/icons';
export default observer((props) => {
const compileTime = moment(Number(process.env.REACT_APP_BUILD_TIME)).format('YYYY-MM-DD ddd HH:mm:ss');
return (
<>
<div>
<SketchOutlined /> <AntCloudOutlined /> <SlackOutlined /> <RedditOutlined /> <GithubOutlined />
<div>欢迎! </div>
<div>当前版本: v<span>{APP_VERSION}</span></div>
<div>编译时间: <span>{compileTime}</span></div>
<SketchOutlined /> <AntCloudOutlined /> <SlackOutlined /> <RedditOutlined /> <GithubOutlined />
</div>
</>
);
});

@ -0,0 +1,334 @@
import React, { useContext, useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import { Link } from 'react-router-dom';
import { stores_Context, DATE_FORMAT, SMALL_DATETIME_FORMAT } from '../../config';
import moment from 'moment';
import { Row, Col, Table, Select, Spin, Tag } from 'antd';
import SearchForm from '../../components/search/SearchForm';
import MixFieldsDetail from '../../components/MixFieldsDetail';
import { fixTo2Decimals, isEmpty, pick } from '../../utils/commons';
import Column from '../../components/Column';
import { groupsMappedByKey } from '../../libs/ht';
const COLOR_SETS = [
"#FFFFFF",
// "#5B8FF9",
// "#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const transparentHex = '1A';
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals((number/(1000*scale)));
};
export default observer((props) => {
const { SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const _formValuesToSub = pick(formValuesToSub, ['DepartmentList', 'WebCode']);
const { searchValues, resetData, results } = SalesCRMDataStore;
const operatorObjects = results.details.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel, text: v.groupsLabel }));
// console.log(operatorObjects);
const pageRefresh = async (obj) => {
resetData('results');
const deptList = obj.DepartmentList.split(',');
const includeCH = ['1', '2', '7'].some(ele => deptList.includes(ele));
const includeAH = ['28'].every(ele => deptList.includes(ele));
const includeGH = ['33'].every(ele => deptList.includes(ele));
const otherDept = deptList.filter(ele => !['1', '2', '7', '28', '33'].includes(ele));
const separateParam = [];
if (includeCH) {
const inCH = deptList.filter(k => ['1', '2', '7'].includes(k)).join(',');
separateParam.push({ DepartmentList: inCH, retLabel: 'CH'});
}
if (includeAH) {
separateParam.push({ DepartmentList: '28', retLabel: 'AH'});
}
if (includeGH) {
separateParam.push({ DepartmentList: '33', retLabel: 'GH'});
}
if (!isEmpty(otherDept) && (!isEmpty(includeAH) || !isEmpty(includeCH) || !isEmpty(includeGH))) {
separateParam.push({ DepartmentList: otherDept.join(','), retLabel: otherDept.map(k => groupsMappedByKey[k].label).join(', ') }); // ''
}
if (!includeAH && !includeCH && !includeGH) {
separateParam.push({ DepartmentList: obj.DepartmentList });
}
// console.log('separateParam', separateParam, otherDept);
// console.log('formValuesToSub --- pageRefresh', formValuesToSub.DepartmentList);
// return;
for await (const subParam of separateParam) {
// console.log(subParam);
await SalesCRMDataStore.get90n180Data({
...(obj || _formValuesToSub),
...subParam,
groupType: 'overview',
// groupType: 'dept', // todo:
groupDateType: '',
});
await SalesCRMDataStore.get90n180Data({
...(obj || _formValuesToSub),
...subParam,
groupType: 'operator',
groupDateType: '',
});
}
};
const getFullYearDiagramData = async (obj) => {
// console.log('invoke --- getFullYearDiagramData');
// console.log('formValuesToSub --- getFullYearDiagramData', formValuesToSub.DepartmentList);
await SalesCRMDataStore.getResultData({
..._formValuesToSub,
Date1: moment(obj.date).startOf('year').format(DATE_FORMAT),
Date2: moment(obj.date).endOf('year').format(SMALL_DATETIME_FORMAT),
groupType: 'overview',
// groupType: 'operator',
groupDateType: 'month',
...(obj),
});
};
const getDiagramData = async (obj) => {
// console.log('invoke --- getDiagramData');
// console.log(_formValuesToSub, SalesCRMDataStore.searchValuesToSub);
await SalesCRMDataStore.getResultData({
..._formValuesToSub,
...SalesCRMDataStore.searchValuesToSub,
// Date1: moment().startOf('year').format(DATE_FORMAT),
// Date2: moment().endOf('year').format(SMALL_DATETIME_FORMAT),
// groupType: 'overview',
groupType: 'operator',
groupDateType: '',
...(obj),
});
};
const retPropsMinRatesSet = { 'CH': 12, 'AH': 8, 'default': 10 }; //
/**
* 业绩数据列
* ! 成行率: CH个人成行率<12%, AH<8%
*/
const dataFields = (suffix, colRootIndex) => [
{
key: 'ConfirmRates' + suffix,
title: '成行率',
dataIndex: [suffix, 'ConfirmRates'],
width: '5em',
// CH<12%, AH<8%
render: (val, r) => ({
props: { style: { color: val < (retPropsMinRatesSet?.[r?.retProps || 'default'] || retPropsMinRatesSet.default) ? 'red' : 'green', backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: val ? `${val}%` : '',
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmRates - b[suffix].ConfirmRates),
},
{
key: 'SumML' + suffix,
title: '业绩/万',
dataIndex: [suffix, 'SumML'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: numberConvert10K(_),
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumML - b[suffix].SumML),
},
{
key: 'ConfirmOrder' + suffix,
title: '团数',
dataIndex: [suffix, 'ConfirmOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmOrder - b[suffix].ConfirmOrder),
},
{
key: 'SumOrder' + suffix,
title: '订单数',
dataIndex: [suffix, 'SumOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumOrder - b[suffix].SumOrder),
},
{
key: 'ResumeOrder' + suffix,
title: '老客户团数',
dataIndex: [suffix, 'ResumeOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeOrder - b[suffix].ResumeOrder),
},
{
key: 'ResumeRates' + suffix,
title: '老客户成行率',
dataIndex: [suffix, 'ResumeRates'],
width: '5em',
render: (val, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: val ? `${val}%` : ''
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeRates - b[suffix].ResumeRates),
},
];
const dashboardTableProps = {
pagination: false,
size: 'small',
showSorterTooltip: false,
columns: [
{
title: '',
dataIndex: 'groupsLabel',
key: 'name',
width: '5em',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType === 'overview',
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
},
{
title: () => (<>前90 -30<br/>{searchValues.date90.Date1} {searchValues.date90.Date2}</>),
key: 'date',
children: dataFields('result90', 0),
},
{
title: () => (<>前180 -50<br/>{searchValues.date180.Date1} {searchValues.date180.Date2}</>),
key: 'department',
children: dataFields('result180', 1),
},
],
rowClassName: (record, rowIndex) => {
return record.groupType === 'overview' ? 'ant-tag-blue' : '';
},
};
const columnConfig = {
xField: 'groupsLabel',
yField: 'SumML',
label: { position: 'top' },
};
const [clickColumn, setClickColumn] = useState({});
const [clickColumnTitle, setClickColumnTitle] = useState('');
const onChartItemClick = (colData) => {
// console.log('onChartItemClick', colData);
if (colData.groupType === 'operator') {
// test: 0
return false; // ,
}
setClickColumn(colData);
setClickColumnTitle(moment(colData.groupDateVal).format('YYYY-MM'));
};
const chartsConfig = {
colFields: ['ConfirmOrder', 'SumOrder'],
lineFields: ['SumML', 'ConfirmRates'],
seriesField: null,
xField: 'groupDateVal',
itemClick: onChartItemClick,
};
useEffect(() => {
if (isEmpty(clickColumnTitle)) {
return () => {};
}
getDiagramData({
Date1: moment(clickColumn.groupDateVal).startOf('month').format(DATE_FORMAT),
Date2: moment(clickColumn.groupDateVal).endOf('month').format(SMALL_DATETIME_FORMAT),
groupType: 'operator', // test: overview
groupDateType: '',
});
return () => {};
}, [clickColumnTitle]);
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...SalesCRMDataStore.searchValues,
},
shows: ['DepartmentList', 'WebCode', 'date'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
WebCode: { show_all: false, mode: 'multiple', col: 5 },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
SalesCRMDataStore.setSearchValues(obj, form);
pageRefresh(obj);
getFullYearDiagramData({ groupType: 'overview', ...obj });
}}
/>
</Col>
</Row>
<section>
<Table {...dashboardTableProps} bordered dataSource={[...results.dataSource, ...results.details]} loading={results.loading} sticky />
</section>
<section>
<Row gutter={16}>
<Col flex={'12em'}><h3>每月数据</h3></Col>
<Col flex={'auto'}>
<Select
labelInValue
// mode={'multiple'}
style={{ width: '100%' }}
placeholder={'选择顾问'}
// onChange={sale_store.setPickSales}
// onSelect={}
onChange={(labelInValue) => labelInValue ? getFullYearDiagramData({ groupType: 'operator', opisn: labelInValue.value, }) : false}
onClear={() => getFullYearDiagramData({ groupType: 'overview', opisn: -1, })}
// value={sale_store.salesTrade.pickSales}
// maxTagCount={1}
// maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} ...`}
allowClear={true}
options={operatorObjects}
/>
{/* {operatorObjects.map((ele) => (
<Select.Option key={ele.key} value={ele.value}>
{ele.label}
</Select.Option>
))}
</Select> */}
</Col>
</Row>
<Spin spinning={results.loading}>
{/* 小组每月; x轴: 日期; y轴: [订单数, ...] */}
<MixFieldsDetail {...chartsConfig} dataSource={results.byDate} />
</Spin>
<h3>
点击上方图表的柱状图, 查看当月 <Tag color='orange'>业绩</Tag>数据: <Tag color='orange'>{clickColumnTitle}</Tag>
</h3>
{/* 显示小组的详情: 所有顾问? */}
<Spin spinning={results.loading}>
<Column {...columnConfig} dataSource={results.byOperator} />
</Spin>
{/* <Table columns={[{ title: '', dataIndex: 'date', key: 'date', width: '5em' }, ...dataFields]} bordered size='small' /> */}
</section>
<section>
{/* 月份×小组的详情; x轴: 顾问; y轴: [订单数, ...] */}
{/* <MixFieldsDetail {...chartsConfig} xField={'label'} dataSource={[]} /> */}
</section>
</>
);
});

@ -0,0 +1,258 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { observer } from 'mobx-react';
import { stores_Context } from '../../config';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Row, Col, Table, Tooltip } from 'antd';
import SearchForm from '../../components/search/SearchForm';
import { pick } from '../../utils/commons';
const COLOR_SETS = [
"#FFFFFF",
// "#5B8FF9",
// "#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const transparentHex = '1A';
export default observer((props) => {
const { SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
// const { formValues, siderBroken } = searchFormStore;
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const _formValuesToSub = pick(formValuesToSub, ['DepartmentList', 'WebCode']);
const { resetData, process } = SalesCRMDataStore;
const operatorObjects = process.details.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel, text: v.groupsLabel }));
const pageRefresh = async (obj) => {
resetData('process');
Promise.allSettled([
SalesCRMDataStore.getProcessData({
...(obj || _formValuesToSub),
// ...subParam,
// groupType: 'overview',
groupType: 'dept',
groupDateType: '',
}),
SalesCRMDataStore.getProcessData({
...(obj || _formValuesToSub),
// ...subParam,
groupType: 'operator',
groupDateType: '',
}),
]);
};
const tableSorter = (a, b, colName) => (a.groupType !== 'operator' ? -1 : b.groupType !== 'operator' ? 0 : a[colName] - b[colName]);
const percentageRender = val => val ? `${val}%` : '';
const activityTableProps = {
rowKey: 'groupsKey',
pagination: { pageSize: 10, showSizeChanger: false },
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
showSorterTooltip: false,
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType !== 'operator',
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
}, // :
{
title: '顾问动作',
key: 'date',
children: [
{ title: () => (
<>
首次响应率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstTouch24', key: 'firstTouch24', render: percentageRender,
sorter: (a, b) => tableSorter(a, b, 'firstTouch24'),
},
{ title: () => (
<>
48H内报价率{' '}
<Tooltip title="达到48H内报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstQuote48', key: 'firstQuote48',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'firstQuote48'), },
{ title: () => (
<>
一次报价率{' '}
<Tooltip title="首次报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'quote1', key: 'quote1',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote1'), },
{ title: () => (
<>
二次报价率{' '}
<Tooltip title="二次报价的订单数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'quote2', key: 'quote2',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote2'), },
{ title: () => (
<>
&gt;50条会话{' '}
<Tooltip title=">50条会话的订单数/总订单">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'turnsGT50', key: 'turnsGT50', render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'turnsGT50'), },
{ title: () => (
<>
违规数{' '}
<Tooltip title="未遵循24H回复的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'violations', key: 'violations',
sorter: (a, b) => tableSorter(a, b, 'violations'), },
],
},
{
title: '客人回复',
key: 'department',
children: [
{
title: () => (
<>
首次回复率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstReply24', key: 'firstReply24',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'firstReply24'), },
{
title: () => (
<>
48H内报价回复率{' '}
<Tooltip title="48H内报价后的回复数/报价的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'replyQuote48', key: 'replyQuote48',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote48'), },
{
title: () => (
<>
一次报价回复率{' '}
<Tooltip title="一次报价后回复率/订单总数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'replyQuote1', key: 'replyQuote1',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote1'), },
{
title: () => (
<>
二次报价回复率{' '}
<Tooltip title="二次报价后回复数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'replyQuote2', key: 'replyQuote2',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote2'), },
],
},
],
};
const riskTableProps = {
rowKey: 'groupsKey',
pagination: { pageSize: 10, showSizeChanger: false },
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
showSorterTooltip: false,
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType !== 'operator',
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
}, // :
{ title: '>24H回复', dataIndex: 'lostTouch24', key: 'lostTouch24',
sorter: (a, b) => tableSorter(a, b, 'lostTouch24'),
},
{ title: '首次报价周期>48h', dataIndex: 'lostQuote48', key: 'lostQuote48',
sorter: (a, b) => tableSorter(a, b, 'lostQuote48'),
},
{ title: '报价次数<1次', dataIndex: 'lostQuote1', key: 'lostQuote1',
sorter: (a, b) => tableSorter(a, b, 'lostQuote1'),
},
{ title: '报价次数<2次', dataIndex: 'lostQuote2', key: 'lostQuote2',
sorter: (a, b) => tableSorter(a, b, 'lostQuote2'),
},
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...SalesCRMDataStore.searchValues,
},
shows: ['DepartmentList', 'WebCode', 'DateType', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
WebCode: { show_all: false, mode: 'multiple', col: 5 },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
SalesCRMDataStore.setSearchValues(obj, form);
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Table {...activityTableProps} bordered dataSource={[...process.dataSource, ...process.details]} loading={process.loading} sticky />
</section>
<section>
<h3>未成行订单 数量</h3>
<Table {...riskTableProps} bordered dataSource={[...process.dataSource, ...process.details]} loading={process.loading} sticky />
</section>
</>
);
});

@ -0,0 +1,397 @@
import React, { useContext, useEffect } from 'react';
import { NavLink, useParams } from 'react-router-dom';
import { observer } from 'mobx-react';
import { stores_Context, DATE_FORMAT, SMALL_DATETIME_FORMAT } from '../../config';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Row, Col, Table, Divider, Button, Popover, Tooltip } from 'antd';
import { fixTo2Decimals } from '../../utils/commons';
import MixFieldsDetail from '../../components/MixFieldsDetail';
const COLOR_SETS = [
'#FFFFFF',
// "#5B8FF9",
// "#FF6B3B",
'#9FB40F',
'#76523B',
'#DAD5B5',
'#E19348',
'#F383A2',
];
const transparentHex = '1A';
const percentageRender = (val) => (val ? `${val}%` : '');
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals((number/(1000*scale)));
};
const retPropsMinRatesSet = { 'CH': 12, 'AH': 8, 'default': 10 }; //
export default observer((props) => {
const { opisn, opi_name } = useParams();
const { sale_store, SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues, siderBroken } = searchFormStore;
const { searchValues, searchValuesToSub, resetData, results, process, risk } = SalesCRMDataStore;
useEffect(() => {
Promise.allSettled([
//
SalesCRMDataStore.get90n180Data({ opisn, groupType: 'operator', groupDateType: '' }),
// :
SalesCRMDataStore.getResultData({
opisn,
Date1: searchValues.date.clone().startOf('year').format(DATE_FORMAT),
Date2: searchValues.date.clone().endOf('year').format(SMALL_DATETIME_FORMAT),
groupType: 'overview',
// groupType: 'operator',
groupDateType: 'month',
}),
//
SalesCRMDataStore.getProcessData({ opisn, groupType: 'operator', groupDateType: '' }),
//
SalesCRMDataStore.getRiskDetailData({ opisn }),
]);
}, [opisn]);
const dataFields = (suffix, colRootIndex) => [
{
key: 'ConfirmRates' + suffix,
title: '成行率',
dataIndex: [suffix, 'ConfirmRates'],
width: '5em',
// CH<12%, AH<8%
render: (val, r) => ({
props: { style: { color: val < (retPropsMinRatesSet?.[r?.retProps || 'default'] || retPropsMinRatesSet.default) ? 'red' : 'green', backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: val ? `${val}%` : '',
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmRates - b[suffix].ConfirmRates),
},
{
key: 'SumML' + suffix,
title: '业绩/万',
dataIndex: [suffix, 'SumML'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: numberConvert10K(_),
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumML - b[suffix].SumML),
},
{
key: 'ConfirmOrder' + suffix,
title: '团数',
dataIndex: [suffix, 'ConfirmOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmOrder - b[suffix].ConfirmOrder),
},
{
key: 'SumOrder' + suffix,
title: '订单数',
dataIndex: [suffix, 'SumOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumOrder - b[suffix].SumOrder),
},
{
key: 'ResumeOrder' + suffix,
title: '老客户团数',
dataIndex: [suffix, 'ResumeOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeOrder - b[suffix].ResumeOrder),
},
{
key: 'ResumeRates' + suffix,
title: '老客户成行率',
dataIndex: [suffix, 'ResumeRates'],
width: '5em',
render: (val, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: val ? `${val}%` : ''
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeRates - b[suffix].ResumeRates),
},
];
const dashboardTableProps = {
rowKey: 'groupsKey',
pagination: false,
size: 'small',
showSorterTooltip: false,
columns: [
{
title: '',
dataIndex: 'groupsLabel',
key: 'name',
width: '5em',
},
{
title: () => (<>前90 -30<br/>{searchValues.date90.Date1} {searchValues.date90.Date2}</>),
key: 'date',
children: dataFields('result90', 0),
},
{
title: () => (<>前180 -50<br/>{searchValues.date180.Date1} {searchValues.date180.Date2}</>),
key: 'department',
children: dataFields('result180', 1),
},
],
rowClassName: (record, rowIndex) => {
return record.groupType === 'overview' ? 'ant-tag-blue' : '';
},
};
const activityTableProps = {
rowKey: 'groupsKey',
pagination: false,
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '' : 'ant-tag-blue';
},
showSorterTooltip: false,
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem' }, // :
{
title: '顾问动作',
key: 'date',
children: [
{
title: () => (
<>
首次响应率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'firstTouch24',
key: 'firstTouch24',
render: percentageRender,
},
{
title: () => (
<>
48H内报价率{' '}
<Tooltip title="达到48H内报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'firstQuote48',
key: 'firstQuote48',
render: percentageRender,
},
{
title: () => (
<>
一次报价率{' '}
<Tooltip title="首次报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'quote1',
key: 'quote1',
render: percentageRender,
},
{
title: () => (
<>
二次报价率{' '}
<Tooltip title="二次报价的订单数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'quote2',
key: 'quote2',
render: percentageRender,
},
{
title: () => (
<>
&gt;50条会话{' '}
<Tooltip title=">50条会话的订单数/总订单">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'turnsGT50',
key: 'turnsGT50',
render: percentageRender,
},
{
title: () => (
<>
违规数{' '}
<Tooltip title="未遵循24H回复的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'violations',
key: 'violations',
},
],
},
{
title: '客人回复',
key: 'department',
children: [
{
title: () => (
<>
首次回复率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'firstReply24',
key: 'firstReply24',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
{
title: () => (
<>
48H内报价回复率{' '}
<Tooltip title="48H内报价后的回复数/报价的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'replyQuote48',
key: 'replyQuote48',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
{
title: () => (
<>
一次报价回复率{' '}
<Tooltip title="一次报价后回复率/订单总数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'replyQuote1',
key: 'replyQuote1',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
{
title: () => (
<>
二次报价回复率{' '}
<Tooltip title="二次报价后回复数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'replyQuote2',
key: 'replyQuote2',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
],
},
],
};
const riskTableProps = {
loading: risk.loading,
sticky: true,
// scroll: { x: 1000, y: 400 },
pagination: false,
rowKey: (row) => row.coli_id,
columns: [
{ title: '客人姓名', dataIndex: 'guest_name', key: 'guest_name', width: '6rem' },
{ title: '团号', dataIndex: 'coli_id', key: 'coli_id', width: '6rem' },
{
title: '表单内容',
dataIndex: 'coli_contents',
key: 'coli_contents',
width: '6rem',
render: (text, record) => (
<Popover
title={`${record.coli_id} ${record.guest_name}`}
content={<pre dangerouslySetInnerHTML={{ __html: text }} style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }} />}
trigger={['click']}
placement="right"
overlayStyle={{ width: '500px', maxHeight: '500px' }}
autoAdjustOverflow={false}
>
<Button type="link" size="small">
表单内容
</Button>
</Popover>
),
},
{ title: '顾问', dataIndex: 'opi_name', key: 'opi_name', width: '6rem' },
{ title: '预定时间', dataIndex: 'coli_applydate', key: 'coli_applydate', width: '6rem' },
],
};
const chartsConfig = {
colFields: ['ConfirmOrder', 'SumOrder'],
lineFields: ['SumML', 'ConfirmRates'],
seriesField: null,
xField: 'groupDateVal',
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={12} xxl={14}>
<NavLink to={-1}>返回</NavLink>
</Col>
</Row>
<section>
<h2>结果指标 @ {searchValues.date.format(DATE_FORMAT)}</h2>
<Table {...dashboardTableProps} bordered dataSource={results[`operator_${opisn}`]} loading={results.loading} sticky />
</section>
<section>
<h3>全年每月业绩 @ {searchValues.date.format('YYYY')}</h3>
<MixFieldsDetail {...chartsConfig} dataSource={results[`operator_byDate_${opisn}`] || []} />
</section>
<Divider />
<section>
<h2>过程指标 @ {searchValuesToSub.Date1} {searchValuesToSub.Date2}</h2>
<Table {...activityTableProps} bordered dataSource={process[`operator_${opisn}`]} loading={process.loading} sticky />
</section>
<section>
<h2>违规明细</h2>
<h3>&gt;24H回复</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostTouch24 || []} />
</section>
<section>
<h3>首次报价周期&gt;48h</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostQuote48 || []} />
</section>
<section>
<h3>报价次数&lt;1</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostQuote1 || []} />
</section>
<section>
<h3>报价次数&lt;2</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostQuote2 || []} />
</section>
</>
);
});
Loading…
Cancel
Save