輸入人民幣金額的參數要求:
輸入要求:
- 通過鍵盤,只允許輸入負號、小數點、數字、退格鍵、刪除鍵、方向左鍵、方向右鍵、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
示例代碼:
<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-inputv-model="total"@input="total = formatInputRMB($event)"@blur="total = formatRMB(formatToRMB(total))"@keyup.enter.native="$event.target.blur()"placeholder="請輸入金額,按回車確認" />
</template>
?formatter.ts
/*** 格式化輸入人民幣金額* @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
示例代碼:
<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-inputv-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
優點:輸入方便,可以自由輸入,保持原值
缺點:無
<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];}*/// 如果過濾后為空 或 無效內容,則清空 totalif (!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-inputv-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
?文件:
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];}*/// 如果過濾后為空 或 無效內容,則清空 totalif (!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
?文件如下:
<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-inputv-model="inputValue"@focus="handleFocus"@blur="handleBlur"@keydown="handleKeyDown"@input="handleInput"@keyup.enter.native="$event.target.blur()"placeholder="請輸入金額,按回車確認" />
</template>
?? 效果:
輸入前
輸入中
回車確認后
? 在組件中使用這個 Hook,使用方式二,直接定義使用
組件?MaterialOut.vue
?文件如下:
<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-inputv-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 是一個 refreturn {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
?文件如下:
?
<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-inputv-model="inputRMBHooks.inputValue"@focus="inputRMBHooks.handleFocus"@blur="inputRMBHooks.handleBlur"@keydown="inputRMBHooks.handleKeyDown"@input="inputRMBHooks.handleInput"@keyup.enter.native="$event.target.blur()"placeholder="請輸入金額,按回車確認" />
</template>?