Vue3数字输入框(InputNumber)

Vue3数字输入框(InputNumber) 可自定义设置以下属性数字输入框宽度width类型string | number单位 px默认 90最小值min类型number默认 -Infinity最大值max类型number默认 Infinity每次改变步数可以为小数step类型number默认 1数值精度precision类型number默认 0前缀图标prefix类型string | slot默认 undefined指定展示值的格式formatter类型(value: number | string) string默认 undefined指定从 formatter 里转换回数字的方式和 formatter 搭配使用parser类型(value: string) number默认 undefined是否启用键盘快捷键行为上方向键增下方向键减keyboard类型boolean默认 true是否禁用disabled类型boolean默认 false数字输入的占位符placeholder类型string默认 undefined当前值v-model:value类型number默认 undefined效果如下图在线预览①创建数组输入框组件InputNumber.vue其中引入使用了以下工具函数消除js精度的加法函数add监听插槽存在 useSlotsExist获取依赖注入 useInjectscript setup langts import { ref, computed, watch, nextTick } from vue import { useSlotsExist, add, useInject } from components/utils export interface Props { width?: string | number // 数字输入框宽度单位 px min?: number // 最小值 max?: number // 最大值 step?: number // 每次改变步数可以为小数 precision?: number // 数值精度 prefix?: string // 前缀图标 string | slot formatter?: (value: string | number) string // 指定展示值的格式 parser?: (value: string) number // 指定从 formatter 里转换回数字的方式和 formatter 搭配使用 keyboard?: boolean // 是否启用键盘快捷键行为上方向键增下方向键减 disabled?: boolean // 是否禁用 placeholder?: string // 数字输入的占位符 value?: number // (v-model) 当前值 valueModifiers?: object // 用于访问组件的 v-model 上添加的修饰符 } const props withDefaults(definePropsProps(), { width: 90, min: -Infinity, max: Infinity, step: 1, precision: 0, prefix: undefined, formatter: undefined, parser: undefined, keyboard: true, disabled: false, placeholder: undefined, value: undefined, valueModifiers: () ({}) }) const inputRef ref() // input 模板引用 const numValue refstring() // 数字输入框的内容 const { colorPalettes, shadowColor } useInject(InputNumber) // 主题色注入 const emits defineEmits([update:value, change, enter]) const slotsExist useSlotsExist([prefix]) 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 showPrefix computed(() { return slotsExist.prefix || props.prefix }) const lazyInput computed(() { return lazy in props.valueModifiers }) watch( [() props.value, () precision.value, () props.formatter], async () { if (props.value ! undefined) { if (inputRef.value) { const { selectionStart: start, selectionEnd: end, value } inputRef.value const beforeTxt value.slice(0, start) const afterTxt value.slice(end) numValue.value getFormatValue() // 获取格式化后的值 await nextTick() restoreCursor(start, beforeTxt, afterTxt) } else { numValue.value getFormatValue() } } }, { immediate: true, flush: post } ) function restoreCursor(start: number, beforeTxt: string, afterTxt: string): void { const { value: inputValue } inputRef.value let startPos inputValue.length if (inputValue.endsWith(afterTxt)) { startPos inputValue.length - afterTxt.length } else if (inputValue.startsWith(beforeTxt)) { startPos beforeTxt.length } else { const beforeLastChar beforeTxt[start - 1] const newIndex inputValue.indexOf(beforeLastChar, start - 1) if (newIndex ! -1) { startPos newIndex 1 } } inputRef.value.setSelectionRange(startPos, startPos) } function emitValue(value: number | undefined): void { emits(update:value, value) // 保证在 change 回调时能获取到最新数据 emits(change, value) } function getFormatValue(): string | undefined { if (props.formatter) { return props.formatter(props.value?.toFixed(precision.value) as string) } else { return props.value?.toFixed(precision.value) } } function getNumberValue(value: string): number { let numberValue parseFloat(value) if (numberValue props.max) { numberValue props.max } if (numberValue props.min) { numberValue props.min } return numberValue } function updateValue(value: string): void { // Number.isNaN() 判断传递的值是否为 NaN并检测器类型是否为 Number if (!Number.isNaN(parseFloat(value))) { const numberValue getNumberValue(value) if (numberValue ! props.value) { emitValue(numberValue) } else { numValue.value getFormatValue() } } else { numValue.value getFormatValue() } } function onInput(e: Event): void { if (!lazyInput.value) { const target e.target as HTMLInputElement const value props.parser ? String(props.parser(target.value)) : target.value if (value !Number.isNaN(getNumberValue(value)) getNumberValue(value) ! props.value) { updateValue(value) } if (!value props.value ! undefined) { emitValue(undefined) } } } function onChange(e: Event): void { const target e.target as HTMLInputElement const value props.parser ? String(props.parser(target.value)) : target.value updateValue(value) } function onKeyboard(e: KeyboardEvent): void { if (e.key ArrowUp) { onUp() } if (e.key ArrowDown) { onDown() } } function onEnter(e: KeyboardEvent): void { emits(enter, e) if (lazyInput.value) { const target e.target as HTMLInputElement const value props.parser ? String(props.parser(target.value)) : target.value updateValue(value) } } function onUp(): void { const res Math.min(props.max, add(props.value || 0, props.step)).toFixed(precision.value) emitValue(getNumberValue(res)) } function onDown(): void { const res Math.max(props.min, add(props.value || 0, -props.step)).toFixed(precision.value) emitValue(getNumberValue(res)) } /script template div tabindex1 classinput-number-wrap :class{ input-number-disabled: disabled } :style --input-number-width: ${inputWidth}; --input-number-primary-color: ${colorPalettes[5]}; --input-number-primary-color-hover: ${colorPalettes[4]}; --input-number-primary-color-focus: ${colorPalettes[4]}; --input-number-primary-shadow-color: ${shadowColor}; div classinput-number-container span v-ifshowPrefix classinput-prefix slot nameprefix{{ prefix }}/slot /span input refinputRef classinput-number autocompleteoff :disableddisabled :placeholderplaceholder v-modelnumValue inputonInput changeonChange keydown.up.prevent keydownkeyboard ? onKeyboard($event) : () false keydown.enter.preventonEnter / /div div classinput-number-handler-wrap span classinput-number-arrow up-arrow :class{ arrow-disabled: (value || 0) max } click(value || 0) max ? () false : onUp() svg classicon-svg focusablefalse >script setup langts import InputNumber from ./InputNumber.vue import { ref, watchEffect } from vue import { formatNumber } from vue-amazing-ui const value ref(3) const lazyValue ref(6) const formatValue1 ref(1000) const formatValue2 ref(100) const disabled ref(true) watchEffect(() { console.log(value, value.value) }) watchEffect(() { console.log(lazyValue, lazyValue.value) }) watchEffect(() { console.log(formatValue1, formatValue1.value) }) watchEffect(() { console.log(formatValue2, formatValue2.value) }) function formatter(value: number): string { return formatNumber(value, 2) % } function parser(value: string): number { return Number(value.replace(/[,%]/g, )) } function onChange(number: number) { console.log(change, number) } function onEnter(e: KeyboardEvent) { console.log(enter, e) } /script template div h1{{ $route.name }} {{ $route.meta.title }}/h1 h2 classmt30 mb10基本使用/h2 Space gapsmall vertical Alert typeinfo .lazy: br / 默认情况下v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。 br / 你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据 br / {{ InputNumber v-model:value.lazyvalue / }} /Alert InputNumber :width120 v-model:valuevalue placeholderBasic usage changeonChange enteronEnter / InputNumber :width120 v-model:value.lazylazyValue placeholderLazy usage changeonChange enteronEnter / /Space h2 classmt30 mb10步数为小数/h2 InputNumber :step0.1 v-model:valuevalue / h2 classmt30 mb10设置数值精度/h2 InputNumber :min-10 :max10 :step0.6 :precision2 v-model:valuevalue / h2 classmt30 mb10格式化展示/h2 Space InputNumber :width120 v-model:valueformatValue1 :formatter(value: number) $ ${value}.replace(/\B(?(\d{3})(?!\d))/g, ,) :parser(value: string) value.replace(/\$\s?|(,*)/g, ) / InputNumber :width120 v-model:valueformatValue2 :formatterformatter :parserparser / /Space h2 classmt30 mb10自定义最大最小值/h2 InputNumber :min0 :max10 v-model:valuevalue / h2 classmt30 mb10添加前缀图标 $/h2 InputNumber prefix$ v-model:valuevalue / h2 classmt30 mb10禁用/h2 Space vertical Space aligncenterdisabled: Switch v-modeldisabled //Space InputNumber v-model:valuevalue :disableddisabled / /Space /div /template