先看实现的成果:
次要性能点:反对传入分组的组数,反对传入不同的色彩色条,可拖动左侧滑块扭转占比,可通过输出右侧 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,'线性回归方程');
},
};