From 836d4b9597dc1a6ae2654e6f7dec1ebeedff70a2 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Sat, 9 Sep 2023 02:00:15 +0800 Subject: [PATCH 01/19] =?UTF-8?q?todo:=20KPI=20=E8=AE=BE=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 5 +- src/mock/2.0/trade.json | 33 ++++++- src/stores/Index.js | 3 +- src/stores/KPI.js | 135 ++++++++++++++++++++++++++ src/utils/commons.js | 126 ++++++++++++++++++++++++ src/utils/request.js | 3 +- src/views/KPI.jsx | 206 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 505 insertions(+), 6 deletions(-) create mode 100644 src/stores/KPI.js create mode 100644 src/views/KPI.jsx diff --git a/src/App.jsx b/src/App.jsx index 9689a1b..28f12ec 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,7 +10,7 @@ import { DollarOutlined, AreaChartOutlined, WechatOutlined, - UserOutlined, + UserOutlined, FlagOutlined, } from '@ant-design/icons'; import { Layout, Menu, Image, Spin } from 'antd'; import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom'; @@ -35,6 +35,7 @@ import Logo from './logo.png'; import { stores_Context } from './config'; import { observer } from 'mobx-react'; import ExchangeRate from './charts/ExchangeRate'; +import KPI from './views/KPI'; const App = () => { const { Content, Footer, Sider } = Layout; @@ -119,6 +120,7 @@ const App = () => { }, ], }, + { key: 'kpi', label: 目标, icon: }, ]; return ( @@ -159,6 +161,7 @@ const App = () => { > } /> + } /> }> } /> } /> diff --git a/src/mock/2.0/trade.json b/src/mock/2.0/trade.json index 12d533c..0a885ac 100644 --- a/src/mock/2.0/trade.json +++ b/src/mock/2.0/trade.json @@ -104,19 +104,40 @@ {} ] }, - "get|/service-web/QueryData/Getkpi": { + "get|/service-web/QueryData/GetTradeSection": { "errcode": 0, "errmsg": "", "loading": false, "data": null, - "result1|10": [ + "result1|24": [ + { + "groups|1": "@pick([\"1\",\"2_5\"])", + "groupDateVal": "@date('2023-MM')", + "key": "@increment", + "SumML": "@integer(99,9999)", + "SumOrder": "@integer(99,9999)", + "SumPersonNum": "@integer(99,9999)", + "ConfirmOrder": "@integer(99,9999)", + "ConfirmRates": "@float(20,100,0,2)", + "AvgML": "@integer(99,9999)" + }], + "result2": [ + {} + ] + }, + "get|/service-Analyse2/getkpi/test": { + "errcode": 0, + "errmsg": "", + "loading": false, + "data": null, + "result|10": [ { "object|1": "@pick([\"dept\",\"sales\", \"group\"])", "subject|1": "@pick([\"OrderCount\",\"SumML\", \"AvgML\", \"SuccessRate\"])", "object_id": "@integer(10,100)", "object_name": "@cname", "date_type": "@pick([\"ConfirmDate\",\"startDate\", \"applyDate\"])", - "start_date": "@date(\"yyyy-MM-dd\")", + "start_date": "@date(\"2023-MM-dd\")", "end_date": "@datetime(\"yyyy-MM-dd HH:mm:ss\")", "value": "@integer(99,9999)", "key": "@increment" @@ -124,5 +145,11 @@ "result2": [ {} ] + }, + "post|/service-Analyse2/setkpi_multi/test": { + "errcode": 0, + "errmsg": "", + "loading": false, + "data": null } } diff --git a/src/stores/Index.js b/src/stores/Index.js index d7948a2..45aed26 100644 --- a/src/stores/Index.js +++ b/src/stores/Index.js @@ -11,7 +11,7 @@ import WechatStore from "./Wechat"; import WhatsAppStore from "./WhatsApp"; import CustomerServicesStore from "./CustomerServices"; import TradeStore from "./Trade"; - +import KPI from "./KPI"; class Index { constructor() { this.dashboard_store = new DashboardStore(this); @@ -26,6 +26,7 @@ class Index { this.whatsAppStore = new WhatsAppStore(this); this.customerServicesStore = new CustomerServicesStore(this); this.TradeStore = new TradeStore(this); + this.KPIStore = new KPI(this); makeAutoObservable(this); } diff --git a/src/stores/KPI.js b/src/stores/KPI.js new file mode 100644 index 0000000..a1e6296 --- /dev/null +++ b/src/stores/KPI.js @@ -0,0 +1,135 @@ +import { makeAutoObservable, runInAction, toJS } from 'mobx'; +import * as req from '../utils/request'; +import { isEmpty, sortBy, groupBy, cloneDeep } from '../utils/commons'; +import moment from 'moment'; + +const currentYear = moment().year(); + +class KPI { + constructor(appStore) { + this.appStore = appStore; + makeAutoObservable(this); + } + + saveOrUpdate(x) { + console.log('ssssssssssss', x); + console.log(toJS(this.pageData)); + const tableData = this.pageData.reduce((r, curObj) => { + const allMonth = new Array(12).fill(1).map((_, index) => { + const mIndex = index+1; + const startM = moment([this.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:ss'), + value: curObj[`M${mIndex}`], + // ...(curObj[`M${mIndex}_id`] ? { kpi_id: curObj[`M${mIndex}_id`] } : {}), + kpi_id: curObj[`M${mIndex}_id`] || undefined, + key: undefined, + }; + }); + console.log('cccccccccc', allMonth); + return r.concat(allMonth); + }, []); + console.log('ppppp', tableData); + const data = { 'kpis': tableData }; + req.postJSON('/service-Analyse2/setkpi_multi/test', data).then((json) => { + // req.postJSON('/service-Analyse2/setkpi_multi', data).then((json) => { + if (json.errcode === 0) { + runInAction(() => { + console.log({ loading: false, ...json }, 'post kpi'); + }); + } + }); + } + + getList() { + const param = { + date_type: 'applyDate', + start_date: '2020-01-01', + end_date: '2024-12-31 23:59:59', + }; + // return req.fetchJSON('/service-Analyse2/getkpi/test', param).then((json) => { + return req.fetchJSON('/service-Analyse2/getkpi', param).then((json) => { + if (json.errcode === 0) { + runInAction(() => { + this.originData = json.result; + const yearData = parseKPI(json.result, ['subject', 'object_id']); + console.log(111, yearData, yearData[this.settingYear]); + this.pageData = yearData?.[this.settingYear]?.[this.settingSubject] || []; + }); + } + return this.pageData; + }); + } + + handleTableEdit(data) { + console.log('handle change ', data); + // this.pageData = data; + } + + settingYear = 2023; + settingSubject = 'sum_profit'; + + originData =[]; + pageData = []; +} + +/** + * 把kpi数据按照对象和类型转换格式 + * @param {array} keyArr 转换的字段的顺序, 返回结果的key层级 + * @example + * * parseKPI(json.result, ['object_id', 'subject']); + * // => { 2023: { 114: [{...}, {...}], 216: [...] } } + * * parseKPI(json.result, ['subject', 'object_id']); + * // => { 2023: { in_order_count: [{...}, {...}], sum_profit: [...] } } + */ +export const parseKPI = (kpis, keyArr = []) => { + const result = kpis.map((row) => ({ + ...row, + yearIndex: moment(row.start_date).year(), + monthIndex: moment(row.start_date).month() + 1, + monthRange: [moment(row.start_date).month() + 1, moment(row.end_date).month() + 1], + fullYear: moment(row.start_date).month() === 0 && moment(row.end_date).month() === 11, + })); + const byYear = groupBy(result, (ele) => ele.yearIndex); + const yearData = {}; + const initialPercentKey = new Array(12).fill(1).reduce((r, v, i) => ({ ...r, [`M${i+1}Percent`]: 0 }), {}); + const ret = {}; + const [key0, key1] = keyArr; + Object.keys(byYear).map((_yearVal) => { + const _subjectRet = groupBy(byYear[_yearVal], (ele) => `${ele[key0]}`); + Object.keys(_subjectRet).map((_subject) => { + const subjectObject = groupBy(_subjectRet[_subject], (row) => row[key1]); + const afterGroup = Object.keys(subjectObject).map((oID) => { + const _ByFull = subjectObject[oID].reduce((r, v) => { + (r[String(v.fullYear)] || (r[String(v.fullYear)] = [])).push(v); + return r; + }, {}); + const kpiYear = (_ByFull?.true || []).reduce((r, v) => { + // r.push({ monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id }); + return { monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id }; + }, {}); + const kpiData = _ByFull.false.reduce((r, v) => { + r.push({ monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id, percentVal: (v.value/kpiYear.value*100) }); + return r; + }, []); + const kpiDataMapped = kpiData.reduce((r, v) => ({...r, [`M${v.monthIndex}`]: v }), {}); + const kpiDataFlat = kpiData.reduce((r, v) => ({...r, [`M${v.monthIndex}Val`]: v.value, [`M${v.monthIndex}Percent`]: v.percentVal}), {}); + const { start_date, end_date, kpi_id, value, unit, monthIndex, monthRange, ...objectEle } = subjectObject[oID][0]; + return { ...cloneDeep(initialPercentKey), ...objectEle, ...kpiDataFlat, kpiData, kpiDataMapped, kpiYear, yearValue: kpiYear?.value || 0 }; + }); + ret[_subject] = afterGroup; + return 1; + }); + Object.assign(yearData, { [_yearVal]: ret }); + return _yearVal; + }); + return yearData; +}; +export default KPI; diff --git a/src/utils/commons.js b/src/utils/commons.js index e1c856d..8b5087f 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -315,3 +315,129 @@ export function merge(...objects) { return result; } + +/** + * 数组分组 + * - 相当于 lodash 的 _.groupBy + * @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity + */ +export function groupBy(array, callback) { + return array.reduce((groups, item) => { + const key = callback(item); + + if (!groups[key]) { + groups[key] = []; + } + + groups[key].push(item); + return groups; + }, {}); +} + +/** + * 创建一个从 object 中选中的属性的对象。 + * @param {*} object + * @param {array} keys + */ +export function pick(object, keys) { + return keys.reduce((obj, key) => { + if (object && Object.prototype.hasOwnProperty.call(object, key)) { + obj[key] = object[key]; + } + return obj; + }, {}); +} + +/** + * 深拷贝 + */ +export function cloneDeep(value) { + if (typeof value !== 'object' || value === null) { + return value; + } + + const result = Array.isArray(value) ? [] : {}; + + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + result[key] = cloneDeep(value[key]); + } + } + + return result; +} + +/** + * 向零四舍五入, 固定精度设置 + */ +function curriedFix(precision = 0) { + return function(number) { + // Shift number by precision places + const shift = Math.pow(10, precision); + const shiftedNumber = number * shift; + + // Round to nearest integer + const roundedNumber = Math.round(shiftedNumber); + + // Shift back decimal place + return roundedNumber / shift; + }; +} +/** + * 向零四舍五入, 保留2位小数 + */ +export const fixTo2Decimals = curriedFix(2); +/** + * 向零四舍五入, 保留4位小数 + */ +export const fixTo4Decimals = curriedFix(4); + +/** + * 映射 + * @example + * const keyMap = { + a: [{key: 'a1'}, {key: 'a2', transform: v => v * 2}], + b: {key: 'b1'} + }; + const result = objectMapper({a: 1, b: 3}, keyMap); + // result = {a1: 1, a2: 2, b1: 3} + * + */ +export function objectMapper(input, keyMap) { + // Loop through array mapping + if (Array.isArray(input)) { + return input.map((obj) => objectMapper(obj, keyMap)); + } + + if (typeof input === 'object') { + const mappedObj = {}; + + Object.keys(input).forEach((key) => { + // Keep original keys not in keyMap + if (!keyMap[key]) { + mappedObj[key] = input[key]; + } + // Handle array of maps + if (Array.isArray(keyMap[key])) { + keyMap[key].forEach((map) => { + let value = input[key]; + if (map.transform) value = map.transform(value); + mappedObj[map.key] = value; + }); + + // Handle single map + } else { + const map = keyMap[key]; + if (map) { + let value = input[key]; + if (map.transform) value = map.transform(value); + mappedObj[map.key || key] = value; + } + } + }); + + return mappedObj; + } + + return input; +} diff --git a/src/utils/request.js b/src/utils/request.js index 663f389..e9289eb 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -51,7 +51,8 @@ export function postForm(url, data) { } export function postJSON(url, obj) { - return fetch(url, { + const host = /^https?:\/\//i.test(url) ? '': HT_HOST; + return fetch(`${host}${url}`, { method: 'POST', body: JSON.stringify(obj), headers: { diff --git a/src/views/KPI.jsx b/src/views/KPI.jsx new file mode 100644 index 0000000..eda8640 --- /dev/null +++ b/src/views/KPI.jsx @@ -0,0 +1,206 @@ +import { useContext, useEffect, useState, useRef } from 'react'; +import { observer, useLocalStore } from 'mobx-react'; +import { toJS } from 'mobx'; +// import type { ProColumns } from '@ant-design/pro-components'; +import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components'; +import { Button, Table, Switch, Input, Space, Typography } from 'antd'; +import { stores_Context } from '../config'; +import { isEmpty, fixTo4Decimals, cloneDeep } from './../utils/commons'; + +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 }), {}); + +export default observer((props) => { + const [dataSource, setDataSource] = useState([]); + const { KPIStore, DictDataStore } = useContext(stores_Context); + const { settingYear, } = KPIStore; + const { operator } = DictDataStore; + useEffect(() => { + // KPIStore.saveOrUpdate(); + KPIStore.getList().then((data) => { + setDataSource(data); + }); + DictDataStore.fetchDictData('operator'); + return () => {}; + }, []); + const [editOpen, setEditOpen] = useState(false); // test: + const [editableRowsKeys, setEditableRowKeys] = useState([]); + // console.log(toJS(KPIStore.pageData ), dataSource, '00000'); + const PercentInput = ({ value, onChange, record, ...extProps }) => { + // console.log(extProps, '22222222'); + const initialPercent = record.kpiDataMapped?.[`M${extProps.month}`]?.percentVal; + const [inputVal, setInputVal] = useState(value); + const calcV = inputVal ? fixTo4Decimals((Number(record?.yearValue) * inputVal) / 100) : 0; + + const handleInputChange = ({ target: { value } }) => { + setInputVal(value); + }; + const handleInputConfirm = () => { + // onChange?.(inputVal); + // setInputVal(''); + }; + return ( + + + {/* onBlur={handleInputConfirm} onPressEnter={handleInputConfirm} */} + {/* 1 */} + {calcV} + + ); + }; + const RenderInput = (row, mon) => { + // console.log(toJS(row), mon); + return ( + +
{row.kpiDataMapped?.[`M${mon}`]?.percentVal}%
+
{row.kpiDataMapped?.[`M${mon}`]?.value}
+
+ ); + }; + const monthCol = new Array(12).fill(1).map((_, index) => { + return { + title: `${index + 1}月`, + dataIndex: `M${index + 1}Percent`, + valueType: 'percent', + width: '6.5em', + fieldProps: { min: 0, max: 100, style: { width: '4em' } }, + renderFormItem: ({ dataIndex, ...item }, { record, isEditable ,...e }, form) => { + return ; + }, + render: (_, row) => RenderInput(row, index + 1), + }; + }); + const initialRow = monthCol.reduce((r, v) => ({ ...r, [v.dataIndex]: 0 }), {}); // v.formItemProps.initialValue + const columns = [ + { + title: '对象', + dataIndex: 'object_id', + valueType: 'select', + // ...valueEnum + // fieldProps: { labelInValue: true }, + render: (_, r) => r.object_name + }, + // { + // title: 'Name', + // dataIndex: 'title', + // //...form rules + // formItemProps: { + // rules: [ + // { + // required: true, + // whitespace: true, + // message: '此项是必填项', + // }, + // ], + // }, + // }, + { + title: '年度目标', + dataIndex: 'yearValue', + valueType: 'digit', + fieldProps: { style: { width: '100%' } }, + formItemProps: { + style: { width: '100%' }, + }, + }, + ...monthCol, + // { + // title: '完成进度', + // dataIndex: 'place', + // valueType: 'percent', + // editable: false, + // width: '6em', + // }, + { + title: '操作', + valueType: 'option', + // width: 250, + render: () => { + return null; + }, + }, + ]; + const onTableChange = (...argrs) => { + console.log(argrs[0], 'who who who'); + setEditableRowKeys(argrs[0].map((ele) => ele.key)); + // KPIStore.setEditableRowsKeys(argrs[0].map((ele) => ele.key)); + setDataSource(argrs[0]); + // KPIStore.handleTableEdit(argrs[0]); + }; + return ( + <> + ({ + key: (Date.now().toString(32)), // dataSource.length + 1, // Number(Date.now().toString()), + ...initialRow, + object_name: '', + value: 0, + yearValue: 0, + ...cloneDeep(initialPercentKey), + }), + } + : false + } + toolBarRender={() => { + return [ + { + setEditOpen(e); + setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); + // KPIStore.setEditableRowsKeys(e ? dataSource.map((ele) => ele.key) : []); + }} + />, + , + ]; + }} + editable={{ + type: 'multiple', + editableKeys: editableRowsKeys, + actionRender: (row, config, defaultDoms) => { + // console.log(row, config, defaultDoms); + return [defaultDoms.delete]; + }, + onValuesChange: (record, recordList) => { + console.log('on edit, onValuesChange'); + onTableChange(recordList); + }, + onChange: (editableKeys, editableRows) => { + console.log('editable onValuesChange'); + onTableChange(editableRows); + // KPIStore.setEditableRowsKeys() + }, + }} + /> + {(operator?.dataSource || []).map(ele =>
{ele.label} {ele.mobile}
)} + + ); +}); From 08aecb6fdb4b084b85765d59af6ccbaab6348141 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 15 Sep 2023 16:24:29 +0800 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20filter=20form=20=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Data.jsx | 9 +- src/components/DateGroupRadio/date.js | 2 + src/components/search/BusinessSelect.jsx | 15 +- src/components/search/DataTypeSelect.jsx | 24 +- src/components/search/DatePickerCharts.jsx | 20 +- src/components/search/GroupSelect.jsx | 12 +- src/components/search/SearchForm.jsx | 324 +++++++++++++++++++++ src/components/search/SiteSelect.jsx | 14 +- src/components/search/YearPickerCharts.jsx | 70 +++++ src/components/search/search.css | 4 + src/mock/2.0/1.json | 27 -- src/mock/2.0/baseinfo.json | 19 ++ src/stores/DictData.js | 54 ++++ src/stores/Index.js | 2 + src/stores/Trade.js | 5 + src/utils/commons.js | 57 ++++ src/views/Home.jsx | 42 ++- 17 files changed, 639 insertions(+), 61 deletions(-) create mode 100644 src/components/search/SearchForm.jsx create mode 100644 src/components/search/YearPickerCharts.jsx create mode 100644 src/components/search/search.css delete mode 100644 src/mock/2.0/1.json create mode 100644 src/mock/2.0/baseinfo.json create mode 100644 src/stores/DictData.js diff --git a/src/components/Data.jsx b/src/components/Data.jsx index f907dd2..d503f07 100644 --- a/src/components/Data.jsx +++ b/src/components/Data.jsx @@ -1,10 +1,13 @@ import { Tag } from 'antd'; import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons'; -export const VSTag = (diffPercent, diffData, data1, data2) => { +export const VSTag = (props) => { + const { diffPercent, diffData, data1, data2 } = props; const CaretIcon = parseInt(diffPercent) < 0 ? CaretDownOutlined : CaretUpOutlined; - const tagColor = parseInt(diffPercent) < 0 ? "gold" : "lime"; - return parseInt(diffPercent) === 0 ? "-" : ( + const tagColor = parseInt(diffPercent) < 0 ? 'gold' : 'lime'; + return parseInt(diffPercent) === 0 ? ( + '-' + ) : (
{data1} vs {data2} diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index 904f5d5..4e367f9 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -119,6 +119,7 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, const parseData2 = parse2.data.map((ele) => ({ [fieldMapper.dateKey]: ele.groupKey, [fieldMapper.valueKey]: ele.value, + [fieldMapper.seriesKey]: ele[fieldMapper.seriesKey], groups: _data2[0].groups, dateKey: ele.dateKey, dateRange: ele.dateRange, @@ -128,5 +129,6 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, const reindecData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `X.${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey })); const retData = [].concat(parseData1, reindecData2); const avg1 = parse1.avgVal; + // console.log('callback', dateGroup, retData); cb(dateGroup, retData, avg1); }; diff --git a/src/components/search/BusinessSelect.jsx b/src/components/search/BusinessSelect.jsx index b97fcb3..5cd1f25 100644 --- a/src/components/search/BusinessSelect.jsx +++ b/src/components/search/BusinessSelect.jsx @@ -5,16 +5,23 @@ import { biz } from '../../libs/ht'; const Business_unit = (props) => { - const {store} = props; + const { store, mode, value, onChange, show_all, ...extProps } = props; return (
store.onChange_datetype(value)} + // onChange={(value) => store.onChange_datetype(value)} + onChange={this.handleChange} > {dateTypes.map((ele) => ( @@ -24,7 +39,6 @@ class DataTypeSelect extends Component { ))} -
); } } diff --git a/src/components/search/DatePickerCharts.jsx b/src/components/search/DatePickerCharts.jsx index 3712ff8..bb73445 100644 --- a/src/components/search/DatePickerCharts.jsx +++ b/src/components/search/DatePickerCharts.jsx @@ -1,5 +1,5 @@ import React, { Component } from "react"; -import { Col, DatePicker, Row } from "antd"; +import { Col, DatePicker, Row, Form } from "antd"; import { observer } from "mobx-react"; import * as config from "../../config"; import moment from "moment"; @@ -21,12 +21,13 @@ class DatePickerCharts extends Component {
+ { date_picker_store.onChange_dataPicker(e); if (typeof this.props.onChange === 'function') { @@ -45,20 +46,27 @@ class DatePickerCharts extends Component { 今年: [moment().startOf("year"), moment().endOf("year")], 去年: [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")], }} - /> + /> {this.props.hide_vs ? ( "" ) : ( + { + if (typeof this.props.onChange === 'function') { + this.props.onChange(value); + } + date_picker_store?.onChange_dataPicker_cp(value); + }} ranges={{ 上一时间段: date_picker_store.previous_date(), 去年同期: date_picker_store.previous_year(), @@ -68,7 +76,7 @@ class DatePickerCharts extends Component { 前三个月: [moment().subtract(5, "month").startOf("month"), moment().subtract(3, "month").endOf("month")], 去年: [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")], }} - /> + /> )} diff --git a/src/components/search/GroupSelect.jsx b/src/components/search/GroupSelect.jsx index 95a0e6e..0dc2fc6 100644 --- a/src/components/search/GroupSelect.jsx +++ b/src/components/search/GroupSelect.jsx @@ -10,22 +10,28 @@ class GroupSelect extends Component { render() { const { store, mode, value, onChange, show_all, ...extProps } = this.props; + const _mode = mode || store?.group_select_mode || null; + const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all; return (
+ + + + , + 2 + ), + // + item( + 'DateType', + 99, + + + , + 2 + ), + // item( + // 'applyDate', + // 99, + // + // + // , + // midCol + // ), + // item( + // 'applyDate2', + // 99, + // + // + // + // + // + // + // + // , + // 12 + // ), + item( + 'dates', + 99, + + + , + midCol + ), + item( + 'year', + 99, + + {/* */} + + ,2 + ), + ]; + baseChildren = baseChildren + .map((x) => { + x.hide = false; + if (props.sort === undefined) { + return x; + } + const tmpSort = props.sort; + for (const key in tmpSort) { + if (Object.prototype.hasOwnProperty.call(tmpSort, key)) { + if (x.name === key) { + x.sort = tmpSort[key]; + } + } + } + return x; + }) + .map((x) => { + if (props.hides.length === 0 && props.shows.length === 0) { + return x; + } + if (props.hides.length === 0) { + x.hide = !props.shows.includes(x.name); + } else if (props.shows.length === 0) { + x.hide = props.hides.includes(x.name); + } + return x; + }) + .filter((x) => !x.hide) + .sort((a, b) => { + return a.sort < b.sort ? -1 : 1; + }); + const children = []; + const leftStyle = {}; // { borderRight: '1px solid #dedede' }; + for (let i = 0; i < baseChildren.length; i++) { + let style = { padding: '0px 2px' }; + style = i % 2 === 0 && baseChildren[i].col === 12 ? { ...style, ...leftStyle } : style; + style = !baseChildren[i].hide ? { ...style, display: 'block' } : { ...style, display: 'none' }; + const Item = ( + + {baseChildren[i].render} + + ); + children.push(Item); + } + return children; +} diff --git a/src/components/search/SiteSelect.jsx b/src/components/search/SiteSelect.jsx index abb138b..3d22f93 100644 --- a/src/components/search/SiteSelect.jsx +++ b/src/components/search/SiteSelect.jsx @@ -11,22 +11,28 @@ class SiteSelect extends Component { render() { const { store, mode, value, onChange, show_all, ...extProps } = this.props; - return ( + const _mode = mode || store?.group_select_mode || null; + const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all; + return (
diff --git a/src/components/search/YearPickerCharts.jsx b/src/components/search/YearPickerCharts.jsx new file mode 100644 index 0000000..1a5a6f7 --- /dev/null +++ b/src/components/search/YearPickerCharts.jsx @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; +import { Col, DatePicker, Row, Form } from 'antd'; +import { observer } from 'mobx-react'; +import * as config from '../../config'; +import moment from 'moment'; +import 'moment/locale/zh-cn'; +import locale from 'antd/es/date-picker/locale/zh_CN'; +import { stores_Context } from '../../config'; + +// 用于日期选择,计算上一时间段、同比时间等 +class DatePickerCharts extends Component { + static contextType = stores_Context; + + constructor(props) { + super(props); + } + + render() { + const { date_picker_store } = this.context; + return ( +
+ + + + { + const fullYear = [e.clone().set('month', 0).set('date', 1), e.clone().set('month', 11).set('date', 31)]; + date_picker_store.onChange_dataPicker(fullYear); + if (typeof this.props.onChange === 'function') { + this.props.onChange(fullYear); + } + }} + /> + + + {this.props.hide_vs ? ( + '' + ) : ( + + + { + const fullYear = [value.clone().set('month', 0).set('date', 1), value.clone().set('month', 11).set('date', 31)]; + if (typeof this.props.onChange === 'function') { + this.props.onChange(fullYear); + } + date_picker_store?.onChange_dataPicker_cp(fullYear); + }} + /> + + + )} + +
+ ); + } +} + +export default observer(DatePickerCharts); diff --git a/src/components/search/search.css b/src/components/search/search.css new file mode 100644 index 0000000..9f8f546 --- /dev/null +++ b/src/components/search/search.css @@ -0,0 +1,4 @@ +.ant-form-item{ + margin: 0; + margin-bottom: 8px; +} diff --git a/src/mock/2.0/1.json b/src/mock/2.0/1.json deleted file mode 100644 index 9b25a55..0000000 --- a/src/mock/2.0/1.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "get|/service-web/QueryData/GetOrderCount/test": { - "errcode": "", - "errmsg": "", - "data": null, - "ordercount1|10": [ - { - "id|10-99": 1, - "key|10-99": 1, - "orderCount|1-100": 5, - "WebCode|1": ["ah", "cht"], - "groups": "groups1", - "ApplyDate": "@date(\"2023-08-dd\")" - } - ], - "ordercount2|10": [ - { - "id|100-200": 1, - "key|100-200": 1, - "orderCount|1-100": 5, - "WebCode|1": ["ah", "cht"], - "groups": "groups2", - "ApplyDate": "@date(\"2023-08-dd\")" - } - ] - } -} diff --git a/src/mock/2.0/baseinfo.json b/src/mock/2.0/baseinfo.json new file mode 100644 index 0000000..e02e285 --- /dev/null +++ b/src/mock/2.0/baseinfo.json @@ -0,0 +1,19 @@ +{ + "get|/service-web/baseinfo/operator/test": { + "errcode": 0, + "errmsg": "", + "data": null, + "loading": null, + "result|10": [ + { + "mobile": "13@integer(99999999,999999999)", + "op_id": "@integer(10,99)", + "cn_name": "@cname", + "email": "@email", + "en_name": "@first", + "code": "@word(2,3)", + "key": "@increment" + } + ] + } +} diff --git a/src/stores/DictData.js b/src/stores/DictData.js new file mode 100644 index 0000000..a9306f2 --- /dev/null +++ b/src/stores/DictData.js @@ -0,0 +1,54 @@ +import { makeAutoObservable, runInAction, toJS } from 'mobx'; +import * as req from '../utils/request'; +import { isEmpty, sortBy, objectMapper } from '../utils/commons'; + +const modelMapper = { + 'operator': { + url: '/service-web/baseinfo/operator/test', + mapper: { + op_id: [{ key: 'key' }, { key: 'value' }], + cn_name: { key: 'label' }, + en_name: { key: 'label_alias' }, + }, + }, + 'vendor': { + url: '/service-web/QueryData/GetVEIName', + mapper: { + CAV_VEI_SN: [{ key: 'key' }, { key: 'value' }], + VEI2_CompanyBN: { key: 'label' }, + }, + }, + 'creditcardbilltype': { + url: '/service-web/QueryData/GetCreditCardBillType', + mapper: { + cb_billtype: [{ key: 'key' }, { key: 'value' }, { key: 'label' }], + }, + }, +}; + +class DictData { + constructor(appStore) { + this.appStore = appStore; + makeAutoObservable(this); + } + + async fetchDictData(model = '') { + const mkey = model.toLowerCase(); + this[mkey] = { loading: true, dataSource: [] }; + const json = await req.fetchJSON(modelMapper[mkey].url); + if (json.errcode === 0) { + runInAction(() => { + this[mkey].loading = false; + this[mkey].dataSource = objectMapper(json.result, modelMapper[mkey].mapper); + console.log({ loading: false, ...json }, model, 'DictData', toJS(this[mkey])); + }); + } + return this[mkey]; + } + + data = {}; + operator = { loading: false, dataSource: [] }; + vendor = { loading: false, dataSource: [] }; + creditcardbilltype = { loading: false, dataSource: [] }; +} +export default DictData; diff --git a/src/stores/Index.js b/src/stores/Index.js index 45aed26..0d38106 100644 --- a/src/stores/Index.js +++ b/src/stores/Index.js @@ -12,6 +12,7 @@ import WhatsAppStore from "./WhatsApp"; import CustomerServicesStore from "./CustomerServices"; import TradeStore from "./Trade"; import KPI from "./KPI"; +import DictData from "./DictData"; class Index { constructor() { this.dashboard_store = new DashboardStore(this); @@ -27,6 +28,7 @@ class Index { this.customerServicesStore = new CustomerServicesStore(this); this.TradeStore = new TradeStore(this); this.KPIStore = new KPI(this); + this.DictDataStore = new DictData(this); makeAutoObservable(this); } diff --git a/src/stores/Trade.js b/src/stores/Trade.js index 113d473..cfe3aed 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -76,10 +76,15 @@ class Trade { }); } + setStateSearch(body) { + this.searchPayloadHome = body; + } + summaryData = { loading: false, dataSource: [] }; sideData = { loading: false, dataSource: {}, kpi: {}, monthData: [] }; topData = {}; defaultDataSubject = 'CJCount'; + searchPayloadHome = {}; } export default Trade; diff --git a/src/utils/commons.js b/src/utils/commons.js index 8b5087f..5050b07 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -441,3 +441,60 @@ export function objectMapper(input, keyMap) { return input; } + +/** + * 创建一个对应于对象路径的值数组 + */ +export function at(obj, path) { + let result; + if (Array.isArray(obj)) { + // array case + const indexes = path.split('.').map((i) => parseInt(i)); + result = []; + for (let i = 0; i < indexes.length; i++) { + result.push(obj[indexes[i]]); + } + } else { + // object case + const indexes = path.split('.').map((i) => i); + result = [obj]; + for (let i = 0; i < indexes.length; i++) { + result = [result[0][indexes[i]]]; + } + } + return result; +} +/** + * 删除 null/undefined + */ +export function flush(collection) { + let result, len, i; + if (!collection) { + return undefined; + } + if (Array.isArray(collection)) { + result = []; + len = collection.length; + for (i = 0; i < len; i++) { + const elem = collection[i]; + if (elem != null) { + result.push(elem); + } + } + return result; + } + if (typeof collection === 'object') { + result = {}; + const keys = Object.keys(collection); + len = keys.length; + for (i = 0; i < len; i++) { + const key = keys[i]; + const value = collection[key]; + if (value != null) { + result[key] = value; + } + } + return result; + } + return undefined; +} diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 75b7d6b..7649fc0 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -9,7 +9,7 @@ import Bullet from '../components/Bullet'; import Waterfall from '../components/Waterfall'; import Column from '../components/Column'; import DataFieldRadio from './../components/DateFieldRadio'; -import DatePickerCharts from './../components/search/DatePickerCharts'; +import SearchForm from './../components/search/SearchForm'; import { empty } from './../utils/commons'; import './home.css'; @@ -17,7 +17,7 @@ import './home.css'; export default observer(() => { const navigate = useNavigate(); const { TradeStore } = useContext(stores_Context); - const { sideData, summaryData, topData } = TradeStore; + const { searchPayloadHome, sideData, summaryData, topData } = TradeStore; const topSeries = [ { key: 'Country', label: '国籍' }, @@ -28,15 +28,19 @@ export default observer(() => { useEffect(() => { if (empty(summaryData.dataSource)) { - TradeStore.fetchSummaryData(); - TradeStore.fetchTradeDataByMonth(); - for (const iterator of topSeries) { - TradeStore.fetchTradeDataByType(iterator.key); - } + // pageRefresh(); } return () => {}; }, []); + const pageRefresh = (queryData) => { + TradeStore.fetchSummaryData(); + TradeStore.fetchTradeDataByMonth(queryData); + for (const iterator of topSeries) { + TradeStore.fetchTradeDataByType(iterator.key, queryData); + } + }; + const layoutProps = { gutter: { xs: 8, sm: 8, lg: 16 }, lg: { span: 6 }, @@ -106,10 +110,30 @@ export default observer(() => { return ( <> + + + { + TradeStore.setStateSearch(form); + pageRefresh(obj); + }} + /> + +

年度业绩

-
@@ -146,7 +170,7 @@ export default observer(() => { {topSeries.map((item) => ( - +

{item.label}

From 85d78d1cb66ae21a6a09187a31619e469393e9df Mon Sep 17 00:00:00 2001 From: Lei OT Date: Mon, 18 Sep 2023 13:56:27 +0800 Subject: [PATCH 03/19] =?UTF-8?q?todo:=20KPI=20=E8=AE=BE=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=20=20=E5=88=86=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=90=84=E7=A7=8D=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/SearchForm.jsx | 9 ++++- src/libs/ht.js | 13 ++++++ src/views/KPI.jsx | 59 +++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index 2a0447a..74d634e 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -268,13 +268,20 @@ function getFields(props) { midCol ), item( - 'year', + 'years', 99, {/* */} ,2 ), + item( + 'months', + 99, + + + ,2 + ), ]; baseChildren = baseChildren .map((x) => { diff --git a/src/libs/ht.js b/src/libs/ht.js index dd6d8a5..ed93759 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -88,3 +88,16 @@ export const dataFieldAlias = dataFieldOptions.reduce( }), {} ); + +/** + * KPI对象 + */ +export const KPIObjects = [ + { value: 'overview', label: '总额' }, + { value: 'bu', label: '事业部' }, + { value: 'dept', label: '小组' }, + { value: 'du', label: '顾问小组' }, + { value: 'operator', label: '顾问' }, + { value: 'destination', label: '目的地' }, + { value: 'country', label: '国籍' }, +]; diff --git a/src/views/KPI.jsx b/src/views/KPI.jsx index eda8640..267bb9e 100644 --- a/src/views/KPI.jsx +++ b/src/views/KPI.jsx @@ -3,17 +3,19 @@ import { observer, useLocalStore } from 'mobx-react'; import { toJS } from 'mobx'; // import type { ProColumns } from '@ant-design/pro-components'; import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components'; -import { Button, Table, Switch, Input, Space, Typography } from 'antd'; +import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio } from 'antd'; import { stores_Context } from '../config'; import { isEmpty, fixTo4Decimals, cloneDeep } from './../utils/commons'; +import SearchForm from './../components/search/SearchForm'; +import { KPIObjects } from './../libs/ht'; -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 { Text } = Typography; +const initialPercentKey = new Array(12).fill(1).reduce((r, v, i) => ({ ...r, [`M${i + 1}Percent`]: [8, 9].includes(i) ? 10 : 8 }), {}); export default observer((props) => { const [dataSource, setDataSource] = useState([]); const { KPIStore, DictDataStore } = useContext(stores_Context); - const { settingYear, } = KPIStore; + const { settingYear } = KPIStore; const { operator } = DictDataStore; useEffect(() => { // KPIStore.saveOrUpdate(); @@ -41,7 +43,7 @@ export default observer((props) => { }; return ( - + {/* onBlur={handleInputConfirm} onPressEnter={handleInputConfirm} */} {/* 1 */} {calcV} @@ -52,7 +54,10 @@ export default observer((props) => { // console.log(toJS(row), mon); return ( -
{row.kpiDataMapped?.[`M${mon}`]?.percentVal}%
+
+ {row.kpiDataMapped?.[`M${mon}`]?.percentVal} + % +
{row.kpiDataMapped?.[`M${mon}`]?.value}
); @@ -64,7 +69,7 @@ export default observer((props) => { valueType: 'percent', width: '6.5em', fieldProps: { min: 0, max: 100, style: { width: '4em' } }, - renderFormItem: ({ dataIndex, ...item }, { record, isEditable ,...e }, form) => { + renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => { return ; }, render: (_, row) => RenderInput(row, index + 1), @@ -78,7 +83,7 @@ export default observer((props) => { valueType: 'select', // ...valueEnum // fieldProps: { labelInValue: true }, - render: (_, r) => r.object_name + render: (_, r) => r.object_name, }, // { // title: 'Name', @@ -127,8 +132,38 @@ export default observer((props) => { setDataSource(argrs[0]); // KPIStore.handleTableEdit(argrs[0]); }; + const handleRadioChange = (val) => { + console.log(val, 'sss'); + }; return ( <> + + + handleRadioChange(e.target.value)} /> + + + + + { + // TradeStore.setStateSearch(form); + // pageRefresh(obj); + }} + /> + + { ? { newRecordType: 'dataSource', record: () => ({ - key: (Date.now().toString(32)), // dataSource.length + 1, // Number(Date.now().toString()), + key: Date.now().toString(32), // dataSource.length + 1, // Number(Date.now().toString()), ...initialRow, object_name: '', value: 0, @@ -200,7 +235,11 @@ export default observer((props) => { }, }} /> - {(operator?.dataSource || []).map(ele =>
{ele.label} {ele.mobile}
)} + {(operator?.dataSource || []).map((ele) => ( +
+ {ele.label} {ele.mobile} +
+ ))} ); }); From 44647bee3b4a9cca51dbbf720bd61249dedd9637 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Mon, 18 Sep 2023 14:22:09 +0800 Subject: [PATCH 04/19] =?UTF-8?q?=E9=A6=96=E9=A1=B5=20=E5=B9=B4=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mock/2.0/trade.json | 2 +- src/stores/Trade.js | 34 +++++++++++++++------------------- src/views/Home.jsx | 9 +++++---- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/mock/2.0/trade.json b/src/mock/2.0/trade.json index 0a885ac..69ceb97 100644 --- a/src/mock/2.0/trade.json +++ b/src/mock/2.0/trade.json @@ -79,7 +79,7 @@ {} ] }, - "get|/service-web/QueryData/GetTradeProcess": { + "get|/service-Analyse2/GetTradeProcess/test": { "errcode": 0, "errmsg": "", "loading": false, diff --git a/src/stores/Trade.js b/src/stores/Trade.js index cfe3aed..52b1343 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -1,23 +1,18 @@ -import { makeAutoObservable, runInAction } from 'mobx'; +import { makeAutoObservable, runInAction, toJS } from 'mobx'; import * as req from '../utils/request'; import { isEmpty, sortBy } from '../utils/commons'; -/** - * 计算变化值 - */ -const calcRate = (r1, r2) => { - // -}; - class Trade { constructor(rootStore) { this.rootStore = rootStore; makeAutoObservable(this); } - fetchSummaryData() { + fetchSummaryData(queryData) { this.summaryData.loading = true; - req.fetchJSON('/service-web/QueryData/GetTradeSummary').then((json) => { + queryData.groupType = 'overview'; + queryData.groupDateType = 'year'; + req.fetchJSON('/service-Analyse2/GetTradeProcess/test', queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { const summaryData = { @@ -25,7 +20,7 @@ class Trade { dataSource: [ { title: '成团', value: json.result1?.SumOrder, VSrate: json.result1?.SumOrderrate, KPIrate: json.result1?.SumOrderKPIrate, hasKPI: !isEmpty(json.result1?.SumOrderKPIrate) }, { title: '毛利', value: json.result1?.SumML, VSrate: json.result1?.SumMLrate, KPIrate: json.result1?.SumMLKPIrate, hasKPI: !isEmpty(json.result1?.SumMLKPIrate) }, - { title: '完成率', value: `${json.result1?.SumMLKPIrate}%`, hasKPI: false }, + { title: '完成率', value: `${json.result1?.SumMLKPIrate || ''}%`, hasKPI: false }, { title: '人数', value: json.result1?.SumPersonNum, @@ -41,10 +36,10 @@ class Trade { }); } - fetchTradeDataByMonth() { + fetchTradeDataByMonth(queryData) { this.sideData.loading = true; - req.fetchJSON('/service-web/QueryData/GetTradeProcess').then((json) => { - // req.fetchJSON('/service-web/QueryData/GetTradeByMonth').then((json) => { + Object.assign(queryData, { groupType: 'bu', groupDateType: 'month' }); + req.fetchJSON('/service-Analyse2/GetTradeProcess/test', queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { const sortResult = json.result1.sort(sortBy('groupDateVal')); @@ -52,8 +47,8 @@ class Trade { (r[v.groups] || (r[v.groups] = [])).push(v); return r; }, {}); - console.log(groupsData, 'groupsData'); - const kpi = { label: '', value: 1200000 }; // 标注KPI + console.log(groupsData, 'groupsData', queryData); + const kpi = { label: '', value: 1200000 }; // todo: 标注KPI this.sideData.loading = false; this.sideData.dataSource = groupsData; this.sideData.monthData = sortResult; @@ -63,9 +58,10 @@ class Trade { }); } - fetchTradeDataByType(orderType) { + fetchTradeDataByType(orderType, queryData) { this.topData[orderType] = { loading: true, dataSource: [] }; - req.fetchJSON('/service-web/QueryData/GetTradeOrderByType').then((json) => { + Object.assign(queryData, { groupType: 'orderType', groupDateType: 'year' }); + req.fetchJSON('/service-web/QueryData/GetTradeOrderByType', queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { this.topData[orderType].loading = false; @@ -81,7 +77,7 @@ class Trade { } summaryData = { loading: false, dataSource: [] }; - sideData = { loading: false, dataSource: {}, kpi: {}, monthData: [] }; + sideData = { loading: false, dataSource: {}, kpi: {}, monthData: [] }; topData = {}; defaultDataSubject = 'CJCount'; searchPayloadHome = {}; diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 7649fc0..0071d86 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -34,7 +34,7 @@ export default observer(() => { }, []); const pageRefresh = (queryData) => { - TradeStore.fetchSummaryData(); + TradeStore.fetchSummaryData(queryData); TradeStore.fetchTradeDataByMonth(queryData); for (const iterator of topSeries) { TradeStore.fetchTradeDataByType(iterator.key, queryData); @@ -110,14 +110,15 @@ export default observer(() => { return ( <> - - + + {/* style={{ margin: '-16px -8px', padding: 0 }} */} + Date: Mon, 18 Sep 2023 17:04:41 +0800 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20=E6=90=9C=E7=B4=A2=E7=BB=84?= =?UTF-8?q?=E4=BB=B6:=20=E5=9F=BA=E7=A1=80=E4=BF=A1=E6=81=AF=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/DatePickerCharts.jsx | 26 +++- src/components/search/GroupSelect.jsx | 2 +- src/components/search/Input.jsx | 133 +++++++++++++++++++++ src/components/search/SearchForm.jsx | 51 ++++++-- src/libs/ht.js | 53 ++++---- 5 files changed, 225 insertions(+), 40 deletions(-) create mode 100644 src/components/search/Input.jsx diff --git a/src/components/search/DatePickerCharts.jsx b/src/components/search/DatePickerCharts.jsx index bb73445..d6dc4f0 100644 --- a/src/components/search/DatePickerCharts.jsx +++ b/src/components/search/DatePickerCharts.jsx @@ -7,6 +7,16 @@ import "moment/locale/zh-cn"; import locale from "antd/es/date-picker/locale/zh_CN"; import { stores_Context } from "../../config"; +// 1.0的搜索组件 不需要Form包裹 +const SectionWrapper = ({ isform, id, title, children, right, ...extProps }) => + !isform ? ( + <>{children} + ) : ( + + {children} + + ); + // 用于日期选择,计算上一时间段、同比时间等 class DatePickerCharts extends Component { static contextType = stores_Context; @@ -17,17 +27,20 @@ class DatePickerCharts extends Component { render() { const { date_picker_store } = this.context; + const { isform } = this.props; + const defaultV = [date_picker_store.start_date, date_picker_store.end_date]; + const defaultVdiff = [date_picker_store.start_date_cp, date_picker_store.end_date_cp]; return (
- + { date_picker_store.onChange_dataPicker(e); if (typeof this.props.onChange === 'function') { @@ -46,19 +59,20 @@ class DatePickerCharts extends Component { 今年: [moment().startOf("year"), moment().endOf("year")], 去年: [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")], }} - /> + /> + {this.props.hide_vs ? ( "" ) : ( - + { @@ -76,7 +90,7 @@ class DatePickerCharts extends Component { 前三个月: [moment().subtract(5, "month").startOf("month"), moment().subtract(3, "month").endOf("month")], 去年: [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")], }} - /> + /> )} diff --git a/src/components/search/GroupSelect.jsx b/src/components/search/GroupSelect.jsx index 0dc2fc6..24daa00 100644 --- a/src/components/search/GroupSelect.jsx +++ b/src/components/search/GroupSelect.jsx @@ -32,7 +32,7 @@ class GroupSelect extends Component { {...extProps} > {_show_all ? ( - + 所有小组 ) : ( diff --git a/src/components/search/Input.jsx b/src/components/search/Input.jsx new file mode 100644 index 0000000..2317d5c --- /dev/null +++ b/src/components/search/Input.jsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { Select } from 'antd'; +import querystring from 'querystring'; +// import * as oMapper from 'object-mapper'; +import { fetchJSON } from './../../utils/request'; +import { observer } from 'mobx-react'; +import { objectMapper } from './../../utils/commons'; + +const { Option } = Select; + +let timeout; +let currentValue; + +function curl(opts, callback) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + currentValue = opts.value; + + function fake() { + // console.log(currentValue, opts.value); + if (currentValue === opts.value && opts.value === '空') { + const _p = [{ 'key': '0', 'label': '空' }]; + return callback(_p); + } + const param = { + code: 'utf-8', + q: opts.value, + }; + // const str = new URLSearchParams({ + // code: 'utf-8', + // q: opts.value, + // }).toString(); + + fetchJSON(`${opts.url}`, param) + .then(d => { + if (currentValue === opts.value) { + const result = objectMapper(d.result, opts.map) || []; + callback(result); + } + }); + } + + timeout = setTimeout(fake, 300); +} + +/** + * 异步请求的下拉菜单, 可搜索 + * @property {array} defaultOptions 默认选项 [{key: '', label: '' }] + * @property {string} url + * @property {object} map 异步结果的字段转换定义 + * @property {boolean} autoGet 首次默认请求 + * @property {string} resultkey 结果的根字段 + */ +class SearchInput extends React.Component { + + constructor(props) { + super(props); + this.state = { + data: this.props.defaultOptions || [], + value: undefined, + autoData: this.props.defaultOptions || [], + }; + } + + componentDidMount() { + if (this.props.autoGet === true) { + const { map, resultkey } = this.props; + const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {}); + curl({ value: '', url: this.props.url || '', map: mapKey, resultkey }, (data) => + this.setState({ data, autoData: data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : '')) + ); + } + } + + componentDidUpdate(prevProps) { + if (this.props.value !== prevProps.value) { + this.setState({ value: undefined }); + } + } + + handleClear = () => { + this.setState({ data: this.state.autoData }); + }; + + handleSearch = value => { + if ( ! this.props.url && this.props.defaultOptions?.length) { + const f = this.props.defaultOptions.filter(r => String(r.label).indexOf(value) !== -1); + this.setState({ data: f || [] }); + return false; + } + const { map, resultkey } = this.props; + const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {}); + if (value) { + curl({ value, url: this.props.url || '', map: mapKey, resultkey }, (data) => + this.setState({ data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : '')) + ); + } else { + this.setState({ data: this.state.autoData || [] }); + } + }; + + handleChange = (value, option) => { + this.setState({ value }, () => this.props.onChange(value, option)); + }; + + render() { + const options = this.state.data.map(d => ); + const { onSearchAfter, defaultOptions, autoGet, ...props } = this.props; + return ( + + ); + } +} +export default observer(SearchInput); diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index 74d634e..02b0ad6 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -1,4 +1,5 @@ import { createContext } from 'react'; +import { toJS } from "mobx"; import { observer } from 'mobx-react'; import { DATE_FORMAT } from './../../config'; import { SearchOutlined, } from "@ant-design/icons"; @@ -11,7 +12,9 @@ import SiteSelect from './SiteSelect'; import DateTypeSelect from './DataTypeSelect'; import DatePickerCharts from './DatePickerCharts'; import YearPickerCharts from './YearPickerCharts'; +import SearchInput from './Input'; import { objectMapper, at } from './../../utils/commons'; + import './search.css'; const EditableContext = createContext(); @@ -19,8 +22,12 @@ const Option = Select.Option; /** * 搜索表单 - * @property defaultValue - * * { initialValue, fieldProps, hides, shows, sort } + * @property defaultValue { initialValue, fieldProps, hides, shows, sort } + * * {object} initialValue 默认值 + * * {object} fieldProps 表单项属性 + * * {array} hides 隐藏的表单项 + * * {array} shows 显示的表单项 + * * {object} sort 表单项排序 * @property onSubmit */ export default observer((props) => { @@ -33,7 +40,6 @@ export default observer((props) => { shows: [], ...props.defaultValue, }; - const { onSubmit } = props; const onFinish = (values) => { @@ -54,14 +60,14 @@ export default observer((props) => { 'DepartmentList': { key: 'DepartmentList', 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 ? value.key : 'ALL'; }, default: '', }, 'WebCode': { key: 'WebCode', 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 ? (value.key) : 'ALL'; }, default: '', }, @@ -70,6 +76,11 @@ export default observer((props) => { transform: (value) => value?.key || '', default: '', }, + 'operator': { + key: 'operator', + transform: (value) => value?.key || '', + default: '', + }, 'applyDate': [ { key: 'Date1', @@ -147,7 +158,7 @@ export default observer((props) => {
- {getFields({ sort, initialValue, hides, shows, fieldProps })} + {getFields({ sort, initialValue, hides, shows, fieldProps, form })} {/* 'textAlign': 'right' */} @@ -166,7 +177,7 @@ export default observer((props) => { }); function getFields(props) { - const { fieldProps } = props; + const { fieldProps, form } = props; const bigCol = 4 * 2; const midCol = 6; const layoutProps = { @@ -198,7 +209,7 @@ function getFields(props) { item( 'DepartmentList', 99, - + ), @@ -263,7 +274,7 @@ function getFields(props) { 'dates', 99, - + , midCol ), @@ -273,14 +284,30 @@ function getFields(props) { {/* */} - ,2 + , + 2 ), item( 'months', 99, - - ,2 + + , + 2 + ), + item( + 'operator', + 99, + + + + ), + item( + 'country', + 99, + + + ), ]; baseChildren = baseChildren diff --git a/src/libs/ht.js b/src/libs/ht.js index ed93759..381fbcc 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -7,29 +7,40 @@ export const biz = [ { key: '2', label: '国际事业部', code: '' }, { key: '4', label: '孵化学院', code: '' }, ]; +export const bu = [ + { key: '91001', value: '91001', label: 'CH事业部' }, + { key: '91002', value: '91002', label: '商旅事业部' }, + { key: '91003', value: '91003', label: '国际事业部' }, + { key: '91004', value: '91004', label: 'CT事业部' }, + { key: '91005', value: '91005', label: '德语事业部' }, + { key: '91006', value: '91006', label: 'AH亚洲项目组' }, + { key: '91009', value: '91009', label: 'Trippest项目组' }, + { key: '91010', value: '91010', label: '花梨鹰' }, + { key: '91012', value: '91012', label: '西语组' }, +]; /** * 小组 */ export const groups = [ - { key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [] }, - { key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [] }, - { key: '10,18,16,30', label: '孵化学院', code: '', children: [] }, - { key: '1', label: 'CH直销', code: '', children: [] }, - { key: '2', label: 'CH大客户', code: '', children: [] }, - { key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] }, - { key: '33', label: 'GH项目组', code: '', children: [] }, - { key: '7', label: '市场推广', code: '', children: [] }, - { key: '8', label: '德语', code: '', children: [] }, - { key: '9', label: '日语', code: '', children: [] }, - { key: '11', label: '法语', code: '', children: [] }, - { key: '12', label: '西语', code: '', children: [] }, - { key: '20', label: '俄语', code: '', children: [] }, - { key: '21', label: '意语', code: '', children: [] }, - { key: '10', label: '商旅', code: '', children: [] }, - { key: '18', label: 'CT', code: 'CT', children: [] }, - { key: '16', label: 'APP', code: 'APP', children: [] }, - { key: '30', label: 'Trippest', code: 'TP', children: [] }, - { key: '31', label: '花梨鹰', code: '', children: [] }, + { value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1,2,28,7,33] }, + { value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8,9,11,12,20,21] }, + { value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10,18,16,30] }, + { value: '1', key: '1', label: 'CH直销', code: '', children: [] }, + { value: '2', key: '2', label: 'CH大客户', code: '', children: [] }, + { value: '28', key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] }, + { value: '33', key: '33', label: 'GH项目组', code: '', children: [] }, + { value: '7', key: '7', label: '市场推广', code: '', children: [] }, + { value: '8', key: '8', label: '德语', code: '', children: [] }, + { value: '9', key: '9', label: '日语', code: '', children: [] }, + { value: '11', key: '11', label: '法语', code: '', children: [] }, + { value: '12', key: '12', label: '西语', code: '', children: [] }, + { value: '20', key: '20', label: '俄语', code: '', children: [] }, + { value: '21', key: '21', label: '意语', code: '', children: [] }, + { value: '10', key: '10', label: '商旅', code: '', children: [] }, + { value: '18', key: '18', label: 'CT', code: 'CT', children: [] }, + { value: '16', key: '16', label: 'APP', code: 'APP', children: [] }, + { value: '30', key: '30', label: 'Trippest', code: 'TP', children: [] }, + { value: '31', key: '31', label: '花梨鹰', code: '', children: [] }, ]; export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {}); @@ -42,7 +53,7 @@ export const sites = [ { key: '163', label: 'GH', code: 'GH' }, { key: '28', label: '客运中国', code: 'GHKYZG' }, { key: '7', label: '客运海外', code: 'GHKYHW' }, - { key: '172', label: 'B业务', code: 'GHTOB' }, + { key: '172', label: 'GH TO B业务', code: 'GHTOB' }, { key: '11,12,20,21,10,18', label: '国际(入境)', code: 'JP,VAC,IT,GM,RU,VC' }, { key: '122,200,211,100,188', label: '国际(海外)', code: 'VACHW,ITHW,GMHW,RUHW,VCHW' }, { key: '11', label: '日语', code: 'JP' }, @@ -70,7 +81,7 @@ export const dateTypes = [ * 结果字段 */ export const dataFieldOptions = [ - { label: '毛利', value: 'SumML', formatter: (v) => `${v / 1000} K` }, + { label: '毛利', value: 'SumML', formatter: (v) => `${v / 1000} K`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } }, { label: '订单数', value: 'OrderCount', formatter: (v) => v }, { label: '成交数', value: 'CJCount', formatter: (v) => v }, // { label: '成交人数', value: 'CJPersonNum', formatter: (v) => v }, From 2b279718c05acfb7506317bdfe6584cc678d52fe Mon Sep 17 00:00:00 2001 From: Lei OT Date: Tue, 19 Sep 2023 15:41:41 +0800 Subject: [PATCH 06/19] =?UTF-8?q?=E9=A6=96=E9=A1=B5=20=E5=B9=B4=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Bullet.jsx => BulletWithSort.jsx} | 17 ++- src/components/Waterfall.jsx | 15 ++- src/libs/ht.js | 12 +- src/mock/2.0/trade.json | 1 + src/stores/Trade.js | 118 ++++++++++++++---- src/views/Home.jsx | 76 +++++------ 6 files changed, 161 insertions(+), 78 deletions(-) rename src/components/{Bullet.jsx => BulletWithSort.jsx} (79%) diff --git a/src/components/Bullet.jsx b/src/components/BulletWithSort.jsx similarity index 79% rename from src/components/Bullet.jsx rename to src/components/BulletWithSort.jsx index c2e22d7..bd9fb6d 100644 --- a/src/components/Bullet.jsx +++ b/src/components/BulletWithSort.jsx @@ -2,19 +2,24 @@ import { useEffect, useState } from 'react'; import { observer } from 'mobx-react'; import { Bullet } from '@ant-design/plots'; import { sortBy, merge } from '../utils/commons'; -import { dataFieldAlias } from './../libs/ht'; +import { dataFieldAlias } from '../libs/ht'; + +// const layoutLabel = { +// 'vertical': +// }; export default observer((props) => { - const { dataSource, ...extProps } = 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 maxValue = Math.max(...(origin || []).map((ele) => ele[measureField])); const _max = Math.max(maxKPI, maxValue); - const sortData = origin.sort(sortBy(measureField)); + const sortData = origin.sort(sortBy(measureField)).slice(-itemLength); // 顶格的值定在更远 const _parseData = sortData?.map((ele) => ({ ...ele, [rangeField]: [0, Math.ceil(_max / 0.9)], [measureField]: [ele[measureField]] })); + console.log(_parseData, 'vvvvvvvvvvvvvv'); return _parseData; }; @@ -26,16 +31,16 @@ export default observer((props) => { const config = merge({ color: { - range: ['#FFbcb8', '#FFe0b0', '#bfeec8'], + range: [ '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0', measure: '#5B8FF9', target: '#FF9845', }, label: { // target: true, // measure: { - // position: 'middle', + // position: extProps?.vertical === 'vertical' ? 'top' : 'right', // style: { - // fill: '#fff', + // fill: '#063CAA', // }, // }, }, diff --git a/src/components/Waterfall.jsx b/src/components/Waterfall.jsx index 9fc15f0..8c106ac 100644 --- a/src/components/Waterfall.jsx +++ b/src/components/Waterfall.jsx @@ -5,7 +5,8 @@ import { merge } from '../utils/commons'; export default observer((props) => { const { dataSource, line, title, ...extProps } = props; - const yMax = Math.max(line?.value || 0, ...dataSource.map((ele) => ele[extProps.yField])); + const yMax = (Math.max(line?.value || 0, ...dataSource.map((ele) => ele[extProps.yField])))*10; + console.log(title, 'title waterfall', yMax); const annotationsLine = line ? [ { @@ -36,6 +37,7 @@ export default observer((props) => { /** 展示总计 */ total: { + // label: `${title}总`, label: `${title}总`, style: { fill: '#96a6a6', @@ -57,6 +59,17 @@ export default observer((props) => { formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v, }, }, + xAxis: { + type: 'cat', + }, + tooltip: { + customItems: (originalItems) => { + // process originalItems, + const items = originalItems.map((ele) => ({ ...ele, title: `${ele.title} ${ele.data.groupsLabel}`, name: dataFieldAlias[ele.name]?.alias || ele.name })); + console.log(originalItems, items, 'llll'); + return items; + }, + }, }, extProps); return ( <> diff --git a/src/libs/ht.js b/src/libs/ht.js index 381fbcc..a68a693 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -82,10 +82,10 @@ export const dateTypes = [ */ export const dataFieldOptions = [ { label: '毛利', value: 'SumML', formatter: (v) => `${v / 1000} K`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } }, - { label: '订单数', value: 'OrderCount', formatter: (v) => v }, - { label: '成交数', value: 'CJCount', formatter: (v) => v }, - // { label: '成交人数', value: 'CJPersonNum', formatter: (v) => v }, - { label: '成交率', value: 'CJrate', formatter: (v) => v }, + { label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } }, + { label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } }, + { label: '成交率', value: 'ConfirmRates', formatter: (v) => v, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, + // { label: '人数', value: 'CJPersonNum', formatter: (v) => v }, // todo: more... ]; /** @@ -94,8 +94,8 @@ export const dataFieldOptions = [ export const dataFieldAlias = dataFieldOptions.reduce( (a, c) => ({ ...a, - [c.value]: { alias: c.label, formatter: (v) => c.formatter(v) }, - [`${c.value}KPI`]: { alias: `${c.label}目标`, formatter: (v) => c.formatter(v) }, + [c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) }, + [c.nestkey.v]: { ...c, alias: `${c.label}目标`, formatter: (v) => c.formatter(v) }, }), {} ); diff --git a/src/mock/2.0/trade.json b/src/mock/2.0/trade.json index 69ceb97..6d65617 100644 --- a/src/mock/2.0/trade.json +++ b/src/mock/2.0/trade.json @@ -87,6 +87,7 @@ "result1|24": [ { "groups|1": "@pick([\"inside\",\"outside\"])", + "groupsLabel|1": "@pick([\"inside\",\"outside\"])", "groupDateVal": "@date('2023-MM')", "SumML": "@increment(1000)", "SumMLVSrate": "@float(0,70,2,2)", diff --git a/src/stores/Trade.js b/src/stores/Trade.js index 52b1343..7837a2c 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -1,6 +1,7 @@ import { makeAutoObservable, runInAction, toJS } from 'mobx'; import * as req from '../utils/request'; -import { isEmpty, sortBy } from '../utils/commons'; +import { isEmpty, sortBy, pick } from '../utils/commons'; +import { dataFieldAlias } from './../libs/ht'; class Trade { constructor(rootStore) { @@ -8,76 +9,151 @@ class Trade { makeAutoObservable(this); } + /** + * 年度总额 + */ fetchSummaryData(queryData) { this.summaryData.loading = true; queryData.groupType = 'overview'; - queryData.groupDateType = 'year'; - req.fetchJSON('/service-Analyse2/GetTradeProcess/test', queryData).then((json) => { + // queryData.groupDateType = 'year'; + this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { + const summary = json.result1?.[0] || {}; const summaryData = { loading: false, dataSource: [ - { title: '成团', value: json.result1?.SumOrder, VSrate: json.result1?.SumOrderrate, KPIrate: json.result1?.SumOrderKPIrate, hasKPI: !isEmpty(json.result1?.SumOrderKPIrate) }, - { title: '毛利', value: json.result1?.SumML, VSrate: json.result1?.SumMLrate, KPIrate: json.result1?.SumMLKPIrate, hasKPI: !isEmpty(json.result1?.SumMLKPIrate) }, - { title: '完成率', value: `${json.result1?.SumMLKPIrate || ''}%`, hasKPI: false }, + { + title: '成团', + value: summary?.ConfirmOrder, + // VSrate: summary?.ConfirmOrderrate, + KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p], + hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]), + }, + { title: '毛利', value: summary?.SumML, KPIrate: summary?.[dataFieldAlias.SumML.nestkey.p], hasKPI: !isEmpty(summary?.[dataFieldAlias.SumML.nestkey.p]) }, + { title: '完成率', value: `${summary?.[dataFieldAlias.SumML.nestkey.p] || ''}%`, hasKPI: false }, { title: '人数', - value: json.result1?.SumPersonNum, - VSrate: json.result1?.SumPersonNumrate, - KPIrate: json.result1?.SumPersonNumKPIrate, - hasKPI: !isEmpty(json.result1?.SumPersonNumKPIrate), + value: summary?.SumPersonNum, + // VSrate: summary?.SumPersonNumrate, + // KPIrate: summary?.[dataFieldAlias.SumPersonNum.nestkey.p], + hasKPI: false, // // !isEmpty(summary?.[dataFieldAlias.SumPersonNum.nestkey.p]), }, ], }; this.summaryData = summaryData; + const kpi = { label: '', value: summary.MLKPIvalue }; + this.summaryData.kpi = kpi; + }); + } + }); + } + + /** + * 时间轴 + */ + fetchTradeDataByDate(queryData) { + this.timeData.loading = true; + Object.assign(queryData, { groupType: 'overview', groupDateType: 'month' }); + this.fetchTradeData(queryData).then((json) => { + if (json.errcode === 0) { + runInAction(() => { + const data = json.result1; + // 标注KPI + + this.timeData.loading = false; + this.timeData.dataSource = data; }); } }); } + /** + * 事业部年度 + */ + fetchTradeDataByBU(queryData) { + this.BuData.loading = true; + Object.assign(queryData, { groupType: 'bu', groupDateType: 'year' }); + this.fetchTradeData(queryData).then((json) => { + if (json.errcode === 0) { + runInAction(() => { + const data = json.result1; + // 标注KPI + + this.BuData.loading = false; + this.BuData.dataSource = data; + }); + } + }); + } + + /** + * 业务区域, 按月 + */ fetchTradeDataByMonth(queryData) { this.sideData.loading = true; + // Object.assign(queryData, { groupType: 'bizarea', groupDateType: 'month' }); + // todo: groupType: bizarea Object.assign(queryData, { groupType: 'bu', groupDateType: 'month' }); - req.fetchJSON('/service-Analyse2/GetTradeProcess/test', queryData).then((json) => { + this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { const sortResult = json.result1.sort(sortBy('groupDateVal')); + /** + * test: '91006' + */ const groupsData = sortResult.reduce((r, v) => { - (r[v.groups] || (r[v.groups] = [])).push(v); + if (v.groupsLabel && ['91001', '91006'].includes(v.groupsKey)) { // , '91006' + (r[v.groupsLabel] || (r[v.groupsLabel] = [])).push(v); + } return r; }, {}); - console.log(groupsData, 'groupsData', queryData); - const kpi = { label: '', value: 1200000 }; // todo: 标注KPI this.sideData.loading = false; this.sideData.dataSource = groupsData; this.sideData.monthData = sortResult; - this.sideData.kpi = kpi; + // const kpi = { label: '', value: 1200000 }; // 标注KPI + // this.sideData.kpi = kpi; }); } }); } + /** + * TOP + */ fetchTradeDataByType(orderType, queryData) { - this.topData[orderType] = { loading: true, dataSource: [] }; - Object.assign(queryData, { groupType: 'orderType', groupDateType: 'year' }); - req.fetchJSON('/service-web/QueryData/GetTradeOrderByType', queryData).then((json) => { + this.topData[orderType] = { loading: true, dataSource: [], originData: [] }; + Object.assign(queryData, { groupType: orderType, groupDateType: 'year' }); + this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { this.topData[orderType].loading = false; this.topData[orderType].dataSource = json.result1; - console.log({ loading: false, ...json }, orderType, 'topData'); }); } }); } + /** + * 获取业绩数据 + */ + async fetchTradeData(queryData) { + const json = await req.fetchJSON('/service-Analyse2/GetTradeProcess', queryData); + if (json.errcode === 0) { + return json; + } + return null; + } + setStateSearch(body) { this.searchPayloadHome = body; } - summaryData = { loading: false, dataSource: [] }; - sideData = { loading: false, dataSource: {}, kpi: {}, monthData: [] }; + summaryData = { loading: false, dataSource: [], kpi: {}, }; + timeData = { loading: false, dataSource: [] }; + BuData = { loading: false, dataSource: [] }; + sideData = { loading: false, dataSource: {}, monthData: [] }; + dataForSort = {}; topData = {}; defaultDataSubject = 'CJCount'; searchPayloadHome = {}; diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 0071d86..98336c1 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -3,27 +3,26 @@ import { observer } from 'mobx-react'; import { Row, Col, Spin, Space } from 'antd'; import { stores_Context } from '../config'; import { useNavigate } from 'react-router-dom'; -// import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; import StatisticCard from '../components/StatisticCard'; -import Bullet from '../components/Bullet'; +import Bullet from '../components/BulletWithSort'; import Waterfall from '../components/Waterfall'; -import Column from '../components/Column'; import DataFieldRadio from './../components/DateFieldRadio'; import SearchForm from './../components/search/SearchForm'; - import { empty } from './../utils/commons'; +import { dataFieldAlias } from './../libs/ht'; import './home.css'; export default observer(() => { const navigate = useNavigate(); const { TradeStore } = useContext(stores_Context); - const { searchPayloadHome, sideData, summaryData, topData } = TradeStore; + const { searchPayloadHome, sideData, summaryData, BuData, topData } = TradeStore; const topSeries = [ - { key: 'Country', label: '国籍' }, - { key: 'Area', label: '目的地' }, - { key: 'Sales', label: '顾问' }, - { key: 'GuestGroupType', label: '客群类别' }, + { key: 'country', label: '国籍' }, + { key: 'dept', label: '小组' }, + { key: 'operator', label: '顾问' }, + { key: 'destination', label: '目的地' }, + // { key: 'GuestGroupType', label: '客群类别' }, ]; useEffect(() => { @@ -35,6 +34,7 @@ export default observer(() => { const pageRefresh = (queryData) => { TradeStore.fetchSummaryData(queryData); + TradeStore.fetchTradeDataByBU(queryData); TradeStore.fetchTradeDataByMonth(queryData); for (const iterator of topSeries) { TradeStore.fetchTradeDataByType(iterator.key, queryData); @@ -59,55 +59,42 @@ export default observer(() => { const [BulletConfig, setBulletConfig] = useState({ measureField: 'SumML', // rangeField: 'SumMLRange', // - targetField: 'SumMLKPI', // - xField: 'OrderType', + targetField: 'MLKPIvalue', // + xField: 'groupsLabel', }); const handleChangeValueKey = (key) => { setValueKey(key); setBulletConfig({ measureField: key, rangeField: `${key}Range`, - targetField: `${key}KPI`, - xField: 'OrderType', + // targetField: `${key}KPI`, + targetField: dataFieldAlias[key].nestkey.p, + xField: 'groupsLabel', }); }; const WaterfallConfig = { xField: 'groupDateVal', yField: 'SumML', + seriesField: 'groupsLabel', meta: { groupDateVal: { alias: '月份', + // type: 'cat', }, }, label: { - formatter: (v) => ((v.SumML / sideData.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) + '%', }, }; - const ColumnConfig = { - xField: 'groupDateVal', - yField: 'SumML', - seriesField: 'groups', - label: { - formatter: (v) => ((v.SumML / sideData.kpi.value) * 100).toFixed(2) + '%', - }, - legend: false, - // annotations: sideData.monthData.map((d, ...args) => { - // console.log('aaa', d, args); - // return { - // type: 'dataMarker', - // position: d, - // point: { - // style: { - // stroke: '#FF6B3B', - // lineWidth: 1.5, - // }, - // }, - // }; - // }), + const BUConfig = { + measureField: 'SumML', // + rangeField: 'SumMLRange', // + targetField: 'MLKPIvalue', // + xField: 'groupsLabel', + layout: 'vertical', }; - return ( <> @@ -115,14 +102,14 @@ export default observer(() => { { @@ -148,14 +135,15 @@ export default observer(() => {

市场进度

- + - + {Object.keys(sideData.dataSource).map((key) => ( - + +

{key}

))}
@@ -173,7 +161,7 @@ export default observer(() => {

{item.label}

- +
))} From 7f7cef72b81ed3f1b27e01243071dc7f493d5eab Mon Sep 17 00:00:00 2001 From: Lei OT Date: Tue, 19 Sep 2023 16:27:20 +0800 Subject: [PATCH 07/19] =?UTF-8?q?=E9=A6=96=E9=A1=B5=20=E5=B9=B4=E5=BA=A6?= =?UTF-8?q?=20=E6=97=B6=E9=97=B4=E8=BD=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Waterfall.jsx | 1 - src/stores/Trade.js | 2 +- src/views/Home.jsx | 63 ++++++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/components/Waterfall.jsx b/src/components/Waterfall.jsx index 8c106ac..4df8f2d 100644 --- a/src/components/Waterfall.jsx +++ b/src/components/Waterfall.jsx @@ -66,7 +66,6 @@ export default observer((props) => { customItems: (originalItems) => { // process originalItems, const items = originalItems.map((ele) => ({ ...ele, title: `${ele.title} ${ele.data.groupsLabel}`, name: dataFieldAlias[ele.name]?.alias || ele.name })); - console.log(originalItems, items, 'llll'); return items; }, }, diff --git a/src/stores/Trade.js b/src/stores/Trade.js index 7837a2c..10c176f 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -54,7 +54,7 @@ class Trade { */ fetchTradeDataByDate(queryData) { this.timeData.loading = true; - Object.assign(queryData, { groupType: 'overview', groupDateType: 'month' }); + Object.assign(queryData, { groupType: 'overview', groupDateType: 'week' }); this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 98336c1..b7c88e1 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -8,14 +8,15 @@ import Bullet from '../components/BulletWithSort'; import Waterfall from '../components/Waterfall'; import DataFieldRadio from './../components/DateFieldRadio'; import SearchForm from './../components/search/SearchForm'; -import { empty } from './../utils/commons'; +import { empty, cloneDeep } from './../utils/commons'; import { dataFieldAlias } from './../libs/ht'; +import { Line } from '@ant-design/charts'; import './home.css'; export default observer(() => { const navigate = useNavigate(); const { TradeStore } = useContext(stores_Context); - const { searchPayloadHome, sideData, summaryData, BuData, topData } = TradeStore; + const { searchPayloadHome, sideData, summaryData, BuData, topData, timeData } = TradeStore; const topSeries = [ { key: 'country', label: '国籍' }, @@ -34,6 +35,7 @@ export default observer(() => { const pageRefresh = (queryData) => { TradeStore.fetchSummaryData(queryData); + TradeStore.fetchTradeDataByDate(queryData); TradeStore.fetchTradeDataByBU(queryData); TradeStore.fetchTradeDataByMonth(queryData); for (const iterator of topSeries) { @@ -95,11 +97,52 @@ export default observer(() => { xField: 'groupsLabel', layout: 'vertical', }; + + const lineConfigSet = { + // data: orders_data.data, + padding: 'auto', + xField: 'groupDateVal', + yField: 'SumML', + seriesField: 'groupsLabel', + xAxis: { + type: 'cat', + }, + smooth: true, + point: { + size: 4, + shape: "cicle", + }, + legend: false, + meta: { ...cloneDeep(dataFieldAlias) + // [extProps.yField]: { + // alias: dataFieldAlias[extProps.yField]?.alias || extProps.yField, + // formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v, + // max: Math.ceil(yMax / 0.95), + // }, + }, + }; + const [timeDataField, setTimeDataField] = useState('SumML'); + const [lineConfig, setLineConfig] = useState(cloneDeep(lineConfigSet)); + const handleChangetimeDataField = (key) => { + setTimeDataField(key); + setLineConfig({ + ...lineConfig, + yField: key, + // ? 全局的alias不起作用 + tooltip: { + customItems: (originalItems) => { + // process originalItems, + const items = originalItems.map((ele) => ({ ...ele, name: dataFieldAlias[key]?.alias || key })); + return items; + }, + }, + }); + }; return ( <> - + {/* style={{ margin: '-16px -8px', padding: 0 }} */} - + { }, shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'], fieldProps: { - DepartmentList: { show_all: true, }, - WebCode: { show_all: true, }, + DepartmentList: { show_all: true }, + WebCode: { show_all: true }, years: { hide_vs: true }, }, }} @@ -133,6 +176,14 @@ export default observer(() => {
+
+ +

趋势

+ +
+
+ +

市场进度

From 9985d4272be7437c462c45e71165339bc3b99286 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Tue, 19 Sep 2023 17:23:08 +0800 Subject: [PATCH 08/19] . --- src/components/kpi/BUPanel.jsx | 13 +++++++ src/components/kpi/OverviewPanel.jsx | 37 +++++++++++++++++++ src/components/search/SearchForm.jsx | 10 +++-- src/libs/ht.js | 14 +++---- src/stores/DictData.js | 14 +++++-- src/views/KPI.jsx | 55 +++++++++++++++++++++------- src/views/kpi.css | 3 ++ 7 files changed, 120 insertions(+), 26 deletions(-) create mode 100644 src/components/kpi/BUPanel.jsx create mode 100644 src/components/kpi/OverviewPanel.jsx create mode 100644 src/views/kpi.css diff --git a/src/components/kpi/BUPanel.jsx b/src/components/kpi/BUPanel.jsx new file mode 100644 index 0000000..e159abe --- /dev/null +++ b/src/components/kpi/BUPanel.jsx @@ -0,0 +1,13 @@ +import { useContext } from 'react'; +import { observer } from "mobx-react"; +// import { stores_Context } from '../config'; +import { Table } from 'antd'; + +export default observer((props) => { + // const { } = useContext(stores_Context); + return ( + <> + {props.title} + + ); +}); diff --git a/src/components/kpi/OverviewPanel.jsx b/src/components/kpi/OverviewPanel.jsx new file mode 100644 index 0000000..f25e336 --- /dev/null +++ b/src/components/kpi/OverviewPanel.jsx @@ -0,0 +1,37 @@ +import { useContext } from 'react'; +import { observer } from "mobx-react"; +// import { stores_Context } from '../config'; +import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd'; +import SearchForm from './../search/SearchForm'; +import { bu } from './../../libs/ht'; + +export default observer((props) => { + // const { } = useContext(stores_Context); + return ( + <> + + + { + // TradeStore.setStateSearch(form); + // pageRefresh(obj); + }} + /> + + + + ); +}); diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index 02b0ad6..f9640b8 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -40,7 +40,7 @@ export default observer((props) => { shows: [], ...props.defaultValue, }; - const { onSubmit } = props; + const { onSubmit, confirmText } = props; const onFinish = (values) => { console.log('Received values of form, origin form value: ', values); @@ -152,10 +152,14 @@ export default observer((props) => { // 'DateType': undefined, }); }; + const onValuesChange = (...args) => { + const [changedValues, allValues] = args; + console.log('form onValuesChange', args); + }; return ( // layout="inline" - + {getFields({ sort, initialValue, hides, shows, fieldProps, form })} @@ -163,7 +167,7 @@ export default observer((props) => { {/*
); }; - +/** + * 基于组织架构 + */ export default observer(Business_unit); diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index f9640b8..725b5ab 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -7,6 +7,7 @@ import { Form, Row, Col, Select, Button, Space, DatePicker } from 'antd'; import moment from 'moment'; // import locale from 'antd/es/date-picker/locale/zh_CN'; import BusinessSelect from './BusinessSelect'; +import BusinessUnitSelect from './BusinessUnitSelect'; import GroupSelect from './GroupSelect'; import SiteSelect from './SiteSelect'; import DateTypeSelect from './DataTypeSelect'; @@ -203,6 +204,13 @@ function getFields(props) { }; let baseChildren = []; baseChildren = [ + item( + 'HTBusinessUnits', + 99, + + + + ), item( 'businessUnits', 99, diff --git a/src/libs/ht.js b/src/libs/ht.js index cd04159..65654ed 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -81,7 +81,7 @@ export const dateTypes = [ * 结果字段 */ export const dataFieldOptions = [ - { label: '毛利', value: 'SumML', formatter: (v) => `${v / 1000} K`, 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: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } }, { label: '成交率', value: 'ConfirmRates', formatter: (v) => v, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, @@ -95,7 +95,7 @@ export const dataFieldAlias = dataFieldOptions.reduce( (a, c) => ({ ...a, [c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) }, - [c.nestkey.v]: { ...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) }, }), {} ); @@ -104,11 +104,27 @@ export const dataFieldAlias = dataFieldOptions.reduce( * KPI对象 */ export const KPIObjects = [ - { key: 'overview', value: 'overview', label: '总额' }, + { key: 'overview', value: 'overview', label: '海纳' }, { key: 'bu', value: 'bu', label: '事业部' }, { key: 'dept', value: 'dept', label: '小组' }, - { key: 'du', value: 'du', label: '顾问小组' }, + { key: 'du', value: 'du', label: '销售小组' }, { key: 'operator', value: 'operator', label: '顾问' }, { key: 'destination', value: 'destination', label: '目的地' }, { key: 'country', value: 'country', label: '国籍' }, ]; +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: 'first_reply_rates', value: 'first_reply_rates', label: '首报回复率'}, + // { key: 'quote_rates', value: 'quote_rates', label: '报价率'}, + // { key: 'first_post_time', value: 'first_post_time', label: '订单到首邮发送时间'}, + // { key: 'reply_rates_wechat', value: 'reply_rates_wechat', label: '微信回复率'}, + // { 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: '人数'}, +]; diff --git a/src/stores/Trade.js b/src/stores/Trade.js index 10c176f..a726e92 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -30,7 +30,7 @@ class Trade { KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p], hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]), }, - { title: '毛利', value: summary?.SumML, KPIrate: summary?.[dataFieldAlias.SumML.nestkey.p], hasKPI: !isEmpty(summary?.[dataFieldAlias.SumML.nestkey.p]) }, + { title: '毛利', value: summary?.SumML, KPIrate: summary?.[dataFieldAlias.SumML.nestkey.p], hasKPI: false }, { title: '完成率', value: `${summary?.[dataFieldAlias.SumML.nestkey.p] || ''}%`, hasKPI: false }, { title: '人数', @@ -92,9 +92,9 @@ class Trade { */ fetchTradeDataByMonth(queryData) { this.sideData.loading = true; - // Object.assign(queryData, { groupType: 'bizarea', groupDateType: 'month' }); // todo: groupType: bizarea - Object.assign(queryData, { groupType: 'bu', groupDateType: 'month' }); + // Object.assign(queryData, { groupType: 'bu', groupDateType: 'month' }); + Object.assign(queryData, { groupType: 'bizarea', groupDateType: 'month' }); this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { @@ -103,7 +103,7 @@ class Trade { * test: '91006' */ const groupsData = sortResult.reduce((r, v) => { - if (v.groupsLabel && ['91001', '91006'].includes(v.groupsKey)) { // , '91006' + if (v.groupsLabel ) { // && ['91001', '91006'].includes(v.groupsKey) (r[v.groupsLabel] || (r[v.groupsLabel] = [])).push(v); } return r; diff --git a/src/utils/commons.js b/src/utils/commons.js index 5050b07..6a9adff 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -391,6 +391,7 @@ export const fixTo2Decimals = curriedFix(2); * 向零四舍五入, 保留4位小数 */ export const fixTo4Decimals = curriedFix(4); +export const fixToInt = curriedFix(0); /** * 映射 @@ -498,3 +499,10 @@ export function flush(collection) { } return undefined; } + +/** + * 千分位 格式化数字 + */ +export const numberFormatter = (number) => { + return new Intl.NumberFormat().format(number); +}; diff --git a/src/views/Home.jsx b/src/views/Home.jsx index b7c88e1..9109c48 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -70,7 +70,7 @@ export default observer(() => { measureField: key, rangeField: `${key}Range`, // targetField: `${key}KPI`, - targetField: dataFieldAlias[key].nestkey.p, + targetField: dataFieldAlias[key].nestkey.v, xField: 'groupsLabel', }); }; @@ -128,7 +128,6 @@ export default observer(() => { setLineConfig({ ...lineConfig, yField: key, - // ? 全局的alias不起作用 tooltip: { customItems: (originalItems) => { // process originalItems, From 33751b3eca896704a60822e4decf2a7ba8781f7f Mon Sep 17 00:00:00 2001 From: Lei OT Date: Wed, 20 Sep 2023 17:07:10 +0800 Subject: [PATCH 10/19] kpi --- src/App.css | 15 +- src/components/kpi/BUPanel.jsx | 17 +- src/components/kpi/KPISettings.jsx | 256 ++++++++++++++++++ src/components/kpi/OverviewPanel.jsx | 258 ++++++++++++++++-- src/components/kpi/SumProfitPanel.jsx | 57 ++++ src/components/search/BusinessUnitSelect.jsx | 36 +++ src/components/search/SearchForm.jsx | 8 +- src/stores/DatePickerStore.js | 15 +- src/stores/KPI.js | 76 ++---- src/utils/request.js | 15 ++ src/views/KPI.jsx | 259 ++----------------- src/views/kpi.css | 14 + 12 files changed, 707 insertions(+), 319 deletions(-) create mode 100644 src/components/kpi/KPISettings.jsx create mode 100644 src/components/kpi/SumProfitPanel.jsx create mode 100644 src/components/search/BusinessUnitSelect.jsx diff --git a/src/App.css b/src/App.css index 2460a39..678262a 100644 --- a/src/App.css +++ b/src/App.css @@ -6,4 +6,17 @@ .align_left{ text-align: left; -} \ No newline at end of file +} + +.mb-1{ + margin-bottom: 1em; +} +.mb-n1{ + margin-bottom: -1em; +} +.p-none{ + padding: 0; +} +.p-s1{ + padding: .5em; +} diff --git a/src/components/kpi/BUPanel.jsx b/src/components/kpi/BUPanel.jsx index e159abe..0ebdd84 100644 --- a/src/components/kpi/BUPanel.jsx +++ b/src/components/kpi/BUPanel.jsx @@ -1,13 +1,18 @@ 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 { bu, KPISubjects } from './../../libs/ht'; export default observer((props) => { // const { } = useContext(stores_Context); - return ( - <> - {props.title} - - ); + const searchProps = { + // shows: ['DateType', 'years', 'HTBusinessUnits'], + }; + return ( + <> + + + ); }); diff --git a/src/components/kpi/KPISettings.jsx b/src/components/kpi/KPISettings.jsx new file mode 100644 index 0000000..f4c9e65 --- /dev/null +++ b/src/components/kpi/KPISettings.jsx @@ -0,0 +1,256 @@ +import { useContext, useState, useEffect } from 'react'; +import { observer } from 'mobx-react'; +import { stores_Context } from './../../config'; +import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd'; +import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components'; +import SearchForm from './../search/SearchForm'; +import { bu, KPIObjects, KPISubjects } from './../../libs/ht'; +import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter } from './../../utils/commons'; + +export const KPIObjectsMapped = KPIObjects.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); +const { Text } = Typography; +const numberConvert10K = (number, scale = 10) => { + return fixTo2Decimals(number / (1000 * scale)) + '万'; +}; + +export default observer((props) => { + const { KPIStore, DictDataStore } = useContext(stores_Context); + const { sort, initialValue, hides, shows, fieldProps } = { + sort: '', + initialValue: {}, + fieldProps: { + years: { hide_vs: true }, + }, + hides: [], + shows: ['DateType', 'years'], + ...props.searchProps, + }; + const { curObject, objects, KPISubjects, onSearchSubmit } = props; + + const curObjectItem = KPIObjectsMapped[curObject]; +console.log(curObjectItem, KPIObjectsMapped, curObject, 'cocococo'); + const [dataSource, setDataSource] = useState([]); + const { settingYear } = KPIStore; + + const [editOpen, setEditOpen] = useState(false); // test: + const [editableRowsKeys, setEditableRowKeys] = useState([]); + // console.log(toJS(KPIStore.pageData ), dataSource, '00000'); + + const PercentInput = ({ value, onChange, record, ...extProps }) => { + // console.log(extProps, '22222222'); + const initialPercent = record.kpiDataMapped?.[`M${extProps.month}`]?.percentVal; + const [inputVal, setInputVal] = useState(value); + const calcV = inputVal ? numberConvert10K(fixTo4Decimals((Number(record?.yearValue) * inputVal) / 100)) : 0; + + const handleInputChange = ({ target: { value } }) => { + setInputVal(value); + }; + const handleInputConfirm = () => { + // onChange?.(inputVal); + // setInputVal(''); + }; + return ( + + + {/* onBlur={handleInputConfirm} onPressEnter={handleInputConfirm} */} + {/* 1 */} + {calcV} + + ); + }; + const RenderInput = (row, mon) => { + // console.log(toJS(row), mon); + return ( + +
+ {row.kpiDataMapped?.[`M${mon}`]?.percentVal} + % +
+
{row.kpiDataMapped?.[`M${mon}`]?.value}
+
+ ); + }; + const monthCol = new Array(12).fill(1).map((_, index) => { + return { + title: `${index + 1}月`, + dataIndex: `M${index + 1}Percent`, + valueType: 'digit', + width: '6.5em', + fieldProps: { min: 0, max: 100, style: { width: '4em' } }, + renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => { + return ; + }, + render: (_, row) => RenderInput(row, index + 1), + }; + }); + const initialRow = monthCol.reduce((r, v) => ({ ...r, [v.dataIndex]: 0 }), {}); // v.formItemProps.initialValue + const columns = [ + { + title: curObjectItem.label, + dataIndex: 'object_id', + valueType: 'select', + // ...valueEnum + // fieldProps: { labelInValue: true }, + render: (_, r) => r.object_name, + }, + // { + // title: 'Name', + // dataIndex: 'title', + // //...form rules + // formItemProps: { + // rules: [ + // { + // required: true, + // whitespace: true, + // message: '此项是必填项', + // }, + // ], + // }, + // }, + { + title: '年度目标', + dataIndex: 'yearValue', + valueType: 'digit', + fieldProps: { style: { width: '100%' } }, + formItemProps: { + style: { width: '100%' }, + }, + }, + ...monthCol, + // { + // title: '完成进度', + // dataIndex: 'place', + // valueType: 'percent', + // editable: false, + // width: '6em', + // }, + { + title: ( + + 操作 + { + setEditOpen(e); + setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); + // KPIStore.setEditableRowsKeys(e ? dataSource.map((ele) => ele.key) : []); + }} + /> + + ), + valueType: 'option', + // width: 250, + render: () => { + return null; + }, + }, + ]; + const onTableChange = (...argrs) => { + console.log(argrs[0], 'who who who'); + setEditableRowKeys(argrs[0].map((ele) => ele.key)); + // KPIStore.setEditableRowsKeys(argrs[0].map((ele) => ele.key)); + setDataSource(argrs[0]); + // KPIStore.handleTableEdit(argrs[0]); + }; + return ( + <> + + + { + // TradeStore.setStateSearch(form); + // pageRefresh(obj); + }} + /> + + + + + { + const id = String(i); + return { + ...ele, + children: ( + { + // return [ + // { + // setEditOpen(e); + // setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); + // // KPIStore.setEditableRowsKeys(e ? dataSource.map((ele) => ele.key) : []); + // }} + // />, + // , + // ]; + // }} + editable={{ + type: 'multiple', + editableKeys: editableRowsKeys, + actionRender: (row, config, defaultDoms) => { + // console.log(row, config, defaultDoms); + return [defaultDoms.delete]; + }, + onValuesChange: (record, recordList) => { + console.log('on edit, onValuesChange'); + onTableChange(recordList); + }, + onChange: (editableKeys, editableRows) => { + console.log('editable onValuesChange'); + onTableChange(editableRows); + // KPIStore.setEditableRowsKeys() + }, + }} + /> + ), + }; + })} + /> + + + + ); +}); diff --git a/src/components/kpi/OverviewPanel.jsx b/src/components/kpi/OverviewPanel.jsx index f25e336..49f8760 100644 --- a/src/components/kpi/OverviewPanel.jsx +++ b/src/components/kpi/OverviewPanel.jsx @@ -1,37 +1,263 @@ -import { useContext } from 'react'; -import { observer } from "mobx-react"; -// import { stores_Context } from '../config'; -import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd'; +import { useContext, useState, useEffect, useMemo } from 'react'; +import { observer } from 'mobx-react'; +import { toJS } from 'mobx'; +import moment from 'moment'; +import { stores_Context } from './../../config'; +import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs, message } from 'antd'; +import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components'; import SearchForm from './../search/SearchForm'; import { bu } from './../../libs/ht'; +import { isEmpty, objectMapper, fixToInt, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter } from './../../utils/commons'; + +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 = 10) => { + return fixTo2Decimals((number/(1000*scale))) + '万'; +}; export default observer((props) => { - // const { } = useContext(stores_Context); + const { KPIStore, date_picker_store: searchFormStore } = useContext(stores_Context); + const { curObject } = props; + const [dataSource, setDataSource] = useState([]); + useEffect(() => { + onSearchSubmit({ + object: curObject, + date_type: 'applyDate', + start_date: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'), + end_date: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'), + }); + return () => {}; + }, []); + + const onSearchSubmit = (obj, formVal={}) => { + const getkpiParam = objectMapper(obj, { DateType: { key: 'date_type' }, Date1: { key: 'start_date' }, Date2: { key: 'end_date' } }); + Object.assign(getkpiParam, { object: curObject }); + KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear); + KPIStore.getList(getkpiParam).then((data) => { + setDataSource(data); + }); + }; + const [editOpen, setEditOpen] = useState(false); + const [editableRowsKeys, setEditableRowKeys] = useState([]); + + 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(fixTo4Decimals((Number(record?.yearValue) * inputValue) / 100)) : 0; + return ( + + + {calcV} + + ); + }, + [] + ); + const RenderMonthCell = (row, mon) => { return ( - <> - - + +
+ {fixTo2Decimals(row?.[`M${mon}Percent`])} + % +
+
{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}
+
+ ); + }; + const monthCol = new Array(12).fill(1).map((_, index) => { + return { + title: `${index + 1}月`, + dataIndex: `M${index + 1}Percent`, + valueType: 'digit', + width: '6.5em', + // fieldProps: { min: 0, max: 100, style: { width: '4em' } }, + renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => { + return ; + }, + render: (_, row) => RenderMonthCell(row, index + 1), + }; + }); + const columns = [ + { + title: '年度目标', + dataIndex: 'yearValue', + valueType: 'digit', + fieldProps: { style: { width: '100%' }, step: 10000 * 100 }, + formItemProps: { + style: { width: '100%' }, + }, + render: (_, row, ...a) => numberConvert10K(row.yearValue), + }, + ...monthCol, + { + title: '操作', + 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, + }; + }); + 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, + }))(dataSource?.[0] || {}); + tableData.unshift(yearRow); + console.log('sub', tableData, 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*100, + object: curObject, + object_name: '', + object_id: -1, + subject: 'sum_profit', + date_type: searchFormStore.formValuesToSub.DateType, + kpiDataMapped: {}, + key: Date.now().toString(32) + }), + {} + ); // v.formItemProps.initialValue + const makeInitialTable = (e) => { + setEditOpen(e); + if (e && isEmpty(dataSource)) { + const _initialRow = Object.assign({}, initialRow, initialPercentKey); + setDataSource([_initialRow]); + setEditableRowKeys([_initialRow.key]); + return false; + } + setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); + }; + const [delKpiIds, setDelKpiIds] = useState([]); + return ( + <> + + { // TradeStore.setStateSearch(form); // pageRefresh(obj); + onSearchSubmit(obj, form); + setEditOpen(false); + setEditableRowKeys([]); }} /> - - ); + { + return [ + { + makeInitialTable(e); + }} + />, + , + ]; + }} + editable={{ + type: 'multiple', + editableKeys: editableRowsKeys, + actionRender: (row, config, defaultDoms) => { + return [defaultDoms.delete]; + }, + onDelete: (_key, _row) => { + // console.log('del', _key, _row); + const rowKpiIds = (_row?.kpiData || []).map(ele => ele.kpi_id); + rowKpiIds.push(_row?.kpiYear?.kpi_id); + setDelKpiIds(rowKpiIds); + }, + onValuesChange: (record, recordList) => { + // console.log('on edit, onValuesChange',record, recordList); + onTableChange(recordList); + }, + onChange: (editableKeys, editableRows) => { + setEditableRowKeys(editableKeys); + }, + }} + /> + + ); }); diff --git a/src/components/kpi/SumProfitPanel.jsx b/src/components/kpi/SumProfitPanel.jsx new file mode 100644 index 0000000..0818a00 --- /dev/null +++ b/src/components/kpi/SumProfitPanel.jsx @@ -0,0 +1,57 @@ +import { useContext } from 'react'; +import { observer } from 'mobx-react'; +// import { stores_Context } from '../config'; +import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd'; +import SearchForm from './../search/SearchForm'; +import { bu, KPIObjects } from './../../libs/ht'; + +export default observer((props) => { + // const { } = useContext(stores_Context); + return ( + <> + + + { + // TradeStore.setStateSearch(form); + // pageRefresh(obj); + }} + /> + + + + + { + const id = String(i); + return { + ...ele, + children: `Content of tab ${id}`, + }; + })} + /> + + + + ); +}); diff --git a/src/components/search/BusinessUnitSelect.jsx b/src/components/search/BusinessUnitSelect.jsx new file mode 100644 index 0000000..1b0738c --- /dev/null +++ b/src/components/search/BusinessUnitSelect.jsx @@ -0,0 +1,36 @@ +import React, {Component} from 'react'; +import {Select} from 'antd'; +import {observer} from "mobx-react"; +import { biz, bu } from '../../libs/ht'; + + +const Business_unit = (props) => { + const { store, mode, value, onChange, show_all, ...extProps } = props; + const _show_all = ['tags', 'multiple'].includes(mode) ? false : show_all; + return ( +
+ +
+ ); +}; +/** + * HT的事业部 + */ +export default observer(Business_unit); diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index 725b5ab..cf63c57 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -1,7 +1,7 @@ -import { createContext } from 'react'; +import { createContext, useContext } from 'react'; import { toJS } from "mobx"; import { observer } from 'mobx-react'; -import { DATE_FORMAT } from './../../config'; +import { DATE_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'; @@ -32,6 +32,7 @@ const Option = Select.Option; * @property onSubmit */ export default observer((props) => { + const { date_picker_store: searchFormStore } = useContext(stores_Context); const [form] = Form.useForm(); const { sort, initialValue, hides, shows, fieldProps } = { sort: '', @@ -143,6 +144,8 @@ export default observer((props) => { Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]); console.log('form value send to onSubmit:', dest); const str = new URLSearchParams(dest).toString(); + searchFormStore.setFormValues(values); + searchFormStore.setFormValuesToSub(dest); if (typeof onSubmit === 'function') { onSubmit(null, dest, values, str); } @@ -156,6 +159,7 @@ export default observer((props) => { const onValuesChange = (...args) => { const [changedValues, allValues] = args; console.log('form onValuesChange', args); + searchFormStore.setFormValues(allValues); }; return ( diff --git a/src/stores/DatePickerStore.js b/src/stores/DatePickerStore.js index ff7ac74..9735607 100644 --- a/src/stores/DatePickerStore.js +++ b/src/stores/DatePickerStore.js @@ -1,6 +1,8 @@ import {makeAutoObservable} from "mobx"; import moment from "moment"; - +/** + * 管理搜索组件的状态 + */ class DatePickerStore { constructor(rootStore) { @@ -8,6 +10,9 @@ class DatePickerStore { makeAutoObservable(this); } + formValues = {}; + formValuesToSub = {}; + start_date = moment().startOf('week').subtract(7, 'days'); end_date = moment().endOf('week').subtract(7, 'days'); start_date_cp = false; @@ -38,8 +43,14 @@ class DatePickerStore { return [moment(this.start_date).subtract(1, 'year'), moment(this.end_date).subtract(1, 'year')]; } -} + setFormValues(data){ + this.formValues = data; + } + setFormValuesToSub(data){ + this.formValuesToSub = data; + } +} export default DatePickerStore; diff --git a/src/stores/KPI.js b/src/stores/KPI.js index a1e6296..d31f81b 100644 --- a/src/stores/KPI.js +++ b/src/stores/KPI.js @@ -1,61 +1,41 @@ import { makeAutoObservable, runInAction, toJS } from 'mobx'; import * as req from '../utils/request'; -import { isEmpty, sortBy, groupBy, cloneDeep } from '../utils/commons'; +import { isEmpty, sortBy, groupBy, cloneDeep, fixTo4Decimals, flush } from '../utils/commons'; import moment from 'moment'; -const currentYear = moment().year(); - class KPI { constructor(appStore) { this.appStore = appStore; makeAutoObservable(this); } - saveOrUpdate(x) { - console.log('ssssssssssss', x); - console.log(toJS(this.pageData)); - const tableData = this.pageData.reduce((r, curObj) => { - const allMonth = new Array(12).fill(1).map((_, index) => { - const mIndex = index+1; - const startM = moment([this.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:ss'), - value: curObj[`M${mIndex}`], - // ...(curObj[`M${mIndex}_id`] ? { kpi_id: curObj[`M${mIndex}_id`] } : {}), - kpi_id: curObj[`M${mIndex}_id`] || undefined, - key: undefined, - }; - }); - console.log('cccccccccc', allMonth); - return r.concat(allMonth); - }, []); - console.log('ppppp', tableData); + async delByID(ids) { + const data = { 'kpi_ids': ids }; + const json = await req.delJSON('/service-Analyse2/delkpi_multi', data); + return json.errcode === 0; + } + + async saveOrUpdate(tableData) { const data = { 'kpis': tableData }; - req.postJSON('/service-Analyse2/setkpi_multi/test', data).then((json) => { - // req.postJSON('/service-Analyse2/setkpi_multi', data).then((json) => { - if (json.errcode === 0) { - runInAction(() => { - console.log({ loading: false, ...json }, 'post kpi'); - }); - } - }); + const json = await req.postJSON('/service-Analyse2/setkpi_multi', data); + return json.errcode === 0; + } + + async onSubmit(tableData, { delQueue }) { + const flushData = tableData.filter(row => !isEmpty(row.value) || !isEmpty(row?.kpi_id)); + const postRes = isEmpty(flushData) ? true : await this.saveOrUpdate(flushData); + const delRes = isEmpty(flush(delQueue)) ? true : await this.delByID(delQueue); + return postRes && delRes; } - getList() { - const param = { + getList(param = {}) { + const _param = { date_type: 'applyDate', start_date: '2020-01-01', end_date: '2024-12-31 23:59:59', + ...param, }; - // return req.fetchJSON('/service-Analyse2/getkpi/test', param).then((json) => { - return req.fetchJSON('/service-Analyse2/getkpi', param).then((json) => { + return req.fetchJSON('/service-Analyse2/getkpi', _param).then((json) => { if (json.errcode === 0) { runInAction(() => { this.originData = json.result; @@ -68,12 +48,11 @@ class KPI { }); } - handleTableEdit(data) { - console.log('handle change ', data); - // this.pageData = data; + settingYear = moment().year(); + setSettingYear(v) { + this.settingYear = v; } - settingYear = 2023; settingSubject = 'sum_profit'; originData =[]; @@ -115,14 +94,15 @@ export const parseKPI = (kpis, keyArr = []) => { // r.push({ monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id }); return { monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id }; }, {}); - const kpiData = _ByFull.false.reduce((r, v) => { - r.push({ monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id, percentVal: (v.value/kpiYear.value*100) }); + const kpiData = (_ByFull?.false || []).reduce((r, v) => { + r.push({ monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id, percentVal: (fixTo4Decimals(v.value/kpiYear.value)*100) }); return r; }, []); const kpiDataMapped = kpiData.reduce((r, v) => ({...r, [`M${v.monthIndex}`]: v }), {}); const kpiDataFlat = kpiData.reduce((r, v) => ({...r, [`M${v.monthIndex}Val`]: v.value, [`M${v.monthIndex}Percent`]: v.percentVal}), {}); const { start_date, end_date, kpi_id, value, unit, monthIndex, monthRange, ...objectEle } = subjectObject[oID][0]; - return { ...cloneDeep(initialPercentKey), ...objectEle, ...kpiDataFlat, kpiData, kpiDataMapped, kpiYear, yearValue: kpiYear?.value || 0 }; + const allKey = !isEmpty(kpiData) ? kpiData.map(ek => ek.kpi_id).join('_') : `${Object.values(kpiYear).join('_')}`; + return { ...cloneDeep(initialPercentKey), ...objectEle, ...kpiDataFlat, kpiData, kpiDataMapped, kpiYear, yearValue: kpiYear?.value || 0, key: allKey }; }); ret[_subject] = afterGroup; return 1; diff --git a/src/utils/request.js b/src/utils/request.js index e9289eb..d954743 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -78,3 +78,18 @@ export function postStream(url, obj) { throw error; }); } + +export function delJSON(url, obj) { + const host = /^https?:\/\//i.test(url) ? '': HT_HOST; + return fetch(`${host}${url}`, { + method: 'DELETE', + body: JSON.stringify(obj), + headers: { + 'Content-type': 'application/json; charset=UTF-8' + } + }).then(checkStatus) + .then(response => response.json()) + .catch(error => { + throw error; + }); +} diff --git a/src/views/KPI.jsx b/src/views/KPI.jsx index 38e4dab..4ecc327 100644 --- a/src/views/KPI.jsx +++ b/src/views/KPI.jsx @@ -1,19 +1,12 @@ -import { useContext, useEffect, useState, useRef } from 'react'; -import { observer, useLocalStore } from 'mobx-react'; -import { toJS } from 'mobx'; -// import type { ProColumns } from '@ant-design/pro-components'; -import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components'; -import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd'; -import { stores_Context } from '../config'; -import { isEmpty, fixTo4Decimals, cloneDeep } from './../utils/commons'; -import SearchForm from './../components/search/SearchForm'; +import { useEffect, useState } from 'react'; +import { observer } from 'mobx-react'; +import { Row, Col, Tabs } from 'antd'; +import { KPIObjects } from './../libs/ht'; import BUPanel from './../components/kpi/BUPanel'; import OverviewPanel from './../components/kpi/OverviewPanel'; -import { KPIObjects } from './../libs/ht'; import './kpi.css'; -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 itemComponents = { + +const objectComponents = { 'overview': OverviewPanel, 'bu': BUPanel, 'dept': BUPanel, @@ -22,253 +15,31 @@ const itemComponents = { 'destination': BUPanel, 'country': BUPanel, }; -const tabsItems = KPIObjects.map((ele) => ({ - ...ele, - label: ele.label, - key: ele.value, - children: BUPanel, -})); -console.log(tabsItems); export default observer((props) => { - const [dataSource, setDataSource] = useState([]); - const { KPIStore, DictDataStore } = useContext(stores_Context); - const { settingYear } = KPIStore; - const { operator } = DictDataStore; - useEffect(() => { - // KPIStore.saveOrUpdate(); - KPIStore.getList().then((data) => { - setDataSource(data); - }); - DictDataStore.fetchDictData('operator', { is_assign: 1 }); - DictDataStore.fetchDictData('country'); - return () => {}; - }, []); - const [editOpen, setEditOpen] = useState(false); // test: - const [editableRowsKeys, setEditableRowKeys] = useState([]); - // console.log(toJS(KPIStore.pageData ), dataSource, '00000'); - const PercentInput = ({ value, onChange, record, ...extProps }) => { - // console.log(extProps, '22222222'); - const initialPercent = record.kpiDataMapped?.[`M${extProps.month}`]?.percentVal; - const [inputVal, setInputVal] = useState(value); - const calcV = inputVal ? fixTo4Decimals((Number(record?.yearValue) * inputVal) / 100) : 0; - - const handleInputChange = ({ target: { value } }) => { - setInputVal(value); - }; - const handleInputConfirm = () => { - // onChange?.(inputVal); - // setInputVal(''); - }; - return ( - - - {/* onBlur={handleInputConfirm} onPressEnter={handleInputConfirm} */} - {/* 1 */} - {calcV} - - ); - }; - const RenderInput = (row, mon) => { - // console.log(toJS(row), mon); - return ( - -
- {row.kpiDataMapped?.[`M${mon}`]?.percentVal} - % -
-
{row.kpiDataMapped?.[`M${mon}`]?.value}
-
- ); - }; - const monthCol = new Array(12).fill(1).map((_, index) => { - return { - title: `${index + 1}月`, - dataIndex: `M${index + 1}Percent`, - valueType: 'percent', - width: '6.5em', - fieldProps: { min: 0, max: 100, style: { width: '4em' } }, - renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => { - return ; - }, - render: (_, row) => RenderInput(row, index + 1), - }; - }); - const initialRow = monthCol.reduce((r, v) => ({ ...r, [v.dataIndex]: 0 }), {}); // v.formItemProps.initialValue - const columns = [ - { - title: '对象', - dataIndex: 'object_id', - valueType: 'select', - // ...valueEnum - // fieldProps: { labelInValue: true }, - render: (_, r) => r.object_name, - }, - // { - // title: 'Name', - // dataIndex: 'title', - // //...form rules - // formItemProps: { - // rules: [ - // { - // required: true, - // whitespace: true, - // message: '此项是必填项', - // }, - // ], - // }, - // }, - { - title: '年度目标', - dataIndex: 'yearValue', - valueType: 'digit', - fieldProps: { style: { width: '100%' } }, - formItemProps: { - style: { width: '100%' }, - }, - }, - ...monthCol, - // { - // title: '完成进度', - // dataIndex: 'place', - // valueType: 'percent', - // editable: false, - // width: '6em', - // }, - { - title: '操作', - valueType: 'option', - // width: 250, - render: () => { - return null; - }, - }, - ]; - const onTableChange = (...argrs) => { - console.log(argrs[0], 'who who who'); - setEditableRowKeys(argrs[0].map((ele) => ele.key)); - // KPIStore.setEditableRowsKeys(argrs[0].map((ele) => ele.key)); - setDataSource(argrs[0]); - // KPIStore.handleTableEdit(argrs[0]); - }; - const handleRadioChange = (val) => { - console.log(val, 'sss'); + // useEffect(() => { + // return () => {}; + // }, []); + const [curObject, setCurObject] = useState('overview'); + const onObjectChange = (object) => { + setCurObject(object); }; return ( <> - {/* handleRadioChange(e.target.value)} /> */} { - const ItemComponent = itemComponents[ele.key]; + const ItemComponent = objectComponents[ele.key]; return { ...ele, - children: , + children: , }; })} /> - - - { - // TradeStore.setStateSearch(form); - // pageRefresh(obj); - }} - /> - - - ({ - key: Date.now().toString(32), // dataSource.length + 1, // Number(Date.now().toString()), - ...initialRow, - object_name: '', - value: 0, - yearValue: 0, - ...cloneDeep(initialPercentKey), - }), - } - : false - } - toolBarRender={() => { - return [ - { - setEditOpen(e); - setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); - // KPIStore.setEditableRowsKeys(e ? dataSource.map((ele) => ele.key) : []); - }} - />, - , - ]; - }} - editable={{ - type: 'multiple', - editableKeys: editableRowsKeys, - actionRender: (row, config, defaultDoms) => { - // console.log(row, config, defaultDoms); - return [defaultDoms.delete]; - }, - onValuesChange: (record, recordList) => { - console.log('on edit, onValuesChange'); - onTableChange(recordList); - }, - onChange: (editableKeys, editableRows) => { - console.log('editable onValuesChange'); - onTableChange(editableRows); - // KPIStore.setEditableRowsKeys() - }, - }} - /> - {(operator?.dataSource || []).map((ele) => ( -
- {ele.label} {ele.mobile} -
- ))} ); }); diff --git a/src/views/kpi.css b/src/views/kpi.css index c652f22..ac1d799 100644 --- a/src/views/kpi.css +++ b/src/views/kpi.css @@ -1,3 +1,17 @@ .ant-tabs.ant-tabs-card > .ant-tabs-nav { margin-bottom: 0; } +.ant-tabs.ant-tabs-left .ant-tabs-content-holder{ + padding: 0; +} +.ant-tabs-content.ant-tabs-content-left .ant-tabs-tabpane.ant-tabs-tabpane-active{ + padding-left: 0; +} +.ant-tabs-content.ant-tabs-content-left .ant-tabs-tabpane .ant-pro-table .ant-pro-card-body{ + padding: 0; +} + +.ant-form-item-control-input .ant-input-affix-wrapper{ + padding-left: 4px; + padding-right: 4px; +} From 11e0684edf3aae9d1d70322b04f58c414552ef6d Mon Sep 17 00:00:00 2001 From: Lei OT Date: Thu, 21 Sep 2023 10:52:54 +0800 Subject: [PATCH 11/19] =?UTF-8?q?=E9=A6=96=E9=A1=B5=20=E5=B9=B4=E5=BA=A6?= =?UTF-8?q?=20=E6=97=B6=E9=97=B4=E8=BD=B4=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...{DateFieldRadio.jsx => DataFieldRadio.jsx} | 4 +- src/components/DateGroupRadio/date.js | 4 +- src/components/DateGroupRadio/index.jsx | 4 ++ src/components/StatisticCard.jsx | 11 ++-- src/stores/Trade.js | 19 +++--- src/views/Home.jsx | 61 ++++++++++++------- 6 files changed, 63 insertions(+), 40 deletions(-) rename src/components/{DateFieldRadio.jsx => DataFieldRadio.jsx} (87%) diff --git a/src/components/DateFieldRadio.jsx b/src/components/DataFieldRadio.jsx similarity index 87% rename from src/components/DateFieldRadio.jsx rename to src/components/DataFieldRadio.jsx index b14047f..773ec5d 100644 --- a/src/components/DateFieldRadio.jsx +++ b/src/components/DataFieldRadio.jsx @@ -1,11 +1,11 @@ import { observer } from 'mobx-react'; import { Radio, Select } from 'antd'; -import { dataFieldOptions } from './../libs/ht'; +import { dataFieldOptions } from '../libs/ht'; export default observer((props) => { const { visible, dataRaw, dataMapper, fieldMapper, onChange, ...extProps } = props; const handleChange = (value) => { - console.log('handleChange', value); + // console.log('handleChange', value); if (typeof onChange === 'function') { onChange(value); } diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index 4e367f9..adb75fb 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -4,7 +4,7 @@ export const datePartOptions = [ { label: '日', value: 'day' }, { label: '周', value: 'week' }, { label: '月', value: 'month' }, - { label: '季', value: 'quarter' }, + { label: '季', value: 'season' }, { label: '年', value: 'year' }, ]; export const datePartMethod = { @@ -20,7 +20,7 @@ export const datePartMethod = { const dateKey = `${year}-W${String(week).padStart(2, '0')}`; return { dateKey, 'groupKey': dateKey, date }; }, - 'quarter': (date) => { + 'season': (date) => { const dateO = moment(date); // const key = dateO.format('YYYY-Q'); const key = `${dateO.year()}-${String(dateO.quarter()).padStart(2, 'Q')}`; diff --git a/src/components/DateGroupRadio/index.jsx b/src/components/DateGroupRadio/index.jsx index e8b1fd2..16a325f 100644 --- a/src/components/DateGroupRadio/index.jsx +++ b/src/components/DateGroupRadio/index.jsx @@ -2,6 +2,10 @@ import { Radio } from 'antd'; import { observer } from 'mobx-react'; import { datePartOptions, resultDataCb } from './date'; +/** + * 仅前端转换数据 + * 把按天的数据转换成 按周,季,月,年 + */ export default observer((props) => { const { visible, dataRaw, dataMapper, fieldMapper, onChange, ...extProps } = props; const _dataMapper = dataMapper || { 'data1': null, data2: null }; diff --git a/src/components/StatisticCard.jsx b/src/components/StatisticCard.jsx index 9a771f8..5847223 100644 --- a/src/components/StatisticCard.jsx +++ b/src/components/StatisticCard.jsx @@ -1,13 +1,15 @@ -import { observer } from "mobx-react"; +import { observer } from 'mobx-react'; import { Card, Statistic, Progress } from 'antd'; import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; export default observer((props) => { - const valueStyle = { color: props.VSrate < 0 ? '#3f8600' : '#cf1322' }; - const VSIcon = () => (props.VSrate < 0 ? : ); + const ValueIcon = props.icon; + const valueStyle = { color: (props?.VSrate || -1) < 0 ? '#3f8600' : '#cf1322' }; + const VSIcon = () => ((props?.VSrate || -1) < 0 ? : ); return ( { ) } - {...props} + prefix={} + // title={{props.title}} /> {props.showProgress !== false && `${props.KPIrate}%`} />} diff --git a/src/stores/Trade.js b/src/stores/Trade.js index a726e92..e165dae 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -28,7 +28,8 @@ class Trade { value: summary?.ConfirmOrder, // VSrate: summary?.ConfirmOrderrate, KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p], - hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]), + // hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]), + hasKPI: false }, { title: '毛利', value: summary?.SumML, KPIrate: summary?.[dataFieldAlias.SumML.nestkey.p], hasKPI: false }, { title: '完成率', value: `${summary?.[dataFieldAlias.SumML.nestkey.p] || ''}%`, hasKPI: false }, @@ -54,7 +55,8 @@ class Trade { */ fetchTradeDataByDate(queryData) { this.timeData.loading = true; - Object.assign(queryData, { groupType: 'overview', groupDateType: 'week' }); + queryData = queryData || this.searchPayloadHome; + Object.assign(queryData, { groupType: 'overview', groupDateType: this.timeLineKey }); this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { @@ -92,16 +94,11 @@ class Trade { */ fetchTradeDataByMonth(queryData) { this.sideData.loading = true; - // todo: groupType: bizarea - // Object.assign(queryData, { groupType: 'bu', groupDateType: 'month' }); Object.assign(queryData, { groupType: 'bizarea', groupDateType: 'month' }); this.fetchTradeData(queryData).then((json) => { if (json.errcode === 0) { runInAction(() => { const sortResult = json.result1.sort(sortBy('groupDateVal')); - /** - * test: '91006' - */ const groupsData = sortResult.reduce((r, v) => { if (v.groupsLabel ) { // && ['91001', '91006'].includes(v.groupsKey) (r[v.groupsLabel] || (r[v.groupsLabel] = [])).push(v); @@ -111,8 +108,6 @@ class Trade { this.sideData.loading = false; this.sideData.dataSource = groupsData; this.sideData.monthData = sortResult; - // const kpi = { label: '', value: 1200000 }; // 标注KPI - // this.sideData.kpi = kpi; }); } }); @@ -149,13 +144,17 @@ class Trade { this.searchPayloadHome = body; } + timeLineKey = 'week'; + setTimeLineKey(v) { + this.timeLineKey = v; + } + summaryData = { loading: false, dataSource: [], kpi: {}, }; timeData = { loading: false, dataSource: [] }; BuData = { loading: false, dataSource: [] }; sideData = { loading: false, dataSource: {}, monthData: [] }; dataForSort = {}; topData = {}; - defaultDataSubject = 'CJCount'; searchPayloadHome = {}; } diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 9109c48..07a9b72 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -1,30 +1,36 @@ import { useContext, useEffect, useState } from 'react'; import { observer } from 'mobx-react'; -import { Row, Col, Spin, Space } from 'antd'; +import { Row, Col, Spin, Space, Radio } from 'antd'; +import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone, } from '@ant-design/icons'; import { stores_Context } from '../config'; import { useNavigate } from 'react-router-dom'; import StatisticCard from '../components/StatisticCard'; import Bullet from '../components/BulletWithSort'; import Waterfall from '../components/Waterfall'; -import DataFieldRadio from './../components/DateFieldRadio'; +import DataFieldRadio from '../components/DataFieldRadio'; +import { datePartOptions } from './../components/DateGroupRadio/date'; import SearchForm from './../components/search/SearchForm'; -import { empty, cloneDeep } from './../utils/commons'; +import { empty, cloneDeep, isEmpty } from './../utils/commons'; import { dataFieldAlias } from './../libs/ht'; import { Line } from '@ant-design/charts'; import './home.css'; -export default observer(() => { - const navigate = useNavigate(); - const { TradeStore } = useContext(stores_Context); - const { searchPayloadHome, sideData, summaryData, BuData, topData, timeData } = TradeStore; +const topSeries = [ + { key: 'country', label: '国籍' }, + { key: 'dept', label: '小组' }, + { key: 'operator', label: '顾问' }, + { key: 'destination', label: '目的地' }, + // { key: 'GuestGroupType', label: '客群类别' }, +]; + +// const iconSets = [CheckCircleTwoTone, , , , ,,]; +const iconSets = [CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone]; - const topSeries = [ - { key: 'country', label: '国籍' }, - { key: 'dept', label: '小组' }, - { key: 'operator', label: '顾问' }, - { key: 'destination', label: '目的地' }, - // { key: 'GuestGroupType', label: '客群类别' }, - ]; +export default observer(() => { + // const navigate = useNavigate(); + const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context); + const { sideData, summaryData, BuData, topData, timeData, timeLineKey } = TradeStore; + const { formValues, } = searchFormStore; useEffect(() => { if (empty(summaryData.dataSource)) { @@ -137,6 +143,14 @@ export default observer(() => { }, }); }; + const [dateField, setDateField] = useState(timeLineKey); + const handleChangeDateType = ({target: {value}}) => { + setDateField(value); + TradeStore.setTimeLineKey(value); + if (!isEmpty(TradeStore.searchPayloadHome)) { + TradeStore.fetchTradeDataByDate(); + } + }; return ( <> @@ -145,7 +159,7 @@ export default observer(() => { { }, }} onSubmit={(_err, obj, form, str) => { - TradeStore.setStateSearch(form); + TradeStore.setStateSearch(obj); pageRefresh(obj); }} /> @@ -167,9 +181,9 @@ export default observer(() => { - {summaryData.dataSource.map((item) => ( + {summaryData.dataSource.map((item, i) => ( - + ))} @@ -177,11 +191,13 @@ export default observer(() => {
-

趋势

+

走势

-
+
- + + +

市场进度

@@ -189,11 +205,12 @@ export default observer(() => { +

{`各事业部总业绩`}

{Object.keys(sideData.dataSource).map((key) => ( -

{key}

+

{`${key}每月业绩`}

))}
From 87cd4f6caa8e9a3cae9cf57d6f8aedd00a90b66d Mon Sep 17 00:00:00 2001 From: Lei OT Date: Thu, 21 Sep 2023 14:42:35 +0800 Subject: [PATCH 12/19] =?UTF-8?q?perf(=E6=90=9C=E7=B4=A2=E7=BB=84=E4=BB=B6?= =?UTF-8?q?):=20=E8=A1=A5=E5=85=85=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/SearchForm.jsx | 49 ++++++---------- src/libs/ht.js | 88 ++++++++++++++++------------ 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index cf63c57..1a4a40a 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -1,8 +1,8 @@ import { createContext, useContext } from 'react'; -import { toJS } from "mobx"; +import { toJS } from 'mobx'; import { observer } from 'mobx-react'; import { DATE_FORMAT, stores_Context } from './../../config'; -import { SearchOutlined, } from "@ant-design/icons"; +import { SearchOutlined } from '@ant-design/icons'; import { Form, Row, Col, Select, Button, Space, DatePicker } from 'antd'; import moment from 'moment'; // import locale from 'antd/es/date-picker/locale/zh_CN'; @@ -52,6 +52,13 @@ export default observer((props) => { transform: (value) => value?.key || '', default: '', }, + 'HTBusinessUnits': { + key: 'HTBusinessUnits', + transform: (value) => { + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '-1'; + }, + default: '', + }, 'businessUnits': { key: 'businessUnits', transform: (value) => { @@ -69,7 +76,7 @@ export default observer((props) => { 'WebCode': { key: 'WebCode', transform: (value) => { - return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (value.key) : 'ALL'; + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : 'ALL'; }, default: '', }, @@ -131,6 +138,11 @@ export default observer((props) => { default: '', }, ], + 'country': { + key: 'country', + transform: (value) => value?.key || '', + default: '', + }, }; let dest = {}; const { applyDate, applyDate2, year, yearDiff, ...omittedValue } = values; @@ -169,7 +181,7 @@ export default observer((props) => { {getFields({ sort, initialValue, hides, shows, fieldProps, form })} {/* 'textAlign': 'right' */} - + , - // ]; - // }} - editable={{ - type: 'multiple', - editableKeys: editableRowsKeys, - actionRender: (row, config, defaultDoms) => { - // console.log(row, config, defaultDoms); - return [defaultDoms.delete]; - }, - onValuesChange: (record, recordList) => { - console.log('on edit, onValuesChange'); - onTableChange(recordList); - }, - onChange: (editableKeys, editableRows) => { - console.log('editable onValuesChange'); - onTableChange(editableRows); - // KPIStore.setEditableRowsKeys() - }, - }} - /> - ), + children: }; })} /> diff --git a/src/components/kpi/ObjectPanel.jsx b/src/components/kpi/ObjectPanel.jsx new file mode 100644 index 0000000..a78c231 --- /dev/null +++ b/src/components/kpi/ObjectPanel.jsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; +import { observer } from 'mobx-react'; +// import { stores_Context } from '../config'; +import { Table } from 'antd'; +import KPISettings from './KPISettings'; +import { bu, KPISubjects } from '../../libs/ht'; + +const searchFormItemSet = { + 'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'] }, + 'dept': { shows: ['DateType', 'years', 'DepartmentList'], fieldProps: { DepartmentList: { allowClear: true } } }, + 'operator': { shows: ['DateType', 'years', 'DepartmentList'] }, // , 'operator' + 'destination': { shows: ['DateType', 'years', 'destination'] }, + 'country': { shows: ['DateType', 'years', 'country'] }, +}; + +export default observer((props) => { + const searchProps = searchFormItemSet?.[props.curObject] || {}; + return ( + <> + + + ); +}); diff --git a/src/components/kpi/SubjectTable/Profit.jsx b/src/components/kpi/SubjectTable/Profit.jsx new file mode 100644 index 0000000..777adb4 --- /dev/null +++ b/src/components/kpi/SubjectTable/Profit.jsx @@ -0,0 +1,252 @@ +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 = 10) => { + return fixTo2Decimals(number / (1000 * scale)) + '万'; +}; + +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); + setEditOpen(false); + return () => {}; + }, [KPIStore.pageData]); + + + const PercentInput = useMemo( + () => + // eslint-disable-next-line react/display-name + ({ value, onChange, record, ...extProps }) => { + // // eslint-disable-next-line react-hooks/rules-of-hooks + const [inputValue, setInputValue] = useState(value); + const handleInputChange = (e) => { + setInputValue(e.target.value); + onChange?.(e.target.value); + }; + const calcV = inputValue ? numberConvert10K(fixToInt((Number(record?.yearValue) * inputValue) / 100)) : 0; + return ( + + + {calcV} + + ); + }, + [] + ); + const RenderMonthCell = (row, mon) => { + return ( + +
+ {fixTo2Decimals(row?.[`M${mon}Percent`])} + % +
+
{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}
+
+ ); + }; + const monthCol = new Array(12).fill(1).map((_, index) => { + return { + title: `${index + 1}月`, + dataIndex: `M${index + 1}Percent`, + valueType: 'digit', + width: '6.5em', + // fieldProps: { min: 0, max: 100, style: { width: '4em' } }, + renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => { + return ; + }, + render: (_, row) => RenderMonthCell(row, index + 1), + }; + }); + const columns = [ + { + title: curObjectItem.label, + dataIndex: 'object_id', + editable: false, + render: (_, r) => r.object_name, + }, + { + title: '年度目标', + dataIndex: 'yearValue', + valueType: 'digit', + fieldProps: { style: { width: '100%' }, step: 10000*100 }, + formItemProps: { + style: { width: '100%' }, + }, + }, + ...monthCol, + { + title: ( + + 操作 + { + makeInitialTable(e); + }} + /> + + ), + valueType: 'option', + // width: 250, + render: () => { + return null; + }, + }, + ]; + + const onTableChange = (...argrs) => { + setEditableRowKeys(argrs[0].map((ele) => ele.key)); + setDataSource(argrs[0]); + }; + const onTableSubmit = () => { + const tableData = dataSource.reduce((r, curObj) => { + const allMonth = new Array(12).fill(1).map((_, index) => { + const mIndex = index + 1; + const mVal = (Number(curObj.yearValue) * Number(curObj[`M${mIndex}Percent`])) / 100; + const startM = moment([KPIStore.settingYear, index, 1]); + const pick = (({ object, object_name, object_id, subject, date_type }) => ({ + object, + object_name, + object_id, + subject, + date_type, + }))(curObj); + return { + ...pick, + start_date: startM.format('YYYY-MM-DD'), + end_date: startM.endOf('M').format('YYYY-MM-DD HH:mm'), + value: mVal, + kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined, + key: undefined, + }; + }); + 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, + }))(dataSource?.[0] || {}); + tableData.unshift(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 * 100, + object: curObject, + object_name: '', + object_id: -1, + subject: 'sum_profit', + date_type: searchFormStore.formValuesToSub.DateType, + kpiDataMapped: {}, + key: Date.now().toString(32), + }), + {} + ); // v.formItemProps.initialValue + const makeInitialTable = (e) => { + setEditOpen(e); + // todo: 单独设置之后, 清空筛选会导致无法批量设置新的 + if (e && isEmpty(dataSource)) { + const _initialRow = Object.assign({}, initialRow, initialPercentKey); + const _objects = isEmpty(objects) ? (curObjectItem?.data || []) : objects; + console.log('ooo', objects, isEmpty(objects), curObjectItem?.data || []); + const _initialTable = _objects.map((obj) => ({ + ...cloneDeep(_initialRow), + object_name: obj.label, + object_id: obj.value, + key: Date.now().toString(32) + obj.value, + })); + console.log(_initialRow, 'iiiii'); + setDataSource(_initialTable); + setEditableRowKeys(_initialTable.map((ele) => ele.key)); + return false; + } + setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []); + }; + const [delKpiIds, setDelKpiIds] = useState([]); + return ( + <> + + + { + return [defaultDoms.delete]; + }, + onDelete: (_key, _row) => { + // console.log('del', _key, _row); + const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id); + rowKpiIds.push(_row?.kpiYear?.kpi_id); + setDelKpiIds(rowKpiIds); + }, + onValuesChange: (record, recordList) => { + // console.log('on edit, onValuesChange',record, recordList); + onTableChange(recordList); + }, + onChange: (editableKeys, editableRows) => { + setEditableRowKeys(editableKeys); + }, + }} + /> + + + + + + + ); +}); diff --git a/src/stores/KPI.js b/src/stores/KPI.js index d31f81b..aa3b922 100644 --- a/src/stores/KPI.js +++ b/src/stores/KPI.js @@ -34,10 +34,14 @@ class KPI { start_date: '2020-01-01', end_date: '2024-12-31 23:59:59', ...param, + object_id: [0, 1, 'ALL'].includes(param?.object_id || 0) ? '' : param.object_id, }; + this.listLoading = true; + this.pageData = []; return req.fetchJSON('/service-Analyse2/getkpi', _param).then((json) => { if (json.errcode === 0) { runInAction(() => { + this.listLoading = false; this.originData = json.result; const yearData = parseKPI(json.result, ['subject', 'object_id']); console.log(111, yearData, yearData[this.settingYear]); @@ -53,7 +57,15 @@ class KPI { this.settingYear = v; } + listLoading = false; + setListLoading(v) { + this.listLoading = v; + } + settingSubject = 'sum_profit'; + setSettingSubject(v) { + this.settingSubject = v; + } originData =[]; pageData = []; diff --git a/src/views/KPI.jsx b/src/views/KPI.jsx index 4ecc327..288e041 100644 --- a/src/views/KPI.jsx +++ b/src/views/KPI.jsx @@ -1,27 +1,72 @@ -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; +import { stores_Context } from './../config'; import { observer } from 'mobx-react'; -import { Row, Col, Tabs } from 'antd'; +import { Row, Col, Tabs, Spin } from 'antd'; +import { flush, objectMapper } from './../utils/commons'; import { KPIObjects } from './../libs/ht'; -import BUPanel from './../components/kpi/BUPanel'; +import ObjectPanel from '../components/kpi/ObjectPanel'; import OverviewPanel from './../components/kpi/OverviewPanel'; import './kpi.css'; const objectComponents = { 'overview': OverviewPanel, - 'bu': BUPanel, - 'dept': BUPanel, - 'du': BUPanel, - 'operator': BUPanel, - 'destination': BUPanel, - 'country': BUPanel, + 'bu': ObjectPanel, + 'dept': ObjectPanel, + 'du': ObjectPanel, + 'operator': ObjectPanel, + 'destination': ObjectPanel, + 'country': ObjectPanel, +}; +const objectFilterKey = { + 'bu': 'HTBusinessUnits', + 'dept': 'DepartmentList', + // 'du': 'du', + // 'operator': 'operator', + // 'destination': 'destination', + // 'country': 'country', }; export default observer((props) => { + const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context); // useEffect(() => { // return () => {}; // }, []); const [curObject, setCurObject] = useState('overview'); const onObjectChange = (object) => { setCurObject(object); + setRetObjects([]); + }; + useEffect(() => { + onSearchSubmit(searchFormStore.formValuesToSub); + + return () => {}; + }, [curObject]); + + const [retObjects, setRetObjects] = useState([]); + const onSearchSubmit = (obj, formVal={}) => { + const getkpiParam = objectMapper(obj, { + DateType: { key: 'date_type' }, + Date1: { key: 'start_date' }, + Date2: { key: 'end_date' }, + HTBusinessUnits: { key: 'object_id' }, + DepartmentList: { key: 'object_id' }, + businessUnits: { key: 'object_id' }, + WebCode: { key: 'object_id' }, + operator: { key: 'object_id' }, + country: { key: 'object_id' }, + }); + Object.assign(getkpiParam, { object: curObject }); + KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear); + console.log('invoke on search', obj, formVal, getkpiParam); + KPIStore.getList(getkpiParam).then((data) => { + // setDataSource(data); + if (objectFilterKey?.[curObject]) { + const selectItem = searchFormStore.formValues[objectFilterKey[curObject]]; + if (selectItem) { + selectItem.value = selectItem.key; + } + setRetObjects(flush([selectItem])); + } + }); }; return ( <> @@ -31,10 +76,14 @@ export default observer((props) => { onChange={onObjectChange} type="card" items={KPIObjects.map((ele, i) => { - const ItemComponent = objectComponents[ele.key]; + const ObjectItemPanel = objectComponents[ele.key]; return { ...ele, - children: , + children: ( + + + + ), }; })} /> From a56ff400e7d92b19f4c7196d019fd962b765f671 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Thu, 21 Sep 2023 16:25:23 +0800 Subject: [PATCH 14/19] =?UTF-8?q?fix:=20=E9=BB=98=E8=AE=A4=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E7=9A=84=E5=80=BC=20=20=E4=B8=8D=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stores/KPI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/KPI.js b/src/stores/KPI.js index aa3b922..40e5b69 100644 --- a/src/stores/KPI.js +++ b/src/stores/KPI.js @@ -34,7 +34,7 @@ class KPI { start_date: '2020-01-01', end_date: '2024-12-31 23:59:59', ...param, - object_id: [0, 1, 'ALL'].includes(param?.object_id || 0) ? '' : param.object_id, + object_id: [0, -1, 'ALL'].includes(param?.object_id || 0) ? '' : param.object_id, }; this.listLoading = true; this.pageData = []; From 2cac46436327a5f336572a38894de043d115a3c5 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 22 Sep 2023 10:53:05 +0800 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20=E7=BB=9F=E8=AE=A1=E5=88=86?= =?UTF-8?q?=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 5 +- src/stores/DictData.js | 1 + src/stores/Distribution.js | 61 +++++++++++++++++++ src/stores/Index.js | 2 + src/views/Distribution.jsx | 118 +++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/stores/Distribution.js create mode 100644 src/views/Distribution.jsx diff --git a/src/App.jsx b/src/App.jsx index 28f12ec..1ce1708 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,7 +10,7 @@ import { DollarOutlined, AreaChartOutlined, WechatOutlined, - UserOutlined, FlagOutlined, + UserOutlined, FlagOutlined, PieChartOutlined } from '@ant-design/icons'; import { Layout, Menu, Image, Spin } from 'antd'; import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom'; @@ -36,6 +36,7 @@ import { stores_Context } from './config'; import { observer } from 'mobx-react'; import ExchangeRate from './charts/ExchangeRate'; import KPI from './views/KPI'; +import Distribution from './views/Distribution'; const App = () => { const { Content, Footer, Sider } = Layout; @@ -121,6 +122,7 @@ const App = () => { ], }, { key: 'kpi', label: 目标, icon: }, + { key: 'distribution', label: 统计分布, icon: }, ]; return ( @@ -162,6 +164,7 @@ const App = () => { } /> } /> + } /> }> } /> } /> diff --git a/src/stores/DictData.js b/src/stores/DictData.js index 916d8a5..cdef2cd 100644 --- a/src/stores/DictData.js +++ b/src/stores/DictData.js @@ -56,6 +56,7 @@ class DictData { data = {}; operator = { loading: false, dataSource: [] }; + country = { loading: false, dataSource: [] }; vendor = { loading: false, dataSource: [] }; creditcardbilltype = { loading: false, dataSource: [] }; } diff --git a/src/stores/Distribution.js b/src/stores/Distribution.js new file mode 100644 index 0000000..e583983 --- /dev/null +++ b/src/stores/Distribution.js @@ -0,0 +1,61 @@ +import { makeAutoObservable, runInAction, toJS } from 'mobx'; +import * as req from '../utils/request'; +import { isEmpty, 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); + } + + async getData(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]; + + } + + resetData() { + 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; + + tourDays = { loading: false, dataSource: [] }; + PML = { loading: false, dataSource: [] }; + ConfirmDays = { loading: false, dataSource: [] }; + ApplyDays = { loading: false, dataSource: [] }; + PersonNum = { loading: false, dataSource: [] }; + destination = { loading: false, dataSource: [] }; + GlobalDestination = { loading: false, dataSource: [] }; +} +export default Distribution; diff --git a/src/stores/Index.js b/src/stores/Index.js index 0d38106..48dfba1 100644 --- a/src/stores/Index.js +++ b/src/stores/Index.js @@ -13,6 +13,7 @@ import CustomerServicesStore from "./CustomerServices"; import TradeStore from "./Trade"; import KPI from "./KPI"; import DictData from "./DictData"; +import Distribution from "./Distribution"; class Index { constructor() { this.dashboard_store = new DashboardStore(this); @@ -29,6 +30,7 @@ class Index { this.TradeStore = new TradeStore(this); this.KPIStore = new KPI(this); this.DictDataStore = new DictData(this); + this.DistributionStore = new Distribution(this); makeAutoObservable(this); } diff --git a/src/views/Distribution.jsx b/src/views/Distribution.jsx new file mode 100644 index 0000000..68a07f0 --- /dev/null +++ b/src/views/Distribution.jsx @@ -0,0 +1,118 @@ +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 { RingProgress } from '@ant-design/plots'; +import SearchForm from './../components/search/SearchForm'; +import "./kpi.css"; +import { empty } from '../utils/commons'; + +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((props) => { + const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context); + const { formValues, formValuesToSub } = searchFormStore; + const { curTab } = DistributionStore; + const pageRefresh = (obj) => { + DistributionStore.getData({ + DateType: 'applyDate', + Date1: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'), + Date2: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'), + ...(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, + // percent: Number(_)/100, + color: ['#5B8FF9', '#E8EDF3'], + }; + const columns = [ + { + title: '', + dataIndex: 'label', + }, + { title: '团数', dataIndex: 'ConfirmOrder'}, + { title: '业绩', dataIndex: 'SumML', render1: (v) => `1`}, + { title: '团数占比', dataIndex: 'ConfirmOrderPercent', render: (v) => }, + { title: '业绩占比', dataIndex: 'SumMLPercent', render: (v) => }, + ]; + return ( + <> + + {/* style={{ margin: '-16px -8px', padding: 0 }} */} + + { + pageRefresh(obj); + }} + /> + + +
+ { + // const ObjectItemPanel = objectComponents[ele.key]; + return { + ...ele, + children: ( + + {/* */} + + record.label} + loading={DistributionStore[curTab].loading} + pagination={false} + scroll={{ x: '100%' }} + /> + + ), + }; + })} + /> + + + ); +}); From d574f59111c564599045aff3f027840622c1b01d Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 22 Sep 2023 10:53:34 +0800 Subject: [PATCH 16/19] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/SearchForm.jsx | 11 ++++++++--- src/stores/DatePickerStore.js | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index 1a4a40a..9dca78c 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext, useEffect } from 'react'; import { toJS } from 'mobx'; import { observer } from 'mobx-react'; import { DATE_FORMAT, stores_Context } from './../../config'; @@ -14,7 +14,7 @@ import DateTypeSelect from './DataTypeSelect'; import DatePickerCharts from './DatePickerCharts'; import YearPickerCharts from './YearPickerCharts'; import SearchInput from './Input'; -import { objectMapper, at } from './../../utils/commons'; +import { objectMapper, at, empty } from './../../utils/commons'; import './search.css'; @@ -44,7 +44,7 @@ export default observer((props) => { }; const { onSubmit, confirmText } = props; - const onFinish = (values) => { + const formValuesMapper = (values) => { console.log('Received values of form, origin form value: ', values); const destinationObject = { 'DateType': { @@ -155,6 +155,11 @@ export default observer((props) => { // omit empty Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]); console.log('form value send to onSubmit:', dest); + return dest; + }; + + const onFinish = (values) => { + const dest = formValuesMapper(values); const str = new URLSearchParams(dest).toString(); searchFormStore.setFormValues(values); searchFormStore.setFormValuesToSub(dest); diff --git a/src/stores/DatePickerStore.js b/src/stores/DatePickerStore.js index 9735607..d4f24eb 100644 --- a/src/stores/DatePickerStore.js +++ b/src/stores/DatePickerStore.js @@ -10,8 +10,22 @@ class DatePickerStore { makeAutoObservable(this); } - formValues = {}; - formValuesToSub = {}; + formValues = { + 'DepartmentList': { 'key': 'ALL', 'label': '所有小组' }, + 'WebCode': { 'key': 'ALL', 'label': '所有来源' }, + 'IncludeTickets': { 'key': '1', 'label': '含门票' }, + 'DateType': { 'key': 'applyDate', 'label': '提交日期' }, + 'year': moment(), + }; + + formValuesToSub = { + DepartmentList: 'ALL', + WebCode: 'ALL', + IncludeTickets: '1', + DateType: 'applyDate', + Date1: moment().startOf('year').format('YYYY-MM-DD'), + Date2: moment().endOf('year').format('YYYY-MM-DD 23:59'), + }; start_date = moment().startOf('week').subtract(7, 'days'); end_date = moment().endOf('week').subtract(7, 'days'); From 16b1f7f44b3ffb7365096e17307a9dea16909e25 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 22 Sep 2023 11:52:20 +0800 Subject: [PATCH 17/19] style: BulletWithSort xAxis label --- src/components/BulletWithSort.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/BulletWithSort.jsx b/src/components/BulletWithSort.jsx index b10572e..a2d7c73 100644 --- a/src/components/BulletWithSort.jsx +++ b/src/components/BulletWithSort.jsx @@ -48,6 +48,10 @@ export default observer((props) => { }, xAxis: { line: null, + label: { + autoHide: false, + autoRotate: true, + }, }, yAxis: false, // 自定义 legend From 6f4d14033613307b163c54b7ae2b55d373d93ab5 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 22 Sep 2023 13:26:41 +0800 Subject: [PATCH 18/19] =?UTF-8?q?fix:=20=E8=AE=A2=E5=8D=95=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=AF=B9=E6=AF=94=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DateGroupRadio/date.js | 7 +++-- src/stores/OrdersStore.js | 43 +++++++++++++++++++++++---- src/views/Orders.jsx | 2 +- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index adb75fb..6d08642 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -117,7 +117,8 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, })); const parse2 = parseDateType(_data2, dateGroup, fieldMapper); const parseData2 = parse2.data.map((ele) => ({ - [fieldMapper.dateKey]: ele.groupKey, + [fieldMapper.dateKey]: ele[fieldMapper.dateKey], + // [fieldMapper.dateKey]: ele.groupKey, [fieldMapper.valueKey]: ele.value, [fieldMapper.seriesKey]: ele[fieldMapper.seriesKey], groups: _data2[0].groups, @@ -126,8 +127,8 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, dateGroup: ele[fieldMapper.dateKey], })); const useKeys = parseData1.map((ele) => ele[fieldMapper.dateKey]); - const reindecData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `X.${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey })); - const retData = [].concat(parseData1, reindecData2); + const reindexData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `_${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey })); + const retData = [].concat(parseData1, reindexData2 ); const avg1 = parse1.avgVal; // console.log('callback', dateGroup, retData); cb(dateGroup, retData, avg1); diff --git a/src/stores/OrdersStore.js b/src/stores/OrdersStore.js index 50ab2a4..e8baed2 100644 --- a/src/stores/OrdersStore.js +++ b/src/stores/OrdersStore.js @@ -1,4 +1,4 @@ -import { makeAutoObservable, runInAction } from "mobx"; +import { makeAutoObservable, runInAction, toJS } from "mobx"; import { CaretUpOutlined, CaretDownOutlined } from "@ant-design/icons"; import { Tag } from "antd"; import * as config from "../config"; @@ -121,16 +121,46 @@ class OrdersStore { (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); return r; }, {}); - this.orderCountData = Object.keys(groupByDate) + const _data = Object.keys(groupByDate) .reduce((r, _d) => { - const summaryVal = groupByDate[_d].reduce((rows, row) => rows + row.orderCount, 0); - r.push({ ...groupByDate[_d][0], orderCount: summaryVal }); + const xAxisGroup = groupByDate[_d].reduce((a, v) => { + (a[v.groups] || (a[v.groups] = [])).push(v); + return a; + }, {}); + Object.keys(xAxisGroup).map((_group) => { + const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal }); + return _group; + }); return r; }, []) .map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups })); + this.orderCountData = _data; this.avgLine1 = avg1; }; + summaryAllWebcode = (json) => { + const groupByDate1 = json.ordercount1.reduce((r, v) => { + (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); + return r; + }, {}); + const ordercount1 = Object.keys(groupByDate1).reduce((r, _date) => { + const summaryVal = groupByDate1[_date].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...groupByDate1[_date][0], orderCount: summaryVal }); + return r; + }, []); + const groupByDate2 = json.ordercount2.reduce((r, v) => { + (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); + return r; + }, {}); + const ordercount2 = Object.keys(groupByDate2).reduce((r, _date) => { + const summaryVal = groupByDate2[_date].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...groupByDate2[_date][0], orderCount: summaryVal }); + return r; + }, []); + return { ...json, ordercount1, ordercount2 }; + }; + parseOrderCount = (orderCountData, dateGroup) => { resultDataCb(orderCountData, dateGroup, this.orderCountDataMapper, this.orderCountDataFieldMapper, this.onChangeDateGroup); }; @@ -175,9 +205,10 @@ class OrdersStore { .then(response => response.json()) .then(json => { runInAction(() => { - this.orderCountDataRaw = json; + const data = this.summaryAllWebcode(json); + this.orderCountDataRaw = data; // 第一次得到数据 - this.parseOrderCount(json, 'day'); + this.parseOrderCount(data, 'day'); this.loading = false; }); }) diff --git a/src/views/Orders.jsx b/src/views/Orders.jsx index 50588cf..917a8d5 100644 --- a/src/views/Orders.jsx +++ b/src/views/Orders.jsx @@ -272,7 +272,7 @@ class Orders extends Component { tooltip: { customItems: (originalItems) => { // process originalItems, - return originalItems.map(ele => ({...ele, name: ele.data?.seriesKey || ele.data?.xField})); + return originalItems.map(ele => ({...ele, name: ele.data?.seriesField || ele.data?.xField})); }, title: (title, datum) => { let ret = title; From 9b982aa243e0dae85bafc3760a0c6d8a67eafdf0 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 22 Sep 2023 13:58:33 +0800 Subject: [PATCH 19/19] style: --- src/components/StatisticCard.jsx | 3 ++- src/libs/ht.js | 2 +- src/stores/Trade.js | 1 + src/views/Home.jsx | 7 +++++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/StatisticCard.jsx b/src/components/StatisticCard.jsx index 5847223..e6fe898 100644 --- a/src/components/StatisticCard.jsx +++ b/src/components/StatisticCard.jsx @@ -9,7 +9,6 @@ export default observer((props) => { return ( { } prefix={} // title={{props.title}} + {...props} + value={props.valueSuffix ? `${props.value} ${props.valueSuffix}` : props.value} /> {props.showProgress !== false && `${props.KPIrate}%`} />} diff --git a/src/libs/ht.js b/src/libs/ht.js index 2c0bbaf..b8b1cbf 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -96,7 +96,7 @@ export const dataFieldOptions = [ { label: '毛利', value: 'SumML', formatter: (v) => `${v / 10000} 万`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } }, { label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } }, { label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } }, - { label: '成交率', value: 'ConfirmRates', formatter: (v) => v, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, + { label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } }, // { label: '人数', value: 'CJPersonNum', formatter: (v) => v }, // todo: more... ]; diff --git a/src/stores/Trade.js b/src/stores/Trade.js index e165dae..0806e18 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -26,6 +26,7 @@ class Trade { { title: '成团', value: summary?.ConfirmOrder, + valueSuffix: summary?.ConfirmRates ? ` / ${summary.ConfirmRates} %` : undefined, // VSrate: summary?.ConfirmOrderrate, KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p], // hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]), diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 07a9b72..409955b 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -102,6 +102,13 @@ export default observer(() => { targetField: 'MLKPIvalue', // xField: 'groupsLabel', layout: 'vertical', + xAxis: { + line: null, + label: { + autoHide: true, + autoRotate: false, + }, + } }; const lineConfigSet = {