Merge branch 'main' into feature/person-num

# Conflicts:
#	src/App.jsx
feature/hotel-cruise
Lei OT 2 years ago
commit ea7048e41b

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

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

@ -28,6 +28,7 @@
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<div id="root"></div>
<!--
This HTML file is a template.

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

@ -1,5 +1,5 @@
import './App.css';
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import {
HomeOutlined,
TeamOutlined,
@ -10,9 +10,13 @@ import {
DollarOutlined,
AreaChartOutlined,
WechatOutlined,
UserOutlined, FlagOutlined, PieChartOutlined, BarChartOutlined
UserOutlined,
FlagOutlined,
PieChartOutlined,
BarChartOutlined,
CoffeeOutlined,
} from '@ant-design/icons';
import { Layout, Menu, Image, Badge } from 'antd';
import { Layout, Menu, Image, Badge, } from 'antd';
import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
import Home from './views/Home';
import Dashboard from './views/Dashboard';
@ -32,21 +36,27 @@ import Credit_card_bill from './views/Credit_card_bill';
import Sale from './views/Sale';
import Sale_sub from './views/Sale_sub';
import Sale_KPI from './views/Sale_KPI';
import Logo from './logo.png';
import { stores_Context } from './config';
// import Logo from './logo.png';
import { observer } from 'mobx-react';
import ExchangeRate from './charts/ExchangeRate';
import KPI from './views/KPI';
import Distribution from './views/Distribution';
import Detail from './views/Detail';
import ServicePersonNum from './views/ServicePersonNum';
import DataPivot from './views/DataPivot';
import Welcome from './views/Welcome';
import { stores_Context, APP_VERSION } from './config';
import { WaterMark } from '@ant-design/pro-components';
const App = () => {
const { Content, Footer, Sider } = Layout;
const { auth_store } = useContext(stores_Context);
const { Content, Footer, Sider, } = Layout;
const { auth_store, date_picker_store } = useContext(stores_Context);
const [collapsed, setCollapsed] = useState(false);
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: 2,
label: '市场',
@ -55,12 +65,17 @@ const App = () => {
{
key: 21,
label: <NavLink to="/orders">订单数据</NavLink>,
icon: <FileProtectOutlined />,
// icon: <FileProtectOutlined />,
},
{
key: 22,
label: <NavLink to="/dashboard">仪表盘</NavLink>,
icon: <DashboardOutlined />,
// icon: <DashboardOutlined />,
},
{
key: 'orders-pivot',
label: <NavLink to="/orders/pivot">数据透视</NavLink>,
// icon: <DashboardOutlined />,
},
],
},
@ -72,6 +87,7 @@ const App = () => {
{ 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> },
],
},
{
@ -141,88 +157,107 @@ const App = () => {
// },
];
const callDebug = () => {
const vConsole = new window.VConsole({ theme: 'dark' });
auth_store.get_auth();
};
return (
<BrowserRouter>
<Layout
hasSider
style={{
minHeight: '100vh',
}}
>
<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
<WaterMark content={['HT统计']} gapY={100} gapX={100} rotate={-20} offsetLeft={150} offsetTop={150} fontColor="rgba(0,0,0,.06)" zIndex={20}>
<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}>
<Layout
hasSider
style={{
padding: 16,
minHeight: 480,
minHeight: '100vh',
}}
>
<Routes>
<Route path="/" element={<Home />} />
<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 path="/service_person_num" element={<ServicePersonNum />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu']} />}>
<Route path="/kpi" element={<KPI />} />
</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>
</Routes>
</Content>
<Footer
style={{
textAlign: 'center',
}}
>
<UserOutlined /> {auth_store.user.name} ({auth_store.user.userid})
<br />
Hainatravel Dashboard ©2022 Created by IT
</Footer>
</Layout>
</Layout>
<Sider
collapsible={true}
breakpoint="lg"
collapsedWidth="0"
collapsed={collapsed}
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>
<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 path="/distribution" element={<Distribution />} />
<Route path="/:page/pivot" element={<DataPivot />} />
</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 onClick={callDebug}> Created by IT</span>
</Footer>
</Layout>
</Layout>
</WaterMark>
</WaterMark>
</BrowserRouter>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

@ -1,19 +1,9 @@
import React, {useContext, useEffect} from 'react';
import {Row, Col, Button, Divider, Table, Space, Radio, Tooltip} from 'antd';
import {
ContainerOutlined,
SearchOutlined,
} from '@ant-design/icons';
import {Row, Col, Divider, Table} from 'antd';
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} 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 SearchForm from './../components/search/SearchForm';
const Customer_care_inchina = () => {
@ -27,38 +17,35 @@ const Customer_care_inchina = () => {
return (
<div>
<Row>
<Col span={8}>
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<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>
</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}>
<Table dataSource={inchina_data.data} columns={
<Table dataSource={inchina_data.data} loading={inchina_data.loading} columns={
[
{
title: '统计条目',
@ -101,7 +88,7 @@ const Customer_care_inchina = () => {
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={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: '订单号',

@ -1,19 +1,9 @@
import React, {useContext, useEffect} from 'react';
import {Row, Col, Button, Divider, Table, Space, Radio, Tooltip} from 'antd';
import {
ContainerOutlined,
SearchOutlined,
} from '@ant-design/icons';
import {Row, Col, Divider, Table} from 'antd';
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} 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 SearchForm from './../components/search/SearchForm';
const Customer_care_potential = () => {
@ -21,43 +11,41 @@ const Customer_care_potential = () => {
const potential_data = customer_store.potential_data;
useEffect(() => {
}, []);
return (
<div>
<Row>
<Col span={8}>
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<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>
</Col>
<Col span={15}>
<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={1}></Col> */}
<Col span={24}>
<Table dataSource={potential_data.data} columns={
<Table dataSource={potential_data.data} loading={potential_data.loading} columns={
[
{
title: '订单数',
@ -95,7 +83,7 @@ const Customer_care_potential = () => {
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={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: '订单号',
@ -165,9 +153,9 @@ const Customer_care_potential = () => {
key: 'SourceType',
},
{
title: '在华',
dataIndex: 'ZH',
key: 'ZH',
title: '页面类型',
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
]
} size="small"

@ -1,189 +1,184 @@
import React, {useContext, useEffect} from 'react';
import {Row, Col, Button, Divider, Table, Space, Radio, Tooltip} from 'antd';
import {read, utils, writeFileXLSX} from 'xlsx';
import {
ContainerOutlined,
SearchOutlined,
} from '@ant-design/icons';
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} 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 React, { useContext, useEffect } from 'react';
import { Row, Col, Divider, Table } from 'antd';
import { utils, writeFileXLSX } from 'xlsx';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import SearchForm from './../components/search/SearchForm';
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);
const regular_data = customer_store.regular_data;
useEffect(() => {
}, []);
return (
<div>
<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>
);
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'],
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, 'regular_data');
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: '订单数',
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}
loading={regular_data.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: '订单状态',
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',
},
]}
size="small"
rowKey={(record) => record.COLI_ID}
/>
</Col>
</Row>
</div>
);
};
export default observer(Customer_care_regular);

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

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

@ -13,32 +13,47 @@ export default observer((props) => {
const maxKPI = Math.max(...(origin || []).map((ele) => (ele?.[targetField] || 0)));
const maxValue = Math.max(...(origin || []).map((ele) => ele[measureField]));
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 _parseData = sortData?.map((ele) => ({ ...ele,
[rangeField]: [0, Math.ceil(_max / 0.9)],
[rangeField]: [_min, Math.ceil(_max / 0.9)],
// [measureField]: [ele[measureField]],
[measureField]: ele[measureFieldArrKey] || [ele[measureField]],
[targetField]: (ele?.[targetField] || 0)
}));
return _parseData;
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 [maxV, setMaxV] = useState(0);
const [minV, setMinV] = useState(0);
useEffect(() => {
setParseData(dataParser(dataSource));
const _pdata = dataParser(dataSource);
setParseData(_pdata._parseData);
setMaxV(_pdata._max);
setMinV(_pdata._min);
return () => {};
}, [extProps.measureField, dataSource]);
const config = merge({
color: {
range: [ '#FFF3E1', '#FFF3E1'],
// range: [ '#FFF3E1', '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0',
range: [].concat((minV < 0 ? ['#ffe4e4'] : []), [ '#FFF3E1', '#FFF3E1']),
measure: ['#5B8FF9', '#61ddaa'],
target: '#FF9845',
},
bulletStyle: {
measure: (item, ...r) => {
if (item[extProps.measureField] < 0) {
return {
fill: '#F4664A',
};
}
},
},
label: {
target: false,
measure: {

@ -1,5 +1,8 @@
import { Tag } from 'antd';
import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons';
import { useState, useEffect } from "react";
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
@ -24,3 +27,51 @@ export const VSTag = (props) => {
</span>
);
};
/**
* 导出表格数据存为xlsx
*/
export const TableExportBtn = (props) => {
const output_name = `${props.label}`;
const [columnsMap, setColumnsMap] = useState([]);
useEffect(() => {
const flatCols = props.columns.flatMap((v, k) => (v.children ? v.children.map((vc) => ({ ...vc, title: `${v?.titleX || v.title}-${vc.title || ''}` })) : v)).filter(c => c.title);
// console.log('flatCols', flatCols);
setColumnsMap(flatCols);
return () => {};
}, [props.columns]);
const onExport = () => {
if (isEmpty(props.dataSource)) {
message.warning('无结果.');
return false;
}
const data = props.dataSource.map((item) => {
const itemMapped = columnsMap.reduce((sv, kset) => {
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 v = { [kset.title]: data_val || render_val };
return { ...sv, ...v };
}, {});
return itemMapped;
});
// console.log('data', data);
const ws = utils.json_to_sheet(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
type="link"
icon={<DownloadOutlined />}
size="small"
disabled={false}
onClick={onExport}
>
导出excel
</Button>
);
};

@ -1,5 +1,5 @@
import moment from 'moment';
import { groupBy } from '../../utils/commons';
import { fixTo2Decimals, groupBy } from '../../utils/commons';
export const datePartOptions = [
{ label: '日', value: 'day' },
@ -83,7 +83,7 @@ export const parseDateType = (data, dateType = 'day', { dateKey, valueKey, serie
const dateRange = [min, max];
const summaryVal = everySeries[key].reduce((rows, row) => rows + row[valueKey], 0);
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;
}, []);
const avgDiv = [...new Set(dateArr)].length;
@ -108,7 +108,7 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
const _data2 = data2 ? dataRaw[data2] : [];
const parse1 = parseDateType(_data1, dateGroup, fieldMapper);
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.seriesKey]: ele[fieldMapper.seriesKey],
groups: _data1[0].groups,
@ -118,7 +118,7 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
}));
const parse2 = parseDateType(_data2, dateGroup, fieldMapper);
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.valueKey]: ele.value,
[fieldMapper.seriesKey]: ele[fieldMapper.seriesKey],
@ -151,8 +151,15 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
[fieldMapper.dateKey]: keyMapped[ele[fieldMapper.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;
// console.log('callback', dateGroup, retData, data1KeyMappedStr, data2KeyMappedStr);
cb(dateGroup, retData, avg1);
// console.log('callback','\ndateGroup', dateGroup,
// '\nretData', retData,
// '\navg1', avg1,
// '\nparse2', parse2.avgVal,
// '\ndata1KeyMappedStr', data1KeyMappedStr,
// '\ndata2KeyMappedStr', data2KeyMappedStr
// );
cb(dateGroup, retData, avg1, parse2.avgVal);
};

@ -4,44 +4,100 @@ import { merge, isEmpty, groupBy, sortBy } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
export default observer((props) => {
const { dataSource, ...config } = props;
const { dataSource, showKPI, ...config } = props;
const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v;
const seriesData = groupBy(dataSource, ele => ele[config.seriesField]);
const splitData = dataSource.reduce((r, v) => {
const splitData = showKPI ? dataSource.reduce((r, v) => {
r.push(v);
if ( ! isEmpty(v[kpiKey])) { // 线, 线
r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: `${v[config.seriesField]} ${dataFieldAlias[kpiKey].label}`, extraLine: true,});
}
return r;
}, []).sort(sortBy(config.xField));
}, []).sort(sortBy(config.xField)) : (dataSource.slice()).sort(sortBy(config.xField));
const dataColors = [
"#5B8FF9","#5AD8A6","#5D7092","#F6BD16","#6F5EF9","#6DC8EC","#945FB9","#FF9845","#1E9493",
"#FF99C3","#FF6B3B","#626681","#FFC100","#9FB40F","#76523B","#DAD5B5","#0E8E89","#E19348",
"#F383A2","#247FEA",
"#5D7092","#F6BD16","#6F5EF9","#6DC8EC","#945FB9","#FF9845","#1E9493",
"#FF99C3","#FF6B3B","#626681","#FFC100","#9FB40F","#76523B","#DAD5B5",
"#0E8E89","#E19348","#F383A2","#247FEA","#5B8FF9","#5AD8A6",
];
const colorSets = Object.keys(seriesData).sort().reduce((obj, k, i) => ({...obj, [k]: dataColors[i]}), {});
const mergeLineConfig = merge({
color: (item) => {
const thisSeries = item[config.seriesField]?.split(' ')?.[0];
return colorSets[thisSeries];
// return thisSeries.includes('') ? '#F4664A' : colorSets[thisSeries];
},
lineStyle: (data) => {
if (data[config.seriesField].includes('目标')) {
const colorSets = Object.keys(seriesData)
.sort()
.filter((ele) => !ele.includes(' '))
.reduce((obj, k, i) => ({ ...obj, [k]: dataColors[i] || dataColors[i % 20] }), {});
// console.log('colorSets', colorSets);
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 {
lineDash: [4, 8],
opacity: 0.7,
opacity: 1,
};
}
return {
opacity: 1,
};
},
legend: {
custom: true,
items: Object.keys(seriesData).map((ele) => ({ id: ele, name: ele, value: ele, marker: { symbol: 'circle', style: { fill: colorSets[ele], color: colorSets[ele] } } })),
},
legend: {
custom: true,
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={splitData} />;
});

@ -0,0 +1,109 @@
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, ...extConfig } = props;
const [mdataSource, setMdataSource] = useState([]);
useEffect(() => {
const dataMapped = (cloneDeep(dataSource) || []).reduce((r, v) => ({...r,
[(v.groupsLabel || '_').replace('(待删除)', '')]: v
}), {});
if (dataMapped?.['中国']) {
dataMapped['中国'].groupsLabel = '中华人民共和国';
}
setMdataSource(Object.values(dataMapped));
return () => {};
}, [dataSource, valueField]);
const config = {
container: '#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]),
joinBy: {
geoField: 'name',
sourceField: sourceField || 'name',
},
},
autoFit: true,
color: {
field: valueField || 'value',
value: [
'#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} />;
});

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

@ -1,5 +1,5 @@
import { observer } from 'mobx-react';
import { Mix } from '@ant-design/plots';
import { Mix, getCanvasPattern } from '@ant-design/plots';
import { merge, isEmpty, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
@ -11,6 +11,10 @@ const COLOR_SETS = [
"#E19348",
"#F383A2",
];
const COLOR_SETS2 = [
"#5B8FF9",
"#61DDAA",
"#65789B",];
/**
* 当期数据; 同比; 环比
*/
@ -62,7 +66,18 @@ export default observer((props) => {
// 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,
@ -75,7 +90,58 @@ export default observer((props) => {
// return items;
// },
},
legend: {position: 'top',layout: 'horizontal' },
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',
@ -91,8 +157,8 @@ export default observer((props) => {
}),
// color: '#b32b19',
// color: '#f58269',
legend: {},
smooth: true,
legend: false, // {},
// smooth: true,
yAxis: {
type: 'linear',
tickCount: 4,
@ -107,6 +173,8 @@ export default observer((props) => {
},
},
label: false,
color: COLOR_SETS2,
pattern,
},
},
{
@ -118,7 +186,7 @@ export default observer((props) => {
yField: 'yField',
seriesField: 'yGroup',
xAxis: false,
legend: {},
legend: false, // {},
meta: merge(
{
...cloneDeep(dataFieldAlias),
@ -126,7 +194,7 @@ export default observer((props) => {
{ yField: dataFieldAlias[yFields[1]] }
),
// color: '#1AAF8B',
smooth: true,
// smooth: true,
point: {
size: 4,
shape: 'cicle',
@ -147,6 +215,17 @@ export default observer((props) => {
lineWidth: 1,
},
},
lineStyle: (datum) => {
if (String(datum.yGroup).includes(' ')) {
return {
lineDash: [4, 4],
opacity: 0.75,
};
}
return {
opacity: 1,
};
},
},
},
{
@ -175,9 +254,16 @@ export default observer((props) => {
max: 250,
tickCount: 4,
},
legend: {},
legend: false, // {},
color: COLOR_SETS,
annotations: diffLine,
minColumnWidth: 5,
maxColumnWidth: 5,
// ()
dodgePadding: 1,
// ()
// intervalPadding: 20,
},
},
],

@ -5,23 +5,24 @@ import { RingProgress, Progress, Bullet } from '@ant-design/plots';
import RcResizeObserver from 'rc-resize-observer';
import { stores_Context } from '../config';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import { Table } from 'antd';
import { Table, Space } from 'antd';
const { Statistic, Divider } = StatisticCard;
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 valueStyle = { color: (props?.VSrate || -1) < 0 ? '#3f8600' : '#cf1322' };
const VSIcon = () => ((props?.VSrate || -1) < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />);
const valueStyle = { color: '#3f8600' };
// const valueStyle = { color: (props?.VSrate || -1) < 0 ? '#3f8600' : '#cf1322' };
// const VSIcon = () => ((props?.VSrate || -1) < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />);
// console.log(props, ';;;;');
const [responsive, setResponsive] = useState(false);
const showMulti = traditional.value && biz.value;
const rangeMax = Math.max(originVal, kpiVal);
const bulletData = [
{
title: '',
// ranges: [0, kpiVal || (traditional.value + biz.value + 100 )],
ranges: [0, Math.ceil(originVal * 1.1)],
ranges: [0, Math.ceil(rangeMax / 0.95)], // ,
measures: [traditional.value, biz.value],
target: kpiVal || 0,
},
@ -70,9 +71,16 @@ export default observer((props) => {
valueStyle,
...extProps,
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}
footer={null}
/>
</StatisticCard.Group>
</RcResizeObserver>

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

@ -84,6 +84,7 @@ export default observer((props) => {
dataIndex: 'object_id',
editable: false,
width: '5em',
fixed: 'left',
render: (_, r) => r.object_name,
},
{
@ -92,6 +93,7 @@ export default observer((props) => {
valueType: 'digit',
fieldProps: { style: { width: '100%' }, step: 10000 * 100 },
width: '6em',
fixed: 'left',
formItemProps: {
style: { width: '100%' },
},

@ -35,7 +35,7 @@ class DataTypeSelect extends Component {
{...extProps}
>
{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}
</Select.Option>
))}

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

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

@ -14,7 +14,7 @@ import DateTypeSelect from './DataTypeSelect';
import DatePickerCharts from './DatePickerCharts';
import YearPickerCharts from './YearPickerCharts';
import SearchInput from './Input';
import { objectMapper, at, empty } from './../../utils/commons';
import { objectMapper, at, empty, isEmpty } from './../../utils/commons';
import './search.css';
@ -62,21 +62,21 @@ export default observer((props) => {
'businessUnits': {
key: 'businessUnits',
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: '',
},
'DepartmentList': {
key: 'DepartmentList',
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: '',
},
'WebCode': {
key: 'WebCode',
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: '',
},
@ -148,6 +148,26 @@ export default observer((props) => {
transform: (value) => value?.key || '',
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: '',
},
};
let dest = {};
const { applyDate, applyDate2, year, yearDiff, dates, months, ...omittedValue } = values;
@ -198,10 +218,10 @@ export default observer((props) => {
// layout="inline"
<Form form={form} name="advanced_search" className="orders-search-form" onFinish={onFinish} onValuesChange={onValuesChange}>
<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 })}
{/* '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">
<Button size={'middle'} type="primary" icon={<SearchOutlined />} htmlType="submit">
{confirmText || '统计'}
@ -224,22 +244,51 @@ function getFields(props) {
const layoutProps = {
gutter: { xs: 8, sm: 8, lg: 16 },
lg: { span: 4 },
md: { span: 8 },
sm: { span: 12 },
xs: { span: 24 },
};
const item = (name, sort = 0, render, col) => {
const customCol = col || 4;
const mdCol = customCol * 2;
return {
'key': '',
sort,
name,
render,
'hide': false,
'col': { lg: { span: customCol } },
'col': { lg: { span: customCol }, md: { span: mdCol < 8 ? 10 : mdCol}, flex: mdCol < 8 ? "1 0" : "" },
};
};
let baseChildren = [];
baseChildren = [
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="所有地接社"
/>
</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(
'HTBusinessUnits',
99,
@ -257,16 +306,22 @@ function getFields(props) {
item(
'DepartmentList',
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} />
</Form.Item>
</Form.Item>,
fieldProps?.DepartmentList?.col
),
item(
'WebCode',
99,
<Form.Item name={`WebCode`} initialValue={at(props, 'initialValue.WebCode')[0] || (fieldProps?.WebCode?.show_all ? { key: 'ALL', label: '所有来源' } : undefined)}>
<SiteSelect {...fieldProps.WebCode} labelInValue={true} />
</Form.Item>
</Form.Item>,
fieldProps?.WebCode?.col
),
item(
'IncludeTickets',
@ -282,16 +337,52 @@ function getFields(props) {
</Option>
</Select>
</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>
{fieldProps?.countryArea?.show_all && <Option key="all" value="">
所有国家
</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>
{fieldProps?.orderStatus?.show_all && <Select.Option key="所有" value="-1">
所有
</Select.Option> }
<Select.Option key="已成行" value="1">
已成行
</Select.Option>
<Select.Option key="未成行" value="0">
未成行
</Select.Option>
</Select>
</Form.Item>,
3
),
//
item(
'DateType',
99,
<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>,
2
fieldProps?.DateType?.col || 3
),
item(
'years',
@ -300,7 +391,7 @@ function getFields(props) {
{/* <DatePicker picker="year" placeholder='年份' /> */}
<YearPickerCharts {...fieldProps.years} />
</Form.Item>,
2
3
),
item(
'months',
@ -308,7 +399,7 @@ function getFields(props) {
<Form.Item>
<DatePicker picker="month" placeholder="月份" />
</Form.Item>,
2
3
),
item(
'dates',
@ -316,12 +407,12 @@ function getFields(props) {
<Form.Item>
<DatePickerCharts isform={true} {...fieldProps.dates} form={form} />
</Form.Item>,
midCol
fieldProps?.dates?.col || midCol
),
item(
'operator',
99,
<Form.Item name={'operator'} dependencies={['DepartmentList']} >
<Form.Item name={'operator'} dependencies={['DepartmentList']}>
<SearchInput
autoGet
url="/service-Analyse2/GetOperatorInfo"

@ -13,13 +13,17 @@ class SiteSelect extends Component {
const { store, mode, value, onChange, show_all, ...extProps } = this.props;
const _mode = mode || store?.group_select_mode || null;
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 (
<div>
<Select
mode={_mode}
style={{width: '100%'}}
placeholder="选择来源"
defaultValue={value || store?.webcode || undefined }
placeholder="所有来源"
value={_value}
onChange={(value) => {
if (typeof onChange === 'function') {
onChange(value);
@ -28,12 +32,12 @@ class SiteSelect extends Component {
}}
labelInValue={false}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
maxTagPlaceholder={(omittedValues) => ` +${omittedValues.length}...`}
allowClear={_mode != null}
{...extProps}
>
{_show_all===true ? <Select.Option key="1" value="ALL">所有来源</Select.Option> : ''}
{sites.map(ele => <Select.Option key={ele.key} value={ele.code}>{ele.label}</Select.Option>)}
{_show_all===true ? <Select.Option key="ALL" value="ALL">所有来源</Select.Option> : ''}
{sites.map(ele => <Select.Option key={ele.code} value={ele.code}>{ele.label}</Select.Option>)}
</Select>
</div>
);

@ -51,7 +51,7 @@ class DatePickerCharts extends Component {
locale={locale}
placeholder={"对比 Year"}
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') {
this.props.onChange(fullYear);
}

@ -1,5 +1,7 @@
import React from "react";
import packageInfo from './../package.json';
export const APP_VERSION = packageInfo.version;
export const stores_Context = React.createContext();
export const DATE_FORMAT = "YYYY-MM-DD";
export const SMALL_DATETIME_FORMAT = 'YYYY-MM-DD 23:59:00';

@ -1,4 +1,4 @@
import { fixTo4Decimals, fixTo1Decimals } from "../utils/commons";
import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush, fixTo2Decimals, isEmpty } from '../utils/commons';
/**
* 事业部
@ -57,6 +57,7 @@ export const groups = [
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
];
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 overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
/**
@ -87,9 +88,10 @@ export const sites = [
{ value: '30', key: '30', label: 'TP', code: 'trippest' },
{ 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 = [
{ key: 'applyDate', value: 'applyDate', label: '提交日期' },
{ key: 'ConfirmDate', value: 'ConfirmDate', label: '确认日期' },
{ key: 'confirmDate', value: 'confirmDate', label: '确认日期' },
{ key: 'startDate', value: 'startDate', label: '走团日期' },
];
@ -121,11 +123,7 @@ export const dataFieldAlias = dataFieldOptions.reduce(
* KPI对象
*/
export const KPIObjects = [
{ key: 'overview', value: 'overview', label: '海纳', data: [
{ key: 'ALL', value: 'ALL', label: '海纳' },
...overviewGroup
]
},
{ key: 'overview', value: 'overview', label: '海纳', data: [{ key: 'ALL', value: 'ALL', label: '海纳' }, ...overviewGroup] },
{
key: 'bizarea',
value: 'bizarea',
@ -171,3 +169,286 @@ export const KPISubjects = [
// { key: 'reply_eff_wa', value: 'reply_eff_wa', label: 'WA回复效率'},
// { key: 'sum_person_num', value: 'sum_person_num', label: '人数' },
];
/**
* 数据透视计算
* @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]));
// 数组的字段值, 拆分处理
let data = cloneDeep(_data);
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;
}, []);
}
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,
SumOrder: _len,
SumPersonNum: 0,
ConfirmPersonNum: 0,
ConfirmOrder: 0,
transactions: 0,
SumML: 0,
SumML_txt: '',
quotePrice: 0,
tourdays: 0,
applyDays: 0,
confirmDays: 0,
SingleML: 0,
OrderValue: 0,
};
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.transactions += v.transactions;
r.SumML += Number(v.orderState) === 1 ? v.ML : 0;
r.quotePrice += v.quotePrice;
r.tourdays += v.tourdays;
r.applyDays += v.applyDays;
r.confirmDays += v.confirmDays;
return r;
}, initialData);
// Calculations
calculatedData.tourdays = Math.ceil(calculatedData.tourdays / _len);
calculatedData.applyDays = Math.ceil(calculatedData.applyDays / _len);
calculatedData.confirmDays = Math.ceil(calculatedData.confirmDays / _len);
const _rowCalc = {
ConfirmRates: calculatedData.ConfirmOrder ? fixTo4Decimals(calculatedData.ConfirmOrder / calculatedData.SumOrder) : 0,
OrderValue: calculatedData.SumOrder ? fixToInt(calculatedData.SumML / calculatedData.SumOrder) : 0,
SingleML: calculatedData.ConfirmOrder ? fixToInt(calculatedData.SumML / calculatedData.ConfirmOrder) : 0,
};
// 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);
DataGroupByKeys[colKey] = { ...calculatedData, ..._rowCalc };
});
return { groupByKeys: DataGroupByKeys, key: outerKeys.join('_') };
};
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 _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 };
})
.map((everyR) => {
const _colKey = dataKey || 'dataKey';
const allColumns = Object.values(everyR[_colKey]).reduce((r, c) => r.concat([c]), []);
const summaryCalc = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum', 'ConfirmPersonNum', 'quotePrice', 'tourdays', 'applyDays', 'confirmDays'].reduce(
(r, skey) => ({ ...r, [skey]: allColumns.reduce((a, c) => Number(c.orderState) === 1 ? a : fixTo2Decimals(a + c[skey]), 0) }),
everyR
);
summaryCalc.tourdays = Math.ceil(summaryCalc.tourdays / allColumns.length);
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.OrderValue = summaryCalc.SumOrder ? fixToInt(summaryCalc.SumML / summaryCalc.SumOrder) : 0;
summaryCalc.SingleML = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.SumML / summaryCalc.ConfirmOrder) : 0;
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]);
console.timeEnd('pivot3----');
return { data: pivotResult, columnValues: [rowsKeys, columnsKeys, dateKeys], summaryRows, summaryColumns, pivotKeys: groupbyKeys };
};
// 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]])
};
};

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

@ -59,9 +59,11 @@ class CustomerServices {
{
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>
}
]
@ -297,8 +299,10 @@ class CustomerServices {
{
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>
}
]
@ -453,6 +457,20 @@ class CustomerServices {
});
}
searchValues = {
DateType: { key: 'startDate', label: '走团日期'},
};
setSearchValues(obj, values) {
this.dateType = obj.DateType;
this.selectedAgent = obj.agency;
this.startDateString = obj.Date1;
this.endDateString = obj.Date2;
this.selectedCountry = obj.countryArea;
this.selectedTeam = obj.DepartmentList.replace('ALL', '');
this.selectedOrderStatus = obj.orderStatus;
}
selectDateRange(startDate, endDate) {
this.startDate = startDate;
this.endDate = endDate;
@ -492,7 +510,7 @@ class CustomerServices {
inProgress;
agentList = [];
groupList = [];
groupListColumns = [];
@ -518,6 +536,7 @@ class CustomerServices {
{
title: '地接社名称',
dataIndex: 'VendorName',
fixed: 'left',
render: (text, record) => {
if (record.EOI_ObjSN === -1) {
return text;
@ -569,4 +588,4 @@ class CustomerServices {
];
}
export default CustomerServices;
export default CustomerServices;

@ -1,5 +1,6 @@
import {makeAutoObservable, runInAction} from "mobx";
import * as config from "../config";
import { groupsMappedByKey, dataFieldAlias, sitesMappedByCode } from './../libs/ht';
class CustomerStore {
@ -16,9 +17,9 @@ class CustomerStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-tourdesign/PotentialCusOrder';
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';
} 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';
}else {
url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0';
@ -79,6 +80,12 @@ class CustomerStore {
potential_customer_order: this.potential_customer_order.bind(this),
onChange_show_detail_table: this.onChange_show_detail_table.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
@ -89,9 +96,9 @@ class CustomerStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-tourdesign/RegularCusOrder';
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';
} 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';
}else {
url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0';
@ -152,6 +159,12 @@ class CustomerStore {
regular_customer_order: this.regular_customer_order.bind(this),
onChange_show_detail_table: this.onChange_show_detail_table_regular.bind(this),
handleChange_webcode: this.handleChange_webcode_regular.bind(this),
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: undefined, // ['ALL'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
},
};
// 老客户 end
@ -162,9 +175,9 @@ class CustomerStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-tourdesign/RegularCusInChinaOrder';
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';
} 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';
}else {
url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0';
@ -225,9 +238,22 @@ class CustomerStore {
inchina_customer_order: this.inchina_customer_order.bind(this),
onChange_show_detail_table: this.onChange_show_detail_table_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
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 * as config from "../config";
import { resultDataCb } from '../components/DateGroupRadio/date';
import { groupsMappedByKey } from './../libs/ht';
class DashboardStore {
constructor(rootStore) {
@ -197,7 +198,17 @@ class DashboardStore {
group_handleChange: this.handleChange_group_select.bind(this),
onChange_datetype: this.onChange_datetype.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
// 汇率变化 begin

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

@ -45,7 +45,7 @@ class DatePickerStore {
'DepartmentList': { 'key': 'ALL', 'label': '所有小组' },
'WebCode': { 'key': 'ALL', 'label': '所有来源' },
'IncludeTickets': { 'key': '1', 'label': '含门票' },
'DateType': { 'key': 'ConfirmDate', 'label': '确认日期' },
'DateType': { 'key': 'confirmDate', 'label': '确认日期' },
'year': this.start_date,
// 'months': [moment(), moment()],
'dates': [this.start_date, this.end_date],
@ -55,7 +55,7 @@ class DatePickerStore {
DepartmentList: 'ALL',
WebCode: 'ALL',
IncludeTickets: '1',
DateType: 'ConfirmDate',
DateType: 'confirmDate',
Date1: this.start_date.format('YYYY-MM-DD'),
Date2: this.end_date.format('YYYY-MM-DD 23:59:59'),
};
@ -67,6 +67,11 @@ class DatePickerStore {
setFormValuesToSub(data){
this.formValuesToSub = data;
}
siderBroken = false;
setSiderBroken(broken){
this.siderBroken = broken;
}
}
export default DatePickerStore;

@ -1,19 +1,19 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { DATE_FORMAT } from './../config';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment';
import { isEmpty, pick, sortBy, fixTo2Decimals, cloneDeep, unique } from '../utils/commons';
const modelMapper = {
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays', keySort: true },
'PML': { url: '/service-Analyse2/GetTradeApartByPML', keySort: true },
'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays', keySort: true },
'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays', keySort: true },
'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum', keySort: true },
'destination': { url: '/service-Analyse2/GetTradeApartByDestination', keySort: false },
'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination', keySort: false },
'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry', keySort: false },
'guestCountry': { url: '/service-Analyse2/GetTradeApartByGuestCountry', keySort: false },
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays', keySort: true, dynamicsX: false },
'PML': { url: '/service-Analyse2/GetTradeApartByPML', keySort: true, dynamicsX: false },
'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays', keySort: true, dynamicsX: false },
'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays', keySort: true, dynamicsX: false },
'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum', keySort: true, dynamicsX: false },
'destination': { url: '/service-Analyse2/GetTradeApartByDestination', keySort: false, dynamicsX: true, },
'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination', keySort: false, dynamicsX: true, },
'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry', keySort: false, dynamicsX: true, },
'guestCountry': { url: '/service-Analyse2/GetTradeApartByGuestCountry', keySort: false, dynamicsX: true, },
};
class Distribution {
constructor(appStore) {
@ -37,24 +37,38 @@ class Distribution {
// 同比的参数: 去年同期
const [DateToY1, DateToY2] = [moment(param.Date1).subtract(1, 'year'), moment(param.Date2).subtract(1, 'year')];
param.DateToY1 = DateToY1.format(DATE_FORMAT);
param.DateToY2 = DateToY2.format(`${DATE_FORMAT} 23:59:59`);
param.DateToY2 = DateToY2.format(SMALL_DATETIME_FORMAT);
param.DateToQ1 = DateToQ1.format(DATE_FORMAT);
param.DateToQ2 = DateToQ2.format(`${DATE_FORMAT} 23:59:59`);
const json = await req.fetchJSON(modelMapper[mkey].url, param);
param.DateToQ2 = DateToQ2.format(SMALL_DATETIME_FORMAT);
const dynamicsX = modelMapper[mkey].dynamicsX;
const json = dynamicsX === false ? await req.fetchJSON(modelMapper[mkey].url, param) : await this.getApartDataStep(param);
if (json.errcode === 0) {
const dataLength = json.result.length;
const pickResult = dataLength > 20 ? json.result.slice(0, 30) : json.result;
const dataSource = calcDiff({ result: pickResult, resultToY: json.resultToY, resultToQ: json.resultToQ }, modelMapper[mkey].keySort);
const dataSource = calcDiff({ result: json.result, resultToY: json.resultToY, resultToQ: json.resultToQ }, modelMapper[mkey].keySort);
runInAction(() => {
this[mkey].loading = false;
this[mkey].originData = json.result;
this[mkey].dataSource = dataSource;
this[mkey].dataSource = dataLength > 20 ? dataSource.slice(0,30) : dataSource;
this.pageLoading = false;
});
}
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 };
};
/**
* 明细
*/
@ -69,7 +83,7 @@ class Distribution {
this.scatterDays = daysData;
});
}
return this.detailData;
return json.result;
};
resetData = () => {
@ -153,6 +167,17 @@ const calcDiff = ({ result, resultToY, resultToQ }, keySort) => {
: 0,
ConfirmOrderDiffY: resultMapped[row.key].ConfirmOrder - resultToYMapped[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] };
});

@ -85,11 +85,17 @@ class FinancialStore {
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() {
const date_picker_store = this.rootStore.date_picker_store;
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';
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';
@ -110,7 +116,7 @@ class FinancialStore {
get_credit_card_bills_by_type() {
const date_picker_store = this.rootStore.date_picker_store;
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';
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';

@ -14,6 +14,7 @@ import TradeStore from "./Trade";
import KPI from "./KPI";
import DictData from "./DictData";
import Distribution from "./Distribution";
import DataPivot from './DataPivot';
class Index {
constructor() {
this.dashboard_store = new DashboardStore(this);
@ -31,6 +32,7 @@ class Index {
this.KPIStore = new KPI(this);
this.DictDataStore = new DictData(this);
this.DistributionStore = new Distribution(this);
this.DataPivotStore = new DataPivot(this);
makeAutoObservable(this);
}

@ -73,6 +73,21 @@ class OrdersStore {
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) {
this.active_tab_key_sub = active_key;

@ -32,8 +32,22 @@ class SaleStore {
type_data_sub = []; // 类型的子维度数据
date_title = 'date_title'; // 日期段,只用于显示,防止日期选择控件的变化导致页面刷新
searchValues = {
DateType: { key: 'confirmDate', 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;
}
salesTrade = {
groupType: 'dept', loading: false,
groupType: 'dept', loading: false, tableDataSource: [],
operator: [], dept: [], overview: [],
operatorMapped: {}, pickSales: [], pickSalesData: [],
};
@ -251,7 +265,7 @@ class SaleStore {
sorter: (a, b) => b.WXNewGuestCount - a.WXNewGuestCount,
},
];
result.dataSource = json.result1;
result.dataSource = json.result1.sort(comm.sortBy('PriceTime')).reverse();
} else if (this.active_tab_key === 'ResponseRateWhatsApp') {
result.columns = [
{
@ -327,7 +341,7 @@ class SaleStore {
sorter: (a, b) => b.COLI_ConfirmTimeAVG - a.COLI_ConfirmTimeAVG,
},
];
result.dataSource = json.result1;
result.dataSource = json.result1.sort(comm.sortBy('COLI_ConfirmTimeAVG')).reverse();
} else {
const diffDateFlagYes = !comm.isEmpty(date_moment.start_date_cp);
// if (this.active_tab_key == "Country")
@ -405,13 +419,17 @@ class SaleStore {
vs: comm.fixTo2Decimals(((total_data_value-total_data_value_diff)/total_data_value_diff)*100)+'%',
};
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 }],
sorter: (a, b) => b[`TV_${item.SubTypeSN}`] - a[`TV_${item.SubTypeSN}`],
});
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(() => {
@ -511,15 +529,36 @@ 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) {
this.salesTrade.loading = true;
const param1 = Object.assign({}, queryData, {groupType, groupDateType: 'year' });
const yearData = await this.fetchTradeDataAll(param1);
const { mergeRows: yearMergeRows } = yearData.result1;
const yData = parseSaleData(yearMergeRows, ['groupsKey', 'groupDateType']);
const param2 = Object.assign({}, queryData, {groupType, groupDateType: 'month' });
const monthData = await this.fetchTradeDataAll(param2);
const { mergeRows: monthMergeRows } = monthData.result1;
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 => ({
...yData[ykey],
@ -529,6 +568,17 @@ class SaleStore {
yearML: Object.values(yData[ykey].data)[0]?.SumML || 0, // 整理排序用
}));
const accountYData = parseSaleData(yearMergeLabelsRows, ['groupsKey', 'groupDateType']);
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}), {});
@ -542,18 +592,16 @@ class SaleStore {
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;
});
runInAction(() => {
this.salesTrade.loading = false;
this.salesTrade[groupType] = Object.values(salesTradeDataMapped).sort(comm.sortBy('yearML')).reverse();
this.salesTrade[`${groupType}Mapped`] = Object.values(salesTradeDataMapped).reduce((r, v) => ({...r, [v.groupsKey]: Object.values(v.mData)}), {});
});
}
return { salesTradeDataMapped, accountSalesTradeDataMapped, accountMergeYearMonth };
};
async getKPISettings(curObject, queryData, objects) {
const getkpiParam = comm.objectMapper(queryData, {

@ -1,7 +1,9 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import moment from 'moment';
import { isEmpty, sortBy, pick, merge, fixTo2Decimals, groupBy, sortKeys, fixToInt, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from './../libs/ht';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
class Trade {
constructor(rootStore) {
@ -14,19 +16,27 @@ class Trade {
*/
async fetchSummaryData(queryData) {
this.summaryData.loading = true;
queryData.groupType = queryData?.groupType || 'overview';
queryData.groupDateType = 'year';
const multiData = await this.fetchTradeDataAll(cloneDeep(queryData));
const curQueryData = cloneDeep(queryData);
curQueryData.groupType = curQueryData?.groupType || 'overview';
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;
// console.log(JSON.stringify(summary), 'mmmmmmmmmmm');
const { summary: summary2, traditional: traditional2, biz: biz2 } = multiData.result2;
// console.log(JSON.stringify(summary), 'mmmmmmmmmmm', multiData);
const summaryData = {
loading: false,
dataSource: [
{
title: '成团',
col: 6,
value: summary?.[0]?.ConfirmOrder,
originVal: (summary?.[0]?.ConfirmOrder || 0),
valueSuffix: summary?.[0]?.ConfirmRates ? ` / ${summary?.[0]?.ConfirmRates} %` : undefined,
originVal: summary?.[0]?.ConfirmOrder || 0,
valueSuffix: undefined,
// valueSuffix: summary?.[0]?.ConfirmRates ? ` / ${summary?.[0]?.ConfirmRates} %` : undefined,
// VSrate: summary?.[0]?.ConfirmOrderrate,
KPIrate: 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],
traditional: { title: '传统', value: traditional?.[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: '毛利',
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)),
col: 8,
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],
hasKPI: false,
childrenVisible: true,
kpiVal: summary?.[0]?.[dataFieldAlias.SumML.nestkey.v],
traditional: { title: '传统', value: (traditional?.[0]?.SumML || 0) },
biz: { title: '商务', value: (biz?.[0]?.SumML || 0) },
traditional: { title: '传统', value: traditional?.[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: '完成率',
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] || ''}%`,
hasKPI: false,
childrenVisible: false,
kpiVal: 0 , // summary?.[0]?.[dataFieldAlias.SumML.nestkey.p],
traditional: { title: '传统', value: traditional?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0, },
biz: { title: '商务', value: biz?.[0]?.[dataFieldAlias.SumML.nestkey.p] || 0, },
kpiVal: 0, // summary?.[0]?.[dataFieldAlias.SumML.nestkey.p],
traditional: { title: '传统', value: traditional?.[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: '人数',
originVal: (summary?.[0]?.SumPersonNum || 0),
col: 5,
originVal: summary?.[0]?.SumPersonNum || 0,
value: summary?.[0]?.SumPersonNum,
// VSrate: summary?.[0]?.SumPersonNumrate,
// KPIrate: summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.p],
hasKPI: false, // // !isEmpty(summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.p]),,
childrenVisible: true,
// kpiVal: summary?.[0]?.[dataFieldAlias.SumPersonNum.nestkey.v],
traditional: { title: '传统', value: traditional?.[0]?.SumPersonNum, },
biz: { title: '商务', value: biz?.[0]?.SumPersonNum, },
traditional: { title: '传统', value: traditional?.[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,
},
}
: {}),
},
],
};
@ -87,15 +141,59 @@ class Trade {
queryData.groupType = queryData?.groupType || 'overview';
Object.assign(queryData, { groupDateType: this.timeLineKey });
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);
const mergeData = [].concat(traditional, biz);
const dateKeyData = groupBy(mergeData, ele => ele.groupDateVal);
const sortByDateKey = Object.values(sortKeys(dateKeyData)).reduce( (a, b) => a.concat(b), []);
runInAction(() => {
this.timeData.loading = false;
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;
});
}
@ -240,18 +338,26 @@ class Trade {
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.searchValues = form;
}
timeLineKey = 'week';
timeLineKey = 'month';
setTimeLineKey(v) {
this.timeLineKey = v;
}
groupKey = 'overview';
setGroupKey(v) {
this.groupKey = v;
}
resetData = () => {
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.sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
this.topData = {};
@ -261,7 +367,8 @@ class Trade {
searchPayloadHome = {};
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: [] };
sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
topData = {};
@ -275,6 +382,7 @@ class Trade {
{key: 'transactions', title: '营收', dataIndex: 'transactions', render: (v) => dataFieldAlias.transactions.formatter(v)},
], dataSource: [] };
}
export const parseMergeItem = ({traditional, biz}) => {
return ['result1', 'result2'].reduce((res, resKey) => {
const mergeItem = {
@ -284,7 +392,8 @@ export const parseMergeItem = ({traditional, biz}) => {
const mergeRes = [].concat(traditional[resKey], biz[resKey]);
const groupsData = mergeRes.reduce((r, v) => {
if (v.groupsKey ) {
(r[v.groupsKey] || (r[v.groupsKey] = [])).push(v);
const groupsKeyLower = v.groupsKey.toLowerCase();
(r[groupsKeyLower] || (r[groupsKeyLower] = [])).push(v);
}
return r;
}, {});
@ -307,8 +416,8 @@ export const parseMergeItem = ({traditional, biz}) => {
// ConfirmRatesKPIrates: 0, // todo:
}));
// 按对象汇总
const TMapped = traditional[resKey].reduce((r, v) => ({...r, [v.groupsKey]: v}), {});
const BMapped = biz[resKey].reduce((r, v) => ({...r, [v.groupsKey]: v}), {});
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(
@ -327,9 +436,9 @@ export const parseMergeItem = ({traditional, biz}) => {
}));
// 按每一行
// const ByDate = sortKeys(groupBy(mergeRes, ele => ele.groupDateVal));
const allRowsKeysData = groupBy(mergeRes, ele => `${ele.groupsKey}@${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 || '';
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),
@ -348,7 +457,32 @@ export const parseMergeItem = ({traditional, biz}) => {
OrderKPIrates: row.OrderKPIvalue ? fixTo2Decimals((row.SumOrder/row.OrderKPIvalue)*100) : 0,
// ConfirmRatesKPIrates: 0, // todo:
}));
return Object.assign(res, { [resKey]: Object.assign({}, mergeItem, { mergeRows, summary, summaryRows }) });
// 合并账户
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 }) });
}, {});
};

@ -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) {
return JSON.parse(JSON.stringify(obj));
}
@ -353,6 +366,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)
)
);
}
/**
* 深拷贝
*/
@ -513,3 +540,30 @@ export function flush(collection) {
export const numberFormatter = (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,53 @@
import React, { useContext, useEffect } from 'react';
import { Row, Col, Typography, Space, DatePicker, Button, Select, Table, Divider } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { useContext, useEffect } from 'react';
import { Row, Col, Typography, Space, Table, Divider } from 'antd';
import { stores_Context } from '../config';
import * as config from '../config';
import { observer } from 'mobx-react';
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 GroupSelect from './../components/search/GroupSelect';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const AgentGroupCount = () => {
const { customerServicesStore } = useContext(stores_Context);
const agentList = customerServicesStore.agentList;
const { customerServicesStore, date_picker_store } = useContext(stores_Context);
const agentGroupList = customerServicesStore.agentGroupList;
const agentGroupListColumns = customerServicesStore.agentGroupListColumns;
const { startDate, endDate, dateType, inProgress } = customerServicesStore;
const { inProgress } = customerServicesStore;
useEffect(() => {
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 (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{ sm: 16, lg: 32 }} justify="end">
<Col md={24} lg={8} xxl={8}>
<Select
value={customerServicesStore.selectedAgent}
style={{ width: '95%' }}
showSearch
onSearch={(value) => {
console.log('search:', value);
}}
filterOption={(input, option) => {
return option.children.indexOf(input) > -1;
}}
onChange={(value) => customerServicesStore.selectAgent(value)}
>
<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]);
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...customerServicesStore.searchValues,
},
shows: ['agency', 'DateType', 'DepartmentList', 'countryArea', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
DateType: { disabledKeys: ['applyDate'] },
},
}}
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'),
],
onSubmit={(_err, obj, form) => {
customerServicesStore.setSearchValues(obj, form);
customerServicesStore.fetchAgentGroupCount();
}}
/>
</Col>
<Col md={24} lg={8} xxl={8}>
<Button
type="primary"
icon={<SearchOutlined />}
loading={inProgress}
onClick={() => {
handleSearchClick();
}}
>
统计
</Button>
</Col>
</Row>
<Row>
<Col span={24}>
<Typography.Title level={3}>地接社团信息</Typography.Title>
<Table
sticky
id="agentGroupList"
dataSource={agentGroupList}
columns={agentGroupListColumns}
@ -144,19 +55,10 @@ const AgentGroupCount = () => {
rowKey={(record) => record.key}
loading={inProgress}
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>
<TableExportBtn label={'地接社团信息'} {...{ columns: agentGroupListColumns, dataSource: agentGroupList }} />
</Divider>
</Col>
</Row>

@ -1,111 +1,72 @@
import React, {useContext, useEffect} from 'react';
import {Row, Col, Typography, Space, DatePicker, Button, Select, Table, List} from 'antd';
import {
SearchOutlined,
} from '@ant-design/icons';
import {stores_Context} from '../config';
import * as config from "../config";
import {observer} from 'mobx-react';
import { NavLink, useParams } from "react-router-dom";
import { useContext, useEffect } from 'react';
import { Row, Col, Typography, Space, Table, List } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import { NavLink, useParams } from 'react-router-dom';
import 'moment/locale/zh-cn';
import moment from "moment";
import zhCNlocale from 'antd/es/date-picker/locale/zh_CN';
import GroupSelect from './../components/search/GroupSelect';
import SearchForm from './../components/search/SearchForm';
const AgentGroupList = () => {
const {agentId} = useParams();
const { customerServicesStore } = useContext(stores_Context);
const { agentId } = useParams();
const { customerServicesStore, date_picker_store } = useContext(stores_Context);
useEffect(() => {
customerServicesStore.fetchGroupListByAgentId(agentId);
}, []);
customerServicesStore.fetchGroupListByAgentId(agentId);
}, []);
const groupList = customerServicesStore.groupList;
const groupListColumns = customerServicesStore.groupListColumns;
const {startDate, endDate, dateType, inProgress} = customerServicesStore;
const { startDate, endDate, dateType, inProgress } = customerServicesStore;
return (
<>
<Space direction="vertical" style={{width: '100%'}}>
<Row gutter={{md: 24}} justify="end">
<Col span={8}>
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{ md: 24 }} justify="end">
<Col span={24}>
<NavLink to={'/agent/group/count'}>返回</NavLink>
</Col>
<Col span={4}>
<GroupSelect value={customerServicesStore.selectedTeam}
onChange={(value) => customerServicesStore.selectTeam(value)}
style={{ width: '95%' }} show_all={true}
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...customerServicesStore.searchValues,
},
shows: ['DateType', 'DepartmentList', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
DateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form, str) => {
customerServicesStore.setSearchValues(obj, form);
customerServicesStore.fetchGroupListByAgentId(agentId);
}}
/>
</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>
</Col>
</Row>
<Row>
<Col span={24}>
<Typography.Title level={3}>{customerServicesStore.agentCompany}</Typography.Title>
<Table
sticky
dataSource={groupList}
columns={groupListColumns}
size="small"
rowKey={(record) => record.key}
loading={inProgress}
pagination={false}
scroll={{ x: "100%" }}
scroll={{ x: 1000 }}
expandable={{
expandedRowRender: (record) => (
<List
itemLayout="horizontal"
>
<List itemLayout="horizontal">
<List.Item>
<List.Item.Meta
title='经过城市'
description={record.PassCity}
/>
<List.Item.Meta title="经过城市" description={record.PassCity} />
</List.Item>
<List.Item>
<List.Item.Meta
title='评论内容'
description={record.ECI_Content}
/>
<List.Item.Meta title="评论内容" description={record.ECI_Content} />
</List.Item>
</List>
),
@ -113,9 +74,9 @@ const AgentGroupList = () => {
/>
</Col>
</Row>
</Space>
</>
);
</Space>
</>
);
};
export default observer(AgentGroupList);

@ -9,6 +9,7 @@ import BillTypeSelect from "../components/search/BillTypeSelect";
import GroupSelect from "../components/search/GroupSelect";
import Business_unit from "../components/search/BusinessSelect";
import DatePickerCharts from "../components/search/DatePickerCharts";
import SearchForm from './../components/search/SearchForm';
import { Line, Pie } from "@ant-design/charts";
import * as comm from "../utils/commons";
import * as config from "../config";
@ -266,36 +267,32 @@ const Credit_card_bill = () => {
return (
<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}>
<h2>信用卡账单</h2>
</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>
@ -315,7 +312,7 @@ const Credit_card_bill = () => {
</span>
}
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">
<a
onClick={() => {
@ -334,7 +331,7 @@ const Credit_card_bill = () => {
</span>
}
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">
<a
onClick={() => {

@ -22,11 +22,24 @@ class Dashboard extends Component {
<Col span={24}>
<ExchangeRate />
</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 />
</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 />
</Col>
</Row>

@ -0,0 +1,589 @@
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: 'operatorName', label: '' },
{ key: 'WebCode', label: '来源站点' },
// todo: , ,
{ key: 'destinationCountry_AsJOSN', label: '目的地国籍' },
];
const filterFieldsMapped = filterFields.reduce((r, v) => ({ ...r, [v.key]: v }), {});
/** 预设的选项, 只有行 */
const quickOptions = [
{ 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' },
],
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: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em' },
{ key: 'OrderValue', title: '单个订单价值', dataIndex: 'OrderValue', 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 [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 } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, [curXfield]));
// console.log('data====', data, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns);
setShowPassCountryTips(pivotKeys.includes('destinationCountry_AsJOSN'));
setDataBeforePick(data.sort(sortBy(curXfield)));
// 线
setDataSource(data.sort(sortBy(curXfield)));
//
const sortRowData = cloneDeep(summaryRows).sort(sortBy(defaultValKey)).reverse();
setPivotTableDataSource(sortRowData);
//
const sortColData = summaryColumns.sort(sortBy(defaultValKey)).reverse();
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, //
},
};
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',
})),
})),
],
};
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'} {...{ columns: targetTableProps.columns, dataSource: pivotTableDataSource }} />
</Divider>
<Table {...targetTableProps} dataSource={pivotTableDataSource} components={{ body: { cell: TdCell } }} />
</Spin>
</section>
</div>
);
});

@ -1,109 +1,51 @@
import React, {useContext, useEffect} from 'react';
import { Row, Col, Typography, Space, DatePicker, Button, Select, Table, Divider } from 'antd';
import {
SearchOutlined,
} from '@ant-design/icons';
import { useContext, useEffect } from 'react';
import { Row, Col, Typography, Space, Table, Divider } from 'antd';
import { stores_Context } from '../config';
import * as config from "../config";
import { observer } from 'mobx-react';
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 GroupSelect from './../components/search/GroupSelect';
import { utils, writeFileXLSX } from 'xlsx';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const DestinationGroupCount = () => {
const {customerServicesStore} = useContext(stores_Context);
const { customerServicesStore, date_picker_store } = useContext(stores_Context);
const destinationGroupCount = customerServicesStore.destinationGroupCount;
const destinationGroupCountColumns = customerServicesStore.destinationGroupCountColumns;
const {startDate, endDate, dateType, inProgress} = customerServicesStore;
useEffect(() => {
customerServicesStore.selectCountry('china');
}, []);
const handleSearchClick = () => {
customerServicesStore.fetchDestinationGroupCount();
};
const { startDate, endDate, dateType, inProgress } = customerServicesStore;
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{ sm: 16, lg: 32 }} justify="end">
<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="china" value="china">
国内
</Select.Option>
<Select.Option key="foreign" value="foreign">
国外
</Select.Option>
</Select>
</Col>
<Col md={24} lg={8} xxl={8}>
<Select value={customerServicesStore.selectedOrderStatus} style={{ width: "95%" }} onChange={(value) => customerServicesStore.selectStatus(value)}>
<Select.Option key="所有" value="-1">
所有
</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')],
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
countryArea: { key: 'china', label: '国内' },
...customerServicesStore.searchValues,
},
shows: ['DateType', 'DepartmentList', 'countryArea', 'orderStatus', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
orderStatus: { show_all: true },
countryArea: { show_all: false },
dates: { hide_vs: true },
DateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form) => {
customerServicesStore.setSearchValues(obj, form);
customerServicesStore.fetchDestinationGroupCount();
}}
/>
</Col>
<Col md={24} lg={8} xxl={8}>
<Button
type="primary"
icon={<SearchOutlined />}
loading={inProgress}
onClick={() => {
handleSearchClick();
}}>
统计
</Button>
</Col>
</Row>
<Row>
<Col span={24}>
<Typography.Title level={3}>目的地团信息</Typography.Title>
<Table
sticky
id="destinationGroupCount"
dataSource={destinationGroupCount}
columns={destinationGroupCountColumns}
@ -111,16 +53,10 @@ const DestinationGroupCount = () => {
rowKey={(record) => record.key}
loading={inProgress}
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>
<TableExportBtn label={'目的地团信息'} {...{ columns: destinationGroupCountColumns, dataSource: destinationGroupCount }} />
</Divider>
</Col>
</Row>

@ -1,112 +1,74 @@
import React, {useContext, useEffect} from 'react';
import {Row, Col, Typography, Space, DatePicker, Button, Select, Table, List} from 'antd';
import {
SearchOutlined,
} from '@ant-design/icons';
import {stores_Context} from '../config';
import * as config from "../config";
import {observer} from 'mobx-react';
import { NavLink, useParams } from "react-router-dom";
import { useContext, useEffect } from 'react';
import { Row, Col, Space, Table, List } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import { NavLink, useParams } from 'react-router-dom';
import 'moment/locale/zh-cn';
import moment from "moment";
import zhCNlocale from 'antd/es/date-picker/locale/zh_CN';
import GroupSelect from './../components/search/GroupSelect';
import SearchForm from './../components/search/SearchForm';
const DestinationGroupList = () => {
const {destinationId} = useParams();
const { customerServicesStore } = useContext(stores_Context);
const { destinationId } = useParams();
const { customerServicesStore, date_picker_store } = useContext(stores_Context);
useEffect(() => {
customerServicesStore.fetchGroupListByDestinationId(destinationId);
}, []);
customerServicesStore.fetchGroupListByDestinationId(destinationId);
}, []);
const destinationGroupList = customerServicesStore.destinationGroupList;
const destinationGroupListColumns = customerServicesStore.destinationGroupListColumns;
const {startDate, endDate, dateType, inProgress} = customerServicesStore;
const { inProgress } = customerServicesStore;
return (
<>
<Space direction="vertical" style={{width: '100%'}}>
<Row gutter={{md: 24}} justify="end">
<Col span={8}>
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{ md: 24 }} justify="end">
<Col span={24}>
<NavLink to={'/destination/group/count'}>返回</NavLink>
</Col>
<Col span={4}>
<GroupSelect value={customerServicesStore.selectedTeam}
onChange={(value) => customerServicesStore.selectTeam(value)}
style={{ width: '95%' }} show_all={true}
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
countryArea: { key: 'china', label: '国内' },
...customerServicesStore.searchValues,
},
shows: ['DateType', 'DepartmentList', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
orderStatus: { show_all: true },
countryArea: { show_all: false },
dates: { hide_vs: true },
DateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form) => {
customerServicesStore.setSearchValues(obj, form);
customerServicesStore.fetchGroupListByDestinationId(destinationId);
customerServicesStore.fetchDestinationGroupCount();
}}
/>
</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>
<Col span={24}>
<Table
sticky
dataSource={destinationGroupList}
columns={destinationGroupListColumns}
size="small"
rowKey={(record) => record.key}
loading={inProgress}
pagination={false}
scroll={{ x: "100%" }}
scroll={{ x: 600 }}
expandable={{
expandedRowRender: (record) => (
<List
itemLayout="horizontal"
>
<List itemLayout="horizontal">
<List.Item>
<List.Item.Meta
title='经过城市'
description={record.PassCity}
/>
<List.Item.Meta title="经过城市" description={record.PassCity} />
</List.Item>
<List.Item>
<List.Item.Meta
title='地接社'
description={record.VendorList}
/>
<List.Item.Meta title="地接社" description={record.VendorList} />
</List.Item>
</List>
),
@ -114,9 +76,9 @@ const DestinationGroupList = () => {
/>
</Col>
</Row>
</Space>
</>
);
</Space>
</>
);
};
export default observer(DestinationGroupList);

@ -1,12 +1,12 @@
import { useContext, useEffect } from 'react';
import { observer } from 'mobx-react';
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 SearchForm from './../components/search/SearchForm';
import { empty } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
import { VSTag } from './../components/Data';
import { VSTag, TableExportBtn } from './../components/Data';
import MixYnQ from './../components/MixYnQ';
import './kpi.css';
@ -19,15 +19,22 @@ const apartOptions = [
{ key: 'ConfirmDays', value: 'ConfirmDays', label: '成团周期' },
{ key: 'ApplyDays', value: 'ApplyDays', label: '预定周期' },
{ key: 'PersonNum', value: 'PersonNum', label: '人等' },
{ key: 'destination', value: 'destination', label: '国内目的地' },
{ key: 'GlobalDestination', value: 'GlobalDestination', label: '海外目的地' },
{ key: 'destinationCountry', value: 'destinationCountry', label: '目的地国籍' },
{ key: 'guestCountry', value: 'guestCountry', label: '客人国籍' },
{ key: 'destination', value: 'destination', label: '国内目的地', },
{ key: 'GlobalDestination', 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(() => {
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { curTab, dateStringQ, dateStringY } = DistributionStore;
const pageRefresh = (obj) => {
@ -73,7 +80,30 @@ export default observer(() => {
innerRadius: 0.90,
};
const columns = [
{ title: '', dataIndex: 'label' },
{ title: '#', dataIndex: 'label' },
{
title: '预定',
dataIndex: 'SumOrder',
render: (v, r) => (
<>
<Row align={'middle'}>
<Col flex={"100px"}>
<Text strong>{v}</Text>
</Col>
<Col flex={'auto'}>
<Space direction={'vertical'}>
<span>
<span>同比: </span> <VSTag diffPercent={r.SumOrderToY} diffData={r.SumOrderDiffY} />
</span>
<span>
<span>环比: </span> <VSTag diffPercent={r.SumOrderToQ} diffData={r.SumOrderDiffQ} />
</span>
</Space>
</Col>
</Row>
</>
),
},
{
title: '团数',
dataIndex: 'ConfirmOrder',
@ -132,36 +162,38 @@ export default observer(() => {
},
{
title: () => <><div>去年同期</div><div>{dateStringY}</div></>,
titleX: ['去年同期', dateStringY], //
align: 'center',
children: [
{
title: '团数占比',
width: 90,
dataIndex: 'ConfirmOrderPercent',
dataIndex: ['resultToY', 'ConfirmOrderPercent'], // 'ConfirmOrderPercent',
render: (v, r) => r.resultToY.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.ConfirmOrderPercent / 100} /> : '-',
},
{
title: '业绩占比',
width: 90,
dataIndex: 'SumMLPercent',
dataIndex: ['resultToY', 'SumMLPercent'], // 'SumMLPercent',
render: (v, r) => r.resultToY.SumMLPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.SumMLPercent / 100} /> : '-',
},
],
},
{
title: () => <><div>上个时间段</div><div>{dateStringQ}</div></>,
titleX: ['上个时间段', dateStringY], //
align: 'center',
children: [
{
title: '团数占比',
width: 90,
dataIndex: 'ConfirmOrderPercent',
dataIndex: ['resultToQ', 'ConfirmOrderPercent'], // 'ConfirmOrderPercent',
render: (v, r) => r.resultToQ.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.ConfirmOrderPercent / 100} /> : '-',
},
{
title: '业绩占比',
width: 90,
dataIndex: 'SumMLPercent',
dataIndex: ['resultToQ', 'SumMLPercent'], // 'SumMLPercent',
render: (v, r) => r.resultToQ.SumMLPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.SumMLPercent / 100} /> : '-',
},
],
@ -174,7 +206,7 @@ export default observer(() => {
};
return (
<>
<Row gutter={16} style={{ margin: '-16px -8px' }}>
<Row gutter={16} className={siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -205,8 +237,12 @@ export default observer(() => {
children: (
<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
id="table_to_xlsx_sale"
components={{ body: { cell: TdCell } }}
dataSource={DistributionStore[curTab].dataSource}
columns={columns}
size="small"

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from '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 { stores_Context } from '../config';
import StatisticCard2 from '../components/StatisticCard2';
@ -8,6 +8,8 @@ import Bullet from '../components/BulletWithSort';
import Waterfall from '../components/Waterfall';
import MixTBWithKPI from './../components/MixTBWithKPI';
import Donut from './../components/Donut';
import MapCountry from './../components/MapCountry';
import LineWithKPI from '../components/LineWithKPI';
import DataFieldRadio from '../components/DataFieldRadio';
import { datePartOptions } from './../components/DateGroupRadio/date';
import SearchForm from './../components/search/SearchForm';
@ -16,11 +18,18 @@ import { dataFieldAlias } from './../libs/ht';
import './home.css';
const topSeries = [
{ key: 'dept', label: '小组', graphVisible: true },
{ key: 'operator', label: '顾问', graphVisible: true },
{ key: 'country', label: '国籍', graphVisible: true },
{ key: 'GuestGroupType', label: '客群类别', graphVisible: false },
{ key: 'destination', label: '目的地', graphVisible: true },
{ key: 'dept', value: 'dept', label: '小组', graphVisible: true },
{ key: 'operator', value: 'operator', label: '顾问', graphVisible: true },
{ key: 'destination', value: 'destination', label: '目的地', graphVisible: true },
{ key: 'GuestGroupType', value: 'GuestGroupType', label: '客群类别', graphVisible: false },
{ key: 'country', value: 'country', label: '国籍', graphVisible: true },
{ key: 'webcode', value: 'webcode', label: '站点', graphVisible: false },
{ key: 'bizarea', value: 'bizarea', label: '国境', graphVisible: false },
];
const allGroupTypes = [
{ key: 'overview', value: 'overview', label: '总额' },
...topSeries,
];
// const iconSets = [CheckCircleTwoTone, <MoneyCollectTwoTone />, <FlagTwoTone />, <ClockCircleTwoTone />, <DashboardTwoTone />,<SmileTwoTone />,];
@ -29,8 +38,8 @@ const iconSets = [CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwo
export default observer(() => {
// const navigate = useNavigate();
const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { sideData, summaryData, BuData, topData, timeData, timeLineKey, targetTableProps } = TradeStore;
const { formValues } = searchFormStore;
const { searchValues, sideData, summaryData, BuData, topData, timeData, timeLineKey, targetTableProps, timeDiffData, groupKey } = TradeStore;
const { formValues, siderBroken } = searchFormStore;
useEffect(() => {
if (empty(summaryData.dataSource)) {
@ -47,9 +56,11 @@ export default observer(() => {
const groupType = _overviewFlag ? 'overview' : 'dept';
queryData.groupType = groupType;
setGroupTypeVal(groupType);
setDiffGroupKey('overview');
TradeStore.resetData();
TradeStore.fetchSummaryData(Object.assign({}, queryData, { groupType }));
TradeStore.fetchTradeDataByDate(queryData);
TradeStore.fetchTradeDataDiffByDate(queryData);
// // TradeStore.fetchTradeDataByBU(queryData);
TradeStore.fetchTradeDataByMonth(queryData);
const topSeriesF = _overviewFlag ? topSeries : topSeries.filter((ele) => ele.key !== 'dept');
@ -132,7 +143,6 @@ export default observer(() => {
xAxis: {
type: 'cat',
},
smooth: true,
point: {
size: 4,
shape: 'cicle',
@ -157,27 +167,38 @@ export default observer(() => {
TradeStore.setTimeLineKey(value);
if (!isEmpty(TradeStore.searchPayloadHome)) {
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 (
<>
<Row gutter={16} style={{ margin: '-16px -8px', position: 'sticky', top: 0, zIndex: 10 }}>
{/* style={{ margin: '-16px -8px', padding: 0 }} */}
<Row gutter={16} className={siderBroken ? "" : "sticky-top"}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
years: { hide_vs: true },
years: { hide_vs: false },
},
}}
onSubmit={(_err, obj, form, str) => {
TradeStore.setStateSearch(obj);
TradeStore.setSearch(obj, form);
pageRefresh(obj);
}}
/>
@ -185,7 +206,9 @@ export default observer(() => {
</Row>
<section>
<Space>
<h2>年度业绩=传统+商务</h2>
<h2>
年度业绩<span style={{ fontSize: 'small' }}> =传统+商务</span>
</h2>
</Space>
<Spin spinning={summaryData.loading}>
<Row gutter={layoutProps.gutter}>
@ -195,7 +218,7 @@ export default observer(() => {
</Col>
))} */}
{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]} />
</Col>
))}
@ -203,22 +226,44 @@ export default observer(() => {
</Spin>
</section>
<section>
<Space gutter={16} size={'large'}>
<h3>走势</h3>
<DataFieldRadio value={timeDataField} onChange={handleChangetimeDataField} />
<Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} />
</Space>
<Spin spinning={timeData.loading}>
{/* <LineWithKPI dataSource={timeData.dataSource} {...lineConfig} /> */}
<MixTBWithKPI dataSource={timeData.dataSource} summaryData={timeData.origin?.summaryRows || []} {...lineConfig} />
</Spin>
<Row gutter={16}>
<Col flex={'4em'}><h3>{showDiff === false ? '走势' : '对比'}</h3></Col>
<Col ><DataFieldRadio value={timeDataField} onChange={handleChangetimeDataField} /></Col>
<Col ><Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} /></Col>
<Col >{searchValues.yearDiff && (
<Button type="primary" ghost size={'small'} onClick={() => setShowDiff(!showDiff)}>
{showDiff === false ? '显示对比' : '返回本期走势'}
</Button>
)}</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>
<h3>市场 (仅传统订单) </h3>
<Spin spinning={BuData.loading}>
<Spin spinning={sideData.loading}>
<Row gutter={layoutProps3.gutter}>
<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 ? (
<>
<Bullet {...BUConfig} dataSource={BuData?.dataSource || []} />
@ -228,12 +273,14 @@ export default observer(() => {
<><Donut {...{angleField: 'SumML', colorField: 'groupsLabel'}} title={formValues.DepartmentList?.label} dataSource={sideData.yearData} /></>
)} */}
</Col>
{Object.keys(sideData.dataSource).sort().map((key) => (
<Col {...layoutProps3} key={key}>
<Waterfall key={key} {...WaterfallConfig} title={key} dataSource={sideData.dataSource[key]} line={summaryData.kpi} />
<h3 style={{ textAlign: 'center' }}>{`${key}每月业绩`}</h3>
</Col>
))}
{Object.keys(sideData.dataSource)
.sort()
.map((key) => (
<Col {...layoutProps3} key={key}>
<Waterfall key={key} {...WaterfallConfig} title={key} dataSource={sideData.dataSource[key]} line={summaryData.kpi} />
<h3 style={{ textAlign: 'center' }}>{`${key}每月业绩`}</h3>
</Col>
))}
</Row>
</Spin>
</section>
@ -241,21 +288,19 @@ export default observer(() => {
<h3>
英语区目标客户
<Spin spinning={topData?.GuestGroupType?.loading || false}>
<Table {...targetTableProps} pagination={false} />
<Table {...targetTableProps} pagination={false} rowKey={'groupsLabel'} />
</Spin>
</h3>
</section>
<section>
<Space>
<h3>TOP</h3>
<div>
<DataFieldRadio value={valueKey} onChange={handleChangeValueKey} />
</div>
</Space>
<Row gutter={layoutProps.gutter}>
<Row gutter={16}>
<Col flex={'4em'}><h3>TOP</h3></Col>
<Col ><DataFieldRadio value={valueKey} onChange={handleChangeValueKey} /></Col>
</Row>
<Row gutter={layoutProps3.gutter}>
{topSeriesSet.map((item) =>
item.graphVisible ? (
<Col {...layoutProps} key={item.key}>
<Col {...layoutProps3} key={item.key}>
<Spin spinning={topData[item.key]?.loading || false}>
<h3 style={{ textAlign: 'center' }}>{item.label}</h3>
<Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} itemLength={10} key={item.key} />
@ -263,6 +308,13 @@ export default observer(() => {
</Col>
) : null
)}
<Col key={'mapG'} flex={'1 0 600px'} >
<Spin spinning={topData?.country?.loading || false}>
<div id="topC" style={{ height: '700px' }}>
<MapCountry sourceField={'groupsLabel'} valueField={BUConfig.measureField} dataSource={topData?.country?.dataSource || []} />
</div>
</Spin>
</Col>
</Row>
</section>
</>

@ -1,18 +1,17 @@
import React, { Component } from "react";
import { Row, Col, Button, Tabs, Table, Divider, Select, Radio } from "antd";
import { ContainerOutlined, CarryOutOutlined, BlockOutlined, SmileOutlined, TagsOutlined, GlobalOutlined, SearchOutlined, FullscreenOutlined, DingtalkOutlined } from "@ant-design/icons";
import { Row, Col, Tabs, Table, Divider, Spin } from "antd";
import { ContainerOutlined, BlockOutlined, SmileOutlined, TagsOutlined, GlobalOutlined, FullscreenOutlined, DingtalkOutlined, CarryOutOutlined } from "@ant-design/icons";
import { stores_Context } from "../config";
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 DatePickerCharts from "../components/search/DatePickerCharts";
import * as config from "../config";
import { NavLink } from "react-router-dom";
import * as comm from "../utils/commons";
import { utils, writeFileXLSX } from "xlsx";
import DateGroupRadio from '../components/DateGroupRadio';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
class Orders extends Component {
static contextType = stores_Context;
@ -31,6 +30,7 @@ class Orders extends Component {
result.columns = [
{
title: "",
fixed: 'left',
children: [
{
title: (
@ -44,6 +44,7 @@ class Orders extends Component {
</span>
),
dataIndex: "OrderType",
fixed: 'left',
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>,
},
],
@ -166,6 +167,7 @@ class Orders extends Component {
result.columns = [
{
title: "",
fixed: 'left',
children: [
{
title: (
@ -175,7 +177,8 @@ class Orders extends Component {
</div>
</span>
),
dataIndex: "OrderType",
fixed: 'left',
dataIndex: "OrderType",
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>,
},
],
@ -219,7 +222,7 @@ class Orders extends Component {
}
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 data_source = orders_store.orderCountData ? orders_store.orderCountData : [];
const avg_line_y = Math.round(orders_store.avgLine1);
@ -287,7 +290,7 @@ class Orders extends Component {
return ret;
},
},
smooth: true,
// smooth: true,
};
const pie_config = {
appendPadding: 10,
@ -310,55 +313,44 @@ class Orders extends Component {
],
};
return (
<div>
<Row gutter={{ sm: 16, lg: 32 }}>
<Col md={24} lg={12} xxl={14}></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();
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'}}>
const tableProps = {
dataSource: table_data.dataSource,
columns: table_data.columns,
size: 'small',
pagination: false,
scroll: { x: (100*(table_data.columns.length)) },
loading: orders_store.loading,
};
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,
...orders_store.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
orders_store.setSearchValues(obj, form);
orders_store.getOrderCount();
orders_store.onChange_Tabs(orders_store.active_tab_key);
}}
/>
</Col>
</Row>
<Row gutter={[16, { sm: 16, lg: 32 }]}>
<Col span={24} style={{ textAlign: 'right' }}>
<DateGroupRadio
visible={data_source.length!==0}
visible={data_source.length !== 0}
dataRaw={orders_store.orderCountDataRaw}
onChange={orders_store.onChangeDateGroup}
value={orders_store.lineChartXGroup}
@ -366,177 +358,114 @@ class Orders extends Component {
fieldMapper={orders_store.orderCountDataFieldMapper}
/>
</Col>
<Col span={24}>
<Line {...config} />
</Col>
<Col span={24}>
<Spin spinning={orders_store.loading}>
<Line {...config} />
</Spin>
</Col>
<Col span={24}>
<Tabs activeKey={orders_store.active_tab_key} onChange={active_key => orders_store.onChange_Tabs(active_key)}>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined />
来源类型
</span>
}
key="Form">
<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>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_form").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "来源类型.xlsx");
}}>
导出excel
</a>
</Divider>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<CarryOutOutlined />
产品类型
</span>
}
key="Product">
<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={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_product").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "产品类型.xlsx");
}}>
导出excel
</a>
</Divider>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<SmileOutlined />
国籍
</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
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_country").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "国籍.xlsx");
}}>
导出excel
</a>
</Divider>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<TagsOutlined />
线路
</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>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById("table_to_xlsx_line").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "线路.xlsx");
}}>
导出excel
</a>
</Divider>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<GlobalOutlined />
目的地
</span>
}
key="city">
<Table id="table_to_xlsx_city" 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_city").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "目的地.xlsx");
}}>
导出excel
</a>
</Divider>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<BlockOutlined />
页面类型
</span>
}
key="LineClass">
<Table id="table_to_xlsx_LineClass" 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_LineClass").getElementsByTagName("table")[0]);
writeFileXLSX(wb, "页面类型.xlsx");
}}>
导出excel
</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>
);
<Col span={24}>
<Tabs
activeKey={orders_store.active_tab_key}
onChange={(active_key) => orders_store.onChange_Tabs(active_key)}
items={[
{
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: 'GuestGroupType',
label: (
<span>
<FullscreenOutlined />
客群类别
</span>
),
},
{
key: 'TravelMotivation',
label: (
<span>
<DingtalkOutlined />
出行动机
</span>
),
},
].map((ele) => {
return {
...ele,
children: (
<>
<Table sticky id={`table_to_xlsx_${ele.key}`} {...tableProps} />
<Divider orientation="right" plain>
<TableExportBtn label={ele.key} {...{ columns: tableProps.columns, dataSource: tableProps.dataSource }} />
</Divider>
</>
),
};
})}
/>
<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 { Row, Col, Button, Tabs, Table, Select, Divider } from "antd";
import { ContainerOutlined, SearchOutlined } from "@ant-design/icons";
import React, { useContext, useEffect, useState } from "react";
import { Row, Col, Tabs, Table, Divider } from "antd";
import { ContainerOutlined } from "@ant-design/icons";
import { stores_Context } from "../config";
import { Line } from "@ant-design/charts";
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 * as comm from "../utils/commons";
import * as config from "../config";
import { utils, writeFileXLSX } from "xlsx";
import DateGroupRadio from '../components/DateGroupRadio';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const Orders_sub = () => {
const { ordertype, ordertype_sub, ordertype_title } = useParams();
@ -178,173 +176,152 @@ const Orders_sub = () => {
const table_data_p = format_data(orders_store.orderCountData_Form_sub.ordercount1);
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 }]}>
<Col span={24} style={{textAlign: 'right'}}>
<DateGroupRadio
visible={data_source.length!==0}
dataRaw={orders_store.orderCountDataRaw_type}
onChange={orders_store.onChangeDateGroupSub}
value={orders_store.orderCount_type_dateRadio.lineChartXGroup}
dataMapper={orders_store.orderCount_type_dateRadio.orderCountDataMapper}
fieldMapper={orders_store.orderCount_type_dateRadio.orderCountDataFieldMapper}
/>
</Col>
<Col className="gutter-row" span={24}>
<Line {...line} />
</Col>
const tab_items = [
{
key: 'detail', 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
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}>
{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}>
<Tabs activeKey={orders_store.active_tab_key_sub} onChange={active_key => orders_store.onChange_Tabs_sub(ordertype, ordertype_sub, active_key)}>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined />
订单内容
</span>
}
key="detail">
<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}>
{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_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" />
</Col>
</Row>)},
{ key: 'page_cxstate1', 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 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>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined />
访问路径
</span>
}
key="page">
<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}>
{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_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" />
</Col>
</Row>)},
];
const [propsForExport, setPropsForExport] = useState(tab_items[0]);
const tabItemsMapped = tab_items.reduce((r, v) => ({...r, [v.key]: v}), {});
const onTabsChange = (active_key) => {
setPropsForExport(tabItemsMapped[active_key]);
orders_store.onChange_Tabs_sub(ordertype, ordertype_sub, active_key);
};
return (
<div>
<Row gutter={{ sm: 16, lg: 32 }} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col md={24} lg={12} xxl={14}>
<NavLink to={`/orders`}>返回</NavLink>
</Col>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
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}>
{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_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" />
</Col>
</Row>
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined />
访问路径成行
</span>
}
key="page_cxstate1">
<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>
<Row gutter={[16, { xs: 8, sm: 16, md: 24, lg: 32 }]}>
<Col span={24} style={{ textAlign: 'right' }}>
<DateGroupRadio
visible={data_source.length !== 0}
dataRaw={orders_store.orderCountDataRaw_type}
onChange={orders_store.onChangeDateGroupSub}
value={orders_store.orderCount_type_dateRadio.lineChartXGroup}
dataMapper={orders_store.orderCount_type_dateRadio.orderCountDataMapper}
fieldMapper={orders_store.orderCount_type_dateRadio.orderCountDataFieldMapper}
/>
</Col>
<Col className="gutter-row" span={24}>
<Line {...line} />
</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_p.dataSource} rowKey={record => record.key} columns={table_data2_p.columns} size="small" />
</Col>
</Row>
</Tabs.TabPane>
</Tabs>
</Col>
</Row>
</div>
);
<Col className="gutter-row" span={24}>
<Tabs
activeKey={orders_store.active_tab_key_sub}
onChange={onTabsChange}
tabBarExtraContent={{
right: (
<TableExportBtn
label={propsForExport.title}
{...(orders_store.active_tab_key_sub === 'detail'
? { 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);

@ -1,36 +1,84 @@
import React, {useContext, useEffect} from 'react';
import {Row, Col, Button, Tabs, Spin, Result, Space} from 'antd';
import {
ContainerOutlined,
SearchOutlined,
} from '@ant-design/icons';
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";
import { useContext } from 'react';
import { Button, Result, message, Typography, Image } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import { Outlet, useLocation } from 'react-router-dom';
import authExample from './../auth-apply.png';
const ProtectedRoute = ({auth}) => {
const {auth_store} = useContext(stores_Context);
const { Text } = Typography;
if (auth_store.has_permission(auth)) {
return <Outlet/>;
}
const ProtectedRoute = ({ auth }) => {
const { auth_store } = useContext(stores_Context);
return (
<div>
<Result
status="403"
title="403 权限不足"
subTitle={"试着联系一下技术,所需权限: "+auth.toString()}
extra=''
/>
</div>
);
if (auth_store.has_permission(auth)) {
return <Outlet />;
}
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="403"
title="403 权限不足"
subTitle={
<>
<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>
</>
}
/>
</div>
);
};
export default observer(ProtectedRoute);

@ -1,17 +1,13 @@
import React, { useContext, useEffect } from 'react';
import { Row, Col, Button, Tabs, Table, Divider, Radio, Select } from 'antd';
import { ContainerOutlined, SearchOutlined, UserSwitchOutlined } from '@ant-design/icons';
import React, { useContext } from 'react';
import { Row, Col, Tabs, Table, Divider, Spin } from 'antd';
import { ContainerOutlined, UserSwitchOutlined } from '@ant-design/icons';
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 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 config from '../config';
import SiteSelect from '../components/search/SiteSelect';
import GroupSelect from '../components/search/GroupSelect';
import { utils, writeFileXLSX } from 'xlsx';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const Sale = () => {
const { sale_store, date_picker_store } = useContext(stores_Context);
@ -204,51 +200,34 @@ 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 (
<div>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
<Col md={24} lg={12} xxl={14}></Col>
<Col md={24} lg={12} xxl={10}>
<Row>
<Col md={24} lg={8} xxl={8}>
<GroupSelect store={sale_store} />
</Col>
<Col md={24} lg={8} xxl={8}>
<SiteSelect store={sale_store} show_all={true} />
</Col>
<Col md={24} lg={8} xxl={8}>
<Select style={{ width: '100%' }} placeholder="是否含门票" value={sale_store.include_tickets} onChange={sale_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={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 gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...sale_store.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
sale_store.setSearchValues(obj, form);
sale_store.get_department_order_ml_by_type(date_picker_store);
}}
/>
</Col>
</Row>
{/* <Row>
<Col md={24} lg={12} xxl={12}>
@ -284,12 +263,11 @@ const Sale = () => {
</Select>
</Col>
</Row> */}
</Col>
</Row>
<Row>
<Col className="gutter-row" md={24}>
<Column {...column_config} />
<Spin spinning={sale_store.loading_table} >
<Column {...column_config} /></Spin>
</Col>
<Col className="gutter-row" md={24}>
@ -303,25 +281,18 @@ const Sale = () => {
></Tabs>
<Row>
<Col span={24}>
<Table
<Table sticky
id="table_to_xlsx_sale"
dataSource={type_data.dataSource}
columns={type_data.columns}
columns={tableColumns}
size="small"
rowKey={(record) => record.key}
loading={sale_store.loading_table}
pagination={false}
scroll={{ x: '100%' }}
scroll={{ x: (100*(tableColumns.length)) }}
/>
<Divider orientation="right" plain>
<a
onClick={() => {
const wb = utils.table_to_book(document.getElementById('table_to_xlsx_sale').getElementsByTagName('table')[0]);
writeFileXLSX(wb, 'sale.xlsx');
}}
>
导出excel
</a>
<TableExportBtn label={'sale'} {...{ columns: tableColumns, dataSource: type_data.dataSource }} />
</Divider>
</Col>
<Col sm={24} lg={12}>

@ -1,31 +1,38 @@
import React, { useContext } from 'react';
import { Row, Col, Table, Select, Space, Typography, Progress, Spin } from 'antd';
import React, { useContext, useState } from 'react';
import { Row, Col, Table, Select, Space, Typography, Progress, Spin, Divider, Button, Switch } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import * as comm from '../utils/commons';
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 overviewGroupKeys = overviewGroup.map(item => item.key);
const Sale_KPI = () => {
const { sale_store, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues } = searchFormStore;
const { groupType, loading, operator, } = sale_store.salesTrade;
const { formValues, siderBroken } = searchFormStore;
const { groupType, loading, operator, tableDataSource: dataSource } = sale_store.salesTrade;
const dataSource = [].concat(sale_store.salesTrade[groupType], operator);
const yearData = Object.values(sale_store.salesTrade[groupType]?.[0]?.mData || {});
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 = 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';
sale_store.setGroupType(_groupType);
sale_store.fetchOperatorTradeData(_groupType, { ...queryData, groupDateType: 'year' });
sale_store.fetchOperatorTradeData('operator', { ...queryData, groupDateType: 'year' });
await sale_store.fetchOperatorTradeData(_groupType, { ...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) => {
return {
title: `${index + 1}`,
@ -68,11 +75,13 @@ const Sale_KPI = () => {
dataIndex: 'groupsLabel',
editable: false,
width: '7.5em',
fixed: 'left',
},
{
title: '年度',
dataIndex: 'yearValue',
width: '10em',
fixed: 'left',
render: (_, row) => (
<Space direction={'vertical'}>
<div>
@ -100,24 +109,72 @@ const Sale_KPI = () => {
},
...monthCol,
];
const lineConfig = { appendPadding: 10, xField: 'groupDateVal', yField: 'SumML', seriesField: 'groupsLabel', isGroup: true, smooth: true, meta: comm.cloneDeep(dataFieldAlias), };
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 (
<div>
<Row gutter={16} style={{ margin: '-16px -8px' }}>
<Row gutter={16} className={siderBroken ? "" : "sticky-top"}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...sale_store.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
WebCode: { show_all: false, mode: 'multiple', col: 5 },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
sale_store.setSearchValues(obj, form);
pageRefresh(obj);
}}
/>
@ -126,45 +183,63 @@ const Sale_KPI = () => {
<Spin spinning={loading}>
<h2 style={{ marginTop: '.5em' }}>年度业绩组成和走势</h2>
<Row>
<Col className="gutter-row" md={8}>
<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}>
<LineWithKPI dataSource={yearData} {...lineConfig} {...{legend: false}} />
<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}>
<Space gutter={16} size={'large'}>
<h2>顾问业绩走势</h2>
<Select
labelInValue
mode={'multiple'}
style={{ width: '400px' }}
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>
</Space>
<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" md={24}>
<LineWithKPI dataSource={sale_store.salesTrade.pickSalesData} {...lineConfig} />
<Col className="gutter-row" span={24}>
<LineWithKPI dataSource={sale_store.salesTrade.pickSalesData} showKPI={true} {...lineConfig} />
</Col>
</Row>
<Row>
<Col className="gutter-row" md={24}>
<Divider orientation="right">
{dataSource.length > 0 && (
<Switch
unCheckedChildren="各账户"
checkedChildren="合并"
key={'openOrMerge'}
checked={ifmerge}
onChange={(e) => {
sale_store.setTableDataSource(e);
setIfmerge(e);
}}
/>
)}
<TableExportBtn label={'sales kpi'} {...{ columns: columnsForExport, dataSource: dataForExport }} />
</Divider>
<Table
sticky
key={`salesTradeTable`}

@ -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>
</>
);
});
Loading…
Cancel
Save