feat: home: 瀑布图-百分比; 排名图-进度

feature/2.0-sales-trade
Lei OT 2 years ago
parent d83e7dec92
commit 230ec4be81

@ -1,100 +0,0 @@
import { observer } from 'mobx-react';
import { Bullet } from '@ant-design/plots';
export default observer((props) => {
const { dataSource, ...extProps } = props;
// todo: , number -> array
const parseData = dataSource?.map(ele => ({...ele, CJCountRange: [0,1000], [extProps.measureField]: [ele[extProps.measureField]]}));
const config = {
// data,
// measureField: 'measures',
// rangeField: 'ranges',
// targetField: 'target',
// xField: 'title',
color: {
range: ['#FFbcb8', '#FFe0b0', '#bfeec8'],
measure: '#5B8FF9',
target: '#39a3f4',
},
label: {
// target: true,
// measure: {
// position: 'middle',
// style: {
// fill: '#fff',
// },
// },
},
xAxis: {
line: null,
},
yAxis: false,
// legend
legend: {
custom: true,
position: 'bottom',
items: [
// {
// value: '',
// name: '',
// marker: {
// symbol: 'square',
// style: {
// fill: '#FFbcb8',
// r: 5,
// },
// },
// },
// {
// value: '',
// name: '',
// marker: {
// symbol: 'square',
// style: {
// fill: '#FFe0b0',
// r: 5,
// },
// },
// },
// {
// value: '',
// name: '',
// marker: {
// symbol: 'square',
// style: {
// fill: '#bfeec8',
// r: 5,
// },
// },
// },
{
value: '实际',
name: '实际',
marker: {
symbol: 'square',
style: {
fill: '#5B8FF9',
r: 5,
},
},
},
{
value: '目标',
name: '目标',
marker: {
symbol: 'line',
style: {
stroke: '#39a3f4',
r: 5,
},
},
},
],
},
};
return (
<>
<Bullet {...config} {...extProps} data={parseData} />
</>
);
});

@ -0,0 +1,88 @@
import { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Bullet } from '@ant-design/plots';
import { sortBy } from '../utils/commons';
import { dataFieldAlias } from './../libs/ht';
export default observer((props) => {
const { dataSource, ...extProps } = props;
// , number -> array
const dataParser = (origin) => {
const { measureField, rangeField, targetField } = extProps;
const maxKPI = Math.max(...(origin || []).map((ele) => ele[targetField]));
const maxValue = Math.max(...(origin || []).map((ele) => ele[measureField]));
const _max = Math.max(maxKPI, maxValue);
const sortData = origin.sort(sortBy(measureField));
//
const _parseData = sortData?.map((ele) => ({ ...ele, [rangeField]: [0, Math.ceil(_max / 0.9)], [measureField]: [ele[measureField]] }));
return _parseData;
};
const [parseData, setParseData] = useState([]);
useEffect(() => {
setParseData(dataParser(dataSource));
return () => {};
}, [extProps.measureField, dataSource]);
const config = {
color: {
range: ['#FFbcb8', '#FFe0b0', '#bfeec8'],
measure: '#5B8FF9',
target: '#FF9845',
},
label: {
// target: true,
// measure: {
// position: 'middle',
// style: {
// fill: '#fff',
// },
// },
},
xAxis: {
line: null,
},
yAxis: false,
// legend
legend: {
custom: true,
position: 'bottom',
items: [
{
value: '实际',
name: '实际',
marker: {
symbol: 'square',
style: {
fill: '#5B8FF9',
r: 5,
},
},
},
{
value: '目标',
name: '目标',
marker: {
symbol: 'line',
style: {
stroke: '#FF9845', // '#39a3f4',
r: 5,
},
},
},
],
},
// alias
tooltip: {
customItems: (originalItems) => {
// process originalItems,
return originalItems.map((ele) => ({ ...ele, name: dataFieldAlias[ele.name]?.alias || ele.name }));
},
},
};
return (
<>
<Bullet {...config} {...extProps} data={parseData} />
</>
);
});

@ -3,7 +3,7 @@ import { Card, Statistic, Progress } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
export default observer((props) => { export default observer((props) => {
const valueStyle = { color: props.VSrate < 0 ? '#cf1322' : '#3f8600' }; const valueStyle = { color: props.VSrate < 0 ? '#3f8600' : '#cf1322' };
const VSIcon = () => (props.VSrate < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />); const VSIcon = () => (props.VSrate < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />);
return ( return (
<Card> <Card>

@ -0,0 +1,67 @@
import { observer } from 'mobx-react';
import { Waterfall } from '@ant-design/plots';
import { dataFieldAlias } from './../libs/ht';
export default observer((props) => {
const { dataSource, line, title, ...extProps } = props;
const yMax = Math.max(line?.value || 0, ...dataSource.map((ele) => ele[extProps.yField]));
const annotationsLine = line
? [
{
type: 'text',
position: ['start', line.value],
content: `${line.label} ${line.value / 1000} K`,
// offsetX: -15,
style: {
fill: '#F4664A',
textBaseline: 'bottom',
},
},
{
type: 'line',
start: [-10, line.value],
end: ['max', line.value],
style: {
stroke: '#F4664A',
lineDash: [2, 2],
},
},
]
: [];
const config = {
padding: 'auto',
appendPadding: [20, 0, 0, 0],
/** 展示总计 */
total: {
label: `${title}`,
style: {
fill: '#96a6a6',
},
},
legend: false,
/** 数据标签展示模式:绝对值 */
labelMode: 'absolute',
label: {
style: {
fontSize: 12,
},
},
annotations: [...annotationsLine],
meta: {
...extProps.mergeMeta,
[extProps.yField]: {
...extProps.mergeMeta[extProps.yField],
max: Math.ceil(yMax / 0.95),
alias: dataFieldAlias[extProps.yField]?.alias || extProps.yField,
formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v,
},
},
};
return (
<>
<Waterfall {...config} {...extProps} data={dataSource} />
</>
);
});

@ -41,27 +41,9 @@
"month": "@date('MM')", "month": "@date('MM')",
"key": "@increment" "key": "@increment"
} }
],
"inside|12": [
{
"SumML": "@float(999,9999,2,2)",
"SumMLKPIrate": "@float(0,70,2,2)",
"COLI_Department": "1,2,28,7",
"month|+1": -11,
"key": "@increment"
}
],
"outside|12": [
{
"SumML": "@float(999,9999,2,2)",
"SumMLKPIrate": "@float(0,70,2,2)",
"COLI_Department": "1,2,28,7",
"month|+1": -11,
"key": "@increment"
}
] ]
}, },
"get|/service-web/QueryData/GetTradeByType": { "get|/service-web/QueryData/GetTradeOrderByType": {
"errcode": 0, "errcode": 0,
"errmsg": "", "errmsg": "",
"loading": false, "loading": false,
@ -71,18 +53,21 @@
"OrderTypeSN": "@integer(1,10)", "OrderTypeSN": "@integer(1,10)",
"OrderType": "@cname", "OrderType": "@cname",
"OrderCount": "@integer(9,999)", "OrderCount": "@integer(9,999)",
"OrderCountKPI": "@integer(100,1000)",
"CJCount": "@integer(9,999)", "CJCount": "@integer(9,999)",
"CJCountKPI": "@integer(100,1000)", "CJCountKPI": "@integer(100,1000)",
"CJPersonNum": "@integer(9,999)", "CJPersonNum": "@integer(9,999)",
"YJLY": "@integer(99,9999)", "YJLY": "@integer(99,9999)",
"CJrate": "@float(1,99,0,2)", "CJrate": "@float(1,99,0,2)",
"CJrateKPI": "@float(1,99,0,2)",
"Ordervalue": "@integer(9,999)", "Ordervalue": "@integer(9,999)",
"SumML": "@integer(99,9999)", "SumML": "@integer(99,9999)",
"SumMLKPI": "@integer(99,9999)",
"SumMLrate": "@float(-20,100,0,2)", "SumMLrate": "@float(-20,100,0,2)",
"SumMLKPIrate": "@float(20,100,0,2)", "SumMLKPIrate": "@float(20,100,0,2)",
"SumOrder": "@integer(9,999)", "SumOrder": "@integer(9,999)",
"SumOrderrate": "@float(-20,20,0,2)", "OrderCountVSrate": "@float(-20,20,0,2)",
"SumOrderKPIrate": "@float(20,100,0,2)", "OrderCountKPIrate": "@float(20,100,0,2)",
"SumPersonNum": "@integer(99,9999)", "SumPersonNum": "@integer(99,9999)",
"SumPersonNumrate": "@float(-50,1,0,2)", "SumPersonNumrate": "@float(-50,1,0,2)",
"SumPersonNumKPIrate": "@float(20,100,0,2)", "SumPersonNumKPIrate": "@float(20,100,0,2)",
@ -93,5 +78,30 @@
"result2": [ "result2": [
{} {}
] ]
},
"get|/service-web/QueryData/GetTradeProcess": {
"errcode": 0,
"errmsg": "",
"loading": false,
"data": null,
"result1|24": [
{
"groups|1": "@pick([\"inside\",\"outside\"])",
"SumML": "@increment(1000)",
"SumMLVSrate": "@float(0,70,2,2)",
"SumMLKPI": "@integer(1000,10000)",
"SumMLKPIrate": "@float(0,70,2,2)",
"month": "@date('2023-MM')",
"SumOrder": "@integer(9,999)",
"SumOrderVSrate": "@float(-20,20,0,2)",
"SumOrderKPIrate": "@float(20,100,0,2)",
"SumPersonNum": "@integer(99,9999)",
"SumPersonNumrate": "@float(-50,1,0,2)",
"SumPersonNumKPIrate": "@float(20,100,0,2)",
"key": "@increment"
}],
"result2": [
{}
]
} }
} }

@ -1,12 +1,12 @@
import { makeAutoObservable, runInAction } from 'mobx'; import { makeAutoObservable, runInAction } from 'mobx';
import * as req from '../utils/request'; import * as req from '../utils/request';
import { isEmpty } from '../utils/commons'; import { isEmpty, sortBy } from '../utils/commons';
/** /**
* 计算变化值 * 计算变化值
*/ */
const calcRate = (r1, r2) => { const calcRate = (r1, r2) => {
// //
}; };
class Trade { class Trade {
@ -43,15 +43,20 @@ class Trade {
fetchTradeDataByMonth() { fetchTradeDataByMonth() {
this.sideData.loading = true; this.sideData.loading = true;
req.fetchJSON('/service-web/QueryData/GetTradeByMonth').then((json) => { req.fetchJSON('/service-web/QueryData/GetTradeProcess').then((json) => {
// req.fetchJSON('/service-web/QueryData/GetTradeByMonth').then((json) => {
if (json.errcode === 0) { if (json.errcode === 0) {
runInAction(() => { runInAction(() => {
const _sideData = json.data.reduce((r, v) => { const sortResult = json.result1.sort(sortBy('month'));
(r[v.biz_side] || (r[v.biz_side] = [])).push(v); const groupsData = sortResult.reduce((r, v) => {
(r[v.groups] || (r[v.groups] = [])).push(v);
return r; return r;
}, {}); }, {});
console.log(_sideData, '_sideData'); console.log(groupsData, 'groupsData');
this.sideData = { loading: false, ...json }; const kpi = { label: '', value: 1200000 }; // 标注KPI
this.sideData.loading = false;
this.sideData.dataSource = groupsData;
this.sideData.kpi = kpi;
}); });
} }
}); });
@ -59,7 +64,7 @@ class Trade {
fetchTradeDataByType(orderType) { fetchTradeDataByType(orderType) {
this.topData[orderType] = { loading: true, dataSource: [] }; this.topData[orderType] = { loading: true, dataSource: [] };
req.fetchJSON('/service-web/QueryData/GetTradeByType').then((json) => { req.fetchJSON('/service-web/QueryData/GetTradeOrderByType').then((json) => {
if (json.errcode === 0) { if (json.errcode === 0) {
runInAction(() => { runInAction(() => {
this.topData[orderType].loading = false; this.topData[orderType].loading = false;
@ -71,8 +76,9 @@ class Trade {
} }
summaryData = { loading: false, dataSource: [] }; summaryData = { loading: false, dataSource: [] };
sideData = { loading: false }; sideData = { loading: false, dataSource: {}, kpi: {}, };
topData = { }; topData = {};
defaultDataSubject = 'CJCount';
} }
export default Trade; export default Trade;

@ -275,3 +275,10 @@ export function set_array_index(result) {
} }
return result_array; return result_array;
} }
/**
* 排序
*/
export const sortBy = (key) => {
return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0);
};

@ -1,12 +1,15 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Row, Col, Spin } from 'antd'; import { Row, Col, Spin, Space } from 'antd';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import StatisticCard from './../charts/StatisticCard'; import StatisticCard from '../components/StatisticCard';
import Bullet from './../charts/Bullet'; import Bullet from '../components/Bullet';
import Waterfall from '../components/Waterfall';
import DataFieldRadio from './../components/DateFieldRadio'; import DataFieldRadio from './../components/DateFieldRadio';
import DatePickerCharts from './../components/search/DatePickerCharts';
import { empty } from './../utils/commons'; import { empty } from './../utils/commons';
import './home.css'; import './home.css';
@ -16,10 +19,10 @@ export default observer(() => {
const { sideData, summaryData, topData } = TradeStore; const { sideData, summaryData, topData } = TradeStore;
const topSeries = [ const topSeries = [
{ key: 'Country', label: '国籍'}, { key: 'Country', label: '国籍' },
{ key: 'Area', label: '目的地'}, { key: 'Area', label: '目的地' },
{ key: 'Sales', label: '顾问'}, { key: 'Sales', label: '顾问' },
{ key: 'GuestGroupType', label: '客群类别'}, { key: 'GuestGroupType', label: '客群类别' },
]; ];
useEffect(() => { useEffect(() => {
@ -28,7 +31,6 @@ export default observer(() => {
TradeStore.fetchTradeDataByMonth(); TradeStore.fetchTradeDataByMonth();
for (const iterator of topSeries) { for (const iterator of topSeries) {
TradeStore.fetchTradeDataByType(iterator.key); TradeStore.fetchTradeDataByType(iterator.key);
// TradeStore.fetchTradeDataByType('Area');
} }
} }
return () => {}; return () => {};
@ -41,17 +43,50 @@ export default observer(() => {
xs: { span: 24 }, xs: { span: 24 },
}; };
const BulletConfig = { const layoutProps3 = {
measureField: 'CJCount', // gutter: { xs: 8, sm: 8, lg: 16 },
rangeField: 'CJCountRange', // lg: { span: 8 },
targetField: 'CJCountKPI', // sm: { span: 12 },
xs: { span: 24 },
};
const [valueKey, setValueKey] = useState('SumML');
const [BulletConfig, setBulletConfig] = useState({
measureField: 'SumML', //
rangeField: 'SumMLRange', //
targetField: 'SumMLKPI', //
xField: 'OrderType', xField: 'OrderType',
});
const handleChangeValueKey = (key) => {
setValueKey(key);
setBulletConfig({
measureField: key,
rangeField: `${key}Range`,
targetField: `${key}KPI`,
xField: 'OrderType',
});
};
const WaterfallConfig = {
xField: 'month',
yField: 'SumML',
mergeMeta: {
month: {
alias: '月份',
},
},
label: {
formatter: (v) => ((v.SumML / sideData.kpi.value) * 100).toFixed(2) + '%',
},
}; };
return ( return (
<> <>
<h2>年度业绩</h2>
<section> <section>
<Space>
<h2>年度业绩</h2>
<DatePickerCharts hide_vs={true} />
</Space>
<Spin spinning={summaryData.loading}> <Spin spinning={summaryData.loading}>
<Row gutter={layoutProps.gutter}> <Row gutter={layoutProps.gutter}>
{summaryData.dataSource.map((item) => ( {summaryData.dataSource.map((item) => (
@ -62,18 +97,30 @@ export default observer(() => {
</Row> </Row>
</Spin> </Spin>
</section> </section>
<h3>市场进度</h3>
<section></section>
<h3>TOP
<div style={{float: 'right', display: 'inline-block'}}>
<DataFieldRadio />
</div></h3>
<section> <section>
<h3>市场进度</h3>
<Spin spinning={sideData.loading}>
<Row gutter={layoutProps3.gutter}>
{Object.keys(sideData.dataSource).map((key) => (
<Col {...layoutProps3} key={key}>
<Waterfall {...WaterfallConfig} title={key} dataSource={sideData.dataSource[key]} line={sideData.kpi} />
</Col>
))}
</Row>
</Spin>
</section>
<section>
<Space>
<h3>TOP</h3>
<div>
<DataFieldRadio value={valueKey} onChange={handleChangeValueKey} />
</div>
</Space>
<Row gutter={layoutProps.gutter}> <Row gutter={layoutProps.gutter}>
{topSeries.map((item) => ( {topSeries.map((item) => (
<Col {...layoutProps} key={item.key}> <Col {...layoutProps} key={item.key}>
<Spin spinning={topData[item.key]?.loading}> <Spin spinning={topData[item.key]?.loading}>
<h3 style={{textAlign: 'center'}}>{item.label}</h3> <h3 style={{ textAlign: 'center' }}>{item.label}</h3>
<Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} /> <Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} />
</Spin> </Spin>
</Col> </Col>

Loading…
Cancel
Save