Vue3数字输入框(InputNumber)

简介: 这是一个可定制的数字输入框组件,支持设置宽度、最小值、最大值、步长、精度等属性,并可添加前缀图标及自定义显示格式。组件兼容键盘快捷键操作,具备禁用功能。示例代码展示了如何使用该组件实现不同场景下的数值输入与格式化展示。组件还利用 `add` 函数解决了 JS 精度问题,并通过 `useSlotsExist` 监听插槽。

可自定义设置以下属性:

  • 数字输入框宽度(width),类型:string | number,单位 px,默认 90

  • 最小值(min),类型:number,默认 -Infinity

  • 最大值(max),类型:number,默认 Infinity

  • 每次改变步数,可以为小数(step),类型:number,默认 1

  • 数值精度(precision),类型:number,默认 0

  • 前缀图标(prefix),类型:string | slot,默认 undefined

  • 指定展示值的格式(formatter),类型:Function,默认 (value: string) => value

  • 是否启用键盘快捷键行为(上方向键增,下方向键减)(keyboard),类型:boolean,默认 true

  • 是否禁用(disabled),类型:boolean,默认 false

  • 当前值(v-model:value),类型:number | null,默认 null

效果如下图:在线预览

其中引入使用了以下工具函数:

①创建数组输入框组件InputNumber.vue:

<script setup lang="ts">
defineOptions({
  inheritAttrs: false
})
import { ref, computed, watch } from 'vue'
import { useSlotsExist, add } from '../utils'
interface Props {
  width?: string | number // 数字输入框宽度,单位 px
  min?: number // 最小值
  max?: number // 最大值
  step?: number // 每次改变步数,可以为小数
  precision?: number // 数值精度
  prefix?: string // 前缀图标 string | slot
  formatter?: Function // 指定展示值的格式
  keyboard?: boolean // 是否启用键盘快捷键行为(上方向键增,下方向键减)
  disabled?: boolean // 是否禁用
  value?: number | null // (v-model) 当前值
}
const props = withDefaults(defineProps<Props>(), {
  width: 90,
  min: -Infinity,
  max: Infinity,
  step: 1,
  precision: 0,
  prefix: undefined,
  formatter: (value: string) => value,
  keyboard: true,
  disabled: false,
  value: null
})
const inputWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  }
  return props.width
})
const precision = computed(() => {
  // 数值精度取步长和精度中较大者
  const stepPrecision = String(props.step).split('.')[1]?.length || 0
  return Math.max(props.precision, stepPrecision)
})
const slotsExist = useSlotsExist(['prefix'])
const showPrefix = computed(() => {
  return slotsExist.prefix || props.prefix
})
const numValue = ref(props.formatter(props.value?.toFixed(precision.value)))
watch(
  () => props.value,
  (to) => {
    numValue.value = to === null ? null : props.formatter(to?.toFixed(precision.value))
  }
)
watch(numValue, (to) => {
  if (!to) {
    emitValue(null)
  }
})
const emits = defineEmits(['update:value', 'change'])
function emitValue(value: number | null) {
  emits('change', value)
  emits('update:value', value)
}
function onChange(e: Event) {
  const value = (e.target as HTMLInputElement).value.replace(/,/g, '')
  if (!Number.isNaN(parseFloat(value))) {
    // Number.isNaN() 判断传递的值是否为NaN,并检测器类型是否为 Number
    if (parseFloat(value) > props.max) {
      emitValue(props.max)
      return
    }
    if (parseFloat(value) < props.min) {
      emitValue(props.min)
      return
    }
    if (parseFloat(value) !== props.value) {
      emitValue(parseFloat(value))
    } else {
      numValue.value = props.value?.toFixed(precision.value)
    }
  } else {
    numValue.value = props.value?.toFixed(precision.value)
  }
}
function onKeyboard(e: KeyboardEvent) {
  if (e.key === 'ArrowUp') {
    onUp()
  }
  if (e.key === 'ArrowDown') {
    onDown()
  }
}
function onUp() {
  const res = parseFloat(Math.min(props.max, add(props.value || 0, +props.step)).toFixed(precision.value))
  emitValue(res)
}
function onDown() {
  const res = parseFloat(Math.max(props.min, add(props.value || 0, -props.step)).toFixed(precision.value))
  emitValue(res)
}
</script>
<template>
  <div
    tabindex="1"
    class="m-input-number"
    :class="{ 'input-number-disabled': disabled }"
    :style="`width: ${inputWidth};`"
  >
    <div class="m-input-number-wrap">
      <span v-if="showPrefix" class="input-prefix">
        <slot name="prefix">{
  
  { prefix }}</slot>
      </span>
      <input
        v-if="keyboard"
        class="input-number"
        autocomplete="off"
        :disabled="disabled"
        v-model="numValue"
        @keydown.up.prevent
        @keydown="onKeyboard"
        @change="onChange"
        v-bind="$attrs"
      />
      <input v-else autocomplete="off" class="input-number" @change="onChange" v-model="numValue" v-bind="$attrs" />
    </div>
    <div class="m-handler-wrap">
      <span class="m-arrow up-arrow" :class="{ 'arrow-disabled': (value || 0) >= max }" @click="onUp">
        <svg focusable="false" class="icon-svg" data-icon="up" aria-hidden="true" viewBox="64 64 896 896">
          <path
            d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
          ></path>
        </svg>
      </span>
      <span class="m-arrow down-arrow" :class="{ 'arrow-disabled': (value || 0) <= min }" @click="onDown">
        <svg focusable="false" class="icon-svg" data-icon="down" aria-hidden="true" viewBox="64 64 896 896">
          <path
            d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
          ></path>
        </svg>
      </span>
    </div>
  </div>
</template>
<style lang="less" scoped>
.m-input-number {
  position: relative;
  display: inline-block;
  height: 32px;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.88);
  line-height: 1.5714285714285714;
  padding: 0 11px;
  background-color: #ffffff;
  border-radius: 6px;
  border: 1px solid #d9d9d9;
  transition: all 0.2s;
  &:hover {
    border-color: #4096ff;
    .m-handler-wrap {
      background: #fff;
      opacity: 1;
    }
  }
  &:focus-within {
    // 激活时样式
    border-color: #4096ff;
    box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
  }
  .m-input-number-wrap {
    height: 100%;
    display: flex;
    .input-prefix {
      pointer-events: none;
      margin-inline-end: 4px;
      display: inline-flex;
      align-items: center;
    }
    .input-number {
      font-size: 14px;
      color: rgba(0, 0, 0, 0.88);
      width: 100%;
      height: 100%;
      background: transparent;
      border-radius: 6px;
      transition: all 0.2s linear;
      appearance: textfield;
      border: none;
      outline: none;
    }
    input::-webkit-input-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
    input:-moz-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
    input::-moz-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
    input:-ms-input-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
  }
  .m-handler-wrap {
    position: absolute;
    top: 0;
    right: 0;
    width: 22px;
    height: 100%;
    background: transparent;
    border-radius: 0 6px 6px 0;
    opacity: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch; // 默认值,元素被拉伸以适应容器
    transition: all 0.2s linear 0.2s;
    .icon-svg {
      width: 7px;
      height: 7px;
      fill: rgba(0, 0, 0, 0.45);
      user-select: none;
    }
    .m-arrow {
      display: flex;
      align-items: center;
      justify-content: center;
      flex: auto;
      height: 40%;
      border-left: 1px solid #d9d9d9;
      cursor: pointer;
      transition: all 0.2s linear;
      &:hover {
        height: 60%;
        .icon-svg {
          fill: @themeColor;
        }
      }
    }
    .up-arrow {
      border-top-right-radius: 6px;
    }
    .down-arrow {
      border-top: 1px solid #d9d9d9;
      border-bottom-right-radius: 6px;
    }
    .arrow-disabled {
      cursor: not-allowed;
    }
  }
}
.input-number-disabled {
  color: rgba(0, 0, 0, 0.25);
  background-color: rgba(0, 0, 0, 0.04);
  border-color: #d9d9d9;
  box-shadow: none;
  cursor: not-allowed;
  opacity: 1;
  &:hover {
    border-color: #d9d9d9;
  }
  &:focus-within {
    // 激活时样式
    border-color: #d9d9d9;
    box-shadow: none;
  }
  .m-input-number-wrap .input-number {
    cursor: not-allowed;
  }
  .m-handler-wrap {
    display: none;
  }
}
</style>

②在要使用的页面引入:

其中 moneyFormat() 数值格式化方法请参考:日期格式化 | Vue Amazing UI

<script setup lang="ts">
import InputNumber from './InputNumber.vue'
import { ref, watchEffect } from 'vue'
import { formatNumber } from 'vue-amazing-ui'
const value = ref(3)
const formatValue = ref(1000)
watchEffect(() => {
  console.log('value:', value.value)
})
watchEffect(() => {
  console.log('formatValue:', formatValue.value)
})
function formatter(num: string): string {
  return formatNumber(num, 2)
}
function onChange(number: number | null) {
  console.log('change:', number)
}
</script>
<template>
  <div>
    <h1>{
  
  { $route.name }} {
  
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <InputNumber v-model:value="value" placeholder="请输入" @change="onChange" />
    <h2 class="mt30 mb10">步数为小数</h2>
    <InputNumber :step="0.1" v-model:value="value" />
    <h2 class="mt30 mb10">设置数值精度</h2>
    <InputNumber :min="-10" :max="10" :step="0.6" :precision="2" v-model:value="value" />
    <h2 class="mt30 mb10">格式化展示</h2>
    <InputNumber :width="120" :step="10" :formatter="formatter" v-model:value="formatValue" />
    <h2 class="mt30 mb10">自定义最大最小值</h2>
    <InputNumber :min="0" :max="10" v-model:value="value" />
    <h2 class="mt30 mb10">添加前缀图标 $</h2>
    <InputNumber prefix="$" v-model:value="value" />
    <h2 class="mt30 mb10">禁用</h2>
    <InputNumber v-model:value="value" disabled />
  </div>
</template>
相关文章
|
6月前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
491 1
|
6月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
893 139
|
6月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
546 0
|
7月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
771 11
|
8月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
836 1
|
8月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
445 0
|
9月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
205 0
|
11月前
|
JavaScript 前端开发 API
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
2245 0
|
11月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
1258 5
|
11月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
1060 17