在 Vue 项目中做金额录入时,vxe-number-input 的 type="amount" 可以直接作为数值型输入框使用。除了基本的数值约束,它还提供了浮动标签(float-content)和工具提示(show-tooltip)两类展示能力:前者适合在输入框上方标注“万”“元”这类计量单位,后者适合在输入过程中实时反馈当前数值对应的中文大写金额。下面按“基础标签配置—工具提示—自定义提示内容—金额转中文实现”的顺序整理具体写法。
一、用 float-content 在输入框上方显示浮动标签
给组件加上 float-content 属性,并填入需要提示的文字,该文字就会以浮动标签形式出现在输入框上方。下面的例子把标签设为“万”,用于提示当前输入值的计量单位:
- <template>
- <div>
- <vxe-number-input
- v-model="val1"
- placeholder="显示浮动标签"
- type="amount"
- float-content="万"
- clearable
- ></vxe-number-input>
- </div>
- </template>
- <script setup>
- import { ref } from 'vue'
- const val1 = ref(null)
- </script>
复制代码
这里的 float-content 只负责展示,不参与数值计算,输入框的绑定值仍然是 val1。因此金额单位换算、精度校验等逻辑仍需要在业务层自行处理,浮动标签只是把“当前输入的是什么单位”这一信息显式呈现出来。
二、用 show-tooltip 打开实时工具提示
如果希望输入时能看到当前数值的提示信息,可以加上 show-tooltip。此时工具提示会跟随输入值实时更新:
- <template>
- <div>
- <vxe-number-input
- v-model="val1"
- type="amount"
- placeholder="实时显示工具提示"
- show-tooltip
- ></vxe-number-input>
- </div>
- </template>
- <script setup>
- import { ref } from 'vue'
- const val1 = ref(null)
- </script>
复制代码
默认提示内容通常只包含数值本身,真正的定制空间在 tooltip-config 上。
三、用 tooltip-config.contentMethod 自定义提示内容
tooltip-config 支持 contentMethod,用来接管工具提示的文本生成。它接收一个对象参数 { value, label }:value 是当前输入值,label 是输入框的标签或占位符。返回值就是最终展示的提示文本。下面的例子把中文大写金额拼进提示中:
- <template>
- <div>
- <vxe-number-input
- v-model="val1"
- type="amount"
- placeholder="显示中文金额"
- :tooltip-config="tooltipConfig"
- show-tooltip
- clearable
- ></vxe-number-input>
- </div>
- </template>
- <script setup>
- import { ref, reactive } from 'vue'
- import { amountToChinese } from './util'
- const val1 = ref(null)
- const tooltipConfig = reactive({
- contentMethod({ value, label }) {
- return value ? `${label}\n${amountToChinese(value)}` : ''
- }
- })
- </script>
复制代码
示例中提示文本由两行组成:第一行是标签或占位符,第二行是转换后的中文金额;当 value 为空时返回空字符串,工具提示不展示内容。需要注意 contentMethod 是纯展示逻辑,不要在里面对 v-model 绑定的值做二次修改,否则容易和输入框自身的数值处理产生冲突。
四、金额转中文大写金额的函数实现
amountToChinese 的目标是把 123.45 这类数值转换为“壹佰贰拾叁元肆角伍分”。整体流程可以拆成四步:数值归一化、整数与小数拆分、整数部分按四位分组转换、拼接元角分并处理“整”和负号。原文实现如下:
- import XEUtils from 'xe-utils'
- /**
- * 将数字金额转换为中文大写
- */
- export function amountToChinese(num) {
- let amount = XEUtils.toNumber(num)
- if (amount === 0) {
- return '零元整'
- }
- // 处理负数
- const isNegative = amount < 0
- amount = Math.abs(amount)
- // 分离整数和小数部分
- const amountStr = amount.toFixed(2)
- const [integerPart, decimalPart] = amountStr.split('.')
- // 中文大写数字
- const digitsMap = {
- 0: '零', 1: '壹', 2: '贰', 3: '叁', 4: '肆',
- 5: '伍', 6: '陆', 7: '柒', 8: '捌', 9: '玖'
- }
- // 单位(每4位一组)
- const unitGroups = ['', '万', '亿', '万亿', '亿亿']
- // 每组的单位(千、百、十)
- const units = ['', '拾', '佰', '仟']
- // 转换整数部分
- function convertIntegerPart(numStr) {
- if (numStr === '0') return ''
- let result = ''
- // 按4位一组分组
- const groups = []
- let temp = numStr
- while (temp.length > 4) {
- groups.unshift(temp.slice(-4))
- temp = temp.slice(0, -4)
- }
- if (temp) {
- groups.unshift(temp)
- }
- groups.forEach((group, groupIndex) => {
- if (group === '0000') {
- // 整组为零,跳过但记录需要补零
- result += '零'
- return
- }
- let groupResult = ''
- let needZero = false // 是否需要在前面补零
- for (let i = 0; i < group.length; i++) {
- const digit = group[i]
- const position = group.length - 1 - i // 当前位数
- const unitIndex = position % 4
- if (digit !== '0') {
- // 如果上一个字符是0或者需要补零,且当前不是第一位
- if (needZero || (i > 0 && group[i - 1] === '0')) {
- // 避免多个零之间重复加"零"
- if (!groupResult.endsWith('零')) {
- groupResult += '零'
- }
- needZero = false
- }
- groupResult += digitsMap[digit]
- if (units[unitIndex]) {
- groupResult += units[unitIndex]
- }
- } else {
- needZero = true
- }
- }
- // 添加组单位
- if (groupResult && groupIndex < groups.length - 1) {
- groupResult += unitGroups[groups.length - 1 - groupIndex]
- }
- // 合并结果,处理相邻组的零
- if (groupResult) {
- if (result.endsWith('零')) {
- result = result.slice(0, -1) // 去掉多余的零
- }
- result += groupResult
- }
- })
- return result
- }
- // 转换小数部分
- function convertDecimalPart(numStr) {
- if (!numStr || numStr === '00') return ''
- let result = ''
- const [jiao, fen] = numStr.split('').map((d) => digitsMap[d])
- if (jiao !== '零') {
- result += jiao + '角'
- } else if (fen !== '零') {
- result += '零'
- }
- if (fen !== '零') {
- result += fen + '分'
- }
- return result
- }
- // 组装结果
- let result = ''
- // 整数部分
- const integerResult = convertIntegerPart(integerPart)
- if (integerResult) {
- result += integerResult + '元'
- } else if (decimalPart !== '00') {
- // 整数部分为零且有小数
- result = '零'
- }
- // 小数部分
- const decimalResult = convertDecimalPart(decimalPart)
- result += decimalResult
- // 如果只有整数没有小数,添加"整"
- if (decimalPart === '00') {
- result += '整'
- }
- // 处理负数
- if (isNegative) {
- result = '负' + result
- }
- return result
- }
复制代码
实现中有几个关键点需要注意:
1. 入口先用 XEUtils.toNumber 统一类型,避免字符串或空值直接参与运算;随后用 Math.abs 处理负数并记录符号。
2. 金额统一通过 toFixed(2) 保留两位小数,再按小数点拆分整数部分和小数部分,这样角、分的取值都来自固定两位字符串。
3. 整数部分按四位一组切分,组单位数组依次对应空、万、亿、万亿、亿亿;组内单位数组依次对应个、拾、佰、仟。
4. 组内遇到连续的 0 时用 needZero 标记,并借助 endsWith('零') 去重,避免出现“零零”这类连续零。
5. 小数部分中,角为零而分不为零时先补一个“零”,例如 0.05 会得到“零元零伍分”这种表达所需的前置零。
6. 整数部分为空但存在小数时先把结果置为“零”;若小数部分正好是 00,则在末尾补“整”。
五、适用场景与落地建议
这套组合适合财务、报销、订单金额、合同金额等需要明确单位和中文大写的录入场景:float-content 负责静态单位提示,show-tooltip 配合 contentMethod 负责动态金额反馈,amountToChinese 则作为独立的格式化工具函数复用于表格、详情页或打印模板。
实际使用时还应注意:金额精度最终取决于 toFixed(2) 的取整行为,涉及分位舍入规则时应按业务要求单独处理;unitGroups 只覆盖到“亿亿”,超大金额需要扩展单位数组;分组补零逻辑中的分支较多,建议用 1001、10001、100000001、0.05、-123.45 等边界值做用例验证,确认输出符合财务口径后再接入表单。 |