HarmonyOS ArkTS 实战:实现一个房贷计算器

项目效果

本文实现的是一个基于 HarmonyOS 和 ArkTS 的房贷计算器。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现贷款金额、期限、利率输入,支持等额本息和等额本金两种计算方式,实时计算月供、总还款和总利息,并显示详细的还款信息。

最终运行效果如下:
在这里插入图片描述

页面主要包含以下内容:

  • 顶部应用标题;
  • 贷款类型切换(商业贷款/公积金/组合贷);
  • 贷款金额输入;
  • 贷款期限选择;
  • 年利率输入;
  • 还款方式切换(等额本息/等额本金);
  • 计算结果卡片;
  • 月供、总还款、总利息显示;
  • 页面整体采用 ArkUI 声明式布局。

本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个财务计算类单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。

前言

买房是很多人人生中的大事,而房贷计算是买房过程中必不可少的环节。了解月供、总利息等信息可以帮助我们更好地规划财务,选择合适的贷款方案。手动计算房贷公式复杂,容易出错,一个好用的房贷计算器可以快速准确地给出结果。

从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、表单输入、单选按钮、实时计算、数学公式和结果展示等内容。

本文基于 HarmonyOS 和 ArkTS 实现一个房贷计算器。用户可以输入贷款金额、期限和利率,选择还款方式,实时看到月供、总还款和总利息等关键数据,帮助用户做出购房决策。

这个项目的核心不是简单的数学公式,而是准确的财务计算和清晰的结果展示。每一次参数修改和方式切换,本质上都是对计算状态的管理。状态变化后,页面会自动刷新,这正是 ArkUI 声明式开发的基本思想。

一、项目目标

本次实践主要实现以下目标:

  • 创建 HarmonyOS ArkTS 页面;
  • 使用 @Entry@Component 定义页面组件;
  • 使用 @State 管理页面状态;
  • 支持商业贷款计算;
  • 支持等额本息还款方式;
  • 支持等额本金还款方式;
  • 贷款金额输入(万元);
  • 贷款期限选择(年);
  • 年利率输入(%);
  • 实时计算月供;
  • 计算总还款额;
  • 计算总利息;
  • 显示首月/末月月供(等额本金);
  • 使用 @Builder 封装输入项和结果卡片;
  • 完成一个可以运行的房贷计算器页面。

这个项目虽然是单页面应用,但它实现了专业的房贷计算功能,有完整的输入、计算和结果展示流程,比普通静态页面更适合练习 ArkTS。

二、技术栈

类型 内容
开发方向 HarmonyOS 应用开发
开发语言 ArkTS
UI 框架 ArkUI
SDK 版本 HarmonyOS API 23 及以上
工程模型 Stage 模型
核心组件 Text / TextInput / Button / Radio / Column / Row
状态管理 @State
数据处理 财务计算 / 数学公式 / 实时响应
项目入口 entry/src/main/ets/pages/Index.ets
运行平台 模拟器或真机

本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。

三、为什么选择房贷计算器项目

房贷计算器适合作为 ArkTS 练习项目,主要有以下几个原因。

第一,实用性强。房贷计算是很多人买房时都会用到的功能,有很强的实际需求。

第二,业务场景真实。购房决策需要了解月供压力和利息成本,是非常实用的财务工具。

第三,适合练习状态管理。贷款金额、期限、利率、还款方式都属于页面状态。状态变化后,页面会自动刷新。

第四,适合练习表单交互。数字输入、单选按钮等表单控件的使用是前端开发基础。

第五,适合练习复杂计算。等额本息和等额本金有不同的计算公式,可以锻炼逻辑能力。

第六,扩展空间比较大。基础功能完成后,可以继续增加公积金贷款、组合贷、提前还款计算、还款明细和税费计算。

在本项目中,输入参数和还款方式是核心数据。所有计算结果都基于这些参数实时计算。

四、功能规则说明

还款方式:

方式 特点
等额本息 每月还款额相同,前期利息多本金少
等额本金 每月本金相同,月供逐月递减,总利息少

计算公式:
等额本息:

  • 月供 = [贷款本金×月利率×(1+月利率)^还款月数] ÷ [(1+月利率)^还款月数-1]
  • 总还款 = 月供 × 还款月数
  • 总利息 = 总还款 - 贷款本金

等额本金:

  • 首月月供 = (贷款本金÷还款月数) + (贷款本金-已还本金)×月利率
  • 每月递减 = 贷款本金÷还款月数×月利率
  • 总利息 = (还款月数+1)×贷款本金×月利率÷2
  • 总还款 = 贷款本金 + 总利息

默认参数:

  • 贷款金额:100万元
  • 贷款期限:30年
  • 年利率:4.9%(商业贷款基准利率)
  • 还款方式:等额本息

五、项目结构

本项目主要修改首页文件:

entry
└── src
    └── main
        └── ets
            └── pages
                └── Index.ets

其中:

文件 作用
Index.ets 编写页面结构、状态数据和房贷计算逻辑

本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。

六、核心实现思路

本项目的核心流程如下:

  1. 使用 @State 保存贷款金额、期限、年利率和还款方式;
  2. 用户输入贷款金额(万元);
  3. 用户选择贷款期限;
  4. 用户输入年利率;
  5. 用户选择还款方式;
  6. 参数变化时自动重新计算;
  7. 根据还款方式使用对应公式计算;
  8. 显示月供、总还款、总利息等结果;
  9. 等额本金时显示首月和末月月供;
  10. 格式化金额显示,添加千分位分隔符。

项目中最重要的状态变量如下:

@State loanAmount: string = '100'
@State loanYears: number = 30
@State interestRate: string = '4.9'
@State repaymentType: number = 0

其中:

状态变量 作用
loanAmount 贷款金额(万元)
loanYears 贷款期限(年)
interestRate 年利率(%)
repaymentType 还款方式(0=等额本息,1=等额本金)

这些参数是计算的基础,任何一个变化都会触发重新计算。

七、Index.ets 完整代码

打开文件:

entry/src/main/ets/pages/Index.ets

将其中内容替换为下面代码:

@Entry
@Component
struct Index {
  @State loanAmount: string = '100'
  @State loanYears: number = 30
  @State interestRate: string = '4.9'
  @State repaymentType: number = 0

  private yearOptions: number[] = [5, 10, 15, 20, 25, 30]

  private getMonthlyPayment(): number {
    let principal = this.getPrincipal()
    let months = this.loanYears * 12
    let monthlyRate = this.getMonthlyRate()

    if (principal <= 0 || monthlyRate <= 0 || months <= 0) return 0

    if (this.repaymentType === 0) {
      return principal * monthlyRate * Math.pow(1 + monthlyRate, months) / 
             (Math.pow(1 + monthlyRate, months) - 1)
    } else {
      return principal / months + principal * monthlyRate
    }
  }

  private getLastMonthlyPayment(): number {
    if (this.repaymentType === 0) return this.getMonthlyPayment()
    
    let principal = this.getPrincipal()
    let months = this.loanYears * 12
    let monthlyRate = this.getMonthlyRate()
    let monthlyPrincipal = principal / months
    
    return monthlyPrincipal + monthlyPrincipal * monthlyRate
  }

  private getTotalPayment(): number {
    let principal = this.getPrincipal()
    let months = this.loanYears * 12
    let monthlyRate = this.getMonthlyRate()

    if (this.repaymentType === 0) {
      return this.getMonthlyPayment() * months
    } else {
      return principal + (months + 1) * principal * monthlyRate / 2
    }
  }

  private getTotalInterest(): number {
    return this.getTotalPayment() - this.getPrincipal()
  }

  private getPrincipal(): number {
    let amount = parseFloat(this.loanAmount)
    return isNaN(amount) ? 0 : amount * 10000
  }

  private getMonthlyRate(): number {
    let rate = parseFloat(this.interestRate)
    return isNaN(rate) ? 0 : rate / 100 / 12
  }

  private formatMoney(amount: number): string {
    return amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
  }

  @Builder
  InputItem(label: string, value: string, unit: string, onChange: (v: string) => void, placeholder?: string) {
    Row() {
      Text(label)
        .fontSize(15)
        .fontColor('#182431')
        .width(90)
      TextInput({
        text: value,
        placeholder: placeholder || ''
      })
        .layoutWeight(1)
        .height(44)
        .fontSize(16)
        .textAlign(TextAlign.End)
        .backgroundColor(Color.Transparent)
        .type(InputType.Number)
        .onChange(onChange)
      Text(unit)
        .fontSize(15)
        .fontColor('#6B7280')
        .margin({ left: 8 })
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
    .height(56)
    .backgroundColor('#F9FAFB')
    .borderRadius(12)
    .margin({ bottom: 12 })
  }

  @Builder
  ResultItem(label: string, value: string, highlight: boolean = false) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#6B7280')
      Blank()
      Text(value)
        .fontSize(highlight ? 24 : 18)
        .fontWeight(highlight ? FontWeight.Bold : FontWeight.Medium)
        .fontColor(highlight ? '#0A59F7' : '#182431')
    }
    .width('100%')
    .padding({ top: 12, bottom: 12 })
  }

  build() {
    Column() {
      Text('房贷计算器')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .margin({ top: 22 })
      Text('基于 HarmonyOS ArkTS 的房贷计算工具')
        .fontSize(14)
        .fontColor('#6B7280')
        .margin({ top: 8, bottom: 20 })

      Column() {
        Text('贷款信息')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 12 })

        this.InputItem('贷款金额', this.loanAmount, '万元', (v: string) => {
          this.loanAmount = v
        }, '请输入金额')

        Text('贷款期限')
          .fontSize(15)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 8 })
        
        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.yearOptions, (year: number) => {
            Button(`${year}`)
              .height(36)
              .padding({ left: 16, right: 16 })
              .fontSize(14)
              .margin({ right: 8, bottom: 8 })
              .fontColor(this.loanYears === year ? Color.White : '#4B5563')
              .backgroundColor(this.loanYears === year ? '#0A59F7' : '#F3F4F6')
              .borderRadius(18)
              .onClick(() => {
                this.loanYears = year
              })
          }, (y: number) => y.toString())
        }
        .width('100%')
        .margin({ bottom: 12 })

        this.InputItem('年利率', this.interestRate, '%', (v: string) => {
          this.interestRate = v
        }, '请输入利率')

        Text('还款方式')
          .fontSize(15)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 8 })
        
        Row() {
          Row() {
            Radio({ value: '0', group: 'repayType' })
              .checked(this.repaymentType === 0)
              .onChange((isChecked: boolean) => {
                if (isChecked) this.repaymentType = 0
              })
            Text('等额本息')
              .fontSize(15)
              .fontColor('#182431')
              .margin({ left: 8 })
          }
          .onClick(() => { this.repaymentType = 0 })
          
          Blank().width(40)
          
          Row() {
            Radio({ value: '1', group: 'repayType' })
              .checked(this.repaymentType === 1)
              .onChange((isChecked: boolean) => {
                if (isChecked) this.repaymentType = 1
              })
            Text('等额本金')
              .fontSize(15)
              .fontColor('#182431')
              .margin({ left: 8 })
          }
          .onClick(() => { this.repaymentType = 1 })
        }
        .width('100%')
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Column() {
        Text('计算结果')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 8 })

        this.ResultItem(
          this.repaymentType === 0 ? '每月月供' : '首月月供',
          `${this.formatMoney(this.getMonthlyPayment())}`,
          true
        )

        if (this.repaymentType === 1) {
          this.ResultItem('末月月供', `${this.formatMoney(this.getLastMonthlyPayment())}`)
          Divider().color('#F3F4F6')
        }

        this.ResultItem('还款总额', `${this.formatMoney(this.getTotalPayment())}`)
        Divider().color('#F3F4F6')
        this.ResultItem('支付利息', `${this.formatMoney(this.getTotalInterest())}`)
      }
      .width('100%')
      .padding(20)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Text('* 计算结果仅供参考,实际以银行审批为准')
        .fontSize(12)
        .fontColor('#9CA3AF')
        .margin({ top: 16 })
    }
    .width('100%')
    .height('100%')
    .padding({ left: 18, right: 18 })
    .backgroundColor('#F5F7FA')
  }
}

八、代码实现说明

1. 等额本息计算

使用等额本息公式计算每月还款额:

return principal * monthlyRate * Math.pow(1 + monthlyRate, months) / 
       (Math.pow(1 + monthlyRate, months) - 1)

总还款 = 月供 × 月数,总利息 = 总还款 - 本金。

2. 等额本金计算

等额本金首月月供最高,逐月递减:

// 首月月供 = 月还本金 + 全额本金月利息
return principal / months + principal * monthlyRate
// 末月月供 = 月还本金 + 剩余1个月本金利息
return monthlyPrincipal + monthlyPrincipal * monthlyRate

总利息使用等差数列求和公式计算。

3. 实时计算

参数变化时自动重新计算,不需要点击计算按钮,提供流畅体验。

4. 金额格式化

使用正则表达式添加千分位分隔符,让大额数字更易读:

return amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
5. 期限快捷选择

提供常用贷款期限的快捷按钮,点击即可选择,比输入更方便。

6. 还款方式切换

使用Radio组件选择还款方式,切换后自动重新计算,等额本金时额外显示末月月供。

九、运行项目

代码编写完成后,在DevEco Studio中运行项目。测试不同金额、期限、利率和还款方式的计算结果,验证公式正确性,检查结果格式化是否正确。

十、开发中遇到的问题

  • 财务公式准确性:确保等额本息和等额本金公式正确
  • 边界情况处理:输入0或非法值时返回0,避免NaN
  • 金额格式化:正确添加千分位分隔符,保留两位小数
  • 等额本金特殊显示:需要显示首月和末月月供,因为每月递减

十一、总结

本文基于HarmonyOS和ArkTS实现了一个房贷计算器。项目通过@State管理输入参数,使用专业的财务公式实现等额本息和等额本金计算,实时显示月供、总还款和总利息。这个项目展示了ArkTS中表单输入、实时计算和数学公式应用的基本方法,适合作为入门练习项目。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐