鸿蒙应用开发实战【54】— 应用详情页AppDetailPage开发(下)

鸿蒙应用开发实战【54】--- 应用详情页AppDetailPage开发(下)

本文是「号码助手全栈开发系列」第 54 篇,持续更新中...

开源社区:https://openharmonycrossplatform.csdn.net


前言

上篇完成了 AppDetailPage 的参数获取与数据加载。本篇聚焦编辑交互与保存------包括绑定卡号切换、分类选择、状态分段控件、备注编辑、保存修改、二次确认删除。

本篇涵盖:FormRow @Builder 复用、状态分段控件(Segmented)、ActionMenu 选择卡号/分类、保存逻辑、deleteById 删除、AlertDialog 二次确认。


一、页面布局

复制代码
┌──────────────────────┐
│  ‹     应用详情     保存 │
├──────────────────────┤
│                      │
│      ┌────┐          │
│      │ W  │ 微信      │  ← 大 Avatar
│      └────┘          │
│                      │
│  ┌──────────────────┐│
│  │ 绑定卡号  卡1·138›││  ← 可编辑
│  │──────────────────││
│  │ 分类       APP  ›││
│  └──────────────────┘│
│                      │
│  当前状态              │
│  [使用中][待换绑][待注销]│ ← 分段控件
│                      │
│  ┌──────────────────┐│
│  │ [备注文字]        ││
│  │──────────────────││
│  │ 创建时间 2026-01-15││
│  │ 更新时间 2026-03-20││
│  └──────────────────┘│
│                      │
│  [    保存修改      ]│
│                      │
│    删除该应用          │
└──────────────────────┘

二、FormRow 通用 Builder

2.1 组件定义

typescript 复制代码
@Builder
private FormRow(label: string, value: string, onTap: () => void) {
  Row() {
    Text(label).fontSize(AppFonts.SIZE_BODY).fontColor(AppColors.TEXT)
    Blank()
    Text(value).fontSize(AppFonts.SIZE_BODY).fontWeight(AppFonts.WEIGHT_MEDIUM)
      .fontColor(AppColors.TEXT_2)
    Text('›').fontSize(Size.font14).fontColor(AppColors.TEXT_3).margin({ left: 4 })
  }
  .width('100%').height(Size.s44)
  .onClick(() => onTap())
}

2.2 使用

typescript 复制代码
this.FormRow(
  '绑定卡号',
  `${this.cards[this.selectedCardIndex]?.label ?? ''} · ${this.formatPhone(...)}`,
  () => this.pickCard()
)
this.FormRow('分类', CATEGORIES[this.selectedCategoryIndex], () => this.pickCategory())

三、ActionMenu 选择器

3.1 选择卡号

typescript 复制代码
private async pickCard(): Promise<void> {
  if (this.cards.length === 0) return
  const buttons = this.cards.map(c => ({
    text: `${c.label} · ${c.phone_number}`,
    color: AppColors.TEXT
  }))
  const result = await promptAction.showActionMenu({
    title: '选择绑定卡号',
    buttons: buttons as [MenuBtn, MenuBtn?, MenuBtn?, MenuBtn?, MenuBtn?, MenuBtn?]
  })
  this.selectedCardIndex = result.index
}

3.2 选择分类

typescript 复制代码
private async pickCategory(): Promise<void> {
  const buttons = CATEGORIES.map(c => ({ text: c, color: AppColors.TEXT }))
  const result = await promptAction.showActionMenu({
    title: '选择分类',
    buttons: buttons as [MenuBtn, MenuBtn?, MenuBtn?, MenuBtn?, MenuBtn?, MenuBtn?]
  })
  this.selectedCategoryIndex = result.index
}

四、状态分段控件

4.1 UI 实现

typescript 复制代码
Column() {
  Text('当前状态')
    .fontSize(AppFonts.SIZE_SUB).fontColor(AppColors.TEXT_2)
    .alignSelf(ItemAlign.Start).margin({ bottom: 8 })

  Row() {
    ForEach(STATUS_OPTIONS, (s: string, index: number) => {
      Text(s)
        .fontSize(AppFonts.SIZE_SUB).fontWeight(AppFonts.WEIGHT_MEDIUM)
        .fontColor(this.selectedStatusIndex === index ? '#FFFFFF' : AppColors.TEXT_2)
        .backgroundColor(this.selectedStatusIndex === index ? AppColors.PRIMARY : Color.Transparent)
        .layoutWeight(1).textAlign(TextAlign.Center)
        .padding({ top: 8, bottom: 8 }).borderRadius(Size.s10)
        .onClick(() => this.selectedStatusIndex = index)
    })
  }
  .width('100%')
  .backgroundColor(AppColors.MUTED_BG)
  .borderRadius(Size.s12)
  .padding(Size.s3)
}

4.2 状态枚举

typescript 复制代码
const STATUS_OPTIONS: BindingStatus[] = ['使用中', '待换绑', '待注销', '已停用']

4 个状态横向均分,容器使用 MUTED_BG 灰色背景,选中项使用 PRIMARY 蓝色填充。


五、时间信息

typescript 复制代码
Row() {
  Text('创建时间').fontSize(AppFonts.SIZE_BODY).fontColor(AppColors.TEXT)
  Blank()
  Text(formatDate(this.binding.created_at)).fontSize(AppFonts.SIZE_BODY).fontColor(AppColors.TEXT_2)
}
Row() {
  Text('更新时间').fontSize(AppFonts.SIZE_BODY).fontColor(AppColors.TEXT)
  Blank()
  Text(formatDate(this.binding.updated_at)).fontSize(AppFonts.SIZE_BODY).fontColor(AppColors.TEXT_2)
}

六、保存修改

typescript 复制代码
private async onSave(): Promise<void> {
  if (!this.binding || this.saving) return
  this.saving = true
  try {
    const now = Date.now()
    const card = this.cards[this.selectedCardIndex]
    await AppBindingDao.update({
      id: this.binding.id,
      app_name: this.binding.app_name,
      icon_key: this.binding.icon_key,
      category: CATEGORIES[this.selectedCategoryIndex],
      card_id: card?.id ?? this.binding.card_id,
      status: STATUS_OPTIONS[this.selectedStatusIndex],
      remark: this.remark.trim(),
      source: this.binding.source,
      created_at: this.binding.created_at,
      updated_at: now
    })
    promptAction.showToast({ message: '已保存' })
  } catch (e) {
    promptAction.showToast({ message: '保存失败,请重试' })
  } finally {
    this.saving = false
  }
}

注意 card_id 的兜底:card?.id ?? this.binding.card_id,防止 cards 数组为空时出错。


七、删除应用

7.1 二次确认

typescript 复制代码
private onDeleteTap(): void {
  this.getUIContext().showAlertDialog({
    title: '删除该应用',
    message: '删除后无法恢复,确定要删除该应用绑定吗?',
    buttons: [
      { value: '取消', action: () => {} },
      { value: '确定删除', action: () => this.doDelete() }
    ]
  })
}

7.2 执行删除

typescript 复制代码
private async doDelete(): Promise<void> {
  if (!this.binding || this.binding.id === undefined) return
  try {
    await AppBindingDao.deleteById(this.binding.id)
    promptAction.showToast({ message: '已删除' })
    this.getUIContext().getRouter().back()
  } catch (e) {
    promptAction.showToast({ message: '删除失败,请重试' })
  }
}

删除后自动 back() 返回上一页,首页的 onPageShow 会自动刷新。


操作 交互组件 数据方法 反馈方式
切换绑定卡号 ActionMenu selectedCardIndex 更新 选择后自动关闭
切换分类 ActionMenu selectedCategoryIndex 更新 选择后自动关闭
切换状态 分段控件 selectedStatusIndex 更新 即时切换
编辑备注 TextArea remark 绑定 onChange 即时更新
保存修改 Button AppBindingDao.update Toast「已保存」
删除应用 Button + AlertDialog AppBindingDao.deleteById Toast + router.back()

八、完整操作流程

复制代码
AppDetailPage
    │
    ├── 加载数据(loadData)
    │
    ├── 编辑绑定卡号 → ActionMenu
    ├── 编辑分类 → ActionMenu
    ├── 切换状态 → 分段控件 onClick
    ├── 编辑备注 → TextArea
    │
    ├── 保存修改
    │   ├── onSave → AppBindingDao.update
    │   └── Toast "已保存"
    │
    └── 删除应用
        ├── AlertDialog 确认
        └── AppBindingDao.deleteById → Toast → back

小结

要点 说明
FormRow 可复用 Builder:label + value + right arrow
ActionMenu 选择 pickCard / pickCategory 原生弹窗
状态分段控件 MUTED_BG 容器 + Primary 选中
保存逻辑 update 全部字段 + updated_at 时间戳
删除 AlertDialog 二次确认 + deleteById + back
数据刷新 子页面返回后首页 onPageShow 自动刷新

下一篇文章,我们开发搜索页 SearchPage------实时搜索、历史记录、无结果快速添加。


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


相关资源:

相关推荐
l134062082352 小时前
鸿蒙应用开发实战【85】— 应用签名与正式HAP打包
华为·harmonyos·鸿蒙系统
爸爸6192 小时前
鸿蒙应用开发实战【06】— 应用入口与生命周期管理(EntryAbility 深度解析)
华为·harmonyos·鸿蒙系统
不羁的木木2 小时前
《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第8篇:智能家居控制——手机控制智能灯实战
智能手机·智能家居·harmonyos
用户0934077735143 小时前
HarmonyOS WPS Open SDK 实践:用能力分层重构 OpenFileRequest
typescript·harmonyos
春卷同学4 小时前
HarmonyOS掌上记账APP开发实践第21篇:Tabs 底部导航 — 鸿蒙应用的标准导航范式
华为·harmonyos
子非鱼94274 小时前
03-Flutter 鸿蒙实战 03:登录注册与本地会话持久化
网络·flutter·华为·harmonyos
子非鱼94274 小时前
01-Flutter 鸿蒙实战 01:会说话的相册项目架构拆解
flutter·华为·harmonyos
不羁的木木4 小时前
《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第10篇:多设备协同——构建星闪网络实现数据同步
网络·华为·harmonyos
春卷同学4 小时前
HarmonyOS掌上记账APP开发实践第29篇:条件渲染与 ForEach — 动态 UI 的构建策略
ui·华为·harmonyos