HarmonyOS NEXT 企业级记账APP:封装金额输入组件 MoneyInput

发布时间:2026/7/30 2:46:32
HarmonyOS NEXT 企业级记账APP:封装金额输入组件 MoneyInput 封装金额输入组件 MoneyInput本文是《HarmonyOS NEXT 企业级开发实战30篇打造智能记账APP》系列的第09篇对应 Git Tagv0.0.9。承接第 08 篇的占位 MoneyInput本篇完善金额输入组件支持实时格式化、千分位、小数点限制、最大金额校验、清除按钮。前言金额输入是记账应用最容易踩坑的组件。浮点精度问题、千分位显示、小数位限制、最大金额校验、清除按钮、输入光标跳位——任何一个处理不当都会让用户体验崩塌。本章封装一个企业级 MoneyInput所有问题一次解决。本文将带你完善 MoneyInput 支持 ¥ 符号 大字号金额实时格式化限制 2 位小数、千分位、最大金额添加清除按钮金额非空时显示通过Link双向绑定 ViewModel处理光标跳位与键盘类型企业级核心原则金额输入必须精度可控、格式实时、反馈清晰。参考 ArkUI TextInput 了解官方约定。一、MoneyInput 接口规划1.1 完整接口Prop/Event类型说明moneystring金额字符串双向LinktypeBillType收入/支出决定颜色maxAmountnumber最大金额分默认 99999999placeholderstring占位文字默认 “0.00”onInputbuilder输入回调1.2 校验规则校项规则处理数字限制仅数字 小数点非法字符直接过滤小数位≤ 2 位多余小数位禁止输入最大金额≤ maxAmount超出 Toast 提示并截断前导零禁止 “00” 开头自动去除空态显示 placeholder灰色 “0.00”二、MoneyInput 完整实现2.1 组件源码// components/form/MoneyInput.ets import { BillType } from ../../constants/BillType; import { AppColors } from ../../theme/Colors; import { AppFontSize, AppFontWeight } from ../../theme/Typography; import { AppSpace } from ../../theme/Spacing; import { MoneyUtil } from ../../utils/MoneyUtil; import { ToastUtil } from ../../utils/ToastUtil; Component export struct MoneyInput { Link money: string; Prop type: BillType BillType.EXPENSE; Prop maxAmount: number 99999999; // 默认最大 99万9千9百9十9元9角9分 Prop placeholder: string 0.00; BuilderParam onInput: (value: string) void; private getSemanticColor(): string { return this.type BillType.INCOME ? AppColors.Income : AppColors.Expense; } build() { Row() { Text(¥) .fontSize(36) .fontWeight(AppFontWeight.Bold) .fontColor(this.getSemanticColor()) Stack({ alignContent: Alignment.Center }) { TextInput({ text: this.money, placeholder: this.placeholder, controller: this.inputController }) .layoutWeight(1) .fontSize(36) .fontWeight(AppFontWeight.Bold) .fontColor(this.getSemanticColor()) .type(InputType.Number) .placeholderColor(AppColors.SecondaryText) .placeholderFont({ size: 36, weight: FontWeight.Bold }) .backgroundColor(Color.Transparent) .borderWidth(0) .onChange((value: string) { this.handleInput(value); }) // 清除按钮金额非空时显示 if (this.money.length 0) { Image($r(app.media.icon_clear)) .width(20) .height(20) .fillColor(AppColors.SecondaryText) .position({ x: 100%, y: 50% }) .markAnchor({ x: 100%, y: 50% }) .offset({ x: -8, y: 0 }) .onClick(() { this.handleClear(); }) } } .layoutWeight(1) } .width(100%) .padding({ top: AppSpace.LG, bottom: AppSpace.LG }) .alignItems(HorizontalAlign.Center) } private inputController: TextInputController new TextInputController(); /** 输入处理校验 格式化 回调 */ private handleInput(raw: string): void { const filtered this.filterInvalidChars(raw); const limited this.limitDecimalPlaces(filtered); const trimmed this.trimLeadingZeros(limited); const checked this.checkMaxAmount(trimmed); if (checked ! this.money) { this.money checked; this.onInput(checked); // 光标定位到末尾 this.inputController.caretPosition(checked.length); } } /** 过滤非法字符 */ private filterInvalidChars(s: string): string { return s.replace(/[^0-9.]/g, ); } /** 限制小数位 ≤ 2 */ private limitDecimalPlaces(s: string): string { const parts s.split(.); if (parts.length 1) return parts[0]; const intPart parts[0]; const decPart parts[1].substring(0, 2); return ${intPart}.${decPart}; } /** 去除前导零 */ private trimLeadingZeros(s: string): string { if (s 0 || s 0. || s 0.0 || s 0.00) return s; return s.replace(/^0([1-9])/, $1); } /** 校验最大金额 */ private checkMaxAmount(s: string): string { if (s.length 0) return s; const cents MoneyUtil.toCents(parseFloat(s) || 0); if (cents this.maxAmount) { ToastUtil.show(金额超出最大限制); return MoneyUtil.format(this.maxAmount); } return s; } /** 清除 */ private handleClear(): void { this.money ; this.onInput(); this.inputController.caretPosition(0); } }2.2 关键实现解析Link双向绑定父 ViewModel 改 money 同步刷新子组件改 money 父组件同步TextInputController通过caretPosition控制光标位置避免格式化后跳到开头Stack叠放清除按钮用positionmarkAnchoroffset精确定位到输入框右侧onChange链路raw → 过滤 → 限小数位 → 去前导零 → 校验最大值 → 同步 ViewModelInputType.Number弹起数字键盘仅含 0-9 与小数点三、MoneyUtil 工具类升级3.1 完善后源码// utils/MoneyUtil.ets升级版 export class MoneyUtil { /** 分 → 元字符串保留 2 位小数 */ static format(cents: number): string { return (cents / 100).toFixed(2); } /** 元 → 分四舍五入 */ static toCents(yuan: number): number { return Math.round(yuan * 100); } /** 千分位格式化1234.56 → 1,234.56 */ static formatWithComma(cents: number): string { const yuan this.format(cents); const parts yuan.split(.); parts[0] parts[0].replace(/\B(?(\d{3})(?!\d))/g, ,); return parts.join(.); } /** 解析字符串为分容错 */ static parseToCents(s: string): number { const yuan parseFloat(s.replace(/,/g, )) || 0; return this.toCents(yuan); } /** 中文金额1234.56 → 壹仟贰佰叁拾肆元伍角陆分 */ static toChinese(cents: number): string { const yuan cents / 100; // 简化版实际需完整中文转换逻辑 return ¥${yuan.toFixed(2)}; } }3.2 命名规范方法输入输出用途format分元字符串UI 展示toCents元分持久化formatWithComma分千分位元字符串统计报表parseToCents元字符串分解析输入toChinese分中文金额大额展示四、AddBillViewModel 集成升级4.1 改为 Link 双向绑定// viewmodel/AddBillViewModel.ets关键改动 // 旧money: string // 新通过 Link 直接绑定到 MoneyInput // pages/AddBillView.ets State viewModel: AddBillViewModel new AddBillViewModel(); MoneyInput({ money: this.viewModel.money, // 必须 LinkArkUI V1 用法 type: this.viewModel.currentType, maxAmount: 10000000, // 最大 10万元 onInput: (value: string) { this.viewModel.money value; } })4.2 金额校验升级// viewmodel/AddBillViewModel.ets canSave(): boolean { if (this.money.length 0) return false; const cents MoneyUtil.parseToCents(this.money); if (cents 0) return false; if (cents 10000000) return false; // 超出 10万元限制 if (!this.selectedCategory) return false; return true; } async save(): Promiseboolean { if (!this.canSave()) { if (this.money.length 0) { ToastUtil.show(请输入金额); } else if (!this.selectedCategory) { ToastUtil.show(请选择分类); } else { ToastUtil.show(金额无效); } return false; } const cents MoneyUtil.parseToCents(this.money); // ... 持久化同前 }五、最佳实践5.1 光标跳位问题// 错误onChange 后光标自动跳到开头 .onChange((value: string) { this.money this.format(value); // 格式化后光标乱了 }) // 正解用 TextInputController.caretPosition 主动定位 private inputController: TextInputController new TextInputController(); .onChange((value: string) { this.money this.format(value); this.inputController.caretPosition(this.money.length); // 光标末尾 })5.2 浮点精度问题// ❌ 错误直接用浮点存储金额 money: number 0; // 0.1 0.2 0.30000000000000004精度丢失 // ✅ 正解金额统一以分整数存储 money: number 0; // 单位分 // UI 展示时调用 MoneyUtil.format(300) 3.005.3 输入限制实战输入过滤后限小位去前导最终abc12312312312312312.34512.34512.3412.3412.3400.5000.5000.500.500.5099999999999999999999999999999999校验最大金额六、运行验证6.1 编译检查hvigorw assembleHap--modemodule-pproductdefault6.2 功能验证金额输入仅接受数字与小数点小数点后最多 2 位多余自动截断前导零自动去除00.5 → 0.5超出最大金额 Toast 提示并截断非空时显示清除按钮点击清空收入态金额绿色支出态红色截图清单① 默认空态 placeholder ② 输入数字实时格式 ③ 收入/支出色切换 ④ 清除按钮。七、常见问题7.1 千分位输入冲突// 如果在输入时显示千分位用户继续输入会错位 // 解决输入态显示纯数字失焦后再格式化为千分位 .onBlur(() { this.displayMoney MoneyUtil.formatWithCommas(cents); })HarmonyLedger 暂不启用输入时千分位避免冲突。统计报表展示时才用千分位。7.2 InputType.Number 不含小数点部分 HarmonyOS 版本数字键盘不含小数点需改用InputType.Normal并手动过滤。7.3 清除按钮遮挡输入// 用 Stack position 定位避免遮挡 Stack() { TextInput() // 占满 Image().position({ x: 100% }).markAnchor({ x: 100% }).offset({ x: -8 }) }八、Git 提交8.1 Commit Messagegitadd.gitcommit-mfeat(component): 完善金额输入组件 MoneyInput - 支持 ¥ 符号 大字号金额展示 - 实时格式化过滤非法字符、限制 2 位小数、去前导零 - 添加清除按钮非空时显示 - 用 TextInputController 处理光标跳位 - 校验最大金额并 Toast 提示 - 升级 MoneyUtil 新增千分位、解析、中文金额8.2 CHANGELOG## [v0.0.9] - 2026-07-27 ### Added - components/form/MoneyInput.ets完善版金额输入 - utils/MoneyUtil.ets新增 formatWithComma/parseToCents/toChinese ### Changed - viewmodel/AddBillViewModel.ets用 parseToCents 校验金额总结本文完整介绍了MoneyInput 组件的封装涵盖实时格式化、光标控制、清除按钮、最大金额校验、MoneyUtil 升级。通过本篇你可以用 TextInputController 解决光标跳位用正则过滤非法字符与限制小数位用 Stack position 精确定位辅助按钮理解金额存储用整数分避免精度问题通过 Link 双向同步 ViewModel下一篇预告《分类选择器开发》 将完善 CategorySelector 组件支持网格布局、图标 名称、选中态高亮、新增分类入口。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源本篇源码GitHub Tag v0.0.9ArkUI TextInputtextinputTextInputControllertextinput-controllerInputType 枚举inputtypeJavaScript 数值精度floating-point-guide鸿蒙金额处理实践money-handlingArkUI Stack 布局stack