Henry Henry
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)

Henry

小学生中的前端大佬
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Vue

  • ElementUI

    • 基于 ElementUI 封装的 TextEllipsis
    • 基于 ElementUI 封装的 Tree
    • 基于 ElementUI 封装的 Tree2
    • 基于 ElementUI 封装的 SelectTree
    • 基于 ElementUI 封装的 NumberInput
      • 同事封装的组件
      • 分析组件
      • 改造组件
        • 增加一个参数, 来判断正负数
        • 验证输入
      • 问题
        • - 或 . 问题(未解决)
        • 初始值为不合法字符串(已解决)
        • 默认值为 undefined 会造成页面卡顿甚至卡死(已解决)
        • 传入的 value 为 Number 类型时无法验证(已解决)
      • 扩展
    • 基于 ElementUI 封装的基础 table 和 form
    • 基于 ElementUI 封装的 TreeTable
    • 基于 VuePress 搭建一个类似 ElementUI 的说明文档
    • el-tree 节点过滤加载对应子节点
    • 简单调试 node_modules 源码
    • ElementUI 问题集合
    • 树形表格更新后保持折叠状态
    • 开始和完成时间互相限制
  • React

  • AntD

  • 前端
  • ElementUI
Henry
2020-05-10
目录

基于 ElementUI 封装的 NumberInput

最近在做项目的时候遇到 input 只能输入整数, 不能输入小数的需求. 想到以前同事封装过这个组件, 可以拿来一用. 结果发现组件只支持正整数, 不支持负数, 看来还需要修改一下了

国际惯例: 源码 (opens new window)奉上

# 同事封装的组件

<template>
  <el-input v-model="model" v-bind="$attrs" @input="_input" v-on="listeners">
    <slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot>
  </el-input>
</template>
1
2
3
4
5
<script>
export default {
  inheritAttrs: false,
  name: 'NumberInput',
  props: {
    value: [Number, String],
    type: {
      // integer 整数, decimal 小数
      type: String,
      default: 'decimal'
    },
    places: {
      //保留小数位数
      type: [Number, String],
      default: 2 //默认两位小数
    },
    Event: {
      type: String,
      default: 'input'
    }
  },
  data() {
    return {
      model: this.value
    }
  },
  watch: {
    value() {
      this.model = this.value
    }
  },
  methods: {
    _input(val) {
      const numReg = /^[0-9]$/
      let v
      if (this.type === 'integer') {
        // 验证 整数
        v = val.replace(/./g, function(e) {
          if (numReg.test(parseInt(e))) {
            return e
          } else {
            return ''
          }
        })
        this.$nextTick(() => {
          this.model = v
          this.$emit('input', v)
        })
        return
      }
      if (this.type === 'decimal') {
        // 验证 只留小数点和数字
        v = val.replace(/./g, function(e) {
          if (e === '.' || numReg.test(parseInt(e))) {
            return e
          } else {
            return ''
          }
        })
      }
      //小数位数
      if (v.indexOf('.') > -1) {
        let l, r
        // 阻止输入两个小数点
        if (v.split('.').length > 2) {
          v = v.slice(0, v.length - 1)
        }
        // 截取小数点
        l = v.split('.')[0]
        r = v.split('.')[1] || ''
        if (r.length > +this.places) {
          r = r.slice(0, +this.places)
        }
        v = l + '.' + r
      }
      this.$nextTick(() => {
        this.model = v
        this.$emit('input', v)
      })
    }
  },
  computed: {
    listeners() {
      return Object.assign({}, this.$listeners, { [this.Event]: this._input })
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

# 分析组件

首先 html 部分没啥好说的, 都是基本操作, 这里就不再赘述了

js 部分主要看一下 _input 中的代码:

首先定义了一个验证数字的正则, 其实 /^\d+$/ 就可以了.

先看验证整数的逻辑:

if (this.type === 'integer') {
  // 验证 整数
  // 依次替换 val 中的每个值
  v = val.replace(/./g, function(e) {
    // 如果是数字, 就返回本身
    if (numReg.test(parseInt(e))) {
      return e
    // 否则返回空
    } else {
      return ''
    }
  })
  this.$nextTick(() => {
    // 修改绑定值
    this.model = v
    // emit 值
    this.$emit('input', v)
  })
  return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这里一切正常, 就是无法判断负整数, 所以我们需要修改一下正则, 并且还需要增加一个参数来判断是正数还是负数

再看验证小数的逻辑:

if (this.type === 'decimal') {
  // 验证 只留小数点和数字
  // 依次替换 val 中的每个值
  v = val.replace(/./g, function(e) {
    // 如果是小数点和数字, 就返回本身
    // 所以只输入一个小数点, 而不输入数字也会验证通过的
    // 注: 这里会返回多个小数点
    if (e === '.' || numReg.test(parseInt(e))) {
      return e
    } else {
      return ''
    }
  })
}
//小数位数
// 判断是否有小数点
if (v.indexOf('.') > -1) {
  let l, r
  // 阻止输入两个小数点
  if (v.split('.').length > 2) {
    // 如果输入两个小数点, 就截断最后一个字符
    // 注: 这里有一个问题
    // 我们正常输入都是从左到右, 当输入第二个小数点后, 截断第二个是正常的
    // 但用户有可能会移动光标到第一个小数点前面输入小数点
    // 这样截断最后一个字符其实是数字
    // 而且现在有两个小数点了
    v = v.slice(0, v.length - 1)
  }
  // 截取小数点
  // 用小数点分隔, 判断小数位数是否超过预定值
  // 其实这里变相的处理了两个小数点的问题
  // 因为分隔后, 其实数组中有 3 个值, 这里只取了前两个
  // 那必然会导致数据丢失
  l = v.split('.')[0]
  r = v.split('.')[1] || ''
  if (r.length > +this.places) {
    r = r.slice(0, +this.places)
  }
  v = l + '.' + r
}
this.$nextTick(() => {
  this.model = v
  this.$emit('input', v)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

小数验证一下就出来这么多问题了:

  1. 小数点后没有数字也会验证通过
  2. 在第一个小数点前面再输入一个小数点会导致数据丢失

关于数据丢失的例子:

输入 处理 输出
123.45 验证通过 123.45
12.3.45 进入阻止输入两个小数点的逻辑, 截断最后一个字符 5 12.3.4
继续进入截取小数点的逻辑, 只取数组前两位, 并验证小数位数是否超过预定值 12.3

# 改造组件

# 增加一个参数, 来判断正负数

props: {
  type: {
    // integer 整数, decimal 小数
    type: String,
    default: 'decimal'
  },
  signed: {
    // plus 正数, minus: 负数, all: 全部
    type: String,
    default: 'plus'
  },
  places: {
    // 保留小数位数
    type: [Number, String],
    default: 2 // 默认两位小数
  }
},
computed: {
  regs() {
    return {
      // 注:
      // 如果是负数的话, 用户首先会输入 -, 这个要验证通过, 所以数字后面的量词为 *
      // 这也就有一个问题了, 用户只输入一个 - 就提交, 也能验证通过, 目前暂时没有好办法处理
      'integer-plus': /^\d+$/,
      'integer-minus': /^-\d*$/,
      'integer-all': /^-?\d*$/,
      // 这里是在正则中使用变量的方式
      // \ 要写两次
      'decimal-plus': new RegExp(`^\\d+(\\.\\d{0,${this.places}})?$`),
      'decimal-minus': new RegExp(`^-(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`),
      'decimal-all': new RegExp(`^-?(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`)
    }
  },
  regName() {
    return `${this.type}-${this.signed}`
  },
  reg() {
    return this.regs[this.regName]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 验证输入

思路: 感觉同事验证整数和小数的逻辑太麻烦, 其实可以根据不同情况生成不同正则, 如果验证通过就返回, 否则就截断最后一个字符再返回. 但这样也犯了同事处理阻止输入两个小数点的问题: 如果在字符串中间输入非法字符, 会截断最后一个合法字符, 而留下这个非法字符.

其实, 如果本次输入验证不通过, 那我就返回上一次输入的. 这就需要记录本次输入和上次输入的值, 需要请出 watch 大神了, 并且需要一个变量记录当前的输入值

data() {
  return {
    model: this.value,
    temporary: this.value // 这里给临时变量赋值为 value 初始值, 防止用户首次输入非法字符后, old 为空, 清空输入
  }
},
watch: {
  value(n) {
    this.temporary = n
  },
  temporary(n, o) {
    // 注:
    // n 有值才判断, 防止删除的时候无法删除最后一个字符
    if (n.length && !this.reg.test(n)) {
      // 如果验证未通过
      // 当前输入赋值为上一次的合法输入
      n = o
      // 临时变量也赋值为上一次
      this.temporary = o
      // 由于这里临时变量改变了, 会再进入 watch, 并且会验证通过, 所以这里不需要 赋值和 emit 操作
      return
    }
    this.model = n
    this.$emit('input', n)
  }
},
methods: {
  _input(val) {
    this.temporary = val
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

至此就算正式改造完成了

# 问题

# - 或 . 问题(未解决)

可以输入 - 的时候, 只输入一个 - 也可以验证通过

可以输入 . 的时候, . 后面没有数字也可以验证通过

这两种情况暂时没有好的解决方案.

# 初始值为不合法字符串(已解决)

如果初始值就是一个不合法的字符串, 会进入无限循环中

可以在 created 对 value 判断一下, 如果不合法就给 temporary 和 model 赋值为空.

if (!this.reg.test(this.value)) {
  // 这里要同时修改这两个值, 防止 temporary 的 n 和 o 与实际不符
  this.model = ''
  this.temporary = ''
}
1
2
3
4
5

这种虽然会改变输入值, 但你传入的就是一个非法值, 总比陷入无限循环而卡死页面强吧

# 默认值为 undefined 会造成页面卡顿甚至卡死(已解决)

当在表格中大量使用该组件并且默认值为 undefined 时, 渲染需要很长时间, 甚至卡死浏览器

如图:

number-input in el-table

这个表格默认查最近 4 天的数据, 由于是动态表格, 所以由后端返回, prop 是当日的时间戳.

表头数据:

data: [
  {
    children: [
      {
        prop: '1588953600000',
        label: '09日',
      },
      {
        prop: '1589040000000',
        label: '10日',
      },
      {
        prop: '1589126400000',
        label: '11日',
      },
      {
        prop: '1589212800000',
        label: '12日',
      },
    ],
    prop: '1589262808249',
    label: '2020年05月',
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

表格数据:

后端目前的做法是当日如果有数据就返回当日 key 和 value, 否则就没有当日这个字段

data: [
  {
    id: '1611934516379702',
    '1588953600000': 10,
    name: '11'
  },
  {
    id: '1611934516379703',
    name: '22'
  },
]
1
2
3
4
5
6
7
8
9
10
11

第一条 09 日有数据, 就返回了, 其余日都没数据, 就没有返回; 第二条都没有数据, 所以都没有返回

这样取 10 - 12 日的数据就是 undefined, 当页面中有大约 150 个该组件并且都是 undefined 的时候, 渲染就非常卡顿了, 数据量再大就会卡死页面了

为什么发现是 undefined 引起的呢? 因为还有一个类似的页面并不卡顿, 差别就在那个页面后端默认返回的是 0, 而不是 undefined.

一开始想的是让后端也默认返回 0, 但以后出现类似情况都需要后端处理, 作为一个成熟的组件, 咱要从自身解决这个问题

说说我的看法: 当前的问题是 undefined 引起的, 为什么会这样呢? 其实是因为咱们接收 value 值规定了只接收 [Number, String], 但传入的是 undefined, 但 vue 却没有报类型错误, 应该是内部对这种情况做了处理, 导致占用了大量资源, 导致渲染卡顿. 所以咱们只要给一个类型是 Number 或 String 的默认值, 问题就解决了

props: {
 value: {
   type: [Number, String],
   default: '' // '' 或者 0 看需求吧
 }
}
1
2
3
4
5
6

# 传入的 value 为 Number 类型时无法验证(已解决)

复现路径: 不通过输入框修改 value, 而是通过代码将 value 值改变为 Number 类型

以前都是通过输入框来验证组件是否可以控制输入, 这种方式会将 Number 转化为 String, 导致一直没有测出这个 bug

由于 Number 没有 length 属性, 并且 RegExp 只能验证字符串(验证数值有问题), 所以 n.length && !this.reg.test(n) 这个判断就进不去了, 从而导致无法验证

所以我们需要先转化为 String 再验证

watch: {
  temporary(n, o) {
    n = n + ''
    // 判断 n.length 和 n 是一样的效果, 这里就判断 n 了
    if (n && !this.reg.test(n)) {
      n = o
      this.temporary = o
      return
    }
    this.model = n
    this.$emit('input', n)
  }
},
created() {
  const val = this.value + ''
  if (val && !this.reg.test(val)) {
    // 这里 temporary 改变后会触发 watch, 那里会给 model 赋值, 所以这里不用赋值了
    this.temporary = ''
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 扩展

既然现在是通过传入参数来生成正则表达式, 那何不封装一个接收正则表达式的组件, 这样不就可以让使用者自定义 input 值了吗?

RegInput.vue

<template>
  <el-input v-model="model" v-bind="$attrs" @input="_input" v-on="$listeners">
    <slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot>
  </el-input>
</template>
1
2
3
4
5
<script>
export default {
  inheritAttrs: false,
  name: 'RegInput',
  props: {
    value: {
      type: [Number, String],
      default: ''
    },
    reg: {
      type: RegExp,
      default: /./
    }
  },
  data() {
    return {
      model: this.value,
      temporary: this.value
    }
  },
  watch: {
    value(n) {
      this.temporary = n
    },
    temporary(n, o) {
      n = n + ''
      if (this.test(n)) {
        n = o
        this.temporary = o
        return
      }
      this.model = n
      this.$emit('input', n)
    }
  },
  methods: {
    _input(val) {
      this.temporary = val
    },
    test(val) {
      return val && !this.reg.test(val)
    }
  },
  created() {
    const val = this.value + ''
    if (this.test(val)) {
      this.temporary = ''
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

验证发现中文(/^[\u4E00-\u9FA5]*$/)和数字(/^-?(\d*|\d+(\.\d*)?)$/)可以正常输入, 但手机号(/^1[3456789]\d{9}$/)等却无法输入

原因是因为这一类有最少位数, 而我们输入只能一个一个输入, 第一次输入一个, 验证不通过, 返回上一个结果(空值), 如此反复, 永远也无法输入了

对于这一类正则, 目前想到两种方法:

  1. 不让用户输入, 只能粘贴.
  2. 修改正则表达式, 一位也需要验证通过.

两种办法其实都不算好办法, 但目前没有找到合适的解决方案, 等以后找到了再说吧

先把源码 (opens new window)奉上

编辑 (opens new window)
#Js#ElementUI
上次更新: 5/27/2023, 1:02:05 PM
基于 ElementUI 封装的 SelectTree
基于 ElementUI 封装的基础 table 和 form

← 基于 ElementUI 封装的 SelectTree 基于 ElementUI 封装的基础 table 和 form→

最近更新
01
version 1.15
07-01
02
version 1.14
06-27
03
version 1.13
06-27
更多文章>
Theme by Vdoing | Copyright © 2017-2023 HenryTSZ | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式