标签(Tag)是现代应用中无处不在的信息组织方式。从 Gmail 的邮件分类标签、Jira 的任务状态标记、Notion 的页面属性标签到微信的联系人分组——标签系统帮助用户在大量信息中快速分类、筛选和检索。

HarmonyOS NEXT ArkUI 虽然没有独立的"Tag 组件",但通过基础组件的组合(TextInput、Grid、Text、Row),可以构建出功能完整的标签管理系统。本文将深入讲解如何用基础组件构建标签系统,并通过一个"标签管理工具"实战案例展示添加、删除、搜索、多选和批量操作等功能。

关键词:HarmonyOS、ArkUI、标签管理、搜索筛选、多选操作、组件组合

一、标签系统的设计目标

一个实用的标签管理系统需要支持以下操作:

  1. 添加标签:用户输入文字快速创建新标签,自动分配颜色
  2. 删除标签:每个标签有删除入口,支持逐个删除
  3. 搜索筛选:在大量标签中快速定位目标标签
  4. 多选操作:选中多个标签进行批量操作(如批量删除)

这些操作看似简单,但背后的交互设计需要考虑:输入验证、空状态处理、搜索匹配策略、选中状态视觉反馈、删除操作的不可逆性等。

二、数据模型与状态管理

2.1 标签数据结构

每个标签包含唯一 ID、文本内容、颜色、选中状态:

interface TagItem {
  id: number;
  text: string;     // 标签文字
  color: string;     // 标签颜色
  selected: boolean; // 是否被选中
}

颜色使用预设调色板循环分配,确保每个新标签都有可区分的颜色:

private tagColors: string[] = [
  '#1677FF', '#00B894', '#E09F3E', '#6C5CE7', '#FF6B6B', '#0984E3'
];

addTag(): void {
  let text = this.inputText.trim();
  if (text.length === 0) return;
  let copy = this.tags.slice();
  copy.push({
    id: this.nextId++,
    text: text,
    color: this.tagColors[this.colorIdx % this.tagColors.length],
    selected: false
  });
  this.tags = copy;
  this.colorIdx++;
  this.inputText = '';
}

6 种颜色轮换使用,相邻创建的标签颜色不同,在视觉上自然形成区分。不要求同色标签不连续(6 种颜色足够分散),只需保证最基本的可区分性。

2.2 状态更新的数组模式

延续上一篇文章的教训,所有修改 @State tags 数组的方法都使用 slice() + 重新赋值的模式:

deleteTag(id: number): void {
  let copy = this.tags.slice();
  let target = -1;
  for (let i = 0; i < copy.length; i++) {
    if (copy[i].id === id) { target = i; break; }
  }
  if (target !== -1) {
    copy.splice(target, 1);
    this.tags = copy;  // 新数组引用触发状态更新
  }
}

toggleSelect 方法在切换选中状态时,需要创建修改元素的新对象(而非修改原对象属性):

toggleSelect(id: number): void {
  let copy = this.tags.slice();
  for (let i = 0; i < copy.length; i++) {
    if (copy[i].id === id) {
      copy[i] = {
        id: copy[i].id, text: copy[i].text, color: copy[i].color,
        selected: !copy[i].selected  // 新对象,新 selected 值
      };
      break;
    }
  }
  this.tags = copy;
}

这里的关键是:不仅需要新数组引用(slice()),还需要被修改的元素有新的对象引用(手动创建新对象)。两个层级的引用更新确保了 @StateForEach 能正确检测到变化。
在这里插入图片描述

三、搜索筛选

3.1 搜索输入

搜索栏使用 TextInput 组件,通过 onChange 回调实时更新 searchText 状态:

TextInput({ placeholder: '搜索标签...' })
  .fontSize(13)
  .layoutWeight(1)
  .height(36)
  .backgroundColor('#F5F5F5')
  .borderRadius(8)
  .onChange((value: string) => { this.searchText = value; })

搜索是"即时筛选"而非"点击搜索按钮后筛选"——每输入一个字符,标签网格立即更新。这种模式适合标签总数在 100 以内的场景。如果标签数量上千,需要加防抖(debounce 300ms)避免高频渲染。

3.2 筛选逻辑

getFiltered(): TagItem[] {
  if (!this.searchText || this.searchText.length === 0) {
    return this.tags;  // 无搜索词 → 返回全部
  }
  let result: TagItem[] = [];
  for (let i = 0; i < this.tags.length; i++) {
    if (this.tags[i].text.indexOf(this.searchText) !== -1) {
      result.push(this.tags[i]);
    }
  }
  return result;
}

使用简单的 indexOf 子串匹配——输入"工作"可以匹配"工作中"和"日常工作"。这是最直观的搜索策略,不需要用户了解正则或通配符。

搜索状态下,页面显示当前搜索结果数量和快速操作入口:

搜索: "项目" — 3 个结果          删除这些

"删除这些"按钮允许用户一次性删除当前搜索匹配的所有标签。这是一个强大的批量操作——先搜索定位,再统一删除。比逐个寻找和删除高效得多。
在这里插入图片描述

四、多选操作

4.1 选中交互

点击标签切换选中状态。选中的标签视觉反馈是双重的:

// 选中标签的样式
.backgroundColor(tag.selected ? tag.color : tag.color.concat('18'))
.border({
  width: tag.selected ? 2 : 1,
  color: tag.selected ? tag.color : tag.color.concat('40'),
  style: BorderStyle.Solid
})

未选中时:标签使用非常淡的背景色(主题色 + 18 即 9% 透明度)+ 细边框(主题色 + 40 即 25% 透明度),文字为深色。这是一种"轻量"的视觉效果——标签可见但不夺目。

选中时:标签使用实色背景(主题色 100% 不透明)+ 粗边框(2vp),文字变为白色。这是"高亮"效果——选中标签从标签群中脱颖而出。

4.2 选中计数与清除

选中至少一个标签后,操作栏自动出现:

if (this.getSelectedCount() > 0) {
  Row() {
    Text('已选 ' + this.getSelectedCount().toString() + ' 个')
      .fontSize(12).fontColor('#1677FF').fontWeight(FontWeight.Medium)
    Blank()
    Text('清除选择')
      .fontSize(12).fontColor('#FF4D4F')
      .padding({ top: 4, bottom: 4, left: 10, right: 10 })
      .borderRadius(10).backgroundColor('#FFF2F0')
      .onClick(() => { this.clearSelection(); })
  }
}

操作栏的显示和隐藏完全由状态驱动——if (this.getSelectedCount() > 0) 条件渲染。ArkUI 支持条件渲染,当选中数从 0 变为 1 时操作栏出现,从 1 变为 0 时消失。无额外过渡动画,但清晰直观。

clearSelection() 的实现遍历所有标签,将每个选中标签替换为新对象(selected: false)。为避免不必要的状态更新,只有当确实有标签被清除时才重新赋值 this.tags

五、标签网格布局

使用 3 列 Grid 展示标签,每个标签是一个圆角矩形:

Grid() {
  ForEach(this.getFiltered(), (tag: TagItem) => {
    GridItem() {
      Row() {
        Text(tag.text)
          .fontSize(13)
          .fontColor(tag.selected ? '#FFFFFF' : '#1a1a2e')
          .layoutWeight(1)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text('✕')
          .fontSize(11)
          .fontColor('#9999AA')
          .padding({ left: 6 })
          .onClick(() => { this.deleteTag(tag.id); })
      }
      .width('100%').padding({ top: 10, bottom: 10, left: 12, right: 8 })
      .borderRadius(8)
      .onClick(() => { this.toggleSelect(tag.id); })
    }
  }, (tag: TagItem) => tag.id.toString())
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(8).rowsGap(8)

每个标签项有两个可点击区域:

  • 标签主体RowonClick):切换选中状态
  • 删除按钮):删除标签

两个点击区域的处理不同——删除按钮的 onClick 事件不会冒泡到父级(ArkUI 的事件模型默认不冒泡),所以点击 ✕ 只会触发删除,不会触发选中。

六、空状态与边界处理

6.1 搜索无结果

当搜索词匹配不到任何标签时,显示友好的空状态提示:

if (this.getFiltered().length === 0) {
  Text('没有匹配的标签,试试修改搜索条件')
    .fontSize(13).fontColor('#BBBBCC')
    .width('100%').textAlign(TextAlign.Center)
    .padding({ top: 20, bottom: 20 })
}

灰色文字居中显示,提示用户调整搜索条件。空状态是用户体验的关键细节——不要留白,给用户明确指引。

6.2 空标签输入防护

addTag(): void {
  let text = this.inputText.trim();
  if (text.length === 0) {
    return;  // 忽略空白输入
  }
  // ... 添加逻辑
}

trim() 去除首尾空格后检查长度。纯空格输入(如连续空格)会被拦截,不创建空标签。

6.3 预设标签

页面首次加载时提供 7 个预设标签(工作、个人、紧急、学习、项目A、待跟进、收藏),让用户进入页面后立即看到标签系统的展示效果:

aboutToAppear(): void {
  let preset: string[] = ['工作', '个人', '紧急', '学习', '项目A', '待跟进', '收藏'];
  // 用预设数据初始化 this.tags
}

预设标签覆盖了常见的分类维度(优先级、场景、状态),为用户提供了标签命名的参考。用户可以直接使用这些预设,也可以删除它们创建自己的体系。

七、完整交互流程

  1. 初始状态:7 个预设标签以 3 列网格展示,颜色各异,无选中
  2. 添加标签:在顶部输入框输入"设计稿",点击"添加",新标签(橙色)出现在网格末尾
  3. 搜索筛选:在搜索栏输入"项目",网格只显示"项目A",提示"1 个结果"
  4. 选中标签:点击"紧急"和"待跟进",两个标签变为实色背景 + 白字,操作栏显示"已选 2 个"
  5. 清除选择:点击"清除选择",所有标签恢复默认样式,操作栏消失
  6. 删除标签:点击"项目A"的 ✕,该标签从网格中消失,总数减少
  7. 批量删除:搜索"工作",结果中出现"工作",点击"删除这些",该标签被删除,搜索栏清空

八、进阶扩展方向

8.1 标签编辑

当前标签文字不可修改。扩展为点击标签文字触发编辑模式——文字变为 TextInput,修改后保存。配合 onBlur 或回车完成编辑。

8.2 标签拖拽排序

配合 onItemDragStart / onItemDrop 事件,支持长按标签卡片拖动到目标位置重新排序。标签的排列顺序在某些场景中是语义化的(如优先级从高到低)。

8.3 标签分组

将标签按类别分组(如"工作相关"、“个人相关”),每组有标题和可折叠的标签列表。这对标签数量超过 50 的复杂系统尤为重要。

8.4 标签颜色自定义

当前颜色由系统自动轮换分配。扩展为点击颜色圆点弹出颜色选择器,用户可以自定义标签颜色。配合 PersistentStorage 持久化颜色配置。

8.5 标签关联数据

在实际应用中,标签通常关联到具体的数据对象(如任务、笔记、联系人)。扩展为"标签 → 数据对象"的关联关系和反向查询(点击标签查看所有关联的数据项)。

九、总结

本文通过"标签管理工具"这个实战案例,展示了如何用基础 ArkUI 组件(TextInput、Grid、Row、Text)构建功能完整的标签系统。核心知识点包括:

  1. 数据模型设计:标签的字段选择——id、text、color、selected
  2. 状态更新模式slice() + 新对象 → 新数组引用 → 触发 @State 更新
  3. 搜索筛选策略:即时筛选 + 子串匹配,适合中小规模数据集
  4. 多选交互:视觉反馈(双层样式变化)、计数显示、清除操作
  5. 批量操作:结合搜索的"删除这些"——先筛选定位,再批量删除
  6. 空状态处理:搜索无结果的友好提示、空白输入拦截
  7. 颜色编码:6 色调色板轮换分配、选中态的实色/半透明双重样式

标签系统看似简单,但它包含了 CRUD 操作(增删改查)、搜索筛选、多选管理和批量操作——这些是几乎每个管理后台和效率工具都需要的基础功能。掌握了这些模式,你可以将它们应用到任务分类、内容标记、联系人分组、商品属性标签等任何需要"轻量级数据组织"的场景。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐