import classNames from 'classnames' import _toString from 'lodash/toString' import PropTypes, { InferProps } from 'prop-types' import React from 'react' import Taro from '@tarojs/taro' import { Input, View } from '@tarojs/components' import { CommonEvent, ITouchEvent } from '@tarojs/components/types/common' import { AtInputNumberProps, InputError } from '../../../types/input-number' import AtIcon from '../icon' // TODO: Check all types // 实现两数相加并保留小数点后最短尾数 function addNum(num1: number, num2: number): number { let sq1: number, sq2: number try { sq1 = _toString(num1).split('.')[1].length } catch (e) { sq1 = 0 } try { sq2 = _toString(num2).split('.')[1].length } catch (e) { sq2 = 0 } const m = Math.pow(10, Math.max(sq1, sq2)) return (Math.round(num1 * m) + Math.round(num2 * m)) / m } // 格式化数字,处理01变成1,并且不处理1. 这种情况 function parseValue(num: string): string { if (num === '') return '0' const numStr = _toString(num) if (numStr.indexOf('0') === 0 && numStr.indexOf('.') === -1) { // 处理01变成1,并且不处理1. return _toString(parseFloat(num)) } return _toString(num) } type ExtendEvent = { target: { value: string | number } } export default class AtInputNumber extends React.Component { public static defaultProps: AtInputNumberProps public static propTypes: InferProps state = { width: null, } widths = [] as number[] private handleClick(clickType: 'minus' | 'plus', e: CommonEvent): void { const { disabled, value, min = 0, max = 100, step = 1 } = this.props const lowThanMin = clickType === 'minus' && value <= min const overThanMax = clickType === 'plus' && value >= max if (lowThanMin || overThanMax || disabled) { const deltaValue = clickType === 'minus' ? -step : step const errorValue = addNum(Number(value), deltaValue) if (disabled) { this.handleError({ type: 'DISABLED', errorValue, }) } else { this.handleError({ type: lowThanMin ? 'LOW' : 'OVER', errorValue, }) } return } const deltaValue = clickType === 'minus' ? -step : step let newValue = addNum(Number(value), deltaValue) newValue = Number(this.handleValue(newValue)) this.props.onChange(newValue, e) } private handleValue = (value: string | number): string => { const { max = 100, min = 0 } = this.props let resultValue = value === '' ? min : value // 此处不能使用 Math.max,会是字符串变数字,并丢失 . if (resultValue > max) { resultValue = max this.handleError({ type: 'OVER', errorValue: resultValue, }) } if (resultValue < min) { resultValue = min this.handleError({ type: 'LOW', errorValue: resultValue, }) } if (resultValue && !Number(resultValue)) { resultValue = parseFloat(String(resultValue)) || min this.handleError({ type: 'OVER', errorValue: resultValue, }) } resultValue = parseValue(String(resultValue)) return resultValue } private handleInput = (e: CommonEvent & ExtendEvent): string => { const { value } = e.target const { disabled } = this.props if (disabled) return '' const newValue = this.handleValue(value) this.props.onChange(Number(newValue), e) return newValue } private handleBlur = (event: ITouchEvent): void => this.props.onBlur && this.props.onBlur(event) private handleError = (errorValue: InputError): void => { if (!this.props.onErrorInput) { return } this.props.onErrorInput(errorValue) } private onLayout = (type?: string) => (event: any) => { const { width } = event.nativeEvent.layout this.widths.push(width) if (this.widths.length === 2) { const widths = type === 'input' ? this.widths : this.widths.reverse() const [btnWidth, inputWidth] = widths this.setState({ width: btnWidth * 2 + inputWidth + 2, }) } // console.log('event.nativeEvent.layout', event.nativeEvent.layout) } public render(): JSX.Element { const { customStyle, className, width, disabled, value, type, min = 0, max = 100, size, disabledInput, } = this.props const inputStyle: any = {} if (width) { inputStyle.width = Taro.pxTransform(width) } const rootStyle: any = {} if (this.state.width) { rootStyle.width = this.state.width } const inputValue = Number(this.handleValue(value)) const rootCls = classNames( 'at-input-number', { 'at-input-number--lg': size === 'large', }, className, ) const minusBtnCls = classNames('at-input-number__btn') const plusBtnCls = classNames('at-input-number__btn') const minusDisabled = inputValue <= min || disabled const addDisabled = inputValue >= max || disabled return ( ) } } AtInputNumber.defaultProps = { customStyle: {}, className: '', disabled: false, disabledInput: false, value: 1, type: 'number', width: 0, min: 0, max: 100, step: 1, size: 'normal', // eslint-disable-next-line @typescript-eslint/no-empty-function onChange: (): void => {}, } AtInputNumber.propTypes = { customStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), className: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), type: PropTypes.oneOf(['number', 'digit']), disabled: PropTypes.bool, width: PropTypes.number, min: PropTypes.number, max: PropTypes.number, step: PropTypes.number, size: PropTypes.oneOf(['normal', 'large']), disabledInput: PropTypes.bool, onChange: PropTypes.func, onBlur: PropTypes.func, onErrorInput: PropTypes.func, }