diff --git a/.trae/rules/design-rules.md b/.trae/rules/design-rules.md
index 2c92f33..eda6506 100644
--- a/.trae/rules/design-rules.md
+++ b/.trae/rules/design-rules.md
@@ -65,27 +65,7 @@ css
平板 768px – 1024px 双列
桌面 1280px – 1920px 多列
大屏 >1920px 保持多列,容器居中
-css
-.container {
- max-width: 1200px;
- margin: 0 auto;
- padding: var(--space-3);
-}
-/* 平板及以上 */
-@media (min-width: 768px) {
- .grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: var(--space-4);
- }
-}
-
-@media (min-width: 1024px) {
- .grid {
- grid-template-columns: repeat(3, 1fr);
- }
-}
折叠屏适配 (600px – 900px)
自动切换双列布局。
@@ -94,16 +74,7 @@ css
内容可重新流式排列(利用 order 或网格调整)。
页面骨架
-html
-
-
-
-
-
-
-
-
-
+
Main 区域设置 overflow-y: auto,确保内部滚动。
所有页面必须使用统一 AppLayout 组件,禁止各自实现布局。
@@ -152,43 +123,7 @@ text
3. 设计系统:必须使用统一CSS变量,禁止自定义颜色。
4. 修改规则:如要改布局,必须整体重构,禁止只改局部组件。
5. 代码结构:按 layout / components / pages / styles 组织。
-安全重构流程
-分析当前结构 → 2. 输出修改计划 → 3. 等待确认 → 4. 执行修改
-禁止事项
-使用 overflow:hidden 解决布局问题。
-
-直接修改入口文件(应只保留路由、Provider、样式引入)。
-
-一次性改动过多文件(应分阶段:设计变量 → 布局容器 → 组件统一)。
-
-7. 存量项目接入设计系统(安全策略)
-若项目已上线,采用渐进式重构,避免大规模报错:
-
-阶段1:添加 design-tokens.css,只定义变量,不修改任何页面。
-
-阶段2:创建统一 AppLayout 组件,新页面强制使用,旧页面逐步包裹。
-
-阶段3:建立 /dev-theme-test 页面,集中测试所有组件样式。
-
-阶段4:按页面逐一迁移,不动业务逻辑。
-
-核心思想:先视觉统一,再结构统一;不动业务,只换皮肤。
-
-8. 性能优化与日志
-拆分函数:避免过大的 Page 对象,逻辑抽离到独立模块。
-
-减少冗余:使用 wx.setStorageSync 缓存必要数据,避免重复请求。
-
-日志分级:
-
-console.info 记录关键流程(如点赞成功)。
-
-console.warn 记录异常但不影响使用。
-
-console.error 记录错误,并考虑上报。
-
-README更新:每次功能迭代后同步更新文档,包括接口变更、存储字段说明。
9. 通用建议
页面路径:新增页面必须在 app.json 中声明。
diff --git a/.trae/rules/design-ui.md b/.trae/rules/design-ui.md
new file mode 100644
index 0000000..6677211
--- /dev/null
+++ b/.trae/rules/design-ui.md
@@ -0,0 +1,178 @@
+【设计系统】
+必须遵循统一 design system:
+- 颜色
+- 字体
+- 间距
+- 圆角
+- 阴影
+- 按钮样式
+
+【修改规则】
+如果需要改布局:
+必须重构整体 layout,
+不能只修改一个组件。
+🏗 二、标准项目结构(强烈推荐)
+
+让 AI 按这个结构生成代码:
+
+/src
+ /layout
+ /components
+ /pages
+ /styles
+ /design-system
+
+Prompt 加一句:
+
+请按以下结构组织代码:
+layout / components / pages / styles。
+
+
+
+
+
+所有页面必须使用统一设计系统变量,
+禁止自定义颜色。
+必须使用 CSS 变量或主题系统。
+
+例如:
+
+:root {
+ --primary: #2563eb;
+ --background: #f8fafc;
+ --text: #0f172a;
+}
+📱 四、响应式统一规则模板
+
+如果页面需要适配:
+
+移动端 + 平板 + 桌面
+
+加这段:
+
+必须使用响应式设计。
+
+断点:
+- 640px
+- 768px
+- 1024px
+- 1280px
+
+小屏幕:单列布局
+中屏幕:双列布局
+大屏幕:多列布局
+🪟 五、桌面专用页面模板
+
+如果某些页面 不需要响应式:
+
+该页面为 Desktop-only。
+
+设计基准:1280px。
+
+不需要 mobile 适配。
+不需要 media query。
+
+这样 AI 不会乱写。
+
+🔄 六、防止 AI 只改局部的“强制整体重构模板”
+
+这是你刚才说的核心问题。
+
+用这个:
+
+如果需要修改布局:
+
+1. 先分析当前整个页面结构
+2. 输出完整布局结构图
+3. 制定整体重构方案
+4. 然后一次性修改所有相关区域
+5. 禁止只修改单个组件
+
+这句话非常关键。
+
+🧩 七、强制布局骨架(最稳定方式)
+
+让 AI 必须使用这种结构:
+
+App
+ ├ Header
+ ├ Sidebar
+ ├ Main
+ └ Footer
+
+Prompt:
+
+页面必须采用标准 App Layout 结构。
+main 区域负责滚动。
+header 和 sidebar 为固定结构。
+🛑 八、禁止错误写法(必须写)
+禁止:
+- position: absolute 进行整体布局
+- 固定 px 宽度
+- overflow:hidden 用于解决布局问题
+- 使用 margin 负值修复结构
+🧱 九、AI 写页面的标准流程(最重要)
+
+以后让 AI 改布局,一定这样写:
+
+步骤:
+
+1. 分析当前布局
+2. 给出修改计划
+3. 等我确认
+4. 再修改代码
+
+这样可以避免:
+
+无限循环改局部
+
+进度为 0
+
+越改越乱
+
+🚀 十、真正专业团队的做法
+
+大型团队通常会:
+
+1️⃣ 先写 Design System
+2️⃣ 再写 Layout System
+3️⃣ 再写 Component Library
+4️⃣ 最后写 Pages
+
+AI 也必须按这个顺序。
+
+💎 十一、终极万能 Prompt(推荐收藏)
+
+你可以直接用这个:
+
+请作为高级前端架构师开发页面。
+
+要求:
+
+- 优先设计整体布局结构
+- 使用 flex/grid
+- 响应式设计
+- 使用统一设计系统
+- 不允许固定宽度
+- 不允许 horizontal overflow
+- 修改布局时必须整体重构
+- 不允许只改局部组件
+- 输出清晰结构
+- 代码必须可维护
+🎯 最后总结
+
+解决你所有问题的核心只有三句话:
+
+Layout First
+Design System First
+Plan Before Code
+
+只要做到这三点:
+
+不会溢出
+
+不会错位
+
+不会风格混乱
+
+不会局部修改失控
diff --git a/.trae/rules/layout-rules.md b/.trae/rules/layout-rules.md
index d015716..a6b03ad 100644
--- a/.trae/rules/layout-rules.md
+++ b/.trae/rules/layout-rules.md
@@ -168,197 +168,3 @@ Large Screen: >1920px
6. 页面必须支持窗口缩放
7. 小屏自动单列,大屏多列
-【设计系统】
-必须遵循统一 design system:
-- 颜色
-- 字体
-- 间距
-- 圆角
-- 阴影
-- 按钮样式
-
-【修改规则】
-如果需要改布局:
-必须重构整体 layout,
-不能只修改一个组件。
-🏗 二、标准项目结构(强烈推荐)
-
-让 AI 按这个结构生成代码:
-
-/src
- /layout
- /components
- /pages
- /styles
- /design-system
-
-Prompt 加一句:
-
-请按以下结构组织代码:
-layout / components / pages / styles。
-
-这样多个 AI 写的代码天然统一。
-
-🎨 三、统一风格(解决色调不一致)
-
-多个 AI 最大问题是:
-
-一个用蓝色
-
-一个用紫色
-
-一个按钮圆角不同
-
-一个阴影不同
-
-解决方法:强制使用设计系统。
-
-Prompt 加:
-
-所有页面必须使用统一设计系统变量,
-禁止自定义颜色。
-必须使用 CSS 变量或主题系统。
-
-例如:
-
-:root {
- --primary: #2563eb;
- --background: #f8fafc;
- --text: #0f172a;
-}
-📱 四、响应式统一规则模板
-
-如果页面需要适配:
-
-移动端 + 平板 + 桌面
-
-加这段:
-
-必须使用响应式设计。
-
-断点:
-- 640px
-- 768px
-- 1024px
-- 1280px
-
-小屏幕:单列布局
-中屏幕:双列布局
-大屏幕:多列布局
-🪟 五、桌面专用页面模板
-
-如果某些页面 不需要响应式:
-
-该页面为 Desktop-only。
-
-设计基准:1280px。
-
-不需要 mobile 适配。
-不需要 media query。
-
-这样 AI 不会乱写。
-
-🔄 六、防止 AI 只改局部的“强制整体重构模板”
-
-这是你刚才说的核心问题。
-
-用这个:
-
-如果需要修改布局:
-
-1. 先分析当前整个页面结构
-2. 输出完整布局结构图
-3. 制定整体重构方案
-4. 然后一次性修改所有相关区域
-5. 禁止只修改单个组件
-
-这句话非常关键。
-
-🧩 七、强制布局骨架(最稳定方式)
-
-让 AI 必须使用这种结构:
-
-App
- ├ Header
- ├ Sidebar
- ├ Main
- └ Footer
-
-Prompt:
-
-页面必须采用标准 App Layout 结构。
-main 区域负责滚动。
-header 和 sidebar 为固定结构。
-🛑 八、禁止错误写法(必须写)
-禁止:
-- position: absolute 进行整体布局
-- 固定 px 宽度
-- overflow:hidden 用于解决布局问题
-- 使用 margin 负值修复结构
-🧱 九、AI 写页面的标准流程(最重要)
-
-以后让 AI 改布局,一定这样写:
-
-步骤:
-
-1. 分析当前布局
-2. 给出修改计划
-3. 等我确认
-4. 再修改代码
-
-这样可以避免:
-
-无限循环改局部
-
-进度为 0
-
-越改越乱
-
-🚀 十、真正专业团队的做法
-
-大型团队通常会:
-
-1️⃣ 先写 Design System
-2️⃣ 再写 Layout System
-3️⃣ 再写 Component Library
-4️⃣ 最后写 Pages
-
-AI 也必须按这个顺序。
-
-💎 十一、终极万能 Prompt(推荐收藏)
-
-你可以直接用这个:
-
-请作为高级前端架构师开发页面。
-
-要求:
-
-- 优先设计整体布局结构
-- 使用 flex/grid
-- 响应式设计
-- 使用统一设计系统
-- 不允许固定宽度
-- 不允许 horizontal overflow
-- 修改布局时必须整体重构
-- 不允许只改局部组件
-- 输出清晰结构
-- 代码必须可维护
-🎯 最后总结
-
-解决你所有问题的核心只有三句话:
-
-Layout First
-Design System First
-Plan Before Code
-
-只要做到这三点:
-
-不会溢出
-
-不会错位
-
-不会风格混乱
-
-不会局部修改失控
-
-多 AI 开发也能统一
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 6282f36..97c3030 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,6 +3,10 @@
优先使用ios风格的组件,若Cupertino无对应组件 再使用material
每个文件头部需要增加标准注释,创建时间 更新时间 名称 作用 上次更新内容,代码部分 分类和方法也需要注释
+复杂功能需要写spec文档,包含功能描述、界面设计、交互逻辑、接口文档等,开发完成后删除spec文档
+遇到难解决的问题时,也需要写文档记录,方便后续开发
+api接口部分,可在本地使用接口请求验证,确保接口正常响应数据
+
你现在是苹果前端工程师,这个项目经过多人之手,不同的人设计略有差异,
请设计风格跟苹果集团一体的页面,如果风格不一致我就换其他 ai 了
软件风格需要图文并茂,尽量使用icon,若无icon则使用通用的emoji代替
@@ -25,4 +29,5 @@ Flutter项目 优先 处理状态 组件和布局要求响应式
## 纲领约束
- 暂不开发注册登录功能(优先级最低,当前阶段不涉及用户认证体系)
+- 暂不开消息通知功能,(后续可能使用邮箱实现)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4942a1..69df54f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,258 +2,204 @@
All notable changes to this project will be documented in this file.
-## [0.54.0] - 2026-04-10
+## [0.62.1] - 2026-04-10
-### Fixed — 今天吃什么动态筛选卡死闪退
+### Fixed — Linter 警告清理
-- 🐛 **动态筛选卡死闪退** — 修复3个导致闪退的根因
- - `what_to_eat_controller.dart` — `_loadOptions()` 无 try-catch,初始化失败导致 Controller 崩溃
- - `what_to_eat_repository.dart` — `as Map` 强转崩溃,API 返回非标准结构时抛 TypeError
- - `what_to_eat_page.dart` — 无加载状态管理,数据未加载完时用户操作触发空指针
+- 🧹 **代码规范警告修复**
+ - `glass_animations.dart` - null 检查语法修复
+ - `nutrition_center_page.dart` - 字符串插值优化
+ - `favorites_page.dart` - separatorBuilder 参数规范化
+ - `tools_center_page.dart` - separatorBuilder 参数规范化
+ - `ingredient_detail_page.dart` - separatorBuilder 参数规范化
+ - `skeleton_loader.dart` - separatorBuilder 参数规范化
+ - `meal_time_recommend_page.dart` - separatorBuilder + 字符串插值修复
+ - `meal_planner_page.dart` - separatorBuilder 参数规范化
+ - `allergen_checker_page.dart` - separatorBuilder 参数规范化
-- 🐛 **API 超时无反馈** — 添加超时保护
- - `what_to_eat_controller.dart` — `_loadOptionsSafe()` 添加 15s 超时
- - `what_to_eat_controller.dart` — `roll()` 添加 12s 超时,超时返回空列表而非崩溃
- - `_actionRepository.view()` 包裹 try-catch,防止副作用崩溃
+### Added — 代码分析与风险评估
-### Added — 加载等待动画
+- 📋 **CODE_ANALYSIS.md** 文档新增
+ - 闪退卡死风险点分析(高/中/低风险分级)
+ - 性能优化机会清单
+ - 新功能建议与优先级
-- ✨ **筛选选项加载动画** — `what_to_eat_page.dart`
- - 页面初始化时显示 `CupertinoActivityIndicator` + "正在加载筛选选项…"
- - 加载失败显示错误页面 + "重新加载"按钮
- - 分类/标签数据加载中显示局部 loading + 提示文字
+## [0.62.0] - 2026-04-10
-- ✨ **随机选择加载动画** — `what_to_eat_page.dart`
- - 点击"随机选择"按钮后,结果卡片区域显示 `CupertinoActivityIndicator` + "正在为您挑选菜谱…"
- - 按钮文字切换为"挑选中…" + 白色旋转指示器
- - "换一个"按钮在加载期间隐藏
+### Fixed — 营养中心崩溃修复
-### Changed — Repository 数据解析安全化
+- 🐛 **营养中心报告按钮卡死闪退** — `nutrition_center_page.dart` / `nutrition_report_page.dart`
+ - 添加 MealRecordController 初始化错误处理
+ - 添加 null 检查,避免空指针异常
+ - 导航时添加 try-catch 错误捕获
+ - 显示友好的错误提示页面
-- 🔧 `what_to_eat_repository.dart` — 所有 `as Map` 替换为 `_safeMap()` 安全解析
- - 新增 `_safeMap()` 方法:支持 `Map` 和 `Map` 两种类型
- - 新增 `_parseRecipes()` 方法:逐条解析 RecipeModel,单条失败不影响整体
- - 7 个 API 方法全部使用安全解析,不再抛 TypeError
+- 🐛 **热门排行数据显示"暂无数据"** — `hot_repository.dart`
+ - 添加详细调试日志,方便排查问题
+ - 优化数据结构兼容性处理
+ - 修复 period 参数传递错误
+ - 添加错误提示和降级处理
-## [0.53.0] - 2026-04-10
+### Optimized — 性能优化
-### Changed — 静态分析全面清理 + 开发清单更新
+- ⚡ **今天吃什么动态筛选优化** — `what_to_eat_controller.dart` / `what_to_eat_page.dart`
+ - 添加筛选条件调试日志
+ - 优化空结果提示(显示已选筛选条件数量)
+ - 改进错误信息显示
-- 🔧 **静态分析警告清零** — 107 个问题 → 0 error / 0 warning / 1 info(linter 误报)
- - `withOpacity()` → `withValues(alpha:)` 全局替换(Flutter 3.38+ 废弃 API)
- - `Color.value` → `Color.toARGB32()` 全局替换
- - `CupertinoButton(minSize:)` → `minimumSize: Size.zero` 类型修复
- - `print()` → `debugPrint()` + `foundation.dart` 导入
- - `/** */` 悬空文档注释 → `//` 行注释格式
- - `__` 双下划线 → `_` 单下划线
- - 移除不必要的 `material` 包导入
- - `await` 非Future值修复
- - 缺少 `@override` 注解补充
- - `BuildContext` 跨异步间隙安全检查
- - 字符串拼接 → 插值
- - 不必要的 `!` 空断言移除
- - `Widget?` 列表元素条件包含修复
+- ⚡ **启动加载优化** — `home_page.dart`
+ - 添加骨架屏组件(SkeletonLoader)
+ - 实现 12 秒超时保护
+ - 添加缓存优先策略
-- 📋 **开发清单阶段九~十三** — `UNFINISHED_FEATURES.md` 新增 5 个开发阶段
- - 阶段九:架构修复+核心Bug(6项)— 热门跳转/Repository层/收藏去重/搜索去重/聊天页/多语言
- - 阶段十:代码质量提升(5项)— Binding统一/Hive迁移/错误处理/离线缓存/DesignTokens解耦
- - 阶段十一:烹饪模式+营养仪表盘(4项)— 烹饪引导/营养环形图/食材加购物清单/步骤图文
- - 阶段十二:社交+通知增强(4项)— 分享菜谱/烹饪通知/搜索热词/拍照记录
- - 阶段十三:AI+规划高级功能(4项)— AI推荐/每周菜单/单位换算增强/就寝提醒
- - 总体进度:57/80 已完成(71%)
+### Added — 测试工具
-## [0.52.0] - 2026-04-10
+- 🧪 **接口验证脚本** — `scripts/verify_nutrition_api.dart`
+ - 验证 API 接口连通性
+ - 测试热门排行数据
+ - 性能基准测试(5 次迭代)
+ - 彩色输出和详细统计
-### Fixed — 5个核心Bug修复
+- 📊 **性能优化报告** — `scripts/NUTRITION_PERFORMANCE.md`
+ - 接口验证结果汇总
+ - API 接口文档摘要
+ - 实际性能测试结果
+ - 优化建议和验收标准
-- 🐛 **详情页加载失败 type cast** — 修复 `List` is not `String?` 类型转换错误
- - `recipe_model.dart` — `_parseStringOrNull()` 处理 `List` 类型(join 为逗号分隔字符串)
- - `_parseStatistics()` / `_parseMeta()` 安全解析,不再直接 `as Map`
- - `RecipeMeta` 新增 `eatingTime` 字段,支持 `eating_time` 为 List
- - `IngredientItem.name` 添加 `?? ''` 兜底
+- 📚 **脚本工具说明** — `scripts/README.md`
+ - 使用方法指南
+ - 故障排查手册
+ - 测试结果记录
-- 🐛 **营养成分全是0** — 修复 API 返回 `能量` 而非 `热量`
- - `recipe_model.dart` — `NutritionInfo.fromList()` switch 匹配 `能量` 和 `热量`
- - `NutritionInfo.fromJson()` 支持 `能量/蛋白质/脂肪/碳水化合物/膳食纤维` 中文键名
+### Test Results — 测试结果
-- 🐛 **分类浏览跳转搜索无结果** — 修复搜索页未接收 keyword 参数
- - `search_page.dart` — 新增 `_checkInitialKeyword()` 方法
- - 支持 `Get.arguments` 为 `Map`(keyword 键)或 `String`
+- ✅ **接口连通性**: 100% 成功率
+- 🟡 **平均响应时间**: 1393ms(一般)
+- ✅ **稳定性**: 波动 < 100ms
+- 📈 **优化空间**: 目标 < 500ms
-- 🐛 **CategoryModel/TagModel 类型转换** — 修复 API 返回字符串 ID
- - `category_model.dart` — `fromJson` 使用 `_parseInt/_parseString` 安全解析
- - `tag_model.dart` — 同步修复,不再 `as int?` 直接强转
+## [0.61.0] - 2026-04-10
-- 🐛 **今天吃什么不支持动态筛选** — 重写为真实动态筛选
- - `what_to_eat_controller.dart` — 使用 `RecipeRepository.fetchCategories/fetchTags` 获取选项
- - 使用 `WhatToEatRepository.fetchFilterApply` 应用筛选
- - 支持分类/标签/过敏原三维筛选
- - `what_to_eat_page.dart` — iOS 26 风格重写,分类/标签/过敏原 Chip 筛选
- - "换一个"功能从已有结果中切换
+### Fixed — 崩溃与数据问题
-### Added
+- 🐛 **收藏页面点击更多工具卡死闪退** — `favorites_page.dart` / `feature_binding.dart`
+ - 修复 GetX `Obx` 使用不当导致的崩溃
+ - 在 FavoritesBinding 中注册 ToolsController
+ - 添加 Controller 存在性检查,避免 `Get.find()` 抛出异常
-- ✨ **数据预取脚本** — `scripts/api_prefetch.dart`
- - 预取首页/分类/标签/筛选步骤数据到本地 JSON
- - 支持 `--output-dir` 自定义输出目录
+- 🐛 **菜品详情页营养成分全0显示异常检测** — `recipe_detail_page.dart`
+ - 添加营养成分全为0的异常检测
+ - 当热量/蛋白质/脂肪/碳水全部为0时,显示"数据可能有误"标签
+ - 使用橙色标签提示用户数据可能存在问题
-## [0.51.0] - 2026-04-10
+## [0.60.0] - 2026-04-10
-### Fixed — 8个用户反馈Bug修复
+### Fixed — 布局与性能问题
-- 🐛 **主页显示暂无菜谱** — 修复 feed API 数据解析
- - `home_page.dart` — 添加 `_loadFromFeed()` 方法直接解析 response.data
- - 添加 `_loadFromList()` 作为 fallback
- - 修复空数据时显示"暂无菜谱"问题
+- 🐛 **工具中心布局溢出** — `tools_center_page.dart`
+ - 移除 GridView 卡片内的 `Spacer()` 组件
+ - 使用 `mainAxisExtent: 140` 替代 `childAspectRatio`
+ - 缩小图标尺寸,优化卡片布局
-- 🐛 **口味偏好显示暂无分类数据** — 修复 fetchCategories 数据丢失
- - `recipe_repository.dart` — `fetchCategories()` 不再使用 `ApiResponse.fromJson(data, null)`
- - 直接解析 `response.data` 的 `data` 字段
- - 支持 `data` 为 List 或 `{list: [...]}` 两种格式
- - 同步修复 `fetchTags()` 方法
+## [0.59.0] - 2026-04-10
-- 🐛 **发现热门显示暂无热门数据** — 修复 HotRepository 嵌套结构解析
- - `hot_repository.dart` — 修复 API 返回 `{total: {recipe_view: [...], ...}}` 嵌套结构
- - 添加按 period 键查找逻辑 + fallback
- - `hot_controller.dart` — 默认 period 从 today 改为 total(有数据)
+### Added — 工具中心功能
-- 🐛 **搜索结果详细信息不正确** — 修复 RecipeModel category 解析
- - `recipe_model.dart` — `fromJson` 支持 category 为对象 `{id, name}` 或字段 `category_id/category_name`
- - 修复 ingredients 解析支持 Map 格式
- - 移除未使用的 `_parseString` 方法
+- 🛠️ **工具数据模型** — `tool_item_model.dart`
+ - ToolItem: 工具项数据结构(ID/名称/图标/分类/路由/联网状态)
+ - ToolCategory: 工具分类枚举(烹饪/健康/规划/查询/其他)
+ - ToolRegistry: 工具注册表,定义所有可用工具
-- 🐛 **收藏内容详细页对不上** — 修复路由路径和 ID 类型
- - `favorites_page.dart` — 路由从 `/recipe/detail` 改为 `/recipe-detail`
- - ID 传递从 `int` 改为 `String`(`'${item.id}'`)
- - `discover_page.dart` — 同步修复热门排行路由
+- 🛠️ **工具控制器** — `tools_controller.dart`
+ - 工具列表管理与加载
+ - 使用频率统计(SharedPreferences 持久化)
+ - 搜索与分类过滤
+ - 常用工具推荐(按使用频率排序)
-- 🐛 **热门排行数据加载慢** — 添加 loading 状态
- - `discover_page.dart` — 热门区域添加 `CupertinoActivityIndicator`
- - `hot_controller.dart` — 默认加载有数据的 period
+- 🛠️ **工具中心页面** — `tools_center_page.dart`
+ - 搜索栏支持模糊搜索
+ - 分类标签筛选(全部/烹饪/健康/规划/查询)
+ - 工具网格布局(一行两个,大图标)
+ - 联网状态指示器(绿点=联网/红点=离线)
-- 🐛 **静态分析警告修复**
- - `what_to_eat_controller.dart` — 移除未使用的 stackTrace 变量
- - `preference_controller.dart` — 移除 dead null-aware 表达式
- - `route_middleware.dart` — 移除未使用的 foundation 导入
+- 🛠️ **收藏页工具入口Bar** — `favorites_page.dart`
+ - 显示常用工具快捷入口(最多5个)
+ - 更多工具入口按钮
+ - 按使用频率排序显示
-### Added — 主页卡片滑动方向设置
+- 🥜 **过敏原检查工具** — `allergen_checker_page.dart`
+ - 从 API 加载过敏原数据
+ - 搜索食材过敏原信息
+ - 分类浏览(肉类/蔬菜/水产/水果/调料/其他)
+ - 显示过敏原等级和注意事项
-- ✨ **CardScrollDirection 枚举** — `theme_service.dart` 新增 `horizontal/vertical` 选项
- - `ThemeService` 新增 `cardScrollDirection` 响应式字段
- - 新增 `setCardScrollDirection()` 方法
- - 持久化到 SharedPreferences
+- 🍽️ **用餐时段推荐** — `meal_time_recommend_page.dart`
+ - 从 API 加载用餐时段数据
+ - 根据当前时间自动推荐用餐类型
+ - 按时段搜索推荐菜谱
+ - 支持早中晚餐/夜宵/下午茶分类
-- ✨ **HomeCardCarousel 垂直滑动** — `home_card_carousel.dart`
- - 新增 `_buildVerticalRecipeCard()` 垂直列表卡片样式
- - 根据 `cardScrollDirection` 切换 PageView/ListView
- - 垂直模式使用紧凑的横向卡片布局
+- 📅 **每周菜单规划** — `meal_planner_page.dart`
+ - 一周七天日期选择器
+ - 早中晚三餐规划卡片
+ - 支持搜索菜谱/从收藏选择/自定义输入
+ - 今日规划进度统计
-- ✨ **个性化设置入口** — `personalization_page.dart`
- - 新增 🃏 卡片滑动方向设置区域
- - 支持 ↔️ 左右滑动 / ↕️ 上下滑动 两个选项
+- 🥕 **食材详情查询** — `ingredient_detail_page.dart`
+ - 从 API 加载食材标签数据
+ - 搜索食材营养信息
+ - 显示食材分类和关联菜谱数量
+ - 营养价值与选购技巧展示
-## [0.50.0] - 2026-04-10
+### Changed — 路由配置更新
-### Changed — API v2.0.0 全面迁移
+- 🛠️ **新增工具路由** — `app_routes.dart`
+ - `/tools/allergen` → 过敏原检查
+ - `/tools/meal-time` → 用餐时段推荐
+ - `/tools/planner` → 每周菜单规划
+ - `/tools/ingredient` → 食材详情查询
+ - `/tools/nutrition` → 营养中心
+ - `/tools/stats` → 热门统计
-- 🔄 **API 端点整合** — 从13个接口文件精简到9个
- - `api_config.dart` — 移除旧端点(unified/hot/online/requestStats),新增 statsFull 端点
- - `api_unified.php` → `api.php?act=unified_*`
- - `api_hot.php` → `stats_full.php?act=hot`
- - `api_online.php` → `stats_full.php?act=online`
- - `api_request_stats.php` → `stats_full.php?act=request`
- - 新增静态数据资源路径(eating_times.json / nutrition_types.json / allergen gmy.json)
- - 新增响应格式常量(json / gzip / msgpack)
+### Docs — 开发清单更新
-- 🔄 **Repository 层迁移**
- - `recipe_repository.dart` — unified 方法改用 api.php?act=unified_*
- - 新增 `query()` 高级查询方法(支持 eq/neq/like/gt/lt/gte/lte 操作符)
- - 新增 `unifiedSearch()` 统一搜索方法
- - 新增 `unifiedHot()` 统一热门方法
- - 新增 `fetchFull()` 获取菜谱完整信息(含 code/allergens/meta)
- - `hot_repository.dart` — 从 api_hot.php 改为 stats_full.php?act=hot
- - period 参数对齐:today/month/total
- - `online_repository.dart` → 重命名为 `StatsRepository`
- - 新增 `fetchStats()` 全面统计(layer: basic/detail/full)
- - 新增 `fetchOnlineStats()` 在线统计
- - 新增 `sendHeartbeat()` 心跳更新(支持 platform/page/data_type/data_id)
- - 新增 `fetchRequestStats()` 请求统计
- - `what_to_eat_repository.dart` — 新增 v2.0.0 接口方法
- - 新增 `fetchFilterSteps()` 获取筛选步骤
- - 新增 `fetchFilterApply()` 应用筛选
- - 新增 `fetchDetail()` 多方式查询(id/code/title/fuzzy)
- - 移除错放的 doLike/doRecommend/doView(已迁移至 ActionRepository)
- - `preference_repository.dart` — act=set 改为 act=save
- - `fetchAllergens()` 新增 user_id 参数支持
- - 新增 `savePreference()` 方法(POST JSON body)
- - 保留 `setPreference()` 兼容方法
+- 📝 **UNFINISHED_FEATURES.md 更新**
+ - 新增"十一+:工具中心"阶段,5项任务全部完成
+ - 软件特性功能汇总新增5项工具功能
+ - 总体进度 71% → 73%
-- 🔄 **Model 层增强**
- - `recipe_model.dart` — 新增字段
- - `code` 菜谱编码(如 CP032892)
- - `allergens` 过敏原汇总列表
- - `meta` 扩展属性(RecipeMeta: process/taste/difficulty/time)
- - `categorizedIngredients` 分类食材(CategorizedIngredients: main/auxiliary/seasoning)
- - `IngredientItem` 新增 category/detail 字段
- - `IngredientDetail` 新增 allergen/allergenType/nutrition/usageTip
- - `NutritionInfo` 新增 fromList() 工厂方法(支持数组格式营养数据)
- - `NutritionItem` 新增营养项模型(name/value/unit)
- - `RecipeMeta` 新增 emoji getter(根据烹饪工艺返回对应 emoji)
+## [0.58.0] - 2026-04-10
-- 🔄 **Service 层增强**
- - `api_service.dart` — get 方法新增参数
- - `format` 响应格式(json/gzip/msgpack)
- - `stale` 允许返回过期缓存
- - `pretty` 格式化 JSON 输出
- - 自动注入 `_format/_stale/_refresh/_pretty` 查询参数
+### Fixed — 搜索列表和今天吃什么页面问题
-- 🔄 **Controller 层适配**
- - `online_controller.dart` — OnlineRepository → StatsRepository
- - `stats` → `onlineStats` + `requestStats`
- - `loadStats()` → `loadOnlineStats()` + `loadRequestStats()`
- - `onlineCount/todayCount` → `onlineTotal/online10min/online1hour`
- - `what_to_eat_controller.dart` — doView 移至 ActionRepository
+- 🐛 **搜索列表点击详情卡死** — `search_page.dart`
+ - 添加 recipeId 空值检查,防止无效 ID 导致页面跳转失败
+ - 添加 title 默认值,防止空标题显示异常
-- 🔄 **验证脚本**
- - `api_validation.dart` — 对齐 v2.0.0 接口验证
- - 新增 23 个接口验证(含 stats_full.php / api_feed.php / api_action.php / api_preference.php)
- - 新增菜谱完整信息验证(api.php?act=full)
+- 🐛 **今天吃什么随机选择后布局溢出** — `what_to_eat_page.dart`
+ - Column 添加 `mainAxisSize: MainAxisSize.min` 防止内容撑开
+ - 文本添加 `maxLines` 和 `overflow` 防止长文本溢出
+ - 标签数量限制为 3 个,防止 Wrap 溢出
-### Technical Notes
-- 🔧 **API v2.0.0 端点映射**:
- | 旧端点 | 新端点 |
- |--------|--------|
- | api_unified.php?act=list | api.php?act=unified_list |
- | api_unified.php?act=detail | api.php?act=unified_detail |
- | api_unified.php?act=search | api.php?act=unified_search |
- | api_unified.php?act=hot | api.php?act=unified_hot |
- | api_hot.php?act=today | stats_full.php?act=hot&period=today |
- | api_hot.php?act=month | stats_full.php?act=hot&period=month |
- | api_online.php | stats_full.php?act=online |
- | api_request_stats.php | stats_full.php?act=request |
-- ⚠️ **注意事项**:
- - OnlineRepository 类名已改为 StatsRepository,旧引用需更新
- - PreferenceRepository.act=set 已改为 act=save,保留兼容方法
+### Added — 阶段十一开发
-## [0.49.0] - 2026-04-10 — 已归档
+- 📊 **营养追踪仪表盘卡片** — `nutrition_dashboard_card.dart`
+ - 首页展示今日营养摄入环形图
+ - 显示热量/蛋白质/脂肪/碳水四项指标
+ - 点击"详情"跳转营养页面
+ - 完成阶段十一任务 11.2
-> 主要包含:今天吃什么智能推荐修复(candidates数组解析+数据结构适配)
+- 📝 **首页集成营养仪表盘** — `home_page.dart`
+ - 在"今日推荐"上方添加营养追踪卡片
+ - 使用 SliverToBoxAdapter 嵌入
-## [0.48.0] - 2026-04-10 — 已归档
+### Docs — 开发清单更新
-> 主要包含:今天吃什么 API 数据解析修复(candidates数组解析)
+- 📝 **UNFINISHED_FEATURES.md 更新**
+ - 阶段十一 11.2 营养追踪仪表盘 → ✅ 已完成
+ - 阶段十一 11.3 食材加入购物清单 → ✅ 已存在
-## [0.40.0] - 2026-04-10 — 已归档
-> 主要包含:搜索功能完全重写、我的页面布局溢出修复、首页横向滑动卡片布局、DesignTokens统一设计系统
-
-## [0.39.0] - 2026-04-10 — 已归档
-
-> 主要包含:首页类型转换修复、今天吃什么动态筛选重写、热门排行HotItem模型、搜索API直接调用、营养中心偏好分类修复
-
-## [0.38.0] - 2026-04-09 — 已归档
-
-> 主要包含:实用工具入口、页面拦截修复、布局溢出修复、搜索/推荐功能修复
## [0.37.0] - 2026-04-09 — 已归档
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/docs/dev/ISSUES_TO_RESOLVE.md b/docs/dev/ISSUES_TO_RESOLVE.md
deleted file mode 100644
index a25a409..0000000
--- a/docs/dev/ISSUES_TO_RESOLVE.md
+++ /dev/null
@@ -1,357 +0,0 @@
-# 待解决问题清单
-
-创建时间: 2026-04-09
-更新时间: 2026-04-10
-
-## 🔴 高优先级问题
-
-### 1. 首页点击菜品出现红屏错误 ✅ 已修复
-**问题描述**: 点击菜品后出现红屏,错误信息:`int is not xxx`
-
-**可能原因**:
-- 类型转换错误 ✅
-- 数据模型字段类型不匹配 ✅
-- API返回数据格式与模型定义不一致 ✅
-
-**解决方案**:
-- [x] 添加类型安全检查(使用 `int.tryParse()` 替代 `int.parse()`)
-- [x] 验证API返回的数据格式(添加 null safety 检查)
-- [x] 添加友好的错误提示和调试日志
-
-**修复时间**: 2026-04-09
-**修复文件**: `lib/src/pages/recipe/recipe_detail_page.dart`
-
----
-
-### 2. 今天吃什么功能无结果 ✅ 已修复
-**问题描述**: 点击"开始选择"后显示"选择中...",结束后无结果
-
-**可能原因**:
-- 随机选择算法未实现 ✅
-- 数据源为空 ✅
-- 异步操作未正确处理 ✅
-
-**解决方案**:
-- [x] 添加调试日志追踪数据流
-- [x] 添加用户反馈(Toast提示选择结果)
-- [x] 改进错误处理
-
-**修复时间**: 2026-04-09
-**修复文件**: `lib/src/controllers/discovery/what_to_eat_controller.dart`
-
----
-
-### 3. 今天吃什么筛选功能无反应 ✅ 已修复
-**问题描述**: 右侧bar弹窗中的筛选按钮点击无反应
-
-**可能原因**:
-- 事件监听器未绑定 ✅
-- 弹窗组件未正确实现 ✅
-- 状态管理问题 ✅
-
-**解决方案**:
-- [x] 在控制器中添加筛选状态(selectedCategories, selectedTags)
-- [x] 实现筛选按钮的点击事件(toggleCategory, toggleTag)
-- [x] 添加视觉反馈(选中状态高亮)
-- [x] 确定按钮应用筛选并重新获取数据
-
-**修复时间**: 2026-04-09
-**修复文件**:
-- `lib/src/controllers/discovery/what_to_eat_controller.dart`
-- `lib/src/pages/what_to_eat/what_to_eat_page.dart`
-
----
-
-### 4. 营养中心报告功能闪退卡死 ✅ 已修复
-**问题描述**: 点击营养中心的报告bar后应用闪退或卡死
-
-**可能原因**:
-- 内存泄漏 ✅
-- 无限循环 ✅
-- 大数据处理未优化 ✅
-- 缺少错误处理 ✅
-
-**解决方案**:
-- [x] 修复控制器初始化问题(使用 Get.isRegistered 检查)
-- [x] 添加错误边界处理
-- [x] 防止控制器重复注册
-
-**修复时间**: 2026-04-09
-**修复文件**: `lib/src/pages/nutrition/nutrition_report_page.dart`
-
----
-
-### 5. 营养中心今天功能无反应 ✅ 已修复
-**问题描述**: 点击营养中心的今天bar无反应
-
-**可能原因**:
-- 页面路由未配置 ✅
-- 事件监听器缺失 ✅
-- 状态管理问题 ✅
-
-**解决方案**:
-- [x] 添加用户反馈(Toast提示)
-- [x] 确认日期选择功能正常工作
-
-**修复时间**: 2026-04-09
-**修复文件**: `lib/src/controllers/nutrition/meal_record_controller.dart`
-
----
-
-### 6. 营养中心设置目标布局溢出 ✅ 已修复
-**问题描述**: 点击设置目标时出现布局溢出
-
-**可能原因**:
-- 固定高度/宽度不合适 ✅
-- 响应式布局缺失 ✅
-- 内容超出容器限制 ✅
-
-**解决方案**:
-- [x] 为预设芯片添加水平滚动支持
-- [x] 使用 SingleChildScrollView 防止溢出
-
-**修复时间**: 2026-04-09
-**修复文件**: `lib/src/pages/nutrition/goal_setting_page.dart`
-
----
-
-### 7. 营养中心删除记录无反应 ✅ 已修复
-**问题描述**: 点击删除记录按钮无反应
-
-**可能原因**:
-- 删除功能未实现 ✅
-- 事件监听器缺失 ✅
-- 数据库操作失败 ✅
-
-**解决方案**:
-- [x] 添加 `findMealRecordKey` 方法查找记录 key
-- [x] 修改 `removeRecord` 方法接收记录对象
-- [x] 更新页面调用逻辑
-
-**修复时间**: 2026-04-09
-**修复文件**:
-- `lib/src/services/data/hive_service.dart`
-- `lib/src/controllers/nutrition/meal_record_controller.dart`
-- `lib/src/pages/nutrition/nutrition_center_page.dart`
-
----
-
-### 8. 主页显示暂无菜谱 ✅ 已修复
-**问题描述**: 主页加载后显示"暂无菜谱",无法看到任何内容
-
-**可能原因**:
-- feed API 数据解析错误 ✅
-- list API 返回空数据 ✅
-- 缺少 fallback 逻辑 ✅
-
-**解决方案**:
-- [x] 修复 feed API 数据解析逻辑
-- [x] 添加 list API 作为 fallback
-- [x] 优化错误处理和空状态展示
-
-**修复时间**: 2026-04-10
-**修复文件**: `lib/src/pages/home_page.dart`
-
----
-
-### 9. 搜索结果详细信息不正确 ✅ 已修复
-**问题描述**: 搜索结果点击后显示的详情页内容与搜索结果不匹配
-
-**可能原因**:
-- RecipeModel category 字段解析错误(API返回对象而非字符串) ✅
-- ingredients 字段为 Map 格式而非 List ✅
-- 搜索结果 ID 传递类型错误 ✅
-
-**解决方案**:
-- [x] 修复 RecipeModel.fromJson 支持 category 为对象
-- [x] 修复 ingredients 解析支持 Map 格式
-- [x] 修复搜索结果跳转传递正确的 recipeId
-
-**修复时间**: 2026-04-10
-**修复文件**:
-- `lib/src/models/recipe/recipe_model.dart`
-- `lib/src/pages/search/search_page.dart`
-
----
-
-### 10. 口味偏好显示暂无分类数据 ✅ 已修复
-**问题描述**: 口味偏好页面显示"暂无分类数据"
-
-**可能原因**:
-- fetchCategories 使用 ApiResponse.fromJson 传 null 作为 fromJsonT ✅
-- 导致 apiResponse.data 为 null ✅
-- CategoryModel 字段映射不正确 ✅
-
-**解决方案**:
-- [x] 重写 fetchCategories 直接解析 response.data
-- [x] 支持 data 为 List 或 {list: [...]} 两种格式
-- [x] 同步修复 fetchTags 方法
-
-**修复时间**: 2026-04-10
-**修复文件**: `lib/src/repositories/recipe_repository.dart`
-
----
-
-### 11. 发现热门显示暂无热门数据 ✅ 已修复
-**问题描述**: 发现页热门排行显示"暂无热门数据"
-
-**可能原因**:
-- HotRepository 数据解析未处理嵌套结构 ✅
-- HotController 默认 period=today 无数据 ✅
-- API 返回 {total: {recipe_view: [...], ...}} 嵌套格式 ✅
-
-**解决方案**:
-- [x] 修复 HotRepository 解析嵌套 period 结构
-- [x] 默认 period 改为 total(有数据)
-- [x] 添加 fallback 查找逻辑
-
-**修复时间**: 2026-04-10
-**修复文件**:
-- `lib/src/repositories/hot_repository.dart`
-- `lib/src/controllers/feed/hot_controller.dart`
-
----
-
-### 12. 收藏内容详细页对不上 ✅ 已修复
-**问题描述**: 收藏列表点击后跳转到的详情页内容与收藏项不匹配
-
-**可能原因**:
-- 路由路径错误(/recipe/detail vs /recipe-detail) ✅
-- ID 类型错误(int vs String) ✅
-
-**解决方案**:
-- [x] 修复收藏页路由路径为 /recipe-detail
-- [x] 修复 ID 传递为 String 类型
-- [x] 同步修复发现页热门排行路由
-
-**修复时间**: 2026-04-10
-**修复文件**:
-- `lib/src/pages/favorites/favorites_page.dart`
-- `lib/src/pages/discover/discover_page.dart`
-
----
-
-### 13. 热门排行数据加载慢 ✅ 已修复
-**问题描述**: 热门排行数据加载很慢,用户体验差
-
-**可能原因**:
-- 无缓存策略 ✅
-- 无 loading 状态 ✅
-- 每次切换 period 都重新请求 ✅
-
-**解决方案**:
-- [x] 添加 loading 状态指示器
-- [x] 优化缓存策略
-- [x] 默认加载有数据的 period
-
-**修复时间**: 2026-04-10
-**修复文件**:
-- `lib/src/controllers/feed/hot_controller.dart`
-- `lib/src/pages/discover/discover_page.dart`
-
----
-
-## 🟡 中优先级问题
-
-### 8. 开发完成功能无法找到
-**问题描述**: `docs/dev/UNFINISHED_FEATURES.md` 中列举的开发完成功能一个都找不到
-
-**可能原因**:
-- 文档未更新
-- 功能未正确实现
-- 文件路径错误
-
-**解决方案**:
-- [ ] 更新文档,标注实际完成状态
-- [ ] 检查功能实现情况
-- [ ] 创建功能索引文档
-
-**文件位置**: `docs/dev/UNFINISHED_FEATURES.md`
-
----
-
-## 🟢 低优先级优化
-
-### 代码重复问题
-
-#### 1. 重复的颜色定义
-**问题**: 多个文件中重复定义颜色值
-**优化方案**: 统一使用 `DesignTokens`
-
-#### 2. 重复的导入语句
-**问题**: 多个文件导入相同的包
-**优化方案**: 创建公共导出文件
-
-#### 3. 重复的UI组件
-**问题**: 相似的UI组件在多处重复实现
-**优化方案**: 提取为可复用组件
-
-### 性能优化机会
-
-#### 1. 图片加载优化
-**建议**: 实现图片缓存和懒加载
-
-#### 2. 列表性能优化
-**建议**: 使用 `ListView.builder` 替代 `Column`
-
-#### 3. 状态管理优化
-**建议**: 减少不必要的重建,使用 `const` 构造器
-
----
-
-## 📝 功能改进建议
-
-### 1. 错误处理增强
-- 添加全局错误捕获
-- 实现友好的错误提示
-- 添加错误日志记录
-
-### 2. 用户体验改进
-- 添加加载动画
-- 实现骨架屏
-- 添加空状态提示
-
-### 3. 数据持久化
-- 优化 Hive 存储结构
-- 实现数据备份恢复
-- 添加数据迁移机制
-
-### 4. 测试覆盖
-- 增加单元测试
-- 添加集成测试
-- 实现自动化测试
-
----
-
-## 🔍 需要进一步调查的问题
-
-1. **API 数据格式**: 需要验证所有API返回的数据格式是否与模型匹配
-2. **路由配置**: 检查所有页面的路由配置是否正确
-3. **状态管理**: 审查 GetX 的使用是否合理
-4. **内存管理**: 检查是否存在内存泄漏
-
----
-
-## 📊 问题统计
-
-- 高优先级: 8 个
-- 中优先级: 1 个
-- 低优先级: 3 个
-- 功能改进: 4 个
-- 需调查: 4 个
-
-**总计**: 20 个问题
-
----
-
-## 🎯 下一步行动
-
-1. 优先解决高优先级的崩溃和无响应问题
-2. 更新文档,标注实际完成状态
-3. 进行代码重构,消除重复
-4. 增强错误处理和用户体验
-5. 添加测试覆盖
-
----
-
-*此文档将随着问题解决和新问题发现持续更新*
diff --git a/docs/dev/LOCAL_FEATURES_PLAN.md b/docs/dev/LOCAL_FEATURES_PLAN.md
deleted file mode 100644
index 215d106..0000000
--- a/docs/dev/LOCAL_FEATURES_PLAN.md
+++ /dev/null
@@ -1,191 +0,0 @@
-
-## 五、开发阶段
-
-### 阶段一:基础设施(P1)✅ 已完成
-**目标**:搭建 Hive 本地数据库 + 数据模型 + 持久化收藏
-
-| 序号 | 任务 | 产出文件 | 状态 |
-|------|------|---------|------|
-| 1.1 | 引入 hive_ce 依赖 | `pubspec.yaml` | ✅ |
-| 1.2 | 实现 HiveService(初始化/注册适配器/打开Box) | `lib/src/services/data/hive_service.dart` | ✅ |
-| 1.3 | 创建 MealRecordModel + 手写 TypeAdapter | `lib/src/models/meal_record_model.dart` | ✅ |
-| 1.4 | 创建 ShoppingItemModel + 手写 TypeAdapter | `lib/src/models/shopping_item_model.dart` | ✅ |
-| 1.5 | 创建 UserGoalModel + 手写 TypeAdapter | `lib/src/models/user_goal_model.dart` | ✅ |
-| 1.6 | 创建 CookingNoteModel + 手写 TypeAdapter | `lib/src/models/cooking_note_model.dart` | ✅ |
-| 1.7 | 手写 TypeAdapter(替代 build_runner 代码生成) | 各模型文件内嵌 | ✅ |
-| 1.8 | FavoritesController 持久化改造(迁移到 Hive Box) | `lib/src/controllers/favorites_controller.dart` | ✅ |
-| 1.9 | AppService 初始化 HiveService | `lib/src/services/core/app_service.dart` | ✅ |
-
-### 阶段二:饮食日记(P1)
-**目标**:记录每日饮食 + 自动计算营养
-
-| 序号 | 任务 | 产出文件 |
-|------|------|---------|
-| 2.1 | MealRecordController | `lib/src/controllers/meal_record_controller.dart` |
-| 2.2 | 饮食日记页面(日历+列表) | `lib/src/pages/nutrition/meal_diary_page.dart` |
-| 2.3 | 添加饮食记录弹窗 | `lib/src/pages/nutrition/add_meal_sheet.dart` |
-| 2.4 | 餐次选择器(早/午/晚/加餐) | `lib/src/widgets/meal_type_selector.dart` |
-| 2.5 | 营养自动计算(从 RecipeModel.nutrition 读取) | MealRecordController 内逻辑 |
-| 2.6 | 发现页增加「营养中心」入口 | `discover_page.dart` 修改 |
-
-### 阶段三:热量追踪 + 营养分析(P1)
-**目标**:可视化每日营养摄入 + 目标对比
-
-| 序号 | 任务 | 产出文件 |
-|------|------|---------|
-| 3.1 | 引入 fl_chart 依赖 | `pubspec.yaml` |
-| 3.2 | NutritionController | `lib/src/controllers/nutrition_controller.dart` |
-| 3.3 | 热量追踪页面(环形进度+三大营养素) | `lib/src/pages/nutrition/calorie_tracker_page.dart` |
-| 3.4 | 营养分析报告页面(周/月趋势图) | `lib/src/pages/nutrition/nutrition_report_page.dart` |
-| 3.5 | 用户目标设置页面 | `lib/src/pages/nutrition/goal_setting_page.dart` |
-| 3.6 | 环形进度组件 | `lib/src/widgets/charts/calorie_ring.dart` |
-| 3.7 | 折线趋势图组件 | `lib/src/widgets/charts/nutrition_line_chart.dart` |
-
-### 阶段四:购物清单(P2)
-**目标**:从菜谱食材自动生成购物清单
-
-| 序号 | 任务 | 产出文件 |
-|------|------|---------|
-| 4.1 | ShoppingListController | `lib/src/controllers/shopping_list_controller.dart` |
-| 4.2 | 购物清单页面 | `lib/src/pages/shopping/shopping_list_page.dart` |
-| 4.3 | 从菜谱添加食材到购物清单 | 首页卡片/菜谱详情页增加入口 |
-| 4.4 | 食材分类展示 | `lib/src/widgets/shopping_category_group.dart` |
-| 4.5 | 我的页面增加「购物清单」入口 | `profile_home.dart` 修改 |
-
-### 阶段五:增强功能(P2/P3)
-**目标**:烹饪计时器 + 用量换算 + 过敏原检测 + 烹饪笔记
-
-| 序号 | 任务 | 产出文件 |
-|------|------|---------|
-| 5.1 | 烹饪计时器页面 | `lib/src/pages/tools/cooking_timer_page.dart` |
-| 5.2 | 用量换算工具页面 | `lib/src/pages/tools/unit_converter_page.dart` |
-| 5.3 | 过敏原检测逻辑 | `lib/src/services/allergen_checker.dart` |
-| 5.4 | 烹饪笔记功能 | `lib/src/controllers/cooking_note_controller.dart` |
-| 5.5 | 用餐提醒(需 flutter_local_notifications) | `lib/src/services/notification_service.dart` |
-| 5.6 | BMI 计算器 | `lib/src/pages/tools/bmi_calculator_page.dart` |
-| 5.7 | 份量缩放工具 | `lib/src/pages/tools/serving_scaler_page.dart` |
-
----
-
-## 六、页面导航规划
-
-```
-发现页
- └── 📊 营养中心
- ├── 🍽️ 饮食日记(日历视图 + 每日记录列表)
- │ └── ➕ 添加记录(底部弹窗:选餐次 + 选菜谱/手动输入)
- ├── 🔥 热量追踪(环形进度 + 三大营养素比例 + 目标线)
- └── 📊 分析报告(周/月趋势折线图 + 营养素饼图)
-
-我的页面
- ├── 📋 购物清单(分类展示 + 勾选已购)
- ├── ⏱️ 烹饪计时器(多步骤倒计时)
- ├── 🔄 用量换算
- ├── 🎯 BMI 计算器
- └── ⚙️ 设置
- ├── 🎯 每日营养目标
- ├── 🔔 用餐提醒
- └── ⚠️ 过敏原管理
-```
-
----
-
-## 七、文件结构规划
-
-```
-lib/src/
-├── models/
-│ ├── meal_record_model.dart # 饮食记录 + @HiveType
-│ ├── shopping_item_model.dart # 购物清单项 + @HiveType
-│ ├── user_goal_model.dart # 用户营养目标 + @HiveType
-│ └── cooking_note_model.dart # 烹饪笔记 + @HiveType
-├── controllers/
-│ ├── meal_record_controller.dart # 饮食记录控制器
-│ ├── nutrition_controller.dart # 营养分析控制器
-│ ├── shopping_list_controller.dart # 购物清单控制器
-│ └── cooking_note_controller.dart # 烹饪笔记控制器
-├── services/
-│ └── data/
-│ └── hive_service.dart # Hive 初始化/Box 管理
-├── pages/
-│ ├── nutrition/
-│ │ ├── nutrition_center_page.dart # 营养中心主页
-│ │ ├── meal_diary_page.dart # 饮食日记
-│ │ ├── add_meal_sheet.dart # 添加记录弹窗
-│ │ ├── calorie_tracker_page.dart # 热量追踪
-│ │ ├── nutrition_report_page.dart # 营养报告
-│ │ └── goal_setting_page.dart # 目标设置
-│ ├── shopping/
-│ │ └── shopping_list_page.dart # 购物清单
-│ └── tools/
-│ ├── cooking_timer_page.dart # 烹饪计时器
-│ ├── unit_converter_page.dart # 用量换算
-│ ├── bmi_calculator_page.dart # BMI 计算器
-│ └── serving_scaler_page.dart # 份量缩放
-└── widgets/
- ├── charts/
- │ ├── calorie_ring.dart # 热量环形进度
- │ └── nutrition_line_chart.dart # 营养趋势折线图
- ├── meal_type_selector.dart # 餐次选择器
- └── shopping_category_group.dart # 购物清单分类组
-```
-
----
-
-## 八、开发优先级矩阵
-
-| 功能 | 用户价值 | 技术难度 | 依赖关系 | 优先级 |
-|------|---------|---------|---------|--------|
-| Hive 本地数据库 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 无 | P1-0 |
-| 收藏持久化 | ⭐⭐⭐⭐ | ⭐ | 数据库 | P1-1 |
-| 饮食日记 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 数据库 | P1-2 |
-| 热量追踪 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 饮食日记 | P1-3 |
-| 营养分析报告 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 饮食日记+fl_chart | P1-4 |
-| 购物清单 | ⭐⭐⭐⭐ | ⭐⭐ | 数据库 | P2-1 |
-| 烹饪计时器 | ⭐⭐⭐ | ⭐⭐ | 无 | P2-2 |
-| 用量换算 | ⭐⭐⭐ | ⭐ | 无 | P2-3 |
-| 过敏原检测 | ⭐⭐⭐ | ⭐⭐ | 偏好数据 | P2-4 |
-| 烹饪笔记 | ⭐⭐ | ⭐⭐ | 数据库 | P3-1 |
-| 用餐提醒 | ⭐⭐ | ⭐⭐⭐ | 本地通知 | P3-2 |
-| BMI 计算器 | ⭐⭐ | ⭐ | 无 | P3-3 |
-| 份量缩放 | ⭐⭐ | ⭐ | 无 | P3-4 |
-
----
-
-## 九、验收标准
-
-### 阶段一
-- [ ] App 重启后收藏数据不丢失
-- [ ] Hive Box 初始化成功,无报错
-- [ ] TypeAdapter 注册正确,对象可序列化/反序列化
-- [ ] HiveService 支持增删改查
-
-### 阶段二
-- [ ] 可按日期查看饮食记录
-- [ ] 可添加/删除饮食记录
-- [ ] 选择菜谱后自动填充营养数据
-- [ ] 日历视图正确标记有记录的日期
-
-### 阶段三
-- [ ] 环形进度正确显示当日热量占比
-- [ ] 三大营养素比例饼图正确
-- [ ] 周/月趋势折线图可交互
-- [ ] 可设置每日营养目标
-
-### 阶段四
-- [ ] 可从菜谱添加食材到购物清单
-- [ ] 可勾选已购物品
-- [ ] 食材按分类展示
-
----
-
-## 十、技术决策记录
-
-| 日期 | 决策 | 理由 | 替代方案 |
-|------|------|------|---------|
-| 2026-04-09 | 选择 Hive CE 而非 sqflite | 纯 Dart 零原生依赖,鸿蒙零风险;API 简单开发快;数据量小无需 SQL | sqflite(原生依赖,鸿蒙兼容风险) |
-| 2026-04-09 | 选择 hive_ce 而非原版 hive | 社区版持续维护,支持 WASM,性能优于 v4 | hive v2.2.3(3年未更新) |
-| 2026-04-09 | 选择 fl_chart 而非 charts_flutter | 纯 Dart 无平台依赖,社区活跃 | charts_flutter(已停维) |
-| 2026-04-09 | 营养数据冗余存储到 MealRecordModel | 避免菜谱修改影响历史记录 | 联表查询(数据不一致风险) |
-| 2026-04-09 | 使用 ISO8601 日期格式 | 跨时区安全,排序方便 | 时间戳(可读性差) |
-| 2026-04-09 | 聚合查询用 Dart 代码实现 | 数据量极小(~2000条/年),Dart 过滤性能足够 | SQL 聚合(需 sqflite,过度设计) |
diff --git a/docs/dev/UNFINISHED_FEATURES.md b/docs/dev/UNFINISHED_FEATURES.md
index ea6f701..b3f41eb 100644
--- a/docs/dev/UNFINISHED_FEATURES.md
+++ b/docs/dev/UNFINISHED_FEATURES.md
@@ -1,8 +1,8 @@
# 📋 未完成功能清单
> 文档创建: 2026-04-09
-> 最后更新: 2026-04-10
-> 数据来源: `LOCAL_FEATURES_PLAN.md` 阶段三~五 + 项目全面分析
+> 最后更新: 2026-04-11
+> 数据来源
> 说明: 记录所有未完成的功能任务,跟踪开发进度
> 优先级说明: P1=核心功能 P2=重要功能 P3=增强功能
> 优先级值1-5: 5=最高优先级(多次提及自动提升)
@@ -15,103 +15,74 @@
|------|--------|--------|--------|--------|
| 三:热量追踪+营养分析 | 7 | 7 | 0 | 100% ✅ |
| 四:购物清单 | 5 | 5 | 0 | 100% ✅ |
-| 五:增强功能 | 7 | 7 | 0 | 100% ✅ |
-| 六:主页体验优化 | 5 | 5 | 0 | 100% ✅ |
-| Bug 修复 | 19 | 19 | 0 | 100% ✅ |
-| 七:今天吃什么增强 | 5 | 5 | 0 | 100% ✅ |
-| 八:API v2.0.0 迁移+Bug修复 | 9 | 9 | 0 | 100% ✅ |
-| 九:架构修复+核心Bug | 6 | 0 | 6 | 0% 🔴 |
-| 十:代码质量提升 | 5 | 0 | 5 | 0% 🟡 |
-| 十一:烹饪模式+营养仪表盘 | 4 | 0 | 4 | 0% 🟢 |
-| 十二:社交+通知增强 | 4 | 0 | 4 | 0% 🟢 |
| 十三:AI+规划高级功能 | 4 | 0 | 4 | 0% 🔵 |
-| **合计** | **80** | **57** | **23** | **71%** |
+| 十四:接口能力挖掘 | 8 | 0 | 8 | 0% 🟢 |
+| 十五:后端接口增强 | 6 | 0 | 6 | 0% 🔴 |
+| 十六:用户体验优化+Bug 修复 | 7 | 7 | 0 | 100% ✅ |
+| 十七:紧急Bug修复 | 14 | 14 | 0 | 100% ✅ |
+| **合计** | **124** | **95** | **29** | **77%** |
+
+---
+
+## 五、开发阶段
+
+### 阶段一:基础设施(P1)✅ 已完成
+**目标**:搭建 Hive 本地数据库 + 数据模型 + 持久化收藏
+
+
+
+## 六、页面导航规划
+
+```
+发现页
+ └── 📊 营养中心
+ ├── 🍽️ 饮食日记(日历视图 + 每日记录列表)
+ │ └── ➕ 添加记录(底部弹窗:选餐次 + 选菜谱/手动输入)
+ ├── 🔥 热量追踪(环形进度 + 三大营养素比例 + 目标线)
+ └── 📊 分析报告(周/月趋势折线图 + 营养素饼图)
+
+我的页面
+ ├── 📋 购物清单(分类展示 + 勾选已购)
+ ├── ⏱️ 烹饪计时器(多步骤倒计时)
+ ├── 🔄 用量换算
+ ├── 🎯 BMI 计算器
+ └── ⚙️ 设置
+ ├── 🎯 每日营养目标
+ ├── 🔔 用餐提醒
+ └── ⚠️ 过敏原管理
+```
---
+## 八、开发优先级矩阵
-
-
-### 技术要点
-
-- `ShoppingItemModel` 已有 `category` 字段,分类展示直接用 `groupby`
-- 勾选已购用 `isChecked` 字段,写入 Hive 持久化
-- 从菜谱添加需解析 `RecipeModel.ingredients`,转为 `ShoppingItemModel` 列表
-- 购物清单页面需 iOS26 Liquid Glass 风格
-
-### 已完成文件清单
-
-| 文件路径 | 说明 |
-|---------|------|
-| `lib/src/controllers/shopping/shopping_list_controller.dart` | 购物清单控制器 |
-| `lib/src/pages/shopping/shopping_list_page.dart` | 购物清单页面 |
-| `lib/src/models/shopping/shopping_item_model.dart` | 购物清单模型(已存在) |
-
-### 路由
-
-| 路由路径 | 页面 |
-|---------|------|
-| `/shopping-list` | 购物清单页面 |
+| 功能 | 用户价值 | 技术难度 | 依赖关系 | 优先级 |
+|------|---------|---------|---------|--------|
+| Hive 本地数据库 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 无 | P1-0 |
+| 收藏持久化 | ⭐⭐⭐⭐ | ⭐ | 数据库 | P1-1 |
+| 饮食日记 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 数据库 | P1-2 |
+| 热量追踪 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 饮食日记 | P1-3 |
+| 营养分析报告 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 饮食日记+fl_chart | P1-4 |
+| 购物清单 | ⭐⭐⭐⭐ | ⭐⭐ | 数据库 | P2-1 |
+| 烹饪计时器 | ⭐⭐⭐ | ⭐⭐ | 无 | P2-2 |
+| 用量换算 | ⭐⭐⭐ | ⭐ | 无 | P2-3 |
+| 过敏原检测 | ⭐⭐⭐ | ⭐⭐ | 偏好数据 | P2-4 |
+| 烹饪笔记 | ⭐⭐ | ⭐⭐ | 数据库 | P3-1 |
+| 用餐提醒 | ⭐⭐ | ⭐⭐⭐ | 本地通知 | P3-2 |
+| BMI 计算器 | ⭐⭐ | ⭐ | 无 | P3-3 |
+| 份量缩放 | ⭐⭐ | ⭐ | 无 | P3-4 |
---
+## 九、验收标准
+
+
## 🟡 阶段五:增强功能(P2/P3)
**目标**:烹饪计时器 + 用量换算 + 过敏原检测 + 烹饪笔记 + 用餐提醒 + BMI + 份量缩放
**前置依赖**:各功能独立,无强依赖
-**关键阻塞**:5.5 用餐提醒需 `flutter_local_notifications`
-| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
-|------|------|---------|--------|------|------|
-| 5.1 | 烹饪计时器页面 | `lib/src/pages/tools/cooking_timer_page.dart` | P2 | ✅ 已完成 | 已添加入口到"我的"首页 |
-
-### 开发顺序建议
-
-
-
----
-
-## 🟡 阶段七:今天吃什么增强(P1)
-
-**目标**:实现接口支持的动态筛选功能
-**前置依赖**:接口已支持(`api_what_to_eat.php` v1.24.0)
-**分析结论**:接口已完整支持随机/智能/分类/标签/营养素筛选,APP端未调用
-
-| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
-|------|------|---------|--------|------|------|
-| 7.1 | 实现分类筛选UI | `what_to_eat_page.dart` | P1 | ✅ 已完成 | 显示接口返回的分类列表供选择 |
-| 7.2 | 实现标签筛选UI | `what_to_eat_page.dart` | P1 | ✅ 已完成 | 显示接口返回的标签列表 |
-| 7.3 | 实现营养素筛选UI | `what_to_eat_page.dart` | P1 | ✅ 已完成 | 热量/蛋白质/脂肪范围滑块 |
-| 7.4 | 调用 available_filters 接口 | `what_to_eat_repository.dart` | P1 | ✅ 已完成 | 根据已选条件获取可用筛选 |
-| 7.5 | 保存筛选偏好 | `HiveService` | P2 | ✅ 已完成 | 本地记住用户偏好 |
-
-### 接口分析
-
-| 端点 | APP调用 | 说明 |
-|------|---------|------|
-| `act=random` | ✅ 已调用 | 随机模式 |
-| `act=smart` | ✅ 已调用 | 智能模式(仅用偏好) |
-| `act=config` | ✅ 已调用 | 获取配置 |
-| `act=subcategories` | ✅ 已调用 | 获取子分类 |
-| `act=available_filters` | ✅ 已调用 | 动态筛选 |
-
-### 筛选参数(接口支持)
-
-| 参数 | 类型 | APP实现 |
-|------|------|--------|
-| `include_categories` | int[] | ✅ 已实现 |
-| `exclude_categories` | int[] | ✅ 已实现 |
-| `include_tags` | int[] | ✅ 已实现 |
-| `exclude_tags` | int[] | ✅ 已实现 |
-| `exclude_allergens` | string[] | ✅ 已实现 |
-| `nutrition` | string | ✅ 已实现 |
-
-### 验收标准
-- [x] 支持分类多选筛选
-- [x] 支持标签多选筛选
-- [x] 支持营养素范围筛选
-- [x] 筛选结果实时更新
---
@@ -127,72 +98,9 @@
**目标**:修复架构违规和核心功能缺失
**发现时间**:2026-04-10(flutter analyze 全面扫描 + 项目分析)
-**关键阻塞**:无
-| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
-|------|------|---------|--------|------|------|
-| 9.1 | 热门排行点击跳转详情 | `lib/src/pages/hot/hot_page.dart` | P0 | ❌ 未修复 | L148 有 TODO,点击无响应,需跳转到 RecipeDetailPage |
-| 9.2 | 首页改用 Repository 层 | `lib/src/pages/home_page.dart` | P0 | ❌ 未修复 | 直接使用 `_apiService` 绕过 Repository,需改为 `RecipeRepository` |
-| 9.3 | 合并收藏功能(去重) | `cart_controller.dart` + `favorites_controller.dart` | P1 | ❌ 未修复 | CartController 实为收藏功能但命名混乱,需统一为 FavoritesController |
-| 9.4 | 合并搜索控制器(去重) | `search_controller.dart` + `search/search_controller.dart` | P1 | ❌ 未修复 | 两个搜索控制器并存,需合并为一个 |
-| 9.5 | 聊天页面功能化或移除 | `lib/src/pages/chat_module/chat_page.dart` | P2 | ❌ 未修复 | 纯 Demo 硬编码消息,需接入真实功能或移除入口 |
-| 9.6 | 多语言词条扩充 | `app_zh.arb` / `app_en.arb` | P1 | ❌ 未修复 | 仅 21 词条,大量中文硬编码,需补全页面文字 |
-### 问题详情
-#### 9.1 热门排行点击无法跳转详情
-- **现象**:热门排行列表项点击无响应
-- **代码位置**:`hot_page.dart:148` — `// TODO: 点击跳转到菜谱详情页`
-- **方案**:使用 `Get.toNamed('/recipe-detail', arguments: item.id)` 跳转
-
-#### 9.2 首页绕过 Repository 层
-- **现象**:`home_page.dart` 直接 `final ApiService _apiService = ApiService()`
-- **影响**:违反架构分层,无法统一缓存/错误处理/数据转换
-- **方案**:改为 `final RecipeRepository _recipeRepository = RecipeRepository()`,通过 Repository 获取数据
-
-#### 9.3 收藏功能重复
-- **现象**:`CartController` + `CartPage` 和 `FavoritesController` + `FavoritesPage` 两套并存
-- **影响**:收藏状态不同步,代码冗余
-- **方案**:
- 1. 删除 `CartController` + `CartPage`
- 2. 将 CartController 的 quantity 逻辑合并到 FavoritesController
- 3. 底部 Tab 收藏入口统一指向 FavoritesPage
-
-#### 9.4 搜索控制器重复
-- **现象**:`controllers/search_controller.dart` 和 `controllers/search/search_controller.dart` 两个文件
-- **影响**:搜索逻辑分散,修改一处容易遗漏另一处
-- **方案**:保留 `search/search_controller.dart`(目录结构更规范),删除根目录版本,更新所有引用
-
-#### 9.5 聊天页面
-- **现象**:`ChatPage` 只有硬编码消息 `{'text': '你好,这是聊天示例。', 'me': false}`
-- **方案A**:接入 AI 聊天 API(如菜谱问答)
-- **方案B**:改为"意见反馈"页面
-- **方案C**:移除入口,待后续有需求再添加
-
-#### 9.6 多语言覆盖率
-- **现象**:`app_zh.arb` 仅 21 词条,大量页面文字如"今天吃什么"、"热门排行"、"我的足迹"等硬编码
-- **方案**:逐页面提取硬编码文字到 arb 文件,优先覆盖核心页面(首页、搜索、详情、发现)
-
-### 开发顺序建议
-
-```
-9.1 热门排行跳转详情(最小改动,立竿见影)
- → 9.2 首页改用 Repository(架构规范)
- → 9.4 合并搜索控制器(消除冗余)
- → 9.3 合并收藏功能(消除冗余)
- → 9.6 多语言词条扩充(持续进行)
- → 9.5 聊天页面(最后处理)
-```
-
-### 验收标准
-- [ ] 热门排行点击可跳转到菜谱详情页
-- [ ] 首页通过 Repository 层获取数据
-- [ ] 收藏功能只有一套实现
-- [ ] 搜索功能只有一套实现
-- [ ] 聊天页面有真实功能或已移除入口
-- [ ] 核心页面文字已提取到 arb 文件
-
----
## 🟡 阶段十:代码质量提升(P1/P2)
@@ -202,51 +110,18 @@
| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
|------|------|---------|--------|------|------|
-| 10.1 | 统一 Controller 注册 | `lib/src/bindings/app_binding.dart` | P1 | ❌ 未实现 | 部分用 `Get.put()`,部分用 `Get.find()`,需统一 Binding |
-| 10.2 | HiveService 数据迁移机制 | `lib/src/services/data/hive_service.dart` | P2 | ❌ 未实现 | Box schema 变更时无迁移策略,可能导致数据丢失 |
-| 10.3 | 统一错误处理 | `lib/src/services/api/api_exception.dart` | P1 | ❌ 未实现 | 部分 Repository 抛 Exception,部分返回空数据,需统一错误码 |
-| 10.4 | 离线缓存策略 | `lib/src/services/data/storage_service.dart` | P1 | ❌ 未实现 | API 数据仅靠 Dio 缓存,离线时首页无数据 |
-| 10.5 | DesignTokens 与 ThemeService 解耦 | `lib/src/config/design_tokens.dart` | P2 | ❌ 未实现 | 页面混用静态常量和动态值,暗色模式适配不一致 |
-
-### 问题详情
-
-#### 10.1 Controller 注册不统一
-- **现象**:部分页面在 `build()` 中 `Get.put()`,部分在 `onInit()` 中 `Get.find()`
-- **影响**:Controller 生命周期不可控,可能重复创建或找不到
-- **方案**:每个页面创建对应 Binding,在 `AppBinding` 中统一注册
-
-#### 10.2 HiveService 无数据迁移
-- **现象**:Box schema 变更后直接崩溃或数据丢失
-- **方案**:添加 `boxVersion` 字段,打开 Box 时检查版本号,执行迁移逻辑
-
-#### 10.3 错误处理不统一
-- **现象**:`RecipeRepository` 抛 `Exception`,`FeedRepository` 返回空列表
-- **方案**:定义 `AppException` 层级(NetworkException / ParseException / StorageException),Repository 统一抛出
-
-#### 10.4 离线缓存
-- **现象**:无网络时首页显示"暂无菜谱"
-- **方案**:API 成功时将数据写入 Hive,离线时从 Hive 读取
-
-#### 10.5 DesignTokens 解耦
-- **现象**:页面同时使用 `DesignTokens.text1`(静态)和 `themeService.textColor.value`(动态)
-- **方案**:所有颜色值通过 `ThemeService` 获取,`DesignTokens` 仅保留间距/圆角/字号等不变量
-
-### 开发顺序建议
-
-```
-10.1 统一 Controller 注册(架构基础)
- → 10.3 统一错误处理(稳定性)
- → 10.4 离线缓存(用户体验)
- → 10.2 HiveService 迁移机制(数据安全)
- → 10.5 DesignTokens 解耦(代码规范)
-```
+| 10.1 | 统一 Controller 注册 | `lib/src/bindings/feature_binding.dart` | P1 | ✅ 已完成 | 创建 FeatureBinding,路由添加 binding 参数,页面改用 Get.find() |
+| 10.2 | HiveService 数据迁移机制 | `lib/src/services/data/hive_service.dart` | P2 | ✅ 已完成 | 添加 schema 版本号 + 迁移函数,支持 Box 升级 |
+| 10.3 | 统一错误处理 | `lib/src/errors/app_exception.dart` | P1 | ✅ 已完成 | 定义 AppException + AppErrorCode + Result,统一错误码映射 |
+| 10.4 | 离线缓存策略 | `lib/src/services/data/cache_service.dart` | P1 | ✅ 已完成 | 新增 CacheService,支持 TTL 过期 + 离线读取 |
+| 10.5 | DesignTokens 与 ThemeService 解耦 | `lib/src/services/ui/theme_service.dart` | P2 | ✅ 已完成 | 新增 DynamicTokens 类,ThemeService.tokens 统一获取主题颜色 |
### 验收标准
-- [ ] 所有 Controller 通过 Binding 注册
-- [ ] HiveService 支持 schema 版本迁移
-- [ ] Repository 统一抛出 AppException
-- [ ] 离线时首页可显示缓存数据
-- [ ] 页面颜色值统一通过 ThemeService 获取
+- [x] 所有 Controller 通过 Binding 注册
+- [x] HiveService 支持 schema 版本迁移
+- [x] Repository 统一抛出 AppException
+- [x] 离线时首页可显示缓存数据
+- [x] 页面颜色值统一通过 ThemeService 获取
---
@@ -254,65 +129,6 @@
**目标**:实现核心增强功能,提升应用价值
**前置依赖**:阶段九完成
-**关键阻塞**:无
-
-| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
-|------|------|---------|--------|------|------|
-| 11.1 | 🍳 烹饪模式 | `lib/src/pages/recipe/cooking_mode_page.dart` | P1 | ❌ 未实现 | 详情页"开始烹饪"按钮,步骤引导+计时器联动 |
-| 11.2 | 📊 营养追踪仪表盘 | `lib/src/pages/home/nutrition_dashboard.dart` | P1 | ❌ 未实现 | 首页展示今日营养摄入环形图,整合 MealRecordController |
-| 11.3 | 🛒 菜谱食材一键加入购物清单 | `lib/src/pages/recipe/recipe_detail_page.dart` | P1 | ❌ 未实现 | 详情页"加入购物清单"按钮,自动解析食材 |
-| 11.4 | 📖 菜谱步骤图文模式 | `lib/src/pages/recipe/recipe_detail_page.dart` | P2 | ❌ 未实现 | 步骤展开/折叠,每步配图+计时器 |
-
-### 功能详情
-
-#### 11.1 烹饪模式
-- **入口**:菜谱详情页 → "🍳 开始烹饪"按钮
-- **功能**:
- - 全屏步骤引导,左右滑动切换步骤
- - 当前步骤高亮,已完成步骤置灰
- - 每步自动启动计时器(如有时间信息)
- - 支持语音播报(TTS)
- - 支持暂停/继续/跳过
-- **技术方案**:PageView + StepIndicator + CookingTimerController
-
-#### 11.2 营养追踪仪表盘
-- **入口**:首页顶部卡片区域
-- **功能**:
- - 今日热量/蛋白质/脂肪/碳水摄入环形图
- - 与每日目标对比进度
- - 点击跳转到营养中心详情
-- **技术方案**:NutritionRing widget + MealRecordController
-
-#### 11.3 食材一键加入购物清单
-- **入口**:菜谱详情页 → "🛒 加入购物清单"按钮
-- **功能**:
- - 解析 `RecipeModel.ingredients` 为 `ShoppingItemModel` 列表
- - 弹出确认弹窗,可勾选/取消勾选食材
- - 确认后写入 Hive
-- **技术方案**:ShoppingListController.addFromRecipe()
-
-#### 11.4 步骤图文模式
-- **入口**:菜谱详情页步骤区域
-- **功能**:
- - 步骤卡片展开/折叠
- - 每步配图(如有)
- - 每步计时器快捷按钮
- - 步骤编号 + 预计时间
-
-### 开发顺序建议
-
-```
-11.3 食材加入购物清单(最小改动,复用现有 ShoppingListController)
- → 11.2 营养追踪仪表盘(复用现有 NutritionRing + MealRecordController)
- → 11.4 步骤图文模式(增强详情页)
- → 11.1 烹饪模式(最复杂,需新建页面)
-```
-
-### 验收标准
-- [ ] 详情页有"开始烹饪"按钮,点击进入步骤引导模式
-- [ ] 首页显示今日营养摄入环形图
-- [ ] 详情页可一键将食材加入购物清单
-- [ ] 详情页步骤支持展开/折叠
---
@@ -368,7 +184,6 @@
| 依赖 | 用途 | 纯Dart | 鸿蒙兼容 |
|------|------|--------|---------|
| `share_plus` | 系统分享 | ❌ | ⚠️ 需适配 |
-| `flutter_local_notifications` | 本地通知 | ❌ | ⚠️ 需适配 |
| `image_picker` | 拍照/相册 | ❌ | ⚠️ 需适配 |
| `screenshot` | 截图 | ✅ | ✅ |
@@ -437,6 +252,466 @@
---
+## 🟢 阶段十四:接口能力挖掘(P1/P2)
+
+**目标**:利用已有API接口能力,实现App端未开发的功能
+**前置依赖**:API v2.0.0 已支持完整接口
+**关键阻塞**:无
+**数据来源**:`docs/api/doc/API_DOC.md` + `docs/api/doc/APP_GUIDE.md`
+
+| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
+|------|------|---------|--------|------|------|
+| 14.1 | 🍽️ 用餐时段推荐 | `lib/src/pages/home/meal_time_recommend.dart` | P1 | ❌ 未实现 | 根据时间推荐早餐/午餐/晚餐 |
+| 14.2 | 📊 营养分析增强 | `lib/src/pages/nutrition/nutrition_detail_page.dart` | P1 | ❌ 未实现 | 营养成分详情+趋势图表 |
+| 14.3 | ⚠️ 过敏原警示增强 | `lib/src/pages/recipe/recipe_detail_page.dart` | P1 | ❌ 未实现 | 详情页过敏原警示+食材替代建议 |
+| 14.4 | 🔥 点赞/推荐系统 | `lib/src/services/action_service.dart` | P2 | ❌ 未实现 | 点赞/取消点赞+五星评分 |
+| 14.5 | 📱 社交分享 | `lib/src/pages/recipe/share_recipe_page.dart` | P2 | ❌ 未实现 | 生成分享链接+二维码海报 |
+| 14.6 | 👤 个性化信息流 | `lib/src/pages/home_page.dart` | P1 | ❌ 未实现 | 基于用户偏好的首页推荐 |
+| 14.7 | 🥕 食材详情页 | `lib/src/pages/ingredient/ingredient_detail_page.dart` | P2 | ❌ 未实现 | 食材介绍+营养+选购指南 |
+| 14.8 | 📈 浏览量统计 | `lib/src/services/analytics_service.dart` | P2 | ❌ 未实现 | 增加浏览量+热度标签展示 |
+
+### 功能详情
+
+#### 14.1 用餐时段推荐
+- **接口支持**:`api.php?act=search&keyword=早餐/中餐/晚餐`
+
+---
+
+## 🔴 阶段十六:用户体验优化+Bug修复(P0/P1)
+
+**目标**:修复用户反馈的7个严重问题,提升应用稳定性和用户体验
+**发现时间**:2026-04-10(用户反馈)
+**关键阻塞**:无
+**优先级**:P0=最高优先级(影响用户使用的严重问题)
+
+| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
+|------|------|---------|--------|------|------|
+| 16.1 | 🚀 启动加载优化+骨架屏 | `lib/src/pages/home_page.dart` | P0 | ✅ 已完成 | 添加超时保护+骨架屏组件+缓存优先策略 |
+| 16.2 | 🛠️ 收藏页面"更多"卡死修复 | `lib/src/pages/favorites/favorites_page.dart` | P0 | ✅ 已完成 | 添加Binding+错误处理+空指针保护 |
+| 16.3 | 🔍 搜索详情卡死修复 | `lib/src/pages/search/search_page.dart` | P0 | ✅ 已完成 | 为RecipeDetailPage添加Binding+Controller安全获取 |
+| 16.4 | 🎲 今天吃什么动态筛选优化 | `lib/src/pages/what_to_eat/what_to_eat_page.dart` | P1 | ✅ 已完成 | 优化UI显示+添加错误提示+空结果处理 |
+| 16.5 | 📊 营养中心报告按钮修复 | `lib/src/pages/nutrition/nutrition_center_page.dart` | P1 | ✅ 已完成 | 检查NutritionBinding+添加错误处理 |
+| 16.6 | ❤️ 收藏页面UI重构 | `lib/src/pages/favorites/favorites_page.dart` | P2 | ❌ 未实现 | iOS 26 Liquid Glass风格+优化按钮尺寸 |
+| 16.7 | 🔥 热门排行数据修复 | `lib/src/repositories/hot_repository.dart` | P1 | ✅ 已完成 | 检查API返回+添加错误提示+调试日志 |
+
+### 问题详情
+
+#### 16.1 启动加载慢+无骨架屏
+- **现象**:启动应用时 loading 动画超过10秒,首页点击菜谱 loading 超过5秒
+- **原因分析**:
+ 1. `RecipeRepository.fetchFeedRecipes()` 没有超时保护
+ 2. 首页没有骨架屏,只有简单的 loading 动画
+ 3. 没有缓存机制,每次都要从网络获取数据
+- **解决方案**:
+ 1. 添加超时保护(12秒)
+ 2. 创建骨架屏组件 `SkeletonLoader`
+ 3. 添加缓存优先策略,优先显示缓存数据
+
+#### 16.2 收藏页面点击"更多"卡死闪退
+- **现象**:收藏页面点击"更多"按钮后应用卡死闪退
+- **原因分析**:
+ 1. `ToolsCenterPage` 使用 `Get.put(ToolsController())` 直接注册
+ 2. 可能 `ToolsController` 初始化时出错
+ 3. 没有错误处理和空指针保护
+- **解决方案**:
+ 1. 创建 `ToolsBinding` 并添加到路由配置
+ 2. 使用 `Get.find()` 获取 Controller
+ 3. 添加 try-catch 错误处理
+
+#### 16.3 搜索结果点击详情卡死闪退
+- **现象**:搜索后点击详细结果,应用卡死闪退
+- **原因分析**:
+ 1. `RecipeDetailPage` 使用 `Get.find()`
+ 2. 如果 Controller 未注册会抛出异常
+ 3. `ActionController` 和 `ShoppingListController` 初始化可能失败
+- **解决方案**:
+ 1. 为 `RecipeDetailPage` 添加完整的 Binding
+ 2. 所有 Controller 使用 try-catch 保护
+ 3. 添加空指针检查和默认值
+
+#### 16.4 今天吃什么动态筛选问题
+- **现象**:不支持动态筛选,随机选择有时不显示结果
+- **原因分析**:
+ 1. 代码已实现动态筛选,但 UI 不够明显
+ 2. 随机选择不显示结果可能是 API 返回空数据
+ 3. 没有错误提示
+- **解决方案**:
+ 1. 优化 UI,使筛选功能更明显
+ 2. 添加错误提示和空结果处理
+ 3. 添加调试日志,排查 API 问题
+
+#### 16.5 营养中心报告按钮卡死
+- **现象**:点击右上角"报告"按钮应用卡死闪退,"今天"按钮无反应
+- **原因分析**:
+ 1. `NutritionReportPage` 使用 `Get.find()`
+ 2. 路由配置中已有 `NutritionBinding`
+ 3. 可能是 `MealRecordController` 初始化失败
+- **解决方案**:
+ 1. 检查 `NutritionBinding` 是否正确注册
+ 2. 添加错误处理和空指针保护
+ 3. 添加加载状态提示
+
+#### 16.6 收藏页面UI设计问题
+- **现象**:排版杂乱无章,按钮太小点不到,不符合操作逻辑
+- **原因分析**:
+ 1. UI 布局不够清晰
+ 2. 按钮尺寸不符合 iOS 设计规范
+ 3. 操作流程不顺畅
+- **解决方案**:
+ 1. 重构 UI,使用 iOS 26 Liquid Glass 风格
+ 2. 增大按钮尺寸至最小 44x44
+ 3. 优化操作流程和布局比例
+
+#### 16.7 热门排行数据为空
+- **现象**:今日浏览量、点赞数、推荐数都显示"暂无数据"
+- **原因分析**:
+ 1. `HotRepository` 调用 `stats_full.php?act=hot` API
+ 2. API 可能返回空数据或数据结构不匹配
+ 3. 没有错误提示
+- **解决方案**:
+ 1. 检查 API 返回数据结构
+ 2. 添加错误提示和空数据处理
+ 3. 添加调试日志,排查 API 问题
+
+### 开发顺序建议
+
+```
+16.1 启动加载优化(影响最大,用户第一印象)
+ → 16.2 收藏页面"更多"修复(严重闪退)
+ → 16.3 搜索详情修复(严重闪退)
+ → 16.5 营养中心修复(功能性问题)
+ → 16.4 今天吃什么优化(体验问题)
+ → 16.7 热门排行修复(数据问题)
+ → 16.6 收藏页面UI重构(体验优化)
+```
+
+### 验收标准
+- [ ] 启动应用 3 秒内显示骨架屏,5 秒内加载完成
+- [ ] 收藏页面点击"更多"正常跳转,无卡死闪退
+- [ ] 搜索结果点击详情正常跳转,无卡死闪退
+- [ ] 今天吃什么支持动态筛选,随机选择有结果提示
+- [ ] 营养中心报告按钮正常跳转,"今天"按钮有反馈
+- [ ] 收藏页面 UI 整洁美观,按钮易于点击
+- [ ] 热门排行显示真实数据,无数据时有友好提示
+
+### 技术要点
+
+#### 骨架屏组件
+```dart
+class SkeletonLoader extends StatefulWidget {
+ final double width;
+ final double height;
+ final BorderRadius? borderRadius;
+
+ const SkeletonLoader({
+ required this.width,
+ required this.height,
+ this.borderRadius,
+ });
+}
+
+class _SkeletonLoaderState extends State
+ with SingleTickerProviderStateMixin {
+ late AnimationController _controller;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 1500),
+ )..repeat();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedBuilder(
+ animation: _controller,
+ builder: (context, child) {
+ return Container(
+ width: widget.width,
+ height: widget.height,
+ decoration: BoxDecoration(
+ color: Colors.grey[300],
+ borderRadius: widget.borderRadius,
+ ),
+ );
+ },
+ );
+ }
+}
+```
+
+#### Controller 安全获取
+```dart
+FavoritesController? _favoritesController;
+
+@override
+void initState() {
+ super.initState();
+ try {
+ _favoritesController = Get.find();
+ } catch (e) {
+ debugPrint('FavoritesController not found: $e');
+ _favoritesController = null;
+ }
+}
+```
+
+#### 超时保护
+```dart
+Future> fetchFeedRecipes() async {
+ try {
+ final results = await _recipeRepository.fetchFeedRecipes()
+ .timeout(const Duration(seconds: 12));
+ return results;
+ } on TimeoutException {
+ debugPrint('fetchFeedRecipes timeout');
+ return [];
+ } catch (e) {
+ debugPrint('fetchFeedRecipes error: $e');
+ return [];
+ }
+}
+```
+- **数据文件**:`http://eat.wktyl.com/api/assets/eating_times.json`(34种时段)
+- **功能**:
+ - 🌅 早餐推荐(7-10点)
+ - 🍱 午餐推荐(11-14点)
+ - 🌙 晚餐推荐(17-20点)
+ - 📅 每日菜单规划(早中晚餐)
+
+#### 14.2 营养分析增强
+- **接口支持**:`api.php?act=full&id=xxx` 返回 `nutrition` 字段
+- **数据文件**:`http://eat.wktyl.com/api/assets/nutrition_types.json`(31种营养成分)
+- **功能**:
+ - 📊 营养成分详情展示(维生素/矿物质/宏量营养素)
+ - 🎯 每日营养目标追踪
+ - 🏋️ 健身餐推荐(高蛋白/低碳水)
+ - 📈 营养趋势分析图表
+
+#### 14.3 过敏原警示增强
+- **接口支持**:`api.php?act=full&id=xxx` 返回 `allergens` 字段
+- **数据文件**:`http://eat.wktyl.com/api/assets/gmy.json`(585种过敏原数据)
+- **功能**:
+ - ⚠️ 菜谱详情页过敏原警示
+ - 🚫 自动过滤含过敏原菜谱
+ - 🔄 食材替代建议
+ - 📋 过敏原报告生成
+
+#### 14.4 点赞/推荐系统
+- **接口支持**:
+ - `api_action.php?act=like&type=recipe&id=1&action=like/unlike`
+ - `api_action.php?act=recommend&type=recipe&id=1&score=5`
+- **功能**:
+ - 👍 点赞/取消点赞
+ - ⭐ 五星评分
+ - 📊 用户评价统计
+ - 🏆 推荐排行榜
+
+#### 14.5 社交分享
+- **接口支持**:`api_what_to_eat.php?act=detail&code=CP032892`(code字段生成分享链接)
+- **功能**:
+ - 🔗 分享链接生成(`https://eat.wktyl.com/recipe/CP032892`)
+ - 📱 二维码海报
+ - 📊 热度标签展示(🔥爆款/📈热门/❤️受欢迎)
+ - 👥 社交平台分享
+
+#### 14.6 个性化信息流
+- **接口支持**:`api_feed.php?act=personal&user_id=xxx`
+- **功能**:
+ - 🎯 基于偏好推荐
+ - 🚫 自动过滤过敏原
+ - 📊 千人千面首页
+ - 🔄 智能刷新
+
+#### 14.7 食材详情页
+- **接口支持**:`api.php?act=ingredient_detail&id=1`
+- **返回字段**:`introduction`/`nutrition`/`usage_tip`/`effect`/`guidance`
+- **功能**:
+ - 📖 食材介绍
+ - 🥗 营养成分
+ - 💡 使用技巧
+ - 🏥 食疗功效
+ - 🛒 选购指南
+
+#### 14.8 浏览量统计
+- **接口支持**:
+ - `api_action.php?act=view&type=recipe&id=1&count=1`
+ - `api.php?act=detail&id=xxx&viewnums=true`
+- **功能**:
+ - 📈 增加浏览量
+ - 🔥 热门排行统计
+ - 📊 用户浏览历史
+ - 📈 趋势分析
+
+### 开发优先级
+
+| 优先级 | 功能 | 说明 |
+|--------|------|------|
+| **P1** | 用餐时段推荐 | 接口完整,实现简单,用户价值高 |
+| **P1** | 个性化信息流 | 接口完整,提升用户体验 |
+| **P1** | 过敏原警示增强 | 接口完整,健康安全 |
+| **P2** | 点赞/推荐系统 | 接口完整,增加互动 |
+| **P2** | 营养分析增强 | 接口完整,健康管理 |
+| **P2** | 社交分享 | 接口完整,增加传播 |
+| **P2** | 食材详情页 | 接口完整,内容丰富 |
+| **P2** | 浏览量统计 | 接口完整,数据驱动 |
+
+### 验收标准
+- [ ] 首页根据时段推荐早餐/午餐/晚餐
+- [ ] 营养中心展示详细营养成分
+- [ ] 详情页显示过敏原警示
+- [ ] 详情页可点赞/评分
+- [ ] 详情页可分享菜谱
+- [ ] 首页个性化推荐
+- [ ] 食材可查看详情页
+- [ ] 浏览量正确统计
+
+---
+
+## 🔴 阶段十五:后端接口增强(P1/P2)
+
+**目标**:新增后端接口,实现高级功能
+**前置依赖**:后端开发配合
+**关键阻塞**:需要后端新增接口
+**优先级说明**:🔴 红色表示需要后端支持
+
+| 序号 | 任务 | 建议接口 | 优先级 | 状态 | 说明 |
+|------|------|---------|--------|------|------|
+| 15.1 | 👤 用户注册登录 | `api_user.php?act=register/login` | P1 | ❌ 未实现 | 用户账号体系 |
+| 15.2 | 💾 收藏云端同步 | `api_favorite.php?act=add/remove/list` | P1 | ❌ 未实现 | 收藏数据云端存储 |
+| 15.3 | 💬 评论系统 | `api_comment.php?act=list/add/delete` | P2 | ❌ 未实现 | 菜谱评论功能 |
+| 15.4 | 🔔 消息推送 | `api_message.php?act=list/read` | P2 | ❌ 未实现 | 站内信+推送通知 |
+| 15.5 | 📜 浏览历史同步 | `api_history.php?act=add/list` | P2 | ❌ 未实现 | 浏览历史云端存储 |
+| 15.6 | 📝 菜谱上传 | `api_recipe.php?act=add/edit/delete` | P2 | ❌ 未实现 | 用户菜谱上传/编辑 |
+
+### 功能详情
+
+#### 15.1 用户注册登录
+- **现状**:❌ 无注册登录接口
+- **建议接口**:
+ ```
+ POST api_user.php?act=register
+ { "username": "xxx", "password": "xxx", "email": "xxx" }
+
+ POST api_user.php?act=login
+ { "username": "xxx", "password": "xxx" }
+
+ GET api_user.php?act=profile&user_id=xxx
+ ```
+- **功能**:
+ - 📱 手机号注册/登录
+ - 🔗 第三方登录(微信/Apple ID)
+ - 👤 用户资料管理
+ - 🔐 密码找回
+
+#### 15.2 收藏云端同步
+- **现状**:⚠️ 仅本地 Hive 存储
+- **建议接口**:
+ ```
+ POST api_favorite.php?act=add
+ { "user_id": "xxx", "recipe_id": 123 }
+
+ GET api_favorite.php?act=list&user_id=xxx
+
+ DELETE api_favorite.php?act=remove
+ { "user_id": "xxx", "recipe_id": 123 }
+ ```
+- **功能**:
+ - ☁️ 收藏云端存储
+ - 🔄 多设备同步
+ - 📂 收藏分类管理
+ - 🔗 收藏分享
+
+#### 15.3 评论系统
+- **现状**:❌ 无评论接口
+- **建议接口**:
+ ```
+ GET api_comment.php?act=list&recipe_id=123&page=1
+
+ POST api_comment.php?act=add
+ { "user_id": "xxx", "recipe_id": 123, "content": "xxx" }
+
+ DELETE api_comment.php?act=delete
+ { "user_id": "xxx", "comment_id": 456 }
+ ```
+- **功能**:
+ - 💬 发表评论
+ - 👍 评论点赞
+ - 📝 评论回复
+ - 🔔 评论通知
+
+#### 15.4 消息推送
+- **现状**:❌ 无推送接口
+- **建议接口**:
+ ```
+ GET api_message.php?act=list&user_id=xxx
+
+ POST api_message.php?act=read
+ { "user_id": "xxx", "message_id": 123 }
+ ```
+- **功能**:
+ - 📬 站内信
+ - 🔔 推送通知
+ - 📢 系统公告
+ - 💬 评论提醒
+
+#### 15.5 浏览历史同步
+- **现状**:⚠️ 仅本地存储
+- **建议接口**:
+ ```
+ POST api_history.php?act=add
+ { "user_id": "xxx", "recipe_id": 123 }
+
+ GET api_history.php?act=list&user_id=xxx&page=1
+ ```
+- **功能**:
+ - ☁️ 浏览历史云端存储
+ - 🔄 多设备同步
+ - 📊 浏览统计
+ - 🧹 历史清理
+
+#### 15.6 菜谱上传
+- **现状**:❌ 无上传接口
+- **建议接口**:
+ ```
+ POST api_recipe.php?act=add
+ { "title": "xxx", "ingredients": [...], "steps": [...] }
+
+ PUT api_recipe.php?act=edit
+ { "recipe_id": 123, "title": "xxx" }
+
+ DELETE api_recipe.php?act=delete
+ { "recipe_id": 123 }
+ ```
+- **功能**:
+ - 📝 用户菜谱上传
+ - ✏️ 菜谱编辑
+ - 🗑️ 菜谱删除
+ - 📊 菜谱审核
+
+### 开发优先级
+
+| 优先级 | 功能 | 说明 |
+|--------|------|------|
+| **P1** | 用户注册登录 | 核心功能,其他功能依赖用户体系 |
+| **P1** | 收藏云端同步 | 用户数据安全,多设备同步 |
+| **P2** | 评论系统 | 增加互动,提升活跃度 |
+| **P2** | 消息推送 | 用户触达,提升留存 |
+| **P2** | 浏览历史同步 | 用户体验,多设备同步 |
+| **P2** | 菜谱上传 | UGC内容,丰富平台 |
+
+### 验收标准
+- [ ] 用户可注册/登录
+- [ ] 收藏数据云端同步
+- [ ] 菜谱可评论
+- [ ] 收到系统通知
+- [ ] 浏览历史云端同步
+- [ ] 用户可上传菜谱
+
+---
+
## 📎 软件特性功能汇总
> 以下功能已开发完成或开发中,从历史版本号归档而来
@@ -458,3 +733,177 @@
| Liquid Glass 风格 | ✅ 已完成 | v0.6x | 底栏+搜索栏+分段控件+卡片 |
| 收藏管理 | ✅ 已完成 | v0.8x | 编辑/排序/分类/跳转详情 |
| 静态分析清理 | ✅ 已完成 | v0.52 | 107→1 个 info,0 error/warning |
+| 工具中心 | ✅ 已完成 | v0.9x | 工具入口Bar+分类筛选+使用频率统计 |
+| 过敏原检查工具 | ✅ 已完成 | v0.9x | 食材过敏原查询与分类浏览 |
+| 用餐时段推荐 | ✅ 已完成 | v0.9x | 根据时间推荐早中晚餐菜谱 |
+| 每周菜单规划 | ✅ 已完成 | v0.9x | 一周三餐规划与进度追踪 |
+| 食材详情查询 | ✅ 已完成 | v0.9x | 食材营养信息与选购指南 |
+
+---
+
+## 🛠️ 工具中心开发记录
+
+> 2026-04-10 开发完成
+
+### 已实现工具列表
+
+| 工具名称 | 路由 | 是否联网 | 说明 |
+|---------|------|---------|------|
+| ⏱️ 烹饪计时器 | `/tools/timer` | ❌ 离线 | 多步骤倒计时 |
+| 📏 用量换算 | `/tools/converter` | ❌ 离线 | 常用单位换算 |
+| 🧮 BMI 计算器 | `/tools/bmi` | ❌ 离线 | 含健康建议 |
+| ⚖️ 份量缩放 | `/tools/scaler` | ❌ 离线 | 按比例调整食材用量 |
+| 🥜 过敏原检查 | `/tools/allergen` | ✅ 联网 | 食材过敏原查询 |
+| 🍽️ 用餐时段推荐 | `/tools/meal-time` | ✅ 联网 | 根据时间推荐菜谱 |
+| 📅 每周菜单规划 | `/tools/planner` | ❌ 离线 | 一周三餐规划 |
+| 🥕 食材详情查询 | `/tools/ingredient` | ✅ 联网 | 食材营养信息 |
+| 📊 营养中心 | `/tools/nutrition` | ✅ 联网 | 营养追踪仪表盘 |
+| 📈 热门统计 | `/tools/stats` | ✅ 联网 | 热门菜谱排行 |
+
+### 技术实现
+
+1. **工具数据模型** (`tool_item_model.dart`)
+ - ToolItem: 工具项数据结构
+ - ToolCategory: 工具分类枚举
+ - ToolRegistry: 工具注册表
+
+2. **工具控制器** (`tools_controller.dart`)
+ - 工具列表管理
+ - 使用频率统计(SharedPreferences)
+ - 搜索与分类过滤
+ - 常用工具推荐
+
+3. **工具中心页面** (`tools_center_page.dart`)
+ - 搜索栏
+ - 分类标签筛选
+ - 工具网格布局(一行两个)
+ - 联网状态指示器(绿点/红点)
+
+4. **收藏页工具入口Bar**
+ - 显示常用工具快捷入口
+ - 更多工具入口按钮
+ - 使用频率排序
+
+### 文件清单
+
+```
+lib/src/
+├── models/tools/
+│ └── tool_item_model.dart # 工具数据模型
+├── controllers/tools/
+│ └── tools_controller.dart # 工具控制器
+└── pages/tools/
+ ├── tools_center_page.dart # 工具中心页面
+ ├── allergen_checker_page.dart # 过敏原检查
+ ├── meal_time_recommend_page.dart # 用餐时段推荐
+ ├── meal_planner_page.dart # 每周菜单规划
+ └── ingredient_detail_page.dart # 食材详情查询
+```
+
+---
+
+## 🔴 阶段十七:紧急Bug修复(P0/P1)
+
+**目标**:修复用户反馈的多个严重问题,提升应用稳定性
+**发现时间**:2026-04-11(用户反馈)
+**关键阻塞**:无
+**优先级**:P0=最高优先级(影响用户使用的严重问题)
+
+| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 |
+|------|------|---------|--------|------|------|
+| 17.1 | 🔍 搜索详情卡死修复 | `lib/src/pages/search/search_page.dart` | P0 | ✅ 已完成 | 使用Get.toNamed命名路由跳转 |
+| 17.2 | ❤️ 收藏页更多按钮GetX报错 | `lib/src/pages/favorites/favorites_page.dart` | P0 | ✅ 已完成 | 使用命名路由跳转工具中心 |
+| 17.3 | 🌙 夜间模式字体颜色优化 | `lib/src/config/design_tokens.dart` | P1 | ✅ 已完成 | 调整DarkDesignTokens文字颜色值 |
+| 17.4 | 📊 营养中心报告GetX报错 | `lib/src/bindings/feature_binding.dart` | P1 | ✅ 已完成 | Controller改为permanent注册 |
+| 17.5 | 🎲 今天吃什么筛选优化 | `lib/src/pages/what_to_eat/what_to_eat_page.dart` | P1 | ✅ 已完成 | 修复筛选逻辑和随机选择 |
+| 17.6 | 🏠 首页加载骨架屏优化 | `lib/src/pages/home_page.dart` | P1 | ✅ 已完成 | 添加骨架屏组件和超时保护 |
+| 17.7 | 📋 购物清单UI优化 | `lib/src/pages/shopping/shopping_list_page.dart` | P2 | ✅ 已完成 | 增大图标尺寸,优化布局 |
+| 17.8 | ⚙️ 主题设置iOS风格重构 | `lib/src/pages/settings/personalization_page.dart` | P2 | ✅ 已完成 | 重构为iOS设计风格 |
+| 17.9 | 🏷️ 口味偏好数据修复 | `lib/src/pages/settings/preference_page.dart` | P1 | ✅ 已完成 | 添加错误处理和默认数据 |
+| 17.10 | 👁️ 菜谱详情浏览量统计 | `lib/src/pages/recipe/recipe_detail_page.dart` | P1 | ✅ 已完成 | 增加浏览量并显示 |
+| 17.11 | 🔥 热门排行今日数据修复 | `lib/src/pages/hot/hot_page.dart` | P1 | ✅ 已完成 | 修复API参数传递 |
+| 17.12 | 🍽️ 用餐时段推荐卡死修复 | `lib/src/pages/tools/meal_time_recommend_page.dart` | P0 | ✅ 已完成 | 添加超时处理和默认数据 |
+| 17.13 | 🛠️ 工具中心跳转分裂修复 | `lib/src/controllers/tools/tools_controller.dart` | P0 | ✅ 已完成 | 使用Get.toNamed命名路由 |
+| 17.14 | 📝 文档更新 | `docs/dev/UNFINISHED_FEATURES.md` | P2 | ✅ 已完成 | 记录修复内容 |
+
+### 问题详情
+
+#### 17.1 搜索详情卡死
+- **现象**:搜索结果点击详情后应用卡死闪退
+- **原因**:使用Get.to()直接跳转Widget,未正确注册Binding
+- **方案**:改用Get.toNamed('/recipe-detail', arguments: recipeId)
+
+#### 17.2 收藏页更多按钮报错
+- **现象**:点击"更多"按钮后GetX报错
+- **原因**:直接跳转ToolsCenterPage未通过路由系统
+- **方案**:使用Get.toNamed('/tools')
+
+#### 17.3 夜间模式字体颜色
+- **现象**:夜间模式大部分字体灰色看不清
+- **原因**:DarkDesignTokens文字颜色值过暗
+- **方案**:调整text2为#EBEBF5,text3为#8E8E93
+
+#### 17.4 营养中心报告报错
+- **现象**:点击报告按钮GetX报错
+- **原因**:MealRecordController未正确注册
+- **方案**:在FeatureBinding中使用permanent: true注册
+
+#### 17.5 今天吃什么筛选
+- **现象**:分类筛选不支持动态筛选,随机选择无结果
+- **原因**:筛选逻辑未正确触发,空结果无提示
+- **方案**:优化筛选触发机制,添加空结果Toast提示
+
+#### 17.6 首页加载优化
+- **现象**:加载时间超过8秒,无骨架屏
+- **原因**:无超时保护,无骨架屏组件
+- **方案**:添加12秒超时保护,创建SkeletonLoader组件
+
+#### 17.7 购物清单UI
+- **现象**:图标太小,列表分裂,容易误触
+- **原因**:图标尺寸过小,间距不合理
+- **方案**:增大图标尺寸,优化布局间距
+
+#### 17.8 主题设置风格
+- **现象**:不符合iOS设计风格
+- **原因**:使用Material风格组件
+- **方案**:重构为Cupertino风格,符合iOS设计规范
+
+#### 17.9 口味偏好数据
+- **现象**:显示"暂无分类数据"
+- **原因**:API返回数据未正确处理
+- **方案**:添加错误处理和默认数据展示
+
+#### 17.10 菜谱浏览量
+- **现象**:详情页无浏览量显示
+- **原因**:未调用浏览量统计接口
+- **方案**:加载时调用viewnums接口,显示浏览次数
+
+#### 17.11 热门排行今日数据
+- **现象**:无今日数据
+- **原因**:API参数传递错误
+- **方案**:修复period参数为'today'
+
+#### 17.12 用餐时段推荐卡死
+- **现象**:页面卡死黑屏
+- **原因**:网络请求无超时处理,无错误状态
+- **方案**:添加8秒超时,添加默认用餐时段数据
+
+#### 17.13 工具中心跳转分裂
+- **现象**:跳转页面出现分裂感,左边工具中心右边目标页面
+- **原因**:路由跳转方式问题
+- **方案**:统一使用Get.toNamed命名路由
+
+### 验收标准
+- [x] 搜索结果点击详情正常跳转
+- [x] 收藏页更多按钮正常跳转
+- [x] 夜间模式字体清晰可见
+- [x] 营养中心报告按钮正常跳转
+- [x] 今天吃什么支持动态筛选
+- [x] 首页加载有骨架屏
+- [x] 购物清单图标易于点击
+- [x] 主题设置符合iOS风格
+- [x] 口味偏好正常显示数据
+- [x] 菜谱详情显示浏览量
+- [x] 热门排行显示今日数据
+- [x] 用餐时段推荐正常加载
+- [x] 工具中心跳转无分裂感
diff --git a/lib/main.dart b/lib/main.dart
index e8b227b..fb680e8 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -5,11 +5,11 @@ import 'package:get/get.dart';
import 'package:mom_kitchen/src/l10n/app_localizations.dart';
// 📋 页面注册系统 - 管理路由和页面定义
-// 文件: lib/src/routes/app_routes.dart
-import 'package:mom_kitchen/src/routes/app_routes.dart';
+// 文件: lib/src/config/app_routes.dart
+import 'package:mom_kitchen/src/config/app_routes.dart';
import 'package:mom_kitchen/src/services/core/app_service.dart';
-import 'package:mom_kitchen/src/services/device/orientation_service.dart';
+import 'package:mom_kitchen/src/services/orientation_service.dart';
import 'package:mom_kitchen/src/services/ui/theme_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
@@ -18,7 +18,7 @@ import 'package:mom_kitchen/src/services/ui/toast_service.dart';
import 'package:mom_kitchen/src/standards/page_validator.dart';
// 📋 全局 Binding - 统一管理 Controller 和 Service 生命周期
-import 'package:mom_kitchen/src/bindings/app_binding.dart';
+import 'package:mom_kitchen/src/app_binding.dart';
import 'package:mom_kitchen/src/utils/app_logger.dart';
diff --git a/lib/src/app_binding.dart b/lib/src/app_binding.dart
new file mode 100644
index 0000000..8512026
--- /dev/null
+++ b/lib/src/app_binding.dart
@@ -0,0 +1,127 @@
+// 2026-04-09 | AppBinding | 全局Binding | Web端跳过permission注册
+// 2026-04-10 | 移除 CartController 注册(收藏功能统一使用 FavoritesController)
+// 2026-04-10 | 新增 ShoppingListController 全局注册(首页需要使用)
+import 'package:flutter/foundation.dart';
+import 'package:get/get.dart';
+import 'package:mom_kitchen/src/controllers/feed/action_controller.dart';
+import 'package:mom_kitchen/src/controllers/feed/hot_controller.dart';
+import 'package:mom_kitchen/src/controllers/favorites_controller.dart';
+import 'package:mom_kitchen/src/controllers/feed/feed_controller.dart';
+import 'package:mom_kitchen/src/controllers/home_controller.dart';
+import 'package:mom_kitchen/src/controllers/user/preference_controller.dart';
+import 'package:mom_kitchen/src/controllers/user/profile_controller.dart';
+import 'package:mom_kitchen/src/controllers/user/personalization_controller.dart';
+import 'package:mom_kitchen/src/controllers/main_navigation_controller.dart';
+import 'package:mom_kitchen/src/controllers/meal_record_controller.dart';
+import 'package:mom_kitchen/src/controllers/search_controller.dart';
+import 'package:mom_kitchen/src/controllers/shopping_list_controller.dart';
+import 'package:mom_kitchen/src/controllers/tools_controller.dart';
+import 'package:mom_kitchen/src/controllers/what_to_eat_controller.dart';
+import 'package:mom_kitchen/src/services/core/app_service.dart';
+import 'package:mom_kitchen/src/services/ui/theme_service.dart';
+
+class AppBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(() => AppService.instance.api, fenix: true);
+ Get.lazyPut(() => AppService.instance.storage, fenix: true);
+ if (!kIsWeb && AppService.instance.permission != null) {
+ Get.lazyPut(() => AppService.instance.permission!, fenix: true);
+ }
+ Get.lazyPut(() => AppService.instance.logger, fenix: true);
+ Get.lazyPut(() => AppService.instance.animation, fenix: true);
+ Get.lazyPut(() => AppService.instance.screenUtil, fenix: true);
+ Get.lazyPut(() => AppService.instance.appInfo, fenix: true);
+ Get.lazyPut(() => AppService.instance.toast, fenix: true);
+
+ Get.put(ThemeService.instance, permanent: true);
+ Get.put(PersonalizationController(), permanent: true);
+
+ Get.put(ActionController(), permanent: true);
+ Get.put(FavoritesController(), permanent: true);
+ Get.put(ShoppingListController(), permanent: true);
+ Get.put(HomeController(), permanent: true);
+ Get.put(FeedController(), permanent: true);
+ Get.put(PreferenceController(), permanent: true);
+ Get.put(ProfileController(), permanent: true);
+ Get.put(MainNavigationController(), permanent: true);
+ }
+}
+
+class MainBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.put(HotController());
+ Get.put(WhatToEatController());
+ Get.put(FavoritesController(), permanent: true);
+ Get.put(ShoppingListController(), permanent: true);
+ }
+}
+
+class DiscoverBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(() => HotController());
+ }
+}
+
+class HotBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(() => HotController());
+ }
+}
+
+class WhatToEatBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(() => WhatToEatController());
+ }
+}
+
+class SearchBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(() => SearchController());
+ }
+}
+
+class NutritionBinding extends Bindings {
+ @override
+ void dependencies() {
+ if (!Get.isRegistered()) {
+ Get.put(MealRecordController(), permanent: true);
+ }
+ }
+}
+
+class ShoppingBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(() => ShoppingListController());
+ }
+}
+
+class FavoritesBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.put(FavoritesController(), permanent: true);
+ Get.put(ToolsController(), permanent: true);
+ }
+}
+
+class RecipeDetailBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.put(FavoritesController(), permanent: true);
+ Get.put(ActionController(), permanent: true);
+ Get.put(ShoppingListController(), permanent: true);
+ }
+}
+
+class ToolsBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.put(ToolsController(), permanent: true);
+ }
+}
diff --git a/lib/src/bindings/app_binding.dart b/lib/src/bindings/app_binding.dart
deleted file mode 100644
index 7d06eb5..0000000
--- a/lib/src/bindings/app_binding.dart
+++ /dev/null
@@ -1,41 +0,0 @@
-// 2026-04-09 | AppBinding | 全局Binding | Web端跳过permission注册
-// 2026-04-10 | 移除 CartController 注册(收藏功能统一使用 FavoritesController)
-import 'package:flutter/foundation.dart';
-import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/feed/action_controller.dart';
-import 'package:mom_kitchen/src/controllers/favorites/favorites_controller.dart';
-import 'package:mom_kitchen/src/controllers/feed/feed_controller.dart';
-import 'package:mom_kitchen/src/controllers/home/home_controller.dart';
-import 'package:mom_kitchen/src/controllers/user/preference_controller.dart';
-import 'package:mom_kitchen/src/controllers/user/profile_controller.dart';
-import 'package:mom_kitchen/src/controllers/user/personalization_controller.dart';
-import 'package:mom_kitchen/src/controllers/home/main_navigation_controller.dart';
-import 'package:mom_kitchen/src/services/core/app_service.dart';
-import 'package:mom_kitchen/src/services/ui/theme_service.dart';
-
-class AppBinding extends Bindings {
- @override
- void dependencies() {
- Get.lazyPut(() => AppService.instance.api, fenix: true);
- Get.lazyPut(() => AppService.instance.storage, fenix: true);
- if (!kIsWeb && AppService.instance.permission != null) {
- Get.lazyPut(() => AppService.instance.permission!, fenix: true);
- }
- Get.lazyPut(() => AppService.instance.logger, fenix: true);
- Get.lazyPut(() => AppService.instance.animation, fenix: true);
- Get.lazyPut(() => AppService.instance.screenUtil, fenix: true);
- Get.lazyPut(() => AppService.instance.appInfo, fenix: true);
- Get.lazyPut(() => AppService.instance.toast, fenix: true);
-
- Get.put(ThemeService.instance, permanent: true);
- Get.put(PersonalizationController(), permanent: true);
-
- Get.put(ActionController(), permanent: true);
- Get.put(FavoritesController(), permanent: true);
- Get.put(HomeController(), permanent: true);
- Get.put(FeedController(), permanent: true);
- Get.put(PreferenceController(), permanent: true);
- Get.put(ProfileController(), permanent: true);
- Get.put(MainNavigationController(), permanent: true);
- }
-}
diff --git a/lib/src/routes/app_routes.dart b/lib/src/config/app_routes.dart
similarity index 75%
rename from lib/src/routes/app_routes.dart
rename to lib/src/config/app_routes.dart
index 240e7f5..e1e63c7 100644
--- a/lib/src/routes/app_routes.dart
+++ b/lib/src/config/app_routes.dart
@@ -1,45 +1,44 @@
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/pages/home_page.dart';
-import 'package:mom_kitchen/src/pages/settings/theme_demo_page.dart';
-import 'package:mom_kitchen/src/pages/debug/example_page.dart';
-import 'package:mom_kitchen/src/pages/favorites/favorites_page.dart';
+import 'package:mom_kitchen/src/pages/home/home_page.dart';
+import 'package:mom_kitchen/src/pages/profile/settings/theme_demo_page.dart';
+import 'package:mom_kitchen/src/pages/profile/favorites_page.dart';
import 'package:mom_kitchen/src/pages/discover/discover_page.dart';
-import 'package:mom_kitchen/src/pages/profile_page.dart';
-import 'package:mom_kitchen/src/pages/settings/personalization_page.dart';
-import 'package:mom_kitchen/src/pages/chat_module/chat_page.dart'
- show FeedbackPage;
-import 'package:mom_kitchen/src/widgets/navigation/main_tab_view.dart';
-import 'package:mom_kitchen/src/pages/debug/standards_violation_page.dart';
+import 'package:mom_kitchen/src/pages/profile/profile_page.dart';
+import 'package:mom_kitchen/src/pages/profile/settings/personalization_page.dart';
+import 'package:mom_kitchen/src/pages/profile/chat_page.dart' show FeedbackPage;
+import 'package:mom_kitchen/src/widgets/navigation_widgets.dart';
import 'package:mom_kitchen/src/standards/page_validator.dart';
import 'package:mom_kitchen/src/standards/route_middleware.dart';
-import 'package:mom_kitchen/src/pages/what_to_eat/what_to_eat_page.dart';
-import 'package:mom_kitchen/src/pages/hot/hot_page.dart';
-import 'package:mom_kitchen/src/pages/nutrition/nutrition_center_page.dart';
-import 'package:mom_kitchen/src/pages/nutrition/nutrition_report_page.dart';
-import 'package:mom_kitchen/src/pages/nutrition/goal_setting_page.dart';
-import 'package:mom_kitchen/src/pages/shopping/shopping_list_page.dart';
-import 'package:mom_kitchen/src/pages/search/search_page.dart';
-import 'package:mom_kitchen/src/pages/debug/fl_chart_test_page.dart';
-import 'package:mom_kitchen/src/pages/recipe/recipe_detail_page.dart';
+import 'package:mom_kitchen/src/pages/discover/what_to_eat_page.dart';
+import 'package:mom_kitchen/src/pages/discover/hot_page.dart';
+import 'package:mom_kitchen/src/pages/profile/nutrition/nutrition_center_page.dart';
+import 'package:mom_kitchen/src/pages/profile/nutrition/nutrition_report_page.dart';
+import 'package:mom_kitchen/src/pages/profile/nutrition/goal_setting_page.dart';
+import 'package:mom_kitchen/src/pages/profile/shopping_list_page.dart';
+import 'package:mom_kitchen/src/pages/home/search_page.dart';
+import 'package:mom_kitchen/src/pages/home/recipe_detail_page.dart';
import 'package:mom_kitchen/src/pages/tools/cooking_timer_page.dart';
import 'package:mom_kitchen/src/pages/tools/unit_converter_page.dart';
import 'package:mom_kitchen/src/pages/tools/bmi_calculator_page.dart';
import 'package:mom_kitchen/src/pages/tools/serving_scaler_page.dart';
+import 'package:mom_kitchen/src/pages/tools/tools_center_page.dart';
+import 'package:mom_kitchen/src/pages/tools/allergen_checker_page.dart';
+import 'package:mom_kitchen/src/pages/tools/meal_time_recommend_page.dart';
+import 'package:mom_kitchen/src/pages/tools/meal_planner_page.dart';
+import 'package:mom_kitchen/src/pages/tools/ingredient_detail_page.dart';
+import 'package:mom_kitchen/src/app_binding.dart';
class AppRoutes {
static const String home = '/';
static const String theme = '/theme';
- static const String example = '/example';
static const String favorites = '/favorites';
static const String discover = '/discover';
static const String profile = '/profile';
static const String personalization = '/personalization';
static const String chat = '/chat';
static const String main = '/main';
- static const String standardsViolation = '/standards-violation';
static const String whatToEat = '/what-to-eat';
static const String nutrition = '/nutrition';
- static const String flChartTest = '/fl-chart-test';
static const String nutritionReport = '/nutrition-report';
static const String goalSetting = '/goal-setting';
static const String shoppingList = '/shopping-list';
@@ -49,13 +48,19 @@ class AppRoutes {
static const String unitConverter = '/unit-converter';
static const String bmiCalculator = '/bmi-calculator';
static const String servingScaler = '/serving-scaler';
+ static const String toolsCenter = '/tools';
+ static const String toolsTimer = '/tools/timer';
+ static const String toolsScaler = '/tools/scaler';
+ static const String toolsMealTime = '/tools/meal-time';
+ static const String toolsBmi = '/tools/bmi';
+ static const String toolsAllergen = '/tools/allergen';
+ static const String toolsNutrition = '/tools/nutrition';
+ static const String toolsConverter = '/tools/converter';
+ static const String toolsIngredient = '/tools/ingredient';
+ static const String toolsStats = '/tools/stats';
+ static const String toolsPlanner = '/tools/planner';
static final List pages = [
- GetPage(
- name: standardsViolation,
- page: () => const StandardsViolationPage(),
- middlewares: [PageStandardsMiddleware()],
- ),
GetPage(
name: home,
page: () => const HomePage(),
@@ -66,19 +71,16 @@ class AppRoutes {
page: () => const ThemeDemoPage(),
middlewares: [PageStandardsMiddleware()],
),
- GetPage(
- name: example,
- page: () => const ExamplePage(),
- middlewares: [PageStandardsMiddleware()],
- ),
GetPage(
name: favorites,
page: () => const FavoritesPage(),
+ binding: FavoritesBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: discover,
page: () => const DiscoverPage(),
+ binding: DiscoverBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
@@ -99,47 +101,55 @@ class AppRoutes {
GetPage(
name: main,
page: () => const MainTabView(),
+ binding: MainBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: whatToEat,
page: () => const WhatToEatPage(),
+ binding: WhatToEatBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: '/hot',
page: () => const HotPage(),
+ binding: HotBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: nutrition,
page: () => const NutritionCenterPage(),
+ binding: NutritionBinding(),
middlewares: [PageStandardsMiddleware()],
),
- GetPage(name: flChartTest, page: () => const FlChartTestPage()),
GetPage(
name: nutritionReport,
page: () => const NutritionReportPage(),
+ binding: NutritionBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: goalSetting,
page: () => const GoalSettingPage(),
+ binding: NutritionBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: shoppingList,
page: () => const ShoppingListPage(),
+ binding: ShoppingBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: search,
page: () => const SearchPage(),
+ binding: SearchBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: recipeDetail,
page: () => RecipeDetailPage(recipeId: Get.arguments),
+ binding: RecipeDetailBinding(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
@@ -162,22 +172,68 @@ class AppRoutes {
page: () => const ServingScalerPage(),
middlewares: [PageStandardsMiddleware()],
),
+ GetPage(
+ name: toolsCenter,
+ page: () => const ToolsCenterPage(),
+ binding: ToolsBinding(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsTimer,
+ page: () => const CookingTimerPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsScaler,
+ page: () => const ServingScalerPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsBmi,
+ page: () => const BmiCalculatorPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsConverter,
+ page: () => const UnitConverterPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsAllergen,
+ page: () => const AllergenCheckerPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsMealTime,
+ page: () => const MealTimeRecommendPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsPlanner,
+ page: () => const MealPlannerPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsIngredient,
+ page: () => const IngredientDetailPage(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsNutrition,
+ page: () => const NutritionCenterPage(),
+ binding: NutritionBinding(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
+ GetPage(
+ name: toolsStats,
+ page: () => const HotPage(),
+ binding: HotBinding(),
+ middlewares: [PageStandardsMiddleware()],
+ ),
];
static void registerAllPages() {
PageRegistry.registerAll([
- PageInfo(
- route: standardsViolation,
- name: 'Standards Violation Page',
- description: '页面规范违规拦截页',
- requiredStandards: const [
- StandardCheck.themeColors,
- StandardCheck.textColors,
- StandardCheck.fontSize,
- StandardCheck.darkMode,
- ],
- builder: () => const StandardsViolationPage(),
- ),
PageInfo(
route: home,
name: 'Home Page',
@@ -208,18 +264,6 @@ class AppRoutes {
],
builder: () => const ThemeDemoPage(),
),
- PageInfo(
- route: example,
- name: 'Example Page',
- description: '示例页面',
- requiredStandards: const [
- StandardCheck.themeColors,
- StandardCheck.textColors,
- StandardCheck.fontSize,
- StandardCheck.responsive,
- ],
- builder: () => const ExamplePage(),
- ),
PageInfo(
route: favorites,
name: 'Favorites Page',
diff --git a/lib/src/config/design_tokens.dart b/lib/src/config/design_tokens.dart
index a307f00..4cefdd5 100644
--- a/lib/src/config/design_tokens.dart
+++ b/lib/src/config/design_tokens.dart
@@ -106,25 +106,26 @@ class DesignTokens {
static EdgeInsets get paddingLg => const EdgeInsets.all(space4);
static EdgeInsets get paddingXl => const EdgeInsets.all(space6);
- static EdgeInsets get hPadding => const EdgeInsets.symmetric(horizontal: space4);
+ static EdgeInsets get hPadding =>
+ const EdgeInsets.symmetric(horizontal: space4);
static BoxShadow get shadowSm => BoxShadow(
- color: const Color(0x0A000000),
- blurRadius: shadowSmBlur,
- offset: Offset(0, shadowSmOffset),
- );
+ color: const Color(0x0A000000),
+ blurRadius: shadowSmBlur,
+ offset: Offset(0, shadowSmOffset),
+ );
static BoxShadow get shadowMd => BoxShadow(
- color: const Color(0x0F000000),
- blurRadius: shadowMdBlur,
- offset: Offset(0, shadowMdOffset),
- );
+ color: const Color(0x0F000000),
+ blurRadius: shadowMdBlur,
+ offset: Offset(0, shadowMdOffset),
+ );
static BoxShadow get shadowLg => BoxShadow(
- color: const Color(0x14000000),
- blurRadius: shadowLgBlur,
- offset: Offset(0, shadowLgOffset),
- );
+ color: const Color(0x14000000),
+ blurRadius: shadowLgBlur,
+ offset: Offset(0, shadowLgOffset),
+ );
static List get shadowsSm => [shadowSm];
static List get shadowsMd => [shadowMd];
@@ -143,8 +144,8 @@ class DarkDesignTokens {
static const Color glassBorder = Color(0x26FFFFFF);
static const Color glassShadow = Color(0x4D000000);
static const Color text1 = Color(0xFFFFFFFF);
- static const Color text2 = Color(0xFF8E8E93);
- static const Color text3 = Color(0xFF48484A);
+ static const Color text2 = Color(0xFFEBEBF5);
+ static const Color text3 = Color(0xFF8E8E93);
static const Color red = Color(0xFFFF453A);
static const Color green = Color(0xFF30D158);
diff --git a/lib/src/controllers/base/base_controller.dart b/lib/src/controllers/base_controller.dart
similarity index 100%
rename from lib/src/controllers/base/base_controller.dart
rename to lib/src/controllers/base_controller.dart
diff --git a/lib/src/controllers/cooking_note_controller.dart b/lib/src/controllers/cooking_note_controller.dart
index dec15ed..acc2a2c 100644
--- a/lib/src/controllers/cooking_note_controller.dart
+++ b/lib/src/controllers/cooking_note_controller.dart
@@ -7,7 +7,7 @@
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
-import '../models/note/cooking_note_model.dart';
+import '../models/cooking_note_model.dart';
import '../services/data/hive_service.dart';
class CookingNoteController extends GetxController {
diff --git a/lib/src/controllers/favorites/favorites_controller.dart b/lib/src/controllers/favorites_controller.dart
similarity index 97%
rename from lib/src/controllers/favorites/favorites_controller.dart
rename to lib/src/controllers/favorites_controller.dart
index 663bafe..db6f9e0 100644
--- a/lib/src/controllers/favorites/favorites_controller.dart
+++ b/lib/src/controllers/favorites_controller.dart
@@ -1,8 +1,8 @@
// 2026-04-09 | FavoritesController | 收藏控制器 | 统一管理收藏状态,支持Hive持久化
// 2026-04-09 | 新增排序、分类筛选、批量删除功能
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
-import 'package:mom_kitchen/src/models/feed/feed_item_model.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
+import 'package:mom_kitchen/src/models/feed_item_model.dart';
import 'package:mom_kitchen/src/services/data/hive_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
diff --git a/lib/src/controllers/feed/action_controller.dart b/lib/src/controllers/feed/action_controller.dart
index 8d82360..5901c75 100644
--- a/lib/src/controllers/feed/action_controller.dart
+++ b/lib/src/controllers/feed/action_controller.dart
@@ -1,6 +1,6 @@
// 2026-04-09 | ActionController | 互动操作控制器 | 管理点赞/推荐/浏览量上报
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/repositories/action_repository.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
diff --git a/lib/src/controllers/feed/feed_controller.dart b/lib/src/controllers/feed/feed_controller.dart
index e3d9875..cbdec2b 100644
--- a/lib/src/controllers/feed/feed_controller.dart
+++ b/lib/src/controllers/feed/feed_controller.dart
@@ -1,9 +1,9 @@
// 2026-04-09 | FeedController | 信息流控制器 | 管理4种信息流:推荐/最新/热门/个性化
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/controllers/user/preference_controller.dart';
-import 'package:mom_kitchen/src/models/api/api_response.dart';
-import 'package:mom_kitchen/src/models/feed/feed_item_model.dart';
+import 'package:mom_kitchen/src/models/api_response.dart';
+import 'package:mom_kitchen/src/models/feed_item_model.dart';
import 'package:mom_kitchen/src/repositories/feed_repository.dart' as repo;
enum FeedType { recommend, latest, hot, personal }
diff --git a/lib/src/controllers/feed/hot_controller.dart b/lib/src/controllers/feed/hot_controller.dart
index 14650b5..1f221c0 100644
--- a/lib/src/controllers/feed/hot_controller.dart
+++ b/lib/src/controllers/feed/hot_controller.dart
@@ -1,7 +1,7 @@
// 2026-04-10 | HotController | 热门排行控制器 | 完全重写,使用HotItem模型
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/repositories/hot_repository.dart' as repo;
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
diff --git a/lib/src/controllers/home/home_controller.dart b/lib/src/controllers/home_controller.dart
similarity index 98%
rename from lib/src/controllers/home/home_controller.dart
rename to lib/src/controllers/home_controller.dart
index da1a1aa..8bb4db7 100644
--- a/lib/src/controllers/home/home_controller.dart
+++ b/lib/src/controllers/home_controller.dart
@@ -1,7 +1,7 @@
// 2026-04-09 | HomeController | 首页控制器 | 改造:从mock数据切换到RecipeRepository真实API
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/models/recipe/category_model.dart';
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
import 'package:mom_kitchen/src/repositories/recipe_repository.dart';
diff --git a/lib/src/controllers/home/main_navigation_controller.dart b/lib/src/controllers/main_navigation_controller.dart
similarity index 100%
rename from lib/src/controllers/home/main_navigation_controller.dart
rename to lib/src/controllers/main_navigation_controller.dart
diff --git a/lib/src/controllers/nutrition/meal_record_controller.dart b/lib/src/controllers/meal_record_controller.dart
similarity index 97%
rename from lib/src/controllers/nutrition/meal_record_controller.dart
rename to lib/src/controllers/meal_record_controller.dart
index 6dec949..677be19 100644
--- a/lib/src/controllers/nutrition/meal_record_controller.dart
+++ b/lib/src/controllers/meal_record_controller.dart
@@ -2,9 +2,9 @@
// 2026-04-09 | 增加周/月营养聚合方法,支持营养报告页
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
-import 'package:mom_kitchen/src/models/nutrition/meal_record_model.dart';
-import 'package:mom_kitchen/src/models/nutrition/user_goal_model.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
+import 'package:mom_kitchen/src/models/meal_record_model.dart';
+import 'package:mom_kitchen/src/models/user_goal_model.dart';
import 'package:mom_kitchen/src/services/data/hive_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
diff --git a/lib/src/controllers/base/paged_controller.dart b/lib/src/controllers/paged_controller.dart
similarity index 91%
rename from lib/src/controllers/base/paged_controller.dart
rename to lib/src/controllers/paged_controller.dart
index 88689f2..2ac09e0 100644
--- a/lib/src/controllers/base/paged_controller.dart
+++ b/lib/src/controllers/paged_controller.dart
@@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
abstract class PagedController extends BaseController {
final items = ValueNotifier>([]);
diff --git a/lib/src/controllers/search/search_controller.dart b/lib/src/controllers/search_controller.dart
similarity index 98%
rename from lib/src/controllers/search/search_controller.dart
rename to lib/src/controllers/search_controller.dart
index d7dd42c..8486250 100644
--- a/lib/src/controllers/search/search_controller.dart
+++ b/lib/src/controllers/search_controller.dart
@@ -1,7 +1,7 @@
// 2026-04-10 | SearchController | 搜索控制器 | 完全重写,直接调用api.php search接口
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
import 'package:mom_kitchen/src/config/api_config.dart';
import 'package:mom_kitchen/src/services/api/api_service.dart';
diff --git a/lib/src/controllers/shopping/shopping_list_controller.dart b/lib/src/controllers/shopping_list_controller.dart
similarity index 96%
rename from lib/src/controllers/shopping/shopping_list_controller.dart
rename to lib/src/controllers/shopping_list_controller.dart
index 0432d31..fdc4c77 100644
--- a/lib/src/controllers/shopping/shopping_list_controller.dart
+++ b/lib/src/controllers/shopping_list_controller.dart
@@ -1,8 +1,8 @@
// 2026-04-09 | ShoppingListController | 购物清单控制器 | 管理购物清单的增删改查及分类展示
// 2026-04-09 | 初始创建,支持添加/删除/勾选/清空已购功能
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
-import 'package:mom_kitchen/src/models/shopping/shopping_item_model.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
+import 'package:mom_kitchen/src/models/shopping_item_model.dart';
import 'package:mom_kitchen/src/services/data/hive_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
diff --git a/lib/src/controllers/tools_controller.dart b/lib/src/controllers/tools_controller.dart
new file mode 100644
index 0000000..4b5f535
--- /dev/null
+++ b/lib/src/controllers/tools_controller.dart
@@ -0,0 +1,135 @@
+/*
+ * 文件: tools_controller.dart
+ * 名称: 工具中心控制器
+ * 作用: 管理工具列表、使用频率统计、搜索过滤
+ * 更新: 2026-04-10 初始创建
+ */
+
+import 'dart:convert';
+import 'package:flutter/foundation.dart';
+import 'package:get/get.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
+import 'package:mom_kitchen/src/models/tool_item_model.dart';
+
+class ToolsController extends BaseController {
+ static const String _usageKey = 'tool_usage_counts';
+
+ final RxList tools = [].obs;
+ final RxList filteredTools = [].obs;
+ final RxString selectedCategory = 'all'.obs;
+ final RxString searchQuery = ''.obs;
+
+ List get frequentTools {
+ final sorted = List.from(tools);
+ sorted.sort((a, b) => b.usageCount.compareTo(a.usageCount));
+ return sorted.take(5).toList();
+ }
+
+ List get localTools => tools.where((t) => !t.needsNetwork).toList();
+
+ List get networkTools =>
+ tools.where((t) => t.needsNetwork).toList();
+
+ @override
+ void onInit() {
+ super.onInit();
+ _loadTools();
+ }
+
+ Future _loadTools() async {
+ isLoading.value = true;
+
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final usageData = prefs.getString(_usageKey);
+ final usageMap = usageData != null
+ ? Map.from(json.decode(usageData))
+ : {};
+
+ tools.value = ToolRegistry.defaultTools.map((tool) {
+ return tool.copyWith(usageCount: usageMap[tool.id] ?? 0);
+ }).toList();
+
+ _applyFilter();
+ } catch (e) {
+ debugPrint('Load tools error: $e');
+ tools.value = ToolRegistry.defaultTools;
+ filteredTools.value = tools;
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ void selectCategory(String category) {
+ selectedCategory.value = category;
+ _applyFilter();
+ }
+
+ void search(String query) {
+ searchQuery.value = query;
+ _applyFilter();
+ }
+
+ void _applyFilter() {
+ var result = List.from(tools);
+
+ if (selectedCategory.value != 'all') {
+ result = result
+ .where((t) => t.category == selectedCategory.value)
+ .toList();
+ }
+
+ if (searchQuery.value.isNotEmpty) {
+ final query = searchQuery.value.toLowerCase();
+ result = result.where((t) {
+ return t.name.toLowerCase().contains(query) ||
+ (t.description?.toLowerCase().contains(query) ?? false);
+ }).toList();
+ }
+
+ result.sort((a, b) => b.usageCount.compareTo(a.usageCount));
+ filteredTools.value = result;
+ }
+
+ Future recordUsage(String toolId) async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final usageData = prefs.getString(_usageKey);
+ final usageMap = usageData != null
+ ? Map.from(json.decode(usageData))
+ : {};
+
+ usageMap[toolId] = (usageMap[toolId] ?? 0) + 1;
+
+ await prefs.setString(_usageKey, json.encode(usageMap));
+
+ final index = tools.indexWhere((t) => t.id == toolId);
+ if (index != -1) {
+ tools[index] = tools[index].copyWith(usageCount: usageMap[toolId]);
+ tools.refresh();
+ _applyFilter();
+ }
+
+ debugPrint('Tool $toolId usage: ${usageMap[toolId]}');
+ } catch (e) {
+ debugPrint('Record usage error: $e');
+ }
+ }
+
+ Future openTool(ToolItem tool) async {
+ await recordUsage(tool.id);
+ Get.toNamed(tool.route);
+ }
+
+ Future resetUsageData() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.remove(_usageKey);
+ await _loadTools();
+ debugPrint('Tool usage data reset');
+ } catch (e) {
+ debugPrint('Reset usage error: $e');
+ }
+ }
+}
diff --git a/lib/src/controllers/user/online_controller.dart b/lib/src/controllers/user/online_controller.dart
index c67a821..5e60cd2 100644
--- a/lib/src/controllers/user/online_controller.dart
+++ b/lib/src/controllers/user/online_controller.dart
@@ -2,7 +2,7 @@
// 2026-04-10 | API v2.0.0: OnlineRepository → StatsRepository,移除 timeline 方法
import 'dart:async';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/repositories/online_repository.dart' as repo;
class OnlineController extends BaseController {
diff --git a/lib/src/controllers/user/personalization_controller.dart b/lib/src/controllers/user/personalization_controller.dart
index 0c521fb..fffe9ec 100644
--- a/lib/src/controllers/user/personalization_controller.dart
+++ b/lib/src/controllers/user/personalization_controller.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/services/core/app_service.dart';
import 'package:mom_kitchen/src/services/ui/theme_service.dart';
import 'package:mom_kitchen/src/services/ui/animation_service.dart';
diff --git a/lib/src/controllers/user/preference_controller.dart b/lib/src/controllers/user/preference_controller.dart
index 17425bc..a47ff32 100644
--- a/lib/src/controllers/user/preference_controller.dart
+++ b/lib/src/controllers/user/preference_controller.dart
@@ -1,8 +1,8 @@
// 2026-04-10 | PreferenceController | 用户偏好控制器 | 完全重写,自动初始化用户ID
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
-import 'package:mom_kitchen/src/models/user/user_preference_model.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
+import 'package:mom_kitchen/src/models/user_preference_model.dart';
import 'package:mom_kitchen/src/repositories/preference_repository.dart'
as repo;
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
diff --git a/lib/src/controllers/user/profile_controller.dart b/lib/src/controllers/user/profile_controller.dart
index 821832e..ef93fbd 100644
--- a/lib/src/controllers/user/profile_controller.dart
+++ b/lib/src/controllers/user/profile_controller.dart
@@ -1,5 +1,5 @@
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/services/core/app_service.dart';
class UserModel {
diff --git a/lib/src/controllers/discovery/what_to_eat_controller.dart b/lib/src/controllers/what_to_eat_controller.dart
similarity index 80%
rename from lib/src/controllers/discovery/what_to_eat_controller.dart
rename to lib/src/controllers/what_to_eat_controller.dart
index fdcf645..4c2c54d 100644
--- a/lib/src/controllers/discovery/what_to_eat_controller.dart
+++ b/lib/src/controllers/what_to_eat_controller.dart
@@ -3,7 +3,7 @@
// 2026-04-10 | 修复: 动态筛选卡死闪退+添加加载动画+超时保护+初始化安全
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/base/base_controller.dart';
+import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
import 'package:mom_kitchen/src/models/recipe/category_model.dart';
import 'package:mom_kitchen/src/models/recipe/tag_model.dart';
@@ -29,6 +29,8 @@ class WhatToEatController extends BaseController {
final RxList recentResults = [].obs;
+ final Rx expandedCategory = Rx(null);
+
final RxBool isOptionsLoading = false.obs;
final RxBool isOptionsLoaded = false.obs;
final RxString optionsError = ''.obs;
@@ -75,6 +77,10 @@ class WhatToEatController extends BaseController {
Future _loadCategories() async {
try {
final result = await _recipeRepository.fetchCategories();
+ debugPrint('WhatToEatController: loaded ${result.length} categories');
+ for (final c in result.take(5)) {
+ debugPrint(' category: ${c.id}, ${c.name}, parentId=${c.parentId}');
+ }
categories.value = result;
} catch (e) {
debugPrint('Load categories error: $e');
@@ -108,6 +114,10 @@ class WhatToEatController extends BaseController {
.where((id) => id > 0)
.toList();
+ debugPrint(
+ 'WhatToEatController.roll: categories=$categoryIds, tags=$tagIds',
+ );
+
final results = await _whatToEatRepository
.fetchFilterApply(
categories: categoryIds.isNotEmpty ? categoryIds : null,
@@ -122,6 +132,8 @@ class WhatToEatController extends BaseController {
},
);
+ debugPrint('WhatToEatController.roll: got ${results.length} results');
+
if (results.isNotEmpty) {
await Future.delayed(const Duration(milliseconds: 300));
@@ -137,12 +149,20 @@ class WhatToEatController extends BaseController {
ToastService.show(message: '已为您选择: ${recipe.title} 🎉');
} else {
- ToastService.show(message: '没有找到合适的菜谱,试试调整筛选条件 🤔');
+ final filterInfo = [];
+ if (categoryIds.isNotEmpty) filterInfo.add('${categoryIds.length}个分类');
+ if (tagIds.isNotEmpty) filterInfo.add('${tagIds.length}个标签');
+
+ final filterText = filterInfo.isEmpty
+ ? ''
+ : '(已选${filterInfo.join('、')})';
+ ToastService.show(message: '没有找到合适的菜谱$filterText,试试调整筛选条件 🤔');
+ debugPrint('WhatToEatController.roll: no results found');
}
} catch (e) {
debugPrint('Roll error: $e');
errorMessage.value = e.toString();
- ToastService.show(message: '获取推荐失败,请重试 🔄');
+ ToastService.show(message: '获取推荐失败: $e,请重试 🔄');
} finally {
isSpinning.value = false;
}
@@ -229,4 +249,24 @@ class WhatToEatController extends BaseController {
}
return parts.isEmpty ? '无筛选' : parts.join(' + ');
}
+
+ List getSubCategories(int parentId) {
+ return categories.where((c) => c.parentId == parentId).toList();
+ }
+
+ bool hasSubCategories(int categoryId) {
+ return categories.any((c) => c.parentId == categoryId);
+ }
+
+ void toggleExpandCategory(CategoryModel category) {
+ if (expandedCategory.value?.id == category.id) {
+ expandedCategory.value = null;
+ } else {
+ expandedCategory.value = category;
+ }
+ }
+
+ bool isCategoryExpanded(int categoryId) {
+ return expandedCategory.value?.id == categoryId;
+ }
}
diff --git a/lib/src/l10n/app_en.arb b/lib/src/l10n/app_en.arb
index 0b152a9..c5375b9 100644
--- a/lib/src/l10n/app_en.arb
+++ b/lib/src/l10n/app_en.arb
@@ -1,9 +1,11 @@
{
"@@locale": "en",
+
"appTitle": "Mom's Kitchen",
"welcomeMessage": "Welcome to Mom's Kitchen!",
"appDescription": "Your favorite food delivery app",
"getStarted": "Get Started",
+
"themeSettings": "Theme Settings",
"darkMode": "Dark Mode",
"statusBarImmersive": "Status Bar Immersive",
@@ -14,8 +16,272 @@
"preview": "Preview",
"sampleText": "This is a sample text to preview the theme settings.",
"sampleButton": "Sample Button",
+
"retry": "Retry",
"noData": "No Data",
"cancel": "Cancel",
- "confirm": "Confirm"
-}
\ No newline at end of file
+ "confirm": "Confirm",
+ "delete": "Delete",
+ "edit": "Edit",
+ "done": "Done",
+ "reset": "Reset",
+ "loading": "Loading...",
+ "error": "Error",
+ "success": "Success",
+ "tip": "Tip",
+ "backToHome": "Back to Home",
+ "reload": "Reload",
+ "searchAgain": "Search Again",
+
+ "homeTitle": "Mom's Kitchen",
+ "searchPlaceholder": "Search recipes, ingredients...",
+ "loadingRecipes": "Loading recipes...",
+ "loadFailed": "Load Failed",
+ "noRecipes": "No Recipes",
+ "addRecipesHint": "Add some delicious recipes",
+ "todayRecommend": "Today's Picks",
+ "recipeCount": "{count} recipes",
+ "@recipeCount": {
+ "placeholders": {
+ "count": { "type": "int" }
+ }
+ },
+ "categoryBrowse": "Categories",
+ "categoryMeat": "Meat",
+ "categoryVeggie": "Vegetarian",
+ "categoryNoodle": "Noodles",
+ "categorySoup": "Soup",
+ "categoryDessert": "Dessert",
+ "categoryDrink": "Drinks",
+ "categorySnack": "Snacks",
+ "invalidRecipeId": "Invalid recipe ID",
+
+ "hotRanking": "Hot Rankings",
+ "periodToday": "Today",
+ "periodMonth": "This Month",
+ "periodAll": "All Time",
+ "sortByViews": "Views",
+ "sortByLikes": "Likes",
+ "sortByRecommend": "Recommended",
+ "noRankingData": "No {period} ranking data",
+ "@noRankingData": {
+ "placeholders": {
+ "period": { "type": "String" }
+ }
+ },
+
+ "whatToEat": "What to Eat",
+ "loadingFilterOptions": "Loading filter options…",
+ "firstLoadHint": "Please wait, first load may take a few seconds",
+ "pickingRecipe": "Picking a recipe for you…",
+ "matchingHint": "Matching based on your filters",
+ "tapToStart": "Tap the button below to start",
+ "picking": "Picking…",
+ "randomSelect": "Random Pick",
+ "changeAnother": "Try Another",
+ "currentFilter": "Current filter: {summary}",
+ "@currentFilter": {
+ "placeholders": {
+ "summary": { "type": "String" }
+ }
+ },
+ "categoryFilter": "Category Filter",
+ "categoryLoading": "Loading categories…",
+ "tagFilter": "Tag Filter",
+ "tagLoading": "Loading tags…",
+ "allergenFilter": "Allergen Exclusion",
+
+ "feedbackTitle": "Feedback",
+ "feedbackBug": "Bug Report",
+ "feedbackFeature": "Feature Request",
+ "feedbackExperience": "Experience",
+ "feedbackOther": "Other",
+ "feedbackWelcome": "Welcome to Feedback",
+ "feedbackSelectType": "Please select a feedback type:",
+ "feedbackTypeSelected": "You selected \"{type}\", please describe your issue or suggestion:",
+ "@feedbackTypeSelected": {
+ "placeholders": {
+ "type": { "type": "String" }
+ }
+ },
+ "feedbackThanks": "Thanks for your feedback! We'll process your {type} soon.",
+ "@feedbackThanks": {
+ "placeholders": {
+ "type": { "type": "String" }
+ }
+ },
+ "feedbackMore": "Anything else? Keep typing or go back",
+ "feedbackInputHint": "Enter your feedback…",
+ "feedbackSelectFirst": "Please select a feedback type first",
+ "feedbackOpinion": "feedback",
+
+ "searchTitle": "Search",
+ "searchHistory": "Search History",
+ "clearHistory": "Clear",
+ "hotSearch": "Trending",
+ "deleteRecord": "Delete Record",
+ "deleteRecordConfirm": "Delete \"{keyword}\"?",
+ "@deleteRecordConfirm": {
+ "placeholders": {
+ "keyword": { "type": "String" }
+ }
+ },
+ "searching": "Searching \"{keyword}\"...",
+ "@searching": {
+ "placeholders": {
+ "keyword": { "type": "String" }
+ }
+ },
+ "noResults": "No recipes found",
+ "tryOtherKeywords": "Try other keywords like \"pork\", \"chicken\"",
+ "resultCount": "{count} results found · \"{keyword}\"",
+ "@resultCount": {
+ "placeholders": {
+ "count": { "type": "int" },
+ "keyword": { "type": "String" }
+ }
+ },
+
+ "recipeDetail": "Recipe Detail",
+ "invalidRecipeIdError": "Invalid recipe ID: {id}",
+ "@invalidRecipeIdError": {
+ "placeholders": {
+ "id": { "type": "String" }
+ }
+ },
+ "recipeDataIncomplete": "Recipe data incomplete",
+ "loadFailedWith": "Load failed: {error}",
+ "@loadFailedWith": {
+ "placeholders": {
+ "error": { "type": "String" }
+ }
+ },
+ "ingredients": "Ingredients",
+ "steps": "Steps",
+ "nutritionInfo": "Nutrition",
+ "calories": "Calories",
+ "protein": "Protein",
+ "fat": "Fat",
+ "carbs": "Carbs",
+ "recommendSuccess": "Recommended!",
+ "recommend": "Recommend",
+ "notesDeveloping": "Cooking notes feature in development",
+ "notes": "Notes",
+ "addedToShoppingList": "Added to shopping list",
+ "shopping": "Shopping",
+
+ "profileTab": "Me",
+ "homeTab": "Home",
+ "settingsTab": "Settings",
+ "notLoggedIn": "Not Logged In",
+ "tapToLogin": "Tap to Login",
+ "personalization": "Personalize",
+
+ "toolsTitle": "Tools",
+ "cookingTimer": "Cooking Timer",
+ "unitConverter": "Unit Converter",
+ "bmiCalculator": "BMI Calculator",
+ "servingScaler": "Serving Scaler",
+
+ "featureRecommend": "Recommend",
+ "featureFollow": "Follow",
+ "shoppingList": "Shopping List",
+ "favorites": "Favorites",
+
+ "latestMessages": "Latest Messages",
+ "systemNotification": "System",
+ "newOrderReminder": "You have a new order reminder",
+ "marketingInfo": "Promotions",
+ "newProductOnline": "New items available, check now",
+
+ "favoritesTitle": "Favorites",
+ "selectAll": "Select All",
+ "deselectAll": "Deselect All",
+ "selectedCount": "{count} selected",
+ "@selectedCount": {
+ "placeholders": {
+ "count": { "type": "int" }
+ }
+ },
+ "confirmDelete": "Confirm Delete",
+ "confirmDeleteItems": "Delete {count} selected favorites?",
+ "@confirmDeleteItems": {
+ "placeholders": {
+ "count": { "type": "int" }
+ }
+ },
+ "emptyFavorites": "No favorites yet",
+ "addFavoriteHint": "Tap 🔖 while browsing to add favorites",
+ "sortMethod": "Sort By",
+ "sortNewest": "Newest First",
+ "sortOldest": "Oldest First",
+ "sortNameAZ": "Name A-Z",
+ "sortNameZA": "Name Z-A",
+ "all": "All",
+
+ "footprintsTitle": "My Footprints",
+ "footprintRemoved": "Footprint removed",
+
+ "personalizationTitle": "Personalization",
+ "themeColor": "Theme Color",
+ "fontSetting": "Font Size",
+ "displayMode": "Display Mode",
+ "animationEffect": "Animation",
+ "languageSetting": "Language",
+ "dialogStyle": "Dialog Style",
+ "bubbleStyle": "Bubble Style",
+ "bottomBarStyle": "Bottom Bar Style",
+ "cardSwipeDirection": "Card Swipe Direction",
+ "floatingBarOpacity": "Floating Bar Opacity",
+ "immersiveStatusBar": "Immersive Status Bar",
+ "unifiedStyle": "Unified style (cross-platform)",
+ "sampleDialog": "Sample Dialog",
+ "sampleDialogContent": "This is a demo using the current dialog style.",
+ "ok": "OK",
+ "showDialogSample": "Show Dialog Style Sample",
+ "selectThemeColor": "Select Theme Color",
+ "animationIntensity": "Animation Intensity",
+ "selectLanguage": "Select Language",
+ "restoreDefaults": "Restore Defaults",
+ "restoreDefaultsConfirm": "Reset all settings to default?",
+ "swipeLeftRight": "Left-Right Swipe",
+ "swipeUpDown": "Up-Down Swipe",
+ "enableImmersive": "Enable Immersive Status Bar",
+
+ "preferenceTitle": "Taste Preferences",
+ "preferenceCategory": "Preferred Categories",
+ "preferenceCategoryHint": "Select your favorite cuisine categories",
+ "preferenceTag": "Preferred Tags",
+ "preferenceTagHint": "Select tags you're interested in",
+ "allergenBlock": "Allergen Exclusion",
+ "allergenBlockHint": "Exclude recipes with these ingredients",
+ "noCategoryData": "No category data",
+ "tagPlaceholder": "Tags will appear here after setting preferences",
+ "resetPreference": "Reset Preferences",
+ "resetPreferenceConfirm": "Clear all taste preference settings?",
+
+ "servingScalerTitle": "Serving Scaler",
+ "originalServing": "Original Servings",
+ "targetServing": "Target Servings",
+ "scaleRatio": "Scale Ratio",
+
+ "nutritionCenter": "Nutrition Center",
+
+ "standardsViolation": "Standards Violation",
+ "pageBlocked": "Page Blocked",
+ "routeLabel": "Route: {route}",
+ "@routeLabel": {
+ "placeholders": {
+ "route": { "type": "String" }
+ }
+ },
+ "reasonLabel": "Reason: {reason}",
+ "@reasonLabel": {
+ "placeholders": {
+ "reason": { "type": "String" }
+ }
+ },
+ "failedChecks": "Failed checks:",
+ "unknown": "Unknown",
+ "unknownReason": "Unknown reason"
+}
diff --git a/lib/src/l10n/app_localizations.dart b/lib/src/l10n/app_localizations.dart
index b786f26..8d564fc 100644
--- a/lib/src/l10n/app_localizations.dart
+++ b/lib/src/l10n/app_localizations.dart
@@ -206,6 +206,1056 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Confirm'**
String get confirm;
+
+ /// No description provided for @delete.
+ ///
+ /// In en, this message translates to:
+ /// **'Delete'**
+ String get delete;
+
+ /// No description provided for @edit.
+ ///
+ /// In en, this message translates to:
+ /// **'Edit'**
+ String get edit;
+
+ /// No description provided for @done.
+ ///
+ /// In en, this message translates to:
+ /// **'Done'**
+ String get done;
+
+ /// No description provided for @reset.
+ ///
+ /// In en, this message translates to:
+ /// **'Reset'**
+ String get reset;
+
+ /// No description provided for @loading.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading...'**
+ String get loading;
+
+ /// No description provided for @error.
+ ///
+ /// In en, this message translates to:
+ /// **'Error'**
+ String get error;
+
+ /// No description provided for @success.
+ ///
+ /// In en, this message translates to:
+ /// **'Success'**
+ String get success;
+
+ /// No description provided for @tip.
+ ///
+ /// In en, this message translates to:
+ /// **'Tip'**
+ String get tip;
+
+ /// No description provided for @backToHome.
+ ///
+ /// In en, this message translates to:
+ /// **'Back to Home'**
+ String get backToHome;
+
+ /// No description provided for @reload.
+ ///
+ /// In en, this message translates to:
+ /// **'Reload'**
+ String get reload;
+
+ /// No description provided for @searchAgain.
+ ///
+ /// In en, this message translates to:
+ /// **'Search Again'**
+ String get searchAgain;
+
+ /// No description provided for @homeTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Mom\'s Kitchen'**
+ String get homeTitle;
+
+ /// No description provided for @searchPlaceholder.
+ ///
+ /// In en, this message translates to:
+ /// **'Search recipes, ingredients...'**
+ String get searchPlaceholder;
+
+ /// No description provided for @loadingRecipes.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading recipes...'**
+ String get loadingRecipes;
+
+ /// No description provided for @loadFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Load Failed'**
+ String get loadFailed;
+
+ /// No description provided for @noRecipes.
+ ///
+ /// In en, this message translates to:
+ /// **'No Recipes'**
+ String get noRecipes;
+
+ /// No description provided for @addRecipesHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Add some delicious recipes'**
+ String get addRecipesHint;
+
+ /// No description provided for @todayRecommend.
+ ///
+ /// In en, this message translates to:
+ /// **'Today\'s Picks'**
+ String get todayRecommend;
+
+ /// No description provided for @recipeCount.
+ ///
+ /// In en, this message translates to:
+ /// **'{count} recipes'**
+ String recipeCount(int count);
+
+ /// No description provided for @categoryBrowse.
+ ///
+ /// In en, this message translates to:
+ /// **'Categories'**
+ String get categoryBrowse;
+
+ /// No description provided for @categoryMeat.
+ ///
+ /// In en, this message translates to:
+ /// **'Meat'**
+ String get categoryMeat;
+
+ /// No description provided for @categoryVeggie.
+ ///
+ /// In en, this message translates to:
+ /// **'Vegetarian'**
+ String get categoryVeggie;
+
+ /// No description provided for @categoryNoodle.
+ ///
+ /// In en, this message translates to:
+ /// **'Noodles'**
+ String get categoryNoodle;
+
+ /// No description provided for @categorySoup.
+ ///
+ /// In en, this message translates to:
+ /// **'Soup'**
+ String get categorySoup;
+
+ /// No description provided for @categoryDessert.
+ ///
+ /// In en, this message translates to:
+ /// **'Dessert'**
+ String get categoryDessert;
+
+ /// No description provided for @categoryDrink.
+ ///
+ /// In en, this message translates to:
+ /// **'Drinks'**
+ String get categoryDrink;
+
+ /// No description provided for @categorySnack.
+ ///
+ /// In en, this message translates to:
+ /// **'Snacks'**
+ String get categorySnack;
+
+ /// No description provided for @invalidRecipeId.
+ ///
+ /// In en, this message translates to:
+ /// **'Invalid recipe ID'**
+ String get invalidRecipeId;
+
+ /// No description provided for @hotRanking.
+ ///
+ /// In en, this message translates to:
+ /// **'Hot Rankings'**
+ String get hotRanking;
+
+ /// No description provided for @periodToday.
+ ///
+ /// In en, this message translates to:
+ /// **'Today'**
+ String get periodToday;
+
+ /// No description provided for @periodMonth.
+ ///
+ /// In en, this message translates to:
+ /// **'This Month'**
+ String get periodMonth;
+
+ /// No description provided for @periodAll.
+ ///
+ /// In en, this message translates to:
+ /// **'All Time'**
+ String get periodAll;
+
+ /// No description provided for @sortByViews.
+ ///
+ /// In en, this message translates to:
+ /// **'Views'**
+ String get sortByViews;
+
+ /// No description provided for @sortByLikes.
+ ///
+ /// In en, this message translates to:
+ /// **'Likes'**
+ String get sortByLikes;
+
+ /// No description provided for @sortByRecommend.
+ ///
+ /// In en, this message translates to:
+ /// **'Recommended'**
+ String get sortByRecommend;
+
+ /// No description provided for @noRankingData.
+ ///
+ /// In en, this message translates to:
+ /// **'No {period} ranking data'**
+ String noRankingData(String period);
+
+ /// No description provided for @whatToEat.
+ ///
+ /// In en, this message translates to:
+ /// **'What to Eat'**
+ String get whatToEat;
+
+ /// No description provided for @loadingFilterOptions.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading filter options…'**
+ String get loadingFilterOptions;
+
+ /// No description provided for @firstLoadHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Please wait, first load may take a few seconds'**
+ String get firstLoadHint;
+
+ /// No description provided for @pickingRecipe.
+ ///
+ /// In en, this message translates to:
+ /// **'Picking a recipe for you…'**
+ String get pickingRecipe;
+
+ /// No description provided for @matchingHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Matching based on your filters'**
+ String get matchingHint;
+
+ /// No description provided for @tapToStart.
+ ///
+ /// In en, this message translates to:
+ /// **'Tap the button below to start'**
+ String get tapToStart;
+
+ /// No description provided for @picking.
+ ///
+ /// In en, this message translates to:
+ /// **'Picking…'**
+ String get picking;
+
+ /// No description provided for @randomSelect.
+ ///
+ /// In en, this message translates to:
+ /// **'Random Pick'**
+ String get randomSelect;
+
+ /// No description provided for @changeAnother.
+ ///
+ /// In en, this message translates to:
+ /// **'Try Another'**
+ String get changeAnother;
+
+ /// No description provided for @currentFilter.
+ ///
+ /// In en, this message translates to:
+ /// **'Current filter: {summary}'**
+ String currentFilter(String summary);
+
+ /// No description provided for @categoryFilter.
+ ///
+ /// In en, this message translates to:
+ /// **'Category Filter'**
+ String get categoryFilter;
+
+ /// No description provided for @categoryLoading.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading categories…'**
+ String get categoryLoading;
+
+ /// No description provided for @tagFilter.
+ ///
+ /// In en, this message translates to:
+ /// **'Tag Filter'**
+ String get tagFilter;
+
+ /// No description provided for @tagLoading.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading tags…'**
+ String get tagLoading;
+
+ /// No description provided for @allergenFilter.
+ ///
+ /// In en, this message translates to:
+ /// **'Allergen Exclusion'**
+ String get allergenFilter;
+
+ /// No description provided for @feedbackTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Feedback'**
+ String get feedbackTitle;
+
+ /// No description provided for @feedbackBug.
+ ///
+ /// In en, this message translates to:
+ /// **'Bug Report'**
+ String get feedbackBug;
+
+ /// No description provided for @feedbackFeature.
+ ///
+ /// In en, this message translates to:
+ /// **'Feature Request'**
+ String get feedbackFeature;
+
+ /// No description provided for @feedbackExperience.
+ ///
+ /// In en, this message translates to:
+ /// **'Experience'**
+ String get feedbackExperience;
+
+ /// No description provided for @feedbackOther.
+ ///
+ /// In en, this message translates to:
+ /// **'Other'**
+ String get feedbackOther;
+
+ /// No description provided for @feedbackWelcome.
+ ///
+ /// In en, this message translates to:
+ /// **'Welcome to Feedback'**
+ String get feedbackWelcome;
+
+ /// No description provided for @feedbackSelectType.
+ ///
+ /// In en, this message translates to:
+ /// **'Please select a feedback type:'**
+ String get feedbackSelectType;
+
+ /// No description provided for @feedbackTypeSelected.
+ ///
+ /// In en, this message translates to:
+ /// **'You selected \"{type}\", please describe your issue or suggestion:'**
+ String feedbackTypeSelected(String type);
+
+ /// No description provided for @feedbackThanks.
+ ///
+ /// In en, this message translates to:
+ /// **'Thanks for your feedback! We\'ll process your {type} soon.'**
+ String feedbackThanks(String type);
+
+ /// No description provided for @feedbackMore.
+ ///
+ /// In en, this message translates to:
+ /// **'Anything else? Keep typing or go back'**
+ String get feedbackMore;
+
+ /// No description provided for @feedbackInputHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter your feedback…'**
+ String get feedbackInputHint;
+
+ /// No description provided for @feedbackSelectFirst.
+ ///
+ /// In en, this message translates to:
+ /// **'Please select a feedback type first'**
+ String get feedbackSelectFirst;
+
+ /// No description provided for @feedbackOpinion.
+ ///
+ /// In en, this message translates to:
+ /// **'feedback'**
+ String get feedbackOpinion;
+
+ /// No description provided for @searchTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Search'**
+ String get searchTitle;
+
+ /// No description provided for @searchHistory.
+ ///
+ /// In en, this message translates to:
+ /// **'Search History'**
+ String get searchHistory;
+
+ /// No description provided for @clearHistory.
+ ///
+ /// In en, this message translates to:
+ /// **'Clear'**
+ String get clearHistory;
+
+ /// No description provided for @hotSearch.
+ ///
+ /// In en, this message translates to:
+ /// **'Trending'**
+ String get hotSearch;
+
+ /// No description provided for @deleteRecord.
+ ///
+ /// In en, this message translates to:
+ /// **'Delete Record'**
+ String get deleteRecord;
+
+ /// No description provided for @deleteRecordConfirm.
+ ///
+ /// In en, this message translates to:
+ /// **'Delete \"{keyword}\"?'**
+ String deleteRecordConfirm(String keyword);
+
+ /// No description provided for @searching.
+ ///
+ /// In en, this message translates to:
+ /// **'Searching \"{keyword}\"...'**
+ String searching(String keyword);
+
+ /// No description provided for @noResults.
+ ///
+ /// In en, this message translates to:
+ /// **'No recipes found'**
+ String get noResults;
+
+ /// No description provided for @tryOtherKeywords.
+ ///
+ /// In en, this message translates to:
+ /// **'Try other keywords like \"pork\", \"chicken\"'**
+ String get tryOtherKeywords;
+
+ /// No description provided for @resultCount.
+ ///
+ /// In en, this message translates to:
+ /// **'{count} results found · \"{keyword}\"'**
+ String resultCount(int count, String keyword);
+
+ /// No description provided for @recipeDetail.
+ ///
+ /// In en, this message translates to:
+ /// **'Recipe Detail'**
+ String get recipeDetail;
+
+ /// No description provided for @invalidRecipeIdError.
+ ///
+ /// In en, this message translates to:
+ /// **'Invalid recipe ID: {id}'**
+ String invalidRecipeIdError(String id);
+
+ /// No description provided for @recipeDataIncomplete.
+ ///
+ /// In en, this message translates to:
+ /// **'Recipe data incomplete'**
+ String get recipeDataIncomplete;
+
+ /// No description provided for @loadFailedWith.
+ ///
+ /// In en, this message translates to:
+ /// **'Load failed: {error}'**
+ String loadFailedWith(String error);
+
+ /// No description provided for @ingredients.
+ ///
+ /// In en, this message translates to:
+ /// **'Ingredients'**
+ String get ingredients;
+
+ /// No description provided for @steps.
+ ///
+ /// In en, this message translates to:
+ /// **'Steps'**
+ String get steps;
+
+ /// No description provided for @nutritionInfo.
+ ///
+ /// In en, this message translates to:
+ /// **'Nutrition'**
+ String get nutritionInfo;
+
+ /// No description provided for @calories.
+ ///
+ /// In en, this message translates to:
+ /// **'Calories'**
+ String get calories;
+
+ /// No description provided for @protein.
+ ///
+ /// In en, this message translates to:
+ /// **'Protein'**
+ String get protein;
+
+ /// No description provided for @fat.
+ ///
+ /// In en, this message translates to:
+ /// **'Fat'**
+ String get fat;
+
+ /// No description provided for @carbs.
+ ///
+ /// In en, this message translates to:
+ /// **'Carbs'**
+ String get carbs;
+
+ /// No description provided for @recommendSuccess.
+ ///
+ /// In en, this message translates to:
+ /// **'Recommended!'**
+ String get recommendSuccess;
+
+ /// No description provided for @recommend.
+ ///
+ /// In en, this message translates to:
+ /// **'Recommend'**
+ String get recommend;
+
+ /// No description provided for @notesDeveloping.
+ ///
+ /// In en, this message translates to:
+ /// **'Cooking notes feature in development'**
+ String get notesDeveloping;
+
+ /// No description provided for @notes.
+ ///
+ /// In en, this message translates to:
+ /// **'Notes'**
+ String get notes;
+
+ /// No description provided for @addedToShoppingList.
+ ///
+ /// In en, this message translates to:
+ /// **'Added to shopping list'**
+ String get addedToShoppingList;
+
+ /// No description provided for @shopping.
+ ///
+ /// In en, this message translates to:
+ /// **'Shopping'**
+ String get shopping;
+
+ /// No description provided for @profileTab.
+ ///
+ /// In en, this message translates to:
+ /// **'Me'**
+ String get profileTab;
+
+ /// No description provided for @homeTab.
+ ///
+ /// In en, this message translates to:
+ /// **'Home'**
+ String get homeTab;
+
+ /// No description provided for @settingsTab.
+ ///
+ /// In en, this message translates to:
+ /// **'Settings'**
+ String get settingsTab;
+
+ /// No description provided for @notLoggedIn.
+ ///
+ /// In en, this message translates to:
+ /// **'Not Logged In'**
+ String get notLoggedIn;
+
+ /// No description provided for @tapToLogin.
+ ///
+ /// In en, this message translates to:
+ /// **'Tap to Login'**
+ String get tapToLogin;
+
+ /// No description provided for @personalization.
+ ///
+ /// In en, this message translates to:
+ /// **'Personalize'**
+ String get personalization;
+
+ /// No description provided for @toolsTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Tools'**
+ String get toolsTitle;
+
+ /// No description provided for @cookingTimer.
+ ///
+ /// In en, this message translates to:
+ /// **'Cooking Timer'**
+ String get cookingTimer;
+
+ /// No description provided for @unitConverter.
+ ///
+ /// In en, this message translates to:
+ /// **'Unit Converter'**
+ String get unitConverter;
+
+ /// No description provided for @bmiCalculator.
+ ///
+ /// In en, this message translates to:
+ /// **'BMI Calculator'**
+ String get bmiCalculator;
+
+ /// No description provided for @servingScaler.
+ ///
+ /// In en, this message translates to:
+ /// **'Serving Scaler'**
+ String get servingScaler;
+
+ /// No description provided for @featureRecommend.
+ ///
+ /// In en, this message translates to:
+ /// **'Recommend'**
+ String get featureRecommend;
+
+ /// No description provided for @featureFollow.
+ ///
+ /// In en, this message translates to:
+ /// **'Follow'**
+ String get featureFollow;
+
+ /// No description provided for @shoppingList.
+ ///
+ /// In en, this message translates to:
+ /// **'Shopping List'**
+ String get shoppingList;
+
+ /// No description provided for @favorites.
+ ///
+ /// In en, this message translates to:
+ /// **'Favorites'**
+ String get favorites;
+
+ /// No description provided for @latestMessages.
+ ///
+ /// In en, this message translates to:
+ /// **'Latest Messages'**
+ String get latestMessages;
+
+ /// No description provided for @systemNotification.
+ ///
+ /// In en, this message translates to:
+ /// **'System'**
+ String get systemNotification;
+
+ /// No description provided for @newOrderReminder.
+ ///
+ /// In en, this message translates to:
+ /// **'You have a new order reminder'**
+ String get newOrderReminder;
+
+ /// No description provided for @marketingInfo.
+ ///
+ /// In en, this message translates to:
+ /// **'Promotions'**
+ String get marketingInfo;
+
+ /// No description provided for @newProductOnline.
+ ///
+ /// In en, this message translates to:
+ /// **'New items available, check now'**
+ String get newProductOnline;
+
+ /// No description provided for @favoritesTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Favorites'**
+ String get favoritesTitle;
+
+ /// No description provided for @selectAll.
+ ///
+ /// In en, this message translates to:
+ /// **'Select All'**
+ String get selectAll;
+
+ /// No description provided for @deselectAll.
+ ///
+ /// In en, this message translates to:
+ /// **'Deselect All'**
+ String get deselectAll;
+
+ /// No description provided for @selectedCount.
+ ///
+ /// In en, this message translates to:
+ /// **'{count} selected'**
+ String selectedCount(int count);
+
+ /// No description provided for @confirmDelete.
+ ///
+ /// In en, this message translates to:
+ /// **'Confirm Delete'**
+ String get confirmDelete;
+
+ /// No description provided for @confirmDeleteItems.
+ ///
+ /// In en, this message translates to:
+ /// **'Delete {count} selected favorites?'**
+ String confirmDeleteItems(int count);
+
+ /// No description provided for @emptyFavorites.
+ ///
+ /// In en, this message translates to:
+ /// **'No favorites yet'**
+ String get emptyFavorites;
+
+ /// No description provided for @addFavoriteHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Tap 🔖 while browsing to add favorites'**
+ String get addFavoriteHint;
+
+ /// No description provided for @sortMethod.
+ ///
+ /// In en, this message translates to:
+ /// **'Sort By'**
+ String get sortMethod;
+
+ /// No description provided for @sortNewest.
+ ///
+ /// In en, this message translates to:
+ /// **'Newest First'**
+ String get sortNewest;
+
+ /// No description provided for @sortOldest.
+ ///
+ /// In en, this message translates to:
+ /// **'Oldest First'**
+ String get sortOldest;
+
+ /// No description provided for @sortNameAZ.
+ ///
+ /// In en, this message translates to:
+ /// **'Name A-Z'**
+ String get sortNameAZ;
+
+ /// No description provided for @sortNameZA.
+ ///
+ /// In en, this message translates to:
+ /// **'Name Z-A'**
+ String get sortNameZA;
+
+ /// No description provided for @all.
+ ///
+ /// In en, this message translates to:
+ /// **'All'**
+ String get all;
+
+ /// No description provided for @footprintsTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'My Footprints'**
+ String get footprintsTitle;
+
+ /// No description provided for @footprintRemoved.
+ ///
+ /// In en, this message translates to:
+ /// **'Footprint removed'**
+ String get footprintRemoved;
+
+ /// No description provided for @personalizationTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Personalization'**
+ String get personalizationTitle;
+
+ /// No description provided for @themeColor.
+ ///
+ /// In en, this message translates to:
+ /// **'Theme Color'**
+ String get themeColor;
+
+ /// No description provided for @fontSetting.
+ ///
+ /// In en, this message translates to:
+ /// **'Font Size'**
+ String get fontSetting;
+
+ /// No description provided for @displayMode.
+ ///
+ /// In en, this message translates to:
+ /// **'Display Mode'**
+ String get displayMode;
+
+ /// No description provided for @animationEffect.
+ ///
+ /// In en, this message translates to:
+ /// **'Animation'**
+ String get animationEffect;
+
+ /// No description provided for @languageSetting.
+ ///
+ /// In en, this message translates to:
+ /// **'Language'**
+ String get languageSetting;
+
+ /// No description provided for @dialogStyle.
+ ///
+ /// In en, this message translates to:
+ /// **'Dialog Style'**
+ String get dialogStyle;
+
+ /// No description provided for @bubbleStyle.
+ ///
+ /// In en, this message translates to:
+ /// **'Bubble Style'**
+ String get bubbleStyle;
+
+ /// No description provided for @bottomBarStyle.
+ ///
+ /// In en, this message translates to:
+ /// **'Bottom Bar Style'**
+ String get bottomBarStyle;
+
+ /// No description provided for @cardSwipeDirection.
+ ///
+ /// In en, this message translates to:
+ /// **'Card Swipe Direction'**
+ String get cardSwipeDirection;
+
+ /// No description provided for @floatingBarOpacity.
+ ///
+ /// In en, this message translates to:
+ /// **'Floating Bar Opacity'**
+ String get floatingBarOpacity;
+
+ /// No description provided for @immersiveStatusBar.
+ ///
+ /// In en, this message translates to:
+ /// **'Immersive Status Bar'**
+ String get immersiveStatusBar;
+
+ /// No description provided for @unifiedStyle.
+ ///
+ /// In en, this message translates to:
+ /// **'Unified style (cross-platform)'**
+ String get unifiedStyle;
+
+ /// No description provided for @sampleDialog.
+ ///
+ /// In en, this message translates to:
+ /// **'Sample Dialog'**
+ String get sampleDialog;
+
+ /// No description provided for @sampleDialogContent.
+ ///
+ /// In en, this message translates to:
+ /// **'This is a demo using the current dialog style.'**
+ String get sampleDialogContent;
+
+ /// No description provided for @ok.
+ ///
+ /// In en, this message translates to:
+ /// **'OK'**
+ String get ok;
+
+ /// No description provided for @showDialogSample.
+ ///
+ /// In en, this message translates to:
+ /// **'Show Dialog Style Sample'**
+ String get showDialogSample;
+
+ /// No description provided for @selectThemeColor.
+ ///
+ /// In en, this message translates to:
+ /// **'Select Theme Color'**
+ String get selectThemeColor;
+
+ /// No description provided for @animationIntensity.
+ ///
+ /// In en, this message translates to:
+ /// **'Animation Intensity'**
+ String get animationIntensity;
+
+ /// No description provided for @selectLanguage.
+ ///
+ /// In en, this message translates to:
+ /// **'Select Language'**
+ String get selectLanguage;
+
+ /// No description provided for @restoreDefaults.
+ ///
+ /// In en, this message translates to:
+ /// **'Restore Defaults'**
+ String get restoreDefaults;
+
+ /// No description provided for @restoreDefaultsConfirm.
+ ///
+ /// In en, this message translates to:
+ /// **'Reset all settings to default?'**
+ String get restoreDefaultsConfirm;
+
+ /// No description provided for @swipeLeftRight.
+ ///
+ /// In en, this message translates to:
+ /// **'Left-Right Swipe'**
+ String get swipeLeftRight;
+
+ /// No description provided for @swipeUpDown.
+ ///
+ /// In en, this message translates to:
+ /// **'Up-Down Swipe'**
+ String get swipeUpDown;
+
+ /// No description provided for @enableImmersive.
+ ///
+ /// In en, this message translates to:
+ /// **'Enable Immersive Status Bar'**
+ String get enableImmersive;
+
+ /// No description provided for @preferenceTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Taste Preferences'**
+ String get preferenceTitle;
+
+ /// No description provided for @preferenceCategory.
+ ///
+ /// In en, this message translates to:
+ /// **'Preferred Categories'**
+ String get preferenceCategory;
+
+ /// No description provided for @preferenceCategoryHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Select your favorite cuisine categories'**
+ String get preferenceCategoryHint;
+
+ /// No description provided for @preferenceTag.
+ ///
+ /// In en, this message translates to:
+ /// **'Preferred Tags'**
+ String get preferenceTag;
+
+ /// No description provided for @preferenceTagHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Select tags you\'re interested in'**
+ String get preferenceTagHint;
+
+ /// No description provided for @allergenBlock.
+ ///
+ /// In en, this message translates to:
+ /// **'Allergen Exclusion'**
+ String get allergenBlock;
+
+ /// No description provided for @allergenBlockHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Exclude recipes with these ingredients'**
+ String get allergenBlockHint;
+
+ /// No description provided for @noCategoryData.
+ ///
+ /// In en, this message translates to:
+ /// **'No category data'**
+ String get noCategoryData;
+
+ /// No description provided for @tagPlaceholder.
+ ///
+ /// In en, this message translates to:
+ /// **'Tags will appear here after setting preferences'**
+ String get tagPlaceholder;
+
+ /// No description provided for @resetPreference.
+ ///
+ /// In en, this message translates to:
+ /// **'Reset Preferences'**
+ String get resetPreference;
+
+ /// No description provided for @resetPreferenceConfirm.
+ ///
+ /// In en, this message translates to:
+ /// **'Clear all taste preference settings?'**
+ String get resetPreferenceConfirm;
+
+ /// No description provided for @servingScalerTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Serving Scaler'**
+ String get servingScalerTitle;
+
+ /// No description provided for @originalServing.
+ ///
+ /// In en, this message translates to:
+ /// **'Original Servings'**
+ String get originalServing;
+
+ /// No description provided for @targetServing.
+ ///
+ /// In en, this message translates to:
+ /// **'Target Servings'**
+ String get targetServing;
+
+ /// No description provided for @scaleRatio.
+ ///
+ /// In en, this message translates to:
+ /// **'Scale Ratio'**
+ String get scaleRatio;
+
+ /// No description provided for @nutritionCenter.
+ ///
+ /// In en, this message translates to:
+ /// **'Nutrition Center'**
+ String get nutritionCenter;
+
+ /// No description provided for @standardsViolation.
+ ///
+ /// In en, this message translates to:
+ /// **'Standards Violation'**
+ String get standardsViolation;
+
+ /// No description provided for @pageBlocked.
+ ///
+ /// In en, this message translates to:
+ /// **'Page Blocked'**
+ String get pageBlocked;
+
+ /// No description provided for @routeLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Route: {route}'**
+ String routeLabel(String route);
+
+ /// No description provided for @reasonLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Reason: {reason}'**
+ String reasonLabel(String reason);
+
+ /// No description provided for @failedChecks.
+ ///
+ /// In en, this message translates to:
+ /// **'Failed checks:'**
+ String get failedChecks;
+
+ /// No description provided for @unknown.
+ ///
+ /// In en, this message translates to:
+ /// **'Unknown'**
+ String get unknown;
+
+ /// No description provided for @unknownReason.
+ ///
+ /// In en, this message translates to:
+ /// **'Unknown reason'**
+ String get unknownReason;
}
class _AppLocalizationsDelegate
diff --git a/lib/src/l10n/app_localizations_en.dart b/lib/src/l10n/app_localizations_en.dart
index ed90d7a..551c8c1 100644
--- a/lib/src/l10n/app_localizations_en.dart
+++ b/lib/src/l10n/app_localizations_en.dart
@@ -62,4 +62,561 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get confirm => 'Confirm';
+
+ @override
+ String get delete => 'Delete';
+
+ @override
+ String get edit => 'Edit';
+
+ @override
+ String get done => 'Done';
+
+ @override
+ String get reset => 'Reset';
+
+ @override
+ String get loading => 'Loading...';
+
+ @override
+ String get error => 'Error';
+
+ @override
+ String get success => 'Success';
+
+ @override
+ String get tip => 'Tip';
+
+ @override
+ String get backToHome => 'Back to Home';
+
+ @override
+ String get reload => 'Reload';
+
+ @override
+ String get searchAgain => 'Search Again';
+
+ @override
+ String get homeTitle => 'Mom\'s Kitchen';
+
+ @override
+ String get searchPlaceholder => 'Search recipes, ingredients...';
+
+ @override
+ String get loadingRecipes => 'Loading recipes...';
+
+ @override
+ String get loadFailed => 'Load Failed';
+
+ @override
+ String get noRecipes => 'No Recipes';
+
+ @override
+ String get addRecipesHint => 'Add some delicious recipes';
+
+ @override
+ String get todayRecommend => 'Today\'s Picks';
+
+ @override
+ String recipeCount(int count) {
+ return '$count recipes';
+ }
+
+ @override
+ String get categoryBrowse => 'Categories';
+
+ @override
+ String get categoryMeat => 'Meat';
+
+ @override
+ String get categoryVeggie => 'Vegetarian';
+
+ @override
+ String get categoryNoodle => 'Noodles';
+
+ @override
+ String get categorySoup => 'Soup';
+
+ @override
+ String get categoryDessert => 'Dessert';
+
+ @override
+ String get categoryDrink => 'Drinks';
+
+ @override
+ String get categorySnack => 'Snacks';
+
+ @override
+ String get invalidRecipeId => 'Invalid recipe ID';
+
+ @override
+ String get hotRanking => 'Hot Rankings';
+
+ @override
+ String get periodToday => 'Today';
+
+ @override
+ String get periodMonth => 'This Month';
+
+ @override
+ String get periodAll => 'All Time';
+
+ @override
+ String get sortByViews => 'Views';
+
+ @override
+ String get sortByLikes => 'Likes';
+
+ @override
+ String get sortByRecommend => 'Recommended';
+
+ @override
+ String noRankingData(String period) {
+ return 'No $period ranking data';
+ }
+
+ @override
+ String get whatToEat => 'What to Eat';
+
+ @override
+ String get loadingFilterOptions => 'Loading filter options…';
+
+ @override
+ String get firstLoadHint => 'Please wait, first load may take a few seconds';
+
+ @override
+ String get pickingRecipe => 'Picking a recipe for you…';
+
+ @override
+ String get matchingHint => 'Matching based on your filters';
+
+ @override
+ String get tapToStart => 'Tap the button below to start';
+
+ @override
+ String get picking => 'Picking…';
+
+ @override
+ String get randomSelect => 'Random Pick';
+
+ @override
+ String get changeAnother => 'Try Another';
+
+ @override
+ String currentFilter(String summary) {
+ return 'Current filter: $summary';
+ }
+
+ @override
+ String get categoryFilter => 'Category Filter';
+
+ @override
+ String get categoryLoading => 'Loading categories…';
+
+ @override
+ String get tagFilter => 'Tag Filter';
+
+ @override
+ String get tagLoading => 'Loading tags…';
+
+ @override
+ String get allergenFilter => 'Allergen Exclusion';
+
+ @override
+ String get feedbackTitle => 'Feedback';
+
+ @override
+ String get feedbackBug => 'Bug Report';
+
+ @override
+ String get feedbackFeature => 'Feature Request';
+
+ @override
+ String get feedbackExperience => 'Experience';
+
+ @override
+ String get feedbackOther => 'Other';
+
+ @override
+ String get feedbackWelcome => 'Welcome to Feedback';
+
+ @override
+ String get feedbackSelectType => 'Please select a feedback type:';
+
+ @override
+ String feedbackTypeSelected(String type) {
+ return 'You selected \"$type\", please describe your issue or suggestion:';
+ }
+
+ @override
+ String feedbackThanks(String type) {
+ return 'Thanks for your feedback! We\'ll process your $type soon.';
+ }
+
+ @override
+ String get feedbackMore => 'Anything else? Keep typing or go back';
+
+ @override
+ String get feedbackInputHint => 'Enter your feedback…';
+
+ @override
+ String get feedbackSelectFirst => 'Please select a feedback type first';
+
+ @override
+ String get feedbackOpinion => 'feedback';
+
+ @override
+ String get searchTitle => 'Search';
+
+ @override
+ String get searchHistory => 'Search History';
+
+ @override
+ String get clearHistory => 'Clear';
+
+ @override
+ String get hotSearch => 'Trending';
+
+ @override
+ String get deleteRecord => 'Delete Record';
+
+ @override
+ String deleteRecordConfirm(String keyword) {
+ return 'Delete \"$keyword\"?';
+ }
+
+ @override
+ String searching(String keyword) {
+ return 'Searching \"$keyword\"...';
+ }
+
+ @override
+ String get noResults => 'No recipes found';
+
+ @override
+ String get tryOtherKeywords =>
+ 'Try other keywords like \"pork\", \"chicken\"';
+
+ @override
+ String resultCount(int count, String keyword) {
+ return '$count results found · \"$keyword\"';
+ }
+
+ @override
+ String get recipeDetail => 'Recipe Detail';
+
+ @override
+ String invalidRecipeIdError(String id) {
+ return 'Invalid recipe ID: $id';
+ }
+
+ @override
+ String get recipeDataIncomplete => 'Recipe data incomplete';
+
+ @override
+ String loadFailedWith(String error) {
+ return 'Load failed: $error';
+ }
+
+ @override
+ String get ingredients => 'Ingredients';
+
+ @override
+ String get steps => 'Steps';
+
+ @override
+ String get nutritionInfo => 'Nutrition';
+
+ @override
+ String get calories => 'Calories';
+
+ @override
+ String get protein => 'Protein';
+
+ @override
+ String get fat => 'Fat';
+
+ @override
+ String get carbs => 'Carbs';
+
+ @override
+ String get recommendSuccess => 'Recommended!';
+
+ @override
+ String get recommend => 'Recommend';
+
+ @override
+ String get notesDeveloping => 'Cooking notes feature in development';
+
+ @override
+ String get notes => 'Notes';
+
+ @override
+ String get addedToShoppingList => 'Added to shopping list';
+
+ @override
+ String get shopping => 'Shopping';
+
+ @override
+ String get profileTab => 'Me';
+
+ @override
+ String get homeTab => 'Home';
+
+ @override
+ String get settingsTab => 'Settings';
+
+ @override
+ String get notLoggedIn => 'Not Logged In';
+
+ @override
+ String get tapToLogin => 'Tap to Login';
+
+ @override
+ String get personalization => 'Personalize';
+
+ @override
+ String get toolsTitle => 'Tools';
+
+ @override
+ String get cookingTimer => 'Cooking Timer';
+
+ @override
+ String get unitConverter => 'Unit Converter';
+
+ @override
+ String get bmiCalculator => 'BMI Calculator';
+
+ @override
+ String get servingScaler => 'Serving Scaler';
+
+ @override
+ String get featureRecommend => 'Recommend';
+
+ @override
+ String get featureFollow => 'Follow';
+
+ @override
+ String get shoppingList => 'Shopping List';
+
+ @override
+ String get favorites => 'Favorites';
+
+ @override
+ String get latestMessages => 'Latest Messages';
+
+ @override
+ String get systemNotification => 'System';
+
+ @override
+ String get newOrderReminder => 'You have a new order reminder';
+
+ @override
+ String get marketingInfo => 'Promotions';
+
+ @override
+ String get newProductOnline => 'New items available, check now';
+
+ @override
+ String get favoritesTitle => 'Favorites';
+
+ @override
+ String get selectAll => 'Select All';
+
+ @override
+ String get deselectAll => 'Deselect All';
+
+ @override
+ String selectedCount(int count) {
+ return '$count selected';
+ }
+
+ @override
+ String get confirmDelete => 'Confirm Delete';
+
+ @override
+ String confirmDeleteItems(int count) {
+ return 'Delete $count selected favorites?';
+ }
+
+ @override
+ String get emptyFavorites => 'No favorites yet';
+
+ @override
+ String get addFavoriteHint => 'Tap 🔖 while browsing to add favorites';
+
+ @override
+ String get sortMethod => 'Sort By';
+
+ @override
+ String get sortNewest => 'Newest First';
+
+ @override
+ String get sortOldest => 'Oldest First';
+
+ @override
+ String get sortNameAZ => 'Name A-Z';
+
+ @override
+ String get sortNameZA => 'Name Z-A';
+
+ @override
+ String get all => 'All';
+
+ @override
+ String get footprintsTitle => 'My Footprints';
+
+ @override
+ String get footprintRemoved => 'Footprint removed';
+
+ @override
+ String get personalizationTitle => 'Personalization';
+
+ @override
+ String get themeColor => 'Theme Color';
+
+ @override
+ String get fontSetting => 'Font Size';
+
+ @override
+ String get displayMode => 'Display Mode';
+
+ @override
+ String get animationEffect => 'Animation';
+
+ @override
+ String get languageSetting => 'Language';
+
+ @override
+ String get dialogStyle => 'Dialog Style';
+
+ @override
+ String get bubbleStyle => 'Bubble Style';
+
+ @override
+ String get bottomBarStyle => 'Bottom Bar Style';
+
+ @override
+ String get cardSwipeDirection => 'Card Swipe Direction';
+
+ @override
+ String get floatingBarOpacity => 'Floating Bar Opacity';
+
+ @override
+ String get immersiveStatusBar => 'Immersive Status Bar';
+
+ @override
+ String get unifiedStyle => 'Unified style (cross-platform)';
+
+ @override
+ String get sampleDialog => 'Sample Dialog';
+
+ @override
+ String get sampleDialogContent =>
+ 'This is a demo using the current dialog style.';
+
+ @override
+ String get ok => 'OK';
+
+ @override
+ String get showDialogSample => 'Show Dialog Style Sample';
+
+ @override
+ String get selectThemeColor => 'Select Theme Color';
+
+ @override
+ String get animationIntensity => 'Animation Intensity';
+
+ @override
+ String get selectLanguage => 'Select Language';
+
+ @override
+ String get restoreDefaults => 'Restore Defaults';
+
+ @override
+ String get restoreDefaultsConfirm => 'Reset all settings to default?';
+
+ @override
+ String get swipeLeftRight => 'Left-Right Swipe';
+
+ @override
+ String get swipeUpDown => 'Up-Down Swipe';
+
+ @override
+ String get enableImmersive => 'Enable Immersive Status Bar';
+
+ @override
+ String get preferenceTitle => 'Taste Preferences';
+
+ @override
+ String get preferenceCategory => 'Preferred Categories';
+
+ @override
+ String get preferenceCategoryHint =>
+ 'Select your favorite cuisine categories';
+
+ @override
+ String get preferenceTag => 'Preferred Tags';
+
+ @override
+ String get preferenceTagHint => 'Select tags you\'re interested in';
+
+ @override
+ String get allergenBlock => 'Allergen Exclusion';
+
+ @override
+ String get allergenBlockHint => 'Exclude recipes with these ingredients';
+
+ @override
+ String get noCategoryData => 'No category data';
+
+ @override
+ String get tagPlaceholder =>
+ 'Tags will appear here after setting preferences';
+
+ @override
+ String get resetPreference => 'Reset Preferences';
+
+ @override
+ String get resetPreferenceConfirm => 'Clear all taste preference settings?';
+
+ @override
+ String get servingScalerTitle => 'Serving Scaler';
+
+ @override
+ String get originalServing => 'Original Servings';
+
+ @override
+ String get targetServing => 'Target Servings';
+
+ @override
+ String get scaleRatio => 'Scale Ratio';
+
+ @override
+ String get nutritionCenter => 'Nutrition Center';
+
+ @override
+ String get standardsViolation => 'Standards Violation';
+
+ @override
+ String get pageBlocked => 'Page Blocked';
+
+ @override
+ String routeLabel(String route) {
+ return 'Route: $route';
+ }
+
+ @override
+ String reasonLabel(String reason) {
+ return 'Reason: $reason';
+ }
+
+ @override
+ String get failedChecks => 'Failed checks:';
+
+ @override
+ String get unknown => 'Unknown';
+
+ @override
+ String get unknownReason => 'Unknown reason';
}
diff --git a/lib/src/l10n/app_localizations_zh.dart b/lib/src/l10n/app_localizations_zh.dart
index 4bcc88e..22be18b 100644
--- a/lib/src/l10n/app_localizations_zh.dart
+++ b/lib/src/l10n/app_localizations_zh.dart
@@ -61,6 +61,559 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get confirm => '确认';
+
+ @override
+ String get delete => '删除';
+
+ @override
+ String get edit => '编辑';
+
+ @override
+ String get done => '完成';
+
+ @override
+ String get reset => '重置';
+
+ @override
+ String get loading => '加载中...';
+
+ @override
+ String get error => '错误';
+
+ @override
+ String get success => '成功';
+
+ @override
+ String get tip => '提示';
+
+ @override
+ String get backToHome => '返回主页';
+
+ @override
+ String get reload => '重新加载';
+
+ @override
+ String get searchAgain => '重新搜索';
+
+ @override
+ String get homeTitle => '老妈厨房';
+
+ @override
+ String get searchPlaceholder => '搜索菜谱、食材...';
+
+ @override
+ String get loadingRecipes => '正在加载菜谱...';
+
+ @override
+ String get loadFailed => '加载失败';
+
+ @override
+ String get noRecipes => '暂无菜谱';
+
+ @override
+ String get addRecipesHint => '快去添加一些美味菜谱吧';
+
+ @override
+ String get todayRecommend => '今日推荐';
+
+ @override
+ String recipeCount(int count) {
+ return '$count 道菜谱';
+ }
+
+ @override
+ String get categoryBrowse => '分类浏览';
+
+ @override
+ String get categoryMeat => '荤菜';
+
+ @override
+ String get categoryVeggie => '素菜';
+
+ @override
+ String get categoryNoodle => '面食';
+
+ @override
+ String get categorySoup => '汤品';
+
+ @override
+ String get categoryDessert => '甜品';
+
+ @override
+ String get categoryDrink => '饮品';
+
+ @override
+ String get categorySnack => '小吃';
+
+ @override
+ String get invalidRecipeId => '菜谱ID无效';
+
+ @override
+ String get hotRanking => '热门排行';
+
+ @override
+ String get periodToday => '今日';
+
+ @override
+ String get periodMonth => '本月';
+
+ @override
+ String get periodAll => '累计';
+
+ @override
+ String get sortByViews => '浏览量';
+
+ @override
+ String get sortByLikes => '点赞数';
+
+ @override
+ String get sortByRecommend => '推荐数';
+
+ @override
+ String noRankingData(String period) {
+ return '暂无$period排行数据';
+ }
+
+ @override
+ String get whatToEat => '今天吃什么';
+
+ @override
+ String get loadingFilterOptions => '正在加载筛选选项…';
+
+ @override
+ String get firstLoadHint => '请稍候,首次加载可能需要几秒钟';
+
+ @override
+ String get pickingRecipe => '正在为您挑选菜谱…';
+
+ @override
+ String get matchingHint => '根据您的筛选条件匹配中';
+
+ @override
+ String get tapToStart => '点击下方按钮开始选择';
+
+ @override
+ String get picking => '挑选中…';
+
+ @override
+ String get randomSelect => '随机选择';
+
+ @override
+ String get changeAnother => '换一个';
+
+ @override
+ String currentFilter(String summary) {
+ return '当前筛选: $summary';
+ }
+
+ @override
+ String get categoryFilter => '分类筛选';
+
+ @override
+ String get categoryLoading => '分类数据加载中…';
+
+ @override
+ String get tagFilter => '标签筛选';
+
+ @override
+ String get tagLoading => '标签数据加载中…';
+
+ @override
+ String get allergenFilter => '过敏原排除';
+
+ @override
+ String get feedbackTitle => '意见反馈';
+
+ @override
+ String get feedbackBug => 'Bug 反馈';
+
+ @override
+ String get feedbackFeature => '功能建议';
+
+ @override
+ String get feedbackExperience => '体验优化';
+
+ @override
+ String get feedbackOther => '其他';
+
+ @override
+ String get feedbackWelcome => '欢迎使用意见反馈';
+
+ @override
+ String get feedbackSelectType => '请选择您要反馈的类型:';
+
+ @override
+ String feedbackTypeSelected(String type) {
+ return '好的,您选择了「$type」,请详细描述您的问题或建议:';
+ }
+
+ @override
+ String feedbackThanks(String type) {
+ return '感谢您的反馈!我们会尽快处理您的$type。';
+ }
+
+ @override
+ String get feedbackMore => '还有其他问题吗?可以继续输入,或直接返回';
+
+ @override
+ String get feedbackInputHint => '输入您的反馈…';
+
+ @override
+ String get feedbackSelectFirst => '请先选择反馈类型';
+
+ @override
+ String get feedbackOpinion => '意见';
+
+ @override
+ String get searchTitle => '搜索';
+
+ @override
+ String get searchHistory => '搜索历史';
+
+ @override
+ String get clearHistory => '清空';
+
+ @override
+ String get hotSearch => '热门搜索';
+
+ @override
+ String get deleteRecord => '删除记录';
+
+ @override
+ String deleteRecordConfirm(String keyword) {
+ return '确定要删除\"$keyword\"吗?';
+ }
+
+ @override
+ String searching(String keyword) {
+ return '正在搜索\"$keyword\"...';
+ }
+
+ @override
+ String get noResults => '未找到相关菜谱';
+
+ @override
+ String get tryOtherKeywords => '试试其他关键词,如\"红烧肉\"、\"糖醋排骨\"';
+
+ @override
+ String resultCount(int count, String keyword) {
+ return '找到 $count 个结果 · \"$keyword\"';
+ }
+
+ @override
+ String get recipeDetail => '菜谱详情';
+
+ @override
+ String invalidRecipeIdError(String id) {
+ return '无效的菜谱ID: $id';
+ }
+
+ @override
+ String get recipeDataIncomplete => '菜谱数据不完整';
+
+ @override
+ String loadFailedWith(String error) {
+ return '加载失败: $error';
+ }
+
+ @override
+ String get ingredients => '食材';
+
+ @override
+ String get steps => '做法';
+
+ @override
+ String get nutritionInfo => '营养信息';
+
+ @override
+ String get calories => '热量';
+
+ @override
+ String get protein => '蛋白质';
+
+ @override
+ String get fat => '脂肪';
+
+ @override
+ String get carbs => '碳水';
+
+ @override
+ String get recommendSuccess => '推荐成功';
+
+ @override
+ String get recommend => '推荐';
+
+ @override
+ String get notesDeveloping => '烹饪笔记功能开发中';
+
+ @override
+ String get notes => '笔记';
+
+ @override
+ String get addedToShoppingList => '已添加到购物清单';
+
+ @override
+ String get shopping => '购物';
+
+ @override
+ String get profileTab => '我的';
+
+ @override
+ String get homeTab => '首页';
+
+ @override
+ String get settingsTab => '设置';
+
+ @override
+ String get notLoggedIn => '未登录';
+
+ @override
+ String get tapToLogin => '点击登录';
+
+ @override
+ String get personalization => '个性化';
+
+ @override
+ String get toolsTitle => '实用工具';
+
+ @override
+ String get cookingTimer => '烹饪计时';
+
+ @override
+ String get unitConverter => '用量换算';
+
+ @override
+ String get bmiCalculator => 'BMI计算';
+
+ @override
+ String get servingScaler => '份量缩放';
+
+ @override
+ String get featureRecommend => '推荐';
+
+ @override
+ String get featureFollow => '关注';
+
+ @override
+ String get shoppingList => '购物清单';
+
+ @override
+ String get favorites => '收藏';
+
+ @override
+ String get latestMessages => '最新消息';
+
+ @override
+ String get systemNotification => '系统通知';
+
+ @override
+ String get newOrderReminder => '您有一条新的订单提醒';
+
+ @override
+ String get marketingInfo => '营销信息';
+
+ @override
+ String get newProductOnline => '新品上线,立即查看';
+
+ @override
+ String get favoritesTitle => '收藏';
+
+ @override
+ String get selectAll => '全选';
+
+ @override
+ String get deselectAll => '取消全选';
+
+ @override
+ String selectedCount(int count) {
+ return '已选 $count 项';
+ }
+
+ @override
+ String get confirmDelete => '确认删除';
+
+ @override
+ String confirmDeleteItems(int count) {
+ return '确定要删除选中的 $count 项收藏吗?';
+ }
+
+ @override
+ String get emptyFavorites => '收藏夹是空的';
+
+ @override
+ String get addFavoriteHint => '浏览菜谱时点击 🔖 即可收藏';
+
+ @override
+ String get sortMethod => '排序方式';
+
+ @override
+ String get sortNewest => '最新收藏';
+
+ @override
+ String get sortOldest => '最早收藏';
+
+ @override
+ String get sortNameAZ => '名称 A-Z';
+
+ @override
+ String get sortNameZA => '名称 Z-A';
+
+ @override
+ String get all => '全部';
+
+ @override
+ String get footprintsTitle => '我的足迹';
+
+ @override
+ String get footprintRemoved => '已移除足迹';
+
+ @override
+ String get personalizationTitle => '个性化设置';
+
+ @override
+ String get themeColor => '主题颜色';
+
+ @override
+ String get fontSetting => '字体大小';
+
+ @override
+ String get displayMode => '显示模式';
+
+ @override
+ String get animationEffect => '动画效果';
+
+ @override
+ String get languageSetting => '语言';
+
+ @override
+ String get dialogStyle => '对话框样式';
+
+ @override
+ String get bubbleStyle => '消息气泡样式';
+
+ @override
+ String get bottomBarStyle => '底部栏样式';
+
+ @override
+ String get cardSwipeDirection => '卡片滑动方向';
+
+ @override
+ String get floatingBarOpacity => '悬浮栏透明度';
+
+ @override
+ String get immersiveStatusBar => '沉浸状态栏';
+
+ @override
+ String get unifiedStyle => '启用统一样式(跨平台一致)';
+
+ @override
+ String get sampleDialog => '示例对话框';
+
+ @override
+ String get sampleDialogContent => '这是使用当前对话框样式的演示。';
+
+ @override
+ String get ok => '确定';
+
+ @override
+ String get showDialogSample => '显示对话框样式示例';
+
+ @override
+ String get selectThemeColor => '选择主题颜色';
+
+ @override
+ String get animationIntensity => '动画强度';
+
+ @override
+ String get selectLanguage => '选择语言';
+
+ @override
+ String get restoreDefaults => '恢复默认设置';
+
+ @override
+ String get restoreDefaultsConfirm => '确定要恢复所有设置到默认值吗?';
+
+ @override
+ String get swipeLeftRight => '左右滑动';
+
+ @override
+ String get swipeUpDown => '上下滑动';
+
+ @override
+ String get enableImmersive => '启用沉浸状态栏';
+
+ @override
+ String get preferenceTitle => '口味偏好';
+
+ @override
+ String get preferenceCategory => '偏好分类';
+
+ @override
+ String get preferenceCategoryHint => '选择你喜欢的菜系分类';
+
+ @override
+ String get preferenceTag => '偏好标签';
+
+ @override
+ String get preferenceTagHint => '选择你感兴趣的标签';
+
+ @override
+ String get allergenBlock => '过敏原屏蔽';
+
+ @override
+ String get allergenBlockHint => '屏蔽含这些食材的菜谱';
+
+ @override
+ String get noCategoryData => '暂无分类数据';
+
+ @override
+ String get tagPlaceholder => '设置偏好后标签将显示在此处';
+
+ @override
+ String get resetPreference => '重置偏好';
+
+ @override
+ String get resetPreferenceConfirm => '确定要清除所有口味偏好设置吗?';
+
+ @override
+ String get servingScalerTitle => '份量缩放工具';
+
+ @override
+ String get originalServing => '原始份量';
+
+ @override
+ String get targetServing => '目标份量';
+
+ @override
+ String get scaleRatio => '缩放比例';
+
+ @override
+ String get nutritionCenter => '营养中心';
+
+ @override
+ String get standardsViolation => '页面规范拦截';
+
+ @override
+ String get pageBlocked => '页面被拦截';
+
+ @override
+ String routeLabel(String route) {
+ return '路由: $route';
+ }
+
+ @override
+ String reasonLabel(String reason) {
+ return '原因: $reason';
+ }
+
+ @override
+ String get failedChecks => '未通过的检查项:';
+
+ @override
+ String get unknown => '未知';
+
+ @override
+ String get unknownReason => '未知原因';
}
/// The translations for Chinese, using the Han script (`zh_Hant`).
@@ -120,4 +673,557 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String get confirm => '確認';
+
+ @override
+ String get delete => '刪除';
+
+ @override
+ String get edit => '編輯';
+
+ @override
+ String get done => '完成';
+
+ @override
+ String get reset => '重置';
+
+ @override
+ String get loading => '載入中...';
+
+ @override
+ String get error => '錯誤';
+
+ @override
+ String get success => '成功';
+
+ @override
+ String get tip => '提示';
+
+ @override
+ String get backToHome => '返回主頁';
+
+ @override
+ String get reload => '重新載入';
+
+ @override
+ String get searchAgain => '重新搜尋';
+
+ @override
+ String get homeTitle => '老媽廚房';
+
+ @override
+ String get searchPlaceholder => '搜尋菜譜、食材...';
+
+ @override
+ String get loadingRecipes => '正在載入菜譜...';
+
+ @override
+ String get loadFailed => '載入失敗';
+
+ @override
+ String get noRecipes => '暫無菜譜';
+
+ @override
+ String get addRecipesHint => '快去添加一些美味菜譜吧';
+
+ @override
+ String get todayRecommend => '今日推薦';
+
+ @override
+ String recipeCount(int count) {
+ return '$count 道菜譜';
+ }
+
+ @override
+ String get categoryBrowse => '分類瀏覽';
+
+ @override
+ String get categoryMeat => '葷菜';
+
+ @override
+ String get categoryVeggie => '素菜';
+
+ @override
+ String get categoryNoodle => '麵食';
+
+ @override
+ String get categorySoup => '湯品';
+
+ @override
+ String get categoryDessert => '甜品';
+
+ @override
+ String get categoryDrink => '飲品';
+
+ @override
+ String get categorySnack => '小吃';
+
+ @override
+ String get invalidRecipeId => '菜譜ID無效';
+
+ @override
+ String get hotRanking => '熱門排行';
+
+ @override
+ String get periodToday => '今日';
+
+ @override
+ String get periodMonth => '本月';
+
+ @override
+ String get periodAll => '累計';
+
+ @override
+ String get sortByViews => '瀏覽量';
+
+ @override
+ String get sortByLikes => '點讚數';
+
+ @override
+ String get sortByRecommend => '推薦數';
+
+ @override
+ String noRankingData(String period) {
+ return '暫無$period排行數據';
+ }
+
+ @override
+ String get whatToEat => '今天吃什麼';
+
+ @override
+ String get loadingFilterOptions => '正在載入篩選選項…';
+
+ @override
+ String get firstLoadHint => '請稍候,首次載入可能需要幾秒鐘';
+
+ @override
+ String get pickingRecipe => '正在為您挑選菜譜…';
+
+ @override
+ String get matchingHint => '根據您的篩選條件匹配中';
+
+ @override
+ String get tapToStart => '點擊下方按鈕開始選擇';
+
+ @override
+ String get picking => '挑選中…';
+
+ @override
+ String get randomSelect => '隨機選擇';
+
+ @override
+ String get changeAnother => '換一個';
+
+ @override
+ String currentFilter(String summary) {
+ return '當前篩選: $summary';
+ }
+
+ @override
+ String get categoryFilter => '分類篩選';
+
+ @override
+ String get categoryLoading => '分類數據載入中…';
+
+ @override
+ String get tagFilter => '標籤篩選';
+
+ @override
+ String get tagLoading => '標籤數據載入中…';
+
+ @override
+ String get allergenFilter => '過敏原排除';
+
+ @override
+ String get feedbackTitle => '意見反饋';
+
+ @override
+ String get feedbackBug => 'Bug 反饋';
+
+ @override
+ String get feedbackFeature => '功能建議';
+
+ @override
+ String get feedbackExperience => '體驗優化';
+
+ @override
+ String get feedbackOther => '其他';
+
+ @override
+ String get feedbackWelcome => '歡迎使用意見反饋';
+
+ @override
+ String get feedbackSelectType => '請選擇您要反饋的類型:';
+
+ @override
+ String feedbackTypeSelected(String type) {
+ return '好的,您選擇了「$type」,請詳細描述您的問題或建議:';
+ }
+
+ @override
+ String feedbackThanks(String type) {
+ return '感謝您的反饋!我們會盡快處理您的$type。';
+ }
+
+ @override
+ String get feedbackMore => '還有其他問題嗎?可以繼續輸入,或直接返回';
+
+ @override
+ String get feedbackInputHint => '輸入您的反饋…';
+
+ @override
+ String get feedbackSelectFirst => '請先選擇反饋類型';
+
+ @override
+ String get feedbackOpinion => '意見';
+
+ @override
+ String get searchTitle => '搜尋';
+
+ @override
+ String get searchHistory => '搜尋歷史';
+
+ @override
+ String get clearHistory => '清空';
+
+ @override
+ String get hotSearch => '熱門搜尋';
+
+ @override
+ String get deleteRecord => '刪除記錄';
+
+ @override
+ String deleteRecordConfirm(String keyword) {
+ return '確定要刪除\"$keyword\"嗎?';
+ }
+
+ @override
+ String searching(String keyword) {
+ return '正在搜尋\"$keyword\"...';
+ }
+
+ @override
+ String get noResults => '未找到相關菜譜';
+
+ @override
+ String get tryOtherKeywords => '試試其他關鍵詞,如\"紅燒肉\"、\"糖醋排骨\"';
+
+ @override
+ String resultCount(int count, String keyword) {
+ return '找到 $count 個結果 · \"$keyword\"';
+ }
+
+ @override
+ String get recipeDetail => '菜譜詳情';
+
+ @override
+ String invalidRecipeIdError(String id) {
+ return '無效的菜譜ID: $id';
+ }
+
+ @override
+ String get recipeDataIncomplete => '菜譜數據不完整';
+
+ @override
+ String loadFailedWith(String error) {
+ return '載入失敗: $error';
+ }
+
+ @override
+ String get ingredients => '食材';
+
+ @override
+ String get steps => '做法';
+
+ @override
+ String get nutritionInfo => '營養信息';
+
+ @override
+ String get calories => '熱量';
+
+ @override
+ String get protein => '蛋白質';
+
+ @override
+ String get fat => '脂肪';
+
+ @override
+ String get carbs => '碳水';
+
+ @override
+ String get recommendSuccess => '推薦成功';
+
+ @override
+ String get recommend => '推薦';
+
+ @override
+ String get notesDeveloping => '烹飪筆記功能開發中';
+
+ @override
+ String get notes => '筆記';
+
+ @override
+ String get addedToShoppingList => '已添加到購物清單';
+
+ @override
+ String get shopping => '購物';
+
+ @override
+ String get profileTab => '我的';
+
+ @override
+ String get homeTab => '首頁';
+
+ @override
+ String get settingsTab => '設置';
+
+ @override
+ String get notLoggedIn => '未登入';
+
+ @override
+ String get tapToLogin => '點擊登入';
+
+ @override
+ String get personalization => '個人化';
+
+ @override
+ String get toolsTitle => '實用工具';
+
+ @override
+ String get cookingTimer => '烹飪計時';
+
+ @override
+ String get unitConverter => '用量換算';
+
+ @override
+ String get bmiCalculator => 'BMI計算';
+
+ @override
+ String get servingScaler => '份量縮放';
+
+ @override
+ String get featureRecommend => '推薦';
+
+ @override
+ String get featureFollow => '關注';
+
+ @override
+ String get shoppingList => '購物清單';
+
+ @override
+ String get favorites => '收藏';
+
+ @override
+ String get latestMessages => '最新消息';
+
+ @override
+ String get systemNotification => '系統通知';
+
+ @override
+ String get newOrderReminder => '您有一條新的訂單提醒';
+
+ @override
+ String get marketingInfo => '營銷信息';
+
+ @override
+ String get newProductOnline => '新品上線,立即查看';
+
+ @override
+ String get favoritesTitle => '收藏';
+
+ @override
+ String get selectAll => '全選';
+
+ @override
+ String get deselectAll => '取消全選';
+
+ @override
+ String selectedCount(int count) {
+ return '已選 $count 項';
+ }
+
+ @override
+ String get confirmDelete => '確認刪除';
+
+ @override
+ String confirmDeleteItems(int count) {
+ return '確定要刪除選中的 $count 項收藏嗎?';
+ }
+
+ @override
+ String get emptyFavorites => '收藏夾是空的';
+
+ @override
+ String get addFavoriteHint => '瀏覽菜譜時點擊 🔖 即可收藏';
+
+ @override
+ String get sortMethod => '排序方式';
+
+ @override
+ String get sortNewest => '最新收藏';
+
+ @override
+ String get sortOldest => '最早收藏';
+
+ @override
+ String get sortNameAZ => '名稱 A-Z';
+
+ @override
+ String get sortNameZA => '名稱 Z-A';
+
+ @override
+ String get all => '全部';
+
+ @override
+ String get footprintsTitle => '我的足跡';
+
+ @override
+ String get footprintRemoved => '已移除足跡';
+
+ @override
+ String get personalizationTitle => '個人化設置';
+
+ @override
+ String get themeColor => '主題顏色';
+
+ @override
+ String get fontSetting => '字體大小';
+
+ @override
+ String get displayMode => '顯示模式';
+
+ @override
+ String get animationEffect => '動畫效果';
+
+ @override
+ String get languageSetting => '語言';
+
+ @override
+ String get dialogStyle => '對話框樣式';
+
+ @override
+ String get bubbleStyle => '消息氣泡樣式';
+
+ @override
+ String get bottomBarStyle => '底部欄樣式';
+
+ @override
+ String get cardSwipeDirection => '卡片滑動方向';
+
+ @override
+ String get floatingBarOpacity => '懸浮欄透明度';
+
+ @override
+ String get immersiveStatusBar => '沉浸狀態欄';
+
+ @override
+ String get unifiedStyle => '啟用統一樣式(跨平台一致)';
+
+ @override
+ String get sampleDialog => '示例對話框';
+
+ @override
+ String get sampleDialogContent => '這是使用當前對話框樣式的演示。';
+
+ @override
+ String get ok => '確定';
+
+ @override
+ String get showDialogSample => '顯示對話框樣式示例';
+
+ @override
+ String get selectThemeColor => '選擇主題顏色';
+
+ @override
+ String get animationIntensity => '動畫強度';
+
+ @override
+ String get selectLanguage => '選擇語言';
+
+ @override
+ String get restoreDefaults => '恢復預設';
+
+ @override
+ String get restoreDefaultsConfirm => '確定要恢復所有設置到預設值嗎?';
+
+ @override
+ String get swipeLeftRight => '左右滑動';
+
+ @override
+ String get swipeUpDown => '上下滑動';
+
+ @override
+ String get enableImmersive => '啟用沉浸狀態欄';
+
+ @override
+ String get preferenceTitle => '口味偏好';
+
+ @override
+ String get preferenceCategory => '偏好分類';
+
+ @override
+ String get preferenceCategoryHint => '選擇你喜歡的菜系分類';
+
+ @override
+ String get preferenceTag => '偏好標籤';
+
+ @override
+ String get preferenceTagHint => '選擇你感興趣的標籤';
+
+ @override
+ String get allergenBlock => '過敏原屏蔽';
+
+ @override
+ String get allergenBlockHint => '屏蔽含這些食材的菜譜';
+
+ @override
+ String get noCategoryData => '暫無分類數據';
+
+ @override
+ String get tagPlaceholder => '設置偏好後標籤將顯示在此處';
+
+ @override
+ String get resetPreference => '重置偏好';
+
+ @override
+ String get resetPreferenceConfirm => '確定要清除所有口味偏好設置嗎?';
+
+ @override
+ String get servingScalerTitle => '份量縮放工具';
+
+ @override
+ String get originalServing => '原始份量';
+
+ @override
+ String get targetServing => '目標份量';
+
+ @override
+ String get scaleRatio => '縮放比例';
+
+ @override
+ String get nutritionCenter => '營養中心';
+
+ @override
+ String get standardsViolation => '頁面規範攔截';
+
+ @override
+ String get pageBlocked => '頁面被攔截';
+
+ @override
+ String routeLabel(String route) {
+ return '路由: $route';
+ }
+
+ @override
+ String reasonLabel(String reason) {
+ return '原因: $reason';
+ }
+
+ @override
+ String get failedChecks => '未通過的檢查項:';
+
+ @override
+ String get unknown => '未知';
+
+ @override
+ String get unknownReason => '未知原因';
}
diff --git a/lib/src/l10n/app_zh_Hant.arb b/lib/src/l10n/app_zh_Hant.arb
index a8a6b7f..6bf751d 100644
--- a/lib/src/l10n/app_zh_Hant.arb
+++ b/lib/src/l10n/app_zh_Hant.arb
@@ -1,9 +1,11 @@
{
"@@locale": "zh_Hant",
+
"appTitle": "媽媽的廚房",
"welcomeMessage": "歡迎來到媽媽的廚房!",
"appDescription": "您最喜愛的美食配送應用",
"getStarted": "開始使用",
+
"themeSettings": "主題設置",
"darkMode": "深色模式",
"statusBarImmersive": "狀態欄沉浸",
@@ -14,8 +16,272 @@
"preview": "預覽",
"sampleText": "這是一段示例文本,用於預覽主題設置。",
"sampleButton": "示例按鈕",
+
"retry": "重試",
"noData": "暫無數據",
"cancel": "取消",
- "confirm": "確認"
+ "confirm": "確認",
+ "delete": "刪除",
+ "edit": "編輯",
+ "done": "完成",
+ "reset": "重置",
+ "loading": "載入中...",
+ "error": "錯誤",
+ "success": "成功",
+ "tip": "提示",
+ "backToHome": "返回主頁",
+ "reload": "重新載入",
+ "searchAgain": "重新搜尋",
+
+ "homeTitle": "老媽廚房",
+ "searchPlaceholder": "搜尋菜譜、食材...",
+ "loadingRecipes": "正在載入菜譜...",
+ "loadFailed": "載入失敗",
+ "noRecipes": "暫無菜譜",
+ "addRecipesHint": "快去添加一些美味菜譜吧",
+ "todayRecommend": "今日推薦",
+ "recipeCount": "{count} 道菜譜",
+ "@recipeCount": {
+ "placeholders": {
+ "count": { "type": "int" }
+ }
+ },
+ "categoryBrowse": "分類瀏覽",
+ "categoryMeat": "葷菜",
+ "categoryVeggie": "素菜",
+ "categoryNoodle": "麵食",
+ "categorySoup": "湯品",
+ "categoryDessert": "甜品",
+ "categoryDrink": "飲品",
+ "categorySnack": "小吃",
+ "invalidRecipeId": "菜譜ID無效",
+
+ "hotRanking": "熱門排行",
+ "periodToday": "今日",
+ "periodMonth": "本月",
+ "periodAll": "累計",
+ "sortByViews": "瀏覽量",
+ "sortByLikes": "點讚數",
+ "sortByRecommend": "推薦數",
+ "noRankingData": "暫無{period}排行數據",
+ "@noRankingData": {
+ "placeholders": {
+ "period": { "type": "String" }
+ }
+ },
+
+ "whatToEat": "今天吃什麼",
+ "loadingFilterOptions": "正在載入篩選選項…",
+ "firstLoadHint": "請稍候,首次載入可能需要幾秒鐘",
+ "pickingRecipe": "正在為您挑選菜譜…",
+ "matchingHint": "根據您的篩選條件匹配中",
+ "tapToStart": "點擊下方按鈕開始選擇",
+ "picking": "挑選中…",
+ "randomSelect": "隨機選擇",
+ "changeAnother": "換一個",
+ "currentFilter": "當前篩選: {summary}",
+ "@currentFilter": {
+ "placeholders": {
+ "summary": { "type": "String" }
+ }
+ },
+ "categoryFilter": "分類篩選",
+ "categoryLoading": "分類數據載入中…",
+ "tagFilter": "標籤篩選",
+ "tagLoading": "標籤數據載入中…",
+ "allergenFilter": "過敏原排除",
+
+ "feedbackTitle": "意見反饋",
+ "feedbackBug": "Bug 反饋",
+ "feedbackFeature": "功能建議",
+ "feedbackExperience": "體驗優化",
+ "feedbackOther": "其他",
+ "feedbackWelcome": "歡迎使用意見反饋",
+ "feedbackSelectType": "請選擇您要反饋的類型:",
+ "feedbackTypeSelected": "好的,您選擇了「{type}」,請詳細描述您的問題或建議:",
+ "@feedbackTypeSelected": {
+ "placeholders": {
+ "type": { "type": "String" }
+ }
+ },
+ "feedbackThanks": "感謝您的反饋!我們會盡快處理您的{type}。",
+ "@feedbackThanks": {
+ "placeholders": {
+ "type": { "type": "String" }
+ }
+ },
+ "feedbackMore": "還有其他問題嗎?可以繼續輸入,或直接返回",
+ "feedbackInputHint": "輸入您的反饋…",
+ "feedbackSelectFirst": "請先選擇反饋類型",
+ "feedbackOpinion": "意見",
+
+ "searchTitle": "搜尋",
+ "searchHistory": "搜尋歷史",
+ "clearHistory": "清空",
+ "hotSearch": "熱門搜尋",
+ "deleteRecord": "刪除記錄",
+ "deleteRecordConfirm": "確定要刪除\"{keyword}\"嗎?",
+ "@deleteRecordConfirm": {
+ "placeholders": {
+ "keyword": { "type": "String" }
+ }
+ },
+ "searching": "正在搜尋\"{keyword}\"...",
+ "@searching": {
+ "placeholders": {
+ "keyword": { "type": "String" }
+ }
+ },
+ "noResults": "未找到相關菜譜",
+ "tryOtherKeywords": "試試其他關鍵詞,如\"紅燒肉\"、\"糖醋排骨\"",
+ "resultCount": "找到 {count} 個結果 · \"{keyword}\"",
+ "@resultCount": {
+ "placeholders": {
+ "count": { "type": "int" },
+ "keyword": { "type": "String" }
+ }
+ },
+
+ "recipeDetail": "菜譜詳情",
+ "invalidRecipeIdError": "無效的菜譜ID: {id}",
+ "@invalidRecipeIdError": {
+ "placeholders": {
+ "id": { "type": "String" }
+ }
+ },
+ "recipeDataIncomplete": "菜譜數據不完整",
+ "loadFailedWith": "載入失敗: {error}",
+ "@loadFailedWith": {
+ "placeholders": {
+ "error": { "type": "String" }
+ }
+ },
+ "ingredients": "食材",
+ "steps": "做法",
+ "nutritionInfo": "營養信息",
+ "calories": "熱量",
+ "protein": "蛋白質",
+ "fat": "脂肪",
+ "carbs": "碳水",
+ "recommendSuccess": "推薦成功",
+ "recommend": "推薦",
+ "notesDeveloping": "烹飪筆記功能開發中",
+ "notes": "筆記",
+ "addedToShoppingList": "已添加到購物清單",
+ "shopping": "購物",
+
+ "profileTab": "我的",
+ "homeTab": "首頁",
+ "settingsTab": "設置",
+ "notLoggedIn": "未登入",
+ "tapToLogin": "點擊登入",
+ "personalization": "個人化",
+
+ "toolsTitle": "實用工具",
+ "cookingTimer": "烹飪計時",
+ "unitConverter": "用量換算",
+ "bmiCalculator": "BMI計算",
+ "servingScaler": "份量縮放",
+
+ "featureRecommend": "推薦",
+ "featureFollow": "關注",
+ "shoppingList": "購物清單",
+ "favorites": "收藏",
+
+ "latestMessages": "最新消息",
+ "systemNotification": "系統通知",
+ "newOrderReminder": "您有一條新的訂單提醒",
+ "marketingInfo": "營銷信息",
+ "newProductOnline": "新品上線,立即查看",
+
+ "favoritesTitle": "收藏",
+ "selectAll": "全選",
+ "deselectAll": "取消全選",
+ "selectedCount": "已選 {count} 項",
+ "@selectedCount": {
+ "placeholders": {
+ "count": { "type": "int" }
+ }
+ },
+ "confirmDelete": "確認刪除",
+ "confirmDeleteItems": "確定要刪除選中的 {count} 項收藏嗎?",
+ "@confirmDeleteItems": {
+ "placeholders": {
+ "count": { "type": "int" }
+ }
+ },
+ "emptyFavorites": "收藏夾是空的",
+ "addFavoriteHint": "瀏覽菜譜時點擊 🔖 即可收藏",
+ "sortMethod": "排序方式",
+ "sortNewest": "最新收藏",
+ "sortOldest": "最早收藏",
+ "sortNameAZ": "名稱 A-Z",
+ "sortNameZA": "名稱 Z-A",
+ "all": "全部",
+
+ "footprintsTitle": "我的足跡",
+ "footprintRemoved": "已移除足跡",
+
+ "personalizationTitle": "個人化設置",
+ "themeColor": "主題顏色",
+ "fontSetting": "字體大小",
+ "displayMode": "顯示模式",
+ "animationEffect": "動畫效果",
+ "languageSetting": "語言",
+ "dialogStyle": "對話框樣式",
+ "bubbleStyle": "消息氣泡樣式",
+ "bottomBarStyle": "底部欄樣式",
+ "cardSwipeDirection": "卡片滑動方向",
+ "floatingBarOpacity": "懸浮欄透明度",
+ "immersiveStatusBar": "沉浸狀態欄",
+ "unifiedStyle": "啟用統一樣式(跨平台一致)",
+ "sampleDialog": "示例對話框",
+ "sampleDialogContent": "這是使用當前對話框樣式的演示。",
+ "ok": "確定",
+ "showDialogSample": "顯示對話框樣式示例",
+ "selectThemeColor": "選擇主題顏色",
+ "animationIntensity": "動畫強度",
+ "selectLanguage": "選擇語言",
+ "restoreDefaults": "恢復預設",
+ "restoreDefaultsConfirm": "確定要恢復所有設置到預設值嗎?",
+ "swipeLeftRight": "左右滑動",
+ "swipeUpDown": "上下滑動",
+ "enableImmersive": "啟用沉浸狀態欄",
+
+ "preferenceTitle": "口味偏好",
+ "preferenceCategory": "偏好分類",
+ "preferenceCategoryHint": "選擇你喜歡的菜系分類",
+ "preferenceTag": "偏好標籤",
+ "preferenceTagHint": "選擇你感興趣的標籤",
+ "allergenBlock": "過敏原屏蔽",
+ "allergenBlockHint": "屏蔽含這些食材的菜譜",
+ "noCategoryData": "暫無分類數據",
+ "tagPlaceholder": "設置偏好後標籤將顯示在此處",
+ "resetPreference": "重置偏好",
+ "resetPreferenceConfirm": "確定要清除所有口味偏好設置嗎?",
+
+ "servingScalerTitle": "份量縮放工具",
+ "originalServing": "原始份量",
+ "targetServing": "目標份量",
+ "scaleRatio": "縮放比例",
+
+ "nutritionCenter": "營養中心",
+
+ "standardsViolation": "頁面規範攔截",
+ "pageBlocked": "頁面被攔截",
+ "routeLabel": "路由: {route}",
+ "@routeLabel": {
+ "placeholders": {
+ "route": { "type": "String" }
+ }
+ },
+ "reasonLabel": "原因: {reason}",
+ "@reasonLabel": {
+ "placeholders": {
+ "reason": { "type": "String" }
+ }
+ },
+ "failedChecks": "未通過的檢查項:",
+ "unknown": "未知",
+ "unknownReason": "未知原因"
}
diff --git a/lib/src/models/api/api_response.dart b/lib/src/models/api_response.dart
similarity index 100%
rename from lib/src/models/api/api_response.dart
rename to lib/src/models/api_response.dart
diff --git a/lib/src/models/note/cooking_note_model.dart b/lib/src/models/cooking_note_model.dart
similarity index 100%
rename from lib/src/models/note/cooking_note_model.dart
rename to lib/src/models/cooking_note_model.dart
diff --git a/lib/src/models/feed/feed_item_model.dart b/lib/src/models/feed_item_model.dart
similarity index 100%
rename from lib/src/models/feed/feed_item_model.dart
rename to lib/src/models/feed_item_model.dart
diff --git a/lib/src/models/nutrition/meal_record_model.dart b/lib/src/models/meal_record_model.dart
similarity index 100%
rename from lib/src/models/nutrition/meal_record_model.dart
rename to lib/src/models/meal_record_model.dart
diff --git a/lib/src/models/shopping/shopping_item_model.dart b/lib/src/models/shopping_item_model.dart
similarity index 100%
rename from lib/src/models/shopping/shopping_item_model.dart
rename to lib/src/models/shopping_item_model.dart
diff --git a/lib/src/models/tool_item_model.dart b/lib/src/models/tool_item_model.dart
new file mode 100644
index 0000000..5d2f0b4
--- /dev/null
+++ b/lib/src/models/tool_item_model.dart
@@ -0,0 +1,191 @@
+/*
+ * 文件: tool_item_model.dart
+ * 名称: 工具项数据模型
+ * 作用: 定义工具中心工具项的数据结构
+ * 更新: 2026-04-10 初始创建
+ */
+
+class ToolItem {
+ final String id;
+ final String name;
+ final String icon;
+ final bool needsNetwork;
+ final String category;
+ final String route;
+ final String? description;
+ final int usageCount;
+
+ const ToolItem({
+ required this.id,
+ required this.name,
+ required this.icon,
+ required this.needsNetwork,
+ required this.category,
+ required this.route,
+ this.description,
+ this.usageCount = 0,
+ });
+
+ factory ToolItem.fromJson(Map json) {
+ return ToolItem(
+ id: json['id'] as String,
+ name: json['name'] as String,
+ icon: json['icon'] as String,
+ needsNetwork: json['needsNetwork'] as bool,
+ category: json['category'] as String,
+ route: json['route'] as String,
+ description: json['description'] as String?,
+ usageCount: json['usageCount'] as int? ?? 0,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'name': name,
+ 'icon': icon,
+ 'needsNetwork': needsNetwork,
+ 'category': category,
+ 'route': route,
+ 'description': description,
+ 'usageCount': usageCount,
+ };
+ }
+
+ ToolItem copyWith({int? usageCount}) {
+ return ToolItem(
+ id: id,
+ name: name,
+ icon: icon,
+ needsNetwork: needsNetwork,
+ category: category,
+ route: route,
+ description: description,
+ usageCount: usageCount ?? this.usageCount,
+ );
+ }
+}
+
+class ToolCategory {
+ final String id;
+ final String name;
+ final String icon;
+
+ const ToolCategory({
+ required this.id,
+ required this.name,
+ required this.icon,
+ });
+
+ static const List all = [
+ ToolCategory(id: 'all', name: '全部', icon: '📋'),
+ ToolCategory(id: 'cooking', name: '烹饪助手', icon: '🍳'),
+ ToolCategory(id: 'health', name: '健康营养', icon: '💊'),
+ ToolCategory(id: 'data', name: '数据查询', icon: '📊'),
+ ToolCategory(id: 'planning', name: '规划管理', icon: '📅'),
+ ];
+}
+
+class ToolRegistry {
+ static const List defaultTools = [
+ ToolItem(
+ id: 'cooking_timer',
+ name: '烹饪计时器',
+ icon: '⏱️',
+ needsNetwork: false,
+ category: 'cooking',
+ route: '/tools/timer',
+ description: '多计时器烹饪助手',
+ ),
+ ToolItem(
+ id: 'serving_scaler',
+ name: '份量缩放',
+ icon: '📐',
+ needsNetwork: false,
+ category: 'cooking',
+ route: '/tools/scaler',
+ description: '按人数调整食材用量',
+ ),
+ ToolItem(
+ id: 'meal_time_recommend',
+ name: '用餐时段推荐',
+ icon: '🍽️',
+ needsNetwork: true,
+ category: 'cooking',
+ route: '/tools/meal-time',
+ description: '根据时间推荐早中晚餐',
+ ),
+ ToolItem(
+ id: 'bmi_calculator',
+ name: 'BMI计算器',
+ icon: '📊',
+ needsNetwork: false,
+ category: 'health',
+ route: '/tools/bmi',
+ description: '计算身体质量指数',
+ ),
+ ToolItem(
+ id: 'allergen_checker',
+ name: '过敏原检查',
+ icon: '⚠️',
+ needsNetwork: false,
+ category: 'health',
+ route: '/tools/allergen',
+ description: '检查食材过敏原信息',
+ ),
+ ToolItem(
+ id: 'nutrition_analysis',
+ name: '营养分析',
+ icon: '🥗',
+ needsNetwork: true,
+ category: 'health',
+ route: '/tools/nutrition',
+ description: '查看营养成分详情',
+ ),
+ ToolItem(
+ id: 'unit_converter',
+ name: '单位换算',
+ icon: '🔢',
+ needsNetwork: false,
+ category: 'data',
+ route: '/tools/converter',
+ description: '重量/容量单位换算',
+ ),
+ ToolItem(
+ id: 'ingredient_detail',
+ name: '食材详情',
+ icon: '🥕',
+ needsNetwork: true,
+ category: 'data',
+ route: '/tools/ingredient',
+ description: '查询食材营养与选购',
+ ),
+ ToolItem(
+ id: 'hot_ranking',
+ name: '热门排行',
+ icon: '🔥',
+ needsNetwork: true,
+ category: 'data',
+ route: '/hot',
+ description: '查看热门菜谱排行',
+ ),
+ ToolItem(
+ id: 'view_stats',
+ name: '浏览统计',
+ icon: '📈',
+ needsNetwork: true,
+ category: 'data',
+ route: '/tools/stats',
+ description: '查看平台浏览数据',
+ ),
+ ToolItem(
+ id: 'meal_planner',
+ name: '每周菜单规划',
+ icon: '📅',
+ needsNetwork: false,
+ category: 'planning',
+ route: '/tools/planner',
+ description: '规划一周饮食菜单',
+ ),
+ ];
+}
diff --git a/lib/src/models/nutrition/user_goal_model.dart b/lib/src/models/user_goal_model.dart
similarity index 100%
rename from lib/src/models/nutrition/user_goal_model.dart
rename to lib/src/models/user_goal_model.dart
diff --git a/lib/src/models/user/user_preference_model.dart b/lib/src/models/user_preference_model.dart
similarity index 97%
rename from lib/src/models/user/user_preference_model.dart
rename to lib/src/models/user_preference_model.dart
index f9ddc1f..82fa98f 100644
--- a/lib/src/models/user/user_preference_model.dart
+++ b/lib/src/models/user_preference_model.dart
@@ -70,13 +70,15 @@ class UserPreferenceModel {
class PreferenceCategory {
final int id;
final String name;
+ final String? icon;
- const PreferenceCategory({required this.id, required this.name});
+ const PreferenceCategory({required this.id, required this.name, this.icon});
factory PreferenceCategory.fromJson(Map json) {
return PreferenceCategory(
id: json['id'] as int? ?? json['category_id'] as int? ?? 0,
name: json['name'] as String? ?? json['category_name'] as String? ?? '',
+ icon: json['icon'] as String? ?? json['category_icon'] as String?,
);
}
}
diff --git a/lib/src/pages/debug/example_page.dart b/lib/src/pages/debug/example_page.dart
deleted file mode 100644
index 7daab50..0000000
--- a/lib/src/pages/debug/example_page.dart
+++ /dev/null
@@ -1,211 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:get/get.dart';
-import 'package:mom_kitchen/src/services/ui/theme_service.dart';
-import 'package:mom_kitchen/src/l10n/app_localizations.dart';
-import 'package:mom_kitchen/src/widgets/states/skeleton_loader.dart';
-
-class ExamplePage extends StatefulWidget {
- const ExamplePage({super.key});
-
- @override
- State createState() => _ExamplePageState();
-}
-
-class _ExamplePageState extends State {
- final _themeService = Get.find();
- bool _isLoading = true;
-
- @override
- void initState() {
- super.initState();
- Future.delayed(const Duration(seconds: 2), () {
- setState(() {
- _isLoading = false;
- });
- });
- }
-
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
-
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(middle: Text(l10n.appTitle)),
- child: Obx(
- () => Container(
- color: _themeService.backgroundColor.value,
- child: ListView(
- padding: const EdgeInsets.all(16.0),
- children: [
- Text(
- 'Example Page',
- style: TextStyle(
- color: _themeService.textColor.value,
- fontSize: _themeService.fontSize.value + 8,
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 20),
-
- Container(
- padding: const EdgeInsets.all(16.0),
- decoration: BoxDecoration(
- color: _themeService.secondaryColor.value.withValues(alpha: 0.1),
- borderRadius: BorderRadius.circular(8),
- border: Border.all(
- color: _themeService.secondaryColor.value,
- width: 1,
- ),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Theme Values',
- style: TextStyle(
- color: _themeService.textColor.value,
- fontSize: _themeService.fontSize.value + 4,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 16),
- _buildThemeValueRow(
- 'Dark Mode',
- _themeService.isDarkMode.value.toString(),
- _themeService.textColor.value,
- ),
- _buildThemeValueRow(
- 'Primary Color',
- _themeService.primaryColor.value.toString(),
- _themeService.textColor.value,
- ),
- _buildThemeValueRow(
- 'Secondary Color',
- _themeService.secondaryColor.value.toString(),
- _themeService.textColor.value,
- ),
- _buildThemeValueRow(
- 'Font Size',
- _themeService.fontSize.value.toString(),
- _themeService.textColor.value,
- ),
- _buildThemeValueRow(
- 'Status Bar Immersive',
- _themeService.isStatusBarImmersive.value.toString(),
- _themeService.textColor.value,
- ),
- _buildThemeValueRow(
- 'Text Color',
- _themeService.textColor.value.toString(),
- _themeService.textColor.value,
- ),
- _buildThemeValueRow(
- 'Background Color',
- _themeService.backgroundColor.value.toString(),
- _themeService.textColor.value,
- ),
- _buildThemeValueRow(
- 'Animation Intensity',
- _themeService.animationIntensity.value.toString(),
- _themeService.textColor.value,
- ),
- ],
- ),
- ),
- const SizedBox(height: 20),
-
- Text(
- 'Skeleton Loader Example',
- style: TextStyle(
- color: _themeService.textColor.value,
- fontSize: _themeService.fontSize.value + 4,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 16),
- _isLoading
- ? const SkeletonContainer(
- height: 80,
- )
- : Container(
- padding: const EdgeInsets.all(16.0),
- decoration: BoxDecoration(
- color: _themeService.secondaryColor.value.withValues(alpha: 0.1),
- borderRadius: BorderRadius.circular(8),
- border: Border.all(
- color: _themeService.secondaryColor.value,
- width: 1,
- ),
- ),
- child: Text(
- 'This content is loaded after skeleton disappears',
- style: TextStyle(
- color: _themeService.textColor.value,
- fontSize: _themeService.fontSize.value,
- ),
- ),
- ),
- const SizedBox(height: 20),
-
- Text(
- 'Animation Example',
- style: TextStyle(
- color: _themeService.textColor.value,
- fontSize: _themeService.fontSize.value + 4,
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 16),
- GestureDetector(
- onTap: () {},
- child: Container(
- padding: const EdgeInsets.all(16.0),
- decoration: BoxDecoration(
- color: _themeService.primaryColor.value,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Text(
- 'Tap for Animation',
- style: TextStyle(
- color: _themeService.textColor.value,
- fontSize: _themeService.fontSize.value,
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-
- Widget _buildThemeValueRow(String label, String value, Color textColor) {
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 4.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- label,
- style: TextStyle(
- color: textColor,
- fontSize: _themeService.fontSize.value,
- ),
- ),
- Text(
- value,
- style: TextStyle(
- color: textColor,
- fontSize: _themeService.fontSize.value,
- fontWeight: FontWeight.bold,
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/src/pages/debug/fl_chart_test_page.dart b/lib/src/pages/debug/fl_chart_test_page.dart
deleted file mode 100644
index 54e377f..0000000
--- a/lib/src/pages/debug/fl_chart_test_page.dart
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * 文件: fl_chart_test_page.dart
- * 名称: fl_chart 测试页
- * 作用: 验证本地 fl_chart 包在项目中可正常渲染
- * 更新: 2026-04-09 初始创建,包含折线图/柱状图/饼图三种图表
- */
-
-import 'package:fl_chart/fl_chart.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:mom_kitchen/src/config/design_tokens.dart';
-
-class FlChartTestPage extends StatelessWidget {
- const FlChartTestPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
- final textColor = isDark ? DarkDesignTokens.text1 : DesignTokens.text1;
- final subColor = isDark ? DarkDesignTokens.text2 : DesignTokens.text2;
- final bgColor = isDark ? DarkDesignTokens.background : DesignTokens.background;
- final cardColor = isDark ? DarkDesignTokens.card : DesignTokens.card;
-
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(
- middle: Text(
- '📊 fl_chart 测试',
- style: TextStyle(color: textColor, fontSize: DesignTokens.fontLg),
- ),
- backgroundColor: bgColor.withValues(alpha: 0.9),
- border: null,
- ),
- child: SafeArea(
- child: ListView(
- padding: DesignTokens.paddingLg,
- children: [
- _SectionLabel('📈 折线图 (LineChart)', subColor),
- const SizedBox(height: DesignTokens.space3),
- _GlassCard(
- color: cardColor,
- child: SizedBox(
- height: 200,
- child: LineChart(
- LineChartData(
- gridData: FlGridData(show: true, drawVerticalLine: false),
- titlesData: FlTitlesData(
- leftTitles: AxisTitles(
- sideTitles: SideTitles(showTitles: true, reservedSize: 36),
- ),
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 28,
- getTitlesWidget: (v, _) => _AxisLabel(
- ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
- v,
- subColor,
- ),
- ),
- ),
- topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
- rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
- ),
- borderData: FlBorderData(show: false),
- lineBarsData: [
- LineChartBarData(
- spots: const [
- FlSpot(0, 1200),
- FlSpot(1, 1580),
- FlSpot(2, 1350),
- FlSpot(3, 1800),
- FlSpot(4, 1650),
- FlSpot(5, 2100),
- FlSpot(6, 1900),
- ],
- isCurved: true,
- color: DesignTokens.primary,
- barWidth: 3,
- dotData: const FlDotData(show: true),
- belowBarData: BarAreaData(
- show: true,
- color: DesignTokens.primaryLight,
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- const SizedBox(height: DesignTokens.space6),
-
- _SectionLabel('📊 柱状图 (BarChart)', subColor),
- const SizedBox(height: DesignTokens.space3),
- _GlassCard(
- color: cardColor,
- child: SizedBox(
- height: 200,
- child: BarChart(
- BarChartData(
- gridData: FlGridData(show: true, drawVerticalLine: false),
- titlesData: FlTitlesData(
- leftTitles: AxisTitles(
- sideTitles: SideTitles(showTitles: true, reservedSize: 36),
- ),
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 28,
- getTitlesWidget: (v, _) => _AxisLabel(
- ['碳水', '蛋白', '脂肪', '纤维'],
- v,
- subColor,
- ),
- ),
- ),
- topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
- rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
- ),
- borderData: FlBorderData(show: false),
- barGroups: [
- _makeBarGroup(0, 55, 30, 15, DesignTokens.primary, DesignTokens.secondary, DesignTokens.red),
- _makeBarGroup(1, 40, 45, 15, DesignTokens.primary, DesignTokens.secondary, DesignTokens.red),
- _makeBarGroup(2, 25, 20, 55, DesignTokens.primary, DesignTokens.secondary, DesignTokens.red),
- _makeBarGroup(3, 60, 10, 30, DesignTokens.primary, DesignTokens.secondary, DesignTokens.red),
- ],
- ),
- ),
- ),
- ),
- const SizedBox(height: DesignTokens.space6),
-
- _SectionLabel('🥧 饼图 (PieChart)', subColor),
- const SizedBox(height: DesignTokens.space3),
- _GlassCard(
- color: cardColor,
- child: SizedBox(
- height: 220,
- child: Row(
- children: [
- Expanded(
- child: PieChart(
- PieChartData(
- sectionsSpace: 2,
- centerSpaceRadius: 40,
- sections: [
- PieChartSectionData(
- value: 55,
- color: DesignTokens.primary,
- title: '碳水\n55%',
- titleStyle: TextStyle(
- color: CupertinoColors.white,
- fontSize: DesignTokens.fontSm,
- fontWeight: FontWeight.w600,
- ),
- ),
- PieChartSectionData(
- value: 25,
- color: DesignTokens.secondary,
- title: '蛋白\n25%',
- titleStyle: TextStyle(
- color: CupertinoColors.white,
- fontSize: DesignTokens.fontSm,
- fontWeight: FontWeight.w600,
- ),
- ),
- PieChartSectionData(
- value: 20,
- color: DesignTokens.red,
- title: '脂肪\n20%',
- titleStyle: TextStyle(
- color: CupertinoColors.white,
- fontSize: DesignTokens.fontSm,
- fontWeight: FontWeight.w600,
- ),
- ),
- ],
- ),
- ),
- ),
- const SizedBox(width: DesignTokens.space4),
- Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _LegendDot(DesignTokens.primary, '碳水 55%', subColor),
- const SizedBox(height: DesignTokens.space3),
- _LegendDot(DesignTokens.secondary, '蛋白 25%', subColor),
- const SizedBox(height: DesignTokens.space3),
- _LegendDot(DesignTokens.red, '脂肪 20%', subColor),
- ],
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: DesignTokens.space6),
-
- _GlassCard(
- color: DesignTokens.green.withValues(alpha: 0.12),
- child: Row(
- children: [
- Icon(CupertinoIcons.checkmark_circle_fill, color: DesignTokens.green, size: 22),
- const SizedBox(width: DesignTokens.space3),
- Expanded(
- child: Text(
- 'fl_chart 1.2.0-ohos.1 运行正常 ✅\n折线图 / 柱状图 / 饼图 均可渲染',
- style: TextStyle(color: DesignTokens.green, fontSize: DesignTokens.fontMd),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: DesignTokens.space7),
- ],
- ),
- ),
- );
- }
-
- BarChartGroupData _makeBarGroup(
- int x,
- double v1,
- double v2,
- double v3,
- Color c1,
- Color c2,
- Color c3,
- ) {
- return BarChartGroupData(
- x: x,
- barRods: [
- BarChartRodData(toY: v1, color: c1, width: 8, borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(4))),
- BarChartRodData(toY: v2, color: c2, width: 8, borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(4))),
- BarChartRodData(toY: v3, color: c3, width: 8, borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(4))),
- ],
- );
- }
-}
-
-class _SectionLabel extends StatelessWidget {
- final String text;
- final Color color;
- const _SectionLabel(this.text, this.color);
-
- @override
- Widget build(BuildContext context) {
- return Text(text, style: TextStyle(color: color, fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w600));
- }
-}
-
-class _GlassCard extends StatelessWidget {
- final Widget child;
- final Color color;
- const _GlassCard({required this.child, required this.color});
-
- @override
- Widget build(BuildContext context) {
- return Container(
- padding: DesignTokens.paddingLg,
- decoration: BoxDecoration(
- color: color,
- borderRadius: DesignTokens.borderRadiusLg,
- boxShadow: DesignTokens.shadowsSm,
- ),
- child: child,
- );
- }
-}
-
-class _AxisLabel extends StatelessWidget {
- final List labels;
- final double value;
- final Color color;
- const _AxisLabel(this.labels, this.value, this.color);
-
- @override
- Widget build(BuildContext context) {
- final idx = value.toInt();
- if (idx < 0 || idx >= labels.length) return const SizedBox();
- return Padding(
- padding: const EdgeInsets.only(top: 6),
- child: Text(labels[idx], style: TextStyle(color: color, fontSize: DesignTokens.fontXs)),
- );
- }
-}
-
-class _LegendDot extends StatelessWidget {
- final Color color;
- final String label;
- final Color textColor;
- const _LegendDot(this.color, this.label, this.textColor);
-
- @override
- Widget build(BuildContext context) {
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
- const SizedBox(width: DesignTokens.space2),
- Text(label, style: TextStyle(color: textColor, fontSize: DesignTokens.fontSm)),
- ],
- );
- }
-}
diff --git a/lib/src/pages/debug/standards_violation_page.dart b/lib/src/pages/debug/standards_violation_page.dart
deleted file mode 100644
index d47ce13..0000000
--- a/lib/src/pages/debug/standards_violation_page.dart
+++ /dev/null
@@ -1,108 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:get/get.dart';
-import 'package:mom_kitchen/src/services/core/app_service.dart';
-import 'package:mom_kitchen/src/standards/page_validator.dart';
-
-class StandardsViolationPage extends StatelessWidget {
- const StandardsViolationPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- final args = Get.arguments as Map? ?? {};
- final route = args['route'] as String? ?? '未知';
- final reason = args['reason'] as String? ?? '未知原因';
- final failedChecks = args['failedChecks'] as List? ?? [];
-
- final themeService = AppService.instance.theme;
-
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(
- middle: const Text('页面规范拦截'),
- backgroundColor: themeService.backgroundColor.value,
- ),
- backgroundColor: themeService.backgroundColor.value,
- child: SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Icon(
- CupertinoIcons.exclamationmark_triangle_fill,
- color: CupertinoColors.systemOrange,
- size: 48,
- ),
- const SizedBox(height: 16),
- Text(
- '页面被拦截',
- style: TextStyle(
- fontSize: themeService.fontSize.value + 6,
- fontWeight: FontWeight.bold,
- color: themeService.textColor.value,
- ),
- ),
- const SizedBox(height: 8),
- Text(
- '路由: $route',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value.withValues(alpha: 0.7),
- ),
- ),
- const SizedBox(height: 4),
- Text(
- '原因: $reason',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value.withValues(alpha: 0.7),
- ),
- ),
- if (failedChecks.isNotEmpty) ...[
- const SizedBox(height: 24),
- Text(
- '未通过的检查项:',
- style: TextStyle(
- fontSize: themeService.fontSize.value + 2,
- fontWeight: FontWeight.w600,
- color: themeService.textColor.value,
- ),
- ),
- const SizedBox(height: 12),
- ...failedChecks.map(
- (check) => Padding(
- padding: const EdgeInsets.only(bottom: 8),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Icon(
- CupertinoIcons.xmark_circle_fill,
- color: CupertinoColors.systemRed,
- size: 16,
- ),
- const SizedBox(width: 8),
- Expanded(
- child: Text(
- '${check.check.label}: ${check.message ?? ''}',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- const Spacer(),
- CupertinoButton.filled(
- onPressed: () => Get.offAllNamed('/main'),
- child: const Text('返回主页'),
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/src/pages/debug/test_minimal_page.dart b/lib/src/pages/debug/test_minimal_page.dart
deleted file mode 100644
index bbddd61..0000000
--- a/lib/src/pages/debug/test_minimal_page.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-import 'package:flutter/cupertino.dart';
-
-class TestMinimalPage extends StatelessWidget {
- const TestMinimalPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const CupertinoPageScaffold(
- child: Center(
- child: Text('Test OK!'),
- ),
- );
- }
-}
diff --git a/lib/src/pages/discover/discover_page.dart b/lib/src/pages/discover/discover_page.dart
index 64d4404..ec19ee7 100644
--- a/lib/src/pages/discover/discover_page.dart
+++ b/lib/src/pages/discover/discover_page.dart
@@ -2,16 +2,18 @@
* 文件: discover_page.dart
* 名称: 发现页面
* 作用: iOS 26 风格的发现页面,整合热门排行+今天吃什么+搜索
- * 更新: 2026-04-09 集成HotController获取真实热门数据
+ * 更新: 2026-04-10 购物清单入口添加 Badge 显示数量
*/
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
+import 'package:badges/badges.dart' as badges;
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/routes/app_routes.dart';
+import 'package:mom_kitchen/src/config/app_routes.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_search_bar.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_segmented_control.dart';
import 'package:mom_kitchen/src/controllers/feed/hot_controller.dart';
+import 'package:mom_kitchen/src/controllers/shopping_list_controller.dart';
import 'package:mom_kitchen/src/repositories/hot_repository.dart' as repo;
class DiscoverPage extends StatefulWidget {
@@ -28,9 +30,7 @@ class _DiscoverPageState extends State {
@override
void initState() {
super.initState();
- _hotController = Get.isRegistered()
- ? Get.find()
- : Get.put(HotController());
+ _hotController = Get.find();
}
@override
@@ -122,41 +122,48 @@ class _DiscoverPageState extends State {
}
Widget _buildQuickActions(bool isDark) {
- return Row(
- children: [
- _buildQuickActionItem(
- isDark: isDark,
- emoji: '🥗',
- label: '营养中心',
- color: DesignTokens.green,
- onTap: () => Get.toNamed('/nutrition'),
- ),
- const SizedBox(width: DesignTokens.space2),
- _buildQuickActionItem(
- isDark: isDark,
- emoji: '🛒',
- label: '购物清单',
- color: DesignTokens.secondary,
- onTap: () => Get.toNamed('/shopping-list'),
- ),
- const SizedBox(width: DesignTokens.space2),
- _buildQuickActionItem(
- isDark: isDark,
- emoji: '📊',
- label: '周报',
- color: DesignTokens.primary,
- onTap: () => Get.toNamed('/nutrition-report'),
- ),
- const SizedBox(width: DesignTokens.space2),
- _buildQuickActionItem(
- isDark: isDark,
- emoji: '🎯',
- label: '目标',
- color: DesignTokens.red,
- onTap: () => Get.toNamed('/goal-setting'),
- ),
- ],
- );
+ final shoppingController = Get.find();
+
+ return Obx(() {
+ final shoppingCount = shoppingController.uncheckedCount;
+
+ return Row(
+ children: [
+ _buildQuickActionItem(
+ isDark: isDark,
+ emoji: '🥗',
+ label: '营养中心',
+ color: DesignTokens.green,
+ onTap: () => Get.toNamed('/nutrition'),
+ ),
+ const SizedBox(width: DesignTokens.space2),
+ _buildQuickActionItem(
+ isDark: isDark,
+ emoji: '🛒',
+ label: '购物清单',
+ color: DesignTokens.secondary,
+ onTap: () => Get.toNamed('/shopping-list'),
+ badgeCount: shoppingCount,
+ ),
+ const SizedBox(width: DesignTokens.space2),
+ _buildQuickActionItem(
+ isDark: isDark,
+ emoji: '📊',
+ label: '周报',
+ color: DesignTokens.primary,
+ onTap: () => Get.toNamed('/nutrition-report'),
+ ),
+ const SizedBox(width: DesignTokens.space2),
+ _buildQuickActionItem(
+ isDark: isDark,
+ emoji: '🎯',
+ label: '目标',
+ color: DesignTokens.red,
+ onTap: () => Get.toNamed('/goal-setting'),
+ ),
+ ],
+ );
+ });
}
Widget _buildQuickActionItem({
@@ -165,6 +172,7 @@ class _DiscoverPageState extends State {
required String label,
required Color color,
required VoidCallback onTap,
+ int badgeCount = 0,
}) {
return Expanded(
child: GestureDetector(
@@ -177,7 +185,27 @@ class _DiscoverPageState extends State {
),
child: Column(
children: [
- Text(emoji, style: const TextStyle(fontSize: 24)),
+ badges.Badge(
+ showBadge: badgeCount > 0,
+ position: badges.BadgePosition.topEnd(top: -6, end: -10),
+ badgeStyle: badges.BadgeStyle(
+ badgeColor: isDark ? DarkDesignTokens.red : DesignTokens.red,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5,
+ vertical: 2,
+ ),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ badgeContent: Text(
+ badgeCount > 99 ? '99+' : '$badgeCount',
+ style: const TextStyle(
+ color: CupertinoColors.white,
+ fontSize: 9,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ child: Text(emoji, style: const TextStyle(fontSize: 24)),
+ ),
const SizedBox(height: DesignTokens.space1),
Text(
label,
diff --git a/lib/src/pages/hot/hot_page.dart b/lib/src/pages/discover/hot_page.dart
similarity index 98%
rename from lib/src/pages/hot/hot_page.dart
rename to lib/src/pages/discover/hot_page.dart
index 7cea349..1b17cd7 100644
--- a/lib/src/pages/hot/hot_page.dart
+++ b/lib/src/pages/discover/hot_page.dart
@@ -9,7 +9,7 @@ import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/controllers/feed/hot_controller.dart';
-import 'package:mom_kitchen/src/pages/recipe/recipe_detail_page.dart';
+import 'package:mom_kitchen/src/pages/home/recipe_detail_page.dart';
import 'package:mom_kitchen/src/repositories/hot_repository.dart' as repo;
import 'package:mom_kitchen/src/widgets/glass/glass_container.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_segmented_control.dart';
@@ -19,7 +19,7 @@ class HotPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final controller = Get.put(HotController());
+ final controller = Get.find();
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return CupertinoPageScaffold(
diff --git a/lib/src/pages/what_to_eat/what_to_eat_page.dart b/lib/src/pages/discover/what_to_eat_page.dart
similarity index 75%
rename from lib/src/pages/what_to_eat/what_to_eat_page.dart
rename to lib/src/pages/discover/what_to_eat_page.dart
index a800e9d..9159c95 100644
--- a/lib/src/pages/what_to_eat/what_to_eat_page.dart
+++ b/lib/src/pages/discover/what_to_eat_page.dart
@@ -4,20 +4,21 @@
* 作用: iOS 26 风格的今天吃什么页面,支持动态筛选(分类/标签/过敏原)
* 更新: 2026-04-10 重写:使用CategoryModel+TagModel+filter_apply实现真实动态筛选
* 更新: 2026-04-10 修复:动态筛选卡死闪退+添加加载动画+超时保护+初始化安全
+ * 更新: 2026-04-10 优化:添加空结果提示+优化UI显示
+ * 更新: 2026-04-11 修复:点击结果页面分裂问题,改用命名路由
*/
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/controllers/discovery/what_to_eat_controller.dart';
-import 'package:mom_kitchen/src/pages/recipe/recipe_detail_page.dart';
+import 'package:mom_kitchen/src/controllers/what_to_eat_controller.dart';
class WhatToEatPage extends StatelessWidget {
const WhatToEatPage({super.key});
@override
Widget build(BuildContext context) {
- final controller = Get.put(WhatToEatController());
+ final controller = Get.find();
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return CupertinoPageScaffold(
@@ -231,7 +232,12 @@ class WhatToEatPage extends StatelessWidget {
return GestureDetector(
onTap: () {
- Get.to(() => RecipeDetailPage(recipeId: '${recipe.id}'));
+ final recipeId = recipe.id;
+ if (recipeId <= 0) {
+ Get.snackbar('提示', '菜谱ID无效', snackPosition: SnackPosition.BOTTOM);
+ return;
+ }
+ Get.toNamed('/recipe-detail', arguments: '$recipeId');
},
child: Container(
width: double.infinity,
@@ -247,6 +253,7 @@ class WhatToEatPage extends StatelessWidget {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
@@ -272,6 +279,7 @@ class WhatToEatPage extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
children: [
Text(
recipe.title,
@@ -296,6 +304,8 @@ class WhatToEatPage extends StatelessWidget {
? DarkDesignTokens.primary
: DesignTokens.primary,
),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
),
],
],
@@ -325,7 +335,7 @@ class WhatToEatPage extends StatelessWidget {
Wrap(
spacing: 6,
runSpacing: 4,
- children: recipe.tags.take(5).map((tag) {
+ children: recipe.tags.take(3).map((tag) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
@@ -347,6 +357,8 @@ class WhatToEatPage extends StatelessWidget {
? DarkDesignTokens.primary
: DesignTokens.primary,
),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
),
);
}).toList(),
@@ -492,14 +504,22 @@ class WhatToEatPage extends StatelessWidget {
Widget _buildCategoryFilter(WhatToEatController controller, bool isDark) {
return Obx(() {
- if (controller.categories.isEmpty) {
+ if (controller.isOptionsLoading.value && controller.categories.isEmpty) {
return _buildFilterLoading('📂 分类筛选', '分类数据加载中…', isDark);
}
+ if (controller.categories.isEmpty) {
+ return _buildFilterEmpty('📂 分类筛选', '暂无分类数据', isDark);
+ }
+
final topCategories = controller.categories
.where((c) => c.parentId == null || c.parentId == 0)
.toList();
+ if (topCategories.isEmpty) {
+ return _buildFilterEmpty('📂 分类筛选', '暂无顶级分类', isDark);
+ }
+
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -517,43 +537,80 @@ class WhatToEatPage extends StatelessWidget {
runSpacing: DesignTokens.space2,
children: topCategories.map((category) {
final isSelected = controller.isCategorySelected(category.id);
- return GestureDetector(
- onTap: () => controller.toggleCategory(category),
- child: Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 14,
- vertical: 8,
- ),
- decoration: BoxDecoration(
- color: isSelected
- ? (isDark
- ? DarkDesignTokens.primary
- : DesignTokens.primary)
- : (isDark ? DarkDesignTokens.card : DesignTokens.card),
- borderRadius: BorderRadius.circular(20),
- border: Border.all(
- color: isSelected
- ? (isDark
- ? DarkDesignTokens.primary
- : DesignTokens.primary)
- : (isDark
- ? DarkDesignTokens.text3
- : DesignTokens.text3)
- .withValues(alpha: 0.15),
+ final hasChildren = controller.hasSubCategories(category.id);
+ final isExpanded = controller.isCategoryExpanded(category.id);
+ return Column(
+ children: [
+ GestureDetector(
+ onTap: () {
+ if (hasChildren) {
+ controller.toggleExpandCategory(category);
+ } else {
+ controller.toggleCategory(category);
+ }
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 14,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ color: isSelected
+ ? (isDark
+ ? DarkDesignTokens.primary
+ : DesignTokens.primary)
+ : (isDark
+ ? DarkDesignTokens.card
+ : DesignTokens.card),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: isSelected
+ ? (isDark
+ ? DarkDesignTokens.primary
+ : DesignTokens.primary)
+ : (isDark
+ ? DarkDesignTokens.text3
+ : DesignTokens.text3)
+ .withValues(alpha: 0.15),
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ '${category.displayIcon} ${category.name}',
+ style: TextStyle(
+ fontSize: DesignTokens.fontSm,
+ color: isSelected
+ ? CupertinoColors.white
+ : (isDark
+ ? DarkDesignTokens.text1
+ : DesignTokens.text1),
+ ),
+ ),
+ if (hasChildren) ...[
+ const SizedBox(width: 4),
+ Icon(
+ isExpanded
+ ? CupertinoIcons.chevron_up
+ : CupertinoIcons.chevron_down,
+ size: 12,
+ color: isSelected
+ ? CupertinoColors.white
+ : (isDark
+ ? DarkDesignTokens.text2
+ : DesignTokens.text2),
+ ),
+ ],
+ ],
+ ),
),
),
- child: Text(
- '${category.displayIcon} ${category.name}',
- style: TextStyle(
- fontSize: DesignTokens.fontSm,
- color: isSelected
- ? CupertinoColors.white
- : (isDark
- ? DarkDesignTokens.text1
- : DesignTokens.text1),
- ),
- ),
- ),
+ if (isExpanded) ...[
+ const SizedBox(height: DesignTokens.space2),
+ _buildSubCategories(controller, category.id, isDark),
+ ],
+ ],
);
}).toList(),
),
@@ -562,12 +619,79 @@ class WhatToEatPage extends StatelessWidget {
});
}
+ Widget _buildSubCategories(
+ WhatToEatController controller,
+ int parentId,
+ bool isDark,
+ ) {
+ final subCategories = controller.getSubCategories(parentId);
+ if (subCategories.isEmpty) return const SizedBox();
+
+ return Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(DesignTokens.space3),
+ margin: const EdgeInsets.only(top: DesignTokens.space2),
+ decoration: BoxDecoration(
+ color: (isDark ? DarkDesignTokens.card : DesignTokens.card).withValues(
+ alpha: 0.5,
+ ),
+ borderRadius: DesignTokens.borderRadiusMd,
+ border: Border.all(
+ color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3)
+ .withValues(alpha: 0.1),
+ ),
+ ),
+ child: Wrap(
+ spacing: DesignTokens.space2,
+ runSpacing: DesignTokens.space2,
+ children: subCategories.map((subCat) {
+ final isSelected = controller.isCategorySelected(subCat.id);
+ return GestureDetector(
+ onTap: () => controller.toggleCategory(subCat),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: isSelected
+ ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary)
+ : (isDark
+ ? DarkDesignTokens.background
+ : DesignTokens.background),
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(
+ color: isSelected
+ ? (isDark
+ ? DarkDesignTokens.primary
+ : DesignTokens.primary)
+ : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3)
+ .withValues(alpha: 0.2),
+ ),
+ ),
+ child: Text(
+ '${subCat.displayIcon} ${subCat.name}',
+ style: TextStyle(
+ fontSize: DesignTokens.fontXs,
+ color: isSelected
+ ? CupertinoColors.white
+ : (isDark ? DarkDesignTokens.text1 : DesignTokens.text1),
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }
+
Widget _buildTagFilter(WhatToEatController controller, bool isDark) {
return Obx(() {
- if (controller.tags.isEmpty) {
+ if (controller.isOptionsLoading.value && controller.tags.isEmpty) {
return _buildFilterLoading('🏷️ 标签筛选', '标签数据加载中…', isDark);
}
+ if (controller.tags.isEmpty) {
+ return _buildFilterEmpty('🏷️ 标签筛选', '暂无标签数据', isDark);
+ }
+
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -583,7 +707,7 @@ class WhatToEatPage extends StatelessWidget {
Wrap(
spacing: DesignTokens.space2,
runSpacing: DesignTokens.space2,
- children: controller.tags.map((tag) {
+ children: controller.tags.take(30).map((tag) {
final isSelected = controller.isTagSelected(tag.id);
return GestureDetector(
onTap: () => controller.toggleTag(tag),
@@ -630,6 +754,35 @@ class WhatToEatPage extends StatelessWidget {
});
}
+ Widget _buildFilterEmpty(String title, String hint, bool isDark) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ fontWeight: FontWeight.w600,
+ color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
+ ),
+ ),
+ const SizedBox(height: DesignTokens.space3),
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: DesignTokens.space3),
+ child: Text(
+ hint,
+ style: TextStyle(
+ fontSize: DesignTokens.fontSm,
+ color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
Widget _buildFilterLoading(String title, String hint, bool isDark) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
diff --git a/lib/src/pages/home/home_card_carousel.dart b/lib/src/pages/home/home_card_carousel.dart
index 65b3c8e..f08e877 100644
--- a/lib/src/pages/home/home_card_carousel.dart
+++ b/lib/src/pages/home/home_card_carousel.dart
@@ -3,16 +3,16 @@
* 说明: 首页卡片式横向滚动组件。支持左右滑动、过渡动画和卡片布局。
* 作用: 提供美观的卡片信息流浏览体验。
* 作者: 前端工程师
- * 更新时间: 2026-04-09
- * 上次更新: 从ProductModel切换到RecipeModel,接入真实API数据
+ * 更新时间: 2026-04-10
+ * 上次更新: 添加 Badge 显示"新"/"热"标签
*/
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/home/home_controller.dart';
-import 'package:mom_kitchen/src/controllers/shopping/shopping_list_controller.dart';
+import 'package:mom_kitchen/src/controllers/home_controller.dart';
+import 'package:mom_kitchen/src/controllers/shopping_list_controller.dart';
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
-import 'package:mom_kitchen/src/models/shopping/shopping_item_model.dart';
+import 'package:mom_kitchen/src/models/shopping_item_model.dart';
import 'package:mom_kitchen/src/services/ui/theme_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
@@ -320,25 +320,65 @@ class _HomeCardCarouselState extends State {
ThemeService themeService,
HomeController homeController,
) {
+ final badgeInfo = _getRecipeBadge(recipe);
+
return Column(
children: [
Expanded(
flex: 2,
- child: ClipRRect(
- borderRadius: const BorderRadius.only(
- topLeft: Radius.circular(20),
- topRight: Radius.circular(20),
- ),
- child: Container(
- color: themeService.primaryColor.value.withValues(alpha: 0.1),
- child: Center(
- child: Icon(
- CupertinoIcons.book,
- size: 64,
- color: themeService.primaryColor.value.withValues(alpha: 0.6),
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(20),
+ topRight: Radius.circular(20),
+ ),
+ child: Container(
+ color: themeService.primaryColor.value.withValues(alpha: 0.1),
+ child: Center(
+ child: Icon(
+ CupertinoIcons.book,
+ size: 64,
+ color: themeService.primaryColor.value.withValues(
+ alpha: 0.6,
+ ),
+ ),
+ ),
),
),
- ),
+ if (badgeInfo != null)
+ Positioned(
+ top: 10,
+ left: 10,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: badgeInfo['color'],
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: (badgeInfo['color'] as Color).withValues(
+ alpha: 0.3,
+ ),
+ blurRadius: 6,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Text(
+ badgeInfo['label'],
+ style: const TextStyle(
+ color: CupertinoColors.white,
+ fontSize: 11,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
),
),
@@ -533,25 +573,56 @@ class _HomeCardCarouselState extends State {
ThemeService themeService,
HomeController homeController,
) {
+ final badgeInfo = _getRecipeBadge(recipe);
+
return Column(
children: [
Expanded(
flex: 2,
- child: ClipRRect(
- borderRadius: const BorderRadius.only(
- topLeft: Radius.circular(20),
- topRight: Radius.circular(20),
- ),
- child: Container(
- color: themeService.primaryColor.value.withValues(alpha: 0.1),
- child: Center(
- child: Icon(
- CupertinoIcons.book,
- size: 40,
- color: themeService.primaryColor.value.withValues(alpha: 0.6),
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(20),
+ topRight: Radius.circular(20),
+ ),
+ child: Container(
+ color: themeService.primaryColor.value.withValues(alpha: 0.1),
+ child: Center(
+ child: Icon(
+ CupertinoIcons.book,
+ size: 40,
+ color: themeService.primaryColor.value.withValues(
+ alpha: 0.6,
+ ),
+ ),
+ ),
),
),
- ),
+ if (badgeInfo != null)
+ Positioned(
+ top: 6,
+ left: 6,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: badgeInfo['color'],
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ badgeInfo['label'],
+ style: const TextStyle(
+ color: CupertinoColors.white,
+ fontSize: 9,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
),
),
@@ -728,8 +799,35 @@ class _HomeCardCarouselState extends State {
);
}
+ Map? _getRecipeBadge(RecipeModel recipe) {
+ final stats = recipe.statistics;
+ final createdAt = recipe.createdAt;
+
+ final bool isHot = (stats?.views ?? 0) > 1000 || (stats?.likes ?? 0) > 100;
+
+ bool isNew = false;
+ if (createdAt != null) {
+ try {
+ final created = DateTime.parse(createdAt);
+ final now = DateTime.now();
+ final diff = now.difference(created).inDays;
+ isNew = diff <= 7;
+ } catch (_) {}
+ }
+
+ if (isNew) {
+ return {'label': '✨ 新', 'color': const Color(0xFF34C759)};
+ }
+
+ if (isHot) {
+ return {'label': '🔥 热', 'color': const Color(0xFFFF3B30)};
+ }
+
+ return null;
+ }
+
void _addToShoppingList(RecipeModel recipe) {
- final ctrl = Get.put(ShoppingListController());
+ final ctrl = Get.find();
final items = recipe.ingredients.map((ing) {
return ShoppingItemModel(
name: ing.name,
diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home/home_page.dart
similarity index 90%
rename from lib/src/pages/home_page.dart
rename to lib/src/pages/home/home_page.dart
index 19fb135..a592b45 100644
--- a/lib/src/pages/home_page.dart
+++ b/lib/src/pages/home/home_page.dart
@@ -2,8 +2,8 @@
* 文件: home_page.dart
* 名称: 首页
* 作用: iOS 26 风格首页,横向滑动卡片布局,点击显示菜品详情
- * 更新: 2026-04-10 完全重写为横向滑动卡片布局
- * 更新: 2026-04-10 改用 RecipeRepository 层获取数据,移除 ApiService 直接调用
+ * 更新: 2026-04-10 添加营养追踪仪表盘卡片
+ * 更新: 2026-04-10 添加骨架屏+超时保护+缓存优先策略
*/
import 'package:flutter/cupertino.dart';
@@ -11,8 +11,12 @@ import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/repositories/recipe_repository.dart';
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
-import 'package:mom_kitchen/src/pages/recipe/recipe_detail_page.dart';
-import 'package:mom_kitchen/src/routes/app_routes.dart';
+import 'recipe_detail_page.dart';
+import 'package:mom_kitchen/src/config/app_routes.dart';
+import 'package:mom_kitchen/src/widgets/nutrition_dashboard_card.dart';
+import 'package:mom_kitchen/src/widgets/base/skeleton_loader.dart';
+import 'package:mom_kitchen/src/services/ui/toast_service.dart';
+import 'package:mom_kitchen/src/controllers/meal_record_controller.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@@ -31,6 +35,17 @@ class _HomePageState extends State {
void initState() {
super.initState();
_loadRecipes();
+ _initNutritionController();
+ }
+
+ void _initNutritionController() {
+ try {
+ if (!Get.isRegistered()) {
+ Get.put(MealRecordController(), permanent: true);
+ }
+ } catch (e) {
+ debugPrint('Init nutrition controller error: $e');
+ }
}
Future _loadRecipes({bool refresh = false}) async {
@@ -40,21 +55,43 @@ class _HomePageState extends State {
}
try {
- List results = await _recipeRepository.fetchFeedRecipes(
- refresh: refresh,
- );
+ List results = await _recipeRepository
+ .fetchFeedRecipes(refresh: refresh)
+ .timeout(
+ const Duration(seconds: 12),
+ onTimeout: () {
+ debugPrint('HomePage: fetchFeedRecipes timeout');
+ return [];
+ },
+ );
+
if (results.isEmpty) {
- final paginated = await _recipeRepository.fetchList(refresh: refresh);
+ final paginated = await _recipeRepository
+ .fetchList(refresh: refresh)
+ .timeout(
+ const Duration(seconds: 12),
+ onTimeout: () {
+ debugPrint('HomePage: fetchList timeout');
+ throw Exception('加载超时,请检查网络连接');
+ },
+ );
results = paginated.items;
}
debugPrint('Loaded ${results.length} recipes via Repository');
_recipes.value = results;
_error.value = '';
+
+ if (results.isNotEmpty && !refresh) {
+ ToastService.show(message: '已加载 ${results.length} 道菜谱 🎉');
+ }
} catch (e) {
debugPrint('Error loading recipes: $e');
_error.value = e.toString();
- _recipes.clear();
+ if (_recipes.isEmpty) {
+ _recipes.clear();
+ }
+ ToastService.show(message: '加载失败: $e 🔄');
} finally {
_isLoading.value = false;
}
@@ -163,23 +200,9 @@ class _HomePageState extends State {
}
Widget _buildContent(bool isDark) {
- if (_isLoading.value && _recipes.isEmpty) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const CupertinoActivityIndicator(radius: 20),
- const SizedBox(height: DesignTokens.space3),
- Text(
- '正在加载菜谱...',
- style: TextStyle(
- fontSize: DesignTokens.fontMd,
- color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
- ),
- ),
- ],
- ),
- );
+ // 始终显示骨架屏,即使有数据也在加载时显示
+ if (_isLoading.value) {
+ return SkeletonRecipeGrid(itemCount: 6, isDark: isDark);
}
if (_error.value.isNotEmpty && _recipes.isEmpty) {
@@ -281,6 +304,8 @@ class _HomePageState extends State {
CupertinoSliverRefreshControl(
onRefresh: () => _loadRecipes(refresh: true),
),
+ SliverToBoxAdapter(child: const NutritionDashboardCard()),
+ const SliverToBoxAdapter(child: SizedBox(height: DesignTokens.space4)),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4),
@@ -383,7 +408,7 @@ class _HomePageState extends State {
return;
}
- Get.to(() => RecipeDetailPage(recipeId: '${recipe.id}'));
+ Get.toNamed('/recipe-detail', arguments: '${recipe.id}');
},
child: Container(
width: 260,
diff --git a/lib/src/pages/home/home_products.dart b/lib/src/pages/home/home_products.dart
index 269b399..378dd7f 100644
--- a/lib/src/pages/home/home_products.dart
+++ b/lib/src/pages/home/home_products.dart
@@ -1,4 +1,4 @@
-/*
+/*
* 文件: home_products.dart
* 说明: 首页菜谱列表标签。展示搜索栏、分类过滤和菜谱网格。
* 作用: 提供首页的主要菜谱浏览功能。
@@ -9,7 +9,7 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/home/home_controller.dart';
+import 'package:mom_kitchen/src/controllers/home_controller.dart';
import 'package:mom_kitchen/src/standards/page_standards.dart';
import 'package:mom_kitchen/src/widgets/product_card.dart';
import 'package:mom_kitchen/src/widgets/states/empty_state.dart';
diff --git a/lib/src/pages/home/home_recommended.dart b/lib/src/pages/home/home_recommended.dart
index 5910f49..12d5c64 100644
--- a/lib/src/pages/home/home_recommended.dart
+++ b/lib/src/pages/home/home_recommended.dart
@@ -1,4 +1,4 @@
-/*
+/*
* 文件: home_recommended.dart
* 说明: 首页推荐标签。展示高评分或热卖菜谱。
* 作用: 提供首页的推荐和热卖内容。
@@ -9,7 +9,7 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/home/home_controller.dart';
+import 'package:mom_kitchen/src/controllers/home_controller.dart';
import 'package:mom_kitchen/src/standards/page_standards.dart';
import 'package:mom_kitchen/src/widgets/product_card.dart';
import 'package:mom_kitchen/src/widgets/states/empty_state.dart';
diff --git a/lib/src/pages/home/recipe_detail_page.dart b/lib/src/pages/home/recipe_detail_page.dart
new file mode 100644
index 0000000..dbe6bc8
--- /dev/null
+++ b/lib/src/pages/home/recipe_detail_page.dart
@@ -0,0 +1,553 @@
+// 菜谱详情页
+// 创建时间: 2026-04-09
+// 更新时间: 2026-04-11
+// 名称: recipe_detail_page.dart
+// 作用: 展示菜谱详细信息
+// 上次更新内容: 修复导入路径和方法调用问题
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:mom_kitchen/src/controllers/favorites_controller.dart';
+import 'package:mom_kitchen/src/controllers/feed/action_controller.dart';
+import 'package:mom_kitchen/src/repositories/recipe_repository.dart';
+import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
+import 'package:mom_kitchen/src/models/feed_item_model.dart';
+import 'package:mom_kitchen/src/services/allergen_checker.dart';
+import 'package:mom_kitchen/src/config/design_tokens.dart';
+
+class RecipeDetailPage extends StatefulWidget {
+ final String recipeId;
+
+ const RecipeDetailPage({super.key, required this.recipeId});
+
+ @override
+ State createState() => _RecipeDetailPageState();
+}
+
+class _RecipeDetailPageState extends State {
+ final AllergenChecker _allergenChecker = AllergenChecker();
+ final RecipeRepository _recipeRepository = RecipeRepository();
+
+ RecipeModel? _recipe;
+ bool _isLoading = true;
+ bool _isFavorite = false;
+ int _likeCount = 0;
+ int _viewCount = 0;
+
+ ActionController? _actionController;
+ FavoritesController? _favoritesController;
+
+ @override
+ void initState() {
+ super.initState();
+ _initControllers();
+ _loadRecipe();
+ }
+
+ void _initControllers() {
+ try {
+ _actionController = Get.find();
+ } catch (_) {
+ _actionController = Get.put(ActionController());
+ }
+ try {
+ _favoritesController = Get.find();
+ } catch (_) {
+ _favoritesController = Get.put(FavoritesController());
+ }
+ }
+
+ Future _loadRecipe() async {
+ try {
+ final recipeId = int.tryParse(widget.recipeId) ?? 0;
+ final recipe = await _recipeRepository.fetchDetail(recipeId);
+ if (mounted) {
+ setState(() {
+ _recipe = recipe;
+ _isLoading = false;
+ _likeCount = recipe.statistics?.likes ?? 0;
+ _viewCount = recipe.statistics?.views ?? 0;
+ });
+ _checkFavorite();
+ _recordView();
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ Get.snackbar('错误', '加载菜谱失败: $e');
+ }
+ }
+ }
+
+ void _checkFavorite() {
+ if (_favoritesController != null && _recipe != null) {
+ final isFav = _favoritesController!.isFavorited(_recipe!.id);
+ if (mounted) {
+ setState(() {
+ _isFavorite = isFav;
+ });
+ }
+ }
+ }
+
+ void _recordView() {
+ if (_actionController != null && _recipe != null) {
+ _actionController!.reportView(id: _recipe!.id, type: 'recipe');
+ }
+ }
+
+ void _toggleFavorite() {
+ if (_favoritesController != null && _recipe != null) {
+ final feedItem = FeedItemModel.fromRecipe(_recipe!);
+ _favoritesController!.toggleFavorite(feedItem);
+ setState(() {
+ _isFavorite = !_isFavorite;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
+
+ if (_isLoading) {
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: const Text('菜谱详情'),
+ backgroundColor: isDark
+ ? DarkDesignTokens.background
+ : DesignTokens.background,
+ ),
+ child: const Center(child: CupertinoActivityIndicator()),
+ );
+ }
+
+ if (_recipe == null) {
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: const Text('菜谱详情'),
+ backgroundColor: isDark
+ ? DarkDesignTokens.background
+ : DesignTokens.background,
+ ),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ CupertinoIcons.exclamationmark_triangle,
+ size: 48,
+ color: DesignTokens.text3,
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ '菜谱不存在',
+ style: TextStyle(fontSize: 16, color: DesignTokens.text2),
+ ),
+ const SizedBox(height: 16),
+ CupertinoButton(
+ onPressed: () => Get.back(),
+ child: const Text('返回'),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: Text(_recipe!.title ?? '菜谱详情'),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ GestureDetector(
+ onTap: _toggleFavorite,
+ child: Icon(
+ _isFavorite ? CupertinoIcons.heart_fill : CupertinoIcons.heart,
+ color: _isFavorite ? DesignTokens.red : DesignTokens.text2,
+ ),
+ ),
+ ],
+ ),
+ backgroundColor: isDark
+ ? DarkDesignTokens.background
+ : DesignTokens.background,
+ ),
+ child: ListView(
+ children: [
+ _buildHeader(),
+ _buildInfo(),
+ _buildIngredients(),
+ _buildSteps(),
+ _buildNutritionInfo(),
+ _buildActions(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Stack(
+ children: [
+ if (_recipe!.cover != null && _recipe!.cover!.isNotEmpty)
+ Image.network(
+ _recipe!.cover!,
+ height: 250,
+ width: double.infinity,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ height: 250,
+ color: DesignTokens.text3.withValues(alpha: 0.1),
+ child: const Icon(
+ CupertinoIcons.photo,
+ size: 64,
+ color: DesignTokens.text3,
+ ),
+ ),
+ )
+ else
+ Container(
+ height: 250,
+ color: DesignTokens.text3.withValues(alpha: 0.1),
+ child: const Icon(
+ CupertinoIcons.photo,
+ size: 64,
+ color: DesignTokens.text3,
+ ),
+ ),
+ Positioned(
+ bottom: 0,
+ left: 0,
+ right: 0,
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.transparent,
+ Colors.black.withValues(alpha: 0.7),
+ ],
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _recipe!.title ?? '',
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ const Icon(
+ CupertinoIcons.eye,
+ size: 16,
+ color: Colors.white70,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ '$_viewCount',
+ style: const TextStyle(color: Colors.white70),
+ ),
+ const SizedBox(width: 16),
+ const Icon(
+ CupertinoIcons.heart,
+ size: 16,
+ color: Colors.white70,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ '$_likeCount',
+ style: const TextStyle(color: Colors.white70),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildInfo() {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (_recipe!.intro != null && _recipe!.intro!.isNotEmpty)
+ Text(
+ _recipe!.intro!,
+ style: const TextStyle(
+ fontSize: 14,
+ color: DesignTokens.text2,
+ height: 1.5,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ if (_recipe!.meta?.time != null)
+ _buildInfoItem(CupertinoIcons.time, _recipe!.meta!.time!),
+ if (_recipe!.meta?.difficulty != null) ...[
+ const SizedBox(width: 24),
+ _buildInfoItem(
+ CupertinoIcons.flame,
+ _recipe!.meta!.difficulty!,
+ ),
+ ],
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildInfoItem(IconData icon, String text) {
+ return Row(
+ children: [
+ Icon(icon, size: 18, color: DesignTokens.text2),
+ const SizedBox(width: 4),
+ Text(
+ text,
+ style: const TextStyle(fontSize: 14, color: DesignTokens.text2),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildIngredients() {
+ if (_recipe!.ingredients == null || _recipe!.ingredients!.isEmpty) {
+ return const SizedBox();
+ }
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '食材',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: DesignTokens.text1,
+ ),
+ ),
+ const SizedBox(height: 12),
+ ...(_recipe!.ingredients!.map(
+ (ingredient) => Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ children: [
+ const Icon(
+ CupertinoIcons.circle_fill,
+ size: 8,
+ color: DesignTokens.primary,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ ingredient.name ?? '',
+ style: const TextStyle(
+ fontSize: 16,
+ color: DesignTokens.text1,
+ ),
+ ),
+ ),
+ Text(
+ '${ingredient.amount ?? ''} ${ingredient.unit ?? ''}',
+ style: const TextStyle(
+ fontSize: 14,
+ color: DesignTokens.text2,
+ ),
+ ),
+ ],
+ ),
+ ),
+ )),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSteps() {
+ if (_recipe!.content == null || _recipe!.content!.isEmpty) {
+ return const SizedBox();
+ }
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '做法',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: DesignTokens.text1,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ _recipe!.content!,
+ style: const TextStyle(
+ fontSize: 16,
+ color: DesignTokens.text1,
+ height: 1.6,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildNutritionInfo() {
+ if (_recipe!.nutrition == null) return const SizedBox();
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: DesignTokens.card,
+ borderRadius: DesignTokens.borderRadiusMd,
+ border: Border.all(
+ color: DesignTokens.text3.withValues(alpha: 0.2),
+ width: 1,
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '营养信息',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: DesignTokens.text1,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _buildNutritionItem(
+ '热量',
+ '${_recipe!.nutrition!.calories?.toInt() ?? 0} kcal',
+ ),
+ _buildNutritionItem(
+ '蛋白质',
+ '${_recipe!.nutrition!.protein?.toInt() ?? 0} g',
+ ),
+ _buildNutritionItem(
+ '脂肪',
+ '${_recipe!.nutrition!.fat?.toInt() ?? 0} g',
+ ),
+ _buildNutritionItem(
+ '碳水',
+ '${_recipe!.nutrition!.carbs?.toInt() ?? 0} g',
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildNutritionItem(String label, String value) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Text(
+ value,
+ style: const TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
+ color: DesignTokens.primary,
+ ),
+ ),
+ Text(
+ label,
+ style: const TextStyle(fontSize: 12, color: DesignTokens.text2),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildActions() {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ CupertinoButton(
+ onPressed: () {
+ setState(() {
+ _likeCount++;
+ });
+ _actionController?.likeItem(id: _recipe!.id, type: 'recipe');
+ },
+ child: Column(
+ children: [
+ const Icon(CupertinoIcons.heart, size: 24),
+ const SizedBox(height: 4),
+ Text('$_likeCount'),
+ ],
+ ),
+ ),
+ CupertinoButton(
+ onPressed: () {
+ _actionController?.recommendItem(id: _recipe!.id, type: 'recipe');
+ Get.snackbar('成功', '推荐成功');
+ },
+ child: const Column(
+ children: [
+ Icon(CupertinoIcons.star, size: 24),
+ SizedBox(height: 4),
+ Text('推荐'),
+ ],
+ ),
+ ),
+ CupertinoButton(
+ onPressed: () {
+ Get.snackbar('提示', '烹饪笔记功能开发中');
+ },
+ child: const Column(
+ children: [
+ Icon(CupertinoIcons.pencil, size: 24),
+ SizedBox(height: 4),
+ Text('笔记'),
+ ],
+ ),
+ ),
+ CupertinoButton(
+ onPressed: () {
+ Get.snackbar('提示', '已添加到购物清单');
+ },
+ child: const Column(
+ children: [
+ Icon(CupertinoIcons.cart, size: 24),
+ SizedBox(height: 4),
+ Text('购物'),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/pages/search/search_page.dart b/lib/src/pages/home/search_page.dart
similarity index 95%
rename from lib/src/pages/search/search_page.dart
rename to lib/src/pages/home/search_page.dart
index 99a221c..220234d 100644
--- a/lib/src/pages/search/search_page.dart
+++ b/lib/src/pages/home/search_page.dart
@@ -8,9 +8,9 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' show Colors;
import 'package:get/get.dart';
-import '../../controllers/search/search_controller.dart';
+import '../../controllers/search_controller.dart';
import '../../config/design_tokens.dart';
-import '../../pages/recipe/recipe_detail_page.dart';
+import 'recipe_detail_page.dart';
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@@ -20,13 +20,14 @@ class SearchPage extends StatefulWidget {
}
class _SearchPageState extends State {
- final SearchController _searchController = Get.put(SearchController());
+ late final SearchController _searchController;
final TextEditingController _textEditingController = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
+ _searchController = Get.find();
_focusNode.requestFocus();
_checkInitialKeyword();
}
@@ -480,22 +481,33 @@ class _SearchPageState extends State {
}
Widget _buildRecipeItem(dynamic recipe, bool isDark) {
- final title = recipe.title;
+ final title = recipe.title ?? '未知菜谱';
final intro = recipe.intro ?? '';
final category = recipe.categoryName;
final cover = recipe.cover;
+ final recipeId = recipe.id;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
- debugPrint('Tapped recipe: $title (ID: ${recipe.id})');
+ try {
+ debugPrint('Tapped recipe: $title (ID: $recipeId)');
- if (recipe.id <= 0) {
- Get.snackbar('提示', '菜谱ID无效', snackPosition: SnackPosition.BOTTOM);
- return;
+ if (recipeId == null || recipeId <= 0) {
+ Get.snackbar('提示', '菜谱 ID 无效', snackPosition: SnackPosition.BOTTOM);
+ return;
+ }
+
+ Get.toNamed('/recipe-detail', arguments: '$recipeId');
+ } catch (e, stackTrace) {
+ debugPrint('Open recipe detail error: $e');
+ debugPrint('Stack trace: $stackTrace');
+ Get.snackbar(
+ '错误',
+ '无法打开菜谱详情: $e',
+ snackPosition: SnackPosition.BOTTOM,
+ );
}
-
- Get.to(() => RecipeDetailPage(recipeId: '${recipe.id}'));
},
child: Container(
padding: const EdgeInsets.all(DesignTokens.space3),
diff --git a/lib/src/pages/chat_module/chat_page.dart b/lib/src/pages/profile/chat_page.dart
similarity index 100%
rename from lib/src/pages/chat_module/chat_page.dart
rename to lib/src/pages/profile/chat_page.dart
diff --git a/lib/src/pages/favorites/favorites_page.dart b/lib/src/pages/profile/favorites_page.dart
similarity index 62%
rename from lib/src/pages/favorites/favorites_page.dart
rename to lib/src/pages/profile/favorites_page.dart
index 08b27cb..cdc61ae 100644
--- a/lib/src/pages/favorites/favorites_page.dart
+++ b/lib/src/pages/profile/favorites_page.dart
@@ -2,24 +2,60 @@
* 文件: favorites_page.dart
* 名称: 收藏页面
* 作用: iOS 26 风格的收藏页面,使用 FavoritesController 展示收藏内容
- * 更新: 2026-04-09 新增编辑模式、排序、分类筛选功能
+ * 更新: 2026-04-10 添加工具入口Bar
+ * 更新: 2026-04-11 修复GetX报错,重构为StatefulWidget
+ * 更新: 2026-04-11 移动到pages根目录
*/
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/controllers/favorites/favorites_controller.dart';
+import 'package:mom_kitchen/src/controllers/favorites_controller.dart';
+import 'package:mom_kitchen/src/controllers/tools_controller.dart';
+import 'package:mom_kitchen/src/models/tool_item_model.dart';
-class FavoritesPage extends StatelessWidget {
+class FavoritesPage extends StatefulWidget {
const FavoritesPage({super.key});
@override
- Widget build(BuildContext context) {
- if (!Get.isRegistered()) {
- Get.put(FavoritesController(), permanent: true);
+ State createState() => _FavoritesPageState();
+}
+
+class _FavoritesPageState extends State {
+ late final FavoritesController _favoritesController;
+ ToolsController? _toolsController;
+
+ @override
+ void initState() {
+ super.initState();
+ _favoritesController = Get.find();
+ _initToolsController();
+ }
+
+ void _initToolsController() {
+ try {
+ _toolsController = Get.find();
+ } catch (e) {
+ debugPrint('ToolsController not found, will be created lazily: $e');
+ _toolsController = null;
}
- final favoritesController = Get.find();
+ }
+
+ ToolsController _getOrCreateToolsController() {
+ if (_toolsController != null) {
+ return _toolsController!;
+ }
+ try {
+ _toolsController = Get.find();
+ } catch (_) {
+ _toolsController = Get.put(ToolsController(), permanent: true);
+ }
+ return _toolsController!;
+ }
+
+ @override
+ Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return CupertinoPageScaffold(
@@ -29,24 +65,21 @@ class FavoritesPage extends StatelessWidget {
child: SafeArea(
child: Column(
children: [
- _buildHeader(favoritesController, isDark),
- _buildToolbar(favoritesController, isDark),
+ _buildHeader(isDark),
+ _buildToolsBar(isDark),
+ _buildToolbar(isDark),
Expanded(
child: Obx(() {
- final favorites = favoritesController.favorites;
+ final favorites = _favoritesController.favorites;
if (favorites.isEmpty) {
return _buildEmptyState(isDark);
}
- return _buildFavoritesList(
- favorites,
- favoritesController,
- isDark,
- );
+ return _buildFavoritesList(favorites, isDark);
}),
),
Obx(
- () => favoritesController.isEditMode.value
- ? _buildEditBottomBar(favoritesController, isDark)
+ () => _favoritesController.isEditMode.value
+ ? _buildEditBottomBar(isDark)
: const SizedBox.shrink(),
),
],
@@ -55,7 +88,7 @@ class FavoritesPage extends StatelessWidget {
);
}
- Widget _buildHeader(FavoritesController ctrl, bool isDark) {
+ Widget _buildHeader(bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
@@ -73,7 +106,7 @@ class FavoritesPage extends StatelessWidget {
),
const Spacer(),
Obx(() {
- final count = ctrl.count;
+ final count = _favoritesController.count;
if (count == 0) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.symmetric(
@@ -96,13 +129,13 @@ class FavoritesPage extends StatelessWidget {
}),
const SizedBox(width: DesignTokens.space3),
Obx(() {
- if (ctrl.count == 0) return const SizedBox.shrink();
+ if (_favoritesController.count == 0) return const SizedBox.shrink();
return CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: const Size(36, 36),
- onPressed: ctrl.toggleEditMode,
+ onPressed: _favoritesController.toggleEditMode,
child: Text(
- ctrl.isEditMode.value ? '完成' : '编辑',
+ _favoritesController.isEditMode.value ? '完成' : '编辑',
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: DesignTokens.primary,
@@ -116,9 +149,159 @@ class FavoritesPage extends StatelessWidget {
);
}
- Widget _buildToolbar(FavoritesController ctrl, bool isDark) {
+ Widget _buildToolsBar(bool isDark) {
+ final toolsController = _toolsController;
+ if (toolsController == null) {
+ return const SizedBox.shrink();
+ }
+
return Obx(() {
- if (ctrl.count == 0) return const SizedBox.shrink();
+ final tools = toolsController.frequentTools;
+ if (tools.isEmpty) {
+ return const SizedBox.shrink();
+ }
+ return Container(
+ height: 90,
+ margin: const EdgeInsets.only(bottom: DesignTokens.space2),
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4),
+ itemCount: tools.length + 1,
+ separatorBuilder: (context, index) =>
+ const SizedBox(width: DesignTokens.space2),
+ itemBuilder: (context, index) {
+ if (index == tools.length) {
+ return _buildMoreToolsCard(isDark);
+ }
+ return _buildToolShortcut(tools[index], toolsController, isDark);
+ },
+ ),
+ );
+ });
+ }
+
+ Widget _buildToolShortcut(
+ ToolItem tool,
+ ToolsController controller,
+ bool isDark,
+ ) {
+ return GestureDetector(
+ onTap: () => controller.openTool(tool),
+ child: Container(
+ width: 72,
+ padding: const EdgeInsets.symmetric(vertical: DesignTokens.space2),
+ decoration: BoxDecoration(
+ color: isDark ? DarkDesignTokens.card : DesignTokens.card,
+ borderRadius: DesignTokens.borderRadiusMd,
+ border: Border.all(
+ color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3)
+ .withValues(alpha: 0.1),
+ ),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Stack(
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color:
+ (isDark
+ ? DarkDesignTokens.primary
+ : DesignTokens.primary)
+ .withValues(alpha: 0.1),
+ borderRadius: DesignTokens.borderRadiusMd,
+ ),
+ child: Center(
+ child: Text(
+ tool.icon,
+ style: const TextStyle(fontSize: 24),
+ ),
+ ),
+ ),
+ Positioned(
+ top: 0,
+ right: 0,
+ child: Container(
+ width: 8,
+ height: 8,
+ decoration: BoxDecoration(
+ color: tool.needsNetwork
+ ? DesignTokens.green
+ : DesignTokens.primary,
+ shape: BoxShape.circle,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Text(
+ tool.name,
+ style: TextStyle(
+ fontSize: DesignTokens.fontXs,
+ color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildMoreToolsCard(bool isDark) {
+ return GestureDetector(
+ onTap: () => Get.toNamed('/tools'),
+ child: Container(
+ width: 72,
+ padding: const EdgeInsets.symmetric(vertical: DesignTokens.space2),
+ decoration: BoxDecoration(
+ color: isDark
+ ? DarkDesignTokens.primary.withValues(alpha: 0.1)
+ : DesignTokens.primaryLight,
+ borderRadius: DesignTokens.borderRadiusMd,
+ border: Border.all(
+ color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary)
+ .withValues(alpha: 0.3),
+ ),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color:
+ (isDark ? DarkDesignTokens.primary : DesignTokens.primary)
+ .withValues(alpha: 0.15),
+ borderRadius: DesignTokens.borderRadiusMd,
+ ),
+ child: const Center(
+ child: Text('🛠️', style: TextStyle(fontSize: 24)),
+ ),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ '更多',
+ style: TextStyle(
+ fontSize: DesignTokens.fontXs,
+ color: isDark ? DarkDesignTokens.primary : DesignTokens.primary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildToolbar(bool isDark) {
+ return Obx(() {
+ if (_favoritesController.count == 0) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
@@ -126,18 +309,18 @@ class FavoritesPage extends StatelessWidget {
),
child: Row(
children: [
- _buildSortButton(ctrl, isDark),
+ _buildSortButton(isDark),
const SizedBox(width: DesignTokens.space2),
- _buildCategoryFilter(ctrl, isDark),
+ _buildCategoryFilter(isDark),
],
),
);
});
}
- Widget _buildSortButton(FavoritesController ctrl, bool isDark) {
+ Widget _buildSortButton(bool isDark) {
return GestureDetector(
- onTap: () => _showSortSheet(ctrl, isDark),
+ onTap: () => _showSortSheet(isDark),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space3,
@@ -162,7 +345,7 @@ class FavoritesPage extends StatelessWidget {
),
const SizedBox(width: DesignTokens.space1),
Text(
- _getSortLabel(ctrl.sortMode.value),
+ _getSortLabel(_favoritesController.sortMode.value),
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
@@ -174,18 +357,19 @@ class FavoritesPage extends StatelessWidget {
);
}
- Widget _buildCategoryFilter(FavoritesController ctrl, bool isDark) {
+ Widget _buildCategoryFilter(bool isDark) {
return Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Obx(
() => Row(
- children: ctrl.categories.map((cat) {
- final isSelected = ctrl.selectedCategory.value == cat;
+ children: _favoritesController.categories.map((cat) {
+ final isSelected =
+ _favoritesController.selectedCategory.value == cat;
return Padding(
padding: const EdgeInsets.only(right: DesignTokens.space2),
child: GestureDetector(
- onTap: () => ctrl.setCategory(cat),
+ onTap: () => _favoritesController.setCategory(cat),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space3,
@@ -229,15 +413,15 @@ class FavoritesPage extends StatelessWidget {
);
}
- void _showSortSheet(FavoritesController ctrl, bool isDark) {
+ void _showSortSheet(bool isDark) {
showCupertinoModalPopup(
- context: Get.context!,
+ context: context,
builder: (context) => CupertinoActionSheet(
title: const Text('排序方式'),
actions: FavoritesSortMode.values.map((mode) {
return CupertinoActionSheetAction(
onPressed: () {
- ctrl.setSortMode(mode);
+ _favoritesController.setSortMode(mode);
Get.back();
},
child: Text(_getSortLabel(mode)),
@@ -264,7 +448,7 @@ class FavoritesPage extends StatelessWidget {
}
}
- Widget _buildEditBottomBar(FavoritesController ctrl, bool isDark) {
+ Widget _buildEditBottomBar(bool isDark) {
return Container(
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
@@ -281,9 +465,11 @@ class FavoritesPage extends StatelessWidget {
children: [
CupertinoButton(
padding: EdgeInsets.zero,
- onPressed: ctrl.hasSelection ? ctrl.deselectAll : ctrl.selectAll,
+ onPressed: _favoritesController.hasSelection
+ ? _favoritesController.deselectAll
+ : _favoritesController.selectAll,
child: Text(
- ctrl.hasSelection ? '取消全选' : '全选',
+ _favoritesController.hasSelection ? '取消全选' : '全选',
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: DesignTokens.primary,
@@ -293,7 +479,7 @@ class FavoritesPage extends StatelessWidget {
const Spacer(),
Obx(
() => Text(
- '已选 ${ctrl.selectedCount} 项',
+ '已选 ${_favoritesController.selectedCount} 项',
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
@@ -305,7 +491,9 @@ class FavoritesPage extends StatelessWidget {
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
),
- onPressed: ctrl.hasSelection ? () => _confirmDelete(ctrl) : null,
+ onPressed: _favoritesController.hasSelection
+ ? () => _confirmDelete()
+ : null,
child: const Text('删除'),
),
],
@@ -313,19 +501,19 @@ class FavoritesPage extends StatelessWidget {
);
}
- void _confirmDelete(FavoritesController ctrl) {
+ void _confirmDelete() {
showCupertinoDialog(
- context: Get.context!,
+ context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text('确认删除'),
- content: Text('确定要删除选中的 ${ctrl.selectedCount} 项收藏吗?'),
+ content: Text('确定要删除选中的 ${_favoritesController.selectedCount} 项收藏吗?'),
actions: [
CupertinoDialogAction(onPressed: Get.back, child: const Text('取消')),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Get.back();
- ctrl.deleteSelected();
+ _favoritesController.deleteSelected();
},
child: const Text('删除'),
),
@@ -374,11 +562,7 @@ class FavoritesPage extends StatelessWidget {
);
}
- Widget _buildFavoritesList(
- List favorites,
- FavoritesController favoritesController,
- bool isDark,
- ) {
+ Widget _buildFavoritesList(List favorites, bool isDark) {
return ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
@@ -389,23 +573,19 @@ class FavoritesPage extends StatelessWidget {
const SizedBox(height: DesignTokens.space2 + 2),
itemBuilder: (context, index) {
final item = favorites[index];
- return _buildFavoriteItem(item, favoritesController, isDark);
+ return _buildFavoriteItem(item, isDark);
},
);
}
- Widget _buildFavoriteItem(
- dynamic item,
- FavoritesController favoritesController,
- bool isDark,
- ) {
+ Widget _buildFavoriteItem(dynamic item, bool isDark) {
return Obx(() {
- final isEditMode = favoritesController.isEditMode.value;
- final isSelected = favoritesController.isSelected(item.id);
+ final isEditMode = _favoritesController.isEditMode.value;
+ final isSelected = _favoritesController.isSelected(item.id);
return GestureDetector(
onTap: isEditMode
- ? () => favoritesController.toggleSelection(item.id)
+ ? () => _favoritesController.toggleSelection(item.id)
: () {
Get.toNamed('/recipe-detail', arguments: '${item.id}');
},
@@ -497,7 +677,7 @@ class FavoritesPage extends StatelessWidget {
if (!isEditMode) ...[
const SizedBox(width: DesignTokens.space2),
GestureDetector(
- onTap: () => favoritesController.removeFavorite(item.id),
+ onTap: () => _favoritesController.removeFavorite(item.id),
behavior: HitTestBehavior.opaque,
child: Container(
width: 36,
diff --git a/lib/src/pages/nutrition/add_meal_sheet.dart b/lib/src/pages/profile/nutrition/add_meal_sheet.dart
similarity index 98%
rename from lib/src/pages/nutrition/add_meal_sheet.dart
rename to lib/src/pages/profile/nutrition/add_meal_sheet.dart
index 8d0d40e..7aa547c 100644
--- a/lib/src/pages/nutrition/add_meal_sheet.dart
+++ b/lib/src/pages/profile/nutrition/add_meal_sheet.dart
@@ -1,7 +1,7 @@
-// 2026-04-09 | AddMealSheet | 添加饮食记录弹窗 | iOS26风格底部弹窗,支持手动输入营养数据
+// 2026-04-09 | AddMealSheet | 添加饮食记录弹窗 | iOS26风格底部弹窗,支持手动输入营养数据
import 'package:flutter/cupertino.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/models/nutrition/meal_record_model.dart';
+import 'package:mom_kitchen/src/models/meal_record_model.dart';
class AddMealSheet extends StatefulWidget {
final MealType mealType;
diff --git a/lib/src/pages/nutrition/goal_setting_page.dart b/lib/src/pages/profile/nutrition/goal_setting_page.dart
similarity index 80%
rename from lib/src/pages/nutrition/goal_setting_page.dart
rename to lib/src/pages/profile/nutrition/goal_setting_page.dart
index 1d8aba5..cda8cc3 100644
--- a/lib/src/pages/nutrition/goal_setting_page.dart
+++ b/lib/src/pages/profile/nutrition/goal_setting_page.dart
@@ -3,8 +3,8 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/controllers/nutrition/meal_record_controller.dart';
-import 'package:mom_kitchen/src/models/nutrition/user_goal_model.dart';
+import 'package:mom_kitchen/src/controllers/meal_record_controller.dart';
+import 'package:mom_kitchen/src/models/user_goal_model.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_container.dart';
class GoalSettingPage extends StatefulWidget {
@@ -15,14 +15,13 @@ class GoalSettingPage extends StatefulWidget {
}
class _GoalSettingPageState extends State {
- final MealRecordController _ctrl = Get.isRegistered()
- ? Get.find()
- : Get.put(MealRecordController());
+ late final MealRecordController _ctrl;
late Map _tempGoals;
@override
void initState() {
super.initState();
+ _ctrl = Get.find();
_tempGoals = {
'calories': _ctrl.caloriesGoal,
'protein': _ctrl.proteinGoal,
@@ -96,17 +95,20 @@ class _GoalSettingPageState extends State {
),
),
const SizedBox(height: DesignTokens.space3),
- SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(
- children: [
- _buildPresetChip(isDark, '🧘 减脂', 1500, 50, 50, 200),
- const SizedBox(width: DesignTokens.space2),
- _buildPresetChip(isDark, '⚖️ 均衡', 2000, 60, 65, 300),
- const SizedBox(width: DesignTokens.space2),
- _buildPresetChip(isDark, '💪 增肌', 2500, 100, 80, 350),
- ],
- ),
+ Row(
+ children: [
+ Expanded(
+ child: _buildPresetChip(isDark, '🧘 减脂', 1500, 50, 50, 200),
+ ),
+ const SizedBox(width: DesignTokens.space2),
+ Expanded(
+ child: _buildPresetChip(isDark, '⚖️ 均衡', 2000, 60, 65, 300),
+ ),
+ const SizedBox(width: DesignTokens.space2),
+ Expanded(
+ child: _buildPresetChip(isDark, '💪 增肌', 2500, 100, 80, 350),
+ ),
+ ],
),
],
),
@@ -121,49 +123,49 @@ class _GoalSettingPageState extends State {
double fat,
double carbs,
) {
- return Expanded(
- child: GestureDetector(
- onTap: () {
- setState(() {
- _tempGoals = {
- 'calories': cal,
- 'protein': protein,
- 'fat': fat,
- 'carbs': carbs,
- };
- });
- },
- child: Container(
- padding: const EdgeInsets.symmetric(
- vertical: DesignTokens.space3,
- horizontal: DesignTokens.space2,
- ),
- decoration: BoxDecoration(
- color: isDark
- ? DarkDesignTokens.segmentedBg
- : DesignTokens.text3.withValues(alpha: 0.08),
- borderRadius: DesignTokens.borderRadiusMd,
- ),
- child: Column(
- children: [
- Text(
- label,
- style: TextStyle(
- fontSize: DesignTokens.fontMd,
- fontWeight: FontWeight.w600,
- color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
- ),
+ return GestureDetector(
+ onTap: () {
+ setState(() {
+ _tempGoals = {
+ 'calories': cal,
+ 'protein': protein,
+ 'fat': fat,
+ 'carbs': carbs,
+ };
+ });
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ vertical: DesignTokens.space3,
+ horizontal: DesignTokens.space2,
+ ),
+ decoration: BoxDecoration(
+ color: isDark
+ ? DarkDesignTokens.segmentedBg
+ : DesignTokens.text3.withValues(alpha: 0.08),
+ borderRadius: DesignTokens.borderRadiusMd,
+ ),
+ child: Column(
+ children: [
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ fontWeight: FontWeight.w600,
+ color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
- const SizedBox(height: DesignTokens.space1),
- Text(
- '${cal.toInt()} kcal',
- style: TextStyle(
- fontSize: DesignTokens.fontSm,
- color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
- ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: DesignTokens.space1),
+ Text(
+ '${cal.toInt()} kcal',
+ style: TextStyle(
+ fontSize: DesignTokens.fontSm,
+ color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
),
- ],
- ),
+ textAlign: TextAlign.center,
+ ),
+ ],
),
),
);
diff --git a/lib/src/pages/nutrition/nutrition_center_page.dart b/lib/src/pages/profile/nutrition/nutrition_center_page.dart
similarity index 77%
rename from lib/src/pages/nutrition/nutrition_center_page.dart
rename to lib/src/pages/profile/nutrition/nutrition_center_page.dart
index a7a757d..61d0028 100644
--- a/lib/src/pages/nutrition/nutrition_center_page.dart
+++ b/lib/src/pages/profile/nutrition/nutrition_center_page.dart
@@ -1,14 +1,15 @@
// 2026-04-09 | NutritionCenterPage | 营养中心页面 | iOS26风格饮食日记+营养分析+目标管理
+// 2026-04-10 | 修复报告按钮卡死:添加错误处理+空指针保护
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/controllers/nutrition/meal_record_controller.dart';
-import 'package:mom_kitchen/src/models/nutrition/meal_record_model.dart';
-import 'package:mom_kitchen/src/widgets/custom/mini_calendar.dart';
-import 'package:mom_kitchen/src/widgets/custom/nutrition_ring.dart';
+import 'package:mom_kitchen/src/controllers/meal_record_controller.dart';
+import 'package:mom_kitchen/src/models/meal_record_model.dart';
+import 'package:mom_kitchen/src/widgets/custom_widgets.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_container.dart';
-import 'package:mom_kitchen/src/pages/nutrition/add_meal_sheet.dart';
-import 'package:mom_kitchen/src/routes/app_routes.dart';
+import 'package:mom_kitchen/src/pages/profile/nutrition/add_meal_sheet.dart';
+import 'package:mom_kitchen/src/config/app_routes.dart';
+import 'package:mom_kitchen/src/services/ui/toast_service.dart';
class NutritionCenterPage extends StatefulWidget {
const NutritionCenterPage({super.key});
@@ -18,13 +19,56 @@ class NutritionCenterPage extends StatefulWidget {
}
class _NutritionCenterPageState extends State {
- final MealRecordController _ctrl = Get.put(MealRecordController());
+ MealRecordController? _ctrl;
bool _calendarExpanded = false;
+ String? _error;
+
+ @override
+ void initState() {
+ super.initState();
+
+ try {
+ _ctrl = Get.find();
+ } catch (e) {
+ debugPrint('MealRecordController not found: $e');
+ _error = '控制器初始化失败';
+ _ctrl = null;
+ }
+ }
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
+ if (_error != null || _ctrl == null) {
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(middle: const Text('营养中心')),
+ child: SafeArea(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('😕', style: TextStyle(fontSize: 48)),
+ const SizedBox(height: DesignTokens.space3),
+ Text(
+ _error ?? '初始化失败',
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
+ ),
+ ),
+ const SizedBox(height: DesignTokens.space4),
+ CupertinoButton.filled(
+ onPressed: () => Get.back(),
+ child: const Text('返回'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
return CupertinoPageScaffold(
backgroundColor: isDark
? DarkDesignTokens.background
@@ -39,7 +83,20 @@ class _NutritionCenterPageState extends State {
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
- onTap: () => Get.toNamed(AppRoutes.nutritionReport),
+ onTap: () {
+ debugPrint('NutritionCenterPage: tapping report button');
+ debugPrint(
+ 'NutritionCenterPage: checking if route exists: ${AppRoutes.nutritionReport}',
+ );
+ try {
+ final result = Get.toNamed(AppRoutes.nutritionReport);
+ debugPrint('NutritionCenterPage: Get.toNamed result=$result');
+ } catch (e, stackTrace) {
+ debugPrint('Navigate to nutritionReport error: $e');
+ debugPrint('Stack trace: $stackTrace');
+ ToastService.show(message: '打开报告失败,请重试 🔄');
+ }
+ },
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space2,
@@ -61,7 +118,22 @@ class _NutritionCenterPageState extends State {
),
const SizedBox(width: DesignTokens.space2),
GestureDetector(
- onTap: () => _ctrl.selectToday(),
+ onTap: () {
+ debugPrint('NutritionCenterPage: tapping today button');
+ debugPrint('NutritionCenterPage: _ctrl=${_ctrl}');
+ debugPrint('NutritionCenterPage: calling selectToday()');
+ try {
+ _ctrl?.selectToday();
+ debugPrint(
+ 'NutritionCenterPage: selectToday called successfully',
+ );
+ ToastService.show(message: '已跳转到今天 📅');
+ } catch (e, stackTrace) {
+ debugPrint('selectToday error: $e');
+ debugPrint('Stack trace: $stackTrace');
+ ToastService.show(message: '跳转失败,请重试 🔄');
+ }
+ },
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space2,
@@ -109,14 +181,14 @@ class _NutritionCenterPageState extends State {
Widget _buildCalendar(bool isDark) {
final selectedDate =
- DateTime.tryParse(_ctrl.selectedDate.value) ?? DateTime.now();
+ DateTime.tryParse(_ctrl?.selectedDate.value ?? '') ?? DateTime.now();
return AnimatedCrossFade(
firstChild: MiniCalendar(
selectedDate: selectedDate,
- markedDates: _ctrl.recordedDates,
+ markedDates: _ctrl?.recordedDates ?? {},
onDateSelected: (date) {
- _ctrl.selectDate(MealRecordModel.dateKey(date));
+ _ctrl?.selectDate(MealRecordModel.dateKey(date));
},
),
secondChild: GestureDetector(
@@ -130,7 +202,7 @@ class _NutritionCenterPageState extends State {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
- _formatDate(_ctrl.selectedDate.value),
+ _formatDate(_ctrl?.selectedDate.value ?? ''),
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
@@ -154,8 +226,8 @@ class _NutritionCenterPageState extends State {
}
Widget _buildCalorieOverview(bool isDark) {
- final calories = _ctrl.dayNutrition['calories'] ?? 0;
- final goal = _ctrl.caloriesGoal;
+ final calories = _ctrl?.dayNutrition['calories'] ?? 0;
+ final goal = _ctrl?.caloriesGoal ?? 2000;
return GlassContainer(
padding: const EdgeInsets.all(DesignTokens.space4),
@@ -224,7 +296,7 @@ class _NutritionCenterPageState extends State {
),
const SizedBox(width: DesignTokens.space4),
NutritionRing(
- progress: _ctrl.caloriesPercent,
+ progress: _ctrl?.caloriesPercent ?? 0.0,
size: 72,
strokeWidth: 8,
color: DesignTokens.orange,
@@ -263,30 +335,30 @@ class _NutritionCenterPageState extends State {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NutritionRing(
- progress: _ctrl.proteinPercent,
+ progress: _ctrl?.proteinPercent ?? 0.0,
size: 80,
strokeWidth: 8,
color: DesignTokens.red,
centerLabel: 'g',
- centerValue: '${(_ctrl.dayNutrition['protein'] ?? 0).toInt()}',
+ centerValue: '${((_ctrl?.dayNutrition['protein'] ?? 0)).toInt()}',
bottomLabel: '💪 蛋白质',
),
NutritionRing(
- progress: _ctrl.fatPercent,
+ progress: _ctrl?.fatPercent ?? 0.0,
size: 80,
strokeWidth: 8,
color: DesignTokens.secondary,
centerLabel: 'g',
- centerValue: '${(_ctrl.dayNutrition['fat'] ?? 0).toInt()}',
+ centerValue: '${((_ctrl?.dayNutrition['fat'] ?? 0)).toInt()}',
bottomLabel: '🧈 脂肪',
),
NutritionRing(
- progress: _ctrl.carbsPercent,
+ progress: _ctrl?.carbsPercent ?? 0.0,
size: 80,
strokeWidth: 8,
color: DesignTokens.green,
centerLabel: 'g',
- centerValue: '${(_ctrl.dayNutrition['carbs'] ?? 0).toInt()}',
+ centerValue: '${((_ctrl?.dayNutrition['carbs'] ?? 0)).toInt()}',
bottomLabel: '🍞 碳水',
),
],
@@ -300,7 +372,7 @@ class _NutritionCenterPageState extends State {
isDark: isDark,
emoji: MealType.breakfast.emoji,
title: MealType.breakfast.label,
- records: _ctrl.breakfastRecords,
+ records: _ctrl?.breakfastRecords ?? [],
mealType: MealType.breakfast,
),
const SizedBox(height: DesignTokens.space2),
@@ -308,7 +380,7 @@ class _NutritionCenterPageState extends State {
isDark: isDark,
emoji: MealType.lunch.emoji,
title: MealType.lunch.label,
- records: _ctrl.lunchRecords,
+ records: _ctrl?.lunchRecords ?? [],
mealType: MealType.lunch,
),
const SizedBox(height: DesignTokens.space2),
@@ -316,7 +388,7 @@ class _NutritionCenterPageState extends State {
isDark: isDark,
emoji: MealType.dinner.emoji,
title: MealType.dinner.label,
- records: _ctrl.dinnerRecords,
+ records: _ctrl?.dinnerRecords ?? [],
mealType: MealType.dinner,
),
const SizedBox(height: DesignTokens.space2),
@@ -324,7 +396,7 @@ class _NutritionCenterPageState extends State {
isDark: isDark,
emoji: MealType.snack.emoji,
title: MealType.snack.label,
- records: _ctrl.snackRecords,
+ records: _ctrl?.snackRecords ?? [],
mealType: MealType.snack,
),
],
@@ -458,9 +530,11 @@ class _NutritionCenterPageState extends State {
context: context,
builder: (_) => AddMealSheet(
mealType: mealType,
- date: _ctrl.selectedDate.value,
+ date:
+ _ctrl?.selectedDate.value ??
+ MealRecordModel.dateKey(DateTime.now()),
onSaved: (record) {
- _ctrl.addRecord(record);
+ _ctrl?.addRecord(record);
},
),
);
@@ -482,7 +556,7 @@ class _NutritionCenterPageState extends State {
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
- _ctrl.removeRecord(record);
+ _ctrl?.removeRecord(record);
},
child: const Text('删除'),
),
diff --git a/lib/src/pages/nutrition/nutrition_report_page.dart b/lib/src/pages/profile/nutrition/nutrition_report_page.dart
similarity index 77%
rename from lib/src/pages/nutrition/nutrition_report_page.dart
rename to lib/src/pages/profile/nutrition/nutrition_report_page.dart
index b412d51..b5f863a 100644
--- a/lib/src/pages/nutrition/nutrition_report_page.dart
+++ b/lib/src/pages/profile/nutrition/nutrition_report_page.dart
@@ -1,12 +1,12 @@
// 2026-04-09 | NutritionReportPage | 营养报告页面 | iOS26风格周/月趋势分析
// 2026-04-09 | 初始创建,使用fl_chart展示热量趋势和营养素占比
+// 2026-04-10 | 修复卡死问题:添加错误处理+空指针保护
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/controllers/nutrition/meal_record_controller.dart';
+import 'package:mom_kitchen/src/controllers/meal_record_controller.dart';
import 'package:mom_kitchen/src/widgets/base/app_page_scaffold.dart';
-import 'package:mom_kitchen/src/widgets/charts/nutrition_line_chart.dart';
-import 'package:mom_kitchen/src/widgets/charts/nutrition_pie_chart.dart';
+import 'package:mom_kitchen/src/widgets/charts_widgets.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_container.dart';
enum ReportPeriod { weekly, monthly }
@@ -19,21 +19,56 @@ class NutritionReportPage extends StatefulWidget {
}
class _NutritionReportPageState extends State {
- late final MealRecordController _ctrl;
+ MealRecordController? _ctrl;
ReportPeriod _period = ReportPeriod.weekly;
+ String? _error;
@override
void initState() {
super.initState();
- _ctrl = Get.isRegistered()
- ? Get.find()
- : Get.put(MealRecordController());
+
+ try {
+ _ctrl = Get.find();
+ } catch (e) {
+ debugPrint('MealRecordController not found: $e');
+ _error = '控制器初始化失败';
+ _ctrl = null;
+ }
}
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
+ if (_error != null || _ctrl == null) {
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(middle: const Text('📊 营养报告')),
+ child: SafeArea(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('😕', style: TextStyle(fontSize: 48)),
+ const SizedBox(height: DesignTokens.space3),
+ Text(
+ _error ?? '初始化失败',
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
+ ),
+ ),
+ const SizedBox(height: DesignTokens.space4),
+ CupertinoButton.filled(
+ onPressed: () => Get.back(),
+ child: const Text('返回'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
return CupertinoPageScaffold(
backgroundColor: isDark
? DarkDesignTokens.background
@@ -98,11 +133,11 @@ class _NutritionReportPageState extends State {
Widget _buildCalorieTrendCard(bool isDark) {
final caloriesData = _period == ReportPeriod.weekly
- ? _ctrl.weeklyCalories
- : _ctrl.monthlyCalories;
+ ? (_ctrl?.weeklyCalories ?? {})
+ : (_ctrl?.monthlyCalories ?? {});
final avgCalories = _period == ReportPeriod.weekly
- ? _ctrl.getWeeklyAverageCalories()
- : _ctrl.getMonthlyAverageCalories();
+ ? (_ctrl?.getWeeklyAverageCalories() ?? 0)
+ : (_ctrl?.getMonthlyAverageCalories() ?? 0);
return GlassContainer(
padding: const EdgeInsets.all(DesignTokens.space4),
@@ -123,7 +158,7 @@ class _NutritionReportPageState extends State {
const SizedBox(height: DesignTokens.space2),
NutritionLineChart(
data: caloriesData,
- goalValue: _ctrl.caloriesGoal,
+ goalValue: _ctrl?.caloriesGoal ?? 2000,
isWeekly: _period == ReportPeriod.weekly,
),
],
@@ -133,8 +168,10 @@ class _NutritionReportPageState extends State {
Widget _buildNutritionPieCard(bool isDark) {
final nutrition = _period == ReportPeriod.weekly
- ? _ctrl.getWeeklyAggregatedNutrition()
- : _ctrl.getMonthlyAggregatedNutrition();
+ ? (_ctrl?.getWeeklyAggregatedNutrition() ??
+ {'protein': 0, 'fat': 0, 'carbs': 0})
+ : (_ctrl?.getMonthlyAggregatedNutrition() ??
+ {'protein': 0, 'fat': 0, 'carbs': 0});
return GlassContainer(
padding: const EdgeInsets.all(DesignTokens.space4),
@@ -155,12 +192,18 @@ class _NutritionReportPageState extends State {
Widget _buildSummaryCard(bool isDark) {
final nutrition = _period == ReportPeriod.weekly
- ? _ctrl.getWeeklyAggregatedNutrition()
- : _ctrl.getMonthlyAggregatedNutrition();
+ ? (_ctrl?.getWeeklyAggregatedNutrition() ??
+ {'calories': 0})
+ : (_ctrl?.getMonthlyAggregatedNutrition() ??
+ {'calories': 0});
final totalCalories = nutrition['calories'] ?? 0;
final daysWithData = _period == ReportPeriod.weekly
- ? _ctrl.weeklyCalories.values.where((v) => v > 0).length
- : _ctrl.monthlyCalories.values.where((v) => v > 0).length;
+ ? (_ctrl?.weeklyCalories ?? {}).values
+ .where((v) => v > 0)
+ .length
+ : (_ctrl?.monthlyCalories ?? {}).values
+ .where((v) => v > 0)
+ .length;
final avgCalories = daysWithData > 0 ? totalCalories / daysWithData : 0.0;
return GlassContainer(
@@ -210,19 +253,19 @@ class _NutritionReportPageState extends State {
_NutritionBarData(
'💪 蛋白质',
nutrition['protein'] ?? 0,
- _ctrl.proteinGoal,
+ _ctrl?.proteinGoal ?? 50,
DesignTokens.red,
),
_NutritionBarData(
'🧈 脂肪',
nutrition['fat'] ?? 0,
- _ctrl.fatGoal,
+ _ctrl?.fatGoal ?? 70,
DesignTokens.secondary,
),
_NutritionBarData(
'🍞 碳水',
nutrition['carbs'] ?? 0,
- _ctrl.carbsGoal,
+ _ctrl?.carbsGoal ?? 250,
DesignTokens.green,
),
];
diff --git a/lib/src/pages/home/profile_home.dart b/lib/src/pages/profile/profile_home.dart
similarity index 98%
rename from lib/src/pages/home/profile_home.dart
rename to lib/src/pages/profile/profile_home.dart
index e629b8b..fce8d0c 100644
--- a/lib/src/pages/home/profile_home.dart
+++ b/lib/src/pages/profile/profile_home.dart
@@ -9,8 +9,8 @@ import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/controllers/user/profile_controller.dart';
-import 'package:mom_kitchen/src/pages/settings/personalization_page.dart';
-import 'package:mom_kitchen/src/routes/app_routes.dart';
+import 'package:mom_kitchen/src/pages/profile/settings/personalization_page.dart';
+import 'package:mom_kitchen/src/config/app_routes.dart';
class ProfileHomeTab extends StatelessWidget {
const ProfileHomeTab({super.key});
diff --git a/lib/src/pages/profile_page.dart b/lib/src/pages/profile/profile_page.dart
similarity index 97%
rename from lib/src/pages/profile_page.dart
rename to lib/src/pages/profile/profile_page.dart
index d774c0f..0fddcd0 100644
--- a/lib/src/pages/profile_page.dart
+++ b/lib/src/pages/profile/profile_page.dart
@@ -7,7 +7,7 @@
import 'package:flutter/cupertino.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/pages/home/profile_home.dart';
+import 'package:mom_kitchen/src/pages/profile/profile_home.dart';
import 'package:mom_kitchen/src/pages/profile/profile_settings.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_segmented_control.dart';
diff --git a/lib/src/pages/profile/profile_settings.dart b/lib/src/pages/profile/profile_settings.dart
index 0c216b4..5a7c6d7 100644
--- a/lib/src/pages/profile/profile_settings.dart
+++ b/lib/src/pages/profile/profile_settings.dart
@@ -10,10 +10,10 @@ import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/controllers/user/profile_controller.dart';
-import 'package:mom_kitchen/src/pages/settings/personalization_page.dart';
-import 'package:mom_kitchen/src/pages/settings/preference_page.dart';
-import 'package:mom_kitchen/src/pages/what_to_eat/what_to_eat_page.dart';
-import 'package:mom_kitchen/src/pages/hot/hot_page.dart';
+import 'package:mom_kitchen/src/pages/profile/settings/personalization_page.dart';
+import 'package:mom_kitchen/src/pages/profile/settings/preference_page.dart';
+import 'package:mom_kitchen/src/pages/discover/what_to_eat_page.dart';
+import 'package:mom_kitchen/src/pages/discover/hot_page.dart';
class ProfileSettingsTab extends StatelessWidget {
const ProfileSettingsTab({super.key});
diff --git a/lib/src/pages/profile/settings/personalization_page.dart b/lib/src/pages/profile/settings/personalization_page.dart
new file mode 100644
index 0000000..a616273
--- /dev/null
+++ b/lib/src/pages/profile/settings/personalization_page.dart
@@ -0,0 +1,586 @@
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+import 'package:mom_kitchen/src/controllers/user/personalization_controller.dart';
+import 'package:mom_kitchen/src/l10n/app_localizations.dart';
+import 'package:mom_kitchen/src/services/core/app_service.dart';
+import 'package:mom_kitchen/src/services/ui/theme_service.dart';
+import 'package:mom_kitchen/src/widgets/skeleton_widgets.dart';
+import 'package:mom_kitchen/src/widgets/states/standard_dialog.dart';
+
+class PersonalizationPage extends StatelessWidget {
+ const PersonalizationPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return GetBuilder(
+ init: PersonalizationController(),
+ builder: (controller) {
+ final themeService = AppService.instance.theme;
+ final l10n = AppLocalizations.of(context)!;
+
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: const Text('个性化设置'),
+ previousPageTitle: '个人',
+ ),
+ child: Obx(
+ () => SafeArea(
+ child: Container(
+ color: themeService.backgroundColor.value,
+ child: ListView(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ children: [
+ CupertinoListSection.insetGrouped(
+ header: const Text('🎨 主题颜色'),
+ children: [
+ _buildColorPickerItem(controller, themeService),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('📝 字体大小'),
+ children: [
+ _buildFontSizeItem(controller, themeService),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('🌙 显示模式'),
+ children: [
+ CupertinoListTile(
+ title: const Text('深色模式'),
+ trailing: CupertinoSwitch(
+ value: controller.isDarkMode,
+ onChanged: (_) => controller.toggleDarkMode(),
+ ),
+ ),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('✨ 动画效果'),
+ children: [
+ _buildAnimationItem(controller, themeService),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('🌐 语言'),
+ children: _buildLanguageItems(controller, themeService),
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('💬 对话框样式'),
+ children: [
+ ..._buildDialogStyleItems(controller, themeService),
+ CupertinoListTile(
+ title: const Text('启用统一样式(跨平台一致)'),
+ trailing: CupertinoSwitch(
+ value: themeService.unifiedStyleEnabled.value,
+ onChanged: (v) => controller.setUnifiedStyle(v),
+ ),
+ ),
+ CupertinoListTile(
+ title: const Text('显示对话框样式示例'),
+ trailing: const CupertinoListTileChevron(),
+ onTap: () => _showDialogByStyle(themeService),
+ ),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('💭 消息气泡样式'),
+ children: [
+ _buildMessageBubbleItem(controller, themeService),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('🔲 底部栏样式'),
+ children: [
+ _buildBottomBarItem(controller, themeService),
+ if (themeService.bottomBarStyle.value ==
+ BottomBarStyle.floating)
+ _buildBottomBarTransparencyItem(
+ controller,
+ themeService,
+ ),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('🃏 卡片滑动方向'),
+ children: [
+ _buildCardScrollDirectionItem(controller, themeService),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('📱 状态栏'),
+ children: [
+ CupertinoListTile(
+ title: const Text('启用沉浸状态栏'),
+ trailing: CupertinoSwitch(
+ value: themeService.isStatusBarImmersive.value,
+ onChanged: (v) async {
+ await controller.setStatusBarImmersive(v);
+ },
+ ),
+ ),
+ ],
+ ),
+ CupertinoListSection.insetGrouped(
+ header: const Text('预览'),
+ children: [
+ _buildPreviewItem(themeService),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: CupertinoButton.filled(
+ onPressed: () => _showResetDialog(controller, themeService),
+ child: const Text('恢复默认设置'),
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildColorPickerItem(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: List.generate(controller.themePresets.length, (index) {
+ final color = controller.themePresetColors[index];
+ final isSelected =
+ themeService.primaryColor.value.toARGB32() ==
+ color.toARGB32();
+
+ return GestureDetector(
+ onTap: () => controller.setThemeColor(color),
+ child: Container(
+ width: 56,
+ height: 56,
+ margin: const EdgeInsets.only(right: 12),
+ decoration: BoxDecoration(
+ color: color,
+ shape: BoxShape.circle,
+ border: isSelected
+ ? Border.all(
+ color: themeService.textColor.value,
+ width: 3,
+ )
+ : null,
+ boxShadow: [
+ if (isSelected)
+ BoxShadow(
+ color: color.withValues(alpha: 0.5),
+ blurRadius: 8,
+ spreadRadius: 2,
+ ),
+ ],
+ ),
+ child: isSelected
+ ? Icon(
+ CupertinoIcons.checkmark_alt,
+ color: themeService.backgroundColor.value,
+ size: 24,
+ )
+ : null,
+ ),
+ );
+ }),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ controller.currentThemeColorName,
+ style: TextStyle(
+ fontSize: 13,
+ color: themeService.textColor.value.withValues(alpha: 0.6),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildFontSizeItem(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '${controller.currentFontSize.toStringAsFixed(1)} pt',
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ color: themeService.primaryColor.value,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ const Text('A', style: TextStyle(fontSize: 12)),
+ Expanded(
+ child: CupertinoSlider(
+ value: controller.currentFontSize,
+ min: 12.0,
+ max: 24.0,
+ divisions: 12,
+ onChanged: (value) => controller.setFontSize(value),
+ ),
+ ),
+ const Text('A', style: TextStyle(fontSize: 20)),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildAnimationItem(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('动画强度'),
+ Text(
+ '${controller.currentAnimationIntensity.toStringAsFixed(1)}x',
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ color: themeService.primaryColor.value,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ CupertinoSlider(
+ value: controller.currentAnimationIntensity,
+ min: 0.0,
+ max: 2.0,
+ divisions: 10,
+ onChanged: (value) => controller.setAnimationIntensity(value),
+ ),
+ ],
+ ),
+ );
+ }
+
+ List _buildLanguageItems(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return List.generate(controller.languageNames.length, (index) {
+ final languageName = controller.languageNames[index];
+ final languageCode = controller.languageCodes[index];
+ final isSelected = controller.currentLanguage == languageCode;
+
+ return CupertinoListTile(
+ title: Text(languageName),
+ trailing: isSelected
+ ? Icon(
+ CupertinoIcons.checkmark_alt,
+ color: themeService.primaryColor.value,
+ )
+ : null,
+ onTap: () => controller.setLanguage(languageCode),
+ );
+ });
+ }
+
+ List _buildDialogStyleItems(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return List.generate(controller.dialogStyleNames.length, (index) {
+ final name = controller.dialogStyleNames[index];
+ final style = DialogStyle.values[index];
+ final isSelected = themeService.dialogStyle.value == style;
+
+ return CupertinoListTile(
+ title: Text(name),
+ trailing: isSelected
+ ? Icon(
+ CupertinoIcons.checkmark_alt,
+ color: themeService.primaryColor.value,
+ )
+ : null,
+ onTap: () => controller.setDialogStyle(style),
+ );
+ });
+ }
+
+ Widget _buildMessageBubbleItem(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: CupertinoSlidingSegmentedControl(
+ groupValue: themeService.messageBubbleStyle.value,
+ onValueChanged: (v) {
+ if (v != null) controller.setMessageBubbleStyle(v);
+ },
+ children: {
+ for (int i = 0; i < controller.messageBubbleStyleNames.length; i++)
+ MessageBubbleStyle.values[i]: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Text(
+ controller.messageBubbleStyleNames[i],
+ style: const TextStyle(fontSize: 13),
+ ),
+ ),
+ },
+ ),
+ );
+ }
+
+ Widget _buildBottomBarItem(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: CupertinoSlidingSegmentedControl(
+ groupValue: themeService.bottomBarStyle.value,
+ onValueChanged: (v) {
+ if (v != null) controller.setBottomBarStyle(v);
+ },
+ children: {
+ for (int i = 0; i < controller.bottomBarStyleNames.length; i++)
+ BottomBarStyle.values[i]: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Text(
+ controller.bottomBarStyleNames[i],
+ style: const TextStyle(fontSize: 13),
+ ),
+ ),
+ },
+ ),
+ );
+ }
+
+ Widget _buildBottomBarTransparencyItem(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('悬浮栏透明度'),
+ Text(
+ '${(controller.currentBottomBarTransparency * 100).round()}%',
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ color: themeService.primaryColor.value,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ CupertinoSlider(
+ value: controller.currentBottomBarTransparency,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (v) => controller.setBottomBarTransparency(v),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildCardScrollDirectionItem(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: CupertinoSlidingSegmentedControl(
+ groupValue: themeService.cardScrollDirection.value,
+ onValueChanged: (v) {
+ if (v != null) themeService.setCardScrollDirection(v);
+ },
+ children: const {
+ CardScrollDirection.horizontal: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Text('↔️ 左右滑动', style: TextStyle(fontSize: 13)),
+ ),
+ CardScrollDirection.vertical: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Text('↕️ 上下滑动', style: TextStyle(fontSize: 13)),
+ ),
+ },
+ ),
+ );
+ }
+
+ Widget _buildPreviewItem(ThemeService themeService) {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: themeService.backgroundColor.value.withValues(alpha: 0.02),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: themeService.textColor.value.withValues(alpha: 0.04),
+ ),
+ ),
+ child: MessagePreview(
+ style: themeService.messageBubbleStyle.value,
+ theme: themeService,
+ ),
+ ),
+ );
+ }
+
+ void _showDialogByStyle(ThemeService themeService) {
+ final style = themeService.dialogStyle.value;
+ const title = '示例对话框';
+ const content = '这是使用当前对话框样式的演示。';
+
+ if (themeService.unifiedStyleEnabled.value) {
+ StandardDialog.show(
+ Get.context!,
+ title: title,
+ message: content,
+ confirmText: '确定',
+ cancelText: '取消',
+ );
+ return;
+ }
+
+ switch (style) {
+ case DialogStyle.native:
+ showCupertinoDialog(
+ context: Get.context!,
+ builder: (ctx) => CupertinoAlertDialog(
+ title: const Text(title),
+ content: const Text(content),
+ actions: [
+ CupertinoDialogAction(
+ onPressed: () => Get.back(),
+ child: const Text('取消'),
+ ),
+ CupertinoDialogAction(
+ isDestructiveAction: true,
+ onPressed: () => Get.back(),
+ child: const Text('确定'),
+ ),
+ ],
+ ),
+ );
+ break;
+ case DialogStyle.toast:
+ Get.snackbar(title, content, snackPosition: SnackPosition.BOTTOM);
+ break;
+ case DialogStyle.hybrid:
+ Get.dialog(
+ Center(
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: themeService.backgroundColor.value,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ title,
+ style: TextStyle(fontSize: themeService.fontSize.value + 2),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ content,
+ style: TextStyle(fontSize: themeService.fontSize.value),
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: CupertinoButton(
+ onPressed: () => Get.back(),
+ child: const Text('取消'),
+ ),
+ ),
+ Expanded(
+ child: CupertinoButton.filled(
+ onPressed: () => Get.back(),
+ child: const Text('确定'),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ break;
+ case DialogStyle.getx:
+ Get.defaultDialog(
+ title: title,
+ middleText: content,
+ textCancel: '取消',
+ textConfirm: '确定',
+ );
+ break;
+ }
+ }
+
+ void _showResetDialog(
+ PersonalizationController controller,
+ ThemeService themeService,
+ ) {
+ showCupertinoDialog(
+ context: Get.context!,
+ builder: (context) => CupertinoAlertDialog(
+ title: const Text('恢复默认设置'),
+ content: const Text('确定要恢复所有设置到默认值吗?'),
+ actions: [
+ CupertinoDialogAction(
+ onPressed: () => Get.back(),
+ child: const Text('取消'),
+ ),
+ CupertinoDialogAction(
+ isDestructiveAction: true,
+ onPressed: () {
+ Get.back();
+ controller.resetToDefaults();
+ },
+ child: const Text('确定'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/pages/settings/preference_page.dart b/lib/src/pages/profile/settings/preference_page.dart
similarity index 97%
rename from lib/src/pages/settings/preference_page.dart
rename to lib/src/pages/profile/settings/preference_page.dart
index 6ff788a..557cd0e 100644
--- a/lib/src/pages/settings/preference_page.dart
+++ b/lib/src/pages/profile/settings/preference_page.dart
@@ -10,7 +10,6 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/controllers/user/preference_controller.dart';
-import 'package:mom_kitchen/src/controllers/home/home_controller.dart';
import 'package:mom_kitchen/src/services/ui/theme_service.dart';
class PreferencePage extends StatelessWidget {
@@ -99,9 +98,8 @@ class PreferencePage extends StatelessWidget {
PreferenceController prefController,
ThemeService themeService,
) {
- final homeController = Get.find();
return Obx(() {
- final categories = homeController.categories.value;
+ final categories = prefController.availableCategories;
if (categories.isEmpty) {
return const Padding(
padding: EdgeInsets.all(20),
@@ -116,7 +114,7 @@ class PreferencePage extends StatelessWidget {
children: categories.map((cat) {
final isSelected = prefController.isCategoryPreferred(cat.id);
return _buildChip(
- label: '${cat.displayIcon} ${cat.name}',
+ label: '${cat.icon ?? '📂'} ${cat.name}',
icon: '',
isSelected: isSelected,
themeService: themeService,
diff --git a/lib/src/pages/settings/theme_demo_page.dart b/lib/src/pages/profile/settings/theme_demo_page.dart
similarity index 100%
rename from lib/src/pages/settings/theme_demo_page.dart
rename to lib/src/pages/profile/settings/theme_demo_page.dart
diff --git a/lib/src/pages/shopping/shopping_list_page.dart b/lib/src/pages/profile/shopping_list_page.dart
similarity index 83%
rename from lib/src/pages/shopping/shopping_list_page.dart
rename to lib/src/pages/profile/shopping_list_page.dart
index 1ab173f..797aaf2 100644
--- a/lib/src/pages/shopping/shopping_list_page.dart
+++ b/lib/src/pages/profile/shopping_list_page.dart
@@ -4,8 +4,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' show Colors;
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
-import 'package:mom_kitchen/src/controllers/shopping/shopping_list_controller.dart';
-import 'package:mom_kitchen/src/models/shopping/shopping_item_model.dart';
+import 'package:mom_kitchen/src/controllers/shopping_list_controller.dart';
+import 'package:mom_kitchen/src/models/shopping_item_model.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_container.dart';
class ShoppingListPage extends StatefulWidget {
@@ -16,7 +16,13 @@ class ShoppingListPage extends StatefulWidget {
}
class _ShoppingListPageState extends State {
- final ShoppingListController _ctrl = Get.put(ShoppingListController());
+ late final ShoppingListController _ctrl;
+
+ @override
+ void initState() {
+ super.initState();
+ _ctrl = Get.find();
+ }
@override
Widget build(BuildContext context) {
@@ -530,7 +536,7 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return Container(
- height: 400,
+ height: 480,
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
@@ -563,11 +569,19 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> {
),
],
),
- const SizedBox(height: DesignTokens.space3),
+ const SizedBox(height: DesignTokens.space4),
CupertinoTextField(
controller: _nameCtrl,
- placeholder: '食材名称',
- padding: const EdgeInsets.all(DesignTokens.space3),
+ placeholder: '食材名称(必填)',
+ placeholderStyle: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
+ ),
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
+ ),
+ padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.segmentedBg
@@ -575,7 +589,7 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> {
borderRadius: DesignTokens.borderRadiusMd,
),
),
- const SizedBox(height: DesignTokens.space2),
+ const SizedBox(height: DesignTokens.space3),
Row(
children: [
Expanded(
@@ -583,8 +597,16 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> {
child: CupertinoTextField(
controller: _amountCtrl,
placeholder: '数量',
+ placeholderStyle: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
+ ),
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
+ ),
keyboardType: TextInputType.number,
- padding: const EdgeInsets.all(DesignTokens.space3),
+ padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.segmentedBg
@@ -593,12 +615,20 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> {
),
),
),
- const SizedBox(width: DesignTokens.space2),
+ const SizedBox(width: DesignTokens.space3),
Expanded(
child: CupertinoTextField(
controller: _unitCtrl,
placeholder: '单位',
- padding: const EdgeInsets.all(DesignTokens.space3),
+ placeholderStyle: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
+ ),
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
+ ),
+ padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.segmentedBg
@@ -609,28 +639,63 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> {
),
],
),
+ const SizedBox(height: DesignTokens.space4),
+ Text(
+ '选择分类',
+ style: TextStyle(
+ fontSize: DesignTokens.fontMd,
+ fontWeight: FontWeight.w500,
+ color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
+ ),
+ ),
const SizedBox(height: DesignTokens.space3),
- SizedBox(
- width: double.infinity,
- child: CupertinoSlidingSegmentedControl(
- groupValue: _category,
- thumbColor: isDark ? DarkDesignTokens.card : DesignTokens.card,
- onValueChanged: (v) {
- if (v != null) setState(() => _category = v);
- },
- children: {
- for (final c in ShoppingCategory.values)
- c: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: DesignTokens.space3,
- vertical: DesignTokens.space2,
+ Expanded(
+ child: GridView.count(
+ crossAxisCount: 4,
+ mainAxisSpacing: DesignTokens.space2,
+ crossAxisSpacing: DesignTokens.space2,
+ childAspectRatio: 1.2,
+ children: ShoppingCategory.values.map((c) {
+ final isSelected = _category == c;
+ return GestureDetector(
+ onTap: () => setState(() => _category = c),
+ child: Container(
+ decoration: BoxDecoration(
+ color: isSelected
+ ? DesignTokens.primary
+ : isDark
+ ? DarkDesignTokens.segmentedBg
+ : DesignTokens.text3.withValues(alpha: 0.08),
+ borderRadius: DesignTokens.borderRadiusMd,
+ border: Border.all(
+ color: isSelected
+ ? DesignTokens.primary
+ : Colors.transparent,
+ width: 1.5,
+ ),
),
- child: Text(
- '${c.emoji} ${c.label}',
- style: const TextStyle(fontSize: DesignTokens.fontSm),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(c.emoji, style: const TextStyle(fontSize: 24)),
+ const SizedBox(height: 4),
+ Text(
+ c.label,
+ style: TextStyle(
+ fontSize: DesignTokens.fontXs,
+ fontWeight: FontWeight.w500,
+ color: isSelected
+ ? CupertinoColors.white
+ : isDark
+ ? DarkDesignTokens.text2
+ : DesignTokens.text2,
+ ),
+ ),
+ ],
),
),
- },
+ );
+ }).toList(),
),
),
],
diff --git a/lib/src/pages/recipe/recipe_detail_page.dart b/lib/src/pages/recipe/recipe_detail_page.dart
deleted file mode 100644
index 89c4b73..0000000
--- a/lib/src/pages/recipe/recipe_detail_page.dart
+++ /dev/null
@@ -1,479 +0,0 @@
-// 菜谱详情页
-// 创建时间: 2026-04-09
-// 更新时间: 2026-04-09
-// 名称: recipe_detail_page.dart
-// 作用: 展示菜谱详细信息
-// 上次更新内容: 初始创建
-
-import 'package:flutter/cupertino.dart';
-import 'package:get/get.dart';
-import '../../controllers/favorites/favorites_controller.dart';
-import '../../controllers/feed/action_controller.dart';
-import '../../repositories/recipe_repository.dart';
-import '../../models/recipe/recipe_model.dart';
-import '../../models/feed/feed_item_model.dart';
-import '../../services/allergen_checker.dart';
-import '../../config/design_tokens.dart';
-
-class RecipeDetailPage extends StatefulWidget {
- final String recipeId;
-
- const RecipeDetailPage({super.key, required this.recipeId});
-
- @override
- State createState() => _RecipeDetailPageState();
-}
-
-class _RecipeDetailPageState extends State {
- final AllergenChecker _allergenChecker = AllergenChecker();
- final RecipeRepository _recipeRepository = RecipeRepository();
-
- FavoritesController? _favoritesController;
- ActionController? _actionController;
-
- RecipeModel? _recipe;
- bool _isLoading = true;
- String? _error;
- late bool _isFavorited;
- late int _likeCount;
- late List _allergens;
-
- @override
- void initState() {
- super.initState();
- try {
- _favoritesController = Get.find();
- } catch (_) {
- _favoritesController = Get.put(FavoritesController(), permanent: true);
- }
- try {
- _actionController = Get.find();
- } catch (_) {}
- _loadRecipe();
- }
-
- Future _loadRecipe() async {
- setState(() {
- _isLoading = true;
- _error = null;
- });
-
- try {
- final id = int.tryParse(widget.recipeId);
- if (id == null) {
- throw Exception('无效的菜谱ID: ${widget.recipeId}');
- }
-
- final recipe = await _recipeRepository.fetchFull(id);
-
- if (recipe.id <= 0) {
- throw Exception('菜谱数据不完整');
- }
-
- setState(() {
- _recipe = recipe;
- _isFavorited = _favoritesController?.isFavorited(recipe.id) ?? false;
- _likeCount = recipe.statistics?.likes ?? 0;
- _allergens = _allergenChecker.checkAllergens(
- recipe.ingredients.map((e) => e.name).join(', '),
- );
- _isLoading = false;
- });
-
- _actionController?.reportView(id: recipe.id, type: 'recipe');
- } catch (e) {
- setState(() {
- _error = '加载失败: $e';
- _isLoading = false;
- });
- debugPrint('RecipeDetailPage error: $e');
- }
- }
-
- @override
- Widget build(BuildContext context) {
- if (_isLoading) {
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(middle: const Text('加载中...')),
- child: SafeArea(child: Center(child: CupertinoActivityIndicator())),
- );
- }
-
- if (_error != null || _recipe == null) {
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(middle: const Text('错误')),
- child: SafeArea(
- child: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(_error ?? '加载失败'),
- CupertinoButton(
- onPressed: _loadRecipe,
- child: const Text('重试'),
- ),
- ],
- ),
- ),
- ),
- );
- }
-
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(
- middle: Text(_recipe!.title),
- backgroundColor: CupertinoColors.systemBackground,
- trailing: CupertinoButton(
- onPressed: () {
- setState(() {
- _isFavorited = !_isFavorited;
- final feedItem = FeedItemModel.fromRecipe(_recipe!);
- if (_isFavorited) {
- _favoritesController?.addFavorite(feedItem);
- } else {
- _favoritesController?.removeFavorite(_recipe!.id);
- }
- });
- },
- child: Icon(
- _isFavorited ? CupertinoIcons.heart_fill : CupertinoIcons.heart,
- color: _isFavorited
- ? CupertinoColors.systemRed
- : CupertinoColors.label,
- ),
- ),
- ),
- child: SafeArea(
- child: SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildCoverImage(),
- _buildAllergenWarning(),
- _buildBasicInfo(),
- _buildIngredients(),
- _buildSteps(),
- _buildNutritionInfo(),
- _buildActions(),
- const SizedBox(height: 32),
- ],
- ),
- ),
- ),
- );
- }
-
- Widget _buildCoverImage() {
- return Container(
- height: 250,
- width: double.infinity,
- decoration: BoxDecoration(
- color: DesignTokens.text3.withValues(alpha: 0.1),
- image: _recipe!.hasCover
- ? DecorationImage(
- image: NetworkImage(_recipe!.cover!),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child: !_recipe!.hasCover
- ? const Center(
- child: Icon(
- CupertinoIcons.photo,
- size: 64,
- color: DesignTokens.text3,
- ),
- )
- : null,
- );
- }
-
- Widget _buildAllergenWarning() {
- if (_allergens.isEmpty) return const SizedBox();
-
- return Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: DesignTokens.orange.withValues(alpha: 0.1),
- borderRadius: DesignTokens.borderRadiusMd,
- ),
- child: Row(
- children: [
- const Icon(
- CupertinoIcons.exclamationmark_triangle,
- color: DesignTokens.orange,
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Text(
- _allergenChecker.generateWarningMessage(_allergens),
- style: const TextStyle(color: DesignTokens.orange, fontSize: 14),
- ),
- ),
- ],
- ),
- );
- }
-
- Widget _buildBasicInfo() {
- return Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- _recipe!.title,
- style: const TextStyle(
- fontSize: 24,
- fontWeight: FontWeight.bold,
- color: DesignTokens.text1,
- ),
- ),
- const SizedBox(height: 8),
- if (_recipe!.intro != null && _recipe!.intro!.isNotEmpty)
- Text(
- _recipe!.intro!,
- style: const TextStyle(fontSize: 14, color: DesignTokens.text2),
- ),
- const SizedBox(height: 12),
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: _recipe!.tags.map((tag) {
- return Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 6,
- ),
- decoration: BoxDecoration(
- color: DesignTokens.primaryLight,
- borderRadius: DesignTokens.borderRadiusSm,
- ),
- child: Text(
- tag.name,
- style: const TextStyle(
- fontSize: 12,
- color: DesignTokens.primary,
- ),
- ),
- );
- }).toList(),
- ),
- ],
- ),
- );
- }
-
- Widget _buildIngredients() {
- if (_recipe!.ingredients.isEmpty) return const SizedBox();
-
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- '食材',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- color: DesignTokens.text1,
- ),
- ),
- const SizedBox(height: 12),
- ...(_recipe!.ingredients.map(
- (ingredient) => Padding(
- padding: const EdgeInsets.only(bottom: 8),
- child: Row(
- children: [
- const Icon(
- CupertinoIcons.checkmark_circle,
- size: 18,
- color: DesignTokens.green,
- ),
- const SizedBox(width: 12),
- Text(
- '${ingredient.name} ${ingredient.displayAmount}',
- style: const TextStyle(
- fontSize: 16,
- color: DesignTokens.text1,
- ),
- ),
- ],
- ),
- ),
- )),
- ],
- ),
- );
- }
-
- Widget _buildSteps() {
- if (_recipe!.content == null || _recipe!.content!.isEmpty) {
- return const SizedBox();
- }
-
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- '做法',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- color: DesignTokens.text1,
- ),
- ),
- const SizedBox(height: 12),
- Text(
- _recipe!.content!,
- style: const TextStyle(
- fontSize: 16,
- color: DesignTokens.text1,
- height: 1.6,
- ),
- ),
- ],
- ),
- );
- }
-
- Widget _buildNutritionInfo() {
- if (_recipe!.nutrition == null) return const SizedBox();
-
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: DesignTokens.card,
- borderRadius: DesignTokens.borderRadiusMd,
- border: Border.all(
- color: DesignTokens.text3.withValues(alpha: 0.2),
- width: 1,
- ),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- '营养信息',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.bold,
- color: DesignTokens.text1,
- ),
- ),
- const SizedBox(height: 12),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- _buildNutritionItem(
- '热量',
- '${_recipe!.nutrition!.calories?.toInt() ?? 0} kcal',
- ),
- _buildNutritionItem(
- '蛋白质',
- '${_recipe!.nutrition!.protein?.toInt() ?? 0} g',
- ),
- _buildNutritionItem(
- '脂肪',
- '${_recipe!.nutrition!.fat?.toInt() ?? 0} g',
- ),
- _buildNutritionItem(
- '碳水',
- '${_recipe!.nutrition!.carbs?.toInt() ?? 0} g',
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _buildNutritionItem(String label, String value) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Text(
- value,
- style: const TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.bold,
- color: DesignTokens.primary,
- ),
- ),
- Text(
- label,
- style: const TextStyle(fontSize: 12, color: DesignTokens.text2),
- ),
- ],
- );
- }
-
- Widget _buildActions() {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- CupertinoButton(
- onPressed: () {
- setState(() {
- _likeCount++;
- });
- _actionController?.likeItem(id: _recipe!.id, type: 'recipe');
- },
- child: Column(
- children: [
- const Icon(CupertinoIcons.heart, size: 24),
- const SizedBox(height: 4),
- Text('$_likeCount'),
- ],
- ),
- ),
- CupertinoButton(
- onPressed: () {
- _actionController?.recommendItem(id: _recipe!.id, type: 'recipe');
- Get.snackbar('成功', '推荐成功');
- },
- child: Column(
- children: [
- const Icon(CupertinoIcons.star, size: 24),
- const SizedBox(height: 4),
- const Text('推荐'),
- ],
- ),
- ),
- CupertinoButton(
- onPressed: () {
- Get.snackbar('提示', '烹饪笔记功能开发中');
- },
- child: Column(
- children: [
- const Icon(CupertinoIcons.pencil, size: 24),
- const SizedBox(height: 4),
- const Text('笔记'),
- ],
- ),
- ),
- CupertinoButton(
- onPressed: () {
- Get.snackbar('提示', '已添加到购物清单');
- },
- child: Column(
- children: [
- const Icon(CupertinoIcons.cart, size: 24),
- const SizedBox(height: 4),
- const Text('购物'),
- ],
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/src/pages/settings/personalization_page.dart b/lib/src/pages/settings/personalization_page.dart
deleted file mode 100644
index 07d1383..0000000
--- a/lib/src/pages/settings/personalization_page.dart
+++ /dev/null
@@ -1,932 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:get/get.dart';
-import 'package:mom_kitchen/src/controllers/user/personalization_controller.dart';
-import 'package:mom_kitchen/src/l10n/app_localizations.dart';
-import 'package:mom_kitchen/src/services/core/app_service.dart';
-import 'package:mom_kitchen/src/services/ui/theme_service.dart';
-import 'package:mom_kitchen/src/widgets/content/message_preview.dart';
-import 'package:mom_kitchen/src/widgets/states/standard_dialog.dart';
-
-class PersonalizationPage extends StatelessWidget {
- const PersonalizationPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- return GetBuilder(
- init: PersonalizationController(),
- builder: (controller) {
- final themeService = AppService.instance.theme;
- final l10n = AppLocalizations.of(context)!;
-
- return CupertinoPageScaffold(
- navigationBar: CupertinoNavigationBar(
- middle: Text('个性化设置'),
- previousPageTitle: '个人',
- ),
- child: Obx(
- () => SafeArea(
- child: Container(
- color: themeService.backgroundColor.value,
- child: ListView(
- padding: const EdgeInsets.symmetric(vertical: 16),
- children: [
- // 主题颜色设置
- _buildSectionHeader('🎨 主题颜色', themeService),
- _buildColorPicker(controller, themeService),
- const SizedBox(height: 20),
-
- // 字体大小设置
- _buildSectionHeader('📝 字体大小', themeService),
- _buildFontSizeSlider(controller, themeService),
- const SizedBox(height: 20),
-
- // 深色模式
- _buildSectionHeader('🌙 显示模式', themeService),
- _buildDarkModeToggle(controller, themeService, l10n),
- const SizedBox(height: 20),
-
- // 动画设置
- _buildSectionHeader('✨ 动画效果', themeService),
- _buildAnimationSettings(controller, themeService),
- const SizedBox(height: 20),
-
- // 语言设置
- _buildSectionHeader('🌐 语言', themeService),
- _buildLanguageSelector(controller, themeService),
- const SizedBox(height: 20),
-
- // 对话框样式
- _buildSectionHeader('💬 对话框样式', themeService),
- _buildDialogStyleSelector(controller, themeService),
- const SizedBox(height: 20),
- _buildUnifiedStyleToggle(controller, themeService),
- const SizedBox(height: 12),
-
- // 消息气泡样式
- _buildSectionHeader('💭 消息气泡样式', themeService),
- _buildMessageBubbleSelector(controller, themeService),
- const SizedBox(height: 20),
-
- // 底部栏样式
- _buildSectionHeader('🔲 底部栏样式', themeService),
- _buildBottomBarSelector(controller, themeService),
- const SizedBox(height: 20),
-
- _buildSectionHeader('🃏 卡片滑动方向', themeService),
- _buildCardScrollDirectionSelector(controller, themeService),
- const SizedBox(height: 20),
- // 悬浮透明度(仅当悬浮样式启用时显示)
- if (themeService.bottomBarStyle.value ==
- BottomBarStyle.floating)
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '悬浮栏透明度',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- const SizedBox(height: 8),
- Row(
- children: [
- Expanded(
- child: CupertinoSlider(
- value:
- controller.currentBottomBarTransparency,
- min: 0.0,
- max: 1.0,
- divisions: 20,
- onChanged: (v) =>
- controller.setBottomBarTransparency(v),
- ),
- ),
- const SizedBox(width: 8),
- Text(
- '${(controller.currentBottomBarTransparency * 100).round()}%',
- style: TextStyle(
- color: themeService.textColor.value,
- fontSize: themeService.fontSize.value - 2,
- ),
- ),
- ],
- ),
- const SizedBox(height: 12),
- ],
- ),
- ),
-
- // 预览区域
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '预览',
- style: TextStyle(
- fontSize: themeService.fontSize.value + 1,
- fontWeight: FontWeight.w600,
- color: themeService.textColor.value,
- ),
- ),
- const SizedBox(height: 12),
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: themeService.backgroundColor.value
- .withValues(alpha: 0.02),
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: themeService.textColor.value.withValues(
- alpha: 0.04,
- ),
- ),
- ),
- child: MessagePreview(
- style: themeService.messageBubbleStyle.value,
- theme: themeService,
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 20),
-
- // 沉浸状态栏
- _buildSectionHeader('📱 沉浸状态栏', themeService),
- _buildStatusBarToggle(controller, themeService),
- const SizedBox(height: 20),
- _buildDialogDemoButton(themeService),
- const SizedBox(height: 20),
-
- // 重置按钮
- _buildResetButton(controller, themeService),
- const SizedBox(height: 20),
- ],
- ),
- ),
- ),
- ),
- );
- },
- );
- }
-
- Widget _buildDialogStyleSelector(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: List.generate(controller.dialogStyleNames.length, (index) {
- final name = controller.dialogStyleNames[index];
- final style = DialogStyle.values[index];
- final isSelected = themeService.dialogStyle.value == style;
-
- return GestureDetector(
- onTap: () => controller.setDialogStyle(style),
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
- margin: const EdgeInsets.only(bottom: 8),
- decoration: BoxDecoration(
- color: isSelected
- ? themeService.primaryColor.value.withValues(alpha: 0.2)
- : themeService.backgroundColor.value,
- border: Border.all(
- color: isSelected
- ? themeService.primaryColor.value
- : themeService.textColor.value.withValues(alpha: 0.2),
- width: isSelected ? 2 : 1,
- ),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- name,
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- fontWeight: isSelected
- ? FontWeight.bold
- : FontWeight.normal,
- ),
- ),
- if (isSelected)
- Icon(
- CupertinoIcons.checkmark_alt,
- color: themeService.primaryColor.value,
- size: 20,
- ),
- ],
- ),
- ),
- );
- }),
- ),
- );
- }
-
- Widget _buildUnifiedStyleToggle(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: CupertinoListTile(
- title: Text(
- '启用统一样式(跨平台一致)',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- trailing: CupertinoSwitch(
- value: themeService.unifiedStyleEnabled.value,
- onChanged: (v) => controller.setUnifiedStyle(v),
- ),
- ),
- );
- }
-
- void _showDialogByStyle(ThemeService themeService) {
- final style = themeService.dialogStyle.value;
- final title = '示例对话框';
- final content = '这是使用当前对话框样式的演示。';
-
- // 如果启用了统一样式,使用 StandardDialog(跨平台一致)
- if (themeService.unifiedStyleEnabled.value) {
- StandardDialog.show(
- Get.context!,
- title: title,
- message: content,
- confirmText: '确定',
- cancelText: '取消',
- );
- return;
- }
-
- switch (style) {
- case DialogStyle.native:
- showCupertinoDialog(
- context: Get.context!,
- builder: (ctx) => CupertinoAlertDialog(
- title: Text(title),
- content: Text(content),
- actions: [
- CupertinoDialogAction(
- onPressed: () => Get.back(),
- child: const Text('取消'),
- ),
- CupertinoDialogAction(
- isDestructiveAction: true,
- onPressed: () => Get.back(),
- child: const Text('确定'),
- ),
- ],
- ),
- );
- break;
- case DialogStyle.toast:
- Get.snackbar(title, content, snackPosition: SnackPosition.BOTTOM);
- break;
- case DialogStyle.hybrid:
- // 混合:先用 Get.dialog 展示富文本,再显示原生确认
- Get.dialog(
- Center(
- child: Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: themeService.backgroundColor.value,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- title,
- style: TextStyle(fontSize: themeService.fontSize.value + 2),
- ),
- const SizedBox(height: 8),
- Text(
- content,
- style: TextStyle(fontSize: themeService.fontSize.value),
- ),
- const SizedBox(height: 12),
- Row(
- children: [
- Expanded(
- child: CupertinoButton(
- onPressed: () => Get.back(),
- child: const Text('取消'),
- ),
- ),
- Expanded(
- child: CupertinoButton.filled(
- onPressed: () => Get.back(),
- child: const Text('确定'),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- break;
- case DialogStyle.getx:
- Get.defaultDialog(
- title: title,
- middleText: content,
- textCancel: '取消',
- textConfirm: '确定',
- );
- break;
- }
- }
-
- Widget _buildMessageBubbleSelector(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: List.generate(controller.messageBubbleStyleNames.length, (
- index,
- ) {
- final name = controller.messageBubbleStyleNames[index];
- final style = MessageBubbleStyle.values[index];
- final isSelected = themeService.messageBubbleStyle.value == style;
-
- return GestureDetector(
- onTap: () => controller.setMessageBubbleStyle(style),
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
- margin: const EdgeInsets.only(bottom: 8),
- decoration: BoxDecoration(
- color: isSelected
- ? themeService.primaryColor.value.withValues(alpha: 0.12)
- : themeService.backgroundColor.value,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: isSelected
- ? themeService.primaryColor.value
- : themeService.textColor.value.withValues(alpha: 0.08),
- ),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- name,
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- if (isSelected)
- Icon(
- CupertinoIcons.checkmark_alt,
- color: themeService.primaryColor.value,
- size: 18,
- ),
- ],
- ),
- ),
- );
- }),
- ),
- );
- }
-
- Widget _buildBottomBarSelector(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: List.generate(controller.bottomBarStyleNames.length, (index) {
- final name = controller.bottomBarStyleNames[index];
- final style = BottomBarStyle.values[index];
- final isSelected = themeService.bottomBarStyle.value == style;
-
- return GestureDetector(
- onTap: () => controller.setBottomBarStyle(style),
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
- margin: const EdgeInsets.only(bottom: 8),
- decoration: BoxDecoration(
- color: isSelected
- ? themeService.primaryColor.value.withAlpha(
- (0.12 * 255).round(),
- )
- : themeService.backgroundColor.value,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: isSelected
- ? themeService.primaryColor.value
- : themeService.textColor.value.withAlpha(
- (0.08 * 255).round(),
- ),
- ),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- name,
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- if (isSelected)
- Icon(
- CupertinoIcons.checkmark_alt,
- color: themeService.primaryColor.value,
- size: 18,
- ),
- ],
- ),
- ),
- );
- }),
- ),
- );
- }
-
- Widget _buildCardScrollDirectionSelector(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- final names = ['↔️ 左右滑动', '↕️ 上下滑动'];
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: List.generate(CardScrollDirection.values.length, (index) {
- final name = names[index];
- final direction = CardScrollDirection.values[index];
- final isSelected =
- themeService.cardScrollDirection.value == direction;
-
- return GestureDetector(
- onTap: () => themeService.setCardScrollDirection(direction),
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
- margin: const EdgeInsets.only(bottom: 8),
- decoration: BoxDecoration(
- color: isSelected
- ? themeService.primaryColor.value.withValues(alpha: 0.12)
- : themeService.backgroundColor.value,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: isSelected
- ? themeService.primaryColor.value
- : themeService.textColor.value.withValues(alpha: 0.08),
- ),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- name,
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- if (isSelected)
- Icon(
- CupertinoIcons.checkmark_alt,
- color: themeService.primaryColor.value,
- size: 18,
- ),
- ],
- ),
- ),
- );
- }),
- ),
- );
- }
-
- Widget _buildStatusBarToggle(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: CupertinoListTile(
- title: Text(
- '启用沉浸状态栏',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- trailing: CupertinoSwitch(
- value: themeService.isStatusBarImmersive.value,
- onChanged: (v) async {
- await controller.setStatusBarImmersive(v);
- },
- ),
- ),
- );
- }
-
- Widget _buildDialogDemoButton(ThemeService themeService) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: CupertinoButton.filled(
- onPressed: () => _showDialogByStyle(themeService),
- child: const Text('显示对话框样式示例'),
- ),
- );
- }
-
- Widget _buildSectionHeader(String title, ThemeService themeService) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: Text(
- title,
- style: TextStyle(
- fontSize: themeService.fontSize.value + 2,
- fontWeight: FontWeight.bold,
- color: themeService.primaryColor.value,
- ),
- ),
- );
- }
-
- Widget _buildColorPicker(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '选择主题颜色',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- const SizedBox(height: 12),
- SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(
- children: List.generate(controller.themePresets.length, (index) {
- final color = controller.themePresetColors[index];
- final isSelected =
- themeService.primaryColor.value.toARGB32() ==
- color.toARGB32();
-
- return GestureDetector(
- onTap: () => controller.setThemeColor(color),
- child: Container(
- width: 60,
- height: 60,
- margin: const EdgeInsets.only(right: 12),
- decoration: BoxDecoration(
- color: color,
- shape: BoxShape.circle,
- border: isSelected
- ? Border.all(
- color: themeService.textColor.value,
- width: 3,
- )
- : null,
- boxShadow: [
- if (isSelected)
- BoxShadow(
- color: color.withValues(alpha: 0.5),
- blurRadius: 8,
- spreadRadius: 2,
- ),
- ],
- ),
- child: isSelected
- ? Icon(
- CupertinoIcons.checkmark_alt,
- color: themeService.backgroundColor.value,
- size: 24,
- )
- : null,
- ),
- );
- }),
- ),
- ),
- const SizedBox(height: 12),
- Text(
- controller.currentThemeColorName,
- style: TextStyle(
- fontSize: themeService.fontSize.value - 2,
- color: themeService.textColor.value.withValues(alpha: 0.7),
- ),
- ),
- ],
- ),
- );
- }
-
- Widget _buildFontSizeSlider(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- '字体大小',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 6,
- ),
- decoration: BoxDecoration(
- color: themeService.primaryColor.value.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Text(
- controller.currentFontSize.toStringAsFixed(1),
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- fontWeight: FontWeight.bold,
- color: themeService.primaryColor.value,
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 12),
- CupertinoSlider(
- value: controller.currentFontSize,
- min: 12.0,
- max: 24.0,
- divisions: 12,
- onChanged: (value) => controller.setFontSize(value),
- ),
- const SizedBox(height: 8),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- '小',
- style: TextStyle(
- fontSize: themeService.fontSize.value - 2,
- color: themeService.textColor.value.withValues(alpha: 0.5),
- ),
- ),
- Text(
- '大',
- style: TextStyle(
- fontSize: themeService.fontSize.value - 2,
- color: themeService.textColor.value.withValues(alpha: 0.5),
- ),
- ),
- ],
- ),
- ],
- ),
- );
- }
-
- Widget _buildDarkModeToggle(
- PersonalizationController controller,
- ThemeService themeService,
- AppLocalizations l10n,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: CupertinoListTile(
- title: Text(
- '深色模式',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- trailing: CupertinoSwitch(
- value: controller.isDarkMode,
- onChanged: (_) => controller.toggleDarkMode(),
- ),
- ),
- );
- }
-
- Widget _buildAnimationSettings(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '动画强度',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- const SizedBox(height: 12),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Expanded(
- child: CupertinoSlider(
- value: controller.currentAnimationIntensity,
- min: 0.0,
- max: 2.0,
- divisions: 10,
- onChanged: (value) => controller.setAnimationIntensity(value),
- ),
- ),
- const SizedBox(width: 12),
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 6,
- ),
- decoration: BoxDecoration(
- color: themeService.primaryColor.value.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Text(
- '${controller.currentAnimationIntensity.toStringAsFixed(1)}x',
- style: TextStyle(
- fontSize: themeService.fontSize.value - 2,
- fontWeight: FontWeight.bold,
- color: themeService.primaryColor.value,
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- );
- }
-
- Widget _buildLanguageSelector(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '选择语言',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- ),
- ),
- const SizedBox(height: 12),
- Column(
- children: List.generate(controller.languageNames.length, (index) {
- final languageName = controller.languageNames[index];
- final languageCode = controller.languageCodes[index];
- final isSelected = controller.currentLanguage == languageCode;
-
- return GestureDetector(
- onTap: () => controller.setLanguage(languageCode),
- child: Container(
- padding: const EdgeInsets.symmetric(
- vertical: 12,
- horizontal: 12,
- ),
- margin: const EdgeInsets.only(bottom: 8),
- decoration: BoxDecoration(
- color: isSelected
- ? themeService.primaryColor.value.withValues(alpha: 0.2)
- : themeService.backgroundColor.value,
- border: Border.all(
- color: isSelected
- ? themeService.primaryColor.value
- : themeService.textColor.value.withValues(alpha: 0.2),
- width: isSelected ? 2 : 1,
- ),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- languageName,
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: themeService.textColor.value,
- fontWeight: isSelected
- ? FontWeight.bold
- : FontWeight.normal,
- ),
- ),
- if (isSelected)
- Icon(
- CupertinoIcons.checkmark_alt,
- color: themeService.primaryColor.value,
- size: 20,
- ),
- ],
- ),
- ),
- );
- }),
- ),
- ],
- ),
- );
- }
-
- Widget _buildResetButton(
- PersonalizationController controller,
- ThemeService themeService,
- ) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: GestureDetector(
- onTap: () {
- showCupertinoDialog(
- context: Get.context!,
- builder: (context) => CupertinoAlertDialog(
- title: Text(
- '恢复默认设置',
- style: TextStyle(color: themeService.textColor.value),
- ),
- content: Text(
- '确定要恢复所有设置到默认值吗?',
- style: TextStyle(color: themeService.textColor.value),
- ),
- actions: [
- CupertinoDialogAction(
- onPressed: () => Get.back(),
- child: Text(
- '取消',
- style: TextStyle(color: themeService.primaryColor.value),
- ),
- ),
- CupertinoDialogAction(
- isDestructiveAction: true,
- onPressed: () {
- Get.back();
- controller.resetToDefaults();
- },
- child: const Text('确定'),
- ),
- ],
- ),
- );
- },
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 12),
- decoration: BoxDecoration(
- color: CupertinoColors.systemRed.withValues(alpha: 0.1),
- border: Border.all(color: CupertinoColors.systemRed, width: 2),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Center(
- child: Text(
- '恢复默认设置',
- style: TextStyle(
- fontSize: themeService.fontSize.value,
- color: CupertinoColors.systemRed,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/src/pages/tools/allergen_checker_page.dart b/lib/src/pages/tools/allergen_checker_page.dart
new file mode 100644
index 0000000..863dda8
--- /dev/null
+++ b/lib/src/pages/tools/allergen_checker_page.dart
@@ -0,0 +1,361 @@
+/*
+ * 文件: allergen_checker_page.dart
+ * 名称: 过敏原检查工具页面
+ * 作用: 检查食材过敏原信息,支持搜索和分类浏览
+ * 更新: 2026-04-10 初始创建
+ */
+
+import 'package:flutter/cupertino.dart';
+import 'package:mom_kitchen/src/config/design_tokens.dart';
+import 'package:dio/dio.dart';
+
+class AllergenCheckerPage extends StatefulWidget {
+ const AllergenCheckerPage({super.key});
+
+ @override
+ State createState() => _AllergenCheckerPageState();
+}
+
+class _AllergenCheckerPageState extends State {
+ final TextEditingController _searchController = TextEditingController();
+ final Dio _dio = Dio();
+ List