Merge remote-tracking branch 'origin/feature/2.0'

feature/2.0-sales-trade
Lei OT 2 years ago
commit e5ac09e030

@ -10,9 +10,9 @@ import {
DollarOutlined, DollarOutlined,
AreaChartOutlined, AreaChartOutlined,
WechatOutlined, WechatOutlined,
UserOutlined, FlagOutlined, UserOutlined, FlagOutlined, PieChartOutlined, BarChartOutlined
} from '@ant-design/icons'; } 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 { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
import Home from './views/Home'; import Home from './views/Home';
import Dashboard from './views/Dashboard'; import Dashboard from './views/Dashboard';
@ -36,6 +36,8 @@ import { stores_Context } from './config';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import ExchangeRate from './charts/ExchangeRate'; import ExchangeRate from './charts/ExchangeRate';
import KPI from './views/KPI'; import KPI from './views/KPI';
import Distribution from './views/Distribution';
import Detail from './views/Detail';
const App = () => { const App = () => {
const { Content, Footer, Sider } = Layout; const { Content, Footer, Sider } = Layout;
@ -121,6 +123,16 @@ const App = () => {
], ],
}, },
{ key: 'kpi', label: <NavLink to="/kpi">目标</NavLink>, icon: <FlagOutlined /> }, { key: 'kpi', label: <NavLink to="/kpi">目标</NavLink>, icon: <FlagOutlined /> },
{ key: 'distribution', label: <NavLink to="/distribution">统计分布</NavLink>, icon: <PieChartOutlined /> },
{
key: 'detail',
label: (
<NavLink to="/detail">
<Badge.Ribbon text="Beta">统计分析</Badge.Ribbon>
</NavLink>
),
icon: <BarChartOutlined />,
},
]; ];
return ( return (
@ -162,6 +174,8 @@ const App = () => {
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/kpi" element={<KPI />} /> <Route path="/kpi" element={<KPI />} />
<Route path="/distribution" element={<Distribution />} />
<Route path="/detail" element={<Detail />} />
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'marketing']} />}> <Route element={<ProtectedRoute auth={['admin', 'director_bu', 'marketing']} />}>
<Route path="/orders" element={<Orders />} /> <Route path="/orders" element={<Orders />} />
<Route path="/orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<Orders_sub />} /> <Route path="/orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<Orders_sub />} />

@ -30,7 +30,7 @@ export default observer((props) => {
const config = merge({ const config = merge({
color: { color: {
range: [ '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0', range: [ '#FFF3E1', '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0',
measure: '#5B8FF9', measure: '#5B8FF9',
target: '#FF9845', target: '#FF9845',
}, },
@ -48,6 +48,10 @@ export default observer((props) => {
}, },
xAxis: { xAxis: {
line: null, line: null,
label: {
autoHide: false,
autoRotate: true,
},
}, },
yAxis: false, yAxis: false,
// legend // legend

@ -117,7 +117,8 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
})); }));
const parse2 = parseDateType(_data2, dateGroup, fieldMapper); const parse2 = parseDateType(_data2, dateGroup, fieldMapper);
const parseData2 = parse2.data.map((ele) => ({ const parseData2 = parse2.data.map((ele) => ({
[fieldMapper.dateKey]: ele.groupKey, [fieldMapper.dateKey]: ele[fieldMapper.dateKey],
// [fieldMapper.dateKey]: ele.groupKey,
[fieldMapper.valueKey]: ele.value, [fieldMapper.valueKey]: ele.value,
[fieldMapper.seriesKey]: ele[fieldMapper.seriesKey], [fieldMapper.seriesKey]: ele[fieldMapper.seriesKey],
groups: _data2[0].groups, groups: _data2[0].groups,
@ -126,8 +127,8 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
dateGroup: ele[fieldMapper.dateKey], dateGroup: ele[fieldMapper.dateKey],
})); }));
const useKeys = parseData1.map((ele) => 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 reindexData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `_${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey }));
const retData = [].concat(parseData1, reindecData2); const retData = [].concat(parseData1, reindexData2 );
const avg1 = parse1.avgVal; const avg1 = parse1.avgVal;
// console.log('callback', dateGroup, retData); // console.log('callback', dateGroup, retData);
cb(dateGroup, retData, avg1); cb(dateGroup, retData, avg1);

@ -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 <Line {...mergeLineConfig} data={_data} />;
});

@ -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 <Scatter {...config} data={dataSource} />;
});

@ -9,7 +9,6 @@ export default observer((props) => {
return ( return (
<Card> <Card>
<Statistic <Statistic
{...props}
className={'__hn-sta-wrapper'} className={'__hn-sta-wrapper'}
valueStyle={valueStyle} valueStyle={valueStyle}
suffix={ suffix={
@ -23,6 +22,8 @@ export default observer((props) => {
} }
prefix={<ValueIcon twoToneColor="#89B67F" />} prefix={<ValueIcon twoToneColor="#89B67F" />}
// title={<Space><ValueIcon twoToneColor="#89B67F" /><span>{props.title}</span></Space>} // title={<Space><ValueIcon twoToneColor="#89B67F" /><span>{props.title}</span></Space>}
{...props}
value={props.valueSuffix ? `${props.value} ${props.valueSuffix}` : props.value}
/> />
{props.showProgress !== false && <Progress percent={props.KPIrate} size="small" format={(percent) => `${props.KPIrate}%`} />} {props.showProgress !== false && <Progress percent={props.KPIrate} size="small" format={(percent) => `${props.KPIrate}%`} />}
</Card> </Card>

@ -5,7 +5,7 @@ import { merge } from '../utils/commons';
export default observer((props) => { export default observer((props) => {
const { dataSource, line, title, ...extProps } = 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 const annotationsLine = line
? [ ? [
{ {

@ -6,6 +6,9 @@ import SearchForm from './../search/SearchForm';
import { bu, KPIObjects, KPISubjects } from './../../libs/ht'; import { bu, KPIObjects, KPISubjects } from './../../libs/ht';
import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt, merge } from './../../utils/commons'; import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt, merge } from './../../utils/commons';
import ProfitTable from './SubjectTable/Profit'; import ProfitTable from './SubjectTable/Profit';
import Count from './SubjectTable/Count';
import Rates from './SubjectTable/Rates';
import { toJS } from 'mobx';
const Todo = (props) => { const Todo = (props) => {
return <h2>TODO</h2>; return <h2>TODO</h2>;
@ -13,12 +16,12 @@ const Todo = (props) => {
const subjectComponents = { const subjectComponents = {
'sum_profit': ProfitTable, 'sum_profit': ProfitTable,
'in_order_count': Todo, 'in_order_count': Count,
'confirm_order_count': Todo, 'confirm_order_count': Count,
'depart_order_count': Todo, 'depart_order_count': Count,
'confirm_rates': Todo, 'confirm_rates': Rates,
'praise_rates': Todo, 'praise_rates': Rates,
'sum_person_num': Todo, 'sum_person_num': Count,
}; };
export const KPIObjectsMapped = KPIObjects.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); 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 { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { sort, initialValue, hides, shows, fieldProps: _fieldProps } = { const { sort, initialValue, hides, shows, fieldProps: _fieldProps } = {
sort: '', sort: '',
initialValue: searchFormStore.formValues, initialValue: '', // searchFormStore.formValues,
fieldProps: {}, fieldProps: {},
hides: [], hides: [],
shows: ['DateType', 'years'], shows: ['DateType', 'years'],
@ -43,15 +46,15 @@ export default observer((props) => {
<Col className="gutter-row mb-n1 p-none" span={24}> <Col className="gutter-row mb-n1 p-none" span={24}>
<SearchForm <SearchForm
defaultValue={{ defaultValue={{
initialValue, sort, initialValue,
shows, shows,
fieldProps, fieldProps,
}} }}
confirmText="查询" confirmText="查询"
onSubmit={(_err, obj, form, str) => { onSubmit={(_err, obj, form, str) => {
console.log('invoke kpi setting search'); // console.log('invoke kpi setting search');
if (typeof onSearchSubmit === 'function') { if (typeof onSearchSubmit === 'function') {
onSearchSubmit(obj); onSearchSubmit(obj, form);
} }
}} }}
/> />
@ -63,6 +66,7 @@ export default observer((props) => {
tabPosition={'left'} tabPosition={'left'}
onChange={(sub) => { onChange={(sub) => {
KPIStore.setSettingSubject(sub); KPIStore.setSettingSubject(sub);
onSearchSubmit(searchFormStore.formValuesToSub);
}} }}
items={KPISubjects.map((ele, i) => { items={KPISubjects.map((ele, i) => {
const SubjectTableComponent = subjectComponents[ele.key]; const SubjectTableComponent = subjectComponents[ele.key];

@ -1,23 +1,22 @@
import { useContext } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
// import { stores_Context } from '../config';
import { Table } from 'antd';
import KPISettings from './KPISettings'; import KPISettings from './KPISettings';
import { bu, KPISubjects } from '../../libs/ht'; import { bu, KPISubjects } from '../../libs/ht';
const sort = { DateType: 10, years: 11 };
const yearInitial = {};
const searchFormItemSet = { const searchFormItemSet = {
'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'] }, 'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'], sort },
'dept': { shows: ['DateType', 'years', 'DepartmentList'], fieldProps: { DepartmentList: { allowClear: true } } }, 'dept': { shows: ['DateType', 'years', 'DepartmentList'], sort, fieldProps: { DepartmentList: { allowClear: true,isLeaf: true, show_all: false } }, },
'operator': { shows: ['DateType', 'years', 'DepartmentList'] }, // , 'operator' 'operator': { shows: ['DateType', 'years', 'DepartmentList', 'operator'], fieldProps: { DepartmentList: { allowClear: true, isLeaf: true }, operator: { param: { is_assign: 1 } } }, sort },
'destination': { shows: ['DateType', 'years', 'destination'] }, 'destination': { shows: ['DateType', 'years', 'destination'], sort },
'country': { shows: ['DateType', 'years', 'country'] }, 'country': { shows: ['DateType', 'years', 'country'], sort },
}; };
export default observer((props) => { export default observer((props) => {
const searchProps = searchFormItemSet?.[props.curObject] || {}; const searchProps = searchFormItemSet?.[props.curObject] || {};
return ( return (
<> <>
<KPISettings {...{ searchProps, objects: bu, KPISubjects }} {...props} /> <KPISettings {...{ searchProps, KPISubjects }} {...props} />
</> </>
); );
}); });

@ -20,12 +20,12 @@ export default observer((props) => {
const { curObject } = props; const { curObject } = props;
const [dataSource, setDataSource] = useState([]); const [dataSource, setDataSource] = useState([]);
useEffect(() => { useEffect(() => {
onSearchSubmit({ // onSearchSubmit({
object: curObject, // object: curObject,
date_type: 'applyDate', // date_type: 'applyDate',
start_date: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'), // start_date: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'),
end_date: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'), // end_date: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'),
}); // });
return () => {}; return () => {};
}, []); }, []);
@ -129,6 +129,7 @@ export default observer((props) => {
value: mVal, value: mVal,
kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined, kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined,
key: undefined, key: undefined,
group_date_type: 'month',
}; };
}); });
return r.concat(allMonth); return r.concat(allMonth);
@ -143,6 +144,7 @@ export default observer((props) => {
start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'), 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'), end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'),
kpi_id: kpiYear?.kpi_id || undefined, kpi_id: kpiYear?.kpi_id || undefined,
group_date_type: 'year',
}))(dataSource?.[0] || {}); }))(dataSource?.[0] || {});
tableData.unshift(yearRow); tableData.unshift(yearRow);
console.log('sub', tableData, delKpiIds); console.log('sub', tableData, delKpiIds);

@ -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 (
<Space direction={'vertical'}>
<Input key={'input'} suffix="%" type={'number'} value={inputValue} onChange={handleInputChange} step={0.1} />
<Text type={'secondary'}>{calcV}</Text>
</Space>
);
},
[]
);
const RenderMonthCell = (row, mon) => {
return (
<Space direction={'vertical'}>
<div>
{fixTo2Decimals(row?.[`M${mon}Percent`])}
<span>%</span>
</div>
<div>{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}</div>
</Space>
);
};
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 <PercentInput {...{ record }} month={index + 1} key={`M${index + 1}`} />;
},
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: (
<Space>
<span>操作</span>
<Switch
unCheckedChildren="查看"
checkedChildren="编辑"
key={'openEdit'}
// defaultChecked={true}
checked={editOpen}
onChange={(e) => {
makeInitialTable(e);
}}
/>
</Space>
),
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 (
<>
<Row gutter={16} className="mb-1 ">
<Col className="gutter-row mb-n1 p-none" span={24}>
<EditableProTable
key={KPIStore.settingYear}
// headerTitle={``}
columns={columns}
rowKey="key"
scroll={{
x: 1000,
}}
value={dataSource}
onChange={onTableChange}
recordCreatorProps={false}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
actionRender: (row, config, defaultDoms) => {
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);
},
}}
/>
</Col>
<Col className="gutter-row mb-n1 p-none mt-1 align_center" span={24}>
<Space size={'large'}>
<Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
保存数据
</Button>
{!editOpen && <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
编辑设置
</Button>}
</Space>
</Col>
</Row>
</>
);
});

@ -24,11 +24,11 @@ export default observer((props) => {
const [editableRowsKeys, setEditableRowKeys] = useState([]); const [editableRowsKeys, setEditableRowKeys] = useState([]);
useEffect(() => { useEffect(() => {
setDataSource(KPIStore.pageData); setDataSource(KPIStore.pageData);
setEditableRowKeys([]);
setEditOpen(false); setEditOpen(false);
return () => {}; return () => {};
}, [KPIStore.pageData]); }, [KPIStore.pageData]);
const PercentInput = useMemo( const PercentInput = useMemo(
() => () =>
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
@ -84,7 +84,7 @@ export default observer((props) => {
title: '年度目标', title: '年度目标',
dataIndex: 'yearValue', dataIndex: 'yearValue',
valueType: 'digit', valueType: 'digit',
fieldProps: { style: { width: '100%' }, step: 10000*100 }, fieldProps: { style: { width: '100%' }, step: 10000 * 100 },
formItemProps: { formItemProps: {
style: { width: '100%' }, style: { width: '100%' },
}, },
@ -138,23 +138,24 @@ export default observer((props) => {
value: mVal, value: mVal,
kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined, kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined,
key: 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 }) => ({ // console.log('sub', tableData, 'del:', delKpiIds);
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);
// return false; // debug: // return false; // debug:
KPIStore.onSubmit(tableData, { delQueue: delKpiIds }).then((res) => { KPIStore.onSubmit(tableData, { delQueue: delKpiIds }).then((res) => {
if (res) { if (res) {
@ -180,28 +181,37 @@ export default observer((props) => {
date_type: searchFormStore.formValuesToSub.DateType, date_type: searchFormStore.formValuesToSub.DateType,
kpiDataMapped: {}, kpiDataMapped: {},
key: Date.now().toString(32), key: Date.now().toString(32),
group_date_type: 'month',
}), }),
{} {}
); // v.formItemProps.initialValue ); // v.formItemProps.initialValue
const makeInitialTable = (e) => { const makeInitialTable = (e) => {
setEditOpen(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)) { 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); setDataSource(_initialTable);
setEditableRowKeys(_initialTable.map((ele) => ele.key)); setEditableRowKeys(_initialTable.map((ele) => ele.key));
return false; return false;
} }
setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); setDataSource(mergePageData);
setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []);
}; };
const [delKpiIds, setDelKpiIds] = useState([]); const [delKpiIds, setDelKpiIds] = useState([]);
return ( return (
@ -226,13 +236,11 @@ export default observer((props) => {
return [defaultDoms.delete]; return [defaultDoms.delete];
}, },
onDelete: (_key, _row) => { onDelete: (_key, _row) => {
// console.log('del', _key, _row);
const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id); const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id);
rowKpiIds.push(_row?.kpiYear?.kpi_id); rowKpiIds.push(_row?.kpiYear?.kpi_id);
setDelKpiIds(rowKpiIds); setDelKpiIds(rowKpiIds);
}, },
onValuesChange: (record, recordList) => { onValuesChange: (record, recordList) => {
// console.log('on edit, onValuesChange',record, recordList);
onTableChange(recordList); onTableChange(recordList);
}, },
onChange: (editableKeys, editableRows) => { onChange: (editableKeys, editableRows) => {
@ -242,9 +250,14 @@ export default observer((props) => {
/> />
</Col> </Col>
<Col className="gutter-row mb-n1 p-none mt-1 align_center" span={24}> <Col className="gutter-row mb-n1 p-none mt-1 align_center" span={24}>
<Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}> <Space size={'large'}>
保存数据 <Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
</Button> 保存数据
</Button>
{!editOpen && <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
编辑设置
</Button>}
</Space>
</Col> </Col>
</Row> </Row>
</> </>

@ -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 (
<Input key={'input'} suffix="%" type={'number'} value={inputValue} onChange={handleInputChange} step={0.1} />
);
},
[]
);
const RenderMonthCell = (row, mon) => {
return (
<Space direction={'vertical'}>
<div>
{fixTo2Decimals(row?.[`M${mon}Val`])}
<span>%</span>
</div>
</Space>
);
};
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 <PercentInput {...{ record }} month={index + 1} key={`M${index + 1}`} />;
},
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: () => <PercentInput />
},
...monthCol,
{
title: (
<Space>
<span>操作</span>
<Switch
unCheckedChildren="查看"
checkedChildren="编辑"
key={'openEdit'}
// defaultChecked={true}
checked={editOpen}
onChange={(e) => {
makeInitialTable(e);
}}
/>
</Space>
),
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 (
<>
<Row gutter={16} className="mb-1 ">
<Col className="gutter-row mb-n1 p-none" span={24}>
<EditableProTable
key={KPIStore.settingYear}
// headerTitle={``}
columns={columns}
rowKey="key"
scroll={{
x: 1000,
}}
value={dataSource}
onChange={onTableChange}
recordCreatorProps={false}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
actionRender: (row, config, defaultDoms) => {
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);
},
}}
/>
</Col>
<Col className="gutter-row mb-n1 p-none mt-1 align_center" span={24}>
<Space size={'large'}>
<Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
保存数据
</Button>
{!editOpen && <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
编辑设置
</Button>}
</Space>
</Col>
</Row>
</>
);
});

@ -2,8 +2,8 @@ import { useContext } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
// import { stores_Context } from '../config'; // import { stores_Context } from '../config';
import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd'; import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd';
import SearchForm from './../search/SearchForm'; import SearchForm from '../../search/SearchForm';
import { bu, KPIObjects } from './../../libs/ht'; import { bu, KPIObjects } from '../../../libs/ht';
export default observer((props) => { export default observer((props) => {
// const { } = useContext(stores_Context); // const { } = useContext(stores_Context);

@ -20,7 +20,7 @@ const Business_unit = (props) => {
} }
store?.bu_handleChange(value); store?.bu_handleChange(value);
}} }}
labelInValue={true} labelInValue={false}
{...extProps} {...extProps}
> >
{props.show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''} {props.show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''}

@ -21,7 +21,7 @@ const Business_unit = (props) => {
} }
// store?.bu_handleChange(value); // store?.bu_handleChange(value);
}} }}
labelInValue={true} labelInValue={false}
{...extProps} {...extProps}
> >
{_show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''} {_show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''}

@ -23,15 +23,16 @@ class DataTypeSelect extends Component {
}; };
render() { render() {
const store = this.props.store; const { store, ...extProps } = this.props;
return ( return (
<Select labelInValue={true} <Select labelInValue={false}
// value={store.date_type} // value={store.date_type}
value={this.props?.value || store?.date_type} value={this.props?.value || store?.date_type}
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder="选择日期类型" placeholder="选择日期类型"
// onChange={(value) => store.onChange_datetype(value)} // onChange={(value) => store.onChange_datetype(value)}
onChange={this.handleChange} onChange={this.handleChange}
{...extProps}
> >
{dateTypes.map((ele) => ( {dateTypes.map((ele) => (
<Select.Option key={ele.key} value={ele.key}> <Select.Option key={ele.key} value={ele.key}>

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { groups } from '../../libs/ht'; import { groups, leafGroup } from '../../libs/ht';
class GroupSelect extends Component { class GroupSelect extends Component {
constructor(props) { constructor(props) {
@ -9,9 +9,10 @@ class GroupSelect extends Component {
} }
render() { render() {
const { store, mode, value, onChange, show_all, ...extProps } = this.props; const { store, mode, value, onChange, show_all, isLeaf, ...extProps } = this.props;
const _mode = mode || store?.group_select_mode || null; const _mode = mode || store?.group_select_mode || null;
const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all; const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all;
const options = isLeaf===true ? leafGroup : groups;
return ( return (
<div> <div>
<Select <Select
@ -25,7 +26,7 @@ class GroupSelect extends Component {
} }
store?.group_handleChange(value); store?.group_handleChange(value);
}} }}
labelInValue={true} labelInValue={false}
maxTagCount={1} maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`} maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={_mode != null} allowClear={_mode != null}
@ -38,7 +39,7 @@ class GroupSelect extends Component {
) : ( ) : (
'' ''
)} )}
{groups.map((ele) => ( {options.map((ele) => (
<Select.Option key={ele.key} value={ele.key}> <Select.Option key={ele.key} value={ele.key}>
{ele.label} {ele.label}
</Select.Option> </Select.Option>

@ -4,7 +4,7 @@ import querystring from 'querystring';
// import * as oMapper from 'object-mapper'; // import * as oMapper from 'object-mapper';
import { fetchJSON } from './../../utils/request'; import { fetchJSON } from './../../utils/request';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { objectMapper } from './../../utils/commons'; import { merge, objectMapper } from './../../utils/commons';
const { Option } = Select; const { Option } = Select;
@ -24,10 +24,7 @@ function curl(opts, callback) {
const _p = [{ 'key': '0', 'label': '空' }]; const _p = [{ 'key': '0', 'label': '空' }];
return callback(_p); return callback(_p);
} }
const param = { const param = merge({ code: 'utf-8', q: opts.value }, opts.param);
code: 'utf-8',
q: opts.value,
};
// const str = new URLSearchParams({ // const str = new URLSearchParams({
// code: 'utf-8', // code: 'utf-8',
// q: opts.value, // q: opts.value,
@ -66,9 +63,9 @@ class SearchInput extends React.Component {
componentDidMount() { componentDidMount() {
if (this.props.autoGet === true) { 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] } }), {}); 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) : '')) 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 || [] }); this.setState({ data: f || [] });
return false; 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] } }), {}); const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {});
if (value) { 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) : '')) this.setState({ data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : ''))
); );
} else { } else {
@ -102,6 +99,7 @@ class SearchInput extends React.Component {
}; };
handleChange = (value, option) => { handleChange = (value, option) => {
// console.log('invoke denpendencies change', value);
this.setState({ value }, () => this.props.onChange(value, option)); this.setState({ value }, () => this.props.onChange(value, option));
}; };

@ -1,4 +1,4 @@
import { createContext, useContext } from 'react'; import { createContext, useContext, useEffect } from 'react';
import { toJS } from 'mobx'; import { toJS } from 'mobx';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { DATE_FORMAT, stores_Context } from './../../config'; import { DATE_FORMAT, stores_Context } from './../../config';
@ -14,7 +14,7 @@ import DateTypeSelect from './DataTypeSelect';
import DatePickerCharts from './DatePickerCharts'; import DatePickerCharts from './DatePickerCharts';
import YearPickerCharts from './YearPickerCharts'; import YearPickerCharts from './YearPickerCharts';
import SearchInput from './Input'; import SearchInput from './Input';
import { objectMapper, at } from './../../utils/commons'; import { objectMapper, at, empty } from './../../utils/commons';
import './search.css'; import './search.css';
@ -44,8 +44,7 @@ export default observer((props) => {
}; };
const { onSubmit, confirmText } = props; const { onSubmit, confirmText } = props;
const onFinish = (values) => { const formValuesMapper = (values) => {
console.log('Received values of form, origin form value: ', values);
const destinationObject = { const destinationObject = {
'DateType': { 'DateType': {
key: 'DateType', key: 'DateType',
@ -55,28 +54,28 @@ export default observer((props) => {
'HTBusinessUnits': { 'HTBusinessUnits': {
key: 'HTBusinessUnits', key: 'HTBusinessUnits',
transform: (value) => { 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: '', default: '',
}, },
'businessUnits': { 'businessUnits': {
key: 'businessUnits', key: 'businessUnits',
transform: (value) => { 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: '', default: '',
}, },
'DepartmentList': { 'DepartmentList': {
key: 'DepartmentList', key: 'DepartmentList',
transform: (value) => { 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: '', default: '',
}, },
'WebCode': { 'WebCode': {
key: 'WebCode', key: 'WebCode',
transform: (value) => { 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: '', default: '',
}, },
@ -154,6 +153,12 @@ export default observer((props) => {
} }
// omit empty // omit empty
Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]); 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); console.log('form value send to onSubmit:', dest);
const str = new URLSearchParams(dest).toString(); const str = new URLSearchParams(dest).toString();
searchFormStore.setFormValues(values); searchFormStore.setFormValues(values);
@ -170,8 +175,11 @@ export default observer((props) => {
}; };
const onValuesChange = (...args) => { const onValuesChange = (...args) => {
const [changedValues, allValues] = 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.setFormValues(allValues);
searchFormStore.setFormValuesToSub(dest);
}; };
return ( return (
@ -224,28 +232,28 @@ function getFields(props) {
'HTBusinessUnits', 'HTBusinessUnits',
99, 99,
<Form.Item name={`HTBusinessUnits`} initialValue={at(props, 'initialValue.HTBusinessUnits')[0] || undefined}> <Form.Item name={`HTBusinessUnits`} initialValue={at(props, 'initialValue.HTBusinessUnits')[0] || undefined}>
<BusinessUnitSelect {...fieldProps.HTBusinessUnits} /> <BusinessUnitSelect {...fieldProps.HTBusinessUnits} labelInValue={true} />
</Form.Item> </Form.Item>
), ),
item( item(
'businessUnits', 'businessUnits',
99, 99,
<Form.Item name={`businessUnits`} initialValue={at(props, 'initialValue.businessUnits')[0] || undefined}> <Form.Item name={`businessUnits`} initialValue={at(props, 'initialValue.businessUnits')[0] || undefined}>
<BusinessSelect {...fieldProps.businessUnits} /> <BusinessSelect {...fieldProps.businessUnits} labelInValue={true} />
</Form.Item> </Form.Item>
), ),
item( item(
'DepartmentList', 'DepartmentList',
99, 99,
<Form.Item name={`DepartmentList`} initialValue={at(props, 'initialValue.DepartmentList')[0] || (fieldProps?.DepartmentList?.show_all ? { key: 'ALL', label: '所有小组' } : undefined)}> <Form.Item name={`DepartmentList`} initialValue={at(props, 'initialValue.DepartmentList')[0] || (fieldProps?.DepartmentList?.show_all ? { key: 'ALL', label: '所有小组' } : undefined)}>
<GroupSelect {...fieldProps.DepartmentList} /> <GroupSelect {...fieldProps.DepartmentList} labelInValue={true} />
</Form.Item> </Form.Item>
), ),
item( item(
'WebCode', 'WebCode',
99, 99,
<Form.Item name={`WebCode`} initialValue={at(props, 'initialValue.WebCode')[0] || (fieldProps?.WebCode?.show_all ? { key: 'ALL', label: '所有来源' } : undefined)}> <Form.Item name={`WebCode`} initialValue={at(props, 'initialValue.WebCode')[0] || (fieldProps?.WebCode?.show_all ? { key: 'ALL', label: '所有来源' } : undefined)}>
<SiteSelect {...fieldProps.WebCode} /> <SiteSelect {...fieldProps.WebCode} labelInValue={true} />
</Form.Item> </Form.Item>
), ),
item( item(
@ -269,18 +277,10 @@ function getFields(props) {
'DateType', 'DateType',
99, 99,
<Form.Item name={`DateType`} initialValue={at(props, 'initialValue.DateType')[0] || { key: 'applyDate', label: '提交日期' }}> <Form.Item name={`DateType`} initialValue={at(props, 'initialValue.DateType')[0] || { key: 'applyDate', label: '提交日期' }}>
<DateTypeSelect /> <DateTypeSelect labelInValue={true} />
</Form.Item>, </Form.Item>,
2 2
), ),
item(
'dates',
99,
<Form.Item>
<DatePickerCharts isform={true} {...fieldProps.dates} form={form} />
</Form.Item>,
midCol
),
item( item(
'years', 'years',
99, 99,
@ -298,11 +298,26 @@ function getFields(props) {
</Form.Item>, </Form.Item>,
2 2
), ),
item(
'dates',
99,
<Form.Item>
<DatePickerCharts isform={true} {...fieldProps.dates} form={form} />
</Form.Item>,
midCol
),
item( item(
'operator', 'operator',
99, 99,
<Form.Item name={'operator'}> <Form.Item name={'operator'} dependencies={['DepartmentList']} >
<SearchInput autoGet url="/service-Analyse2/GetOperatorInfo" map={{ 'op_id': 'key', 'cn_name': 'label' }} resultkey={'result'} placeholder="输入搜索顾问: 中/英名字" /> <SearchInput
autoGet
url="/service-Analyse2/GetOperatorInfo"
map={{ 'op_id': 'key', 'cn_name': 'label' }}
resultkey={'result'}
placeholder="输入搜索顾问: 中/英名字"
param={{ dept_id: (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', ''), ...(fieldProps?.operator?.param || {}) }}
/>
</Form.Item> </Form.Item>
), ),
item( item(

@ -26,7 +26,7 @@ class SiteSelect extends Component {
} }
store?.handleChange_webcode(value); store?.handleChange_webcode(value);
}} }}
labelInValue={true} labelInValue={false}
maxTagCount={1} maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`} maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={_mode != null} allowClear={_mode != null}

@ -55,7 +55,7 @@ export const groups = [
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] }, { value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
]; ];
export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {}); 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: 'SumML', formatter: (v) => `${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' } },
// { label: '人数', value: 'CJPersonNum', formatter: (v) => v }, // { label: '人数', value: 'CJPersonNum', formatter: (v) => v },
// todo: more... // todo: more...
]; ];
@ -108,7 +108,7 @@ export const dataFieldAlias = dataFieldOptions.reduce(
(a, c) => ({ (a, c) => ({
...a, ...a,
[c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) }, [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 = [ export const KPIObjects = [
{ key: 'overview', value: 'overview', label: '海纳' }, { key: 'overview', value: 'overview', label: '海纳' },
{ key: 'bu', value: 'bu', label: '事业部', data: bu }, { key: 'bu', value: 'bu', label: 'HT事业部', data: bu },
{ key: 'dept', value: 'dept', label: '小组', data: groups }, { key: 'dept', value: 'dept', label: '小组', data: leafGroup },
{ key: 'du', value: 'du', label: '销售小组', data: deptUnits }, { key: 'du', value: 'du', label: '销售小组', data: deptUnits },
{ key: 'operator', value: 'operator', label: '顾问' }, { key: 'operator', value: 'operator', label: '顾问' },
{ key: 'destination', value: 'destination', label: '目的地' }, { key: 'destination', value: 'destination', label: '目的地' },

@ -1,15 +1,13 @@
{ {
"get|/service-web/baseinfo/operator/test": { "get|/service-Analyse2/GetDestinationInfo/test": {
"errcode": 0, "errcode": 0,
"errmsg": "", "errmsg": "",
"data": null, "data": null,
"loading": null, "loading": null,
"result|10": [ "result|10": [
{ {
"mobile": "13@integer(99999999,999999999)", "id": "@integer(10,99)",
"op_id": "@integer(10,99)",
"cn_name": "@cname", "cn_name": "@cname",
"email": "@email",
"en_name": "@first", "en_name": "@first",
"code": "@word(2,3)", "code": "@word(2,3)",
"key": "@increment" "key": "@increment"

@ -10,9 +10,6 @@ class DatePickerStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
formValues = {};
formValuesToSub = {};
start_date = moment().startOf('week').subtract(7, 'days'); start_date = moment().startOf('week').subtract(7, 'days');
end_date = moment().endOf('week').subtract(7, 'days'); end_date = moment().endOf('week').subtract(7, 'days');
start_date_cp = false; 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')]; 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){ setFormValues(data){
this.formValues = data; this.formValues = data;
} }

@ -19,6 +19,14 @@ const modelMapper = {
en_name: { key: 'label_alias' }, 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': { 'vendor': {
url: '/service-web/QueryData/GetVEIName', url: '/service-web/QueryData/GetVEIName',
mapper: { mapper: {
@ -48,7 +56,7 @@ class DictData {
runInAction(() => { runInAction(() => {
this[mkey].loading = false; this[mkey].loading = false;
this[mkey].dataSource = objectMapper(json.result, modelMapper[mkey].mapper); 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]; return this[mkey];
@ -56,6 +64,7 @@ class DictData {
data = {}; data = {};
operator = { loading: false, dataSource: [] }; operator = { loading: false, dataSource: [] };
country = { loading: false, dataSource: [] };
vendor = { loading: false, dataSource: [] }; vendor = { loading: false, dataSource: [] };
creditcardbilltype = { loading: false, dataSource: [] }; creditcardbilltype = { loading: false, dataSource: [] };
} }

@ -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;

@ -13,6 +13,7 @@ import CustomerServicesStore from "./CustomerServices";
import TradeStore from "./Trade"; import TradeStore from "./Trade";
import KPI from "./KPI"; import KPI from "./KPI";
import DictData from "./DictData"; import DictData from "./DictData";
import Distribution from "./Distribution";
class Index { class Index {
constructor() { constructor() {
this.dashboard_store = new DashboardStore(this); this.dashboard_store = new DashboardStore(this);
@ -29,6 +30,7 @@ class Index {
this.TradeStore = new TradeStore(this); this.TradeStore = new TradeStore(this);
this.KPIStore = new KPI(this); this.KPIStore = new KPI(this);
this.DictDataStore = new DictData(this); this.DictDataStore = new DictData(this);
this.DistributionStore = new Distribution(this);
makeAutoObservable(this); makeAutoObservable(this);
} }

@ -22,13 +22,14 @@ class KPI {
} }
async onSubmit(tableData, { delQueue }) { async onSubmit(tableData, { delQueue }) {
this.listLoading = true;
const flushData = tableData.filter(row => !isEmpty(row.value) || !isEmpty(row?.kpi_id)); const flushData = tableData.filter(row => !isEmpty(row.value) || !isEmpty(row?.kpi_id));
const postRes = isEmpty(flushData) ? true : await this.saveOrUpdate(flushData); const postRes = isEmpty(flushData) ? true : await this.saveOrUpdate(flushData);
const delRes = isEmpty(flush(delQueue)) ? true : await this.delByID(delQueue); const delRes = isEmpty(flush(delQueue)) ? true : await this.delByID(delQueue);
return postRes && delRes; return postRes && delRes;
} }
getList(param = {}) { async getList(param = {}) {
const _param = { const _param = {
date_type: 'applyDate', date_type: 'applyDate',
start_date: '2020-01-01', start_date: '2020-01-01',
@ -38,18 +39,17 @@ class KPI {
}; };
this.listLoading = true; this.listLoading = true;
this.pageData = []; this.pageData = [];
return req.fetchJSON('/service-Analyse2/getkpi', _param).then((json) => { const json = await req.fetchJSON('/service-Analyse2/getkpi', _param);
if (json.errcode === 0) { if (json.errcode === 0) {
runInAction(() => { runInAction(() => {
this.listLoading = false; this.listLoading = false;
this.originData = json.result; this.originData = json.result;
const yearData = parseKPI(json.result, ['subject', 'object_id']); const yearData = parseKPI(json.result, ['subject', 'object_id']);
console.log(111, yearData, yearData[this.settingYear]); // console.log(111, yearData, yearData[this.settingYear]);
this.pageData = yearData?.[this.settingYear]?.[this.settingSubject] || []; this.pageData = yearData?.[this.settingYear]?.[this.settingSubject] || [];
}); });
} }
return this.pageData; return this.pageData;
});
} }
settingYear = moment().year(); settingYear = moment().year();

@ -1,4 +1,4 @@
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction, toJS } from "mobx";
import { CaretUpOutlined, CaretDownOutlined } from "@ant-design/icons"; import { CaretUpOutlined, CaretDownOutlined } from "@ant-design/icons";
import { Tag } from "antd"; import { Tag } from "antd";
import * as config from "../config"; import * as config from "../config";
@ -121,16 +121,46 @@ class OrdersStore {
(r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v);
return r; return r;
}, {}); }, {});
this.orderCountData = Object.keys(groupByDate) const _data = Object.keys(groupByDate)
.reduce((r, _d) => { .reduce((r, _d) => {
const summaryVal = groupByDate[_d].reduce((rows, row) => rows + row.orderCount, 0); const xAxisGroup = groupByDate[_d].reduce((a, v) => {
r.push({ ...groupByDate[_d][0], orderCount: summaryVal }); (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; return r;
}, []) }, [])
.map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups })); .map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups }));
this.orderCountData = _data;
this.avgLine1 = avg1; 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) => { parseOrderCount = (orderCountData, dateGroup) => {
resultDataCb(orderCountData, dateGroup, this.orderCountDataMapper, this.orderCountDataFieldMapper, this.onChangeDateGroup); resultDataCb(orderCountData, dateGroup, this.orderCountDataMapper, this.orderCountDataFieldMapper, this.onChangeDateGroup);
}; };
@ -175,9 +205,10 @@ class OrdersStore {
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
runInAction(() => { runInAction(() => {
this.orderCountDataRaw = json; const data = this.summaryAllWebcode(json);
this.orderCountDataRaw = data;
// 第一次得到数据 // 第一次得到数据
this.parseOrderCount(json, 'day'); this.parseOrderCount(data, 'day');
this.loading = false; this.loading = false;
}); });
}) })

@ -15,7 +15,7 @@ class Trade {
fetchSummaryData(queryData) { fetchSummaryData(queryData) {
this.summaryData.loading = true; this.summaryData.loading = true;
queryData.groupType = 'overview'; queryData.groupType = 'overview';
// queryData.groupDateType = 'year'; queryData.groupDateType = 'year';
this.fetchTradeData(queryData).then((json) => { this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) { if (json.errcode === 0) {
runInAction(() => { runInAction(() => {
@ -26,6 +26,7 @@ class Trade {
{ {
title: '成团', title: '成团',
value: summary?.ConfirmOrder, value: summary?.ConfirmOrder,
valueSuffix: summary?.ConfirmRates ? ` / ${summary.ConfirmRates} %` : undefined,
// VSrate: summary?.ConfirmOrderrate, // VSrate: summary?.ConfirmOrderrate,
KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p], KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p],
// hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]), // hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]),

@ -289,7 +289,7 @@ export const sortBy = (key) => {
export function merge(...objects) { export function merge(...objects) {
const isDeep = objects.some(obj => obj !== null && typeof obj === 'object'); 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++) { for (let i = 1; i < objects.length; i++) {
const obj = objects[i]; const obj = objects[i];
@ -301,7 +301,7 @@ export function merge(...objects) {
if (isDeep) { if (isDeep) {
if (Array.isArray(val)) { 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') { } else if (typeof val === 'object') {
result[key] = merge(result[key], val); result[key] = merge(result[key], val);
} else { } else {

@ -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 (
<>
<Row gutter={16} style={{ margin: '-16px -8px' }}>
{/* style={{ margin: '-16px -8px', padding: 0 }} */}
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates', 'country'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
detailRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Spin spinning={detailData.loading}>
<Scatter {...ScatterConfig} dataSource={scatterDays} />
</Spin>
</section>
{/* <section>
<Spin spinning={detailData.loading}>
<Histogram {...HistogramConfig} data={scatterDays} />
</Spin>
</section> */}
</>
);
});

@ -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) => <RingProgress {...RingProgressConfig} percent={v / 100} /> },
{ title: '业绩占比', dataIndex: 'SumMLPercent', render: (v) => <RingProgress {...RingProgressConfig} percent={v / 100} /> },
];
return (
<>
<Row gutter={16} style={{ margin: '-16px -8px' }}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj) => {
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Tabs
onChange={onTabsChange}
type="card"
items={apartOptions.map((ele) => {
return {
...ele,
children: (
<Spin spinning={DistributionStore.pageLoading}>
<Table
id="table_to_xlsx_sale"
dataSource={DistributionStore[curTab].dataSource}
columns={columns}
size="small"
rowKey={(record) => record.label}
loading={DistributionStore[curTab].loading}
pagination={false}
scroll={{ x: '100%' }}
/>
</Spin>
),
};
})}
/>
</section>
</>
);
});

@ -1,12 +1,13 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Row, Col, Spin, Space, Radio } from 'antd'; 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 { stores_Context } from '../config';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import StatisticCard from '../components/StatisticCard'; 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 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';
@ -30,7 +31,7 @@ export default observer(() => {
// const navigate = useNavigate(); // const navigate = useNavigate();
const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context); const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { sideData, summaryData, BuData, topData, timeData, timeLineKey } = TradeStore; const { sideData, summaryData, BuData, topData, timeData, timeLineKey } = TradeStore;
const { formValues, } = searchFormStore; const { formValues } = searchFormStore;
useEffect(() => { useEffect(() => {
if (empty(summaryData.dataSource)) { if (empty(summaryData.dataSource)) {
@ -92,7 +93,7 @@ export default observer(() => {
}, },
}, },
label: { 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', // targetField: 'MLKPIvalue', //
xField: 'groupsLabel', xField: 'groupsLabel',
layout: 'vertical', layout: 'vertical',
xAxis: {
line: null,
label: {
autoHide: true,
autoRotate: false,
},
},
legend: false,
}; };
const lineConfigSet = { const lineConfigSet = {
@ -116,15 +125,11 @@ export default observer(() => {
smooth: true, smooth: true,
point: { point: {
size: 4, size: 4,
shape: "cicle", shape: 'cicle',
}, },
legend: false, legend: false,
meta: { ...cloneDeep(dataFieldAlias) meta: {
// [extProps.yField]: { ...cloneDeep(dataFieldAlias),
// alias: dataFieldAlias[extProps.yField]?.alias || extProps.yField,
// formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v,
// max: Math.ceil(yMax / 0.95),
// },
}, },
}; };
const [timeDataField, setTimeDataField] = useState('SumML'); const [timeDataField, setTimeDataField] = useState('SumML');
@ -132,19 +137,19 @@ export default observer(() => {
const handleChangetimeDataField = (key) => { const handleChangetimeDataField = (key) => {
setTimeDataField(key); setTimeDataField(key);
setLineConfig({ setLineConfig({
...lineConfig, ...cloneDeep(lineConfig),
yField: key, yField: key,
tooltip: { tooltip: {
customItems: (originalItems) => { customItems: (originalItems) => {
// process 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; return items;
}, },
}, },
}); });
}; };
const [dateField, setDateField] = useState(timeLineKey); const [dateField, setDateField] = useState(timeLineKey);
const handleChangeDateType = ({target: {value}}) => { const handleChangeDateType = ({ target: { value } }) => {
setDateField(value); setDateField(value);
TradeStore.setTimeLineKey(value); TradeStore.setTimeLineKey(value);
if (!isEmpty(TradeStore.searchPayloadHome)) { if (!isEmpty(TradeStore.searchPayloadHome)) {
@ -196,7 +201,8 @@ export default observer(() => {
<Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} /> <Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} />
</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} />
</Spin> </Spin>
</section> </section>
<section> <section>
@ -205,7 +211,7 @@ export default observer(() => {
<Row gutter={layoutProps3.gutter}> <Row gutter={layoutProps3.gutter}>
<Col {...layoutProps3}> <Col {...layoutProps3}>
<Bullet {...BUConfig} dataSource={BuData?.dataSource || []} /> <Bullet {...BUConfig} dataSource={BuData?.dataSource || []} />
<h3 style={{ textAlign: 'center' }}>{`各事业部总业绩`}</h3> <h3 style={{ textAlign: 'center' }}>{`各事业部总业绩`}</h3>
</Col> </Col>
{Object.keys(sideData.dataSource).map((key) => ( {Object.keys(sideData.dataSource).map((key) => (
<Col {...layoutProps3} key={key}> <Col {...layoutProps3} key={key}>

@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react';
import { stores_Context } from './../config'; import { stores_Context } from './../config';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Row, Col, Tabs, Spin } from 'antd'; 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 { KPIObjects } from './../libs/ht';
import ObjectPanel from '../components/kpi/ObjectPanel'; import ObjectPanel from '../components/kpi/ObjectPanel';
import OverviewPanel from './../components/kpi/OverviewPanel'; import OverviewPanel from './../components/kpi/OverviewPanel';
@ -23,7 +23,7 @@ const objectFilterKey = {
// 'du': 'du', // 'du': 'du',
// 'operator': 'operator', // 'operator': 'operator',
// 'destination': 'destination', // 'destination': 'destination',
// 'country': 'country', 'country': 'country',
}; };
export default observer((props) => { export default observer((props) => {
const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context); const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
@ -42,13 +42,13 @@ export default observer((props) => {
}, [curObject]); }, [curObject]);
const [retObjects, setRetObjects] = useState([]); const [retObjects, setRetObjects] = useState([]);
const onSearchSubmit = (obj, formVal={}) => { const onSearchSubmit = async (obj, formVal={}) => {
const getkpiParam = objectMapper(obj, { const getkpiParam = objectMapper(obj, {
DateType: { key: 'date_type' }, DateType: { key: 'date_type' },
Date1: { key: 'start_date' }, Date1: { key: 'start_date' },
Date2: { key: 'end_date' }, Date2: { key: 'end_date' },
HTBusinessUnits: { key: 'object_id' }, HTBusinessUnits: { key: 'object_id' },
DepartmentList: { key: 'object_id' }, DepartmentList: { key: curObject === 'dept' ? 'object_id' : 'dept_id' },
businessUnits: { key: 'object_id' }, businessUnits: { key: 'object_id' },
WebCode: { key: 'object_id' }, WebCode: { key: 'object_id' },
operator: { key: 'object_id' }, operator: { key: 'object_id' },
@ -56,9 +56,28 @@ export default observer((props) => {
}); });
Object.assign(getkpiParam, { object: curObject }); Object.assign(getkpiParam, { object: curObject });
KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear); KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear);
console.log('invoke on search', obj, formVal, getkpiParam); // console.log('invoke on search', obj, formVal, getkpiParam);
KPIStore.getList(getkpiParam).then((data) => { if (curObject === 'operator') {
// setDataSource(data); 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]) { if (objectFilterKey?.[curObject]) {
const selectItem = searchFormStore.formValues[objectFilterKey[curObject]]; const selectItem = searchFormStore.formValues[objectFilterKey[curObject]];
if (selectItem) { if (selectItem) {
@ -66,7 +85,7 @@ export default observer((props) => {
} }
setRetObjects(flush([selectItem])); setRetObjects(flush([selectItem]));
} }
}); // });
}; };
return ( return (
<> <>

@ -272,7 +272,7 @@ class Orders extends Component {
tooltip: { tooltip: {
customItems: (originalItems) => { customItems: (originalItems) => {
// process 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) => { title: (title, datum) => {
let ret = title; let ret = title;

@ -53,56 +53,86 @@ const Sale = () => {
// }; // };
const column_config_create = (tab_name) => { const column_config_create = (tab_name) => {
let config_data = { let average_value = 0; //线
//seriesField: "OPI_Name", let config_data = [];
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 `<div>${title}</div>wwwwwww<div></div>`;
// },
// title: (title, datum) => {
// return title; // + " " + comm.getWeek(datum.COLI_Date); //
// },
// },};
};
switch (tab_name) { switch (tab_name) {
case 'All': case 'All':
config_data.data = type_data.dataSource; config_data.data = type_data.dataSource;
config_data.xField = 'OPI_Name'; config_data.xField = 'OPI_Name';
config_data.yField = 'COLI_ML2'; 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; break;
case 'ResponseRateWhatsApp': case 'ResponseRateWhatsApp':
config_data.data = type_data.dataSource; config_data.data = type_data.dataSource;
config_data.xField = 'OPI_Name'; config_data.xField = 'OPI_Name';
config_data.yField = 'COLI_ConfirmTimeAVG'; 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; break;
case 'ResponseRateByWL': case 'ResponseRateByWL':
config_data.data = type_data.dataSource; config_data.data = type_data.dataSource;
config_data.xField = 'OPI_Name'; config_data.xField = 'OPI_Name';
config_data.yField = 'PriceTime'; config_data.yField = 'PriceTime';
average_value = Math.round(config_data.data.reduce((a, b) => a + b.PriceTime, 0) / config_data.data.length);
break; break;
default: default:
config_data.data = []; config_data.data = [];
break; 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 `<div>${title}</div>wwwwwww<div></div>`;
// },
// 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); const column_config = column_config_create(sale_store.active_tab_key);
@ -146,6 +176,65 @@ const Sale = () => {
], ],
}; };
const tab_items = [
{
key: 'All',
label: (
<span>
<ContainerOutlined /> 总览
</span>
),
},
{
key: 'ResponseRateWhatsApp',
label: (
<span>
<UserSwitchOutlined /> 回复率
</span>
),
},
{
key: 'ResponseRateByWL',
label: (
<span>
<UserSwitchOutlined /> 沟通数据
</span>
),
},
{
key: 'Country',
label: (
<span>
<ContainerOutlined /> 国籍
</span>
),
},
{
key: 'Product',
label: (
<span>
<ContainerOutlined /> 产品类型
</span>
),
},
{
key: 'TravelMotivation',
label: (
<span>
<ContainerOutlined /> 出行目的
</span>
),
},
{
key: 'GuestGroupType',
label: (
<span>
<ContainerOutlined /> 成员关系
</span>
),
},
];
return ( return (
<div> <div>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}> <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
@ -240,67 +329,8 @@ const Sale = () => {
sale_store.onChange_Tabs(active_key); sale_store.onChange_Tabs(active_key);
sale_store.get_department_order_ml_by_type(date_picker_store); sale_store.get_department_order_ml_by_type(date_picker_store);
}} }}
> items={tab_items}
<Tabs.TabPane ></Tabs>
tab={
<span>
<ContainerOutlined /> 总览
</span>
}
key="All"
></Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<UserSwitchOutlined /> 回复率
</span>
}
key="ResponseRateWhatsApp"
></Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<UserSwitchOutlined /> 沟通数据
</span>
}
key="ResponseRateByWL"
></Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined /> 国籍
</span>
}
key="Country"
></Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined />
产品类型
</span>
}
key="Product"
></Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined />
出行目的
</span>
}
key="TravelMotivation"
></Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<ContainerOutlined />
成员关系
</span>
}
key="GuestGroupType"
></Tabs.TabPane>
</Tabs>
<Row> <Row>
<Col span={24}> <Col span={24}>
<Table <Table

Loading…
Cancel
Save