Vue3 基于Element Plus 的el-input,封装一个数字输入框组件

需求:在实际项目中,我们有很多需要用户去输入数字的输入框,比如输入经度、纬度、或数量输入框等。封装一个数字输入框组件,不仅可以满足大部分场景,也可以提供维护性、可扩展性,废话不多说,直接上代码

代码解读

xml 复制代码
  <!-- 
    核心部分:
    1. v-bind="$attrs" 将所有传入的属性透传给 el-input
    2. autocomplete="new-password" 禁用浏览器自动填充功能
    3. 插槽部分通过 v-for 遍历所有插槽并透传给 el-input
   -->

  <!-- 
    什么是 $attrs:
      $attrs 是 Vue 实例的一个属性,它包含了父组件传递给子组件的所有非 prop 属性。换句话说,就是父组件传递给子组件的属性中,那些没有在子组件的 props 中声明的属性。
    v-bind="$attrs" 的作用:
      将父组件传递给当前组件的所有非 prop 属性,全部绑定到当前组件模板中的某个元素上。
   -->

  <!-- 实际使用示例 -->
  <!-- 假设你在其他组件中这样使用 NumberInput: -->

  <template>
    <NumberInput
      v-model="price"
      placeholder="请输入价格"
      class="price-input"
      style="width: 200px"
      disabled
    />
  </template>

  <!-- 
    在这个例子中:
        1. v-model 是 NumberInput 组件声明的 prop,会被正确处理
        2. placeholder、class、style、disabled 这些属性没有在 NumberInput 的 props 中声明
    如果没有 v-bind="$attrs",这些属性将不会应用到实际的输入框上。但因为有了 v-bind="$attrs",这些属性会被传递并绑定到 el-input 练级上,最终渲染为:
   -->

  <input placeholder="请输入价格" class="price-input" style="width: 200px" disabled />

  <!-- 动态插槽透传 -->
  <template v-for="(item, key, index) in $slots" :key="index" v-slot:[key]>
    <slot :name="key"></slot>
  </template>
  <!-- 
    这段代码的作用是动态插槽透传,即将父组件传递给当前组件的所有插槽内容,原封不动地传递给内部的 el-input 组件。 
    1. 遍历 $slots 对象,获取所有传递给当前组件的插槽
    2. key 是插槽的名称(如 "prepend"、"append" 等)
    3. index 是遍历的索引,用作 :key 的值
    4. v-slot:[key] 是一个动态插槽,用于将父组件传递给当前组件的插槽内容,原封不动地传递给内部的 el-input 插槽。
    5. <slot :name="key"></slot>在每个动态插槽中,使用同名的插槽内容进行渲染,实现插槽内容的透传
  -->

  <!-- 实际应用场景 -->
  <!-- 假设你在使用 NumberInput 组件时传递了插槽: -->
  <template>
    <NumberInput v-model="price">
      <template #prepend>
        <span>¥</span>
      </template>
      <template #append>
        <span>元</span>
      </template>
    </NumberInput>
  </template>

  <!-- 由于 NumberInput 组件中的这段代码,这些插槽内容会被透传给内部的 el-input 组件,最终渲染效果相当于: -->

  <el-input v-model="price">
    <template #prepend>
      <span>¥</span>
    </template>
    <template #append>
      <span>元</span>
    </template>
  </el-input>

  <!-- 为什么这样做? -->
  <!-- 
     1. 组件封装: NumberInput 是对 el-input 的封装,希望保留 el-input 的所有插槽功能
     2. 减少重复工作: 不需要手动声明和处理 el-input 的每一个插槽
     3. 提高可维护性: 当 el-input 添加新插槽时,无需修改 NumberInput 组件代码
     4. 灵活性: 父组件可以像直接使用 el-input 一样使用 NumberInput 的插槽 
  -->

  <!-- 实际使用示例 -->
  <template>
    <!-- 基本用法 -->
    <NumberInput v-model="price" />

    <!-- 只允许整数,最大3位 -->
    <NumberInput v-model="quantity" :only-int="true" :int-max-length="3" />

    <!-- 允许小数,整数部分最多5位,小数部分最多2位 -->
    <NumberInput v-model="amount" :int-max-length="5" :decimal-max-length="2" />
  </template>

  <!-- 整个流程机制详解 -->
  <!-- 1.初始化时:-->
  <!-- 
    1.1 父组件的 price 值为 '123.45'
    1.2 通过 v-model 传递给 NumberInput 的 modelValue prop
    1.3 NumberInput 内部 value 被初始化为 '123.45'
    1.4 由于设置了 immediate: true,watch 立即执行,将 value.value 设置为 '123.45'
   -->
  <!-- 2. 用户输入时: -->
  <!-- 
    2.1 用户在输入框中输入字符
    2.2 触发 @input="onInput" 事件
    2.3 onInput 函数处理输入内容(过滤非法字符、长度限制等)
    2.4 处理后的值赋给 value.value
    2.5 watch(value, ...) 监听到变化,触发 emit
("update:modelValue", value.value)
    2.6 父组件接收到 update:modelValue 事件,更新 price 的值
    -->
  <!-- 3. 父组件更新时: -->
  <!-- 
      3.1 如果父组件以其他方式改变了 price 的值
      3.2 通过 v-model 传递给 NumberInput 的 modelValue prop
      3.3 watch(() => props.modelValue, ...) 监听到变化
      3.4 将新的值同步到内部的 value.value
      3.5 由于使用了 v-model.trim="value",输入框显示更新后的值
     -->

  <!-- const emit = defineEmits(["update:modelValue", "input"]) 实现原理 -->
  <!-- 
       1. 这是 Vue 3 Composition API 提供的编译器宏(compiler macro)
       2. 用于声明组件可以触发哪些自定义事件
       3. 返回一个 emit 函数,用于实际触发事件
       4. update:modelValue:用于实现 v-model 双向绑定
       5. input:用于通知父组件发生了输入事件
     -->

  <!-- 实际使用场景 -->
  <!-- 1. update:modelValue 事件 -->
  <!-- 
        1.1 在 watch 监听内部 value 变化时触发
        watch(value, () => {
          emit("update:modelValue", value.value);
        });
         当组件内部的值发生变化时,通过触发 update:modelValue 事件通知父组件更新绑定的值
         父组件使用示例:
          <template>
           <!- v-model 会监听 update:modelValue 事件来更新 price ->
            <NumberInput v-model="price" />
          </template>
      -->

  <!-- 2. input 事件 -->
  <!-- 
      2.1 用于通知父组件发生了输入事件:
      const onInput = (text: string) => {
        // ... 处理输入逻辑
        value.value = val;
        emit("input"); // 通知父组件发生输入
      };
      父组件可以监听这个事件:
      <template>
        <NumberInput 
          v-model="price" 
          @input="handleInput" 
        />
      </template>
     -->

子组件

ini 复制代码
<template>
  <el-input
    ref="inputRef"
    v-bind="$attrs"
    v-model.trim="value"
    @input="onInput"
    autocomplete="new-password"
  >
    <template v-for="(item, key, index) in $slots" :key="index" v-slot:[key]>
      <slot :name="key"></slot>
    </template>
  </el-input>
</template>

<script lang="ts" setup>
import { ref, watch } from "vue";

const props = defineProps<{
  // 1. 定义 props,接收父组件传入的 modelValue
  modelValue?: string | number;
  onlyInt?: boolean; // 是否只允许整数 true 时:仅允许整数 false 时:允许小数
  intMaxLength?: number; // 整数部分最大长度
  decimalMaxLength?: number; // 小数部分最大长度
}>();

// 2. 定义事件发射器
const emit = defineEmits(["update:modelValue", "input"]);

// 3. 创建内部响应式值,初始化为 props.modelValue
const value = ref(props.modelValue);

// 4. 监听外部传入props.modelValue 值变化,同步到内部 value
watch(
  () => props.modelValue,
  (val: any) => {
    value.value = val;
  },
  {
    immediate: true, // 立即执行一次
  }
);

// 5. 监听内部 value 变化,通知父组件
watch(value, () => {
  emit("update:modelValue", value.value);
});

// 6. 处理用户输入处理函数
const onInput = (text: string) => {
  let val = "";
  if (props.onlyInt) {
    // 只允许整数
    val = text.replace(/[^0-9]/g, "");
    if (props.intMaxLength) {
      val = val.slice(0, props.intMaxLength);
    }
  } else {
    // 允许小数
    val = text.replace(/[^0-9.]/g, "");
    const parts = val.split(".");
    if (parts.length == 1) {
      // 没有小数点
      if (props.intMaxLength) {
        val = val.slice(0, props.intMaxLength);
      }
    } else {
      // 有小数点
      let [ipart, dpart] = parts;
      if (props.intMaxLength) {
        // 整数部分限制
        ipart = ipart.slice(0, props.intMaxLength);
      }
      if (props.decimalMaxLength) {
        // 小数部分限制
        dpart = dpart.slice(0, props.decimalMaxLength);
      }
      val = `${ipart}.${dpart}`;
    }
  }
  value.value = val;
  emit("input");
};

// 获取 el-input 实例引用
const inputRef = ref();

// 暴露focus方法给父组件调用
const focus = () => {
  inputRef.value.focus();
};

// 暴露blur方法给父组件调用
const blur = () => {
  inputRef.value.blur();
};

// 暴露clear方法给父组件调用
const clear = () => {
  inputRef.value.clear();
};

defineExpose({
  focus,
  blur,
  clear,
});
</script>

<style lang="scss"></style>

父组件

ini 复制代码
<NumberInput
    :disabled="disabled"
    v-model.trim="lng"
    placeholder="请输入经度"
    class="w150"
    />
    
<NumberInput
    :disabled="disabled"
    v-model.trim="lat"
    placeholder="请输入纬度"
    class="w150 ml10"
  />
  
  import NumberInput from "@/components/NumberInput/Index.vue";

END...

相关推荐
四月_h4 分钟前
在 Vue 3 + TypeScript 项目中实现主题切换功能
前端·vue.js·typescript
qq_4275060810 分钟前
vue3写一个简单的时间轴组件
前端·javascript·vue.js
雨枪幻。1 小时前
spring boot开发:一些基础知识
开发语言·前端·javascript
lecepin2 小时前
AI Coding 资讯 2025.8.27
前端·ai编程
TimelessHaze2 小时前
拆解字节面试题:async/await 到底是什么?底层实现 + 最佳实践全解析
前端·javascript·trae
执键行天涯3 小时前
从双重检查锁定的设计意图、锁的作用、第一次检查提升性能的原理三个角度,详细拆解单例模式的逻辑
java·前端·github
青青子衿越3 小时前
微信小程序web-view嵌套H5,小程序与H5通信
前端·微信小程序·小程序
OpenTiny社区3 小时前
TinyEngine 2.8版本正式发布:AI能力、区块管理、Docker部署一键强化,迈向智能时代!
前端·vue.js·低代码
qfZYG3 小时前
Trae 编辑器在 Python 环境缺少 Pylance,怎么解决
前端·vue.js·编辑器