feat: 增加国内外占比; perf: 首页: 走势图中的KPI线;

feature/2.0-sales-trade
Lei OT 2 years ago
parent 1d76cbcc74
commit 48a4010fc9

@ -0,0 +1,84 @@
import { observer } from 'mobx-react';
import { Pie, measureTextWidth } from '@ant-design/plots';
import { fixTo2Decimals, merge } from '../utils/commons';
import { dataFieldAlias } from './../libs/ht';
export default observer((props) => {
const { dataSource, title, ...extProps } = props;
const renderStatistic = (containerWidth, text, style) => {
const { width: textWidth, height: textHeight } = measureTextWidth(text, style);
const R = containerWidth / 2; // r^2 = (w / 2)^2 + (h - offsetY)^2
let scale = 1;
if (containerWidth < textWidth) {
scale = Math.min(Math.sqrt(Math.abs(Math.pow(R, 2) / (Math.pow(textWidth / 2, 2) + Math.pow(textHeight, 2)))), 1);
}
const textStyleStr = `width:${containerWidth}px;`;
return `<div style="${textStyleStr};font-size:${scale}em;line-height:${scale < 1 ? 1 : 'inherit'};">${text}</div>`;
};
const config = merge(
{
appendPadding: 10,
// angleField: 'value',
// colorField: 'type',
radius: 0.7,
innerRadius: 0.65,
label: {
type: 'inner',
offset: '-50%',
autoRotate: false,
// content: '{value}',
content: ({ percent }) => `${fixTo2Decimals(percent * 100)}%`,
style: {
textAlign: 'center',
fontSize: 14,
},
},
interactions: [{ type: 'element-selected' }, { type: 'element-active' }, { type: 'pie-statistic-active' }],
statistic: {
title: {
offsetY: -4,
customHtml: (container, view, datum) => {
const { width, height } = container.getBoundingClientRect();
const d = Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height / 2, 2));
const text = datum ? datum[extProps.colorField] : (title || '');
return renderStatistic(d, text, {
fontSize: '28px',
});
},
},
content: {
// style: {
// whiteSpace: 'pre-wrap',
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// },
// content: title || '',
offsetY: 4,
style: {
fontSize: '28px',
},
customHtml: (container, view, datum, data) => {
const { width } = container.getBoundingClientRect();
const _sum = data.reduce((r, d) => r + d[extProps.angleField], 0);
const showVal = datum ? datum[extProps.angleField] : _sum;
const text = dataFieldAlias[extProps.angleField].formatter(showVal);
return renderStatistic(width, text, {
fontSize: '28px',
});
},
},
},
meta: {
[extProps.angleField]: {
alias: dataFieldAlias[extProps.angleField]?.alias || extProps.angleField,
formatter: (v) => dataFieldAlias[extProps.angleField]?.formatter(v) || v,
},
},
},
extProps
);
return <Pie {...config} data={dataSource} />;
});

@ -24,7 +24,7 @@ const uniqueByKey = (array, key, pickLast) => {
}; };
export default observer((props) => { export default observer((props) => {
const { config, dataSource, ...extProps } = props; const { dataSource, ...config } = props;
const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v; const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v;
const _data = dataSource.reduce((r, v) => { const _data = dataSource.reduce((r, v) => {
r.push(v); r.push(v);

@ -1,7 +1,7 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Waterfall } from '@ant-design/plots'; import { Waterfall } from '@ant-design/plots';
import { dataFieldAlias } from './../libs/ht'; import { dataFieldAlias } from './../libs/ht';
import { merge } from '../utils/commons'; import { fixTo4Decimals, merge } from '../utils/commons';
export default observer((props) => { export default observer((props) => {
const { dataSource, line, title, ...extProps } = props; const { dataSource, line, title, ...extProps } = props;
@ -11,7 +11,7 @@ export default observer((props) => {
{ {
type: 'text', type: 'text',
position: ['start', line.value], position: ['start', line.value],
content: `${line.label} ${line.value / 10000}`, content: `${line.label} ${fixTo4Decimals(line.value / 10000)}`,
// offsetX: -15, // offsetX: -15,
style: { style: {
fill: '#F4664A', fill: '#F4664A',

@ -1,3 +1,5 @@
import { fixTo4Decimals } from "../utils/commons";
/** /**
* 事业部 * 事业部
*/ */
@ -95,8 +97,8 @@ export const dateTypes = [
* 结果字段 * 结果字段
*/ */
export const dataFieldOptions = [ export const dataFieldOptions = [
{ label: '营收', value: 'transactions', formatter: (v) => `${v / 10000}`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } }, { label: '营收', value: 'transactions', formatter: (v) => `${fixTo4Decimals(v / 10000)}`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } },
{ label: '毛利', value: 'SumML', formatter: (v) => `${v / 10000}`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } }, { label: '毛利', value: 'SumML', formatter: (v) => `${fixTo4Decimals(v / 10000)}`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } },
{ label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } }, { label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } },
{ label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } }, { label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } },
{ label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, { label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } },

@ -54,9 +54,9 @@ class Trade {
/** /**
* 时间轴 * 时间轴
*/ */
fetchTradeDataByDate(queryData) { fetchTradeDataByDate(queryData = {}) {
this.timeData.loading = true; this.timeData.loading = true;
queryData = queryData || this.searchPayloadHome; queryData = Object.assign({}, this.searchPayloadHome, queryData); // queryData || this.searchPayloadHome;
queryData.groupType = queryData?.groupType || 'overview'; queryData.groupType = queryData?.groupType || 'overview';
Object.assign(queryData, { groupDateType: this.timeLineKey }); Object.assign(queryData, { groupDateType: this.timeLineKey });
this.fetchTradeData(queryData).then((json) => { this.fetchTradeData(queryData).then((json) => {
@ -123,10 +123,17 @@ class Trade {
} }
return r; return r;
}, {}); }, {});
const summaryData = Object.keys(groupsData).map(groupsKey => {
return ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r, [skey]: groupsData[groupsKey].reduce((a, c) => a + c[skey], 0) }),
groupsData[groupsKey]?.[0] || {}
);
});
runInAction(() => { runInAction(() => {
this.sideData.loading = false; this.sideData.loading = false;
this.sideData.dataSource = groupsData; this.sideData.dataSource = groupsData;
this.sideData.monthData = sortResult; this.sideData.monthData = sortResult;
this.sideData.yearData = summaryData;
}); });
} }
}); });
@ -197,7 +204,7 @@ class Trade {
this.summaryData = { loading: false, dataSource: [], kpi: {}, }; this.summaryData = { loading: false, dataSource: [], kpi: {}, };
this.timeData = { loading: false, dataSource: [] }; this.timeData = { loading: false, dataSource: [] };
this.BuData = { loading: false, dataSource: [] }; this.BuData = { loading: false, dataSource: [] };
this.sideData = { loading: false, dataSource: {}, monthData: [] }; this.sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
this.topData = {}; this.topData = {};
this.targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} }; this.targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} };
this.targetTableProps.dataSource = []; this.targetTableProps.dataSource = [];
@ -207,7 +214,7 @@ class Trade {
summaryData = { loading: false, dataSource: [], kpi: {}, }; summaryData = { loading: false, dataSource: [], kpi: {}, };
timeData = { loading: false, dataSource: [] }; timeData = { loading: false, dataSource: [] };
BuData = { loading: false, dataSource: [] }; BuData = { loading: false, dataSource: [] };
sideData = { loading: false, dataSource: {}, monthData: [] }; sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
topData = {}; topData = {};
targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} }; targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} };
targetTableProps = { loading: false, columns: [ targetTableProps = { loading: false, columns: [

@ -8,6 +8,7 @@ import StatisticCard from '../components/StatisticCard';
import Bullet from '../components/BulletWithSort'; import Bullet from '../components/BulletWithSort';
import Waterfall from '../components/Waterfall'; import Waterfall from '../components/Waterfall';
import LineWithKPI from '../components/LineWithKPI'; import LineWithKPI from '../components/LineWithKPI';
import Donut from './../components/Donut';
import DataFieldRadio from '../components/DataFieldRadio'; import DataFieldRadio from '../components/DataFieldRadio';
import { datePartOptions } from './../components/DateGroupRadio/date'; import { datePartOptions } from './../components/DateGroupRadio/date';
import SearchForm from './../components/search/SearchForm'; import SearchForm from './../components/search/SearchForm';
@ -17,9 +18,9 @@ import { Line } from '@ant-design/charts';
import './home.css'; import './home.css';
const topSeries = [ const topSeries = [
{ key: 'country', label: '国籍', graphVisible: true },
{ key: 'dept', label: '小组', graphVisible: true }, { key: 'dept', label: '小组', graphVisible: true },
{ key: 'operator', label: '顾问', graphVisible: true }, { key: 'operator', label: '顾问', graphVisible: true },
{ key: 'country', label: '国籍', graphVisible: true },
{ key: 'GuestGroupType', label: '客群类别', graphVisible: false }, { key: 'GuestGroupType', label: '客群类别', graphVisible: false },
{ key: 'destination', label: '目的地', graphVisible: true }, { key: 'destination', label: '目的地', graphVisible: true },
]; ];
@ -40,15 +41,23 @@ export default observer(() => {
return () => {}; return () => {};
}, []); }, []);
const [topSeriesSet, setTopSeriesSet] = useState(topSeries);
const [overviewFlag, setOverviewFlag] = useState(true);
const [groupTypeVal, setGroupTypeVal] = useState('overview');
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(',');
queryData.groupType = overviewFlag ? 'overview' : 'dept'; const groupType = _overviewFlag ? 'overview' : 'dept';
queryData.groupType = groupType;
setGroupTypeVal(groupType);
TradeStore.resetData(); TradeStore.resetData();
TradeStore.fetchSummaryData(queryData); TradeStore.fetchSummaryData(Object.assign({}, queryData, { groupType }));
TradeStore.fetchTradeDataByDate(queryData); TradeStore.fetchTradeDataByDate(queryData);
TradeStore.fetchTradeDataByBU(queryData); TradeStore.fetchTradeDataByBU(queryData);
TradeStore.fetchTradeDataByMonth(queryData); TradeStore.fetchTradeDataByMonth(queryData);
for (const iterator of topSeries) { const topSeriesF = _overviewFlag ? topSeries : topSeries.filter(ele => ele.key !== 'dept');
setTopSeriesSet(topSeriesF);
setOverviewFlag(_overviewFlag);
for (const iterator of topSeriesF) {
TradeStore.fetchTradeDataByType(iterator.key, queryData); TradeStore.fetchTradeDataByType(iterator.key, queryData);
} }
}; };
@ -156,12 +165,12 @@ export default observer(() => {
setDateField(value); setDateField(value);
TradeStore.setTimeLineKey(value); TradeStore.setTimeLineKey(value);
if (!isEmpty(TradeStore.searchPayloadHome)) { if (!isEmpty(TradeStore.searchPayloadHome)) {
TradeStore.fetchTradeDataByDate(); TradeStore.fetchTradeDataByDate({groupType: groupTypeVal});
} }
}; };
return ( return (
<> <>
<Row gutter={16} style={{ margin: '-16px -8px', position: 'sticky', top: 0, zIndex: 2, }}> <Row gutter={16} style={{ margin: '-16px -8px', position: 'sticky', top: 0, zIndex: 10 }}>
{/* style={{ margin: '-16px -8px', padding: 0 }} */} {/* style={{ margin: '-16px -8px', padding: 0 }} */}
<Col className="gutter-row" span={24}> <Col className="gutter-row" span={24}>
<SearchForm <SearchForm
@ -205,16 +214,22 @@ export default observer(() => {
</Space> </Space>
<Spin spinning={timeData.loading}> <Spin spinning={timeData.loading}>
{/* <Line {...lineConfig} data={timeData.dataSource} /> */} {/* <Line {...lineConfig} data={timeData.dataSource} /> */}
<LineWithKPI dataSource={timeData.dataSource} config={lineConfig} /> <LineWithKPI dataSource={timeData.dataSource} {...lineConfig} />
</Spin> </Spin>
</section> </section>
<section> <section>
<h3>市场进度</h3> <h3>市场</h3>
<Spin spinning={BuData.loading}> <Spin spinning={BuData.loading}>
<Row gutter={layoutProps3.gutter}> <Row gutter={layoutProps3.gutter}>
<Col {...layoutProps3}> <Col {...layoutProps3}>
<Bullet {...BUConfig} dataSource={BuData?.dataSource || []} /> {overviewFlag ? (
<h3 style={{ textAlign: 'center' }}>{`各事业部总业绩`}</h3> <>
<Bullet {...BUConfig} dataSource={BuData?.dataSource || []} />
<h3 style={{ textAlign: 'center' }}>{`各事业部总业绩`}</h3>
</>
) : (
<><Donut {...{angleField: 'SumML', colorField: 'groupsLabel'}} title={formValues.DepartmentList?.label} dataSource={sideData.yearData} /></>
)}
</Col> </Col>
{Object.keys(sideData.dataSource).map((key) => ( {Object.keys(sideData.dataSource).map((key) => (
<Col {...layoutProps3} key={key}> <Col {...layoutProps3} key={key}>
@ -226,7 +241,8 @@ export default observer(() => {
</Spin> </Spin>
</section> </section>
<section> <section>
<h3>英语区目标客户 <h3>
英语区目标客户
<Spin spinning={topData?.GuestGroupType?.loading || false}> <Spin spinning={topData?.GuestGroupType?.loading || false}>
<Table {...targetTableProps} pagination={false} /> <Table {...targetTableProps} pagination={false} />
</Spin> </Spin>
@ -240,14 +256,16 @@ export default observer(() => {
</div> </div>
</Space> </Space>
<Row gutter={layoutProps.gutter}> <Row gutter={layoutProps.gutter}>
{topSeries.map((item) => item.graphVisible ? ( {topSeriesSet.map((item) =>
<Col {...layoutProps} key={item.key}> item.graphVisible ? (
<Spin spinning={topData[item.key]?.loading || false}> <Col {...layoutProps} key={item.key}>
<h3 style={{ textAlign: 'center' }}>{item.label}</h3> <Spin spinning={topData[item.key]?.loading || false}>
<Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} itemLength={10} /> <h3 style={{ textAlign: 'center' }}>{item.label}</h3>
</Spin> <Bullet key={item.key} {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} itemLength={10} />
</Col> </Spin>
) : null)} </Col>
) : null
)}
</Row> </Row>
</section> </section>
</> </>

@ -1,3 +1,6 @@
.__hn-sta-wrapper .ant-statistic-title{
color: rgb(0, 0, 0, .45);
}
.__hn-sta-wrapper .ant-statistic-content-suffix{ .__hn-sta-wrapper .ant-statistic-content-suffix{
float: right; float: right;
font-size: 18px; font-size: 18px;

Loading…
Cancel
Save