Merge branch 'feature/biz'

main
Lei OT 5 months ago
commit d7cbad1d02

100
package-lock.json generated

@ -12,6 +12,7 @@
"@ant-design/pro-components": "^2.6.16",
"antd": "^4.22.6",
"dingtalk-jsapi": "^3.0.9",
"immer": "^10.2.0",
"insert-css": "^2.0.0",
"mobx": "^6.6.1",
"mobx-react": "^7.5.2",
@ -20,7 +21,8 @@
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"web-vitals": "^2.1.4",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz"
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
"zustand": "^5.0.8"
},
"devDependencies": {
"@types/react": "^18.2.20",
@ -1521,6 +1523,16 @@
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@antv/xflow-core/node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@antv/xflow-extension": {
"version": "1.0.51",
"resolved": "https://registry.npmmirror.com/@antv/xflow-extension/-/xflow-extension-1.0.51.tgz",
@ -5477,7 +5489,7 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
"dev": true
"devOptional": true
},
"node_modules/@types/q": {
"version": "1.5.5",
@ -5498,7 +5510,7 @@
"version": "18.2.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz",
"integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -5536,7 +5548,7 @@
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
"dev": true
"devOptional": true
},
"node_modules/@types/semver": {
"version": "7.3.13",
@ -11457,9 +11469,14 @@
}
},
"node_modules/immer": {
"version": "9.0.15",
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.15.tgz",
"integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ=="
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
@ -17826,6 +17843,16 @@
"node": ">=8"
}
},
"node_modules/react-dev-utils/node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/react-dev-utils/node_modules/loader-utils": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
@ -21314,6 +21341,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}
},
"dependencies": {
@ -22652,6 +22708,13 @@
"mana-syringe": "^0.2.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^6.6.7"
},
"dependencies": {
"immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
}
}
},
"@antv/xflow-extension": {
@ -25409,7 +25472,7 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
"dev": true
"devOptional": true
},
"@types/q": {
"version": "1.5.5",
@ -25430,7 +25493,7 @@
"version": "18.2.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz",
"integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==",
"dev": true,
"devOptional": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -25468,7 +25531,7 @@
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
"dev": true
"devOptional": true
},
"@types/semver": {
"version": "7.3.13",
@ -29901,9 +29964,9 @@
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
},
"immer": {
"version": "9.0.15",
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.15.tgz",
"integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ=="
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="
},
"import-fresh": {
"version": "3.3.0",
@ -34399,6 +34462,11 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
},
"loader-utils": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
@ -37049,6 +37117,12 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
"zustand": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
"requires": {}
}
}
}

@ -7,6 +7,7 @@
"@ant-design/pro-components": "^2.6.16",
"antd": "^4.22.6",
"dingtalk-jsapi": "^3.0.9",
"immer": "^10.2.0",
"insert-css": "^2.0.0",
"mobx": "^6.6.1",
"mobx-react": "^7.5.2",
@ -15,7 +16,8 @@
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"web-vitals": "^2.1.4",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz"
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
"zustand": "^5.0.8"
},
"scripts": {
"start": "react-scripts start",

@ -1,28 +1,26 @@
import './App.css';
import React, { useContext, useState } from 'react';
import Icon, {
HomeOutlined,
TeamOutlined,
import { useContext, useState } from 'react';
import {
DashboardOutlined,
FileProtectOutlined,
CustomerServiceTwoTone,
SnippetsTwoTone,
DollarOutlined,
AreaChartOutlined,
WechatOutlined,
UserOutlined,
FlagOutlined,
PieChartOutlined,
BarChartOutlined,
CoffeeOutlined, DesktopOutlined,
WhatsAppOutlined
WhatsAppOutlined,
ShoppingCartOutlined,
GiftOutlined,
RocketOutlined
} from '@ant-design/icons';
import { Layout, Menu, Image, Badge, Button } from 'antd';
import { Layout, Menu, Button } from 'antd';
import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
import Home from './views/Home';
import Dashboard from './views/Dashboard';
import Orders from './views/Orders';
import Orders_sub from './views/Orders_sub';
import BizOrder from './views/biz/BizOrder';
import BizOrderSub from './views/biz/BizOrderSub';
import ProtectedRoute from './views/ProtectedRoute';
import Customer_care_inchina from './charts/Customer_care_inchina';
import Customer_care_potential from './charts/Customer_care_potential';
@ -58,6 +56,7 @@ import OPRisk from './views/sales-crm/Risk';
import Cruise from './views/Cruise';
import Hotel from './views/Hotel';
import HostCaseCount from './views/HostCaseCount';
import TrainsUpsell from './views/biz/reports/TrainsUpsell';
const App = () => {
const { Content, Footer, Sider, } = Layout;
@ -81,7 +80,7 @@ const App = () => {
key: 'meeting-2024-GH',
label: <NavLink to="/orders/meeting-2024-GH">GH区域数据</NavLink>, // GH-2024
},
]
],
},
{
key: 2,
@ -104,6 +103,13 @@ const App = () => {
},
],
},
{
key: 20, label: '商务市场', icon: <RocketOutlined />,
children: [
{ key: 201, label: <NavLink to="/biz_orders">订单数据</NavLink> },
{ key: 202, label: <NavLink to="/trains">TrainsUpsell</NavLink> },
]
},
{
key: 5,
label: '销售',
@ -118,7 +124,7 @@ const App = () => {
{
key: 3,
label: '客运',
icon: <WechatOutlined />,
icon: <GiftOutlined />,
children: [
{
key: 31,
@ -151,7 +157,7 @@ const App = () => {
{
key: 6,
label: '客服',
icon: <CooperationIcon/>,
icon: <CooperationIcon />,
children: [
{
key: 61,
@ -253,6 +259,9 @@ const App = () => {
<Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/orders/meeting-2024-GH" element={<Meeting2024GH />} />
<Route path="/orders/meeting-2025-GH" element={<Meeting2025GH />} />
<Route path="/biz_orders" element={<BizOrder />} />
<Route path="/biz_orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<BizOrderSub />} />
<Route path="/trains" element={<TrainsUpsell />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'customer_care']} />}>
<Route path="/customer_care_inchina" element={<Customer_care_inchina />} />

@ -161,5 +161,5 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
// '\ndata1KeyMappedStr', data1KeyMappedStr,
// '\ndata2KeyMappedStr', data2KeyMappedStr
// );
cb(dateGroup, retData, avg1, parse2.avgVal);
return cb(dateGroup, retData, avg1, parse2.avgVal);
};

@ -92,6 +92,11 @@ export default observer((props) => {
transform: (value) => value?.key || '',
default: '',
},
'IncludeInternal': {
key: 'IncludeInternal',
transform: (value) => value?.key || '',
default: '',
},
'operator': {
key: 'operator',
// transform: (value) => value?.key || '',
@ -379,6 +384,21 @@ function getFields(props) {
</Form.Item>,
3
),
item(
'IncludeInternal',
99,
<Form.Item name={`IncludeInternal`} initialValue={at(props, 'initialValue.IncludeInternal')[0] || { key: '1', label: '含内部订单' }}>
<Select style={{ width: '100%' }} placeholder="是否含内部订单" labelInValue>
<Option key="1" value="1">
含内部订单
</Option>
<Option key="0" value="0">
不含内部
</Option>
</Select>
</Form.Item>,
3
),
item(
'countryArea',
99,

@ -39,7 +39,7 @@ export const deptUnits = [
export const groups = [
{ value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] },
{ value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] },
{ value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] },
{ value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: 'CTX', children: [10, 18, 16, 30] },
{ value: '1', key: '1', label: 'CH直销', code: '', children: [] },
{ value: '2', key: '2', label: 'CH大客户', code: '', children: [] },
{ value: '28', key: '28', label: 'HT项目组(前AH)', code: 'HT', children: [] },
@ -61,6 +61,11 @@ export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.cod
export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
export const leafGroup = groups.slice(3);
export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
export const groupsCTplus = [
// { value: '10,18', key: '10,18', label: 'CT商旅', code: 'CT+', children: [10, 18,] };
{ value: '18', key: '18', label: 'CT', code: 'CT', children: [] },
{ value: '10', key: '10', label: '商旅', code: '', children: [] },
];
/**
* 来源
*/

@ -327,8 +327,8 @@ class Orders extends Component {
colorField: "OrderType",
radius: 0.8,
label: {
type: "outer",
content: "{name} {value} \n {percentage}",
type: "spider",
content: "{name} {value} \t {percentage}",
},
legend: false, //
interactions: [

@ -0,0 +1,442 @@
import { useContext } from 'react';
import { Row, Col, Tabs, Table, Divider, Spin, Checkbox, Space } from 'antd';
import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined, CustomerServiceOutlined } from '@ant-design/icons';
import { Line, Pie } from '@ant-design/charts';
import { NavLink } from 'react-router-dom';
import * as comm from '../../utils/commons';
import DateGroupRadio from '../../components/DateGroupRadio';
import SearchForm from '../../components/search/SearchForm';
import { TableExportBtn } from '../../components/Data';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
import { stores_Context } from '../../config';
import { useShallow } from 'zustand/shallow';
import useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder';
const BizOrder = observer(() => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [searchValues, setSearchValues] = useBizOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
const [activeTab, setActiveTab] = useBizOrderStore(useShallow((state) => [state.activeTab, state.setActiveTab]));
const [loading, onTabChange] = useBizOrderStore(useShallow((state) => [state.loading, state.onTabChange]));
const orderCountDataRaw = useBizOrderStore((state) => state.orderCountDataRaw);
const [orderCountDataLines, avgLineValue] = useBizOrderStore(useShallow((state) => [state.orderCountDataLines, state.avgLineValue]));
const [onChangeDateGroup, activeDateGroupRadio] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroup, state.activeDateGroupRadio]));
const orderCountDataByType = useBizOrderStore((state) => state.orderCountDataByType);
const result = orderCountDataByType[activeTab] || {};
const getBizOrderCount = useBizOrderStore((state) => state.getBizOrderCount);
const showDiff = !comm.isEmpty(searchFormStore.start_date_cp);
const avg_line_y = Math.round(avgLineValue);
const lineConfig = {
data: orderCountDataLines,
padding: 'auto',
xField: 'xField',
yField: 'yField',
seriesField: 'seriesField',
// xAxis: {
// type: "timeCat",
// },
point: {
size: 4,
shape: 'cicle',
},
annotations: [
{
type: 'text',
position: ['start', avg_line_y],
content: avg_line_y,
offsetX: -15,
style: {
fill: '#F4664A',
textBaseline: 'bottom',
},
},
{
type: 'line',
start: [-10, avg_line_y],
end: ['max', avg_line_y],
style: {
stroke: '#F4664A',
lineDash: [2, 2],
},
},
],
label: {}, //
legend: {
itemValue: {
formatter: (text, item) => {
const items = orderCountDataLines.filter((d) => d.seriesField === item.value); //
return items.length ? items.reduce((a, b) => a + b.yField, 0) : ''; //
},
},
},
tooltip: {
customItems: (originalItems) => {
// process originalItems,
return originalItems.map((ele) => ({ ...ele, name: ele.data?.seriesField || ele.data?.xField }));
},
title: (title, datum) => {
let ret = title;
switch (activeDateGroupRadio) {
case 'day':
ret = `${title} ${comm.getWeek(datum.xField)}`; //
break;
default:
break;
}
return ret;
},
},
// smooth: true,
};
const pieConfig = {
appendPadding: 10,
data: [],
angleField: 'OrderCount',
colorField: 'OrderType',
radius: 0.8,
label: {
type: 'outer',
content: '{name} {value} \n {percentage}',
},
legend: false, //
interactions: [
{
type: 'element-selected',
},
{
type: 'element-active',
},
],
};
const tableProps = {
dataSource: result?.ordercount1 || [], // table_data.dataSource,
columns: [
{
title: '#',
fixed: 'left',
children: [
{
title: (
<span>
<div>{result.ordercountTotal1?.groups}</div>
{showDiff ? <div>{result.ordercountTotal2?.groups}</div> : null}
</span>
),
titleX: `${result.ordercountTotal1?.groups}` + (showDiff ? ` vs ${result.ordercountTotal2?.groups}` : ''),
dataIndex: 'OrderType',
fixed: 'left',
render: (text, record) => <NavLink to={`/biz_orders_sub/${activeTab}/${record.OrderTypeSN}/${encodeURIComponent(record.OrderType)}`}>{text}</NavLink>,
},
],
},
{
title: '数量',
children: [
{
title: !showDiff
? result.ordercountTotal1?.OrderCount
: comm.show_vs_tag(result.ordercountTotal1?.OrderCount_vs, result.ordercountTotal1?.OrderCount_diff, result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount),
titleX: [result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount].join(' vs '),
dataIndex: 'OrderCount',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.OrderCount_vs, r.OrderCount_diff, r.OrderCount, r.diff?.OrderCount)),
},
],
},
{
title: '成交数',
children: [
{
title: !showDiff
? result.ordercountTotal1?.CJCount
: comm.show_vs_tag(result.ordercountTotal1?.CJCount_vs, result.ordercountTotal1?.CJCount_diff, result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount),
titleX: [result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount].join(' vs '),
dataIndex: 'CJCount',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJCount_vs, r.CJCount_diff, r.CJCount, r.diff?.CJCount)),
},
],
},
{
title: '成交人数',
children: [
{
title: !showDiff
? result.ordercountTotal1?.CJPersonNum
: comm.show_vs_tag(result.ordercountTotal1?.CJPersonNum_vs, result.ordercountTotal1?.CJPersonNum_diff, result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum),
titleX: [result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum].join(' vs '),
dataIndex: 'CJPersonNum',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJPersonNum_vs, r.CJPersonNum_diff, r.CJPersonNum, r.diff?.CJPersonNum)),
},
],
},
{
title: '成交率',
children: [
{
title: !showDiff
? result.ordercountTotal1?.CJrate
: comm.show_vs_tag(result.ordercountTotal1?.CJrate_vs, result.ordercountTotal1?.CJrate_diff, result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate),
titleX: [result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate].join(' vs '),
dataIndex: 'CJrate',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJrate_vs, r.CJrate_diff, r.CJrate, r.diff?.CJrate)),
},
],
},
{
title: '成交毛利(预计)',
children: [
{
title: !showDiff
? result.ordercountTotal1?.YJLY
: comm.show_vs_tag(result.ordercountTotal1?.YJLY_vs, result.ordercountTotal1?.YJLY_diff, result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY),
titleX: [result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY].join(' vs '),
dataIndex: 'YJLY',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.YJLY_vs, r.YJLY_diff, r.YJLY, r.diff?.YJLY)),
},
],
},
{
title: '单个订单价值',
children: [
{
title: !showDiff
? result.ordercountTotal1?.Ordervalue
: comm.show_vs_tag(result.ordercountTotal1?.Ordervalue_vs, result.ordercountTotal1?.Ordervalue_diff, result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue),
titleX: [result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue].join(' vs '),
dataIndex: 'Ordervalue',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.Ordervalue_vs, r.Ordervalue_diff, r.Ordervalue, r.diff?.Ordervalue)),
},
],
},
],
size: 'small',
pagination: false,
scroll: { x: 100 * 7 },
loading,
};
return (
<>
<div>
<Row gutter={16} className={toJS(searchFormStore.siderBroken) ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...toJS(searchFormStore.formValues),
...searchValues,
},
// 'WebCode','IncludeTickets',
shows: ['DateType', 'DepartmentList', 'IncludeInternal', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
setSearchValues(obj, form);
getBizOrderCount(obj);
onTabChange(activeTab);
}}
/>
</Col>
</Row>
<Row gutter={[16, { sm: 16, lg: 32 }]}>
<Col span={24} style={{ textAlign: 'right' }}>
<DateGroupRadio
visible={orderCountDataLines.length !== 0}
dataRaw={orderCountDataRaw}
onChange={onChangeDateGroup}
value={activeDateGroupRadio}
dataMapper={orderCountDataMapper}
fieldMapper={orderCountDataFieldMapper}
/>
</Col>
<Col span={24}>
<Spin spinning={loading}>
<Line {...lineConfig} />
</Spin>
</Col>
<Col span={24}>
<Tabs
activeKey={activeTab}
onChange={(active_key) => onTabChange(active_key)}
items={[
{
key: 'servicetype',
label: (
<span>
<CustomerServiceOutlined />
服务类型
</span>
),
},
{
key: 'Form',
label: (
<span>
<ContainerOutlined />
来源类型
</span>
),
},
// {
// key: 'Product',
// label: (
// <span>
// <CarryOutOutlined />
//
// </span>
// ),
// },
{
key: 'Country',
label: (
<span>
<SmileOutlined />
国籍
</span>
),
},
// {
// key: 'line',
// label: (
// <span>
// <TagsOutlined />
// 线
// </span>
// ),
// },
// {
// key: 'city',
// label: (
// <span>
// <GlobalOutlined />
//
// </span>
// ),
// },
{
key: 'LineClass',
label: (
<span>
<BlockOutlined />
页面类型
</span>
),
},
{
key: 'ordersource',
label: (
<span>
<MobileOutlined />
来源设备
</span>
),
},
// {
// key: 'GuestGroupType',
// label: (
// <span>
// <FullscreenOutlined />
//
// </span>
// ),
// },
// {
// key: 'TravelMotivation',
// label: (
// <span>
// <DingtalkOutlined />
//
// </span>
// ),
// },
// {
// key: 'ToB',
// label: (
// <span>
// <ContactsOutlined />
//
// </span>
// ),
// },
// {
// key: 'FoodRequirement',
// label: (
// <span>
// <CoffeeOutlined />
//
// </span>
// ),
// },
// {
// key: 'hobbies',
// label: (
// <span>
// <HeartOutlined />
//
// </span>
// ),
// },
// {
// key: 'ages',
// label: (
// <span>
// <IdcardOutlined />
//
// </span>
// ),
// },
].map((ele) => {
return {
...ele,
children: (
<>
<Table sticky key={`table_to_xlsx_${ele.key}`} {...tableProps} />
<Divider orientation="right" plain>
<TableExportBtn label={ele.key} {...{ columns: tableProps.columns, dataSource: tableProps.dataSource }} />
</Divider>
</>
),
};
})}
/>
</Col>
</Row>
<div>
<h3>各项占比</h3>
{/* <Checkbox
checked={true}
// onChange={(e) => setIsShowEmpty(e.target.checked)}
>
包含空值
</Checkbox> */}
</div>
<Spin spinning={loading}>
<Row>
<Col sm={24} lg={12}>
<Pie {...pieConfig} data={result?.ordercount1 || []} />
</Col>
<Col sm={24} lg={12}>
<Pie {...pieConfig} data={result?.ordercount2 || []} />
</Col>
</Row>
</Spin>
</div>
</>
);
});
export default BizOrder;

@ -0,0 +1,280 @@
import { useContext, useEffect } from 'react';
import { Row, Col, Tabs, Table, Divider, Spin, Space } from 'antd';
import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined } from '@ant-design/icons';
import { Line } from '@ant-design/charts';
import { NavLink, useParams } from 'react-router-dom';
import * as comm from '../../utils/commons';
import DateGroupRadio from '../../components/DateGroupRadio';
import SearchForm from '../../components/search/SearchForm';
import { TableExportBtn } from '../../components/Data';
import { observer } from 'mobx-react';
import { stores_Context } from '../../config';
import { useShallow } from 'zustand/shallow';
import useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder';
//
const addLineBreaksAtCommas = (text) => {
if (!text) return '';
return text.replace(/&amp;/g, '&').replace(//g, '\n').replace(/,/g, ',\n').replace(//g, '\n').replace(/;/g, ';\n');
};
const OrderDetailTable = ({ caption, dataSource, loading, ...props }) => {
const columns = [
{
title: '订单号',
dataIndex: 'COLI_ID',
key: 'COLI_ID',
},
{
title: '网站',
dataIndex: 'COLI_WebCode',
key: 'COLI_WebCode',
},
{
title: '成行',
dataIndex: 'COLI_Success',
key: 'COLI_Success',
render: (text, record) => <span>{text == 1 ? '是' : '否'}</span>,
sorter: (a, b) => b.COLI_Success - a.COLI_Success,
},
// {
// title: "(//)",
// dataIndex: "COLI_PersonNum",
// key: "COLI_PersonNum",
// render: (text, record) => (
// <span>
// {record.COLI_PersonNum}/{record.COLI_ChildNum}/{record.COLI_BabyNum}
// </span>
// ),
// },
{
title: '预计利润',
dataIndex: 'CGI_YJLY',
key: 'CGI_YJLY',
},
{
title: '预定时间',
dataIndex: 'COLI_ApplyDate',
key: 'COLI_ApplyDate',
},
{
title: '出发日期',
dataIndex: 'CGI_ArriveDate',
key: 'CGI_ArriveDate',
},
// {
// title: '',
// dataIndex: 'COLI_CustomerRequest',
// key: 'COLI_CustomerRequest',
// ellipsis: true,
// },
{
title: '订单内容',
dataIndex: 'COLI_OrderDetailText',
key: 'COLI_OrderDetailText',
ellipsis: true,
},
Table.EXPAND_COLUMN,
];
return (
<div>
<Divider orientation="left" plain>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '10px' }}>
<div>{caption}</div>
<TableExportBtn label={caption} columns={columns} dataSource={dataSource} />
</div>
</Divider>
<Table
id="table_to_xlsx_form"
dataSource={dataSource}
columns={columns}
loading={loading}
size="small"
// pagination={false}
rowKey={(record) => record.key}
expandable={{
expandedRowRender: (record) => (
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', fontSize: '16px' }}>
{/* <Divider orientation="left" plain>
客户需求
</Divider>
{record.COLI_CustomerRequest} */}
<Divider orientation="left" plain>
订单内容
</Divider>
{record.COLI_OrderDetailText}
</pre>
),
}}
/>
</div>
);
};
const BizOrderSub = observer(({ ...props }) => {
const { ordertype, ordertype_sub, ordertype_title } = useParams();
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [searchValues, setSearchValues] = useBizOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
const [searchValuesToSub] = useBizOrderStore(useShallow((state) => [state.searchValuesToSub]));
const [loading, typeLoading] = useBizOrderStore(useShallow((state) => [state.loading, state.typeLoading]));
const orderCountDataRawSub = useBizOrderStore((state) => state.orderCountDataRawSub);
const [orderCountDataLinesSub, avgLineValueSub] = useBizOrderStore(useShallow((state) => [state.orderCountDataLinesSub, state.avgLineValueSub]));
const [onChangeDateGroupSub, activeDateGroupRadioSub] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroupSub, state.activeDateGroupRadioSub]));
const orderDetails = useBizOrderStore((state) => state.orderDetails);
const getBizOrderCount = useBizOrderStore((state) => state.getBizOrderCount);
const getBizOrderDetailByType = useBizOrderStore((state) => state.getBizOrderDetailByType);
useEffect(() => {
getBizOrderCount(searchValuesToSub, ordertype, ordertype_sub);
getBizOrderDetailByType(searchValuesToSub, ordertype, ordertype_sub);
}, []);
const avg_line_y = Math.round(avgLineValueSub);
const lineConfig = {
data: orderCountDataLinesSub,
padding: 'auto',
xField: 'xField',
yField: 'yField',
seriesField: 'seriesField',
// xAxis: {
// type: "timeCat",
// },
point: {
size: 4,
shape: 'cicle',
},
annotations: [
{
type: 'text',
position: ['start', avg_line_y],
content: avg_line_y,
offsetX: -15,
style: {
fill: '#F4664A',
textBaseline: 'bottom',
},
},
{
type: 'line',
start: [-10, avg_line_y],
end: ['max', avg_line_y],
style: {
stroke: '#F4664A',
lineDash: [2, 2],
},
},
],
label: {}, //
legend: {
itemValue: {
formatter: (text, item) => {
const items = orderCountDataLinesSub.filter((d) => d.seriesField === item.value); //
return items.length ? items.reduce((a, b) => a + b.yField, 0) : ''; //
},
},
},
tooltip: {
customItems: (originalItems) => {
// process originalItems,
return originalItems.map((ele) => ({ ...ele, name: ele.data?.seriesField || ele.data?.xField }));
},
title: (title, datum) => {
let ret = title;
switch (activeDateGroupRadioSub) {
case 'day':
ret = `${title} ${comm.getWeek(datum.xField)}`; //
break;
default:
break;
}
return ret;
},
},
smooth: true,
};
const tab_items = [
{
key: 'detail',
label: (
<span>
<ContainerOutlined />
订单内容
</span>
),
title: '订单内容',
children: (
<>
<Space direction="vertical">
<OrderDetailTable caption={orderDetails[0]?.dateRangeStr} dataSource={orderDetails[0]?.data || []} loading={typeLoading} />
<OrderDetailTable caption={orderDetails[1]?.dateRangeStr} dataSource={orderDetails[1]?.data || []} loading={typeLoading} />
</Space>
</>
),
},
];
return (
<div>
<Row gutter={{ sm: 16, lg: 32 }} className={searchFormStore.siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={12} xxl={14}>
<NavLink to={`/biz_orders`}>返回</NavLink>
</Col>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...searchFormStore.formValues,
...searchValues,
},
// 'WebCode', 'IncludeTickets',
shows: ['DateType', 'DepartmentList', 'IncludeInternal', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
// dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
setSearchValues(obj, form);
getBizOrderCount(obj, ordertype, ordertype_sub);
getBizOrderDetailByType(obj, ordertype, ordertype_sub);
}}
/>
</Col>
</Row>
<Row gutter={[16, { xs: 8, sm: 16, md: 24, lg: 32 }]}>
<Col span={24} style={{ textAlign: 'right' }}>
<DateGroupRadio
visible={orderCountDataLinesSub.length !== 0}
dataRaw={orderCountDataRawSub}
onChange={onChangeDateGroupSub}
value={activeDateGroupRadioSub}
dataMapper={orderCountDataMapper}
fieldMapper={orderCountDataFieldMapper}
/>
</Col>
<Col className="gutter-row" span={24}>
<Spin spinning={loading}>
<Line {...lineConfig} />
</Spin>
</Col>
<Col className="gutter-row" span={24}>
<Tabs
activeKey={'detail'}
// onChange={onTabsChange}
items={tab_items}
/>
</Col>
</Row>
</div>
);
});
export default BizOrderSub;

@ -0,0 +1,238 @@
import { useContext } from 'react';
import { Row, Col, Table, Spin, Space, Divider } from 'antd';
import { Funnel, Pie } from '@ant-design/charts';
import * as comm from '../../../utils/commons';
import SearchForm from '../../../components/search/SearchForm';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
import { stores_Context } from '../../../config';
import { useShallow } from 'zustand/shallow';
import useTrainsStore from '../../../zustand/Trains';
const buildFunnelData = (data1, data2) => {
// const key = 'OrderCount';
const data01 = [
{ stage: '火车票服务', number: data1?.data?.[0]?.OrderCount, dateRange: data1?.dateRangeStr },
{ stage: '成行出票', number: data1?.data?.[0]?.CJCount, dateRange: data1?.dateRangeStr },
{ stage: '火车票Upsell', number: (data1?.data?.[1]?.OrderCount || 0) + 0, dateRange: data1?.dateRangeStr },
{ stage: 'Upsell成行', number: data1?.data?.[1]?.CJCount, dateRange: data1?.dateRangeStr },
];
const data02 = !comm.isEmpty(data2)
? [
{ stage: '火车票服务', number: data2?.data?.[0]?.OrderCount, dateRange: data2?.dateRangeStr },
{ stage: '成行出票', number: data2?.data?.[0]?.CJCount, dateRange: data2?.dateRangeStr },
{ stage: '火车票Upsell', number: data2?.data?.[1]?.OrderCount, dateRange: data2?.dateRangeStr },
{ stage: 'Upsell成行', number: data2?.data?.[1]?.CJCount, dateRange: data2?.dateRangeStr },
]
: [];
return data01.concat(data02);
// return data02.concat(data01);
};
const buildPieData = (data1, data2) => {
const data01 = !comm.isEmpty(data1)
? [
{ stage: '火车票出票', number: Number((data1?.data?.[0]?.YJLY || '0').replaceAll(',', '')), dateRange: data1?.dateRangeStr },
{ stage: 'Upsell成行', number: Number((data1?.data?.[1]?.YJLY || '0').replaceAll(',', '')), dateRange: data1?.dateRangeStr },
]
: [];
const data02 = !comm.isEmpty(data2)
? [
{ stage: '火车票出票', number: Number((data2?.data?.[0]?.YJLY || '0').replaceAll(',', '')), dateRange: data2?.dateRangeStr },
{ stage: 'Upsell成行', number: Number((data2?.data?.[1]?.YJLY || '0').replaceAll(',', '')), dateRange: data2?.dateRangeStr },
]
: [];
return data01.concat(data02);
};
const TrainsUpsell = observer(({ ...props }) => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [searchValues, setSearchValues] = useTrainsStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
const [loading] = useTrainsStore(useShallow((state) => [state.loading]));
const [trainsOrdersSummary, compareData] = useTrainsStore((state) => state.trainsOrdersSummary);
// const showDiff = !comm.isEmpty(searchValuesToSub.DateDiff2);
const showDiff = !comm.isEmpty(compareData);
const getTrainsWithUpsell = useTrainsStore((state) => state.getTrainsWithUpsell);
const data0 = buildFunnelData(trainsOrdersSummary, compareData);
const chartConfig = {
data: data0,
xField: 'stage',
yField: 'number',
compareField: showDiff ? 'dateRange' : false,
// isTransposed: true,
legend: false,
// tooltip: {
// shared: true,
// showMarkers: false,
// showTitle: false,
// },
label: {
position: 'right',
// offsetX: 10,
style: {
fill: '#002c45',
},
},
shape: 'pyramid',
theme: {
colors10: [
// '#b5d8f3',
'#c4e1ff',
// '#739ec1',
'#7ebdff',
// '#3d759b',
'#5b90f9',
// '#1f5373',
'#526bd1',
'#002c45',
],
},
};
const pieData = buildPieData(trainsOrdersSummary);
const pieData2 = buildPieData(compareData);
// console.log('🟠', pieData, pieData2);
const pieConfig = {
appendPadding: 0,
// data: pieData,
angleField: 'number',
colorField: 'stage',
radius: 0.8,
innerRadius: 0.6,
// startAngle: Math.PI ,
// endAngle: Math.PI * 1.5,
label: {
type: 'spider',
// content: '{name} {value} \n {percentage}',
content: '{name}\n{percentage}',
},
statistic: false,
legend: false, //
interactions: [
{
type: 'element-selected',
},
{
type: 'element-active',
},
],
};
const tableProps = {
title: () => `${trainsOrdersSummary?.dateRangeStr || ''}` + (showDiff ? ` vs ${compareData?.dateRangeStr}` : ''),
dataSource: trainsOrdersSummary?.data || [],
columns: [
{
title: '#',
fixed: 'left',
dataIndex: 'OrderType',
},
{
title: '数量',
dataIndex: 'OrderCount',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.OrderCount_vs, r.OrderCount_diff, r.OrderCount, r.diff?.OrderCount)),
},
{
title: '成交数',
dataIndex: 'CJCount',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJCount_vs, r.CJCount_diff, r.CJCount, r.diff?.CJCount)),
},
{
title: '成交人数',
dataIndex: 'CJPersonNum',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJPersonNum_vs, r.CJPersonNum_diff, r.CJPersonNum, r.diff?.CJPersonNum)),
},
{
title: '成交率',
dataIndex: 'CJrate',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJrate_vs, r.CJrate_diff, r.CJrate, r.diff?.CJrate)),
},
{
title: '成交毛利(预计)',
dataIndex: 'YJLY',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.YJLY_vs, r.YJLY_diff, r.YJLY, r.diff?.YJLY)),
},
{
title: '单个订单价值',
dataIndex: 'Ordervalue',
render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.Ordervalue_vs, r.Ordervalue_diff, r.Ordervalue, r.diff?.Ordervalue)),
},
],
size: 'small',
pagination: false,
scroll: { x: 100 * 7 },
loading,
};
return (
<>
<div>
<Row gutter={16} className={toJS(searchFormStore.siderBroken) ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...toJS(searchFormStore.formValues),
...searchValues,
},
shows: ['DateType', 'DepartmentList', 'IncludeInternal', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
setSearchValues(obj, form);
getTrainsWithUpsell(obj);
}}
/>
</Col>
</Row>
<Space direction={'vertical'}>
<Table sticky key={`table_1`} {...tableProps} />
<Spin spinning={loading}>
<Row gutter={[16, { sm: 16, lg: 32 }]}>
<Col {...{ xs: 24, md: 12, lg: 12 }}>
<h3>订单数量</h3>
<Funnel {...chartConfig} key={showDiff ? 'funnel_2' : 'funnel_1'} />
</Col>
<Col {...{ xs: 24, md: 12, lg: 12 }}>
<h3>预计利润</h3>
<div style={{ display: showDiff ? 'flex' : 'block' }}>
<Pie
key={showDiff ? 'pie1' : 'pie0'}
style={{ display: showDiff ? 'inline' : 'block' }}
{...pieConfig}
startAngle={Math.PI}
endAngle={Math.PI * (showDiff ? 1.5 : 2)}
data={pieData}
/>
{showDiff && (
<Pie
key={'pie2'}
style={{ display: 'inline' }}
{...pieConfig}
startAngle={Math.PI * 1.5}
endAngle={Math.PI * 2}
data={pieData2.reverse()}
color={['#61DDAA', '#5B8FF9']}
pattern={{ type: 'line' }}
/>
)}
</div>
</Col>
</Row>
</Spin>
</Space>
</div>
</>
);
});
export default TrainsUpsell;

@ -0,0 +1,234 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { groupsMappedByCode } from '../libs/ht';
import { fetchJSON } from '../utils/request';
import { HT_HOST } from '../config';
import { resultDataCb } from '../components/DateGroupRadio/date';
import { isEmpty } from '../utils/commons';
const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1 };
export const fetchBizOrderCount = async (params, type = '', typeVal = '') => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_biz', {
...defaultParams,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
OrderType_val: typeVal,
});
return errcode !== 0 ? {} : (result || {});
};
const _typeRes = {
'ordercount1': [],
'ordercount2': [],
'ordercountTotal1': {
'OrderType': '合计',
'OrderCount': 0,
'CJCount': 0,
'CJPersonNum': 0,
'YJLY': '',
'CJrate': '0%',
'Ordervalue': '',
'groups': '',
'key': 1,
},
'ordercountTotal2': {},
};
export const fetchBizOrderCountByType = async (type, params) => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', {
...defaultParams,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
});
const res = errcode !== 0 ? _typeRes : (result || _typeRes);
const rows1Map = res.ordercount1.reduce((a, row1) => ({ ...a, [row1.OrderTypeSN]: row1 }), {});
const rows2Map = res.ordercount2.reduce((a, row2) => ({ ...a, [row2.OrderTypeSN]: row2 }), {});
const mixRow1 = res.ordercount1.map((row1) => ({ ...row1, diff: rows2Map[row1.OrderTypeSN] || {} }));
// Diff: elements in rows2 but not in rows1
const diff = [...new Set(Object.keys(rows2Map).filter((x) => !new Set(Object.keys(rows1Map)).has(x)))];
mixRow1.push(...diff.map((sn) => ({ diff: rows2Map[sn], OrderType: rows2Map[sn].OrderType, OrderTypeSN: rows2Map[sn].OrderTypeSN })));
return { ...res, ordercount1: mixRow1 };
};
const _detailRes = { ordercount1: [], ordercount2: [] };
export const fetchBizOrderDetailByType = async (params, type = '', typeVal = '', orderContent = 'detail') => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_Sub_biz', {
...defaultParams,
SubOrderType: orderContent,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
OrderType_val: typeVal,
});
const res = errcode !== 0 ? _detailRes : (result || _detailRes);
const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~');
const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~');
const ret = [
{ dateRangeStr: dateStr, data: res.ordercount1 },
{ dateRangeStr: dateDiffStr, data: res.ordercount2 },
];
return ret;
};
const calculateLineData = (value, data, avg1) => {
const groupByDate = data.reduce((r, v) => {
(r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v);
return r;
}, {});
const _data = Object.keys(groupByDate)
.reduce((r, _d) => {
const xAxisGroup = groupByDate[_d].reduce((a, v) => {
(a[v.groups] || (a[v.groups] = [])).push(v);
return a;
}, {});
Object.keys(xAxisGroup).map((_group) => {
const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0);
r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal });
return _group;
});
return r;
}, [])
.map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups }));
return { lines: _data, dateRadioValue: value, avgLineValue: avg1 };
};
export const orderCountDataMapper = { 'data1': 'ordercount1', data2: 'ordercount2' };
export const orderCountDataFieldMapper = { 'dateKey': 'ApplyDate', 'valueKey': 'orderCount', 'seriesKey': 'WebCode', _f: 'sum' };
/**
* --------------------------------------------------------------------------------------------------------
*/
const initialState = {
loading: false,
typeLoading: false,
searchValues: {
DateType: { key: 'applyDate', label: '提交日期' },
WebCode: { key: 'all', label: '所有来源' },
IncludeTickets: { key: '1', label: '含门票' },
DepartmentList: groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, //
},
searchValuesToSub: {
DateType: 'applyDate',
WebCode: 'all',
IncludeTickets: '1',
DepartmentList: -1, // -1: All
},
activeTab: 'servicetype',
activeDateGroupRadio: 'day',
orderCountDataRaw: {},
orderCountDataLines: [],
avgLineValue: 0,
orderCountDataByType: {},
// 二级页面
orderCountDataRawSub: {},
orderCountDataLinesSub: [],
avgLineValueSub: 0,
activeDateGroupRadioSub: 'day',
orderDetails: [],
};
const useBizOrderStore = create(
devtools(
immer((set, get) => ({
...initialState,
reset: () => set(initialState),
setLoading: (loading) => set({ loading }),
setTypeLoading: (typeLoading) => set({ typeLoading }),
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
setActiveTab: (tab) => set({ activeTab: tab }),
setOrderCountDataLines: (data) => set({ orderCountDataLines: data }),
setOrderCountDataByType: (type, data) =>
set((state) => {
state.orderCountDataByType[type] = data;
}),
// data ----
onChangeDateGroup: (value, data, avg1) => {
const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1);
set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue });
},
onChangeDateGroupSub: (value, data, avg1) => {
const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1);
set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue });
},
// site effects
getBizOrderCount: async (params, type, typeVal) => {
const { setLoading } = get();
setLoading(true);
try {
const res = await fetchBizOrderCount(params, type, typeVal);
// 第一次得到数据
const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData);
if (isEmpty(type)) {
// index page
set({ orderCountDataRaw: res });
set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue });
} else {
// sub page
set({ orderCountDataRawSub: res });
set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue });
}
} catch (error) {
} finally {
setLoading(false);
}
},
getBizOrderCount_type: async (type) => {
const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get();
setTypeLoading(true);
try {
const res = await fetchBizOrderCountByType(type, searchValuesToSub);
setOrderCountDataByType(type, res);
} catch (error) {
} finally {
setTypeLoading(false);
}
},
onTabChange: async (tab) => {
const { setActiveTab, getBizOrderCount_type } = get();
setActiveTab(tab);
await getBizOrderCount_type(tab);
},
// sub
getBizOrderDetailByType: async (params, type, typeVal) => {
const { setTypeLoading } = get();
try {
setTypeLoading(true);
const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail');
set({ orderDetails: res });
} catch (error) {
} finally {
setTypeLoading(false);
}
},
})),
{ name: 'bizOrder' }
)
);
export default useBizOrderStore;

@ -0,0 +1,178 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { groupsCTplus, groupsMappedByCode } from '../libs/ht';
import { fetchJSON } from '../utils/request';
import { HT_HOST } from '../config';
import { resultDataCb } from '../components/DateGroupRadio/date';
import { groupBy, isEmpty } from '../utils/commons';
const SERVICETYPE_TRAINSBOOKING = '2'; // 火车票服务
const FORM_TRAINSBOOKING = 32024; // 火车票预定
const FORM_TRAINSUPSELL = 32214; // 火车票Upsell
const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1 };
const _res = { ordercount1: [], ordercount2: [] };
export const fetchBizTrainsOrderSummaryByType = async (params, type = 'Form', typeVal = '') => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', {
...defaultParams,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
});
const res = errcode !== 0 ? _res : result || _res;
const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~');
const ret = [
{ dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === typeVal) },
// { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) },
];
const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~');
if (!isEmpty(dateDiffStr)) {
ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) });
}
return ret;
};
/**
* 从传统订单中获取upsell数据
*/
export const fetchTrainsUpsellTSummaryByType = async (params, type = 'Form', typeVal = '') => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType', {
...defaultParams,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
});
const res = errcode !== 0 ? _res : result || _res;
const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~');
const ret = [
{ dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === typeVal) },
// { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) },
];
const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~');
if (!isEmpty(dateDiffStr)) {
ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) });
}
return ret;
};
export const fetchTrainsWithUpsellSummary = async (params) => {
const [trains, upsell] = await Promise.all([
// fetchBizTrainsOrderSummaryByType(params, 'Form', FORM_TRAINSBOOKING), // todo: 换成servicetype=2
fetchBizTrainsOrderSummaryByType(params, 'servicetype', SERVICETYPE_TRAINSBOOKING),
fetchTrainsUpsellTSummaryByType(params, 'Form', FORM_TRAINSUPSELL),
]);
if (!isEmpty(trains[1])) {
trains[0].data.diff = trains[1].data;
upsell[0].data.diff = upsell[1].data;
}
const mergedMap = [...trains, ...upsell].reduce((acc, currentItem, currentIndex) => {
const dateRange = currentItem.dateRangeStr;
if (acc[dateRange]) {
acc[dateRange].data.push(currentItem.data);
for (const key in currentItem) {
if (key !== 'dateRange' && key !== 'data') {
acc[dateRange][key] = currentItem[key];
}
}
} else {
acc[dateRange] = {
...currentItem,
data: [currentItem.data],
};
}
return acc;
}, {});
return Object.values(mergedMap);
// return groupBy([...trains, ...upsell], 'dateRangeStr');
};
/**
* --------------------------------------------------------------------------------------------------------
*/
const initialState = {
loading: false,
typeLoading: false,
searchValues: {
DateType: { key: 'applyDate', label: '提交日期' },
WebCode: { key: 'all', label: '所有来源' },
IncludeTickets: { key: '1', label: '含门票' },
DepartmentList: groupsCTplus, // groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, //
},
searchValuesToSub: {
DateType: 'applyDate',
WebCode: 'all',
IncludeTickets: '1',
DepartmentList: -1, // -1: All
},
trainsOrdersSummary: [],
activeTab: 'Form',
activeDateGroupRadio: 'day',
orderCountDataRaw: {},
orderCountDataLines: [],
avgLineValue: 0,
orderCountDataByType: {},
// 二级页面
orderCountDataRawSub: {},
orderCountDataLinesSub: [],
avgLineValueSub: 0,
activeDateGroupRadioSub: 'day',
orderDetails: [],
};
const useTrainsStore = create(
devtools(
immer((set, get) => ({
...initialState,
reset: () => set(initialState),
setLoading: (loading) => set({ loading }),
setTypeLoading: (typeLoading) => set({ typeLoading }),
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
setActiveTab: (tab) => set({ activeTab: tab }),
setOrderCountDataLines: (data) => set({ orderCountDataLines: data }),
setOrderCountDataByType: (type, data) =>
set((state) => {
state.orderCountDataByType[type] = data;
}),
// data ----
// site effects
getTrainsWithUpsell: async (params, type, typeVal) => {
const { setLoading } = get();
setLoading(true);
set({ trainsOrdersSummary: [] });
try {
const res = await fetchTrainsWithUpsellSummary(params);
set({ trainsOrdersSummary: res });
} catch (error) {
} finally {
setLoading(false);
}
},
// sub
})),
{ name: 'trains' }
)
);
export default useTrainsStore;
Loading…
Cancel
Save