在 Vue 项目里开发金额输入框是非常普遍的需求,但要实现操作顺手、用户体验出色的功能,往往需要精心设计。尤其在转账、证券交易、大额支付等金融场景中,既要确保用户流畅输入,又要保证数据精准、界面整洁。以下自定义指令 v-money-input 将整套逻辑封装完善,开箱即用,能有效解决金额输入中的交互痛点。
我们先来看看该指令的核心能力:

- 位数控制:默认支持 14 位整数加 2 位小数,超出范围的输入会自动拦截。
- 千分位格式:用户在输入及失焦时,系统自动按千位添加逗号分隔,展示为
1,000这样的清晰格式。 - 自适应字号:根据纯数字字符长度,输入框字体在 48px、38px、28px、22px 之间动态切换,兼顾视觉冲击与数据完整展示。
- 粘贴兼容:无论从 Excel、网页或其他工具复制数字,即使带有格式,也能正确解析。
简单来说,这个指令解决的是一个看似简单实则复杂的交互难题——让金额输入既符合用户习惯,又满足业务严谨性。
1. 创建指令文件 directives/moneyInput.js
在 src 目录下新建 directives 文件夹,并创建 moneyInput.js。代码结构清晰,有几个关键点值得关注:
// src/directives/moneyInput.jsconst moneyInput = {
inserted(el, binding) {
// 1. 初始化配置
const config = {
integerLimit: 14, // 整数部分最大位数
decimalLimit: 2, // 小数部分最大位数
...binding.value,
}; // 2. 设置基础样式(防止字体过小影响布局)
el.style.textAlign = 'right';
el.style.fontVariantNumeric = 'tabular-nums'; // 数字等宽,防止抖动
el.style.transition = 'font-size 0.1s ease'; // 3. 绑定事件
el.addEventListener('input', handleInput);
el.addEventListener('blur', handleBlur);
el.addEventListener('focus', handleFocus);
el.addEventListener('paste', handlePaste); // 内部状态记录
el._moneyConfig = config; // --- 核心逻辑函数 --- // 处理输入过程
function handleInput(e) {
let value = e.target.value;
// 过滤非数字和非小数点字符(允许退格、删除、箭头等控制键)
// 注意:这里保留用户正在输入的逗号,但在清理时会去掉
let cleaned = value.replace(/[^d.]/g, ''); // 处理多个小数点的情况,只保留第一个
const decimalIndex = cleaned.indexOf('.');
if (decimalIndex !== -1) {
// 如果有多个小数点,截取第一个及其后面的部分,并去除后续的小数点
const firstPart = cleaned.substring(0, decimalIndex + 1);
const secondPart = cleaned.substring(decimalIndex + 1).replace(/./g, '');
cleaned = firstPart + secondPart;
} // 字符长度限制逻辑
const parts = cleaned.split('.');
let integerPart = parts[0];
let decimalPart = parts[1] || ''; // 限制整数位
if (integerPart.length > config.integerLimit) {
integerPart = integerPart.slice(0, config.integerLimit);
}
// 限制小数位
if (decimalPart.length > config.decimalLimit) {
decimalPart = decimalPart.slice(0, config.decimalLimit);
} // 重组最终值
let finalValue = integerPart;
if (config.decimalLimit > 0) {
finalValue += '.' + decimalPart;
} // 更新 DOM
e.target.value = finalValue;
// 更新字体大小
updateFontSize(el, integerPart.length + decimalPart.length);
} // 处理失去焦点(格式化)
function handleBlur(e) {
let value = e.target.value;
if (!value) return; const parts = value.split('.');
let integerPart = parts[0];
let decimalPart = parts[1] || ''; // 补齐小数位
if (config.decimalLimit > 0 && decimalPart.length < config.decimalLimit) {
decimalPart = decimalPart.padEnd(config.decimalLimit, '0');
} // 添加千位分隔符
integerPart = addCommas(integerPart); e.target.value = decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
updateFontSize(el, integerPart.replace(/,/g, '').length + decimalPart.length);
} // 处理获取焦点(去除格式,方便修改)
function handleFocus(e) {
let value = e.target.value;
// 移除千位分隔符,只保留纯数字
e.target.value = value.replace(/,/g, '');
} // 处理粘贴
function handlePaste(e) {
e.preventDefault();
const pasteData = e.clipboardData.getData('text/plain');
// 模拟 input 事件处理逻辑
const tempDiv = document.createElement('div');
tempDiv.textContent = pasteData;
// 尝试提取数字
const numbers = tempDiv.textContent.replace(/[^d.]/g, '');
// 触发 input 逻辑
el.dispatchEvent(new Event('input'));
} // 辅助函数:添加千位分隔符
function addCommas(str) {
return str.replace(/B(?=(d{3})+(?!d))/g, ',');
} // 辅助函数:更新字体大小
function updateFontSize(element, charLength) {
let newSize = 48; // 默认 1-7 个字符
if (charLength > 16) newSize = 22;
else if (charLength > 12) newSize = 28;
else if (charLength > 8) newSize = 38;
else if (charLength > 7) newSize = 48; element.style.fontSize = `${newSize}px`;
}
}, unbind(el) {
// 销毁事件监听,防止内存泄漏
el.removeEventListener('input', handleInput);
el.removeEventListener('blur', handleBlur);
el.removeEventListener('focus', handleFocus);
el.removeEventListener('paste', handlePaste);
},
};export default moneyInput;
2. 注册全局指令 main.js
在入口文件中引入并注册,一行代码即可全局生效。
import Vue from 'vue'
import App from './App.vue'
import moneyInput from './directives/moneyInput'Vue.directive('money-input', moneyInput)new Vue({
render: h => h(App),
}).$mount('#app')
3. 在组件中使用 (Template)
模板中可直接绑定并传递参数。默认配置为 14 位整数 + 2 位小数;若证券业务需要更大额度(如 18 位整数),只需传入配置对象即可。
<template>
<div class="container">
<h3>转账金额h3>
<input
type="text"
v-money-input
placeholder="请输入金额"
@keyup.enter="submit"
/> <h3>证券经纪存款 (可配置不同位数)h3>
<input
type="text"
v-money-input="{ integerLimit: 18, decimalLimit: 2 }"
placeholder="证券经纪存款"
/>
div>
template><script>
export default {
methods: {
submit(e) {
// 这里的 e.target.value 将是纯数字字符串(例如 "1234567890.00")
console.log('提交金额:', e.target.value);
alert(`实际提交数值: ${e.target.value}`);
}
}
}
script><style scoped>
.container {
padding: 20px;
}
input {
width: 300px;
padding: 10px;
border: 1px solid #ccc;
margin-bottom: 20px;
/* 初始字体大小建议设置为最大的 48px */
font-size: 48px;
font-family: Arial, sans-serif;
outline: none;
}
input:focus {
border-color: #007bff;
}
style>
关键点说明
有几个细节值得重点强调:
- 字符计数逻辑:
updateFontSize函数中计算长度时,使用integerPart.replace(/,/g, '').length + decimalPart.length,将逗号明确排除在外。这正好满足你的需求——字符“不包括小数点和千位分隔符”。 - 空值处理:当用户删除所有内容后,blur 事件不会强制补零,输入框保持空白,这种设计非常人性化,不干扰用户操作。
- 精度问题:该指令仅负责前端展示层的格式化。实际提交给后端时,获取到的是标准的数字字符串(如
1000000.50)。后端通过parseFloat或数据库字段处理,彻底规避了前端浮点数精度常遇的陷阱。
整体思路正是如此。该指令已全面考虑了金额输入中的边界情形:粘贴、退格、多小数点、千位格式化、字体自适应等。只需稍作配置即可直接复用,无需从零开发轮子。这种设计堪称工程化组件的典范。
