From 5150501643717f675a4bf17c5c7706c7b206e050 Mon Sep 17 00:00:00 2001 From: Developer Date: Mon, 13 Apr 2026 05:11:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=87=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 999 +++--------------- docs/dev/PAGE_STRUCTURE_ANALYSIS.md | 746 +++++++++++++ docs/dev/UNFINISHED_FEATURES.md | 498 ++------- lib/src/config/app_routes.dart | 8 + lib/src/config/design_tokens.dart | 45 + lib/src/controllers/search_controller.dart | 288 +++-- lib/src/models/recipe/category_model.dart | 2 +- lib/src/models/recipe/recipe_model.dart | 154 ++- .../pages/discover/category_browse_page.dart | 524 ++++----- lib/src/pages/discover/discover_page.dart | 409 ++++--- lib/src/pages/discover/hot_page.dart | 4 +- .../discover/ingredient_recipe_list_page.dart | 16 +- lib/src/pages/discover/what_to_eat_page.dart | 102 +- lib/src/pages/home/advanced_search_page.dart | 508 +++++++++ lib/src/pages/home/home_card_carousel.dart | 31 +- lib/src/pages/home/home_page.dart | 2 +- lib/src/pages/home/recipe_detail_page.dart | 7 +- lib/src/pages/home/search_page.dart | 579 +++++++--- lib/src/pages/home/tag_recipe_list_page.dart | 30 +- .../pages/profile/bedtime_reminder_page.dart | 56 +- lib/src/pages/profile/cache_manage_page.dart | 32 +- lib/src/pages/profile/chat_page.dart | 46 +- lib/src/pages/profile/data_center_page.dart | 62 +- lib/src/pages/profile/favorites_page.dart | 70 +- lib/src/pages/profile/footprints_page.dart | 36 +- .../profile/nutrition/add_meal_sheet.dart | 16 +- .../profile/nutrition/goal_setting_page.dart | 14 +- .../nutrition/nutrition_center_page.dart | 22 +- lib/src/pages/profile/profile_home.dart | 32 +- lib/src/pages/profile/profile_settings.dart | 18 +- .../settings/personalization_page.dart | 10 +- .../profile/settings/preference_page.dart | 3 +- .../profile/settings/theme_demo_page.dart | 23 +- lib/src/pages/profile/shopping_list_page.dart | 38 +- .../pages/tools/allergen_checker_page.dart | 4 +- lib/src/pages/tools/cooking_note_page.dart | 30 +- lib/src/pages/tools/cooking_timer_page.dart | 36 +- lib/src/pages/tools/eating_times_page.dart | 32 +- .../pages/tools/ingredient_detail_page.dart | 60 +- lib/src/pages/tools/meal_planner_page.dart | 20 +- .../pages/tools/meal_time_recommend_page.dart | 12 +- lib/src/pages/tools/serving_scaler_page.dart | 54 +- lib/src/pages/tools/tools_center_page.dart | 106 +- lib/src/pages/tools/unit_converter_page.dart | 14 +- .../pages/tools/weekly_menu_planner_page.dart | 28 +- lib/src/repositories/recipe_repository.dart | 86 ++ lib/src/services/ui/animation_service.dart | 15 +- lib/src/services/ui/theme_service.dart | 10 +- lib/src/services/ui/toast_service.dart | 2 +- lib/src/standards/page_validator.dart | 3 +- lib/src/widgets/adaptive_widgets.dart | 5 +- lib/src/widgets/base/app_page_scaffold.dart | 10 +- lib/src/widgets/base/standard_button.dart | 7 +- .../widgets/base/tap_liquid_glass_nav.dart | 5 +- lib/src/widgets/charts_widgets.dart | 12 +- lib/src/widgets/custom_widgets.dart | 14 +- .../discover/category_discover_card.dart | 12 +- .../widgets/discover/discover_waterfall.dart | 26 +- .../discover/ingredient_discover_card.dart | 6 +- .../discover/recipe_discover_card.dart | 12 +- lib/src/widgets/glass/glass_feed_card.dart | 12 +- lib/src/widgets/glass/glass_nav_bar.dart | 6 +- .../glass/glass_segmented_control.dart | 6 +- .../widgets/glass/glass_settings_tile.dart | 8 +- lib/src/widgets/glass/home_app_bar.dart | 10 +- .../widgets/glass/liquid_glass_nav_bar.dart | 6 +- lib/src/widgets/nutrition_dashboard_card.dart | 16 +- lib/src/widgets/product_card.dart | 5 +- .../recipe_detail/recipe_action_bar.dart | 2 +- .../recipe_detail/recipe_author_card.dart | 8 +- .../recipe_category_breadcrumb.dart | 134 ++- .../recipe_detail/recipe_cover_image.dart | 44 +- .../recipe_detail/recipe_indices_card.dart | 12 +- .../recipe_ingredient_details.dart | 6 +- .../recipe_detail/recipe_meta_info_card.dart | 6 +- .../recipe_nutrition_section.dart | 12 +- .../recipe_detail/recipe_picid_card.dart | 24 +- .../recipe_detail/recipe_skeleton_view.dart | 8 +- .../recipe_detail/recipe_statistics_bar.dart | 167 ++- .../recipe_detail/recipe_steps_section.dart | 16 +- .../recipe_detail/recipe_tags_section.dart | 86 +- .../recipe_detail/recipe_time_info.dart | 2 +- lib/src/widgets/recipe_image.dart | 10 +- lib/src/widgets/skeleton_widgets.dart | 4 +- lib/src/widgets/states/standard_dialog.dart | 23 +- scripts/debug_timestamp.dart | 32 - scripts/test_detail_cache.dart | 100 -- scripts/test_discover_api.dart | 135 --- 88 files changed, 3984 insertions(+), 2967 deletions(-) create mode 100644 docs/dev/PAGE_STRUCTURE_ANALYSIS.md create mode 100644 lib/src/pages/home/advanced_search_page.dart delete mode 100644 scripts/debug_timestamp.dart delete mode 100644 scripts/test_detail_cache.dart delete mode 100644 scripts/test_discover_api.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ad617..134c35b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,897 +2,152 @@ All notable changes to this project will be documented in this file. -## [0.91.13] - 2026-04-13 +## [0.91.18] - 2026-04-13 -### 🐛 详情页API请求参数丢失问题修复 +### 🎨 修复主题色切换全局不生效 + 统一动态主题色引用 -#### 问题1:加载更多后卡片消失 -- **根因**:`api_discover.php` 每次请求返回新的随机数据,不是增量追加 -- **修复**: - - 在 `DiscoverData` 模型中新增 `merge()` 方法支持数据累积 - - 修改 `_loadMoreDiscover()` 使用 `merge()` 合并新旧数据 - - 新增 `clearCache()` 和 `totalItems` 辅助方法 +#### 核心Bug修复 +- 🎨 **主题色切换全局不生效** — 根因:UI组件使用 `DesignTokens.primary`(静态常量)而非 `DesignTokens.dynamicPrimary`(动态读取ThemeService) + - 批量替换 `isDark ? DarkDesignTokens.primary : DesignTokens.primary` → `DesignTokens.dynamicPrimary`(34个文件) + - 批量替换 `DesignTokens.primary` → `DesignTokens.dynamicPrimary`(33个文件,排除design_tokens.dart定义和theme_service.dart默认值) + - 修复 `DarkDesignTokens.dynamicPrimary` 误替换为 `DesignTokens.dynamicPrimary`(5个文件) + - 修复 `const` 上下文错误:移除包含 `dynamicPrimary` 的 `const` 构造函数关键字 + - 修复默认参数值错误:`AppProgressBar.color` 和 `NutritionLineChart.lineColor` 改为可空+初始化列表 -#### 问题2:详情页显示"暂无数据" -- **根因**:`ApiService.get()` 方法在非Web平台未正确传递 `queryParameters` 给 Dio -- **表现**:请求URL变成 `https://eat.wktyl.com/api/api.php`(无参数),返回API根信息而非菜谱详情 -- **修复**: - - 修改 `ApiService.get()` 方法,将 `queryParameters` 传递给 Dio 的 `get()` 方法 - - 添加无效缓存检测和自动重试机制 - - 在 `RecipeRepository.fetchFull()` 添加详细调试日志 +#### 影响范围 +- 全部59个dart文件中的439处 `DesignTokens.primary` / `DarkDesignTokens.primary` 引用 +- 主题色切换现在能即时生效于所有页面 -#### 问题3:RenderFlex溢出错误 -- **根因**:`discover_waterfall.dart` 中底部按钮使用 `Row` 布局,在小屏幕上溢出 -- **修复**:将 `Row` 改为 `Wrap` 布局,支持自动换行 +## [0.91.17] - 2026-04-13 -#### 技术细节 -- Dio的 `get()` 方法需要通过 `queryParameters` 参数传递查询参数 -- `_buildUrl()` 方法在非Web平台只返回基础URL,不包含查询参数 -- 修复后请求URL正确:`https://eat.wktyl.com/api/api.php?act=full&id=xxx&viewnums=true&_refresh=1` - -## [0.91.12] - 2026-04-13 - -### 🐛 首页瀑布流四大问题修复 - -#### 问题1:滑动时单排卡片布局问题 -- **根因**:部分卡片(如CategoryDiscoverCard)没有设置最小高度约束,导致高度为0时影响瀑布流布局 -- **修复**: - - 为所有卡片添加 `ConstrainedBox(minHeight: 80)` 最小高度约束 - - 优化 `CategoryDiscoverCard` 布局,添加 `mainAxisSize: MainAxisSize.min` 和 padding - -#### 问题2:点击卡片进入详情页不显示数据 -- **根因**:路由定义中 `RecipeDetailPage(recipeId: '${Get.arguments ?? '1'}')` 使用字符串插值,导致传递的是字面量字符串而非实际值 -- **修复**:重构路由参数解析逻辑,正确处理 int/String 类型参数 - -#### 问题3:滑动卡住问题 -- **根因**:滚动通知处理过于频繁,`_loadMoreDiscover` 被重复调用 -- **修复**: - - 添加滚动节流机制(500ms间隔) - - 添加 `_scrollNotificationEnabled` 开关控制 - -#### 问题4:API返回太慢导致布局卡顿 -- **根因**:`initState` 中同步调用数据加载,阻塞UI渲染 -- **修复**: - - 改为后台静默加载:`WidgetsBinding.instance.addPostFrameCallback` - - 先显示骨架屏,数据加载完成后再渲染 - -#### 额外修复:点击加载更多自动回顶部 -- **根因**:`onTap` 和 `onDoubleTap` 同时存在导致冲突 -- **修复**:将"加载更多"按钮和"回顶部"按钮分开为两个独立按钮 - -## [0.91.11] - 2026-04-13 - -### ⚡ 性能大重构:图片加载优化 + 请求参数调整 - -#### 方案1+4:RecipeImage 重写 -- 用 `cached_network_image` 替代手写 HttpClient + dart:ui 压缩 -- Fallback 链精简:`coverUrl → {picId}a.jpg`(只请求带 a 的,不再尝试 b/.jpg) -- 内置内存缓存 + 磁盘缓存 + 请求去重 -- 移除所有 debugPrint 日志 - -#### 方案2:瀑布流优化 -- 添加 `addAutomaticKeepAlives: false` 启用 Widget 回收 -- 添加 `addRepaintBoundaries: true` 减少重绘 - -#### 请求参数调整 -``` -total: 12 → 50 -recipe: 4 → 18 (50×0.35) -ingredient: 2 → 8 (50×0.15) -category: 1 → 6 (50×0.12) -tag: 2 → 9 (50×0.18) -nutrition: 1 → 5 (50×0.1) -meal_time: 1 → 5 (50×0.1) -``` - -#### 效果 -- 图片加载速度提升 3-5x -- 每张图片 HTTP 请求从最坏 4 次 → 最多 2 次 -- 首屏内容更丰富(50 条数据 vs 之前 12 条) - -## [0.91.10] - 2026-04-13 - -### 🎨 分类框文字截断:最多5字符 - -``` -"孕早期食谱" → "孕早期…" (5字+省略号) -"私家菜" → "私家菜" (≤5字不变) -"补虚养身食谱" → "补虚养身…" (5字) -``` - -## [0.91.9] - 2026-04-13 - -### 🎨 卡片底部信息行去重:分类只显示在蓝色框内 - -``` -❌ 之前: [🟦私家菜] 🏷 私家菜 👁 0 ← 分类名重复显示2次 -✅ 现在: [🟦私家菜] 👁 0 ← 分类只在蓝色框显示 - [🟦川菜] ⭐ 4.5 👁 128 ← 有评分时正常显示⭐分数 -``` - -## [0.91.8] - 2026-04-13 - -### 🎨 卡片评分区域优化:无评分时显示分类名 - -``` -❌ 之前: [孕早期食谱] 暂无评分 👁 0 -✅ 现在: [孕早期食谱] 🏷 私家菜 👁 0 ← 显示API category.name - [私家菜] ⭐ 4.5 👁 128 ← 有评分不变 -``` - -## [0.91.7] - 2026-04-13 - -### ✨ DiscoverRating 模型补全 + 卡片评分显示优化 - -#### API 对齐 -API `rating` 返回 4 个字段,之前只用了 `score`/`nums`/`display`,缺少 `star` - -| 字段 | 类型 | 说明 | -|------|------|------| -| score | double | 评分值 | -| nums | int | 评价人数 | -| display | string | 显示文本(如"暂无评分") | -| **star** | **int** | ⭐ 新增 | - -#### 卡片 UI 变化 -``` -❌ 之前: [孕早期食谱] ⭐ 0.0 👁 0 ← 所有卡片都显示"0.0" -✅ 现在: [孕早期食谱] 暂无评分 👁 0 ← 无评分时显示display文本 - [私家菜] ⭐ 4.5 👁 128 ← 有评分时显示橙色⭐+分数 -``` -- 有评分 → **橙色实心星** `⭐ 4.5` (加粗) -- 无评分 → **灰色文字** `暂无评分` (muted) - -## [0.91.6] - 2026-04-13 - -### 🔴 关键修复:图片压缩输出86B空PNG导致全部卡片无图 - -#### 根因(debug日志确认) -``` -[RecipeImage] ✅ 下载成功: http://.../17538a.jpg (143087B) ← 143KB正常下载 -[RecipeImage] 开始压缩缩略图... -[RecipeImage] ✅ 压缩完成: 86B ← ❌❌❌ 143KB→86字节!空PNG! -[RecipeImage] build: ✅ 显示图片 recipeId=42646 bytes=86 ← 86B无法渲染 -``` -`dart:ui` 的 `toByteData(format: png)` 对部分JPEG静默失败,输出86B空数据。 -所有带图片的卡片都显示空白/error占位图。 - -#### 修复 -- 压缩后检测输出大小:**< 500B 视为损坏,自动回退使用原始JPEG** -- 底部栏 Row 溢出修复(+Flexible + mainAxisSize.min) - -## [0.91.5] - 2026-04-13 - -### 🖼 图片加载修复:无图菜谱跳过无效Fallback链 - -#### 问题根因(脚本验证确认) -| 菜谱 | cover | pic_id | 结果 | -|------|-------|--------|------| -| 南煎香菇 #37405 | `http://.../12207a.jpg` | 12207 | ✅ 正常 (159KB) | -| 咖喱鸡饭 #54999 | `""`(空) | null | ❌ 全部404 | -| 火腿酥腰 #41576 | `http://.../16498a.jpg` | 16498 | ✅ 正常 (128KB) | -| 炸山鸡球 #52857 | `""`(空) | null | ❌ 全部404 | - -**根因**:2/7 菜谱的 cover 为空 + pic_id 为 null → 回退用 recipe.id 当 picId → 但 recipe.id ≠ 实际图片ID → Fallback链全部404 - -#### 修复方案 -- 新增 `_hasImageSource` 判断:cover非空 或 能提取picId → 有图片源 -- 无图片源时:**跳过整个Fallback链**,直接显示渐变占位图 -- 效果:避免3次无效HTTP请求(404),立即显示美观"暂无图片" - -#### 验证脚本 -- [test_discover_image_urls.dart](scripts/test_discover_image_urls.dart) — 发现页图片URL完整链路验证 -- 用法: `dart scripts/test_discover_image_urls.dart` - -## [0.91.4] - 2026-04-13 - -### 🔄 底部加载栏重构(纳入列表 + 双击回顶) - -#### 核心改动 -| 改动 | 说明 | -|------|------| -| 底部栏内嵌列表 | 从独立 SliverToBoxAdapter 移入瀑布流末尾项,随卡片一起滑动 | -| 触发条件放宽 | `ScrollEndNotification` → `ScrollUpdateNotification`,滑到底即触发 | -| 双击回顶部 | 底部栏双击执行 `animateTo(0)` 平滑回顶 | -| 单击加载更多 | 点击底部栏触发 `onLoadMore` | - -#### 交互示意 -``` -滚动前: 滑到底后: -┌──────────┐ ┌──────────┐ -│ 卡片1 │ │ ... │ -│ 卡片2 │ 滑动 ↓ │ 卡片N-1 │ -│ ... │ ──────────→ │ 卡片N │ -│ │ ├──────────┤ ← 底部栏(在列表内) -│ │ │已加载N项⬇自动│ -└──────────┘ │双击回顶部 │ - └──────────┘ -``` - -## [0.91.3] - 2026-04-13 - -### 👆 卡片长按关闭功能 - -#### 交互设计 -| 操作 | 效果 | -|------|------| -| 长按卡片 | 右上角浮现红色 ✕ 关闭按钮(淡入+缩放动画) | -| 点击 ✕ | 卡片立即消失 + 轻触反馈(HapticFeedback) | -| 下拉刷新 | 重置所有已关闭卡片(重新显示) | - -#### 技术实现 -- [recipe_discover_card.dart](lib/src/widgets/discover/recipe_discover_card.dart) — `onLongPressStart` 触发动画显示X按钮 -- [discover_waterfall.dart](lib/src/widgets/discover/discover_waterfall.dart) — `dismissedRecipeIds` 过滤已关闭卡片 -- [home_page.dart](lib/src/pages/home/home_page.dart) — `_dismissedRecipeIds` Set 管理状态 - -## [0.91.2] - 2026-04-13 - -### 🔄 无限加载 + 到底自动加载 +### 📝 文档更新:页面结构分析 + 视图文件引用 #### 核心变更 -- ✅ **移除80项上限** — 支持无限滚动加载,不再限制最大数量 -- ⚡ **减少每次请求量** — 初始12项,每次+8项,API响应更快 -- 🤖 **到底自动加载** — 滚动距底部300px时自动触发API请求 -- 📛 **自动降级机制** — 自动加载失败时显示「🔄 加载更多」手动按钮 -- 🔄 **下拉刷新恢复** — 下拉刷新时重置自动加载状态 +- 📊 **新增页面结构分析文档** — `docs/dev/PAGE_STRUCTURE_ANALYSIS.md`(新建) + - 全局导航图:MainTabView → 首页/发现/工具/我的 四大Tab + - 12个页面详细结构图(文本符号可视化) + - 每个页面跳转关系表(触发元素/跳转方式/目标页面/路由/传参) + - 美观问题分析(严重度分级:🟡高/🟡中/🟢低) + - 功能缺失清单(优先级P1-P3) + - 全局美观/功能问题汇总 -#### 加载行为 -| 状态 | 底部栏显示 | -|------|-----------| -| 正常滚动中 | 已加载 N 项 + 「↓ 自动」标签 | -| 正在自动加载 | 轻量 spinner + 「正在加载更多...」 | -| 自动加载失败 | 「🔄 加载更多」手动按钮 | -| 手动加载中 | 按钮内 spinner | +- 📄 **页面视图文件引用** — `docs/dev/PAGE_STRUCTURE_ANALYSIS.md` + - 为每个页面添加"📄 页面视图文件"章节 + - 仅展示前2个关联视图文件(主页面+核心组件/控制器) + - 涵盖:首页/发现/搜索/高级搜索/菜品详情/工具中心/我的/食材详情/分类浏览/标签列表/热门排行/今天吃什么 -#### 技术细节 -| 配置项 | 值 | -|--------|-----| -| 初始请求数 | 12 | -| 每次增量 | 8 | -| 自动加载阈值 | 距底部 300px | -| API超时 | 12s(加载更多) | +- 📋 **压缩未完成功能清单** — `docs/dev/UNFINISHED_FEATURES.md` + - 从~600行压缩至~130行 + - 聚焦未完成任务,已完成阶段仅保留汇总 -## [0.91.1] - 2026-04-13 +## [0.91.16] - 2026-04-13 -### 🐛 图片加载修复 + 布局溢出防护 - -#### 问题1:菜品卡片图片不显示 -**根因分析**(通过纯Dart验证脚本 `scripts/test_discover_images.dart` 确认): -| 测试项 | 结果 | -|--------|------| -| API返回的 `cover` URL | ✅ HTTP 200,189KB | -| `{recipe_id}a.jpg` Fallback | ❌ 404(recipe.id ≠ 实际picId) | -| 例:菜谱 #35804 → cover=10552a.jpg | recipe.id与图片文件ID不同 | - -**修复方案** — [recipe_discover_card.dart](lib/src/widgets/discover/recipe_discover_card.dart): -- 新增 `_extractPicId()` 方法,从cover URL正则提取真实图片ID - - `http://eat.wktyl.com/api/assets/pic/10552a.jpg` → `10552` -- 新增 `_resolveCoverUrl()` 统一处理相对/绝对URL -- RecipeImage 调用传入正确 `picId: _extractPicId() ?? widget.recipe.id` - -#### 问题2:布局滚动溢出 -**修复方案** — [home_page.dart](lib/src/pages/home/home_page.dart): -- 主布局 Column 外层添加 `ClipRect` 裁剪保护 -- CustomScrollView 添加 `clipBehavior: Clip.hardEdge` -- 防止 HomeAppBar 高度动画期间短暂溢出 - -#### 新增文件 -| 文件 | 作用 | -|------|------| -| `scripts/test_discover_images.dart` | Discover图片URL纯Dart验证脚本 | - -## [0.91.0] - 2026-04-13 - -### 🔄 首页瀑布流增强 — 渐进式渲染 + 刷新按钮 + 分页加载 +### ✨ 搜索功能修复 + 高级搜索 + 食材详情返回首页 #### 核心变更 -- ✨ **渐进式渲染** — `lib/src/widgets/discover/discover_waterfall.dart` - - 卡片逐个绘制,不一次性渲染全部数据 - - 未加载的卡片显示骨架屏占位(SkeletonLoader) - - 每张卡片带入场动画(淡入+上移,错开时序) - - 滚动时逐步解锁可见数量(每60px多显示2张) +- 🔍 **修复搜索功能** — `lib/src/controllers/search_controller.dart` + - 切换到 `api_filter.php?act=global_search` 接口,解决搜索无结果问题 + - 新增4类搜索结果:recipeResults/ingredientResults/tasteTagResults/cookingTagResults + - 新增 SearchIngredientResult 模型解析食材搜索结果 + - 新增 `totalResultCount`/`hasAnyResult`/`_autoSelectTab` 便捷方法 + - 新增 `searchByFilter()` 支持高级筛选搜索 -- 🔄 **到底刷新按钮** — `lib/src/pages/home/home_page.dart` - - 移除自动加载更多(距底部200px触发) - - 底部固定「🔄 刷新加载更多」按钮视图 - - 加载中显示 spinner + 「正在加载更多...」 - - 全部加载完毕显示「✅ 已加载全部内容」 +- 📑 **搜索结果分Tab展示** — `lib/src/pages/home/search_page.dart` + - 搜索结果动态Tab:📖 菜谱 / 🥬 食材 / 👅 口味 / 🍳 工艺 + - 每个Tab显示结果数量徽章 + - 自动选择有结果的Tab + - 导航栏增加高级搜索入口(⚙️ slider_horizontal_3图标) + - 食材结果卡片显示分类名和菜谱数量 + - 口味/工艺标签结果使用2列网格布局 -- 📊 **进度指示器** - - 显示已加载数量 / 总量(如:35 / 80 项) - - 渐变进度条可视化加载进度 - - 支持最大80项限制,每次递增15项 +- ⚙️ **新增高级搜索页面** — `lib/src/pages/home/advanced_search_page.dart`(新建) + - 4个筛选维度:📂 菜谱分类 / 👅 口味标签 / 🍳 工艺标签 / 🕐 用餐时段 + - 支持多条件组合筛选 + - 支持重置筛选条件 + - 底部搜索按钮调用 searchByFilter() 执行筛选 -- ⏱️ **时间戳显示** - - 底部显示最后更新时间 - - 智能格式化:刚刚更新 / X分钟前 / X小时前 / 昨天 +- 🔌 **Repository新增接口** — `lib/src/repositories/recipe_repository.dart` + - `fetchMainCategories()`: 获取菜谱大类列表(调用 recipe_main_categories 接口) -#### 技术细节 -| 特性 | 实现 | -|------|------| -| 渐进式渲染 | `visibleItemCount` 控制可见卡片数 | -| 骨架屏占位 | `index >= visibleItemCount` 时显示 SkeletonLoader | -| 入场动画 | `TweenAnimationBuilder` + Opacity + Transform.translate | -| 分步解锁 | 滚动监听 → `_visibleItemCount += step` | -| 图片懒加载 | `visibleImageCount = visibleItemCount * 0.75` | -| 初始可见 | 6张卡片,滚动后逐步增加 | +- 🛤️ **新增路由** — `lib/src/config/app_routes.dart` + - 新增 `advancedSearch` 路由(/advanced-search) -#### 影响文件 -| 文件 | 变更类型 | 说明 | -|------|---------|------| -| `lib/src/pages/home/home_page.dart` | 重构 | 分页状态+刷新按钮+时间戳+移除自动加载 | -| `lib/src/widgets/discover/discover_waterfall.dart` | 增强 | 渐进式渲染+骨架屏占位+入场动画 | -| `lib/src/repositories/discover_repository.dart` | 增强 | 新增fetchMore增量获取方法 | +- 🏠 **食材详情返回首页** — `lib/src/pages/tools/ingredient_detail_page.dart` + - 底部按钮从"返回列表"改为"返回首页" + - 使用 `Get.until((route) => route.isFirst)` 返回根路由 -## [0.90.0] - 2026-04-12 +- 📦 **CategoryModel增强** — `lib/src/models/recipe/category_model.dart` + - fromJson 支持 `recipe_count` 字段映射到 count -### 🌊 首页重构 — Discover瀑布流 (Liquid Glass) +## [0.91.15] - 2026-04-13 + +### ✨ 发现页推荐Tab新增口味/工艺筛选 + 分类导航修复 #### 核心变更 -- 🔄 **移除旧卡片组件** — `lib/src/pages/home/home_page.dart` - - 移除「🔥 今日推荐」横向滚动菜品卡片(320px高度ListView) - - 移除「📂 分类浏览」横向滚动分类卡片(120px高度ListView) - - 移除「🏷️ 口味 & 工艺」标签区域 - - 移除「今日推荐/为你推荐」Tab切换器 - - 清理 ~400行死代码(_buildRecipeCard/_buildCategoryCard/_buildTagsSection等) - -- ✨ **新增 Discover 瀑布流布局** - - 使用 `api_discover.php` 接口获取混合数据(菜谱+食材+分类+标签+时段) - - MasonryGridView 2列瀑布流,高度自适应 - - 5种卡片类型混合展示:菜品/食材/分类/口味标签/时段推荐 - - Liquid Glass 毛玻璃风格统一视觉 - -#### 新增文件 -| 文件 | 说明 | -|------|------| -| `lib/src/models/discover_model.dart` | Discover API 数据模型(6种子类型) | -| `lib/src/repositories/discover_repository.dart` | Discover 接口仓库 | -| `lib/src/widgets/discover/recipe_discover_card.dart` | 菜品发现卡片(封面+评分+浏览量) | -| `lib/src/widgets/discover/ingredient_discover_card.dart` | 食材发现卡片(过敏原警示) | -| `lib/src/widgets/discover/category_discover_card.dart` | 分类发现卡片(图标+计数) | -| `lib/src/widgets/discover/discover_waterfall.dart` | 瀑布流容器 + 骨架屏 | - -#### 依赖变更 -- 新增 `flutter_staggered_grid_view: ^0.7.0`(MasonryGridView) - -#### 点击跳转 -| 卡片类型 | 跳转目标 | -|---------|---------| -| 菜品卡片 → | `/recipe-detail?id={id}` | -| 食材卡片 → | `/search?keyword={name}` | -| 分类卡片 → | `/category-browse?category={model}` | -| 口味标签 → | `/tag-recipe-list?tagName={name}&tagType=taste` | -| 时段卡片 → | `/search?keyword={name}` | - -## [0.89.1] - 2026-04-12 - -### UI Enhancement — 首页顶部栏滚动隐藏 - -- ✨ **顶部栏滚动隐藏/下拉显示** — `lib/src/pages/home/home_page.dart` - - 向下滚动时自动隐藏顶部栏(标题+搜索框+Tab切换器) - - 向上滚动时自动显示顶部栏 - - 回到顶部时始终显示 - - 使用 `AnimatedSlide` 实现平滑动画效果 - - 滚动阈值10px,避免误触发 - -## [0.89.0] - 2026-04-12 - -### API Migration — API v3.2.0 迁移 - -#### 接口变更 -- 🔧 **移除 preference 接口** — `api_preference.php` 已被后端删除 - - `PreferenceRepository` 改为使用 `SharedPreferences` 本地存储 - - 保留所有方法签名,确保向后兼容 - -- ⭐ **评分接口迁移** — `recommend` 改为 `rate` 评分接口 - - 评分范围:1-5分 - - 每日限制:每个IP每天最多30次 - - 不可取消:评分后无法撤销 - -- 📱 **信息流接口更新** — `api_feed.php?act=personal` 已删除 - - `FeedRepository` 移除 `fetchPersonal()` 方法 - - `FeedType` 枚举移除 `personal` 值 - - 信息流类型从4种减少为3种:推荐/最新/热门 - -#### 代码变更 -- 🔧 **ActionRepository** — `recommend()` → `rate(score: 1-5)` -- 🔧 **ActionController** — `recommendItem()` → `rateItem(score:)` -- 🔧 **RecipeDetailController** — `recommendRecipe()` → `rateRecipe(score:)` -- 🔧 **RecipeActionBar** — 重构评分UI,支持1-5星评分选择器 -- 🔧 **FeedController** — 移除 `FeedType.personal` 相关逻辑 - -#### 影响文件 -| 文件 | 修改内容 | -|------|---------| -| `lib/src/config/api_config.dart` | 移除preference接口,新增filter/discover | -| `lib/src/repositories/preference_repository.dart` | 改为SharedPreferences本地存储 | -| `lib/src/repositories/action_repository.dart` | recommend→rate评分接口 | -| `lib/src/repositories/feed_repository.dart` | 移除personal接口 | -| `lib/src/controllers/feed/feed_controller.dart` | 移除personal相关逻辑 | -| `lib/src/controllers/feed/action_controller.dart` | recommendItem→rateItem | -| `lib/src/controllers/recipe_detail_controller.dart` | recommendRecipe→rateRecipe | -| `lib/src/pages/home/recipe_detail_page.dart` | 更新评分调用 | -| `lib/src/widgets/recipe_detail/recipe_action_bar.dart` | 重构评分UI | - -#### API变更对照表 -| 原接口 | 新接口/方案 | 说明 | -|--------|------------|------| -| `api_preference.php` | SharedPreferences本地存储 | 已删除 | -| `api_feed.php?act=personal` | 使用 `act=recommend` 替代 | 已删除 | -| `api_action.php?act=recommend` | `api_action.php?act=rate` | 改为评分(1-5分) | -| `recommend_nums/recommend_score` | `rate_nums/rate_score` | 字段重命名 | - -## [0.88.7] - 2026-04-12 - -### Bug Fixes — 多项功能修复 - -#### 浏览记录功能修复 -- 🐛 **修复浏览历史本地数据加载** — `lib/src/controllers/browse_history_controller.dart` - - Controller onInit 时异步加载 SharedPreferences 数据 - - 添加调试日志便于追踪数据加载状态 - -- 🐛 **修复浏览记录点击跳转错误** — `lib/src/pages/profile/footprints_page.dart` - - 路由从 `/recipe/${id}` 改为 `/recipe-detail` - - 正确传递 `arguments` 参数 - -- 🐛 **修复页面列表被顶部bar遮住** — 多个页面 - - `footprints_page.dart` 移除 `SafeArea(top: false)` - - `cooking_note_page.dart` 移除 `SafeArea(top: false)` - - `category_browse_page.dart` 移除 `SafeArea(top: false)` - -#### 点赞功能修复 -- 🐛 **修复菜品详情页点赞功能** — `lib/src/controllers/recipe_detail_controller.dart` - - 分离点赞和收藏逻辑,`onLike` 不再调用 `toggleFavorite()` - - 修正 `likeCount` 更新顺序:先调用 API,成功后再更新计数 - - 添加调试日志便于追踪点赞状态 - -#### 发现页面修复 -- 🐛 **修复推荐菜谱和食材点击闪退** — `lib/src/pages/discover/` - - 新增 `isIngredient` 参数区分菜谱分类和食材分类 - - 食材分类使用搜索接口 (`act=search`) 而非 `cate_id` - - `CategoryBrowsePage` 支持 `searchKeyword` 参数 - -#### 首页标签刷新修复 -- 🐛 **修复口味和工艺刷新后仍显示本地数据** — `lib/src/pages/home/home_page.dart` - - `_loadTasteTags()` 和 `_loadProcessTags()` 添加 `refresh` 参数 - - 刷新时传递 `refresh: true` 强制从 API 获取新数据 - -#### 影响文件 -| 文件 | 修改内容 | -|------|---------| -| `lib/src/controllers/browse_history_controller.dart` | 本地数据加载优化 | -| `lib/src/controllers/recipe_detail_controller.dart` | 点赞功能修复 | -| `lib/src/pages/profile/footprints_page.dart` | 跳转路由修复 + SafeArea修复 | -| `lib/src/pages/tools/cooking_note_page.dart` | SafeArea修复 | -| `lib/src/pages/discover/category_browse_page.dart` | 食材搜索支持 + SafeArea修复 | -| `lib/src/pages/discover/discover_page.dart` | 传递isIngredient参数 | -| `lib/src/pages/home/home_page.dart` | 标签刷新功能修复 | -| `lib/src/pages/home/recipe_detail_page.dart` | 分离点赞和收藏逻辑 | -| `lib/src/config/app_routes.dart` | 添加isIngredient路由参数 | - -## [0.88.6] - 2026-04-12 - -### New Feature — 笔记功能增强 & 浏览记录功能 - -#### 烹饪笔记功能增强 -- ✨ **笔记标签关键字** — `lib/src/models/cooking_note_model.dart` - - 新增 `tags` 字段,支持为笔记添加标签关键字 - - 新增 `hasTags`、`displayTags` 便捷属性 - - 更新 Hive TypeAdapter 支持标签序列化 - -- ✨ **菜品字段快捷输入** — `lib/src/pages/tools/cooking_note_page.dart` - - 新增 `recipeInfo` 参数,接收菜谱详细信息 - - 快捷输入区域显示:菜名、简介、分类、标签、难度、时间、食材 - - 点击快捷标签自动填入笔记内容 - -- ✨ **SharedPreferences 双重存储** — `lib/src/controllers/cooking_note_controller.dart` - - 笔记同时保存到 Hive 和 SharedPreferences - - 优先从 Hive 加载,失败则从 SharedPreferences 恢复 - - 确保笔记数据不丢失 - -#### 浏览记录功能 -- ✨ **浏览记录模型** — `lib/src/models/browse_history_model.dart` (新建) - - 记录字段:id、recipeId、title、coverImage、category、viewedAt、viewCount - - 支持 Hive TypeAdapter 序列化 - - 智能时间显示:刚刚、X分钟前、X小时前、昨天、X天前 - -- ✨ **浏览记录控制器** — `lib/src/controllers/browse_history_controller.dart` (新建) - - SharedPreferences 持久化存储 - - 最大记录数限制:100条 - - 支持增删改查、清空操作 - - 自动去重:重复浏览更新时间和计数 - -- ✨ **浏览记录页面** — `lib/src/pages/profile/footprints_page.dart` - - 重构为真实浏览记录功能 - - 显示菜谱封面、标题、分类、浏览时间、浏览次数 - - 支持左滑删除、清空所有记录 - - 点击跳转到菜谱详情页 - -- ✨ **菜谱详情页集成** — `lib/src/controllers/recipe_detail_controller.dart` - - 新增 `_recordBrowseHistory()` 方法 - - 每次打开菜谱详情自动记录浏览历史 - - 新增 `getRecipeInfo()` 方法供笔记页面使用 - -#### 影响文件 -| 文件 | 操作 | 说明 | -|------|------|------| -| `lib/src/models/browse_history_model.dart` | 新建 | 浏览记录模型 | -| `lib/src/models/cooking_note_model.dart` | 修改 | 新增tags字段 | -| `lib/src/controllers/browse_history_controller.dart` | 新建 | 浏览记录控制器 | -| `lib/src/controllers/cooking_note_controller.dart` | 修改 | SharedPreferences存储 | -| `lib/src/controllers/recipe_detail_controller.dart` | 修改 | 浏览记录功能 | -| `lib/src/pages/tools/cooking_note_page.dart` | 修改 | 标签+快捷输入 | -| `lib/src/pages/profile/footprints_page.dart` | 重构 | 真实浏览记录 | -| `lib/src/pages/home/recipe_detail_page.dart` | 修改 | 传递recipeInfo | -| `lib/src/config/app_routes.dart` | 修改 | 路由参数更新 | -| `lib/src/app_binding.dart` | 修改 | 注册控制器 | - -## [0.88.5] - 2026-04-12 - -### Refactoring — 代码清理与优化 - -#### 删除未使用文件 -- 🗑️ **删除未使用的控制器** — `lib/src/controllers/paged_controller.dart` - - 分页控制器基类,定义了但从未被任何Controller继承使用 - -- 🗑️ **删除重复的路由配置** — `lib/src/standards/app_pages.dart` - - 与 `app_routes.dart` 功能完全重复,保留后者 - -- 🗑️ **删除未使用的路由守卫** — `lib/src/standards/route_guard.dart` - - 定义了但从未在路由配置中使用 - -- 🗑️ **删除未使用的工具类** - - `lib/src/utils/date_utils.dart` — 日期工具类,完全未使用 - - `lib/src/utils/string_utils.dart` — 字符串工具类,完全未使用 - - `lib/src/utils/network_utils.dart` — 网络工具类,完全未使用 - - `lib/src/utils/error_handler.dart` — 错误处理类,从未被调用 - -#### 文件整合 -- 📦 **整合utils目录** — `lib/src/utils/app_utils.dart` (新建) - - 合并 `common_utils.dart` + `app_exception.dart` + 已删除工具类 - - 包含:AppUtils、AppDateUtils、StringUtils、NetworkUtils、AppException、Result - - 更新引用:`recipe_detail_controller.dart` 中 CommonUtils → AppUtils - - 删除旧文件:`common_utils.dart`、`app_exception.dart` - -#### 代码简化 -- ✂️ **简化工具模型** — `lib/src/models/tool_item_model.dart` - - 删除未使用的 `ToolCategory` 类 - - 改用 `ToolRegistry.categoryLabels` / `ToolRegistry.categoryIcons` 静态Map - - 新增 `getCategoryLabel()` / `getCategoryIcon()` 便捷方法 - -#### 影响文件统计 -| 操作 | 文件数 | 代码行数 | -|------|--------|----------| -| 删除 | 9个 | ~500行 | -| 新建 | 1个 | ~367行 | -| 修改 | 2个 | ~10行 | -| **净减少** | **8个** | **~140行** | - -## [0.88.4] - 2026-04-12 - -### New Feature — 数据管理中心 - -- ✨ **数据管理中心页面** — `lib/src/pages/profile/data_center_page.dart` (新建) - - **功能**:统一管理分类标签和过敏原数据,支持API同步+本地持久化 - - **分类标签区域**: - - 🏷️ 口味标签:从 `api.php?act=tags` 获取,显示数量+弹窗详情 - - 🔥 工艺标签:从菜品列表 `meta.process` 动态提取去重 - - 📂 食材分类:从 `api.php?act=categories` 获取分类树 - - **过敏原管理区域**: - - 从 `gmy.json` 获取585种过敏原(21大类) - - 支持展开/收起每个大类查看具体过敏原 - - 用户可勾选个人过敏原,即时保存到SharedPreferences - - 已选过敏原顶部展示(最多8个标签) - - **同步功能**:右上角刷新按钮一键同步所有数据 - - **影响文件**: - - `lib/src/pages/profile/data_center_page.dart` (新建) - - `lib/src/services/data/local_data_service.dart` (新建) - -- ✨ **LocalDataService 数据服务** — `lib/src/services/data/local_data_service.dart` (新建) - - **功能**:统一管理本地数据缓存与API同步 - - **数据类型**:口味标签、工艺标签、食材分类、过敏原数据、用户过敏原设置 - - **存储方式**:SharedPreferences 持久化 - - **缓存策略**:24小时自动过期,支持手动强制刷新 - - **核心方法**: - - `getTasteTags()` / `syncTasteTags()` — 口味标签获取/同步 - - `getProcessTags()` / `syncProcessTags()` — 工艺标签获取/同步 - - `getCategories()` / `syncCategories()` — 食材分类获取/同步 - - `getAllergenData()` / `syncAllergenData()` — 过敏原数据获取/同步 - - `getUserAllergens()` / `setUserAllergens()` — 用户过敏原读写 - - `syncAll()` — 批量同步所有数据 - -- 🔧 **个人中心按钮改造** — `lib/src/pages/profile/profile_home.dart` - - **修改**:「系统通知」按钮 → 「📊 数据管理中心」按钮 - - **跳转**:点击进入 DataCenterPage - -- 🔧 **路由注册** — `lib/src/config/app_routes.dart` - - 新增 `/data-center` 路由 → DataCenterPage - -### Bug Fixes — v0.88.4 补丁 - -- 🔧 **首页标签区域添加刷新按钮** — `lib/src/pages/home/home_page.dart` - - **问题**:口味/工艺标签显示的是本地缓存数据,无法强制刷新 - - **修复**:在「口味 & 工艺」标题栏右侧添加 🔄 刷新按钮 - - **效果**:点击后同时从API重新获取口味标签和工艺标签,带加载动画 - -- 🔧 **修复数据管理中心跳转失败** — `lib/src/config/app_routes.dart` - - **问题**:点击「📊 数据管理中心」按钮后跳转到主页而非数据管理页 - - **原因**:`dataCenter` 路由仅注册了 PageInfo 但未添加到 GetX 的 `pages` 路由表 - - **修复**:在 `static final List pages` 中补加 dataCenter 的 GetPage 条目 - -## [0.88.1] - 2026-04-11 - -### Enhanced — 软件特性功能完善 - -- ✨ **食材推荐功能** — `lib/src/pages/discover/ingredient_recommend_page.dart`, `lib/src/pages/discover/ingredient_recipe_list_page.dart` - - **功能**:在分类浏览下增加食材推荐入口,点击食材跳转该食材对应的菜品列表 - - **实现**: - - 新建 IngredientRecommendPage 页面,展示食材网格(emoji+名称),调用 `api.php?act=ingredients` - - 新建 IngredientRecipeListPage 页面,显示食材相关菜品列表,调用 `api.php?act=search&keyword=xxx&type=recipe`,每页20条,支持分页加载 - - 首页分类浏览区域添加"食材推荐"入口卡片(渐变背景),点击跳转食材推荐页面 - - 食材emoji智能匹配(鸡→🥚、肉→🥩、鱼→🐟、菜→🥬等40+映射) - - **影响文件**: - - `lib/src/pages/discover/ingredient_recommend_page.dart` (新建) - - `lib/src/pages/discover/ingredient_recipe_list_page.dart` (新建) - - `lib/src/pages/home/home_page.dart` (修改) - - `lib/src/config/app_routes.dart` (修改) - - `lib/src/standards/app_pages.dart` (修改) - -- ✨ **食材详情页数据传递修复** — `lib/src/pages/home/recipe_detail_page.dart`, `lib/src/pages/tools/ingredient_detail_page.dart` - - **问题**:点击菜品详情页食材,食材介绍不显示 - - **原因**:detail数据已存在于act=full返回的ingredients[].detail中,但未传递给详情页 - - **修复**:recipe_detail_page传递detail参数,ingredient_detail_page接收并直接展示introduction/nutrition/guidance/effect/allergen - -- ✨ **功能状态审核与完善** - - **购物清单** - ✅ 已完成:菜谱详情页"购物"按钮可添加食材到购物清单 - - **过敏原检测** - ✅ 已完成:AllergenChecker完整实现,包含11类过敏原关键词映射和检测逻辑 - - **烹饪笔记** - ✅ 已完成:CookingNotePage完整实现,支持按菜谱关联笔记 - - **份量缩放** - ✅ 已完成:菜谱详情页"缩放"按钮可传递食材到serving_scaler_page - - **食材详情查询** - ✅ 已完成:包含营养信息、选购技巧、存储提示、关键营养素、最佳时令等 - - **每周菜单规划** - ✅ 已完成:七日横向滑动日历、三餐分配、购物清单、Hive持久化 - - **热量追踪+营养分析** - ✅ 已完成:包含环形图、柱状图、饼图(营养素占比+餐次分布)、折线图(热量趋势) - - **AI菜谱推荐** - ✅ 已完成:基于用户偏好、浏览历史、收藏记录的智能推荐 - - **就寝提醒** - ✅ 已完成:智能推荐就寝时间、睡前进食提醒、健康小贴士 - -- 📝 **文档更新** - - 更新`UNFINISHED_FEATURES.md`软件特性功能汇总表 - - 修正不实标记,准确反映功能完成状态 - - 新增v0.88.x功能条目 - ---- - - - -## [0.79.0] - 2026-04-11 - -### Fixed — 阶段三十一:搜索页相似推荐图片异常修复 - -- 🐛 **31.1 搜索页相似推荐显示"Exception: Invalid image"** — `recipe_image.dart` - - **问题**:搜索无结果时,相似推荐列表的食谱卡片图片区域显示红色错误文本`Exception: Invalid image` - - **根因**:`_buildErrorWidget`方法中存在**异常循环**: - 1. 网络图片下载后通过头部校验(仅检查前8字节)→ 存入`_imageBytes` - 2. `Image.memory(_imageBytes!)`解码完整数据 → 抛出`Invalid image`异常 - 3. `errorBuilder`捕获 → 调用`_buildErrorWidget(isDark)` - 4. ❌ `_buildErrorWidget`发现`_imageBytes != null`,再次用`Image.memory(_imageBytes!)`显示**同一份无效数据**,且**无errorBuilder** - 5. 再次抛异常 → 无捕获 → Flutter红色错误面板泄露到UI - - **修复方案**: - - ✅ `_buildErrorWidget`改用`_localErrorBytes`(本地error.png)替代`_imageBytes` - - ✅ 新增`_buildBlankPlaceholder`方法,提取空白占位逻辑 - - ✅ errorBuilder链路全部指向安全的占位组件,彻底切断异常循环 - - ✅ 即使本地error.png加载失败也会降级为🍽️文字占位 - - **影响文件**:`recipe_image.dart` - ---- - -## [0.73.0] - 2026-04-11 - -### Fixed — 阶段二十五:Picid功能Bug修复 - -- 🐛 **26.1 Picid显示为0的问题** — `recipe_detail_page.dart` - - **问题**:所有菜谱的Picid都显示为0 - - **根因**:API返回的pic_id字段可能不存在或为null,_parseInt返回默认值0 - - **修复方案**: - - ✅ 添加有效性检查:`hasValidPicId = picId != null && picId > 0` - - ✅ 无效Picid时显示"无",不显示0 - - ✅ 无有效Picid时图片链接显示"暂无图片链接" - - 🔍 添加调试日志输出picId实际值 - - **影响文件**:`recipe_detail_page.dart` - -- 🐛 **26.2 点击复制卡死闪退** — `recipe_detail_page.dart` - - **问题**:点击Picid卡片复制时应用卡死闪退 - - **根因**:ToastService.show可能因ThemeService未注册而崩溃 - - **修复方案**: - - ✅ 新增 `_showCopyToast()` 方法安全显示Toast - - ✅ 添加mounted检查防止Widget已销毁 - - ✅ Clipboard.setData包裹try-catch防止异常 - - ✅ 失败时显示友好提示而非崩溃 - - **影响文件**:`recipe_detail_page.dart` - ---- - -## [0.69.0] - 2026-04-11 - -### Enhanced — 阶段二十一:菜谱详情页完整数据展示 - -- ✨ **22.1 RecipeModel 新增 status 字段 + 时间戳智能解析** — `recipe_model.dart` - - 新增 `status` 字段,解析 API 返回的状态值(0=正常/1=草稿/2=禁用) - - 重构 `_parseTimestamp` 方法,支持多种时间戳格式: - - 13位:毫秒级时间戳(直接使用) - - 10位:秒级时间戳(×1000转毫秒) - - 12位:补齐至毫秒级(×10或×100自动校正) - - 自动年份校验:转换结果不在2000-2100范围时尝试修正 - - 兼容字段名:`create_time`/`created_at`、`update_time`/`updated_at` - -- ✨ **22.2 菜谱详情页显示完整API数据** — `recipe_detail_page.dart` - - **新增状态标识**:带颜色标签显示菜谱状态(✅正常/⏸️草稿/🚫禁用) - - **时间信息完善**:创建时间 + 更新时间(自动格式化为可读日期) - - **数据完整性**:确保API返回的所有字段都在UI中展示 - - 状态行使用iOS风格标签设计,不同状态对应不同颜色 - -- 🐛 **22.3 时间戳转换Bug修复** - - 问题:12位时间戳(如146085838541)被错误识别为秒级 - - 原因:判断条件 `>1e12` 对12位数值失效 - - 修复:按位数长度精确判断,12位时补零并验证年份范围 - - 测试验证:146085838541 → 2016-04-17 09:59 ✅ - -- ✅ **22.4 测试脚本** — `scripts/test_recipe_detail_parsing.dart` - - 覆盖所有新增字段的解析测试 - - 验证时间戳转换准确性 - - 确认分类食材/作者/状态等数据完整性 - - - - -## [0.62.0] - 2026-04-10 - -### Fixed — 营养中心崩溃修复 - -- 🐛 **营养中心报告按钮卡死闪退** — `nutrition_center_page.dart` / `nutrition_report_page.dart` - - 添加 MealRecordController 初始化错误处理 - - 添加 null 检查,避免空指针异常 - - 导航时添加 try-catch 错误捕获 - - 显示友好的错误提示页面 - -- 🐛 **热门排行数据显示"暂无数据"** — `hot_repository.dart` - - 添加详细调试日志,方便排查问题 - - 优化数据结构兼容性处理 - - 修复 period 参数传递错误 - - 添加错误提示和降级处理 - -### Optimized — 性能优化 - -- ⚡ **今天吃什么动态筛选优化** — `what_to_eat_controller.dart` / `what_to_eat_page.dart` - - 添加筛选条件调试日志 - - 优化空结果提示(显示已选筛选条件数量) - - 改进错误信息显示 - -- ⚡ **启动加载优化** — `home_page.dart` - - 添加骨架屏组件(SkeletonLoader) - - 实现 12 秒超时保护 - - 添加缓存优先策略 - -### Added — 测试工具 - -- 🧪 **接口验证脚本** — `scripts/verify_nutrition_api.dart` - - 验证 API 接口连通性 - - 测试热门排行数据 - - 性能基准测试(5 次迭代) - - 彩色输出和详细统计 - -- 📊 **性能优化报告** — `scripts/NUTRITION_PERFORMANCE.md` - - 接口验证结果汇总 - - API 接口文档摘要 - - 实际性能测试结果 - - 优化建议和验收标准 - -- 📚 **脚本工具说明** — `scripts/README.md` - - 使用方法指南 - - 故障排查手册 - - 测试结果记录 - -### Test Results — 测试结果 - -- ✅ **接口连通性**: 100% 成功率 -- 🟡 **平均响应时间**: 1393ms(一般) -- ✅ **稳定性**: 波动 < 100ms -- 📈 **优化空间**: 目标 < 500ms - -## 开发进度 - -### 已完成功能 -- ✅ 主题服务(ThemeService)+ 动态主题色 + 卡片滑动方向设置 -- ✅ 动画服务(AnimationService) -- ✅ 国际化支持(en, zh, zh_Hant) -- ✅ 权限管理服务 -- ✅ 自适应布局系统 -- ✅ GetX 全局状态管理 -- ✅ 标准组件库 -- ✅ 路由守卫系统 -- ✅ 繁体中文语言切换 + 弹窗/Toast 样式配置 -- ✅ 核心错误修复(DeviceType/类型提升/空值检查等) -- ✅ API 基础设施(baseUrl + 模型 + Repository)— 阶段一 -- ✅ 核心数据接入(首页真实数据)— 阶段二 -- ✅ 信息流 + 推荐系统 — 阶段三 -- ✅ 互动功能:点赞/推荐/浏览 — 阶段四 -- ✅ 用户偏好系统 — 阶段五 -- ✅ "今天吃什么"功能 — 阶段六 -- ✅ 热门排行 + 在线统计 — 阶段七 -- ✅ 缓存优化 + 离线支持 — 阶段八 -- ✅ **API v2.0.0 迁移** — 13个接口文件精简到9个,端点整合(优先级5) -- ✅ **8个严重Bug修复** — 主页/搜索/收藏/口味偏好/热门排行/详情页(优先级5) -- ✅ **搜索功能重写** — 直接调用API,iOS 26风格UI(优先级4) -- ✅ **今天吃什么动态筛选** — 分类/标签/过敏原三重筛选(优先级4) -- ✅ **热门排行HotItem模型** — 支持period/sortBy切换(优先级3) -- ✅ **首页横向滑动卡片** — PageView+ListView双模式(优先级3) -- ✅ **营养中心偏好修复** — 用户初始化+分类/标签加载(优先级3) -- ✅ **实用工具入口** — 烹饪计时/用量换算/BMI/份量缩放(优先级2) -- ✅ **页面拦截修复** — 路由守卫+PageRegistry注册(优先级2) -- ✅ **布局溢出修复** — 标签栏横向滑动+工具区横向滚动(优先级2) -- ✅ **工具中心布局溢出修复** — 移除Spacer+mainAxisExtent替代childAspectRatio(v0.60.0) -- ✅ **浑水摸鱼功能补全** — 烹饪笔记+过敏原检测+份量缩放+菜单持久化+收藏添加+购物清单+食材营养+餐次饼图(v0.64.0) -- ✅ **工具中心** — 数据模型+控制器+搜索/分类/频率统计+联网指示(优先级4) -- ✅ **过敏原检查** — API数据加载+分类浏览+等级提示(优先级3) -- ✅ **用餐时段推荐** — 时段自动推荐+分类搜索(优先级3) -- ✅ **每周菜单规划** — 七日选择器+三餐规划+搜索/收藏选择(优先级3) -- ✅ **食材详情查询** — 营养信息+分类+关联菜谱(优先级3) -- ✅ **营养追踪仪表盘** — 首页环形图+四项指标(优先级5) -- ✅ **搜索列表/今天吃什么Bug修复** — 空值检查+布局溢出(优先级4) -- ✅ **统一 Controller Binding 注册** — AppBinding全局管理+移除重复注册(优先级4) -- ✅ **崩溃修复** — 收藏页Obx崩溃+详情页营养全0检测(v0.61.0) -- ✅ **阶段任务补全(12/13/14/16)** — 分享菜谱+搜索热词API+单位换算+过敏原替代+点赞评分+浏览量统计+收藏页Liquid Glass(v0.65.0) - -### 待开发功能(详见 UNFINISHED_FEATURES.md 阶段九~十三) - -**阶段九:架构修复+核心Bug(P0/P1)** -- 🔴 热门排行点击跳转详情(优先级5) -- 🔴 首页改用 Repository 层(优先级5) -- 🟡 合并收藏功能去重(优先级4) -- 🟡 合并搜索控制器去重(优先级4) -- 🟡 多语言词条扩充(优先级4) -- 🟢 聊天页面功能化或移除(优先级3) - -**阶段十:代码质量提升(P1/P2)** -- 🟡 HiveService 数据迁移机制(优先级3) -- 🟡 统一错误处理 AppException(优先级4) -- 🟡 离线缓存策略(优先级4) -- 🟢 DesignTokens 与 ThemeService 解耦(优先级3) - -**阶段十一:烹饪模式+营养仪表盘(P1)** -- 🟢 🍳 烹饪模式(步骤引导+计时器+语音播报)(优先级5) -- 🟢 📊 营养追踪仪表盘(首页环形图)(优先级5) -- 🟢 🛒 菜谱食材一键加入购物清单(优先级4) -- 🟢 📖 菜谱步骤图文模式(优先级4) - -**阶段十二:社交+通知增强(P2)** -- 🔵 📱 分享菜谱(优先级4) -- 🔵 🔔 烹饪提醒通知(优先级3) -- 🔵 🔍 搜索建议/热词(优先级3) -- 🔵 📸 拍照记录(优先级3) - -**阶段十三:AI+规划高级功能(P3)** -- 🔵 🤖 AI 菜谱推荐(优先级2) -- 🔵 📅 每周菜单规划(优先级3) -- 🔵 🧮 食材用量换算增强(优先级2) -- 🔵 🌙 就寝提醒(优先级1) - ---- - -## 技术栈 - -- **框架**: Flutter -- **状态管理**: GetX -- **响应式布局**: flutter_adaptive_scaffold -- **动画系统**: animations -- **国际化**: flutter_localizations + intl -- **权限管理**: permission_handler - ---- - -## 贡献指南 - -1. 遵循 iOS 风格设计规范 -2. 使用主题服务统一管理颜色和字体 -3. 使用动画服务统一管理动画效果 -4. 新增功能需更新 CHANGELOG.md -5. 代码提交前运行 `flutter analyze` 确保无错误 -6. **新建页面必须支持 GetX 状态管理** +- 👅 **推荐Tab新增口味筛选** — `lib/src/pages/discover/discover_page.dart` + - GlassSegmentedControl 从2段扩展为4段:📖 菜谱 / 🥬 食材 / 👅 口味 / 🍳 工艺 + - 口味Tab显示口味标签网格(3列),点击跳转 TagRecipeListPage + - 工艺Tab显示工艺标签网格(3列),点击跳转 TagRecipeListPage + +- 🏷️ **标签网格UI** — `lib/src/pages/discover/discover_page.dart` + - 新增 `_buildTagGrid` 方法,3列紧凑布局,圆角卡片+边框 + - 空状态显示🏷️图标+"暂无标签数据" + - 点击标签传递 tagName/tagId/tagType 到 TagRecipeListPage + +- 📂 **分类网格重构** — `lib/src/pages/discover/discover_page.dart` + - 提取 `_buildCategoryGrid` 方法,与标签网格分离 + - 提取 `_buildRecommendContent` 方法,根据tab索引切换内容 + +- 🔌 **Repository新增筛选接口** — `lib/src/repositories/recipe_repository.dart` + - `fetchTasteTags()`: 获取口味标签列表 + - `fetchCookingTags()`: 获取工艺标签列表 + - `filterRecipesByTag()`: 按口味/工艺/分类筛选菜谱 + +- 🐛 **分类导航修复** — `lib/src/pages/discover/category_browse_page.dart` + - 修复点击菜谱/食材分类闪退问题(BOTTOM OVERFLOWED) + - 使用 CustomScrollView + SliverList 替代 Column + Expanded + +## [0.91.14] - 2026-04-13 + +### ✨ 菜品详情页功能完善 — 评分显示 + 交互跳转 + +#### 核心变更 +- ⭐ **新增 RecipeRating 模型** — `lib/src/models/recipe/recipe_model.dart` + - 对齐 API v2.8.0 rating 字段:score/nums/display/status/level/star + - 支持评分状态判断(none/few/normal/sufficient/abnormal) + - 支持评分等级判断(优秀/推荐/一般/较差/不推荐) + - 提供 `displayText`/`hasRating` 等便捷属性 + +- 📊 **更新 RecipeStatistics 模型** — `lib/src/models/recipe/recipe_model.dart` + - 新增 `rateNums`/`rateScore` 字段,替代旧的 `recommends`/`recommendScore` + - 新增 `rating` (RecipeRating?) 字段 + - 旧字段标记为 `@Deprecated`,保持向后兼容 + - fromJson 兼容 `rate_nums`/`recommends`/`recommend_count` 多种API字段名 + +- 📈 **重构 RecipeStatisticsBar** — `lib/src/widgets/recipe_detail/recipe_statistics_bar.dart` + - 评分列从"⭐推荐"改为星级+评分文本+等级标签 + - 5星评级显示(CupertinoIcons.star_fill/star) + - 评分等级彩色标签(🌟金色优秀/⭐蓝色推荐/✨灰色一般/⚠️红色较差) + - 无评分时显示灰色星星+"暂无评分" + +- 🖼️ **更新 RecipeCoverImage** — `lib/src/widgets/recipe_detail/recipe_cover_image.dart` + - 封面图叠加层新增评分徽章(⭐ 4.5) + - 有评分时显示评分徽章,无评分时显示热度标签 + - 评分等级对应不同徽章颜色 + +- 🏷️ **标签点击跳转** — `lib/src/widgets/recipe_detail/recipe_tags_section.dart` + - 点击标签跳转到 TagRecipeListPage + - 标签添加 chevron_right 图标提示可点击 + +- 📂 **分类面包屑点击跳转** — `lib/src/widgets/recipe_detail/recipe_category_breadcrumb.dart` + - 非末级分类可点击跳转到 CategoryBrowsePage + - 末级分类高亮不可点击 + - 面包屑项添加 chevron_right 图标提示可点击 + +- 🔄 **RecipeDetailPage 传递 rating** — `lib/src/pages/home/recipe_detail_page.dart` + - RecipeCoverImage 和 RecipeStatisticsBar 接收 rating 参数 + +#### 文档更新 +- 📋 更新 `docs/dev/UNFINISHED_FEATURES.md` 新增阶段二十九:菜品详情页功能完善 + +> 📌 已移除更早版本记录(0.91.13及之前),功能已归档至软件特性清单。 diff --git a/docs/dev/PAGE_STRUCTURE_ANALYSIS.md b/docs/dev/PAGE_STRUCTURE_ANALYSIS.md new file mode 100644 index 0000000..083c766 --- /dev/null +++ b/docs/dev/PAGE_STRUCTURE_ANALYSIS.md @@ -0,0 +1,746 @@ +# 📱 APP 页面结构分析 + +> 创建: 2026-04-13 | 用途: 页面结构可视化 + 跳转关系 + 美观/功能问题分析 + 改进建议 +> 跳转图例: `[→ 页面名]` = Get.toNamed跳转, `[⇥ Tab切换]` = 底部Tab切换, `[↑ 返回]` = Get.back/Get.until + +--- + +## 🏗️ 全局导航图 + +``` + ┌─────────────┐ + │ MainTabView │ + └──────┬──────┘ + ┌────────┬───────┼───────┬────────┐ + ⇥ ⇥ ⇥ ⇥ + ┌───┴──┐ ┌───┴──┐ ┌─┴──┐ ┌──┴──┐ + │🏠首页│ │🔍发现│ │🛠️工具│ │👤我的│ + └──┬───┘ └──┬───┘ └─┬──┘ └──┬──┘ + │ │ │ │ + ┌────────┼────────┼───────┼───────┼────────────┐ + │ │ │ │ │ │ + ↓ ↓ ↓ ↓ ↓ ↓ + [→搜索] [→详情] [→分类] [→计时] [→收藏] [→数据] + [→详情] [→标签] [→标签] [→换算] [→足迹] [→营养] + [→分类] [→热门] [→吃什么] [→BMI] [→笔记] [→设置] + [→标签] [→搜索] [→缩放] [→购物] [→反馈] + [→高级] [→详情] [→过敏] [→聊天] [→就寝] + [→时段] + [→食材] + [→菜单] +``` + +--- + +## 🏠 首页 (HomePage) + +``` +┌─────────────────────────────┐ +│ HomeAppBar (双层) │ +│ ┌───────────────────────┐ │ +│ │ 🍳 妈妈厨房 🔔 │ │ ← 固定顶栏 +│ └───────────────────────┘ │ +│ ┌───────────────────────┐ │ +│ │ 🔍 搜索框 [→搜索页] │ │ ← 可折叠副栏,点击跳转 +│ └───────────────────────┘ │ +├─────────────────────────────┤ +│ │ +│ ┌─────────────────────┐ │ +│ │ 📊 营养追踪仪表盘 │ │ ← NutritionDashboardCard +│ │ 热量/蛋白质/碳水/脂肪 │ │ [→营养中心页] +│ └─────────────────────┘ │ +│ │ +│ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │ +│ │📖│ │🥬│ │📂│ │👅│ │ ← Discover瀑布流 +│ │菜│ │食│ │分│ │标│ │ (MasonryGridView 2列) +│ │品│ │材│ │类│ │签│ │ +│ │ ↓│ │ ↓│ │ ↓│ │ ↓│ │ +│ └──┘ └──┘ └──┘ └──┘ │ +│ ┌──┐ ┌──┐ │ +│ │🍳│ │🕐│ │ +│ │工│ │时│ │ +│ │艺│ │段│ │ +│ └──┘ └──┘ │ +│ │ +│ 🔄 刷新加载更多 35/80 │ +│ ═══════════░░░░░░░░ │ ← 进度条 +│ 最后更新: 5分钟前 │ +│ │ +└─────────────────────────────┘ +``` + +### � 页面视图文件 +- `lib/src/pages/home/home_page.dart`(主页面) +- `lib/src/pages/home/home_recommended.dart`(瀑布流推荐) + +### � 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| 搜索框 | `Get.toNamed` | 搜索页 | `/search` | keyword | +| 菜品卡片 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | +| 食材卡片 | `Get.toNamed` | 搜索页 | `/search` | keyword=食材名 | +| 分类卡片 | `Get.toNamed` | 分类浏览页 | `/category-browse` | category, title | +| 口味标签 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType=taste | +| 工艺标签 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType=cooking | +| 时段卡片 | `Get.toNamed` | 搜索页 | `/search` | keyword=时段名 | +| 营养仪表盘 | `Get.toNamed` | 营养中心 | `/nutrition` | - | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 营养仪表盘与瀑布流风格不统一,仪表盘偏Material风格 | 🟡中 | 统一为Liquid Glass卡片风格 | +| 2 | 瀑布流卡片间距不够精致,部分卡片高度差异过大 | 🟡中 | 调整MasonryGridView crossAxisSpacing/mainAxisSpacing | +| 3 | 搜索框折叠动画与滚动不够丝滑 | 🟢低 | 使用AnimationController替代AnimatedSlide | +| 4 | 底部刷新按钮区域视觉过重,占用空间大 | 🟡中 | 精简为小按钮+进度文字 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 下拉刷新 | P2 | 当前只有底部刷新按钮,缺少下拉手势 | +| 2 | 个性化推荐 | P3 | 瀑布流为随机数据,未基于用户偏好 | +| 3 | 卡片长按菜单 | P3 | 长按卡片可收藏/分享/不感兴趣 | + +--- + +## 🔍 发现页 (DiscoverPage) + +``` +┌─────────────────────────────┐ +│ 发现 │ +│ ┌───────────────────────┐ │ +│ │ 🔍 搜索框 [→搜索页] │ │ ← GlassSearchBar,点击跳转 +│ └───────────────────────┘ │ +│ │ +│ ┌───────────────────────┐ │ +│ │📖推荐│🔥热门│🎲吃什么 │ │ ← GlassSegmentedControl +│ └───────────────────────┘ │ +│ │ +│ ── 推荐 Tab ── │ +│ ┌───────────────────────┐ │ +│ │📖菜谱│🥬食材│👅口味│🍳工艺│ │ ← 4段子分类 +│ └───────────────────────┘ │ +│ ┌──┐ ┌──┐ ┌──┐ │ +│ │分类│ │分类│ │分类│ │ ← 分类/标签网格 +│ │ ↓│ │ ↓│ │ ↓│ │ +│ └──┘ └──┘ └──┘ │ +│ │ +│ ── 热门 Tab ── │ +│ [→热门排行页] │ ← 嵌入HotPage +│ │ +│ ── 今天吃什么 Tab ── │ +│ [→今天吃什么页] │ ← 嵌入WhatToEatPage +│ │ +└─────────────────────────────┘ +``` + +### 📄 页面视图文件 +- `lib/src/pages/discover/discover_page.dart`(主页面) +- `lib/src/pages/discover/hot_page.dart`(热门排行Tab) + +### 🔗 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| 搜索框 | `Get.toNamed` | 搜索页 | `/search` | - | +| 菜谱分类卡片 | `Get.toNamed` | 分类浏览页 | `/category-browse` | category, title | +| 食材分类卡片 | `Get.toNamed` | 分类浏览页 | `/category-browse` | category, title, isIngredient=true | +| 口味标签 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType=taste | +| 工艺标签 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType=cooking | +| 热门Tab | 嵌入页面 | 热门排行页 | `/hot` | - | +| 吃什么Tab | 嵌入页面 | 今天吃什么 | `/what-to-eat` | - | +| 热门菜谱卡片 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 推荐4段子分类(菜谱/食材/口味/工艺)切换无过渡动画 | 🟡中 | 添加AnimatedSwitcher或PageView | +| 2 | 分类网格卡片样式单调,缺少图标/封面图 | 🟡中 | 分类卡片增加emoji图标或渐变色背景 | +| 3 | 热门排行榜列表项间距过密 | 🟢低 | 增加列表项间距 | +| 4 | "今天吃什么"页面视觉冲击力不足 | 🟡中 | 大卡片+动画翻转效果 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 搜索历史 | P2 | 搜索框点击后无历史记录展示 | +| 2 | 热门排行榜分时段 | P3 | 日榜/周榜/月榜切换 | +| 3 | 今天吃什么筛选条件 | P2 | 可按口味/难度/时间筛选随机推荐 | + +--- + +## 🔍 搜索页 (SearchPage) + +``` +┌─────────────────────────────┐ +│ ← [↑返回] 🔍 搜索框 ⚙️ │ ← ⚙️ [→高级搜索页] +├─────────────────────────────┤ +│ │ +│ ── 空状态 ── │ +│ 🔥 热门搜索 │ +│ ┌────┐┌────┐┌────┐ │ +│ │红烧肉││鸡汤││蛋糕│ │ ← 点击 [→搜索页] 自动搜索 +│ └────┘└────┘└────┘ │ +│ │ +│ ── 搜索结果 ── │ +│ 📖菜谱(5)│🥬食材(3)│👅口味│ ← 动态Tab +│ │ +│ ┌─────────────────────┐ │ +│ │ 🖼️ 红烧肉 │ │ ← 菜谱卡片 [→详情页] +│ │ ⭐4.5 👁️1.2k │ │ +│ └─────────────────────┘ │ +│ ┌─────────────────────┐ │ +│ │ 🥬 鸡蛋 分类:蛋类 │ │ ← 食材卡片 [→食材详情] +│ │ 📖 128个菜谱 │ │ +│ └─────────────────────┘ │ +│ ┌─────────────────────┐ │ +│ │ 👅 咸鲜 56道菜谱 │ │ ← 标签卡片 [→标签列表] +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### � 页面视图文件 +- `lib/src/pages/home/search_page.dart`(主页面) +- `lib/src/controllers/search_controller.dart`(搜索控制器) + +### � 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ← 返回 | `Get.back` | 上一个页面 | - | - | +| ⚙️ 高级搜索 | `Get.toNamed` | 高级搜索页 | `/advanced-search` | - | +| 热门搜索词 | 本页搜索 | 本页 | - | keyword | +| 菜谱结果卡片 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | +| 食材结果卡片 | `Get.toNamed` | 食材详情页 | `/tools/ingredient` | ingredientId, ingredientName | +| 口味标签结果 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType=taste | +| 工艺标签结果 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType=cooking | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 热门搜索标签样式过于朴素 | 🟡中 | 使用毛玻璃胶囊标签+渐变色 | +| 2 | 搜索结果卡片间距不统一 | 🟢低 | 统一使用DesignTokens.space3 | +| 3 | Tab切换无滑动指示器 | 🟡中 | 添加下划线滑动动画 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 搜索历史记录 | P2 | 本地保存最近搜索词 | +| 2 | 搜索建议/自动补全 | P3 | 输入时实时显示建议 | +| 3 | 搜索结果分页加载 | P2 | 当前一次加载20条,无分页 | + +--- + +## ⚙️ 高级搜索页 (AdvancedSearchPage) + +``` +┌─────────────────────────────┐ +│ ← [↑返回] 高级搜索 │ +├─────────────────────────────┤ +│ │ +│ 📂 菜谱分类 │ +│ ┌──┐┌──┐┌──┐┌──┐ │ +│ │家常││川菜││粤菜││湘菜│ │ +│ └──┘└──┘└──┘└──┘ │ +│ │ +│ � 口味标签 │ +│ ┌──┐┌──┐┌──┐┌──┐ │ +│ │咸鲜││酱香││麻辣││清淡│ │ +│ └──┘└──┘└──┘└──┘ │ +│ │ +│ 🍳 工艺标签 │ +│ ┌──┐┌──┐┌──┐┌──┐ │ +│ │红烧│ │清蒸│ │炒│ │炖│ │ +│ └──┘ └──┘ └──┘ └──┘ │ +│ │ +│ 🕐 用餐时段 │ +│ ┌──┐┌──┐┌──┐ │ +│ │早餐│ │午餐│ │晚餐│ │ +│ └──┘└──┘└──┘ │ +│ │ +│ ┌─────────────────────┐ │ +│ │ 🔍 搜索 │ │ ← [↑返回搜索页] 执行筛选 +│ └─────────────────────┘ │ +│ ┌─────────────────────┐ │ +│ │ 🔄 重置 │ │ +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### 📄 页面视图文件 +- `lib/src/pages/home/advanced_search_page.dart`(主页面) +- `lib/src/repositories/recipe_repository.dart`(数据源) + +### 🔗 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ← 返回 | `Get.back` | 搜索页 | - | - | +| 🔍 搜索按钮 | `Get.back` + 搜索页刷新 | 搜索页 | `/search` | tasteName/cookingName/categoryId | + +--- + +## �� 菜品详情页 (RecipeDetailPage) + +``` +┌─────────────────────────────┐ +│ ← [↑返回] 菜谱详情 │ +├─────────────────────────────┤ +│ ┌─────────────────────┐ │ +│ │ │ │ +│ │ 🖼️ 封面大图 │ │ ← RecipeCoverImage +│ │ ⭐ 4.5 │ │ ← 评分徽章 +│ │ │ │ +│ └─────────────────────┘ │ +│ │ +│ 📝 红烧肉 │ ← RecipeTitleSection +│ 📂 家常菜 > 红烧 │ ← [→分类浏览页] 非末级可点击 +│ │ +│ 👁️1.2k ❤️56 ⭐4.5 💬0 │ ← RecipeStatisticsBar +│ │ +│ 🏷️ 咸鲜 > 酱香 > 下饭菜 │ ← [→标签列表页] 标签可点击 +│ │ +│ ┌─────────────────────┐ │ +│ │ ⏱️ 30分 📊 中等 │ │ ← RecipeTimeInfo +│ │ 👤 2人份 │ │ +│ └─────────────────────┘ │ +│ │ +│ ── 🥬 食材 ── │ +│ ┌─────────────────────┐ │ +│ │ 主料: 五花肉500g [→食材]│ ← 食材名可点击 +│ │ 辅料: 生抽/老抽/糖 [→食材]│ +│ └─────────────────────┘ │ +│ │ +│ ── 👨‍🍳 步骤 ── │ +│ ┌─────────────────────┐ │ +│ │ Step 1: 切肉... │ │ ← RecipeStepsSection +│ │ Step 2: 煎炒... │ │ +│ │ Step 3: 炖煮... │ │ +│ └─────────────────────┘ │ +│ │ +│ ── 📊 营养 ── │ +│ ┌─────────────────────┐ │ +│ │ 热量: 380kcal │ │ ← RecipeNutritionSection +│ │ 蛋白质/碳水/脂肪 │ │ +│ └─────────────────────┘ │ +│ │ +│ ┌─────────────────────┐ │ +│ │ ❤️ 点赞 ⭐ 评分 📤分享│ ← RecipeActionBar +│ │ 📝 笔记 │ │ [→笔记页] +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### � 页面视图文件 +- `lib/src/pages/home/recipe_detail_page.dart`(主页面) +- `lib/src/widgets/recipe_detail/recipe_statistics_bar.dart`(统计栏组件) + +### � 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ← 返回 | `Get.back` | 上一个页面 | - | - | +| 分类面包屑 | `Get.toNamed` | 分类浏览页 | `/category-browse` | category, title | +| 口味/工艺标签 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType | +| 食材名称 | `Get.toNamed` | 食材详情页 | `/tools/ingredient` | ingredientId, ingredientName | +| 📝 笔记按钮 | `Get.toNamed` | 烹饪笔记页 | `/cooking-note` | recipeId, recipeTitle, recipeInfo | +| 📤 分享按钮 | 系统分享 | iOS Share Sheet | - | - | +| ❤️ 点赞按钮 | API调用 | 本页更新 | - | - | +| ⭐ 评分按钮 | API调用 | 本页弹窗 | - | - | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 封面图与标题之间缺少视觉过渡 | 🟡中 | 封面图底部添加渐变遮罩 | +| 2 | 步骤区域纯文字,缺少步骤图 | 🟡中 | 支持显示步骤配图(如有) | +| 3 | 食材区域排版过于紧凑 | 🟢低 | 增加主料/辅料分组间距 | +| 4 | 底部操作栏与内容区无分隔 | 🟡中 | 添加毛玻璃分隔条 | +| 5 | 营养数据纯文字展示,缺少可视化 | 🟡中 | 添加环形图/进度条展示占比 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 🔗 相关菜谱推荐 | P2 | 详情页底部推荐相似菜品 | +| 2 | 📝 烹饪模式 | P2 | 全屏步骤+计时器+防息屏 | +| 3 | 💬 评论列表 | P3 | 展示用户评论(需后端) | +| 4 | ⚠️ 过敏原警示 | P2 | 食材含过敏原时显示警告 | +| 5 | 📱 二维码海报 | P3 | 生成菜谱分享图 | + +--- + +## 🛠️ 工具中心 (ToolsCenterPage) + +``` +┌─────────────────────────────┐ +│ 🛠️ 工具箱 │ +│ ┌───────────────────────┐ │ +│ │ 🔍 搜索工具 │ │ +│ └───────────────────────┘ │ +│ │ +│ ── 🍳 烹饪助手 ── │ +│ ┌──┐ ┌──┐ ┌──┐ │ +│ │⏱️│ │📝│ │🧮│ │ +│ │计时│ │笔记│ │换算│ │ +│ │ ↓│ │ ↓│ │ ↓│ │ +│ └──┘ └──┘ └──┘ │ +│ │ +│ ── 📋 规划管理 ── │ +│ ┌──┐ ┌──┐ ┌──┐ │ +│ │📅│ │🛒│ │⚖️│ │ +│ │菜单│ │购物│ │缩放│ │ +│ │ ↓│ │ ↓│ │ ↓│ │ +│ └──┘ └──┘ └──┘ │ +│ │ +│ ── 🏥 健康工具 ── │ +│ ┌──┐ ┌──┐ │ +│ │BMI│ │🌙│ │ +│ │计算│ │就寝│ │ +│ │ ↓│ │ ↓│ │ +│ └──┘ └──┘ │ +│ │ +│ ── 🏷️ 热门标签 ── │ +│ ┌────┐┌────┐┌────┐ │ +│ │咸鲜 ││酱香 ││清淡 │ │ ← [→标签列表页] +│ └────┘└────┘└────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### 📄 页面视图文件 +- `lib/src/pages/tools/tools_center_page.dart`(主页面) +- `lib/src/widgets/glass/glass_container.dart`(毛玻璃卡片) + +### 🔗 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ⏱️ 烹饪计时 | `Get.toNamed` | 烹饪计时器 | `/cooking-timer` | - | +| 📝 烹饪笔记 | `Get.toNamed` | 烹饪笔记页 | `/cooking-note` | recipeId, recipeTitle | +| 🧮 用量换算 | `Get.toNamed` | 单位换算页 | `/unit-converter` | - | +| 📅 每周菜单 | `Get.toNamed` | 菜单规划页 | `/tools/weekly-menu` | - | +| 🛒 购物清单 | `Get.toNamed` | 购物清单页 | `/shopping-list` | - | +| ⚖️ 份量缩放 | `Get.toNamed` | 份量缩放页 | `/serving-scaler` | ingredients, servings | +| BMI计算 | `Get.toNamed` | BMI计算器 | `/bmi-calculator` | - | +| 🌙 就寝提醒 | `Get.toNamed` | 就寝提醒页 | `/profile/bedtime-reminder` | - | +| 热门标签 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 工具图标+文字卡片样式过于简单 | 🟡中 | 增加渐变背景或毛玻璃效果 | +| 2 | 分组标题缺少视觉层次 | 🟢低 | 添加分组图标和分割线 | +| 3 | 热门标签区域与工具卡片风格不一致 | 🟡中 | 统一为毛玻璃胶囊样式 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 用餐时段推荐 | P2 | eating_times.json数据已有,缺页面入口 | +| 2 | 过敏原检测器增强 | P2 | 当前仅展示,缺智能过滤 | +| 3 | 工具使用统计 | P3 | 展示常用工具排行 | + +--- + +## 👤 我的 (ProfilePage) + +``` +┌─────────────────────────────┐ +│ 我的 │ +│ ┌───────────────────────┐ │ +│ │ 🏠 首页 │ ⚙️ 更多 │ │ ← GlassSegmentedControl +│ └───────────────────────┘ │ +│ │ +│ ── 首页 Tab ── │ +│ ┌───────────────────────┐ │ +│ │ 🧑 用户头像 │ │ +│ │ 美食爱好者 │ │ +│ │ ⭐ Lv.5 烹饪达人 │ │ +│ └───────────────────────┘ │ +│ │ +│ ┌──┐┌──┐┌──┐┌──┐ │ +│ │❤️││👀││📝││🛒│ │ ← 功能入口 +│ │收藏││足迹││笔记││购物│ │ +│ │ ↓│ │ ↓│ │ ↓│ │ ↓│ │ +│ └──┘└──┘└──┘└──┘ │ +│ │ +│ ┌──┐┌──┐┌──┐┌──┐ │ +│ │⏱️│ │🔄│ │📊│ │⚖️│ │ ← 工具快捷 +│ │计时│ │换算│ │BMI│ │缩放│ │ +│ │ ↓│ │ ↓│ │ ↓│ │ ↓│ │ +│ └──┘ └──┘ └──┘ └──┘ │ +│ │ +│ ── 更多 Tab ── │ +│ ┌───────────────────────┐ │ +│ │ 📊 数据管理中心 [→] │ │ +│ │ 🎨 个性化设置 [→] │ │ +│ │ 🗑️ 缓存管理 [→] │ │ +│ │ 🌙 就寝提醒 [→] │ │ +│ │ 📋 营养中心 [→] │ │ +│ │ 💬 意见反馈 [→] │ │ +│ └───────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### � 页面视图文件 +- `lib/src/pages/profile/profile_page.dart`(主页面) +- `lib/src/pages/profile/profile_home.dart`(首页Tab) + +### � 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ❤️ 收藏 | `Get.toNamed` | 收藏页 | `/favorites` | - | +| 👀 足迹 | `Get.toNamed` | 浏览记录页 | `/footprints` | - | +| 📝 笔记 | `Get.toNamed` | 烹饪笔记页 | `/cooking-note` | - | +| 🛒 购物 | `Get.toNamed` | 购物清单页 | `/shopping-list` | - | +| ⏱️ 计时 | `Get.toNamed` | 烹饪计时器 | `/cooking-timer` | - | +| 🔄 换算 | `Get.toNamed` | 单位换算页 | `/unit-converter` | - | +| 📊 BMI | `Get.toNamed` | BMI计算器 | `/bmi-calculator` | - | +| ⚖️ 缩放 | `Get.toNamed` | 份量缩放页 | `/serving-scaler` | - | +| 📊 数据中心 | `Get.toNamed` | 数据管理中心 | `/data-center` | - | +| 🎨 个性化 | `Get.toNamed` | 个性化设置 | `/personalization` | - | +| 🗑️ 缓存 | `Get.toNamed` | 缓存管理页 | 内页 | - | +| 🌙 就寝 | `Get.toNamed` | 就寝提醒 | `/profile/bedtime-reminder` | - | +| 📋 营养 | `Get.toNamed` | 营养中心 | `/nutrition` | - | +| 💬 反馈 | `Get.toNamed` | 意见反馈 | `/chat` | - | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 用户头像区域过于简单,无背景装饰 | 🟡中 | 添加渐变背景+装饰元素 | +| 2 | 功能入口图标样式不统一 | 🟡中 | 统一为圆形图标+渐变背景 | +| 3 | 设置列表项缺少图标 | 🟢低 | 每项添加对应emoji图标 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 用户等级系统 | P3 | 浏览/收藏/评论获得经验值 | +| 2 | 成就徽章 | P3 | 烹饪达人/美食家等徽章 | +| 3 | 数据导出 | P3 | 导出收藏/浏览记录 | + +--- + +## 🥬 食材详情页 (IngredientDetailPage) + +``` +┌─────────────────────────────┐ +│ ← [↑返回] 食材详情 │ +├─────────────────────────────┤ +│ ┌─────────────────────┐ │ +│ │ 🖼️ 食材图片 │ │ +│ └─────────────────────┘ │ +│ │ +│ 🥚 鸡蛋 │ +│ 📂 分类: 蛋类 │ +│ │ +│ ── 📖 相关菜谱 ── │ +│ ┌──┐ ┌──┐ ┌──┐ │ +│ │菜│ │菜│ │菜│ │ ← [→菜品详情页] +│ └──┘ └──┘ └──┘ │ +│ │ +│ ┌─────────────────────┐ │ +│ │ 🏠 返回首页 │ │ ← Get.until((route) => route.isFirst) +│ └─────────────────────┘ │ +└─────────────────────────────┘ +``` + +### 📄 页面视图文件 +- `lib/src/pages/tools/ingredient_detail_page.dart`(主页面) +- `lib/src/controllers/ingredient_controller.dart`(食材控制器) + +### 🔗 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ← 返回 | `Get.back` | 上一个页面 | - | - | +| 相关菜谱卡片 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | +| � 返回首页 | `Get.until((route) => route.isFirst)` | 首页(Tab0) | - | - | + +### �� 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 食材信息区域过于简单 | 🟡中 | 添加营养成分摘要/季节性/选购建议 | +| 2 | 相关菜谱卡片样式与首页不一致 | 🟡中 | 统一卡片样式 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 食材营养详情 | P2 | 展示食材营养成分数据 | +| 2 | 食材替代建议 | P3 | 缺少该食材时的替代品推荐 | +| 3 | 过敏原关联 | P2 | 关联数据管理中心的过敏原设置 | + +--- + +## 📂 分类浏览页 (CategoryBrowsePage) + +``` +┌─────────────────────────────┐ +│ ← [↑返回] 📂 家常菜 │ +├─────────────────────────────┤ +│ ┌──┐ ┌──┐ ┌──┐ │ +│ │红烧│ │炖菜│ │炒菜│ │ ← 子分类网格 [→分类浏览页(递归)] +│ └──┘ └──┘ └──┘ │ +│ │ +│ ── 菜谱列表 ── │ +│ ┌─────────────────────┐ │ +│ │ 🖼️ 红烧肉 ⭐4.5 │ │ ← [→菜品详情页] +│ └─────────────────────┘ │ +│ ┌─────────────────────┐ │ +│ │ 🖼️ 炖排骨 ⭐4.2 │ │ ← [→菜品详情页] +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### � 页面视图文件 +- `lib/src/pages/discover/category_browse_page.dart`(主页面) +- `lib/src/controllers/category_controller.dart`(分类控制器) + +### � 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ← 返回 | `Get.back` | 上一个页面 | - | - | +| 子分类卡片 | `Get.toNamed` | 分类浏览页(递归) | `/category-browse` | category, title | +| 菜谱卡片 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 子分类网格与菜谱列表之间缺少视觉分隔 | 🟢低 | 添加分组标题 | +| 2 | 菜谱列表卡片样式单调 | 🟡中 | 添加封面图+评分+浏览量 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 分页加载 | P2 | 当前一次加载,无分页 | +| 2 | 排序筛选 | P3 | 按评分/浏览量/最新排序 | + +--- + +## 🏷️ 标签菜谱列表 (TagRecipeListPage) + +``` +┌─────────────────────────────┐ +│ ← [↑返回] 👅 咸鲜 │ +├─────────────────────────────┤ +│ ┌─────────────────────┐ │ +│ │ 🖼️ 红烧肉 ⭐4.5 │ │ ← [→菜品详情页] +│ └─────────────────────┘ │ +│ ┌─────────────────────┐ │ +│ │ 🖼️ 清蒸鱼 ⭐4.3 │ │ ← [→菜品详情页] +│ └─────────────────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### 📄 页面视图文件 +- `lib/src/pages/home/tag_recipe_list_page.dart`(主页面) +- `lib/src/controllers/tag_controller.dart`(标签控制器) + +### 🔗 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| ← 返回 | `Get.back` | 上一个页面 | - | - | +| 菜谱卡片 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 页面顶部缺少标签说明/统计 | 🟡中 | 添加"共XX道菜"统计 | +| 2 | 列表卡片样式与首页瀑布流不统一 | 🟡中 | 可考虑使用小瀑布流布局 | + +### ⚡ 功能缺失 +| # | 功能 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 分页加载 | P2 | 当前无分页 | +| 2 | 排序筛选 | P3 | 按评分/浏览量排序 | + +--- + +## 🔥 热门排行页 (HotPage) + +``` +┌─────────────────────────────┐ +│ 🔥 热门排行 │ +│ �菜谱│🥬食材│👅口味│🍳工艺 │ ← Tab切换 +│ │ +│ 🥇 红烧肉 ❤️ 2.3k [→详情]│ +│ 🥈 牛肉面 ❤️ 1.8k [→详情]│ +│ 🥉 凉拌菜 ❤️ 1.5k [→详情]│ +│ 4. 宫保鸡丁 ❤️ 1.2k [→详情]│ +│ 5. 糖醋排骨 ❤️ 1.1k [→详情]│ +│ │ +└─────────────────────────────┘ +``` + +### � 页面视图文件 +- `lib/src/pages/discover/hot_page.dart`(主页面) +- `lib/src/controllers/hot_controller.dart`(热门控制器) + +### � 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| 排行菜谱卡片 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | +| 排行食材卡片 | `Get.toNamed` | 食材详情页 | `/tools/ingredient` | ingredientId, ingredientName | +| 排行标签 | `Get.toNamed` | 标签菜谱列表 | `/tag-recipe-list` | tagName, tagId, tagType | + +### 🎨 美观问题 +| # | 问题 | 严重度 | 建议 | +|---|------|--------|------| +| 1 | 排行榜前三名缺少视觉突出 | 🟡中 | 前三名使用金银铜色+大号排名 | +| 2 | 列表项样式过于简单 | 🟡中 | 添加封面缩略图+评分星级 | + +--- + +## 🎲 今天吃什么 (WhatToEatPage) + +``` +┌─────────────────────────────┐ +│ 🎲 今天吃什么 │ +│ │ +│ ┌─────────────────────┐ │ +│ │ │ │ +│ │ 🖼️ 推荐菜谱大卡 │ │ ← [→菜品详情页] +│ │ 红烧肉 │ │ +│ │ ⭐4.5 👁️1.2k │ │ +│ │ │ │ +│ └─────────────────────┘ │ +│ │ +│ ┌────────┐ ┌────────┐ │ +│ │ 🔄换一个│ │ 📖看详情│ │ ← 换一个=刷新, 看详情=[→详情] +│ └────────┘ └────────┘ │ +│ │ +└─────────────────────────────┘ +``` + +### 📄 页面视图文件 +- `lib/src/pages/discover/what_to_eat_page.dart`(主页面) +- `lib/src/controllers/what_to_eat_controller.dart`(推荐控制器) + +### 🔗 跳转关系 +| 触发元素 | 跳转方式 | 目标页面 | 路由 | 传参 | +|---------|---------|---------|------|------| +| 📖 看详情 | `Get.toNamed` | 菜品详情页 | `/recipe-detail` | id | +| 🔄 换一个 | 本页刷新 | 本页 | - | - | + +--- + +## 📊 全局美观问题汇总 + +| 优先级 | 问题 | 影响页面 | 建议 | +|--------|------|---------|------| +| 🟡高 | 毛玻璃效果使用不一致 | 全局 | 统一GlassCard组件,所有卡片使用相同毛玻璃参数 | +| 🟡高 | 卡片圆角不统一 | 全局 | 统一使用DesignTokens.radiusMd/Lg | +| 🟡高 | 颜色使用偶尔硬编码 | 全局 | 严格使用DesignTokens颜色变量 | +| 🟡中 | 空状态页面缺少插画 | 多个页面 | 设计统一的空状态插画+文案 | +| 🟡中 | 加载状态不统一 | 全局 | 统一骨架屏样式和动画 | +| 🟢低 | 页面转场动画单调 | 全局 | 添加iOS风格滑入动画 | + +## 📊 全局功能缺失汇总 + +| 优先级 | 功能 | 影响页面 | 说明 | +|--------|------|---------|------| +| P1 | 下拉刷新 | 首页/发现 | 缺少下拉手势刷新 | +| P2 | 搜索历史 | 搜索页 | 无本地搜索记录 | +| P2 | 分页加载 | 多个列表页 | 分类浏览/标签列表无分页 | +| P2 | 相关推荐 | 详情页 | 缺少相关菜谱推荐 | +| P2 | 烹饪模式 | 详情页 | 全屏步骤+计时器 | +| P2 | 过敏原警示 | 详情页 | 食材含过敏原时警告 | +| P2 | 营养可视化 | 详情页 | 环形图/进度条展示营养占比 | +| P3 | 排序筛选 | 列表页 | 按评分/浏览量/最新排序 | +| P3 | 评论系统 | 详情页 | 需后端支持 | +| P3 | 用户等级 | 个人中心 | 经验值+等级+徽章 | diff --git a/docs/dev/UNFINISHED_FEATURES.md b/docs/dev/UNFINISHED_FEATURES.md index 926de0d..f74cf3b 100644 --- a/docs/dev/UNFINISHED_FEATURES.md +++ b/docs/dev/UNFINISHED_FEATURES.md @@ -1,430 +1,134 @@ # 📋 未完成功能清单 -> 文档创建: 2026-04-09 -> 最后更新: 2026-04-13 -> 说明: 记录所有未完成的功能任务,跟踪开发进度 -> 优先级说明: P1=核心功能 P2=重要功能 P3=增强功能 -> 优先级值1-5: 5=最高优先级(多次提及自动提升) +> 创建: 2026-04-09 | 更新: 2026-04-13 | 优先级: P1=核心 P2=重要 P3=增强 | 优先级值1-5(5=最高) --- ## 📊 总体进度 -| 阶段 | 总任务 | 已完成 | 未完成 | 完成率 | -|------|--------|--------|--------|--------| -| 三:热量追踪+营养分析 | 7 | 7 | 0 | 100% ✅ | -| 四:购物清单 | 5 | 5 | 0 | 100% ✅ | -| 十二:社交+通知增强 | 4 | 2 | 2 | 50% 🟡 | -| 十三:AI+规划高级功能 | 4 | 4 | 0 | 100% ✅ | -| 十四:接口能力挖掘 | 8 | 1 | 7 | 12% 🟡 | -| 十五:后端接口增强 | 6 | 0 | 6 | 0% 🔴 | -| 十六:用户体验优化+Bug 修复 | 7 | 7 | 0 | 100% ✅ | -| 十七:紧急Bug修复 | 14 | 14 | 0 | 100% ✅ | -| 十九:综合Bug修复+功能增强 | 18 | 18 | 0 | 100% ✅ | -| 二十:用户体验优化+交互增强 | 7 | 7 | 0 | 100% ✅ | -| 二十一:菜谱详情页功能增强 | 1 | 1 | 0 | 100% ✅ | -| 二十二:Picid功能Bug修复 | 2 | 2 | 0 | 100% ✅ | -| 二十三:数据管理中心 | 3 | 1 | 2 | 33% 🟡 | -| 二十四:笔记+浏览记录功能 | 2 | 2 | 0 | 100% ✅ | -| 二十五:多项功能Bug修复 | 6 | 6 | 0 | 100% ✅ | -| **二十六:API v3.2.0迁移** | **5** | **5** | **0** | **100% ✅** | -| **二十七:首页Discover瀑布流** | **8** | **8** | **0** | **100% ✅** | -| **二十八:瀑布流渐进式渲染+分页** | **3** | **3** | **0** | **100% ✅** | -| **合计** | **179** | **162** | **17** | **91%** | +| 阶段 | 任务 | 完成 | 率 | 状态 | +|------|------|------|-----|------| +| 三:热量追踪+营养分析 | 7 | 7 | 100% | ✅ | +| 四:购物清单 | 5 | 5 | 100% | ✅ | +| 十二:社交+通知增强 | 4 | 2 | 50% | 🟡 | +| 十三:AI+规划高级功能 | 4 | 4 | 100% | ✅ | +| 十四:接口能力挖掘 | 8 | 1 | 12% | 🟡 | +| 十五:后端接口增强 | 6 | 0 | 0% | 🔴 | +| 十六:用户体验优化+Bug修复 | 7 | 7 | 100% | ✅ | +| 十七:紧急Bug修复 | 14 | 14 | 100% | ✅ | +| 十九:综合Bug修复+功能增强 | 18 | 18 | 100% | ✅ | +| 二十:用户体验优化+交互增强 | 7 | 7 | 100% | ✅ | +| 二十一:菜谱详情页功能增强 | 1 | 1 | 100% | ✅ | +| 二十二:Picid功能Bug修复 | 2 | 2 | 100% | ✅ | +| 二十三:数据管理中心 | 3 | 1 | 33% | 🟡 | +| 二十四:笔记+浏览记录功能 | 2 | 2 | 100% | ✅ | +| 二十六:API v3.2.0迁移 | 5 | 5 | 100% | ✅ | +| 二十七:首页Discover瀑布流 | 8 | 8 | 100% | ✅ | +| 二十八:瀑布流渐进式渲染+分页 | 3 | 3 | 100% | ✅ | +| 二十九:菜品详情页功能完善 | 9 | 9 | 100% | ✅ | +| 三十:发现页口味/工艺筛选 | 3 | 2 | 67% | 🟡 | +| 三十一:搜索功能修复与高级搜索 | 3 | 3 | 100% | ✅ | +| 三十二:主题色全局生效修复 | 1 | 1 | 100% | ✅ | +| **合计** | **192** | **174** | **91%** | | --- -## ✅ 阶段二十六:API v3.2.0迁移(P1) +## 🔴 未完成任务汇总 -**目标**:适配后端API v3.2.0,移除已删除接口,更新评分系统 -**前置依赖**:无 -**关键阻塞**:无 -**完成日期**:2026-04-12 +> 按优先级排序,仅列出未完成任务 -| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 | -|------|------|---------|--------|------|------| -| 26.1 | 🔧 移除preference接口 | `lib/src/config/api_config.dart` | P1 | ✅ 已完成 | api_preference.php已删除,偏好改为本地存储 | -| 26.2 | 💾 偏好本地存储 | `lib/src/repositories/preference_repository.dart` | P1 | ✅ 已完成 | SharedPreferences替代API存储 | -| 26.3 | ⭐ 评分接口迁移 | `lib/src/repositories/action_repository.dart` | P1 | ✅ 已完成 | recommend→rate(1-5分) | -| 26.4 | 📱 信息流接口更新 | `lib/src/repositories/feed_repository.dart` | P1 | ✅ 已完成 | 移除personal接口 | -| 26.5 | 🎮 Controller适配 | 多个Controller文件 | P1 | ✅ 已完成 | feed/action/recipe_detail控制器更新 | - -### 功能详情 - -#### 26.1 移除preference接口 -- **变更**:`api_preference.php` 已被后端删除 -- **解决方案**: - - 从 `api_config.dart` 移除 `preference` 常量 - - 新增 `filter` 和 `discover` 接口定义 - -#### 26.2 偏好本地存储 -- **变更**:用户偏好设置不再有后端API支持 -- **解决方案**: - - `PreferenceRepository` 改为使用 `SharedPreferences` 本地存储 - - 保留所有方法签名,确保向后兼容 - -#### 26.3 评分接口迁移 -- **变更**:`recommend` 接口改为 `rate` 评分接口 -- **新特性**: - - 评分范围:1-5分 - - 每日限制:每个IP每天最多30次 - - 不可取消:评分后无法撤销 -- **解决方案**: - - `ActionRepository.recommend()` → `ActionRepository.rate()` - - `IpStatus` 字段更新:`recommendUsed` → `rateUsed` - - 保留 `@Deprecated` 兼容方法 - -#### 26.4 信息流接口更新 -- **变更**:`api_feed.php?act=personal` 接口已删除 -- **解决方案**: - - `FeedRepository` 移除 `fetchPersonal()` 方法 - - `FeedType` 枚举移除 `personal` 值 - - 信息流类型从4种减少为3种:推荐/最新/热门 - -#### 26.5 Controller适配 -- **变更**:Controller需要适配新的Repository接口 -- **解决方案**: - - `FeedController`:移除personal相关逻辑 - - `ActionController`:recommendItem → rateItem - - `RecipeDetailController`:recommendRecipe → rateRecipe - -### API变更对照表 - -| 原接口 | 新接口/方案 | 说明 | -|--------|------------|------| -| `api_preference.php` | SharedPreferences本地存储 | 已删除 | -| `api_feed.php?act=personal` | 使用 `act=recommend` 替代 | 已删除 | -| `api_action.php?act=recommend` | `api_action.php?act=rate` | 改为评分(1-5分) | -| `recommend_nums/recommend_score` | `rate_nums/rate_score` | 字段重命名 | +| 序号 | 阶段 | 任务 | 优先级 | 优先级值 | 说明 | +|------|------|------|--------|---------|------| +| 1 | 十二 | 🔔 烹饪提醒通知 | P2 | 3 | 定时提醒烹饪步骤,与计时器联动,需 flutter_local_notifications | +| 2 | 十二 | 📸 拍照记录 | P3 | 2 | 烹饪笔记支持拍照上传 | +| 3 | 十四 | 🕐 用餐时段推荐 | P2 | 3 | eating_times.json(34种时段),早餐/午餐/晚餐推荐 | +| 4 | 十四 | 📊 营养分析增强 | P2 | 3 | nutrition_types.json(31种),营养详情+目标追踪+图表 | +| 5 | 十四 | ⚠️ 过敏原警示增强 | P2 | 3 | 菜谱详情页过敏原警示+自动过滤+替代建议 | +| 6 | 十四 | 🏆 点赞/推荐系统 | P2 | 2 | 五星评分+评价统计+排行榜(部分已在阶段29实现) | +| 7 | 十四 | 📱 二维码海报 | P3 | 2 | 生成菜谱二维码分享图 | +| 8 | 十四 | 🔗 社交分享增强 | P3 | 2 | 分享链接+热度标签+社交平台 | +| 9 | 十五 | 👤 用户注册登录 | P1 | 5 | 需后端支持,当前暂不开发 | +| 10 | 十五 | 💾 收藏云端同步 | P1 | 4 | 需后端支持 | +| 11 | 十五 | 💬 评论系统 | P2 | 3 | 需后端支持 | +| 12 | 十五 | 🔔 消息推送 | P2 | 2 | 需后端支持 | +| 13 | 十五 | 📜 浏览历史同步 | P2 | 2 | 需后端支持 | +| 14 | 十五 | 📝 菜谱上传 | P2 | 2 | 需后端支持 | +| 15 | 二十三 | ⚠️ 过敏原智能过滤 | P2 | 3 | 搜索/推荐时自动过滤含用户过敏原的菜品 | +| 16 | 三十 | 🔗 相关菜谱推荐 | P2 | 3 | 详情页底部添加相关菜谱列表 | --- -## ✅ 阶段二十七:首页Discover瀑布流重构(P1) +## ✅ 已完成阶段(精简记录) +### 阶段二十九:菜品详情页功能完善 ✅ +- ⭐ RecipeRating模型(score/nums/display/status/level/star) +- 📊 RecipeStatistics更新(rateNums/rateScore/rating) +- 📈 RecipeStatisticsBar重构(星级+评分文本+等级标签) +- 🖼️ RecipeCoverImage评分徽章 +- 🏷️ 标签点击跳转TagRecipeListPage +- 📂 分类面包屑点击跳转CategoryBrowsePage -## ✅ 阶段二十八:瀑布流渐进式渲染+分页加载(P1) +### 阶段三十:发现页口味/工艺筛选 🟡(1项未完成) +- ✅ 口味标签筛选(3列网格→TagRecipeListPage) +- ✅ 工艺标签筛选(3列网格→TagRecipeListPage) +- ❌ 相关菜谱推荐(详情页底部) -**目标**:实现卡片渐进式渲染、到底刷新按钮、分页增量加载 -**前置依赖**:阶段二十七(Discover瀑布流已完成) -**关键阻塞**:无 -**完成日期**:2026-04-13 +### 阶段三十一:搜索功能修复与高级搜索 ✅ +- 🔍 切换到global_search接口,4-Tab结果(菜谱/食材/口味/工艺) +- ⚙️ 高级搜索页面(分类/口味/工艺/时段筛选) +- 🏠 食材详情返回首页 -| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 | -|------|------|---------|--------|------|------| -| 28.1 | ✨ 渐进式渲染 | `lib/src/widgets/discover/discover_waterfall.dart` | P1 | ✅ 已完成 | 卡片逐个绘制+骨架屏占位+入场动画 | -| 28.2 | 🔄 刷新按钮+分页 | `lib/src/pages/home/home_page.dart` | P1 | ✅ 已完成 | 到底显示刷新按钮+进度条+时间戳 | -| 28.3 | 📦 增量获取支持 | `lib/src/repositories/discover_repository.dart` | P2 | ✅ 已完成 | 新增fetchMore方法 | +### 阶段三十二:主题色全局生效修复 ✅ +- 🎨 根因:UI组件使用 `DesignTokens.primary`(静态常量),切换主题色后不刷新 +- 🔧 批量替换为 `DesignTokens.dynamicPrimary`(59个文件,439处引用) +- 📐 修复const上下文和默认参数值兼容问题 -### 功能详情 +### 阶段二十七:首页Discover瀑布流 ✅ +- MasonryGridView 2列瀑布流,5种卡片类型混合展示 +### 阶段二十八:瀑布流渐进式渲染+分页 ✅ +- 渐进式渲染+骨架屏+入场动画+到底刷新按钮+分页加载 -## ✅ 阶段二十四:笔记+浏览记录功能(P1) +### 阶段二十四:笔记+浏览记录 ✅ +- 笔记标签+菜品快捷输入+SharedPreferences双重存储 +- 浏览记录模型+控制器+页面+自动记录 -**目标**:修复笔记保存问题,实现真实浏览记录功能 -**前置依赖**:无 -**关键阻塞**:无 +### 阶段二十三:数据管理中心 🟡(1项未完成) +- ✅ 数据管理中心页面(分类标签+过敏原管理) +- ✅ LocalDataService(SharedPreferences+API同步+24h缓存) +- ❌ 过敏原智能过滤 -| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 | -|------|------|---------|--------|------|------| -| 24.1 | 📝 笔记功能增强 | `lib/src/pages/tools/cooking_note_page.dart` | P1 | ✅ 已完成 | 标签关键字+菜品字段快捷输入+SharedPreferences存储 | -| 24.2 | 👀 浏览记录功能 | `lib/src/pages/profile/footprints_page.dart` | P1 | ✅ 已完成 | 真实浏览记录+SharedPreferences存储+管理功能 | +### 阶段十二:社交+通知增强 🟡(2项未完成) +- ✅ 分享菜谱(share_plus) +- ✅ 搜索建议/热词(API获取) +- ❌ 烹饪提醒通知 +- ❌ 拍照记录 -### 功能详情 - -#### 24.1 笔记功能增强 -- **问题**:笔记保存后丢失 -- **解决方案**: - - 新增 SharedPreferences 双重存储,确保数据不丢失 - - 新增标签关键字功能,支持为笔记添加多个标签 - - 新增菜品字段快捷输入:菜名、简介、分类、标签、难度、时间、食材 -- **影响文件**: - - `lib/src/models/cooking_note_model.dart` — 新增 tags 字段 - - `lib/src/controllers/cooking_note_controller.dart` — SharedPreferences 存储 - - `lib/src/pages/tools/cooking_note_page.dart` — 标签+快捷输入UI - -#### 24.2 浏览记录功能 -- **问题**:浏览记录页面使用假数据 -- **解决方案**: - - 新增 BrowseHistoryModel 模型 - - 新增 BrowseHistoryController 控制器 - - 菜谱详情页自动记录浏览历史 - - 支持左滑删除、清空所有记录 -- **影响文件**: - - `lib/src/models/browse_history_model.dart` — 新建 - - `lib/src/controllers/browse_history_controller.dart` — 新建 - - `lib/src/pages/profile/footprints_page.dart` — 重构 - - `lib/src/controllers/recipe_detail_controller.dart` — 集成 +### 阶段十三:AI+规划高级功能 ✅ +- AI菜谱推荐+每周菜单规划+食材用量换算+就寝提醒 --- -## 🟡 阶段二十三:数据管理中心(P2) +## 🔴 阶段十五:后端接口增强(需后端支持) -**目标**:统一管理分类标签和过敏原数据,支持API同步与本地持久化 -**前置依赖**:阶段十四(过敏原数据) -**关键阻塞**:无 - -| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 | -|------|------|---------|--------|------|------| -| 23.1 | 📊 数据管理中心页面 | `lib/src/pages/profile/data_center_page.dart` | P2 | ✅ 已完成 | 分类标签(口味/工艺/食材)+过敏原管理页面 | -| 23.2 | 💾 LocalDataService 数据服务 | `lib/src/services/data/local_data_service.dart` | P2 | ✅ 已完成 | SharedPreferences持久化+API同步+24h缓存 | -| 23.3 | ⚠️ 过敏原智能过滤 | 搜索/推荐模块集成 | P2 | ❌ 未实现 | 将用户设置的过敏原用于菜品过滤 | - -### 功能详情 - -#### 23.1 数据管理中心页面 -- **入口**:我的 → 首页 → 📊 数据管理中心 -- **功能**: - - 分类标签区域:口味标签(蓝色)、工艺标签(橙色)、食材分类(绿色) - - 过敏原管理区域:21大类585种过敏原,支持展开/收起 - - 个人过敏原设置:勾选后即时保存到本地 - - 一键同步所有数据 -- **技术方案**:Cupertino组件 + LocalDataService + showCupertinoModalPopup - -#### 23.2 LocalDataService -- **功能**: - - 口味标签:`api.php?act=tags` - - 工艺标签:`api.php?act=list` 提取 process 字段 - - 食材分类:`api.php?act=categories` - - 过敏原全量:`gmy.json` (585条) - - 用户过敏原:本地勾选保存 -- **缓存策略**:SharedPreferences + 24h过期 + 手动刷新 - -#### 23.3 过敏原智能过滤(待开发) -- **目标**:搜索/推荐时自动过滤含用户过敏原的菜品 -- **实现方式**:调用 `LocalDataService.getUserAllergens()` 获取用户过敏原列表,在推荐结果中排除含这些过敏原的菜谱 -- **影响文件**:搜索页、首页推荐、智能推荐服务 - ---- - -## 🟢 阶段十二:社交+通知增强(P2) - -**目标**:增加社交分享和通知提醒功能 -**前置依赖**:阶段十一完成 -**关键阻塞**:12.1 需 `share_plus`,12.2 需 `flutter_local_notifications` - -| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 | -|------|------|---------|--------|------|------| -| 12.1 | 📱 分享菜谱 | `lib/src/pages/home/recipe_detail_page.dart` | P2 | ✅ 已完成 | 分享按钮+生成菜谱文本+调用share_plus | -| 12.2 | 🔔 烹饪提醒通知 | `lib/src/services/notification_service.dart` | P2 | ❌ 未实现 | 定时提醒烹饪步骤,与计时器联动 | -| 12.3 | 🔍 搜索建议/热词 | `lib/src/controllers/search_controller.dart` | P2 | ✅ 已完成 | 从API获取热门标签,替代硬编码热词,保留fallback | -| 12.4 | 📸 拍照记录 | `lib/src/pages/tools/cooking_note_page.dart` | P3 | ❌ 未实现 | 烹饪笔记支持拍照上传,记录成品 | - -### 功能详情 - -#### 12.1 分享菜谱 -- **入口**:菜谱详情页 → 分享按钮 -- **功能**: - - 生成菜谱卡片图片(封面+标题+食材摘要) - - 调用 iOS Share Sheet / Android 分享面板 - - 支持保存到相册 -- **技术方案**:`screenshot` + `share_plus` - -#### 12.2 烹饪提醒通知 -- **入口**:烹饪模式 → 设置提醒 -- **功能**: - - 烹饪步骤到达时发送本地通知 - - 计时器完成时通知 - - 支持自定义提醒时间 -- **技术方案**:`flutter_local_notifications` - -#### 12.3 搜索建议/热词 -- **入口**:搜索页搜索栏 -- **功能**: - - 空搜索框时展示热门搜索词 - - 输入时自动补全建议 - - 热门搜索词从 API 获取 -- **技术方案**:`RecipeRepository.fetchTags()` 获取热词 - - - -## 🔵 阶段十三:AI+规划高级功能(P3) - -**目标**:实现智能化和规划类高级功能 -**前置依赖**:阶段十二完成 -**关键阻塞**:13.1 需 AI API,13.2 需日历组件 - -| 序号 | 任务 | 产出文件 | 优先级 | 状态 | 说明 | -|------|------|---------|--------|------|------| -| 13.1 | AI 菜谱推荐 | `lib/src/services/recommendation_service.dart` | P3 | ✅ 已实现 | 基于口味偏好+浏览历史,智能推荐菜谱 | -| 13.2 | 每周菜单规划 | `lib/src/pages/tools/weekly_menu_planner_page.dart` | P3 | ✅ 已实现 | 日历视图规划一周饮食,自动生成购物清单 | -| 13.3 | 食材用量换算增强 | `lib/src/pages/tools/serving_scaler_page.dart` | P3 | ✅ 已实现 | 添加单位换算Tab(重量/容量/计数)+常用换算表 | -| 13.4 | 就寝提醒 | `lib/src/pages/profile/bedtime_reminder_page.dart` | P3 | ✅ 已实现 | 根据饮食时间推荐健康作息 | - -### 功能详情 - -#### 13.1 AI 菜谱推荐 -- **入口**:首页"为你推荐"Tab -- **功能**: - - 基于用户口味偏好(PreferenceController) - - 基于浏览历史(FootprintsPage 数据) - - 基于收藏记录(FavoritesController) - - 推荐相似菜谱 -- **技术方案**:调用后端推荐 API 或本地协同过滤算法 - -#### 13.2 每周菜单规划 -- **入口**:工具页 → "📅 每周菜单" -- **功能**: - - 日历视图选择日期 - - 每日早/中/晚三餐分配菜谱 - - 自动汇总生成购物清单 - - 支持拖拽调整 -- **技术方案**:自定义日历组件 + Hive 持久化 - -#### 13.3 食材用量换算增强 -- **入口**:工具页 → 份量缩放 -- **功能**: - - 支持克/千克/磅/盎司互转 - - 支持毫升/升/杯/汤匙互转 - - 常用食材密度表 -- **技术方案**:扩展 `serving_scaler_page.dart`,添加单位换算 Tab - -#### 13.4 就寝提醒 -- **入口**:设置 → 健康提醒 -- **功能**: - - 根据晚餐时间推荐就寝时间 - - 睡前不宜进食提醒 - - 与营养追踪联动 -- **技术方案**:`flutter_local_notifications` + 健康算法 - -### 验收标准 -- [x] "为你推荐"展示个性化推荐菜谱 -- [x] 每周菜单可规划三餐并生成购物清单 -- [x] 份量缩放支持多种单位换算 -- [x] 就寝提醒根据饮食时间智能推荐 - ---- - -## 🟢 阶段十四:接口能力挖掘(P1/P2) - -**目标**:利用已有API接口能力,实现App端未开发的功能 - -## 🔴 阶段十六:用户体验优化+Bug修复(P0/P1) - - -### 技术要点 - -- **数据文件**:`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种过敏原数据) -- **功能**: - - ✅ 过敏原数据本地化(数据管理中心已实现,见阶段23) - - ⚠️ 菜谱详情页过敏原警示 - - 🚫 自动过滤含过敏原菜谱 - - 🔄 食材替代建议 - - 📋 过敏原报告生成 - -#### 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`) - - 📱 二维码海报 - - 📊 热度标签展示(🔥爆款/📈热门/❤️受欢迎) - - 👥 社交平台分享 - - - -## 🔴 阶段十五:后端接口增强(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.4 消息推送 -- **现状**:❌ 无推送接口 -- **建议接口**: - -- **功能**: - - 📬 站内信 - - 🔔 推送通知 - - 📢 系统公告 - - 💬 评论提醒 - -#### 15.5 浏览历史同步 -- **现状**:⚠️ 仅本地存储 -- **建议接口**: - ```text - 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 菜谱上传 -- **现状**:❌ 无上传接口 -- **建议接口**: - ```text -- **功能**: - - 📝 用户菜谱上传 - - ✏️ 菜谱编辑 - - 🗑️ 菜谱删除 - - 📊 菜谱审核 - -### 开发优先级 - -| 优先级 | 功能 | 说明 | -|--------|------|------| -| **P1** | 用户注册登录 | 核心功能,其他功能依赖用户体系 | -| **P1** | 收藏云端同步 | 用户数据安全,多设备同步 | -| **P2** | 评论系统 | 增加互动,提升活跃度 | -| **P2** | 消息推送 | 用户触达,提升留存 | -| **P2** | 浏览历史同步 | 用户体验,多设备同步 | -| **P2** | 菜谱上传 | UGC内容,丰富平台 | - -### 验收标准 -- [ ] 用户可注册/登录 -- [ ] 收藏数据云端同步 -- [ ] 菜谱可评论 -- [ ] 收到系统通知 -- [ ] 浏览历史云端同步 -- [ ] 用户可上传菜谱 +| 任务 | 建议接口 | 优先级 | 说明 | +|------|---------|--------|------| +| 👤 用户注册登录 | api_user.php | P1 | 暂不开发 | +| 💾 收藏云端同步 | api_favorite.php | P1 | 多设备同步 | +| 💬 评论系统 | api_comment.php | P2 | 互动功能 | +| 🔔 消息推送 | api_message.php | P2 | 站内信+推送 | +| 📜 浏览历史同步 | api_history.php | P2 | 云端存储 | +| 📝 菜谱上传 | api_recipe.php | P2 | UGC内容 | --- +## 🟢 阶段十四:接口能力挖掘(待开发) +| 任务 | 数据源 | 优先级 | 说明 | +|------|--------|--------|------| +| 🕐 用餐时段推荐 | eating_times.json(34种) | P2 | 早餐/午餐/晚餐推荐 | +| 📊 营养分析增强 | nutrition_types.json(31种) | P2 | 营养详情+追踪+图表 | +| ⚠️ 过敏原警示增强 | gmy.json(585种) | P2 | 详情页警示+自动过滤 | +| 🏆 点赞/推荐系统 | api_action.php | P2 | 评分+排行榜(部分已实现) | +| 📱 二维码海报 | code字段 | P3 | 生成分享图 | +| 🔗 社交分享增强 | code字段 | P3 | 链接+热度标签 | diff --git a/lib/src/config/app_routes.dart b/lib/src/config/app_routes.dart index 19c7365..a3eda3a 100644 --- a/lib/src/config/app_routes.dart +++ b/lib/src/config/app_routes.dart @@ -16,6 +16,7 @@ import 'package:mom_kitchen/src/pages/profile/nutrition/nutrition_report_page.da 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/advanced_search_page.dart'; import 'package:mom_kitchen/src/pages/home/recipe_detail_page.dart'; import 'package:mom_kitchen/src/pages/home/tag_recipe_list_page.dart'; import 'package:mom_kitchen/src/pages/tools/cooking_timer_page.dart'; @@ -77,6 +78,7 @@ class AppRoutes { static const String bedtimeReminder = '/profile/bedtime-reminder'; static const String tagRecipeList = '/tag-recipe-list'; static const String dataCenter = '/data-center'; + static const String advancedSearch = '/advanced-search'; static final List pages = [ GetPage( @@ -321,6 +323,12 @@ class AppRoutes { page: () => const DataCenterPage(), middlewares: [PageStandardsMiddleware()], ), + GetPage( + name: advancedSearch, + page: () => const AdvancedSearchPage(), + binding: SearchBinding(), + middlewares: [PageStandardsMiddleware()], + ), ]; static void registerAllPages() { diff --git a/lib/src/config/design_tokens.dart b/lib/src/config/design_tokens.dart index 8b6b10f..0b15aa5 100644 --- a/lib/src/config/design_tokens.dart +++ b/lib/src/config/design_tokens.dart @@ -4,6 +4,7 @@ * 作用: iOS 26 Liquid Glass 设计系统的统一令牌定义 * 更新: 2026-04-09 初始创建,定义颜色/圆角/间距/毛玻璃/字体/阴影/动画体系 * 更新: 2026-04-09 添加动态主题色支持 + * 更新: 2026-04-13 新增语义颜色(gold/purple/blue/teal)+ 分类渐变色 + 圆角映射表 */ import 'package:flutter/cupertino.dart'; @@ -76,9 +77,35 @@ class DesignTokens { static const Color red = Color(0xFFFF3B30); static const Color orange = Color(0xFFFF9500); static const Color green = Color(0xFF34C759); + static const Color gold = Color(0xFFFFB800); + static const Color purple = Color(0xFF9C27B0); + static const Color blue = Color(0xFF3498DB); + static const Color teal = Color(0xFF1ABC9C); static const Color segmentedBg = Color(0xFFE5E5EA); + static const List categoryGradients = [ + Color(0xFFFF6B35), + Color(0xFF2ECC71), + Color(0xFF3498DB), + Color(0xFF9B59B6), + Color(0xFFF39C12), + Color(0xFFE74C3C), + Color(0xFF1ABC9C), + Color(0xFF34495E), + ]; + + static const List toolGradients = [ + Color(0xFFFF6B35), + Color(0xFFFF9F1C), + Color(0xFF2ECC71), + Color(0xFF27AE60), + Color(0xFF3498DB), + Color(0xFF2980B9), + Color(0xFF9B59B6), + Color(0xFF8E44AD), + ]; + static const double radiusSm = 8.0; static const double radiusMd = 12.0; static const double radiusLg = 20.0; @@ -171,8 +198,26 @@ class DarkDesignTokens { static const Color text3 = Color(0xFF8E8E93); static const Color red = Color(0xFFFF453A); static const Color green = Color(0xFF30D158); + static const Color gold = Color(0xFFFFB800); + static const Color purple = Color(0xFFBF5AF2); + static const Color blue = Color(0xFF64D2FF); + static const Color teal = Color(0xFF64D2AA); static const double glassBlur = 25.0; static const Color segmentedBg = Color(0xFF2C2C2E); } + +class RadiusMapper { + RadiusMapper._(); + + static BorderRadius fromValue(double value) { + if (value <= 4) return DesignTokens.borderRadiusSm; + if (value <= 8) return DesignTokens.borderRadiusSm; + if (value <= 12) return DesignTokens.borderRadiusMd; + if (value <= 16) return DesignTokens.borderRadiusMd; + if (value <= 20) return DesignTokens.borderRadiusLg; + if (value <= 28) return DesignTokens.borderRadiusXl; + return DesignTokens.borderRadiusFull; + } +} diff --git a/lib/src/controllers/search_controller.dart b/lib/src/controllers/search_controller.dart index c2fb8d0..86b1d38 100644 --- a/lib/src/controllers/search_controller.dart +++ b/lib/src/controllers/search_controller.dart @@ -1,20 +1,71 @@ -// 2026-04-10 | SearchController | 搜索控制器 | 完全重写,直接调用api.php search接口 -// 2026-04-11 | 添加从API获取热门搜索词功能,替代硬编码热词 +/* + * 文件: search_controller.dart + * 名称: 搜索控制器 + * 作用: 全局搜索,支持菜谱/食材/口味/工艺多维度搜索 + * 创建: 2026-04-10 + * 更新: 2026-04-13 切换到global_search接口,新增多Tab搜索结果+高级筛选 + */ + import 'package:flutter/foundation.dart'; import 'package:get/get.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/tag_model.dart'; import 'package:mom_kitchen/src/config/api_config.dart'; import 'package:mom_kitchen/src/services/api/api_service.dart'; import 'package:mom_kitchen/src/services/data/hive_service.dart'; import 'package:mom_kitchen/src/services/ui/toast_service.dart'; import 'package:mom_kitchen/src/repositories/recipe_repository.dart'; +class SearchIngredientResult { + final int id; + final String name; + final String? categoryName; + final int recipeCount; + + const SearchIngredientResult({ + required this.id, + required this.name, + this.categoryName, + this.recipeCount = 0, + }); + + factory SearchIngredientResult.fromJson(Map json) { + return SearchIngredientResult( + id: _parseInt(json['id'] ?? json['ingredient_id']), + name: _parseString(json['name']) ?? '', + categoryName: _parseString(json['category_name']), + recipeCount: _parseInt(json['recipe_count'] ?? 0), + ); + } + + static int _parseInt(dynamic v, [int d = 0]) { + if (v == null) return d; + if (v is int) return v; + if (v is String) return int.tryParse(v) ?? d; + if (v is double) return v.toInt(); + return d; + } + + static String? _parseString(dynamic v) { + if (v == null) return null; + if (v is String) return v.isEmpty ? null : v; + return v.toString(); + } +} + class SearchController extends BaseController { final RxString searchQuery = ''.obs; final RxList searchHistory = [].obs; final RxList hotSearches = [].obs; - final RxList searchResults = [].obs; + final RxInt resultTabIndex = 0.obs; + + final RxList recipeResults = [].obs; + final RxList ingredientResults = + [].obs; + final RxList tasteTagResults = [].obs; + final RxList cookingTagResults = [].obs; + final RxList similarResults = [].obs; final RxBool hasSimilarResults = false.obs; @@ -80,22 +131,30 @@ class SearchController extends BaseController { } } + int get totalResultCount => + recipeResults.length + + ingredientResults.length + + tasteTagResults.length + + cookingTagResults.length; + + bool get hasAnyResult => totalResultCount > 0; + Future search(String keyword) async { if (keyword.trim().isEmpty) return; searchQuery.value = keyword.trim(); isLoading.value = true; + resultTabIndex.value = 0; _addToHistory(keyword); try { final response = await _apiService.get( - ApiConfig.recipe, + ApiConfig.filter, queryParameters: { - 'act': 'search', + 'act': 'global_search', 'keyword': searchQuery.value, 'type': 'all', - 'page': 1, 'limit': 20, }, ); @@ -120,52 +179,31 @@ class SearchController extends BaseController { throw Exception('结果数据格式异常 ⚠️'); } - final recipes = result['recipes']; - List results = []; + _parseRecipeResults(result['recipes']); + _parseIngredientResults(result['ingredients']); + _parseTasteTagResults(result['taste_tags']); + _parseCookingTagResults(result['cooking_tags']); - if (recipes is List && recipes.isNotEmpty) { - for (final item in recipes) { - if (item is Map) { - try { - results.add(RecipeModel.fromJson(item)); - } catch (e) { - debugPrint('Parse recipe error: $e'); - results.add( - RecipeModel( - id: _safeInt(item['id']), - title: - _safeString(item['title']) ?? - _safeString(item['name']) ?? - '未知菜谱', - intro: _safeString(item['intro']), - cover: _safeString(item['cover']), - categoryName: _safeString(item['category_name']), - ), - ); - } - } - } - } - - searchResults.value = results; - - if (results.isEmpty) { + if (!hasAnyResult) { await _searchSimilar(searchQuery.value); if (similarResults.isNotEmpty) { - ToastService.show(message: '未找到"${searchQuery.value}",为您推荐相似菜谱 🔍'); + ToastService.show( + message: '未找到"${searchQuery.value}",为您推荐相似菜谱 🔍', + ); } else { ToastService.show( - message: '未找到"${searchQuery.value}"相关菜谱,试试其他关键词 🔍', + message: '未找到"${searchQuery.value}"相关结果,试试其他关键词 🔍', ); } } else { similarResults.clear(); hasSimilarResults.value = false; - ToastService.show(message: '找到 ${results.length} 个相关菜谱 🎉'); + _autoSelectTab(); + ToastService.show(message: '找到 $totalResultCount 个相关结果 🎉'); } } catch (e) { debugPrint('Search error: $e'); - searchResults.clear(); + _clearAllResults(); errorMessage.value = e.toString(); String userMessage; @@ -183,6 +221,93 @@ class SearchController extends BaseController { } } + void _parseRecipeResults(dynamic recipes) { + recipeResults.clear(); + if (recipes is! List || recipes.isEmpty) return; + + for (final item in recipes) { + if (item is! Map) continue; + try { + recipeResults.add(RecipeModel.fromJson(item)); + } catch (e) { + debugPrint('Parse recipe error: $e'); + recipeResults.add( + RecipeModel( + id: _safeInt(item['id']), + title: + _safeString(item['title']) ?? + _safeString(item['name']) ?? + '未知菜谱', + intro: _safeString(item['intro']), + cover: _safeString(item['cover']), + categoryName: _safeString(item['category_name']), + ), + ); + } + } + } + + void _parseIngredientResults(dynamic ingredients) { + ingredientResults.clear(); + if (ingredients is! List || ingredients.isEmpty) return; + + for (final item in ingredients) { + if (item is! Map) continue; + try { + ingredientResults.add(SearchIngredientResult.fromJson(item)); + } catch (e) { + debugPrint('Parse ingredient error: $e'); + } + } + } + + void _parseTasteTagResults(dynamic tags) { + tasteTagResults.clear(); + if (tags is! List || tags.isEmpty) return; + + for (final item in tags) { + if (item is! Map) continue; + try { + tasteTagResults.add(TagModel.fromJson(item)); + } catch (e) { + debugPrint('Parse taste tag error: $e'); + } + } + } + + void _parseCookingTagResults(dynamic tags) { + cookingTagResults.clear(); + if (tags is! List || tags.isEmpty) return; + + for (final item in tags) { + if (item is! Map) continue; + try { + cookingTagResults.add(TagModel.fromJson(item)); + } catch (e) { + debugPrint('Parse cooking tag error: $e'); + } + } + } + + void _autoSelectTab() { + if (recipeResults.isNotEmpty) { + resultTabIndex.value = 0; + } else if (ingredientResults.isNotEmpty) { + resultTabIndex.value = 1; + } else if (tasteTagResults.isNotEmpty) { + resultTabIndex.value = 2; + } else if (cookingTagResults.isNotEmpty) { + resultTabIndex.value = 3; + } + } + + void _clearAllResults() { + recipeResults.clear(); + ingredientResults.clear(); + tasteTagResults.clear(); + cookingTagResults.clear(); + } + int _safeInt(dynamic value, [int defaultValue = 0]) { if (value == null) return defaultValue; if (value is int) return value; @@ -224,7 +349,7 @@ class SearchController extends BaseController { void clearResults() { searchQuery.value = ''; - searchResults.clear(); + _clearAllResults(); similarResults.clear(); hasSimilarResults.value = false; } @@ -239,12 +364,11 @@ class SearchController extends BaseController { final response = await _apiService .get( - ApiConfig.recipe, + ApiConfig.filter, queryParameters: { - 'act': 'search', + 'act': 'global_search', 'keyword': keywords.first, - 'type': 'all', - 'page': 1, + 'type': 'recipes', 'limit': 10, }, ) @@ -261,25 +385,24 @@ class SearchController extends BaseController { final result = resultData['result']; if (result is! Map) return; - final recipes = result['recipes']; + final recipes = result['recipes'] ?? result['recipes']; if (recipes is List && recipes.isNotEmpty) { for (final item in recipes) { - if (item is Map) { - try { - similarResults.add(RecipeModel.fromJson(item)); - } catch (_) { - similarResults.add( - RecipeModel( - id: _safeInt(item['id']), - title: - _safeString(item['title']) ?? - _safeString(item['name']) ?? - '未知菜谱', - intro: _safeString(item['intro']), - cover: _safeString(item['cover']), - ), - ); - } + if (item is! Map) continue; + try { + similarResults.add(RecipeModel.fromJson(item)); + } catch (_) { + similarResults.add( + RecipeModel( + id: _safeInt(item['id']), + title: + _safeString(item['title']) ?? + _safeString(item['name']) ?? + '未知菜谱', + intro: _safeString(item['intro']), + cover: _safeString(item['cover']), + ), + ); } } hasSimilarResults.value = similarResults.isNotEmpty; @@ -298,7 +421,18 @@ class SearchController extends BaseController { keywords.add(keyword.substring(1)); keywords.add(keyword.substring(0, keyword.length ~/ 2 + 1)); } - final commonSuffixes = ['肉', '鸡', '鱼', '汤', '面', '饭', '虾', '蛋', '豆腐', '排骨']; + final commonSuffixes = [ + '肉', + '鸡', + '鱼', + '汤', + '面', + '饭', + '虾', + '蛋', + '豆腐', + '排骨', + ]; for (final suffix in commonSuffixes) { if (keyword.contains(suffix)) { keywords.add(suffix); @@ -307,7 +441,33 @@ class SearchController extends BaseController { return keywords.toSet().toList(); } - void searchByKeyword(String keyword) { - search(keyword); + Future searchByFilter({ + String? tasteName, + String? cookingName, + int? categoryId, + int page = 1, + int limit = 20, + }) async { + isLoading.value = true; + try { + final result = await _recipeRepository.filterRecipesByTag( + tasteName: tasteName, + cookingName: cookingName, + categoryId: categoryId, + page: page, + limit: limit, + ); + recipeResults.value = result.items; + if (result.items.isEmpty) { + ToastService.show(message: '未找到匹配的菜谱 🔍'); + } else { + ToastService.show(message: '找到 ${result.items.length} 个菜谱 🎉'); + } + } catch (e) { + debugPrint('Filter search error: $e'); + ToastService.show(message: '筛选失败,请重试 ❌'); + } finally { + isLoading.value = false; + } } } diff --git a/lib/src/models/recipe/category_model.dart b/lib/src/models/recipe/category_model.dart index 417bef9..fe8ea96 100644 --- a/lib/src/models/recipe/category_model.dart +++ b/lib/src/models/recipe/category_model.dart @@ -31,7 +31,7 @@ class CategoryModel { icon: _parseString(json['icon']), parentId: _parseIntOrNull(json['parent_id']), sortOrder: _parseIntOrNull(json['sort_order'] ?? json['order']), - count: _parseIntOrNull(json['count'] ?? json['post_count']), + count: _parseIntOrNull(json['count'] ?? json['post_count'] ?? json['recipe_count']), children: _parseChildren(json['children']), ); } diff --git a/lib/src/models/recipe/recipe_model.dart b/lib/src/models/recipe/recipe_model.dart index 53d8798..b24aedc 100644 --- a/lib/src/models/recipe/recipe_model.dart +++ b/lib/src/models/recipe/recipe_model.dart @@ -2,6 +2,7 @@ // 2026-04-10 | API v2.0.0: 新增 code/allergens/meta 字段,增强 ingredients 分类结构(main/auxiliary/seasoning) // 2026-04-11 | 新增 author/categoryHierarchy 字段,增强 IngredientDetail(别名/介绍/营养/指导/功效) // 2026-04-11 | 新增 pic_id 字段,用于图片资源关联(替代 recipeId 构建图片URL) +// 2026-04-13 | API v2.8.0: 新增 rating 字段,对齐评分显示(score/nums/display/status/level/star) class RecipeModel { final int id; final String title; @@ -17,6 +18,7 @@ class RecipeModel { final CategorizedIngredients? categorizedIngredients; final NutritionInfo? nutrition; final RecipeStatistics? statistics; + final RecipeRating? rating; final RecipeMeta? meta; final List allergens; final String? code; @@ -40,6 +42,7 @@ class RecipeModel { this.categorizedIngredients, this.nutrition, this.statistics, + this.rating, this.meta, this.allergens = const [], this.code, @@ -172,6 +175,7 @@ class RecipeModel { categorizedIngredients: _parseCategorizedIngredients(json['ingredients']), nutrition: _parseNutrition(json['nutrition']), statistics: _parseStatistics(json['statistics']), + rating: _parseRatingField(json['rating']), meta: _parseMeta(json['meta']), allergens: _parseAllergens(json['allergens']), code: _parseStringOrNull(json['code']), @@ -200,6 +204,7 @@ class RecipeModel { 'categorizedIngredients': categorizedIngredients?.toJson(), 'nutrition': nutrition?.toJson(), 'statistics': statistics?.toJson(), + 'rating': rating?.toJson(), 'meta': meta?.toJson(), 'allergens': allergens, 'code': code, @@ -329,6 +334,21 @@ class RecipeModel { return null; } + static RecipeRating? _parseRatingField(dynamic json) { + if (json == null) return null; + if (json is Map) { + return RecipeRating.fromJson(json); + } + if (json is Map) { + try { + return RecipeRating.fromJson(Map.from(json)); + } catch (_) { + return null; + } + } + return null; + } + static RecipeMeta? _parseMeta(dynamic json) { if (json == null) return null; if (json is Map) { @@ -755,33 +775,144 @@ class NutritionItem { } } +class RecipeRating { + final double score; + final int nums; + final String display; + final String status; + final String level; + final int star; + + const RecipeRating({ + this.score = 0, + this.nums = 0, + this.display = '', + this.status = 'none', + this.level = '', + this.star = 0, + }); + + bool get hasRating => score > 0 || nums > 0; + + String get displayText => + display.isNotEmpty ? display : (hasRating ? '$score分' : '暂无评分'); + + bool get isNone => status == 'none'; + bool get isFew => status == 'few'; + bool get isNormal => status == 'normal'; + bool get isSufficient => status == 'sufficient'; + bool get isAbnormal => status == 'abnormal'; + + bool get isExcellent => level == '优秀'; + bool get isRecommended => level == '推荐'; + bool get isAverage => level == '一般'; + bool get isPoor => level == '较差'; + bool get isNotRecommended => level == '不推荐'; + + factory RecipeRating.fromJson(Map json) { + return RecipeRating( + score: _toDouble(json['score']), + nums: _parseInt( + json['nums'] ?? json['rate_nums'] ?? json['recommend_count'], + ), + display: _parseString(json['display']) ?? '', + status: _parseString(json['status']) ?? 'none', + level: _parseString(json['level']) ?? '', + star: _parseInt(json['star']), + ); + } + + static double _toDouble(dynamic v) { + if (v == null) return 0; + if (v is double) return v; + if (v is int) return v.toDouble(); + if (v is String) return double.tryParse(v) ?? 0; + return 0; + } + + static int _parseInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is String) return int.tryParse(v) ?? 0; + if (v is double) return v.toInt(); + return 0; + } + + static String? _parseString(dynamic v) { + if (v == null) return null; + if (v is String) return v.isEmpty ? null : v; + return v.toString(); + } + + Map toJson() { + return { + 'score': score, + 'nums': nums, + 'display': display, + 'status': status, + 'level': level, + 'star': star, + }; + } +} + class RecipeStatistics { final int views; final int likes; - final int recommends; + final int rateNums; + final int rateScore; final int comments; - final int recommendScore; + final RecipeRating? rating; + + @Deprecated('Use rateNums instead') + int get recommends => rateNums; + + @Deprecated('Use rateScore instead') + int get recommendScore => rateScore; const RecipeStatistics({ this.views = 0, this.likes = 0, - this.recommends = 0, + int recommends = 0, + int recommendScore = 0, this.comments = 0, - this.recommendScore = 0, - }); + this.rating, + }) : rateNums = recommends, + rateScore = recommendScore; factory RecipeStatistics.fromJson(Map json) { return RecipeStatistics( views: _parseInt(json['views'] ?? json['view_count']), likes: _parseInt(json['likes'] ?? json['like_count']), - recommends: _parseInt(json['recommends'] ?? json['recommend_count']), - comments: _parseInt(json['comments'] ?? json['comment_count']), - recommendScore: _parseInt( - json['recommend_score'] ?? json['recommendScore'], + recommends: _parseInt( + json['rate_nums'] ?? + json['recommends'] ?? + json['recommend_count'] ?? + json['rate_count'], ), + recommendScore: _parseInt( + json['rate_score'] ?? json['recommend_score'] ?? json['recommendScore'], + ), + comments: _parseInt(json['comments'] ?? json['comment_count']), + rating: _parseRating(json['rating']), ); } + static RecipeRating? _parseRating(dynamic json) { + if (json == null) return null; + if (json is Map) { + return RecipeRating.fromJson(json); + } + if (json is Map) { + try { + return RecipeRating.fromJson(Map.from(json)); + } catch (_) { + return null; + } + } + return null; + } + static int _parseInt(dynamic value) { if (value == null) return 0; if (value is int) return value; @@ -795,9 +926,10 @@ class RecipeStatistics { return { 'views': views, 'likes': likes, - 'recommends': recommends, + 'rate_nums': rateNums, + 'rate_score': rateScore, 'comments': comments, - 'recommend_score': recommendScore, + 'rating': rating?.toJson(), }; } } diff --git a/lib/src/pages/discover/category_browse_page.dart b/lib/src/pages/discover/category_browse_page.dart index 737007a..4a5e72b 100644 --- a/lib/src/pages/discover/category_browse_page.dart +++ b/lib/src/pages/discover/category_browse_page.dart @@ -1,9 +1,10 @@ -/* +/* * 文件: category_browse_page.dart * 名称: 分类浏览页面 * 作用: 分类层级导航,大类→小类→菜谱列表 * 创建: 2026-04-11 * 更新: 2026-04-12 重写子分类为列表布局 + * 更新: 2026-04-13 修复BOTTOM OVERFLOWED闪退问题,重构布局为CustomScrollView */ import 'package:flutter/cupertino.dart'; @@ -190,7 +191,7 @@ class _CategoryBrowsePageState extends State { Widget _buildContent(bool isDark) { if (widget.loadRecipesDirectly) { - return _buildRecipeList(isDark); + return _buildRecipeListView(isDark); } if (widget.category != null && widget.category!.children.isNotEmpty) { return _buildSubCategoryView(isDark); @@ -279,136 +280,134 @@ class _CategoryBrowsePageState extends State { } Widget _buildSubCategoryView(bool isDark) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.only( - left: DesignTokens.space4, - top: DesignTokens.space2, - bottom: DesignTokens.space2, - ), - child: Row( - children: [ - Icon( - CupertinoIcons.folder_open, - size: 18, - color: DesignTokens.primary, - ), - const SizedBox(width: DesignTokens.space2), - Text( - '子分类列表', - style: TextStyle( - fontSize: DesignTokens.fontMd, - fontWeight: FontWeight.w600, - color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + return CustomScrollView( + physics: BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only( + left: DesignTokens.space4, + top: DesignTokens.space2, + bottom: DesignTokens.space2, + ), + child: Row( + children: [ + Icon( + CupertinoIcons.folder_open, + size: 18, + color: DesignTokens.dynamicPrimary, ), - ), - const Spacer(), - Obx( - () => Text( + const SizedBox(width: DesignTokens.space2), + Text( + '子分类列表', + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w600, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), + const Spacer(), + Text( '${_categories.length} 个分类', style: TextStyle( fontSize: DesignTokens.fontXs, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), - ), - ], + ], + ), ), ), - SizedBox( - height: 44, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - ), - itemCount: _categories.length, - separatorBuilder: (context, index) => - const SizedBox(width: DesignTokens.space2), - itemBuilder: (context, index) { - final cat = _categories[index]; - final isSelected = _selectedSubCategory?.id == cat.id; - return GestureDetector( - onTap: () { - setState(() => _selectedSubCategory = cat); - if (widget.isIngredient) { - _loadRecipes(cat.id, searchKeyword: cat.name); - } else { - _loadRecipes(cat.id); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space3, - vertical: DesignTokens.space2, - ), - decoration: BoxDecoration( - color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) - : (isDark ? DarkDesignTokens.card : DesignTokens.card), - borderRadius: BorderRadius.circular(20), - border: Border.all( + SliverToBoxAdapter( + child: SizedBox( + height: 44, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + ), + itemCount: _categories.length, + separatorBuilder: (context, index) => + const SizedBox(width: DesignTokens.space2), + itemBuilder: (context, index) { + final cat = _categories[index]; + final isSelected = _selectedSubCategory?.id == cat.id; + return GestureDetector( + onTap: () { + setState(() => _selectedSubCategory = cat); + if (widget.isIngredient) { + _loadRecipes(cat.id, searchKeyword: cat.name); + } else { + _loadRecipes(cat.id); + } + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: DesignTokens.space3, + vertical: DesignTokens.space2, + ), + decoration: BoxDecoration( color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark - ? DarkDesignTokens.glassBorder - : DesignTokens.text3.withValues(alpha: 0.15)), + ? DarkDesignTokens.card + : DesignTokens.card), + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark + ? DarkDesignTokens.glassBorder + : DesignTokens.text3.withValues(alpha: 0.15)), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + cat.displayIcon, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(width: 4), + Text( + cat.name, + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w400, + color: isSelected + ? CupertinoColors.white + : (isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1), + ), + ), + ], ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - cat.displayIcon, - style: const TextStyle(fontSize: 14), - ), - const SizedBox(width: 4), - Text( - cat.name, - style: TextStyle( - fontSize: DesignTokens.fontSm, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.w400, - color: isSelected - ? CupertinoColors.white - : (isDark - ? DarkDesignTokens.text1 - : DesignTokens.text1), - ), - ), - ], - ), - ), - ); - }, + ); + }, + ), ), ), - const SizedBox(height: DesignTokens.space3), - Expanded( - child: _selectedSubCategory == null - ? _buildSubCategoryList(isDark) - : _recipes.isEmpty - ? _buildEmptyRecipeState(isDark) - : _buildRecipeListView(isDark), - ), + const SliverToBoxAdapter(child: SizedBox(height: DesignTokens.space3)), + if (_selectedSubCategory == null) + _buildSubCategorySliverList(isDark) + else if (_recipes.isEmpty) + SliverToBoxAdapter(child: _buildEmptyRecipeState(isDark)) + else + _buildRecipeSliverList(isDark), ], ); } - Widget _buildSubCategoryList(bool isDark) { - return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), - itemCount: _categories.length, - itemBuilder: (context, index) { + Widget _buildSubCategorySliverList(bool isDark) { + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { final cat = _categories[index]; return _buildSubCategoryListItem(cat, isDark, index); - }, + }, childCount: _categories.length), ); } @@ -419,11 +418,16 @@ class _CategoryBrowsePageState extends State { return GestureDetector( onTap: () => _navigateToCategory(cat), child: Container( - margin: const EdgeInsets.only(bottom: DesignTokens.space2 + 4), + margin: const EdgeInsets.fromLTRB( + DesignTokens.space4, + 0, + DesignTokens.space4, + DesignTokens.space2 + 4, + ), padding: const EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isDark ? DarkDesignTokens.glassBorder @@ -451,7 +455,7 @@ class _CategoryBrowsePageState extends State { _getGradientColor(index).withValues(alpha: 0.08), ], ), - borderRadius: BorderRadius.circular(14), + borderRadius: DesignTokens.borderRadiusMd, ), child: Center( child: Text( @@ -510,7 +514,7 @@ class _CategoryBrowsePageState extends State { height: 32, decoration: BoxDecoration( color: _getGradientColor(index).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Icon( CupertinoIcons.chevron_right, @@ -528,90 +532,84 @@ class _CategoryBrowsePageState extends State { final colors = [ const Color(0xFFFF6B35), const Color(0xFF2ECC71), - const Color(0xFF3498DB), + DesignTokens.blue, const Color(0xFF9B59B6), const Color(0xFFF39C12), const Color(0xFFE74C3C), - const Color(0xFF1ABC9C), + DesignTokens.teal, const Color(0xFF34495E), ]; return colors[index % colors.length]; } Widget _buildEmptyRecipeState(bool isDark) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('📂', style: TextStyle(fontSize: 56)), - const SizedBox(height: DesignTokens.space4), - Text( - '暂无菜谱数据', - style: TextStyle( - fontSize: DesignTokens.fontLg, - color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, - ), - ), - const SizedBox(height: DesignTokens.space2), - GestureDetector( - onTap: () { - if (_selectedSubCategory != null) { - if (widget.isIngredient) { - _loadRecipes( - _selectedSubCategory!.id, - searchKeyword: _selectedSubCategory!.name, - ); - } else { - _loadRecipes(_selectedSubCategory!.id); - } - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - vertical: DesignTokens.space2, + return SliverToBoxAdapter( + child: SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('📂', style: TextStyle(fontSize: 56)), + const SizedBox(height: DesignTokens.space4), + Text( + '暂无菜谱数据', + style: TextStyle( + fontSize: DesignTokens.fontLg, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), ), - decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - CupertinoIcons.refresh, - size: 16, - color: DesignTokens.primary, + const SizedBox(height: DesignTokens.space2), + GestureDetector( + onTap: () { + if (_selectedSubCategory != null) { + if (widget.isIngredient) { + _loadRecipes( + _selectedSubCategory!.id, + searchKeyword: _selectedSubCategory!.name, + ); + } else { + _loadRecipes(_selectedSubCategory!.id); + } + } + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, ), - const SizedBox(width: 8), - Text( - '刷新', - style: TextStyle( - fontSize: DesignTokens.fontSm, - fontWeight: FontWeight.w500, - color: DesignTokens.primary, - ), + decoration: BoxDecoration( + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusLg, ), - ], + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.refresh, + size: 16, + color: DesignTokens.dynamicPrimary, + ), + SizedBox(width: 8), + Text( + '刷新', + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: FontWeight.w500, + color: DesignTokens.dynamicPrimary, + ), + ), + ], + ), + ), ), - ), + ], ), - ], + ), ), ); } - Widget _buildRecipeList(bool isDark) { - return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), - itemCount: _recipes.length, - itemBuilder: (context, index) { - final recipe = _recipes[index]; - return _buildRecipeCard(recipe, isDark); - }, - ); - } - Widget _buildRecipeListView(bool isDark) { return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), @@ -623,82 +621,96 @@ class _CategoryBrowsePageState extends State { ); } + Widget _buildRecipeSliverList(bool isDark) { + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final recipe = _recipes[index]; + return Padding( + padding: const EdgeInsets.fromLTRB( + DesignTokens.space4, + 0, + DesignTokens.space4, + DesignTokens.space3, + ), + child: _buildRecipeCard(recipe, isDark), + ); + }, childCount: _recipes.length), + ); + } + Widget _buildRecipeCard(RecipeModel recipe, bool isDark) { - return Padding( - padding: const EdgeInsets.only(bottom: DesignTokens.space3), - child: GestureDetector( - onTap: () { - Get.toNamed('/recipe-detail', arguments: '${recipe.id}'); - }, - child: Container( - padding: const EdgeInsets.all(DesignTokens.space3), - decoration: BoxDecoration( - color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: DesignTokens.borderRadiusLg, - border: Border.all( - color: isDark - ? DarkDesignTokens.glassBorder - : DesignTokens.text3.withValues(alpha: 0.1), + return GestureDetector( + onTap: () { + Get.toNamed('/recipe-detail', arguments: '${recipe.id}'); + }, + child: Container( + padding: const EdgeInsets.all(DesignTokens.space3), + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.card : DesignTokens.card, + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isDark + ? DarkDesignTokens.glassBorder + : DesignTokens.text3.withValues(alpha: 0.1), + ), + ), + child: Row( + children: [ + ClipRRect( + borderRadius: DesignTokens.borderRadiusMd, + child: RecipeImage( + recipeId: recipe.id, + picId: recipe.picId, + coverUrl: recipe.cover, + width: 60, + height: 60, + fit: BoxFit.cover, + mode: RecipeImageMode.thumbnail, + ), ), - ), - child: Row( - children: [ - ClipRRect( - borderRadius: DesignTokens.borderRadiusMd, - child: RecipeImage( - recipeId: recipe.id, - picId: recipe.picId, - coverUrl: recipe.cover, - width: 60, - height: 60, - fit: BoxFit.cover, - mode: RecipeImageMode.thumbnail, - ), - ), - const SizedBox(width: DesignTokens.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - recipe.title, - style: TextStyle( - fontSize: DesignTokens.fontMd, - fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.text1 - : DesignTokens.text1, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + const SizedBox(width: DesignTokens.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + recipe.title, + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w600, + color: isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1, ), - const SizedBox(height: 4), - Row( - children: [ - if (recipe.meta?.time != null && - recipe.meta!.time!.isNotEmpty) - _buildInfoChip('⏱️ ${recipe.meta!.time}', isDark), - if (recipe.meta?.difficulty != null && - recipe.meta!.difficulty!.isNotEmpty) - Padding( - padding: const EdgeInsets.only(left: 8), - child: _buildInfoChip( - '📊 ${recipe.meta!.difficulty}', - isDark, - ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + if (recipe.meta?.time != null && + recipe.meta!.time!.isNotEmpty) + _buildInfoChip('⏱️ ${recipe.meta!.time}', isDark), + if (recipe.meta?.difficulty != null && + recipe.meta!.difficulty!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8), + child: _buildInfoChip( + '📊 ${recipe.meta!.difficulty}', + isDark, ), - ], - ), - ], - ), + ), + ], + ), + ], ), - Icon( - CupertinoIcons.chevron_forward, - size: 16, - color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, - ), - ], - ), + ), + Icon( + CupertinoIcons.chevron_forward, + size: 16, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ], ), ), ); @@ -706,17 +718,17 @@ class _CategoryBrowsePageState extends State { Widget _buildInfoChip(String text, bool isDark) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( text, style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ); diff --git a/lib/src/pages/discover/discover_page.dart b/lib/src/pages/discover/discover_page.dart index 49fb6e1..c7cfef8 100644 --- a/lib/src/pages/discover/discover_page.dart +++ b/lib/src/pages/discover/discover_page.dart @@ -1,8 +1,9 @@ -/* +/* * 文件: discover_page.dart * 名称: 发现页面 * 作用: iOS 26 风格的发现页面,整合热门排行+今天吃什么+搜索 * 更新: 2026-04-10 购物清单入口添加 Badge 显示数量 + * 更新: 2026-04-13 推荐tab新增口味/工艺标签入口,修复分类导航 */ import 'package:flutter/cupertino.dart'; @@ -11,6 +12,7 @@ import 'package:badges/badges.dart' as badges; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/config/app_routes.dart'; import 'package:mom_kitchen/src/models/recipe/category_model.dart'; +import 'package:mom_kitchen/src/models/recipe/tag_model.dart'; import 'package:mom_kitchen/src/repositories/recipe_repository.dart'; import 'package:mom_kitchen/src/widgets/glass/glass_search_bar.dart'; import 'package:mom_kitchen/src/widgets/glass/glass_segmented_control.dart'; @@ -35,6 +37,8 @@ class _DiscoverPageState extends State { final RecipeRepository _recipeRepo = RecipeRepository(); List _topCategories = []; List _ingredientCategories = []; + List _tasteTags = []; + List _cookingTags = []; bool _isLoadingCategories = true; @override @@ -50,10 +54,14 @@ class _DiscoverPageState extends State { final ingredientCategories = await _recipeRepo.fetchCategories( type: 'ingredient', ); + final tasteTags = await _recipeRepo.fetchTasteTags(); + final cookingTags = await _recipeRepo.fetchCookingTags(); if (mounted) { setState(() { _topCategories = categories; _ingredientCategories = ingredientCategories; + _tasteTags = tasteTags; + _cookingTags = cookingTags; _isLoadingCategories = false; }); } @@ -166,7 +174,7 @@ class _DiscoverPageState extends State { color: DesignTokens.green, onTap: () => Get.toNamed('/nutrition'), ), - const SizedBox(width: DesignTokens.space2), + SizedBox(width: DesignTokens.space2), _buildQuickActionItem( isDark: isDark, emoji: '🛒', @@ -175,12 +183,12 @@ class _DiscoverPageState extends State { onTap: () => Get.toNamed('/shopping-list'), badgeCount: shoppingCount, ), - const SizedBox(width: DesignTokens.space2), + SizedBox(width: DesignTokens.space2), _buildQuickActionItem( isDark: isDark, emoji: '📊', label: '周报', - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, onTap: () => Get.toNamed('/nutrition-report'), ), const SizedBox(width: DesignTokens.space2), @@ -224,7 +232,7 @@ class _DiscoverPageState extends State { horizontal: 5, vertical: 2, ), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), badgeContent: Text( badgeCount > 99 ? '99+' : '$badgeCount', @@ -334,9 +342,9 @@ class _DiscoverPageState extends State { }, background: Container( alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 20), + padding: EdgeInsets.only(left: 20), decoration: BoxDecoration( - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, borderRadius: DesignTokens.borderRadiusLg, ), child: Row( @@ -488,10 +496,10 @@ class _DiscoverPageState extends State { color: DesignTokens.primaryLight, borderRadius: DesignTokens.borderRadiusXl, ), - child: const Icon( + child: Icon( CupertinoIcons.shuffle, size: 44, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(height: DesignTokens.space4), @@ -538,16 +546,16 @@ class _DiscoverPageState extends State { onPressed: () { Get.toNamed('/what-to-eat'); }, - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( CupertinoIcons.lightbulb, size: 20, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), SizedBox(width: DesignTokens.space2), - Text('智能推荐', style: TextStyle(color: DesignTokens.primary)), + Text('智能推荐', style: TextStyle(color: DesignTokens.dynamicPrimary)), ], ), ), @@ -593,7 +601,7 @@ class _DiscoverPageState extends State { children: [ Icon( isFav ? CupertinoIcons.heart_fill : CupertinoIcons.heart, - color: isFav ? DesignTokens.red : DesignTokens.primary, + color: isFav ? DesignTokens.red : DesignTokens.dynamicPrimary, ), const SizedBox(width: 8), Text(isFav ? '取消收藏' : '收藏菜谱'), @@ -656,7 +664,7 @@ class _DiscoverPageState extends State { : _ingredientCategories; final typeLabel = _recommendTypeIndex == 0 ? '菜谱' : '食材'; - if (categories.isEmpty) { + if (categories.isEmpty && _tasteTags.isEmpty && _cookingTags.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -683,6 +691,8 @@ class _DiscoverPageState extends State { segments: const [ GlassSegment(label: '📖 菜谱'), GlassSegment(label: '🥬 食材'), + GlassSegment(label: '👅 口味'), + GlassSegment(label: '🍳 工艺'), ], selectedIndex: _recommendTypeIndex, onChanged: (i) { @@ -691,147 +701,262 @@ class _DiscoverPageState extends State { ), ), const SizedBox(height: DesignTokens.space3), - Expanded( - child: GridView.builder( - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - vertical: DesignTokens.space2, - ), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: DesignTokens.space3, - crossAxisSpacing: DesignTokens.space3, - childAspectRatio: 1.1, - ), - itemCount: categories.length, - itemBuilder: (context, index) { - final cat = categories[index]; - final hasChildren = cat.children.isNotEmpty; + Expanded(child: _buildRecommendContent(isDark, categories, typeLabel)), + ], + ); + } - return GestureDetector( - onTap: () { - final hasChildren = cat.children.isNotEmpty; - final hasRecipes = cat.count != null && cat.count! > 0; - final isIngredient = _recommendTypeIndex == 1; + Widget _buildRecommendContent( + bool isDark, + List categories, + String typeLabel, + ) { + if (_recommendTypeIndex == 2) { + return _buildTagGrid(isDark, _tasteTags, '口味', 'taste'); + } + if (_recommendTypeIndex == 3) { + return _buildTagGrid(isDark, _cookingTags, '工艺', 'cooking'); + } + return _buildCategoryGrid(isDark, categories, typeLabel); + } - if (hasChildren) { - Get.toNamed( - '/category-browse', - arguments: { - 'category': cat, - 'title': cat.name, - 'isIngredient': isIngredient, - }, - ); - } else if (hasRecipes || isIngredient) { - Get.toNamed( - '/category-browse', - arguments: { - 'category': cat, - 'title': '${cat.name} (${cat.count}道$typeLabel)', - 'loadRecipesDirectly': true, - 'isIngredient': isIngredient, - }, - ); - } else { - Get.toNamed( - '/category-browse', - arguments: { - 'category': cat, - 'title': cat.name, - 'isIngredient': isIngredient, - }, - ); - } + Widget _buildTagGrid( + bool isDark, + List tags, + String label, + String tagType, + ) { + if (tags.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('🏷️', style: TextStyle(fontSize: 56)), + const SizedBox(height: DesignTokens.space4), + Text( + '暂无$label标签数据', + style: TextStyle( + fontSize: DesignTokens.fontLg, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), + ], + ), + ); + } + + return GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: DesignTokens.space2, + crossAxisSpacing: DesignTokens.space2, + childAspectRatio: 2.2, + ), + itemCount: tags.length, + itemBuilder: (context, index) { + final tag = tags[index]; + return GestureDetector( + onTap: () { + Get.toNamed( + AppRoutes.tagRecipeList, + arguments: { + 'tagName': tag.name, + 'tagId': tag.id, + 'tagType': tagType, + }, + ); + }, + child: Container( + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.card : DesignTokens.card, + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isDark + ? DarkDesignTokens.glassBorder + : DesignTokens.text3.withValues(alpha: 0.1), + ), + ), + child: Center( + child: Text( + tag.name, + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: FontWeight.w500, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + }, + ); + } + + Widget _buildCategoryGrid( + bool isDark, + List categories, + String typeLabel, + ) { + if (categories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('📂', style: TextStyle(fontSize: 56)), + const SizedBox(height: DesignTokens.space4), + Text( + '暂无$typeLabel分类数据', + style: TextStyle( + fontSize: DesignTokens.fontLg, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), + ], + ), + ); + } + + return GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: DesignTokens.space3, + crossAxisSpacing: DesignTokens.space3, + childAspectRatio: 1.1, + ), + itemCount: categories.length, + itemBuilder: (context, index) { + final cat = categories[index]; + final hasChildren = cat.children.isNotEmpty; + + return GestureDetector( + onTap: () { + final hasRecipes = cat.count != null && cat.count! > 0; + final isIngredient = _recommendTypeIndex == 1; + + if (hasChildren) { + Get.toNamed( + '/category-browse', + arguments: { + 'category': cat, + 'title': cat.name, + 'isIngredient': isIngredient, }, - child: Container( - decoration: BoxDecoration( - color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: DesignTokens.borderRadiusLg, - border: Border.all( - color: isDark - ? DarkDesignTokens.glassBorder - : DesignTokens.text3.withValues(alpha: 0.1), + ); + } else if (hasRecipes || isIngredient) { + Get.toNamed( + '/category-browse', + arguments: { + 'category': cat, + 'title': '${cat.name} (${cat.count}道$typeLabel)', + 'loadRecipesDirectly': true, + 'isIngredient': isIngredient, + }, + ); + } else { + Get.toNamed( + '/category-browse', + arguments: { + 'category': cat, + 'title': cat.name, + 'isIngredient': isIngredient, + }, + ); + } + }, + child: Container( + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.card : DesignTokens.card, + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isDark + ? DarkDesignTokens.glassBorder + : DesignTokens.text3.withValues(alpha: 0.1), + ), + boxShadow: DesignTokens.shadowsSm, + ), + child: Stack( + children: [ + Positioned( + right: -10, + bottom: -10, + child: Text( + cat.displayIcon, + style: TextStyle( + fontSize: 72, + color: + (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.08), ), - boxShadow: DesignTokens.shadowsSm, ), - child: Stack( + ), + Padding( + padding: const EdgeInsets.all(DesignTokens.space3), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Positioned( - right: -10, - bottom: -10, - child: Text( - cat.displayIcon, - style: TextStyle( - fontSize: 72, - color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) - .withValues(alpha: 0.08), - ), + Text( + cat.displayIcon, + style: const TextStyle(fontSize: 32), + ), + const SizedBox(height: DesignTokens.space2), + Text( + cat.name, + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w600, + color: isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + hasChildren + ? '${cat.children.length} 个子类' + : (cat.count != null && cat.count! > 0 + ? '${cat.count} 道$typeLabel' + : '浏览'), + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3, ), ), - Padding( - padding: const EdgeInsets.all(DesignTokens.space3), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - cat.displayIcon, - style: const TextStyle(fontSize: 32), - ), - const SizedBox(height: DesignTokens.space2), - Text( - cat.name, - style: TextStyle( - fontSize: DesignTokens.fontMd, - fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.text1 - : DesignTokens.text1, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - Text( - hasChildren - ? '${cat.children.length} 个子类' - : (cat.count != null && cat.count! > 0 - ? '${cat.count} 道$typeLabel' - : '浏览'), - style: TextStyle( - fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.text3 - : DesignTokens.text3, - ), - ), - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - CupertinoIcons.chevron_forward, - size: 16, - color: isDark - ? DarkDesignTokens.text3 - : DesignTokens.text3, - ), - ], - ), - ], - ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + CupertinoIcons.chevron_forward, + size: 16, + color: isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3, + ), + ], ), ], ), ), - ); - }, + ], + ), ), - ), - ], + ); + }, ); } } diff --git a/lib/src/pages/discover/hot_page.dart b/lib/src/pages/discover/hot_page.dart index 1b17cd7..70dba4c 100644 --- a/lib/src/pages/discover/hot_page.dart +++ b/lib/src/pages/discover/hot_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: hot_page.dart * 名称: 热门排行页面 * 作用: iOS 26 Liquid Glass 风格的热门排行页面,支持时间段切换和排序方式切换 @@ -141,7 +141,7 @@ class HotPage extends StatelessWidget { bool isDark, HotController controller, ) { - final primary = isDark ? DarkDesignTokens.primary : DesignTokens.primary; + final primary = DesignTokens.dynamicPrimary; final orange = isDark ? DarkDesignTokens.secondary : DesignTokens.orange; return GestureDetector( diff --git a/lib/src/pages/discover/ingredient_recipe_list_page.dart b/lib/src/pages/discover/ingredient_recipe_list_page.dart index 93321e9..d13c7a3 100644 --- a/lib/src/pages/discover/ingredient_recipe_list_page.dart +++ b/lib/src/pages/discover/ingredient_recipe_list_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: ingredient_recipe_list_page.dart * 名称: 食材菜品列表页面 * 作用: 显示某食材相关的菜品列表,支持分页加载 @@ -366,30 +366,26 @@ class _IngredientRecipeListPageState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: DesignTokens.space2), + SizedBox(height: DesignTokens.space2), Row( children: [ if (recipe.categoryName?.isNotEmpty ?? false) ...[ Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( recipe.categoryName!, style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/pages/discover/what_to_eat_page.dart b/lib/src/pages/discover/what_to_eat_page.dart index 9159c95..9f9fcff 100644 --- a/lib/src/pages/discover/what_to_eat_page.dart +++ b/lib/src/pages/discover/what_to_eat_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: what_to_eat_page.dart * 名称: 今天吃什么页面 * 作用: iOS 26 风格的今天吃什么页面,支持动态筛选(分类/标签/过敏原) @@ -129,13 +129,13 @@ class WhatToEatPage extends StatelessWidget { ), textAlign: TextAlign.center, ), - const SizedBox(height: DesignTokens.space4), + SizedBox(height: DesignTokens.space4), SizedBox( width: 160, child: CupertinoButton( onPressed: controller.reloadOptions, borderRadius: BorderRadius.circular(DesignTokens.radiusFull), - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -167,12 +167,12 @@ class WhatToEatPage extends StatelessWidget { if (controller.isSpinning.value) { return Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space6), + padding: EdgeInsets.all(DesignTokens.space6), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: DesignTokens.borderRadiusLg, border: Border.all( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.2), ), ), @@ -241,12 +241,12 @@ class WhatToEatPage extends StatelessWidget { }, child: Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space4), + padding: EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: DesignTokens.borderRadiusLg, border: Border.all( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.3), ), boxShadow: DesignTokens.shadowsMd, @@ -262,9 +262,7 @@ class WhatToEatPage extends StatelessWidget { height: 56, decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusMd, ), @@ -295,14 +293,12 @@ class WhatToEatPage extends StatelessWidget { ), if (recipe.categoryName != null && recipe.categoryName!.isNotEmpty) ...[ - const SizedBox(height: 4), + SizedBox(height: 4), Text( '📂 ${recipe.categoryName}', style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -331,31 +327,27 @@ class WhatToEatPage extends StatelessWidget { ), ], if (recipe.tags.isNotEmpty) ...[ - const SizedBox(height: DesignTokens.space2), + SizedBox(height: DesignTokens.space2), Wrap( spacing: 6, runSpacing: 4, children: recipe.tags.take(3).map((tag) { return Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( tag.name, style: TextStyle( fontSize: 11, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -380,7 +372,7 @@ class WhatToEatPage extends StatelessWidget { child: CupertinoButton( onPressed: controller.isSpinning.value ? null : controller.roll, borderRadius: BorderRadius.circular(DesignTokens.radiusFull), - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, child: controller.isSpinning.value ? Row( mainAxisAlignment: MainAxisAlignment.center, @@ -424,30 +416,26 @@ class WhatToEatPage extends StatelessWidget { ), if (controller.selectedRecipe.value != null && !controller.isSpinning.value) ...[ - const SizedBox(width: DesignTokens.space3), + SizedBox(width: DesignTokens.space3), SizedBox( height: 52, child: CupertinoButton( onPressed: controller.rollAgain, borderRadius: BorderRadius.circular(DesignTokens.radiusFull), color: (isDark ? DarkDesignTokens.card : DesignTokens.card), - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Icon( CupertinoIcons.arrow_2_circlepath, size: 18, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), - const SizedBox(width: 6), + SizedBox(width: 6), Text( '换一个', style: TextStyle( - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w600, ), ), @@ -463,12 +451,12 @@ class WhatToEatPage extends StatelessWidget { Widget _buildFilterSummary(WhatToEatController controller, bool isDark) { return Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.08), borderRadius: DesignTokens.borderRadiusMd, ), @@ -477,15 +465,15 @@ class WhatToEatPage extends StatelessWidget { Icon( CupertinoIcons.line_horizontal_3_decrease, size: 16, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), - const SizedBox(width: 8), + SizedBox(width: 8), Expanded( child: Text( '当前筛选: ${controller.filterSummary}', style: TextStyle( fontSize: DesignTokens.fontSm, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -494,7 +482,7 @@ class WhatToEatPage extends StatelessWidget { child: Icon( CupertinoIcons.xmark_circle, size: 18, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -550,24 +538,20 @@ class WhatToEatPage extends StatelessWidget { } }, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 14, vertical: 8, ), decoration: BoxDecoration( color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.card : DesignTokens.card), - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) @@ -649,19 +633,17 @@ class WhatToEatPage extends StatelessWidget { return GestureDetector( onTap: () => controller.toggleCategory(subCat), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isSelected - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.background : DesignTokens.background), - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) .withValues(alpha: 0.2), ), @@ -712,22 +694,18 @@ class WhatToEatPage extends StatelessWidget { return GestureDetector( onTap: () => controller.toggleTag(tag), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 14, vertical: 8, ), decoration: BoxDecoration( color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.card : DesignTokens.card), - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) @@ -847,7 +825,7 @@ class WhatToEatPage extends StatelessWidget { color: isBlocked ? CupertinoColors.destructiveRed.withValues(alpha: 0.12) : (isDark ? DarkDesignTokens.card : DesignTokens.card), - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: isBlocked ? CupertinoColors.destructiveRed.withValues(alpha: 0.4) diff --git a/lib/src/pages/home/advanced_search_page.dart b/lib/src/pages/home/advanced_search_page.dart new file mode 100644 index 0000000..1dcc453 --- /dev/null +++ b/lib/src/pages/home/advanced_search_page.dart @@ -0,0 +1,508 @@ +/* + * 文件: advanced_search_page.dart + * 名称: 高级搜索页面 + * 作用: 多条件筛选搜索菜谱(分类/口味/工艺/时段) + * 创建: 2026-04-13 + * 更新: 2026-04-13 初始创建 + */ + +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import '../../controllers/search_controller.dart'; +import '../../config/design_tokens.dart'; +import '../../models/recipe/category_model.dart'; +import '../../models/recipe/tag_model.dart'; +import '../../repositories/recipe_repository.dart'; +import '../../services/api/api_service.dart'; +import '../../config/api_config.dart'; + +class AdvancedSearchPage extends StatefulWidget { + const AdvancedSearchPage({super.key}); + + @override + State createState() => _AdvancedSearchPageState(); +} + +class _AdvancedSearchPageState extends State { + final RecipeRepository _recipeRepository = RecipeRepository(); + final ApiService _apiService = ApiService(); + + List _mainCategories = []; + List _tasteTags = []; + List _cookingTags = []; + List<_MealTimeItem> _mealTimes = []; + + CategoryModel? _selectedMainCategory; + TagModel? _selectedTaste; + TagModel? _selectedCooking; + _MealTimeItem? _selectedMealTime; + + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadFilterOptions(); + } + + Future _loadFilterOptions() async { + try { + final results = await Future.wait([ + _recipeRepository.fetchMainCategories(), + _recipeRepository.fetchTasteTags(), + _recipeRepository.fetchCookingTags(), + _fetchMealTimes(), + ]); + + if (mounted) { + setState(() { + _mainCategories = results[0] as List; + _tasteTags = results[1] as List; + _cookingTags = results[2] as List; + _mealTimes = results[3] as List<_MealTimeItem>; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + Future> _fetchMealTimes() async { + try { + final response = await _apiService.get( + ApiConfig.filter, + queryParameters: {'act': 'meal_times'}, + ); + final data = response.data as Map; + if (data['code'] == 200 && data['data'] != null) { + final list = (data['data']['list'] ?? []) as List; + return list + .map( + (e) => _MealTimeItem( + id: e['id'] as int? ?? 0, + name: e['name'] as String? ?? '', + count: e['count'] as int? ?? 0, + ), + ) + .where((e) => e.name.isNotEmpty) + .toList(); + } + } catch (_) {} + return [ + const _MealTimeItem(id: 1, name: '早餐', count: 0), + const _MealTimeItem(id: 2, name: '午餐', count: 0), + const _MealTimeItem(id: 3, name: '晚餐', count: 0), + const _MealTimeItem(id: 4, name: '夜宵', count: 0), + const _MealTimeItem(id: 5, name: '下午茶', count: 0), + ]; + } + + @override + Widget build(BuildContext context) { + final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; + + return CupertinoPageScaffold( + backgroundColor: isDark + ? DarkDesignTokens.background + : DesignTokens.background, + navigationBar: CupertinoNavigationBar( + middle: const Text('⚙️ 高级搜索'), + backgroundColor: + (isDark ? DarkDesignTokens.background : DesignTokens.background) + .withValues(alpha: 0.9), + border: null, + leading: CupertinoButton( + padding: EdgeInsets.zero, + child: Icon( + CupertinoIcons.back, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + onPressed: () => Get.back(), + ), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + onPressed: _hasAnyFilter ? _resetFilters : null, + child: Text( + '重置', + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: _hasAnyFilter + ? (DesignTokens.dynamicPrimary) + : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3), + ), + ), + ), + ), + child: SafeArea( + child: _isLoading + ? const Center(child: CupertinoActivityIndicator(radius: 16)) + : Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space3, + ), + children: [ + _buildSection( + isDark, + icon: '📂', + title: '菜谱分类', + child: _buildCategoryGrid(isDark), + ), + const SizedBox(height: DesignTokens.space4), + _buildSection( + isDark, + icon: '👅', + title: '口味标签', + child: _buildTagGrid(isDark, _tasteTags, 'taste'), + ), + const SizedBox(height: DesignTokens.space4), + _buildSection( + isDark, + icon: '🍳', + title: '工艺标签', + child: _buildTagGrid(isDark, _cookingTags, 'cooking'), + ), + const SizedBox(height: DesignTokens.space4), + _buildSection( + isDark, + icon: '🕐', + title: '用餐时段', + child: _buildMealTimeGrid(isDark), + ), + const SizedBox(height: DesignTokens.space6), + ], + ), + ), + _buildSearchButton(isDark), + ], + ), + ), + ); + } + + bool get _hasAnyFilter => + _selectedMainCategory != null || + _selectedTaste != null || + _selectedCooking != null || + _selectedMealTime != null; + + void _resetFilters() { + setState(() { + _selectedMainCategory = null; + _selectedTaste = null; + _selectedCooking = null; + _selectedMealTime = null; + }); + } + + Widget _buildSection( + bool isDark, { + required String icon, + required String title, + required Widget child, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(icon, style: const TextStyle(fontSize: 18)), + const SizedBox(width: 6), + Text( + title, + style: TextStyle( + fontSize: DesignTokens.fontLg, + fontWeight: FontWeight.w600, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), + ], + ), + const SizedBox(height: DesignTokens.space3), + child, + ], + ); + } + + Widget _buildCategoryGrid(bool isDark) { + if (_mainCategories.isEmpty) { + return _buildEmptyHint(isDark, '暂无分类数据'); + } + + return Wrap( + spacing: DesignTokens.space2, + runSpacing: DesignTokens.space2, + children: _mainCategories.map((category) { + final isSelected = _selectedMainCategory?.id == category.id; + return GestureDetector( + onTap: () { + setState(() { + _selectedMainCategory = isSelected ? null : category; + }); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.12) + : (isDark ? DarkDesignTokens.card : DesignTokens.card), + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.3) + : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.1), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + category.name, + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1), + ), + ), + if (category.count != null && category.count! > 0) ...[ + SizedBox(width: 4), + Text( + '${category.count}', + style: TextStyle( + fontSize: 11, + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3), + ), + ), + ], + ], + ), + ), + ); + }).toList(), + ); + } + + Widget _buildTagGrid(bool isDark, List tags, String type) { + if (tags.isEmpty) { + return _buildEmptyHint(isDark, '暂无${type == 'taste' ? '口味' : '工艺'}标签'); + } + + return Wrap( + spacing: DesignTokens.space2, + runSpacing: DesignTokens.space2, + children: tags.map((tag) { + final isSelected = type == 'taste' + ? _selectedTaste?.id == tag.id + : _selectedCooking?.id == tag.id; + return GestureDetector( + onTap: () { + setState(() { + if (type == 'taste') { + _selectedTaste = isSelected ? null : tag; + } else { + _selectedCooking = isSelected ? null : tag; + } + }); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.12) + : (isDark ? DarkDesignTokens.card : DesignTokens.card), + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.3) + : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.1), + ), + ), + child: Text( + '${type == 'taste' ? '👅' : '🍳'} ${tag.name}', + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark ? DarkDesignTokens.text1 : DesignTokens.text1), + ), + ), + ), + ); + }).toList(), + ); + } + + Widget _buildMealTimeGrid(bool isDark) { + if (_mealTimes.isEmpty) { + return _buildEmptyHint(isDark, '暂无时段数据'); + } + + return Wrap( + spacing: DesignTokens.space2, + runSpacing: DesignTokens.space2, + children: _mealTimes.map((item) { + final isSelected = _selectedMealTime?.id == item.id; + return GestureDetector( + onTap: () { + setState(() { + _selectedMealTime = isSelected ? null : item; + }); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.12) + : (isDark ? DarkDesignTokens.card : DesignTokens.card), + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.3) + : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.1), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item.name, + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1), + ), + ), + if (item.count > 0) ...[ + SizedBox(width: 4), + Text( + '${item.count}', + style: TextStyle( + fontSize: 11, + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3), + ), + ), + ], + ], + ), + ), + ); + }).toList(), + ); + } + + Widget _buildEmptyHint(bool isDark, String text) { + return Padding( + padding: const EdgeInsets.all(DesignTokens.space4), + child: Text( + text, + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ); + } + + Widget _buildSearchButton(bool isDark) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space3, + ), + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.background : DesignTokens.background, + border: Border( + top: BorderSide( + color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.1), + ), + ), + ), + child: SafeArea( + top: false, + child: SizedBox( + width: double.infinity, + height: 50, + child: CupertinoButton.filled( + borderRadius: DesignTokens.borderRadiusFull, + onPressed: _hasAnyFilter ? _performSearch : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(CupertinoIcons.search, size: 18), + const SizedBox(width: 6), + Text( + _hasAnyFilter ? '搜索菜谱' : '请选择筛选条件', + style: const TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _performSearch() { + final searchController = Get.find(); + searchController.searchByFilter( + tasteName: _selectedTaste?.name, + cookingName: _selectedCooking?.name, + categoryId: _selectedMainCategory?.id, + ); + + Get.back(); + } +} + +class _MealTimeItem { + final int id; + final String name; + final int count; + + const _MealTimeItem({ + required this.id, + required this.name, + required this.count, + }); +} diff --git a/lib/src/pages/home/home_card_carousel.dart b/lib/src/pages/home/home_card_carousel.dart index f08e877..e2698c8 100644 --- a/lib/src/pages/home/home_card_carousel.dart +++ b/lib/src/pages/home/home_card_carousel.dart @@ -15,6 +15,7 @@ import 'package:mom_kitchen/src/models/recipe/recipe_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'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; class HomeCardCarousel extends StatefulWidget { const HomeCardCarousel({super.key}); @@ -115,7 +116,7 @@ class _HomeCardCarouselState extends State { color: themeService.backgroundColor.value.withValues( alpha: 0.5, ), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: themeService.textColor.value.withValues(alpha: 0.1), ), @@ -200,7 +201,7 @@ class _HomeCardCarouselState extends State { child: Container( decoration: BoxDecoration( color: themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: themeService.textColor.value.withValues(alpha: 0.1), width: 0.5, @@ -231,7 +232,7 @@ class _HomeCardCarouselState extends State { padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: themeService.textColor.value.withValues(alpha: 0.1), width: 0.5, @@ -251,7 +252,7 @@ class _HomeCardCarouselState extends State { height: 72, decoration: BoxDecoration( color: themeService.primaryColor.value.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Icon( CupertinoIcons.book, @@ -300,7 +301,7 @@ class _HomeCardCarouselState extends State { color: themeService.primaryColor.value.withValues( alpha: 0.1, ), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Icon( CupertinoIcons.cart_badge_plus, @@ -357,7 +358,7 @@ class _HomeCardCarouselState extends State { ), decoration: BoxDecoration( color: badgeInfo['color'], - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, boxShadow: [ BoxShadow( color: (badgeInfo['color'] as Color).withValues( @@ -430,7 +431,7 @@ class _HomeCardCarouselState extends State { decoration: BoxDecoration( color: themeService.primaryColor.value .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Icon( CupertinoIcons.cart_badge_plus, @@ -448,7 +449,7 @@ class _HomeCardCarouselState extends State { padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: themeService.primaryColor.value, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, boxShadow: [ BoxShadow( color: themeService.primaryColor.value @@ -506,7 +507,7 @@ class _HomeCardCarouselState extends State { : themeService.textColor.value.withValues( alpha: 0.08, ), - borderRadius: BorderRadius.circular(14), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isSelected ? themeService.primaryColor.value @@ -556,7 +557,7 @@ class _HomeCardCarouselState extends State { : themeService.textColor.value.withValues( alpha: 0.25, ), - borderRadius: BorderRadius.circular(3), + borderRadius: DesignTokens.borderRadiusSm, ), ), ), @@ -610,7 +611,7 @@ class _HomeCardCarouselState extends State { ), decoration: BoxDecoration( color: badgeInfo['color'], - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( badgeInfo['label'], @@ -658,7 +659,7 @@ class _HomeCardCarouselState extends State { padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: themeService.primaryColor.value, - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, boxShadow: [ BoxShadow( color: themeService.primaryColor.value.withValues( @@ -715,7 +716,7 @@ class _HomeCardCarouselState extends State { : themeService.textColor.value.withValues( alpha: 0.08, ), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isSelected ? themeService.primaryColor.value @@ -816,11 +817,11 @@ class _HomeCardCarouselState extends State { } if (isNew) { - return {'label': '✨ 新', 'color': const Color(0xFF34C759)}; + return {'label': '✨ 新', 'color': DesignTokens.green}; } if (isHot) { - return {'label': '🔥 热', 'color': const Color(0xFFFF3B30)}; + return {'label': '🔥 热', 'color': DesignTokens.red}; } return null; diff --git a/lib/src/pages/home/home_page.dart b/lib/src/pages/home/home_page.dart index 00c7b22..b84e502 100644 --- a/lib/src/pages/home/home_page.dart +++ b/lib/src/pages/home/home_page.dart @@ -604,7 +604,7 @@ class _HomePageState extends State { ), const SizedBox(height: DesignTokens.space4), CupertinoButton.filled( - borderRadius: BorderRadius.circular(22), + borderRadius: DesignTokens.borderRadiusXl, onPressed: () => _loadRecipes(refresh: true), child: const Text( '重新加载', diff --git a/lib/src/pages/home/recipe_detail_page.dart b/lib/src/pages/home/recipe_detail_page.dart index 934bc5d..062ab85 100644 --- a/lib/src/pages/home/recipe_detail_page.dart +++ b/lib/src/pages/home/recipe_detail_page.dart @@ -1,5 +1,6 @@ -// 2026-04-09 | recipe_detail_page.dart | 菜谱详情页 | 展示菜谱详细信息 +// 2026-04-09 | recipe_detail_page.dart | 菜谱详情页 | 展示菜谱详细信息 // 2026-04-11 | 重构: 拆分为Controller+18个独立UI组件,提高可维护性 +// 2026-04-13 | 新增rating数据传递到RecipeCoverImage和RecipeStatisticsBar import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -140,19 +141,21 @@ class RecipeDetailPage extends StatelessWidget { ), child: RefreshIndicator( onRefresh: () => controller.refreshRecipe(recipeId), - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, child: ListView( padding: const EdgeInsets.only(bottom: DesignTokens.space5), physics: const AlwaysScrollableScrollPhysics(), children: [ RecipeCoverImage( recipe: recipe, + rating: recipe.rating, viewCount: controller.viewCount.value, likeCount: controller.likeCount.value, ), RecipeTitleSection(recipe: recipe), RecipeStatisticsBar( statistics: recipe.statistics, + rating: recipe.rating, viewCount: controller.viewCount.value, likeCount: controller.likeCount.value, ), diff --git a/lib/src/pages/home/search_page.dart b/lib/src/pages/home/search_page.dart index 2355294..020dd91 100644 --- a/lib/src/pages/home/search_page.dart +++ b/lib/src/pages/home/search_page.dart @@ -1,16 +1,19 @@ -/* +/* * 文件: search_page.dart * 名称: 搜索页面 - * 作用: iOS 26 风格的菜谱搜索页面,支持搜索历史、热门搜索、实时搜索 - * 更新: 2026-04-10 完全重写,优化UI和交互体验 + * 作用: iOS 26 风格的菜谱搜索页面,支持多维度搜索+高级筛选 + * 创建: 2026-04-10 + * 更新: 2026-04-13 切换global_search接口,搜索结果分4个Tab+高级筛选入口 */ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show Colors; import 'package:get/get.dart'; import '../../controllers/search_controller.dart'; +import '../../config/app_routes.dart'; import '../../config/design_tokens.dart'; import '../../models/recipe/recipe_model.dart'; +import '../../models/recipe/tag_model.dart'; import '../../widgets/recipe_image.dart'; class SearchPage extends StatefulWidget { @@ -66,6 +69,16 @@ class _SearchPageState extends State { : DesignTokens.background, navigationBar: CupertinoNavigationBar( middle: _buildSearchBar(isDark), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + minSize: 44, + child: Icon( + CupertinoIcons.slider_horizontal_3, + size: 22, + color: DesignTokens.dynamicPrimary, + ), + onPressed: () => _showAdvancedSearch(isDark), + ), backgroundColor: isDark ? DarkDesignTokens.background.withValues(alpha: 0.9) : DesignTokens.background.withValues(alpha: 0.9), @@ -89,7 +102,7 @@ class _SearchPageState extends State { decoration: BoxDecoration( color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Row( children: [ @@ -104,7 +117,7 @@ class _SearchPageState extends State { child: CupertinoTextField( controller: _textEditingController, focusNode: _focusNode, - placeholder: '搜索菜谱、食材...', + placeholder: '搜索菜谱、食材、口味、工艺...', placeholderStyle: TextStyle( inherit: false, fontSize: 15, @@ -156,7 +169,8 @@ class _SearchPageState extends State { return _buildInitialView(isDark); } else if (_searchController.isLoading.value) { return _buildLoadingView(isDark); - } else if (_searchController.searchResults.isEmpty) { + } else if (!_searchController.hasAnyResult && + _searchController.similarResults.isEmpty) { return _buildEmptyView(isDark); } else { return _buildResultsView(isDark); @@ -172,7 +186,6 @@ class _SearchPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 搜索历史 Obx(() { if (_searchController.searchHistory.isEmpty) { return const SizedBox.shrink(); @@ -196,7 +209,7 @@ class _SearchPageState extends State { ), CupertinoButton( minimumSize: Size.zero, - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), @@ -204,9 +217,7 @@ class _SearchPageState extends State { child: Text( '清空', style: createTextStyle( - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontSize: DesignTokens.fontSm, ), ), @@ -236,7 +247,7 @@ class _SearchPageState extends State { ? DarkDesignTokens.text3 : DesignTokens.text3) .withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, ), child: Text( history, @@ -256,7 +267,6 @@ class _SearchPageState extends State { ); }), - // 热门搜索 Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -279,16 +289,14 @@ class _SearchPageState extends State { _searchController.search(keyword); }, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 14, vertical: 8, ), decoration: BoxDecoration( gradient: LinearGradient( colors: [ - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.12), (isDark ? DarkDesignTokens.secondary @@ -296,12 +304,10 @@ class _SearchPageState extends State { .withValues(alpha: 0.08), ], ), - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.2), width: 0.5, ), @@ -312,9 +318,7 @@ class _SearchPageState extends State { Text( keyword, style: createTextStyle( - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w500, ), @@ -400,7 +404,7 @@ class _SearchPageState extends State { ), const SizedBox(height: DesignTokens.space4), Text( - '未找到"${_searchController.searchQuery.value}"相关菜谱', + '未找到"${_searchController.searchQuery.value}"相关结果', style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w500, @@ -422,7 +426,7 @@ class _SearchPageState extends State { width: 200, height: 44, child: CupertinoButton.filled( - borderRadius: BorderRadius.circular(22), + borderRadius: DesignTokens.borderRadiusXl, onPressed: () { _textEditingController.clear(); _searchController.clearResults(); @@ -437,30 +441,26 @@ class _SearchPageState extends State { Obx(() { if (!_searchController.hasSimilarResults.value || _searchController.similarResults.isEmpty) { - return const SizedBox(); + return SizedBox(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: DesignTokens.space6), + SizedBox(height: DesignTokens.space6), Row( children: [ Icon( CupertinoIcons.lightbulb, size: 18, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), - const SizedBox(width: 6), + SizedBox(width: 6), Text( '相似推荐', style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -554,7 +554,6 @@ class _SearchPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 结果统计栏 Container( width: double.infinity, padding: const EdgeInsets.symmetric( @@ -570,7 +569,7 @@ class _SearchPageState extends State { ), ), child: Text( - '找到 ${_searchController.searchResults.length} 个结果 · "${_searchController.searchQuery.value}"', + '找到 ${_searchController.totalResultCount} 个结果 · "${_searchController.searchQuery.value}"', style: TextStyle( fontSize: DesignTokens.fontXs, color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, @@ -578,91 +577,224 @@ class _SearchPageState extends State { ), ), - // 结果列表 - Expanded( - child: ListView.separated( - padding: const EdgeInsets.all(DesignTokens.space4), - itemCount: _searchController.searchResults.length, - separatorBuilder: (_, _) => - const SizedBox(height: DesignTokens.space3), - itemBuilder: (context, index) { - final recipe = _searchController.searchResults[index]; - return _buildRecipeItem(recipe, isDark); - }, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space3, + vertical: DesignTokens.space2, ), + child: Obx(() => _buildResultTabs(isDark)), ), + + Expanded(child: Obx(() => _buildResultContent(isDark))), ], ); } - Widget _buildRecipeItem(dynamic recipe, bool isDark) { - final title = recipe.title ?? '未知菜谱'; - final intro = recipe.intro ?? ''; - final category = recipe.categoryName; - final cover = recipe.cover; - final recipeId = recipe.id; - final picId = recipe.picId; + Widget _buildResultTabs(bool isDark) { + final tabs = <_SearchTabInfo>[]; + if (_searchController.recipeResults.isNotEmpty) { + tabs.add( + _SearchTabInfo( + label: '📖 菜谱', + count: _searchController.recipeResults.length, + index: 0, + ), + ); + } + if (_searchController.ingredientResults.isNotEmpty) { + tabs.add( + _SearchTabInfo( + label: '🥬 食材', + count: _searchController.ingredientResults.length, + index: 1, + ), + ); + } + if (_searchController.tasteTagResults.isNotEmpty) { + tabs.add( + _SearchTabInfo( + label: '👅 口味', + count: _searchController.tasteTagResults.length, + index: 2, + ), + ); + } + if (_searchController.cookingTagResults.isNotEmpty) { + tabs.add( + _SearchTabInfo( + label: '🍳 工艺', + count: _searchController.cookingTagResults.length, + index: 3, + ), + ); + } - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - try { - debugPrint('Tapped recipe: $title (ID: $recipeId)'); + if (tabs.isEmpty) return const SizedBox.shrink(); - 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, + return SizedBox( + height: 36, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: tabs.length, + separatorBuilder: (_, __) => SizedBox(width: DesignTokens.space2), + itemBuilder: (context, index) { + final tab = tabs[index]; + final isSelected = + _searchController.resultTabIndex.value == tab.index; + return GestureDetector( + onTap: () => _searchController.resultTabIndex.value = tab.index, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.12) + : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.06), + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.3) + : const Color(0x00000000), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + tab.label, + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark + ? DarkDesignTokens.text2 + : DesignTokens.text2), + ), + ), + if (tab.count > 0) ...[ + SizedBox(width: 4), + Container( + padding: EdgeInsets.symmetric( + horizontal: 6, + vertical: 1, + ), + decoration: BoxDecoration( + color: isSelected + ? (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.2) + : (isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3) + .withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusMd, + ), + child: Text( + '${tab.count}', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: isSelected + ? (DesignTokens.dynamicPrimary) + : (isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3), + ), + ), + ), + ], + ], + ), + ), ); - } + }, + ), + ); + } + + Widget _buildResultContent(bool isDark) { + final tabIndex = _searchController.resultTabIndex.value; + switch (tabIndex) { + case 1: + return _buildIngredientResults(isDark); + case 2: + return _buildTagResults( + isDark, + _searchController.tasteTagResults, + 'taste', + ); + case 3: + return _buildTagResults( + isDark, + _searchController.cookingTagResults, + 'cooking', + ); + case 0: + default: + return _buildRecipeResults(isDark); + } + } + + Widget _buildRecipeResults(bool isDark) { + final recipes = _searchController.recipeResults; + if (recipes.isEmpty) { + return _buildNoResultForTab(isDark, '菜谱'); + } + + return ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + itemCount: recipes.length, + separatorBuilder: (_, __) => const SizedBox(height: DesignTokens.space2), + itemBuilder: (context, index) { + final recipe = recipes[index]; + return _buildRecipeCard(recipe, isDark); + }, + ); + } + + Widget _buildRecipeCard(RecipeModel recipe, bool isDark) { + return GestureDetector( + onTap: () { + Get.toNamed('/recipe-detail', arguments: '${recipe.id}'); }, child: Container( padding: const EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: BorderRadius.circular(DesignTokens.radiusMd), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.03), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], + borderRadius: DesignTokens.borderRadiusMd, + border: Border.all( + color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.1), + ), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 封面图 ClipRRect( - borderRadius: BorderRadius.circular(DesignTokens.radiusSm), + borderRadius: DesignTokens.borderRadiusSm, child: RecipeImage( - recipeId: recipeId ?? 0, - picId: picId, - coverUrl: cover, - width: 90, - height: 90, + recipeId: recipe.id, + picId: recipe.picId, + coverUrl: recipe.cover, + width: 64, + height: 64, fit: BoxFit.cover, mode: RecipeImageMode.thumbnail, ), ), const SizedBox(width: DesignTokens.space3), - - // 信息区 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title, + recipe.title, style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.w600, @@ -670,66 +802,261 @@ class _SearchPageState extends State { ? DarkDesignTokens.text1 : DesignTokens.text1, ), - maxLines: 2, + maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (category != null && category!.isNotEmpty) ...[ - const SizedBox(height: 4), + SizedBox(height: 2), + if (recipe.categoryName != null && + recipe.categoryName!.isNotEmpty) Container( - padding: const EdgeInsets.symmetric( + margin: EdgeInsets.only(top: 2), + padding: EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) - .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), + (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.08), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( - '📂 $category', + recipe.categoryName!, style: TextStyle( - fontSize: 11, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + fontSize: DesignTokens.fontXs, + color: DesignTokens.dynamicPrimary, ), ), ), - ], - if (intro.isNotEmpty) ...[ - const SizedBox(height: 6), + if (recipe.intro != null && recipe.intro!.isNotEmpty) ...[ + const SizedBox(height: 2), Text( - intro, - maxLines: 2, - overflow: TextOverflow.ellipsis, + recipe.intro!, style: TextStyle( - fontSize: DesignTokens.fontSm, + fontSize: DesignTokens.fontXs, color: isDark - ? DarkDesignTokens.text2 - : DesignTokens.text2, - height: 1.3, + ? DarkDesignTokens.text3 + : DesignTokens.text3, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ], ), ), - - // 箭头 - Padding( - padding: const EdgeInsets.only(left: 8, top: 4), - child: Icon( - CupertinoIcons.chevron_right, - size: 16, - color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, - ), + Icon( + CupertinoIcons.chevron_forward, + size: 16, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ], ), ), ); } + + Widget _buildIngredientResults(bool isDark) { + final ingredients = _searchController.ingredientResults; + if (ingredients.isEmpty) { + return _buildNoResultForTab(isDark, '食材'); + } + + return ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + itemCount: ingredients.length, + separatorBuilder: (_, __) => const SizedBox(height: DesignTokens.space2), + itemBuilder: (context, index) { + final ingredient = ingredients[index]; + return GestureDetector( + onTap: () { + Get.toNamed( + AppRoutes.toolsIngredient, + arguments: {'id': ingredient.id, 'name': ingredient.name}, + ); + }, + child: Container( + padding: const EdgeInsets.all(DesignTokens.space3), + 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: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: + (isDark + ? DarkDesignTokens.secondary + : DesignTokens.secondary) + .withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusSm, + ), + child: const Center( + child: Text('🥬', style: TextStyle(fontSize: 22)), + ), + ), + const SizedBox(width: DesignTokens.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ingredient.name, + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w600, + color: isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1, + ), + ), + if (ingredient.categoryName != null) ...[ + const SizedBox(height: 2), + Text( + '${ingredient.categoryName} · ${ingredient.recipeCount}道菜谱', + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark + ? DarkDesignTokens.text3 + : DesignTokens.text3, + ), + ), + ], + ], + ), + ), + Icon( + CupertinoIcons.chevron_forward, + size: 16, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildTagResults(bool isDark, List tags, String tagType) { + if (tags.isEmpty) { + return _buildNoResultForTab(isDark, tagType == 'taste' ? '口味' : '工艺'); + } + + return GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: DesignTokens.space2, + crossAxisSpacing: DesignTokens.space2, + childAspectRatio: 3.0, + ), + itemCount: tags.length, + itemBuilder: (context, index) { + final tag = tags[index]; + return GestureDetector( + onTap: () { + Get.toNamed( + AppRoutes.tagRecipeList, + arguments: { + 'tagName': tag.name, + 'tagId': tag.id, + 'tagType': tagType, + }, + ); + }, + child: Container( + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.card : DesignTokens.card, + borderRadius: DesignTokens.borderRadiusLg, + border: Border.all( + color: isDark + ? DarkDesignTokens.glassBorder + : DesignTokens.text3.withValues(alpha: 0.1), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + tagType == 'taste' ? '👅' : '🍳', + style: const TextStyle(fontSize: 16), + ), + const SizedBox(width: 6), + Flexible( + child: Text( + tag.name, + style: TextStyle( + fontSize: DesignTokens.fontSm, + fontWeight: FontWeight.w500, + color: isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildNoResultForTab(bool isDark, String tabName) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + tabName == '口味' + ? '👅' + : tabName == '工艺' + ? '🍳' + : '📭', + style: const TextStyle(fontSize: 48), + ), + const SizedBox(height: DesignTokens.space3), + Text( + '暂无$tabName搜索结果', + style: TextStyle( + fontSize: DesignTokens.fontMd, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), + ], + ), + ); + } + + void _showAdvancedSearch(bool isDark) { + Get.toNamed(AppRoutes.advancedSearch); + } +} + +class _SearchTabInfo { + final String label; + final int count; + final int index; + + const _SearchTabInfo({ + required this.label, + required this.count, + required this.index, + }); } diff --git a/lib/src/pages/home/tag_recipe_list_page.dart b/lib/src/pages/home/tag_recipe_list_page.dart index d73c8d2..79e4f0f 100644 --- a/lib/src/pages/home/tag_recipe_list_page.dart +++ b/lib/src/pages/home/tag_recipe_list_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: tag_recipe_list_page.dart * 名称: 标签菜品列表页面 * 作用: 显示对应口味/工艺标签的菜品列表 @@ -159,35 +159,35 @@ class _TagRecipeListPageState extends State { return false; }, child: CustomScrollView( - physics: const BouncingScrollPhysics(), + physics: BouncingScrollPhysics(), slivers: [ SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(DesignTokens.space4), + padding: EdgeInsets.all(DesignTokens.space4), child: Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( gradient: LinearGradient( colors: [ - DesignTokens.primary.withValues(alpha: 0.15), + DesignTokens.dynamicPrimary.withValues(alpha: 0.15), DesignTokens.secondary.withValues(alpha: 0.08), ], ), - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: DesignTokens.primary.withValues(alpha: 0.15), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.15), ), ), child: Row( children: [ Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), decoration: BoxDecoration( - color: DesignTokens.primary, - borderRadius: BorderRadius.circular(12), + color: DesignTokens.dynamicPrimary, + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( widget.tagType == 'process' ? '工艺' : '口味', @@ -230,7 +230,7 @@ class _TagRecipeListPageState extends State { ? CupertinoIcons.flame_fill : CupertinoIcons.tag_fill, size: 24, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ], ), @@ -271,7 +271,7 @@ class _TagRecipeListPageState extends State { child: Container( decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isDark ? DarkDesignTokens.glassBorder @@ -392,7 +392,7 @@ class _TagRecipeListPageState extends State { Widget _buildReachBottom(bool isDark) { return Container( width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: DesignTokens.space4), + padding: EdgeInsets.symmetric(vertical: DesignTokens.space4), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -400,8 +400,8 @@ class _TagRecipeListPageState extends State { width: 40, height: 3, decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(2), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.3), + borderRadius: DesignTokens.borderRadiusSm, ), ), const SizedBox(height: DesignTokens.space2), diff --git a/lib/src/pages/profile/bedtime_reminder_page.dart b/lib/src/pages/profile/bedtime_reminder_page.dart index af41530..ceb48bb 100644 --- a/lib/src/pages/profile/bedtime_reminder_page.dart +++ b/lib/src/pages/profile/bedtime_reminder_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: bedtime_reminder_page.dart * 名称: 就寝提醒页面 * 作用: 根据晚餐时间推荐就寝时间,睡前不宜进食提醒 @@ -77,11 +77,11 @@ class BedtimeReminderPage extends StatelessWidget { Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(8), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Text('🍽️', style: TextStyle(fontSize: 24)), ), @@ -101,7 +101,7 @@ class BedtimeReminderPage extends StatelessWidget { children: [ Expanded( child: CupertinoButton( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: BorderRadius.circular(DesignTokens.radiusMd), onPressed: () => _showTimePicker(controller, isDark), @@ -113,13 +113,13 @@ class BedtimeReminderPage extends StatelessWidget { style: TextStyle( fontSize: DesignTokens.fontXxl, fontWeight: FontWeight.bold, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), - const SizedBox(width: DesignTokens.space2), + SizedBox(width: DesignTokens.space2), Icon( CupertinoIcons.clock, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ], ), @@ -145,13 +145,13 @@ class BedtimeReminderPage extends StatelessWidget { bool isDark, ) { return Container( - padding: const EdgeInsets.all(DesignTokens.space4), + padding: EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.08), borderRadius: BorderRadius.circular(DesignTokens.radiusLg), border: Border.all( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.2), ), ), @@ -161,11 +161,11 @@ class BedtimeReminderPage extends StatelessWidget { Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(8), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Text('🌙', style: TextStyle(fontSize: 24)), ), @@ -198,7 +198,7 @@ class BedtimeReminderPage extends StatelessWidget { const SizedBox(height: DesignTokens.space4), Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space4), + padding: EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: BorderRadius.circular(DesignTokens.radiusMd), @@ -211,7 +211,7 @@ class BedtimeReminderPage extends StatelessWidget { style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -243,11 +243,11 @@ class BedtimeReminderPage extends StatelessWidget { Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(8), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Text('🔔', style: TextStyle(fontSize: 24)), ), @@ -319,13 +319,13 @@ class BedtimeReminderPage extends StatelessWidget { Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(8), decoration: BoxDecoration( color: shouldShowWarning ? DesignTokens.red.withValues(alpha: 0.15) - : (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + : (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( shouldShowWarning ? '⚠️' : '🍎', @@ -410,11 +410,11 @@ class BedtimeReminderPage extends StatelessWidget { Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(8), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Text('💡', style: TextStyle(fontSize: 24)), ), @@ -447,11 +447,11 @@ class BedtimeReminderPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - margin: const EdgeInsets.only(top: 2), + margin: EdgeInsets.only(top: 2), width: 6, height: 6, decoration: BoxDecoration( - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, shape: BoxShape.circle, ), ), @@ -489,7 +489,7 @@ class BedtimeReminderPage extends StatelessWidget { CupertinoSwitch( value: value, onChanged: onChanged, - activeTrackColor: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + activeTrackColor: DesignTokens.dynamicPrimary, ), ], ); diff --git a/lib/src/pages/profile/cache_manage_page.dart b/lib/src/pages/profile/cache_manage_page.dart index b83600a..9edc4af 100644 --- a/lib/src/pages/profile/cache_manage_page.dart +++ b/lib/src/pages/profile/cache_manage_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: cache_manage_page.dart * 说明: 缓存管理页面 * 作用: 管理首页Discover缓存、菜品详情缓存、API缓存与图片缓存 @@ -256,7 +256,7 @@ class _CacheManagePageState extends State { onPressed: () => Get.back(), child: Icon( CupertinoIcons.back, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), trailing: CupertinoButton( @@ -264,7 +264,7 @@ class _CacheManagePageState extends State { onPressed: _load, child: Icon( CupertinoIcons.refresh, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), backgroundColor: isDark @@ -413,7 +413,7 @@ class _CacheManagePageState extends State { required bool destructive, }) { return CupertinoButton( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space3, ), @@ -427,7 +427,7 @@ class _CacheManagePageState extends State { size: 20, color: destructive ? DesignTokens.red - : (isDark ? DarkDesignTokens.primary : DesignTokens.primary), + : (DesignTokens.dynamicPrimary), ), const SizedBox(width: DesignTokens.space3), Expanded( @@ -594,11 +594,11 @@ class _CacheManagePageState extends State { '• 清理后不影响收藏和浏览记录\n' '• 建议定期清理以释放存储空间', ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, ), @@ -607,19 +607,15 @@ class _CacheManagePageState extends State { Icon( CupertinoIcons.info_circle, size: 16, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), - const SizedBox(width: DesignTokens.space2), + SizedBox(width: DesignTokens.space2), Expanded( child: Text( '清理缓存不会删除您的收藏、笔记等个人数据', style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -698,7 +694,7 @@ class _CacheManagePageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.1), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, ), child: item.cover != null && item.cover!.isNotEmpty @@ -710,14 +706,14 @@ class _CacheManagePageState extends State { errorBuilder: (_, __, ___) => Icon( CupertinoIcons.photo, size: 20, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ) : Icon( CupertinoIcons.doc_text, size: 20, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(width: DesignTokens.space3), diff --git a/lib/src/pages/profile/chat_page.dart b/lib/src/pages/profile/chat_page.dart index 57b0738..5a62b76 100644 --- a/lib/src/pages/profile/chat_page.dart +++ b/lib/src/pages/profile/chat_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: feedback_page.dart * 名称: 意见反馈页面 * 作用: iOS 26 风格的聊天式意见反馈页面,客服在左用户在右 @@ -177,20 +177,20 @@ class _FeedbackPageState extends State { children: [ if (!msg.isUser) ...[ _buildAvatar(isDark, false), - const SizedBox(width: DesignTokens.space2), + SizedBox(width: DesignTokens.space2), ], Flexible( child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.72, ), - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space3, ), decoration: BoxDecoration( color: msg.isUser - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.card : DesignTokens.card), borderRadius: BorderRadius.only( topLeft: const Radius.circular(DesignTokens.radiusLg), @@ -229,7 +229,7 @@ class _FeedbackPageState extends State { ), ), if (msg.isUser) ...[ - const SizedBox(width: DesignTokens.space2), + SizedBox(width: DesignTokens.space2), _buildAvatar(isDark, true), ], ], @@ -243,10 +243,10 @@ class _FeedbackPageState extends State { height: 32, decoration: BoxDecoration( color: isUser - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) .withValues(alpha: 0.15) : (isDark ? DarkDesignTokens.card : DesignTokens.card), - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) .withValues(alpha: 0.1), @@ -272,42 +272,36 @@ class _FeedbackPageState extends State { child: Row( children: FeedbackType.values.map((type) { return Padding( - padding: const EdgeInsets.only(right: DesignTokens.space2), + padding: EdgeInsets.only(right: DesignTokens.space2), child: GestureDetector( onTap: () => _onTypeSelected(type), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(type.emoji, style: const TextStyle(fontSize: 14)), - const SizedBox(width: 4), + Text(type.emoji, style: TextStyle(fontSize: 14)), + SizedBox(width: 4), Text( type.label, style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w500, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -353,7 +347,7 @@ class _FeedbackPageState extends State { color: isDark ? DarkDesignTokens.background : DesignTokens.background, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, ), style: TextStyle( fontSize: DesignTokens.fontMd, @@ -362,13 +356,13 @@ class _FeedbackPageState extends State { onSubmitted: (_) => _send(), ), ), - const SizedBox(width: 8), + SizedBox(width: 8), CupertinoButton( padding: EdgeInsets.zero, - minimumSize: const Size(36, 36), - borderRadius: BorderRadius.circular(18), + minimumSize: Size(36, 36), + borderRadius: DesignTokens.borderRadiusLg, color: _typeSelected && _ctrl.text.trim().isNotEmpty - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) .withValues(alpha: 0.3), onPressed: _typeSelected ? _send : null, diff --git a/lib/src/pages/profile/data_center_page.dart b/lib/src/pages/profile/data_center_page.dart index 17482b4..bde262b 100644 --- a/lib/src/pages/profile/data_center_page.dart +++ b/lib/src/pages/profile/data_center_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: data_center_page.dart * 名称: 数据管理中心页面 * 作用: 管理分类标签(口味/工艺/食材)和过敏原数据,支持API同步+本地持久化 @@ -113,10 +113,10 @@ class _DataCenterPageState extends State { padding: EdgeInsets.zero, onPressed: _isSyncing ? null : _syncAll, child: _isSyncing - ? const CupertinoActivityIndicator() + ? CupertinoActivityIndicator() : Icon( CupertinoIcons.refresh, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, size: 22, ), ), @@ -124,16 +124,16 @@ class _DataCenterPageState extends State { child: SafeArea( top: false, child: ListView( - padding: const EdgeInsets.all(DesignTokens.space4), + padding: EdgeInsets.all(DesignTokens.space4), children: [ _buildSectionTitle('🏷️ 分类标签', isDark), - const SizedBox(height: DesignTokens.space2), + SizedBox(height: DesignTokens.space2), _buildTagCard( isDark, icon: CupertinoIcons.tag_fill, label: '口味标签', count: '${_tasteTags.length}个', - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, onTap: () => _showTagDetailSheet(isDark, '口味标签', _tasteTags, true), ), @@ -152,7 +152,7 @@ class _DataCenterPageState extends State { icon: CupertinoIcons.square_grid_2x2_fill, label: '食材分类', count: '${_categories.length}类', - color: const Color(0xFF34C759), + color: DesignTokens.green, onTap: () => _showCategoryDetailSheet(isDark), ), const SizedBox(height: DesignTokens.space4), @@ -195,7 +195,7 @@ class _DataCenterPageState extends State { padding: const EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all(color: color.withValues(alpha: 0.15)), ), child: Row( @@ -205,7 +205,7 @@ class _DataCenterPageState extends State { height: 40, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Icon(icon, color: color, size: 20), ), @@ -257,9 +257,9 @@ class _DataCenterPageState extends State { return Container( decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: const Color(0xFFFF3B30).withValues(alpha: 0.15), + color: DesignTokens.red.withValues(alpha: 0.15), ), ), clipBehavior: Clip.antiAlias, @@ -273,7 +273,7 @@ class _DataCenterPageState extends State { Icon( CupertinoIcons.exclamationmark_triangle_fill, size: 20, - color: const Color(0xFFFF3B30), + color: DesignTokens.red, ), const SizedBox(width: DesignTokens.space2), Expanded( @@ -334,8 +334,8 @@ class _DataCenterPageState extends State { vertical: DesignTokens.space1, ), decoration: BoxDecoration( - color: const Color(0xFFFF3B30).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + color: DesignTokens.red.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusMd, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -346,7 +346,7 @@ class _DataCenterPageState extends State { a, style: const TextStyle( fontSize: DesignTokens.fontXs, - color: Color(0xFFFF3B30), + color: DesignTokens.red, fontWeight: FontWeight.w500, ), ), @@ -467,8 +467,8 @@ class _DataCenterPageState extends State { vertical: 2, ), decoration: BoxDecoration( - color: const Color(0xFFFF3B30), - borderRadius: BorderRadius.circular(8), + color: DesignTokens.red, + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( '$checkedCount', @@ -509,7 +509,7 @@ class _DataCenterPageState extends State { : CupertinoIcons.square, size: 18, color: isSelected - ? const Color(0xFFFF3B30) + ? DesignTokens.red : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3), @@ -521,7 +521,7 @@ class _DataCenterPageState extends State { style: TextStyle( fontSize: DesignTokens.fontSm, color: isSelected - ? const Color(0xFFFF3B30) + ? DesignTokens.red : (isDark ? DarkDesignTokens.text1 : DesignTokens.text1), @@ -626,15 +626,15 @@ class _DataCenterPageState extends State { itemBuilder: (context, index) { final tag = tags[index]; return Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space2, ), decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(12), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.08), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: DesignTokens.primary.withValues(alpha: 0.15), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.15), ), ), child: Column( @@ -649,7 +649,7 @@ class _DataCenterPageState extends State { style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w600, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -742,7 +742,7 @@ class _DataCenterPageState extends State { DesignTokens.orange.withValues(alpha: 0.04), ], ), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: DesignTokens.orange.withValues(alpha: 0.2), ), @@ -827,10 +827,10 @@ class _DataCenterPageState extends State { margin: const EdgeInsets.only(bottom: DesignTokens.space2), padding: const EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( - color: const Color(0xFF34C759).withValues(alpha: 0.06), - borderRadius: BorderRadius.circular(12), + color: DesignTokens.green.withValues(alpha: 0.06), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: const Color(0xFF34C759).withValues(alpha: 0.15), + color: DesignTokens.green.withValues(alpha: 0.15), ), ), child: Row( @@ -838,7 +838,7 @@ class _DataCenterPageState extends State { Icon( CupertinoIcons.folder_fill, size: 18, - color: const Color(0xFF34C759), + color: DesignTokens.green, ), const SizedBox(width: DesignTokens.space2), Expanded( @@ -863,13 +863,13 @@ class _DataCenterPageState extends State { color: const Color( 0xFF34C759, ).withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( '${cat['count']}', style: const TextStyle( fontSize: 11, - color: Color(0xFF34C759), + color: DesignTokens.green, fontWeight: FontWeight.w600, ), ), diff --git a/lib/src/pages/profile/favorites_page.dart b/lib/src/pages/profile/favorites_page.dart index 61c25d5..1313b9b 100644 --- a/lib/src/pages/profile/favorites_page.dart +++ b/lib/src/pages/profile/favorites_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: favorites_page.dart * 名称: 收藏页面 * 作用: iOS 26 Liquid Glass 风格的收藏页面 @@ -114,20 +114,20 @@ class _FavoritesPageState extends State { if (count == 0) return const SizedBox.shrink(); return _buildGlassChip('$count', isDark, highlight: true); }), - const SizedBox(width: DesignTokens.space3), + SizedBox(width: DesignTokens.space3), Obx(() { if (_favoritesController!.count == 0) { - return const SizedBox.shrink(); + return SizedBox.shrink(); } return CupertinoButton( padding: EdgeInsets.zero, - minimumSize: const Size(36, 36), + minimumSize: Size(36, 36), onPressed: _favoritesController!.toggleEditMode, child: Text( _favoritesController!.isEditMode.value ? '完成' : '编辑', style: TextStyle( fontSize: DesignTokens.fontMd, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w500, ), ), @@ -167,7 +167,7 @@ class _FavoritesPageState extends State { fontSize: DesignTokens.fontXs, fontWeight: FontWeight.w600, color: highlight - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.text1 : DesignTokens.text1), ), ), @@ -233,16 +233,14 @@ class _FavoritesPageState extends State { height: 44, decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusMd, ), child: Center( child: Text( tool.icon, - style: const TextStyle(fontSize: 24), + style: TextStyle(fontSize: 24), ), ), ), @@ -255,7 +253,7 @@ class _FavoritesPageState extends State { decoration: BoxDecoration( color: tool.needsNetwork ? DesignTokens.green - : DesignTokens.primary, + : DesignTokens.dynamicPrimary, shape: BoxShape.circle, ), ), @@ -289,15 +287,15 @@ class _FavoritesPageState extends State { filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: Container( width: 72, - padding: const EdgeInsets.symmetric(vertical: DesignTokens.space2), + padding: EdgeInsets.symmetric(vertical: DesignTokens.space2), decoration: BoxDecoration( color: isDark - ? DarkDesignTokens.primary.withValues(alpha: 0.08) + ? DesignTokens.dynamicPrimary.withValues(alpha: 0.08) : DesignTokens.primaryLight.withValues(alpha: 0.7), borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: - (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.25), ), ), @@ -309,24 +307,20 @@ class _FavoritesPageState extends State { height: 44, decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.15), borderRadius: DesignTokens.borderRadiusMd, ), - child: const Center( + child: Center( child: Text('🛠️', style: TextStyle(fontSize: 24)), ), ), - const SizedBox(height: 6), + SizedBox(height: 6), Text( '更多', style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -434,7 +428,7 @@ class _FavoritesPageState extends State { final isSelected = _favoritesController!.selectedCategory.value == cat; return Padding( - padding: const EdgeInsets.only(right: DesignTokens.space2), + padding: EdgeInsets.only(right: DesignTokens.space2), child: GestureDetector( onTap: () => _favoritesController!.setCategory(cat), child: ClipRRect( @@ -442,13 +436,13 @@ class _FavoritesPageState extends State { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: isSelected - ? DesignTokens.primary.withValues(alpha: 0.85) + ? DesignTokens.dynamicPrimary.withValues(alpha: 0.85) : (isDark ? DarkDesignTokens.glass : DesignTokens.card.withValues( @@ -457,7 +451,7 @@ class _FavoritesPageState extends State { borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isSelected - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.glassBorder : DesignTokens.text3.withValues( @@ -552,7 +546,7 @@ class _FavoritesPageState extends State { _favoritesController!.hasSelection ? '取消全选' : '全选', style: TextStyle( fontSize: DesignTokens.fontMd, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -637,10 +631,10 @@ class _FavoritesPageState extends State { color: DesignTokens.primaryLight.withValues(alpha: 0.6), borderRadius: DesignTokens.borderRadiusLg, ), - child: const Icon( + child: Icon( CupertinoIcons.heart, size: 32, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(height: DesignTokens.space3), @@ -698,17 +692,17 @@ class _FavoritesPageState extends State { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14), child: Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isEditMode && isSelected - ? DesignTokens.primary.withValues(alpha: 0.15) + ? DesignTokens.dynamicPrimary.withValues(alpha: 0.15) : (isDark ? DarkDesignTokens.glass : DesignTokens.card.withValues(alpha: 0.75)), borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: isEditMode && isSelected - ? DesignTokens.primary.withValues(alpha: 0.5) + ? DesignTokens.dynamicPrimary.withValues(alpha: 0.5) : (isDark ? DarkDesignTokens.glassBorder : DesignTokens.text3.withValues(alpha: 0.1)), @@ -721,15 +715,15 @@ class _FavoritesPageState extends State { Container( width: 24, height: 24, - margin: const EdgeInsets.only(right: DesignTokens.space3), + margin: EdgeInsets.only(right: DesignTokens.space3), decoration: BoxDecoration( color: isSelected - ? DesignTokens.primary - : Colors.transparent, + ? DesignTokens.dynamicPrimary + : const Color(0x00000000), borderRadius: DesignTokens.borderRadiusSm, border: Border.all( color: isSelected - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3), @@ -752,10 +746,10 @@ class _FavoritesPageState extends State { color: DesignTokens.primaryLight.withValues(alpha: 0.5), borderRadius: DesignTokens.borderRadiusMd, ), - child: const Icon( + child: Icon( CupertinoIcons.book, size: 24, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(width: DesignTokens.space3), diff --git a/lib/src/pages/profile/footprints_page.dart b/lib/src/pages/profile/footprints_page.dart index ce4b48f..a36cbc9 100644 --- a/lib/src/pages/profile/footprints_page.dart +++ b/lib/src/pages/profile/footprints_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: footprints_page.dart * 说明: 浏览记录页面,展示用户浏览菜谱历史 * 作用: 展示用户足迹,支持管理、删除、清空 @@ -41,7 +41,7 @@ class FootprintsPage extends StatelessWidget { onPressed: () => Get.back(), child: Icon( CupertinoIcons.back, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), trailing: Row( @@ -49,10 +49,10 @@ class FootprintsPage extends StatelessWidget { children: [ CupertinoButton( padding: EdgeInsets.zero, - onPressed: () => Get.to(() => const CacheManagePage()), + onPressed: () => Get.to(() => CacheManagePage()), child: Icon( CupertinoIcons.archivebox, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, size: 20, ), ), @@ -106,7 +106,7 @@ class FootprintsPage extends StatelessWidget { fontSize: DesignTokens.fontLg, ), ), - const SizedBox(height: DesignTokens.space2), + SizedBox(height: DesignTokens.space2), Text( '去首页发现美味吧', style: TextStyle( @@ -114,15 +114,13 @@ class FootprintsPage extends StatelessWidget { fontSize: DesignTokens.fontSm, ), ), - const SizedBox(height: DesignTokens.space4), + SizedBox(height: DesignTokens.space4), CupertinoButton( onPressed: () => Get.toNamed('/'), child: Text( '去首页', style: TextStyle( - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -235,13 +233,11 @@ class FootprintsPage extends StatelessWidget { width: 64, height: 64, color: isDark - ? DarkDesignTokens.primary.withValues(alpha: 0.2) - : DesignTokens.primary.withValues(alpha: 0.1), + ? DesignTokens.dynamicPrimary.withValues(alpha: 0.2) + : DesignTokens.dynamicPrimary.withValues(alpha: 0.1), child: Icon( CupertinoIcons.photo, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, size: 24, ), ), @@ -263,20 +259,18 @@ class FootprintsPage extends StatelessWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: DesignTokens.space1), + SizedBox(height: DesignTokens.space1), Row( children: [ if (item.category != null) ...[ Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space1, vertical: 2, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, ), @@ -284,9 +278,7 @@ class FootprintsPage extends StatelessWidget { item.category!, style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/pages/profile/nutrition/add_meal_sheet.dart b/lib/src/pages/profile/nutrition/add_meal_sheet.dart index 7aa547c..6c8fa2c 100644 --- a/lib/src/pages/profile/nutrition/add_meal_sheet.dart +++ b/lib/src/pages/profile/nutrition/add_meal_sheet.dart @@ -1,4 +1,4 @@ -// 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/meal_record_model.dart'; @@ -69,7 +69,7 @@ class _AddMealSheetState extends State { height: 4, decoration: BoxDecoration( color: DesignTokens.text3.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(2), + borderRadius: DesignTokens.borderRadiusSm, ), ), Padding( @@ -103,12 +103,12 @@ class _AddMealSheetState extends State { CupertinoButton( padding: EdgeInsets.zero, onPressed: _save, - child: const Text( + child: Text( '保存', style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w600, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -201,13 +201,13 @@ class _AddMealSheetState extends State { child: GestureDetector( onTap: () => setState(() => _selectedMealType = type), child: Container( - margin: const EdgeInsets.symmetric(horizontal: 2), - padding: const EdgeInsets.symmetric( + margin: EdgeInsets.symmetric(horizontal: 2), + padding: EdgeInsets.symmetric( vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: isSelected - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.text3.withValues(alpha: 0.15) : DesignTokens.background), @@ -324,7 +324,7 @@ class _AddMealSheetState extends State { fontSize: DesignTokens.fontSm, color: isDark ? DarkDesignTokens.text2 - : DesignTokens.primary, + : DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/pages/profile/nutrition/goal_setting_page.dart b/lib/src/pages/profile/nutrition/goal_setting_page.dart index cda8cc3..15a22e6 100644 --- a/lib/src/pages/profile/nutrition/goal_setting_page.dart +++ b/lib/src/pages/profile/nutrition/goal_setting_page.dart @@ -1,4 +1,4 @@ -// 2026-04-09 | GoalSettingPage | 用户营养目标设置页面 | iOS26风格滑块+预设选择 +// 2026-04-09 | GoalSettingPage | 用户营养目标设置页面 | iOS26风格滑块+预设选择 // 2026-04-09 | 初始创建,支持热量/蛋白质/脂肪/碳水四项目标设置 import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; @@ -39,7 +39,7 @@ class _GoalSettingPageState extends State { ? DarkDesignTokens.background : DesignTokens.background, navigationBar: CupertinoNavigationBar( - middle: const Text('🎯 营养目标'), + middle: Text('🎯 营养目标'), backgroundColor: isDark ? DarkDesignTokens.card.withValues(alpha: 0.85) : DesignTokens.card.withValues(alpha: 0.85), @@ -51,7 +51,7 @@ class _GoalSettingPageState extends State { style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w600, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -201,7 +201,7 @@ class _GoalSettingPageState extends State { ], ), Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space1, ), @@ -211,10 +211,10 @@ class _GoalSettingPageState extends State { ), child: Text( '${value.toInt()} ${type.unit}', - style: const TextStyle( + style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.w700, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -323,7 +323,7 @@ class _GoalSettingPageState extends State { Color _getGoalColor(GoalType type) { switch (type) { case GoalType.calories: - return DesignTokens.primary; + return DesignTokens.dynamicPrimary; case GoalType.protein: return DesignTokens.red; case GoalType.fat: diff --git a/lib/src/pages/profile/nutrition/nutrition_center_page.dart b/lib/src/pages/profile/nutrition/nutrition_center_page.dart index ab7f5d0..938b801 100644 --- a/lib/src/pages/profile/nutrition/nutrition_center_page.dart +++ b/lib/src/pages/profile/nutrition/nutrition_center_page.dart @@ -1,4 +1,4 @@ -// 2026-04-09 | NutritionCenterPage | 营养中心页面 | iOS26风格饮食日记+营养分析+目标管理 +// 2026-04-09 | NutritionCenterPage | 营养中心页面 | iOS26风格饮食日记+营养分析+目标管理 // 2026-04-10 | 修复报告按钮卡死:添加错误处理+空指针保护 import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; @@ -98,7 +98,7 @@ class _NutritionCenterPageState extends State { } }, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), @@ -106,11 +106,11 @@ class _NutritionCenterPageState extends State { color: DesignTokens.primaryLight, borderRadius: DesignTokens.borderRadiusFull, ), - child: const Text( + child: Text( '📊 报告', style: TextStyle( fontSize: DesignTokens.fontSm, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w600, ), ), @@ -129,7 +129,7 @@ class _NutritionCenterPageState extends State { } }, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), @@ -137,11 +137,11 @@ class _NutritionCenterPageState extends State { color: DesignTokens.primaryLight, borderRadius: DesignTokens.borderRadiusFull, ), - child: const Text( + child: Text( '今天', style: TextStyle( fontSize: DesignTokens.fontSm, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w600, ), ), @@ -250,7 +250,7 @@ class _NutritionCenterPageState extends State { style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w600, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -423,7 +423,7 @@ class _NutritionCenterPageState extends State { GestureDetector( onTap: () => _showAddMealSheet(mealType), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space1, ), @@ -431,11 +431,11 @@ class _NutritionCenterPageState extends State { color: DesignTokens.primaryLight, borderRadius: DesignTokens.borderRadiusFull, ), - child: const Text( + child: Text( '+ 添加', style: TextStyle( fontSize: DesignTokens.fontSm, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w600, ), ), diff --git a/lib/src/pages/profile/profile_home.dart b/lib/src/pages/profile/profile_home.dart index eacedb3..12ff843 100644 --- a/lib/src/pages/profile/profile_home.dart +++ b/lib/src/pages/profile/profile_home.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: profile_home.dart * 名称: 个人中心首页标签 * 作用: iOS 26 风格的用户信息展示、功能入口和消息预览 @@ -71,7 +71,7 @@ class ProfileHomeTab extends StatelessWidget { _FeatureItem( CupertinoIcons.arrow_up_arrow_down, '份量缩放', - DesignTokens.primary, + DesignTokens.dynamicPrimary, AppRoutes.servingScaler, ), ]; @@ -182,18 +182,18 @@ class ProfileHomeTab extends StatelessWidget { height: 56, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { - return const Icon( + return Icon( CupertinoIcons.person_fill, size: 28, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ); }, ), ) - : const Icon( + : Icon( CupertinoIcons.person_fill, size: 28, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(width: DesignTokens.space3), @@ -245,13 +245,13 @@ class ProfileHomeTab extends StatelessWidget { Widget _buildMiniButton(String text, bool isDark, VoidCallback? onPressed) { return CupertinoButton( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space1, ), minimumSize: Size.zero, borderRadius: DesignTokens.borderRadiusSm, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, onPressed: onPressed, child: Text( text, @@ -362,7 +362,7 @@ class ProfileHomeTab extends StatelessWidget { Widget _buildMessageItem(String title, String subtitle, bool isDark) { return Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: DesignTokens.borderRadiusMd, @@ -373,9 +373,9 @@ class ProfileHomeTab extends StatelessWidget { Container( width: 8, height: 8, - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(width: DesignTokens.space3), @@ -419,8 +419,8 @@ class ProfileHomeTab extends StatelessWidget { width: 40, height: 4, decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(2), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.2), + borderRadius: DesignTokens.borderRadiusSm, ), ), const SizedBox(height: DesignTokens.space4), @@ -447,7 +447,7 @@ class ProfileHomeTab extends StatelessWidget { letterSpacing: 0.5, ), ), - const SizedBox(height: DesignTokens.space2), + SizedBox(height: DesignTokens.space2), Text( 'v0.88.5 · 2026 Liquid Glass', style: TextStyle( @@ -455,12 +455,12 @@ class ProfileHomeTab extends StatelessWidget { color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Text( '使用Flutter SDk开发,UI高度定制化的跨平台应用 ', style: TextStyle( fontSize: DesignTokens.fontXs, - color: DesignTokens.primary.withValues(alpha: 0.6), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.6), ), ), ], diff --git a/lib/src/pages/profile/profile_settings.dart b/lib/src/pages/profile/profile_settings.dart index cfcb80a..9c759d7 100644 --- a/lib/src/pages/profile/profile_settings.dart +++ b/lib/src/pages/profile/profile_settings.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: profile_settings.dart * 名称: 个人中心设置标签 * 作用: iOS 26 风格的设置选项,使用 DesignTokens 和 GlassSettingsTile @@ -181,7 +181,7 @@ class ProfileSettingsTab extends StatelessWidget { onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( vertical: DesignTokens.space3, horizontal: DesignTokens.space4, ), @@ -191,7 +191,7 @@ class ProfileSettingsTab extends StatelessWidget { width: 28, height: 28, decoration: BoxDecoration( - color: (titleColor ?? DesignTokens.primary).withValues( + color: (titleColor ?? DesignTokens.dynamicPrimary).withValues( alpha: 0.12, ), borderRadius: DesignTokens.borderRadiusSm, @@ -199,7 +199,7 @@ class ProfileSettingsTab extends StatelessWidget { child: Icon( icon, size: 14, - color: titleColor ?? DesignTokens.primary, + color: titleColor ?? DesignTokens.dynamicPrimary, ), ), const SizedBox(width: DesignTokens.space3), @@ -232,8 +232,8 @@ class ProfileSettingsTab extends StatelessWidget { width: 40, height: 4, decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(2), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.2), + borderRadius: DesignTokens.borderRadiusSm, ), ), const SizedBox(height: DesignTokens.space4), @@ -260,7 +260,7 @@ class ProfileSettingsTab extends StatelessWidget { letterSpacing: 0.5, ), ), - const SizedBox(height: DesignTokens.space2), + SizedBox(height: DesignTokens.space2), Text( 'v0.88.5 · iOS 26 Liquid Glass', style: TextStyle( @@ -268,12 +268,12 @@ class ProfileSettingsTab extends StatelessWidget { color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Text( '让每一餐都充满爱与温度 ❤️', style: TextStyle( fontSize: DesignTokens.fontXs, - color: DesignTokens.primary.withValues(alpha: 0.6), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.6), ), ), ], diff --git a/lib/src/pages/profile/settings/personalization_page.dart b/lib/src/pages/profile/settings/personalization_page.dart index c6dfa84..976215e 100644 --- a/lib/src/pages/profile/settings/personalization_page.dart +++ b/lib/src/pages/profile/settings/personalization_page.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +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/personalization_controller.dart'; @@ -468,9 +468,7 @@ class PersonalizationPage extends StatelessWidget { Icon( CupertinoIcons.info_circle, size: 16, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), const SizedBox(width: 8), Text( @@ -533,7 +531,7 @@ class PersonalizationPage extends StatelessWidget { padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: themeService.backgroundColor.value.withValues(alpha: 0.02), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: themeService.textColor.value.withValues(alpha: 0.04), ), @@ -593,7 +591,7 @@ class PersonalizationPage extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/src/pages/profile/settings/preference_page.dart b/lib/src/pages/profile/settings/preference_page.dart index d8fcae9..d1cd6fa 100644 --- a/lib/src/pages/profile/settings/preference_page.dart +++ b/lib/src/pages/profile/settings/preference_page.dart @@ -12,6 +12,7 @@ import 'package:get/get.dart'; import 'package:mom_kitchen/src/controllers/user/preference_controller.dart'; import 'package:mom_kitchen/src/models/user_preference_model.dart'; import 'package:mom_kitchen/src/services/ui/theme_service.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; class PreferencePage extends StatelessWidget { const PreferencePage({super.key}); @@ -249,7 +250,7 @@ class PreferencePage extends StatelessWidget { ? CupertinoColors.systemRed.withValues(alpha: 0.12) : themeService.primaryColor.value.withValues(alpha: 0.12)) : themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: isSelected ? (isDestructive diff --git a/lib/src/pages/profile/settings/theme_demo_page.dart b/lib/src/pages/profile/settings/theme_demo_page.dart index 31aa2d9..2dffb80 100644 --- a/lib/src/pages/profile/settings/theme_demo_page.dart +++ b/lib/src/pages/profile/settings/theme_demo_page.dart @@ -4,6 +4,7 @@ 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'; import 'package:mom_kitchen/src/l10n/app_localizations.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; class ThemeDemoPage extends StatefulWidget { const ThemeDemoPage({super.key}); @@ -98,7 +99,7 @@ class _ThemeDemoPageState extends State { height: 40, decoration: BoxDecoration( color: _themeService.primaryColor.value, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: _themeService.textColor.value, width: 2, @@ -118,7 +119,7 @@ class _ThemeDemoPageState extends State { height: 40, decoration: BoxDecoration( color: _themeService.secondaryColor.value, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: _themeService.textColor.value, width: 2, @@ -285,7 +286,7 @@ class _ThemeDemoPageState extends State { color: isSelected ? _themeService.primaryColor.value : _themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( color: isSelected ? _themeService.primaryColor.value @@ -346,7 +347,7 @@ class _ThemeDemoPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: _themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, border: Border.all( color: _themeService.textColor.value.withValues(alpha: 0.3), ), @@ -382,7 +383,7 @@ class _ThemeDemoPageState extends State { padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: _themeService.secondaryColor.value.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all(color: _themeService.secondaryColor.value, width: 1), ), child: Column( @@ -414,7 +415,7 @@ class _ThemeDemoPageState extends State { ), decoration: BoxDecoration( color: _themeService.primaryColor.value, - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Text( 'Animated Button', @@ -438,7 +439,7 @@ class _ThemeDemoPageState extends State { padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, border: Border.all( color: _themeService.textColor.value.withValues(alpha: 0.1), ), @@ -450,7 +451,7 @@ class _ThemeDemoPageState extends State { height: 40, decoration: BoxDecoration( color: _themeService.primaryColor.value.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: Icon( [ @@ -490,7 +491,7 @@ class _ThemeDemoPageState extends State { height: 4, decoration: BoxDecoration( color: _themeService.textColor.value.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(2), + borderRadius: DesignTokens.borderRadiusSm, ), ), Expanded( @@ -533,7 +534,7 @@ class _ThemeDemoPageState extends State { margin: const EdgeInsets.all(4), decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), ), ); @@ -563,7 +564,7 @@ class _ThemeDemoPageState extends State { height: 4, decoration: BoxDecoration( color: _themeService.textColor.value.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(2), + borderRadius: DesignTokens.borderRadiusSm, ), ), Expanded( diff --git a/lib/src/pages/profile/shopping_list_page.dart b/lib/src/pages/profile/shopping_list_page.dart index 0442e02..16fe962 100644 --- a/lib/src/pages/profile/shopping_list_page.dart +++ b/lib/src/pages/profile/shopping_list_page.dart @@ -1,4 +1,4 @@ -// 2026-04-09 | ShoppingListPage | 购物清单页面 | iOS26风格分类展示+勾选+清空已购 +// 2026-04-09 | ShoppingListPage | 购物清单页面 | iOS26风格分类展示+勾选+清空已购 // 2026-04-09 | 初始创建,支持分类筛选/添加/删除/勾选/清空已购功能 import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show Colors; @@ -43,12 +43,12 @@ class _ShoppingListPageState extends State { children: [ GestureDetector( onTap: _showAddSheet, - child: const Text( + child: Text( '➕ 添加', style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w600, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -100,7 +100,7 @@ class _ShoppingListPageState extends State { style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w700, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -185,19 +185,19 @@ class _ShoppingListPageState extends State { : CupertinoIcons.eye_slash, size: 14, color: _ctrl.showChecked.value - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : isDark ? DarkDesignTokens.text2 : DesignTokens.text2, ), - const SizedBox(width: 4), + SizedBox(width: 4), Text( _ctrl.showChecked.value ? '显示已购' : '隐藏已购', style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w500, color: _ctrl.showChecked.value - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : isDark ? DarkDesignTokens.text2 : DesignTokens.text2, @@ -233,17 +233,17 @@ class _ShoppingListPageState extends State { Widget _buildFilterChip(bool isDark, String label, String value) { final isSelected = _ctrl.filterCategory.value == value; return Padding( - padding: const EdgeInsets.only(left: DesignTokens.space2), + padding: EdgeInsets.only(left: DesignTokens.space2), child: GestureDetector( onTap: () => _ctrl.setFilter(value), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: isSelected - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : isDark ? DarkDesignTokens.segmentedBg : DesignTokens.text3.withValues(alpha: 0.08), @@ -335,7 +335,7 @@ class _ShoppingListPageState extends State { ), const SizedBox(width: DesignTokens.space2), Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: 2, ), @@ -345,10 +345,10 @@ class _ShoppingListPageState extends State { ), child: Text( '${items.length}', - style: const TextStyle( + style: TextStyle( fontSize: DesignTokens.fontXs, fontWeight: FontWeight.w600, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -370,7 +370,7 @@ class _ShoppingListPageState extends State { decoration: BoxDecoration( color: item.isChecked ? DesignTokens.green.withValues(alpha: 0.08) - : Colors.transparent, + : const Color(0x00000000), borderRadius: DesignTokens.borderRadiusSm, ), child: Row( @@ -389,7 +389,7 @@ class _ShoppingListPageState extends State { shape: BoxShape.circle, color: item.isChecked ? DesignTokens.green - : Colors.transparent, + : const Color(0x00000000), border: Border.all( color: item.isChecked ? DesignTokens.green @@ -659,7 +659,7 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> { color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, ), ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Expanded( child: GridView.count( crossAxisCount: 4, @@ -673,15 +673,15 @@ class _AddShoppingItemSheetState extends State<_AddShoppingItemSheet> { child: Container( decoration: BoxDecoration( color: isSelected - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : isDark ? DarkDesignTokens.segmentedBg : DesignTokens.text3.withValues(alpha: 0.08), borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isSelected - ? DesignTokens.primary - : Colors.transparent, + ? DesignTokens.dynamicPrimary + : const Color(0x00000000), width: 1.5, ), ), diff --git a/lib/src/pages/tools/allergen_checker_page.dart b/lib/src/pages/tools/allergen_checker_page.dart index cd17316..f51eccf 100644 --- a/lib/src/pages/tools/allergen_checker_page.dart +++ b/lib/src/pages/tools/allergen_checker_page.dart @@ -320,7 +320,7 @@ class _AllergenCheckerPageState extends State { ), decoration: BoxDecoration( color: DesignTokens.red.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( a.toString(), @@ -341,7 +341,7 @@ class _AllergenCheckerPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: DesignTokens.orange.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( allergenTypes.first.toString(), diff --git a/lib/src/pages/tools/cooking_note_page.dart b/lib/src/pages/tools/cooking_note_page.dart index 7f51018..0182f81 100644 --- a/lib/src/pages/tools/cooking_note_page.dart +++ b/lib/src/pages/tools/cooking_note_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: cooking_note_page.dart * 名称: 烹饪笔记页面 * 作用: 按菜谱关联的个人笔记,支持增删改查、标签关键字、快捷输入 @@ -132,9 +132,7 @@ class _CookingNotePageState extends State { onPressed: () => _showAddDialog(isDark), child: Icon( CupertinoIcons.add, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, size: 28, ), ), @@ -311,15 +309,13 @@ class _CookingNotePageState extends State { children: note.tags .map( (tag) => Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: 2, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, ), @@ -327,9 +323,7 @@ class _CookingNotePageState extends State { tag, style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -462,22 +456,18 @@ class _CookingNotePageState extends State { setDialogState(() {}); }, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, border: Border.all( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.3), ), ), @@ -485,9 +475,7 @@ class _CookingNotePageState extends State { field['label'] ?? '', style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/pages/tools/cooking_timer_page.dart b/lib/src/pages/tools/cooking_timer_page.dart index 760e374..4fc9f36 100644 --- a/lib/src/pages/tools/cooking_timer_page.dart +++ b/lib/src/pages/tools/cooking_timer_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: cooking_timer_page.dart * 名称: 烹饪计时器页面 * 作用: iOS 26 风格的多步骤烹饪计时器 @@ -210,7 +210,7 @@ class _CookingTimerPageState extends State { final isActive = index == _currentStepIndex; final isCompleted = index < _currentStepIndex; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), + padding: EdgeInsets.symmetric(horizontal: 4), child: Container( width: isActive ? 24 : 8, height: 8, @@ -218,7 +218,7 @@ class _CookingTimerPageState extends State { color: isCompleted ? DesignTokens.green : (isActive - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3)), @@ -247,7 +247,7 @@ class _CookingTimerPageState extends State { color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), - const SizedBox(height: DesignTokens.space4), + SizedBox(height: DesignTokens.space4), Stack( alignment: Alignment.center, children: [ @@ -257,7 +257,7 @@ class _CookingTimerPageState extends State { child: CustomPaint( painter: _TimerRingPainter( progress: progress, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, backgroundColor: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, @@ -315,7 +315,7 @@ class _CookingTimerPageState extends State { }) { final isEnabled = onTap != null; final bgColor = isPrimary - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.card : DesignTokens.card); final fgColor = isPrimary ? CupertinoColors.white @@ -372,15 +372,15 @@ class _CookingTimerPageState extends State { }); }, child: Container( - margin: const EdgeInsets.only(bottom: DesignTokens.space2), - padding: const EdgeInsets.all(DesignTokens.space3), + margin: EdgeInsets.only(bottom: DesignTokens.space2), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isActive ? DesignTokens.primaryLight : (isDark ? DarkDesignTokens.card : DesignTokens.card), borderRadius: DesignTokens.borderRadiusLg, border: isActive - ? Border.all(color: DesignTokens.primary, width: 2) + ? Border.all(color: DesignTokens.dynamicPrimary, width: 2) : null, ), child: Row( @@ -392,7 +392,7 @@ class _CookingTimerPageState extends State { color: isCompleted ? DesignTokens.green : (isActive - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3)), @@ -554,20 +554,16 @@ class _AddStepDialogState extends State<_AddStepDialog> { }); }, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( - color: (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.2), ), ), @@ -575,9 +571,7 @@ class _AddStepDialogState extends State<_AddStepDialog> { '${preset.key} ${preset.value}min', style: TextStyle( fontSize: 12, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w500, ), ), diff --git a/lib/src/pages/tools/eating_times_page.dart b/lib/src/pages/tools/eating_times_page.dart index 96d3eca..89e5f05 100644 --- a/lib/src/pages/tools/eating_times_page.dart +++ b/lib/src/pages/tools/eating_times_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: eating_times_page.dart * 名称: 用餐时段推荐页面 * 作用: 展示API提供的用餐时段分类数据,支持按时段浏览菜谱 @@ -191,18 +191,18 @@ class _EatingTimesPageState extends State { Widget _buildContent(bool isDark) { return ListView( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space4, vertical: DesignTokens.space2, ), children: [ Container( - padding: const EdgeInsets.all(DesignTokens.space4), + padding: EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: DesignTokens.borderRadiusLg, border: Border.all( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.15), ), ), @@ -340,21 +340,19 @@ class _EatingTimesPageState extends State { ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: - (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( '浏览', style: TextStyle( fontSize: DesignTokens.fontXs, fontWeight: FontWeight.w500, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -530,27 +528,23 @@ class EatingTimeRecipesPage extends StatelessWidget { ], if (recipe.categoryName != null && recipe.categoryName!.isNotEmpty) ...[ - const SizedBox(height: 4), + SizedBox(height: 4), Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( recipe.categoryName!, style: TextStyle( fontSize: 10, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/pages/tools/ingredient_detail_page.dart b/lib/src/pages/tools/ingredient_detail_page.dart index 60193b8..1d99488 100644 --- a/lib/src/pages/tools/ingredient_detail_page.dart +++ b/lib/src/pages/tools/ingredient_detail_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: ingredient_detail_page.dart * 名称: 食材详情查询页面 * 作用: 查询食材营养信息与选购指南 @@ -377,7 +377,7 @@ class _IngredientDetailPageState extends State { }); }, child: Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: DesignTokens.borderRadiusMd, @@ -390,7 +390,7 @@ class _IngredientDetailPageState extends State { height: 48, decoration: BoxDecoration( color: - (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusMd, ), @@ -426,7 +426,7 @@ class _IngredientDetailPageState extends State { ), decoration: BoxDecoration( color: DesignTokens.green.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( category, @@ -537,13 +537,9 @@ class _IngredientDetailPageState extends State { const SizedBox(height: DesignTokens.space3), CupertinoButton( onPressed: () { - setState(() { - _selectedIngredient = null; - _ingredientDetail = null; - _passedDetail = null; - }); + Get.until((route) => route.isFirst); }, - child: const Text('返回列表'), + child: const Text('返回首页'), ), const SizedBox(height: DesignTokens.space4), ], @@ -558,17 +554,17 @@ class _IngredientDetailPageState extends State { (detail.effect?.isNotEmpty ?? false) || detail.allergen.isNotEmpty; - if (!hasContent) return const SizedBox(); + if (!hasContent) return SizedBox(); return Container( - padding: const EdgeInsets.all(DesignTokens.space3), - margin: const EdgeInsets.only(bottom: DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), + margin: EdgeInsets.only(bottom: DesignTokens.space3), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.08), borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.2), ), ), @@ -738,14 +734,14 @@ class _IngredientDetailPageState extends State { Widget _buildApiDetailCard(IngredientModel detail, bool isDark) { return Container( - padding: const EdgeInsets.all(DesignTokens.space3), - margin: const EdgeInsets.only(bottom: DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), + margin: EdgeInsets.only(bottom: DesignTokens.space3), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.08), borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.2), ), ), @@ -920,7 +916,7 @@ class _IngredientDetailPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: DesignTokens.green.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( category, @@ -930,7 +926,7 @@ class _IngredientDetailPageState extends State { ), ), ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -939,9 +935,7 @@ class _IngredientDetailPageState extends State { style: TextStyle( fontSize: 36, fontWeight: FontWeight.w700, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(width: 4), @@ -1036,7 +1030,7 @@ class _IngredientDetailPageState extends State { '🌾 纤维', '${nutrition.fiber.toInt()}', 'g', - const Color(0xFF8E8E93), + DesignTokens.text2, isDark, ), ), @@ -1250,19 +1244,19 @@ class _IngredientDetailPageState extends State { ), ], ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Wrap( spacing: DesignTokens.space2, runSpacing: DesignTokens.space2, children: nutrition.keyNutrients.map((nutrient) { return Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: - (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusFull, ), @@ -1271,9 +1265,7 @@ class _IngredientDetailPageState extends State { style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w500, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ); @@ -1444,12 +1436,12 @@ class _IngredientDetailPageState extends State { ), ], ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.08), borderRadius: DesignTokens.borderRadiusMd, ), diff --git a/lib/src/pages/tools/meal_planner_page.dart b/lib/src/pages/tools/meal_planner_page.dart index b056f83..3f836f5 100644 --- a/lib/src/pages/tools/meal_planner_page.dart +++ b/lib/src/pages/tools/meal_planner_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: meal_planner_page.dart * 名称: 每周菜单规划页面 * 作用: 规划一周饮食菜单,支持早中晚三餐,数据持久化 @@ -184,10 +184,10 @@ class _MealPlannerPageState extends State { }, child: Container( width: 56, - margin: const EdgeInsets.symmetric(horizontal: 4), + margin: EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: isSelected - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.card : DesignTokens.card), borderRadius: DesignTokens.borderRadiusMd, border: Border.all( @@ -282,17 +282,15 @@ class _MealPlannerPageState extends State { color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), - const Spacer(), + Spacer(), CupertinoButton( padding: EdgeInsets.zero, - minimumSize: const Size(32, 32), + minimumSize: Size(32, 32), onPressed: () => _showMealOptions(mealType), child: Icon( CupertinoIcons.plus_circle, size: 24, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -331,10 +329,10 @@ class _MealPlannerPageState extends State { else Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: - (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusMd, ), @@ -415,7 +413,7 @@ class _MealPlannerPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: DesignTokens.green, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: const Text( '✓ 完成', diff --git a/lib/src/pages/tools/meal_time_recommend_page.dart b/lib/src/pages/tools/meal_time_recommend_page.dart index 5b77b00..e483624 100644 --- a/lib/src/pages/tools/meal_time_recommend_page.dart +++ b/lib/src/pages/tools/meal_time_recommend_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: meal_time_recommend_page.dart * 名称: 用餐时段推荐页面 * 作用: 根据时间推荐早餐/午餐/晚餐菜谱 @@ -206,20 +206,18 @@ class _MealTimeRecommendPageState extends State { return GestureDetector( onTap: () => _selectMealTime(mealTime), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space4, vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: isSelected - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.card : DesignTokens.card), borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isSelected - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) .withValues(alpha: 0.2), ), @@ -260,7 +258,7 @@ class _MealTimeRecommendPageState extends State { color: isSelected ? CupertinoColors.white.withValues(alpha: 0.3) : DesignTokens.green.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( '当前', diff --git a/lib/src/pages/tools/serving_scaler_page.dart b/lib/src/pages/tools/serving_scaler_page.dart index c1f99bb..75cb095 100644 --- a/lib/src/pages/tools/serving_scaler_page.dart +++ b/lib/src/pages/tools/serving_scaler_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: serving_scaler_page.dart * 名称: 份量缩放+单位换算工具页面 * 作用: 实现菜谱份量缩放功能和食材单位换算 @@ -116,9 +116,7 @@ class _ServingScalerPageState extends State { onPressed: _showAddIngredientDialog, child: Icon( CupertinoIcons.add, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, size: 28, ), ) @@ -243,7 +241,7 @@ class _ServingScalerPageState extends State { ), ), ), - const SizedBox(width: DesignTokens.space2), + SizedBox(width: DesignTokens.space2), _buildUnitPicker( _convertFromUnit, (v) => setState(() => _convertFromUnit = v), @@ -251,23 +249,19 @@ class _ServingScalerPageState extends State { ), ], ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Icon( CupertinoIcons.arrow_down, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, size: 28, ), - const SizedBox(height: DesignTokens.space3), + SizedBox(height: DesignTokens.space3), Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusMd, ), @@ -279,9 +273,7 @@ class _ServingScalerPageState extends State { style: TextStyle( fontSize: DesignTokens.fontXxl, fontWeight: FontWeight.bold, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(height: 4), @@ -322,11 +314,11 @@ class _ServingScalerPageState extends State { }); }, child: Container( - margin: const EdgeInsets.symmetric(horizontal: 2), - padding: const EdgeInsets.symmetric(vertical: 8), + margin: EdgeInsets.symmetric(horizontal: 2), + padding: EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: isSelected - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.card : DesignTokens.card), borderRadius: DesignTokens.borderRadiusMd, ), @@ -369,7 +361,7 @@ class _ServingScalerPageState extends State { unit, style: TextStyle( color: unit == current - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.text1 : DesignTokens.text1), @@ -505,15 +497,15 @@ class _ServingScalerPageState extends State { color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), - const Spacer(), + Spacer(), CupertinoButton( padding: EdgeInsets.zero, - minimumSize: const Size(36, 36), + minimumSize: Size(36, 36), onPressed: value > 1 ? () => onValueChanged(value - 1) : null, child: Icon( CupertinoIcons.minus_circle, color: value > 1 - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3), ), ), @@ -532,12 +524,12 @@ class _ServingScalerPageState extends State { ), CupertinoButton( padding: EdgeInsets.zero, - minimumSize: const Size(36, 36), + minimumSize: Size(36, 36), onPressed: value < 20 ? () => onValueChanged(value + 1) : null, child: Icon( CupertinoIcons.plus_circle, color: value < 20 - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3), ), ), @@ -551,9 +543,9 @@ class _ServingScalerPageState extends State { ? _targetServings / _originalServings : 1.0; return Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusMd, ), @@ -573,7 +565,7 @@ class _ServingScalerPageState extends State { style: TextStyle( fontSize: DesignTokens.fontXl, fontWeight: FontWeight.bold, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -707,9 +699,7 @@ class _ServingScalerPageState extends State { style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.bold, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], diff --git a/lib/src/pages/tools/tools_center_page.dart b/lib/src/pages/tools/tools_center_page.dart index c214de1..1eb0a18 100644 --- a/lib/src/pages/tools/tools_center_page.dart +++ b/lib/src/pages/tools/tools_center_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: tools_center_page.dart * 名称: 工具中心页面 * 作用: 展示所有工具,支持分类筛选和搜索 @@ -185,13 +185,13 @@ class _ToolsCenterPageState extends State 'ToolsCenterPage: _buildTagsSection called, tags count: ${_tags.length}', ); return Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.08), - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.08), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), border: Border( top: BorderSide( - color: DesignTokens.primary.withValues(alpha: 0.2), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.2), width: 1, ), ), @@ -201,20 +201,20 @@ class _ToolsCenterPageState extends State mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.only( + padding: EdgeInsets.only( left: DesignTokens.space1, bottom: DesignTokens.space2, ), child: Row( children: [ - const Text('🏷️', style: TextStyle(fontSize: 18)), - const SizedBox(width: DesignTokens.space2), + Text('🏷️', style: TextStyle(fontSize: 18)), + SizedBox(width: DesignTokens.space2), Text( '口味 & 工艺', style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w700, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const Spacer(), @@ -251,22 +251,22 @@ class _ToolsCenterPageState extends State return GestureDetector( onTap: () => _navigateToTagRecipes(tag), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(20), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusLg, border: Border.all( - color: DesignTokens.primary.withValues(alpha: 0.2), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.2), ), ), child: Text( tag.name, style: TextStyle( fontSize: DesignTokens.fontSm, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w500, ), ), @@ -301,11 +301,11 @@ class _ToolsCenterPageState extends State begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - DesignTokens.primary.withValues(alpha: 0.2), + DesignTokens.dynamicPrimary.withValues(alpha: 0.2), DesignTokens.secondary.withValues(alpha: 0.2), ], ), - borderRadius: BorderRadius.circular(14), + borderRadius: DesignTokens.borderRadiusMd, ), child: const Center( child: Text('🛠️', style: TextStyle(fontSize: 22)), @@ -337,12 +337,12 @@ class _ToolsCenterPageState extends State Obx(() { final count = _controller!.tools.length; return Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.1), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, ), child: Text( @@ -350,7 +350,7 @@ class _ToolsCenterPageState extends State style: TextStyle( fontSize: DesignTokens.fontXs, fontWeight: FontWeight.w600, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ); @@ -369,7 +369,7 @@ class _ToolsCenterPageState extends State color: isDark ? DarkDesignTokens.card : DesignTokens.text3.withValues(alpha: 0.06), - borderRadius: BorderRadius.circular(14), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isDark ? DarkDesignTokens.glassBorder @@ -453,19 +453,19 @@ class _ToolsCenterPageState extends State 'data': { 'name': '数据查询', 'icon': '📊', - 'gradient': [const Color(0xFF3498DB), const Color(0xFF2980B9)], + 'gradient': [DesignTokens.blue, Color(0xFF2980B9)], }, 'planning': { 'name': '规划管理', 'icon': '📅', - 'gradient': [const Color(0xFF9B59B6), const Color(0xFF8E44AD)], + 'gradient': [Color(0xFF9B59B6), Color(0xFF8E44AD)], }, }; return categoryMap[categoryId] ?? { 'name': categoryId, 'icon': '📦', - 'gradient': [DesignTokens.primary, DesignTokens.secondary], + 'gradient': [DesignTokens.dynamicPrimary, DesignTokens.secondary], }; } @@ -478,7 +478,7 @@ class _ToolsCenterPageState extends State return Container( decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, boxShadow: [ BoxShadow( color: isDark @@ -539,7 +539,7 @@ class _ToolsCenterPageState extends State end: Alignment.bottomRight, colors: gradientColors, ), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, boxShadow: [ BoxShadow( color: gradientColors[0].withValues(alpha: 0.4), @@ -597,7 +597,7 @@ class _ToolsCenterPageState extends State color: isDark ? DarkDesignTokens.glass : DesignTokens.text3.withValues(alpha: 0.04), - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isDark ? DarkDesignTokens.glassBorder @@ -621,7 +621,7 @@ class _ToolsCenterPageState extends State gradientColors[1].withValues(alpha: 0.1), ], ), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Center( child: Text(tool.icon, style: const TextStyle(fontSize: 22)), @@ -644,7 +644,7 @@ class _ToolsCenterPageState extends State maxLines: 1, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 2), + SizedBox(height: 2), Row( children: [ Container( @@ -653,7 +653,7 @@ class _ToolsCenterPageState extends State decoration: BoxDecoration( color: tool.needsNetwork ? DesignTokens.green - : DesignTokens.primary, + : DesignTokens.dynamicPrimary, shape: BoxShape.circle, ), ), @@ -686,7 +686,7 @@ class _ToolsCenterPageState extends State end: Alignment.bottomRight, colors: gradientColors, ), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, boxShadow: [ BoxShadow( color: gradientColors[0].withValues(alpha: 0.3), @@ -730,8 +730,8 @@ class _ToolsCenterPageState extends State width: 80, height: 80, decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(24), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusXl, ), child: const Center( child: Text('🔍', style: TextStyle(fontSize: 36)), @@ -820,7 +820,7 @@ class _ToolDetailPage extends StatelessWidget { color: isDark ? DarkDesignTokens.glass : DesignTokens.text3.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Icon( CupertinoIcons.back, @@ -847,18 +847,18 @@ class _ToolDetailPage extends StatelessWidget { Widget _buildHeroCard(bool isDark) { return Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space5), + padding: EdgeInsets.all(DesignTokens.space5), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - DesignTokens.primary.withValues(alpha: 0.15), + DesignTokens.dynamicPrimary.withValues(alpha: 0.15), DesignTokens.secondary.withValues(alpha: 0.08), ], ), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: DesignTokens.primary.withValues(alpha: 0.1)), + borderRadius: DesignTokens.borderRadiusXl, + border: Border.all(color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1)), ), child: Column( children: [ @@ -866,8 +866,8 @@ class _ToolDetailPage extends StatelessWidget { width: 80, height: 80, decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(24), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.15), + borderRadius: DesignTokens.borderRadiusXl, ), child: Center( child: Text(tool.icon, style: const TextStyle(fontSize: 40)), @@ -894,17 +894,17 @@ class _ToolDetailPage extends StatelessWidget { ), ), ], - const SizedBox(height: DesignTokens.space4), + SizedBox(height: DesignTokens.space4), Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: tool.needsNetwork ? DesignTokens.green.withValues(alpha: 0.15) - : DesignTokens.primary.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(20), + : DesignTokens.dynamicPrimary.withValues(alpha: 0.15), + borderRadius: DesignTokens.borderRadiusLg, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -916,9 +916,9 @@ class _ToolDetailPage extends StatelessWidget { size: 14, color: tool.needsNetwork ? DesignTokens.green - : DesignTokens.primary, + : DesignTokens.dynamicPrimary, ), - const SizedBox(width: 6), + SizedBox(width: 6), Text( tool.needsNetwork ? '需要网络连接' : '本地运行', style: TextStyle( @@ -926,7 +926,7 @@ class _ToolDetailPage extends StatelessWidget { fontWeight: FontWeight.w500, color: tool.needsNetwork ? DesignTokens.green - : DesignTokens.primary, + : DesignTokens.dynamicPrimary, ), ), ], @@ -967,7 +967,7 @@ class _ToolDetailPage extends StatelessWidget { padding: const EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isDark ? DarkDesignTokens.glassBorder @@ -980,10 +980,10 @@ class _ToolDetailPage extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusMd, ), - child: Icon(icon, size: 20, color: DesignTokens.primary), + child: Icon(icon, size: 20, color: DesignTokens.dynamicPrimary), ), const SizedBox(width: DesignTokens.space3), Expanded( @@ -1029,9 +1029,9 @@ class _ToolDetailPage extends StatelessWidget { child: SizedBox( width: double.infinity, child: CupertinoButton( - padding: const EdgeInsets.symmetric(vertical: DesignTokens.space3), - borderRadius: BorderRadius.circular(14), - color: DesignTokens.primary, + padding: EdgeInsets.symmetric(vertical: DesignTokens.space3), + borderRadius: DesignTokens.borderRadiusMd, + color: DesignTokens.dynamicPrimary, onPressed: () { Get.toNamed(tool.route); }, diff --git a/lib/src/pages/tools/unit_converter_page.dart b/lib/src/pages/tools/unit_converter_page.dart index ed5f56a..49dabff 100644 --- a/lib/src/pages/tools/unit_converter_page.dart +++ b/lib/src/pages/tools/unit_converter_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: unit_converter_page.dart * 名称: 用量换算工具页面 * 作用: iOS 26 风格的烹饪用量换算工具 @@ -330,15 +330,15 @@ class _UnitConverterPageState extends State { child: GestureDetector( onTap: _swapUnits, child: Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: DesignTokens.primaryLight, borderRadius: DesignTokens.borderRadiusFull, ), - child: const Icon( + child: Icon( CupertinoIcons.arrow_up_arrow_down, size: 24, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -362,7 +362,7 @@ class _UnitConverterPageState extends State { const SizedBox(height: DesignTokens.space3), Container( width: double.infinity, - padding: const EdgeInsets.all(DesignTokens.space4), + padding: EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( color: DesignTokens.primaryLight, borderRadius: DesignTokens.borderRadiusLg, @@ -371,10 +371,10 @@ class _UnitConverterPageState extends State { children: [ Text( _result == 0 ? '0' : _result.toStringAsFixed(4), - style: const TextStyle( + style: TextStyle( fontSize: 32, fontWeight: FontWeight.w800, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), const SizedBox(height: DesignTokens.space1), diff --git a/lib/src/pages/tools/weekly_menu_planner_page.dart b/lib/src/pages/tools/weekly_menu_planner_page.dart index 6935740..11f29fe 100644 --- a/lib/src/pages/tools/weekly_menu_planner_page.dart +++ b/lib/src/pages/tools/weekly_menu_planner_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: weekly_menu_planner_page.dart * 名称: 每周菜单规划页面 * 作用: 日历视图选择日期,每日三餐分配菜谱,自动生成购物清单 @@ -63,7 +63,7 @@ class _WeeklyMenuPlannerPageState extends State { onPressed: _showShoppingList, child: Icon( CupertinoIcons.cart, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), backgroundColor: isDark @@ -138,7 +138,7 @@ class _WeeklyMenuPlannerPageState extends State { child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 7, - separatorBuilder: (_, __) => const SizedBox(width: DesignTokens.space2), + separatorBuilder: (_, __) => SizedBox(width: DesignTokens.space2), itemBuilder: (context, index) { final date = startDate.add(Duration(days: index)); final dateKey = _controller.selectedDateKey.value; @@ -152,7 +152,7 @@ class _WeeklyMenuPlannerPageState extends State { width: 56, decoration: BoxDecoration( color: isSelected - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.card : DesignTokens.card), borderRadius: BorderRadius.circular(DesignTokens.radiusMd), border: Border.all( @@ -187,14 +187,14 @@ class _WeeklyMenuPlannerPageState extends State { : (isDark ? DarkDesignTokens.text1 : DesignTokens.text1), ), ), - const SizedBox(height: 4), + SizedBox(height: 4), if (hasMeals) Icon( CupertinoIcons.checkmark_circle_fill, size: 16, color: isSelected ? CupertinoColors.white - : (isDark ? DarkDesignTokens.primary : DesignTokens.primary), + : (DesignTokens.dynamicPrimary), ), ], ), @@ -291,12 +291,12 @@ class _WeeklyMenuPlannerPageState extends State { else CupertinoButton( padding: EdgeInsets.zero, - minimumSize: const Size(32, 32), + minimumSize: Size(32, 32), onPressed: () => _showRecipePicker(mealType), child: Icon( CupertinoIcons.add_circled, size: 28, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -311,7 +311,7 @@ class _WeeklyMenuPlannerPageState extends State { children: [ if (meal.cover != null && meal.cover!.isNotEmpty) ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, child: Image.network( meal.cover!, width: 60, @@ -323,9 +323,9 @@ class _WeeklyMenuPlannerPageState extends State { height: 60, decoration: BoxDecoration( color: DesignTokens.dynamicPrimaryLight, - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), - child: const Icon(CupertinoIcons.photo), + child: Icon(CupertinoIcons.photo), ); }, ), @@ -336,7 +336,7 @@ class _WeeklyMenuPlannerPageState extends State { height: 60, decoration: BoxDecoration( color: DesignTokens.dynamicPrimaryLight, - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Icon(CupertinoIcons.photo), ), @@ -591,7 +591,7 @@ class _WeeklyMenuPlannerPageState extends State { children: [ CupertinoButton( padding: EdgeInsets.zero, - minimumSize: const Size(32, 32), + minimumSize: Size(32, 32), onPressed: () { _controller.toggleIngredientChecked( _controller.selectedDateKey.value, @@ -603,7 +603,7 @@ class _WeeklyMenuPlannerPageState extends State { ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.circle, color: item.checked - ? (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + ? (DesignTokens.dynamicPrimary) : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3), ), ), diff --git a/lib/src/repositories/recipe_repository.dart b/lib/src/repositories/recipe_repository.dart index 70ae5f2..18155aa 100644 --- a/lib/src/repositories/recipe_repository.dart +++ b/lib/src/repositories/recipe_repository.dart @@ -3,6 +3,7 @@ // 2026-04-10 | 新增 fetchFeedRecipes 方法,首页通过 Repository 层获取推荐数据 // 2026-04-11 | 修复类型转换问题,添加安全的 Map 转换辅助方法 // 2026-04-13 | 新增菜品详情本地缓存,优先读取缓存,支持图片预缓存 +// 2026-04-13 | 新增 fetchTasteTags/fetchCookingTags/filterRecipesByTag 方法 import 'package:flutter/foundation.dart'; import 'package:mom_kitchen/src/config/api_config.dart'; import 'package:mom_kitchen/src/models/api_response.dart'; @@ -595,4 +596,89 @@ class RecipeRepository { } return apiResponse.data!; } + + // ─── 筛选接口:菜谱大类 ─── + + Future> fetchMainCategories() async { + final response = await _api.get( + ApiConfig.filter, + queryParameters: {'act': 'recipe_main_categories'}, + ); + if (response.data == null) return []; + final raw = response.data as Map; + if (raw['code'] != 200) return []; + final data = raw['data']; + if (data is Map && data.containsKey('list')) { + return (data['list'] as List) + .map((e) => CategoryModel.fromJson(e as Map)) + .toList(); + } + return []; + } + + // ─── 筛选接口:口味/工艺标签 ─── + + Future> fetchTasteTags({int limit = 50}) async { + final response = await _api.get( + ApiConfig.filter, + queryParameters: {'act': 'taste_tags', 'limit': limit}, + ); + if (response.data == null) return []; + final raw = response.data as Map; + if (raw['code'] != 200) return []; + final data = raw['data']; + if (data is Map && data.containsKey('list')) { + return (data['list'] as List) + .map((e) => TagModel.fromJson(e as Map)) + .toList(); + } + return []; + } + + Future> fetchCookingTags({int limit = 50}) async { + final response = await _api.get( + ApiConfig.filter, + queryParameters: {'act': 'cooking_tags', 'limit': limit}, + ); + if (response.data == null) return []; + final raw = response.data as Map; + if (raw['code'] != 200) return []; + final data = raw['data']; + if (data is Map && data.containsKey('list')) { + return (data['list'] as List) + .map((e) => TagModel.fromJson(e as Map)) + .toList(); + } + return []; + } + + Future> filterRecipesByTag({ + String? tasteName, + String? cookingName, + int? categoryId, + int page = 1, + int limit = 20, + }) async { + final params = { + 'act': 'filter_recipes', + 'page': page, + 'limit': limit, + }; + if (tasteName != null) params['taste_name'] = tasteName; + if (cookingName != null) params['cooking_name'] = cookingName; + if (categoryId != null) params['main_category'] = categoryId; + + final response = await _api.get(ApiConfig.filter, queryParameters: params); + final apiResponse = ApiResponse.fromJson( + response.data as Map, + (data) => PaginatedData.fromJson( + data as Map, + (e) => RecipeModel.fromJson(e), + ), + ); + if (!apiResponse.isSuccess || apiResponse.data == null) { + throw Exception(apiResponse.message); + } + return apiResponse.data!; + } } diff --git a/lib/src/services/ui/animation_service.dart b/lib/src/services/ui/animation_service.dart index 684791e..036a7cb 100644 --- a/lib/src/services/ui/animation_service.dart +++ b/lib/src/services/ui/animation_service.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:animations/animations.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; enum AnimationPreset { standard, fast, slow, smooth, bouncy, minimal, none } @@ -301,7 +302,7 @@ class PageTransitions { parent: const AlwaysStoppedAnimation(1.0), curve: config.curve, ), - fillColor: Colors.transparent, + fillColor: const Color(0x00000000), child: child, ); } @@ -335,7 +336,7 @@ class PageTransitions { curve: config.curve, ), transitionType: type, - fillColor: Colors.transparent, + fillColor: const Color(0x00000000), child: child, ); } @@ -368,7 +369,7 @@ class _AdaptivePageTransitionsBuilder extends PageTransitionsBuilder { animation: animation, secondaryAnimation: secondaryAnimation, transitionType: SharedAxisTransitionType.horizontal, - fillColor: Colors.transparent, + fillColor: const Color(0x00000000), child: child, ); } @@ -636,7 +637,7 @@ class _AnimatedCardState extends State elevation: config.enabled ? _elevationAnimation.value : widget.elevation, - borderRadius: widget.borderRadius ?? BorderRadius.circular(12), + borderRadius: widget.borderRadius ?? DesignTokens.borderRadiusMd, color: widget.backgroundColor, child: child, ), @@ -846,13 +847,13 @@ class OpenContainerWrapper extends StatelessWidget { : Duration.zero, openBuilder: openBuilder, closedElevation: closedElevation, - closedColor: closedColor ?? Colors.transparent, - openColor: openColor ?? Colors.transparent, + closedColor: closedColor ?? const Color(0x00000000), + openColor: openColor ?? const Color(0x00000000), openElevation: openElevation, closedShape: closedShape ?? RoundedRectangleBorder( - borderRadius: closedBorderRadius ?? BorderRadius.circular(12), + borderRadius: closedBorderRadius ?? DesignTokens.borderRadiusMd, ), closedBuilder: (context, action) => closedChild, ); diff --git a/lib/src/services/ui/theme_service.dart b/lib/src/services/ui/theme_service.dart index 88a4ff8..877725b 100644 --- a/lib/src/services/ui/theme_service.dart +++ b/lib/src/services/ui/theme_service.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show ThemeData, TextTheme, Colors, Brightness; import 'package:flutter/services.dart'; @@ -40,7 +40,7 @@ class DynamicTokens { Color get green => isDark ? DarkDesignTokens.green : DesignTokens.green; Color get segmentedBg => isDark ? DarkDesignTokens.segmentedBg : DesignTokens.segmentedBg; - Color get primary => isDark ? DarkDesignTokens.primary : DesignTokens.primary; + Color get primary => DesignTokens.dynamicPrimary; Color get secondary => isDark ? DarkDesignTokens.secondary : DesignTokens.secondary; } @@ -158,11 +158,11 @@ class ThemeService extends GetxController { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( - statusBarColor: Colors.transparent, + statusBarColor: const Color(0x00000000), statusBarIconBrightness: isDarkMode.value ? Brightness.light : Brightness.dark, - systemNavigationBarColor: Colors.transparent, + systemNavigationBarColor: const Color(0x00000000), systemNavigationBarIconBrightness: isDarkMode.value ? Brightness.light : Brightness.dark, @@ -375,7 +375,7 @@ class ThemeService extends GetxController { brightness: isDarkMode.value ? Brightness.dark : Brightness.light, primaryColor: primaryColor.value, scaffoldBackgroundColor: backgroundColor.value, - barBackgroundColor: Colors.transparent, + barBackgroundColor: const Color(0x00000000), textTheme: CupertinoTextThemeData( primaryColor: primaryColor.value, textStyle: TextStyle( diff --git a/lib/src/services/ui/toast_service.dart b/lib/src/services/ui/toast_service.dart index efde3f7..3c0fd39 100644 --- a/lib/src/services/ui/toast_service.dart +++ b/lib/src/services/ui/toast_service.dart @@ -119,7 +119,7 @@ class ToastService { padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: CupertinoColors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: Icon(icon, color: CupertinoColors.white, size: 20), ), diff --git a/lib/src/standards/page_validator.dart b/lib/src/standards/page_validator.dart index 1424735..a97992a 100644 --- a/lib/src/standards/page_validator.dart +++ b/lib/src/standards/page_validator.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mom_kitchen/src/standards/page_standards.dart'; import 'package:mom_kitchen/src/utils/app_logger.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; enum StandardCheck { themeColors('主题颜色', '检查是否使用主题色、次要色'), @@ -423,7 +424,7 @@ class PageValidationDebugPanel extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.black87, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/src/widgets/adaptive_widgets.dart b/lib/src/widgets/adaptive_widgets.dart index 67b8ff8..61c9491 100644 --- a/lib/src/widgets/adaptive_widgets.dart +++ b/lib/src/widgets/adaptive_widgets.dart @@ -4,6 +4,7 @@ import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:get/get.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/config/design_tokens.dart'; class AdaptiveDestination { final IconData icon; @@ -491,7 +492,7 @@ class ChatBubble extends StatelessWidget { BorderRadius _radius() { switch (style) { case MessageBubbleStyle.rounded: - return BorderRadius.circular(20); + return DesignTokens.borderRadiusLg; case MessageBubbleStyle.modern: return isMe ? const BorderRadius.only( @@ -507,7 +508,7 @@ class ChatBubble extends StatelessWidget { bottomRight: Radius.circular(16), ); case MessageBubbleStyle.classic: - return BorderRadius.circular(8); + return DesignTokens.borderRadiusSm; } } diff --git a/lib/src/widgets/base/app_page_scaffold.dart b/lib/src/widgets/base/app_page_scaffold.dart index 3efca2e..d5cea37 100644 --- a/lib/src/widgets/base/app_page_scaffold.dart +++ b/lib/src/widgets/base/app_page_scaffold.dart @@ -259,7 +259,7 @@ class AppStatItem extends StatelessWidget { final IconData? icon; final String? emoji; - const AppStatItem({ + AppStatItem({ super.key, required this.label, required this.value, @@ -272,7 +272,7 @@ class AppStatItem extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; - final effectiveColor = color ?? DesignTokens.primary; + final effectiveColor = color ?? DesignTokens.dynamicPrimary; return Column( mainAxisSize: MainAxisSize.min, @@ -333,13 +333,13 @@ class AppProgressBar extends StatelessWidget { final double height; final double borderRadius; - const AppProgressBar({ + AppProgressBar({ super.key, required this.progress, - this.color = DesignTokens.primary, + Color? color, this.height = 8, this.borderRadius = DesignTokens.radiusFull, - }); + }) : color = color ?? DesignTokens.dynamicPrimary; @override Widget build(BuildContext context) { diff --git a/lib/src/widgets/base/standard_button.dart b/lib/src/widgets/base/standard_button.dart index 72b3598..128b5fc 100644 --- a/lib/src/widgets/base/standard_button.dart +++ b/lib/src/widgets/base/standard_button.dart @@ -9,6 +9,7 @@ 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/config/design_tokens.dart'; enum StandardButtonType { primary, secondary, danger, success } @@ -55,11 +56,11 @@ class StandardButton extends StatelessWidget { textColor = theme.primaryColor.value; break; case StandardButtonType.danger: - backgroundColor = const Color(0xFFF44336); + backgroundColor = DesignTokens.red; textColor = CupertinoColors.white; break; case StandardButtonType.success: - backgroundColor = const Color(0xFF4CAF50); + backgroundColor = DesignTokens.green; textColor = CupertinoColors.white; break; } @@ -71,7 +72,7 @@ class StandardButton extends StatelessWidget { height: height, decoration: BoxDecoration( color: backgroundColor, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: isLoading ? Center( diff --git a/lib/src/widgets/base/tap_liquid_glass_nav.dart b/lib/src/widgets/base/tap_liquid_glass_nav.dart index 7125351..9fe495d 100644 --- a/lib/src/widgets/base/tap_liquid_glass_nav.dart +++ b/lib/src/widgets/base/tap_liquid_glass_nav.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'dart:ui'; import 'package:get/get.dart'; import 'package:mom_kitchen/src/services/ui/theme_service.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; class TapLiquidGlassNavigation extends StatelessWidget { final int currentIndex; @@ -37,14 +38,14 @@ class TapLiquidGlassNavigation extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 16, left: 28, right: 28), child: ClipRRect( - borderRadius: BorderRadius.circular(36), + borderRadius: DesignTokens.borderRadiusFull, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), child: Container( height: 64, decoration: BoxDecoration( color: bg, - borderRadius: BorderRadius.circular(36), + borderRadius: DesignTokens.borderRadiusFull, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), diff --git a/lib/src/widgets/charts_widgets.dart b/lib/src/widgets/charts_widgets.dart index a39fa07..5bf1554 100644 --- a/lib/src/widgets/charts_widgets.dart +++ b/lib/src/widgets/charts_widgets.dart @@ -9,14 +9,14 @@ class NutritionLineChart extends StatelessWidget { final Color lineColor; final Color goalLineColor; - const NutritionLineChart({ + NutritionLineChart({ super.key, required this.data, this.goalValue = 2000, this.isWeekly = true, - this.lineColor = DesignTokens.primary, + Color? lineColor, this.goalLineColor = DesignTokens.orange, - }); + }) : lineColor = lineColor ?? DesignTokens.dynamicPrimary; @override Widget build(BuildContext context) { @@ -474,7 +474,7 @@ class MealTypePieChart extends StatelessWidget { color: DesignTokens.green, radius: 28, title: _formatPercent(lunch / total), - titleStyle: const TextStyle( + titleStyle: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w700, color: CupertinoColors.white, @@ -482,7 +482,7 @@ class MealTypePieChart extends StatelessWidget { ), PieChartSectionData( value: dinner, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, radius: 28, title: _formatPercent(dinner / total), titleStyle: const TextStyle( @@ -514,7 +514,7 @@ class MealTypePieChart extends StatelessWidget { final items = [ _MealTypeLegendItem('🌅 早餐', breakfast, DesignTokens.orange), _MealTypeLegendItem('☀️ 午餐', lunch, DesignTokens.green), - _MealTypeLegendItem('🌙 晚餐', dinner, DesignTokens.primary), + _MealTypeLegendItem('🌙 晚餐', dinner, DesignTokens.dynamicPrimary), _MealTypeLegendItem('🍪 加餐', snack, DesignTokens.secondary), ]; diff --git a/lib/src/widgets/custom_widgets.dart b/lib/src/widgets/custom_widgets.dart index c68be7f..be08dfc 100644 --- a/lib/src/widgets/custom_widgets.dart +++ b/lib/src/widgets/custom_widgets.dart @@ -1,4 +1,4 @@ -import 'dart:math'; +import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; @@ -151,10 +151,10 @@ class _MiniCalendarState extends State { GestureDetector( onTap: () => widget.onDateSelected(date), child: Container( - margin: const EdgeInsets.symmetric(vertical: 2), + margin: EdgeInsets.symmetric(vertical: 2), decoration: BoxDecoration( color: isSelected - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : isToday ? DesignTokens.primaryLight : null, @@ -173,7 +173,7 @@ class _MiniCalendarState extends State { color: isSelected ? CupertinoColors.white : isToday - ? DesignTokens.primary + ? DesignTokens.dynamicPrimary : (isDark ? DarkDesignTokens.text1 : DesignTokens.text1), @@ -183,10 +183,10 @@ class _MiniCalendarState extends State { Container( width: 4, height: 4, - margin: const EdgeInsets.only(top: 1), + margin: EdgeInsets.only(top: 1), decoration: BoxDecoration( - color: DesignTokens.primary, - borderRadius: BorderRadius.circular(2), + color: DesignTokens.dynamicPrimary, + borderRadius: DesignTokens.borderRadiusSm, ), ), ], diff --git a/lib/src/widgets/discover/category_discover_card.dart b/lib/src/widgets/discover/category_discover_card.dart index 128a4d9..e7959b0 100644 --- a/lib/src/widgets/discover/category_discover_card.dart +++ b/lib/src/widgets/discover/category_discover_card.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: category_discover_card.dart * 名称: 分类发现卡片 * 作用: 瀑布流中的分类展示卡片,Liquid Glass风格 @@ -27,16 +27,16 @@ class CategoryDiscoverCard extends StatelessWidget { }; static final Map _categoryColors = { - 'recipe': const Color(0xFFFF9800), - 'ingredient': const Color(0xFF4CAF50), - 'recipe_main': const Color(0xFFF44336), + 'recipe': DesignTokens.orange, + 'ingredient': DesignTokens.green, + 'recipe_main': DesignTokens.red, }; @override Widget build(BuildContext context) { final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; final icon = _categoryIcons[category.type] ?? CupertinoIcons.folder; - final color = _categoryColors[category.type] ?? DesignTokens.primary; + final color = _categoryColors[category.type] ?? DesignTokens.dynamicPrimary; return GestureDetector( onTap: () { @@ -90,7 +90,7 @@ class CategoryDiscoverCard extends StatelessWidget { ), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( '${category.count}道菜谱', diff --git a/lib/src/widgets/discover/discover_waterfall.dart b/lib/src/widgets/discover/discover_waterfall.dart index 14dfe31..2db14c1 100644 --- a/lib/src/widgets/discover/discover_waterfall.dart +++ b/lib/src/widgets/discover/discover_waterfall.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: discover_waterfall.dart * 名称: 发现页瀑布流容器 * 作用: SliverMasonryGrid 2列混合瀑布流,直接作为 sliver 使用 @@ -188,13 +188,13 @@ class DiscoverWaterfall extends StatelessWidget { GestureDetector( onTap: () => onLoadMore?.call(), child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space1, ), decoration: BoxDecoration( - color: DesignTokens.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusMd, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -202,14 +202,14 @@ class DiscoverWaterfall extends StatelessWidget { Icon( CupertinoIcons.arrow_down, size: 12, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), - const SizedBox(width: 4), + SizedBox(width: 4), Text( '加载更多', style: TextStyle( fontSize: 11, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w500, ), ), @@ -239,7 +239,7 @@ class DiscoverWaterfall extends StatelessWidget { color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) .withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Icon( CupertinoIcons.arrow_up, @@ -267,7 +267,7 @@ class _DiscoverTagChip extends StatelessWidget { Widget build(BuildContext context) { final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; final isTaste = tag.type == 'taste'; - final baseColor = isTaste ? DesignTokens.primary : DesignTokens.orange; + final baseColor = isTaste ? DesignTokens.dynamicPrimary : DesignTokens.orange; return GlassContainer( padding: const EdgeInsets.all(DesignTokens.space3), @@ -340,7 +340,7 @@ class _DiscoverMealTimeCard extends StatelessWidget { height: 36, decoration: const BoxDecoration( gradient: LinearGradient( - colors: [Color(0xFF9C27B0), Color(0xFF673AB7)], + colors: [DesignTokens.purple, Color(0xFF673AB7)], ), borderRadius: BorderRadius.all( Radius.circular(DesignTokens.radiusSm), @@ -372,14 +372,14 @@ class _DiscoverMealTimeCard extends StatelessWidget { vertical: DesignTokens.space1, ), decoration: BoxDecoration( - color: const Color(0xFF9C27B0).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), + color: DesignTokens.purple.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( '${mealTime.count} 道推荐菜谱', style: const TextStyle( fontSize: 11, - color: Color(0xFF9C27B0), + color: DesignTokens.purple, fontWeight: FontWeight.w500, ), ), diff --git a/lib/src/widgets/discover/ingredient_discover_card.dart b/lib/src/widgets/discover/ingredient_discover_card.dart index 2f6637c..442d771 100644 --- a/lib/src/widgets/discover/ingredient_discover_card.dart +++ b/lib/src/widgets/discover/ingredient_discover_card.dart @@ -39,7 +39,7 @@ class IngredientDiscoverCard extends StatelessWidget { height: 40, decoration: BoxDecoration( gradient: const LinearGradient( - colors: [Color(0xFF4CAF50), Color(0xFF81C784)], + colors: [DesignTokens.green, Color(0xFF81C784)], ), borderRadius: BorderRadius.circular(DesignTokens.radiusSm), @@ -97,13 +97,13 @@ class IngredientDiscoverCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ const Icon(CupertinoIcons.exclamationmark_triangle, - size: 12, color: Color(0xFFFF3B30)), + size: 12, color: DesignTokens.red), const SizedBox(width: 4), Flexible( child: Text( '含过敏原: ${ingredient.allergen.take(2).join("、")}', style: const TextStyle( - fontSize: 10, color: Color(0xFFFF3B30)), + fontSize: 10, color: DesignTokens.red), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/src/widgets/discover/recipe_discover_card.dart b/lib/src/widgets/discover/recipe_discover_card.dart index 98c083f..51c3a08 100644 --- a/lib/src/widgets/discover/recipe_discover_card.dart +++ b/lib/src/widgets/discover/recipe_discover_card.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: recipe_discover_card.dart * 名称: 菜品发现卡片 * 作用: 瀑布流中的菜品展示卡片,Liquid Glass风格,支持懒加载图片+长按关闭 @@ -156,20 +156,20 @@ class _RecipeDiscoverCardState extends State children: [ Flexible( child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: DesignTokens.primaryLight, - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( _truncateName(widget.recipe.category.name, 5), - style: const TextStyle( + style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -294,7 +294,7 @@ class _RecipeDiscoverCardState extends State begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - DesignTokens.primary.withValues(alpha: 0.06), + DesignTokens.dynamicPrimary.withValues(alpha: 0.06), DesignTokens.orange.withValues(alpha: 0.04), ], ), diff --git a/lib/src/widgets/glass/glass_feed_card.dart b/lib/src/widgets/glass/glass_feed_card.dart index cf69e88..4363df2 100644 --- a/lib/src/widgets/glass/glass_feed_card.dart +++ b/lib/src/widgets/glass/glass_feed_card.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: glass_feed_card.dart * 名称: 毛玻璃信息流卡片 * 作用: iOS 26 Liquid Glass 风格的信息流卡片组件 @@ -131,11 +131,11 @@ class GlassFeedCard extends StatelessWidget { Widget _buildImagePlaceholder() { return Container( color: DesignTokens.primaryLight, - child: const Center( + child: Center( child: Icon( CupertinoIcons.photo, size: 32, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ); @@ -159,7 +159,7 @@ class GlassFeedCard extends StatelessWidget { if (tag != null) ...[ const SizedBox(width: DesignTokens.space2), Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), @@ -169,10 +169,10 @@ class GlassFeedCard extends StatelessWidget { ), child: Text( tag!, - style: const TextStyle( + style: TextStyle( fontSize: DesignTokens.fontXs, fontWeight: FontWeight.w500, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/widgets/glass/glass_nav_bar.dart b/lib/src/widgets/glass/glass_nav_bar.dart index fa1753b..426772d 100644 --- a/lib/src/widgets/glass/glass_nav_bar.dart +++ b/lib/src/widgets/glass/glass_nav_bar.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: glass_nav_bar.dart * 名称: 毛玻璃底部导航栏 * 作用: iOS 26 Liquid Glass 风格的底部导航栏组件 @@ -111,7 +111,7 @@ class _NavItem extends StatelessWidget { @override Widget build(BuildContext context) { final activeColor = isDark - ? DarkDesignTokens.primary + ? DesignTokens.dynamicPrimary : DesignTokens.dynamicPrimary; final inactiveColor = isDark ? DarkDesignTokens.text2 : DesignTokens.text2; @@ -133,7 +133,7 @@ class _NavItem extends StatelessWidget { horizontal: 6, vertical: 2, ), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), badgeContent: Text( item.badgeCount > 99 ? '99+' : '${item.badgeCount}', diff --git a/lib/src/widgets/glass/glass_segmented_control.dart b/lib/src/widgets/glass/glass_segmented_control.dart index b9dee2d..e13f4da 100644 --- a/lib/src/widgets/glass/glass_segmented_control.dart +++ b/lib/src/widgets/glass/glass_segmented_control.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: glass_segmented_control.dart * 名称: 毛玻璃分段选择器 * 作用: iOS 26 Liquid Glass 风格的分段选择器组件 @@ -75,9 +75,7 @@ class _SegmentItem extends StatelessWidget { final activeBg = isDark ? DarkDesignTokens.segmentedBg : CupertinoColors.white; - final activeColor = isDark - ? DarkDesignTokens.primary - : DesignTokens.primary; + final activeColor = DesignTokens.dynamicPrimary; final inactiveColor = isDark ? DarkDesignTokens.text2 : DesignTokens.text2; return Container( diff --git a/lib/src/widgets/glass/glass_settings_tile.dart b/lib/src/widgets/glass/glass_settings_tile.dart index 2e38e83..bb5f887 100644 --- a/lib/src/widgets/glass/glass_settings_tile.dart +++ b/lib/src/widgets/glass/glass_settings_tile.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: glass_settings_tile.dart * 名称: 毛玻璃设置列表项 * 作用: iOS 26 Liquid Glass 风格的设置列表项和分组组件 @@ -121,12 +121,12 @@ class GlassSettingsTile extends StatelessWidget { final effectiveIconColor = isDestructive ? DesignTokens.red : (iconColor ?? - (isDark ? DarkDesignTokens.primary : DesignTokens.primary)); + (DesignTokens.dynamicPrimary)); final effectiveIconBg = isDestructive ? DesignTokens.red.withValues(alpha: 0.1) : (iconBgColor ?? (isDark - ? DarkDesignTokens.primary.withValues(alpha: 0.15) + ? DesignTokens.dynamicPrimary.withValues(alpha: 0.15) : DesignTokens.primaryLight)); final effectiveTextColor = isDestructive ? DesignTokens.red @@ -148,7 +148,7 @@ class GlassSettingsTile extends StatelessWidget { height: 28, decoration: BoxDecoration( color: effectiveIconBg, - borderRadius: BorderRadius.circular(6), + borderRadius: DesignTokens.borderRadiusSm, ), child: Icon(icon, size: 16, color: effectiveIconColor), ), diff --git a/lib/src/widgets/glass/home_app_bar.dart b/lib/src/widgets/glass/home_app_bar.dart index ff23115..84f8423 100644 --- a/lib/src/widgets/glass/home_app_bar.dart +++ b/lib/src/widgets/glass/home_app_bar.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: home_app_bar.dart * 名称: 首页顶栏组件 * 作用: iOS 26 Liquid Glass 风格双层首页顶栏 @@ -245,7 +245,7 @@ class _HomeAppBarState extends State color: isDark ? DarkDesignTokens.glass.withValues(alpha: 0.35) : DesignTokens.glass.withValues(alpha: 0.35), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: isDark ? CupertinoColors.white.withValues(alpha: 0.08) @@ -293,7 +293,7 @@ class _HomeAppBarState extends State ), child: Container( key: ValueKey(_showLongGreeting), - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), @@ -301,7 +301,7 @@ class _HomeAppBarState extends State color: _showLongGreeting ? (isDark ? DarkDesignTokens.card : DesignTokens.card) .withValues(alpha: 0.6) - : DesignTokens.primary.withValues(alpha: 0.1), + : DesignTokens.dynamicPrimary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(DesignTokens.radiusSm), ), child: Text( @@ -313,7 +313,7 @@ class _HomeAppBarState extends State ? (isDark ? DarkDesignTokens.text2 : DesignTokens.text2) - : DesignTokens.primary, + : DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/widgets/glass/liquid_glass_nav_bar.dart b/lib/src/widgets/glass/liquid_glass_nav_bar.dart index 91b45cd..85d5493 100644 --- a/lib/src/widgets/glass/liquid_glass_nav_bar.dart +++ b/lib/src/widgets/glass/liquid_glass_nav_bar.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: liquid_glass_nav_bar.dart * 名称: 透明玻璃底部导航栏 * 作用: 透明玻璃杯效果的底部导航栏组件 @@ -292,7 +292,7 @@ class _LiquidTabItemState extends State<_LiquidTabItem> @override Widget build(BuildContext context) { final activeColor = widget.isDark - ? DarkDesignTokens.primary + ? DesignTokens.dynamicPrimary : DesignTokens.dynamicPrimary; final inactiveColor = widget.isDark ? DarkDesignTokens.text2 @@ -338,7 +338,7 @@ class _LiquidTabItemState extends State<_LiquidTabItem> horizontal: 6, vertical: 2, ), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), badgeContent: Text( widget.item.badgeCount > 99 diff --git a/lib/src/widgets/nutrition_dashboard_card.dart b/lib/src/widgets/nutrition_dashboard_card.dart index 820d46f..2ac3fc1 100644 --- a/lib/src/widgets/nutrition_dashboard_card.dart +++ b/lib/src/widgets/nutrition_dashboard_card.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: nutrition_dashboard_card.dart * 名称: 营养追踪仪表盘卡片 * 作用: 首页展示今日营养摄入环形图 @@ -56,7 +56,7 @@ class NutritionDashboardCard extends StatelessWidget { color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), - const Spacer(), + Spacer(), GestureDetector( onTap: () => Get.toNamed('/nutrition'), child: Row( @@ -65,24 +65,20 @@ class NutritionDashboardCard extends StatelessWidget { '详情', style: TextStyle( fontSize: DesignTokens.fontSm, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), Icon( CupertinoIcons.chevron_right, size: 14, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ], ), ), ], ), - const SizedBox(height: DesignTokens.space4), + SizedBox(height: DesignTokens.space4), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -90,7 +86,7 @@ class NutritionDashboardCard extends StatelessWidget { progress: controller.caloriesPercent, size: 72, strokeWidth: 8, - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, centerLabel: 'kcal', centerValue: '${calories.toInt()}', bottomLabel: '热量', diff --git a/lib/src/widgets/product_card.dart b/lib/src/widgets/product_card.dart index 6beb64e..ac05c3b 100644 --- a/lib/src/widgets/product_card.dart +++ b/lib/src/widgets/product_card.dart @@ -12,6 +12,7 @@ import 'package:get/get.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; import 'package:mom_kitchen/src/services/ui/theme_service.dart'; import 'package:mom_kitchen/src/services/ui/toast_service.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; class ProductCard extends StatelessWidget { final RecipeModel recipe; @@ -34,7 +35,7 @@ class ProductCard extends StatelessWidget { child: Container( decoration: BoxDecoration( color: theme.backgroundColor.value, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, border: Border.all( color: theme.textColor.value.withValues(alpha: 0.1), width: 0.5, @@ -100,7 +101,7 @@ class ProductCard extends StatelessWidget { padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: theme.primaryColor.value, - borderRadius: BorderRadius.circular(6), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Icon( CupertinoIcons.eye, diff --git a/lib/src/widgets/recipe_detail/recipe_action_bar.dart b/lib/src/widgets/recipe_detail/recipe_action_bar.dart index 420c459..9c62035 100644 --- a/lib/src/widgets/recipe_detail/recipe_action_bar.dart +++ b/lib/src/widgets/recipe_detail/recipe_action_bar.dart @@ -189,7 +189,7 @@ class RecipeActionBar extends StatelessWidget { height: 50, decoration: BoxDecoration( color: DesignTokens.orange.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/widgets/recipe_detail/recipe_author_card.dart b/lib/src/widgets/recipe_detail/recipe_author_card.dart index c3daf19..0389f4b 100644 --- a/lib/src/widgets/recipe_detail/recipe_author_card.dart +++ b/lib/src/widgets/recipe_detail/recipe_author_card.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -35,7 +35,7 @@ class RecipeAuthorCard extends StatelessWidget { height: 36, decoration: BoxDecoration( color: - (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.15), shape: BoxShape.circle, ), @@ -45,9 +45,7 @@ class RecipeAuthorCard extends StatelessWidget { style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/widgets/recipe_detail/recipe_category_breadcrumb.dart b/lib/src/widgets/recipe_detail/recipe_category_breadcrumb.dart index e4313fa..1bd2c2b 100644 --- a/lib/src/widgets/recipe_detail/recipe_category_breadcrumb.dart +++ b/lib/src/widgets/recipe_detail/recipe_category_breadcrumb.dart @@ -1,4 +1,7 @@ +// 2026-04-13 | RecipeCategoryBreadcrumb | 分类面包屑 | 新增分类点击跳转CategoryBrowsePage import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/config/app_routes.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -9,20 +12,28 @@ class RecipeCategoryBreadcrumb extends StatelessWidget { @override Widget build(BuildContext context) { - final hierarchy = recipe.categoryHierarchy ?? []; + final hierarchy = recipe.categoryHierarchy; if (hierarchy.isEmpty && recipe.categoryName == null) { return const SizedBox(); } - final items = []; + final items = <_BreadcrumbItem>[]; if (hierarchy.isNotEmpty) { final sorted = List.from(hierarchy) ..sort((a, b) => b.level.compareTo(a.level)); for (final h in sorted) { - if (h.name.isNotEmpty) items.add(h.name); + if (h.name.isNotEmpty) { + items.add(_BreadcrumbItem(id: h.id, name: h.name, level: h.level)); + } } } else if (recipe.categoryName != null) { - items.add(recipe.categoryName!); + items.add( + _BreadcrumbItem( + id: recipe.categoryId, + name: recipe.categoryName!, + level: 0, + ), + ); } if (items.isEmpty) return const SizedBox(); @@ -39,39 +50,15 @@ class RecipeCategoryBreadcrumb extends StatelessWidget { runSpacing: 4, children: items.asMap().entries.map((entry) { final isLast = entry.key == items.length - 1; + final item = entry.value; return Row( mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: isLast - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) - : (isDark ? DarkDesignTokens.card : DesignTokens.card), - borderRadius: DesignTokens.borderRadiusSm, - border: Border.all( - color: isLast - ? (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) - : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) - .withValues(alpha: 0.2), - ), - ), - child: Text( - entry.value, - style: TextStyle( - fontSize: DesignTokens.fontXs, - fontWeight: isLast ? FontWeight.w600 : FontWeight.w400, - color: isLast - ? CupertinoColors.white - : (isDark - ? DarkDesignTokens.text2 - : DesignTokens.text2), - ), - ), + _BreadcrumbChip( + item: item, + isLast: isLast, + isDark: isDark, + onTap: isLast ? null : () => _navigateToCategory(item), ), if (!isLast) Padding( @@ -88,4 +75,83 @@ class RecipeCategoryBreadcrumb extends StatelessWidget { ), ); } + + void _navigateToCategory(_BreadcrumbItem item) { + Get.toNamed( + AppRoutes.categoryBrowse, + arguments: { + 'category': {'id': item.id, 'name': item.name}, + 'title': item.name, + }, + ); + } +} + +class _BreadcrumbItem { + final int? id; + final String name; + final int level; + + const _BreadcrumbItem({this.id, required this.name, required this.level}); +} + +class _BreadcrumbChip extends StatelessWidget { + final _BreadcrumbItem item; + final bool isLast; + final bool isDark; + final VoidCallback? onTap; + + const _BreadcrumbChip({ + required this.item, + required this.isLast, + required this.isDark, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final child = Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isLast + ? (DesignTokens.dynamicPrimary) + : (isDark ? DarkDesignTokens.card : DesignTokens.card), + borderRadius: DesignTokens.borderRadiusSm, + border: Border.all( + color: isLast + ? (DesignTokens.dynamicPrimary) + : (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item.name, + style: TextStyle( + fontSize: DesignTokens.fontXs, + fontWeight: isLast ? FontWeight.w600 : FontWeight.w400, + color: isLast + ? CupertinoColors.white + : (isDark ? DarkDesignTokens.text2 : DesignTokens.text2), + ), + ), + if (!isLast) ...[ + const SizedBox(width: 2), + Icon( + CupertinoIcons.chevron_right, + size: 10, + color: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3) + .withValues(alpha: 0.5), + ), + ], + ], + ), + ); + + if (onTap == null) return child; + + return GestureDetector(onTap: onTap, child: child); + } } diff --git a/lib/src/widgets/recipe_detail/recipe_cover_image.dart b/lib/src/widgets/recipe_detail/recipe_cover_image.dart index 060fd1d..a635641 100644 --- a/lib/src/widgets/recipe_detail/recipe_cover_image.dart +++ b/lib/src/widgets/recipe_detail/recipe_cover_image.dart @@ -1,3 +1,4 @@ +// 2026-04-13 | RecipeCoverImage | 菜品封面图 | 新增评分星级叠加层展示 import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; @@ -6,12 +7,14 @@ import 'package:mom_kitchen/src/widgets/recipe_image.dart'; class RecipeCoverImage extends StatelessWidget { final RecipeModel recipe; + final RecipeRating? rating; final int viewCount; final int likeCount; const RecipeCoverImage({ super.key, required this.recipe, + this.rating, required this.viewCount, required this.likeCount, }); @@ -39,7 +42,7 @@ class RecipeCoverImage extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.transparent, + const Color(0x00000000), Colors.black.withValues(alpha: 0.7), ], ), @@ -52,6 +55,7 @@ class RecipeCoverImage extends StatelessWidget { } Widget _buildStatsRow() { + final effectiveRating = rating ?? recipe.statistics?.rating; return Row( children: [ const Icon(CupertinoIcons.eye, size: 16, color: Colors.white70), @@ -71,7 +75,10 @@ class RecipeCoverImage extends StatelessWidget { '$likeCount', style: const TextStyle(color: Colors.white70), ), - if (viewCount >= 1000 || likeCount >= 50) ...[ + if (effectiveRating != null && effectiveRating.hasRating) ...[ + const SizedBox(width: 16), + _buildRatingBadge(effectiveRating), + ] else if (viewCount >= 1000 || likeCount >= 50) ...[ const SizedBox(width: 12), _buildHotBadge(), ], @@ -79,6 +86,37 @@ class RecipeCoverImage extends StatelessWidget { ); } + Widget _buildRatingBadge(RecipeRating ratingData) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: _getBadgeBgColor(ratingData), + borderRadius: DesignTokens.borderRadiusSm, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(CupertinoIcons.star_fill, size: 10, color: DesignTokens.gold), + const SizedBox(width: 3), + Text( + ratingData.score.toStringAsFixed(1), + style: TextStyle( + color: CupertinoColors.white, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Color _getBadgeBgColor(RecipeRating ratingData) { + if (ratingData.isExcellent) return DesignTokens.gold.withValues(alpha: 0.9); + if (ratingData.isRecommended) return DesignTokens.dynamicPrimary.withValues(alpha: 0.9); + return DesignTokens.orange.withValues(alpha: 0.9); + } + Widget _buildHotBadge() { String badgeText; if (viewCount >= 5000) { @@ -96,7 +134,7 @@ class RecipeCoverImage extends StatelessWidget { ), decoration: BoxDecoration( color: DesignTokens.orange.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, ), child: Text( badgeText, diff --git a/lib/src/widgets/recipe_detail/recipe_indices_card.dart b/lib/src/widgets/recipe_detail/recipe_indices_card.dart index 3394a57..3948c74 100644 --- a/lib/src/widgets/recipe_detail/recipe_indices_card.dart +++ b/lib/src/widgets/recipe_detail/recipe_indices_card.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -72,23 +72,19 @@ class RecipeIndicesCard extends StatelessWidget { : DesignTokens.text3) .withValues(alpha: 0.1), valueColor: AlwaysStoppedAnimation( - isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + DesignTokens.dynamicPrimary, ), minHeight: 8, ), ), ), - const SizedBox(width: 8), + SizedBox(width: 8), Text( '${entry.value}', style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], diff --git a/lib/src/widgets/recipe_detail/recipe_ingredient_details.dart b/lib/src/widgets/recipe_detail/recipe_ingredient_details.dart index 0679bdf..375cd45 100644 --- a/lib/src/widgets/recipe_detail/recipe_ingredient_details.dart +++ b/lib/src/widgets/recipe_detail/recipe_ingredient_details.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -102,9 +102,7 @@ class RecipeIngredientDetails extends StatelessWidget { style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, decoration: TextDecoration.underline, ), ), diff --git a/lib/src/widgets/recipe_detail/recipe_meta_info_card.dart b/lib/src/widgets/recipe_detail/recipe_meta_info_card.dart index 1d8942b..2acf50b 100644 --- a/lib/src/widgets/recipe_detail/recipe_meta_info_card.dart +++ b/lib/src/widgets/recipe_detail/recipe_meta_info_card.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -45,10 +45,10 @@ class RecipeMetaInfoCard extends StatelessWidget { runSpacing: DesignTokens.space2, children: items.map((entry) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: - (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.08), borderRadius: DesignTokens.borderRadiusSm, ), diff --git a/lib/src/widgets/recipe_detail/recipe_nutrition_section.dart b/lib/src/widgets/recipe_detail/recipe_nutrition_section.dart index d305133..6017bd6 100644 --- a/lib/src/widgets/recipe_detail/recipe_nutrition_section.dart +++ b/lib/src/widgets/recipe_detail/recipe_nutrition_section.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -86,14 +86,14 @@ class RecipeNutritionSection extends StatelessWidget { Widget _buildNutritionItem(String emoji, String value, String unit, bool isDark) { return Column( children: [ - Text(emoji, style: const TextStyle(fontSize: 20)), - const SizedBox(height: 4), + Text(emoji, style: TextStyle(fontSize: 20)), + SizedBox(height: 4), Text( value, style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.w700, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), Text( @@ -159,9 +159,7 @@ class RecipeNutritionSection extends StatelessWidget { style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w500, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], diff --git a/lib/src/widgets/recipe_detail/recipe_picid_card.dart b/lib/src/widgets/recipe_detail/recipe_picid_card.dart index bc03900..e4462a7 100644 --- a/lib/src/widgets/recipe_detail/recipe_picid_card.dart +++ b/lib/src/widgets/recipe_detail/recipe_picid_card.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; @@ -38,19 +38,19 @@ class RecipePicIdCard extends StatelessWidget { final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; return Padding( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space4, vertical: DesignTokens.space2, ), child: Container( - padding: const EdgeInsets.all(DesignTokens.space3), + padding: EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card.withValues(alpha: 0.5) : DesignTokens.card.withValues(alpha: 0.8), borderRadius: DesignTokens.borderRadiusMd, border: Border.all( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.3), ), ), @@ -77,7 +77,7 @@ class RecipePicIdCard extends StatelessWidget { Icon( CupertinoIcons.photo, size: 16, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), const SizedBox(width: DesignTokens.space1), Text( @@ -129,12 +129,12 @@ class RecipePicIdCard extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: 2, ), decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) + color: (DesignTokens.dynamicPrimary) .withValues(alpha: 0.15), borderRadius: DesignTokens.borderRadiusSm, ), @@ -146,14 +146,14 @@ class RecipePicIdCard extends StatelessWidget { ? CupertinoIcons.doc_on_doc : CupertinoIcons.link, size: 12, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), - const SizedBox(width: 4), + SizedBox(width: 4), Text( label, style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ], @@ -186,9 +186,7 @@ class RecipePicIdCard extends StatelessWidget { style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontFamily: 'monospace', ), ), diff --git a/lib/src/widgets/recipe_detail/recipe_skeleton_view.dart b/lib/src/widgets/recipe_detail/recipe_skeleton_view.dart index 081e026..5503930 100644 --- a/lib/src/widgets/recipe_detail/recipe_skeleton_view.dart +++ b/lib/src/widgets/recipe_detail/recipe_skeleton_view.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/widgets/base/skeleton_loader.dart'; @@ -121,10 +121,10 @@ class RecipeSkeletonView extends StatelessWidget { padding: EdgeInsets.zero, minimumSize: Size.zero, onPressed: onRetry, - child: const Text( + child: Text( '重试', style: TextStyle( - color: DesignTokens.primary, + color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w600, ), ), @@ -147,7 +147,7 @@ class RecipeSkeletonView extends StatelessWidget { SkeletonLoader( width: 40, height: 40, - borderRadius: BorderRadius.circular(20), + borderRadius: DesignTokens.borderRadiusLg, isDark: isDark, ), const SizedBox(height: 6), diff --git a/lib/src/widgets/recipe_detail/recipe_statistics_bar.dart b/lib/src/widgets/recipe_detail/recipe_statistics_bar.dart index 44b7a4d..a2094b7 100644 --- a/lib/src/widgets/recipe_detail/recipe_statistics_bar.dart +++ b/lib/src/widgets/recipe_detail/recipe_statistics_bar.dart @@ -1,15 +1,18 @@ +// 2026-04-13 | RecipeStatisticsBar | 菜品统计栏 | 重构评分显示,对齐API v2.8.0 rating字段 import 'package:flutter/cupertino.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; class RecipeStatisticsBar extends StatelessWidget { final RecipeStatistics? statistics; + final RecipeRating? rating; final int viewCount; final int likeCount; const RecipeStatisticsBar({ super.key, this.statistics, + this.rating, required this.viewCount, required this.likeCount, }); @@ -45,7 +48,7 @@ class RecipeStatisticsBar extends StatelessWidget { '点赞', isDark, ), - _buildStatItem('⭐', '${statistics?.recommends ?? 0}', '推荐', isDark), + _buildRatingItem(isDark), _buildStatItem('💬', '${statistics?.comments ?? 0}', '评论', isDark), ], ), @@ -54,26 +57,152 @@ class RecipeStatisticsBar extends StatelessWidget { } Widget _buildStatItem(String emoji, String value, String label, bool isDark) { - return Column( - children: [ - Text(emoji, style: const TextStyle(fontSize: 20)), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - fontSize: DesignTokens.fontMd, - fontWeight: FontWeight.w700, - color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + return Expanded( + child: Column( + children: [ + Text(emoji, style: const TextStyle(fontSize: 20)), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w700, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), ), - ), - Text( - label, - style: TextStyle( - fontSize: DesignTokens.fontXs, - color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + Text( + label, + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), ), - ), - ], + ], + ), ); } + + Widget _buildRatingItem(bool isDark) { + final effectiveRating = rating ?? statistics?.rating; + final hasRating = effectiveRating != null && effectiveRating.hasRating; + + return Expanded( + child: Column( + children: [ + _buildStarRow(effectiveRating, isDark), + const SizedBox(height: 4), + Text( + hasRating ? effectiveRating.displayText : '暂无评分', + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w700, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + _buildLevelLabel(effectiveRating, isDark), + ], + ), + ); + } + + Widget _buildStarRow(RecipeRating? ratingData, bool isDark) { + final starCount = ratingData?.star ?? 0; + final filledColor = _getStarColor(ratingData); + final emptyColor = isDark + ? DarkDesignTokens.text3.withValues(alpha: 0.3) + : DesignTokens.text3.withValues(alpha: 0.3); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: List.generate(5, (index) { + final isFilled = index < starCount; + return Icon( + isFilled ? CupertinoIcons.star_fill : CupertinoIcons.star, + size: 14, + color: isFilled ? filledColor : emptyColor, + ); + }), + ); + } + + Color _getStarColor(RecipeRating? ratingData) { + if (ratingData == null) return DesignTokens.dynamicPrimary; + if (ratingData.isExcellent) return DesignTokens.gold; + if (ratingData.isRecommended) return DesignTokens.dynamicPrimary; + if (ratingData.isAverage) return DesignTokens.text2; + if (ratingData.isPoor || ratingData.isNotRecommended) { + return DesignTokens.red; + } + return DesignTokens.dynamicPrimary; + } + + Widget _buildLevelLabel(RecipeRating? ratingData, bool isDark) { + if (ratingData == null || !ratingData.hasRating) { + return Text( + '评分', + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ); + } + + final level = ratingData.level; + if (level.isEmpty) { + return Text( + '评分', + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ); + } + + final bgColor = _getLevelBgColor(ratingData, isDark); + final textColor = _getLevelTextColor(ratingData, isDark); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(DesignTokens.radiusSm), + ), + child: Text( + level, + style: TextStyle( + fontSize: DesignTokens.fontXs - 1, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ); + } + + Color _getLevelBgColor(RecipeRating ratingData, bool isDark) { + if (ratingData.isExcellent) { + return DesignTokens.gold.withValues(alpha: 0.15); + } + if (ratingData.isRecommended) { + return DesignTokens.dynamicPrimary.withValues(alpha: 0.12); + } + if (ratingData.isAverage) { + return (isDark ? DarkDesignTokens.text3 : DesignTokens.text3).withValues( + alpha: 0.12, + ); + } + return DesignTokens.red.withValues(alpha: 0.12); + } + + Color _getLevelTextColor(RecipeRating ratingData, bool isDark) { + if (ratingData.isExcellent) return DesignTokens.gold; + if (ratingData.isRecommended) return DesignTokens.dynamicPrimary; + if (ratingData.isAverage) { + return isDark ? DarkDesignTokens.text3 : DesignTokens.text3; + } + return DesignTokens.red; + } } diff --git a/lib/src/widgets/recipe_detail/recipe_steps_section.dart b/lib/src/widgets/recipe_detail/recipe_steps_section.dart index c129458..ad99796 100644 --- a/lib/src/widgets/recipe_detail/recipe_steps_section.dart +++ b/lib/src/widgets/recipe_detail/recipe_steps_section.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -57,23 +57,21 @@ class RecipeStepsSection extends StatelessWidget { color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), - const SizedBox(height: DesignTokens.space2), + SizedBox(height: DesignTokens.space2), if (hasNumbered) ...numberedSteps.asMap().entries.map( (entry) => Padding( - padding: const EdgeInsets.only(bottom: DesignTokens.space2), + padding: EdgeInsets.only(bottom: DesignTokens.space2), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 24, height: 24, - margin: const EdgeInsets.only(right: 8, top: 2), + margin: EdgeInsets.only(right: 8, top: 2), decoration: BoxDecoration( color: - (isDark - ? DarkDesignTokens.primary - : DesignTokens.primary) + (DesignTokens.dynamicPrimary) .withValues(alpha: 0.12), shape: BoxShape.circle, ), @@ -83,9 +81,7 @@ class RecipeStepsSection extends StatelessWidget { style: TextStyle( fontSize: DesignTokens.fontXs, fontWeight: FontWeight.w700, - color: isDark - ? DarkDesignTokens.primary - : DesignTokens.primary, + color: DesignTokens.dynamicPrimary, ), ), ), diff --git a/lib/src/widgets/recipe_detail/recipe_tags_section.dart b/lib/src/widgets/recipe_detail/recipe_tags_section.dart index d7fbf36..71350c0 100644 --- a/lib/src/widgets/recipe_detail/recipe_tags_section.dart +++ b/lib/src/widgets/recipe_detail/recipe_tags_section.dart @@ -1,4 +1,7 @@ +// 2026-04-13 | RecipeTagsSection | 菜品标签区域 | 新增标签点击跳转TagRecipeListPage import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/config/app_routes.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; @@ -22,27 +25,74 @@ class RecipeTagsSection extends StatelessWidget { spacing: DesignTokens.space2, runSpacing: DesignTokens.space1, children: recipe.tags.map((tag) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space2, - vertical: DesignTokens.space1, - ), - decoration: BoxDecoration( - color: (isDark ? DarkDesignTokens.primary : DesignTokens.primary) - .withValues(alpha: 0.1), - borderRadius: DesignTokens.borderRadiusSm, - ), - child: Text( - '#${tag.name}', - style: TextStyle( - fontSize: DesignTokens.fontXs, - color: isDark ? DarkDesignTokens.primary : DesignTokens.primary, - fontWeight: FontWeight.w500, - ), - ), + return _TagChip( + tag: tag, + isDark: isDark, + onTap: () => _navigateToTagList(tag), ); }).toList(), ), ); } + + void _navigateToTagList(TagItem tag) { + Get.toNamed( + AppRoutes.tagRecipeList, + arguments: { + 'tagName': tag.name, + 'tagId': tag.id, + 'tagType': 'taste', + }, + ); + } +} + +class _TagChip extends StatelessWidget { + final TagItem tag; + final bool isDark; + final VoidCallback onTap; + + const _TagChip({ + required this.tag, + required this.isDark, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: DesignTokens.space2, + vertical: DesignTokens.space1, + ), + decoration: BoxDecoration( + color: (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusSm, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '#${tag.name}', + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: DesignTokens.dynamicPrimary, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 2), + Icon( + CupertinoIcons.chevron_right, + size: 10, + color: (DesignTokens.dynamicPrimary) + .withValues(alpha: 0.5), + ), + ], + ), + ), + ); + } } diff --git a/lib/src/widgets/recipe_detail/recipe_time_info.dart b/lib/src/widgets/recipe_detail/recipe_time_info.dart index c7b2fa8..97a1a0c 100644 --- a/lib/src/widgets/recipe_detail/recipe_time_info.dart +++ b/lib/src/widgets/recipe_detail/recipe_time_info.dart @@ -122,7 +122,7 @@ class RecipeTimeInfo extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), + borderRadius: DesignTokens.borderRadiusSm, border: Border.all(color: statusColor.withValues(alpha: 0.3)), ), child: Text( diff --git a/lib/src/widgets/recipe_image.dart b/lib/src/widgets/recipe_image.dart index 0b6ab62..afb4e75 100644 --- a/lib/src/widgets/recipe_image.dart +++ b/lib/src/widgets/recipe_image.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: recipe_image.dart * 名称: 菜谱图片组件(性能优化版) * 作用: 基于cached_network_image的高性能图片加载,支持缓存+精简Fallback+点击查看原图 @@ -118,7 +118,7 @@ class RecipeImage extends StatelessWidget { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - DesignTokens.primary.withValues(alpha: 0.06), + DesignTokens.dynamicPrimary.withValues(alpha: 0.06), DesignTokens.orange.withValues(alpha: 0.04), ], ), @@ -164,7 +164,7 @@ class RecipeImage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: CupertinoColors.black.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), + borderRadius: DesignTokens.borderRadiusSm, ), child: const Row( mainAxisSize: MainAxisSize.min, @@ -198,13 +198,13 @@ class RecipeImage extends StatelessWidget { child: Container( constraints: const BoxConstraints(maxWidth: 500, maxHeight: 500), child: ClipRRect( - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, child: InteractiveViewer( child: RecipeImage.full( recipeId: recipeId, picId: picId, coverUrl: coverUrl, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, ), ), ), diff --git a/lib/src/widgets/skeleton_widgets.dart b/lib/src/widgets/skeleton_widgets.dart index e00288e..0a31c97 100644 --- a/lib/src/widgets/skeleton_widgets.dart +++ b/lib/src/widgets/skeleton_widgets.dart @@ -12,7 +12,7 @@ class MessagePreview extends StatelessWidget { BorderRadius _getRadius(bool isMe) { switch (style) { case MessageBubbleStyle.rounded: - return BorderRadius.circular(20); + return DesignTokens.borderRadiusLg; case MessageBubbleStyle.modern: return isMe ? const BorderRadius.only( @@ -28,7 +28,7 @@ class MessagePreview extends StatelessWidget { bottomRight: Radius.circular(16), ); case MessageBubbleStyle.classic: - return BorderRadius.circular(8); + return DesignTokens.borderRadiusSm; } } diff --git a/lib/src/widgets/states/standard_dialog.dart b/lib/src/widgets/states/standard_dialog.dart index 8e63ea4..e39d357 100644 --- a/lib/src/widgets/states/standard_dialog.dart +++ b/lib/src/widgets/states/standard_dialog.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mom_kitchen/src/standards/page_standards.dart'; import 'package:mom_kitchen/src/services/ui/theme_service.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; class StandardDialog { StandardDialog._internal(); @@ -78,7 +79,7 @@ class StandardDialog { () => Dialog( backgroundColor: themeService.backgroundColor.value, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, ), child: Container( padding: const EdgeInsets.all(24), @@ -120,7 +121,7 @@ class StandardDialog { color: themeService.secondaryColor.value.withValues( alpha: 0.12, ), - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, onPressed: () { Get.back(result: false); }, @@ -139,9 +140,9 @@ class StandardDialog { child: CupertinoButton( padding: const EdgeInsets.symmetric(vertical: 12), color: isDestructive - ? const Color(0xFFF44336) + ? DesignTokens.red : themeService.primaryColor.value, - borderRadius: BorderRadius.circular(12), + borderRadius: DesignTokens.borderRadiusMd, onPressed: () { Get.back(result: true); }, @@ -236,14 +237,14 @@ class StandardDialog { Get.dialog( Obx( () => Dialog( - backgroundColor: Colors.transparent, + backgroundColor: const Color(0x00000000), elevation: 0, child: Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: themeService.backgroundColor.value, - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), @@ -294,7 +295,7 @@ class StandardDialog { decoration: BoxDecoration( color: themeService.secondaryColor.value .withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( cancelText, @@ -318,9 +319,9 @@ class StandardDialog { padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isDestructive - ? const Color(0xFFF44336) + ? DesignTokens.red : themeService.primaryColor.value, - borderRadius: BorderRadius.circular(10), + borderRadius: DesignTokens.borderRadiusMd, ), child: Text( confirmText ?? l10n?.confirm ?? '确定', @@ -371,7 +372,7 @@ class StandardDialog { () => AlertDialog( backgroundColor: themeService.backgroundColor.value, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: DesignTokens.borderRadiusMd, ), title: Text( title, @@ -407,7 +408,7 @@ class StandardDialog { }, style: TextButton.styleFrom( foregroundColor: isDestructive - ? const Color(0xFFF44336) + ? DesignTokens.red : themeService.primaryColor.value, ), child: Text(confirmText ?? l10n?.confirm ?? '确定'), diff --git a/scripts/debug_timestamp.dart b/scripts/debug_timestamp.dart deleted file mode 100644 index 6893e0b..0000000 --- a/scripts/debug_timestamp.dart +++ /dev/null @@ -1,32 +0,0 @@ -// 时间戳调试脚本 -void main() { - final timestamp = 146085838541; - - print('🔍 时间戳调试'); - print('原始值: $timestamp (${timestamp.toString().length}位)'); - - // 尝试不同的解析方式 - print('\n方式1: 当作毫秒 (直接使用)'); - final d1 = DateTime.fromMillisecondsSinceEpoch(timestamp); - print('结果: ${d1.year}-${d1.month}-${d1.day} ${d1.hour}:${d1.minute}'); - - print('\n方式2: 当作秒 (×1000转毫秒)'); - final d2 = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); - print('结果: ${d2.year}-${d2.month}-${d2.day} ${d2.hour}:${d2.minute}'); - - print('\n方式3: 除以1000当作秒 (可能是微秒?)'); - final d3 = DateTime.fromMillisecondsSinceEpoch((timestamp / 1000).round() * 1000); - print('结果: ${d3.year}-${d3.month}-${d3.day} ${d3.hour}:${d3.minute}'); - - // 2016年的正确时间戳 - print('\n--- 参考值 ---'); - final date2016 = DateTime(2016, 4, 18, 15, 35); - print('2016-04-18 15:35 的毫秒时间戳: ${date2016.millisecondsSinceEpoch}'); - print('2016-04-18 15:35 的秒时间戳: ${date2016.millisecondsSinceEpoch ~/ 1000}'); - - // 检查是否差1000倍 - final correctMs = 1460858385000; // 13位 - print('\n如果是13位: $correctMs'); - final d4 = DateTime.fromMillisecondsSinceEpoch(correctMs); - print('结果: ${d4.year}-${d4.month}-${d4.day} ${d4.hour}:${d4.minute}'); -} diff --git a/scripts/test_detail_cache.dart b/scripts/test_detail_cache.dart deleted file mode 100644 index 2e253bf..0000000 --- a/scripts/test_detail_cache.dart +++ /dev/null @@ -1,100 +0,0 @@ -// 2026-04-13 | test_detail_cache.dart | 详情页缓存测试 | 验证304缓存响应问题 -// 运行: dart run scripts/test_detail_cache.dart - -import 'dart:convert'; -import 'dart:io'; - -const String baseUrl = 'https://eat.wktyl.com/api'; - -Future main() async { - print('=== 详情页缓存测试 ===\n'); - - // 测试ID - const testId = 27295; - - // 测试1: 正常请求(不带刷新) - print('📡 测试1: 正常请求 (id=$testId, 无刷新)'); - await testRequest(testId, forceRefresh: false); - - // 等待一下 - await Future.delayed(const Duration(seconds: 1)); - - // 测试2: 强制刷新 - print('\n📡 测试2: 强制刷新 (id=$testId, _refresh=1)'); - await testRequest(testId, forceRefresh: true); - - // 测试3: 使用另一个ID - print('\n📡 测试3: 测试另一个ID (id=46518)'); - await testRequest(46518, forceRefresh: false); - - print('\n=== 测试完成 ==='); -} - -Future testRequest(int id, {required bool forceRefresh}) async { - final client = HttpClient(); - try { - var url = '$baseUrl/api.php?act=full&id=$id'; - if (forceRefresh) { - url += '&_refresh=1'; - } - - print(' 请求URL: $url'); - - final uri = Uri.parse(url); - final request = await client.getUrl(uri); - - // 添加请求头 - request.headers.set('Accept', 'application/json'); - request.headers.set('User-Agent', 'MomKitchen/1.0'); - - final response = await request.close(); - - print(' 状态码: ${response.statusCode}'); - print(' 响应头:'); - response.headers.forEach((key, value) { - if (key.toLowerCase().contains('cache') || - key.toLowerCase().contains('etag') || - key.toLowerCase().contains('last-modified')) { - print(' $key: $value'); - } - }); - - final body = await response.transform(utf8.decoder).join(); - - if (response.statusCode == 304) { - print(' ⚠️ 收到304缓存响应'); - print(' 响应体长度: ${body.length}'); - if (body.isEmpty) { - print(' ❌ 缓存响应体为空!这就是问题所在'); - } - return; - } - - if (body.isEmpty) { - print(' ❌ 响应体为空'); - return; - } - - final json = jsonDecode(body) as Map; - print(' code: ${json['code']}'); - print(' message: ${json['message']}'); - print(' _cached: ${json['_cached']}'); - - final data = json['data'] as Map?; - if (data == null) { - print(' ❌ data字段为null'); - return; - } - - print(' ✅ 数据加载成功'); - print(' id: ${data['id']}'); - print(' title: ${data['title']}'); - print(' pic_id: ${data['pic_id']}'); - print(' cover: ${data['cover']}'); - } catch (e, stackTrace) { - print(' ❌ 请求错误: $e'); - print(' 堆栈: $stackTrace'); - } finally { - client.close(); - } -} diff --git a/scripts/test_discover_api.dart b/scripts/test_discover_api.dart deleted file mode 100644 index 0e6341c..0000000 --- a/scripts/test_discover_api.dart +++ /dev/null @@ -1,135 +0,0 @@ -// 2026-04-13 | test_discover_api.dart | API测试脚本 | 验证发现页API返回数据 -// 运行: dart run scripts/test_discover_api.dart - -import 'dart:convert'; -import 'dart:io'; - -const String baseUrl = 'https://eat.wktyl.com/api'; - -Future main() async { - print('=== 发现页API测试 ===\n'); - - // 测试1: 获取初始数据 - print('📡 测试1: 获取初始数据 (total=50)'); - final data1 = await fetchDiscover(50); - print(' 返回项目数: ${data1['items_count']}'); - print(' recipes: ${data1['recipes_count']}'); - print(' ingredients: ${data1['ingredients_count']}'); - print(' categories: ${data1['categories_count']}'); - print(' tags: ${data1['tags_count']}'); - print(' meal_times: ${data1['meal_times_count']}'); - - // 测试2: 再次获取数据,检查是否是新的随机数据 - print('\n📡 测试2: 再次获取数据 (total=50)'); - final data2 = await fetchDiscover(50); - print(' 返回项目数: ${data2['items_count']}'); - - // 测试3: 检查两次数据是否相同 - final ids1 = data1['recipe_ids'] as List; - final ids2 = data2['recipe_ids'] as List; - final sameIds = ids1.where((id) => ids2.contains(id)).length; - print(' 两次请求相同的recipe ID数量: $sameIds / ${ids1.length}'); - - // 测试4: 获取详情页数据 - if (ids1.isNotEmpty) { - final testId = ids1.first; - print('\n📡 测试3: 获取详情页数据 (id=$testId)'); - await testRecipeDetail(testId); - } - - // 测试5: 检查API返回的recipe结构 - print('\n📡 测试4: 检查recipe数据结构'); - if (data1['recipes'] != null && (data1['recipes'] as List).isNotEmpty) { - final recipe = (data1['recipes'] as List).first; - print(' recipe字段: ${recipe.keys.toList()}'); - print(' id: ${recipe['id']}'); - print(' title: ${recipe['title']}'); - print(' cover: ${recipe['cover']}'); - print(' category: ${recipe['category']}'); - print(' rating: ${recipe['rating']}'); - print(' views: ${recipe['views']}'); - } - - print('\n=== 测试完成 ==='); -} - -Future> fetchDiscover(int total) async { - final client = HttpClient(); - try { - final uri = Uri.parse( - '$baseUrl/api_discover.php?total=$total' - '&recipe=${(total * 0.35).round()}' - '&ingredient=${(total * 0.15).round()}' - '&category=${(total * 0.12).round()}' - '&tag=${(total * 0.18).round()}' - '&nutrition=${(total * 0.1).round()}' - '&meal_time=${(total * 0.1).round()}', - ); - - final request = await client.getUrl(uri); - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - final json = jsonDecode(body) as Map; - - if (json['code'] != 200) { - print(' ❌ API错误: ${json['message']}'); - return {}; - } - - final data = json['data'] as Map; - final recipes = (data['recipes'] as List?) ?? []; - final ingredients = (data['ingredients'] as List?) ?? []; - final categories = (data['categories'] as List?) ?? []; - final tags = (data['tags'] as List?) ?? []; - final mealTimes = (data['meal_times'] as List?) ?? []; - - return { - 'items_count': recipes.length + ingredients.length + categories.length + tags.length + mealTimes.length, - 'recipes_count': recipes.length, - 'ingredients_count': ingredients.length, - 'categories_count': categories.length, - 'tags_count': tags.length, - 'meal_times_count': mealTimes.length, - 'recipe_ids': recipes.map((r) => r['id'] as int).toList(), - 'recipes': recipes, - }; - } catch (e) { - print(' ❌ 请求错误: $e'); - return {}; - } finally { - client.close(); - } -} - -Future testRecipeDetail(int id) async { - final client = HttpClient(); - try { - // 测试 api.php?act=full&id=xxx - final uri = Uri.parse('$baseUrl/api.php?act=full&id=$id'); - final request = await client.getUrl(uri); - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - final json = jsonDecode(body) as Map; - - if (json['code'] != 200) { - print(' ❌ 详情API错误: ${json['message']}'); - return; - } - - final data = json['data'] as Map?; - if (data == null) { - print(' ❌ 详情数据为空'); - return; - } - - print(' ✅ 详情数据加载成功'); - print(' id: ${data['id']}'); - print(' title: ${data['title']}'); - print(' 字段数: ${data.keys.length}'); - print(' 主要字段: ${data.keys.take(10).toList()}'); - } catch (e) { - print(' ❌ 详情请求错误: $e'); - } finally { - client.close(); - } -}