顾问: 销售进度: 图表

feature/person-num
Lei OT 2 years ago
parent 64898b7533
commit c3194869fd

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

@ -1,53 +1,36 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Line } from '@ant-design/plots'; import { Line } from '@ant-design/plots';
import { merge, isEmpty, groupBy } from '../utils/commons'; import { merge, isEmpty, groupBy, sortBy } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht'; import { dataFieldAlias } from '../libs/ht';
const uniqueByKey = (array, key, pickLast) => {
const seen = new Map();
const isPickLast = pickLast === true;
return array.filter(item => {
const k = item[key];
const storedItem = seen.get(k);
if(storedItem) {
if(isPickLast) {
seen.set(k, item); // update with last item
}
return false;
}
seen.set(k, item);
return true;
});
};
export default observer((props) => { export default observer((props) => {
const { dataSource, ...config } = props; const { dataSource, ...config } = props;
const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v; const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v;
const seriesData = groupBy(dataSource, ele => ele[config.seriesField]); const seriesData = groupBy(dataSource, ele => ele[config.seriesField]);
const pickKey4KPI = Object.keys(seriesData)[0]; const splitData = dataSource.reduce((r, v) => {
const KPIData = (seriesData?.[pickKey4KPI] || []).reduce((r, v) => { r.push(v);
if ( ! isEmpty(v[kpiKey])) { // 线 : #F4664A if ( ! isEmpty(v[kpiKey])) { // 线, 线
r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: dataFieldAlias[kpiKey].label, extraLine: true,}); r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: `${v[config.seriesField]} ${dataFieldAlias[kpiKey].label}`, extraLine: true,});
} }
return r; return r;
}, []); }, []).sort(sortBy(config.xField));
const dataColors = ['#598cf3', '#69deae', '#FAAD14']; const dataColors = [
"#5B8FF9","#5AD8A6","#5D7092","#F6BD16","#6F5EF9","#6DC8EC","#945FB9","#FF9845","#1E9493",
"#FF99C3","#FF6B3B","#626681","#FFC100","#9FB40F","#76523B","#DAD5B5","#0E8E89","#E19348",
"#F383A2","#247FEA",
];
const colorSets = Object.keys(seriesData).sort().reduce((obj, k, i) => ({...obj, [k]: dataColors[i]}), {}); const colorSets = Object.keys(seriesData).sort().reduce((obj, k, i) => ({...obj, [k]: dataColors[i]}), {});
const mergeLineConfig = merge({ const mergeLineConfig = merge({
// color: ['#598cf3', '#69deae', '#F4664A', '#FAAD14'],
color: (item) => { color: (item) => {
const thisSeries = item[config.seriesField]; const thisSeries = item[config.seriesField].split(' ')?.[0];
return thisSeries.includes('目标') ? '#F4664A' : colorSets[thisSeries]; return colorSets[thisSeries];
// return thisSeries.includes('') ? '#F4664A' : colorSets[thisSeries];
}, },
lineStyle: (data) => { lineStyle: (data) => {
// console.log(data);
if (data[config.seriesField].includes('目标')) { if (data[config.seriesField].includes('目标')) {
return { return {
lineDash: [4, 4], lineDash: [4, 8],
opacity: 0.5, opacity: 0.7,
}; };
} }
@ -55,6 +38,10 @@ export default observer((props) => {
opacity: 1, 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] } } })),
},
}, config); }, config);
return <Line {...mergeLineConfig} data={[...dataSource, ...KPIData]} />; return <Line {...mergeLineConfig} data={splitData} />;
}); });

@ -30,11 +30,23 @@ class SaleStore {
type_data_sub = []; // 类型的子维度数据 type_data_sub = []; // 类型的子维度数据
date_title = 'date_title'; // 日期段,只用于显示,防止日期选择控件的变化导致页面刷新 date_title = 'date_title'; // 日期段,只用于显示,防止日期选择控件的变化导致页面刷新
salesTrade = { groupType: 'dept', loading: false, operator: [], dept: [], overview: [] }; salesTrade = {
groupType: 'dept', loading: false, operator: [], dept: [], overview: [],
operatorMapped: {}, pickSales: [], pickSalesData: [],
};
setGroupType(v) { setGroupType(v) {
this.salesTrade.groupType = v; this.salesTrade.groupType = v;
} }
setPickSales = (v) => {
this.salesTrade.pickSales = v;
setTimeout(() => {
const data = v.reduce((r, ele) => r.concat(this.salesTrade.operatorMapped[ele.key]), []);
this.salesTrade.pickSalesData = data;
}, 500);
};
// 是否包含门票 // 是否包含门票
handleChange_include_tickets = (value) => { handleChange_include_tickets = (value) => {
this.include_tickets = value; this.include_tickets = value;
@ -514,6 +526,7 @@ class SaleStore {
runInAction(() => { runInAction(() => {
this.salesTrade.loading = false; this.salesTrade.loading = false;
this.salesTrade[groupType] = mergeYearMonth; this.salesTrade[groupType] = mergeYearMonth;
this.salesTrade[`${groupType}Mapped`] = mergeYearMonth.reduce((r, v) => ({...r, [v.groupsKey]: Object.values(v.mData)}), {});
}); });
} }

@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Row, Col, Button, Tabs, Table, Divider, Radio, Select, Space, Typography, Progress } from 'antd'; import { Row, Col, Button, Tabs, Table, Divider, Radio, Select, Space, Typography, Progress, Spin } from 'antd';
import { ContainerOutlined, SearchOutlined, UserSwitchOutlined } from '@ant-design/icons'; import { ContainerOutlined, SearchOutlined, UserSwitchOutlined } from '@ant-design/icons';
import { stores_Context } from '../config'; import { stores_Context } from '../config';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
@ -9,6 +9,9 @@ import * as config from '../config';
import { utils, writeFileXLSX } from 'xlsx'; import { utils, writeFileXLSX } from 'xlsx';
import SearchForm from './../components/search/SearchForm'; import SearchForm from './../components/search/SearchForm';
import { dataFieldAlias } from '../libs/ht'; import { dataFieldAlias } from '../libs/ht';
import Donut from './../components/Donut';
import LineWithKPI from '../components/LineWithKPI';
import { Line, DualAxes } from '@ant-design/plots';
const { Text } = Typography; const { Text } = Typography;
@ -18,13 +21,15 @@ const Sale_KPI = () => {
const { groupType, loading, operator } = sale_store.salesTrade; const { groupType, loading, operator } = sale_store.salesTrade;
const dataSource = [].concat(sale_store.salesTrade[groupType], operator); const dataSource = [].concat(sale_store.salesTrade[groupType], operator);
const yearData = Object.values(sale_store.salesTrade[groupType]?.[0]?.mData || {});
const operatorObjects = operator.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel }));
const pageRefresh = (queryData) => { const pageRefresh = (queryData) => {
const overviewFlag = queryData.DepartmentList.toLowerCase() === 'all' || queryData.DepartmentList.toLowerCase().includes(','); const overviewFlag = queryData.DepartmentList.toLowerCase() === 'all' || queryData.DepartmentList.toLowerCase().includes(',');
const _groupType = overviewFlag ? 'overview' : 'dept'; const _groupType = overviewFlag ? 'overview' : 'dept';
sale_store.setGroupType(_groupType); sale_store.setGroupType(_groupType);
sale_store.fetchOperatorTradeData(_groupType, { ...queryData, groupDateType: 'year' }); sale_store.fetchOperatorTradeData(_groupType, { ...queryData, groupDateType: 'year' });
sale_store.fetchOperatorTradeData('operator', { ...queryData, groupDateType: 'year' }); sale_store.fetchOperatorTradeData('operator', { ...queryData, groupDateType: 'year' });
sale_store.setPickSales([]);
}; };
const monthCol = new Array(12).fill(1).map((_, index) => { const monthCol = new Array(12).fill(1).map((_, index) => {
return { return {
@ -98,6 +103,7 @@ const Sale_KPI = () => {
}, },
...monthCol, ...monthCol,
]; ];
const lineConfig = { appendPadding: 10, xField: 'groupDateVal', yField: 'SumML', seriesField: 'groupsLabel', isGroup: true, smooth: true, meta: comm.cloneDeep(dataFieldAlias), };
return ( return (
<div> <div>
<Row gutter={16} style={{ margin: '-16px -8px' }}> <Row gutter={16} style={{ margin: '-16px -8px' }}>
@ -120,10 +126,50 @@ const Sale_KPI = () => {
/> />
</Col> </Col>
</Row> </Row>
<Spin spinning={loading}>
<h2 style={{ marginTop: '.5em' }}>年度业绩组成和走势</h2>
<Row>
<Col className="gutter-row" md={8}>
<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>
<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>
</Col>
<Col className="gutter-row" md={24}>
<LineWithKPI dataSource={sale_store.salesTrade.pickSalesData} {...lineConfig} />
</Col>
</Row>
<Row> <Row>
<Col className="gutter-row" md={24}> <Col className="gutter-row" md={24}>
<Table <Table
sticky
key={`salesTradeTable`} key={`salesTradeTable`}
loading={loading} loading={loading}
columns={columns} columns={columns}
@ -136,6 +182,7 @@ const Sale_KPI = () => {
/> />
</Col> </Col>
</Row> </Row>
</Spin>
</div> </div>
); );
}; };

Loading…
Cancel
Save