基于Vue3.0原生封装input实现【数字输入框】

529 阅读3分钟

应业务需要,现在系统需要有很多输入框仅支持输入正负数,可以控制最大值,超过最大值、最小值出现红框提示、给出错误提示等详细的补充。使用的地方比较多,特此抽取数字输入框供项目组使用。

image.png


一、核心功能

  1. 基础输入控制
  • 仅允许输入数字(含小数点/负号)
  • 输入时实时格式化(如固定小数位、非负数的过滤、超过最大位数后的截取)
  • 最大/最小值范围限制
  1. 交互优化
  • 粘贴内容过滤(自动去除非数字字符)
  • 键盘事件拦截(阻止非数字按键输入)
  • 鼠标滚轮增减数值
  • 空值/非法输入提示
  1. 兼容性处理
  • 支持整数、浮点数、等多种格式
  • 多语言/地区数字格式适配(如逗号分隔)
  • 与Vue3响应式数据双向绑定(v-model)

二、技术实现思路

封装组件结构

<el-input
  v-model.trim="inputValue"
  v-bind="$attrs"
  :placeholder="placeholder"
  @input="validateInput"
  @change="changeInput"
  :disabled="disabled"
  :clearable="clearable"
  :class="{ error: isError }"
  :title="errorMessage"
></el-input>

核心逻辑

基于 Element Plus 的 el-input 组件进行二次封装

双向绑定:

  • v-model.trim:自动去除首尾空格(结合原生修饰符)
  • 通过 $attrs 传递未显式声明的属性(如 sizeprefix-icon 等)

事件处理:

  • @input:实时输入时触发格式校验
  • @change:失去焦点或回车时触发值变更

状态控制:

  • :disabled 控制输入框禁用状态

  • :clearable:显示清除按钮

动态样式

  • :class="{ error: isError }":错误状态时应用红色边框
  • :title="errorMessage":悬浮显示错误提示

三、脚本逻辑解析

1. Props 定义

const props = defineProps({
  allowNegative: { type: Boolean, default: false },  // 是否允许负数
  disabled: { type: Boolean, default: false },        // 禁用状态
  clearable: { type: Boolean, default: false },       // 清除按钮
  value: { default: null },                           // 输入值
  maxDecimalDigits: { type: [String, Number], default: 4 },  // 最大小数位数
  maxIntegerDigits: { type: [String, Number], default: 16 }, // 最大整数位数
  isPositive: { type: Boolean, default: false },      // 是否为正整数
  placeholder: { type: String, default: '请输入' },   // 占位符
  maxValue: { type: Number, default: null },          // 最大值限制
  maxValueValidTips: { type: String, default: '' }    // 超限提示文案
})

设计特点:

  • 支持数字格式的全面控制(正负/小数/位数)
  • 提供完整的错误提示体系
  • 兼容原生 input 的所有属性(通过 $attrs

2. Emits 定义

const emit = defineEmits(['update:value', 'change'])
  • update:value:实现 v-model 的双向绑定
  • change:值变更时的回调通知

3. 响应式变量

const inputValue = ref(props.value)
  • 使用 ref 创建响应式数据
  • 初始化值为传入的 props.value

4. 计算属性

const isError = computed(() => {
  return props.maxValue !== null && inputValue.value > props.maxValue
})

const errorMessage = computed(() => {
  return isError.value ? props.maxValueValidTips : ''
})
  • 实时校验数值是否超限
  • 动态生成错误提示信息

5. 核心校验逻辑

const validateInput = (value) => {
  // 1. 字符过滤
  value = value.replace(/[^\d.]/g, '') // 移除非数字和小数点
  value = value.replace(/^./g, '')    // 阻止首字符为小数点

  // 2. 小数点处理
  value = value.replace(/.{2,}/g, '.') // 合并多个小数点
  const parts = value.split('.')       // 分割整数和小数部分

  // 3. 位数限制
  if (parts[0].length > props.maxIntegerDigits) {
    value = parts[0].slice(0, props.maxIntegerDigits)
  }
  if (parts[1]?.length > props.maxDecimalDigits) {
    value = parts[0] + '.' + parts[1].slice(0, props.maxDecimalDigits)
  }

  // 4. 特殊格式处理
  value = value.replace(/^0+\d+?/, '0') // 处理前导零
  if (props.isPositive) {
    value = value.replace(/^(0+)|[^\d]+/, '') // 强制正整数
  }

  // 5. 最终赋值
  inputValue.value = value
  emit('update:value', value)
}

关键处理步骤:

  1. 字符过滤:通过正则表达式限制输入字符范围
  2. 小数控制:分割处理整数和小数部分
  3. 位数限制:动态截断超限数字
  4. 格式优化:处理前导零和非法字符
  5. 类型转换:确保最终值为合法数字字符串

四、特性解析

1. 输入格式控制

  • 数字范围:通过 maxIntegerDigits 和 maxDecimalDigits 限制位数

  • 符号控制:allowNegative 控制正负号

  • 特殊类型:

    • isPositive:强制输入正整数(0 被排除)
    • maxValue:绝对值上限校验

2. 错误处理机制

  • 实时校验:输入时立即检测超限
  • 视觉反馈:通过 CSS 类 .error 改变边框颜色
  • 提示文案:悬浮显示自定义错误信息

3. 生命周期管理

watch(
  () => props.value,
  (val) => {
    inputValue.value = val
  },
  { deep: true }
)
  • 监听 props.value 变化,实现父子组件双向同步
  • 使用 deep: true 确保深层对象更新触发

五、样式实现

.error {
  :deep(.el-input__wrapper) {
    border: 1px solid #e34d59; // 覆盖 Element Plus 默认样式
  }
}
  • 作用域样式:scoped 保证样式局部作用域
  • 深度选择器::deep() 修改子组件样式

六、使用场景

该组件适用于:

  1. 表单中的数值输入场景
  2. 需要精确控制数字格式的场景(如金额、数量)
  3. 需要实时校验输入合法性的场景
  4. 需要兼容 Element Plus 样式的定制化输入组件

改进建议

  1. 增强类型安全:
interface InputNumberProps {
  allowNegative: boolean
  maxDecimalDigits: number
  // 其他属性...
}
const props = defineProps<InputNumberProps>()

2. 添加输入防抖:

import { debounce } from 'lodash'
const debouncedValidate = debounce(validateInput, 300)

源码示例

<template>
  <el-input
    v-model.trim="inputValue"
    v-bind="$attrs"
    :placeholder="placeholder"
    @input="validateInput"
    @change="changeInput"
    :disabled="disabled"
    :clearable="clearable"
    :class="{ error: isError }"
    :title="errorMessage"
  ></el-input>
</template>

<script setup name="InputNumber">
  import debounce from 'lodash/debounce'
  import { formInputNumber } from '@/utils/number'
  const props = defineProps({
    allowNegative: {
      // 是否允许负数
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    },
    clearable: {
      type: Boolean,
      default: false
    },
    value: {
      default: null
    },
    maxDecimalDigits: {
      // 小数后位数
      type: [String, Number],
      default: 4
    },
    maxIntegerDigits: {
      // 整数位 16 最大值为15亿就是10位,只控制位数,不控制具体值
      type: [String, Number],
      default: 16
    },
    isPositive: {
      type: Boolean,
      default: false
    },
    placeholder: {
      type: String,
      default: '请输入'
    },
    // 最大值
    maxValue: {
      type: Number,
      default: null
    },
    // 超过最大值时的提示文案
    maxValueValidTips: {
      type: String,
      default: ''
    }
  })

  const emit = defineEmits(['update:value', 'change'])
  const inputValue = ref(props.value)

  // 是否显示错误样式
  const isError = computed(() => {
    if (props.maxValue !== null && inputValue.value !== null) {
      return inputValue.value > props.maxValue
    }
    return false
  })

  // 错误提示文案
  const errorMessage = computed(() => {
    if (isError.value) {
      return props.maxValueValidTips
    }
    return ''
  })

  const validateInput = debounce((value) => {
    // 先把非数字的都替换掉,除了数字和.
    if (!props.allowNegative) {
      value = value.replace(/[^\d.]/g, '')
    } else {
      value = value.replace(/(?!^-)[^\d.]/g, '')
      // value = handleNegative(value+'')
    }
    // 保证只有出现一个.而没有多个.
    value = value.replace(/.{2,}/g, '.')
    // 必须保证第一个为数字而不是.
    value = value.replace(/^./g, '')
    // 保证.只出现一次,而不能出现两次以上
    value = value.replace('.', '$#$').replace(/./g, '').replace('$#$', '.')
    // 只能输入4个小数
    const parts = value.split('.')
    if (parts.length === 1) {
      if (parts[0].length > props.maxIntegerDigits) {
        value = parts[0].slice(0, props.maxIntegerDigits)
      }
    } else if (parts.length > 1 && parts[0].length > props.maxIntegerDigits) {
      value = parts[0].slice(0, props.maxIntegerDigits) + '.' + parts[1]
    }

    if (parts.length > 1 && parts[1].length > props.maxDecimalDigits) {
      value = parts[0] + '.' + parts[1].slice(0, props.maxDecimalDigits)
    }
    // 开头只能输入一个0
    value = value.replace(/^0+\d+?/g, '0')
    // 非负整数 包含0
    if (props.maxDecimalDigits === 0) {
      // 去除开头可能出现的0(除了单个0的情况)
      value = value.replace(/^0{2,}/, '0')
      // 去除非数字字符
      value = value.replace(/\D/g, '')
    }
    // 正整数 不包含0,必须>=1
    if (props.isPositive) {
      value = value.replace(/^(0+)|[^\d]+/g, '')
    }

    // 整数位和小数位超过位数后自动截取
    value = formInputNumber(value, props.maxDecimalDigits, props.maxIntegerDigits)

    inputValue.value = value
    console.log('inputNumber', value)
    emit('update:value', value)
  }, 300)

  const changeInput = (value) => {
    emit('change', value)
  }

  watch(
    () => props.value,
    (val) => {
      inputValue.value = val
    },
    {
      deep: true
    }
  )
</script>

<style lang="scss" scoped>
  .error {
    :deep(.el-input__wrapper) {
      border: 1px solid #e34d59;
    }
  }
</style>

使用示例

<template>
  <td class="value-col">
    <InputNumber
      v-if="canEditPage && row.month !== 'total'"
      v-model:value="row.topAmount"
      :allowNegative="true"
      :maxDecimalDigits="2"
      @change="changeEletricAmount(row, 'topAmount')"
      :maxIntegerDigits="10"
      :maxValue="15000000000"
      maxValueValidTips="增量金额不能超过15亿元"
    />
    <span v-else>{{ toThousands(row.topAmount) }}</span>
  </td>
</template>

以上内容可根据具体需求进一步细化实现细节。 该文章仅用于学习记录,不涉及商业传播。