当前位置: 首页 > news >正文

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>
http://www.cnnetsun.cn/news/4225173.html

相关文章:

  • 基于MCP Server构建AI可查询的错误知识库:从协议到实践
  • MCP Server实践:AI错误诊断工具,让报错不再难懂
  • 来看界面控件DevExtreme如何实现数据表单的高效动态更新
  • AI网站构建器背后:结构化页面表示与生成链路解析
  • 基于高德API与异步Python构建智能选址AI Skill实战
  • Matlab数据导入实战:从格式兼容到内存优化
  • 数维杯B题建模思路1.0:数据沼泽中的最小可行闭环
  • Codex限流与配置故障排查:从429到config.toml修复指南
  • DeepSeek V4 Flash测评框架:性能、延迟与成本控制实战
  • Gemini反代API工程指南:密钥、协议转换与排查
  • 用户价值分析最小闭环:从埋点到RFM分群与流失预警
  • 20天高效备战大厂面试:策略与实战指南
  • VMware Workstation安装Windows 11虚拟机完整指南与踩坑排查
  • HyperMesh与Inspire协同:拓扑优化到尺寸优化完整流程
  • PostgreSQL与MySQL语法差异详解:从建表到高级查询的实战对比
  • 当汽车电机控制器遇上工业液冷电源:热管理驱动的跨界机遇
  • CodeX、Ollama、Coze多智能体协作:企业级AI编码工作流实战
  • MATLAB fmincon非线性规划实战:从报错到收敛的完整指南
  • C语言宏定义括号规范:避免运算符优先级陷阱与副作用风险
  • 机器学习数据预处理:标准化、归一化与正则化的原理与应用
  • 超低功耗信号处理实战:从数据搬运到事件驱动的能效设计
  • Dinic算法性能飞跃:详解当前弧优化原理与实战代码
  • 共享单车调度优化建模实战:从问题解构到三层决策框架
  • 二进制速率乘法器(BRM)原理、Verilog实现与工程实战
  • VexFlow:10分钟实现Web动态乐谱渲染与交互开发
  • 基于AgentScope的企业级智能体平台全生命周期管理实践
  • RVM相关向量机实战:从SVM调参到稀疏概率预测全解析
  • STM32F103R8T6中文开发实战:从芯片解析到工程落地
  • Steam Deck装Android实战:Waydroid容器化部署指南
  • OpenCV+CNN车牌识别系统实战:从定位到字符识别全流程解析