标签是另一种组织笔记的方式,与文件夹的层级结构不同,标签更加灵活自由。一条笔记可以有多个标签,通过标签可以从不同维度查看和筛选笔记。本文将介绍如何实现一个实用的标签管理系统。
请添加图片描述

标签页面的整体布局

标签管理页面展示所有标签及其包含的笔记数量,用户可以点击查看详情或删除标签。

class TagPage extends StatelessWidget {
  const TagPage({super.key});

  
  Widget build(BuildContext context) {
    final controller = Get.find<NoteController>();
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('标签管理'),
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: () => _showCreateTagDialog(context, controller),
          ),
        ],
      ),

页面使用StatelessWidget,状态由GetX控制器管理。AppBar显示标题和添加按钮,点击添加按钮弹出创建标签对话框。这种简洁的设计让用户可以快速创建新标签。标签管理不需要像文件夹那样复杂的层级结构,所以界面更加简单直观。

标签列表的响应式展示

标签列表使用Obx包裹,实现数据变化时自动更新界面。

      body: Obx(() {
        if (controller.tags.isEmpty) {
          return const EmptyState(
            icon: Icons.label_outline,
            title: '暂无标签',
            subtitle: '点击右上角创建标签',
          );
        }
        return ListView.builder(
          padding: EdgeInsets.all(12.w),
          itemCount: controller.tags.length,
          itemBuilder: (context, index) {
            final tag = controller.tags[index];
            final noteCount = controller.getNotesByTag(tag.name).length;

当标签列表为空时,显示空状态提示,引导用户创建第一个标签。有标签时使用ListView.builder构建列表,只渲染可见的列表项。对于每个标签,通过getNotesByTag方法获取包含该标签的笔记数量。这个数量会显示在卡片上,让用户了解标签的使用情况。

标签卡片的设计

每个标签显示为一个卡片,包含标签名称、笔记数量和操作按钮。

            return Card(
              margin: EdgeInsets.only(bottom: 8.h),
              child: ListTile(
                leading: const Icon(Icons.label, color: Color(0xFF2196F3)),
                title: Text('#${tag.name}'),
                subtitle: Text('$noteCount 篇笔记'),
                trailing: IconButton(
                  icon: const Icon(Icons.delete_outline),
                  onPressed: () => _confirmDelete(context, controller, tag),
                ),
                onTap: () => Get.to(() => TagDetailPage(tag: tag)),
              ),
            );
          },
        );
      }),
    );
  }

ListTile是Material Design的标准列表项组件,leading显示标签图标,title显示标签名称并添加#前缀,subtitle显示笔记数量。trailing放置删除按钮,点击后弹出确认对话框。整个卡片可点击,点击后跳转到标签详情页面查看所有带该标签的笔记。这种设计让标签管理变得简单直观。

创建标签对话框

创建标签对话框只需要输入标签名称,比创建文件夹更简单。

  void _showCreateTagDialog(BuildContext context, NoteController controller) {
    final nameController = TextEditingController();
    
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('新建标签'),
        content: TextField(
          controller: nameController,
          decoration: const InputDecoration(
            labelText: '标签名称',
            border: OutlineInputBorder(),
          ),
        ),

对话框包含一个TextField用于输入标签名称。使用OutlineInputBorder提供清晰的边框样式,labelText显示输入提示。TextEditingController管理输入内容,可以获取用户输入的文本。标签名称应该简短有意义,方便用户记忆和使用。

        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              if (nameController.text.isNotEmpty) {
                controller.createTag(nameController.text);
                Navigator.pop(context);
              }
            },
            child: const Text('创建'),
          ),
        ],
      ),
    );
  }

对话框底部有取消和创建两个按钮。创建按钮验证名称不为空后调用控制器的createTag方法创建标签,然后关闭对话框。这种简单的验证能够避免创建空名称的标签。标签创建成功后会立即显示在列表中,因为使用了响应式更新。

删除标签的确认

删除标签会影响所有使用该标签的笔记,需要用户确认。

  void _confirmDelete(BuildContext context, NoteController controller, tag) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('删除标签'),
        content: Text('确定要删除"#${tag.name}"吗?该标签将从所有笔记中移除。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),

确认对话框显示标签名称,并说明删除后的影响。标签会从所有笔记中移除,但笔记本身不会被删除。这种设计确保用户清楚了解操作的后果。content使用字符串插值显示标签名称,让用户确认要删除的是哪个标签。

          ElevatedButton(
            style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
            onPressed: () {
              controller.deleteTag(tag.id);
              Navigator.pop(context);
            },
            child: const Text('删除'),
          ),
        ],
      ),
    );
  }
}

删除按钮使用红色背景,警示用户这是危险操作。点击后调用控制器的deleteTag方法删除标签,然后关闭对话框。控制器会遍历所有笔记,将该标签从笔记的tags列表中移除。这种确认机制能够有效防止误操作。

标签的数据模型

标签模型非常简单,只需要ID和名称两个字段。

class Tag {
  final String id;
  final String name;
  final DateTime createdAt;

  Tag({
    required this.id,
    required this.name,
    required this.createdAt,
  });
}

Tag类包含三个字段:id是唯一标识,name是标签名称,createdAt记录创建时间。标签不需要像文件夹那样的层级结构,所以模型更加简单。这种扁平的结构让标签管理变得轻量高效。

控制器中的标签管理

控制器负责标签的增删改查和数据持久化。

final RxList<Tag> tags = <Tag>[].obs;

void createTag(String name) {
  // 检查是否已存在同名标签
  if (tags.any((t) => t.name == name)) {
    Get.snackbar('提示', '标签已存在', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  final tag = Tag(
    id: DateTime.now().millisecondsSinceEpoch.toString(),
    name: name,
    createdAt: DateTime.now(),
  );
  tags.add(tag);
  saveData();
}

tags使用RxList实现响应式,任何修改都会自动通知UI更新。createTag方法首先检查是否已存在同名标签,避免重复。如果不存在就创建新标签,使用时间戳作为唯一ID。将新标签添加到列表后调用saveData持久化。这种设计确保标签名称的唯一性。

void deleteTag(String id) {
  final tag = tags.firstWhereOrNull((t) => t.id == id);
  if (tag == null) return;
  
  tags.removeWhere((t) => t.id == id);
  // 从所有笔记中移除该标签
  for (var note in notes) {
    note.tags.remove(tag.name);
  }
  saveData();
}

deleteTag方法删除指定ID的标签,然后遍历所有笔记,将该标签从笔记的tags列表中移除。这种级联更新确保数据的一致性,不会留下孤立的标签引用。删除后调用saveData持久化修改。

获取标签下的笔记

提供方法获取包含指定标签的所有笔记。

List<Note> getNotesByTag(String tagName) {
  return activeNotes.where((n) => n.tags.contains(tagName)).toList();
}

getNotesByTag方法筛选出tags列表包含指定标签名称的笔记。笔记的tags字段是一个字符串列表,使用contains方法检查是否包含目标标签。这个方法在标签详情页面和统计分析中都会用到。

标签的重命名

用户应该能够修改标签名称,同时更新所有使用该标签的笔记。

void renameTag(String id, String newName) {
  final tag = tags.firstWhereOrNull((t) => t.id == id);
  if (tag == null) return;
  
  // 检查新名称是否已存在
  if (tags.any((t) => t.name == newName && t.id != id)) {
    Get.snackbar('提示', '标签名称已存在', snackPosition: SnackPosition.BOTTOM);
    return;
  }
  
  final oldName = tag.name;
  final index = tags.indexWhere((t) => t.id == id);
  tags[index] = tag.copyWith(name: newName);
  
  // 更新所有笔记中的标签名称
  for (var note in notes) {
    final tagIndex = note.tags.indexOf(oldName);
    if (tagIndex != -1) {
      note.tags[tagIndex] = newName;
    }
  }
  saveData();
}

renameTag方法首先检查新名称是否已存在,避免重复。然后更新标签对象的名称,并遍历所有笔记,将旧标签名称替换为新名称。这种全局更新确保标签名称的一致性。

标签的合并

支持将多个标签合并为一个,整合相关内容。

void mergeTags(List<String> tagIds, String targetName) {
  final tagNames = tagIds.map((id) => tags.firstWhereOrNull((t) => t.id == id)?.name).whereType<String>().toList();
  
  // 创建或获取目标标签
  if (!tags.any((t) => t.name == targetName)) {
    createTag(targetName);
  }
  
  // 更新所有笔记
  for (var note in notes) {
    final hasAnyTag = note.tags.any((t) => tagNames.contains(t));
    if (hasAnyTag) {
      note.tags.removeWhere((t) => tagNames.contains(t));
      if (!note.tags.contains(targetName)) {
        note.tags.add(targetName);
      }
    }
  }
  
  // 删除被合并的标签
  tags.removeWhere((t) => tagIds.contains(t.id));
  saveData();
}

mergeTags方法接收要合并的标签ID列表和目标标签名称。首先确保目标标签存在,然后遍历所有笔记,将被合并的标签替换为目标标签。最后删除被合并的标签。这个功能在整理标签时很有用。

标签的使用频率统计

统计每个标签的使用频率,帮助用户了解标签的重要性。

Map<String, int> getTagUsageStats() {
  final stats = <String, int>{};
  for (var tag in tags) {
    stats[tag.name] = getNotesByTag(tag.name).length;
  }
  // 按使用频率降序排序
  final sorted = Map.fromEntries(
    stats.entries.toList()..sort((a, b) => b.value.compareTo(a.value))
  );
  return sorted;
}

getTagUsageStats方法返回一个Map,键是标签名称,值是使用次数。遍历所有标签,统计每个标签包含的笔记数量。然后按使用次数降序排序,最常用的标签排在前面。这个统计可以在标签管理页面展示。

标签的颜色标记

为标签添加颜色,让用户可以通过颜色快速识别不同类型的标签。

class Tag {
  final String id;
  final String name;
  final String? color;
  final DateTime createdAt;

  Tag({
    required this.id,
    required this.name,
    this.color,
    required this.createdAt,
  });
}

在Tag模型中添加可选的color字段,存储十六进制颜色字符串。用户可以为不同主题的标签设置不同颜色,比如工作相关的用蓝色,生活相关的用绿色。这种视觉标记能够提升标签的可识别性。

标签的搜索

支持按名称搜索标签,快速定位目标。

List<Tag> searchTags(String query) {
  if (query.isEmpty) return [];
  
  final lowerQuery = query.toLowerCase();
  return tags.where((t) => t.name.toLowerCase().contains(lowerQuery)).toList();
}

searchTags方法实现简单的模糊搜索,不区分大小写。返回名称包含查询字符串的所有标签。这个功能在标签数量很多时特别有用,能够帮助用户快速找到目标标签。

标签的自动建议

在用户输入标签时,提供自动建议功能,减少输入工作量。

List<String> getTagSuggestions(String input) {
  if (input.isEmpty) return [];
  
  final lowerInput = input.toLowerCase();
  final suggestions = tags
      .where((t) => t.name.toLowerCase().startsWith(lowerInput))
      .map((t) => t.name)
      .toList();
  
  // 按使用频率排序
  suggestions.sort((a, b) {
    final aCount = getNotesByTag(a).length;
    final bCount = getNotesByTag(b).length;
    return bCount.compareTo(aCount);
  });
  
  return suggestions.take(5).toList();
}

getTagSuggestions方法根据用户输入返回匹配的标签建议。使用startsWith而不是contains,只匹配以输入开头的标签。然后按使用频率排序,最常用的排在前面。最多返回5条建议,避免列表过长。

标签的批量操作

支持批量添加或删除标签,提升操作效率。

void addTagsToNotes(List<String> noteIds, List<String> tagNames) {
  for (var noteId in noteIds) {
    final note = notes.firstWhereOrNull((n) => n.id == noteId);
    if (note != null) {
      for (var tagName in tagNames) {
        if (!note.tags.contains(tagName)) {
          note.tags.add(tagName);
        }
      }
    }
  }
  saveData();
}

void removeTagsFromNotes(List<String> noteIds, List<String> tagNames) {
  for (var noteId in noteIds) {
    final note = notes.firstWhereOrNull((n) => n.id == noteId);
    if (note != null) {
      note.tags.removeWhere((t) => tagNames.contains(t));
    }
  }
  saveData();
}

addTagsToNotes方法为多个笔记批量添加标签,removeTagsFromNotes方法批量删除标签。这两个方法在批量编辑笔记时很有用,可以一次性为多条笔记添加或删除相同的标签。

标签的导出

支持导出标签及其关联的笔记,方便备份和分享。

String exportTag(String tagName) {
  final notes = getNotesByTag(tagName);
  final buffer = StringBuffer();
  
  buffer.writeln('# 标签: $tagName');
  buffer.writeln('笔记数量: ${notes.length}');
  buffer.writeln('');
  
  for (var note in notes) {
    buffer.writeln('## ${note.title}');
    buffer.writeln(note.content);
    buffer.writeln('');
  }
  
  return buffer.toString();
}

exportTag方法将标签及其所有笔记导出为Markdown格式的文本。标签名称作为一级标题,每个笔记的标题作为二级标题,后面跟着笔记内容。这种格式易于阅读和处理,可以分享给其他人或导入到其他应用。

标签的排序

标签列表应该按照一定规则排序,让用户更容易找到目标。

List<Tag> get sortedTags {
  final list = List<Tag>.from(tags);
  list.sort((a, b) {
    // 按使用频率降序排序
    final aCount = getNotesByTag(a.name).length;
    final bCount = getNotesByTag(b.name).length;
    if (aCount != bCount) {
      return bCount.compareTo(aCount);
    }
    // 使用频率相同时按名称排序
    return a.name.compareTo(b.name);
  });
  return list;
}

sortedTags是一个计算属性,返回排序后的标签列表。排序规则是按使用频率降序,最常用的标签排在前面。使用频率相同时按名称字母顺序排列。这种排序让标签列表更加有序,用户可以快速找到常用标签。

标签云的展示

使用标签云的方式展示所有标签,标签大小反映使用频率。

Widget buildTagCloud() {
  final stats = getTagUsageStats();
  final maxCount = stats.values.isEmpty ? 1 : stats.values.reduce((a, b) => a > b ? a : b);
  
  return Wrap(
    spacing: 8.w,
    runSpacing: 8.h,
    children: stats.entries.map((e) {
      final size = 12 + (e.value / maxCount * 12);
      return ActionChip(
        label: Text(
          '#${e.key}',
          style: TextStyle(fontSize: size.sp),
        ),
        onPressed: () => Get.to(() => TagDetailPage(tag: tags.firstWhere((t) => t.name == e.key))),
      );
    }).toList(),
  );
}

buildTagCloud方法创建一个标签云组件。首先获取标签使用统计,计算最大使用次数。然后为每个标签创建ActionChip,字体大小根据使用频率动态调整。使用频率越高,字体越大。这种可视化方式让用户可以直观地看到哪些标签最常用。

标签的智能推荐

根据笔记内容智能推荐相关标签,减少手动输入。

List<String> recommendTags(String content) {
  final recommendations = <String>[];
  final lowerContent = content.toLowerCase();
  
  for (var tag in tags) {
    if (lowerContent.contains(tag.name.toLowerCase())) {
      recommendations.add(tag.name);
    }
  }
  
  // 按使用频率排序
  recommendations.sort((a, b) {
    final aCount = getNotesByTag(a).length;
    final bCount = getNotesByTag(b).length;
    return bCount.compareTo(aCount);
  });
  
  return recommendations.take(5).toList();
}

recommendTags方法分析笔记内容,推荐相关标签。遍历所有标签,如果标签名称出现在内容中就添加到推荐列表。然后按使用频率排序,最多返回5个推荐。这个功能可以在编辑笔记时使用,帮助用户快速添加合适的标签。

标签管理是笔记应用的重要功能,它提供了一种灵活的组织方式。与文件夹的层级结构不同,标签允许一条笔记属于多个分类,从不同角度查看和筛选内容。通过支持创建、删除、搜索、统计等功能,我们实现了一个功能完善的标签管理系统。用户可以根据自己的需求灵活使用标签,建立个性化的笔记组织方式。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐