家庭记账本小应用 - HarmonyOS ArkUI 开发实战-Tabs与List组件-PC版本

一、应用背景与需求分析

在日常生活中,家庭财务管理是一项重要的任务。传统的纸质记账方式存在诸多不便:记录容易丢失、统计困难、难以追溯历史数据。随着智能手机的普及,电子记账应用成为越来越多家庭的选择。

家庭记账本小应用正是基于这一需求而开发。它采用HarmonyOS ArkUI框架构建,具有界面简洁、操作便捷、功能实用的特点。用户可以快速记录每笔支出,包括金额、分类、备注等信息,并实时查看总支出统计和支出记录列表。

1.1 核心功能需求

根据家庭记账的实际场景,本应用需要实现以下核心功能:

  • 支出记录:支持输入金额、选择分类、填写备注
  • 分类管理:提供餐饮、购物、交通、娱乐、医疗、其他等常用分类
  • 实时统计:自动计算并显示总支出金额
  • 记录列表:以列表形式展示所有支出记录
  • 删除功能:支持删除不需要的支出记录
  • 数据持久化:应用重启后数据不丢失(扩展功能)

1.2 技术选型

本应用采用HarmonyOS原生开发技术栈:

技术组件 说明
开发框架 ArkUI声明式UI框架
开发语言 ArkTS(TypeScript扩展)
UI组件 Column、Row、List、TextInput、Button等
状态管理 @State装饰器
数据存储 内存数组(可扩展为Preferences)

二、应用架构设计

2.1 整体架构

家庭记账本应用采用单页面架构,所有功能在一个页面中完成。整体架构分为三层:
#mermaid-svg-ZNhCOOWUdYWjRuuE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZNhCOOWUdYWjRuuE .error-icon{fill:#552222;}#mermaid-svg-ZNhCOOWUdYWjRuuE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZNhCOOWUdYWjRuuE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .marker.cross{stroke:#333333;}#mermaid-svg-ZNhCOOWUdYWjRuuE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZNhCOOWUdYWjRuuE p{margin:0;}#mermaid-svg-ZNhCOOWUdYWjRuuE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .cluster-label text{fill:#333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .cluster-label span{color:#333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .cluster-label span p{background-color:transparent;}#mermaid-svg-ZNhCOOWUdYWjRuuE .label text,#mermaid-svg-ZNhCOOWUdYWjRuuE span{fill:#333;color:#333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .node rect,#mermaid-svg-ZNhCOOWUdYWjRuuE .node circle,#mermaid-svg-ZNhCOOWUdYWjRuuE .node ellipse,#mermaid-svg-ZNhCOOWUdYWjRuuE .node polygon,#mermaid-svg-ZNhCOOWUdYWjRuuE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZNhCOOWUdYWjRuuE .rough-node .label text,#mermaid-svg-ZNhCOOWUdYWjRuuE .node .label text,#mermaid-svg-ZNhCOOWUdYWjRuuE .image-shape .label,#mermaid-svg-ZNhCOOWUdYWjRuuE .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZNhCOOWUdYWjRuuE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZNhCOOWUdYWjRuuE .rough-node .label,#mermaid-svg-ZNhCOOWUdYWjRuuE .node .label,#mermaid-svg-ZNhCOOWUdYWjRuuE .image-shape .label,#mermaid-svg-ZNhCOOWUdYWjRuuE .icon-shape .label{text-align:center;}#mermaid-svg-ZNhCOOWUdYWjRuuE .node.clickable{cursor:pointer;}#mermaid-svg-ZNhCOOWUdYWjRuuE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .arrowheadPath{fill:#333333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZNhCOOWUdYWjRuuE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZNhCOOWUdYWjRuuE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZNhCOOWUdYWjRuuE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZNhCOOWUdYWjRuuE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZNhCOOWUdYWjRuuE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZNhCOOWUdYWjRuuE .cluster text{fill:#333;}#mermaid-svg-ZNhCOOWUdYWjRuuE .cluster span{color:#333;}#mermaid-svg-ZNhCOOWUdYWjRuuE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZNhCOOWUdYWjRuuE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZNhCOOWUdYWjRuuE rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZNhCOOWUdYWjRuuE .icon-shape,#mermaid-svg-ZNhCOOWUdYWjRuuE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZNhCOOWUdYWjRuuE .icon-shape p,#mermaid-svg-ZNhCOOWUdYWjRuuE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZNhCOOWUdYWjRuuE .icon-shape .label rect,#mermaid-svg-ZNhCOOWUdYWjRuuE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZNhCOOWUdYWjRuuE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZNhCOOWUdYWjRuuE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZNhCOOWUdYWjRuuE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} UI展示层
业务逻辑层
数据模型层
总支出展示
金额输入区
分类选择器
备注输入框
记录按钮
记录列表
金额验证
分类选择
记录添加
记录删除
总支出计算
ExpenseItem数据类
支出记录数组
分类数组

2.2 数据模型设计

应用的核心数据模型是ExpenseItem类,用于表示单条支出记录:

typescript 复制代码
class ExpenseItem_1 {
  amount_1: number = 0;      // 支出金额
  category_1: string = '';   // 支出分类
  note_1: string = '';       // 备注信息
  date_1: string = '';      // 记录日期

  constructor(amount_1: number, category_1: string, note_1: string, date_1: string) {
    this.amount_1 = amount_1;
    this.category_1 = category_1;
    this.note_1 = note_1;
    this.date_1 = date_1;
  }
}

设计要点:

  1. 类型安全:所有属性都有明确的类型声明,符合ArkTS的强类型要求
  2. 默认值:为每个属性设置默认值,避免空值问题
  3. 构造函数:提供便捷的对象创建方式
  4. 命名规范 :使用_1后缀避免命名冲突(ArkTS编译器要求)

2.3 状态管理

应用使用@State装饰器管理响应式状态:

typescript 复制代码
@Entry
@Component
struct ExpenseTrackerApp {
  @State amount_1: number = 0;                          // 当前输入金额
  @State category_1: string = '餐饮';                   // 当前选中分类
  @State note_1: string = '';                          // 当前备注
  @State expenses_1: ExpenseItem_1[] = [               // 支出记录数组
    new ExpenseItem_1(35, '餐饮', '午餐', '今天'),
    new ExpenseItem_1(128, '购物', '衣服', '今天'),
    new ExpenseItem_1(200, '交通', '加油', '昨天')
  ];

  categories_1: string[] = ['餐饮', '购物', '交通', '娱乐', '医疗', '其他'];
}

状态说明:

  • amount_1:用户输入的支出金额,初始为0
  • category_1:用户选择的支出分类,默认为"餐饮"
  • note_1:用户输入的备注信息,初始为空字符串
  • expenses_1:存储所有支出记录的数组,预置了3条示例数据
  • categories_1:可选的支出分类列表(非响应式)

三、UI界面实现

3.1 页面布局结构

应用采用垂直布局(Column)作为主容器,从上到下依次为标题栏、总支出展示、输入区域和记录列表:

typescript 复制代码
Column() {
  Row() {
    // 标题栏:返回按钮 + 标题
  }
  
  Column() {
    // 总支出圆形展示
    // 金额输入区
    // 分类选择器
    // 备注输入框
    // 记录按钮
    // 最近记录标题
    // 记录列表
  }
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')

3.2 标题栏实现

标题栏采用水平布局(Row),包含返回按钮和应用标题:

typescript 复制代码
Row() {
  Button('返回')
    .onClick(() => router.back())
  Text('家庭记账本小应用')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .layoutWeight(1)
    .textAlign(TextAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor('#F1F3F5')

实现细节:

  1. 返回按钮 :点击后调用router.back()返回上一页
  2. 标题文本 :使用layoutWeight(1)占据剩余空间,居中显示
  3. 背景色 :使用浅灰色#F1F3F5作为背景,与内容区域区分

3.3 总支出展示

总支出采用圆形卡片设计,突出显示金额:

typescript 复制代码
Stack() {
  Circle()
    .width(120)
    .height(120)
    .fill('#E8F0FE')
  Column() {
    Text('总支出')
      .fontSize(14)
      .fontColor('#666666')
    Text('¥' + String(this.totalExpense_1()))
      .fontSize(28)
      .fontWeight(FontWeight.Bold)
      .fontColor('#0A59F7')
      .margin({ top: 4 })
  }
}
.margin({ top: 30 })

设计要点:

  1. Stack布局:使用栈布局实现圆形背景与文字的叠加
  2. 圆形背景 :使用Circle组件绘制浅蓝色圆形
  3. 文字层次:"总支出"标签使用小字号灰色,金额使用大字号蓝色加粗
  4. 颜色搭配 :浅蓝背景#E8F0FE + 深蓝文字#0A59F7,视觉舒适

3.4 金额输入区

金额输入使用TextInput组件,限制为数字输入:

typescript 复制代码
Text('金额')
  .fontSize(16)
  .fontWeight(FontWeight.Medium)
  .margin({ top: 30 })

TextInput({ placeholder: '0.00' })
  .width('80%')
  .height(48)
  .margin({ top: 8 })
  .type(InputType.Number)
  .onChange((value_1: string) => {
    this.amount_1 = parseFloat(value_1) || 0;
  })

关键实现:

  1. 输入类型 :设置InputType.Number,弹出数字键盘
  2. 占位符:显示"0.00"提示用户输入格式
  3. 数据转换 :使用parseFloat将字符串转为数字,转换失败时默认为0
  4. 宽度控制:设置80%宽度,两侧留白

3.5 分类选择器

分类选择器使用按钮组实现,选中状态通过背景色区分:

typescript 复制代码
Text('分类')
  .fontSize(16)
  .fontWeight(FontWeight.Medium)
  .margin({ top: 16 })

Row() {
  ForEach(this.categories_1, (cat_1: string) => {
    Button(cat_1)
      .height(36)
      .fontSize(12)
      .backgroundColor(this.category_1 === cat_1 ? '#0A59F7' : '#F1F3F5')
      .fontColor(this.category_1 === cat_1 ? '#FFFFFF' : '#333333')
      .margin({ right: 8 })
      .onClick(() => {
        this.category_1 = cat_1;
      })
  })
}
.margin({ top: 8 })

交互逻辑:

  1. 遍历渲染 :使用ForEach遍历分类数组,动态生成按钮
  2. 选中状态:当前分类与按钮分类相等时,显示蓝色背景白色文字
  3. 未选中状态:显示浅灰背景深灰文字
  4. 点击事件 :点击按钮更新category_1状态,触发UI刷新

3.6 备注输入框

备注输入同样使用TextInput组件:

typescript 复制代码
TextInput({ placeholder: '备注(可选)' })
  .width('80%')
  .height(48)
  .margin({ top: 16 })
  .onChange((value_1: string) => {
    this.note_1 = value_1;
  })

特点:

  • 占位符提示"备注(可选)",说明该项非必填
  • 宽度与金额输入框一致,保持视觉统一
  • 输入内容实时更新到note_1状态

3.7 记录按钮

记录按钮点击后将支出信息添加到记录列表:

typescript 复制代码
Button('记录')
  .width('60%')
  .height(40)
  .margin({ top: 16 })
  .backgroundColor('#0A59F7')
  .onClick(() => {
    if (this.amount_1 > 0) {
      let newExpense_1 = new ExpenseItem_1(this.amount_1, this.category_1, this.note_1, '今天');
      this.expenses_1 = [newExpense_1].concat(this.expenses_1);
      this.amount_1 = 0;
      this.note_1 = '';
    }
  })

业务逻辑:

  1. 金额验证:只有金额大于0时才添加记录
  2. 创建记录 :使用当前输入数据创建ExpenseItem对象
  3. 添加到列表:将新记录添加到数组开头,保证最新记录显示在最前
  4. 清空输入:添加成功后清空金额和备注,准备下一次输入

3.8 记录列表

记录列表使用List组件实现,支持滚动和删除操作:

typescript 复制代码
Text('最近记录')
  .fontSize(16)
  .fontWeight(FontWeight.Medium)
  .margin({ top: 24, left: 20 })
  .width('90%')

List() {
  ForEach(this.expenses_1, (item_1: ExpenseItem_1, index_1: number) => {
    ListItem() {
      Row() {
        Text(item_1.category_1)
          .width(60)
          .fontSize(14)
        Text(item_1.note_1)
          .fontSize(14)
          .fontColor('#666666')
          .layoutWeight(1)
        Text('-¥' + String(item_1.amount_1))
          .fontSize(14)
          .fontColor('#FF3B30')
        Button('删')
          .width(32)
          .height(32)
          .fontSize(12)
          .backgroundColor('#FF3B30')
          .margin({ left: 8 })
          .onClick(() => {
            this.expenses_1 = this.expenses_1.filter((_, i_1: number) => i_1 !== index_1);
          })
      }
      .padding(12)
      .width('100%')
    }
  })
}
.width('90%')
.height(250)
.margin({ top: 8 })

列表项布局:

组件 宽度 内容 颜色
分类 60 支出分类 默认黑色
备注 自适应 备注信息 灰色#666666
金额 自适应 -¥金额 红色#FF3B30
删除按钮 32x32 "删" 红色#FF3B30

删除逻辑:

使用filter方法过滤掉要删除的记录:

typescript 复制代码
this.expenses_1 = this.expenses_1.filter((_, i_1: number) => i_1 !== index_1);

四、核心功能实现

4.1 总支出计算

总支出计算方法遍历所有记录并累加金额:

typescript 复制代码
totalExpense_1(): number {
  let total_1: number = 0;
  for (let i_1 = 0; i_1 < this.expenses_1.length; i_1++) {
    total_1 += this.expenses_1[i_1].amount_1;
  }
  return total_1;
}

实现说明:

  1. 方法定义 :定义在组件内部的方法,返回number类型
  2. 遍历累加 :使用for循环遍历数组,累加每条记录的金额
  3. 实时计算:每次调用都重新计算,保证数据准确性
  4. 性能考虑:对于少量数据,实时计算性能足够;大量数据可考虑缓存

4.2 记录添加流程

记录添加的完整流程如下:
#mermaid-svg-3QekuoeWOAXT2lZa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3QekuoeWOAXT2lZa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3QekuoeWOAXT2lZa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3QekuoeWOAXT2lZa .error-icon{fill:#552222;}#mermaid-svg-3QekuoeWOAXT2lZa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3QekuoeWOAXT2lZa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3QekuoeWOAXT2lZa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3QekuoeWOAXT2lZa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3QekuoeWOAXT2lZa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3QekuoeWOAXT2lZa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3QekuoeWOAXT2lZa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3QekuoeWOAXT2lZa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3QekuoeWOAXT2lZa .marker.cross{stroke:#333333;}#mermaid-svg-3QekuoeWOAXT2lZa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3QekuoeWOAXT2lZa p{margin:0;}#mermaid-svg-3QekuoeWOAXT2lZa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3QekuoeWOAXT2lZa .cluster-label text{fill:#333;}#mermaid-svg-3QekuoeWOAXT2lZa .cluster-label span{color:#333;}#mermaid-svg-3QekuoeWOAXT2lZa .cluster-label span p{background-color:transparent;}#mermaid-svg-3QekuoeWOAXT2lZa .label text,#mermaid-svg-3QekuoeWOAXT2lZa span{fill:#333;color:#333;}#mermaid-svg-3QekuoeWOAXT2lZa .node rect,#mermaid-svg-3QekuoeWOAXT2lZa .node circle,#mermaid-svg-3QekuoeWOAXT2lZa .node ellipse,#mermaid-svg-3QekuoeWOAXT2lZa .node polygon,#mermaid-svg-3QekuoeWOAXT2lZa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3QekuoeWOAXT2lZa .rough-node .label text,#mermaid-svg-3QekuoeWOAXT2lZa .node .label text,#mermaid-svg-3QekuoeWOAXT2lZa .image-shape .label,#mermaid-svg-3QekuoeWOAXT2lZa .icon-shape .label{text-anchor:middle;}#mermaid-svg-3QekuoeWOAXT2lZa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3QekuoeWOAXT2lZa .rough-node .label,#mermaid-svg-3QekuoeWOAXT2lZa .node .label,#mermaid-svg-3QekuoeWOAXT2lZa .image-shape .label,#mermaid-svg-3QekuoeWOAXT2lZa .icon-shape .label{text-align:center;}#mermaid-svg-3QekuoeWOAXT2lZa .node.clickable{cursor:pointer;}#mermaid-svg-3QekuoeWOAXT2lZa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3QekuoeWOAXT2lZa .arrowheadPath{fill:#333333;}#mermaid-svg-3QekuoeWOAXT2lZa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3QekuoeWOAXT2lZa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3QekuoeWOAXT2lZa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3QekuoeWOAXT2lZa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3QekuoeWOAXT2lZa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3QekuoeWOAXT2lZa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3QekuoeWOAXT2lZa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3QekuoeWOAXT2lZa .cluster text{fill:#333;}#mermaid-svg-3QekuoeWOAXT2lZa .cluster span{color:#333;}#mermaid-svg-3QekuoeWOAXT2lZa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3QekuoeWOAXT2lZa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3QekuoeWOAXT2lZa rect.text{fill:none;stroke-width:0;}#mermaid-svg-3QekuoeWOAXT2lZa .icon-shape,#mermaid-svg-3QekuoeWOAXT2lZa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3QekuoeWOAXT2lZa .icon-shape p,#mermaid-svg-3QekuoeWOAXT2lZa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3QekuoeWOAXT2lZa .icon-shape .label rect,#mermaid-svg-3QekuoeWOAXT2lZa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3QekuoeWOAXT2lZa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3QekuoeWOAXT2lZa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3QekuoeWOAXT2lZa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

用户点击记录按钮
金额>0?
不执行任何操作
创建ExpenseItem对象
将新记录添加到数组开头
清空金额和备注输入
UI自动刷新
总支出重新计算

代码实现:

typescript 复制代码
if (this.amount_1 > 0) {
  // 1. 创建新记录
  let newExpense_1 = new ExpenseItem_1(
    this.amount_1,      // 金额
    this.category_1,    // 分类
    this.note_1,        // 备注
    '今天'              // 日期
  );
  
  // 2. 添加到数组开头
  this.expenses_1 = [newExpense_1].concat(this.expenses_1);
  
  // 3. 清空输入
  this.amount_1 = 0;
  this.note_1 = '';
}

4.3 记录删除流程

记录删除使用filter方法实现:
#mermaid-svg-PnUWahMr8TMG3e85{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PnUWahMr8TMG3e85 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PnUWahMr8TMG3e85 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PnUWahMr8TMG3e85 .error-icon{fill:#552222;}#mermaid-svg-PnUWahMr8TMG3e85 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PnUWahMr8TMG3e85 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PnUWahMr8TMG3e85 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PnUWahMr8TMG3e85 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PnUWahMr8TMG3e85 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PnUWahMr8TMG3e85 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PnUWahMr8TMG3e85 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PnUWahMr8TMG3e85 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PnUWahMr8TMG3e85 .marker.cross{stroke:#333333;}#mermaid-svg-PnUWahMr8TMG3e85 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PnUWahMr8TMG3e85 p{margin:0;}#mermaid-svg-PnUWahMr8TMG3e85 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PnUWahMr8TMG3e85 .cluster-label text{fill:#333;}#mermaid-svg-PnUWahMr8TMG3e85 .cluster-label span{color:#333;}#mermaid-svg-PnUWahMr8TMG3e85 .cluster-label span p{background-color:transparent;}#mermaid-svg-PnUWahMr8TMG3e85 .label text,#mermaid-svg-PnUWahMr8TMG3e85 span{fill:#333;color:#333;}#mermaid-svg-PnUWahMr8TMG3e85 .node rect,#mermaid-svg-PnUWahMr8TMG3e85 .node circle,#mermaid-svg-PnUWahMr8TMG3e85 .node ellipse,#mermaid-svg-PnUWahMr8TMG3e85 .node polygon,#mermaid-svg-PnUWahMr8TMG3e85 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PnUWahMr8TMG3e85 .rough-node .label text,#mermaid-svg-PnUWahMr8TMG3e85 .node .label text,#mermaid-svg-PnUWahMr8TMG3e85 .image-shape .label,#mermaid-svg-PnUWahMr8TMG3e85 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PnUWahMr8TMG3e85 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PnUWahMr8TMG3e85 .rough-node .label,#mermaid-svg-PnUWahMr8TMG3e85 .node .label,#mermaid-svg-PnUWahMr8TMG3e85 .image-shape .label,#mermaid-svg-PnUWahMr8TMG3e85 .icon-shape .label{text-align:center;}#mermaid-svg-PnUWahMr8TMG3e85 .node.clickable{cursor:pointer;}#mermaid-svg-PnUWahMr8TMG3e85 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PnUWahMr8TMG3e85 .arrowheadPath{fill:#333333;}#mermaid-svg-PnUWahMr8TMG3e85 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PnUWahMr8TMG3e85 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PnUWahMr8TMG3e85 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PnUWahMr8TMG3e85 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PnUWahMr8TMG3e85 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PnUWahMr8TMG3e85 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PnUWahMr8TMG3e85 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PnUWahMr8TMG3e85 .cluster text{fill:#333;}#mermaid-svg-PnUWahMr8TMG3e85 .cluster span{color:#333;}#mermaid-svg-PnUWahMr8TMG3e85 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PnUWahMr8TMG3e85 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PnUWahMr8TMG3e85 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PnUWahMr8TMG3e85 .icon-shape,#mermaid-svg-PnUWahMr8TMG3e85 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PnUWahMr8TMG3e85 .icon-shape p,#mermaid-svg-PnUWahMr8TMG3e85 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PnUWahMr8TMG3e85 .icon-shape .label rect,#mermaid-svg-PnUWahMr8TMG3e85 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PnUWahMr8TMG3e85 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PnUWahMr8TMG3e85 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PnUWahMr8TMG3e85 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户点击删除按钮
获取记录索引
调用filter方法
过滤掉指定索引的记录
更新expenses数组
UI自动刷新
总支出重新计算

关键技术:

  • 索引传递 :ForEach的第二个参数提供索引值
  • 过滤逻辑:保留索引不等于删除索引的记录
  • 数组更新:将过滤后的新数组赋值给状态变量,触发UI刷新

4.4 数据流分析

应用的数据流遵循单向数据流原则:
#mermaid-svg-LnEmHVpHcoI4bf0g{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LnEmHVpHcoI4bf0g .error-icon{fill:#552222;}#mermaid-svg-LnEmHVpHcoI4bf0g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LnEmHVpHcoI4bf0g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LnEmHVpHcoI4bf0g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LnEmHVpHcoI4bf0g .marker.cross{stroke:#333333;}#mermaid-svg-LnEmHVpHcoI4bf0g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LnEmHVpHcoI4bf0g p{margin:0;}#mermaid-svg-LnEmHVpHcoI4bf0g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LnEmHVpHcoI4bf0g .cluster-label text{fill:#333;}#mermaid-svg-LnEmHVpHcoI4bf0g .cluster-label span{color:#333;}#mermaid-svg-LnEmHVpHcoI4bf0g .cluster-label span p{background-color:transparent;}#mermaid-svg-LnEmHVpHcoI4bf0g .label text,#mermaid-svg-LnEmHVpHcoI4bf0g span{fill:#333;color:#333;}#mermaid-svg-LnEmHVpHcoI4bf0g .node rect,#mermaid-svg-LnEmHVpHcoI4bf0g .node circle,#mermaid-svg-LnEmHVpHcoI4bf0g .node ellipse,#mermaid-svg-LnEmHVpHcoI4bf0g .node polygon,#mermaid-svg-LnEmHVpHcoI4bf0g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LnEmHVpHcoI4bf0g .rough-node .label text,#mermaid-svg-LnEmHVpHcoI4bf0g .node .label text,#mermaid-svg-LnEmHVpHcoI4bf0g .image-shape .label,#mermaid-svg-LnEmHVpHcoI4bf0g .icon-shape .label{text-anchor:middle;}#mermaid-svg-LnEmHVpHcoI4bf0g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LnEmHVpHcoI4bf0g .rough-node .label,#mermaid-svg-LnEmHVpHcoI4bf0g .node .label,#mermaid-svg-LnEmHVpHcoI4bf0g .image-shape .label,#mermaid-svg-LnEmHVpHcoI4bf0g .icon-shape .label{text-align:center;}#mermaid-svg-LnEmHVpHcoI4bf0g .node.clickable{cursor:pointer;}#mermaid-svg-LnEmHVpHcoI4bf0g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LnEmHVpHcoI4bf0g .arrowheadPath{fill:#333333;}#mermaid-svg-LnEmHVpHcoI4bf0g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LnEmHVpHcoI4bf0g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LnEmHVpHcoI4bf0g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LnEmHVpHcoI4bf0g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LnEmHVpHcoI4bf0g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LnEmHVpHcoI4bf0g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LnEmHVpHcoI4bf0g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LnEmHVpHcoI4bf0g .cluster text{fill:#333;}#mermaid-svg-LnEmHVpHcoI4bf0g .cluster span{color:#333;}#mermaid-svg-LnEmHVpHcoI4bf0g div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LnEmHVpHcoI4bf0g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LnEmHVpHcoI4bf0g rect.text{fill:none;stroke-width:0;}#mermaid-svg-LnEmHVpHcoI4bf0g .icon-shape,#mermaid-svg-LnEmHVpHcoI4bf0g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LnEmHVpHcoI4bf0g .icon-shape p,#mermaid-svg-LnEmHVpHcoI4bf0g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LnEmHVpHcoI4bf0g .icon-shape .label rect,#mermaid-svg-LnEmHVpHcoI4bf0g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LnEmHVpHcoI4bf0g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LnEmHVpHcoI4bf0g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LnEmHVpHcoI4bf0g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户输入
状态更新
UI刷新
用户交互
expenses数组
总支出计算
列表渲染
添加记录
删除记录

数据流说明:

  1. 输入流:用户输入 → 状态更新 → UI刷新
  2. 计算流:expenses数组 → 总支出计算 → 显示
  3. 操作流:添加/删除操作 → 更新expenses数组 → 触发计算和渲染

五、样式设计详解

5.1 颜色方案

应用采用简洁的蓝白配色方案:

用途 颜色值 说明
主色调 #0A59F7 蓝色,用于按钮、选中状态、金额
背景色 #FFFFFF 白色,页面背景
次级背景 #F1F3F5 浅灰,标题栏背景
强调背景 #E8F0FE 浅蓝,圆形卡片背景
危险色 #FF3B30 红色,删除按钮、支出金额
次要文字 #666666 灰色,备注、标签

5.2 字体规范

元素 字号 字重 颜色
标题 20 Bold 默认
标签 16 Medium 默认
按钮文字 18/12 Regular 白色/黑色
列表文字 14 Regular 默认/灰色
金额 28 Bold #0A59F7

5.3 间距规范

元素 上边距 左右边距
标题栏 - padding: 12
圆形卡片 30 -
输入框 8/16 宽度80%
按钮 16/30 宽度60%
列表 24/8 宽度90%

六、性能优化策略

6.1 列表渲染优化

使用ListForEach组件实现高效列表渲染:

typescript 复制代码
List() {
  ForEach(this.expenses_1, (item_1: ExpenseItem_1, index_1: number) => {
    ListItem() {
      // 列表项内容
    }
  })
}
.height(250)  // 固定高度,启用虚拟滚动

优化要点:

  1. 虚拟滚动 :设置固定高度后,List组件启用虚拟滚动,只渲染可见项
  2. 唯一标识 :ForEach自动使用数组索引作为标识
  3. 按需更新:只有变化的列表项会重新渲染

6.2 状态更新优化

状态更新采用不可变数据模式:

typescript 复制代码
// 添加记录:创建新数组
this.expenses_1 = [newExpense_1].concat(this.expenses_1);

// 删除记录:过滤生成新数组
this.expenses_1 = this.expenses_1.filter((_, i_1: number) => i_1 !== index_1);

优势:

  • 避免直接修改数组,保证数据一致性
  • ArkUI框架可以准确检测变化,优化渲染
  • 符合函数式编程思想,代码更易维护

6.3 计算缓存

对于总支出计算,可以考虑缓存优化:

typescript 复制代码
// 当前实现:每次都重新计算
totalExpense_1(): number {
  let total_1: number = 0;
  for (let i_1 = 0; i_1 < this.expenses_1.length; i_1++) {
    total_1 += this.expenses_1[i_1].amount_1;
  }
  return total_1;
}

// 优化方案:使用缓存(扩展功能)
@State cachedTotal_1: number = 0;

updateTotal_1() {
  this.cachedTotal_1 = this.totalExpense_1();
}

七、扩展功能设计

7.1 数据持久化

当前应用数据存储在内存中,应用关闭后数据丢失。可以使用Preferences实现数据持久化:

typescript 复制代码
// 导入Preferences模块
import { preferences } from '@kit.ArkData';

// 保存数据
async saveExpenses_1() {
  let context = getContext(this);
  let prefs = await preferences.getPreferences(context, 'expense_data');
  await prefs.put('expenses', JSON.stringify(this.expenses_1));
  await prefs.flush();
}

// 加载数据
async loadExpenses_1() {
  let context = getContext(this);
  let prefs = await preferences.getPreferences(context, 'expense_data');
  let data = await prefs.get('expenses', '[]');
  this.expenses_1 = JSON.parse(data as string);
}

7.2 收入记录

扩展为支持收入记录:

typescript 复制代码
class ExpenseItem_1 {
  amount_1: number = 0;
  category_1: string = '';
  note_1: string = '';
  date_1: string = '';
  type_1: string = 'expense';  // 新增:expense或income
  
  constructor(amount_1: number, category_1: string, note_1: string, date_1: string, type_1: string = 'expense') {
    this.amount_1 = amount_1;
    this.category_1 = category_1;
    this.note_1 = note_1;
    this.date_1 = date_1;
    this.type_1 = type_1;
  }
}

// 计算总收入
totalIncome_1(): number {
  let total_1: number = 0;
  for (let i_1 = 0; i_1 < this.expenses_1.length; i_1++) {
    if (this.expenses_1[i_1].type_1 === 'income') {
      total_1 += this.expenses_1[i_1].amount_1;
    }
  }
  return total_1;
}

// 计算结余
balance_1(): number {
  return this.totalIncome_1() - this.totalExpense_1();
}

7.3 月度统计

添加按月统计功能:

typescript 复制代码
// 获取某月的支出
getMonthExpense_1(year: number, month: number): number {
  let total_1: number = 0;
  for (let i_1 = 0; i_1 < this.expenses_1.length; i_1++) {
    let item = this.expenses_1[i_1];
    // 解析日期,判断是否属于指定月份
    // 这里需要完善日期解析逻辑
    total_1 += item.amount_1;
  }
  return total_1;
}

7.4 图表展示

使用Canvas绘制饼图展示各分类占比:

typescript 复制代码
Canvas(this.context)
  .width(300)
  .height(300)
  .onReady(() => {
    this.drawPieChart();
  })

drawPieChart() {
  // 统计各分类金额
  let categoryTotals: Record<string, number> = {};
  for (let item of this.expenses_1) {
    if (!categoryTotals[item.category_1]) {
      categoryTotals[item.category_1] = 0;
    }
    categoryTotals[item.category_1] += item.amount_1;
  }
  
  // 绘制饼图
  // ...
}

八、用户体验优化

8.1 输入验证

添加输入验证,防止无效输入:

typescript 复制代码
TextInput({ placeholder: '0.00' })
  .width('80%')
  .height(48)
  .margin({ top: 8 })
  .type(InputType.Number)
  .onChange((value_1: string) => {
    let num = parseFloat(value_1);
    // 验证金额范围
    if (num < 0) {
      // 提示用户金额不能为负
      return;
    }
    if (num > 1000000) {
      // 提示用户金额过大
      return;
    }
    this.amount_1 = num || 0;
  })

8.2 操作反馈

添加操作成功提示:

typescript 复制代码
@State showToast_1: boolean = false;
@State toastMessage_1: string = '';

Button('记录')
  .onClick(() => {
    if (this.amount_1 > 0) {
      // 添加记录
      let newExpense_1 = new ExpenseItem_1(this.amount_1, this.category_1, this.note_1, '今天');
      this.expenses_1 = [newExpense_1].concat(this.expenses_1);
      this.amount_1 = 0;
      this.note_1 = '';
      
      // 显示成功提示
      this.toastMessage_1 = '记录成功';
      this.showToast_1 = true;
      setTimeout(() => {
        this.showToast_1 = false;
      }, 2000);
    } else {
      // 显示错误提示
      this.toastMessage_1 = '请输入有效金额';
      this.showToast_1 = true;
      setTimeout(() => {
        this.showToast_1 = false;
      }, 2000);
    }
  })

// Toast提示组件
if (this.showToast_1) {
  Text(this.toastMessage_1)
    .position({ x: '50%', y: '80%' })
    .translate({ x: '-50%' })
    .padding(12)
    .backgroundColor('#000000')
    .fontColor('#FFFFFF')
    .borderRadius(8)
}

8.3 空状态处理

当记录列表为空时显示提示:

typescript 复制代码
if (this.expenses_1.length === 0) {
  Column() {
    Text('暂无记录')
      .fontSize(16)
      .fontColor('#999999')
    Text('点击上方"记录"按钮添加支出')
      .fontSize(12)
      .fontColor('#CCCCCC')
      .margin({ top: 8 })
  }
  .margin({ top: 50 })
} else {
  List() {
    // 列表内容
  }
}

九、代码规范与最佳实践

9.1 命名规范

遵循ArkTS命名规范:

typescript 复制代码
// 组件名:大驼峰
struct ExpenseTrackerApp { }

// 类名:大驼峰
class ExpenseItem_1 { }

// 状态变量:小驼峰 + _1后缀(避免冲突)
@State amount_1: number = 0;

// 方法名:小驼峰 + _1后缀
totalExpense_1(): number { }

// 常量:全大写下划线
categories_1: string[] = ['餐饮', '购物', ...];

9.2 类型安全

严格使用类型声明:

typescript 复制代码
// 明确类型
@State amount_1: number = 0;
@State category_1: string = '餐饮';
@State expenses_1: ExpenseItem_1[] = [];

// 避免使用any
// 错误: let data: any = {};
// 正确: let data: Record<string, string> = {};

// 函数返回类型
totalExpense_1(): number {
  // ...
}

9.3 组件拆分

对于复杂页面,可以拆分为子组件:

typescript 复制代码
// 总支出展示组件
@Component
struct TotalExpenseCard {
  @Prop total_1: number;
  
  build() {
    Stack() {
      Circle()
        .width(120)
        .height(120)
        .fill('#E8F0FE')
      Column() {
        Text('总支出')
          .fontSize(14)
          .fontColor('#666666')
        Text('¥' + String(this.total_1))
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0A59F7')
          .margin({ top: 4 })
      }
    }
  }
}

// 使用
TotalExpenseCard({ total_1: this.totalExpense_1() })

十、测试与调试

10.1 单元测试

为关键方法编写单元测试:

typescript 复制代码
describe('ExpenseTrackerApp', () => {
  it('should calculate total expense correctly', () => {
    let app = new ExpenseTrackerApp();
    app.expenses_1 = [
      new ExpenseItem_1(100, '餐饮', '午餐', '今天'),
      new ExpenseItem_1(200, '购物', '衣服', '今天')
    ];
    expect(app.totalExpense_1()).toBe(300);
  });
  
  it('should add expense correctly', () => {
    let app = new ExpenseTrackerApp();
    app.amount_1 = 50;
    app.category_1 = '交通';
    app.note_1 = '打车';
    
    // 模拟点击记录按钮
    let initialCount = app.expenses_1.length;
    // ...执行添加逻辑
    expect(app.expenses_1.length).toBe(initialCount + 1);
  });
});

10.2 调试技巧

使用日志输出调试:

typescript 复制代码
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'ExpenseTrackerApp';

Button('记录')
  .onClick(() => {
    hilog.info(0x0000, TAG, 'Amount: %{public}d', this.amount_1);
    hilog.info(0x0000, TAG, 'Category: %{public}s', this.category_1);
    
    if (this.amount_1 > 0) {
      let newExpense_1 = new ExpenseItem_1(this.amount_1, this.category_1, this.note_1, '今天');
      this.expenses_1 = [newExpense_1].concat(this.expenses_1);
      
      hilog.info(0x0000, TAG, 'Expense added, total count: %{public}d', this.expenses_1.length);
    }
  })

10.3 性能分析

使用Profiler分析性能:

  1. CPU Profiler:分析方法执行时间
  2. Memory Profiler:检测内存泄漏
  3. Rendering Profiler:分析UI渲染性能

十一、总结与展望

11.1 项目总结

家庭记账本小应用完整展示了HarmonyOS ArkUI开发的核心技术:

技术点 应用场景
@State状态管理 响应式数据更新
List组件 列表数据展示
ForEach循环 动态组件生成
TextInput输入 用户数据录入
Button交互 用户操作触发
数据模型类 结构化数据管理

11.2 核心收获

通过本项目开发,可以掌握:

  1. 声明式UI开发:使用ArkTS构建UI界面
  2. 状态驱动更新:理解@State装饰器的工作原理
  3. 列表渲染优化:使用List和ForEach高效渲染列表
  4. 用户交互处理:处理按钮点击、文本输入等事件
  5. 数据管理:设计数据模型,实现增删改查操作

11.3 未来展望

后续可以扩展的功能方向:

  1. 数据持久化:使用Preferences或数据库存储数据
  2. 图表统计:使用Canvas绘制饼图、柱状图
  3. 分类管理:支持自定义分类,分类图标
  4. 预算管理:设置月度预算,超支提醒
  5. 数据导出:导出Excel或PDF报表
  6. 多设备同步:使用云服务同步数据
  7. 智能分析:AI分析消费习惯,提供理财建议

十二、完整代码

完整源代码请参考项目文件:

文件路径 : entry/src/main/ets/pages/miniApps/ExpenseTrackerApp.ets

代码行数: 约175行

主要组件:

  • ExpenseItem_1: 数据模型类
  • ExpenseTrackerApp: 主页面组件

关键方法:

  • totalExpense_1(): 计算总支出
  • build(): 构建UI界面

开发环境:

  • DevEco Studio 4.0+
  • HarmonyOS SDK API 10+
  • ArkTS 1.0+

参考文档:

相关推荐
至乐活着1 小时前
HarmonyOS开发深度解析:网络请求与数据持久化实战全攻略
网络请求·harmonyos·arkts·数据持久化·鸿蒙开发
星释1 小时前
鸿蒙智能体开发实战:3.创建工作流
华为·harmonyos·智能体
hahjee1 小时前
【鸿蒙 PC三方库构建系统】解决 OpenHarmony SHA 库编译问题:从动态链接错误到静态链接优化
华为·harmonyos
伶俜661 小时前
鸿蒙原生应用实战(二十)ArkUI 课程表 App:Grid 网格 + SQLite 存储 + 周次切换 + 上课提醒
华为·sqlite·harmonyos
Davina_yu2 小时前
画布Canvas:2D绘图上下文(Context2D)绘制复杂图表(33)
harmonyos·鸿蒙·鸿蒙系统
风华圆舞2 小时前
鸿蒙 Flutter 页面怎么感知防窥状态并调整 UI 可见性
flutter·ui·harmonyos
小雨下雨的雨2 小时前
HarmonyOS ArkUI训练营入门-组件掌握系列-Grid 网格布局深度解析-PC版本
学习·华为·harmonyos·鸿蒙·鸿蒙系统
Davina_yu12 小时前
定时器与任务调度:setTimeout与setInterval的正确使用(19)
harmonyos·鸿蒙·鸿蒙系统
祭曦念13 小时前
【共创季稿事节】鸿蒙原生ArkTS布局深度解析_GridRow_Row_Column混合栅格布局实战
华为·harmonyos