分类是组织笔记的基础方式,通过合理的分类体系,用户可以将笔记按主题、项目或用途进行归类。一个好的分类管理系统应该简单直观,支持快速创建和切换。本文将详细介绍如何实现一个实用的分类管理功能。
请添加图片描述

分类页面的整体架构

分类页面集成了多种组织方式,包括分类、文件夹和标签的快捷入口。

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

  
  Widget build(BuildContext context) {
    final controller = Get.find<NoteController>();
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('分类'),
        actions: [
          IconButton(
            icon: const Icon(Icons.calendar_month),
            onPressed: () => Get.to(() => const CalendarPage()),
          ),
        ],
      ),

页面使用StatelessWidget,状态管理交给GetX控制器。AppBar右侧添加日历图标,提供快速跳转到日历视图的入口。这种设计让用户可以在不同的组织视图之间快速切换。

      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildQuickAccess(context, controller),
            SizedBox(height: 24.h),
            _buildCategorySection(context, controller),
            SizedBox(height: 24.h),
            _buildFolderSection(context, controller),
            SizedBox(height: 24.h),
            _buildTagSection(context, controller),
          ],
        ),
      ),
    );
  }

页面主体使用SingleChildScrollView支持滚动,内容分为四个模块:快捷入口、分类区域、文件夹区域和标签区域。每个模块之间用24像素的间距分隔,形成清晰的视觉层次。crossAxisAlignment设置为start让内容左对齐。这种模块化的布局让用户可以快速定位到需要的功能区域。

快捷入口的实现

快捷入口提供文件夹和标签的快速访问,显示数量统计。

  Widget _buildQuickAccess(BuildContext context, NoteController controller) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '快捷入口',
          style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
        ),
        SizedBox(height: 12.h),
        Row(
          children: [
            Expanded(
              child: _QuickAccessCard(
                icon: Icons.folder_outlined,
                title: '文件夹',
                count: controller.folders.length,
                color: Colors.blue,
                onTap: () => Get.to(() => const FolderPage()),
              ),
            ),

快捷入口使用Column布局,标题使用粗体大字号。下方是一个Row包含两个等宽的卡片。第一个卡片显示文件夹信息,使用蓝色主题。Expanded让卡片占据一半宽度,创建对称的布局。点击卡片跳转到文件夹列表页面,让用户可以查看和管理所有文件夹。

            SizedBox(width: 12.w),
            Expanded(
              child: _QuickAccessCard(
                icon: Icons.label_outlined,
                title: '标签',
                count: controller.tags.length,
                color: Colors.green,
                onTap: () => Get.to(() => const TagPage()),
              ),
            ),
          ],
        ),
      ],
    );
  }

第二个卡片显示标签信息,使用绿色主题与文件夹区分。两个卡片之间有12像素的间距。这种快捷入口的设计让用户可以一眼看到文件夹和标签的数量,并快速访问详情页面。颜色的区分也帮助用户快速识别不同的功能。

分类区域的构建

分类区域是页面的核心,展示所有用户创建的分类。

  Widget _buildCategorySection(BuildContext context, NoteController controller) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              '分类',
              style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
            ),
            IconButton(
              icon: const Icon(Icons.add),
              onPressed: () => _showCreateCategoryDialog(context, controller),
            ),
          ],
        ),

分类区域的标题栏使用Row布局,左侧是标题,右侧是添加按钮。mainAxisAlignment设置为spaceBetween让两者分别靠左右两端。这种布局方式在Material Design中很常见,既美观又实用。点击添加按钮弹出创建分类对话框,让用户可以快速创建新分类。

        SizedBox(height: 12.h),
        Obx(() {
          if (controller.categories.isEmpty) {
            return Card(
              child: Padding(
                padding: EdgeInsets.all(24.w),
                child: Center(
                  child: Text(
                    '暂无分类,点击右上角添加',
                    style: TextStyle(color: Colors.grey, fontSize: 14.sp),
                  ),
                ),
              ),
            );
          }

使用Obx包裹内容实现响应式更新。当分类列表为空时,显示一个提示卡片,引导用户点击添加按钮创建分类。这种友好的空状态提示能够降低用户的学习成本,让新用户知道如何开始使用分类功能。

          return GridView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              crossAxisSpacing: 12.w,
              mainAxisSpacing: 12.h,
              childAspectRatio: 1.5,
            ),
            itemCount: controller.categories.length,

分类列表使用GridView展示,两列布局。shrinkWrap设置为true让GridView根据内容自适应高度,physics设置为NeverScrollableScrollPhysics禁用GridView自身的滚动,使用外层的SingleChildScrollView统一管理。childAspectRatio设置为1.5,让卡片呈现横向矩形。这种网格布局能够在有限的空间内展示更多分类。

            itemBuilder: (context, index) {
              final category = controller.categories[index];
              final noteCount = controller.getNotesByCategory(category.id).length;
              return _CategoryCard(
                name: category.name,
                color: Color(int.parse(category.color.replaceFirst('#', '0xFF'))),
                noteCount: noteCount,
                onTap: () => Get.to(() => CategoryDetailPage(category: category)),
                onLongPress: () => _showCategoryOptions(context, controller, category),
              );
            },
          );
        }),
      ],
    );
  }

itemBuilder为每个分类创建一个_CategoryCard组件。通过getNotesByCategory方法获取该分类下的笔记数量,将十六进制颜色字符串转换为Color对象。点击卡片跳转到分类详情页面,长按显示操作菜单。这种交互设计让用户可以快速查看分类内容或进行编辑删除操作。

文件夹区域的展示

文件夹区域显示根级文件夹的预览,不占用太多空间。

  Widget _buildFolderSection(BuildContext context, NoteController controller) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              '文件夹',
              style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
            ),
            TextButton(
              onPressed: () => Get.to(() => const FolderPage()),
              child: const Text('查看全部'),
            ),
          ],
        ),

文件夹区域的标题栏与分类区域类似,但右侧使用TextButton显示"查看全部"文字。这种设计让用户明确知道点击后会看到完整的文件夹列表,比单纯的图标更加直观。文字按钮在这种场景下比图标按钮更容易理解。

        SizedBox(height: 12.h),
        Obx(() {
          final rootFolders = controller.folders.where((f) => f.parentId == null).take(4).toList();
          if (rootFolders.isEmpty) {
            return Card(
              child: Padding(
                padding: EdgeInsets.all(24.w),
                child: Center(
                  child: Text(
                    '暂无文件夹',
                    style: TextStyle(color: Colors.grey, fontSize: 14.sp),
                  ),
                ),
              ),
            );
          }

通过where过滤出parentId为null的根级文件夹,使用take限制最多显示4个。这种预览式的展示既能让用户快速了解文件夹情况,又不会占用太多页面空间。如果没有文件夹,显示空状态提示。这种设计在保持页面简洁的同时提供了足够的信息。

          return Wrap(
            spacing: 8.w,
            runSpacing: 8.h,
            children: rootFolders.map((f) => Chip(
              avatar: const Icon(Icons.folder, size: 18),
              label: Text(f.name),
            )).toList(),
          );
        }),
      ],
    );
  }

文件夹使用Wrap组件展示,会自动换行。每个文件夹是一个Chip组件,包含文件夹图标和名称。spacing和runSpacing设置水平和垂直间距。Chip是Material Design中用于展示标签式内容的组件,非常适合这种预览场景。

标签区域的实现

标签区域的结构与文件夹类似,展示常用标签。

  Widget _buildTagSection(BuildContext context, NoteController controller) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              '标签',
              style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
            ),
            TextButton(
              onPressed: () => Get.to(() => const TagPage()),
              child: const Text('查看全部'),
            ),
          ],
        ),

标签区域的标题栏与文件夹区域保持一致的设计风格,提供统一的用户体验。点击"查看全部"按钮会跳转到标签管理页面,用户可以在那里查看所有标签并进行管理操作。

        SizedBox(height: 12.h),
        Obx(() {
          if (controller.tags.isEmpty) {
            return Card(
              child: Padding(
                padding: EdgeInsets.all(24.w),
                child: Center(
                  child: Text(
                    '暂无标签',
                    style: TextStyle(color: Colors.grey, fontSize: 14.sp),
                  ),
                ),
              ),
            );
          }
          return Wrap(
            spacing: 8.w,
            runSpacing: 8.h,
            children: controller.tags.take(10).map((t) => ActionChip(
              label: Text('#${t.name}'),
              onPressed: () {},
            )).toList(),
          );
        }),
      ],
    );
  }

标签区域最多显示10个标签,使用ActionChip而不是普通Chip,表示这些标签是可点击的。标签名称前添加#符号,符合社交媒体的标签习惯。如果没有标签,显示空状态提示。这种设计让用户可以快速浏览常用标签。

创建分类对话框

创建分类对话框支持输入名称和选择颜色。

  void _showCreateCategoryDialog(BuildContext context, NoteController controller) {
    final nameController = TextEditingController();
    String selectedColor = '#2196F3';
    
    showDialog(
      context: context,
      builder: (context) => StatefulBuilder(
        builder: (context, setState) => AlertDialog(
          title: const Text('新建分类'),

使用StatefulBuilder包裹AlertDialog,让对话框内部可以使用setState更新UI。这对于颜色选择器这种需要实时反馈的交互非常重要。nameController管理分类名称输入,selectedColor存储用户选择的颜色,默认为蓝色。

          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: nameController,
                decoration: const InputDecoration(
                  labelText: '分类名称',
                  border: OutlineInputBorder(),
                ),
              ),
              SizedBox(height: 16.h),

对话框内容包含输入框和颜色选择器。TextField用于输入分类名称,使用OutlineInputBorder提供清晰的边框样式。mainAxisSize设置为min让Column只占用必要的空间,避免对话框过大。输入框和颜色选择器之间有16像素的间距。

              Wrap(
                spacing: 8.w,
                runSpacing: 8.h,
                children: [
                  '#F44336', '#E91E63', '#9C27B0', '#673AB7',
                  '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4',
                  '#009688', '#4CAF50', '#8BC34A', '#CDDC39',
                  '#FFEB3B', '#FFC107', '#FF9800', '#FF5722',
                ].map((c) => GestureDetector(
                  onTap: () => setState(() => selectedColor = c),

颜色选择器提供16种预设颜色,涵盖了Material Design的主要色系。使用Wrap布局让颜色块自动换行。GestureDetector处理点击事件,点击后更新selectedColor并刷新UI。这种即时反馈让用户清楚地知道当前选择的是哪个颜色。

                  child: Container(
                    width: 32.w,
                    height: 32.w,
                    decoration: BoxDecoration(
                      color: Color(int.parse(c.replaceFirst('#', '0xFF'))),
                      shape: BoxShape.circle,
                      border: selectedColor == c
                          ? Border.all(color: Colors.black, width: 2)
                          : null,
                    ),
                  ),
                )).toList(),
              ),
            ],
          ),

每个颜色块是一个32x32的圆形容器。当颜色被选中时,添加黑色边框作为视觉反馈。shape设置为circle创建圆形。这种设计让颜色选择器既美观又易用,用户可以清楚地看到每个颜色的效果。

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

对话框底部有取消和创建两个按钮。取消按钮使用TextButton,创建按钮使用ElevatedButton,形成主次分明的视觉层次。创建按钮会验证名称不为空,然后调用控制器的createCategory方法创建分类,最后关闭对话框。这种简单的验证能够避免创建空名称的分类。

分类操作菜单

长按分类卡片会显示操作菜单,提供编辑和删除功能。

  void _showCategoryOptions(BuildContext context, NoteController controller, category) {
    showModalBottomSheet(
      context: context,
      builder: (context) => SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.edit),
              title: const Text('编辑'),
              onTap: () {
                Navigator.pop(context);
                // TODO: Edit category
              },
            ),

使用ModalBottomSheet显示操作菜单,这是Material Design推荐的移动端操作菜单样式。SafeArea确保内容不会被系统UI遮挡。编辑选项目前是TODO状态,后续可以实现编辑分类名称和颜色的功能。mainAxisSize设置为min让菜单只占用必要的高度。

            ListTile(
              leading: const Icon(Icons.delete, color: Colors.red),
              title: const Text('删除', style: TextStyle(color: Colors.red)),
              onTap: () {
                Navigator.pop(context);
                controller.deleteCategory(category.id);
              },
            ),
          ],
        ),
      ),
    );
  }
}

删除选项使用红色突出显示,警示用户这是一个危险操作。点击后先关闭底部菜单,然后调用控制器的deleteCategory方法删除分类。这种设计让用户在执行删除操作时更加谨慎,避免误操作。

快捷访问卡片组件

_QuickAccessCard是一个自定义组件,用于展示快捷入口。

class _QuickAccessCard extends StatelessWidget {
  final IconData icon;
  final String title;
  final int count;
  final Color color;
  final VoidCallback onTap;

  const _QuickAccessCard({
    required this.icon,
    required this.title,
    required this.count,
    required this.color,
    required this.onTap,
  });

组件接收图标、标题、数量、颜色和点击回调作为参数。这种参数化的设计让组件可以灵活复用,文件夹和标签卡片使用相同的组件但显示不同的内容。使用StatelessWidget是因为卡片本身不维护状态。

  
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: EdgeInsets.all(16.w),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Icon(icon, color: color, size: 28.sp),
              SizedBox(height: 8.h),

Card提供卡片样式,InkWell添加点击效果和水波纹动画。borderRadius设置圆角与Card的圆角保持一致。图标使用传入的颜色和较大的尺寸,作为卡片的视觉焦点。crossAxisAlignment设置为start让内容左对齐。

              Text(
                title,
                style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
              ),
              Text(
                '$count 项',
                style: TextStyle(fontSize: 12.sp, color: Colors.grey),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

标题使用较大的字号和粗体,数量信息使用小字号和灰色,形成清晰的信息层次。这种设计让用户可以快速扫描并获取关键信息。卡片内部使用Column布局,垂直排列图标、标题和数量。

分类卡片组件

_CategoryCard用于展示单个分类,包含名称、颜色和笔记数量。

class _CategoryCard extends StatelessWidget {
  final String name;
  final Color color;
  final int noteCount;
  final VoidCallback onTap;
  final VoidCallback onLongPress;

  const _CategoryCard({
    required this.name,
    required this.color,
    required this.noteCount,
    required this.onTap,
    required this.onLongPress,
  });

组件接收分类名称、颜色、笔记数量以及点击和长按回调。onLongPress用于显示操作菜单,提供编辑和删除功能。这种设计让卡片既能快速访问分类内容,又能进行管理操作。

  
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
        onTap: onTap,
        onLongPress: onLongPress,
        borderRadius: BorderRadius.circular(12),
        child: Container(
          padding: EdgeInsets.all(12.w),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CircleAvatar(
                backgroundColor: color,
                radius: 16.w,
                child: Icon(Icons.folder, color: Colors.white, size: 18.sp),
              ),

InkWell同时处理点击和长按事件。CircleAvatar使用分类的颜色作为背景,内部显示白色的文件夹图标。这种设计让每个分类都有独特的视觉标识,用户可以通过颜色快速识别不同的分类。

              SizedBox(height: 8.h),
              Text(
                name,
                style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              Text(
                '$noteCount 篇笔记',
                style: TextStyle(fontSize: 12.sp, color: Colors.grey),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

分类名称限制为单行显示,超出部分用省略号表示。笔记数量使用灰色小字显示,让用户了解每个分类的使用情况。mainAxisAlignment设置为center让内容垂直居中。这种信息密度适中的设计既不会显得空洞,也不会让用户感到信息过载。

分类管理是笔记应用的核心功能之一,它提供了一种结构化的组织方式。通过合理的分类体系,用户可以将笔记按主题、项目或用途进行归类,快速找到需要的内容。本文介绍的分类管理系统不仅支持基础的增删改查,还集成了文件夹和标签的快捷入口,为用户提供了完整的笔记组织解决方案。


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

Logo

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

更多推荐