数字输入范围组件vue2

0 阅读2分钟

image.png

<template>
  <!-- 无内部 el-form 嵌套,适配外部 el-form 校验 -->
  <div class="number-range-input" ref="rangeInputRef">
    <div class="input-wrap" :style="{ width: inputWidth }">
      <!-- 最小值输入框:强制数字输入,绑定内部值 -->
      <el-input
        v-model="innerMin"
        placeholder="最小值"
        @blur="handleSyncValue"
        @input="handleMinInput"
        @clear="handleSyncValue"
        :clearable="clearable"
        type="number"
        style="width: calc(50% - 6px);"
        size="mini"
        :disabled="disabled"
      ></el-input>

      <!-- 视觉分隔符 -->
      <span class="separator">-</span>

      <!-- 最大值输入框:强制数字输入,绑定内部值 -->
      <el-input
        v-model="innerMax"
        placeholder="最大值"
        @blur="handleSyncValue"
        @input="handleMaxInput"
        @clear="handleSyncValue"
        :clearable="clearable"
        type="number"
        style="width: calc(50% - 6px);"
        size="mini"
        :disabled="disabled"
      ></el-input>
    </div>
  </div>
</template>

<script>
export default {
  name: 'NumberRangeInput',
  // Vue2 语法:props 接收外部配置(适配 el-form 校验和回显)
  props: {
    // 双向绑定值:"1,20" 格式字符串(支持回显和 el-form 校验)
    value: {
      type: String,
      default: ''
    },
    // 输入框整体宽度
    inputWidth: {
      type: String,
      default: '300px'
    },
    // 是否显示清除按钮
    clearable: {
      type: Boolean,
      default: true
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false
    },
    // 最大值不能小于最小值的提示文本
    rangeTip: {
      type: String,
      default: '最小值不能大於最大值'
    }
  },
  data() {
    return {
      // 内部缓存值(隔离外部 props,避免直接修改,保留字符串格式)
      innerMin: '',
      innerMax: ''
    };
  },
  watch: {
    // 1. 监听外部 value 变化,实现数据回显(支持 "1,20" 格式回显)
    value: {
      immediate: true,
      handler(val) {
        if (!val || typeof val !== 'string') {
          this.innerMin = '';
          this.innerMax = '';
          return;
        }

        // 分割 "1,20" 格式字符串,解析为最小值和最大值
        const [minStr, maxStr] = val.split('&&').map(item => item.trim());
        const minNum = Number(minStr);
        const maxNum = Number(maxStr);

        // 校验解析结果有效性,有效则填充输入框
        if (!isNaN(minNum) && !isNaN(maxNum) && minStr && maxStr) {
          this.innerMin = String(Math.min(minNum, maxNum));
          this.innerMax = String(Math.max(minNum, maxNum));
        } else {
          this.innerMin = '';
          this.innerMax = '';
        }
      }
    }
  },
  methods: {
    /**
     * 最小值输入监听:联动最大值,确保最小值 ≤ 最大值(保留字符串格式)
     */
    handleMinInput() {
      const { innerMin, innerMax } = this;
      // 转换为数字判断大小,避免字符串比较误差
      if (innerMin && innerMax && !isNaN(Number(innerMin)) && !isNaN(Number(innerMax))) {
        const minNum = Number(innerMin);
        const maxNum = Number(innerMax);
        if (minNum > maxNum) {
          this.innerMax = innerMin;
          this.$message?.warning(this.rangeTip);
        }
      }
    },

    /**
     * 最大值输入监听:联动最小值,确保最大值 ≥ 最小值(保留字符串格式)
     */
    handleMaxInput() {
      const { innerMin, innerMax } = this;
      // 转换为数字判断大小,避免字符串比较误差
      if (innerMin!=='' && innerMax!=='' && !isNaN(Number(innerMin)) && !isNaN(Number(innerMax))) {
        const minNum = Number(innerMin);
        const maxNum = Number(innerMax);
        if (maxNum < minNum) {
          this.innerMin =innerMax;
          this.$message?.warning(this.rangeTip);
        }
      }
    },

    /**
     * 核心:同步内部值到外部,生成 "1,20" 格式字符串(实现双向绑定)
     */
    handleSyncValue() {
      const { innerMin, innerMax } = this;
      console.log("innerMin, innerMax ",innerMin, innerMax )
      console.log("!isNaN(Number(innerMin) ",isNaN(Number(innerMin)))
      console.log("!isNaN(Number(innerMax) ",isNaN(Number(innerMax)))
      let result = '';

      // 校验内部值有效性(均为有效数字且非空)
      if (innerMin!=='' && innerMax!=='' && !isNaN(Number(innerMin)) && !isNaN(Number(innerMax))) {
        const minNum = Number(innerMin);
        const maxNum = Number(innerMax);
        // 生成 "最小值,最大值" 格式字符串,保证顺序正确
        const minStr = String(Math.min(minNum, maxNum));
        const maxStr = String(Math.max(minNum, maxNum));
        result = `${minStr}&&${maxStr}`;
      }else{
        console.log("不滿足")
      }

      // Vue2 双向绑定核心:$emit('input') 同步值到外部 v-model
      this.$emit('input', result);
      // 额外发射事件,方便外部监听详情
      this.$emit('rangeChange', result);
    },

    /**
     * 外部调用:校验方法(适配 el-form 自定义校验,返回是否有效)
     * @returns {Boolean} 校验结果(有效返回 true,无效返回 false)
     */
    validate() {
      const { innerMin, innerMax } = this;
      // 校验规则:两个值均为有效数字且非空
      return innerMin && innerMax && !isNaN(Number(innerMin)) && !isNaN(Number(innerMax));
    },

    /**
     * 外部调用:重置组件(清空输入框,返回空字符串)
     */
    reset() {
      this.innerMin = '';
      this.innerMax = '';
      this.handleSyncValue();
    }
  }
};
</script>

<style scoped>
.number-range-input {
  width: 100%;
  box-sizing: border-box;
}
.input-wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.separator {
  color: #666;
  user-select: none;
}
::v-deep .el-input__inner{
    height: 28px !important;
    line-height: 28px !important;
    padding: 0 3px !important;
    font-size: 12px !important;
    text-align: center;
}
</style>