在实际项目中,我们经常会遇到输入框只能输入数字、限制小数位、范围、补零、是否允许负数等场景。
如果这些逻辑都散落在业务代码里,不仅繁琐,还难以维护。
于是,这里提供一个高可定制的 Vue 指令v-hasNumber,帮你轻松搞定所有数字输入规则。
🔧 使用方式
在el-input上直接绑定指令,并传入配置项:
<el-input v-hasNumber="{ precision: 3 }" />
📑 配置参数说明

💻 指令内容:(hasNumber.ts)
/* eslint-disable */
// | 字段 | 类型 | 默认值 | 说明 |
// | ----------- | ------ | --------- | ---- |
// | `precision` | number | 8 | 小数位数 |
// | `maxInt` | number | 10 | 整数位数 |
// | `min` | number | -Infinity | 最小值 |
// | `max` | number | Infinity | 最大值 |
// | `zero` | boolean | true | 是否补零|
// | `negative` | boolean | false | 是否强制负数 |
// | `equal` | boolean | true | 等号输入 |
import type { App, DirectiveBinding } from 'vue';
function setup(el: HTMLElement) {
const input = el instanceof HTMLInputElement ? el : el.querySelector('input');
if (!input || input.dataset.hasNumberBound === '1') return;
input.dataset.hasNumberBound = '1';
// 添加composition状态标记
let isComposing = false;
// 监听输入法开始事件
input.addEventListener('compositionstart', () => {
isComposing = true;
});
// 监听输入法结束事件
input.addEventListener('compositionend', () => {
isComposing = false;
// 输入法结束时手动触发一次input处理
const event = new Event('input', { bubbles: true });
input.dispatchEvent(event);
});
input.addEventListener('keydown', (e) => {
const config = (el as any)._numberConfig;
if (!config) return;
const { precision, hasPrecision, min, equal } = config;
if (min >= 0 && e.key === '-') e.preventDefault();
if (hasPrecision && precision === 0 && e.key === '.') e.preventDefault();
if (!equal && e.key === '=') e.preventDefault();
});
input.addEventListener('input', () => {
// 如果正在使用输入法,不处理
if (isComposing) return;
const config = (el as any)._numberConfig;
if (!config) return;
const input = el instanceof HTMLInputElement ? el : el.querySelector('input');
let val = input!.value || '';
const { maxInt, precision, hasPrecision, defaultPrecision, equal } = config;
const isNegative = val.startsWith('-');
const allowChars = equal ? /[^0-9.=]/g : /[^0-9.]/g;
val = val.replace(allowChars, '');
if (equal) {
const firstEqualIndex = val.indexOf('=');
if (firstEqualIndex !== -1) {
val =
val.slice(0, firstEqualIndex + 1) +
val
.slice(firstEqualIndex + 1)
.replace(/=/g, '');
}
}
let [intPart = '', decPart = ''] = val.split('.');
intPart = intPart.slice(0, maxInt);
const effPrec = hasPrecision ? precision! : defaultPrecision;
decPart = effPrec > 0 ? decPart.slice(0, effPrec) : '';
let newVal = (isNegative ? '-' : '') + intPart;
if (val.includes('.')) {
newVal += '.' + decPart;
}
if (newVal !== input!.value) {
const pos = input!.selectionStart ?? newVal.length;
input!.value = newVal;
try {
input!.setSelectionRange(pos, pos);
} catch {}
input!.dispatchEvent(new Event('input', { bubbles: true }));
}
});
input.addEventListener('blur', () => {
// blur时重置composition状态
isComposing = false;
const config = (el as any)._numberConfig;
if (!config) return;
const input = el instanceof HTMLInputElement ? el : el.querySelector('input');
const { hasPrecision, precision, min, max, zero, negative } = config;
let val = input!.value;
if (!val || val === '-' || val === '.') {
input!.value = '';
input!.dispatchEvent(new Event('input', { bubbles: true }));
return;
}
let num = parseFloat(val);
if (isNaN(num)) {
input!.value = '';
} else {
if (num < min) num = min;
if (num > max) num = max;
if (negative && num > 0) num = -num;
const truncateDecimal = (num: number, precision: number) => {
const [intPart, decPart = ''] = num.toString().split('.');
return precision <= 0 || !decPart ? intPart : `${intPart}.${decPart.slice(0, precision)}`;
};
input!.value = hasPrecision
? (zero ? num.toFixed(precision!) : truncateDecimal(num, precision!))
: num.toString();
}
input!.dispatchEvent(new Event('input', { bubbles: true }));
},true);
}
// 每次 updated 或 mounted 都刷新配置
function updateConfig(el: HTMLElement, binding: DirectiveBinding) {
const value = binding.value || {};
const hasPrecision = Object.prototype.hasOwnProperty.call(value, 'precision');
(el as any)._numberConfig = {
maxInt: value.maxInt ?? 10,
precision: hasPrecision ? value.precision! : undefined,
hasPrecision,
defaultPrecision: 8,
min: value.min ?? -Infinity,
max: value.max ?? Infinity,
zero: value.zero === true,
negative: value.negative === true,
equal: value.equal === true,
};
}
export function hasNumber(app: App<Element>) {
app.directive('hasNumber', {
mounted(el, binding) {
setup(el);
updateConfig(el, binding);
},
updated(el, binding) {
updateConfig(el, binding);
}
});
}