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 管理页面状态;
  • 支持收入和支出两种类型记录;
  • 实现金额数字输入;
  • 提供常用消费分类选择;
  • 支持添加备注信息;
  • 实现账单记录功能;
  • 显示账单列表;
  • 支持按类型筛选账单;
  • 统计月度总收入和总支出;
  • 计算月度结余;
  • 支持删除单条账单;
  • 使用 ListForEach 渲染账单列表;
  • 使用空状态提示优化无账单页面;
  • 使用 @Builder 封装统计卡片、分类按钮和账单项;
  • 完成一个可以运行的记账本页面。

这个项目虽然是单页面应用,但它有完整的记账、统计、筛选和删除流程,比普通静态页面更适合练习 ArkTS。

二、技术栈

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

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

三、为什么选择记账本项目

简洁记账本应用适合作为 ArkTS 练习项目,主要有以下几个原因。

第一,实用性强。记账是每个人都需要的理财习惯,一个简洁无广告的记账本可以直接应用于日常生活。

第二,业务场景真实。无论是日常消费还是工资收入,都需要记录和管理,记账本是最基础的个人财务工具。

第三,适合练习状态管理。记录类型、金额、分类、备注、筛选条件和账单列表都属于页面状态。状态变化后,页面会自动刷新。

第四,适合练习数据统计。总收入、总支出和结余都需要从账单数组动态计算,可以练习数组的reduce方法。

第五,适合练习表单交互。金额输入、分类选择、备注输入都是常见的表单交互形式。

第六,扩展空间比较大。基础功能完成后,可以继续增加本地存储、多账本、预算管理、消费图表、分类统计和数据导出。

在本项目中,bills 是核心数据源。页面中的统计数据、账单列表和筛选结果都由它计算或渲染得到。

四、功能规则说明

本文中的账单类型分为两类:

类型 颜色 说明
支出 #EF4444 消费支出,金额显示为红色
收入 #10B981 工资等收入,金额显示为绿色

支出分类包括:

分类 图标 说明
餐饮 🍜 吃饭、外卖等
交通 🚗 打车、公交、地铁等
购物 🛒 网购、超市购物等
娱乐 🎮 电影、游戏等
居住 🏠 房租、水电等
医疗 💊 看病、买药等
其他 📝 其他支出

收入分类包括:

分类 图标 说明
工资 💰 工资收入
奖金 🎁 奖金、红包
投资 📈 投资收益
其他 📝 其他收入

筛选规则如下:

全部:显示所有账单
收入:只显示收入记录
支出:只显示支出记录

统计规则如下:

本月收入 = 当月所有收入记录金额之和
本月支出 = 当月所有支出记录金额之和
本月结余 = 本月收入 - 本月支出

每次添加或删除账单后,统计数据都会自动更新。

五、项目结构

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

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

其中:

文件 作用
Index.ets 编写页面结构、状态数据和记账逻辑

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

如果后续继续扩展,可以考虑把金额输入器、分类选择器、账单项和统计卡片拆分成独立组件。

六、核心实现思路

本项目的核心流程如下:

  1. 定义账单数据结构;
  2. 使用 @State 保存记录类型、金额、选中分类、备注、筛选条件和账单列表;
  3. 用户选择收入或支出类型;
  4. 用户输入金额;
  5. 用户选择分类;
  6. 用户输入备注;
  7. 点击记一笔按钮创建新账单;
  8. 使用数组保存所有账单;
  9. 根据筛选条件展示不同账单;
  10. 使用 ForEach 渲染账单列表;
  11. 点击删除按钮删除指定账单;
  12. 顶部统计卡片根据账单数据自动计算;
  13. 没有账单时显示空状态提示。

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

@State recordType: string = '支出'
@State amount: string = ''
@State selectedCategory: string = '餐饮'
@State note: string = ''
@State filterType: string = '全部'
@State bills: Bill[] = []
@State nextId: number = 1

其中:

状态变量 作用
recordType 当前记录类型(收入/支出)
amount 输入的金额
selectedCategory 选中的分类
note 备注内容
filterType 当前筛选条件
bills 账单数组
nextId 生成账单唯一编号

bills 是本项目中最重要的数据。所有账单展示、筛选和统计都依赖这个数组。

七、Index.ets 完整代码

打开文件:

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

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

interface Bill {
  id: number
  type: string
  amount: number
  category: string
  note: string
  time: string
  date: string
}

@Entry
@Component
struct Index {
  @State recordType: string = '支出'
  @State amount: string = ''
  @State selectedCategory: string = '餐饮'
  @State note: string = ''
  @State filterType: string = '全部'
  @State nextId: number = 4
  @State bills: Bill[] = [
    {
      id: 1,
      type: '支出',
      amount: 35.5,
      category: '餐饮',
      note: '午餐外卖',
      time: '12:30',
      date: this.getCurrentDate()
    },
    {
      id: 2,
      type: '支出',
      amount: 15,
      category: '交通',
      note: '打车上班',
      time: '09:15',
      date: this.getCurrentDate()
    },
    {
      id: 3,
      type: '收入',
      amount: 15000,
      category: '工资',
      note: '7月工资',
      time: '10:00',
      date: this.getCurrentDate()
    }
  ]

  private expenseCategories: string[] = ['餐饮', '交通', '购物', '娱乐', '居住', '医疗', '其他']
  private incomeCategories: string[] = ['工资', '奖金', '投资', '其他']

  private addBill(): void {
    let amountNum = parseFloat(this.amount)
    if (isNaN(amountNum) || amountNum <= 0) {
      return
    }
    
    let bill: Bill = {
      id: this.nextId,
      type: this.recordType,
      amount: amountNum,
      category: this.selectedCategory,
      note: this.note.trim() || '无备注',
      time: this.getCurrentTime(),
      date: this.getCurrentDate()
    }
    
    this.bills = [bill, ...this.bills]
    this.nextId++
    this.amount = ''
    this.note = ''
  }

  private deleteBill(id: number): void {
    this.bills = this.bills.filter((bill: Bill) => bill.id !== id)
  }

  private getFilteredBills(): Bill[] {
    if (this.filterType === '收入') {
      return this.bills.filter((bill: Bill) => bill.type === '收入')
    }
    if (this.filterType === '支出') {
      return this.bills.filter((bill: Bill) => bill.type === '支出')
    }
    return this.bills
  }

  private getMonthlyIncome(): number {
    let now = new Date()
    let currentMonth = `${now.getFullYear()}-${this.formatTwo(now.getMonth() + 1)}`
    return this.bills
      .filter((bill: Bill) => bill.type === '收入' && bill.date.startsWith(currentMonth))
      .reduce((sum: number, bill: Bill) => sum + bill.amount, 0)
  }

  private getMonthlyExpense(): number {
    let now = new Date()
    let currentMonth = `${now.getFullYear()}-${this.formatTwo(now.getMonth() + 1)}`
    return this.bills
      .filter((bill: Bill) => bill.type === '支出' && bill.date.startsWith(currentMonth))
      .reduce((sum: number, bill: Bill) => sum + bill.amount, 0)
  }

  private getMonthlyBalance(): number {
    return this.getMonthlyIncome() - this.getMonthlyExpense()
  }

  private getCurrentTime(): string {
    let date = new Date()
    return `${this.formatTwo(date.getHours())}:${this.formatTwo(date.getMinutes())}`
  }

  private getCurrentDate(): string {
    let date = new Date()
    return `${date.getFullYear()}-${this.formatTwo(date.getMonth() + 1)}-${this.formatTwo(date.getDate())}`
  }

  private formatTwo(value: number): string {
    return value < 10 ? '0' + value.toString() : value.toString()
  }

  private getCategoryIcon(category: string): string {
    const icons: Record<string, string> = {
      '餐饮': '🍜', '交通': '🚗', '购物': '🛒', '娱乐': '🎮',
      '居住': '🏠', '医疗': '💊', '工资': '💰', '奖金': '🎁',
      '投资': '📈', '其他': '📝'
    }
    return icons[category] || '📝'
  }

  private getTypeColor(type: string): string {
    return type === '收入' ? '#10B981' : '#EF4444'
  }

  private switchType(type: string): void {
    this.recordType = type
    this.selectedCategory = type === '支出' ? '餐饮' : '工资'
  }

  @Builder
  StatCard(title: string, value: string, color: string) {
    Column() {
      Text(value)
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
      Text(title)
        .fontSize(13)
        .fontColor('#6B7280')
        .margin({ top: 4 })
    }
    .width('31%')
    .height(80)
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({
      radius: 10,
      color: '#12000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  TypeButton(text: string) {
    Button(text)
      .height(40)
      .layoutWeight(1)
      .fontSize(16)
      .fontColor(this.recordType === text ? Color.White : '#4B5563')
      .backgroundColor(this.recordType === text ? this.getTypeColor(text) : '#F3F4F6')
      .borderRadius(20)
      .onClick(() => {
        this.switchType(text)
      })
  }

  @Builder
  CategoryButton(text: string) {
    Button(`${this.getCategoryIcon(text)} ${text}`)
      .height(42)
      .padding({ left: 12, right: 12 })
      .fontSize(13)
      .fontColor(this.selectedCategory === text ? Color.White : '#4B5563')
      .backgroundColor(this.selectedCategory === text ? '#0A59F7' : '#F3F4F6')
      .borderRadius(21)
      .onClick(() => {
        this.selectedCategory = text
      })
  }

  @Builder
  FilterButton(text: string) {
    Button(text)
      .height(36)
      .layoutWeight(1)
      .fontSize(14)
      .fontColor(this.filterType === text ? Color.White : '#4B5563')
      .backgroundColor(this.filterType === text ? '#0A59F7' : '#F3F4F6')
      .borderRadius(18)
      .onClick(() => {
        this.filterType = text
      })
  }

  @Builder
  BillItem(bill: Bill) {
    Row() {
      Text(this.getCategoryIcon(bill.category))
        .fontSize(28)
        .width(44)
        .height(44)
        .textAlign(TextAlign.Center)
        .backgroundColor('#F5F7FA')
        .borderRadius(22)
      
      Column() {
        Text(bill.category)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
        
        Text(bill.note)
          .fontSize(13)
          .fontColor('#6B7280')
          .margin({ top: 4 })
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 12, right: 12 })
      
      Column() {
        Text(`${bill.type === '收入' ? '+' : '-'}¥${bill.amount.toFixed(2)}`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.getTypeColor(bill.type))
        
        Text(bill.time)
          .fontSize(12)
          .fontColor('#9CA3AF')
          .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.End)
      
      Button('×')
        .width(24)
        .height(24)
        .fontSize(18)
        .fontColor('#9CA3AF')
        .backgroundColor(Color.Transparent)
        .margin({ left: 8 })
        .onClick(() => {
          this.deleteBill(bill.id)
        })
    }
    .width('100%')
    .padding(14)
    .margin({ bottom: 10 })
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({
      radius: 6,
      color: '#08000000',
      offsetX: 0,
      offsetY: 2
    })
  }

  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 })

      Row() {
        this.StatCard('本月收入', `¥${this.getMonthlyIncome().toFixed(0)}`, '#10B981')
        this.StatCard('本月支出', `¥${this.getMonthlyExpense().toFixed(0)}`, '#EF4444')
        this.StatCard('本月结余', `¥${this.getMonthlyBalance().toFixed(0)}`, this.getMonthlyBalance() >= 0 ? '#0A59F7' : '#EF4444')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ bottom: 16 })

      Column() {
        Row() {
          this.TypeButton('支出')
          Blank().width(12)
          this.TypeButton('收入')
        }
        .width('100%')
        .margin({ bottom: 16 })

        Text('金额')
          .fontSize(14)
          .fontColor('#6B7280')
          .width('100%')
          .margin({ bottom: 8 })
        
        Row() {
          Text('¥')
            .fontSize(32)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.getTypeColor(this.recordType))
          TextInput({
            placeholder: '0.00',
            text: this.amount
          })
            .layoutWeight(1)
            .height(50)
            .fontSize(32)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.getTypeColor(this.recordType))
            .backgroundColor(Color.Transparent)
            .type(InputType.Number)
            .onChange((value: string) => {
              this.amount = value
            })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
        .backgroundColor('#F9FAFB')
        .borderRadius(12)
        .margin({ bottom: 16 })

        Text('分类')
          .fontSize(14)
          .fontColor('#6B7280')
          .width('100%')
          .margin({ bottom: 8 })
        
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.recordType === '支出' ? this.expenseCategories : this.incomeCategories, (category: string) => {
            this.CategoryButton(category)
          }, (category: string) => category)
        }
        .width('100%')
        .margin({ bottom: 16 })

        Text('备注')
          .fontSize(14)
          .fontColor('#6B7280')
          .width('100%')
          .margin({ bottom: 8 })
        
        TextInput({
          placeholder: '添加备注(选填)',
          text: this.note
        })
          .height(44)
          .fontSize(15)
          .backgroundColor('#F9FAFB')
          .borderRadius(12)
          .padding({ left: 12, right: 12 })
          .margin({ bottom: 16 })
          .onChange((value: string) => {
            this.note = value
          })

        Button('记一笔')
          .height(48)
          .fontSize(18)
          .fontColor(Color.White)
          .backgroundColor(this.getTypeColor(this.recordType))
          .borderRadius(24)
          .width('100%')
          .onClick(() => {
            this.addBill()
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Text('账单明细')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .width('100%')
        .margin({ bottom: 12 })

      Row() {
        this.FilterButton('全部')
        Blank().width(8)
        this.FilterButton('收入')
        Blank().width(8)
        this.FilterButton('支出')
      }
      .width('100%')
      .margin({ bottom: 12 })

      if (this.getFilteredBills().length === 0) {
        Column() {
          Text('暂无账单记录')
            .fontSize(16)
            .fontColor('#6B7280')
          Text('开始记录你的第一笔收支吧')
            .fontSize(13)
            .fontColor('#9CA3AF')
            .margin({ top: 8 })
        }
        .width('100%')
        .padding(30)
        .backgroundColor(Color.White)
        .borderRadius(16)
      } else {
        List() {
          ForEach(this.getFilteredBills(), (bill: Bill) => {
            ListItem() {
              this.BillItem(bill)
            }
          }, (bill: Bill) => bill.id.toString())
        }
        .width('100%')
        .layoutWeight(1)
        .scrollBar(BarState.Off)
      }
    }
    .width('100%')
    .height('100%')
    .padding({ left: 18, right: 18 })
    .backgroundColor('#F5F7FA')
  }
}

八、代码实现说明

1. 定义账单数据结构

项目中使用 Bill 接口描述每一笔账单:

interface Bill {
  id: number
  type: string
  amount: number
  category: string
  note: string
  time: string
  date: string
}

字段说明如下:

字段 作用
id 账单唯一编号
type 账单类型(收入/支出)
amount 金额
category 分类
note 备注
time 记录时间
date 记录日期

其中 id 用于区分不同账单,type 用于筛选和统计,date 用于月度统计。

2. 使用 @State 管理页面状态

本项目中使用多个状态变量:

@State recordType: string = '支出'
@State amount: string = ''
@State selectedCategory: string = '支出'
@State note: string = ''
@State filterType: string = '全部'
@State bills: Bill[] = []
@State nextId: number = 1

这些状态变量分别保存记录类型、金额、选中分类、备注、筛选条件和账单列表。当用户切换类型、输入金额、选择分类或添加账单时,状态数据都会发生变化。ArkUI 会根据最新状态自动刷新页面。

3. 添加账单记录

添加账单通过 addBill 方法实现:

private addBill(): void {
  let amountNum = parseFloat(this.amount)
  if (isNaN(amountNum) || amountNum <= 0) {
    return
  }
  
  let bill: Bill = {
    id: this.nextId,
    type: this.recordType,
    amount: amountNum,
    category: this.selectedCategory,
    note: this.note.trim() || '无备注',
    time: this.getCurrentTime(),
    date: this.getCurrentDate()
  }
  
  this.bills = [bill, ...this.bills]
  this.nextId++
  this.amount = ''
  this.note = ''
}

如果金额无效(非数字或小于等于0)则不添加。备注为空时自动填充"无备注"。新账单放在数组最前面,添加后清空金额和备注输入。

4. 类型切换功能

用户可以在收入和支出之间切换:

private switchType(type: string): void {
  this.recordType = type
  this.selectedCategory = type === '支出' ? '餐饮' : '工资'
}

切换类型时会自动重置选中的分类,避免分类和类型不匹配。

5. 月度统计计算

月度收入、支出和结余通过数组方法动态计算:

private getMonthlyIncome(): number {
  let now = new Date()
  let currentMonth = `${now.getFullYear()}-${this.formatTwo(now.getMonth() + 1)}`
  return this.bills
    .filter((bill: Bill) => bill.type === '收入' && bill.date.startsWith(currentMonth))
    .reduce((sum: number, bill: Bill) => sum + bill.amount, 0)
}

使用 filter 筛选当月的收入记录,再用 reduce 求和。支出和结余的计算方式类似。

6. 账单筛选功能

筛选通过 filterTypegetFilteredBills 方法实现:

private getFilteredBills(): Bill[] {
  if (this.filterType === '收入') {
    return this.bills.filter((bill: Bill) => bill.type === '收入')
  }
  if (this.filterType === '支出') {
    return this.bills.filter((bill: Bill) => bill.type === '支出')
  }
  return this.bills
}

筛选不会改变原始数组,只会改变当前页面展示内容。

7. 删除账单功能

删除账单通过 deleteBill 方法实现:

private deleteBill(id: number): void {
  this.bills = this.bills.filter((bill: Bill) => bill.id !== id)
}

使用唯一id删除账单,避免误删。

8. 分类图标显示

每个分类都有对应的emoji图标,让界面更生动:

private getCategoryIcon(category: string): string {
  const icons: Record<string, string> = {
    '餐饮': '🍜', '交通': '🚗', '购物': '🛒', '娱乐': '🎮',
    '居住': '🏠', '医疗': '💊', '工资': '💰', '奖金': '🎁',
    '投资': '📈', '其他': '📝'
  }
  return icons[category] || '📝'
}

九、运行项目

代码编写完成后,在 DevEco Studio 中选择模拟器或真机运行项目。

运行成功后,页面会显示"简洁记账本"。用户可以切换收入/支出类型,输入金额,选择分类,添加备注,记录账单,查看统计和账单列表。

可以按照以下步骤进行测试:

  1. 打开应用,检查页面是否正常显示预置的示例账单;
  2. 检查统计卡片是否显示正确的本月收入、支出和结余;
  3. 切换收入/支出类型,检查金额颜色和分类是否变化;
  4. 输入金额;
  5. 选择不同的分类;
  6. 输入备注;
  7. 点击"记一笔"按钮,检查新账单是否出现在列表顶部;
  8. 检查统计数据是否更新;
  9. 点击"收入"筛选,检查是否只显示收入记录;
  10. 点击"支出"筛选,检查是否只显示支出记录;
  11. 点击账单右上角的×按钮,检查账单是否被删除;
  12. 检查统计数据是否更新;
  13. 删除所有账单,检查空状态提示是否显示。

测试结果如下:

测试功能 测试结果
页面正常显示 成功
类型切换 成功
金额输入 成功
分类选择 成功
备注输入 成功
添加账单 成功
账单筛选 成功
删除账单 成功
月度统计 成功
空状态提示 成功

经过测试,应用主要功能可以正常运行,账单数据和统计都能根据用户操作及时更新。

十、开发中遇到的问题

1. 金额输入验证

需要验证输入的金额是否为有效数字,避免NaN或负数:

let amountNum = parseFloat(this.amount)
if (isNaN(amountNum) || amountNum <= 0) {
  return
}
2. 类型和分类联动

切换收入/支出类型时,分类选项也要跟着变化,并且默认选中对应类型的第一个分类:

private switchType(type: string): void {
  this.recordType = type
  this.selectedCategory = type === '支出' ? '餐饮' : '工资'
}
3. 月度统计日期匹配

需要正确匹配当月的账单,使用日期字符串前缀匹配:

bill.date.startsWith(currentMonth)
4. 结余颜色显示

结余为正显示蓝色,为负显示红色,直观反映财务状况:

this.getMonthlyBalance() >= 0 ? '#0A59F7' : '#EF4444'
5. 金额格式化

显示金额时保留两位小数,符合货币显示习惯:

bill.amount.toFixed(2)

十一、总结

本文基于 HarmonyOS 和 ArkTS 实现了一个简洁记账本应用。项目通过 @State 管理页面数据,使用 ArkUI 组件完成页面布局,并实现了收支记录、分类选择、金额输入、账单列表、月度统计和账单筛选等功能。

通过本次实践,主要完成了以下内容:

  • 使用 @Entry@Component 创建页面组件;
  • 使用 @State 保存记录类型、金额、分类和账单列表;
  • 使用 TextInput 实现金额和备注输入;
  • 支持收入和支出两种记录类型;
  • 提供丰富的收支分类选择;
  • 实现账单添加和删除功能;
  • 支持按类型筛选账单;
  • 动态计算月度收入、支出和结余;
  • 使用emoji图标美化分类显示;
  • 使用 ListForEach 渲染账单列表;
  • 使用条件渲染显示空状态;
  • 使用 @Builder 封装页面局部结构;
  • 完成一个可以运行的 HarmonyOS 个人财务管理页面。

这个项目虽然是一个单页面练习,但它完整实现了记账应用的核心功能。它展示了 ArkTS 中表单处理、数字计算、数组统计和状态管理的基本方法。

后续可以在这个基础上继续扩展,例如:

  • 增加本地数据持久化;
  • 增加多账本支持;
  • 增加预算管理功能;
  • 增加消费统计图表;
  • 增加分类支出占比分析;
  • 增加账单编辑功能;
  • 增加自定义分类;
  • 增加数据导出功能;
  • 增加深色模式适配;
  • 增加账单搜索功能。

整体来看,简洁记账本应用适合作为 HarmonyOS ArkTS 的练习项目。它功能实用、贴近生活,也能体现 ArkUI 声明式开发和状态驱动页面刷新的基本思想。

Logo

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

更多推荐