Merge branch 'feature/2.0'

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

@ -4,21 +4,17 @@ import { Bullet } from '@ant-design/plots';
import { sortBy, merge } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
// const layoutLabel = {
// 'vertical':
// };
export default observer((props) => {
const { dataSource, itemLength, ...extProps } = props;
// , number -> array
const dataParser = (origin) => {
const { measureField, rangeField, targetField } = extProps;
const maxKPI = Math.max(...(origin || []).map((ele) => ele[targetField]));
const maxKPI = Math.max(...(origin || []).map((ele) => (ele?.[targetField] || 0)));
const maxValue = Math.max(...(origin || []).map((ele) => ele[measureField]));
const _max = Math.max(maxKPI, maxValue);
const sortData = origin.sort(sortBy(measureField)).slice(-itemLength);
//
const _parseData = sortData?.map((ele) => ({ ...ele, [rangeField]: [0, Math.ceil(_max / 0.9)], [measureField]: [ele[measureField]] }));
const _parseData = sortData?.map((ele) => ({ ...ele, [rangeField]: [0, Math.ceil(_max / 0.9)], [measureField]: [ele[measureField]], [targetField]: (ele?.targetField || 0) }));
return _parseData;
};
@ -30,14 +26,15 @@ export default observer((props) => {
const config = merge({
color: {
range: [ '#FFF3E1', '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0',
measure: '#5B8FF9',
range: [ '#FFF3E1', '#FFF3E1'],
// range: [ '#FFF3E1', '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0',
measure: ['#5B8FF9', '#61ddaa'],
target: '#FF9845',
},
label: {
target: false,
measure: {
// position: extProps?.vertical === 'vertical' ? 'top' : 'right',
position: extProps?.layout === 'vertical' ? 'top' : 'right',
// style: {
// fill: '#063CAA',
// },

@ -13,7 +13,7 @@ export default observer((props) => {
const defaultVal = dataFieldOptions[0].value;
const Component = () =>
dataFieldOptions.length < 5 ? (
dataFieldOptions.length < 6 ? (
<Radio.Group options={dataFieldOptions} optionType="button" onChange={(e) => handleChange(e.target.value)} defaultValue={defaultVal} {...extProps} />
) : (
<Select showSearch options={dataFieldOptions} onChange={handleChange} defaultValue={defaultVal} {...extProps} />

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

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

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

@ -31,7 +31,8 @@ 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: '',
initialValue: searchFormStore.formValues,
fieldProps: {},
hides: [],
shows: ['DateType', 'years'],

@ -22,12 +22,17 @@ export default observer((props) => {
const [dataSource, setDataSource] = useState(KPIStore.pageData);
const [editOpen, setEditOpen] = useState(false);
const [editableRowsKeys, setEditableRowKeys] = useState([]);
useEffect(() => {
resetTable();
return () => {};
}, [KPIStore.pageData]);
const resetTable = () => {
setDataSource(KPIStore.pageData);
setEditableRowKeys([]);
setEditOpen(false);
return () => {};
}, [KPIStore.pageData]);
};
const PercentInput = useMemo(
() =>
@ -187,7 +192,6 @@ export default observer((props) => {
); // 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) => ({
@ -202,7 +206,7 @@ export default observer((props) => {
_initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}),
dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {})
)
);
).sort((a, b) => isEmpty(b.kpiDataMapped) ? -1 : 1);
if (e && isEmpty(dataSource)) {
setDataSource(_initialTable);
setEditableRowKeys(_initialTable.map((ele) => ele.key));
@ -212,6 +216,12 @@ export default observer((props) => {
setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []);
};
const [delKpiIds, setDelKpiIds] = useState([]);
const rowHighlight = (record, rowIndex) => {
const all = 'editable-row';
const rowNotSetted = isEmpty(record?.kpiDataMapped) === true;
const warningCls = rowNotSetted ? '' : 'ant-tag-green';
return editOpen ? [all, warningCls].join(' ') : '';
};
return (
<>
<Row gutter={16} className="mb-1 ">
@ -227,6 +237,7 @@ export default observer((props) => {
value={dataSource}
onChange={onTableChange}
recordCreatorProps={false}
rowClassName={rowHighlight}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
@ -252,8 +263,10 @@ export default observer((props) => {
<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);}}>
{!editOpen ? <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
编辑设置
</Button> : <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={resetTable}>
取消
</Button>}
</Space>
</Col>

@ -23,11 +23,15 @@ export default observer((props) => {
const [editOpen, setEditOpen] = useState(false);
const [editableRowsKeys, setEditableRowKeys] = useState([]);
useEffect(() => {
resetTable();
return () => {};
}, [KPIStore.pageData]);
const resetTable = () => {
setDataSource(KPIStore.pageData);
setEditableRowKeys([]);
setEditOpen(false);
return () => {};
}, [KPIStore.pageData]);
};
const PercentInput = useMemo(
() =>
@ -187,24 +191,22 @@ export default observer((props) => {
); // v.formItemProps.initialValue
const makeInitialTable = (e) => {
setEditOpen(e);
// 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_id]: v }), {}),
dataSource.reduce((r, v) => ({ ...r, [v.object_id]: v }), {})
)
);
).sort((a, b) => isEmpty(b.kpiDataMapped) ? -1 : 1);
// console.log(mergePageData, 'iiiii');
if (e && isEmpty(dataSource)) {
setDataSource(_initialTable);
setEditableRowKeys(_initialTable.map((ele) => ele.key));
@ -214,6 +216,12 @@ export default observer((props) => {
setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []);
};
const [delKpiIds, setDelKpiIds] = useState([]);
const rowHighlight = (record, rowIndex) => {
const all = 'editable-row';
const rowNotSetted = isEmpty(record?.kpiDataMapped) === true;
const warningCls = rowNotSetted ? '' : 'ant-tag-green';
return editOpen ? [all, warningCls].join(' ') : '';
};
return (
<>
<Row gutter={16} className="mb-1 ">
@ -229,6 +237,7 @@ export default observer((props) => {
value={dataSource}
onChange={onTableChange}
recordCreatorProps={false}
rowClassName={rowHighlight}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
@ -254,8 +263,10 @@ export default observer((props) => {
<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);}}>
{!editOpen ? <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
编辑设置
</Button> : <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={resetTable}>
取消
</Button>}
</Space>
</Col>

@ -23,12 +23,15 @@ export default observer((props) => {
const [editOpen, setEditOpen] = useState(false);
const [editableRowsKeys, setEditableRowKeys] = useState([]);
useEffect(() => {
// console.log(KPIStore.pageData);
resetTable();
return () => {};
}, [KPIStore.pageData]);
const resetTable = () => {
setDataSource(KPIStore.pageData);
setEditableRowKeys([]);
setEditOpen(false);
return () => {};
}, [KPIStore.pageData]);
};
const PercentInput = useMemo(
() =>
@ -187,7 +190,6 @@ export default observer((props) => {
); // 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) => ({
@ -202,7 +204,7 @@ export default observer((props) => {
_initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}),
dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {})
)
);
).sort((a, b) => isEmpty(b.kpiDataMapped) ? -1 : 1);
if (e && isEmpty(dataSource)) {
setDataSource(_initialTable);
setEditableRowKeys(_initialTable.map((ele) => ele.key));
@ -212,6 +214,12 @@ export default observer((props) => {
setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []);
};
const [delKpiIds, setDelKpiIds] = useState([]);
const rowHighlight = (record, rowIndex) => {
const all = 'editable-row';
const rowNotSetted = isEmpty(record?.kpiDataMapped) === true;
const warningCls = rowNotSetted ? '' : 'ant-tag-green';
return editOpen ? [all, warningCls].join(' ') : '';
};
return (
<>
<Row gutter={16} className="mb-1 ">
@ -227,6 +235,7 @@ export default observer((props) => {
value={dataSource}
onChange={onTableChange}
recordCreatorProps={false}
rowClassName={rowHighlight}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
@ -252,8 +261,10 @@ export default observer((props) => {
<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);}}>
{!editOpen ? <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
编辑设置
</Button> : <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={resetTable}>
取消
</Button>}
</Space>
</Col>

@ -63,7 +63,8 @@ class SearchInput extends React.Component {
componentDidMount() {
if (this.props.autoGet === true) {
const { map, resultkey, param } = this.props;
const { map, resultkey, dependenciesFun } = this.props;
const param = typeof dependenciesFun === 'function' ? dependenciesFun() : {};
const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {});
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) : ''))
@ -87,7 +88,8 @@ class SearchInput extends React.Component {
this.setState({ data: f || [] });
return false;
}
const { map, resultkey, param } = this.props;
const { map, resultkey, dependenciesFun } = this.props;
const param = typeof dependenciesFun === 'function' ? dependenciesFun() : {};
const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {});
if (value) {
curl({ value, url: this.props.url || '', map: mapKey, resultkey, param }, (data) =>
@ -99,13 +101,12 @@ class SearchInput extends React.Component {
};
handleChange = (value, option) => {
// console.log('invoke denpendencies change', value);
this.setState({ value }, () => this.props.onChange(value, option));
};
render() {
const options = this.state.data.map(d => <Option key={d.key} extradata={d.options}>{d.label}</Option>);
const { onSearchAfter, defaultOptions, autoGet, ...props } = this.props;
const { onSearchAfter, defaultOptions, autoGet, dependenciesFun, ...props } = this.props;
return (
<Select
{...props}

@ -1,7 +1,7 @@
import { createContext, useContext, useEffect } from 'react';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { DATE_FORMAT, stores_Context } from './../../config';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT, stores_Context } from './../../config';
import { SearchOutlined } from '@ant-design/icons';
import { Form, Row, Col, Select, Button, Space, DatePicker } from 'antd';
import moment from 'moment';
@ -44,6 +44,7 @@ export default observer((props) => {
};
const { onSubmit, confirmText } = props;
const formValuesMapper = (values) => {
const destinationObject = {
'DateType': {
@ -97,7 +98,7 @@ export default observer((props) => {
},
{
key: 'Date2',
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : moment(value[1]).format(`${DATE_FORMAT} 23:59:59`)),
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : moment(value[1]).format(SMALL_DATETIME_FORMAT)),
default: '',
},
],
@ -109,7 +110,7 @@ export default observer((props) => {
},
{
key: 'DateDiff2',
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : value[1] ? moment(value[1]).format(`${DATE_FORMAT} 23:59:59`) : undefined),
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : value[1] ? moment(value[1]).format(SMALL_DATETIME_FORMAT) : undefined),
default: '',
},
],
@ -121,7 +122,7 @@ export default observer((props) => {
},
{
key: 'Date2',
transform: (value) => (value ? moment(value).format(`YYYY-12-31 23:59:59`) : undefined),
transform: (value) => (value ? moment(value).format(`YYYY-12-31 23:59:00`) : undefined),
default: '',
},
],
@ -133,7 +134,7 @@ export default observer((props) => {
},
{
key: 'DateDiff2',
transform: (value) => (value ? moment(value).format(`YYYY-12-31 23:59:59`) : undefined),
transform: (value) => (value ? moment(value).format(`YYYY-12-31 23:59:00`) : undefined),
default: '',
},
],
@ -149,7 +150,7 @@ export default observer((props) => {
},
};
let dest = {};
const { applyDate, applyDate2, year, yearDiff, ...omittedValue } = values;
const { applyDate, applyDate2, year, yearDiff, dates, months, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
@ -161,6 +162,12 @@ export default observer((props) => {
return dest;
};
useEffect(() => {
const dest = formValuesMapper(searchFormStore.formValues);
searchFormStore.setFormValuesToSub(dest);
return () => {};
}, []);
const onFinish = (values) => {
console.log('Received values of form, origin form value: ', values);
const dest = formValuesMapper(values);
@ -191,7 +198,7 @@ export default observer((props) => {
// layout="inline"
<Form form={form} name="advanced_search" className="orders-search-form" onFinish={onFinish} onValuesChange={onValuesChange}>
<EditableContext.Provider value={form}>
<Row gutter={10} style={{ background: '#f9fafa', margin: '0px 0px 10px 0px', padding: '16px 8px' }}>
<Row gutter={10} style={{ background: '#f9fafa', margin: '0px 0px 10px 0px', padding: '16px 8px', boxShadow: '0px 0px 3px 0px rgba(0,0,0,0.15)' }}>
{getFields({ sort, initialValue, hides, shows, fieldProps, form })}
{/* 'textAlign': 'right' */}
<Col flex="1 0 120px" style={{ padding: '0px 5px' }}>
@ -321,7 +328,7 @@ function getFields(props) {
map={{ 'op_id': 'key', 'cn_name': 'label' }}
resultkey={'result'}
placeholder="输入搜索顾问: 中/英名字"
param={{ dept_id: (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', ''), ...(fieldProps?.operator?.param || {}) }}
dependenciesFun={() => ({ dept_id: (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', ''), ...(fieldProps?.operator?.param || {}) })}
/>
</Form.Item>
),

@ -2,4 +2,6 @@ import React from "react";
export const stores_Context = React.createContext();
export const DATE_FORMAT = "YYYY-MM-DD";
export const SMALL_DATETIME_FORMAT = 'YYYY-MM-DD 23:59:00';
export const DATETIME_FORMAT = 'YYYY-MM-DD 23:59:59';
export const HT_HOST = process.env.NODE_ENV === "production" ? "https://p9axztuwd7x8a7.mycht.cn" : "http://202.103.68.100:890";

@ -1,3 +1,5 @@
import { fixTo4Decimals } from "../utils/commons";
/**
* 事业部
*/
@ -95,7 +97,8 @@ export const dateTypes = [
* 结果字段
*/
export const dataFieldOptions = [
{ label: '毛利', value: 'SumML', formatter: (v) => `${v / 10000}`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } },
{ label: '营收', value: 'transactions', formatter: (v) => `${fixTo4Decimals(v / 10000)}`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } },
{ label: '毛利', value: 'SumML', formatter: (v) => `${fixTo4Decimals(v / 10000)}`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } },
{ label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } },
{ label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } },
{ label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } },
@ -156,9 +159,9 @@ export const KPISubjects = [
{ key: 'sum_profit', value: 'sum_profit', label: '毛利' },
{ key: 'in_order_count', value: 'in_order_count', label: '订单数' },
{ key: 'confirm_order_count', value: 'confirm_order_count', label: '成团' },
{ key: 'depart_order_count', value: 'depart_order_count', label: '走团' },
{ key: 'confirm_rates', value: 'confirm_rates', label: '成率' },
{ key: 'praise_rates', value: 'praise_rates', label: '表扬率' },
// { key: 'depart_order_count', value: 'depart_order_count', label: '走团' }, // 根据日期类型
{ key: 'confirm_rates', value: 'confirm_rates', label: '成率' },
// { key: 'praise_rates', value: 'praise_rates', label: '表扬率' },
// { key: 'first_reply_rates', value: 'first_reply_rates', label: '首报回复率'},
// { key: 'quote_rates', value: 'quote_rates', label: '报价率'},
// { key: 'first_post_time', value: 'first_post_time', label: '订单到首邮发送时间'},
@ -166,5 +169,5 @@ export const KPISubjects = [
// { key: 'reply_rates_wa', value: 'reply_rates_wa', label: 'WA回复率'},
// { key: 'reply_eff_wechat', value: 'reply_eff_wechat', label: '微信回复效率'},
// { key: 'reply_eff_wa', value: 'reply_eff_wa', label: 'WA回复效率'},
{ key: 'sum_person_num', value: 'sum_person_num', label: '人数' },
// { key: 'sum_person_num', value: 'sum_person_num', label: '人数' },
];

@ -46,7 +46,7 @@ class DatePickerStore {
'WebCode': { 'key': 'ALL', 'label': '所有来源' },
'IncludeTickets': { 'key': '1', 'label': '含门票' },
'DateType': { 'key': 'applyDate', 'label': '提交日期' },
'years': this.start_date,
'year': this.start_date,
// 'months': [moment(), moment()],
'dates': [this.start_date, this.end_date],
};

@ -5,14 +5,14 @@ import moment from 'moment';
import { isEmpty, pick, sortBy, fixTo2Decimals, cloneDeep, unique } 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' },
'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry' },
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays', keySort: true },
'PML': { url: '/service-Analyse2/GetTradeApartByPML', keySort: true },
'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays', keySort: true },
'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays', keySort: true },
'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum', keySort: true },
'destination': { url: '/service-Analyse2/GetTradeApartByDestination', keySort: false },
'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination', keySort: false },
'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry', keySort: false },
};
class Distribution {
constructor(appStore) {
@ -41,12 +41,13 @@ class Distribution {
param.DateToQ2 = DateToQ2.format(`${DATE_FORMAT} 23:59:59`);
const json = await req.fetchJSON(modelMapper[mkey].url, param);
if (json.errcode === 0) {
const dataLength = json.result.length;
const pickResult = dataLength > 20 ? json.result.slice(0, 30) : json.result;
const dataSource = calcDiff({ result: pickResult, resultToY: json.resultToY, resultToQ: json.resultToQ }, modelMapper[mkey].keySort);
runInAction(() => {
const dataLength = json.result.length;
this[mkey].loading = false;
this[mkey].originData = json.result;
const pickResult = dataLength > 20 ? json.result.slice(0, 30) : json.result;
this[mkey].dataSource = calcDiff({ result: pickResult, resultToY: json.resultToY, resultToQ: json.resultToQ });
this[mkey].dataSource = dataSource;
this.pageLoading = false;
});
}
@ -89,7 +90,18 @@ class Distribution {
this.curTab = v;
}
setFormDates(param) {
// 环比的参数: 计算上一个时间段
const [DateToQ1, DateToQ2] = [moment(param.Date1).subtract(moment(param.Date2).diff(param.Date1, 'days') + 1, 'days'), moment(param.Date1).subtract(1, 'days')];
// 同比的参数: 去年同期
const [DateToY1, DateToY2] = [moment(param.Date1).subtract(1, 'year'), moment(param.Date2).subtract(1, 'year')];
this.dateStringY = `${DateToY1.format(DATE_FORMAT)}~${DateToY2.format(DATE_FORMAT)}`;
this.dateStringQ = `${DateToQ1.format(DATE_FORMAT)}~${DateToQ2.format(DATE_FORMAT)}`;
}
pageLoading = false;
dateStringY = '';
dateStringQ = '';
detailData = { loading: false, dataSource: [] };
scatterDays = [];
@ -107,15 +119,14 @@ class Distribution {
/**
* 计算 同比, 环比
*/
const calcDiff = ({ result, resultToY, resultToQ }) => {
if (isEmpty(resultToY) || isEmpty(resultToQ)) {
// return result;
}
const initialDataWithAllKeys = unique([].concat(result, resultToY, resultToQ).map((ele) => `${ele.key}@${ele.label}`)).reduce((r, v) => {
const [key, label] = String(v).split('@');
r.push({key: Number(key), label, SumML: 0, ConfirmOrder: 0, SumOrder: 0, SumMLPercent: 0, ConfirmOrderPercent: 0, SumOrderPercent: 0});
return r;
}, []).sort(sortBy('key'));
const calcDiff = ({ result, resultToY, resultToQ }, keySort) => {
const initialDataWithAllKeys = unique([].concat(result, resultToY, resultToQ).map((ele) => `${ele.key}@${ele.label}`))
.reduce((r, v) => {
const [key, label] = String(v).split('@');
r.push({ key: Number(key), label, SumML: 0, ConfirmOrder: 0, SumOrder: 0, SumMLPercent: 0, ConfirmOrderPercent: 0, SumOrderPercent: 0 });
return r;
}, [])
.sort(keySort ? sortBy('key') : undefined);
const initialMapped = initialDataWithAllKeys.reduce((r, v) => ({ ...r, [v.key]: v }), {});
const resultMapped = result.reduce((r, v) => ({ ...r, [v.key]: v }), cloneDeep(initialMapped));
const resultToYMapped = resultToY.reduce((r, v) => ({ ...r, [v.key]: v }), cloneDeep(initialMapped));
@ -140,7 +151,7 @@ const calcDiff = ({ result, resultToY, resultToQ }) => {
ConfirmOrderDiffY: resultMapped[row.key].ConfirmOrder - resultToYMapped[row.key].ConfirmOrder,
ConfirmOrderDiffQ: resultMapped[row.key].ConfirmOrder - resultToQMapped[row.key].ConfirmOrder,
};
return { ...resultMapped[row.key], ...diff };
return { ...resultMapped[row.key], ...diff, resultToY: resultToYMapped[row.key], resultToQ: resultToQMapped[row.key] };
});
// console.log(afterCalc, '==================');
return afterCalc;

@ -32,7 +32,7 @@ class Trade {
// hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]),
hasKPI: false
},
{ title: '毛利', value: summary?.SumML, KPIrate: summary?.[dataFieldAlias.SumML.nestkey.p], hasKPI: false },
{ title: '毛利', value: dataFieldAlias.SumML.formatter(summary?.SumML || 0), KPIrate: summary?.[dataFieldAlias.SumML.nestkey.p], hasKPI: false },
{ title: '完成率', value: `${summary?.[dataFieldAlias.SumML.nestkey.p] || ''}%`, hasKPI: false },
{
title: '人数',
@ -54,17 +54,16 @@ class Trade {
/**
* 时间轴
*/
fetchTradeDataByDate(queryData) {
fetchTradeDataByDate(queryData = {}) {
this.timeData.loading = true;
queryData = queryData || this.searchPayloadHome;
queryData = Object.assign({}, this.searchPayloadHome, queryData); // queryData || this.searchPayloadHome;
queryData.groupType = queryData?.groupType || 'overview';
Object.assign(queryData, { groupDateType: this.timeLineKey });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
const data = json.result1.sort(sortBy('groupDateVal'));
runInAction(() => {
const data = json.result1;
// 标注KPI
this.timeData.loading = false;
this.timeData.dataSource = data;
});
@ -80,8 +79,8 @@ class Trade {
Object.assign(queryData, { groupType: 'bu', groupDateType: 'year' });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
const data = json.result1;
runInAction(() => {
const data = json.result1;
// 标注KPI
this.BuData.loading = false;
@ -117,17 +116,24 @@ class Trade {
Object.assign(queryData, { groupType: 'bizarea', groupDateType: 'month' });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
const sortResult = json.result1.sort(sortBy('groupDateVal'));
const groupsData = sortResult.reduce((r, v) => {
if (v.groupsLabel ) { // && ['91001', '91006'].includes(v.groupsKey)
(r[v.groupsLabel] || (r[v.groupsLabel] = [])).push(v);
}
return r;
}, {});
const summaryData = Object.keys(groupsData).map(groupsKey => {
return ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r, [skey]: groupsData[groupsKey].reduce((a, c) => a + c[skey], 0) }),
groupsData[groupsKey]?.[0] || {}
);
});
runInAction(() => {
const sortResult = json.result1.sort(sortBy('groupDateVal'));
const groupsData = sortResult.reduce((r, v) => {
if (v.groupsLabel ) { // && ['91001', '91006'].includes(v.groupsKey)
(r[v.groupsLabel] || (r[v.groupsLabel] = [])).push(v);
}
return r;
}, {});
this.sideData.loading = false;
this.sideData.dataSource = groupsData;
this.sideData.monthData = sortResult;
this.sideData.yearData = summaryData;
});
}
});
@ -194,12 +200,21 @@ class Trade {
this.timeLineKey = v;
}
resetData = () => {
this.summaryData = { loading: false, dataSource: [], kpi: {}, };
this.timeData = { loading: false, dataSource: [] };
this.BuData = { loading: false, dataSource: [] };
this.sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
this.topData = {};
this.targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} };
this.targetTableProps.dataSource = [];
};
searchPayloadHome = {};
summaryData = { loading: false, dataSource: [], kpi: {}, };
timeData = { loading: false, dataSource: [] };
BuData = { loading: false, dataSource: [] };
sideData = { loading: false, dataSource: {}, monthData: [] };
dataForSort = {};
sideData = { loading: false, dataSource: {}, monthData: [], yearData: [] };
topData = {};
targetData = { targetTotal: {}, targetCountry: {}, targetGuest: {} };
targetTableProps = { loading: false, columns: [

@ -26,7 +26,7 @@ const apartOptions = [
export default observer(() => {
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { curTab } = DistributionStore;
const { curTab, dateStringQ, dateStringY } = DistributionStore;
const pageRefresh = (obj) => {
DistributionStore.getApartData({
@ -41,6 +41,7 @@ export default observer(() => {
}, [curTab]);
useEffect(() => {
DistributionStore.setFormDates(formValuesToSub);
DistributionStore.resetData();
return () => {};
}, [formValuesToSub]);
@ -54,6 +55,21 @@ export default observer(() => {
autoFit: false,
color: ['#5B8FF9', '#E8EDF3'],
};
const RingProgressConfigY = {
height: 50,
width: 50,
autoFit: false,
// color: ['#f6bd16', '#E8EDF3'],
color: ['#61ddaa', '#E8EDF3'], // #7cb305
innerRadius: 0.90,
};
const RingProgressConfigQ = {
height: 50,
width: 50,
autoFit: false,
color: ['#f6bd16', '#E8EDF3'],
innerRadius: 0.90,
};
const columns = [
{ title: '', dataIndex: 'label' },
{
@ -62,7 +78,7 @@ export default observer(() => {
render: (v, r) => (
<>
<Row align={'middle'}>
<Col flex="1 1 100px">
<Col flex={"100px"}>
<Text strong>{v}</Text>
</Col>
<Col flex={'auto'}>
@ -85,7 +101,7 @@ export default observer(() => {
render: (v, r) => (
<>
<Row align={'middle'}>
<Col flex="1 1 100px">
<Col flex={"150px"}>
<Text strong>{dataFieldAlias.SumML.formatter(v)}</Text>
</Col>
<Col flex={'auto'}>
@ -105,12 +121,48 @@ export default observer(() => {
{
title: '团数占比',
dataIndex: 'ConfirmOrderPercent',
render: (v, r) => <RingProgress {...RingProgressConfig} percent={v / 100} />,
render: (v, r) => v ? <RingProgress {...RingProgressConfig} percent={v / 100} /> : '-',
},
{
title: '业绩占比',
dataIndex: 'SumMLPercent',
render: (v, r) => <RingProgress {...RingProgressConfig} percent={v / 100} />,
render: (v, r) => v ? <RingProgress {...RingProgressConfig} percent={v / 100} /> : '-',
},
{
title: () => <><div>去年同期</div><div>{dateStringY}</div></>,
align: 'center',
children: [
{
title: '团数占比',
width: 90,
dataIndex: 'ConfirmOrderPercent',
render: (v, r) => r.resultToY.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.ConfirmOrderPercent / 100} /> : '-',
},
{
title: '业绩占比',
width: 90,
dataIndex: 'SumMLPercent',
render: (v, r) => r.resultToY.SumMLPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.SumMLPercent / 100} /> : '-',
},
],
},
{
title: () => <><div>上个时间段</div><div>{dateStringQ}</div></>,
align: 'center',
children: [
{
title: '团数占比',
width: 90,
dataIndex: 'ConfirmOrderPercent',
render: (v, r) => r.resultToQ.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.ConfirmOrderPercent / 100} /> : '-',
},
{
title: '业绩占比',
width: 90,
dataIndex: 'SumMLPercent',
render: (v, r) => r.resultToQ.SumMLPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.SumMLPercent / 100} /> : '-',
},
],
},
];
return (

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

@ -69,7 +69,7 @@ export default observer((props) => {
|| !isEmpty(searchFormStore.formValues?.operator)) {
// && isEmpty(getkpiParam.object_id)
setRetObjects(searchOperator.dataSource);
// getkpiParam.object_id = searchOperator.dataSource.map((ele) => ele.key).join(','); // todo:
getkpiParam.object_id = searchOperator.dataSource.map((ele) => ele.key).join(',');
delete getkpiParam.dept_id;
}
}

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

Loading…
Cancel
Save