news 2026/7/29 2:19:22

HarmonyOS NEXT 企业级记账APP:首页布局与 SummaryCard 设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS NEXT 企业级记账APP:首页布局与 SummaryCard 设计

首页布局与 SummaryCard 设计

本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第05篇,对应 Git Tagv0.0.5。承接第 04 篇的底导航,本篇开发首页核心 UI:今日支出、今日收入、最近账单列表,并封装 SummaryCard 汇总卡片组件。

前言

首页是用户进入应用后的第一印象,需要在3 秒内告诉用户:今天花了多少、挣了多少、最近账单如何。HarmonyLedger 首页采用卡片化布局,用 SummaryCard 展示收支汇总,用 List 展示最近账单。本篇暂时用 Mock 数据填充,后续 v0.0.7 篇接入真实 Repository。

本文将带你:

  1. 设计首页整体布局(汇总卡 + 最近账单 + FAB)
  2. 封装 SummaryCard 通用汇总卡片组件
  3. 使用 List + ListItem 渲染最近账单列表
  4. 集成主题系统实现深色模式

企业级核心原则:首页必须一目了然、层次清晰、操作直达。参考 ArkUI List 组件 了解官方约定。


一、首页布局规划

1.1 页面结构树

HomeView(首页) ├─ TitleBar(标题栏:今日账单汇总) ├─ SummaryCard(今日支出卡) ├─ SummaryCard(今日收入卡) ├─ SectionTitle(最近账单标题) └─ List(最近账单列表) └ ListItem(账单项)

1.2 关键交互

操作行为
支出汇总卡点击跳转今日支出详情
收入汇总卡点击跳转今日收入详情
账单项点击跳转账单详情
账单项长按弹出删除菜单
FAB 按钮点击跳转新增账单页

二、SummaryCard 组件封装

2.1 组件设计目标

  1. 大数字突出:金额字号 36,加粗显示
  2. 语义色:收入绿、支出红、预算蓝
  3. 图标装饰:右上角小图标辅助识别
  4. 点击回调:通过@BuilderParam暴露点击事件
  5. 响应深色模式:通过 AppStorage 集成主题

2.2 组件实现

// components/card/SummaryCard.ets import { AppColors } from '../../theme/Colors'; import { AppFontSize, AppFontWeight } from '../../theme/Typography'; import { AppSpace } from '../../theme/Spacing'; @Component export struct SummaryCard { @Prop title: string = ''; // "今日支出" @Prop money: string = '0.00'; // "123.45" @Prop icon: Resource = $r('app.media.icon_expense'); // 图标 @Prop semanticColor: string = AppColors.Expense; // 主题色 @BuilderParam onClick: () => void; build() { Column() { Row() { Column() { Text(this.title) .fontSize(AppFontSize.SM) .fontColor(AppColors.SecondaryText) Text(`¥${this.money}`) .fontSize(AppFontSize.Display) .fontColor(this.semanticColor) .fontWeight(AppFontWeight.Bold) .margin({ top: AppSpace.XS }) } .alignItems(HorizontalAlign.Start) Image(this.icon) .width(28) .height(28) .fillColor(this.semanticColor) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .alignItems(HorizontalAlign.Center) } .padding(AppSpace.CardPadding) .backgroundColor(AppColors.CardBackground) .borderRadius(AppSpace.CardRadius) .onClick(() => { this.onClick(); }) } }

2.3 资源占用

资源用途
icon_expense.png支出汇总图标
icon_income.png收入汇总图标
icon_budget.png预算汇总图标

三、首页布局实现

3.1 HomeView 完整代码

// pages/HomeView.ets import { SummaryCard } from '../components/card/SummaryCard'; import { SectionTitle } from '../components/text/SectionTitle'; import { BillCard } from '../components/bill/BillCard'; import { AppColors } from '../theme/Colors'; import { AppSpace } from '../theme/Spacing'; import { HomeViewModel } from '../viewmodel/HomeViewModel'; @Entry @Component struct HomeView { @State viewModel: HomeViewModel = new HomeViewModel(); aboutToAppear() { this.viewModel.loadMockData(); } build() { Scroll() { Column() { // 标题 Text('今日账单汇总') .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ top: AppSpace.XL, bottom: AppSpace.MD }) // 收支汇总卡 Row() { SummaryCard({ title: '今日支出', money: this.viewModel.todayExpense, icon: $r('app.media.icon_expense'), semanticColor: AppColors.Expense, onClick: () => { this.viewModel.goExpenseDetail(); } }) SummaryCard({ title: '今日收入', money: this.viewModel.todayIncome, icon: $r('app.media.icon_income'), semanticColor: AppColors.Income, onClick: () => { this.viewModel.goIncomeDetail(); } }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) // 最近账单标题 SectionTitle({ title: '最近账单' }) .margin({ top: AppSpace.XL, bottom: AppSpace.SM }) // 最近账单列表 List({ space: AppSpace.SM }) { ForEach(this.viewModel.recentBills, (bill: Bill, index: number) => { ListItem() { BillCard({ money: bill.money, type: bill.type, category: bill.category, remark: bill.remark }) } }, (bill: Bill) => bill.id) } .width('100%') .layoutWeight(1) .padding({ bottom: 80 }) } .width('100%') .padding({ left: AppSpace.XL, right: AppSpace.XL }) } .width('100%') .height('100%') .scrollBar(BarState.Off) } }

3.2 FAB 快捷按钮

// 在 HomeView build 内追加 FAB Stack() { Scroll() { ... } // 上述内容 Image($r('app.media.icon_add')) .width(56) .height(56) .fillColor(AppColors.White) .backgroundColor(AppColors.Budget) .borderRadius(28) .shadow({ radius: 8, color: '#66000000', offsetY: 4 }) .position({ x: '85%', y: '85%'' }) .onClick(() => { this.viewModel.goAddBill(); }) }

四、HomeViewModel 雏创

4.1 ViewModel 设计

企业级 MVVM 要求页面只管 UI,业务逻辑放 ViewModel:

// viewmodel/HomeViewModel.ets import { Bill } from '../model/Bill'; import { MoneyUtil } from '../utils/MoneyUtil'; export class HomeViewModel { todayExpense: string = '0.00'; todayIncome: string = '0.00'; recentBills: Bill[] = []; /** 加载今日数据(本篇用 Mock) */ loadMockData(): void { const mockBills: Bill[] = [ new Bill('1', 3500, 'expense', 'food', '午餐', Date.now()), new Bill('2', 8000, 'income', 'salary', '今日工资', Date.now()), new Bill('3', 2500, 'expense', 'transport', '打车', Date.now()) ]; this.recentBills = mockBills; const expenseSum = mockBills.filter(b => b.type === 'expense').sum(b => b.money); const incomeSum = mockBills.filter(b => b.type === 'income').sum(b => b.money); this.todayExpense = MoneyUtil.format(expenseSum); this.todayIncome = MoneyUtil.format(incomeSum); } goExpenseDetail(): void { /* RouterUtil.go('/expense/today'); */ } goIncomeDetail(): void { /* RouterUtil.go('/income/today'); */ } goAddBill(): void { /* RouterUtil.go('/bill/add'); */ } }

4.2 Mock 数据 Bill

// model/Bill.ets(占位版) export class Bill { id: string; money: number; // 分 type: string; // 'expense' / 'income' category: string; // 分类名 remark: string; date: number; constructor(id: string, money: number, type: string, category: string, remark: string, date: number) { this.id = id; this.money = money; this.type = type; this.category = category; this.remark = remark; this.date = date; } }

4.3 MoneyUtil 工具类

// utils/MoneyUtil.ets export class MoneyUtil { /** 分 → 元字符串 */ static format(cents: number): string { return (cents / 100).toFixed(2); } /** 元 → 分 */ static toCents(yuan: number): number { return Math.round(yuan * 100); } }

设计要点:金额统一以为单位存储,避免浮点精度问题。这是金融类应用的基本规范。


五、SectionTitle 与 BillCard 占位

5.1 SectionTitle

// components/text/SectionTitle.ets @Component export struct SectionTitle { @Prop title: string = ''; build() { Text(this.title) .fontSize(18) .fontWeight(FontWeight.Bold) } }

5.2 BillCard 占位

// components/bill/BillCard.ets(占位,v0.0.6 篇完善) import { AppColors } from '../../theme/Colors'; import { AppFontSize } from '../../theme/Typography'; import { AppSpace } from '../../theme/Spacing'; import { MoneyUtil } from '../../utils/MoneyUtil'; @Component export struct BillCard { @Prop money: number = 0; @Prop type: string = 'expense'; @Prop category: string = ''; @Prop remark: string = ''; build() { Row() { Column() { Text(this.category) .fontSize(AppFontSize.MD) .fontColor(AppColors.PrimaryText) Text(this.remark) .fontSize(AppFontSize.SM) .fontColor(AppColors.SecondaryText) .margin({ top: 2 }) } .alignItems(HorizontalAlign.Start) Text(`¥${MoneyUtil.format(this.money)}`) .fontSize(AppFontSize.LG) .fontColor(this.type === 'income' ? AppColors.Income : AppColors.Expense) .fontWeight(FontWeight.Bold) } .width('100%') .padding(AppSpace.CardPadding) .backgroundColor(AppColors.CardBackground) .borderRadius(AppSpace.CardRadius) .justifyContent(FlexAlign.SpaceBetween) .alignItems(HorizontalAlign.Center) } }

六、最佳实践

6.1 MVVM 分层

View (HomeView) ↓ 持有 ViewModel (HomeViewModel) ↓ 调用 Repository (BillRepository) ← v0.0.7 接入 ↓ 操作 Model (Bill)

为什么必须分层?

  1. View 只管渲染,不写业务逻辑
  2. ViewModel 可单元测试
  3. Repository 屏蔽存储实现
  4. Model 集中数据定义

6.2 Mock 数据策略

// 开发期用 Mock,上线期换真实 Repository loadData(): void { if (DEBUG) { this.loadMockData(); // Mock 数据 } else { BillRepository.getTodayBills() // 真实数据 .then(bills => { ... }); } }

6.3 List 性能

配置说明
List({ space: 8 })Item 间距 8dp
.layoutWeight(1)占满剩余空间
ForEachkey必须用 bill.id,禁止用 index

踩坑点ForEachkey 用 index 会导致列表项状态错乱,必须用业务 id。


七、运行验证

7.1 编译检查

hvigorw assembleHap--modemodule-pproduct=default

7.2 视觉验证

  1. 首页顶部显示"今日账单汇总"标题
  2. 支出卡与收入卡并排,金额用对应语义色
  3. 最近账单列表显示 3 条 Mock 数据
  4. FAB 按钮浮在右下角,蓝色圆形
  5. 深色模式自动切换卡片背景色




八、常见问题

8.1 Scroll 嵌套 List 冲突

// 错误:Scroll 嵌套 List 会冲突 Scroll() { Column() { List() { ... } // ❌ List 无法滚动 } } // 解决:用 Column + List.layoutWeight(1) 替代 Column() { Column() { ... } // 顶部固定内容 List() { ... }.layoutWeight(1) // 自动占满剩余 }

8.2 ForEach key 缺失

// 错误:ForEach 缺 key,列表项刷新错乱 ForEach(this.bills, (bill) => { ListItem() { ... } }) // 正解:必须提供稳定 key ForEach(this.bills, (bill) => { ... }, (bill) => bill.id)

九、Git 提交

9.1 Commit Message

gitadd.gitcommit-m"feat(home): 完成首页布局与 SummaryCard 设计 - 新增 SummaryCard 通用汇总卡片组件 - 新增 SectionTitle、BillCard 占位组件 - 集成 HomeView 完整布局:标题 + 汇总卡 + 列表 + FAB - 创建 HomeViewModel 处理业务逻辑 - 创建 Mock Bill 数据模型与 MoneyUtil 工具类"

9.2 CHANGELOG

## [v0.0.5] - 2026-07-27 ### Added - components/card/SummaryCard.ets:汇总卡片 - components/text/SectionTitle.ets:区块标题 - components/bill/BillCard.ets:账单卡片占位 - pages/HomeView.ets:完整首页布局 - viewmodel/HomeViewModel.ets:首页业务逻辑 - model/Bill.ets:账单数据模型(占位版) - utils/MoneyUtil.ets:金额工具类

总结

本文完整介绍了首页布局与 SummaryCard 组件,包含页面结构、组件封装、ViewModel 抽离、Mock 数据策略。通过本篇你可以:

  • 设计首页一目了然的卡片化布局
  • 封装 SummaryCard 通用汇总组件
  • 使用 List + ForEach 渲染账单列表
  • 抽离 HomeViewModel 落地 MVVM 架构
  • 处理 Scroll 与 List 嵌套冲突

下一篇预告:《封装 BillCard 通用组件》将完善 BillCard 组件,支持分类图标、金额语义色、长按删除、滑动删除等交互。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源

  • 本篇源码:GitHub Tag v0.0.5
  • ArkUI List 组件:list
  • ArkUI Scroll 组件:scroll
  • ForEach 性能指南:foreach-performance
  • MVVM 架构实践:mvvm-practice
  • 鸿蒙状态管理:state-management
  • ArkUI Stack 布局:stack
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/29 2:19:19

Allure与Jenkins Pipeline集成:打造一体化测试报告与质量仪表盘

1. 项目概述:为什么我们需要一体化的测试报告在持续集成和持续交付的实践中,自动化测试是保障软件质量的基石。但很多时候,我们面临的困境是:测试跑完了,结果也“通过”了,可我们得到的只是一堆冰冷的日志文…

作者头像 李华
网站建设 2026/7/29 2:17:56

Unity3D动态场景节点管理:架构设计与性能优化实战

1. 项目概述:为什么我们需要动态管理场景节点?在Unity3D项目开发中,尤其是涉及大地图、开放世界、关卡编辑器或者需要运行时动态加载大量内容的游戏时,我们经常会遇到一个核心挑战:如何高效、有序地管理场景中成千上万…

作者头像 李华
网站建设 2026/7/29 2:14:29

Cura图片转3D模型全攻略:从原理到实践,轻松制作个性化浮雕

1. 项目概述:当3D打印遇上平面图片如果你手头有一台3D打印机,脑子里冒出一个想法:能不能把一张普通的JPG照片,比如一张风景照、一个Logo,甚至是一个签名,直接变成可以触摸的立体浮雕打印出来?答…

作者头像 李华
网站建设 2026/7/29 2:07:34

大模型AI-Agent 上下文管理

部分内容可能来自网络或者由AI生成。 如有雷同,纯属巧合,仅供学习参考之用。Agent 上下文管理:从窗口约束到工程化治理决定每一轮交互时,模型的上下文窗口里应该出现哪些信息、以什么结构出现、何时移除。它是介于"提示词工程…

作者头像 李华
网站建设 2026/7/29 2:04:06

Unity UGUI动态UI销毁报错:RectTransform已销毁的根源与解决方案

1. 项目概述:一个看似简单却暗藏玄机的UI报错在Unity UI开发中,尤其是使用UGUI的自动布局系统时,Vertical Layout Group(垂直布局组)和Horizontal Layout Group(水平布局组)是我们快速构建规整界…

作者头像 李华
网站建设 2026/7/29 2:01:36

工业物联网通信系统:LTE Cat 1与STM32硬件设计实践

1. 项目概述:构建工业级物联网通信系统在工业物联网应用中,稳定可靠的通信系统是确保数据实时传输和设备远程控制的关键。本项目采用u-blox LARA-R6401D-00B LTE Cat 1通信模块与STM32F100ZE微控制器组合,构建了一套完整的物联网通信解决方案…

作者头像 李华