输入人民币金额的参数要求:
输入要求:
- 通过键盘,只允许输入负号、小数点、数字、退格键、删除键、方向左键、方向右键、Home键、End键、Tab键;
- 负号只能在开头;
- 只保留第一个小数点;
- 替换全角输入的小数点,即"。"替换为".";
- 小数点开头的前面补0;
- 限制小数点后最多两位;
- 去除前导零(非小数的情况下,去除开头的0);
- 通过复制粘贴的,确保粘贴出来的内容符合上述的要求。
显示要求:
- 聚焦时 @focus 显示原始金额数值,如:1234.56;
- 失焦时 @blur 格式化金额数值,如:¥ 1,234.56;
- 键盘输入 @keydown 按输入要求过滤内容;
- 输入内容 @input 防漏兜底,防止通过粘贴输入的,按输入要求过滤内容;
- 回车时 @keyup.enter.native="$event.target.blur()" 失焦;
方法一:使用 el-input 的 @input 和 @blur
优点:输入方便,可以自由输入
缺点:改变原值
如:
输入的原值为:1234.56
显示的内容为:¥1,234.56
最终值为:¥1,234.56
示例代码:
TypeScript
<script setup lang="ts" name="MaterialOut">
import { ref } from "vue";
import { formatInputRMB, formatToRMB, formatRMB } from "@/utils/formatter";
// 金额
const total = ref<string>("");
</script>
<template>
<!-- 控制输入:@input="total = formatInputRMB($event)" 控制只能输入数字、小数点(两位小数) -->
<!-- 控制显示:@blur="total = formatRMB(formatToRMB(total))" 输入框失去焦点时,将输入框内容格式化为人民币格式 -->
<!-- 控制失焦:@keyup.enter.native="$event.target.blur()" 按回车输入框失去焦点 -->
<el-input
v-model="total"
@input="total = formatInputRMB($event)"
@blur="total = formatRMB(formatToRMB(total))"
@keyup.enter.native="$event.target.blur()"
placeholder="请输入金额,按回车确认" />
</template>
formatter.ts
TypeScript
/**
* 格式化输入人民币金额
* @param val 输入值
* @returns 数字字符串,如:0、123、1234.0、1234.56
*/
export const formatInputRMB = (val: string) => {
let result = val ?? "";
// 格式化输入的内容
result = result
// 替换全角输入的小数点
.replace(/。/g, ".")
// 只保留数字和小数点
.replace(/[^\d.]/g, "")
// 小数点开头的前面补0
.replace(/^\./, "0.")
// 只保留第一个小数点
.replace(/(\..*)\./g, "$1")
// 去除前导零,非小数的情况下,去除开头的0
.replace(/^0+(\d)/, "$1")
// 限制小数点后最多两位
.replace(/^(\d+\.\d{2})\d+/, "$1");
return result;
};
/**
* 格式化为人民币金额
* @param val 字符串或数字
* @param rounding 是否四舍五入(默认 true,若设为 false 则直接截断)
* @returns 数字字符串,如:0.00、1.20、123.04、1234.56
*/
export const formatToRMB = (val: string | number | null, rounding: boolean = true) => {
let result = String(val || "0.00");
// 格式化输入的内容
result = result
// 替换全角输入的小数点
.replace(/。/g, ".")
// 只保留数字和小数点
.replace(/[^\d.]/g, "")
// 小数点开头的前面补0
.replace(/^\./, "0.")
// 只保留第一个小数点
.replace(/(\..*)\./g, "$1")
// 去除前导零,非小数的情况下,去除开头的0
.replace(/^0+(\d)/, "$1");
// 分割整数部分和小数部分
let [integer = "0", decimal = ""] = result.split(".");
// 四舍五入处理小数部分
if (rounding) {
// 四舍五入处理(通过 Number 转换自动处理)
const rounded = Math.round(Number(`${integer}.${decimal}`) * 100) / 100;
return rounded.toFixed(2);
}
// 截断处理小数部分
else {
// 截断并补零
decimal = decimal.slice(0, 2).padEnd(2, "0");
// 确保整数部分存在
integer = integer ?? "0";
return `${integer}.${decimal}`;
}
};
/**
*
* 格式化为带符号和千分位的人民币金额
* @param val 字符串或数字
* @param rounding 是否四舍五入(默认 true,若设为 false 则直接截断)
* @returns 人民币金额字符串,如:¥0.00、¥1.20、¥123.04、¥1,234.56
*/
export const formatRMB = (val: string | number) => {
let result = formatToRMB(val);
// 添加人民币符号 ¥,添加千分位 ,
result = "¥" + result.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return result;
};
效果:
输入前

输入中

回车确认后

方法二:使用 el-input 的 :formatter 和 :parser
优点:保持原值
缺点:输入受限,不能自由输入
如:
输入的原值为:1234.56
显示的内容为:¥1,234.56
最终值为:1234.56
示例代码:
TypeScript
<script setup lang="ts" name="MaterialOut">
import { ref } from "vue";
// 存储原始数值(用于业务逻辑)
const total = ref<number | null>(null);
// 格式化显示金额(用于 input 显示)
const formatRMB = (value: number | null): string => {
if (value === null || isNaN(value)) return "";
const valStr = value.toFixed(2);
const [integer, decimal] = valStr.split(".");
const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return `¥ ${formattedInteger}.${decimal}`;
};
// 解析输入内容(用于 input 输入)
const parseRMB = (value: string): string => {
// 去除非数字和小数点字符
let filtered = value.replace(/[^\d.]/g, "").replace(/(\..*)\./g, "$1");
if (filtered === "." || filtered === "") return "";
const [integer = "0", decimal] = filtered.split(".");
const cleanInteger = integer.replace(/^0+/, "") || "0";
const cleanDecimal = decimal ? decimal.slice(0, 2) : "";
return cleanDecimal ? `${cleanInteger}.${cleanDecimal}` : cleanInteger;
};
// 输入事件处理
const handleInput = (value: string) => {
const parsed = parseRMB(value);
total.value = parsed ? parseFloat(parsed) : null;
};
</script>
<template>
<el-input
v-model="total"
:formatter="(val: string) => formatRMB(val ? parseFloat(val) : null)"
:parser="(val: string) => parseRMB(val)"
@input="handleInput"
@keyup.enter.native="$event.target.blur()"
type="text"
placeholder="请输入金额,按回车确认" />
</template>
效果:
输入前

输入中

回车确认后

方法三(推荐):使用 el-input 的 @focus、@blur、@keydown、@input
优点:输入方便,可以自由输入,保持原值
缺点:无
TypeScript
<script setup lang="ts" name="MaterialOut">
import { ref } from "vue";
// 存储原始数值(用于业务逻辑)
const total = ref<number | null>(null);
// 输入框内部状态(带格式)
const inputValue = ref<string>("");
// 处理键盘输入
function handleKeyDown(e: KeyboardEvent) {
// 只允许输入负号、小数点、数字、退格键、删除键、方向左键、方向右键、Home键、End键、Tab键
const allowedKeys = [
"-",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
".",
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"Home",
"End",
"Tab"
];
// 阻止非法字符输入
if (!allowedKeys.includes(e.key)) {
e.preventDefault();
return;
}
const inputEl = e.target as HTMLInputElement;
const cursorPos = inputEl.selectionStart || 0;
const value = inputValue.value;
// 阻止输入多个小数点
if (e.key === "." && value.includes(".")) {
e.preventDefault();
return;
}
// 阻止输入多个负号 或 负号不在开头
if (e.key === "-" && (value.includes("-") || cursorPos !== 0)) {
e.preventDefault();
return;
}
// 辅助按键,则不阻止,任何时候都允许输入
const assistantKeys = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Home", "End", "Tab"];
if (assistantKeys.includes(e.key)) {
return;
}
// 如果已有小数点,并且光标在小数点后,限制最多两位
if (value.includes(".")) {
const decimalIndex = value.indexOf(".");
const parts = value.split(".");
// 仅当光标在小数点之后时才限制输入
if (parts[1] && parts[1].length >= 2 && cursorPos > decimalIndex) {
e.preventDefault();
}
}
}
// 处理输入内容(防漏兜底,防止不是通过键盘输入,而是通过粘贴输入的)
function handleInput(value: string) {
// 过滤输入的内容
let filtered = value
// 替换全角输入的小数点
.replace(/。/g, ".")
// 只保留负号、数字和小数点
.replace(/[^-\d.]/g, "")
// 小数点开头的前面补0
.replace(/^\./, "0.")
// 只保留第一个小数点
.replace(/(\..*)\./g, "$1")
// 负号只能在开头
.replace(/(\--*)\-/g, "$1")
// 去除前导零(非小数的情况下,去除开头的0)
.replace(/^0+(\d)/, "$1")
// 限制小数点后最多两位
.replace(/^(\d+\.\d{2})\d+/, "$1");
// 负号只能在开头,等效于 replace(/(\--*)\-/g, "$1")
// if (filtered.startsWith("-")) {
// const rest = filtered.slice(1).replace(/[^\d.]/g, "");
// filtered = "-" + rest;
// } else {
// filtered = filtered.replace(/[^\d.]/g, "");
// }
/*
// 处理小数点
const parts = filtered.split(".");
// 只保留第一个小数点,等效于 replace(/(\..*)\./g, "$1")
if (parts.length > 2) {
filtered = parts[0] + "." + parts.slice(1).join("");
}
// 限制小数点后最多两位
if (parts[1]) {
// 限制小数部分最多两位,等效于 replace(/^(\d+\.\d{2})\d+/, "$1")
parts[1] = parts[1].slice(0, 2);
filtered = parts[0] + "." + parts[1];
}
*/
// 如果过滤后为空 或 无效内容,则清空 total
if (!filtered || filtered === "-" || filtered === "." || filtered === "-.") {
total.value = null;
} else {
total.value = parseFloat(filtered);
}
inputValue.value = filtered;
}
// 获得焦点时显示原始值
function handleFocus() {
inputValue.value = total.value?.toString() || "";
}
// 失去焦点后格式化显示为人民币格式
function handleBlur() {
const rawValue = inputValue.value;
if (!rawValue) {
inputValue.value = "";
total.value = null;
return;
}
const num = parseFloat(rawValue);
if (isNaN(num)) {
inputValue.value = "";
total.value = null;
return;
}
total.value = num;
const [integerPart, decimalPart = "00"] = Math.abs(num).toFixed(2).split(".");
// 千分位格式化
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const sign = num < 0 ? "-" : "";
// 格式化为人民币格式
inputValue.value = `¥ ${sign}${formattedInteger}.${decimalPart}`;
}
</script>
<template>
<!-- 控制输入:@keydown="handleKeyDown" 和 @input="handleInput" -->
<!-- 控制显示:@focus="handleFocus"和 @blur="handleBlur" -->
<!-- 控制失焦:@keyup.enter.native="$event.target.blur()" 按回车输入框失去焦点 -->
<el-input
v-model="inputValue"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeyDown"
@input="handleInput"
@keyup.enter.native="$event.target.blur()"
placeholder="请输入金额,按回车确认" />
</template>
效果:
输入前
输入中

回车确认后

将这部分逻辑封装成一个可复用的 Vue Composition API Hook
✅ 封装目标
我们将以下功能抽离为一个 useInputRMB()
Hook:
功能 | 描述 |
---|---|
输入限制 | 键盘拦截非法字符(数字、负号、小数点) |
全角支持 | 支持 。 和 - 自动转换 |
粘贴支持 | Ctrl+V / 鼠标右键粘贴内容自动过滤 |
回车失焦 | 按下 Enter 键后输入框失去焦点 |
数据模型 | 返回 inputValue 用于绑定到 <el-input> |
数值存储 | 返回 total 表示原始 `number |
失焦格式化 | 显示为 ¥ -1,234.56 格式 |
✅ 完整 Hook 实现:useInputRMB.ts
你可以将下面这段代码保存为 src/hooks/useInputRMB.ts
文件:
TypeScript
import { ref } from "vue";
export function useInputRMB() {
// 存储原始数值(用于业务逻辑)
const total = ref<number | null>(null);
// 输入框内部状态(带格式)
const inputValue = ref<string>("");
// 处理键盘输入
function handleKeyDown(e: KeyboardEvent) {
// 只允许输入负号、小数点、数字、退格键、删除键、方向左键、方向右键、Home键、End键、Tab键
const allowedKeys = [
"-",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
".",
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"Home",
"End",
"Tab"
];
// 阻止非法字符输入
if (!allowedKeys.includes(e.key)) {
e.preventDefault();
return;
}
const inputEl = e.target as HTMLInputElement;
const cursorPos = inputEl.selectionStart || 0;
const value = inputValue.value;
// 阻止输入多个小数点
if (e.key === "." && value.includes(".")) {
e.preventDefault();
return;
}
// 阻止输入多个负号 或 负号不在开头
if (e.key === "-" && (value.includes("-") || cursorPos !== 0)) {
e.preventDefault();
return;
}
// 辅助按键,则不阻止,任何时候都允许输入
const assistantKeys = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Home", "End", "Tab"];
if (assistantKeys.includes(e.key)) {
return;
}
// 如果已有小数点,并且光标在小数点后,限制最多两位
if (value.includes(".")) {
const decimalIndex = value.indexOf(".");
const parts = value.split(".");
// 仅当光标在小数点之后时才限制输入
if (parts[1] && parts[1].length >= 2 && cursorPos > decimalIndex) {
e.preventDefault();
}
}
}
// 处理输入内容(防漏兜底,防止不是通过键盘输入,而是通过粘贴输入的)
function handleInput(value: string) {
// 过滤输入的内容
let filtered = value
// 替换全角输入的小数点
.replace(/。/g, ".")
// 只保留负号、数字和小数点
.replace(/[^-\d.]/g, "")
// 小数点开头的前面补0
.replace(/^\./, "0.")
// 只保留第一个小数点
.replace(/(\..*)\./g, "$1")
// 负号只能在开头
.replace(/(\--*)\-/g, "$1")
// 去除前导零(非小数的情况下,去除开头的0)
.replace(/^0+(\d)/, "$1")
// 限制小数点后最多两位
.replace(/^(\d+\.\d{2})\d+/, "$1");
// 负号只能在开头,等效于 replace(/(\--*)\-/g, "$1")
// if (filtered.startsWith("-")) {
// const rest = filtered.slice(1).replace(/[^\d.]/g, "");
// filtered = "-" + rest;
// } else {
// filtered = filtered.replace(/[^\d.]/g, "");
// }
/*
// 处理小数点
const parts = filtered.split(".");
// 只保留第一个小数点,等效于 replace(/(\..*)\./g, "$1")
if (parts.length > 2) {
filtered = parts[0] + "." + parts.slice(1).join("");
}
// 限制小数点后最多两位
if (parts[1]) {
// 限制小数部分最多两位,等效于 replace(/^(\d+\.\d{2})\d+/, "$1")
parts[1] = parts[1].slice(0, 2);
filtered = parts[0] + "." + parts[1];
}
*/
// 如果过滤后为空 或 无效内容,则清空 total
if (!filtered || filtered === "-" || filtered === "." || filtered === "-.") {
total.value = null;
} else {
total.value = parseFloat(filtered);
}
inputValue.value = filtered;
}
// 获得焦点时显示原始值
function handleFocus() {
inputValue.value = total.value?.toString() || "";
}
// 失去焦点后格式化显示为人民币格式
function handleBlur() {
const rawValue = inputValue.value;
if (!rawValue) {
inputValue.value = "";
total.value = null;
return;
}
const num = parseFloat(rawValue);
if (isNaN(num)) {
inputValue.value = "";
total.value = null;
return;
}
total.value = num;
const [integerPart, decimalPart = "00"] = Math.abs(num).toFixed(2).split(".");
// 千分位格式化
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const sign = num < 0 ? "-" : "";
// 格式化为人民币格式
inputValue.value = `¥ ${sign}${formattedInteger}.${decimalPart}`;
}
return {
inputValue,
total,
handleKeyDown,
handleInput,
handleBlur,
handleFocus
};
}
✅ 在组件中使用这个 Hook,使用方式一,解构使用
组件 MaterialOut.vue
文件如下:
TypeScript
<script setup lang="ts" name="MaterialOut">
import { useInputRMB } from "@/hooks/useInputRMB";
const { inputValue, total, handleKeyDown, handleInput, handleBlur, handleFocus } = useInputRMB();
</script>
<template>
<!-- 控制输入:@keydown="handleKeyDown" 和 @input="handleInput" -->
<!-- 控制显示:@focus="handleFocus"和 @blur="handleBlur" -->
<!-- 控制失焦:@keyup.enter.native="$event.target.blur()" 按回车输入框失去焦点 -->
<el-input
v-model="inputValue"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeyDown"
@input="handleInput"
@keyup.enter.native="$event.target.blur()"
placeholder="请输入金额,按回车确认" />
</template>
✅ 效果:
输入前

输入中

回车确认后

✅ 在组件中使用这个 Hook,使用方式二,直接定义使用
组件 MaterialOut.vue
文件如下:
TypeScript
<script setup lang="ts" name="MaterialOut">
import { useInputRMB } from "@/hooks/useInputRMB";
const inputRMBHooks = useInputRMB();
</script>
<template>
<!-- 控制输入:@keydown="handleKeyDown" 和 @input="handleInput" -->
<!-- 控制显示:@focus="handleFocus"和 @blur="handleBlur" -->
<!-- 控制失焦:@keyup.enter.native="$event.target.blur()" 按回车输入框失去焦点 -->
<!-- 这里注意,inputValue 需要 .value -->
<el-input
v-model="inputRMBHooks.inputValue.value"
@focus="inputRMBHooks.handleFocus"
@blur="inputRMBHooks.handleBlur"
@keydown="inputRMBHooks.handleKeyDown"
@input="inputRMBHooks.handleInput"
@keyup.enter.native="$event.target.blur()"
placeholder="请输入金额,按回车确认" />
</template>
直接原因
useInputRMB()
返回的 inputValue
本身是一个 ref
对象,而 inputRMBHooks
是一个普通对象(非响应式对象)。这种情况下,Vue 的模板无法自动解包嵌套在普通对象中的 ref
,所以需要手动通过 .value
访问。
typescript
复制
下载
// 假设 useInputRMB 的实现类似这样:
const useInputRMB = () => {
const inputValue = ref(""); // inputValue 是一个 ref
return {
inputValue, // 将 ref 直接暴露出去
};
};
为什么需要 .value
?
-
当
ref
被包裹在普通对象中时:-
如果
inputRMBHooks
是一个普通对象(如const inputRMBHooks = { inputValue: ref('') }
),模板不会自动解包内部的ref
。 -
此时必须通过
inputRMBHooks.inputValue.value
访问值。
-
-
如果
inputRMBHooks
是响应式对象(如reactive
):-
Vue 会自动解包第一层的
ref
,此时无需.value
。 -
但你的代码中
inputRMBHooks
可能是普通对象,或inputValue
被嵌套在更深层。
-
验证方法
检查 useInputRMB
的实现:
-
如果它返回的是
reactive
包裹的对象,且inputValue
是ref
,模板中应该不需要.value
。 -
如果返回的是普通对象,则需要
.value
。
解决方案(可选)
如果希望省略 .value
,可以将 inputRMBHooks
转为响应式对象:
typescript
复制
下载
const inputRMBHooks = reactive(useInputRMB());
然后在模板中直接使用 v-model="inputRMBHooks.inputValue"
(无需 .value
)。
总结
你的代码中需要 .value
,是因为 inputRMBHooks
是一个普通对象,且 inputValue
是 ref
,而 Vue 不会自动解包普通对象内部的 ref
。通过 .value
显式访问是必要的。
✅ 在组件中使用这个 Hook,使用方式三,reactive定义使用
组件 MaterialOut.vue
文件如下:
TypeScript
<script setup lang="ts" name="MaterialOut">
import { reactive } from "vue";
import { useInputRMB } from "@/hooks/useInputRMB";
const inputRMBHooks = reactive(useInputRMB());
</script>
<template>
<!-- 控制输入:@keydown="handleKeyDown" 和 @input="handleInput" -->
<!-- 控制显示:@focus="handleFocus"和 @blur="handleBlur" -->
<!-- 控制失焦:@keyup.enter.native="$event.target.blur()" 按回车输入框失去焦点 -->
<el-input
v-model="inputRMBHooks.inputValue"
@focus="inputRMBHooks.handleFocus"
@blur="inputRMBHooks.handleBlur"
@keydown="inputRMBHooks.handleKeyDown"
@input="inputRMBHooks.handleInput"
@keyup.enter.native="$event.target.blur()"
placeholder="请输入金额,按回车确认" />
</template>