HarmonyOS NEXT 企业级记账APP:账单编辑与删除
账单编辑与删除本文是《HarmonyOS NEXT 企业级开发实战30篇打造智能记账APP》系列的第14篇对应 Git Tagv0.1.4。承接第 13 篇的 PersistenceV2本篇开发 EditBillView 编辑页面支持长按列表项弹删除菜单、滑动删除手势、二次确认对话框。前言账单的编辑与删除是记账应用的基础功能。删除是不可逆的敏感操作必须有二次确认 滑动手势 长按菜单多重入口避免误触。本章开发 EditBillView 完整编辑页并封装 ConfirmDialog 删除确认对话框。本文将带你开发 EditBillView 完整编辑页面复用 AddBillView 组件实现长按列表项弹删除菜单实现左滑删除手势 红色删除按钮揭示封装 ConfirmDialog 二次确认对话框集成 BillRepository 完成删除与更新企业级核心原则删除操作必须二次确认、可撤销提示、手势入口。参考 ArkUI 手势文档 了解官方约定。一、EditBillView 页面规划1.1 页面结构树EditBillView编辑账单 ├─ TitleBar顶栏返回 标题编辑账单 删除按钮 ├─ TypeSwitch收入/支出切换 ├─ MoneyInput金额输入 ├─ CategorySelector分类选择 ├─ DateSelector TimeSelector日期时间 ├─ RemarkInput备注 └─ ActionBar取消 保存1.2 与 AddBillView 的区别项AddBillViewEditBillView入口FAB 新增列表项点击标题“新增账单”“编辑账单”顶栏仅返回返回 删除初始数据默认空路由参数传入 billId 加载保存createupdate删除无有红色按钮 二次确认二、EditBillViewModel 业务层// viewmodel/EditBillViewModel.ets import { Bill } from ../model/Bill; import { BillType } from ../constants/BillType; import { BillRepository } from ../repository/BillRepository; import { CategoryRepository } from ../repository/CategoryRepository; import { Category } from ../model/Category; import { MoneyUtil } from ../utils/MoneyUtil; import { ToastUtil } from ../utils/ToastUtil; import { UUIDUtil } from ../utils/UUIDUtil; export class EditBillViewModel { billId: string ; currentType: BillType BillType.EXPENSE; money: string ; selectedCategory: Category | null null; date: number Date.now(); remark: string ; availableCategories: Category[] []; private billRepo BillRepository.getInstance(); private categoryRepo CategoryRepository.getInstance(); private originalBill: Bill | null null; /** 加载账单数据 */ async loadBill(billId: string): Promisevoid { this.billId billId; const bill await this.billRepo.findById(billId); if (!bill) { ToastUtil.show(账单不存在); return; } this.originalBill bill; this.currentType bill.type; this.money MoneyUtil.format(bill.money); this.date bill.date; this.remark bill.remark; await this.loadCategories(); // 反查分类 this.selectedCategory this.availableCategories.find(c c.id bill.categoryId) ?? null; } async loadCategories(): Promisevoid { this.availableCategories await this.categoryRepo.findByType(this.currentType); } async switchType(type: BillType): Promisevoid { this.currentType type; this.selectedCategory null; await this.loadCategories(); } canSave(): boolean { if (this.money.length 0) return false; const cents MoneyUtil.parseToCents(this.money); if (cents 0) return false; if (!this.selectedCategory) return false; return true; } /** 更新账单 */ async save(): Promiseboolean { if (!this.canSave() || !this.originalBill) return false; const cents MoneyUtil.parseToCents(this.money); this.originalBill.money cents; this.originalBill.type this.currentType; this.originalBill.categoryId this.selectedCategory!.id; this.originalBill.remark this.remark; this.originalBill.date this.date; await this.billRepo.update(this.originalBill); ToastUtil.show(账单已更新); return true; } /** 删除账单 */ async delete(): Promiseboolean { if (!this.billId) return false; const ok await this.billRepo.delete(this.billId); if (ok) ToastUtil.show(账单已删除); return ok; } }三、EditBillView 页面实现// pages/EditBillView.ets import { EditBillViewModel } from ../viewmodel/EditBillViewModel; import { TypeSwitch } from ../components/form/TypeSwitch; import { MoneyInput } from ../components/form/MoneyInput; import { CategorySelector } from ../components/selector/CategorySelector; import { DateSelector } from ../components/selector/DateSelector; import { TimeSelector } from ../components/selector/TimeSelector; import { AppButton } from ../components/form/AppButton; import { ConfirmDialog } from ../components/dialog/ConfirmDialog; import { AppColors } from ../theme/Colors; import { AppFontSize } from ../theme/Typography; import { AppSpace } from ../../theme/Spacing; import { BillType } from ../constants/BillType; import { router } from kit.ArkUI; Entry Component struct EditBillView { State viewModel: EditBillViewModel new EditBillViewModel(); State isLoading: boolean true; State showDeleteConfirm: boolean false; aboutToAppear() { const params router.getParams() as Recordstring, string; this.loadData(params[id]); } private async loadData(billId: string): Promisevoid { this.isLoading true; await this.viewModel.loadBill(billId); this.isLoading false; } build() { Stack() { Column() { // 顶栏返回 标题 删除 Row() { Image($r(app.media.icon_back)) .width(24).height(24).fillColor(AppColors.PrimaryText) .onClick(() { router.back(); }) Text(编辑账单) .fontSize(AppFontSize.XL).fontWeight(FontWeight.Bold) .layoutWeight(1).textAlign(TextAlign.Center) Image($r(app.media.icon_delete)) .width(24).height(24).fillColor(AppColors.Expense) .onClick(() { this.showDeleteConfirm true; }) } .width(100%).height(56).alignItems(HorizontalAlign.Center) // ... 同 AddBillView 的 TypeSwitch / MoneyInput / CategorySelector / DateSelector / TimeSelector / Remark ... // 底栏取消 保存 Row({ space: AppSpace.MD }) { AppButton({ label: 取消, color: AppColors.SecondaryText, onClick: () { router.back(); } }) AppButton({ label: 保存, color: this.viewModel.currentType BillType.INCOME ? AppColors.Income : AppColors.Expense, enabled: this.viewModel.canSave(), onClick: async () { const ok await this.viewModel.save(); if (ok) router.back(); } }) } .margin({ top: AppSpace.XL }) } .height(100%) .padding({ left: AppSpace.XL, right: AppSpace.XL }) .backgroundColor(AppColors.Background) // 删除确认对话框 if (this.showDeleteConfirm) { ConfirmDialog({ title: 确认删除, message: 删除后无法恢复是否确认删除该账单, confirmText: 删除, confirmColor: AppColors.Expense, onConfirm: async () { const ok await this.viewModel.delete(); this.showDeleteConfirm false; if (ok) router.back(); }, onCancel: () { this.showDeleteConfirm false; } }) } } } }四、ConfirmDialog 二次确认对话框// components/dialog/ConfirmDialog.ets import { AppColors } from ../../theme/Colors; import { AppFontSize, AppFontWeight } from ../../theme/Typography; import { AppSpace } from ../../theme/Spacing; Component export struct ConfirmDialog { Prop title: string 确认; Prop message: string ; Prop confirmText: string 确认; Prop cancelText: string 取消; Prop confirmColor: string AppColors.Budget; BuilderParam onConfirm: () void; BuilderParam onCancel: () void; build() { Stack() { // 蒙层 Column() .width(100%).height(100%) .backgroundColor(#80000000) .onClick(() { this.onCancel(); }) // Dialog 卡片 Column() { Text(this.title) .fontSize(AppFontSize.LG).fontWeight(AppFontWeight.Bold) .margin({ bottom: AppSpace.MD }) Text(this.message) .fontSize(AppFontSize.MD).fontColor(AppColors.SecondaryText) .textAlign(TextAlign.Center) .margin({ bottom: AppSpace.LG }) Row({ space: AppSpace.MD }) { Text(this.cancelText) .layoutWeight(1) .textAlign(TextAlign.Center) .padding({ top: 12, bottom: 12 }) .backgroundColor(AppColors.Background) .borderRadius(24) .onClick(() { this.onCancel(); }) Text(this.confirmText) .layoutWeight(1) .textAlign(TextAlign.Center) .fontColor(#FFFFFF) .padding({ top: 12, bottom: 12 }) .backgroundColor(this.confirmColor) .borderRadius(24) .onClick(() { this.onConfirm(); }) } .width(100%) } .width(80%) .padding(AppSpace.LG) .backgroundColor(AppColors.CardBackground) .borderRadius(16) } .width(100%).height(100%) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } }五、首页列表项长按与滑动删除5.1 长按弹菜单// pages/HomeView.etsBillCard 部分升级 import { ConfirmDialog } from ../components/dialog/ConfirmDialog; State longPressedBillId: string ; build() { // ... ForEach(this.viewModel.recentBills, (bill: Bill) { ListItem() { BillCard({ billId: bill.id, money: bill.money, type: bill.type, categoryName: bill.categoryName, categoryIcon: bill.categoryIcon, remark: bill.remark, date: DateUtil.formatTime(bill.date), onClick: () { this.viewModel.goBillDetail(bill.id); }, onLongPress: () { this.longPressedBillId bill.id; } }) } .gesture( PanGesture({ distance: 80, direction: PanDirection.Horizontal }) .onActionStart(() { this.swipeBillId bill.id; }) .onActionUpdate((e: GestureEvent) { this.swipeOffset e.offset.x; }) .onActionEnd(() { this.handleSwipeEnd(bill.id); }) ) }, (bill: Bill) bill.id) // 长按菜单蒙层 if (this.longPressedBillId.length 0) { this.buildLongPressMenu() } } Builder buildLongPressMenu() { Stack() { Column().width(100%).height(100%).backgroundColor(#80000000) .onClick(() { this.longPressedBillId ; }) Column() { Text(编辑).padding(16).onClick(() { this.viewModel.goEditBill(this.longPressedBillId); this.longPressedBillId ; }) Divider().color(AppColors.Separator) Text(删除).padding(16).fontColor(AppColors.Expense).onClick(() { this.deleteBillId this.longPressedBillId; this.showDeleteConfirm true; this.longPressedBillId ; }) } .backgroundColor(AppColors.CardBackground).borderRadius(12).padding({ left: 16, right: 16 }) } }5.2 左滑删除手势State swipeBillId: string ; State swipeOffset: number 0; private handleSwipeEnd(billId: string): void { // 滑动距离 -80 触发删除确认 if (this.swipeOffset -80) { this.deleteBillId billId; this.showDeleteConfirm true; } this.swipeOffset 0; this.swipeBillId ; } // ListItem 内套用 Stack Stack() { // 底层红色删除按钮 Row() { Image($r(app.media.icon_delete)).width(20).height(20).fillColor(#FFFFFF) } .width(80).height(100%).backgroundColor(AppColors.Expense) .justifyContent(FlexAlign.Center) // 上层 BillCard偏移随滑动 BillCard({ ... }) .offset({ x: this.swipeBillId bill.id ? this.swipeOffset : 0 }) }六、路由更新与返回刷新6.1 路由配置// main_pages.json { src: [ pages/MainView, pages/HomeView, pages/StatisticsView, pages/BudgetView, pages/ProfileView, pages/AddBillView, pages/EditBillView, pages/BillDetailView // 新增 ] }6.2 返回首页刷新// pages/HomeView.ets onPageShow() { // 从编辑/新增返回时重新加载列表 this.loadData(); } private async loadData(): Promisevoid { this.isLoading true; await this.viewModel.loadData(); this.isLoading false; }关键技术onPageShow在页面显示时触发从子页面返回会重新拉数据保持列表新鲜。七、最佳实践7.1 删除二次确认// 删除是不可逆操作必须二次确认 if (!this.showDeleteConfirm) { // 首次点击不直接删先弹 ConfirmDialog this.showDeleteConfirm true; } // 用户在 ConfirmDialog 点确认后才真正调用 Repository.delete7.3 手势类型对比手势触发距离时长用途TapGesture无短按点击LongPressGesture无500ms长按菜单PanGesture80dp无滑动删除合理使用不同手势类型可以显著提升用户操作效率。7.1 删除操作对比操作触发方式确认机制反馈顶栏删除按钮点击图标ConfirmDialog 二次确认Toast 提示长按菜单删除LongPress 500ms弹菜单后选删除ConfirmDialog左滑删除PanGesture 80dp松开即触发 ConfirmDialog红色按钮揭示三种删除入口覆盖不同使用习惯但均经过二次确认保护。7.2 手势与点击冲突// ArkUI 自动处理短按触发 onClick长按触发 LongPress滑动触发 Pan // 距离阈值 distance: 80 避免误触 .gesture(PanGesture({ distance: 80, direction: PanDirection.Horizontal }))7.3 编辑页复用新增页组件AddBillView 与 EditBillView 共用 - TypeSwitch - MoneyInput - CategorySelector - DateSelector / TimeSelector - AppButton - ConfirmDialog仅 Edit 用 差异仅顶栏删除按钮 标题文字 初始数据加载八、运行验证8.1 编译检查hvigorw assembleHap--modemodule-pproductdefault8.2 功能验证点击首页账单项跳转 EditBillView预填数据修改金额、分类、备注后保存返回首页列表刷新顶栏点击删除按钮弹 ConfirmDialog确认后删除并返回长按列表项弹长按菜单含编辑/删除左滑列表项 80dp弹删除确认对话框九、常见问题9.1 路由参数丢失// EditBillView 通过 router.getParams() 获取 billId const params router.getParams() as Recordstring, string; const billId params[id]; // 必须与跳转时传入的 key 一致9.2 滑动距离判断不准// PanGesture distance 是触发阈值offset 是实际偏移 // 负值代表左滑需根据 direction 配置 direction: PanDirection.Horizontal // 仅水平滑动9.3 ConfirmDialog 蒙层穿透// 蒙层必须 onClick 关闭否则点击空白无反应 Column().backgroundColor(#80000000).onClick(() { this.onCancel(); })十、Git 提交gitadd.gitcommit-mfeat(bill): 开发账单编辑与删除功能 - 新增 EditBillView 完整编辑页面 - 新增 EditBillViewModel 处理加载/更新/删除 - 封装 ConfirmDialog 二次确认对话框 - 首页列表项支持长按弹菜单编辑/删除 - 首页列表项支持左滑 80dp 触发删除 - 集成 BillRepository.update 与 delete总结本文完整介绍了账单编辑与删除涵盖 EditBillView、EditBillViewModel、ConfirmDialog、长按菜单、滑动删除。通过本篇你可以复用 AddBillView 组件快速搭编辑页用 ConfirmDialog 实现删除二次确认用 LongPressGesture PanGesture 添加列表交互通过路由参数传递 billId 加载编辑数据从子页面返回时刷新首页列表下一篇预告《账单搜索与筛选》将开发 SearchView 搜索页支持关键字、分类、日期范围、金额范围多维筛选。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源本篇源码GitHub Tag v0.1.4ArkUI 手势文档gesturePanGesture APIpan-gestureLongPressGesturelong-press-gestureArkUI Dialog 系统dialog鸿蒙手势最佳实践gesture-best-practiceArkUI 路由跳转router

相关新闻

AI Agent创业:技术、场景与商业闭环的深度解析

AI Agent创业:技术、场景与商业闭环的深度解析

1. Agent创业的核心挑战解析最近两年AI Agent赛道突然火爆,各种创业项目如雨后春笋般涌现。但作为一个从2018年就开始接触对话式AI的老兵,我亲眼见证过太多Agent项目从风口跌落的过程。Agent创业看似门槛低——有现成的LLM API,搭个前端就能上…

2026/7/31 23:46:29 阅读更多 →
如何用AI轻松制作专业演示文稿:PPT Master完整指南

如何用AI轻松制作专业演示文稿:PPT Master完整指南

如何用AI轻松制作专业演示文稿:PPT Master完整指南 【免费下载链接】ppt-master AI turns documents or topics into real, native PowerPoint decks—with native shapes, transitions and animations, data-backed charts and tables on demand, audio narration …

2026/7/31 23:46:29 阅读更多 →
在Obsidian中创建电子表格:告别Markdown表格限制的终极方案

在Obsidian中创建电子表格:告别Markdown表格限制的终极方案

在Obsidian中创建电子表格:告别Markdown表格限制的终极方案 【免费下载链接】obsidian-excel 项目地址: https://gitcode.com/gh_mirrors/ob/obsidian-excel 还在为Obsidian中笨拙的Markdown表格而烦恼吗?当你需要处理复杂数据、制作财务报表或跟…

2026/7/31 23:46:29 阅读更多 →

最新新闻

边缘 AI 开发者 2026 下半年技能图谱:从模型训练到端侧部署的完整能力树构建

边缘 AI 开发者 2026 下半年技能图谱:从模型训练到端侧部署的完整能力树构建

边缘 AI 开发者 2026 下半年技能图谱:从模型训练到端侧部署的完整能力树构建 一、边缘 AI 开发者的能力光谱 边缘 AI 开发是一个跨学科领域——它与纯后端开发、纯嵌入式开发、纯算法研究都有交集,但又是这三个领域的并集而非交集。一个合格的边缘 AI …

2026/8/1 0:22:56 阅读更多 →
分布式架构实战总结:一年创业中踩过的坑与学到的经验

分布式架构实战总结:一年创业中踩过的坑与学到的经验

分布式架构实战总结:一年创业中踩过的坑与学到的经验 一、创业场景下分布式架构的特殊约束 大厂做分布式架构,资源充足,团队完备。创业公司做分布式架构,面临完全不同的约束。预算有限,人力稀缺,上线时间…

2026/8/1 0:21:56 阅读更多 →
人生结构化的SOP的庖丁解牛

人生结构化的SOP的庖丁解牛

人生结构化,本质是把一个混乱、复杂、充满变量的人生,转换成一个可以观察、分析、调整、优化的运行系统。很多人的痛苦来自: 不是问题太多。 而是:所有问题混在一起,没有结构。第一层:什么是人生结构化&…

2026/8/1 0:21:56 阅读更多 →
告别手动抢票的烦恼:DamaiHelper全能抢票助手深度指南

告别手动抢票的烦恼:DamaiHelper全能抢票助手深度指南

告别手动抢票的烦恼:DamaiHelper全能抢票助手深度指南 【免费下载链接】damaihelper 支持大麦网,淘票票、缤玩岛等多个平台,演唱会演出抢票脚本 项目地址: https://gitcode.com/gh_mirrors/dam/damaihelper 还在为抢不到心仪的演唱会门…

2026/8/1 0:21:56 阅读更多 →
5大核心功能解密:MZmine如何让质谱数据分析变得简单又专业

5大核心功能解密:MZmine如何让质谱数据分析变得简单又专业

5大核心功能解密:MZmine如何让质谱数据分析变得简单又专业 【免费下载链接】mzmine3 mzmine source code repository 项目地址: https://gitcode.com/gh_mirrors/mz/mzmine3 你是否曾经面对海量的质谱数据感到无从下手?是否厌倦了昂贵的商业软件许…

2026/8/1 0:20:56 阅读更多 →
如何用开源工具解锁Wand-Enhancer,实现WeMod功能增强与效率提升?

如何用开源工具解锁Wand-Enhancer,实现WeMod功能增强与效率提升?

如何用开源工具解锁Wand-Enhancer,实现WeMod功能增强与效率提升? 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer 你是否曾经…

2026/8/1 0:20:55 阅读更多 →

日新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/1 0:00:48 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/1 0:00:48 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/1 0:00:48 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/31 1:03:03 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/29 14:34:28 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/31 4:19:39 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/1 0:00:48 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/1 0:00:48 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/1 0:00:48 阅读更多 →