【HarmonyOS 6】活动标签管理页面实现

在时间追踪类应用中,活动标签是核心数据之一。本文将讲解如何使用 HarmonyOS ArkUI 实现一个完整的活动标签管理页面,包含标签列表展示、新增标签对话框、删除确认对话框等功能。

功能概述

活动标签管理页面主要实现以下功能:

  1. 标签列表展示:区分预设标签和自定义标签
  2. 新增标签:包含名称输入、颜色选择、评分开关
  3. 删除标签:支持删除数据或转移数据两种方式

UI 布局实现

标题栏布局

标题栏采用 Row 水平布局,包含返回按钮、标题文字和新增按钮:

typescript 复制代码
Row() {
  Button() {
    Text('←')
      .fontSize(this.fs(24))
      .fontColor($r('app.color.text_primary'))
  }
  .backgroundColor(Color.Transparent)
  .onClick(() => {
    router.back()
  })

  Text('活动标签管理')
    .fontSize(this.fs(20))
    .fontWeight(FontWeight.Bold)
    .fontColor($r('app.color.text_primary'))
    .margin({ left: 12 })
  
  Blank()
  
  Button() {
    Text('+ 新增')
      .fontSize(this.fs(14))
      .fontColor(Color.White)
  }
  .backgroundColor($r('app.color.primary_color'))
  .borderRadius(20)
  .height(36)
  .padding({ left: 16, right: 16 })
  .onClick(() => {
    this.openAddTagDialog()
  })
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.Start)
.alignItems(VerticalAlign.Center)
.backgroundColor($r('app.color.card_background'))

这里使用 Blank() 组件占据中间空间,使新增按钮靠右对齐。

分类标题设计

预设标签和自定义标签的分类标题采用左侧色条设计,视觉上更加清晰:

typescript 复制代码
Row() {
  Row()
    .width(4)
    .height(20)
    .backgroundColor($r('app.color.primary_color'))
    .borderRadius(2)
  
  Text('预设标签')
    .fontSize(this.fs(17))
    .fontWeight(FontWeight.Bold)
    .fontColor($r('app.color.text_primary'))
    .margin({ left: 10 })
}
.width('100%')
.margin({ bottom: 12 })

左侧添加一个 4px 宽的色条作为视觉标识,不同分类使用不同颜色。

标签项布局

每个标签项使用 @Builder 装饰器封装,便于复用:

typescript 复制代码
@Builder
TagItem(tag: ActivityTag, canDelete: boolean) {
  Row() {
    // 颜色指示器
    Row()
      .width(4)
      .height(40)
      .backgroundColor(tag.color)
      .borderRadius(2)
      .margin({ right: 12 })

    Column({ space: 4 }) {
      Text(tag.name)
        .fontSize(this.fs(15))
        .fontColor($r('app.color.text_primary'))
        .fontWeight(FontWeight.Medium)
      
      Text(tag.requiresRating ? '需要评分' : '日常活动')
        .fontSize(this.fs(12))
        .fontColor($r('app.color.text_secondary'))
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    Button('删除')
      .fontSize(this.fs(13))
      .fontColor($r('app.color.error_color'))
      .backgroundColor('rgba(255, 0, 0, 0.1)')
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
      .borderRadius(4)
      .onClick(() => {
        this.openDeleteDialog(tag)
      })
  }
  .width('100%')
  .padding(12)
  .backgroundColor($r('app.color.card_background'))
  .borderRadius(8)
}

标签项的左侧显示颜色指示条,中间显示标签名称和类型,右侧显示删除按钮。

新增标签对话框

新增标签对话框使用 @CustomDialog 装饰器,包含名称输入、颜色选择和评分开关三个表单元素。

颜色选择器

颜色选择器通过预设颜色数组配合 ForEach 实现:

typescript 复制代码
private readonly presetColors: string[] = [
  '#FF5252', '#FF6F00', '#FFD600', '#00C853',
  '#00BFA5', '#00B0FF', '#D500F9', '#FF4081',
  '#7C4DFF', '#536DFE', '#FF6E40', '#69F0AE'
]

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
  ForEach(this.presetColors, (color: string) => {
    Stack() {
      Row()
        .width(40)
        .height(40)
        .backgroundColor(color)
        .borderRadius(20)
        .border({
          width: this.tagColor === color ? 3 : 0,
          color: '#FFFFFF'
        })
        .shadow({
          radius: this.tagColor === color ? 8 : 0,
          color: color,
          offsetX: 0,
          offsetY: 0
        })
      
      if (this.tagColor === color) {
        Text('✓')
          .fontSize(this.fs(20))
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
      }
    }
    .width(40)
    .height(40)
    .margin({ right: 12, bottom: 12 })
    .onClick(() => {
      this.tagColor = color
    })
  })
}

选中的颜色会显示白色边框和发光阴影效果,并叠加一个勾选图标。使用 Stack 组件实现图标叠加效果。

Toggle 开关组件

是否需要评分使用 Toggle 组件实现:

typescript 复制代码
Row() {
  Text('需要评分')
    .fontSize(this.fs(14))
    .fontColor($r('app.color.text_primary'))
    .layoutWeight(1)
  
  Toggle({ type: ToggleType.Switch, isOn: this.requiresRating })
    .onChange((isOn: boolean) => {
      this.requiresRating = isOn
    })
}

ToggleType.Switch 表示开关类型,isOn 绑定当前状态。

打开对话框

在主页面中通过 CustomDialogController 打开对话框:

typescript 复制代码
private openAddTagDialog(): void {
  this.addTagDialogController = new CustomDialogController({
    builder: AddTagDialogContent({
      allTags: this.activityTags,
      onConfirm: async (name: string, color: string, requiresRating: boolean) => {
        await this.handleAddTag(name, color, requiresRating)
      }
    }),
    autoCancel: true,
    alignment: DialogAlignment.Center
  })
  
  this.addTagDialogController.open()
}

builder 指定对话框内容,onConfirm 是回调函数,在用户点击确定后执行。

删除确认对话框

删除对话框提供两种处理方式:删除关联数据或转移到其他标签。

Radio 单选组件

使用 Radio 组件让用户选择删除方式:

typescript 复制代码
Column({ space: 12 }) {
  Row() {
    Radio({ value: 'delete', group: 'deleteGroup' })
      .checked(this.deleteOption === 0)
      .onChange((isChecked: boolean) => {
        if (isChecked) {
          this.deleteOption = 0
        }
      })
    
    Column({ space: 4 }) {
      Text('删除标签及关联数据')
        .fontSize(this.fs(14))
        .fontColor($r('app.color.text_primary'))
      Text('将删除该标签下的所有时间块记录')
        .fontSize(this.fs(12))
        .fontColor($r('app.color.error_color'))
    }
    .alignItems(HorizontalAlign.Start)
    .margin({ left: 8 })
    .layoutWeight(1)
  }
  .width('100%')
  .onClick(() => {
    this.deleteOption = 0
  })

  Row() {
    Radio({ value: 'transfer', group: 'deleteGroup' })
      .checked(this.deleteOption === 1)
      .onChange((isChecked: boolean) => {
        if (isChecked) {
          this.deleteOption = 1
        }
      })
    
    Column({ space: 4 }) {
      Text('转移数据到其他标签')
        .fontSize(this.fs(14))
        .fontColor($r('app.color.text_primary'))
      Text('时间块记录将转移到选定的标签')
        .fontSize(this.fs(12))
        .fontColor($r('app.color.text_secondary'))
    }
    .alignItems(HorizontalAlign.Start)
    .margin({ left: 8 })
    .layoutWeight(1)
  }
  .width('100%')
  .onClick(() => {
    this.deleteOption = 1
  })
}

同一组 Radio 通过 group 参数关联,确保只能选中一个。点击整行也能触发选择。

数据处理逻辑

新增标签

新增标签需要检查名称是否重复,然后创建并保存:

typescript 复制代码
private async handleAddTag(name: string, color: string, requiresRating: boolean): Promise<void> {
  try {
    // 检查名称是否已存在
    const exists = await this.activityTagRepo.nameExists(name.trim())
    if (exists) {
      this.errorMessage = '活动名称已存在'
      setTimeout(() => {
        this.errorMessage = ''
      }, 3000)
      return
    }

    // 创建新标签
    const newTag = new ActivityTag(
      `custom_${Date.now()}`,
      name.trim(),
      color,
      true,
      requiresRating,
      new Date()
    )

    const result = await this.activityTagRepo.save(newTag)
    if (result.success) {
      // 通知其他页面刷新
      setTimeout(() => {
        AppStorage.setOrCreate('needRefreshTimeline', Date.now())
        AppStorage.setOrCreate('needRefreshAnalysis', Date.now())
        AppStorage.setOrCreate('needRefreshProfile', Date.now())
      }, 50)
      
      await this.loadData()
      this.errorMessage = ''
    }
  } catch (error) {
    this.errorMessage = '新增失败'
  }
}

通过 AppStorage.setOrCreate 通知其他页面刷新数据。

删除标签

删除标签有两种处理方式:

typescript 复制代码
private async handleDeleteTag(tag: ActivityTag, deleteData: boolean, targetTagId: string | null): Promise<void> {
  try {
    if (deleteData) {
      // 删除标签及其关联的时间块
      const blocks = await this.timeBlockRepo.findByActivityTagId(tag.id)
      for (let i = 0; i < blocks.length; i++) {
        await this.timeBlockRepo.delete(blocks[i].id)
      }
    } else if (targetTagId !== null) {
      // 转移时间块到目标标签
      const blocks = await this.timeBlockRepo.findByActivityTagId(tag.id)
      for (let i = 0; i < blocks.length; i++) {
        const block = blocks[i]
        const updatedBlock = new TimeBlock(
          block.id,
          block.startTime,
          block.endTime,
          targetTagId,
          block.focusLevel,
          block.valueLevel,
          block.energyCost,
          block.note,
          block.createdAt,
          new Date()
        )
        await this.timeBlockRepo.update(block.id, updatedBlock)
      }
    }

    await this.activityTagRepo.delete(tag.id)
    
    // 通知其他页面刷新
    setTimeout(() => {
      AppStorage.setOrCreate('needRefreshTimeline', Date.now())
      AppStorage.setOrCreate('needRefreshAnalysis', Date.now())
      AppStorage.setOrCreate('needRefreshProfile', Date.now())
    }, 50)
    
    await this.loadData()
  } catch (error) {
    this.errorMessage = '删除失败'
  }
}

如果选择转移数据,需要遍历所有关联的时间块,更新其标签 ID。

小结

本文介绍了 HarmonyOS 活动标签管理页面的实现,主要涉及以下技术点:

  • @CustomDialog:自定义对话框组件
  • @Builder:可复用的 UI 构建函数
  • Radio:单选组件,通过 group 关联
  • Select:下拉选择组件
  • Toggle:开关组件
  • CustomDialogController:对话框控制器

这些组件组合使用,可以构建出完整的表单交互界面。

相关推荐
weixin_430750932 小时前
提升备份效率——网络设备配置
网络·华为·信息与通信·一键备份·提高备份效率
小妖6663 小时前
鸿蒙读取三方应用版本号
鸿蒙
小雨青年3 小时前
鸿蒙 HarmonyOS 6 | 文件系统 沙箱机制与权限拒绝
华为·harmonyos
大雷神5 小时前
HarmonyOS APP<玩转React>开源教程二十一:测验服务层实现
前端·react.js·开源·harmonyos
qq_283720055 小时前
Qt QML 中为 ComBox设置鸿蒙字体(HarmonyOS Sans)——适配 Qt 5.6.x 与 Qt 5.12+
c++·qt·harmonyos
yumgpkpm5 小时前
AI算力纳管工具GPUStack Server+华为鲲鹏+麒麟操作系统 保姆级安装过程
人工智能·hadoop·华为
花先锋队长5 小时前
华为音乐世界睡眠日特别策划上线,在沉浸式空间音频摇篮曲中入梦
华为·智能手机·harmonyos
sdszoe49226 小时前
OSPF多区域基础实验1
网络·华为·ospf多区域实验
卡兰芙的微笑6 小时前
对鸿蒙蓝牙接口进行xts用例编写
华为·harmonyos