可自定义设置以下属性:
数字输入框宽度(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
获取依赖注入 useInject
<script setup lang="ts"> 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(defineProps<Props>(), { 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 = ref<string>() // 数字输入框的内容 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 tabindex="1" class="input-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 class="input-number-container"> <span v-if="showPrefix" class="input-prefix"> <slot name="prefix">{{ prefix }}</slot> </span> <input ref="inputRef" class="input-number" autocomplete="off" :disabled="disabled" :placeholder="placeholder" v-model="numValue" @input="onInput" @change="onChange" @keydown.up.prevent @keydown="keyboard ? onKeyboard($event) : () => false" @keydown.enter.prevent="onEnter" /> </div> <div class="input-number-handler-wrap"> <span class="input-number-arrow up-arrow" :class="{ 'arrow-disabled': (value || 0) >= max }" @click="(value || 0) >= max ? () => false : onUp()" > <svg class="icon-svg" focusable="false" ><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 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 class="mt30 mb10">基本使用</h2> <Space gap="small" vertical> <Alert type="info"> .lazy: <br /> 默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。 <br /> 你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据: <br /> {{ '<InputNumber v-model:value.lazy="value" />' }} </Alert> <InputNumber :width="120" v-model:value="value" placeholder="Basic usage" @change="onChange" @enter="onEnter" /> <InputNumber :width="120" v-model:value.lazy="lazyValue" placeholder="Lazy usage" @change="onChange" @enter="onEnter" /> </Space> <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> <Space> <InputNumber :width="120" v-model:value="formatValue1" :formatter="(value: number) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')" :parser="(value: string) => value.replace(/\$\s?|(,*)/g, '')" /> <InputNumber :width="120" v-model:value="formatValue2" :formatter="formatter" :parser="parser" /> </Space> <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> <Space vertical> <Space align="center">disabled: <Switch v-model="disabled" /></Space> <InputNumber v-model:value="value" :disabled="disabled" /> </Space> </div> </template>