From 48a4010fc9ef4118b336cb49f995586abecf605a Mon Sep 17 00:00:00 2001 From: Lei OT Date: Tue, 10 Oct 2023 11:28:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=9B=BD=E5=86=85?= =?UTF-8?q?=E5=A4=96=E5=8D=A0=E6=AF=94;=20=0Bperf:=20=E9=A6=96=E9=A1=B5:?= =?UTF-8?q?=20=E8=B5=B0=E5=8A=BF=E5=9B=BE=E4=B8=AD=E7=9A=84KPI=E7=BA=BF;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Donut.jsx | 84 ++++++++++++++++++++++++++++++++++ src/components/LineWithKPI.jsx | 2 +- src/components/Waterfall.jsx | 4 +- src/libs/ht.js | 6 ++- src/stores/Trade.js | 15 ++++-- src/views/Home.jsx | 58 +++++++++++++++-------- src/views/home.css | 3 ++ 7 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 src/components/Donut.jsx diff --git a/src/components/Donut.jsx b/src/components/Donut.jsx new file mode 100644 index 0000000..a18447a --- /dev/null +++ b/src/components/Donut.jsx @@ -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 `
${text}
`; + }; + + 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 ; +}); diff --git a/src/components/LineWithKPI.jsx b/src/components/LineWithKPI.jsx index ee8a813..50d196a 100644 --- a/src/components/LineWithKPI.jsx +++ b/src/components/LineWithKPI.jsx @@ -24,7 +24,7 @@ const uniqueByKey = (array, key, pickLast) => { }; export default observer((props) => { - const { config, dataSource, ...extProps } = props; + const { dataSource, ...config } = props; const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v; const _data = dataSource.reduce((r, v) => { r.push(v); diff --git a/src/components/Waterfall.jsx b/src/components/Waterfall.jsx index 87bf2ec..9e55075 100644 --- a/src/components/Waterfall.jsx +++ b/src/components/Waterfall.jsx @@ -1,7 +1,7 @@ import { observer } from 'mobx-react'; import { Waterfall } from '@ant-design/plots'; import { dataFieldAlias } from './../libs/ht'; -import { merge } from '../utils/commons'; +import { fixTo4Decimals, merge } from '../utils/commons'; export default observer((props) => { const { dataSource, line, title, ...extProps } = props; @@ -11,7 +11,7 @@ export default observer((props) => { { type: 'text', position: ['start', line.value], - content: `${line.label} ${line.value / 10000} 万`, + content: `${line.label} ${fixTo4Decimals(line.value / 10000)} 万`, // offsetX: -15, style: { fill: '#F4664A', diff --git a/src/libs/ht.js b/src/libs/ht.js index b3e840a..c7274c1 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -1,3 +1,5 @@ +import { fixTo4Decimals } from "../utils/commons"; + /** * 事业部 */ @@ -95,8 +97,8 @@ export const dateTypes = [ * 结果字段 */ export const dataFieldOptions = [ - { label: '营收', value: 'transactions', formatter: (v) => `${v / 10000} 万`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } }, - { label: '毛利', value: 'SumML', formatter: (v) => `${v / 10000} 万`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } }, + { label: '营收', value: 'transactions', formatter: (v) => `${fixTo4Decimals(v / 10000)} 万`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } }, + { 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: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } }, { label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, diff --git a/src/stores/Trade.js b/src/stores/Trade.js index 5f9cf55..0d41bc3 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -54,9 +54,9 @@ class Trade { /** * 时间轴 */ - fetchTradeDataByDate(queryData) { + fetchTradeDataByDate(queryData = {}) { this.timeData.loading = true; - queryData = queryData || this.searchPayloadHome; + queryData = Object.assign({}, this.searchPayloadHome, queryData); // queryData || this.searchPayloadHome; queryData.groupType = queryData?.groupType || 'overview'; Object.assign(queryData, { groupDateType: this.timeLineKey }); this.fetchTradeData(queryData).then((json) => { @@ -123,10 +123,17 @@ class Trade { } 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(() => { this.sideData.loading = false; this.sideData.dataSource = groupsData; this.sideData.monthData = sortResult; + this.sideData.yearData = summaryData; }); } }); @@ -197,7 +204,7 @@ class Trade { this.summaryData = { loading: false, dataSource: [], kpi: {}, }; this.timeData = { loading: false, dataSource: [] }; this.BuData = { loading: false, dataSource: [] }; - this.sideData = { loading: false, dataSource: {}, monthData: [] }; + this.sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] }; this.topData = {}; this.targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} }; this.targetTableProps.dataSource = []; @@ -207,7 +214,7 @@ class Trade { summaryData = { loading: false, dataSource: [], kpi: {}, }; timeData = { loading: false, dataSource: [] }; BuData = { loading: false, dataSource: [] }; - sideData = { loading: false, dataSource: {}, monthData: [] }; + sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] }; topData = {}; targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} }; targetTableProps = { loading: false, columns: [ diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 6033c83..200ab4f 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -8,6 +8,7 @@ import StatisticCard from '../components/StatisticCard'; import Bullet from '../components/BulletWithSort'; import Waterfall from '../components/Waterfall'; import LineWithKPI from '../components/LineWithKPI'; +import Donut from './../components/Donut'; import DataFieldRadio from '../components/DataFieldRadio'; import { datePartOptions } from './../components/DateGroupRadio/date'; import SearchForm from './../components/search/SearchForm'; @@ -17,9 +18,9 @@ import { Line } from '@ant-design/charts'; import './home.css'; const topSeries = [ - { key: 'country', label: '国籍', graphVisible: true }, { 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 }, ]; @@ -40,15 +41,23 @@ export default observer(() => { return () => {}; }, []); + const [topSeriesSet, setTopSeriesSet] = useState(topSeries); + const [overviewFlag, setOverviewFlag] = useState(true); + const [groupTypeVal, setGroupTypeVal] = useState('overview'); const pageRefresh = (queryData) => { - const overviewFlag = queryData.DepartmentList.toLowerCase() === 'all' || queryData.DepartmentList.toLowerCase().includes(','); - queryData.groupType = overviewFlag ? 'overview' : 'dept'; + const _overviewFlag = queryData.DepartmentList.toLowerCase() === 'all' || queryData.DepartmentList.toLowerCase().includes(','); + const groupType = _overviewFlag ? 'overview' : 'dept'; + queryData.groupType = groupType; + setGroupTypeVal(groupType); TradeStore.resetData(); - TradeStore.fetchSummaryData(queryData); + TradeStore.fetchSummaryData(Object.assign({}, queryData, { groupType })); TradeStore.fetchTradeDataByDate(queryData); TradeStore.fetchTradeDataByBU(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); } }; @@ -156,12 +165,12 @@ export default observer(() => { setDateField(value); TradeStore.setTimeLineKey(value); if (!isEmpty(TradeStore.searchPayloadHome)) { - TradeStore.fetchTradeDataByDate(); + TradeStore.fetchTradeDataByDate({groupType: groupTypeVal}); } }; return ( <> - + {/* style={{ margin: '-16px -8px', padding: 0 }} */} { {/* */} - +
-

市场进度

+

市场

- -

{`各事业部总业绩`}

+ {overviewFlag ? ( + <> + +

{`各事业部总业绩`}

+ + ) : ( + <> + )} {Object.keys(sideData.dataSource).map((key) => ( @@ -226,7 +241,8 @@ export default observer(() => {
-

英语区目标客户 +

+ 英语区目标客户 @@ -240,14 +256,16 @@ export default observer(() => { - {topSeries.map((item) => item.graphVisible ? ( - - -

{item.label}

- -
- - ) : null)} + {topSeriesSet.map((item) => + item.graphVisible ? ( + + +

{item.label}

+ +
+ + ) : null + )} diff --git a/src/views/home.css b/src/views/home.css index 2713a7b..0175da4 100644 --- a/src/views/home.css +++ b/src/views/home.css @@ -1,3 +1,6 @@ +.__hn-sta-wrapper .ant-statistic-title{ + color: rgb(0, 0, 0, .45); +} .__hn-sta-wrapper .ant-statistic-content-suffix{ float: right; font-size: 18px;