使用react/vue封装只能输入数字的输入框

前言

写业务的同学多多少少会碰到一些输入框只能输入数字的需求,再者就是数字格式化千分位等等,基于这些需求,我将使用 vue2vue3 以及react实现这一功能。

需求分析

  1. 输入框只能输入数字
  2. 允许小数存在(可选)
  3. 允许负数存在(可选)
  4. 格式化千分位(可选)

封装组件的优点

  1. 显示的时候带千分位,实际上获取到的值是不带千分位
  2. 抽离相同逻辑,减少代码冗余
  3. 使用更方便,代码维护性强

效果图如下:

vue2 二次封装 el-input

为了方便,我们直接使用element-ui组件库中的el-input来做二次封装,封装涉及的知识点:

  • $attrs
    • 透传 Attributes 是指由父组件传入,且没有被子组件声明为 props 或是组件自定义事件的 attributes 和事件处理函数。
    • 默认情况下,若是单一根节点组件,$attrs 中的所有属性都是直接自动继承自组件的根元素。而多根节点组件则不会如此,同时你也可以通过配置 inheritAttrs 选项来显式地关闭该行为。
    • 参考 透传 Attribute
  • 过滤器filters
  • 计算属性computed
  • 父子传参 this.$emit
  • v-model 语法糖
    • v-model 实际上是个语法糖,什么叫语法糖 ?顾名思义,吃起来很甜,用起来很方便
    • v-bind:value 和 v-on:input 的组合就是v-model
    • <input v-model="name" /> 等价于 <input :value="name" @input="e => name = e.target.value" />
js 复制代码
<template>
  <el-input 
    :value="_value" 
    @input="handleInput" 
    @blur="handleBlur" 
    v-bind="$attrs"
  />
</template>
 
<script>
export default {
  name: 'el-input-number',
  props: {
    value: {
      type: String,
      default: ''
    },
    // 是否支持负数,即支持输入 -,默认可以为负数
    isMinus: {
      type: Boolean,
      default: true,
    },
    // 是否是整数,即不能输入小数.默认可以输入小数
    isInteger: {
      type: Boolean,
      default: false,
    },
    // 显示的时候是否格式化为带千分位的值,默认格式化
    isThousands: {
      type: Boolean,
      default: true,
    }
  },
  filters: {
     // 格式化千分位
    _number(num) {
      let str = num.toString()
      let flag = false
      if (/^-?\d+(.\d+)?$/.test(str)) {
        if (str.includes('.')) {
          str = str.replace(/\.(\d+)$/, ',$1')
        } else {
          str += ',00'
          flag = true
        }
        while (/\d{4}/.test(str)) {
          str = str.replace(/(\d+)(\d{3}\,)/, '$1,$2')
        }
        if (flag) {
          str = str.replace(/\,(\d+)$/, '')
        } else {
          str = str.replace(/\,(\d+)$/, '.$1')
        }
      }
      return str
    }
  },
  methods: {
    // 输入非数字结尾的做特殊处理
    handleBlur() {
      if (['.', '-'].includes(this.value)) {
        this.$emit('input', '');
      } else {
        const [integer, decimal] = this.value.split('.')
        let _value = this.value
        if (!decimal /* || decimal.length === 2 && decimal === '00' */) { // 小数不存在或空或.00 ,转为整数
          _value = integer
        } else if (decimal.length === 1) { // 有一位小数,后补0
          _value = `${this.value}0`
        }
        if (_value === '-0') {
          _value = '0'
        }
        this.$emit('input', _value)
      }
    },
    // 属性控制正则限制输入
    getRegexp(a, b) {
      let key = `${!!a}-${!!b}`
      const map = { // 第一个true:isInteger, 第二个true:isMinus,总共四种情况
        'true-true': /[^0-9\-]/g, // 正负整数
        'true-false': /[^0-9]/g  , // 正整数
        'false-true': /[^0-9\.-]/g, // 正负数(含整数和小数)
        'false-false': /[^0-9\.]/g // 正数(含整数和小数)
      }
      return map[key]
    },

    handleInput(value) {
      const regexp = this.getRegexp(this.isInteger, this.isMinus)
      const _value = value
      .replace(regexp, '') // 根据属性限制输入
      .replace(/^(-?[^-]*)-*/g, '$1') // 只能开头一个 -
      .replace(/\.{2,}/g, '.') // 只能有一个.
      .replace(/(\.\d{1,2})(.*)$/g, '$1') // 1.12.12312 => 1.12 || .12.32 => .12
      .replace(/^(-?)\./, '$10.') // 以 . 或 -. 开头的 => 0. || -. => -0.
      .replace(/^(-?)0([0-9]+)/, '$1$2') // 正负整数部分不能以0开头

      this.$emit('input',_value)

    },
  },
  computed: {
    // 格式化
    _value() {
      if (this.isThousands) return this.$options.filters['_number'](this.value)
      return this.value
    }
  }
};
</script>

vue3 二次封装 el-input

涉及知识点:

  • v-model 语法糖
    • vue3 与 vue2 有些不同,v-model的语法糖由 :modelValue:update:modelValue 组成
  • 计算属性 computed
  • defineProps
  • defineEmits
  • toRefs
  • v-bind="$attrs"
js 复制代码
<template>
  <el-input 
    :modelValue="_modelValue" 
    @update:modelValue="handleInput" 
    @blur="handleBlur" 
    v-bind="$attrs"
  />
</template>

<script lang="ts" setup>
import {  computed, defineProps, defineEmits, toRefs } from 'vue'

type IObject = {
  [props: string]: RegExp
}

const props = defineProps({
  isThousands: {
    type: Boolean,
    default: true
  },
  isInteger: {
    type: Boolean,
    default: false
  },
  isMinus: {
    type: Boolean,
    default: true
  },
  modelValue: {
    type: String,
    default: ''
  }
})

const $emit = defineEmits<{ (event: 'update:modelValue', value: string): void }>()

const { isThousands, isInteger, isMinus, modelValue } = toRefs(props)

const formatNumber = (num: string) => {
  let str = num.toString()
  let flag = false
  if (/^-?\d+(.\d+)?$/.test(str)) {
    if (str.includes('.')) {
      str = str.replace(/\.(\d+)$/, ',$1')
    } else {
      str += ',00'
      flag = true
    }
    while (/\d{4}/.test(str)) {
      str = str.replace(/(\d+)(\d{3}\,)/, '$1,$2')
    }
    if (flag) {
      str = str.replace(/\,(\d+)$/, '')
    } else {
      str = str.replace(/\,(\d+)$/, '.$1')
    }
  }
  return str
}

const _modelValue = computed(() => {
  if (isThousands.value) return formatNumber(modelValue.value)
  return modelValue.value
})

  // 输入非数字结尾的做特殊处理
const handleBlur = () => {
  if (['.', '-'].includes(modelValue.value)) {
    $emit('update:modelValue', '');
  } else {
    const [integer, decimal] = modelValue.value.split('.')
    let _value = modelValue.value
    if (!decimal /* || decimal.length === 2 && decimal === '00' */) { // 小数不存在或空或.00 ,转为整数
      _value = integer
    } else if (decimal.length === 1) { // 有一位小数,后补0
      _value = `${modelValue.value}0`
    }
    if (_value === '-0') {
      _value = '0'
    }
    $emit('update:modelValue', _value)
  }
}
// 属性控制正则限制输入
const getRegexp = (a: boolean, b: boolean): RegExp => {
  let key = `${!!a}-${!!b}`
  const map: IObject = { // 第一个true:isInteger, 第二个true:isMinus,总共四种情况
    'true-true': /[^0-9\-]/g, // 正负整数
    'true-false': /[^0-9]/g  , // 正整数
    'false-true': /[^0-9\.-]/g, // 正负数(含整数和小数)
    'false-false': /[^0-9\.]/g // 正数(含整数和小数)
  }
  return map[key]
}
const handleInput = (value: string) => {
  const regexp = getRegexp(isInteger.value, isMinus.value)
  const _value = value
  .replace(regexp, '') // 根据属性限制输入
  .replace(/^(-?[^-]*)-*/g, '$1') // 只能开头一个 -
  .replace(/\.{2,}/g, '.') // 只能有一个.
  .replace(/(\.\d{1,2})(.*)$/g, '$1') // 1.12.12312 => 1.12 || .12.32 => .12
  .replace(/^(-?)\./, '$10.') // 以 . 或 -. 开头的 => 0. || -. => -0.
  .replace(/^(-?)0([0-9]+)/, '$1$2') // 正负整数部分不能以0开头

  $emit('update:modelValue', _value)
}
</script>

react 二次封装 antd input

基于antd的input组件封装

js 复制代码
import { Input } from "antd";

const InputMoney = (props) => {
  const {
    isMinus = true,
    isInteger = false,
    isThousands = true,
    value,
    onChange,
    ...rest
  } = props;
  // 格式化千分位
  const formatNumber = (num) => {
    let str = num.toString();
    let flag = false;
    if (/^-?\d+(.\d+)?$/.test(str)) {
      if (str.includes(".")) {
        str = str.replace(/\.(\d+)$/, ",$1");
      } else {
        str += ",00";
        flag = true;
      }
      while (/\d{4}/.test(str)) {
        str = str.replace(/(\d+)(\d{3}\,)/, "$1,$2");
      }
      if (flag) {
        str = str.replace(/\,(\d+)$/, "");
      } else {
        str = str.replace(/\,(\d+)$/, ".$1");
      }
    }
    return str;
  };

  // 输入非数字结尾的做特殊处理
  const handleBlur = () => {
    if ([".", "-"].includes(value)) {
      onChange && onChange("");
    } else {
      const [integer, decimal] = value.split(".");
      let _value = value;
      if (!decimal /* || decimal.length === 2 && decimal === '00' */) {
        // 小数不存在或空或.00 ,转为整数
        _value = integer;
      } else if (decimal.length === 1) {
        // 有一位小数,后补0
        _value = `${value}0`;
      }
      if (_value === "-0") {
        _value = "0";
      }
      onChange && onChange(_value);
    }
  };
  // 属性控制正则限制输入
  const getRegexp = (a, b) => {
    let key = `${!!a}-${!!b}`;
    const map = {
      // 第一个true:isInteger, 第二个true:isMinus,总共四种情况
      "true-true": /[^0-9\-]/g, // 正负整数
      "true-false": /[^0-9]/g, // 正整数
      "false-true": /[^0-9\.-]/g, // 正负数(含整数和小数)
      "false-false": /[^0-9\.]/g, // 正数(含整数和小数)
    };
    return map[key];
  };

  const handleChange = (e) => {
    // console.log(value);
    const { value } = e.target;
    const regexp = getRegexp(isInteger, isMinus);
    const _value = value
      .replace(regexp, "") // 根据属性限制输入
      .replace(/^(-?[^-]*)-*/g, "$1") // 只能开头一个 -
      .replace(/\.{2,}/g, ".") // 只能有一个.
      .replace(/(\.\d{1,2})(.*)$/g, "$1") // 1.12.12312 => 1.12 || .12.32 => .12
      .replace(/^(-?)\./, "$10.") // 以 . 或 -. 开头的 => 0. || -. => -0.
      .replace(/^(-?)0([0-9]+)/, "$1$2"); // 正负整数部分不能以0开头

    onChange && onChange(_value);
  };

  return (
    <Input
      {...rest}
      value={formatNumber(value)}
      onChange={handleChange}
      onBlur={handleBlur}
    />
  );
};

export default InputMoney;

使用自定义组件

vue2、vue3、react使用方式有些许不同

react组件使用

js 复制代码
import { useState } from "react";
import "./App.css";
import InputMoney from "./components/InputMoney";

function App() {
  const [money, setMoney] = useState("");
  return (
    <>
      金额:{money}
      <InputMoney value={money} onChange={setMoney} placeholder="请输入金额" />
    </>
  );
}

export default App;

vue3 组件使用

js 复制代码
<script setup lang="ts">
import { ref} from 'vue';
import inputNumber from '@/components/inputNumber.vue'

let money = ref()

</script>

<template>
  {{ money }}
  <input-number v-model="money" placeholder="请输入金额"/>
</template>

vue2 组件使用

js 复制代码
// import elInputNumber from '@/components/el-input-number'
// 如果多个地方使用,全局注册组件  Vue.component('el-input-number', elInputNumber)
// 否则就局部注册 components: { elInputNumber }

<template>
  <div id="app">
    name值:{{ name }}
    <br />
    <el-input-number style="width: 260px" v-model="name" placeholder="请输入金额"/>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      name: ''
    }
  },
}
</script>

总结

二次封装的组件使用上与使用原有组件库的组件方式一样,区别在于多了几个参数用来限制输入,如果要使用原生input做封装,只要稍微改动input事件的参数即可(e.target.value),由于正则表达式写的并不是很好,大家可以基于此正则上做修改或新增限制。

注意:如果要限制输入长度的话,千分位 , 和 小数点 . 是包含在内的。当输入的末位是小数点的话,上面的格式化千分位会导致计算长度出现bug,即12345. 它不会格式化为 12,345.,则可替换为以下函数:

js 复制代码
function formatNumber() {
    let str = num.toString()
    let flag = false
    if (/^-?\d+(.\d*)?$/.test(str)) {
        if (str.includes('.') && str.split('.')[1]) {
          str = str.replace(/\.(\d+)$/, ',$1')
        } else if (str.includes('.') && !str.split('.')[1]){
          str = str.split('.')[0] + ','
        } else {
          str += ',00'
          flag = true
        }
        while (/\d{4}/.test(str)) {
          str = str.replace(/(\d+)(\d{3}\,)/, '$1,$2')
        }
        if (flag) {
          str = str.replace(/\,(\d+)$/, '')
        } else {
          str = str.replace(/\,(\d*)$/, '.$1')
        }
    }
    return str
}
相关推荐
梵得儿SHI3 小时前
Vue 模板语法深度解析:从文本插值到 HTML 渲染的核心逻辑
前端·vue.js·html·模板语法·文本插值·v-text指令·v-html指令
浪裡遊3 小时前
HTML面试题
前端·javascript·react.js·前端框架·html·ecmascript
i小杨3 小时前
React 状态管理库相关收录
前端·react.js·前端框架
PyAIGCMaster4 小时前
ERR_PNPM_ENOENT ENOENT: no such file or directory, scandir的解决方案
react.js
listhi5204 小时前
Vue.js 3的组合式API
android·vue.js·flutter
WYiQIU4 小时前
高级Web前端开发工程师2025年面试题总结及参考答案【含刷题资源库】
前端·vue.js·面试·职场和发展·前端框架·reactjs·飞书
夏之小星星4 小时前
Springboot结合Vue实现分页功能
vue.js·spring boot·后端
韩立学长4 小时前
【开题答辩实录分享】以《自动售货机刷脸支付系统的设计与实现》为例进行答辩实录分享
vue.js·spring boot·后端
静西子4 小时前
Vue标签页切换时的异步更新问题
前端·javascript·vue.js
时间的情敌5 小时前
Vue 3.0 源码导读
前端·javascript·vue.js