在这里插入图片描述

每个热爱烹饪的人都有自己的拿手菜,也会不断创作新的菜谱。今天我们要实现我的菜谱管理功能,让用户能够查看、编辑和管理自己创建的所有菜谱。

我的菜谱的设计思路

我的菜谱页面要解决的核心问题是:如何让用户方便地管理大量的菜谱?我选择了列表布局,因为列表能展示更多信息,也方便滚动浏览。

每个菜谱项显示图片、名称和创建时间。图片能让用户快速识别菜谱,创建时间能帮助用户回忆。这些信息足够用户判断是哪道菜,又不会显得太拥挤。

列表项可以点击进入详情页,也可以长按显示操作菜单。这种设计在文件管理应用中很常见,用户很容易理解。点击是查看,长按是操作,符合用户的使用习惯。

创建无状态组件

我的菜谱页面的内容相对固定,使用 StatelessWidget 就够了。

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('我的菜谱')),
      body: ListView.builder(
        padding: EdgeInsets.all(16.w),
        itemCount: 20,
        itemBuilder: (context, index) => _buildItem(index),
      ),
    );
  }

页面结构很简单,就是一个 AppBar 和一个 ListView。AppBar 只有标题,没有其他按钮,保持简洁。

ListView.builder 的 padding 设置为 16.w,让列表内容不要紧贴屏幕边缘。itemCount 现在是固定的 20,实际开发中应该根据数据动态设置。

实现列表项

每个列表项是一个卡片,包含图片、名称和创建时间。

  Widget _buildItem(int index) {
    return Container(
      margin: EdgeInsets.only(bottom: 12.h),
      padding: EdgeInsets.all(12.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
      ),

卡片使用白色背景和圆角。margin 设置为 bottom: 12.h,让卡片之间有间距。padding 设置为 12.w,让内容不要紧贴边缘。

这里没有设置阴影,因为背景色已经是浅灰色了,白色卡片本身就有足够的对比度。如果背景是白色,就需要添加阴影来区分卡片和背景。

      child: Row(
        children: [
          Container(
            width: 80.w,
            height: 80.h,
            decoration: BoxDecoration(
              color: Colors.orange.shade100,
              borderRadius: BorderRadius.circular(8.r),
            ),
            child: Icon(Icons.restaurant, size: 35.sp, color: Colors.orange),
          ),

左侧是菜谱图片的占位符。实际开发中应该显示真实的图片,这里用图标代替。容器大小 80x80,背景色是橙色的浅色版本。

圆角设置为 8.r,比卡片的圆角小一些,形成层次感。图标大小 35.sp,颜色是橙色,和背景色搭配。

          SizedBox(width: 12.w),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('我的菜谱 ${index + 1}', style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.bold)),
                SizedBox(height: 6.h),
                Text('创建于 2024-01-${(index % 28) + 1}', style: TextStyle(fontSize: 11.sp, color: Colors.grey)),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

右侧是菜谱信息。使用 Expanded 让这部分占据剩余空间。菜谱名称使用粗体,字号 15.sp。

创建时间使用灰色,字号 11.sp,表示这是次要信息。时间使用公式 (index % 28) + 1 来生成不同的日期,让数据看起来更真实。

添加点击交互

点击列表项应该跳转到菜谱详情页:

Widget _buildItem(int index) {
  return GestureDetector(
    onTap: () {
      Get.to(() => RecipeDetailPage(recipeId: 'recipe_$index'));
    },
    child: Container(
      // ...
    ),
  );
}

使用 GestureDetector 包裹整个卡片,点击时跳转到详情页。传递菜谱 ID 作为参数,详情页会根据这个 ID 加载对应的菜谱数据。

也可以使用 InkWell 代替 GestureDetector,这样点击时会有水波纹效果。但要注意 InkWell 需要在 Material 组件下才能显示水波纹。

添加长按菜单

长按列表项应该显示操作菜单,比如编辑、删除、分享等:

GestureDetector(
  onTap: () {
    // 跳转到详情页
  },
  onLongPress: () {
    _showActionMenu(context, index);
  },
  child: Container(
    // ...
  ),
)

onLongPress 回调在用户长按时触发。我们调用 _showActionMenu 方法显示操作菜单。

void _showActionMenu(BuildContext context, int index) {
  showModalBottomSheet(
    context: context,
    builder: (context) {
      return SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: Icon(Icons.edit),
              title: Text('编辑'),
              onTap: () {
                Navigator.pop(context);
                Get.to(() => EditRecipePage(recipeId: 'recipe_$index'));
              },
            ),
            ListTile(
              leading: Icon(Icons.share),
              title: Text('分享'),
              onTap: () {
                Navigator.pop(context);
                // 分享菜谱
              },
            ),
            ListTile(
              leading: Icon(Icons.delete, color: Colors.red),
              title: Text('删除', style: TextStyle(color: Colors.red)),
              onTap: () {
                Navigator.pop(context);
                _confirmDelete(context, index);
              },
            ),
          ],
        ),
      );
    },
  );
}

showModalBottomSheet 会从底部弹出一个菜单。SafeArea 确保内容不会被刘海屏或底部横条遮挡。

菜单包含三个选项:编辑、分享和删除。每个选项使用 ListTile,包含图标和文字。点击选项时,先关闭菜单,然后执行对应的操作。

删除选项使用红色,表示这是危险操作。点击后会弹出确认对话框,避免误删。

实现删除确认

删除是危险操作,需要用户确认:

void _confirmDelete(BuildContext context, int index) {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text('确认删除'),
        content: Text('确定要删除这道菜谱吗?删除后无法恢复。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              // 执行删除操作
              Get.snackbar('成功', '菜谱已删除');
            },
            child: Text('删除', style: TextStyle(color: Colors.red)),
          ),
        ],
      );
    },
  );
}

showDialog 显示一个对话框。对话框包含标题、内容和两个按钮。标题是"确认删除",内容说明删除的后果。

两个按钮分别是"取消"和"删除"。取消按钮只是关闭对话框,删除按钮执行删除操作并显示提示。

删除按钮使用红色,再次强调这是危险操作。这种双重确认能有效避免误删。

添加空状态

如果用户还没有创建任何菜谱,需要显示空状态:

if (recipes.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.restaurant_menu, size: 64.sp, color: Colors.grey),
        SizedBox(height: 16.h),
        Text('还没有创建菜谱', style: TextStyle(color: Colors.grey)),
        SizedBox(height: 8.h),
        ElevatedButton(
          onPressed: () => Get.to(() => CreateRecipePage()),
          child: Text('创建第一道菜谱'),
        ),
      ],
    ),
  );
}

空状态要友好,不要让用户觉得是出错了。使用图标和文字说明,并提供一个按钮引导用户创建菜谱。

图标使用灰色,大小 64.sp,比较大,起到装饰作用。文字也是灰色,表示这是提示信息。按钮使用应用的主题色,吸引用户点击。

添加搜索功能

如果菜谱很多,可以添加搜索功能:

AppBar(
  title: TextField(
    decoration: InputDecoration(
      hintText: '搜索我的菜谱...',
      border: InputBorder.none,
      prefixIcon: Icon(Icons.search),
    ),
    onChanged: (value) {
      // 过滤菜谱列表
    },
  ),
)

搜索框放在 AppBar 的 title 位置,用户输入关键词时实时过滤列表。这需要把 StatelessWidget 改成 StatefulWidget,用一个变量来存储过滤后的列表。

添加排序功能

用户可能想按不同的方式排序菜谱,比如按创建时间、按名称等:

AppBar(
  title: Text('我的菜谱'),
  actions: [
    PopupMenuButton<String>(
      onSelected: (value) {
        // 根据选择的方式排序
      },
      itemBuilder: (context) => [
        PopupMenuItem(value: 'time', child: Text('按时间排序')),
        PopupMenuItem(value: 'name', child: Text('按名称排序')),
      ],
    ),
  ],
)

PopupMenuButton 会在右上角显示一个菜单按钮,点击后弹出菜单。用户选择排序方式后,重新排序列表并刷新页面。

添加批量操作

如果用户想删除多道菜谱,可以添加批量操作功能:

AppBar(
  title: isSelectionMode ? Text('已选择 $selectedCount 项') : Text('我的菜谱'),
  leading: isSelectionMode ? IconButton(
    icon: Icon(Icons.close),
    onPressed: () {
      // 退出选择模式
    },
  ) : null,
  actions: isSelectionMode ? [
    IconButton(
      icon: Icon(Icons.delete),
      onPressed: () {
        // 批量删除
      },
    ),
  ] : [
    IconButton(
      icon: Icon(Icons.select_all),
      onPressed: () {
        // 进入选择模式
      },
    ),
  ],
)

选择模式下,AppBar 显示已选择的数量和批量操作按钮。用户可以勾选多个菜谱,然后一次性删除。

这需要在每个列表项前面添加复选框,并用一个 Set 来存储选中的菜谱 ID。

总结

我的菜谱管理功能使用列表布局,每个菜谱显示图片、名称和创建时间。点击可以查看详情,长按可以显示操作菜单。

通过合理的交互设计,我们让菜谱管理既方便又安全。用户可以轻松查看和编辑菜谱,删除操作有双重确认,避免误删。

下一篇文章我们将实现收藏菜谱功能,让用户能够收藏喜欢的菜谱。


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

Logo

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

更多推荐