前言
写业务的同学多多少少会碰到一些输入框只能输入数字的需求,再者就是数字格式化千分位等等,基于这些需求,我将使用 vue2 、vue3 以及react实现这一功能。
需求分析
- 输入框只能输入数字
- 允许小数存在(可选)
- 允许负数存在(可选)
- 格式化千分位(可选)
封装组件的优点
- 显示的时候带千分位,实际上获取到的值是不带千分位
- 抽离相同逻辑,减少代码冗余
- 使用更方便,代码维护性强
效果图如下:
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
组成
- vue3 与 vue2 有些不同,v-model的语法糖由
- 计算属性 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
}