import React, { ChangeEvent, FC, useCallback, useState } from 'react'; import { useUpdateEffect } from 'react-use'; import styled from '@emotion/styled'; import Input, { InputProps } from '../input'; function isEmpty(value: number | string | undefined) { if ((!value && value !== 0) || !String(value).trim()) { return true; } return false; } function isInvalidate(value: number | string | undefined) { if (isEmpty(value) || Number.isNaN(Number(value))) { return true; } return false; } const StyledInput = styled(Input)` // 删除上下按钮 &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { -webkit-appearance: none !important; } &[type='number'] { -moz-appearance: textfield; } input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none !important; } input[type='number'] { -moz-appearance: textfield; } `; export interface NumberInputProps extends Omit { /** 值 */ value?: number | string; /** 精度 */ precision?: number; /** 最小值 */ min?: number; /** 最大值 */ max?: number; /** 变化回调 */ onChange?: (value?: number) => void; /** 失焦回调 */ onBlur?: (value: string) => void; } const NumberInput: FC = props => { const { min, max, precision = 0, value, onChange, onBlur, ...rest } = props; // 格式化,是否是有输入触发 const formatter = useCallback( (newValue: number | string | undefined, userTyping: boolean) => { // 先转换成 string 类型 const str = typeof newValue === 'number' ? String(newValue) : newValue; // 如果不是由输入触发并且是个值,失去焦点时会走进来格式化 if (!userTyping && !isInvalidate(newValue)) { const numberNewValue = Number(newValue); if (typeof min === 'number' && numberNewValue < min) { return min.toFixed(precision); } if (typeof max === 'number' && numberNewValue > max) { return max.toFixed(precision); } return numberNewValue.toFixed(precision); } // 由输入触发 return str; }, [max, min, precision], ); const [inputValue, setInternalInputValue] = useState(() => { if (isInvalidate(value)) { return ''; } return formatter(value, false); }); // 设置输入框的值和触发 onChange const setInputValue = useCallback( (newValue: number | string | undefined, userTyping: boolean): string | undefined => { const formatterValue = formatter(newValue, userTyping); setInternalInputValue(formatterValue); // 清空输入框时触发 if (isEmpty(newValue) && formatterValue !== formatter(value, userTyping)) { onChange?.(); return ''; } if (!userTyping && !isInvalidate(newValue) && Number(value) !== Number(formatterValue)) { onChange?.(Number(formatterValue)); } return formatterValue; }, [formatter, onChange, value], ); const handleChange = useCallback( (event: ChangeEvent) => { let val = event.target.value; // 中文句号转换 val = val.replace(/。/g, '.'); if (val === '') { setInputValue('', true); return; } if (new RegExp(`^(-|-?(\\d+|\\d?)\\.\\d{0,${precision}}|-?\\d+\\.?)$`).test(val)) { const formatValue = formatter(val, true); setInternalInputValue(formatValue); const numberInnerValue = Number(formatValue); if ( onChange && (typeof min !== 'number' || numberInnerValue >= min) && (typeof max !== 'number' || numberInnerValue <= max) ) { onChange(numberInnerValue); } } }, [formatter, max, min, onChange, precision, setInputValue], ); // 失去焦点时格式化输入 const handleBlur = useCallback( (event: ChangeEvent) => { // 如果输入框内容为空,则跳过格式化 if (event.target.value === '') { onBlur?.(''); return; } const val = setInputValue(event.target.value, false); onBlur?.(val as string); }, [onBlur, setInputValue], ); useUpdateEffect(() => { if (Number(value) !== Number(inputValue) || (inputValue === '' && Number(value) === 0)) { setInputValue(value, false); } }, [value]); return ( ); }; export default NumberInput;