Files
kitchen/CHANGELOG.md
Developer 3a056ca676 feat: 新增菜品对比和食物相生相克功能
- 新增菜品对比功能,支持1v1左右和1vN上下布局切换
- 新增食物相生相克查询工具,包含API服务和详情页面
- 优化平台工具类,移除冗余的鸿蒙系统检测逻辑
- 更新版本号至1.5.1,修改更新日志和版本说明
- 修复多个页面列表分隔符构建器参数警告
- 新增雷达图组件,用于展示多维度对比数据
- 新增可编辑数值组件,支持双击编辑和范围验证
- 优化导出按钮,增加对比功能入口
- 新增搜索引擎枚举工具类
- 更新应用路由配置,添加对比和相生相克相关页面
2026-05-01 09:13:27 +08:00

767 lines
32 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to this project will be documented in this file.
## [0.99.85] - 2026-05-01
### 🐛 修复: ComparisonController注册方式统一 + 硬编码颜色替换
#### 问题1: 注册方式不一致
`ComparisonController` 使用 `Get.put(permanent: true)` 注册,而其他功能控制器均使用 `Get.lazyPut(fenix: true)`
#### 问题2: 硬编码颜色值
`slotColors` 使用硬编码颜色值(`Color(0xFF007AFF)` 等),未使用 DesignTokens 令牌。
#### 修复
| 文件 | 变更 |
|------|------|
| `app_binding.dart` | `Get.put(ComparisonController(), permanent: true)``Get.lazyPut(() => ComparisonController(), fenix: true)` |
| `comparison_controller.dart` | `slotColors` 硬编码颜色替换为 `DesignTokens.primary/orange/green/purple` |
#### 颜色映射
| 槽位 | 修复前 | 修复后 |
|------|--------|--------|
| 0 | `Color(0xFF007AFF)` | `DesignTokens.primary` |
| 1 | `Color(0xFFFF9500)` | `DesignTokens.orange` |
| 2 | `Color(0xFF34C759)` | `DesignTokens.green` |
| 3 | `Color(0xFFAF52DE)` | `DesignTokens.purple` |
> 槽位3颜色从 iOS 系统紫(0xFFAF52DE)调整为 DesignTokens.purple(0xFF9C27B0),与设计系统统一
## [0.99.84] - 2026-05-01
### 🐛 修复: 对比控制器超时保护补全
#### 问题
`ComparisonController``addDish``loadSlotDetail``loadIngredientSlotDetail` 三个方法调用 API 时未设置超时,可能导致长时间阻塞(与已修复的 `addIngredient` 超时问题一致)。
#### 修复内容
| 方法 | 修复 |
|------|------|
| `addDish` | `_repo.fetchDetail` 增加 5 秒超时,超时后使用源数据 |
| `loadSlotDetail` | `_repo.fetchDetail` 增加 5 秒超时,超时后跳过加载 |
| `loadIngredientSlotDetail` | `_repo.fetchIngredientDetail` 增加 5 秒超时,超时后跳过加载 |
#### 修改文件
| 文件 | 变更 |
|------|------|
| `comparison_controller.dart` | 三处 API 调用添加 `.timeout(Duration(seconds: 5))``TimeoutException` 处理 |
## [0.99.83] - 2026-05-01
### ✨ 新功能: 相生相克Sheet二次搜索+无结果说明
#### 交互流程
```
点击「🔗 相生相克」
→ 搜索当前食材名
→ 有结果:显示列表
→ 无结果:显示「📋 查看相关推荐」按钮
→ 点击后截断关键词重搜
→ 有结果:显示列表
→ 无结果:显示「暂无相生相克记录」+ 说明卡片
```
#### 无结果说明卡片内容
| emoji | 说明 |
|-------|------|
| 💡 | 并非所有食材都有相生相克的传统记载。部分食材因食用历史、搭配习惯等因素,尚未形成明确的相生相克说法 |
| 🔬 | 相生相克多源于中医食疗理论,现代营养学对此尚无统一定论。食材搭配宜根据个人体质和健康状况灵活调整 |
| 🥗 | 日常饮食建议遵循「多样均衡、适量搭配」的原则,不必过度拘泥于相生相克的说法 |
#### 修改文件
| 文件 | 变更 |
|------|------|
| `ingredient_detail_page.dart` | 新增 `_hasSearchedRelated` 状态、`_buildNoCompatSection()``_noCompatTip()` |
## [0.99.82] - 2026-05-01
### 🎨 UI调整: 食材详情页按钮布局优化
#### 变更前
```
┌─────────────────────┐
│ 🔗 相生相克 │ ← 全宽渐变按钮(单独一行)
├─────────────────────┤
│ [返回首页] [🥬食材对比] │ ← 左右对齐
└─────────────────────┘
```
#### 变更后
```
┌───────────┬──────────┐
│ 🔗相生相克 │ 🥬食材对比 │ ← 左右等宽对齐,去掉返回首页
└───────────┴──────────┘
```
#### 修改内容
- 删除「返回首页」按钮
- 「🔗 相生相克」与「🥬 食材对比」改为 Row 左右等宽并排
## [0.99.81] - 2026-05-01
### ✨ 新功能: 相生相克详情页 AppBar 新增免责声明入口
#### 变更内容
| 项目 | 说明 |
|------|------|
| 位置 | `CupertinoNavigationBar` 右侧 trailing |
| 图标 | `CupertinoIcons.info_circle` () |
| 交互 | 点击弹出 `CupertinoActionSheet` 免责声明 |
#### 免责声明内容5 条)
| emoji | 标题 | 内容摘要 |
|-------|------|----------|
| 🌐 | 数据来源 | 部分数据来源于网络公开资料(中医药典籍、营养学文献、健康网站等) |
| 📚 | 民间整理 | 部分来自民间饮食经验收集整理,未经系统科学验证 |
| ⚗️ | 科学依据 | 相生相克可能缺乏现代医学/营养学充分科学依据,仅供参考,不构成医疗建议 |
| 🛡️ | 自行辨别 | 结合自身健康状况理性判断,特殊体质/过敏史/疾病请咨询专业医师或营养师 |
| 📝 | 版权声明 | 仅作信息聚合展示,不对准确性、完整性做保证,如有侵权请联系删除 |
#### 修改文件
| 文件 | 变更 |
|------|------|
| `food_compat_detail_page.dart` | 新增 `_showDisclaimer()` + `_disclaimerItem()` + AppBar trailing |
## [0.99.80] - 2026-05-01
### 🎨 UI调整: 删除搜索框,底部按钮上移至详情头部下方
#### 变更前
| 位置 | 组件 |
|------|------|
| 顶部(导航栏下) | 🔍 搜索食材输入框 |
| 底部(列表末尾) | 🔗相生相克 + 返回首页 + 🥬食材对比 |
#### 变更后
| 位置 | 组件 |
|------|------|
| 顶部(导航栏下) | DetailHeaderCard食材名+分类+热量) |
| 头部卡片下方 | 🔗相生相克(渐变按钮)+ 返回首页 + 🥬食材对比 |
#### 修改内容
- 删除 `_buildSearchBar()` 方法及其在 `build()` 中的调用
-`_buildCompatButton` + `Row(返回首页, 食材对比)` 从 ListView 末尾移至 `DetailHeaderCard` 之后
- 简化布局:`SafeArea → _buildContent`(去掉中间 Column 层)
## [0.99.79] - 2026-05-01
### ✨ 新功能: 食材详情页新增「相生相克」按钮 + Sheet弹窗
#### 变更前
| 项目 | 状态 |
|------|------|
| 搜索框 | 调用API实时搜索双区展示(本地食材+相生相克) |
| 交互方式 | 输入框内搜索 |
#### 变更后
| 项目 | 说明 |
|------|------|
| 搜索框 | 恢复为本地食材过滤 |
| 新增按钮 | 🔗 相生相克(橙红渐变按钮,位于详情页底部) |
| 交互方式 | 点击按钮 → 弹出 Sheet → 自动搜索当前食材名称 |
| Sheet内容 | 标题栏(食材名+关闭按钮)+ 结果列表 |
| 结果卡片 | 相生绿色边框🤝 / 相克红色边框⚠️ / 描述摘要 |
| 无结果处理 | 显示"未找到记录"+「查看相关推荐」按钮(截断关键词重搜) |
| 错误处理 | 网络失败显示⚠️+「重试」按钮 |
| 点击跳转 | 点击结果 → 关闭Sheet → `toolsFoodCompatDetail` 详情页 |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `ingredient_detail_page.dart` | 删除`_buildCompatibilityCard`/双区展示;新增`_buildCompatButton`+`_showCompatibilitySheet`+`_CompatSheetContent`(StatefulWidget) |
## [0.99.78] - 2026-05-01
### ✨ 新功能: 食材详情页搜索框改为相生相克搜索
#### 变更前
| 项目 | 状态 |
|------|------|
| 搜索框 | 空壳UI只过滤本地食材列表 |
| placeholder | "搜索食材..." |
#### 变更后
| 项目 | 说明 |
|------|------|
| 搜索框 | 调用 `FoodCompatibilityService.search()` API 实时搜索 |
| placeholder | "搜索相生相克..." |
| 防抖 | 400ms 防抖,避免频繁请求 |
| 双区展示 | 上方"📦 本地食材" + 下方"🔗 相生相克" |
| 结果卡片 | 相生绿色边框🤝 / 相克红色边框⚠️ / 浏览量 / 描述摘要 |
| 点击跳转 | 点击结果 → `toolsFoodCompatDetail` 详情页 |
| 加载状态 | 搜索中显示 CupertinoActivityIndicator |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `ingredient_detail_controller.dart` | 新增 `compatibilityResults`/`isSearchingCompatibility`/`searchCompatibility()` |
| `ingredient_detail_page.dart` | 搜索框改为API搜索+双区展示+`_buildCompatibilityCard`+点击跳转详情 |
## [0.99.77] - 2026-05-01
### 🐛 修复: 条形图左侧坐标值不切实际
#### 问题
| 问题 | 原因 |
|------|------|
| 只显示一个值 | leftTitles 把 Y 轴坐标(0-100)误当组索引查 label |
| 数值不切实际 | 每组独立归一化,不同组柱高不可横向比较 |
#### 修复方案
| 变更项 | 说明 |
|--------|------|
| 全局最大值归一化 | `_globalMax` 遍历所有食材×所有指标取全局最大值,所有组共享同一比例尺 |
| Y轴刻度 5档 | `interval: 25`,显示 100→75→50→25→0 对应的实际数值 |
| 从上到下递减 | fl_chart Y 轴原生从大到小排列,无需额外处理 |
| 数值格式化 | ≥1000 显示为 `1.2k`,整数去掉小数点 |
#### 效果示例百合100 vs 花椒50
```
100 ← 全局最大值
75
50
25
0
┃███████┃█████┃ 热量
┃█████┃██┃ 蛋白质
...
```
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `comparison_bar_chart.dart` | 新增 `_globalMax`/`_formatAxisValue`leftTitles 改为 Y 轴刻度模式 |
## [0.99.76] - 2026-05-01
### 🐛 修复: 点击添加食材等待30秒 + 条形图左侧无坐标值
#### 问题130秒延迟
| 问题 | 原因 |
|------|------|
| 添加食材卡30秒 | `fetchIngredientDetail` 无超时限制API慢/不可达时一直await |
**修复**:所有 `fetchIngredientDetail` 调用增加 `.timeout(Duration(seconds: 5))`,超时后使用源数据继续
#### 问题2条形图左侧无坐标值
| 问题 | 原因 |
|------|------|
| 左侧空白 | `leftTitles: SideTitles(showTitles: false)` 被关闭 |
**修复**:启用 leftTitles显示每组最大值+单位(如 100kcal、5g
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `comparison_controller.dart` | fetchIngredientDetail 增加 5 秒超时 + TimeoutException 处理 |
| `comparison_bar_chart.dart` | leftTitles 启用,显示每组最大值+单位 |
## [0.99.75] - 2026-05-01
### 🐛 修复: 食材对比页面营养数据全为0
#### 根因
| 问题 | 原因 |
|------|------|
| 详情页有数据对比页为0 | nutrition 数据经多层传递详情→addIngredient→slots→_parseNutritionValue任一环节丢失即解析为0 |
| 本地DB未兜底 | `_parseNutritionValue` 解析失败直接返回0没有从 IngredientNutritionDb 补充 |
#### 修复方案
| 变更项 | 说明 |
|--------|------|
| 新增 _resolveNutrition | 优先从 nutrition 字符串解析 → 失败则从 IngredientNutritionDb 直接查询数值 |
| 移除 hasNutrition 守卫 | 不再因任一食材无 nutrition 就隐藏整个图表 |
| 三层保障 | ①源数据字符串 ②本地DB精确匹配 ③分类fallback默认值 |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `ingredient_comparison_page.dart` | 新增 `_resolveNutrition` 方法解析失败→IngredientNutritionDb兜底 |
## [0.99.74] - 2026-05-01
### 🐛 修复: 食材对比条形图只显示一个食材
#### 根因
| 问题 | 原因 |
|------|------|
| 只显示一个条形 | 第二个食材营养值全为0`toY=(0/max)*100=0`柱子高度为0不可见 |
#### 修复方案
| 变更项 | 说明 |
|--------|------|
| 零值最小高度 | `toY` 从 0 改为 3最小可见高度 |
| 零值半透明 | 无数据时颜色 `alpha: 0.3`,区分有/无数据 |
| tooltip优化 | 零值显示"暂无数据"而非"0" |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `comparison_bar_chart.dart` | `_buildBarGroups()`零值给最小高度3+半透明色tooltip显示暂无数据 |
## [0.99.73] - 2026-05-01
### 🐛 修复: 食材对比营养数据空白
#### 根因
| 问题 | 原因 |
|------|------|
| 营养对比全空白 | `addIngredient`始终调用APIAPI不返回nutrition字段覆盖了详情页传入的完整数据 |
| 选择面板食材无营养 | 搜索/浏览记录/快速选择的食材无nutritionAPI也补不上 |
#### 修复方案
| 变更项 | 说明 |
|--------|------|
| 源数据优先策略 | `addIngredient`检测传入食材是否已有nutrition有则锁定不被API覆盖 |
| API仅补充缺失字段 | 有营养数据时API只补充image/categoryName等nutrition保持源数据 |
| 无营养时本地补充 | 从选择面板添加的食材无nutritionmerge后从`IngredientNutritionDb`补充 |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `comparison_controller.dart` | `addIngredient`重构双路径有nutrition→直接用+API补充无nutrition→API+本地DB兜底 |
| `comparison_pick_sheet.dart` | `Get.back()``Navigator.pop()`+isCurrent检查修复选择后返回上一页 |
## [0.99.72] - 2026-05-01
### 🐛 修复: 食材对比选择后自动返回上一页
#### 核心变更
| 变更项 | 说明 |
|--------|------|
| 选择面板关闭逻辑 | `Get.back()` 替换为 `Navigator.of(context).pop()` + `ModalRoute.isCurrent` 检查 |
| 防止误关对比页 | 异步addIngredient/addDish完成后先确认底部弹窗仍在栈顶再关闭避免关闭对比页面 |
| 食材详情→对比数据 | 优先从本地缓存读取食材数据营养数据从IngredientNutritionDb补充 |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `comparison_pick_sheet.dart` | 新增`_closeSheet()`方法7处`Get.back()`全部替换,增加`isCurrent`安全检查 |
| `ingredient_detail_page.dart` | `_navigateToComparison`优先本地缓存,`_enrichWithLocalNutrition`补充营养数据 |
## [0.99.71] - 2026-05-01
### 🆕 新增: 食材详情页增加食材对比入口
#### 核心变更
| 变更项 | 说明 |
|--------|------|
| 食材详情页对比按钮 | 新增「🥬 食材对比」按钮,与「返回首页」左右对齐 |
| 对比跳转+预填 | 点击后跳转食材对比页面,当前食材自动填入第一个槽位 |
| 食材选择面板重构 | ComparisonPickSheet浏览记录Tab区分菜品/食材食材模式使用IngredientCacheService |
| 个人中心入口 | profile_home 中「点餐助手」改为「菜品对比」,跳转对比页面 |
| 工具面板收起 | tools_panel_widget 底部「首页」按钮改为「收起」,点击关闭面板 |
| 食物相克修复 | 列表被AppBar遮住(SafeArea)、相关推荐不再堆叠页面 |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `ingredient_detail_page.dart` | 新增食材对比按钮+预填逻辑优先使用API详情数据 |
| `comparison_pick_sheet.dart` | 浏览记录Tab区分菜品/食材食材模式使用IngredientCacheService |
| `comparison_controller.dart` | 新增browseHistoryIngredients属性从IngredientCacheService获取食材浏览记录 |
| `profile_home.dart` | 点餐助手→菜品对比,图标和路由更新 |
| `tools_panel_widget.dart` | 首页按钮→收起按钮,仅关闭面板 |
| `food_compat_page.dart` | SafeArea(top:false)→SafeArea(),修复列表被遮住 |
| `food_compat_detail_page.dart` | 相关推荐改为当前页面重新加载移除Get依赖 |
## [0.99.70] - 2026-05-01
### 🆕 新增: 食物相生相克工具Flutter页面+路由+工具注册)
#### 核心变更
| 变更项 | 说明 |
|--------|------|
| 食物相生相克主页面 | 新增 FoodCompatPage支持搜索、分类筛选、热门标签、分页加载 |
| 食物相生相克详情页面 | 新增 FoodCompatDetailPage展示详情、复制+搜索、上下条导航、相关推荐 |
| 工具注册 | 在 ToolRegistry.defaultTools 中注册食物相生相克工具项 |
| 路由注册 | 新增 /tools/food-compatibility 和 /tools/food-compatibility/detail 路由 |
| API服务 | FoodCompatibilityService 封装列表/详情/分类/搜索/热门/随机接口 |
#### 新增文件
| 文件 | 职责 |
|------|------|
| `lib/src/pages/tools/health/food_compat/food_compat_page.dart` | 食物相生相克主页面(列表+搜索+分类+热门) |
| `lib/src/pages/tools/health/food_compat/food_compat_detail_page.dart` | 食物相生相克详情页面(详情+复制搜索+导航+相关推荐) |
| `lib/src/services/data/business/food_compatibility_service.dart` | API服务 + 数据模型FoodItem/FoodDetail/FoodCategory/FoodListResult |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `tool_item_model.dart` | 新增 food_compatibility 工具项health分类瀑布流展示 |
| `app_routes.dart` | 新增 toolsFoodCompat 和 toolsFoodCompatDetail 路由及PageInfo |
#### 页面功能
- 🔍 搜索:输入关键词搜索食物相生相克关系
- 📂 分类:按相生/相克/蔬菜/水果等分类筛选
- 🔥 热门标签:一键查看热门食物,支持换一批
- 📄 分页:自动加载更多,滚动到底部触发
- 📋 复制+搜索:详情页支持复制信息和跳转搜索引擎
- ⬅️➡️ 导航:上一条/下一条快速切换
- 🔗 相关推荐:查看相关食物搭配
---
## [0.99.69] - 2026-05-01
### 🔧 重构: SearchEngine枚举和搜索布局提取为公开组件
#### 核心变更
| 变更项 | 说明 |
|--------|------|
| SearchEngine枚举提取 | 从 recipe_email_button.dart 提取到独立公开文件 search_engine.dart |
| 搜索引擎面板提取 | 新增 SearchEngineSheet 公开组件,支持任意页面调用搜索引擎选择 |
| 复制+搜索操作栏 | 新增 CopyAndSearchBar 公开组件,一键复制+跳转搜索 |
| HTML预览增强 | 食物相生相克详情sheet底部增加「📋 复制信息」和「🔍 去搜索」按钮 |
| 搜索引擎菜单 | HTML预览新增搜索引擎选择面板百度/Bing/谷歌/搜狗) |
#### 新增文件
| 文件 | 职责 |
|------|------|
| `lib/src/utils/search_engine.dart` | SearchEngine枚举百度/Bing/谷歌/自定义),公开可用 |
| `lib/src/widgets/common/search_engine_sheet.dart` | 搜索引擎选择面板 + SearchEngineCard + CopyAndSearchBar |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `recipe_email_button.dart` | 移除内联SearchEngine枚举和_SearchEngineDialog改用公开组件 |
| `food_compatibility.html` | 详情sheet底部增加复制+去搜索按钮,新增搜索引擎选择面板 |
| `food_compat_query.html` | 详情页底部增加复制+去搜索按钮,新增搜索引擎选择面板 |
#### 公开组件使用方式
```dart
// 弹出搜索引擎选择Sheet
SearchEngineSheet.show(context, '搜索关键词');
// 复制+搜索操作栏
CopyAndSearchBar(copyText: '复制内容', searchKeyword: '搜索词');
// 单个搜索引擎卡片
SearchEngineCard(engine: SearchEngine.baidu, keyword: '搜索词');
```
---
## [0.99.68] - 2026-05-01
### 🆕 对比功能升级2~4 槽位 + fl_chart 可视化 + 双击编辑
#### 核心变更
| 变更项 | 说明 |
|--------|------|
| 槽位扩展 | 对比从固定 2 槽位升级为 2~4 动态槽位 |
| 动态布局 | 1v1 左右对齐 / 1vN 上下排列自动切换 |
| fl_chart 可视化 | 新增 ComparisonBarChart分组柱状图+ ComparisonRadarChart雷达图 |
| 双击编辑 | 核心营养数值支持双击编辑,带范围验证和编辑标识 |
| 颜色编码 | 4 个槽位分别用蓝/橙/绿/紫颜色标识 |
#### 新增文件
| 文件 | 职责 |
|------|------|
| `lib/src/widgets/comparison/comparison_bar_chart.dart` | 营养对比分组柱状图 (fl_chart BarChart) |
| `lib/src/widgets/comparison/comparison_radar_chart.dart` | 综合雷达图 (fl_chart RadarChart) |
| `lib/src/widgets/comparison/comparison_layout.dart` | 通用对比布局 (1v1左右 / 1vN上下) |
| `lib/src/widgets/comparison/comparison_editable_value.dart` | 可编辑数值组件 (双击+验证+标识) |
#### 修改文件
| 文件 | 变更内容 |
|------|----------|
| `comparison_controller.dart` | maxSlots 2→4新增编辑状态管理 (editedValues/editedKeys),颜色编码 |
| `dish_comparison_page.dart` | 重构为动态多槽位,集成图表组件和可编辑数值 |
| `ingredient_comparison_page.dart` | 重构为动态多槽位,集成图表组件 |
| `comparison_pick_sheet.dart` | 适配多槽位选择,显示槽位颜色标识 |
#### 交互特性
- 🔀 1v1 模式:左右对比 + 交换按钮 + VS 徽章
- 📊 1vN 模式:上下对比 + 颜色进度条 + 添加按钮
- ✏️ 双击营养数值进入编辑Enter/✅ 确认,❌ 取消
- 🏷️ 编辑后显示圆点标识 + 铅笔图标
---
## [0.99.67] - 2026-04-25
### 🎨 新增: 鸿蒙(HarmonyOS)分层图标
#### 问题
- 华为应用审核警告:`应用未配置图标的前景图和后景图标准要求尺寸1024px*1024px`
- 原因:使用单层 `app_icon.png`,未采用鸿蒙分层图标规范
#### 解决方案
按照华为UX设计规范生成 **foreground + background** 双层图标:
| 文件 | 尺寸 | 说明 |
|------|------|------|
| `background_icon.png` | 1024×1024 | 纯色不透明背景 RGB(237,189,109) 金黄色 |
| `foreground_icon.png` | 1024×1024 | 透明PNG图标主体内容含圆角遮罩 |
| `layered_image.json` | - | 分层图标配置文件 |
#### 配置变更
- `AppScope/app.json5`: `"icon"``$media:layered_image`
- `entry/src/main/module.json5`: `"icon"` + `"startWindowIcon"``$media:layered_image`
#### 生成位置
```
ohos/AppScope/resources/base/media/
├── background_icon.png # 背景层
├── foreground_icon.png # 前景层
├── layered_image.json # 分层配置
└── app_icon.png # (保留)
ohos/entry/src/main/resources/base/media/
├── background_icon.png
├── foreground_icon.png
├── layered_image.json
└── icon.png # (保留)
```
#### 工具脚本
- [gen_hmos_icons.py](scripts/gen_hmos_icons.py): 从源图标自动提取前景/背景层
#### 🔧 修复: layered_image.json 格式
- **问题**: 初始生成的 JSON 格式不符合华为规范,警告仍存在
- **原因**: 缺少外层 `"layered-image"` 包装键
- **修复前** (❌):
```json
{ "foreground": "...", "background": "...", "size": {...} }
```
- **修复后** (✅):
```json
{ "layered-image": { "foreground": "$media:foreground_icon", "background": "$media:background_icon" } }
```
---
## [0.99.47] - 2026-04-25
### 🐛 修复: PDF导出中文乱码 + 封面图片缺失
#### 问题1: 中文显示为空白/乱码
- **症状**: PDF导出后中文全部不显示只有线条和序号文件大小仅 6KB
- **根因链**:
1. 原字体为 **OpenType/CFF (.otf)** 格式 → pdf 包不识别为 Unicode 字体
2. 使用 fontTools 转换 .otf → .ttf 后 **缺少 `loca` (Location) 表**
3. `loca` 表存储字形偏移索引 → 缺失导致 PDF 无法定位中文字形 → 空白
- **诊断过程**:
- 创建 `scripts/test_pdf_cjk_debug.dart` 检测字体格式
- 创建 `scripts/check_ttf_integrity.py` 验证字体表完整性
- 发现转换后的 TTF 有 glyf 但**无 loca 表**
- **最终方案**: 从 GitHub [cjk-fonts-ttf](https://github.com/life888888/cjk-fonts-ttf) 下载**官方 TrueType 版本**
- NotoSansCJKsc-Regular.ttf (16.9MB, 65533 字形, glyf+loca 完整)
- NotoSansCJKsc-Bold.ttf (16.8MB, 同上)
- **验证结果**: PDF 从 7KB → **22.5KB** (中文字形成功嵌入),中文正常显示
#### 问题2: 封面图片不显示 (缓存有图但PDF无图)
- **症状**: 菜品详情页已显示封面图(缓存命中),但 PDF 导出无图片
- **根因**: `_getCachedCoverImage()` 仅用 `recipe.cover.toString()` 查缓存,与 **RecipeImage 实际使用的 URL 不匹配**
- RecipeImage 使用 URL 链: `_ensureHttps(cover)` → `pic/{picId}a.jpg` → `pic/{picId}b.jpg` → fallback
- 若 cover 为相对路径 `/pic/123a.jpg` 或 HTTP URL缓存 key 不同 → **查不到**
- **修复**: 重写为**多候选 URL 链 + 缓存/下载双通道**
- 构建 3~5 个候选 URL与 RecipeImage._buildUrlChain 一致)
- 每个 URL 先查缓存,未命中则网络下载
- 任一成功即返回,全部失败才放弃
#### 文件变更
| 文件 | 操作 | 说明 |
|------|------|------|
| `assets/fonts/NotoSansSC-Regular.ttf` | 替换 | 官方TTF版本(16.9MB)替换损坏的fontTools转换版(3.6MB) |
| `assets/fonts/NotoSansSC-Bold.ttf` | 替换 | 同上 |
| `recipe_export_button.dart` | 修改 | 图片加载重写: URL链(与RecipeImage一致) + 缓存/下载双通道 + 详细日志 |
| `scripts/test_pdf_app_flow.dart` | 新增 | App端完整PDF流程模拟脚本 |
| `scripts/check_ttf_integrity.py` | 新增 | 字体完整性验证脚本(glyf/loca/CJK映射) |
#### 保留的诊断工具
- `scripts/test_pdf_cjk_debug.dart` — 字体格式检测 + PDF生成测试
- `scripts/otf_to_ttf.py`, `otf_to_ttf_v2.py` — OTF→TTF转换脚本(备用)
## [0.99.46] - 2026-04-25
### 🐛 修复: PDF导出中文崩溃 "Invalid argument (string): Contains invalid characters"
- **错误**: 导出含中文菜谱PDF时崩溃`Invalid argument (string): Contains invalid characters`
- **根因链**:
1. pdf 包的 `TtfParser.unicode` 仅当字体文件头为 `0x00010000` (TrueType) 时返回 `true`
2. 当前使用的 NotoSansSC 字体是 **OpenType/CFF (.otf)** 格式,文件头为 `0x4F54544F` ('OTTO')
3. `unicode=false` → pdf 包使用 **latin1 编码** → 中文字符超出范围 → 崩溃
- **诊断工具**: 新增 `scripts/test_pdf_cjk_debug.dart` 脚本检测字体格式和PDF生成能力
- **解决方案**: 使用 Python fontTools 将 .otf (CFF轮廓) 转换为 .ttf (TrueType轮廓)
- 转换脚本: `scripts/otf_to_ttf.py` (CFF→glyf字形转换 + 文件头修补)
- 13086个CJK字形全部成功转换
- **文件变更**:
- `assets/fonts/NotoSansSC-Regular.ttf` — 新增(从.otf转换
- `assets/fonts/NotoSansSC-Bold.ttf` — 新增(从.otf转换
- `recipe_export_button.dart` — 字体路径 .otf → .ttf
- `scripts/otf_to_ttf.py` — 新增OTF→TTF转换脚本
- `scripts/test_pdf_cjk_debug.dart` — 新增PDF CJK诊断脚本
## [0.99.45] - 2026-04-25
### 📄 PDF导出重构: 替换 docs_gee.PdfGenerator → pdf ^3.12.0 (纯Dart包)
- **问题**: docs_gee 自带的 PdfGenerator 导出PDF内容空白中文字体渲染异常
- **方案**: 引入成熟的 `pdf` 包Dart生态最流行的PDF生成库GitHub 2k+ stars
- **适配**: 纯Dart包仅改版本号 `3.12.0` → `3.12.0-ohos.1`无需ohos目录
- **依赖冲突解决**: pdf→barcode→qr ^3.0.0 与本地qr包冲突通过 dependency_overrides 解决
- **代码变更**:
- 新增 import: `package:pdf/pdf.dart`, `package:pdf/widgets.dart as pw`
- 重写 `_exportAsPdf()`: 使用 pw.Document + pw.MultiPage API
- 新增辅助方法: `_buildPdfTitle()`, `_pdfText()`, `_buildPdfIngredients()`, `_buildPdfSteps()`, `_buildPdfTags()`
- 支持封面图片、中文TTF字体、自动分页、页脚、食材表格、步骤编号圆圈、标签胶囊样式
- **文件变更**:
- `packages/pdf/` — 新增(从 GitHub 克隆 dart_pdf
- `lib/src/widgets/recipe_detail/interaction/recipe_export_button.dart` — 重写PDF导出逻辑
- `pubspec.yaml` — 添加 pdf 本地依赖 + qr dependency_overrides
## [0.99.44] - 2026-04-25
### 🔴 紧急修复: 鸿蒙端启动闪退 (Invalid relative path 9001005) — 全面排查
- **错误**: `Error message: Invalid relative path, Error code: 9001005`
- **堆栈**: `copyResource → startInitialization → checkLoader → setupFlutterEngine → onCreate`
- **根因**: 9个纯Dart包被错误添加了ohos目录引用不存在的`libs/flutter.har`Flutter引擎启动时逐一扫描全部模块资源
- **排查过程**:
1. 删除 docs_gee/ohos第一轮→ 仍闪退
2. 全量扫描所有 packages 的 ohos-package.json5 → 发现共12个包有ohos目录
3. 逐个检查 Plugin 代码 → 区分空壳 vs 真插件
#### 删除的纯Dart空壳包 (9个)
| 包名 | Plugin代码 | 原因 |
|------|-----------|------|
| docs_gee | `DocsGeePlugin {}` | 纯Dart文档生成库 |
| mailer | `MailerPlugin {}` | 纯Dart邮件库 |
| qr | `QrPlugin {}` | 纯Dart二维码生成 |
| flutter_card_swiper | `FlutterCardSwiperPlugin {}` | 纯Dart卡片滑动组件 |
| flutter_markdown_plus | `FlutterMarkdownPlusPlugin {}` | 纯Dart Markdown渲染 |
| cached_network_image | `CachedNetworkImagePlugin {}` | 纯Dart网络图片缓存 |
| flutter_staggered_grid_view | `FlutterStaggeredGridViewPlugin {}` | 纯Dart瀑布流布局 |
| badges | `BadgesPlugin {}` | 纯Dart徽章组件 |
| fl_chart | `FlChartPlugin {}` | 纯Dart图表库 |
#### 修复的真插件 (3个)
| 包名 | 问题 | 修复 |
|------|------|------|
| mobile_scanner | 缺 libs/flutter.har (Camera/Barcode真实原生代码) | 从 fluttertoast_ohos 复制 har 文件到 libs/ |
| file_picker | 缺 har/flutter.har (文件选择器原生代码) | 创建 har/ 目录并复制 flutter.har |
| fluttertoast_ohos | 原始正确,无需修改 | ✅ 已有 libs/flutter.har |
#### 清理的example目录 (3个)
- get/example_nav2/ohos, get/example/ohos, file_picker/example/ohos不参与主项目编译
#### 最终保留的ohos目录 (仅3个真插件)
```
packages/
├── mobile_scanner/ohos/ ← libs/flutter.har ✅
├── file_picker/ohos/ ← har/flutter.har ✅
└── fluttertoast_ohos/ohos/ ← libs/flutter.har ✅
```
### 📝 文档更新: 修正纯Dart包适配指导
- **文件**: `packages/本地已适配鸿蒙的库.md`
- **修正内容**:
1. 删除「创建空壳 ohos 目录」的完整模板Index.ets / Plugin.ets / module.json5 等)
2. 纯Dart包适配步骤简化为拉取源码 → 改版本号 → 引用项目 → 完成
3. 新增「⛔ 血泪教训」章节,记录 9001005 闪退根因和排查过程
4. 总览表新增「类型」「ohos目录」列明确区分 🟢纯Dart vs 🔴原生插件
5. 兼容性总览表新增类型列,补充 file_picker / fluttertoast_ohos 条目
- **核心原则**: 纯Dart包 = 不需要任何ohos文件Flutter AOT编译直接运行
---
## [0.99.43] - 2026-04-25
### 🐛 修复 — 3项关键Bug鸿蒙端
#### 🔴 修复: 鸿蒙端启动闪退 (Invalid relative path 9001005)
- **错误**: `Error message: Invalid relative path, Error code: 9001005`
- **堆栈**: `copyResource → startInitialization → checkLoader → setupFlutterEngine → onCreate`
- **根因**: `docs_gee` 是纯Dart库但被错误配置为鸿蒙原生har模块
- `ohos/oh-package.json5` 引用不存在的路径 `"@ohos/flutter_ohos": "file:libs/flutter.har"`
- Flutter引擎启动时尝试复制该模块资源 → 路径不存在 → 闪退
- **修复**: 删除 `packages/docs_gee/docx_generator/ohos/` 整个目录
- 纯Dart库不需要原生适配删除后不再触发资源复制
- **教训**: 纯Dart包**禁止**添加 ohos 目录,会导致启动阶段崩溃
- **文件**: `packages/docs_gee/docx_generator/ohos/` (已删除)
#### 🟠 修复: 鸿蒙端文件选择器不弹出
- **问题**: 导出PDF/Word时点击保存 → 不弹选择器 → 直接报"已取消保存"
- **根因**: `FilePickerPlugin.ets` 第101行读取了 bytes 参数但未传给 delegate
- `delegate.saveFile()` 检查 `this.bytesList` 为空 → 报错返回 null
- **修复**:
- `FilePickerPlugin.ets`: 调用 saveFile 前调用 `delegate?.setSaveBytes(fileName, [bytes])`
- `FilePlckerDelegate.ets`: 新增 `setSaveBytes()` 方法设置 savaFilePath 和 bytesList
- **文件**: `packages/file_picker/ohos/src/main/ets/components/plugin/FilePickerPlugin.ets`
- **文件**: `packages/file_picker/ohos/src/main/ets/components/plugin/FilePlckerDelegate.ets`
#### 🔴 修复: PDF全空白(10MB)
- **问题**: 导出PDF文件大小10MB但内容完全空白
- **根因**: CIDFont + Identity-H 编码的字体不能用 `(text)` 格式输出 Tj 操作符
- WinAnsiEncoding 的括号字符串语法与 Identity-H 十六进制编码冲突
- PDF阅读器无法解析 → 显示空白页
- **修复**:
- 新增 `_toHexEncodedText(text)`: 将字符串转为 UTF-16BE 十六进制
- 新增 `_formatTextForTj(text)`: 有嵌入字体用 `<hex>`,无嵌入用 `(text)`
- 修改 `_renderParagraph()` 和表格渲染中的 Tj 输出
- **验证**: `scripts/test_pdf_export_full.dart` 全流程验证脚本(7项测试)
- **文件**: `packages/docs_gee/docx_generator/lib/src/pdf_generator.dart`
---