news 2026/10/7 10:30:25

JS数字转中文大写金额:零的规则与完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS数字转中文大写金额:零的规则与完整实现

上月在做一个合同管理系统,财务那边提了一个看着很不起眼的需求:订单金额要给出一行中文大写。我一开始以为这就是做一张数字映射表,把“10”替换成“壹拾”就行,真正动手写 JS 数字转中文大写金额的功能时才发现,整段逻辑的难点根本不在数字映射,而在那个反复出现的“零”字——什么位置该读零、两个非零段之间要不要补零、尾部的零怎么吞掉。这篇文章把这套实现完整复盘一遍,代码可以直接抄,测试用例也一起给出。

做财务系统、报销单、收银开票、合同审批、支付对账这类业务的前端和全栈同学,大概率都会遇到这个需求。如果只是临时凑一个转换函数,很容易在“壹万零壹元整”“壹拾万零壹佰元整”这种边界输入上翻车。读完你可以直接照搬,也可以根据自己团队的规范改配置。

1. 需求背后的真实场景:大写金额不只是“换字体”

1.1 谁在依赖这个转换工具

中文大写金额的硬性需求主要来自财务合规场景。银行票据、发票、合同、报销单上,数字金额旁边必须有一行大写金额,目的是防止数字被涂改——毕竟“12.00”加一笔就可能变成“12.50”,而“壹拾贰元整”想改造成“壹拾贰元伍角整”就难得多。

实际开发中,这个工具会出现在几个典型位置:

  • 表单页里的联动展示:用户在金额输入框填入数字,下方或右侧实时显示大写。
  • 报表导出:导出 PDF 或 Excel 时,金额列需要同时输出大写列。
  • 打印模板:票据、收据、送货单的打印模板,通过后端渲染或前端打印把大写金额输出到固定栏位。
  • 接口入参校验:有的业务系统严格要求提交时携带大写金额,前端负责在提交前生成并校验。

这个工具的本质是一个纯函数:输入数字,输出合规的大写金额字符串。它不依赖 DOM,不依赖框架,前端后端都能用。

1.2 “零”的规则才是真正的拦路虎

我刚开始写的时候,觉得按照“千百十”逐位拼接就行。比如“1001”就是“壹仟零佰零拾壹”,去掉零单位就是“壹仟零零壹”,再把连续零合并成“壹仟零壹”。看着没问题,但一跑测试就露馅了。

举几个例子,你感受一下:

  • 1001 读作“壹仟零壹”,不是“壹仟零零壹”。
  • 10001 读作“壹万零壹”,万级是 1,个级是 1,两个非零数字之间隔了三个零,要补一个“零”。
  • 10010 读作“壹万零壹拾”,万级是 1,十位是 1,中间的零要读出来。
  • 10100 读作“壹万零壹佰”,万级是 1,百位是 1,中间的零还是要读出来。
  • 11000 却读作“壹万壹仟”,万级是 1,千位是 1,这里不读零。

所以核心问题不是“怎么把数字转汉字”,而是“汉字之间的零到底什么时候出现”。这个规则用口语能说出来,但用代码表达就没那么直觉了。后面我用的方案是“四位一节”分组法,把整数部分切成若干个四位数,逐个处理节内和跨节的零。

2. 整数部分算法:四位一节的组装逻辑

2.1 为什么中文数字要按四位分组

中文读数体系和英文不一样。英文是三位一组,thousand、million、billion;中文是四位一组,个、万、亿、万亿。原因是中文里“万”后面跟着的是一个四位区段,不是三位。“123456789”在中文里是“一亿二千三百四十五万六千七百八十九”,分段为 1|2345|6789,每一段恰好四位。

代码实现里,我会把整数部分补成 4 的倍数长度,然后从左往右切成若干个四位字符串,每个四位字符串称为一“节”。从右往左,节的单位依次是空、万、亿、万亿。

// 100000000 -> 0001|0000|0000 // 节单位:最右边是空,往左是万、亿

这样设计的好处是,每一节内部的读数规则完全一致:“千百十”位权加四个数字的映射,最后拼上节的单位。零的处理也可以集中在“节内”和“跨节”两个层面解决,不用在整个数字串上做状态机。

2.2 节内读法:零的吞与补

先写一个处理四位字符串的内部函数。它的输入是像 “1001”、“0100”、“0010” 这样的四字符数字串,输出是这一节内部的汉字读法,不包含节的单位。

规则我总结成三条:

  • 遇到数字 0 时,只要它前面已经出现过非零数字、后面还存在非零数字,就要补一个“零”。
  • 连续的多个 0 只补一次零。
  • 数字 0 如果位于非零数字之后、且后面没有非零数字了,直接吞掉,不读。

用“1001”验证:千位是 1,输出“壹仟”;百位是 0,后面还有非零的个位 1,所以补“零”;十位还是 0,但零已经补过了,不重复;个位是 1,输出“壹”。最终“壹仟零壹”,正确。

再用“1010”验证:千位 1 -> “壹仟”;百位 0,后面还有非零十位 1,补“零”;十位 1 -> “壹拾”;个位 0,在非零之后且后面没有非零,吞掉。最终“壹仟零壹拾”,正确。

“1000”验证:千位 1,百位 0 后面没有非零数字,不补零。最终“壹仟”,正确。

再看“0010”:这是某个节内千位和百位为 0、十位为 1 的情况。前两位 0 之前没有非零数字,不补零;十位 1 输出“壹拾”;个位 0 吞掉。最终“壹拾”。这种写法保证了“100000”转出来是“壹拾万”,而不是“一十零万”之类。这里多说一句:银行票据规范里高位为 10 时写“壹拾”而不是“一十”,比如“壹拾万元整”,这条很多简单实现会搞错。

const NUM_CN = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; const INT_UNITS = ['', '拾', '佰', '仟']; function fourDigitsToCn(str) { let result = ''; let hasNonZeroBefore = false; let zeroPending = false; for (let i = 0; i < 4; i++) { const digit = Number(str[i]); const unit = INT_UNITS[3 - i]; const hasNonZeroAfter = str.slice(i + 1).includes('1') || str.slice(i + 1).includes('2') || str.slice(i + 1).includes('3') || str.slice(i + 1).includes('4') || str.slice(i + 1).includes('5') || str.slice(i + 1).includes('6') || str.slice(i + 1).includes('7') || str.slice(i + 1).includes('8') || str.slice(i + 1).includes('9'); if (digit === 0) { if (hasNonZeroBefore && hasNonZeroAfter && !zeroPending) { result += NUM_CN[0]; zeroPending = true; } } else { result += NUM_CN[digit] + unit; hasNonZeroBefore = true; zeroPending = false; } } return result || NUM_CN[0]; }

代码里hasNonZeroAfter我用了一段啰嗦的判断,实际项目里可以封装一个hasNonZeroChar(str)辅助函数,或者直接用正则/[1-9]/判断。

2.3 跨节补零:最容易被忽略的细节

节内逻辑搞定后,还要处理节与节之间的零。前面例子“10000”是“壹万”,“10001”是“壹万零壹”。如果只做节内拼接,前者是“壹万”,后者也会被拼成“壹万壹”,漏掉了“零”。

补零规则其实只有两种情况:

  • 某一节整体是 0000,即这一节一个非零数字都没有,那它不输出内容,但要在状态里记一笔“这里出现过空节”。
  • 下一节输出时,如果它自身以 0 开头,或者之前出现过空节,就要在读数前补一个“零”。

为什么“0100”这种以 0 开头的节要补零?“100100”这个数字拆成两个节是“0010”和“0100”,读作“壹拾万零壹佰”。万节读完后,个节从百位开始,万节末尾是 0、个节千位也是 0,两个节之间存在一个需要读出的零。“若当前节千位为 0”这个条件正好覆盖这种情况。

再看“10001000”,拆成“1000”和“1000”。第一节读“壹仟万”,第二节千位不是 0,所以不补零,最终“壹仟万壹仟”。有人会疑惑,难道不是“一千万一千”?对,口语和财务大写在这里一致,是没有中间那个“零”的。

const SECTION_UNITS = ['', '万', '亿', '万亿']; function integerToCn(integerStr) { if (!/^[0-9]+$/.test(integerStr)) { throw new Error('非法的整数数字字符串'); } const padLen = Math.ceil(integerStr.length / 4) * 4; const sections = integerStr.padStart(padLen, '0').match(/\d{4}/g); let result = ''; let sectionPendingZero = false; for (let i = 0; i < sections.length; i++) { const sectionNum = Number(sections[i]); const unit = SECTION_UNITS[sections.length - 1 - i]; if (sectionNum === 0) { sectionPendingZero = true; continue; } const sectionCn = fourDigitsToCn(sections[i]); if (result !== '' && (sectionPendingZero || sections[i][0] === '0')) { result += NUM_CN[0]; } result += sectionCn + unit; sectionPendingZero = false; } return result || NUM_CN[0]; }

这里还要注意SECTION_UNITS数组长度是 4,对应最大 16 位整数,也就是“万亿”级。超过 9999 万亿的数在人民币业务里基本不可能出现,但代码层面上还是建议做一次长度校验,或者把数组继续往高位扩展。真要用到超大数的话,可以在数组里追加“京”“垓”等单位,但那已经脱离财务场景了。

3. 小数部分与特殊金额:角分厘的完整规则

3.1 角分厘怎么读

人民币金额的小数部分比整数部分简单,但细节也不少。小数点后第一位是“角”,第二位是“分”,有的系统还要处理第三位“厘”。

我的默认策略是:

  • 角位非 0 时读“X角”。
  • 角位为 0 但分位非 0 时,如果整数部分不为 0,要在分前面补一个“零”,比如“壹元零伍分”;如果整数部分是 0,直接读“伍分”,不需要零。
  • 分位为 0 时不读“零分”,比如“壹拾贰元叁角”而不是“壹拾贰元叁角零分”。
  • 整数部分为 0、小数部分也为 0 时,输出“零元整”。

“整”字的规则也统一一下:整数部分非 0 且小数部分没有任何输出时,末尾补“整”,比如“壹拾贰元整”。如果小数部分输出了,就以“角”或“分”结尾,不再加“整”。

“1.20”和“1.2”都应该输出“壹元贰角”,不写“零分”。“1.02”输出“壹元零贰分”。“0.01”输出“壹分”。

function decimalToCn(decimalStr, integerPartIsZero) { const jiao = decimalStr[0] ? Number(decimalStr[0]) : 0; const fen = decimalStr[1] ? Number(decimalStr[1]) : 0; // 只处理两位,厘模式由调用方自行扩展 let result = ''; if (jiao !== 0) { result += NUM_CN[jiao] + '角'; } else if (fen !== 0 && !integerPartIsZero) { result += '零'; } if (fen !== 0) { result += NUM_CN[fen] + '分'; } return result; }

如果把精度扩展到厘,模式类似:角、分、厘逐位判断,低一位非零且高位为零时,在进入该位前补一个“零”。例如“1.005”输出“壹元零伍厘”(默认两位精度下只能看到分,三位模式下才看厘)。

3.2 零元、负数、超大数的边界策略

关于负数,财务票据里极少出现,但业务系统里可能出现负数订单或退款。我的做法是加一个可选前缀:转换得到绝对值,再在最前面拼“负”。输出类似“负壹拾贰元叁角肆分”。

关于输入形式,很多人会忽略一点:表单里拿到的金额字符串可能带千分位逗号,比如“1,234.56”,也可能带全角逗号“1,234.56”。还有可能是科学计数法,比如 JS 里1e6调用toString()会得到1000000,但1e21会得到1e+21。

这些都需要在预处理阶段归一化。我会按顺序做三件事:

  • 去掉所有空白和逗号。
  • 处理正负号。
  • 把科学计数法展开为普通数字字符串。

科学计数法展开可以这样处理:

function expandScientific(numStr) { if (!/e/i.test(numStr)) return numStr; const [base, expPart] = numStr.split(/e/i); const exp = Number(expPart); const dotIndex = base.indexOf('.'); if (dotIndex === -1) { return base + '0'.repeat(exp); } const [intPart, decPart] = base.split('.'); const fullDigits = intPart + decPart; const dotPos = intPart.length + exp; if (dotPos <= 0) { return '0.' + '0'.repeat(-dotPos) + fullDigits; } if (dotPos >= fullDigits.length) { return fullDigits + '0'.repeat(dotPos - fullDigits.length); } return fullDigits.slice(0, dotPos) + '.' + fullDigits.slice(dotPos); }

这里我默认输入是数字或十进制字符串,不考虑十六进制之类。如果你在业务里碰到的金额字符串已经被转成Number了,超过 16 位的精度早就丢了,预处理救不回来,所以后面我会专门讲这个坑。

4. 完整代码实现与逐行注释

把前面几块拼起来,再加上主函数和输入归一化,就是一个可以直接放进项目里用的单文件工具。为了便于复用,我保留了options参数,目前支持是否启用“厘”模式。

/** * 将数字金额转换为中文大写金额 * 支持:正负数、千分位逗号、科学计数法、角分厘 * 例子:1234.56 -> 壹仟贰佰叁拾肆元伍角陆分 */ const NUM_CN = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; const SECTION_UNITS = ['', '万', '亿', '万亿']; const INT_UNITS = ['', '拾', '佰', '仟']; function hasNonZeroChar(str) { return /[1-9]/.test(str); } function fourDigitsToCn(str) { let result = ''; let hasNonZeroBefore = false; let zeroPending = false; for (let i = 0; i < 4; i++) { const digit = Number(str[i]); const unit = INT_UNITS[3 - i]; const hasNonZeroAfter = hasNonZeroChar(str.slice(i + 1)); if (digit === 0) { if (hasNonZeroBefore && hasNonZeroAfter && !zeroPending) { result += NUM_CN[0]; zeroPending = true; } } else { result += NUM_CN[digit] + unit; hasNonZeroBefore = true; zeroPending = false; } } return result || NUM_CN[0]; } function integerToCn(integerStr) { if (!/^[0-9]+$/.test(integerStr)) { throw new Error('非法的整数数字字符串'); } const padLen = Math.ceil(integerStr.length / 4) * 4; const sections = integerStr.padStart(padLen, '0').match(/\d{4}/g); let result = ''; let sectionPendingZero = false; for (let i = 0; i < sections.length; i++) { const sectionNum = Number(sections[i]); const unit = SECTION_UNITS[sections.length - 1 - i]; if (sectionNum === 0) { sectionPendingZero = true; continue; } const sectionCn = fourDigitsToCn(sections[i]); if (result !== '' && (sectionPendingZero || sections[i][0] === '0')) { result += NUM_CN[0]; } result += sectionCn + unit; sectionPendingZero = false; } return result || NUM_CN[0]; } function decimalToCn(decimalStr, integerPartIsZero, mode) { const precision = mode === 'li' ? 3 : 2; let result = ''; for (let i = 0; i < precision; i++) { const digit = decimalStr[i] ? Number(decimalStr[i]) : 0; const unit = i === 0 ? '角' : i === 1 ? '分' : '厘'; if (digit !== 0) { result += NUM_CN[digit] + unit; } else if (i < precision - 1 && decimalToCnHasNonZeroInLater(decimalStr, precision, i)) { result += NUM_CN[0]; } } return result; } function decimalToCnHasNonZeroInLater(decimalStr, precision, currentIndex) { for (let i = currentIndex + 1; i < precision; i++) { if (Number(decimalStr[i] || 0) !== 0) { return true; } } return false; } function expandScientific(numStr) { if (!/e/i.test(numStr)) return numStr; const [base, expPart] = numStr.split(/e/i); const exp = Number(expPart); const dotIndex = base.indexOf('.'); if (dotIndex === -1) { return base + '0'.repeat(exp); } const [intPart, decPart] = base.split('.'); const fullDigits = intPart + decPart; const dotPos = intPart.length + exp; if (dotPos <= 0) { return '0.' + '0'.repeat(-dotPos) + fullDigits; } if (dotPos >= fullDigits.length) { return fullDigits + '0'.repeat(dotPos - fullDigits.length); } return fullDigits.slice(0, dotPos) + '.' + fullDigits.slice(dotPos); } function normalizeInput(input) { let raw = String(input).trim().replace(/[,,\s]/g, ''); let negative = false; if (/^[+-]/.test(raw)) { negative = raw[0] === '-'; raw = raw.slice(1); } if (/e/i.test(raw)) { raw = expandScientific(raw); } const dotIndex = raw.indexOf('.'); let intPart = dotIndex >= 0 ? raw.slice(0, dotIndex) : raw; let decPart = dotIndex >= 0 ? raw.slice(dotIndex + 1) : ''; // 去掉整数部分的前导零,避免影响后续分组 intPart = intPart.replace(/^0+/, '') || '0'; return { intPart, decPart, negative }; } /** * @param {number|string} input 金额,如 1234.56 或 '1,234.56' * @param {Object} options 可选配置 * @param {string} options.mode 'fen' 或 'li' */ function rmbUppercase(input, options = {}) { const mode = options.mode || 'fen'; const { intPart, decPart, negative } = normalizeInput(input); // 根据模式截断小数位 const precision = mode === 'li' ? 3 : 2; const finalDecPart = decPart.slice(0, precision); const intValue = Number(intPart); const intCn = integerToCn(intPart); const decCn = decimalToCn(finalDecPart, intValue === 0, mode); let result = ''; const prefix = negative ? '负' : ''; if (intValue === 0) { if (decCn) { result = prefix + decCn; } else { result = prefix + '零元整'; } } else { result = prefix + intCn + '元' + (decCn ? decCn : '整'); } return result; } module.exports = { rmbUppercase }; // ESM: export default rmbUppercase;

主函数里我特意把输入归一化拆成独立函数,方便你在金额输入框直接调用。normalizeInput里的去前导零也很重要:如果用户输入“007.50”,不处理的话分组会出现多余的节空位,导致“零柒元伍角”这种奇怪结果。

我再说说decimalToCn的补零逻辑。默认两位模式下,循环处理角和分。角位为 0、分位非 0 时,laterHasNonZero会返回 true,因此补“零”;角分都为 0 时不补。三位厘模式下,角分位为 0、厘位非 0 时也能正确在厘前补零。

实际上我在第 3 节给的decimalToCn示例是“整数部分为 0 时不补零”的简化版,但上面的完整代码用的是“低一位之后有非零才补零”的统一逻辑。这两者输出的差异只在“0.05”这类场景:完整版由于角位为 0 且分位非 0,会在第五分前补出一个“零”,变成“零伍分”。这跟财务上常见的“伍分”不太一致。

所以在最终交付里,我会把“整数部分为 0 时不补零”作为主逻辑,完整代码如下,和上面保持同一套命名:

function decimalToCn(decimalStr, integerPartIsZero, mode) { const precision = mode === 'li' ? 3 : 2; let result = ''; for (let i = 0; i < precision; i++) { const digit = decimalStr[i] ? Number(decimalStr[i]) : 0; const unit = i === 0 ? '角' : i === 1 ? '分' : '厘'; if (digit !== 0) { if (result === '' && integerPartIsZero) { result += NUM_CN[digit] + unit; } else { result += NUM_CN[digit] + unit; } } else { const hasNonZeroAfter = /[1-9]/.test(decimalStr.slice(i + 1)); if (hasNonZeroAfter && (result !== '' || !integerPartIsZero)) { result += NUM_CN[0]; } } } return result; }

等一下,我重新梳理一下这个函数的逻辑,避免给自己挖坑。

角分厘逐位扫描:

  • 如果当前位非零:直接拼“数字+单位”。如果之前已经拼过内容,不用额外补零,因为前一位如果是 0 且需要补零,已经在上一轮补过了。
  • 如果当前位为零:检查后面还有没有非零数字。如果没有,跳过;如果有,且前面已经输出了内容,补一个零。

“整数部分为 0,角位为 0,分位为 5”的场景:角位为零,后面分位非零,此时 result 为空,hasNonZeroAfter为 true,但result !== ''为 false,integerPartIsZero为 true,所以条件(result !== '' || !integerPartIsZero)是(false || false),不补零。分位为 5 时输出“伍分”。正确。

“整数部分为 1,角位为 0,分位为 5”:角位时 result 为空,但!integerPartIsZero为 true,补零。分位输出“伍分”。最终“壹元零伍分”。正确。

“1.005”厘模式:角分位为 0、厘位为 5,同理整数部分非零时在厘前补零,输出“壹元零伍厘”。正确。

“0.05”:整数部分为 0,角位为 0,分位为 5,不补零,输出“伍分”。正确。

“0.50”:角位 5 输出“伍角”,分位 0 后面无非零,结束。结果“伍角”。正确。

到这里代码基本稳定了。唯一还需要留意的点是:如果decimalToCn返回空字符串且整数部分非零,主函数会补“整”;如果整数部分为零且小数部分为空,主函数输出“零元整”。这套行为符合大多数财务打印模板的需求。

5. 测试用例:覆盖你想象不到的边界

写这种纯函数,最怕的就是“看起来对了,边界一测就崩”。我整理了一套测试用例,建议你直接跑,也可以加到项目的单测里。

5.1 常规金额用例

输入期望输出
1壹元整
10壹拾元整
11壹拾壹元整
100壹佰元整
101壹佰零壹元整
110壹佰壹拾元整
1001壹仟零壹元整
1010壹仟零壹拾元整
1100壹仟壹佰元整
10000壹万元整
10001壹万零壹元整
10010壹万零壹拾元整
10100壹万零壹佰元整
11000壹万壹仟元整
100000壹拾万元整
100010壹拾万零壹拾元整
100100壹拾万零壹佰元整
101000壹拾万壹仟元整
1000000壹佰万元整
10000000壹仟万元整
100000000壹亿元整
100000001壹亿零壹元整
100010000壹亿零壹万元整
123456789.01壹亿贰仟叁佰肆拾伍万陆仟柒佰捌拾玖元零壹分

这些用例覆盖了整数部分主要分组和跨节补零。我最推荐大家重点关注 100010、100100、100000001 这三个,它们分别对应“万节末尾为零但个节千位非零”“万节末尾为零且个节以百位开头”“亿节与个节之间隔着整段空节”三种情况。

5.2 小数与特殊输入用例

输入期望输出
0零元整
0.0零元整
0.5伍角
0.05伍分
0.55伍角伍分
1.00壹元整
1.2壹元贰角
1.20壹元贰角
1.02壹元零贰分
1.23壹元贰角叁分
-12.34负壹拾贰元叁角肆分
1,234.56壹仟贰佰叁拾肆元伍角陆分
1e6壹佰万元整
2e-2零元零贰分

“2e-2”这种科学计数法输入,正常业务里不会出现,但既然函数支持了,测试最好也覆盖到。

5.3 厘模式用例

输入期望输出(mode: 'li')
1.005壹元零伍厘
1.055壹元零伍分伍厘
1.555壹元伍角伍分伍厘
0.055伍分伍厘

跑这些用例的时候,最直观的感受是:整数部分的大分组算法一旦通过,小数部分基本不会出错。真正容易翻车的永远是“零”的位置和数量。

6. 把工具真实接入项目的落地过程

6.1 原生 JS 表单联动

拿到这个纯函数,第一步就是和表单联动。我用原生 HTML 做一个最简单的 demo:

<input id="amountInput" type="text" placeholder="请输入金额" /> <p id="amountUpper"></p> <script> function updateUpper() { const value = document.getElementById('amountInput').value; const upper = document.getElementById('amountUpper'); try { upper.textContent = rmbUppercase(value); } catch (e) { upper.textContent = '输入格式有误'; } } document.getElementById('amountInput').addEventListener('input', updateUpper); updateUpper(); </script>

这里有个容易被忽略的细节:不要把input元素的type设成number。type="number"在用户输入长数字时,取值会变成科学计数法,而且Number类型超过 16 位整数精度会丢失。如果你在做一个合同金额字段,建议用type="text"+inputmode="decimal",或者用pattern校验,不要让浏览器帮你转数字类型。

6.2 Vue/React 里的使用方式

Vue 3 项目里,我习惯用computed主动联动,而不是在事件回调里手动赋值:

import { ref, computed } from 'vue'; import { rmbUppercase } from '@/utils/money'; const amount = ref(''); const upperAmount = computed(() => { try { return rmbUppercase(amount.value); } catch { return ''; } });

React 或者 Vue 模板里,直接把upperAmount绑定到展示节点即可。函数的输入归一化会自己处理undefined、null、空字符串等情况,不会抛异常。这里的catch主要用于异常输入防御,正常情况下rmbUppercase不会抛错。

6.3 我在实际交付中踩过的坑

说几个我真实遇到的问题,希望你能提前避开。

第一个坑是浮点精度。有同事图省事,把后端给的数字先Number(value).toFixed(2)再传给转换函数。结果订单金额是9999999999999999的时候,转出来少了几块钱。因为 JS 浮点数到这个量级已经无法精确表示整数了。正确做法是:后端直接传字符串,或者传单位为“分”的整数,前端除 100 后格式化,尽量不要在 JS 里对金额做大数运算。

第二个坑是“零”的规范口径。不同银行、不同地区的票据模板,对“0.05”是写“伍分”还是“零伍分”可能有细微差异。我的实现默认“伍分”,如果你所在团队要求严格按某地票据规范来,只需要调整decimalToCn里的补零条件即可。

第三个坑是后端校验。财务数据最终要入库,前后端最好共用同一套转换逻辑。最省事的办法是把money.js单独提成一个模块,前端直接用,后端如果是 Node.js 也可以直接require;如果后端是 Java 或 PHP,建议用同一份测试用例跑一遍移植后的函数,保证两边输出一致。我曾经遇到过后端 Java 实现和前端 JS 实现在大写金额上差了十几个用例,后来就是靠统一测试集抓出来的。

第四个坑是性能。有人担心字符串切割和正则会不会影响页面性能。实际上金额输入是低频操作,一次转换就是几十微秒级别,完全不用担心。真正应该担心的是不要在watch里做无意义的重复转换,computed天然有缓存,优先用它。

如果后面业务需要支持繁体大写(比如“貳”、“圓”),我的建议是在NUM_CN、元、角、分这几个常量上做一套映射方案,函数内部不写死单个字符,而是通过选项传入。这样既能保持函数纯净,也便于不同地区模板复用。我在实际项目里就是这么干的:一份money.js,通过locale参数同时支持简体、繁体,财务那边很满意。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 10:30:21

排序算法全解析:从分类实现到工程选型

前几天帮朋友准备面试&#xff0c;他问我&#xff1a;为什么面试官总喜欢问排序算法&#xff1f;我的回答是&#xff0c;排序算法看起来基础&#xff0c;但它的分类维度、底层实现、时间空间复杂度、稳定性&#xff0c;任何一个细节都能反映一个人的计算机基础是否扎实。我见过…

作者头像 李华
网站建设 2026/10/7 10:30:13

交通智能调度实战:Spark清洗、Hive特征工程与决策模型全解析

前年我接手一个城市交通智能调度项目&#xff0c;本来以为算法才是核心&#xff0c;真正做进去才发现&#xff0c;大数据在交通领域的深层难点&#xff0c;是把零散、脏乱的原始数据洗干净&#xff0c;再让它真正参与决策。项目开始第一个月&#xff0c;GPS轨迹在市区主干道大面…

作者头像 李华
网站建设 2026/10/7 10:29:46

WSL2安装到D盘:--location参数把Ubuntu虚拟磁盘迁出C盘的完整指南

做开发这几年&#xff0c;WSL2已经成了我Windows机器上离不开的Linux运行环境。它比传统虚拟机轻量太多&#xff0c;又不依赖云服务器&#xff0c;直接在终端里敲 wsl 就能进入一个完整的Ubuntu环境&#xff0c;跑脚本、起服务、调Nginx、开Docker都跟在物理Linux上一样顺手。…

作者头像 李华
网站建设 2026/10/7 10:29:46

基于Transformer的情绪识别与情感分析项目实战:从环境搭建到推理部署

简介&#xff1a;本资源面向希望快速上手情绪识别与情感分析的开发者与研究人员&#xff0c;提供一套基于Transformer的完整项目实战方案&#xff0c;覆盖从数据预处理、模型训练到评估优化的全流程&#xff0c;适合具备一定深度学习基础、想深入理解自注意力机制在情感任务中应…

作者头像 李华
网站建设 2026/10/7 10:29:09

VCMP协议详解:华为交换机批量VLAN同步与配置管理实战

搞网络的都懂这个场景&#xff1a;网络刚上线的时候只有几十个VLAN&#xff0c;一台一台敲敲还能接受。等用户部门多了&#xff0c;一个VLAN要加端口&#xff0c;另一个VLAN要跨设备打通&#xff0c;你就得登录每一台交换机执行一遍几乎一样的命令。那会儿我最怕的就是深夜变更…

作者头像 李华
网站建设 2026/10/7 10:26:25

嵌入式Linux驱动开发实战:从字符设备到设备树与并发控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华