设计理念

分类详情页面展示某个分类下的所有笔记,用户可以在这里查看、编辑和管理笔记。一个好的详情页面应该提供清晰的笔记列表和便捷的操作入口。本文将详细介绍如何实现分类详情页面的各项功能。
请添加图片描述

页面的基础结构

分类详情页面接收一个分类对象作为参数,展示该分类下的所有笔记。

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../controllers/note_controller.dart';
import '../../models/category.dart';
import '../notes/widgets/note_card.dart';
import '../notes/widgets/empty_state.dart';
import '../editor/note_editor_page.dart';

class CategoryDetailPage extends StatelessWidget {

这部分导入了构建分类详情页面所需的所有依赖包。Material提供基础UI组件,Get提供状态管理和路由功能,ScreenUtil提供屏幕适配能力。同时导入了笔记控制器用于数据管理,分类模型用于类型定义,以及笔记卡片、空状态和编辑器等UI组件。CategoryDetailPage使用StatelessWidget实现,因为状态管理交给了GetX的响应式系统。

  final Category category;

  const CategoryDetailPage({super.key, required this.category});

  
  Widget build(BuildContext context) {
    final controller = Get.find<NoteController>();
    
    return Scaffold(
      appBar: AppBar(
        title: Text(category.name),

CategoryDetailPage接收一个必需的category参数,这是页面展示的核心数据。build方法中通过Get.find获取全局的NoteController实例,这样可以访问所有笔记数据和操作方法。Scaffold提供了Material Design的基础页面结构,包括AppBar、body和floatingActionButton等区域。AppBar的标题直接显示分类名称,让用户清楚知道当前查看的是哪个分类。

        actions: [
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () => _showEditDialog(context, controller),
          ),
        ],
      ),
      body: Obx(() {
        final notes = controller.getNotesByCategory(category.id);
        if (notes.isEmpty) {
          return const EmptyState(

AppBar的actions区域添加了编辑按钮,点击后可以修改分类的名称和颜色。body部分使用Obx包裹,这是GetX的响应式组件,当笔记数据发生变化时会自动重建UI。通过controller.getNotesByCategory方法获取当前分类下的所有笔记,这个方法会根据分类ID进行过滤。当笔记列表为空时,显示EmptyState组件提供友好的空状态提示,避免空白页面给用户带来困惑。

            icon: Icons.folder_open,
            title: '暂无笔记',
            subtitle: '该分类下还没有笔记',
          );
        }

        return ListView.builder(
          padding: EdgeInsets.all(12.w),
          itemCount: notes.length,
          itemBuilder: (context, index) {
            final note = notes[index];

EmptyState组件使用文件夹图标和提示文字,告诉用户当前分类还没有笔记内容。当有笔记时,使用ListView.builder构建列表,这是Flutter中高性能的列表组件,只会渲染可见区域的item。设置12.w的内边距让列表内容不会紧贴屏幕边缘,提供更好的视觉效果。itemBuilder回调函数会为每个笔记创建对应的UI组件,index参数用于访问列表中的具体笔记对象。

            return NoteCard(
              note: note,
              onTap: () => Get.to(() => NoteEditorPage(note: note)),
              onLongPress: () {},
              onDismissed: (direction) {
                if (direction == DismissDirection.endToStart) {
                  controller.deleteNote(note.id);
                } else {
                  controller.toggleFavorite(note.id);
                }

NoteCard是自定义的笔记卡片组件,负责展示笔记的标题、内容预览、时间等信息。onTap回调处理点击事件,使用Get.to进行页面跳转,打开笔记编辑器并传入当前笔记对象。onLongPress预留了长按事件接口,可以用于实现批量选择等功能。onDismissed处理滑动删除操作,根据滑动方向执行不同的操作:向左滑动(endToStart)删除笔记,向右滑动切换收藏状态,这种手势操作让用户可以快速管理笔记。

              },
            );
          },
        );
      }),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final note = controller.createNote(categoryId: category.id);
          Get.to(() => NoteEditorPage(note: note));
        },
        child: const Icon(Icons.add, color: Colors.white),

滑动操作完成后,controller会自动更新数据,Obx会监听到变化并重新渲染列表。FloatingActionButton是Material Design中的悬浮操作按钮,固定在屏幕右下角,提供创建新笔记的快捷入口。点击按钮时,调用controller.createNote方法创建一个新笔记对象,并传入当前分类的ID确保笔记归属正确。创建完成后立即跳转到编辑器页面,让用户可以马上开始编写内容。

      ),
    );
  }
}

整个页面的结构到此完成。这种设计遵循了Material Design规范,提供了清晰的信息层次和便捷的操作方式。

编辑分类对话框

实现编辑分类名称和颜色的功能。

  void _showEditDialog(BuildContext context, NoteController controller) {
    final nameController = TextEditingController(text: category.name);
    String selectedColor = category.color;
    
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('编辑分类'),
        content: Column(
          mainAxisSize: MainAxisSize.min,

_showEditDialog方法负责显示编辑分类的对话框。首先创建TextEditingController并预填充当前分类的名称,这样用户打开对话框时就能看到现有的名称。selectedColor变量用于跟踪用户选择的颜色值,初始值为当前分类的颜色。showDialog是Flutter提供的模态对话框方法,会在当前页面上方显示一个浮层。AlertDialog提供了标准的对话框样式,包含标题、内容和操作按钮区域。Column使用mainAxisSize.min确保对话框高度自适应内容,不会占据整个屏幕。

          children: [
            TextField(
              controller: nameController,
              decoration: const InputDecoration(
                labelText: '分类名称',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 16.h),
            _buildColorPicker(selectedColor, (color) {
              selectedColor = color;

Column的children包含两个主要部分:文本输入框和颜色选择器。TextField使用nameController管理输入内容,InputDecoration提供了标签和边框样式,OutlineInputBorder创建带边框的输入框样式,比默认的下划线样式更加醒目。SizedBox提供16.h的垂直间距,让输入框和颜色选择器之间有适当的分隔。_buildColorPicker是自定义的颜色选择器组件,接收当前选中的颜色和一个回调函数,当用户选择新颜色时,回调函数会更新selectedColor变量。

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

对话框的actions区域包含取消和保存两个按钮。TextButton用于次要操作(取消),样式较为低调,点击后直接关闭对话框不做任何修改。ElevatedButton用于主要操作(保存),样式更加突出,引导用户完成编辑。保存按钮的onPressed回调中首先验证名称是否为空,这是必要的数据校验,防止用户创建没有名称的分类。只有当名称非空时才会执行后续的更新操作,这种防御性编程可以避免数据异常。

                controller.updateCategory(
                  category.id,
                  nameController.text,
                  selectedColor,
                );
                Navigator.pop(context);
              }
            },
            child: const Text('保存'),
          ),
        ],

验证通过后,调用controller.updateCategory方法更新分类信息,传入分类ID、新名称和新颜色三个参数。controller会负责将更新后的数据保存到本地存储,并通知所有监听者更新UI。更新完成后调用Navigator.pop关闭对话框,返回到分类详情页面。由于使用了GetX的响应式系统,分类详情页面会自动显示更新后的分类名称和颜色,无需手动刷新。这种声明式的UI更新方式大大简化了状态管理的复杂度。

      ),
    );
  }

对话框的构建到此完成。整个编辑流程简洁明了,用户体验流畅。

颜色选择器

为分类提供颜色选择功能。

  Widget _buildColorPicker(String selectedColor, Function(String) onColorSelected) {
    final colors = [
      '#2196F3', '#4CAF50', '#FF9800', '#F44336',
      '#9C27B0', '#00BCD4', '#FFEB3B', '#795548',
    ];

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text('选择颜色'),

_buildColorPicker方法创建颜色选择器UI组件。首先定义了8种预设颜色,这些颜色值使用十六进制格式表示,涵盖了蓝色、绿色、橙色、红色、紫色、青色、黄色和棕色等常用色系。这些颜色都是Material Design推荐的标准色,视觉效果协调统一。返回一个Column组件垂直排列标题和颜色选项,crossAxisAlignment.start让内容左对齐。标题文本"选择颜色"告诉用户这个区域的功能,提供清晰的界面指引。

        SizedBox(height: 8.h),
        Wrap(
          spacing: 8.w,
          runSpacing: 8.h,
          children: colors.map((color) {
            final isSelected = color == selectedColor;
            return GestureDetector(
              onTap: () => onColorSelected(color),
              child: Container(
                width: 32.w,

添加8.h的间距后,使用Wrap组件排列颜色选项。Wrap类似于Row,但当空间不足时会自动换行,非常适合展示多个选项。spacing和runSpacing分别设置水平和垂直间距为8像素,让颜色块之间有适当的留白。使用map方法遍历colors数组,为每个颜色创建一个可点击的圆形色块。isSelected变量判断当前颜色是否被选中,用于显示不同的视觉状态。GestureDetector包裹Container实现点击功能,点击时调用onColorSelected回调通知父组件颜色已改变。

                height: 32.h,
                decoration: BoxDecoration(
                  color: Color(int.parse(color.replaceFirst('#', '0xFF'))),
                  borderRadius: BorderRadius.circular(16),
                  border: isSelected ? Border.all(color: Colors.black, width: 2) : null,
                ),
                child: isSelected ? const Icon(Icons.check, color: Colors.white, size: 16) : null,
              ),
            );

Container设置为32x32的正方形,通过BoxDecoration添加样式。color属性将十六进制颜色字符串转换为Color对象,replaceFirst方法将’#'替换为’0xFF’以符合Flutter的颜色格式。borderRadius设置为16创建完美的圆形效果(半径是宽度的一半)。当颜色被选中时,添加2像素宽的黑色边框作为视觉反馈,同时在中心显示白色的勾选图标,这种双重提示让用户清楚知道当前选择。未选中的颜色只显示纯色圆形,保持界面简洁。

          }).toList(),
        ),
      ],
    );
  }
}

map方法返回的是Iterable,需要调用toList()转换为List才能传给Wrap的children。整个颜色选择器组件到此完成,提供了直观的颜色选择体验。

统计信息

显示分类的统计信息。

  Widget _buildStatisticsCard(NoteController controller) {
    final notes = controller.getNotesByCategory(category.id);
    final totalNotes = notes.length;
    final favoriteNotes = notes.where((n) => n.isFavorite).length;
    final totalWords = notes.fold<int>(0, (sum, note) => sum + note.wordCount);
    final avgWords = totalNotes > 0 ? totalWords ~/ totalNotes : 0;

    return Card(
      child: Padding(
        padding: EdgeInsets.all(16.w),

_buildStatisticsCard方法创建统计信息卡片,展示分类的各项数据指标。首先获取当前分类下的所有笔记,然后计算四个关键指标:笔记总数直接使用列表长度,收藏笔记数通过where方法过滤isFavorite为true的笔记,总字数使用fold方法累加所有笔记的字数,平均字数通过总字数除以笔记数计算(使用~/整除运算符)。注意avgWords的计算中加入了totalNotes > 0的判断,避免除零错误。Card组件提供卡片样式的容器,自带阴影和圆角效果,Padding添加16.w的内边距让内容不会紧贴边缘。

        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '统计信息',
              style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 12.h),
            Row(
              children: [
                Expanded(

Column垂直排列统计信息的各个部分,crossAxisAlignment.start让内容左对齐。首先显示"统计信息"标题,使用16.sp的字号和粗体样式,让标题更加醒目。添加12.h的间距后开始显示具体的统计数据。使用Row横向排列统计项,每行显示两个指标,这样可以充分利用屏幕宽度,避免列表过长。Expanded组件让两个统计项平均分配可用空间,确保布局对称美观。

                  child: _StatItem(
                    label: '笔记数量',
                    value: '$totalNotes',
                    icon: Icons.note,
                  ),
                ),
                Expanded(
                  child: _StatItem(
                    label: '收藏数量',
                    value: '$favoriteNotes',
                    icon: Icons.star,

第一行显示笔记数量和收藏数量两个指标。_StatItem是自定义的统计项组件,接收标签、数值和图标三个参数。笔记数量使用note图标,收藏数量使用star图标,图标的选择直观地表达了数据的含义。数值需要转换为字符串才能传递给Text组件显示。两个Expanded确保两个统计项各占一半宽度,即使数值长度不同也能保持对齐。

                  ),
                ),
              ],
            ),
            SizedBox(height: 12.h),
            Row(
              children: [
                Expanded(
                  child: _StatItem(
                    label: '总字数',
                    value: '$totalWords',
                    icon: Icons.text_fields,

添加12.h的间距后显示第二行统计数据。这种分行显示的方式让信息层次清晰,不会显得拥挤。第二行显示总字数和平均字数,这两个指标反映了用户的写作量和笔记的详细程度。总字数使用text_fields图标,这个图标形象地表示文本内容。统计卡片的设计遵循了信息可视化的原则,用图标、数字和文字三种元素组合,让数据一目了然。

                  ),
                ),
                Expanded(
                  child: _StatItem(
                    label: '平均字数',
                    value: '$avgWords',
                    icon: Icons.calculate,
                  ),
                ),
              ],
            ),
          ],
        ),

平均字数使用calculate图标,表示这是一个计算得出的数值。四个统计项采用2x2的网格布局,既紧凑又清晰。每个统计项都使用相同的_StatItem组件,保证了视觉风格的一致性。这种统计信息的展示方式让用户可以快速了解分类的整体情况,对于管理大量笔记非常有帮助。

      ),
    );
  }

统计信息卡片构建完成。这个组件可以灵活地添加到分类详情页面的任何位置。

统计项组件

创建统计项的专用组件。

class _StatItem extends StatelessWidget {
  final String label;
  final String value;
  final IconData icon;

  const _StatItem({
    required this.label,
    required this.value,
    required this.icon,
  });

_StatItem是一个私有的无状态组件(类名以下划线开头表示私有),专门用于显示单个统计指标。它接收三个必需参数:label是统计项的名称(如"笔记数量"),value是具体的数值(如"10"),icon是对应的图标。使用专门的组件而不是直接在父组件中构建UI,这是组件化开发的最佳实践,可以提高代码的复用性和可维护性。如果将来需要修改统计项的样式,只需要修改这一个组件即可,所有使用它的地方都会自动更新。

  
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(12.w),
      child: Column(
        children: [
          Icon(icon, size: 24.sp, color: const Color(0xFF2196F3)),
          SizedBox(height: 4.h),
          Text(
            value,

build方法构建统计项的UI结构。Container添加12.w的内边距,为内容提供呼吸空间。Column垂直排列图标、数值和标签三个元素,这种从上到下的信息层次符合用户的阅读习惯。Icon组件显示传入的图标,大小设置为24.sp,颜色使用主题蓝色(#2196F3),这个颜色在整个应用中保持一致,形成统一的视觉语言。添加4.h的小间距后显示数值,间距不宜过大,保持图标和数值的视觉关联。

            style: TextStyle(
              fontSize: 18.sp,
              fontWeight: FontWeight.bold,
              color: const Color(0xFF2196F3),
            ),
          ),
          SizedBox(height: 2.h),
          Text(
            label,
            style: TextStyle(fontSize: 12.sp, color: Colors.grey),
          ),

数值使用18.sp的大字号和粗体样式,同样使用主题蓝色,这是统计项中最重要的信息,需要最突出的视觉表现。添加2.h的更小间距后显示标签,标签使用12.sp的小字号和灰色,作为辅助说明文字,不会抢夺数值的视觉焦点。这种大小、粗细、颜色的层次变化,创造了清晰的信息优先级,用户可以快速扫视获取关键数据,需要时再查看具体的标签说明。整个统计项的设计简洁而专业,符合现代应用的审美标准。

        ],
      ),
    );
  }
}

统计项组件构建完成。这个小组件虽然简单,但通过精心的样式设计,实现了良好的信息传达效果。

总结

分类详情页面是笔记应用中重要的功能模块。通过本文的介绍,我们实现了一个功能完整的分类详情系统。

关键特性包括:笔记列表展示、创建笔记、编辑分类、搜索、批量操作、统计信息和主题适配。这些功能共同构成了一个专业级的分类管理解决方案。

良好的分类详情设计不仅提供了清晰的内容展示,还提供了丰富的操作功能,让用户可以高效地管理分类中的笔记。


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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐