当前位置: 首页 > news >正文

《ArkUI 记账本开发:状态管理与数据持久化实现》

效果展示:

记账本应用功能分析笔记

1. 页面整合

  • BillAddPage.ets: 添加记账页面
  • BillIndexPage.ets: 首页账单展示
  • BillData.ets: 数据结构和枚举定义

2. 添加页-支出收入高亮切换

需求: 点击切换分类高亮效果,结合BillType枚举完成

实现思路:

  • @State ActiveBill: BillType存储当前选中的账单类型
  • 点击时修改枚举值,UI根据枚举值动态切换样式
  • 支出/收入切换时重置默认分类选择

(没有渲染的数据,也没有下标去选择,用枚举或者也可以自己创建一个类型去判断)

代码实现:

@State ActiveBill: BillType = BillType.Pay // 用枚举当做高亮的判断条件// 点击事件中修改枚举值
.onClick(() => {this.ActiveBill = BillType.Pay // 支出的高亮判断this.selectItem = { id: 1, icon: $r('app.media.food'), name: '餐费' }
})// 根据枚举值切换高亮
.backgroundColor(this.ActiveBill === BillType.Pay ? Color.Black : '#fff')
.fontColor(this.ActiveBill === BillType.Pay ? '#fff' : '#000')

3. 添加页-分类渲染

需求: 根据不同的账单类型,底部渲染不同的列表

实现思路:

  • 定义两个分类数组:payList(支出分类)和incomeList(收入分类)
  • 根据ActiveBill的值,用三元运算符选择渲染哪个分类列表
  • 使用ForEach遍历渲染分类标题和项目

代码实现:

// 支出和收入分类列表
@State payList: UseForCategory[] = payBillCategoryList
@State incomeList: UseForCategory[] = inComBillCategoryList// 根据当前选择的账单类型渲染对应分类
ForEach(this.ActiveBill === BillType.Pay ? this.payList : this.incomeList,(item: UseForCategory, index: number) => {// 渲染分类标题和项目})

4. 添加页-分类高亮切换

需求: 点击切换分类的高亮效果,参考BillItem格式完成

实现思路:

  • @State selectItem保存当前选中的分类项
  • 点击分类时,将选中的项赋值给selectItem
  • UI样式根据selectItem.id === ele.id判断是否高亮

代码实现:

@State selectItem: UseForItem = { id: 1, icon: $r('app.media.food'), name: '餐费' }// 根据保存的项和当前项id是否相等
.border({ width: 1, color: this.selectItem.id === ele.id ? '#5b8161' : Color.Transparent })
.backgroundColor(this.selectItem.id === ele.id ? '#dcf1e4' : Color.Transparent)// 点击事件中保存选中的项
.onClick(() => {this.selectItem = ele
})

5. 添加页-记账

需求: 数据持久化、添加数据、考虑金额为空、添加完毕提示、返回首页

实现思路:

  • @StorageLink绑定持久化数据moneyList
  • 保存时创建新的BillItem对象,支出用负数,收入用正数
  • 使用unshift添加到数组开头,实现最新记录在前
  • 保存后调用PathStack.pop()返回首页

代码实现:

// 用户输入的金额
@State inputMoney: string = ''// 获取仓库
@StorageLink('moneyList')
moneyList: BillItem[] = []// 保存按钮点击事件
.onClick(() => {this.PathStack.pop() // 返回到首页this.moneyList.unshift({id: Date.now(),type: this.ActiveBill,money: this.ActiveBill === BillType.Pay ? -Number(this.inputMoney) : Number(this.inputMoney),useFor: this.selectItem})
})

6. 首页-渲染列表

需求: 渲染列表

实现思路:

  • 使用List组件包裹,ForEach遍历moneyList数组
  • 每个ListItem渲染一个DailyBillSection组件
  • 传递item数据给子组件进行具体渲染

代码实现:

// 使用数组和forEach渲染
List({ space: 10 }) {ForEach(this.moneyList, (item: BillItem, index: number) => {ListItem() {DailyBillSection({ item: item })}})
}

7. 首页-删除数据

需求: 点击删除,删除对应数据,根据id删除

实现思路:

  • 使用swipeAction实现左滑删除
  • 删除函数用filter筛选出不等于指定id的项
  • 通过@Watch监听数据变化,自动重新计算统计

代码实现:

// 滑动删除功能
.swipeAction({end: this.delSection(item.id) // 用filter筛选出没有此id的,实现删除
})// 删除函数
delSection(id: number) {// 使用id筛选删除this.moneyList = this.moneyList.filter(item => item.id !== id)
}

8. 首页-统计

需求: 统计支出、收入、结余,添加、删除、页面打开时均需要计算一次

现思路:

  • 定义@State zhiChu@State shouRu存储统计结果
  • filter按类型分组,reduce计算总和
  • aboutToAppear、添加、删除时都调用changMoney()重新计算
  • 结余 = 收入 - 支出

代码实现:

// 定义对应的State
@State zhiChu: number = 0  // 支出
@State shouRu: number = 0  // 收入// 什么时候计算:添加时、删除时、清空时、Watch
@Watch('changMoney')
moneyList: BillItem[] = []// 计算函数
changMoney() {// 支出的总数this.zhiChu = this.moneyList.filter(item => item.type === BillType.Pay).reduce((sum: number, ele: BillItem) => sum + ele.money, 0)// 收入的总数this.shouRu = this.moneyList.filter(item => item.type === BillType.InCome).reduce((sum: number, ele: BillItem) => sum + ele.money, 0)
}// 一进页面就加载获取数据,调用函数
aboutToAppear(): void {this.changMoney()
}

核心数据结构

// 账单类型枚举
export enum BillType {Pay,      // 支出InCome    // 收入
}// 账单项接口
export interface BillItem {id: numbertype: BillTypemoney: numberuseFor: UseForItem
}// 分类项目接口
export interface UseForItem {id: numbericon: ResourceStrname: string
}

数据持久化

实现思路:

  • 使用PersistentStorage.persistProp初始化数据仓库
  • @StorageLink@StorageProp实现组件间的数据同步
  • 数据变更自动保存到本地存储
// 初始化仓库
PersistentStorage.persistProp('moneyList', [])// 使用@StorageLink和@StorageProp进行数据绑定
@StorageLink('moneyList')
@StorageProp('moneyList')

全部代码:

// 账单类型
export interface UseForCategory {title: stringitems: UseForItem[]
}// 账单项
export interface UseForItem {id: numbericon: ResourceStrname: string
}// 支付类型 枚举
export enum BillType {Pay,InCome
}// 订单
export interface BillItem {id: numbertype: BillTypemoney: numberuseFor: UseForItem
}// 支出的类型分类
export const payBillCategoryList: UseForCategory[] = [{title: '餐饮',items: [{id: 1, icon: $r('app.media.food'), name: '餐费'},{id: 2, icon: $r('app.media.drinks'), name: '酒水饮料'},{id: 3, icon: $r('app.media.dessert'), name: '甜品零食'},]},{title: '出行交通',items: [{id: 4, icon: $r('app.media.taxi'), name: '打车租车'},{id: 5, icon: $r('app.media.longdistance'), name: '旅行票费'},]},{title: '休闲娱乐',items: [{id: 6, icon: $r('app.media.bodybuilding'), name: '运动健身'},{id: 7, icon: $r('app.media.game'), name: '休闲玩乐'},{id: 8, icon: $r('app.media.audio'), name: '媒体影音'},{id: 9, icon: $r('app.media.travel'), name: '旅游度假'},],},{title: '日常支出',items: [{id: 10, icon: $r('app.media.clothes'), name: '衣服裤子'},{id: 11, icon: $r('app.media.bag'), name: '鞋帽包包'},{id: 12, icon: $r('app.media.book'), name: '知识学习'},{id: 13, icon: $r('app.media.promote'), name: '能力提升'},{id: 14, icon: $r('app.media.home'), name: '家装布置'},],},{title: '其他支出',items: [{ id: 15, icon: $r('app.media.community'), name: '社区缴费' }]}
]// 收入的类型分类
export const inComBillCategoryList: UseForCategory[] = [{title: '个人收入',items: [{ id: 16, icon: $r('app.media.salary'), name: '工资' },{ id: 17, icon: $r('app.media.overtimepay'), name: '加班' },{ id: 18, icon: $r('app.media.bonus'), name: '奖金' },],},{title: '其他收入',items: [{ id: 19, icon: $r('app.media.financial'), name: '理财收入' },{ id: 20, icon: $r('app.media.cashgift'), name: '礼金收入' },],},
]
import {BillItem,BillType,inComBillCategoryList,payBillCategoryList,UseForCategory,UseForItem
} from '../Bill/Data/BillData'@Builder
function AddPageBuilder() {NavDestination() {Bill_AddPage()}.title('记一笔').backButtonIcon($r('app.media.ic_public_arrow_left')).backgroundColor('#d7f1e2')
}@Entry
@Component
struct Bill_AddPage {// 存储点击的支出或收入做高亮效果@State ActiveBill: BillType = BillType.Pay //用枚举当做高亮的判断条件@Consume PathStack: NavPathStack// @Link ActiveBill: BillType//支出@State payList: UseForCategory[] = payBillCategoryList//收入@State incomeList: UseForCategory[] = inComBillCategoryList// 分类的高亮@State selectItem: UseForItem = { id: 1, icon: $r('app.media.food'), name: '餐费' }//用户输入的金额@State inputMoney: string = ''// 获取仓库@StorageLink('moneyList')moneyList: BillItem[] = []build() {Column() {// 切换订单类型switchBillTypeBuilder({ ActiveBill: this.ActiveBill, selectItem: this.selectItem })// 输入框区域Stack({ alignContent: Alignment.End }) {Row() {// 输入框TextInput({ placeholder: '0.00', text: $$this.inputMoney }).layoutWeight(1).textAlign(TextAlign.End).backgroundColor(Color.Transparent).fontColor(Color.Gray).type(InputType.NUMBER_DECIMAL)// 只能输入数值.fontSize(20).placeholderFont({ size: 20 }).placeholderColor('#d9d9d9').padding({ right: 20 })}.width('100%')Text('¥').fontSize(25)}.backgroundColor(Color.White).width('80%').margin(20).borderRadius(10).border({ width: 1, color: Color.Gray }).padding({ left: 10, right: 10 })// 订单Column({ space: 10 }) {ForEach(this.ActiveBill === BillType.Pay ? this.payList : this.incomeList,(item: UseForCategory, index: number) => {Column({ space: 20 }) {Text(item.title).alignSelf(ItemAlign.Start).fontColor(Color.Gray).fontSize(14)Row({ space: 10 }) {ForEach(item.items, (ele: UseForItem) => {IconCom({ ele: ele }).borderRadius(5)// 选中的高亮样式.border({ width: 1, color: this.selectItem.id === ele.id ? '#5b8161' : Color.Transparent }).backgroundColor(this.selectItem.id === ele.id ? '#dcf1e4' : Color.Transparent).onClick(() => {this.selectItem = ele})// IconCom()//   .borderRadius(5)// 默认的样式//   .border({ width: 1, color: Color.Transparent })//   .backgroundColor(Color.Transparent)})}.alignSelf(ItemAlign.Start)}.width('100%')})}.padding(15).width('100%').borderRadius(25).layoutWeight(1).backgroundColor(Color.White)Blank()Button('保 存').width('80%').type(ButtonType.Capsule).backgroundColor(Color.Transparent).fontColor('#5b8161').border({ width: 1, color: '#5b8161' }).margin(10).onClick(() => {this.PathStack.pop() //返回到首页// 添加数据this.moneyList.unshift({id: Date.now(),type: this.ActiveBill,money: this.ActiveBill === BillType.Pay ? -Number(this.inputMoney) : Number(this.inputMoney),useFor: this.selectItem})})}.height('100%')}
}@Component
struct switchBillTypeBuilder {@Link ActiveBill: BillType@Link selectItem: UseForItembuild() {// 切换订单类型Row({ space: 15 }) {// 选中时 文本白色,背景黑色Text('支出').tabTextExtend().backgroundColor(this.ActiveBill === BillType.Pay ? Color.Black : '#fff').fontColor(this.ActiveBill === BillType.Pay ? '#fff' : '#000').onClick(() => {this.ActiveBill = BillType.Pay //支出的高亮判断this.selectItem = { id: 1, icon: $r('app.media.food'), name: '餐费' }})Text('收入').tabTextExtend().backgroundColor(this.ActiveBill === BillType.InCome ? Color.Black : '#fff').fontColor(this.ActiveBill === BillType.InCome ? '#fff' : '#000').onClick(() => {this.ActiveBill = BillType.InCome //收入的高亮判断this.selectItem = { id: 16, icon: $r('app.media.salary'), name: '工资' }})}}
}@Component
struct IconCom {@Prop ele: UseForItembuild() {Column({ space: 5 }) {Image(this.ele.icon).width(20)Text(this.ele.name).fontSize(12).width(48).textAlign(TextAlign.Center)}.padding(5)}
}@Extend(Text)
function tabTextExtend() {.borderRadius(15).width(50).height(30).textAlign(TextAlign.Center).fontSize(14)
}
import { BillItem, BillType } from './Data/BillData'// 初始化仓库
PersistentStorage.persistProp('moneyList', [])@Entry
@Component
struct Billd_IndexPage {@Provide PathStack: NavPathStack = new NavPathStack()//取出数据@StorageLink('moneyList')@Watch('changMoney')moneyList: BillItem[] = []// 支出的总数@State zhiChu:number=0// 收入的总数@State shouRu:number=0// 一进页面就加载获取数据,调用函数aboutToAppear(): void {this.changMoney()}changMoney(){// 支出的总数this.zhiChu=this.moneyList.filter(item=>item.type===BillType.Pay).reduce((sum:number,ele:BillItem)=>sum+ele.money,0)//   收入的总数this.shouRu=this.moneyList.filter(item=>item.type===BillType.InCome).reduce((sum:number,ele:BillItem)=>sum+ele.money,0)}build() {Navigation(this.PathStack) {Stack({ alignContent: Alignment.BottomEnd }) {Column({ space: 10 }) {// 顶部区域Column({ space: 30 }) {Text('账单合计').fontSize(25).width('100%')Row() {BillInfo({billName: '支出',billNum: this.zhiChu.toFixed(2).toString()})BillInfo({billName: '收入',billNum: this.shouRu.toFixed(2).toString()})BillInfo({billName: '结余',billNum: `${(this.shouRu-this.zhiChu).toFixed(2)}`})}}.width('100%').height(140).backgroundImage($r('app.media.bill_title_bg')).backgroundImageSize({ width: '100%', height: '100%' }).padding(20)// 账单区域List({ space: 10 }) {ForEach(this.moneyList, (item: BillItem,index:number) => {ListItem() {DailyBillSection({ item: item })}.swipeAction({// end: this.delSection(index)//用下标删除end: this.delSection(item.id)//用filter筛选出没有此id的,实现删除})})}.width('100%').layoutWeight(1).scrollBar(BarState.Off)}.padding(10).width('100%')// 添加按钮AddButton()}.height('100%').backgroundColor('#f6f6f6')}}// 删除标签@Builder// delSection(index:number) {delSection(id:number) {Image($r('app.media.ic_public_delete_filled')).fillColor('#ec6073').width(30).margin(5).onClick(()=>{//this.moneyList= this.moneyList.splice(index,1)//使用下标删除this.moneyList=this.moneyList.filter(item=>item.id!==id)//使用id筛选删除})}
}@Component
struct BillInfo {@Prop billName: string = ''@Prop billNum: string = ''build() {Column({ space: 10 }) {Text(this.billNum).fontSize(20)Text(this.billName).fontSize(12)}.layoutWeight(1)// .alignItems(HorizontalAlign.Start)}
}@Component
struct DailyBillSection {@Prop item: BillItembuild() {// 分割线Row({ space: 10 }) {Image(this.item.useFor.icon).width(20)Text('餐费')Blank()Text(this.item.money.toString()).fontColor(this.item.money > 0 ? '#000' : '#ff8c7b')// 根据是否为支付调整颜色// 支付:#ff8c7b// 收入:Color.Black}.width('100%').borderRadius(10).padding(15).backgroundColor(Color.White)}
}@Component
struct AddButton {@Consume PathStack: NavPathStackbuild() {Image($r('app.media.ic_public_add_filled')).width(40).fillColor('#8e939d').padding(5).borderRadius(20).border({ width: 1, color: '#8e939d' }).translate({ x: -20, y: -20 }).backgroundColor('#f6f6f6').onClick(() => {//  这是router的方法!!// this.PathStack.pushPath({//   url:'pages/Bill/Data/BillAddPage'// })//  this.PathStack.pushPathByName('Bill_AddPage',null)this.PathStack.pushPath({ name: 'Bill_AddPage', param: '' })})}
}

http://www.dtcms.com/a/357812.html

相关文章:

  • 分布式锁在支付关闭订单场景下的思考
  • Product Hunt 每日热榜 | 2025-08-29
  • 逻辑漏洞 跨站脚本漏洞(xss)
  • 早期人类奴役AI实录:用Comate Zulu 10min做一款Chrome插件
  • nacos登录认证
  • 【算法】15. 三数之和
  • 学习做动画7.跳跃
  • UCIE Specification详解(十)
  • 快速深入理解zookeeper特性及核心基本原理
  • 【拍摄学习记录】06-构图、取景
  • Docker03-知识点整理
  • TypeScript:map和set函数
  • 2025 DDC系统选型白皮书:构建高效低碳智慧楼宇的核心指南
  • 【python开发123】三维地球应用开发方案
  • python 解码 视频解码
  • 打工人日报#20250829
  • 人工智能-python-深度学习-批量标准化与模型保存加载详解
  • OpenTenBase 技术解读与实战体验:从架构到行业落地
  • 2024年06月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • c++标准模板库
  • 轨道交通场景下设备状态监测与智能润滑预测性维护探索
  • 动态环境下的人员感知具身导航!HA-VLN:具备动态多人互动的视觉语言导航基准与排行榜
  • Free Subtitles-免费AI在线字幕生成工具,支持111种语言
  • 【ChatMemory聊天记忆】
  • STM32F4系列单片机如何修改主频
  • 从世界人形机器人大会看人形机器人如何实现复杂人类动作的精准复现?
  • 【论文简读】MuGS
  • 【拍摄学习记录】05-对焦、虚化、景深
  • 2025年06月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • Golang 面试题「中级」