Ant Design库问题

847 阅读2分钟

一、InputNumber设置精度后失去焦点会四舍五入

Ant Design(3.x版本)的InputNumber(数字输入框)设置precision(数值精度),输入的值超过精度,失去焦点默认会四舍五入处理。例如:设置precision=2,输入3.145失去焦点后会变成3.15

有些情况是不能四舍五入处理,只能丢弃超过数值精度。比如买卖股票时输入价格,如果四舍五入会影响交易

1、实现方式一

思路:在parser()中获取到输入的值,以"."分割,如果小数位超过精度,就丢失超过精度的值,再重新组装成数值。

注意:不能在onChange中处理,onChange会自动处理末尾的0,例如输入14.20,会被处理成14.2

import * as React from 'react';
import { InputNumber } from 'antd';

interface IInputNumberDiscard {
  defaultValue?: number;
  precision: number;
}

const InputNumberDiscard = ({ defaultValue, precision, ...othersPropsValue }: IInputNumberDiscard) => {
  const [splitValue, setSplitValue] = React.useState([]);
  const [precisionValue, setPrecisionValue] = React.useState<number>();
  const [numberValue, setNumberValue] = React.useState(defaultValue);

  const checkValidValue = (value) => value !== undefined && value !== null && value !== '';

  React.useEffect(() => {
    if (!splitValue && splitValue.length < 1) {
      return;
    }

    const integerValue = splitValue[0];
    let decimalValue = splitValue[1];

    if (Number.isNaN(integerValue) || Number.isNaN(decimalValue)) {
      return;
    }

    if (checkValidValue(decimalValue) && decimalValue.length > precision) {
      decimalValue = decimalValue.slice(0, precision);
    }

    setPrecisionValue(decimalValue?.length);
    setNumberValue(checkValidValue(decimalValue) ? `${integerValue}.${decimalValue}` : integerValue);
  }, [splitValue]);

  React.useEffect(() => {
    if (!checkValidValue(defaultValue)) {
      return;
    }
    const defaultValueArray = String(defaultValue).split('.');
    setSplitValue(defaultValueArray);
  }, []);

  return (
    <InputNumber
      {...othersPropsValue}
      precision={precisionValue}
      value={numberValue}
      parser={(value) => {
        setSplitValue(String(value).split('.'));
        return value;
      }}
    />
  );
};

InputNumberDiscard.defaultProps = {
  defaultValue: undefined,
};

export default InputNumberDiscard;

2、实现方式二

思路:用Input配合一些正则表达式来处理

import * as React from 'react';
import { Input } from 'antd';

interface IInputNumberDiscard {
  defaultValue?: number;
  precision: number;
}

const InputNumberDiscard = ({ defaultValue, precision, ...othersPropsValue }: IInputNumberDiscard) => {
  const [numberValue, setNumberValue] = React.useState(defaultValue);

  const checkValidValue = (value) => value !== undefined && value !== null && value !== '';

  /**
   * 限制输入几位小数
   * @param number
   * @param decimalPlaces
   * @returns
   */
  function limitDecimalPlaces(number, decimalPlaces) {
    // replace(/^(\\-)*(\d+)\.(\d\d).*$/, '$1$2.$3'); // 保留两位
    // $1表示(\\-),是"-"。$2表示(\d+),是0次或多次数字。$3表示(\d\d),是两位数字
    
    // replace(/^(\\-)*(\d+)\.(\d\d\d).*$/, '$1$2.$3'); // 保留三位(保留多少位就加多少个\d)
    // $1表示(\\-),是"-"。$2表示(\d+),是0次或多次数字。$3表示(\d\d\d),是三位数字

    const decimalPattern = `(\\d+\\.\\d{${decimalPlaces}}).*`;
    const regex = new RegExp(`^(\\-)*${decimalPattern}$`);
    const replacement = '$1$2';

    return number.replace(regex, replacement);
  }

  const handleValue = (value) => {
    let newValue = value;
    newValue = value.replace(/[^\d.]/g, ''); // 清除"数字"和"."以外的字符
    newValue = newValue.replace(/^\./g, ''); // 不能为"."开头
    newValue = newValue.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.'); // 只能输入一个小数点且只保留一个
    // replace('.', '$#$'); // 将第一个小数点替换为$#$
    // replace(/\./g, ''); // 移除所有的小数点
    // replace('$#$', '.'); // 将$#$替换回小数点
    newValue = newValue.startsWith('0') && !newValue.startsWith('0.') ? Number(newValue).toString() : newValue; // 处理0123->123
    newValue = limitDecimalPlaces(newValue, precision); // 限制输入几位小数
    setNumberValue(newValue);
  };

  const handleChange = (e) => {
    const { value } = e.target;
    handleValue(value);
  };

  const handleBlur = (e) => {
    const { value } = e.target;
    const zeros = '0'.repeat(precision);
    const newValue = value.endsWith('.') ? `${value}${zeros}` : value; // 处理123.->123.00
    setNumberValue(newValue);
  };

  React.useEffect(() => {
    if (checkValidValue(defaultValue)) {
      handleValue(defaultValue);
    }
  }, []);

  return <Input {...othersPropsValue} type="text" onChange={handleChange} value={numberValue} onBlur={handleBlur} />;
};

InputNumberDiscard.defaultProps = {
  defaultValue: undefined,
};
export default InputNumberDiscard;

1、实现方式三

思路:在parser()中获取输入的值并赋值给Ref对象,在失去焦点(onBlur)把输入的值直接截取并赋值给InputNumber组件

import Decimal from 'decimal.js';

export interface IInputNumberFormItemProps extends InputNumberProps {
  form: any;
  label?: any;
  style?: React.CSSProperties;
  id: string;
  rules?: any[];
  initialValue?: number;
  precision: number;
}

const InputNumberFormItem: React.FC<IInputNumberFormItemProps> = ({
  form,
  label,
  style,
  id,
  rules,
  initialValue,
  precision,
  ...otherProps
}) => {
  const { getFieldDecorator, setFieldsValue } = form;
  const inputValueRef = React.useRef<any>();

  const truncateDecimals = (value) => {
    if (Number.isNaN(Number(value)) || value === null || value === '') return undefined;

    // 不四舍五入,直接截取
    if (value >= 0) {
      return new Decimal(value).toFixed(precision, Decimal.ROUND_FLOOR);
    }
    return new Decimal(value).toFixed(precision, Decimal.ROUND_CEIL);
  };

  const handleValue = () => {
    // 去掉输入第二个.以及.后面的值   
    const num = Number(String(inputValueRef.current).replace(/(\d+\.\d+)\..*/, '$1'));
    setFieldsValue({ [id]: truncateDecimals(num) });
  };

  return (
    <Form.Item label={label}>
      {getFieldDecorator(id, {
        rules,
        initialValue: truncateDecimals(initialValue),
      })(
        <InputNumber
          precision={precision}
          onBlur={() => {
            handleValue();
          }}
          parser={(value) => {
            inputValueRef.current = value;
            return value;
          }}
          style={style ?? { width: '400px' }}
          {...otherProps}
        />,
      )}
    </Form.Item>
  );
};

InputNumberFormItem.defaultProps = {
  label: '',
  rules: undefined,
  initialValue: undefined,
};

export default InputNumberFormItem;

二、下拉框和表单组合,选项框随页面滚动分离

下拉框(Select,TimePicker等)和表单(Form)组合。选项框随页面滚动分离,如下图 form-select.png

1、实现方式一

思路:通过设置getPopupContainer来修改滚动的区域

<Form layout="vertical">
  <Form.Item label="">
    {getFieldDecorator('key', {
      rules: [{ required: true, message: '' }],
      initialValue: '',
    })(
      <Select placeholder="" getPopupContainer={(triggerNode) => triggerNode.parentNode}>
        {[].map((item, index) => (
          <Option key={index} value={item.value}>
            {item.label}
          </Option>
        ))}
      </Select>,
    )}
  </Form.Item>
  ******
</Form>

缺点:如果Form中有多个下拉框,每一个都需要设置

2、实现方式二

思路:Form添加样式设置滚动,在滚动的时候获取焦点

export const ScrollAntForm = ({ children, ...props }) => {
  const handleScroll = (e) => {
    if (e.target?.nodeName !== 'FORM') return;
    e.target.focus();
  };

  return (
    <Form onScroll={handleScroll} {...props} tabIndex={0}>
      {children}
    </Form>
  );
};

<div styleName="form-wrap">
  <ScrollAntForm layout="vertical">
    <Form.Item label="">
      {getFieldDecorator('key', {
        rules: [{ required: true, message: '' }],
        initialValue: '',
      })(
        <Select placeholder="">
          {[].map((item, index) => (
            <Option key={index} value={item.value}>
              {item.label}
            </Option>
          ))}
        </Select>,
      )}
    </Form.Item>
    ******
  </ScrollAntForm>
</div>

.form-wrap {
  height: 60vh;
  overflow: hidden;
  position: relative;

  form {
    padding: 0 32px;
    height: 100%;
    overflow-y: scroll;
  }
}

参考:

antd输入框只能输入数字且保留两位小数