先看实现的成果:
次要性能点:反对传入分组的组数,反对传入不同的色彩色条,可拖动左侧滑块扭转占比,可通过输出右侧input框实现更改占比,反对百分比和数值两种显示模式
间接上代码:正文写的很具体了:
import React, { useEffect, useState } from 'react';import styles from './index.less';import { CaretLeftOutlined } from '@ant-design/icons';import { InputNumber } from 'antd';import Slider from 'rc-slider';import 'rc-slider/assets/index.css';import { bignumberFc } from '@/utils/bigNumber';type colorBlockProps = { groupCount: number; //分的组数 colorGroupArrayHex: string[]; //色彩色块的固定数组 type: 'percent' | 'value'; //百分比或数值的显示模式 min?: number; //数值显示模式的最小值 max?: number; //数值显示模式的最大值 currentColorBlockInfo?: { //保留的以后的组件的信息,还原时候须要 sliderValues: number[]; numberValues: number[]; groupRangeArrs: number[][]; }; // 最大值最小值变动的flag,因为我组件外层会有多个中央触发最大最小值的变动,避免反复渲染,所以传入一个flag去监听,而不是监听min和max minmaxChangeFlag?: string;};const ColorBlock: React.FC<colorBlockProps> = (props: colorBlockProps) => { const { groupCount, colorGroupArrayHex, type, min, max, currentColorBlockInfo, minmaxChangeFlag } = props; // 滑动条组件的values,实现是利用从从0到1,示意百分比,同时也是百分比模式下input框的显示值 const [sliderValues, setSliderValues] = useState<number[]>([]); // 数值类型的数组,它的值与sliderValues与min max 是一一对应的,是数值模式下input框的显示值 const [numberValues, setNumberValues] = useState<number[]>([]); // input输入框的层级数组,聚焦的时候把以后的input框的z值调大,避免被遮挡 const [zIndexArray, setZIndexArray] = useState<number[]>([]); // 拖动滑块的响应函数 const handleSliderChange = (values: number[] | number) => { let valueArray = values as number[]; if (valueArray.includes(0) && valueArray.includes(1)) { setSliderValues(valueArray); } }; // 更改inputNumber提早失效定时器 let timer: any = null; // input输入框手动输出的响应(百分比显示模式) const handlePercentInputChange = (index: number, value: any) => { const newSliderValues = [...sliderValues]; if (typeof value === 'number') { // 解决成整数并且勾销百分比模式 const newValue = Math.round(value) / 100; if (newValue < newSliderValues[index + 1] && newValue > newSliderValues[index - 1]) { newSliderValues[index] = newValue; } if (timer) clearTimeout(timer); timer = setTimeout(() => { setSliderValues(newSliderValues); }, 500); } }; // input输入框手动输出的响应(数值显示模式) const handleValueInputChange = (index: number, value: any) => { const newSliderValues = [...sliderValues]; const newNumberValues = [...numberValues]; if (typeof value === 'number' && max !== undefined && min !== undefined) { // 首先判断输出的范畴是否在相邻两个值的步长范畴内 if ( value < newNumberValues[index + 1] - (max - min) / 100 && value > newNumberValues[index - 1] + (max - min) / 100 ) { // 算出新的占比 newSliderValues[index] = (value - min) / (max - min); } if (timer) clearTimeout(timer); timer = setTimeout(() => { setSliderValues(newSliderValues); }, 500); } }; // 聚焦的input输入框永远在最上方,避免被遮蔽看不到信息 const handleInputOnFocus = (index: number) => { let newZIndexArray = [...zIndexArray]; for (let i: number = 0; i < newZIndexArray.length; i++) { if (i === index) { newZIndexArray[i] = 9999; } else newZIndexArray[i] = 1000; } setZIndexArray(newZIndexArray); }; // 初始化函数,若最大值最值,或分组的组数,或显示模式变动,则从新初始化 useEffect(() => { const initSliderValues: number[] = []; const initZIndexArray: number[] = []; for (let i: number = 0; i < groupCount + 1; i++) { initSliderValues.push(Math.round((1 / groupCount) * i * 100) / 100); initZIndexArray.push(1000); } setSliderValues(initSliderValues); setZIndexArray(initZIndexArray); }, [minmaxChangeFlag, groupCount, type]); // 因数值类型的数组,它的值与sliderValues与min max 是一一对应的,所以sliderValue变动的时候同时更新数值型的数组 useEffect(() => { if (sliderValues && min !== undefined && max !== undefined) { const newNumberValues: number[] = []; for (let i: number = 0; i < groupCount + 1; i++) { newNumberValues.push(min + (max - min) * sliderValues[i]); } setNumberValues(newNumberValues); } }, [sliderValues]); // 还原数据 useEffect(() => { if (currentColorBlockInfo) { setSliderValues(currentColorBlockInfo.sliderValues); setNumberValues(currentColorBlockInfo.numberValues); } }, []); // 组件的要害值的变动,将要害值 交给下层进行存储或者变更解决 useEffect(() => { const groupRangeArrs: number[][] = []; const newSliderValues = [...sliderValues]; const newNumberValues = [...numberValues]; // 百分比显示模式 if (type !== 'percent' && newSliderValues.length > 2) { for (let i: number = 0; i < newSliderValues.length - 1; i++) groupRangeArrs.push([newSliderValues[i], newSliderValues[i + 1]]); } // 数值显示模式 else if (type === 'percent' && newNumberValues.length > 2) { for (let i: number = 0; i < newNumberValues.length - 1; i++) groupRangeArrs.push([newNumberValues[i], newNumberValues[i + 1]]); } // 上面正文掉的为我本人的更新currentColorBlockInfo 的信息的办法,是交给下层进行解决,可依据理论状况本人去实现属性的存储,只需存储这三个属性即可 // { // sliderValues: sliderValues, // numberValues: numberValues, // groupRangeArrs: groupRangeArrs, // }, // setAttr( // { // colorBlockSlider: { // sliderValues: sliderValues, // numberValues: numberValues, // groupRangeArrs: groupRangeArrs, // // timeFlag:moment().format('YYYYMMDDHHmmss') // }, // }, // 'setOtherAttr', // ); }, [sliderValues, numberValues]); return ( <div className={styles.container}> <div> percent <Slider range step={0.01} min={0} max={1} count={groupCount} value={sliderValues} pushable={0.01} trackStyle={colorGroupArrayHex.map((item: string) => { return { width: 30, backgroundColor: item as string }; })} vertical reverse allowCross={true} onChange={handleSliderChange} /> <div> {sliderValues.map((item: number, index: number) => { return type !== 'percent' ? ( // 百分比显示模式 <div key={item} style={{ border: '0px solid #000', marginLeft: 30, marginTop: item === 0 ? -11 : 0, height: sliderValues[index] === 1 ? '1%' : `${(sliderValues[index + 1] - sliderValues[index]) * 100}%`, }} > <CaretLeftOutlined /> <InputNumber disabled={item === 0 || item === 1} size="small" value={item * 100} className={zIndexArray[index] === 9999 ? 'onFocusInput' : ''} precision={0} step={1} onChange={(value) => { handlePercentInputChange(index, value); }} onFocus={() => { handleInputOnFocus(index); }} ></InputNumber> <span>%</span> </div> ) : ( // 数值显示模式 <div key={item} style={{ border: '0px solid #000', marginLeft: 30, marginTop: item === 0 ? -11 : 0, height: sliderValues[index] === 1 ? '1%' : `${(sliderValues[index + 1] - sliderValues[index]) * 100}%`, }} > <CaretLeftOutlined /> <InputNumber // addonAfter={<span>%</span>} disabled={item === 0 || item === 1} size="small" style={{ width: 150 }} value={numberValues[index]} className={zIndexArray[index] === 9999 ? 'onFocusInput' : ''} precision={ numberValues.length > 1 ? bignumberFc .divide(bignumberFc.subtract(numberValues[numberValues.length - 1], numberValues[0]), 100) .toString() .split('.')[1] ? bignumberFc .divide(bignumberFc.subtract(numberValues[numberValues.length - 1], numberValues[0]), 100) .toString() .split('.')[1].length : 0 : 0 } step={ numberValues.length > 1 ? bignumberFc.divide( bignumberFc.subtract(numberValues[numberValues.length - 1], numberValues[0]), 100, ) : 1 } onChange={(value) => { handleValueInputChange(index, value); }} onFocus={() => { handleInputOnFocus(index); }} ></InputNumber> </div> ); })} </div> </div> </div> );};export default ColorBlock;
ps:bigNumberFc是基于bignumber.js封装的更高精度的计算方法,也贴在上面:
import BigNumber from 'bignumber.js';// 高精度计算的处理函数汇合const bignumberFc = { add: (a: number, b: number) => { return new BigNumber(a).plus(b).toNumber(); }, subtract: (a: number, b: number) => { return new BigNumber(a).minus(b).toNumber(); }, multiply: (a: number, b: number) => { return new BigNumber(a).multipliedBy(b).toNumber(); }, divide: (a: number, b: number) => { return new BigNumber(a).dividedBy(b).toNumber(); }, // 均值 avg: (arr: number[]) => { let total: number = 0; for (let i: number = 0; i < arr.length; i++) { total = bignumberFc.add(total, arr[i]); } return bignumberFc.divide(total, arr.length); }, // 方差 variance: (arr: number[]) => { let total: number = 0; for (let i: number = 0; i < arr.length; i++) { total = bignumberFc.add(total, arr[i]); } const mean = bignumberFc.divide(total, arr.length); let totalS: number = 0; for (let i: number = 0; i < arr.length; i++) { const tempEverySubtract: number = bignumberFc.subtract(arr[i], mean); totalS = bignumberFc.add(totalS, bignumberFc.multiply(tempEverySubtract, tempEverySubtract)); } return bignumberFc.divide(totalS, arr.length); }, // 线性回归 regression: (list: any[]) => { if (!list) return ''; if (list.length === 0) return ''; // x,y总数 let totalX: number = 0, totalY: number = 0; // x,y平均数 let avgX: number = 0, avgY: number = 0; // 数组长度 let n: number = list?.length; // xy阶乘 let xy2: number = 0; // x平方阶乘 let x2: number = 0; // 斜率b,常量a let b: number = 0, a: number = 0; // 分子、分母 let fz: number = 0, fm: number = 0; // 回归线 let fn: string = ''; for (let i = 0; i < n; i++) { totalX += list[i][0]; totalY += list[i][1]; xy2 += bignumberFc.multiply(list[i][0], list[i][1]); x2 += bignumberFc.multiply(list[i][0], list[i][0]); } // 均值 avgX = bignumberFc.divide(totalX, n); avgY = bignumberFc.divide(totalY, n); // 分子分母 let nxy = bignumberFc.multiply(n, bignumberFc.multiply(avgX, avgY)); fz = bignumberFc.subtract(xy2, nxy); let nx = bignumberFc.multiply(n, bignumberFc.multiply(avgX, avgX)); fm = bignumberFc.subtract(x2, nx); b = bignumberFc.divide(fz, fm); a = bignumberFc.subtract(list[0][1], bignumberFc.multiply(b, list[0][0])); fn = a < 0 ? 'y=' + (b == 0 ? '' : b + 'x') + a : 'y=' + (b == 0 ? '' : b + 'x+') + a; return fn; // console.log(avgX,'平均数x'); // console.log(avgY,'平均数y'); // console.log(nxy,'nxy'); // console.log(nx,'nx'); // console.log(fz,'分子'); // console.log(x2,'x2'); // console.log(xy2,'xy2'); // console.log(fm,'分母'); // console.log(fn,'线性回归方程'); },};