diff --git a/src/App.jsx b/src/App.jsx index 28f12ec..718dd76 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,9 +10,9 @@ import { DollarOutlined, AreaChartOutlined, WechatOutlined, - UserOutlined, FlagOutlined, + UserOutlined, FlagOutlined, PieChartOutlined, BarChartOutlined } from '@ant-design/icons'; -import { Layout, Menu, Image, Spin } 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'; @@ -36,6 +36,8 @@ import { stores_Context } from './config'; 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'; const App = () => { const { Content, Footer, Sider } = Layout; @@ -121,6 +123,16 @@ const App = () => { ], }, { key: 'kpi', label: 目标, icon: }, + { key: 'distribution', label: 统计分布, icon: }, + { + key: 'detail', + label: ( + + 统计分析 + + ), + icon: , + }, ]; return ( @@ -162,6 +174,8 @@ const App = () => { } /> } /> + } /> + } /> }> } /> } /> diff --git a/src/components/BulletWithSort.jsx b/src/components/BulletWithSort.jsx index b10572e..fc3159d 100644 --- a/src/components/BulletWithSort.jsx +++ b/src/components/BulletWithSort.jsx @@ -30,7 +30,7 @@ export default observer((props) => { const config = merge({ color: { - range: [ '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0', + range: [ '#FFF3E1', '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0', measure: '#5B8FF9', target: '#FF9845', }, @@ -48,6 +48,10 @@ export default observer((props) => { }, xAxis: { line: null, + label: { + autoHide: false, + autoRotate: true, + }, }, yAxis: false, // 自定义 legend diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index adb75fb..6d08642 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -117,7 +117,8 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, })); const parse2 = parseDateType(_data2, dateGroup, fieldMapper); const parseData2 = parse2.data.map((ele) => ({ - [fieldMapper.dateKey]: ele.groupKey, + [fieldMapper.dateKey]: ele[fieldMapper.dateKey], + // [fieldMapper.dateKey]: ele.groupKey, [fieldMapper.valueKey]: ele.value, [fieldMapper.seriesKey]: ele[fieldMapper.seriesKey], groups: _data2[0].groups, @@ -126,8 +127,8 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, dateGroup: ele[fieldMapper.dateKey], })); const useKeys = parseData1.map((ele) => ele[fieldMapper.dateKey]); - const reindecData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `X.${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey })); - const retData = [].concat(parseData1, reindecData2); + const reindexData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `_${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey })); + const retData = [].concat(parseData1, reindexData2 ); const avg1 = parse1.avgVal; // console.log('callback', dateGroup, retData); cb(dateGroup, retData, avg1); diff --git a/src/components/LineWithKPI.jsx b/src/components/LineWithKPI.jsx new file mode 100644 index 0000000..ee8a813 --- /dev/null +++ b/src/components/LineWithKPI.jsx @@ -0,0 +1,53 @@ +import { observer } from 'mobx-react'; +import { Line } from '@ant-design/plots'; +import { merge, isEmpty } 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 { config, dataSource, ...extProps } = props; + const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v; + const _data = dataSource.reduce((r, v) => { + r.push(v); + if ( ! isEmpty(v[kpiKey])) { // 有设目标才多显示一条虚线 颜色: #F4664A + r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: dataFieldAlias[kpiKey].label, extraLine: true,}); + } + return r; + }, []); + const mergeLineConfig = merge({ + color: ['#598cf3', '#F4664A', '#FAAD14'], + lineStyle: (data) => { + // console.log(data); + if (data[config.seriesField].includes('目标')) { + return { + lineDash: [4, 4], + opacity: 0.5, + }; + } + + return { + opacity: 1, + }; + }, + }, config); + return ; +}); diff --git a/src/components/Scatter.jsx b/src/components/Scatter.jsx new file mode 100644 index 0000000..3ef84a2 --- /dev/null +++ b/src/components/Scatter.jsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { observer } from 'mobx-react'; +import { sortBy, merge } from '../utils/commons'; +import { dataFieldAlias } from '../libs/ht'; +import { Mix, Scatter } from '@ant-design/plots'; + +export default observer((props) => { + const { dataSource, ...extProps } = props; + const config = merge( + { + appendPadding: 10, + // xField: 'Revenue (Millions)', + // yField: 'Rating', + shape: 'circle', + // colorField: 'Genre', + size: 4, + yAxis: { + nice: true, + line: { + style: { + stroke: '#aaa', + }, + }, + }, + xAxis: { + min: -100, + grid: { + line: { + style: { + stroke: '#eee', + }, + }, + }, + line: { + style: { + stroke: '#aaa', + }, + }, + }, + }, extProps); + return ; +}); diff --git a/src/components/StatisticCard.jsx b/src/components/StatisticCard.jsx index 5847223..e6fe898 100644 --- a/src/components/StatisticCard.jsx +++ b/src/components/StatisticCard.jsx @@ -9,7 +9,6 @@ export default observer((props) => { return ( { } prefix={} // title={{props.title}} + {...props} + value={props.valueSuffix ? `${props.value} ${props.valueSuffix}` : props.value} /> {props.showProgress !== false && `${props.KPIrate}%`} />} diff --git a/src/components/Waterfall.jsx b/src/components/Waterfall.jsx index 2ec6bd6..87bf2ec 100644 --- a/src/components/Waterfall.jsx +++ b/src/components/Waterfall.jsx @@ -5,7 +5,7 @@ import { merge } from '../utils/commons'; export default observer((props) => { const { dataSource, line, title, ...extProps } = props; - const yMax = (Math.max(line?.value || 0, ...dataSource.map((ele) => ele[extProps.yField])))*1; + const yMax = (Math.max(line?.value || 0, (dataSource.reduce((r, ele) => r+ele[extProps.yField], 0))))*1; const annotationsLine = line ? [ { diff --git a/src/components/kpi/KPISettings.jsx b/src/components/kpi/KPISettings.jsx index ecec5d4..d70599f 100644 --- a/src/components/kpi/KPISettings.jsx +++ b/src/components/kpi/KPISettings.jsx @@ -6,6 +6,9 @@ import SearchForm from './../search/SearchForm'; import { bu, KPIObjects, KPISubjects } from './../../libs/ht'; import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt, merge } 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

TODO

; @@ -13,12 +16,12 @@ const Todo = (props) => { const subjectComponents = { 'sum_profit': ProfitTable, - 'in_order_count': Todo, - 'confirm_order_count': Todo, - 'depart_order_count': Todo, - 'confirm_rates': Todo, - 'praise_rates': Todo, - 'sum_person_num': Todo, + 'in_order_count': Count, + 'confirm_order_count': Count, + 'depart_order_count': Count, + 'confirm_rates': Rates, + 'praise_rates': Rates, + 'sum_person_num': Count, }; export const KPIObjectsMapped = KPIObjects.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); @@ -28,7 +31,7 @@ export default observer((props) => { const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context); const { sort, initialValue, hides, shows, fieldProps: _fieldProps } = { sort: '', - initialValue: searchFormStore.formValues, + initialValue: '', // searchFormStore.formValues, fieldProps: {}, hides: [], shows: ['DateType', 'years'], @@ -43,15 +46,15 @@ export default observer((props) => { { - console.log('invoke kpi setting search'); + // console.log('invoke kpi setting search'); if (typeof onSearchSubmit === 'function') { - onSearchSubmit(obj); + onSearchSubmit(obj, form); } }} /> @@ -63,6 +66,7 @@ export default observer((props) => { tabPosition={'left'} onChange={(sub) => { KPIStore.setSettingSubject(sub); + onSearchSubmit(searchFormStore.formValuesToSub); }} items={KPISubjects.map((ele, i) => { const SubjectTableComponent = subjectComponents[ele.key]; diff --git a/src/components/kpi/ObjectPanel.jsx b/src/components/kpi/ObjectPanel.jsx index a78c231..2caa53a 100644 --- a/src/components/kpi/ObjectPanel.jsx +++ b/src/components/kpi/ObjectPanel.jsx @@ -1,23 +1,22 @@ -import { useContext } from 'react'; import { observer } from 'mobx-react'; -// import { stores_Context } from '../config'; -import { Table } from 'antd'; import KPISettings from './KPISettings'; import { bu, KPISubjects } from '../../libs/ht'; +const sort = { DateType: 10, years: 11 }; +const yearInitial = {}; const searchFormItemSet = { - 'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'] }, - 'dept': { shows: ['DateType', 'years', 'DepartmentList'], fieldProps: { DepartmentList: { allowClear: true } } }, - 'operator': { shows: ['DateType', 'years', 'DepartmentList'] }, // , 'operator' - 'destination': { shows: ['DateType', 'years', 'destination'] }, - 'country': { shows: ['DateType', 'years', 'country'] }, + 'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'], sort }, + 'dept': { shows: ['DateType', 'years', 'DepartmentList'], sort, fieldProps: { DepartmentList: { allowClear: true,isLeaf: true, show_all: false } }, }, + 'operator': { shows: ['DateType', 'years', 'DepartmentList', 'operator'], fieldProps: { DepartmentList: { allowClear: true, isLeaf: true }, operator: { param: { is_assign: 1 } } }, sort }, + 'destination': { shows: ['DateType', 'years', 'destination'], sort }, + 'country': { shows: ['DateType', 'years', 'country'], sort }, }; export default observer((props) => { const searchProps = searchFormItemSet?.[props.curObject] || {}; return ( <> - + ); }); diff --git a/src/components/kpi/OverviewPanel.jsx b/src/components/kpi/OverviewPanel.jsx index 49f8760..e0cac54 100644 --- a/src/components/kpi/OverviewPanel.jsx +++ b/src/components/kpi/OverviewPanel.jsx @@ -20,12 +20,12 @@ export default observer((props) => { const { curObject } = props; const [dataSource, setDataSource] = useState([]); useEffect(() => { - onSearchSubmit({ - object: curObject, - date_type: 'applyDate', - start_date: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'), - end_date: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'), - }); + // onSearchSubmit({ + // object: curObject, + // date_type: 'applyDate', + // start_date: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'), + // end_date: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'), + // }); return () => {}; }, []); @@ -129,6 +129,7 @@ export default observer((props) => { value: mVal, kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined, key: undefined, + group_date_type: 'month', }; }); return r.concat(allMonth); @@ -143,6 +144,7 @@ export default observer((props) => { start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'), end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'), kpi_id: kpiYear?.kpi_id || undefined, + group_date_type: 'year', }))(dataSource?.[0] || {}); tableData.unshift(yearRow); console.log('sub', tableData, delKpiIds); diff --git a/src/components/kpi/SubjectTable/Count.jsx b/src/components/kpi/SubjectTable/Count.jsx new file mode 100644 index 0000000..2c6fc50 --- /dev/null +++ b/src/components/kpi/SubjectTable/Count.jsx @@ -0,0 +1,263 @@ +import { useContext, useState, useEffect, useMemo } from 'react'; +import { observer } from 'mobx-react'; +import moment from 'moment'; +import { stores_Context } from '../../../config'; +import { Button, Switch, Input, Space, Typography, Row, Col, message } from 'antd'; +import { EditableProTable } from '@ant-design/pro-components'; +import { KPIObjects } from '../../../libs/ht'; +import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt } from '../../../utils/commons'; + +export const KPIObjectsMapped = KPIObjects.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); +const { Text } = Typography; +const initialPercentKey = new Array(12).fill(1).reduce((r, v, i) => ({ ...r, [`M${i + 1}Percent`]: [8, 9].includes(i) ? 10 : 8 }), {}); +const numberConvert10K = (number, scale = 1) => { + return fixTo2Decimals(number / (1000 * scale)) + 'K'; +}; + +export default observer((props) => { + const { KPIStore, date_picker_store: searchFormStore } = useContext(stores_Context); + const { curObject, objects, onSearchSubmit } = props; + const curObjectItem = KPIObjectsMapped[curObject]; + + const [dataSource, setDataSource] = useState(KPIStore.pageData); + const [editOpen, setEditOpen] = useState(false); + const [editableRowsKeys, setEditableRowKeys] = useState([]); + useEffect(() => { + setDataSource(KPIStore.pageData); + setEditableRowKeys([]); + setEditOpen(false); + return () => {}; + }, [KPIStore.pageData]); + + const PercentInput = useMemo( + () => + // eslint-disable-next-line react/display-name + ({ value, onChange, record, ...extProps }) => { + // // eslint-disable-next-line react-hooks/rules-of-hooks + const [inputValue, setInputValue] = useState(value); + const handleInputChange = (e) => { + setInputValue(e.target.value); + onChange?.(e.target.value); + }; + const calcV = inputValue ? numberConvert10K(fixToInt((Number(record?.yearValue) * inputValue) / 100)) : 0; + return ( + + + {calcV} + + ); + }, + [] + ); + const RenderMonthCell = (row, mon) => { + return ( + +
+ {fixTo2Decimals(row?.[`M${mon}Percent`])} + % +
+
{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}
+
+ ); + }; + const monthCol = new Array(12).fill(1).map((_, index) => { + return { + title: `${index + 1}月`, + dataIndex: `M${index + 1}Percent`, + valueType: 'digit', + width: '6.5em', + // fieldProps: { min: 0, max: 100, style: { width: '4em' } }, + renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => { + return ; + }, + render: (_, row) => RenderMonthCell(row, index + 1), + }; + }); + const columns = [ + { + title: curObjectItem.label, + dataIndex: 'object_id', + editable: false, + render: (_, r) => r.object_name, + }, + { + title: '年度目标', + dataIndex: 'yearValue', + valueType: 'digit', + fieldProps: { style: { width: '100%' }, step: 10000 * 1 }, + formItemProps: { + style: { width: '100%' }, + }, + }, + ...monthCol, + { + title: ( + + 操作 + { + makeInitialTable(e); + }} + /> + + ), + valueType: 'option', + // width: 250, + render: () => { + return null; + }, + }, + ]; + + const onTableChange = (...argrs) => { + setEditableRowKeys(argrs[0].map((ele) => ele.key)); + setDataSource(argrs[0]); + }; + const onTableSubmit = () => { + const tableData = dataSource.reduce((r, curObj) => { + const allMonth = new Array(12).fill(1).map((_, index) => { + const mIndex = index + 1; + const mVal = (Number(curObj.yearValue) * Number(curObj[`M${mIndex}Percent`])) / 100; + const startM = moment([KPIStore.settingYear, index, 1]); + const pick = (({ object, object_name, object_id, subject, date_type }) => ({ + object, + object_name, + object_id, + subject, + date_type, + }))(curObj); + return { + ...pick, + start_date: startM.format('YYYY-MM-DD'), + end_date: startM.endOf('M').format('YYYY-MM-DD HH:mm'), + value: mVal, + kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined, + key: undefined, + group_date_type: 'month', + }; + }); + const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({ + object, + object_name, + object_id, + subject, + date_type, + value: yearValue, + start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'), + end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'), + kpi_id: kpiYear?.kpi_id || undefined, + group_date_type: 'year', + }))(curObj); + return r.concat(allMonth, yearRow); + }, []); + // console.log('sub', tableData, 'del:', delKpiIds); + // return false; // debug: + KPIStore.onSubmit(tableData, { delQueue: delKpiIds }).then((res) => { + if (res) { + message.success('保存成功'); + setEditOpen(false); + setEditableRowKeys([]); + setDelKpiIds([]); + onSearchSubmit(searchFormStore.formValuesToSub); + return false; + } + message.error('失败, 请重试'); + }); + }; + const initialRow = monthCol.reduce( + (r, v) => ({ + ...r, + [v.dataIndex]: 0, + yearValue: 10000 * 1, + object: curObject, + object_name: '', + object_id: -1, + subject: KPIStore.settingSubject, + date_type: searchFormStore.formValuesToSub.DateType, + kpiDataMapped: {}, + key: Date.now().toString(32), + group_date_type: 'month', + }), + {} + ); // v.formItemProps.initialValue + const makeInitialTable = (e) => { + setEditOpen(e); + // test: 单独设置之后, 清空筛选会导致无法批量设置新的 + const _initialRow = Object.assign({}, initialRow, initialPercentKey); + const _objects = isEmpty(objects) ? curObjectItem?.data || [] : objects; + const _initialTable = _objects.map((obj) => ({ + ...cloneDeep(_initialRow), + object_name: obj.label, + object_id: obj.value, + key: Date.now().toString(32) + obj.value, + })); + const mergePageData = Object.values( + Object.assign( + {}, + _initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}), + dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}) + ) + ); + if (e && isEmpty(dataSource)) { + setDataSource(_initialTable); + setEditableRowKeys(_initialTable.map((ele) => ele.key)); + return false; + } + setDataSource(mergePageData); + setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []); + }; + const [delKpiIds, setDelKpiIds] = useState([]); + return ( + <> + + + { + return [defaultDoms.delete]; + }, + onDelete: (_key, _row) => { + const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id); + rowKpiIds.push(_row?.kpiYear?.kpi_id); + setDelKpiIds(rowKpiIds); + }, + onValuesChange: (record, recordList) => { + onTableChange(recordList); + }, + onChange: (editableKeys, editableRows) => { + setEditableRowKeys(editableKeys); + }, + }} + /> + + + + + {!editOpen && } + + + + + ); +}); diff --git a/src/components/kpi/SubjectTable/Profit.jsx b/src/components/kpi/SubjectTable/Profit.jsx index 777adb4..d4446f0 100644 --- a/src/components/kpi/SubjectTable/Profit.jsx +++ b/src/components/kpi/SubjectTable/Profit.jsx @@ -24,11 +24,11 @@ export default observer((props) => { const [editableRowsKeys, setEditableRowKeys] = useState([]); useEffect(() => { setDataSource(KPIStore.pageData); + setEditableRowKeys([]); setEditOpen(false); return () => {}; }, [KPIStore.pageData]); - const PercentInput = useMemo( () => // eslint-disable-next-line react/display-name @@ -84,7 +84,7 @@ export default observer((props) => { title: '年度目标', dataIndex: 'yearValue', valueType: 'digit', - fieldProps: { style: { width: '100%' }, step: 10000*100 }, + fieldProps: { style: { width: '100%' }, step: 10000 * 100 }, formItemProps: { style: { width: '100%' }, }, @@ -138,23 +138,24 @@ export default observer((props) => { value: mVal, kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined, key: undefined, + group_date_type: 'month', }; }); - return r.concat(allMonth); + const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({ + object, + object_name, + object_id, + subject, + date_type, + value: yearValue, + start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'), + end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'), + kpi_id: kpiYear?.kpi_id || undefined, + group_date_type: 'year', + }))(curObj); + return r.concat(allMonth, yearRow); }, []); - const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({ - object, - object_name, - object_id, - subject, - date_type, - value: yearValue, - start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'), - end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'), - kpi_id: kpiYear?.kpi_id || undefined, - }))(dataSource?.[0] || {}); - tableData.unshift(yearRow); - console.log('sub', tableData, 'del:', delKpiIds); + // console.log('sub', tableData, 'del:', delKpiIds); // return false; // debug: KPIStore.onSubmit(tableData, { delQueue: delKpiIds }).then((res) => { if (res) { @@ -180,28 +181,37 @@ export default observer((props) => { date_type: searchFormStore.formValuesToSub.DateType, kpiDataMapped: {}, key: Date.now().toString(32), + group_date_type: 'month', }), {} ); // v.formItemProps.initialValue const makeInitialTable = (e) => { setEditOpen(e); - // todo: 单独设置之后, 清空筛选会导致无法批量设置新的 + // test: 单独设置之后, 清空筛选会导致无法批量设置新的 + const _initialRow = Object.assign({}, initialRow, initialPercentKey); + const _objects = isEmpty(objects) ? curObjectItem?.data || [] : objects; + // console.log('ooo', objects, isEmpty(objects), curObjectItem?.data || []); + const _initialTable = _objects.map((obj) => ({ + ...cloneDeep(_initialRow), + object_name: obj.label, + object_id: obj.value, + key: Date.now().toString(32) + obj.value, + })); + // console.log(_initialRow, 'iiiii'); + const mergePageData = Object.values( + Object.assign( + {}, + _initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}), + dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}) + ) + ); if (e && isEmpty(dataSource)) { - const _initialRow = Object.assign({}, initialRow, initialPercentKey); - const _objects = isEmpty(objects) ? (curObjectItem?.data || []) : objects; - console.log('ooo', objects, isEmpty(objects), curObjectItem?.data || []); - const _initialTable = _objects.map((obj) => ({ - ...cloneDeep(_initialRow), - object_name: obj.label, - object_id: obj.value, - key: Date.now().toString(32) + obj.value, - })); - console.log(_initialRow, 'iiiii'); setDataSource(_initialTable); setEditableRowKeys(_initialTable.map((ele) => ele.key)); return false; } - setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); + setDataSource(mergePageData); + setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []); }; const [delKpiIds, setDelKpiIds] = useState([]); return ( @@ -226,13 +236,11 @@ export default observer((props) => { return [defaultDoms.delete]; }, onDelete: (_key, _row) => { - // console.log('del', _key, _row); const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id); rowKpiIds.push(_row?.kpiYear?.kpi_id); setDelKpiIds(rowKpiIds); }, onValuesChange: (record, recordList) => { - // console.log('on edit, onValuesChange',record, recordList); onTableChange(recordList); }, onChange: (editableKeys, editableRows) => { @@ -242,9 +250,14 @@ export default observer((props) => { /> - + + + {!editOpen && } + diff --git a/src/components/kpi/SubjectTable/Rates.jsx b/src/components/kpi/SubjectTable/Rates.jsx new file mode 100644 index 0000000..f9a5e26 --- /dev/null +++ b/src/components/kpi/SubjectTable/Rates.jsx @@ -0,0 +1,263 @@ +import { useContext, useState, useEffect, useMemo } from 'react'; +import { observer } from 'mobx-react'; +import moment from 'moment'; +import { stores_Context } from '../../../config'; +import { Button, Switch, Input, Space, Typography, Row, Col, message } from 'antd'; +import { EditableProTable } from '@ant-design/pro-components'; +import { KPIObjects } from '../../../libs/ht'; +import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt } from '../../../utils/commons'; + +export const KPIObjectsMapped = KPIObjects.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); +const { Text } = Typography; +const initialPercentKey = new Array(12).fill(1).reduce((r, v, i) => ({ ...r, [`M${i + 1}Val`]: [8, 9].includes(i) ? 10 : 8 }), {}); +const numberConvert10K = (number, scale = 1) => { + return fixTo2Decimals(number / (1000 * scale)) + 'K'; +}; + +export default observer((props) => { + const { KPIStore, date_picker_store: searchFormStore } = useContext(stores_Context); + const { curObject, objects, onSearchSubmit } = props; + const curObjectItem = KPIObjectsMapped[curObject]; + + const [dataSource, setDataSource] = useState(KPIStore.pageData); + const [editOpen, setEditOpen] = useState(false); + const [editableRowsKeys, setEditableRowKeys] = useState([]); + useEffect(() => { + // console.log(KPIStore.pageData); + setDataSource(KPIStore.pageData); + setEditableRowKeys([]); + setEditOpen(false); + return () => {}; + }, [KPIStore.pageData]); + + const PercentInput = useMemo( + () => + // eslint-disable-next-line react/display-name + ({ value, onChange, record, ...extProps }) => { + // // eslint-disable-next-line react-hooks/rules-of-hooks + const [inputValue, setInputValue] = useState(value); + const handleInputChange = (e) => { + setInputValue(e.target.value); + onChange?.(e.target.value); + }; + return ( + + ); + }, + [] + ); + const RenderMonthCell = (row, mon) => { + return ( + +
+ {fixTo2Decimals(row?.[`M${mon}Val`])} + % +
+
+ ); + }; + const monthCol = new Array(12).fill(1).map((_, index) => { + return { + title: `${index + 1}月`, + dataIndex: `M${index + 1}Val`, + valueType: 'digit', + width: '6.5em', + // fieldProps: { min: 0, max: 100, style: { width: '4em' } }, + renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => { + return ; + }, + render: (_, row) => RenderMonthCell(row, index + 1), + }; + }); + const columns = [ + { + title: curObjectItem.label, + dataIndex: 'object_id', + editable: false, + render: (_, r) => r.object_name, + }, + { + title: '年度目标', + dataIndex: 'yearValue', + valueType: 'percent', + formItemProps: { + style: { width: '100%' }, + }, + renderFormItem: () => + }, + ...monthCol, + { + title: ( + + 操作 + { + makeInitialTable(e); + }} + /> + + ), + valueType: 'option', + // width: 250, + render: () => { + return null; + }, + }, + ]; + + const onTableChange = (...argrs) => { + setEditableRowKeys(argrs[0].map((ele) => ele.key)); + setDataSource(argrs[0]); + }; + const onTableSubmit = () => { + const tableData = dataSource.reduce((r, curObj) => { + const allMonth = new Array(12).fill(1).map((_, index) => { + const mIndex = index + 1; + // const mVal = (Number(curObj.yearValue) * Number(curObj[`M${mIndex}Percent`])) / 100; + const mVal = Number(curObj[`M${mIndex}Val`]); + const startM = moment([KPIStore.settingYear, index, 1]); + const pick = (({ object, object_name, object_id, subject, date_type }) => ({ + object, + object_name, + object_id, + subject, + date_type, + }))(curObj); + return { + ...pick, + start_date: startM.format('YYYY-MM-DD'), + end_date: startM.endOf('M').format('YYYY-MM-DD HH:mm'), + value: mVal, + kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined, + key: undefined, + group_date_type: 'month', + unit: '%', + }; + }); + const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({ + object, + object_name, + object_id, + subject, + date_type, + value: yearValue, + start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'), + end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'), + kpi_id: kpiYear?.kpi_id || undefined, + group_date_type: 'year', + unit: '%', + }))(curObj); + return r.concat(allMonth, yearRow); + }, []); + // console.log('sub', tableData, 'del:', delKpiIds); + // return false; // debug: + KPIStore.onSubmit(tableData, { delQueue: delKpiIds }).then((res) => { + if (res) { + message.success('保存成功'); + setEditOpen(false); + setEditableRowKeys([]); + setDelKpiIds([]); + onSearchSubmit(searchFormStore.formValuesToSub); + return false; + } + message.error('失败, 请重试'); + }); + }; + const initialRow = monthCol.reduce( + (r, v) => ({ + ...r, + [v.dataIndex]: 0, + yearValue: 15, + object: curObject, + object_name: '', + object_id: -1, + subject: KPIStore.settingSubject, + date_type: searchFormStore.formValuesToSub.DateType, + kpiDataMapped: {}, + key: Date.now().toString(32), + group_date_type: 'month', + unit: '%', + }), + {} + ); // v.formItemProps.initialValue + const makeInitialTable = (e) => { + setEditOpen(e); + // test: 单独设置之后, 清空筛选会导致无法批量设置新的 + const _initialRow = Object.assign({}, initialRow, initialPercentKey); + const _objects = isEmpty(objects) ? curObjectItem?.data || [] : objects; + const _initialTable = _objects.map((obj) => ({ + ...cloneDeep(_initialRow), + object_name: obj.label, + object_id: obj.value, + key: Date.now().toString(32) + obj.value, + })); + const mergePageData = Object.values( + Object.assign( + {}, + _initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}), + dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}) + ) + ); + if (e && isEmpty(dataSource)) { + setDataSource(_initialTable); + setEditableRowKeys(_initialTable.map((ele) => ele.key)); + return false; + } + setDataSource(mergePageData); + setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []); + }; + const [delKpiIds, setDelKpiIds] = useState([]); + return ( + <> + + + { + return [defaultDoms.delete]; + }, + onDelete: (_key, _row) => { + const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id); + rowKpiIds.push(_row?.kpiYear?.kpi_id); + setDelKpiIds(rowKpiIds); + }, + onValuesChange: (record, recordList) => { + onTableChange(recordList); + }, + onChange: (editableKeys, editableRows) => { + setEditableRowKeys(editableKeys); + }, + }} + /> + + + + + {!editOpen && } + + + + + ); +}); diff --git a/src/components/kpi/SumProfitPanel.jsx b/src/components/kpi/SubjectTable/SumProfitPanel.jsx similarity index 91% rename from src/components/kpi/SumProfitPanel.jsx rename to src/components/kpi/SubjectTable/SumProfitPanel.jsx index 0818a00..1b5666d 100644 --- a/src/components/kpi/SumProfitPanel.jsx +++ b/src/components/kpi/SubjectTable/SumProfitPanel.jsx @@ -2,8 +2,8 @@ import { useContext } from 'react'; import { observer } from 'mobx-react'; // import { stores_Context } from '../config'; import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd'; -import SearchForm from './../search/SearchForm'; -import { bu, KPIObjects } from './../../libs/ht'; +import SearchForm from '../../search/SearchForm'; +import { bu, KPIObjects } from '../../../libs/ht'; export default observer((props) => { // const { } = useContext(stores_Context); diff --git a/src/components/search/BusinessSelect.jsx b/src/components/search/BusinessSelect.jsx index cb94770..c84eb5e 100644 --- a/src/components/search/BusinessSelect.jsx +++ b/src/components/search/BusinessSelect.jsx @@ -20,7 +20,7 @@ const Business_unit = (props) => { } store?.bu_handleChange(value); }} - labelInValue={true} + labelInValue={false} {...extProps} > {props.show_all ? ALL 事业部 : ''} diff --git a/src/components/search/BusinessUnitSelect.jsx b/src/components/search/BusinessUnitSelect.jsx index 1b0738c..2c24c78 100644 --- a/src/components/search/BusinessUnitSelect.jsx +++ b/src/components/search/BusinessUnitSelect.jsx @@ -21,7 +21,7 @@ const Business_unit = (props) => { } // store?.bu_handleChange(value); }} - labelInValue={true} + labelInValue={false} {...extProps} > {_show_all ? ALL 事业部 : ''} diff --git a/src/components/search/DataTypeSelect.jsx b/src/components/search/DataTypeSelect.jsx index 731c81b..9d199ba 100644 --- a/src/components/search/DataTypeSelect.jsx +++ b/src/components/search/DataTypeSelect.jsx @@ -23,15 +23,16 @@ class DataTypeSelect extends Component { }; render() { - const store = this.props.store; + const { store, ...extProps } = this.props; return ( - ` + ${omittedValues.length} 更多...`} allowClear={_mode != null} @@ -38,7 +39,7 @@ class GroupSelect extends Component { ) : ( '' )} - {groups.map((ele) => ( + {options.map((ele) => ( {ele.label} diff --git a/src/components/search/Input.jsx b/src/components/search/Input.jsx index 2317d5c..ec92225 100644 --- a/src/components/search/Input.jsx +++ b/src/components/search/Input.jsx @@ -4,7 +4,7 @@ import querystring from 'querystring'; // import * as oMapper from 'object-mapper'; import { fetchJSON } from './../../utils/request'; import { observer } from 'mobx-react'; -import { objectMapper } from './../../utils/commons'; +import { merge, objectMapper } from './../../utils/commons'; const { Option } = Select; @@ -24,10 +24,7 @@ function curl(opts, callback) { const _p = [{ 'key': '0', 'label': '空' }]; return callback(_p); } - const param = { - code: 'utf-8', - q: opts.value, - }; + const param = merge({ code: 'utf-8', q: opts.value }, opts.param); // const str = new URLSearchParams({ // code: 'utf-8', // q: opts.value, @@ -66,9 +63,9 @@ class SearchInput extends React.Component { componentDidMount() { if (this.props.autoGet === true) { - const { map, resultkey } = this.props; + const { map, resultkey, param } = this.props; const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {}); - curl({ value: '', url: this.props.url || '', map: mapKey, resultkey }, (data) => + 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,10 +87,10 @@ class SearchInput extends React.Component { this.setState({ data: f || [] }); return false; } - const { map, resultkey } = this.props; + const { map, resultkey, param } = this.props; const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {}); if (value) { - curl({ value, url: this.props.url || '', map: mapKey, resultkey }, (data) => + 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) : '')) ); } else { @@ -102,6 +99,7 @@ class SearchInput extends React.Component { }; handleChange = (value, option) => { + // console.log('invoke denpendencies change', value); this.setState({ value }, () => this.props.onChange(value, option)); }; diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index 1a4a40a..e8051c4 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext, useEffect } from 'react'; import { toJS } from 'mobx'; import { observer } from 'mobx-react'; import { DATE_FORMAT, stores_Context } from './../../config'; @@ -14,7 +14,7 @@ import DateTypeSelect from './DataTypeSelect'; import DatePickerCharts from './DatePickerCharts'; import YearPickerCharts from './YearPickerCharts'; import SearchInput from './Input'; -import { objectMapper, at } from './../../utils/commons'; +import { objectMapper, at, empty } from './../../utils/commons'; import './search.css'; @@ -44,8 +44,7 @@ export default observer((props) => { }; const { onSubmit, confirmText } = props; - const onFinish = (values) => { - console.log('Received values of form, origin form value: ', values); + const formValuesMapper = (values) => { const destinationObject = { 'DateType': { key: 'DateType', @@ -55,28 +54,28 @@ export default observer((props) => { 'HTBusinessUnits': { key: 'HTBusinessUnits', transform: (value) => { - return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '-1'; + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : ''; }, default: '', }, 'businessUnits': { key: 'businessUnits', transform: (value) => { - return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '-1'; + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : ''; }, default: '', }, 'DepartmentList': { key: 'DepartmentList', transform: (value) => { - return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : 'ALL'; + return 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 : 'ALL'; + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : ''; }, default: '', }, @@ -154,6 +153,12 @@ export default observer((props) => { } // omit empty Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]); + return dest; + }; + + const onFinish = (values) => { + console.log('Received values of form, origin form value: ', values); + const dest = formValuesMapper(values); console.log('form value send to onSubmit:', dest); const str = new URLSearchParams(dest).toString(); searchFormStore.setFormValues(values); @@ -170,8 +175,11 @@ export default observer((props) => { }; const onValuesChange = (...args) => { const [changedValues, allValues] = args; - console.log('form onValuesChange', args); + // console.log('form onValuesChange', Object.keys(changedValues), args); + + const dest = formValuesMapper(allValues); searchFormStore.setFormValues(allValues); + searchFormStore.setFormValuesToSub(dest); }; return ( @@ -224,28 +232,28 @@ function getFields(props) { 'HTBusinessUnits', 99, - + ), item( 'businessUnits', 99, - + ), item( 'DepartmentList', 99, - + ), item( 'WebCode', 99, - + ), item( @@ -269,18 +277,10 @@ function getFields(props) { 'DateType', 99, - + , 2 ), - item( - 'dates', - 99, - - - , - midCol - ), item( 'years', 99, @@ -298,11 +298,26 @@ function getFields(props) { , 2 ), + item( + 'dates', + 99, + + + , + midCol + ), item( 'operator', 99, - - + + ), item( diff --git a/src/components/search/SiteSelect.jsx b/src/components/search/SiteSelect.jsx index 3d22f93..2e25905 100644 --- a/src/components/search/SiteSelect.jsx +++ b/src/components/search/SiteSelect.jsx @@ -26,7 +26,7 @@ class SiteSelect extends Component { } store?.handleChange_webcode(value); }} - labelInValue={true} + labelInValue={false} maxTagCount={1} maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`} allowClear={_mode != null} diff --git a/src/libs/ht.js b/src/libs/ht.js index 6f9cc80..7834e61 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -55,7 +55,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 leafGroup = groups.slice(3); /** * 来源 */ @@ -97,7 +97,7 @@ export const dataFieldOptions = [ { label: '毛利', value: 'SumML', formatter: (v) => `${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' } }, + { label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, // { label: '人数', value: 'CJPersonNum', formatter: (v) => v }, // todo: more... ]; @@ -108,7 +108,7 @@ export const dataFieldAlias = dataFieldOptions.reduce( (a, c) => ({ ...a, [c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) }, - [c.nestkey.v]: { ...c, value: c.nestkey.v, alias: `${c.label}目标`, formatter: (v) => c.formatter(v) }, + [c.nestkey.v]: { ...c, value: c.nestkey.v, alias: `${c.label}目标`, label: `${c.label}目标`, formatter: (v) => c.formatter(v) }, }), {} ); @@ -118,8 +118,8 @@ export const dataFieldAlias = dataFieldOptions.reduce( */ export const KPIObjects = [ { key: 'overview', value: 'overview', label: '海纳' }, - { key: 'bu', value: 'bu', label: '事业部', data: bu }, - { key: 'dept', value: 'dept', label: '小组', data: groups }, + { key: 'bu', value: 'bu', label: 'HT事业部', data: bu }, + { key: 'dept', value: 'dept', label: '小组', data: leafGroup }, { key: 'du', value: 'du', label: '销售小组', data: deptUnits }, { key: 'operator', value: 'operator', label: '顾问' }, { key: 'destination', value: 'destination', label: '目的地' }, diff --git a/src/mock/2.0/baseinfo.json b/src/mock/2.0/baseinfo.json index e02e285..cc1df10 100644 --- a/src/mock/2.0/baseinfo.json +++ b/src/mock/2.0/baseinfo.json @@ -1,15 +1,13 @@ { - "get|/service-web/baseinfo/operator/test": { + "get|/service-Analyse2/GetDestinationInfo/test": { "errcode": 0, "errmsg": "", "data": null, "loading": null, "result|10": [ { - "mobile": "13@integer(99999999,999999999)", - "op_id": "@integer(10,99)", + "id": "@integer(10,99)", "cn_name": "@cname", - "email": "@email", "en_name": "@first", "code": "@word(2,3)", "key": "@increment" diff --git a/src/stores/DatePickerStore.js b/src/stores/DatePickerStore.js index 9735607..eba2f83 100644 --- a/src/stores/DatePickerStore.js +++ b/src/stores/DatePickerStore.js @@ -10,9 +10,6 @@ class DatePickerStore { makeAutoObservable(this); } - formValues = {}; - formValuesToSub = {}; - start_date = moment().startOf('week').subtract(7, 'days'); end_date = moment().endOf('week').subtract(7, 'days'); start_date_cp = false; @@ -43,6 +40,26 @@ class DatePickerStore { return [moment(this.start_date).subtract(1, 'year'), moment(this.end_date).subtract(1, 'year')]; } + + formValues = { + 'DepartmentList': { 'key': 'ALL', 'label': '所有小组' }, + 'WebCode': { 'key': 'ALL', 'label': '所有来源' }, + 'IncludeTickets': { 'key': '1', 'label': '含门票' }, + 'DateType': { 'key': 'applyDate', 'label': '提交日期' }, + 'years': this.start_date, + // 'months': [moment(), moment()], + 'dates': [this.start_date, this.end_date], + }; + + formValuesToSub = { + DepartmentList: 'ALL', + WebCode: 'ALL', + IncludeTickets: '1', + DateType: 'applyDate', + Date1: this.start_date.format('YYYY-MM-DD'), + Date2: this.end_date.format('YYYY-MM-DD 23:59:59'), + }; + setFormValues(data){ this.formValues = data; } diff --git a/src/stores/DictData.js b/src/stores/DictData.js index 916d8a5..6c53bd3 100644 --- a/src/stores/DictData.js +++ b/src/stores/DictData.js @@ -19,6 +19,14 @@ const modelMapper = { en_name: { key: 'label_alias' }, }, }, + 'destination': { + url: '/service-Analyse2/GetDestinationInfo/test', + mapper: { + id: [{ key: 'key' }, { key: 'value' }], + cn_name: { key: 'label' }, + en_name: { key: 'label_alias' }, + }, + }, 'vendor': { url: '/service-web/QueryData/GetVEIName', mapper: { @@ -48,7 +56,7 @@ class DictData { runInAction(() => { this[mkey].loading = false; this[mkey].dataSource = objectMapper(json.result, modelMapper[mkey].mapper); - console.log({ loading: false, ...json }, model, 'DictData', toJS(this[mkey])); + // console.log({ loading: false, ...json }, model, 'DictData', toJS(this[mkey])); }); } return this[mkey]; @@ -56,6 +64,7 @@ class DictData { data = {}; operator = { loading: false, dataSource: [] }; + country = { loading: false, dataSource: [] }; vendor = { loading: false, dataSource: [] }; creditcardbilltype = { loading: false, dataSource: [] }; } diff --git a/src/stores/Distribution.js b/src/stores/Distribution.js new file mode 100644 index 0000000..5f62d6c --- /dev/null +++ b/src/stores/Distribution.js @@ -0,0 +1,86 @@ +import { makeAutoObservable, runInAction, toJS } from 'mobx'; +import * as req from '../utils/request'; +import { isEmpty, pick, sortBy } from '../utils/commons'; + +const modelMapper = { + 'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays' }, + 'PML': { url: '/service-Analyse2/GetTradeApartByPML' }, + 'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays' }, + 'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays' }, + 'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum' }, + 'destination': { url: '/service-Analyse2/GetTradeApartByDestination' }, + 'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination' }, +}; +class Distribution { + constructor(appStore){ + this.appStore = appStore; + makeAutoObservable(this); + } + + /** + * 各个类型的分布 + */ + getApartData = async (param) => { + const mkey = this.curTab; + this[mkey] = { loading: true, dataSource: [] }; + const json = await req.fetchJSON(modelMapper[mkey].url, param); + if (json.errcode === 0) { + runInAction(() => { + const dataLength = json.result.length; + this[mkey].loading = false; + this[mkey].originData = json.result; + this[mkey].dataSource = dataLength > 20 ? json.result.slice(0, 30) : json.result; + }); + } + return this[mkey]; + }; + + /** + * 明细 + */ + getDetailData = async (param) => { + this.detailData.loading = true; + const json = await req.fetchJSON('/service-Analyse2/GetTradeApartDetail', param); + if (json.errcode === 0) { + runInAction(() => { + this.detailData.loading = false; + this.detailData.dataSource = json.result; + const daysData = json.result.filter(ele => ele.confirmDays).map(row => pick(row, ['o_id', 'tourdays', 'applyDays', 'personNum', 'country', 'startDate'])); + this.scatterDays = daysData; + }); + } + return this.detailData; + }; + + resetData = () => { + // this.detailData = { loading: false, dataSource: [] }; + // this.scatterDays = []; + + this.tourDays = { loading: false, dataSource: [] }; + this.PML = { loading: false, dataSource: [] }; + this.ConfirmDays = { loading: false, dataSource: [] }; + this.ApplyDays = { loading: false, dataSource: [] }; + this.PersonNum = { loading: false, dataSource: [] }; + this.destination = { loading: false, dataSource: [] }; + this.GlobalDestination = { loading: false, dataSource: [] }; + }; + + curTab = 'tourDays'; + setCurTab(v) { + this.curTab = v; + } + + pageLoading = false; + + detailData = { loading: false, dataSource: [], }; + scatterDays = []; + + tourDays = { loading: false, dataSource: [] }; + PML = { loading: false, dataSource: [] }; + ConfirmDays = { loading: false, dataSource: [] }; + ApplyDays = { loading: false, dataSource: [] }; + PersonNum = { loading: false, dataSource: [] }; + destination = { loading: false, dataSource: [] }; + GlobalDestination = { loading: false, dataSource: [] }; +} +export default Distribution; diff --git a/src/stores/Index.js b/src/stores/Index.js index 0d38106..48dfba1 100644 --- a/src/stores/Index.js +++ b/src/stores/Index.js @@ -13,6 +13,7 @@ import CustomerServicesStore from "./CustomerServices"; import TradeStore from "./Trade"; import KPI from "./KPI"; import DictData from "./DictData"; +import Distribution from "./Distribution"; class Index { constructor() { this.dashboard_store = new DashboardStore(this); @@ -29,6 +30,7 @@ class Index { this.TradeStore = new TradeStore(this); this.KPIStore = new KPI(this); this.DictDataStore = new DictData(this); + this.DistributionStore = new Distribution(this); makeAutoObservable(this); } diff --git a/src/stores/KPI.js b/src/stores/KPI.js index 40e5b69..dd76555 100644 --- a/src/stores/KPI.js +++ b/src/stores/KPI.js @@ -22,13 +22,14 @@ class KPI { } async onSubmit(tableData, { delQueue }) { + this.listLoading = true; const flushData = tableData.filter(row => !isEmpty(row.value) || !isEmpty(row?.kpi_id)); const postRes = isEmpty(flushData) ? true : await this.saveOrUpdate(flushData); const delRes = isEmpty(flush(delQueue)) ? true : await this.delByID(delQueue); return postRes && delRes; } - getList(param = {}) { + async getList(param = {}) { const _param = { date_type: 'applyDate', start_date: '2020-01-01', @@ -38,18 +39,17 @@ class KPI { }; this.listLoading = true; this.pageData = []; - return req.fetchJSON('/service-Analyse2/getkpi', _param).then((json) => { - if (json.errcode === 0) { - runInAction(() => { - this.listLoading = false; - this.originData = json.result; - const yearData = parseKPI(json.result, ['subject', 'object_id']); - console.log(111, yearData, yearData[this.settingYear]); - this.pageData = yearData?.[this.settingYear]?.[this.settingSubject] || []; - }); - } - return this.pageData; - }); + const json = await req.fetchJSON('/service-Analyse2/getkpi', _param); + if (json.errcode === 0) { + runInAction(() => { + this.listLoading = false; + this.originData = json.result; + const yearData = parseKPI(json.result, ['subject', 'object_id']); + // console.log(111, yearData, yearData[this.settingYear]); + this.pageData = yearData?.[this.settingYear]?.[this.settingSubject] || []; + }); + } + return this.pageData; } settingYear = moment().year(); diff --git a/src/stores/OrdersStore.js b/src/stores/OrdersStore.js index 50ab2a4..e8baed2 100644 --- a/src/stores/OrdersStore.js +++ b/src/stores/OrdersStore.js @@ -1,4 +1,4 @@ -import { makeAutoObservable, runInAction } from "mobx"; +import { makeAutoObservable, runInAction, toJS } from "mobx"; import { CaretUpOutlined, CaretDownOutlined } from "@ant-design/icons"; import { Tag } from "antd"; import * as config from "../config"; @@ -121,16 +121,46 @@ class OrdersStore { (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); return r; }, {}); - this.orderCountData = Object.keys(groupByDate) + const _data = Object.keys(groupByDate) .reduce((r, _d) => { - const summaryVal = groupByDate[_d].reduce((rows, row) => rows + row.orderCount, 0); - r.push({ ...groupByDate[_d][0], orderCount: summaryVal }); + const xAxisGroup = groupByDate[_d].reduce((a, v) => { + (a[v.groups] || (a[v.groups] = [])).push(v); + return a; + }, {}); + Object.keys(xAxisGroup).map((_group) => { + const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal }); + return _group; + }); return r; }, []) .map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups })); + this.orderCountData = _data; this.avgLine1 = avg1; }; + summaryAllWebcode = (json) => { + const groupByDate1 = json.ordercount1.reduce((r, v) => { + (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); + return r; + }, {}); + const ordercount1 = Object.keys(groupByDate1).reduce((r, _date) => { + const summaryVal = groupByDate1[_date].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...groupByDate1[_date][0], orderCount: summaryVal }); + return r; + }, []); + const groupByDate2 = json.ordercount2.reduce((r, v) => { + (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); + return r; + }, {}); + const ordercount2 = Object.keys(groupByDate2).reduce((r, _date) => { + const summaryVal = groupByDate2[_date].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...groupByDate2[_date][0], orderCount: summaryVal }); + return r; + }, []); + return { ...json, ordercount1, ordercount2 }; + }; + parseOrderCount = (orderCountData, dateGroup) => { resultDataCb(orderCountData, dateGroup, this.orderCountDataMapper, this.orderCountDataFieldMapper, this.onChangeDateGroup); }; @@ -175,9 +205,10 @@ class OrdersStore { .then(response => response.json()) .then(json => { runInAction(() => { - this.orderCountDataRaw = json; + const data = this.summaryAllWebcode(json); + this.orderCountDataRaw = data; // 第一次得到数据 - this.parseOrderCount(json, 'day'); + this.parseOrderCount(data, 'day'); this.loading = false; }); }) diff --git a/src/stores/Trade.js b/src/stores/Trade.js index e165dae..50e2fed 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -15,7 +15,7 @@ class Trade { fetchSummaryData(queryData) { this.summaryData.loading = true; queryData.groupType = 'overview'; - // queryData.groupDateType = 'year'; + queryData.groupDateType = 'year'; this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { @@ -26,6 +26,7 @@ class Trade { { title: '成团', value: summary?.ConfirmOrder, + valueSuffix: summary?.ConfirmRates ? ` / ${summary.ConfirmRates} %` : undefined, // VSrate: summary?.ConfirmOrderrate, KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p], // hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]), diff --git a/src/utils/commons.js b/src/utils/commons.js index 6a9adff..a90a0f2 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -289,7 +289,7 @@ export const sortBy = (key) => { export function merge(...objects) { const isDeep = objects.some(obj => obj !== null && typeof obj === 'object'); - const result = objects[0] ?? {}; + const result = objects[0] || (isDeep ? {} : objects[0]); for (let i = 1; i < objects.length; i++) { const obj = objects[i]; @@ -301,7 +301,7 @@ export function merge(...objects) { if (isDeep) { if (Array.isArray(val)) { - result[key] = [...result[key] || [], ...val]; + result[key] = [].concat(Array.isArray(result[key]) ? result[key] : [result[key]], val); } else if (typeof val === 'object') { result[key] = merge(result[key], val); } else { diff --git a/src/views/Detail.jsx b/src/views/Detail.jsx new file mode 100644 index 0000000..8ba1c80 --- /dev/null +++ b/src/views/Detail.jsx @@ -0,0 +1,90 @@ +import { useContext, useEffect, useMemo } from 'react'; +import { observer } from 'mobx-react'; +import { stores_Context } from '../config'; +import { Row, Col, Spin, Space, Radio, Tabs, Table } from 'antd'; +import { empty } from '../utils/commons'; +import { dataFieldAlias } from '../libs/ht'; +import Scatter from './../components/Scatter'; +import SearchForm from './../components/search/SearchForm'; +import { Histogram } from '@ant-design/plots'; + +export default observer((props) => { + const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context); + const { formValues, formValuesToSub } = searchFormStore; + const { curTab, scatterDays, detailData } = DistributionStore; + + const detailRefresh = (obj) => { + DistributionStore.getDetailData({ + ...(obj || formValuesToSub), + }); + }; + + useEffect(() => { + if (empty(detailData.dataSource)) { + detailRefresh(); + } + }, []); + + const ScatterConfig = { + xField: 'startDate', + yField: 'tourdays', + colorField: 'country', + size: 4, + // xAxis: { + // min: 0, + // max: 30, + // }, + yAxis: { + min: 0, + max: 10, + }, + // quadrant: { + // xBaseline: 15, + // yBaseline: 5, + // }, + tooltip: false, + legend: { + position: 'right-top', + }, + }; + const HistogramConfig = { + binField: 'personNum', + binWidth: 1, + }; + return ( + <> + + {/* style={{ margin: '-16px -8px', padding: 0 }} */} + + { + detailRefresh(obj); + }} + /> + + + +
+ + + +
+ {/*
+ + + +
*/} + + ); +}); diff --git a/src/views/Distribution.jsx b/src/views/Distribution.jsx new file mode 100644 index 0000000..59c6876 --- /dev/null +++ b/src/views/Distribution.jsx @@ -0,0 +1,110 @@ +import { useContext, useEffect } from 'react'; +import { observer } from 'mobx-react'; +import { stores_Context } from '../config'; +import { Row, Col, Spin, Tabs, Table } 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 './kpi.css'; + +const apartOptions = [ + { key: 'tourDays', value: 'tourDays', label: '团天数' }, + { key: 'PML', value: 'PML', label: '单团毛利' }, + { 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: '海外目的地' }, +]; + +export default observer(() => { + const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context); + const { formValues, formValuesToSub } = searchFormStore; + const { curTab } = DistributionStore; + + const pageRefresh = (obj) => { + DistributionStore.getApartData({ + ...(obj || formValuesToSub), + }); + }; + + useEffect(() => { + if (empty(DistributionStore[curTab].dataSource)) { + pageRefresh(); + } + }, [curTab]); + + useEffect(() => { + DistributionStore.resetData(); + return () => {}; + }, [formValuesToSub]); + + const onTabsChange = (tab) => { + DistributionStore.setCurTab(tab); + }; + const RingProgressConfig = { + height: 60, + width: 60, + autoFit: false, + color: ['#5B8FF9', '#E8EDF3'], + }; + const columns = [ + { title: '', dataIndex: 'label' }, + { title: '团数', dataIndex: 'ConfirmOrder' }, + { title: '业绩', dataIndex: 'SumML', render: (v) => dataFieldAlias.SumML.formatter(v) }, + { title: '团数占比', dataIndex: 'ConfirmOrderPercent', render: (v) => }, + { title: '业绩占比', dataIndex: 'SumMLPercent', render: (v) => }, + ]; + return ( + <> + + + { + pageRefresh(obj); + }} + /> + + + +
+ { + return { + ...ele, + children: ( + + record.label} + loading={DistributionStore[curTab].loading} + pagination={false} + scroll={{ x: '100%' }} + /> + + ), + }; + })} + /> + + + ); +}); diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 07a9b72..6605fb0 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -1,12 +1,13 @@ import { useContext, useEffect, useState } from 'react'; import { observer } from 'mobx-react'; import { Row, Col, Spin, Space, Radio } from 'antd'; -import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone, } from '@ant-design/icons'; +import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone } from '@ant-design/icons'; import { stores_Context } from '../config'; import { useNavigate } from 'react-router-dom'; import StatisticCard from '../components/StatisticCard'; import Bullet from '../components/BulletWithSort'; import Waterfall from '../components/Waterfall'; +import LineWithKPI from '../components/LineWithKPI'; import DataFieldRadio from '../components/DataFieldRadio'; import { datePartOptions } from './../components/DateGroupRadio/date'; import SearchForm from './../components/search/SearchForm'; @@ -30,7 +31,7 @@ export default observer(() => { // const navigate = useNavigate(); const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context); const { sideData, summaryData, BuData, topData, timeData, timeLineKey } = TradeStore; - const { formValues, } = searchFormStore; + const { formValues } = searchFormStore; useEffect(() => { if (empty(summaryData.dataSource)) { @@ -92,7 +93,7 @@ export default observer(() => { }, }, label: { - formatter: (v) => summaryData.kpi.value === 0 ? (dataFieldAlias.SumML?.formatter(v.SumML) || v.SumML) : ((v.SumML / summaryData.kpi.value) * 100).toFixed(2) + '%', + formatter: (v) => (summaryData.kpi.value === 0 ? dataFieldAlias.SumML?.formatter(v.SumML) || v.SumML : ((v.SumML / summaryData.kpi.value) * 100).toFixed(2) + '%'), }, }; @@ -102,6 +103,14 @@ export default observer(() => { targetField: 'MLKPIvalue', // xField: 'groupsLabel', layout: 'vertical', + xAxis: { + line: null, + label: { + autoHide: true, + autoRotate: false, + }, + }, + legend: false, }; const lineConfigSet = { @@ -116,15 +125,11 @@ export default observer(() => { smooth: true, point: { size: 4, - shape: "cicle", + shape: 'cicle', }, legend: false, - meta: { ...cloneDeep(dataFieldAlias) - // [extProps.yField]: { - // alias: dataFieldAlias[extProps.yField]?.alias || extProps.yField, - // formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v, - // max: Math.ceil(yMax / 0.95), - // }, + meta: { + ...cloneDeep(dataFieldAlias), }, }; const [timeDataField, setTimeDataField] = useState('SumML'); @@ -132,19 +137,19 @@ export default observer(() => { const handleChangetimeDataField = (key) => { setTimeDataField(key); setLineConfig({ - ...lineConfig, + ...cloneDeep(lineConfig), yField: key, tooltip: { customItems: (originalItems) => { // process originalItems, - const items = originalItems.map((ele) => ({ ...ele, name: dataFieldAlias[key]?.alias || key })); + const items = originalItems.map((ele) => ({ ...ele, name: ele.data?.extraLine ? ele.name : (dataFieldAlias[key]?.alias || key) })); return items; }, }, }); }; const [dateField, setDateField] = useState(timeLineKey); - const handleChangeDateType = ({target: {value}}) => { + const handleChangeDateType = ({ target: { value } }) => { setDateField(value); TradeStore.setTimeLineKey(value); if (!isEmpty(TradeStore.searchPayloadHome)) { @@ -196,7 +201,8 @@ export default observer(() => { - + {/* */} +
@@ -205,7 +211,7 @@ export default observer(() => {
-

{`各事业部总业绩`}

+

{`各事业部总业绩`}

{Object.keys(sideData.dataSource).map((key) => (
diff --git a/src/views/KPI.jsx b/src/views/KPI.jsx index 288e041..524e364 100644 --- a/src/views/KPI.jsx +++ b/src/views/KPI.jsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react'; import { stores_Context } from './../config'; import { observer } from 'mobx-react'; import { Row, Col, Tabs, Spin } from 'antd'; -import { flush, objectMapper } from './../utils/commons'; +import { flush, isEmpty, objectMapper } from './../utils/commons'; import { KPIObjects } from './../libs/ht'; import ObjectPanel from '../components/kpi/ObjectPanel'; import OverviewPanel from './../components/kpi/OverviewPanel'; @@ -23,7 +23,7 @@ const objectFilterKey = { // 'du': 'du', // 'operator': 'operator', // 'destination': 'destination', - // 'country': 'country', + 'country': 'country', }; export default observer((props) => { const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context); @@ -42,13 +42,13 @@ export default observer((props) => { }, [curObject]); const [retObjects, setRetObjects] = useState([]); - const onSearchSubmit = (obj, formVal={}) => { + const onSearchSubmit = async (obj, formVal={}) => { const getkpiParam = objectMapper(obj, { DateType: { key: 'date_type' }, Date1: { key: 'start_date' }, Date2: { key: 'end_date' }, HTBusinessUnits: { key: 'object_id' }, - DepartmentList: { key: 'object_id' }, + DepartmentList: { key: curObject === 'dept' ? 'object_id' : 'dept_id' }, businessUnits: { key: 'object_id' }, WebCode: { key: 'object_id' }, operator: { key: 'object_id' }, @@ -56,9 +56,28 @@ export default observer((props) => { }); Object.assign(getkpiParam, { object: curObject }); KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear); - console.log('invoke on search', obj, formVal, getkpiParam); - KPIStore.getList(getkpiParam).then((data) => { - // setDataSource(data); + // console.log('invoke on search', obj, formVal, getkpiParam); + if (curObject === 'operator') { + KPIStore.setListLoading(true); + const searchOperator = await DictDataStore.fetchDictData('operator', { + is_assign: 1, + dept_id: (obj?.DepartmentList || '').replace('ALL', ''), + q: searchFormStore.formValues?.operator?.label || '', + }); + if ((!isEmpty(getkpiParam.dept_id) && getkpiParam.dept_id !== 'ALL') + || !isEmpty(searchFormStore.formValues?.operator)) { + // && isEmpty(getkpiParam.object_id) + setRetObjects(searchOperator.dataSource); + getkpiParam.object_id = searchOperator.dataSource.map((ele) => ele.key).join(','); + delete getkpiParam.dept_id; + } + } + await getKPIList(getkpiParam); + }; + const getKPIList = async (getkpiParam) => { + const _data = await KPIStore.getList(getkpiParam); + // KPIStore.getList(getkpiParam).then((data) => { + // // setDataSource(data); if (objectFilterKey?.[curObject]) { const selectItem = searchFormStore.formValues[objectFilterKey[curObject]]; if (selectItem) { @@ -66,7 +85,7 @@ export default observer((props) => { } setRetObjects(flush([selectItem])); } - }); + // }); }; return ( <> diff --git a/src/views/Orders.jsx b/src/views/Orders.jsx index 50588cf..917a8d5 100644 --- a/src/views/Orders.jsx +++ b/src/views/Orders.jsx @@ -272,7 +272,7 @@ class Orders extends Component { tooltip: { customItems: (originalItems) => { // process originalItems, - return originalItems.map(ele => ({...ele, name: ele.data?.seriesKey || ele.data?.xField})); + return originalItems.map(ele => ({...ele, name: ele.data?.seriesField || ele.data?.xField})); }, title: (title, datum) => { let ret = title; diff --git a/src/views/Sale.jsx b/src/views/Sale.jsx index 7452982..26e7b83 100644 --- a/src/views/Sale.jsx +++ b/src/views/Sale.jsx @@ -53,56 +53,86 @@ const Sale = () => { // }; const column_config_create = (tab_name) => { - let config_data = { - //seriesField: "OPI_Name", - label: { - position: 'top', - }, - xAxis: { - label: { - autoHide: false, - autoRotate: true, - }, - }, - // legend: { - // itemValue: { - // formatter: (text, item) => { - // const items = ml_data.filter(d => d.groups === item.value); // 按分组筛选 - // return items.length ? items.reduce((a, b) => a + b.COLI_ML, 0) : ""; // 计算总数 - // }, - // }, - // }, - // tooltip: { - // customContent: (title, items) => { - // const data = items[0].data || {};console.log(data); - // return `
${title}
wwwwwww
`; - // }, - // title: (title, datum) => { - // return title; // + " " + comm.getWeek(datum.COLI_Date); // 显示周几 - // }, - // },}; - }; + let average_value = 0; //平均线的值 + let config_data = []; switch (tab_name) { case 'All': config_data.data = type_data.dataSource; config_data.xField = 'OPI_Name'; config_data.yField = 'COLI_ML2'; + average_value = Math.round(config_data.data.reduce((a, b) => a + b.COLI_ML2, 0) / config_data.data.length); break; case 'ResponseRateWhatsApp': config_data.data = type_data.dataSource; config_data.xField = 'OPI_Name'; config_data.yField = 'COLI_ConfirmTimeAVG'; + average_value = Math.round(config_data.data.reduce((a, b) => a + b.COLI_ConfirmTimeAVG, 0) / config_data.data.length); break; case 'ResponseRateByWL': config_data.data = type_data.dataSource; config_data.xField = 'OPI_Name'; config_data.yField = 'PriceTime'; + average_value = Math.round(config_data.data.reduce((a, b) => a + b.PriceTime, 0) / config_data.data.length); break; default: config_data.data = []; break; } - return config_data; + return { + ...config_data, + ...{ + //seriesField: "OPI_Name",//分组 + label: { + position: 'top', + }, + xAxis: { + label: { + autoHide: false, + autoRotate: true, + }, + }, + // legend: { + // itemValue: { + // formatter: (text, item) => { + // const items = ml_data.filter(d => d.groups === item.value); // 按分组筛选 + // return items.length ? items.reduce((a, b) => a + b.COLI_ML, 0) : ""; // 计算总数 + // }, + // }, + // }, + // tooltip: { + // customContent: (title, items) => { + // const data = items[0].data || {};console.log(data); + // return `
${title}
wwwwwww
`; + // }, + // title: (title, datum) => { + // return title; // + " " + comm.getWeek(datum.COLI_Date); // 显示周几 + // }, + // },}; + annotations: average_value + ? [ + { + type: 'text', + position: ['start', average_value], + content: average_value, + offsetX: -15, + style: { + fill: '#F4664A', + textBaseline: 'bottom', + }, + }, + { + type: 'line', + start: [-10, average_value], + end: ['max', average_value], + style: { + stroke: '#F4664A', + lineDash: [2, 2], + }, + }, + ] + : [], + }, + }; }; const column_config = column_config_create(sale_store.active_tab_key); @@ -146,6 +176,65 @@ const Sale = () => { ], }; + const tab_items = [ + { + key: 'All', + label: ( + + 总览 + + ), + }, + { + key: 'ResponseRateWhatsApp', + label: ( + + 回复率 + + ), + }, + { + key: 'ResponseRateByWL', + label: ( + + 沟通数据 + + ), + }, + { + key: 'Country', + label: ( + + 国籍 + + ), + }, + { + key: 'Product', + label: ( + + 产品类型 + + ), + }, + { + key: 'TravelMotivation', + label: ( + + 出行目的 + + ), + }, + { + key: 'GuestGroupType', + label: ( + + 成员关系 + + ), + }, + ]; + return (
@@ -240,67 +329,8 @@ const Sale = () => { sale_store.onChange_Tabs(active_key); sale_store.get_department_order_ml_by_type(date_picker_store); }} - > - - 总览 - - } - key="All" - > - - 回复率 - - } - key="ResponseRateWhatsApp" - > - - 沟通数据 - - } - key="ResponseRateByWL" - > - - 国籍 - - } - key="Country" - > - - - 产品类型 - - } - key="Product" - > - - - 出行目的 - - } - key="TravelMotivation" - > - - - 成员关系 - - } - key="GuestGroupType" - > - + items={tab_items} + >