[ VUE ] 封装通用数组校验组件,el-input内使用

封装一个js文件,numberLimit.js,进行全局挂载,在具体的页面的el-input录入向内执行录入数字校验。可以校验是否整数、正负数、最大值、最小值、小数位数。

使用案例:允许4位小数,不允许负数,最小值为0.00

XML 复制代码
<el-input v-model="scope.row.step4Price" placeholder="请输入第4阶梯子价格" type="text" 
v-number-limit="{ decimalDigits: 4, allowNegative: false, min: 0.00 }" clearable:readonly="detailsDisabled"/>
numberLimit.js 如下:
javascript 复制代码
export default {
  inserted(el, binding, vnode) {
    // 获取输入框元素(支持直接绑定input或包含input的容器)
    const input = el.tagName === 'INPUT' ? el : el.querySelector('input');
    if (!input) return;

    // 解析参数(默认值配置)
    const options = {
      isInteger: false,         // 是否只能输入整数
      allowNegative: false,     // 是否允许负数
      max: undefined,           // 最大值
      min: undefined,           // 最小值
      decimalDigits: 2,         // 最多允许的小数位数(整数模式下强制为0)
      ...binding.value          // 合并用户传入的参数
    };

    // 处理参数关联:整数模式强制小数位为0
    if (options.isInteger) {
      options.decimalDigits = 0;
    }

    // 校验参数有效性(容错处理)
    options.decimalDigits = Math.max(0, Math.floor(Number(options.decimalDigits) || 0));
    options.max = isNaN(Number(options.max)) ? undefined : Number(options.max);
    options.min = isNaN(Number(options.min)) ? undefined : Number(options.min);

    // 格式化max/min使其与小数位匹配
    const formatLimit = (value) => {
      if (value === undefined) return undefined;
      return options.decimalDigits === 0
        ? Math.round(value)  // 整数模式:四舍五入取整
        : Number(value.toFixed(options.decimalDigits));  // 小数模式:保留指定位数
    };
    const max = formatLimit(options.max);
    const min = formatLimit(options.min);

    // 处理输入法组合输入(避免中间状态触发校验)
    let isComposing = false;
    input.addEventListener('compositionstart', () => { isComposing = true; });
    input.addEventListener('compositionend', () => {
      isComposing = false;
      input.dispatchEvent(new Event('input'));  // 输入法完成后重新触发校验
    });

    // 核心输入处理逻辑
    const handleInput = () => {
      if (isComposing) return;
      let value = input.value.trim();

      // 空值处理(仅完全空值时清空,保留负号作为中间状态)
      if (value === '') {
        if (vnode.model) vnode.model.value = '';
        input.value = '';
        return;
      }

      // 处理负数(仅允许时保留负号)
      let hasNegativeSign = options.allowNegative && value.startsWith('-');
      let numValue = hasNegativeSign ? value.slice(1) : value;

      // 过滤非法字符
      if (options.isInteger) {
        // 整数模式:只保留数字
        numValue = numValue.replace(/[^\d]/g, '');
      } else {
        // 小数模式:保留数字和单个小数点
        numValue = numValue.replace(/[^\d.]/g, '');
        const dotIndex = numValue.indexOf('.');
        if (dotIndex !== -1) {
          // 移除多余的小数点
          numValue = numValue.slice(0, dotIndex + 1) + numValue.slice(dotIndex + 1).replace(/\./g, '');
        }
        // 补前导0(如 ".123" → "0.123")
        if (numValue.startsWith('.')) numValue = '0' + numValue;
      }

      // 限制小数位数(非整数模式)
      if (!options.isInteger && numValue.includes('.')) {
        const [intPart, decPart] = numValue.split('.');
        numValue = `${intPart}.${decPart.slice(0, options.decimalDigits)}`;
      }

      // 还原负号(关键优化:允许负号作为首位,即使还未输入数字)// 有负号标识时,空数字也保留负号
      let finalValue = hasNegativeSign ? (numValue ? `-${numValue}` : '-') : numValue;

      // 范围校验
      const parsedNum = finalValue && finalValue !== '-' ? parseFloat(finalValue) : NaN;
      if (!isNaN(parsedNum)) {
        // 最大值限制
        if (max !== undefined && parsedNum > max) {
          finalValue = options.isInteger
            ? max.toString()
            : max.toFixed(options.decimalDigits);
        }
        // 最小值限制
        if (min !== undefined && parsedNum < min) {
          finalValue = options.isInteger
            ? min.toString()
            : min.toFixed(options.decimalDigits);
        }
      } else {
        // 仅在非负号中间状态时清空(保留"-"作为合法输入过程)
        if (!(options.allowNegative && finalValue === '-')) {
          finalValue = '';
        }
      }

      // 同步输入框与v-model(避免不必要的触发)
      if (finalValue !== input.value) {
        input.value = finalValue;
        if (vnode.model) {
          vnode.model.value = finalValue;  // 保持字符串类型,避免数字自动补位
        }
        input.dispatchEvent(new Event('input'));
      }
    };

    // 绑定事件(覆盖输入、变更、失焦场景)
    input.addEventListener('input', handleInput);
    input.addEventListener('change', handleInput);
    input.addEventListener('blur', handleInput);
    // 存储事件句柄用于解绑
    el._numberLimitHandlers = { handleInput };
  },

  // 解绑时清理事件,避免内存泄漏
  unbind(el) {
    const input = el.tagName === 'INPUT' ? el : el.querySelector('input');
    if (input && el._numberLimitHandlers) {
      const { handleInput } = el._numberLimitHandlers;
      input.removeEventListener('input', handleInput);
      input.removeEventListener('change', handleInput);
      input.removeEventListener('blur', handleInput);
    }
  }
};
相关推荐
弓.长.6 小时前
React Native 鸿蒙跨平台开发:实现一个多功能单位转换器
javascript·react native·react.js
南半球与北海道#6 小时前
前端打印(三联纸票据打印)
前端·vue.js·打印
摘星编程6 小时前
React Native for OpenHarmony 实战:ToggleSwitch 切换开关详解
javascript·react native·react.js
董世昌416 小时前
深入浅出 JavaScript 常用事件:从原理到实战的全维度解析
前端
满栀5856 小时前
分页插件制作
开发语言·前端·javascript·jquery
qq_406176147 小时前
深入剖析JavaScript原型与原型链:从底层机制到实战应用
开发语言·前端·javascript·原型模式
弓.长.7 小时前
React Native 鸿蒙跨平台开发:BottomSheet 底部面板详解
javascript·react native·react.js
开开心心_Every7 小时前
免费窗口置顶小工具:支持多窗口置顶操作
服务器·前端·学习·macos·edge·powerpoint·phpstorm
摘星编程7 小时前
React Native for OpenHarmony 实战:Permissions 权限管理详解
javascript·react native·react.js