news 2026/8/26 13:39:24

Vue3数字输入框(InputNumber)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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

  • 获取依赖注入 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>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 13:38:06

基于MCP Server构建AI可查询的错误知识库:从协议到实践

把一段崩溃日志直接扔给大模型&#xff0c;它往往会回你一句“这可能是网络问题”——这个场景&#xff0c;很多开发者已经熟悉到麻木。原因倒也不难理解&#xff1a;大模型不是一个精确的检索系统&#xff0c;它对具体异常的理解来自训练数据里的概率分布&#xff0c;而不是你…

作者头像 李华
网站建设 2026/8/26 13:37:23

MCP Server实践:AI错误诊断工具,让报错不再难懂

最近 MCP 生态里冒出来的工具&#xff0c;十有八九是“连接数据库”“操作浏览器”“读写文件”这类偏基础设施的 server。这次看到的这个项目方向不太一样&#xff1a;它是一个“能看懂错误消息到底在说什么”的 MCP server。简单说&#xff0c;你把一段报错丢给它&#xff0c…

作者头像 李华
网站建设 2026/8/26 13:35:30

来看界面控件DevExtreme如何实现数据表单的高效动态更新

DevExtreme拥有高性能的HTML5 / JavaScript小部件集合&#xff0c;使您可以利用现代Web开发堆栈&#xff08;包括React&#xff0c;Angular&#xff0c;ASP.NET Core&#xff0c;jQuery&#xff0c;Knockout等&#xff09;构建交互式的Web应用程序&#xff0c;该套件附带功能齐…

作者头像 李华
网站建设 2026/8/26 13:33:32

AI网站构建器背后:结构化页面表示与生成链路解析

AI 网站构建器&#xff08;AI website builder&#xff09;这几年最大的变化&#xff0c;不是它能生成一段漂亮的 HTML&#xff0c;而是它开始用一套可解释的结构化数据来“表示”一个落地页。也就是说&#xff0c;AI 生成 landing page design 的过程&#xff0c;本质上是在完…

作者头像 李华
网站建设 2026/8/26 13:28:06

基于高德API与异步Python构建智能选址AI Skill实战

1. 项目缘起&#xff1a;一个“选址直觉”的诞生与困惑 做线下生意&#xff0c;尤其是开一家新店&#xff0c;选址永远是那个最让人头疼、也最关键的决策。我干了十几年零售和餐饮&#xff0c;从街边小店到商场铺位&#xff0c;踩过的坑比走过的路还多。时间长了&#xff0c;脑…

作者头像 李华
网站建设 2026/8/26 13:25:12

Matlab数据导入实战:从格式兼容到内存优化

1. 为什么“导入数据”是Matlab使用者每天睁眼第一件事 在Matlab里&#xff0c; 导入数据 不是个功能模块&#xff0c;而是整个工作流的起点和命门。我带过二十多个高校课题组、帮八家工业客户做过算法落地&#xff0c;见过太多人卡在第一步&#xff1a;数据还没进内存&#…

作者头像 李华