乐趣区

关于typescript:可拖动滑块的实现-带有输入框支持百分比和数值两种显示模式

先看实现的成果:

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