Harmony鸿蒙实战应用4:随手账本——编辑与删除账单

引言

有新增就要有编辑和删除。本篇实现:点击账单卡片进入编辑页、修改后保存、删除带二次确认。


一、编辑功能

1.1 编辑页面

编辑页与新增页共用布局,区别是编辑页预填已有数据

typescript 复制代码
// pages/EditBillPage.ets
import { BillItem, BillType, BillCategory } from '../model/BillItem';
import { billStore } from '../store/BillStore';
import router from '@ohos.router';

@Entry
@Component
struct EditBillPage {
  @State billId: string = '';
  @State billType: BillType = BillType.EXPENSE;
  @State amount: string = '';
  @State selectedCategory: BillCategory = BillCategory.FOOD;
  @State categories: BillCategory[] = ['餐饮', '交通', '购物', '娱乐', '住房', '其他'] as BillCategory[];
  @State note: string = '';
  @State billDate: string = '';

  aboutToAppear() {
    // 从路由参数获取账单ID
    const params = router.getParams() as Record<string, string>;
    const id = params['id'];
    
    // 查找账单并预填
    const bill = billStore.getById(id);
    if (bill) {
      this.billId = bill.id;
      this.billType = bill.type;
      this.amount = bill.amount.toString();
      this.selectedCategory = bill.category;
      this.note = bill.note;
      this.billDate = bill.date;
      this.loadCategories();
    }
  }

  private saveEdit() {
    const amountNum = parseFloat(this.amount);
    if (isNaN(amountNum) || amountNum <= 0) {
      AlertDialog.show({ message: '请输入有效金额' });
      return;
    }
    
    billStore.update(this.billId, {
      type: this.billType,
      category: this.selectedCategory,
      amount: amountNum,
      note: this.note,
      date: this.billDate
    });
    
    router.back();
  }

  // ... 其余 UI 与新增页相同
}

编辑页实际效果------路由传参后自动预填数据(金额/分类/备注/日期),底部提供保存与删除按钮:

1.2 路由传参

从首页点击卡片跳转到编辑页:

typescript 复制代码
// 在 BillCard 组件中添加点击事件
@Builder
BillCard({ bill }: { bill: BillItem }) {
  Row() {
    // ...卡片内容
  }
  .onClick(() => {
    router.pushUrl({
      url: 'pages/EditBillPage',
      params: { id: bill.id }
    });
  })
}

1.3 BillStore 添加 getById 方法

typescript 复制代码
// store/BillStore.ts 追加
getById(id: string): BillItem | undefined {
  return this.bills.find(b => b.id === id);
}

二、删除功能

2.1 删除按钮 + 二次确认

#mermaid-svg-0qo2yBoWyPp4sa0R{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-0qo2yBoWyPp4sa0R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0qo2yBoWyPp4sa0R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0qo2yBoWyPp4sa0R .error-icon{fill:#552222;}#mermaid-svg-0qo2yBoWyPp4sa0R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0qo2yBoWyPp4sa0R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0qo2yBoWyPp4sa0R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0qo2yBoWyPp4sa0R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0qo2yBoWyPp4sa0R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0qo2yBoWyPp4sa0R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0qo2yBoWyPp4sa0R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0qo2yBoWyPp4sa0R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0qo2yBoWyPp4sa0R .marker.cross{stroke:#333333;}#mermaid-svg-0qo2yBoWyPp4sa0R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0qo2yBoWyPp4sa0R p{margin:0;}#mermaid-svg-0qo2yBoWyPp4sa0R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0qo2yBoWyPp4sa0R .cluster-label text{fill:#333;}#mermaid-svg-0qo2yBoWyPp4sa0R .cluster-label span{color:#333;}#mermaid-svg-0qo2yBoWyPp4sa0R .cluster-label span p{background-color:transparent;}#mermaid-svg-0qo2yBoWyPp4sa0R .label text,#mermaid-svg-0qo2yBoWyPp4sa0R span{fill:#333;color:#333;}#mermaid-svg-0qo2yBoWyPp4sa0R .node rect,#mermaid-svg-0qo2yBoWyPp4sa0R .node circle,#mermaid-svg-0qo2yBoWyPp4sa0R .node ellipse,#mermaid-svg-0qo2yBoWyPp4sa0R .node polygon,#mermaid-svg-0qo2yBoWyPp4sa0R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0qo2yBoWyPp4sa0R .rough-node .label text,#mermaid-svg-0qo2yBoWyPp4sa0R .node .label text,#mermaid-svg-0qo2yBoWyPp4sa0R .image-shape .label,#mermaid-svg-0qo2yBoWyPp4sa0R .icon-shape .label{text-anchor:middle;}#mermaid-svg-0qo2yBoWyPp4sa0R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0qo2yBoWyPp4sa0R .rough-node .label,#mermaid-svg-0qo2yBoWyPp4sa0R .node .label,#mermaid-svg-0qo2yBoWyPp4sa0R .image-shape .label,#mermaid-svg-0qo2yBoWyPp4sa0R .icon-shape .label{text-align:center;}#mermaid-svg-0qo2yBoWyPp4sa0R .node.clickable{cursor:pointer;}#mermaid-svg-0qo2yBoWyPp4sa0R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0qo2yBoWyPp4sa0R .arrowheadPath{fill:#333333;}#mermaid-svg-0qo2yBoWyPp4sa0R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0qo2yBoWyPp4sa0R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0qo2yBoWyPp4sa0R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0qo2yBoWyPp4sa0R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0qo2yBoWyPp4sa0R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0qo2yBoWyPp4sa0R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0qo2yBoWyPp4sa0R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0qo2yBoWyPp4sa0R .cluster text{fill:#333;}#mermaid-svg-0qo2yBoWyPp4sa0R .cluster span{color:#333;}#mermaid-svg-0qo2yBoWyPp4sa0R 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-0qo2yBoWyPp4sa0R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0qo2yBoWyPp4sa0R rect.text{fill:none;stroke-width:0;}#mermaid-svg-0qo2yBoWyPp4sa0R .icon-shape,#mermaid-svg-0qo2yBoWyPp4sa0R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0qo2yBoWyPp4sa0R .icon-shape p,#mermaid-svg-0qo2yBoWyPp4sa0R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0qo2yBoWyPp4sa0R .icon-shape .label rect,#mermaid-svg-0qo2yBoWyPp4sa0R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0qo2yBoWyPp4sa0R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0qo2yBoWyPp4sa0R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0qo2yBoWyPp4sa0R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 确认
取消
点击删除按钮
弹出确认框
执行删除
关闭弹窗
列表自动刷新

typescript 复制代码
// 在编辑页底部添加删除按钮
@Builder
DeleteButton() {
  Button('删除此账单')
    .width('90%')
    .height(48)
    .backgroundColor(Color.White)
    .fontColor('#FF4444')
    .borderColor('#FF4444')
    .borderWidth(1)
    .borderRadius(24)
    .margin({ top: 16, bottom: 40 })
    .onClick(() => {
      this.showDeleteConfirm();
    })
}

private showDeleteConfirm() {
  AlertDialog.show({
    title: '确认删除',
    message: '删除后不可恢复,确定要删除此账单吗?',
    primaryButton: {
      value: '取消',
      action: () => {}
    },
    secondaryButton: {
      value: '确认删除',
      fontColor: '#FF4444',
      action: () => {
        billStore.delete(this.billId);
        router.back();
      }
    },
    cancel: () => {}
  });
}

点击"删除此账单"弹出二次确认对话框,防止误操作:

2.2 滑动删除(列表页直接删除)

typescript 复制代码
// 在首页列表中使用 SwipeAction 实现左滑删除
import { SwipeAction } from '@kit.ArkUI';

@Builder
SwipeableBillCard(item: BillItem) {
  SwipeAction({
    end: {
      builder: this.DeleteAction(item),
      offset: 80
    }
  }) {
    BillCard({ bill: item })
  }
}

@Builder
DeleteAction(item: BillItem) {
  Column() {
    Button('删除')
      .width(80)
      .height('100%')
      .backgroundColor('#FF4444')
      .fontColor(Color.White)
      .borderRadius(12)
      .margin({ left: 8 })
      .onClick(() => {
        AlertDialog.show({
          message: `删除「${item.note || item.category}」¥${item.amount}?`,
          primaryButton: {
            value: '取消',
            action: () => {}
          },
          secondaryButton: {
            value: '删除',
            fontColor: '#FF4444',
            action: () => {
              billStore.delete(item.id);
              this.monthBills = billStore.getAll()
                .filter(b => b.date.startsWith(this.currentMonth));
            }
          }
        });
      })
  }
  .width(80)
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

三、撤销删除

删除后提供短暂撤销(类似 Gmail 的设计):

typescript 复制代码
private deleteWithUndo(id: string) {
  // 先获取备份
  const bill = billStore.getById(id);
  if (!bill) return;
  
  // 执行删除
  billStore.delete(id);
  this.refreshList();
  
  // 显示撤销提示
  AlertDialog.show({
    title: '已删除',
    message: `「${bill.note || bill.category}」¥${bill.amount}`,
    primaryButton: {
      value: '撤销',
      action: () => {
        billStore.addBack(bill); // 恢复到删除前的状态
        this.refreshList();
      }
    },
    secondaryButton: {
      value: '确定',
      action: () => {}
    },
    cancel: () => {}
  });
}

需要在 BillStore 中添加恢复方法:

typescript 复制代码
addBack(bill: BillItem): void {
  // 恢复到原来位置
  const idx = this.bills.findIndex(b => b.createTime < bill.createTime);
  if (idx === -1) {
    this.bills.push(bill);
  } else {
    this.bills.splice(idx, 0, bill);
  }
}

四、数据一致性保障

编辑或删除后,返回首页时数据需要刷新:

typescript 复制代码
// HomePage.ets - 重新显示时刷新
aboutToAppear() {
  this.refreshData();
}

// 或者页面获取焦点时刷新
onPageShow() {
  this.refreshData();
}

private refreshData() {
  this.monthBills = billStore.getAll()
    .filter(b => b.date.startsWith(this.currentMonth));
  this.calculateTotals();
}

删除后的首页------返回时列表与汇总即时刷新(删除"娱乐 ¥88"和"其他 ¥89"后,支出从 ¥560 降为 ¥346):


总结

本篇实现了:

  1. 编辑:复用新增页UI,路由传参预填数据
  2. 删除:二次确认防误操作
  3. 滑动删除:列表页直接左滑
  4. 撤销删除:删除后提供短暂恢复
  5. 数据刷新:页面返回时自动同步

下篇实现搜索筛选与分类统计功能。

相关推荐
独守一片天15 分钟前
HarmonyOS|鸿蒙新生态服务卡片设计与状态同步
华为·harmonyos
黑臂麒麟20 分钟前
HarmonyOS 7 碰一碰实战:手机轻触电脑,精准插入图片到指定位置
华为·智能手机·arkts·鸿蒙
黑臂麒麟39 分钟前
Harmony鸿蒙实战应用5:随手账本——搜索筛选与分类统计
华为·app·arkts·鸿蒙
2501_9197490343 分钟前
华为鸿蒙免费自拍对比APP—小羊自拍
华为·harmonyos·鸿蒙
玖石书2 小时前
WSL2 手动安装 Ubuntu 24.04 到指定磁盘位置
linux·运维·ubuntu·wsl
大锅盖13 小时前
围绕夜航深蓝与霓虹青的跨境画卷构建 HarmonyOS ArkUI 出境旅行向导平台
华为·harmonyos
骐骥112 小时前
学习:使用postMessage()建立应用侧与前端页面的数据通道
harmonyos·鸿蒙·jsbridge·postmessage·数据通道
冰雪青松13 小时前
Ubuntu 运维命令大全:从入门到精通的实战参考手册
运维·服务器·ubuntu
HwJack2018 小时前
鸿蒙开发ArkData 全景与选型决策:三种存储到底用哪个
华为·harmonyos