今天吃什么应用


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

一、项目概述

运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1 应用简介

今天吃什么是一款专为"午餐选择困难症"用户打造的美食决策助手。每当饭点来临,面对琳琅满目的美食选择,你是否也曾陷入"不知道吃什么"的困境?这款应用通过趣味性的"摇一摇"交互方式,让命运替你做出决定,彻底终结选择困难。

应用以温暖的橙红色为主色调,营造出食欲满满的视觉氛围。内置十大美食分类,涵盖中餐、西餐、日料、韩餐、快餐、小吃、甜点、饮品、火锅、烧烤等常见选择,收录27道经典美食。每次决定都会记录在历史中,并生成消费统计和偏好分析,帮助用户了解自己的饮食习惯。

1.2 核心功能

功能模块 功能描述 实现方式
摇一摇决定 随机选择美食 抖动动画+随机算法
快速分类 十大美食分类 分类标签卡片
食物库 美食数据库展示 分组列表
历史记录 决定历史回顾 时间线卡片
统计分析 消费和偏好统计 数据可视化

1.3 美食分类

序号 分类名称 图标 颜色 代表美食
1 中餐 🥢 #E53935 宫保鸡丁、麻婆豆腐、红烧肉
2 西餐 🍝 #1E88E5 意大利面、牛排、披萨
3 日料 🍣 #FDD835 寿司、拉面
4 韩餐 🥘 #43A047 石锅拌饭、韩式烤肉
5 快餐 🍔 #FF9800 汉堡、炸鸡
6 小吃 🍢 #8E24AA 麻辣烫、煎饼果子、肉夹馍
7 甜点 🍰 #EC407A 蛋糕、冰淇淋
8 饮品 🧋 #00ACC1 奶茶、咖啡
9 火锅 🍲 #D32F2F 火锅、串串香
10 烧烤 🍖 #6D4C41 烤肉、烤串

1.4 技术栈

技术领域 技术选型 版本要求
开发框架 Flutter >= 3.0.0
编程语言 Dart >= 2.17.0
设计规范 Material Design 3 -
状态管理 setState -
动画系统 AnimationController -
目标平台 鸿蒙OS / Web API 21+

1.5 项目结构

lib/
└── main_what_to_eat.dart
    ├── WhatToEatApp              # 应用入口
    ├── FoodCategory              # 美食分类枚举
    ├── FoodItem                  # 美食数据模型
    ├── EatHistory                # 决定历史模型
    ├── WhatToEatHomePage         # 主页面(底部导航)
    ├── _buildDecidePage          # 决定页面
    ├── _buildFoodLibraryPage     # 食物库页面
    ├── _buildHistoryPage         # 历史页面
    └── _buildStatsPage           # 统计页面

二、系统架构

2.1 整体架构图

Data Layer

Presentation Layer

主页面
WhatToEatHomePage

决定页

食物库页

历史页

统计页

摇一摇按钮

快速分类

结果弹窗

分类分组

食物列表

历史卡片

概览统计

分类偏好

消费统计

FoodCategory
分类枚举

FoodItem
美食模型

EatHistory
历史模型

2.2 类图设计

uses

manages

records

contains

WhatToEatApp

+Widget build()

«enumeration»

FoodCategory

+chinese 中餐

+western 西餐

+japanese 日料

+korean 韩餐

+fastFood 快餐

+snack 小吃

+dessert 甜点

+drink 饮品

+hotpot 火锅

+bbq 烧烤

+String label

+String emoji

+Color color

FoodItem

+String id

+String name

+FoodCategory category

+String emoji

+int calories

+double price

+List<String> tags

EatHistory

+String id

+FoodItem food

+DateTime decidedAt

+bool? satisfied

WhatToEatHomePage

-int _selectedIndex

-List<FoodItem> _foodList

-List<EatHistory> _history

-FoodItem? _currentFood

-bool _isShaking

-bool _isDeciding

-AnimationController _shakeController

-AnimationController _spinController

-AnimationController _bounceController

-AnimationController _confettiController

+void _startShake()

+void _decideFood()

+void _showResultDialog()

+Map<String,dynamic> _getStatistics()

2.3 页面导航流程

决定

食物库

历史

统计

换一个

就这个

应用启动

主页面

底部导航

决定页面

食物库页面

历史页面

统计页面

点击摇一摇

抖动动画

随机选择

结果弹窗

用户选择

确认决定

点击分类标签

分类随机选择

浏览食物库

点击食物

2.4 摇一摇决策流程时序图

历史记录 结果弹窗 随机算法 抖动动画 决定页 用户 历史记录 结果弹窗 随机算法 抖动动画 决定页 用户 loop [15次抖动] 点击摇一摇按钮 设置_isShaking=true 播放抖动效果 获取随机食物 返回随机美食 更新显示食物 设置_isShaking=false 显示结果底部弹窗 展示最终决定 点击"就这个" 保存决定记录 显示确认提示

三、核心模块设计

3.1 数据模型设计

3.1.1 美食分类枚举 (FoodCategory)
enum FoodCategory {
  chinese('中餐', '🥢', Color(0xFFE53935)),
  western('西餐', '🍝', Color(0xFF1E88E5)),
  japanese('日料', '🍣', Color(0xFFFDD835)),
  korean('韩餐', '🥘', Color(0xFF43A047)),
  fastFood('快餐', '🍔', Color(0xFFFF9800)),
  snack('小吃', '🍢', Color(0xFF8E24AA)),
  dessert('甜点', '🍰', Color(0xFFEC407A)),
  drink('饮品', '🧋', Color(0xFF00ACC1)),
  hotpot('火锅', '🍲', Color(0xFFD32F2F)),
  bbq('烧烤', '🍖', Color(0xFF6D4C41));

  final String label;
  final String emoji;
  final Color color;

  const FoodCategory(this.label, this.emoji, this.color);
}
3.1.2 美食数据模型 (FoodItem)
class FoodItem {
  final String id;              // 唯一标识
  final String name;            // 美食名称
  final FoodCategory category;  // 所属分类
  final String emoji;           // 表情图标
  final int calories;           // 卡路里
  final double price;           // 参考价格
  final List<String> tags;      // 标签列表
}
3.1.3 决定历史模型 (EatHistory)
class EatHistory {
  final String id;              // 唯一标识
  final FoodItem food;          // 选中的美食
  final DateTime decidedAt;     // 决定时间
  final bool? satisfied;        // 是否满意(可选)
}
3.1.4 美食分类分布
19% 15% 11% 11% 7% 7% 7% 7% 7% 7% 食物库分类分布 中餐 西餐 快餐 日料 韩餐 小吃 饮品 甜点 火锅 烧烤

3.2 页面结构设计

3.2.1 主页面布局

WhatToEatHomePage

IndexedStack

决定页

食物库页

历史页

统计页

NavigationBar

决定 Tab

食物库 Tab

历史 Tab

统计 Tab

3.2.2 决定页面结构

决定页面

SliverAppBar

内容区域

渐变背景

应用标题

摇一摇按钮

快速分类卡片

最近决定卡片

抖动动画

手机图标

随机食物emoji

3.2.3 结果弹窗结构

结果弹窗

拖动指示器

食物图标

食物名称

分类标签

信息展示

操作按钮

卡路里

参考价格

换一个按钮

就这个按钮

3.3 动画系统设计

点击摇一摇

15次抖动完成

弹出结果弹窗

确认决定

换一个

空闲状态

抖动中

决定中

显示结果

每100ms切换一次食物
同时播放抖动效果

弹性缩放动画
显示最终决定


四、UI设计规范

4.1 配色方案

应用采用温暖的橙红色为主色调,营造食欲满满的视觉氛围:

颜色类型 色值 用途
主色 #FF6B35 导航、按钮、强调元素
渐变起始 #FF6B35 (40%透明) 头部渐变
渐变结束 #FF6B35 (20%透明) 头部渐变
中餐色 #E53935 红色
西餐色 #1E88E5 蓝色
日料色 #FDD835 黄色
韩餐色 #43A047 绿色
快餐色 #FF9800 橙色
小吃色 #8E24AA 紫色
甜点色 #EC407A 粉色
饮品色 #00ACC1 青色
火锅色 #D32F2F 深红
烧烤色 #6D4C41 棕色

4.2 字体规范

元素 字号 字重 颜色
应用标题 28px Bold #FFFFFF
食物名称 32px Bold 分类色
分类标签 13px Medium 分类色
卡路里/价格 14px Medium #000000
历史时间 11px Regular #757575
统计数值 24px Bold #000000

4.3 组件规范

4.3.1 摇一摇按钮
        ┌─────────────────┐
        │                 │
        │    ┌───────┐    │
        │    │  📱   │    │
        │    │       │    │
        │    │摇一摇 │    │
        │    └───────┘    │
        │                 │
        └─────────────────┘
           圆形白色背景
          橙色阴影效果
4.3.2 分类标签
┌──────────────────┐
│  🥢  中餐        │
└──────────────────┘
   圆角矩形背景
   分类色+透明度
4.3.3 结果弹窗
┌─────────────────────────────────────┐
│              ──────                 │
│                                     │
│           ┌─────────┐              │
│           │   🐔    │              │
│           └─────────┘              │
│                                     │
│            今天吃                   │
│                                     │
│          宫保鸡丁                   │
│                                     │
│        ┌──────────┐                │
│        │ 🥢 中餐  │                │
│        └──────────┘                │
│                                     │
│    🔥 450卡    💰 ¥35              │
│                                     │
│  ┌─────────┐  ┌─────────┐         │
│  │ 换一个  │  │ 就这个  │         │
│  └─────────┘  └─────────┘         │
│                                     │
└─────────────────────────────────────┘

五、核心功能实现

5.1 摇一摇随机选择

void _startShake() {
  if (_isShaking || _isDeciding) return;

  setState(() {
    _isShaking = true;
    _isDeciding = true;
  });

  int shakeCount = 0;
  const maxShakes = 15;

  void doShake() {
    if (shakeCount >= maxShakes) {
      _decideFood();
      return;
    }

    setState(() {
      _currentFood = _foodList[_random.nextInt(_foodList.length)];
    });

    _shakeController.forward(from: 0);

    shakeCount++;
    Future.delayed(const Duration(milliseconds: 100), doShake);
  }

  doShake();
}

5.2 决定结果处理

void _decideFood() {
  setState(() {
    _isShaking = false;
  });

  _bounceController.forward(from: 0);
  _confettiController.forward(from: 0);

  final history = EatHistory(
    id: DateTime.now().millisecondsSinceEpoch.toString(),
    food: _currentFood!,
    decidedAt: DateTime.now(),
  );

  setState(() {
    _history.insert(0, history);
    _isDeciding = false;
  });

  _showResultDialog();
}

5.3 结果弹窗实现

Widget _buildResultSheet() {
  final food = _currentFood!;

  return DraggableScrollableSheet(
    initialChildSize: 0.6,
    maxChildSize: 0.8,
    minChildSize: 0.4,
    builder: (context, scrollController) => Container(
      decoration: const BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
      ),
      child: ListView(
        controller: scrollController,
        padding: const EdgeInsets.all(24),
        children: [
          // 拖动指示器
          Center(
            child: Container(
              width: 40,
              height: 4,
              decoration: BoxDecoration(
                color: Colors.grey[300],
                borderRadius: BorderRadius.circular(2),
              ),
            ),
          ),
          const SizedBox(height: 24),
          // 食物图标(带弹性动画)
          Center(
            child: AnimatedBuilder(
              animation: _bounceController,
              builder: (context, child) {
                return Transform.scale(
                  scale: 1.0 + _bounceController.value * 0.2,
                  child: Container(
                    width: 120,
                    height: 120,
                    decoration: BoxDecoration(
                      color: food.category.color.withValues(alpha: 0.2),
                      shape: BoxShape.circle,
                    ),
                    child: Center(
                      child: Text(food.emoji, style: const TextStyle(fontSize: 56)),
                    ),
                  ),
                );
              },
            ),
          ),
          // 食物名称和分类
          const SizedBox(height: 16),
          Center(
            child: Text('今天吃', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
          ),
          const SizedBox(height: 8),
          Center(
            child: Text(
              food.name,
              style: TextStyle(
                fontSize: 32,
                fontWeight: FontWeight.bold,
                color: food.category.color,
              ),
            ),
          ),
          // 操作按钮
          Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: () {
                    Navigator.pop(context);
                    _startShake();
                  },
                  icon: const Icon(Icons.refresh),
                  label: const Text('换一个'),
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: FilledButton.icon(
                  onPressed: () {
                    Navigator.pop(context);
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(
                        content: Text('决定了!今天吃${food.name}'),
                        backgroundColor: food.category.color,
                      ),
                    );
                  },
                  icon: const Icon(Icons.check),
                  label: const Text('就这个'),
                ),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

5.4 统计数据计算

Map<String, dynamic> _getStatistics() {
  final now = DateTime.now();
  final thisMonth = _history.where((h) =>
      h.decidedAt.year == now.year && h.decidedAt.month == now.month).toList();

  final categoryCounts = <FoodCategory, int>{};
  for (var category in FoodCategory.values) {
    categoryCounts[category] = _history.where((h) => h.food.category == category).length;
  }

  final totalExpense = _history.fold(0.0, (sum, h) => sum + h.food.price);
  final avgExpense = _history.isNotEmpty ? totalExpense / _history.length : 0.0;

  return {
    'total': _history.length,
    'thisMonth': thisMonth.length,
    'categoryCounts': categoryCounts,
    'totalExpense': totalExpense,
    'avgExpense': avgExpense,
  };
}

5.5 快速分类选择

Widget _buildQuickCategories() {
  return Container(
    padding: const EdgeInsets.all(20),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(20),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withValues(alpha: 0.05),
          blurRadius: 10,
          offset: const Offset(0, 5),
        ),
      ],
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '快速分类',
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 16),
        Wrap(
          spacing: 10,
          runSpacing: 10,
          children: FoodCategory.values.take(6).map((category) {
            return GestureDetector(
              onTap: () {
                final foods = _foodList.where((f) => f.category == category).toList();
                if (foods.isNotEmpty) {
                  setState(() {
                    _currentFood = foods[_random.nextInt(foods.length)];
                  });
                  _showResultDialog();
                }
              },
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
                decoration: BoxDecoration(
                  color: category.color.withValues(alpha: 0.1),
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text(category.emoji),
                    const SizedBox(width: 6),
                    Text(
                      category.label,
                      style: TextStyle(
                        color: category.color,
                        fontWeight: FontWeight.w500,
                        fontSize: 13,
                      ),
                    ),
                  ],
                ),
              ),
            );
          }).toList(),
        ),
      ],
    ),
  );
}

六、交互设计

6.1 摇一摇交互流程

结果弹窗 随机算法 抖动动画 摇一摇按钮 用户 结果弹窗 随机算法 抖动动画 摇一摇按钮 用户 loop [15次 (每次100ms)] 点击 禁用重复点击 播放抖动 获取随机食物 返回食物 更新显示emoji 弹出结果 展示最终决定 换一个 重新开始 就这个 关闭并提示

6.2 分类快速选择流程

换一个

就这个

点击分类标签

筛选该分类食物

有食物?

随机选择一个

弹出结果弹窗

无操作

用户操作

重新随机选择

确认决定

保存历史

显示提示

6.3 页面切换状态

点击食物库Tab

点击历史Tab

点击统计Tab

点击决定Tab

点击历史Tab

点击统计Tab

点击决定Tab

点击食物库Tab

点击统计Tab

点击决定Tab

点击食物库Tab

点击历史Tab

点击食物项

点击查看全部

决定页

食物库页

历史页

统计页


七、扩展功能规划

7.1 后续版本规划

2024-01-07 2024-01-14 2024-01-21 2024-01-28 2024-02-04 2024-02-11 2024-02-18 2024-02-25 2024-03-03 2024-03-10 2024-03-17 摇一摇决定 十大分类 食物库展示 历史记录 统计分析 自定义食物 排除功能 附近餐厅 营养分析 饮食建议 分享功能 V1.0 基础版本 V1.1 增强版本 V1.2 进阶版本 今天吃什么开发计划

7.2 功能扩展建议

7.2.1 自定义食物

用户个性化管理:

  • 添加自定义美食
  • 设置偏好权重
  • 标记不喜欢的食物
7.2.2 排除功能

临时排除选项:

  • 今天不想吃火锅
  • 最近吃过的不再推荐
  • 临时排除列表管理
7.2.3 附近餐厅

位置服务集成:

  • 显示附近餐厅
  • 一键导航
  • 优惠信息推送

八、注意事项

8.1 开发注意事项

  1. 随机算法:使用dart:math的Random类生成随机数,确保每次决定公平

  2. 动画控制:四个AnimationController需要在dispose时释放

  3. 状态管理:使用setState管理本地状态,注意_isShaking和_isDeciding的互斥

  4. 表单验证:结果弹窗使用DraggableScrollableSheet实现

8.2 常见问题

问题 原因 解决方案
抖动动画不播放 控制器未初始化 检查initState
重复点击摇一摇 状态未锁定 检查_isDeciding
结果不记录 历史列表未更新 检查setState调用
分类筛选为空 过滤条件问题 检查where条件

8.3 使用提示

🍜 今天吃什么使用小贴士 🍜

选择困难时,让命运替你做决定。
不要纠结,相信第一直觉。
如果不喜欢结果,就换一个!
记住,吃饭本身就是一件快乐的事。


九、运行说明

9.1 环境要求

环境 版本要求
Flutter SDK >= 3.0.0
Dart SDK >= 2.17.0
鸿蒙OS API 21+

9.2 运行命令

# 查看可用设备
flutter devices

# 运行到Web服务器
flutter run -d web-server -t lib/main_what_to_eat.dart --web-port 8121

# 运行到鸿蒙设备
flutter run -d 127.0.0.1:5555 lib/main_what_to_eat.dart

# 运行到Windows
flutter run -d windows -t lib/main_what_to_eat.dart

# 代码分析
flutter analyze lib/main_what_to_eat.dart

十、总结

今天吃什么应用通过趣味性的"摇一摇"交互方式,为选择困难症患者提供了一个轻松愉快的美食决策工具。应用内置十大美食分类,收录27道经典美食,每次决定都会记录在历史中,并生成消费统计和偏好分析。

核心功能涵盖摇一摇决定、快速分类、食物库、历史记录、统计分析五大模块。摇一摇决定采用抖动动画过渡,增加趣味性和仪式感;快速分类支持一键选择特定类型美食;食物库以分组列表形式展示所有美食;历史记录按时间线展示所有决定;统计分析提供消费概览和偏好分析。

应用采用Material Design 3设计规范,以温暖的橙红色为主色调,营造食欲满满的视觉氛围。通过本应用,希望能够帮助用户摆脱"今天吃什么"的选择困难,轻松愉快地享受每一餐。

让每一餐都充满期待

Logo

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

更多推荐