From 836d4b9597dc1a6ae2654e6f7dec1ebeedff70a2 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Sat, 9 Sep 2023 02:00:15 +0800 Subject: [PATCH] =?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}
)} + + ); +});