已开启
提交 NutriLog(智能饮食记录与营养管理)APPDEMO #474
chn9528创建于 28 天前
提交 NutriLog(智能饮食记录与营养管理)APPDEMO #474
已开启
共 220 个文件变更+10436-0
| @@ -0,0 +1,14 @@ | |||
| 1 | +/node_modules | ||
| 2 | +/oh_modules | ||
| 3 | +/local.properties | ||
| 4 | +/.idea | ||
| 5 | +**/build | ||
| 6 | +/.hvigor | ||
| 7 | +.cxx | ||
| 8 | +/.clangd | ||
| 9 | +/.clang-format | ||
| 10 | +/.clang-tidy | ||
| 11 | +**/.test | ||
| 12 | +/.appanalyzer | ||
| 13 | +# Code Scanner cache | ||
| 14 | +.codescanner/ | ||
| @@ -0,0 +1,11 @@ | |||
| 1 | +{ | ||
| 2 | + "app": { | ||
| 3 | + "bundleName": "com.example.NutriLog", | ||
| 4 | + "vendor": "example", | ||
| 5 | + "versionCode": 1000000, | ||
| 6 | + "versionName": "1.0.0", | ||
| 7 | + "buildVersion": "1", | ||
| 8 | + "icon": "$media:layered_image", | ||
| 9 | + "label": "$string:app_name" | ||
| 10 | + } | ||
| 11 | +} | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | +{ | ||
| 2 | + "string": [ | ||
| 3 | + { | ||
| 4 | + "name": "app_name", | ||
| 5 | + "value": "NutriLog" | ||
| 6 | + } | ||
| 7 | + ] | ||
| 8 | +} | ||
| @@ -0,0 +1,7 @@ | |||
| 1 | +{ | ||
| 2 | + "layered-image": | ||
| 3 | + { | ||
| 4 | + "background" : "$media:logo_background", | ||
| 5 | + "foreground" : "$media:logo" | ||
| 6 | + } | ||
| 7 | +} | ||
| @@ -0,0 +1,147 @@ | |||
| 1 | +# NutriLog 开发指南(精简版) | ||
| 2 | + | ||
| 3 | +> 完整版请参阅 [docs/开发指南.md](docs/开发指南.md) | ||
| 4 | + | ||
| 5 | +## 应用简介 | ||
| 6 | + | ||
| 7 | +NutriLog(膳食) 是一款基于 OpenHarmony 的营养追踪应用,支持按餐次记录饮食、追踪热量与三大营养素摄入、饮水管理、周报统计图表、BMI 评估,内置 60+ 种食物数据并支持自定义扩展。 | ||
| 8 | + | ||
| 9 | +## 环境搭建 | ||
| 10 | +## 软件要求 | ||
| 11 | +DevEco Studio版本:DevEco Studio 6.1.1 Release。 | ||
| 12 | +OpenHarmony SDK版本:API version 23。 | ||
| 13 | +## 硬件要求 | ||
| 14 | +开发板类型:润和RK3568开发板。 | ||
| 15 | +OpenHarmony系统:3.2 Release。 | ||
| 16 | +## 环境搭建 | ||
| 17 | +完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行: | ||
| 18 | + | ||
| 19 | + 1.获取OpenHarmony系统版本:标准系统解决方案(二进制)。以3.2 Release版本为例: | ||
| 20 | +  | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + 2.搭建烧录环境。 | ||
| 24 | + | ||
| 25 | + 1.完成DevEco Device Tool的安装 | ||
| 26 | + 2.完成RK3568开发板的烧录 | ||
| 27 | + 3.搭建开发环境。 | ||
| 28 | + | ||
| 29 | + 1.开始前请参考工具准备,完成DevEco Studio的安装和开发环境配置。 | ||
| 30 | + 2.开发环境配置完成后,请参考使用工程向导创建工程(模板选择“Empty Ability”),选择JS或者eTS语言开发。 | ||
| 31 | + 3.工程创建完成后,选择使用真机进行调测。 | ||
| 32 | + | ||
| 33 | +## 功能清单 | ||
| 34 | + | ||
| 35 | +| 功能模块 | 主要功能 | 对应视图 | | ||
| 36 | +|---------|---------|---------| | ||
| 37 | +| 今日概览 | 热量环/营养素卡片/饮水进度/饮食列表 | Home | | ||
| 38 | +| 食物库 | 分类浏览/搜索/收藏/自定义食物 | FoodLibrary | | ||
| 39 | +| 周报统计 | 饼图/柱状图/周均热量/达标天数 | Statistics | | ||
| 40 | +| 饮食记录 | 日历选择/按餐次分组 | History | | ||
| 41 | +| 个人中心 | 用户信息/BMI/目标设置 | Profile | | ||
| 42 | +| 添加记录 | 选餐次/选食物/调份数/增量保存 | AddRecord+AddFood | | ||
| 43 | +| 自定义食物 | 表单验证/重复名校验 | AddFoodPage | | ||
| 44 | +| 编辑记录 | 修改份数/营养自动重算 | EditMeal | | ||
| 45 | + | ||
| 46 | +## 应用截图 | ||
| 47 | + | ||
| 48 | +| 首页 | 食物库 | 统计 | | ||
| 49 | +|:--------------------:|:---:|:---:| | ||
| 50 | +|  |  |  | | ||
| 51 | + | ||
| 52 | +| 记录 | 个人中心 | 添加记录 | | ||
| 53 | +|:---:|:---:|:---:| | ||
| 54 | +|  |  |  | | ||
| 55 | + | ||
| 56 | +| 添加自定义食物 | | ||
| 57 | +|:------------------------------:| | ||
| 58 | +|  | | ||
| 59 | + | ||
| 60 | +## 架构图 | ||
| 61 | + | ||
| 62 | +```mermaid | ||
| 63 | +graph TB | ||
| 64 | + subgraph UI层 | ||
| 65 | + Index["Index.ets — Navigation + 5 Tabs"] | ||
| 66 | + Views["Views — Home / FoodLibrary / Statistics / History / Profile / AddRecord / AddFood / AddFoodPage / EditMeal"] | ||
| 67 | + Components["Components — NutrientCard / MealTypeSelector / FoodTypeList / NutritionSummary / PageTitle"] | ||
| 68 | + end | ||
| 69 | + subgraph 业务层 | ||
| 70 | + NS["NutritionService — 汇总查询"] | ||
| 71 | + BMI["BMICalculate — BMI 计算"] | ||
| 72 | + Utils["MealRecordUtils / FormatDate"] | ||
| 73 | + end | ||
| 74 | + subgraph 数据层 | ||
| 75 | + RDB["FoodRDB — RDB 单例 CRUD"] | ||
| 76 | + Prefs["UserProfilePreferences — 偏好存储"] | ||
| 77 | + InitData["FoodInitData — 60+ 预设食物"] | ||
| 78 | + end | ||
| 79 | + subgraph SDK | ||
| 80 | + ArkData["@kit.ArkData — relationalStore / preferences"] | ||
| 81 | + AbilityKit["@kit.AbilityKit — UIAbility / Context"] | ||
| 82 | + ArkUI["@kit.ArkUI — Navigation / Tabs / List / Gauge / emitter / uiObserver"] | ||
| 83 | + Hilog["@kit.PerformanceAnalysisKit — hilog"] | ||
| 84 | + end | ||
| 85 | + subgraph 第三方 | ||
| 86 | + Charts["@mcui/mccharts — McPieChart / McBarChart"] | ||
| 87 | + end | ||
| 88 | + Index --> Views --> Components | ||
| 89 | + Views --> NS --> RDB --> ArkData | ||
| 90 | + Views --> Prefs --> ArkData | ||
| 91 | + Views --> BMI | ||
| 92 | + Views --> Charts | ||
| 93 | + Index --> ArkUI | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +## 目录结构 | ||
| 97 | + | ||
| 98 | +``` | ||
| 99 | +entry/src/main/ets/ | ||
| 100 | +├── pages/Index.ets # @Entry 根页面 | ||
| 101 | +├── entryability/EntryAbility.ets # UIAbility 入口 | ||
| 102 | +├── view/ # 9 个全页面视图 | ||
| 103 | +├── component/ # 5 个可复用组件 | ||
| 104 | +├── model/ # 类型定义(FoodType/BMIType/BarType/DateInfo/...) | ||
| 105 | +├── data/ # 数据层(FoodRDB/UserProfilePreferences/FoodInitData) | ||
| 106 | +├── service/NutritionService.ets # 业务查询服务 | ||
| 107 | +├── common/ # 工具(AppConstants/BMICalculate/FormatDate/ColorUtils/EventKey/Logger) | ||
| 108 | +├── utils/MealRecordUtils.ets # 记录工具 | ||
| 109 | +└── style/CardStyle.ets # 共享样式 | ||
| 110 | +``` | ||
| 111 | + | ||
| 112 | +## 模块关键逻辑 | ||
| 113 | + | ||
| 114 | +| 模块 | 关键逻辑 | | ||
| 115 | +|-----|---------| | ||
| 116 | +| Index | NavPathStack 路由注册 + @Provide 共享 navPathStack/currentIndex | | ||
| 117 | +| Home | uiObserver 刷新 + 数据加载链 + 饮水 CRUD + 餐次合并 | | ||
| 118 | +| FoodLibrary | IDataSource 懒加载 + 300ms 搜索防抖 + 收藏/最近使用 + eventHub 刷新 | | ||
| 119 | +| Statistics | MCPieChart/McBarChart 配置 + 周范围计算 + 零值填充 | | ||
| 120 | +| History | 日历选择 + SQL JOIN + bindMenu/bindContentCover CRUD + emitter 通信 | | ||
| 121 | +| Profile | @Watch BMI 重算 + Gauge 仪表盘 + preferences 持久化 + 输入校验 | | ||
| 122 | +| AddRecord | bindSheet+TextPicker 份数选择 + 增量更新(toDelete/toUpdate/toInsert) | | ||
| 123 | +| AddFoodPage | 表单验证 + 重复名校验 + eventHub 通知食物库 | | ||
| 124 | +| EditMeal | onWillChange 份数校验 + 比例重算营养素 + pop(result) 返回 | | ||
| 125 | + | ||
| 126 | +## SDK API 使用总览 | ||
| 127 | + | ||
| 128 | +| Kit | API | 用途 | 位置 | | ||
| 129 | +|-----|-----|------|------| | ||
| 130 | +| @ohos.data.ArkData | relationalStore | RDB 建表/CRUD/索引/版本管理 | FoodRDB, NutritionService | | ||
| 131 | +| @ohos.data.ArkData | preferences | 用户配置/最近食物/达标天数 | UserProfilePreferences, FoodLibrary | | ||
| 132 | +| @ohos.app.ability.AbilityKit | UIAbility + Context | 应用入口/RDB初始化 | EntryAbility | | ||
| 133 | +| @ohos.ArkUI | Navigation + NavPathStack | 单页多路由导航 | Index + 所有子页面 | | ||
| 134 | +| @ohos.ArkUI | Tabs + TabContent | 5 标签页 | Index | | ||
| 135 | +| @ohos.ArkUI | List + LazyForEach + IDataSource | 食物列表懒加载 | FoodLibrary | | ||
| 136 | +| @ohos.ArkUI | Gauge + Progress | 热量环/BMI仪表/进度条 | Home, Profile, History | | ||
| 137 | +| @ohos.ArkUI | TextInput + TextPicker | 表单输入/份数选择 | AddRecord, AddFoodPage, EditMeal | | ||
| 138 | +| @ohos.ArkUI | CustomDialog | 饮水/用户资料/食物详情弹窗 | Home, Profile, FoodLibrary | | ||
| 139 | +| @ohos.ArkUI | emitter | 跨组件事件(Home→History) | Home, History | | ||
| 140 | +| @ohos.ArkUI | uiObserver | Tab/导航页面刷新监听 | Home, Statistics | | ||
| 141 | +| @ohos.ArkUI | bindSheet/bindContentCover/bindMenu | 半模态/全屏模态/菜单 | AddRecord, History | | ||
| 142 | +| @ohos.PerformanceAnalysisKit | hilog | 日志系统 | Logger(全项目) | | ||
| 143 | +| @mcui/mccharts | McPieChart + McBarChart | 饼图/柱状图 | Statistics | | ||
| 144 | + | ||
| 145 | +--- | ||
| 146 | + | ||
| 147 | +> 详细内容(功能使用说明、分模块代码逻辑、SDK API 用法示例、数据库表结构、类型速查)请参阅 [docs/开发指南.md](docs/开发指南.md) | ||
| @@ -0,0 +1,56 @@ | |||
| 1 | +{ | ||
| 2 | + "app": { | ||
| 3 | + "products": [ | ||
| 4 | + { | ||
| 5 | + "name": "default", | ||
| 6 | + "signingConfig": "default", | ||
| 7 | + "targetSdkVersion": 23, | ||
| 8 | + "compatibleSdkVersion": 23, | ||
| 9 | + "compileSdkVersion": 23, | ||
| 10 | + "runtimeOS": "OpenHarmony", | ||
| 11 | + "buildOption": { | ||
| 12 | + "strictMode": { | ||
| 13 | + "caseSensitiveCheck": true, | ||
| 14 | + "useNormalizedOHMUrl": true | ||
| 15 | + } | ||
| 16 | + } | ||
| 17 | + } | ||
| 18 | + ], | ||
| 19 | + "buildModeSet": [ | ||
| 20 | + { | ||
| 21 | + "name": "debug" | ||
| 22 | + }, | ||
| 23 | + { | ||
| 24 | + "name": "release" | ||
| 25 | + } | ||
| 26 | + ], | ||
| 27 | + "signingConfigs": [ | ||
| 28 | + { | ||
| 29 | + "name": "default", | ||
| 30 | + "material": { | ||
| 31 | + "certpath": "C:/Users/ymwangfa/.ohos/config/openharmony/default_nutrilogapp-release_7byCtW4hDWR9oIBPaZ25ulugbe9XLbDDMZicQGzx9mM=.cer", | ||
| 32 | + "keyAlias": "debugKey", | ||
| 33 | + "keyPassword": "0000001B472D85FEC69BCCB7FEFC604A36795D484B3B33EC7E1738D7D3BB293B8A6C87485C4AB43533C1BE", | ||
| 34 | + "profile": "C:/Users/ymwangfa/.ohos/config/openharmony/default_nutrilogapp-release_7byCtW4hDWR9oIBPaZ25ulugbe9XLbDDMZicQGzx9mM=.p7b", | ||
| 35 | + "signAlg": "SHA256withECDSA", | ||
| 36 | + "storeFile": "C:/Users/ymwangfa/.ohos/config/openharmony/default_nutrilogapp-release_7byCtW4hDWR9oIBPaZ25ulugbe9XLbDDMZicQGzx9mM=.p12", | ||
| 37 | + "storePassword": "0000001BF1608B3D1E830BA5D3C9E8FF740B6726DE93CC8C73155F8E56321D48A1B3948658B18D26A2BF99" | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + ] | ||
| 41 | + }, | ||
| 42 | + "modules": [ | ||
| 43 | + { | ||
| 44 | + "name": "entry", | ||
| 45 | + "srcPath": "./entry", | ||
| 46 | + "targets": [ | ||
| 47 | + { | ||
| 48 | + "name": "default", | ||
| 49 | + "applyToProducts": [ | ||
| 50 | + "default" | ||
| 51 | + ] | ||
| 52 | + } | ||
| 53 | + ] | ||
| 54 | + } | ||
| 55 | + ] | ||
| 56 | +} | ||
| @@ -0,0 +1,1145 @@ | |||
| 1 | +# NutriLog(膳食) 开发指南 | ||
| 2 | + | ||
| 3 | +> 膳食 — 基于 OpenHarmony 开发的饮食记录与营养分析工具,帮助用户记录每日饮食、追踪热量与营养素摄入、可视化周报统计。 | ||
| 4 | + | ||
| 5 | +--- | ||
| 6 | + | ||
| 7 | +## 目录 | ||
| 8 | + | ||
| 9 | +- [1. 应用概述](#1-应用概述) | ||
| 10 | +- [2. 功能使用说明](#2-功能使用说明) | ||
| 11 | +- [3. 项目整体架构](#3-项目整体架构) | ||
| 12 | +- [4. 项目目录结构](#4-项目目录结构) | ||
| 13 | +- [5. 分模块关键逻辑详解](#5-分模块关键逻辑详解) | ||
| 14 | +- [6. OpenHarmony SDK API 使用总览](#6-OpenHarmony-sdk-api-使用总览) | ||
| 15 | +- [7. 附录](#7-附录) | ||
| 16 | + | ||
| 17 | +--- | ||
| 18 | + | ||
| 19 | +## 1. 应用概述 | ||
| 20 | + | ||
| 21 | +### 1.1 应用简介 | ||
| 22 | + | ||
| 23 | +**NutriLog(膳食)** 是一款基于 OpenHarmony 开发的营养追踪应用,帮助用户: | ||
| 24 | + | ||
| 25 | +- **记录饮食**:按餐次(早餐/午餐/晚餐/加餐)记录每日饮食,支持份数调整 | ||
| 26 | +- **追踪营养**:实时计算热量、蛋白质、碳水化合物、脂肪摄入,与目标对比 | ||
| 27 | +- **饮水管理**:记录每日饮水量,追踪是否达标 | ||
| 28 | +- **可视化统计**:周报饼图展示三大营养素比例,柱状图展示每日热量趋势 | ||
| 29 | +- **BMI 评估**:根据身高体重自动计算 BMI 值并给出分级评估 | ||
| 30 | +- **食物管理**:内置 60+ 种常见食物数据,支持自定义食物添加与收藏 | ||
| 31 | + | ||
| 32 | +**技术特点**: | ||
| 33 | + | ||
| 34 | +- 单模块 Stage 模型项目,使用 `Navigation` + `NavPathStack` 管理页面路由 | ||
| 35 | +- 关系型数据库(`relationalStore`)持久化饮食记录,`preferences` 存储用户配置 | ||
| 36 | +- `@Provide`/`@Consume` 跨组件状态共享,`EventHub` 跨页面事件通信 | ||
| 37 | +- 第三方图表库 `@mcui/mccharts` 实现饼图与柱状图 | ||
| 38 | +- 内置 60+ 个 SVG 食物图标,支持自定义食物扩展 | ||
| 39 | + | ||
| 40 | +### 1.2 功能清单 | ||
| 41 | + | ||
| 42 | +| 功能模块 | 主要功能 | 对应视图 | | ||
| 43 | +|---------|---------|---------| | ||
| 44 | +| 今日概览 | 热量进度环、三大营养素环形图、饮水进度条、当日饮食记录列表 | Home | | ||
| 45 | +| 食物库 | 分类浏览、关键词搜索、食物详情、收藏/取消收藏、添加自定义食物 | FoodLibrary | | ||
| 46 | +| 周报统计 | 营养素比例饼图、每日热量柱状图、周均热量/饮水达标天数 | Statistics | | ||
| 47 | +| 饮食记录 | 日历选择、按日期查看记录、按餐次分组、编辑/删除记录、新增记录 | History | | ||
| 48 | +| 个人中心 | 用户信息编辑、BMI 仪表盘、热量/饮水目标设置 | Profile | | ||
| 49 | +| 添加记录 | 选择餐次、从食物库选取食物、调整份数、保存记录 | AddRecord + AddFood | | ||
| 50 | +| 自定义食物 | 填写食物名称/分类/营养素、重复名校验、插入数据库 | AddFoodPage | | ||
| 51 | +| 编辑记录 | 修改份数、自动重算营养素、保存更新 | EditMeal | | ||
| 52 | + | ||
| 53 | +--- | ||
| 54 | + | ||
| 55 | +## 2. 功能使用说明 | ||
| 56 | + | ||
| 57 | +### 2.1 首页 — 今日营养总览 | ||
| 58 | + | ||
| 59 | +首页展示用户当天的营养摄入概况,是应用的核心信息面板: | ||
| 60 | + | ||
| 61 | +1. **热量环形图**:页面中央显示 `Gauge` 仪表盘,展示当前热量摄入占目标的百分比,未达标时显示绿色,超标时显示红色 | ||
| 62 | +2. **三大营养素卡片**:蛋白质、碳水化合物、脂肪三个 `NutrientCard`,分别显示摄入量占目标比例的环形进度 | ||
| 63 | +3. **饮水进度条**:`Progress` 组件显示当日饮水量(毫升)占饮水目标的百分比,点击可打开饮水记录弹窗 | ||
| 64 | +4. **饮水记录弹窗**(`WaterDialog`):提供 100ml/200ml/300ml/500ml 四个快捷按钮,也可自定义输入饮水量;提交后实时更新进度条 | ||
| 65 | +5. **饮水历史弹窗**(`WaterRecordDialog`):查看当日所有饮水记录,支持多选删除 | ||
| 66 | +6. **当日饮食列表**:按餐次分组显示已记录的食物,点击某条记录可跳转到「记录」页面对应日期 | ||
| 67 | +7. **添加记录按钮**:无记录时显示空状态引导,点击进入「添加记录」页面 | ||
| 68 | +8. **营养素详情弹窗**(`NutrientDetailDialog`):点击任一营养素卡片,弹出该营养素的详细摄入进度条 | ||
| 69 | + | ||
| 70 | +**首页截图**: | ||
| 71 | + | ||
| 72 | + | ||
| 73 | + | ||
| 74 | +### 2.2 食物库 — 浏览/搜索/收藏 | ||
| 75 | + | ||
| 76 | +食物库提供完整的食物数据浏览与管理功能: | ||
| 77 | + | ||
| 78 | +1. **搜索栏**:顶部搜索框,输入关键词实时筛选食物(300ms 防抖),搜索范围覆盖所有分类 | ||
| 79 | +2. **分类标签栏**:水平滚动的分类标签,包括"全部""最近使用""谷物""蔬菜""肉蛋""水果""饮品""其它" | ||
| 80 | +3. **食物列表**:使用 `LazyForEach` + `IDataSource` 懒加载,每项显示食物图标、名称、每 100g 热量 | ||
| 81 | +4. **收藏功能**:点击食物项右侧的心形图标切换收藏状态,收藏数据存储在 `favorite` 数据库表 | ||
| 82 | +5. **最近使用**:切换到「最近使用」标签时,从 `preferences` 读取最近选择的食物 ID(最多 15 个),按使用时间倒序展示 | ||
| 83 | +6. **食物详情弹窗**(`FoodDetailDialog`):点击食物项弹出详情,展示每 100g 的热量、蛋白质、碳水、脂肪、膳食纤维五项数据 | ||
| 84 | +7. **添加自定义食物**:点击底部「添加自定义食物」按钮,导航到 `AddFoodPage` 页面 | ||
| 85 | + | ||
| 86 | +**食物库截图**: | ||
| 87 | + | ||
| 88 | + | ||
| 89 | + | ||
| 90 | +### 2.3 统计 — 周报图表 | ||
| 91 | + | ||
| 92 | +统计页面以周为单位展示营养数据的可视化分析: | ||
| 93 | + | ||
| 94 | +1. **周范围显示**:页面顶部显示当前周的起止日期(周一至周日) | ||
| 95 | +2. **营养素比例饼图**(`McPieChart`):环形饼图展示本周蛋白质/碳水/脂肪的摄入比例,过滤零值后动态渲染,颜色分别为蓝色/橙色/黄色 | ||
| 96 | +3. **每日热量柱状图**(`McBarChart`):7 根柱子分别代表周一至周日的热量摄入,每根柱子颜色不同,顶部显示数值标签 | ||
| 97 | +4. **周数据概览网格**:2x2 网格显示四项统计 — 周均热量、饮水达标天数/7、本周总热量、三大营养素总量 | ||
| 98 | +5. **空状态**:本周无记录时显示空状态引导 | ||
| 99 | +6. **自动刷新**:切换到统计标签或从子页面返回时自动重新加载数据 | ||
| 100 | + | ||
| 101 | +**统计截图**: | ||
| 102 | + | ||
| 103 | + | ||
| 104 | + | ||
| 105 | +### 2.4 记录 — 日历与饮食记录 | ||
| 106 | + | ||
| 107 | +记录页面是饮食记录的核心管理入口: | ||
| 108 | + | ||
| 109 | +1. **日历选择栏**:水平滚动日期列表,从最早记录日期到今天,左右箭头可按周滚动,点击日期切换查看 | ||
| 110 | +2. **日期快捷标签**:"今天""昨天""前天"显示为文字标签,其它日期显示星期名和日期数字 | ||
| 111 | +3. **当日热量进度条**:显示选中日期的热量摄入与目标对比,达标显示绿色,超标显示红色 | ||
| 112 | +4. **按餐次分组**:早餐/午餐/晚餐/加餐四个分组,每组显示该餐次的所有记录 | ||
| 113 | +5. **记录操作菜单**: | ||
| 114 | + - **按右侧按钮**:弹出下拉菜单(`MenuBuilder`),显示"编辑"和"删除"选项 | ||
| 115 | + - **长按**:弹出下拉菜单(`MenuBuilder`),同样显示"编辑"和"删除" | ||
| 116 | +6. **编辑记录**:选择"编辑"后导航到 `EditMeal` 页面 | ||
| 117 | +7. **删除记录**:选择"删除"后弹出全屏确认弹窗(`DeleteDialogBuilder`),确认后从数据库删除 | ||
| 118 | +8. **添加记录**:点击"添加记录"按钮导航到 `AddRecord` 页面,传入选中日期作为参数 | ||
| 119 | +9. **跳转到今天**:从首页点击饮食记录时,通过 `eventHub` 事件通知记录页跳转到今天日期 | ||
| 120 | + | ||
| 121 | + | ||
| 122 | +**记录截图**: | ||
| 123 | + | ||
| 124 | + | ||
| 125 | + | ||
| 126 | +### 2.5 个人中心 — 用户信息与目标 | ||
| 127 | + | ||
| 128 | +个人中心管理用户的身高、体重、BMI 和目标设置: | ||
| 129 | + | ||
| 130 | +1. **用户资料卡片**:显示头像、性别、身高/体重/年龄摘要、饮食记录快捷入口 | ||
| 131 | +2. **BMI 仪表盘**:分段渐变 `Gauge` 组件,从偏瘦(蓝色)到正常(绿色)到偏胖(橙色)到肥胖(红色),指针指向当前 BMI 值,下方显示 BMI 数值和等级文字 | ||
| 132 | +3. **目标设置卡片**: | ||
| 133 | + - **热量目标**:每日热量目标(千卡),点击编辑按钮弹出设置弹窗 | ||
| 134 | + - **饮水目标**:每日饮水目标(毫升),点击编辑按钮弹出设置弹窗 | ||
| 135 | +4. **用户资料编辑弹窗**(`UserProfileDialog`):编辑身高(30-300cm)、体重(1-500kg)、年龄(1-120岁)、性别,保存后自动重新计算 BMI | ||
| 136 | +5. **目标设置弹窗**(`UserTargetDialog`):编辑热量目标(1-1800kcal,初始 1500)、饮水目标(1-2500ml,初始 2000),带输入范围校验 | ||
| 137 | + | ||
| 138 | + | ||
| 139 | +**个人中心截图**: | ||
| 140 | + | ||
| 141 | + | ||
| 142 | + | ||
| 143 | +### 2.6 添加记录 | ||
| 144 | + | ||
| 145 | +添加饮食记录是应用的核心操作流程: | ||
| 146 | + | ||
| 147 | +1. **日期选择**:页面顶部显示当前记录日期(从记录页传入或默认今天) | ||
| 148 | +2. **餐次选择**:`MealTypeSelector` 组件提供早餐/午餐/晚餐/加餐四个选项 | ||
| 149 | +3. **已选食物列表**: | ||
| 150 | + - 每项显示食物图标、名称、份数、热量 | ||
| 151 | + - 份数可点击,弹出半模态选择器(`bindSheet` + `TextPicker`),范围 0.5-10 份,步进 0.5 | ||
| 152 | + - 每项右侧有删除按钮 | ||
| 153 | +4. **添加食物**:点击「添加食物」按钮导航到 `AddFood` 页面,支持多选 | ||
| 154 | +5. **营养素汇总**:底部 `NutritionSummary` 组件显示本餐的总热量/蛋白质/脂肪/碳水 | ||
| 155 | +6. **保存逻辑**: | ||
| 156 | + - 查询当前日期+餐次的已有记录 | ||
| 157 | + - 对比已有记录与当前列表,自动分为三组: | ||
| 158 | + - `toDelete`:已有但当前列表中已删除的记录,从数据库删除 | ||
| 159 | + - `toUpdate`:已有且份数等发生变化的记录,更新数据库 | ||
| 160 | + - `toInsert`:当前新增的记录,插入数据库 | ||
| 161 | + - 按钮文字动态显示「添加记录」或「保存修改」 | ||
| 162 | + | ||
| 163 | + | ||
| 164 | +**添加记录截图**: | ||
| 165 | + | ||
| 166 | + | ||
| 167 | + | ||
| 168 | +### 2.7 添加自定义食物 | ||
| 169 | + | ||
| 170 | +添加自定义食物允许用户扩展食物库: | ||
| 171 | + | ||
| 172 | +1. **食物名称**:TextInput 输入,最多 15 字符,必填 | ||
| 173 | +2. **食物分类**:6 个胶囊按钮选择(谷物/蔬菜/肉蛋/水果/饮品/其它),必选 | ||
| 174 | +3. **营养素输入**:热量(必填,最大 9999kcal)、蛋白质/碳水/脂肪/膳食纤维(选填,每项最大 100g),均支持最多 2 位小数 | ||
| 175 | +4. **重复名校验**:提交前查询数据库检查是否已存在同名食物 | ||
| 176 | +5. **保存流程**:校验通过后插入 `food` 表(`is_custom = 1`,图标默认 `custom.svg`),通过 `eventHub.emit(EVENT_REFRESH_FOOD)` 通知食物库刷新并自动切换到对应分类标签 | ||
| 177 | + | ||
| 178 | +**添加自定义食物截图**: | ||
| 179 | + | ||
| 180 | + | ||
| 181 | + | ||
| 182 | +### 2.8 编辑已有记录 | ||
| 183 | + | ||
| 184 | +编辑已有记录支持修改份数并自动重算营养素: | ||
| 185 | + | ||
| 186 | +1. **显示当前记录信息**:食物名称、图标、当前份数 | ||
| 187 | +2. **份数编辑**:TextInput 输入份数(0-10,必须为 0.5 的倍数),非法输入弹出 Toast 提示 | ||
| 188 | +3. **营养素自动重算**:`updateNutrition()` 按比例 `newServings / originalServings * 原始营养值` 计算新的热量/蛋白质/碳水/脂肪 | ||
| 189 | +4. **四项营养素卡片**:2x2 Grid 展示更新后的热量/蛋白质/碳水/脂肪,带图标和单位 | ||
| 190 | +5. **保存/取消**: | ||
| 191 | + - 「保存修改」:构造更新后的 `MealRecord` 对象,通过 `pageStack.pop(updatedRecord)` 返回,调用方的 `onPop` 回调接收更新并写入数据库 | ||
| 192 | + - 「取消」:`pageStack.pop()` 不传递结果 | ||
| 193 | + | ||
| 194 | +--- | ||
| 195 | + | ||
| 196 | +## 3. 项目整体架构 | ||
| 197 | + | ||
| 198 | +### 3.1 分层架构图 | ||
| 199 | + | ||
| 200 | +```mermaid | ||
| 201 | +graph TB | ||
| 202 | + subgraph UI层 | ||
| 203 | + Index["Index.ets — 根页面: Navigation + 5 Tabs"] | ||
| 204 | + Views["Views 视图层 — Home / FoodLibrary / Statistics / History / Profile / AddRecord / AddFood / AddFoodPage / EditMeal"] | ||
| 205 | + Components["Components 组件层 — NutrientCard / MealTypeSelector / FoodTypeList / NutritionSummary / PageTitle"] | ||
| 206 | + Dialogs["CustomDialogs — NutrientDetailDialog / WaterDialog / WaterRecordDialog / FoodDetailDialog / UserProfileDialog / UserTargetDialog"] | ||
| 207 | + end | ||
| 208 | + | ||
| 209 | + subgraph 业务层 | ||
| 210 | + NutritionService["NutritionService — 每日/每周营养汇总查询"] | ||
| 211 | + BMICalculate["BMICalculate — BMI 计算与分级"] | ||
| 212 | + FormatDate["FormatDate — Date 转 YYYY-MM-DD"] | ||
| 213 | + MealRecordUtils["MealRecordUtils — ValuesBucket 转换 / 营养汇总"] | ||
| 214 | + end | ||
| 215 | + | ||
| 216 | + subgraph 数据层 | ||
| 217 | + FoodRDB["FoodRDB RDB 单例 — 建表 / CRUD / 索引 / 初始数据导入"] | ||
| 218 | + UserProfilePrefs["UserProfilePreferences — 用户配置持久化 / 饮水达标天数"] | ||
| 219 | + FoodInitData["FoodInitData — 60+ 预设食物数据"] | ||
| 220 | + end | ||
| 221 | + | ||
| 222 | + subgraph 模型层 | ||
| 223 | + Models["Model 类型定义 — FoodItem / MealRecord / WaterRecord / DailySummary / UserProfile / BMI / BarType / DateInfo / FoodItemWithServings"] | ||
| 224 | + end | ||
| 225 | + | ||
| 226 | + subgraph OpenHarmony SDK | ||
| 227 | + ArkData["@kit.ArkData — relationalStore / preferences"] | ||
| 228 | + AbilityKit["@kit.AbilityKit — UIAbility / Context"] | ||
| 229 | + ArkUI["@kit.ArkUI — Navigation / Tabs / List / Gauge / Progress / eventHub / uiObserver / CustomDialog"] | ||
| 230 | + Hilog["@kit.PerformanceAnalysisKit — hilog"] | ||
| 231 | + end | ||
| 232 | + | ||
| 233 | + subgraph 第三方库 | ||
| 234 | + McCharts["@mcui/mccharts — McPieChart / McBarChart"] | ||
| 235 | + end | ||
| 236 | + | ||
| 237 | + Index --> Views | ||
| 238 | + Index --> Components | ||
| 239 | + Views --> Components | ||
| 240 | + Views --> Dialogs | ||
| 241 | + Views --> NutritionService | ||
| 242 | + Views --> UserProfilePrefs | ||
| 243 | + Views --> BMICalculate | ||
| 244 | + Views --> MealRecordUtils | ||
| 245 | + NutritionService --> FoodRDB | ||
| 246 | + FoodRDB --> FoodInitData | ||
| 247 | + FoodRDB --> ArkData | ||
| 248 | + UserProfilePrefs --> ArkData | ||
| 249 | + Views --> McCharts | ||
| 250 | + Views --> Models | ||
| 251 | + NutritionService --> Models | ||
| 252 | + Index --> ArkUI | ||
| 253 | + Views --> ArkUI | ||
| 254 | +``` | ||
| 255 | + | ||
| 256 | +### 3.2 导航架构 | ||
| 257 | + | ||
| 258 | +项目采用 `Navigation` + `NavPathStack` 管理页面路由,所有子页面通过 `NavDestination` 注册: | ||
| 259 | + | ||
| 260 | +``` | ||
| 261 | +Navigation (NavPathStack) | ||
| 262 | +├── Tab 0: Home ← 首页标签内容 | ||
| 263 | +├── Tab 1: FoodLibrary ← 食物库标签内容 | ||
| 264 | +├── Tab 2: Statistics ← 统计标签内容 | ||
| 265 | +├── Tab 3: History ← 记录标签内容 | ||
| 266 | +├── Tab 4: Profile ← 个人中心标签内容 | ||
| 267 | +│ | ||
| 268 | +├── NavDestination: AddRecord ← pushPath('addRecord') | ||
| 269 | +├── NavDestination: AddFood ← pushPath('addFood') | ||
| 270 | +├── NavDestination: AddFoodPage ← pushPath('INSERT_FOOD') | ||
| 271 | +└── NavDestination: EditMeal ← pushPath('ROUTE_EDIT_MEAL') | ||
| 272 | +``` | ||
| 273 | + | ||
| 274 | +**关键设计**: | ||
| 275 | + | ||
| 276 | +- `main_pages.json` 仅注册 `pages/Index` 一个页面路由,所有子页面均通过 `NavPathStack` 动态导航 | ||
| 277 | +- `@Provide('navPathStack')` 在 Index 中提供,子页面通过 `@Consume('navPathStack')` 消费 | ||
| 278 | +- 子页面参数传递:`pushPath({ name: route, param: data })`,子页面在 `onReady` 回调中通过 `context.pathInfo.param` 接收 | ||
| 279 | +- 子页面返回值:`pageStack.pop(result)`,调用方通过 `onPop` 回调接收 | ||
| 280 | + | ||
| 281 | +### 3.3 状态管理架构 | ||
| 282 | + | ||
| 283 | +项目使用 ArkUI 声明式状态管理,按作用范围选择不同装饰器: | ||
| 284 | + | ||
| 285 | +| 装饰器 | 作用范围 | 使用场景 | 项目中的用法 | | ||
| 286 | +|-------|---------|---------|------------| | ||
| 287 | +| `@State` | 组件内部 | 组件私有状态 | 各页面的数据、UI 状态(15+ 个 @State/页面) | | ||
| 288 | +| `@Prop` | 父→子(单向) | 子组件仅读展示 | NutrientCard 的 icon/label/value/unit/color | | ||
| 289 | +| `@Link` | 父↔子(双向) | 子组件需修改父状态 | Dialog 中的 userProfile / waterRecords | | ||
| 290 | +| `@Provide`/`@Consume` | 跨层级(广播) | 深层组件共享 | navPathStack、currentIndex、shareFood | | ||
| 291 | +| `@Watch` | 值变化监听 | 触发副作用 | Profile 的 userProfile 变化时重算 BMI | | ||
| 292 | +| `@Observed` | 类实例可观察 | 嵌套对象属性变化 | FoodItemWithServings 的 servings 变化驱动 UI | | ||
| 293 | +| `@ObjectLink` | @Observed 消费 | 接收 @Observed 实例 | 配合 @Observed 使用 | | ||
| 294 | + | ||
| 295 | +### 3.4 数据流架构 | ||
| 296 | + | ||
| 297 | +``` | ||
| 298 | +用户操作 (UI 事件) | ||
| 299 | + │ | ||
| 300 | + ▼ | ||
| 301 | +View 层 (@State 驱动 UI 刷新) | ||
| 302 | + │ | ||
| 303 | + ├──→ NutritionService (业务查询) | ||
| 304 | + │ │ | ||
| 305 | + │ ▼ | ||
| 306 | + │ FoodRDB.querySql() ──→ relationalStore ──→ Foods.db | ||
| 307 | + │ | ||
| 308 | + ├──→ UserProfilePreferences (用户配置) | ||
| 309 | + │ │ | ||
| 310 | + │ ▼ | ||
| 311 | + │ preferences.putSync/getSync ──→ 本地偏好文件 | ||
| 312 | + │ | ||
| 313 | + ├──→ FoodRDB.insert/update/delete (写入操作) | ||
| 314 | + │ │ | ||
| 315 | + │ ▼ | ||
| 316 | + │ relationalStore.batchInsert/executeSql ──→ Foods.db | ||
| 317 | + │ | ||
| 318 | + └──→ eventHub.emit (跨组件通知) | ||
| 319 | + │ | ||
| 320 | + ▼ | ||
| 321 | + eventHub.on ──→ 目标页面刷新数据 | ||
| 322 | +``` | ||
| 323 | + | ||
| 324 | +**刷新机制总览**: | ||
| 325 | + | ||
| 326 | +| 机制 | 触发时机 | 使用位置 | | ||
| 327 | +|-----|---------|---------| | ||
| 328 | +| `uiObserver.on('tabContentUpdate')` | 标签页重新显示时 | Home、Statistics | | ||
| 329 | +| `uiObserver.on('navDestinationSwitch')` | 从子页面返回导航栏时 | Home、Statistics | | ||
| 330 | +| `eventHub.emit/on` | 跨组件事件通知 | Home→History(跳转到今天) | | ||
| 331 | +| `eventHub.emit/on` | 跨 Ability 事件 | AddFoodPage→FoodLibrary(刷新食物列表) | | ||
| 332 | +| `@Watch` | 状态变量值变化 | Profile(userProfile 变化重算 BMI) | | ||
| 333 | + | ||
| 334 | +--- | ||
| 335 | + | ||
| 336 | +## 4. 项目目录结构 | ||
| 337 | + | ||
| 338 | +``` | ||
| 339 | +NutriLog/ | ||
| 340 | +├── AppScope/ # 应用全局配置 | ||
| 341 | +│ └── app.json5 # bundleName、版本号、图标 | ||
| 342 | +│ | ||
| 343 | +├── entry/ # 主模块(HAP) | ||
| 344 | +│ ├── oh-package.json5 # 模块依赖声明 | ||
| 345 | +│ ├── src/ | ||
| 346 | +│ │ ├── main/ | ||
| 347 | +│ │ │ ├── module.json5 # 模块配置:Ability、权限、页面路由 | ||
| 348 | +│ │ │ ├── resources/ # 资源文件 | ||
| 349 | +│ │ │ │ ├── base/ | ||
| 350 | +│ │ │ │ │ ├── element/ | ||
| 351 | +│ │ │ │ │ │ ├── color.json # 40+ 颜色资源(橙色主题) | ||
| 352 | +│ │ │ │ │ │ ├── string.json# 字符串资源 | ||
| 353 | +│ │ │ │ │ │ └── float.json # 尺寸资源 | ||
| 354 | +│ │ │ │ │ ├── media/ # 图标、启动页图片 | ||
| 355 | +│ │ │ │ │ └── profile/ | ||
| 356 | +│ │ │ │ │ └── main_pages.json # 页面路由(仅注册 Index) | ||
| 357 | +│ │ │ │ └── rawfile/ # SVG 食物图标(60+ 个) | ||
| 358 | +│ │ │ │ └── foodImage/ | ||
| 359 | +│ │ │ │ ├── white_rice.svg | ||
| 360 | +│ │ │ │ ├── apple.svg | ||
| 361 | +│ │ │ │ ├── ... (共 60+ 个) | ||
| 362 | +│ │ │ │ └── custom.svg | ||
| 363 | +│ │ │ │ | ||
| 364 | +│ │ │ └── ets/ # ArkTS 源码 | ||
| 365 | +│ │ │ ├── pages/ | ||
| 366 | +│ │ │ │ └── Index.ets # @Entry 根页面 | ||
| 367 | +│ │ │ ├── entryability/ | ||
| 368 | +│ │ │ │ └── EntryAbility.ets # UIAbility 入口,初始化 RDB | ||
| 369 | +│ │ │ ├── view/ # 全页面视图 | ||
| 370 | +│ │ │ │ ├── Home.ets # 首页 | ||
| 371 | +│ │ │ │ ├── FoodLibrary.ets# 食物库 | ||
| 372 | +│ │ │ │ ├── Statistics.ets # 统计 | ||
| 373 | +│ │ │ │ ├── History.ets # 记录 | ||
| 374 | +│ │ │ │ ├── Profile.ets # 个人中心 | ||
| 375 | +│ │ │ │ ├── AddRecord.ets # 添加记录 | ||
| 376 | +│ │ │ │ ├── AddFood.ets # 选择食物 | ||
| 377 | +│ │ │ │ ├── AddFoodPage.ets# 添加自定义食物 | ||
| 378 | +│ │ │ │ └── EditMeal.ets # 编辑记录 | ||
| 379 | +│ │ │ ├── component/ # 可复用组件 | ||
| 380 | +│ │ │ │ ├── NutrientCard.ets # 营养素指示卡片 | ||
| 381 | +│ │ │ │ ├── MealTypeSelector.ets# 餐次选择器 | ||
| 382 | +│ │ │ │ ├── FoodTypeList.ets # 分类标签列表 | ||
| 383 | +│ │ │ │ ├── NutritionSummary.ets# 营养素汇总 | ||
| 384 | +│ │ │ │ └── PageTitle.ets # 页面标题 | ||
| 385 | +│ │ │ ├── model/ # 类型定义 | ||
| 386 | +│ │ │ │ ├── FoodType.ets # 核心业务类型 | ||
| 387 | +│ │ │ │ ├── BMIType.ets # BMI 分级定义 | ||
| 388 | +│ │ │ │ ├── BarType.ets # 图表配置类型 | ||
| 389 | +│ │ │ │ ├── DateInfo.ets # 日期信息与日历常量 | ||
| 390 | +│ │ │ │ └── FoodItemWithServings.ets # 食物+份数包装类 | ||
| 391 | +│ │ │ ├── data/ # 数据层 | ||
| 392 | +│ │ │ │ ├── FoodRDB.ets # RDB 单例 | ||
| 393 | +│ │ │ │ ├── UserProfilePreferences.ets # 用户偏好存储 | ||
| 394 | +│ │ │ │ └── FoodInitData.ets # 预设食物数据 | ||
| 395 | +│ │ │ ├── service/ | ||
| 396 | +│ │ │ │ └── NutritionService.ets # 业务查询服务 | ||
| 397 | +│ │ │ ├── common/ # 工具函数 | ||
| 398 | +│ │ │ │ ├── AppConstants.ets # 全局常量 | ||
| 399 | +│ │ │ │ ├── BMICalculate.ets # BMI 计算 | ||
| 400 | +│ │ │ │ ├── FormatDate.ets # 日期格式化 | ||
| 401 | +│ │ │ │ ├── ColorUtils.ets # 颜色常量 | ||
| 402 | +│ │ │ │ ├── EventKey.ets # 事件键名 | ||
| 403 | +│ │ │ │ └── Logger.ets # hilog 封装 | ||
| 404 | +│ │ │ ├── utils/ | ||
| 405 | +│ │ │ │ └── MealRecordUtils.ets # 记录工具函数 | ||
| 406 | +│ │ │ ├── style/ | ||
| 407 | +│ │ │ │ └── CardStyle.ets # 卡片共享样式 | ||
| 408 | +│ │ │ └── foodImage/ # SVG 图标(同 rawfile) | ||
| 409 | +│ │ └── ohosTest/ # 测试目录 | ||
| 410 | +│ └── build-profile.json5 # 模块构建配置 | ||
| 411 | +│ | ||
| 412 | +├── build-profile.json5 # 应用级构建配置 | ||
| 413 | +├── oh-package.json5 # 应用依赖(@mcui/mccharts) | ||
| 414 | +├── hvigorfile.ts # Hvigor 构建脚本 | ||
| 415 | +├── hvigor/ # Hvigor 缓存 | ||
| 416 | +└── AGENTS.md # Agent 协作规范 | ||
| 417 | +``` | ||
| 418 | + | ||
| 419 | +--- | ||
| 420 | + | ||
| 421 | +## 5. 分模块关键逻辑详解 | ||
| 422 | + | ||
| 423 | +### 5.1 Index.ets — 根页面与导航路由 | ||
| 424 | + | ||
| 425 | +**文件**:`entry/src/main/ets/pages/Index.ets` | ||
| 426 | + | ||
| 427 | +Index 是应用的唯一 `@Entry` 页面,承担两个核心职责: | ||
| 428 | + | ||
| 429 | +1. **底部标签页容器**:`Tabs` 组件包含 5 个 `TabContent`,分别对应 Home/FoodLibrary/Statistics/History/Profile | ||
| 430 | +2. **导航路由注册**:`Navigation` 组件 + `navDestination` 注册所有子页面路由 | ||
| 431 | + | ||
| 432 | +**路由注册机制**: | ||
| 433 | + | ||
| 434 | +```typescript | ||
| 435 | +@Builder | ||
| 436 | +PagesMap(name: string, param: Object) { | ||
| 437 | + if (name === ROUTE_ADD_RECORD) { | ||
| 438 | + AddRecord() | ||
| 439 | + } else if (name === ROUTE_ADD_FOOD) { | ||
| 440 | + AddFood() | ||
| 441 | + } else if (name === INSERT_FOOD) { | ||
| 442 | + AddFoodPage() | ||
| 443 | + } else if (name === ROUTE_EDIT_MEAL) { | ||
| 444 | + EditMeal() | ||
| 445 | + } | ||
| 446 | +} | ||
| 447 | +``` | ||
| 448 | + | ||
| 449 | +**跨组件状态共享**: | ||
| 450 | + | ||
| 451 | +```typescript | ||
| 452 | +@Provide('navPathStack') navPathStack: NavPathStack = new NavPathStack() | ||
| 453 | +@Provide('currentIndex') currentIndex: number = 0 | ||
| 454 | +``` | ||
| 455 | + | ||
| 456 | +子页面通过 `@Consume('navPathStack')` 和 `@Consume('currentIndex')` 获取共享状态,实现跨标签页导航和状态同步。 | ||
| 457 | + | ||
| 458 | +### 5.2 Home — 今日概览与数据刷新 | ||
| 459 | + | ||
| 460 | +**文件**:`entry/src/main/ets/view/Home.ets` | ||
| 461 | + | ||
| 462 | +Home 页面是信息密度最高的页面,包含 3 个自定义弹窗和 15+ 个状态变量。 | ||
| 463 | + | ||
| 464 | +**数据加载链**(`refreshData` 方法): | ||
| 465 | + | ||
| 466 | +``` | ||
| 467 | +NutritionService.getDailySummary(today) | ||
| 468 | + → UserProfilePreferences.getUserProfile() | ||
| 469 | + → 计算目标营养素分配(蛋白质/碳水/脂肪各占多少g) | ||
| 470 | + → 计算各营养素完成百分比 | ||
| 471 | + → NutritionService.getMealRecords(today) | ||
| 472 | + → convertMealRecordsToMeals()(按餐次分组、合并同名食物) | ||
| 473 | + → NutritionService.getWaterRecords(today) | ||
| 474 | +``` | ||
| 475 | + | ||
| 476 | +**刷新触发机制**: | ||
| 477 | + | ||
| 478 | +```typescript | ||
| 479 | +this.uiObserver.on('tabContentUpdate', TAB_HOME_ID, () => { | ||
| 480 | + this.refreshData() | ||
| 481 | +}) | ||
| 482 | +this.uiObserver.on('navDestinationSwitch', TAB_HOME_ID, (dest) => { | ||
| 483 | + if (dest.id === 'navBar') { | ||
| 484 | + this.refreshData() | ||
| 485 | + } | ||
| 486 | +}) | ||
| 487 | +``` | ||
| 488 | + | ||
| 489 | +**饮食记录合并逻辑**(`convertMealRecordsToMeals`):将同一餐次内的同名食物记录合并为一条显示,累加热量和营养素。 | ||
| 490 | + | ||
| 491 | +**饮水管理**:`addWaterAmount` 插入 `water_record` 表并检查达标;`deleteWaterRecord` 删除指定记录;`checkAndIncrementWaterAchieveDays` 每日仅计一次达标。 | ||
| 492 | + | ||
| 493 | +### 5.3 FoodLibrary — 懒加载与搜索防抖 | ||
| 494 | + | ||
| 495 | +**文件**:`entry/src/main/ets/view/FoodLibrary.ets` | ||
| 496 | + | ||
| 497 | +**自定义数据源**(`FoodDataSource implements IDataSource`): | ||
| 498 | + | ||
| 499 | +```typescript | ||
| 500 | +class FoodDataSource implements IDataSource { | ||
| 501 | + private foodList: FoodItem[] = [] | ||
| 502 | + private listeners: DataChangeListener[] = [] | ||
| 503 | + | ||
| 504 | + updateList(newList: FoodItem[]) { | ||
| 505 | + this.foodList = newList | ||
| 506 | + this.listeners.forEach(l => l.onDataReloaded()) | ||
| 507 | + } | ||
| 508 | +} | ||
| 509 | +``` | ||
| 510 | + | ||
| 511 | +使用 `LazyForEach` 替代 `ForEach` 实现按需加载,数据更新时调用 `updateList` 触发 `onDataReloaded` 刷新 UI。 | ||
| 512 | + | ||
| 513 | +**搜索防抖**:300ms `setTimeout`/`clearTimeout` 实现输入防抖。 | ||
| 514 | + | ||
| 515 | +**分类切换逻辑**: | ||
| 516 | + | ||
| 517 | +| 分类标签 | 数据来源 | 筛选方式 | | ||
| 518 | +|---------|---------|---------| | ||
| 519 | +| 全部 | RDB food 表 | category 不过滤 | | ||
| 520 | +| 最近使用 | preferences KEY_COLLECT_FOOD_IDS | 按 ID 列表查询,最多 15 个 | | ||
| 521 | +| 谷物/蔬菜/肉蛋/水果/饮品/其它 | RDB food 表 | category = FoodCategory.XXX | | ||
| 522 | + | ||
| 523 | +**收藏功能**:收藏状态存储在 `favorite` 表,`toggleFavorite` 根据 `favoriteIds: Set<number>` 缓存决定 INSERT 或 DELETE。 | ||
| 524 | + | ||
| 525 | +**事件驱动刷新**:`AddFoodPage` 插入新食物后通过 `eventHub.emit(EVENT_REFRESH_FOOD, category)` 通知 FoodLibrary 刷新并自动切换分类。 | ||
| 526 | + | ||
| 527 | +### 5.4 Statistics — 图表与周数据聚合 | ||
| 528 | + | ||
| 529 | +**文件**:`entry/src/main/ets/view/Statistics.ets` | ||
| 530 | + | ||
| 531 | +**周范围计算**:`getWeekRange()` 根据今天计算周一到周日的日期范围。 | ||
| 532 | + | ||
| 533 | +**饼图配置**(`McPieChart`):过滤零值营养素,避免 0% 扇区,动态更新 `pieOption` 状态变量驱动 UI 刷新。 | ||
| 534 | + | ||
| 535 | +**柱状图配置**(`McBarChart`):7 根柱子分别使用不同颜色(TEAL/CORNFLOWER/LIME/PURPLE/PINK 等),`barStyle.borderRadius` 实现圆角柱子,顶部数值标签。 | ||
| 536 | + | ||
| 537 | +**数据加载链**: | ||
| 538 | + | ||
| 539 | +``` | ||
| 540 | +NutritionService.getWeeklyDailyCalories(weekStart, weekEnd) | ||
| 541 | + → UserProfilePreferences 获取饮水目标和达标天数 | ||
| 542 | + → updateBarChart() | ||
| 543 | + → NutritionService.getWeeklySummary() | ||
| 544 | + → updatePieOption() | ||
| 545 | +``` | ||
| 546 | + | ||
| 547 | +### 5.5 History — 日历与记录 CRUD | ||
| 548 | + | ||
| 549 | +**文件**:`entry/src/main/ets/view/History.ets` | ||
| 550 | + | ||
| 551 | +**日历组件**:水平滚动日期列表,`DateInfo` 类封装 `isoDate`/`displayText`/`showDayNumber`,左右箭头按周滚动。 | ||
| 552 | + | ||
| 553 | +**记录查询**:SQL JOIN 查询 `meal_record` 和 `food` 表获取食物图标,结果按 `MealType` 分组。 | ||
| 554 | + | ||
| 555 | +**CRUD 操作**: | ||
| 556 | + | ||
| 557 | +| 操作 | 交互方式 | 实现方式 | | ||
| 558 | +|-----|---------|---------| | ||
| 559 | +| 编辑 | bindMenu/bindContextMenu → 编辑 | pushPath(ROUTE_EDIT_MEAL, record) → onPop 回调 → rdbStore.update | | ||
| 560 | +| 删除 | bindMenu/bindContextMenu → 删除 | bindContentCover 确认 → rdbStore.delete | | ||
| 561 | +| 新增 | 添加记录按钮 | pushPath(ROUTE_ADD_RECORD, date) → 返回后 refreshData | | ||
| 562 | + | ||
| 563 | +**跨组件通信**:`eventHub.on({ eventId: EVENT_ID_HISTORY_NOW })` 接收首页通知跳转到今天。 | ||
| 564 | + | ||
| 565 | +### 5.6 Profile — 用户资料持久化与 BMI | ||
| 566 | + | ||
| 567 | +**文件**:`entry/src/main/ets/view/Profile.ets` | ||
| 568 | + | ||
| 569 | +**BMI 计算**(`@Watch` 驱动): | ||
| 570 | + | ||
| 571 | +```typescript | ||
| 572 | +@State @Watch('updateBmi') userProfile: UserProfile = ... | ||
| 573 | +updateBmi() { | ||
| 574 | + if (this.userProfile.height && this.userProfile.weight) { | ||
| 575 | + this.bmiResult = bmiCalculate(this.userProfile) | ||
| 576 | + } | ||
| 577 | +} | ||
| 578 | +``` | ||
| 579 | + | ||
| 580 | +**BMI Gauge 仪表盘**:分段渐变 Gauge,四段对应偏瘦/正常/偏胖/肥胖。 | ||
| 581 | + | ||
| 582 | +**用户配置持久化**:`UserProfilePreferences.putUserProfile` 序列化为 JSON 写入 preferences,Dialog 确认保存时 `@Watch` 自动触发 BMI 重算。 | ||
| 583 | + | ||
| 584 | +**输入校验**: | ||
| 585 | + | ||
| 586 | +| 字段 | 最小值 | 最大值 | 默认值 | | ||
| 587 | +|-----|-------|-------|-------| | ||
| 588 | +| 身高 | 30 | 300 | null | | ||
| 589 | +| 体重 | 1 | 500 | null | | ||
| 590 | +| 年龄 | 1 | 120 | null | | ||
| 591 | +| 热量目标 | 1 | 1800 | 1500 | | ||
| 592 | +| 饮水目标 | 1 | 2500 | 2000 | | ||
| 593 | + | ||
| 594 | +### 5.7 AddRecord — 食物选择与增删改逻辑 | ||
| 595 | + | ||
| 596 | +**文件**:`entry/src/main/ets/view/AddRecord.ets` | ||
| 597 | + | ||
| 598 | + | ||
| 599 | +**参数接收**:`onReady((context) => { this.selectedDate = context.pathInfo.param as string || formatDate(new Date()) })` | ||
| 600 | + | ||
| 601 | +**份数选择**:`bindSheet` + `TextPicker`,范围 0.5-10,步进 0.5。 | ||
| 602 | + | ||
| 603 | +**保存逻辑(增量更新)**: | ||
| 604 | + | ||
| 605 | +1. 查询当前日期+餐次的已有记录 | ||
| 606 | +2. 对比已有记录与当前列表,分三组: | ||
| 607 | + - `toDelete`:已有但已删除的 → 从数据库删除 | ||
| 608 | + - `toUpdate`:已有且变化的 → 更新数据库 | ||
| 609 | + - `toInsert`:新增的 → 插入数据库 | ||
| 610 | +3. 分别执行删除、更新、插入操作 | ||
| 611 | + | ||
| 612 | +这种增量更新策略避免了「先删后插」的数据丢失风险。 | ||
| 613 | + | ||
| 614 | +### 5.8 AddFoodPage — 自定义食物与表单验证 | ||
| 615 | + | ||
| 616 | +**文件**:`entry/src/main/ets/view/AddFoodPage.ets` | ||
| 617 | + | ||
| 618 | + | ||
| 619 | +**表单校验**:名称必填最大 15 字符、热量必填 0-9999、营养素选填 0-100、最多 2 位小数。 | ||
| 620 | + | ||
| 621 | +**重复名校验**:`SELECT COUNT(*) FROM food WHERE name = ?`,count > 0 时 Toast 提示。 | ||
| 622 | + | ||
| 623 | +**插入后通知**:`eventHub.emit(EVENT_REFRESH_FOOD, category)` 通知 FoodLibrary 刷新。 | ||
| 624 | + | ||
| 625 | +### 5.9 EditMeal — 份数编辑与营养重算 | ||
| 626 | + | ||
| 627 | +**文件**:`entry/src/main/ets/view/EditMeal.ets` | ||
| 628 | + | ||
| 629 | + | ||
| 630 | +**份数输入校验**:`onWillChange` 验证必须为 0.5 的倍数,范围 0-10。 | ||
| 631 | + | ||
| 632 | +**营养素自动重算**:`ratio = currentServings / originalServings`,按比例乘以原始营养值。 | ||
| 633 | + | ||
| 634 | +**返回更新数据**:`pageStack.pop(updatedRecord)` 带返回值,History 页面通过 `onPop` 回调接收并 `rdbStore.update`。 | ||
| 635 | + | ||
| 636 | +### 5.10 数据层 — RDB 单例与业务服务 | ||
| 637 | + | ||
| 638 | +#### FoodRDB(`data/FoodRDB.ets`) | ||
| 639 | + | ||
| 640 | +**单例模式**:`const foodRDB = new FoodRDB(); export default foodRDB` | ||
| 641 | + | ||
| 642 | +**初始化流程**:`init(context)` → 创建 `initPromise` → `getRdbStore` → 建表/建索引 → 设置 version=1 → 检查空表则 `batchInsert` 导入初始数据 → resolve。 | ||
| 643 | + | ||
| 644 | +**异步竞态保护**:`waitForInit()` 确保所有 CRUD 操作在 `rdbStore` 就绪后执行。 | ||
| 645 | + | ||
| 646 | +**CRUD 方法**: | ||
| 647 | + | ||
| 648 | +| 方法 | 底层 API | 用途 | | ||
| 649 | +|-----|---------|------| | ||
| 650 | +| insert | batchInsert | 批量插入 | | ||
| 651 | +| query | querySql | SQL 查询,返回 ResultSet | | ||
| 652 | +| executeSql | executeSql | 执行非查询 SQL | | ||
| 653 | +| update | update | 按条件更新 | | ||
| 654 | +| delete | delete | 按条件删除 | | ||
| 655 | + | ||
| 656 | +#### NutritionService(`service/NutritionService.ets`) | ||
| 657 | + | ||
| 658 | +**核心查询方法**: | ||
| 659 | + | ||
| 660 | +| 方法 | SQL 逻辑 | 返回类型 | | ||
| 661 | +|-----|---------|---------| | ||
| 662 | +| getDailySummary(date) | SUM + COUNT + SUM(water) | DailySummary | | ||
| 663 | +| getWeeklySummary(start,end) | 同上 + BETWEEN | DailySummary | | ||
| 664 | +| getWeeklyDailyCalories(start,end) | GROUP BY date, calories > 0 | DailyCalories[] | | ||
| 665 | +| getWeeklyDailyWater(start,end) | GROUP BY date, 填充零值天 | DailyWater[] | | ||
| 666 | +| getMealRecords(date) | SELECT WHERE date = ? | MealRecord[] | | ||
| 667 | +| getWaterRecords(date) | SELECT ORDER BY create_time DESC | WaterRecord[] | | ||
| 668 | +| addWaterRecord(date,amount) | INSERT INTO water_record | void | | ||
| 669 | +| deleteWaterRecord(id) | DELETE WHERE id = ? | void | | ||
| 670 | +| hasRecordsInLast7Days() | COUNT > 0 | boolean | | ||
| 671 | + | ||
| 672 | +**getWeeklyDailyWater 填充零值**:先创建 7 个默认对象(water=0),再用查询结果按日期覆盖,确保柱状图连续 7 天。 | ||
| 673 | + | ||
| 674 | +#### UserProfilePreferences(`data/UserProfilePreferences.ets`) | ||
| 675 | + | ||
| 676 | +**偏好存储键**: | ||
| 677 | + | ||
| 678 | +| 键名 | 存储内容 | 类型 | | ||
| 679 | +|-----|---------|------| | ||
| 680 | +| USER_PROFILE_KEY | 用户资料 JSON | string | | ||
| 681 | +| WATER_ACHIEVE_DAYS_KEY | 饮水达标天数 | number | | ||
| 682 | +| CURRENT_WEEK_KEY | 当前周标识 | string | | ||
| 683 | +| LAST_ACHIEVE_DATE_KEY | 最近达标日期 | string | | ||
| 684 | + | ||
| 685 | +**自动重置**:`getWaterAchieveDays` 检查 `CURRENT_WEEK_KEY`,不一致则重置为 0。**每日仅计一次**:`LAST_ACHIEVE_DATE_KEY` 防止同天重复计数。 | ||
| 686 | + | ||
| 687 | +### 5.11 组件层 — 可复用 UI 组件 | ||
| 688 | + | ||
| 689 | +#### NutrientCard | ||
| 690 | + | ||
| 691 | +单营养素指示卡片,`@Prop` 接收 icon/label/value/unit/color/bgColor,100px 高,12px 圆角,左侧图标 + 右侧数值信息。 | ||
| 692 | + | ||
| 693 | +#### MealTypeSelector | ||
| 694 | + | ||
| 695 | +4 选 1 餐次选择器,`@Link selectedMealType` 双向绑定,选中态 `primary_color` 背景。 | ||
| 696 | + | ||
| 697 | +#### FoodTypeList | ||
| 698 | + | ||
| 699 | +水平滚动分类标签列表,`@Prop param: CategoryItem[]` + `onItemClick` 回调,选中/未选中样式统一。 | ||
| 700 | + | ||
| 701 | +#### NutritionSummary | ||
| 702 | + | ||
| 703 | +本餐营养素汇总卡片,`@Prop` 接收四项营养素,热量行 `primary_color` 高亮,`@Builder nutritionRow` 减少重复。 | ||
| 704 | + | ||
| 705 | +#### PageTitle | ||
| 706 | + | ||
| 707 | +`@Builder` 函数(非 `@Component`),渲染 24px 粗体居中标题。 | ||
| 708 | + | ||
| 709 | +--- | ||
| 710 | + | ||
| 711 | +## 6. OpenHarmony SDK API 使用总览 | ||
| 712 | + | ||
| 713 | +### 6.1 API 一览表 | ||
| 714 | + | ||
| 715 | +| Kit | API / 接口 | 用途 | 使用位置 | | ||
| 716 | +|-----|-----------|------|---------| | ||
| 717 | +| `@ohos.data.ArkData` | `relationalStore.getRdbStore` | 获取 RDB 数据库实例 | FoodRDB | | ||
| 718 | +| `@ohos.data.ArkData` | `relationalStore.StoreConfig` | 数据库配置(名称、安全级别) | FoodRDB | | ||
| 719 | +| `@ohos.data.ArkData` | `relationalStore.RdbStore` | RDB 操作接口(CRUD) | FoodRDB | | ||
| 720 | +| `@ohos.data.ArkData` | `relationalStore.RdbPredicates` | 构造查询/更新/删除条件 | History, AddRecord | | ||
| 721 | +| `@ohos.data.ArkData` | `relationalStore.ValuesBucket` | 插入/更新的数据键值对 | FoodRDB, AddRecord, AddFoodPage | | ||
| 722 | +| `@ohos.data.ArkData` | `relationalStore.ResultSet` | 查询结果集 | NutritionService, FoodLibrary | | ||
| 723 | +| `@ohos.data.ArkData` | `relationalStore.SecurityLevel` | 数据库安全级别枚举 | FoodRDB | | ||
| 724 | +| `@ohos.data.ArkData` | `preferences.getPreferencesSync` | 同步获取偏好存储实例 | UserProfilePreferences | | ||
| 725 | +| `@ohos.data.ArkData` | `preferences.Preferences` | 偏好存储读写接口 | UserProfilePreferences, FoodLibrary | | ||
| 726 | +| `@ohos.app.ability.AbilityKit` | `UIAbility` | 应用入口 Ability 基类 | EntryAbility | | ||
| 727 | +| `@ohos.app.ability.AbilityKit` | `AbilityConstant.LaunchParam` | 启动参数 | EntryAbility | | ||
| 728 | +| `@ohos.app.ability.AbilityKit` | `common.UIAbilityContext` | Ability 上下文 | FoodRDB, UserProfilePreferences | | ||
| 729 | +| `@ohos.ArkUI` | `Navigation` / `NavPathStack` | 页面导航栈管理 | Index | | ||
| 730 | +| `@ohos.ArkUI` | `NavDestination` | 子页面容器 | AddRecord, AddFood, AddFoodPage, EditMeal | | ||
| 731 | +| `@ohos.ArkUI` | `Tabs` / `TabContent` | 底部标签页 | Index | | ||
| 732 | +| `@ohos.ArkUI` | `List` / `LazyForEach` / `IDataSource` | 列表懒加载 | FoodLibrary | | ||
| 733 | +| `@ohos.ArkUI` | `Gauge` | 环形仪表盘 | Home, Profile | | ||
| 734 | +| `@ohos.ArkUI` | `Progress` | 进度条 | Home, History | | ||
| 735 | +| `@ohos.ArkUI` | `TextInput` | 文本输入框 | AddFoodPage, EditMeal, Profile Dialog | | ||
| 736 | +| `@ohos.ArkUI` | `TextPicker` | 滚动选择器 | AddRecord | | ||
| 737 | +| `@ohos.ArkUI` | `CustomDialog` | 自定义弹窗 | Home, Profile, FoodLibrary | | ||
| 738 | +| `@ohos.ArkUI` | `eventHub.emit` / `eventHub.on` | 事件通信 | Home, History | | ||
| 739 | +| `@ohos.ArkUI` | `uiObserver.on` | 页面生命周期监听 | Home, Statistics | | ||
| 740 | +| `@ohos.ArkUI` | `bindMenu` / `bindContextMenu` | 菜单绑定 | History | | ||
| 741 | +| `@ohos.ArkUI` | `bindSheet` | 半模态弹窗 | AddRecord | | ||
| 742 | +| `@ohos.ArkUI` | `bindContentCover` | 全屏模态 | History | | ||
| 743 | +| `@ohos.ArkUI` | `RelativeContainer` | 相对布局容器 | Profile | | ||
| 744 | +| `@ohos.PerformanceAnalysisKit` | `hilog.debug/info/warn/error` | 日志输出 | Logger(全项目) | | ||
| 745 | +| 第三方库 | `@mcui/mccharts` McPieChart | 环形饼图 | Statistics | | ||
| 746 | +| 第三方库 | `@mcui/mccharts` McBarChart | 柱状图 | Statistics | | ||
| 747 | + | ||
| 748 | +### 6.2 @ohos.data.ArkData — relationalStore | ||
| 749 | + | ||
| 750 | +关系型数据库(RDB)是应用的核心数据持久化方案,用于存储食物数据、饮食记录、饮水记录和收藏信息。 | ||
| 751 | + | ||
| 752 | +**数据库配置**: | ||
| 753 | + | ||
| 754 | +```typescript | ||
| 755 | +const STORE_CONFIG: relationalStore.StoreConfig = { | ||
| 756 | + name: 'Foods.db', | ||
| 757 | + securityLevel: relationalStore.SecurityLevel.S3, | ||
| 758 | + encrypt: false, | ||
| 759 | + isReadOnly: false, | ||
| 760 | + tokenizer: relationalStore.Tokenizer.ICU_TOKENIZER | ||
| 761 | +} | ||
| 762 | +``` | ||
| 763 | + | ||
| 764 | +**获取数据库实例**: | ||
| 765 | + | ||
| 766 | +```typescript | ||
| 767 | +relationalStore.getRdbStore(context, STORE_CONFIG, async (err, store) => { | ||
| 768 | + this.rdbStore = store | ||
| 769 | +}) | ||
| 770 | +``` | ||
| 771 | + | ||
| 772 | +**建表与版本管理**: | ||
| 773 | + | ||
| 774 | +```typescript | ||
| 775 | +if (store.version === 0) { | ||
| 776 | + await store.executeSql(CREATE_FOOD_TABLE) | ||
| 777 | + await store.executeSql(CREATE_MEAL_RECORD_TABLE) | ||
| 778 | + // ... 更多建表和建索引 SQL | ||
| 779 | + store.version = 1 | ||
| 780 | +} | ||
| 781 | +``` | ||
| 782 | + | ||
| 783 | +**批量插入**: | ||
| 784 | + | ||
| 785 | +```typescript | ||
| 786 | +await this.rdbStore.batchInsert(tableName, valuesBuckets) | ||
| 787 | +``` | ||
| 788 | + | ||
| 789 | +**SQL 查询**: | ||
| 790 | + | ||
| 791 | +```typescript | ||
| 792 | +const resultSet: relationalStore.ResultSet = await this.rdbStore.querySql(querySQL) | ||
| 793 | +resultSet.goToFirstRow() | ||
| 794 | +const value = resultSet.getLong(resultSet.getColumnIndex('column_name')) | ||
| 795 | +resultSet.close() | ||
| 796 | +``` | ||
| 797 | + | ||
| 798 | +**条件更新/删除**: | ||
| 799 | + | ||
| 800 | +```typescript | ||
| 801 | +const predicates = new relationalStore.RdbPredicates('meal_record') | ||
| 802 | +predicates.equalTo('id', recordId) | ||
| 803 | +await this.rdbStore.update(values, predicates) | ||
| 804 | +await this.rdbStore.delete(predicates) | ||
| 805 | +``` | ||
| 806 | + | ||
| 807 | +### 6.3 @ohos.data.ArkData — preferences | ||
| 808 | + | ||
| 809 | +轻量级偏好存储,用于保存用户配置和最近使用记录。 | ||
| 810 | + | ||
| 811 | +**获取实例**: | ||
| 812 | + | ||
| 813 | +```typescript | ||
| 814 | +const prefs = preferences.getPreferencesSync(context, { name: storeId }) | ||
| 815 | +``` | ||
| 816 | + | ||
| 817 | +**写入数据**: | ||
| 818 | + | ||
| 819 | +```typescript | ||
| 820 | +prefs.putSync(key, jsonString) | ||
| 821 | +prefs.flush() | ||
| 822 | +``` | ||
| 823 | + | ||
| 824 | +**读取数据**: | ||
| 825 | + | ||
| 826 | +```typescript | ||
| 827 | +const value = prefs.getSync(key, defaultValue) as string | ||
| 828 | +``` | ||
| 829 | + | ||
| 830 | +**使用场景**: | ||
| 831 | + | ||
| 832 | +| 使用位置 | Store ID | 键名 | 存储内容 | | ||
| 833 | +|---------|----------|------|---------| | ||
| 834 | +| UserProfilePreferences | USER_PROFILE_STORE_ID | USER_PROFILE_KEY | 用户资料 JSON | | ||
| 835 | +| UserProfilePreferences | USER_PROFILE_STORE_ID | WATER_ACHIEVE_DAYS_KEY | 饮水达标天数 | | ||
| 836 | +| UserProfilePreferences | USER_PROFILE_STORE_ID | CURRENT_WEEK_KEY | 当前周标识 | | ||
| 837 | +| UserProfilePreferences | USER_PROFILE_STORE_ID | LAST_ACHIEVE_DATE_KEY | 最近达标日期 | | ||
| 838 | +| FoodLibrary | RECENT_FOOD_STORE_ID | KEY_COLLECT_FOOD_IDS | 最近使用食物 ID 列表 | | ||
| 839 | + | ||
| 840 | +### 6.4 @ohos.app.ability.AbilityKit — UIAbility | ||
| 841 | + | ||
| 842 | +应用入口 Ability,负责初始化数据库和加载根页面。 | ||
| 843 | + | ||
| 844 | +**生命周期回调**: | ||
| 845 | + | ||
| 846 | +```typescript | ||
| 847 | +export default class EntryAbility extends UIAbility { | ||
| 848 | + onCreate(want: Want, launchParam: LaunchParam): void { | ||
| 849 | + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET) | ||
| 850 | + foodRDB.init(this.context) // 初始化 RDB 数据库 | ||
| 851 | + } | ||
| 852 | + | ||
| 853 | + onWindowStageCreate(windowStage: window.WindowStage): void { | ||
| 854 | + windowStage.loadContent('pages/Index', (err) => { ... }) | ||
| 855 | + } | ||
| 856 | +} | ||
| 857 | +``` | ||
| 858 | + | ||
| 859 | +**关键点**:RDB 初始化在 `onCreate` 中执行,确保数据库在任何页面查询前已就绪。`UIAbilityContext` 传递给 FoodRDB 和 UserProfilePreferences 用于获取数据库和偏好存储实例。 | ||
| 860 | + | ||
| 861 | +### 6.5 @ohos.ArkUI — Navigation 导航系统 | ||
| 862 | + | ||
| 863 | +项目使用 `Navigation` + `NavPathStack` 实现单页面多路由架构。 | ||
| 864 | + | ||
| 865 | +**Navigation 容器**: | ||
| 866 | + | ||
| 867 | +```typescript | ||
| 868 | +Navigation(this.navPathStack) | ||
| 869 | + .mode(NavigationMode.Stack) | ||
| 870 | + .navDestination(this.PagesMap) | ||
| 871 | +``` | ||
| 872 | + | ||
| 873 | +**NavPathStack 操作**: | ||
| 874 | + | ||
| 875 | +| 操作 | API | 说明 | | ||
| 876 | +|-----|-----|------| | ||
| 877 | +| 跳转子页面 | `navPathStack.pushPath({ name: route, param: data })` | 带参数跳转 | | ||
| 878 | +| 返回上一页 | `navPathStack.pop()` | 无返回值 | | ||
| 879 | +| 带返回值返回 | `navPathStack.pop(result)` | 传递结果给调用方 | | ||
| 880 | +| 接收参数 | `onReady((context) => { context.pathInfo.param })` | 子页面接收参数 | | ||
| 881 | +| 接收返回值 | `pushPath({ name, param, onPop: (result) => {} })` | 调用方回调 | | ||
| 882 | + | ||
| 883 | +### 6.6 @ohos.ArkUI — 核心组件 API | ||
| 884 | + | ||
| 885 | +**Gauge(环形仪表盘)**:用于 Home 页面热量环和 Profile 页面 BMI 仪表盘。 | ||
| 886 | + | ||
| 887 | +```typescript | ||
| 888 | +Gauge({ value: bmiValue, min: 0, max: 36 }) | ||
| 889 | + .colors([[color1, stop1], [color2, stop2], ...]) | ||
| 890 | + .width(200).height(200) | ||
| 891 | +``` | ||
| 892 | + | ||
| 893 | +**Progress(进度条)**:用于 Home/History 页面热量和饮水进度。 | ||
| 894 | + | ||
| 895 | +```typescript | ||
| 896 | +Progress({ value: current, total: target, type: ProgressType.Linear }) | ||
| 897 | + .color(percentage > 1 ? AppColorMap.STATUS_EXCESS : AppColorMap.STATUS_NORMAL) | ||
| 898 | +``` | ||
| 899 | + | ||
| 900 | +**List + LazyForEach + IDataSource**:用于 FoodLibrary 食物列表懒加载。 | ||
| 901 | + | ||
| 902 | +```typescript | ||
| 903 | +List() { | ||
| 904 | + LazyForEach(this.foodDataSource, (item: FoodItem) => { | ||
| 905 | + ListItem() { ... } | ||
| 906 | + }) | ||
| 907 | +} | ||
| 908 | +``` | ||
| 909 | + | ||
| 910 | +**CustomDialog(自定义弹窗)**:用于饮水记录、用户资料编辑等弹窗。 | ||
| 911 | + | ||
| 912 | +```typescript | ||
| 913 | +@CustomDialog | ||
| 914 | +struct WaterDialog { | ||
| 915 | + controller: CustomDialogController | ||
| 916 | + @Link waterAmount: number | ||
| 917 | +} | ||
| 918 | +``` | ||
| 919 | + | ||
| 920 | +**TextInput(文本输入)**:用于表单输入,配合 `onWillChange` 实现实时校验。 | ||
| 921 | + | ||
| 922 | +```typescript | ||
| 923 | +TextInput({ text: $$this.currentServings }) | ||
| 924 | + .type(InputType.Number) | ||
| 925 | + .onWillChange((value) => { | ||
| 926 | + const num = Number(value.content) | ||
| 927 | + if (num % 0.5 !== 0) return false | ||
| 928 | + return true | ||
| 929 | + }) | ||
| 930 | +``` | ||
| 931 | + | ||
| 932 | +**TextPicker(滚动选择器)**:用于 AddRecord 页面份数选择。 | ||
| 933 | + | ||
| 934 | +```typescript | ||
| 935 | +TextPicker({ range: servingsRange, selected: selectedIndex }) | ||
| 936 | + .onChange((value) => { this.tempServings = Number(value) }) | ||
| 937 | +``` | ||
| 938 | + | ||
| 939 | +**bindSheet / bindContentCover / bindMenu**:半模态、全屏模态、菜单绑定。 | ||
| 940 | + | ||
| 941 | +### 6.7 @ohos.ArkUI — eventHub 基于发布-订阅模式实现的事件通信机制 | ||
| 942 | + | ||
| 943 | +EventHub是系统提供的基于发布-订阅模式实现的事件通信机制。通过事件名,实现了发送方和订阅方之间的解耦,支持不同业务模块间的高效数据传递和状态同步。 | ||
| 944 | + | ||
| 945 | +**发布事件**: | ||
| 946 | + | ||
| 947 | +```typescript | ||
| 948 | +eventHub.emit({ eventId: EVENT_ID_HISTORY_NOW }) | ||
| 949 | +``` | ||
| 950 | + | ||
| 951 | +**订阅事件**: | ||
| 952 | + | ||
| 953 | +```typescript | ||
| 954 | +eventHub.on({ eventId: EVENT_ID_HISTORY_NOW }, () => { | ||
| 955 | + this.switchToDate(todayKey) | ||
| 956 | +}) | ||
| 957 | +``` | ||
| 958 | + | ||
| 959 | +**使用场景**:Home 页面通知 History 页面跳转到今天日期。 | ||
| 960 | + | ||
| 961 | +### 6.8 @ohos.ArkUI — uiObserver 页面刷新监听 | ||
| 962 | + | ||
| 963 | +监听标签页和导航页面的显示/隐藏事件,触发数据刷新。 | ||
| 964 | + | ||
| 965 | +**标签页刷新**: | ||
| 966 | + | ||
| 967 | +```typescript | ||
| 968 | +this.uiObserver.on('tabContentUpdate', TAB_HOME_ID, () => { | ||
| 969 | + this.refreshData() | ||
| 970 | +}) | ||
| 971 | +``` | ||
| 972 | + | ||
| 973 | +**导航返回刷新**: | ||
| 974 | + | ||
| 975 | +```typescript | ||
| 976 | +this.uiObserver.on('navDestinationSwitch', TAB_HOME_ID, (dest) => { | ||
| 977 | + if (dest.id === 'navBar') { | ||
| 978 | + this.refreshData() | ||
| 979 | + } | ||
| 980 | +}) | ||
| 981 | +``` | ||
| 982 | + | ||
| 983 | +### 6.9 @ohos.PerformanceAnalysisKit — hilog | ||
| 984 | + | ||
| 985 | +日志系统,封装为 `Logger` 工具类供全项目使用。 | ||
| 986 | + | ||
| 987 | +**封装**: | ||
| 988 | + | ||
| 989 | +```typescript | ||
| 990 | +import { hilog } from '@kit.PerformanceAnalysisKit' | ||
| 991 | + | ||
| 992 | +const APP_DOMAIN: number = 0x0000 | ||
| 993 | +const APP_TAG: string = 'NutriLog' | ||
| 994 | + | ||
| 995 | +class Logger { | ||
| 996 | + static info(tag: string, msg: string, ...args: Object[]): void { | ||
| 997 | + hilog.info(APP_DOMAIN, tag, msg, ...args) | ||
| 998 | + } | ||
| 999 | + static error(tag: string, msg: string, ...args: Object[]): void { | ||
| 1000 | + hilog.error(APP_DOMAIN, tag, msg, ...args) | ||
| 1001 | + } | ||
| 1002 | +} | ||
| 1003 | +``` | ||
| 1004 | + | ||
| 1005 | +**使用方式**:各文件定义 `const TAG = APP_TAG + '.ComponentName'`,调用 `Logger.info(TAG, '操作成功')`,隐私参数用 `%{public}s` 占位。 | ||
| 1006 | + | ||
| 1007 | +### 6.10 第三方库 — @mcui/mccharts | ||
| 1008 | + | ||
| 1009 | +基于 OpenHarmony 的图表组件库,用于 Statistics 页面的数据可视化。 | ||
| 1010 | + | ||
| 1011 | +**安装**:`oh-package.json5` 中声明 `"@mcui/mccharts": "^2.8.9"` | ||
| 1012 | + | ||
| 1013 | +**McPieChart(环形饼图)**: | ||
| 1014 | + | ||
| 1015 | +```typescript | ||
| 1016 | +McPieChart({ options: this.pieOption }) | ||
| 1017 | +``` | ||
| 1018 | + | ||
| 1019 | +配置项:radius(35%-70% 内外半径)、data(值/名称/颜色数组)、label(显示百分比)。 | ||
| 1020 | + | ||
| 1021 | +**McBarChart(柱状图)**: | ||
| 1022 | + | ||
| 1023 | +```typescript | ||
| 1024 | +McBarChart({ options: this.barOption }) | ||
| 1025 | +``` | ||
| 1026 | + | ||
| 1027 | +配置项:xAxis(日期标签)、series(柱子数据/颜色/宽度/圆角)、yAxis(隐藏轴线/分割线)、tooltip、legend。 | ||
| 1028 | + | ||
| 1029 | +--- | ||
| 1030 | + | ||
| 1031 | +## 7. 附录 | ||
| 1032 | + | ||
| 1033 | +### 7.1 数据库表结构 | ||
| 1034 | + | ||
| 1035 | +#### food 表(食物数据) | ||
| 1036 | + | ||
| 1037 | +| 列名 | 类型 | 约束 | 说明 | | ||
| 1038 | +|-----|------|------|------| | ||
| 1039 | +| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 主键 | | ||
| 1040 | +| name | TEXT | NOT NULL | 食物名称 | | ||
| 1041 | +| category | INTEGER | NOT NULL DEFAULT 5 | 分类(FoodCategory 枚举值) | | ||
| 1042 | +| calories_per_100g | REAL | NOT NULL | 每 100g 热量(kcal) | | ||
| 1043 | +| protein_per_100g | REAL | NOT NULL DEFAULT 0 | 每 100g 蛋白质(g) | | ||
| 1044 | +| carbs_per_100g | REAL | NOT NULL DEFAULT 0 | 每 100g 碳水化合物(g) | | ||
| 1045 | +| fat_per_100g | REAL | NOT NULL DEFAULT 0 | 每 100g 脂肪(g) | | ||
| 1046 | +| fiber_per_100g | REAL | NOT NULL DEFAULT 0 | 每 100g 膳食纤维(g) | | ||
| 1047 | +| icon | TEXT | NOT NULL DEFAULT '' | 图标文件名(如 white_rice.svg) | | ||
| 1048 | +| is_custom | INTEGER | NOT NULL DEFAULT 0 | 是否自定义食物(0=预设 1=自定义) | | ||
| 1049 | + | ||
| 1050 | +#### meal_record 表(饮食记录) | ||
| 1051 | + | ||
| 1052 | +| 列名 | 类型 | 约束 | 说明 | | ||
| 1053 | +|-----|------|------|------| | ||
| 1054 | +| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 主键 | | ||
| 1055 | +| date | TEXT | NOT NULL | 日期(YYYY-MM-DD) | | ||
| 1056 | +| meal_type | INTEGER | NOT NULL | 餐次类型(MealType 枚举值) | | ||
| 1057 | +| food_id | INTEGER | NOT NULL | 关联 food 表 id | | ||
| 1058 | +| food_name | TEXT | NOT NULL | 食物名称(冗余存储) | | ||
| 1059 | +| servings | REAL | NOT NULL DEFAULT 1.0 | 份数(1 份=100g) | | ||
| 1060 | +| calories | REAL | NOT NULL | 本餐热量(kcal) | | ||
| 1061 | +| protein | REAL | NOT NULL DEFAULT 0 | 本餐蛋白质(g) | | ||
| 1062 | +| carbs | REAL | NOT NULL DEFAULT 0 | 本餐碳水化合物(g) | | ||
| 1063 | +| fat | REAL | NOT NULL DEFAULT 0 | 本餐脂肪(g) | | ||
| 1064 | +| create_time | INTEGER | NOT NULL | 创建时间戳(毫秒) | | ||
| 1065 | + | ||
| 1066 | +#### water_record 表(饮水记录) | ||
| 1067 | + | ||
| 1068 | +| 列名 | 类型 | 约束 | 说明 | | ||
| 1069 | +|-----|------|------|------| | ||
| 1070 | +| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 主键 | | ||
| 1071 | +| date | TEXT | NOT NULL | 日期(YYYY-MM-DD) | | ||
| 1072 | +| amount | REAL | NOT NULL | 饮水量(ml) | | ||
| 1073 | +| create_time | INTEGER | NOT NULL | 创建时间戳(毫秒) | | ||
| 1074 | + | ||
| 1075 | +#### favorite 表(收藏记录) | ||
| 1076 | + | ||
| 1077 | +| 列名 | 类型 | 约束 | 说明 | | ||
| 1078 | +|-----|------|------|------| | ||
| 1079 | +| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 主键 | | ||
| 1080 | +| food_id | INTEGER | NOT NULL | 关联 food 表 id | | ||
| 1081 | +| create_time | INTEGER | NOT NULL | 收藏时间戳(毫秒) | | ||
| 1082 | + | ||
| 1083 | +#### 索引 | ||
| 1084 | + | ||
| 1085 | +| 索引名 | 表 | 列 | 类型 | | ||
| 1086 | +|-------|---|-----|------| | ||
| 1087 | +| idx_meal_record_date | meal_record | date | 单列索引 | | ||
| 1088 | +| idx_meal_record_meal_type | meal_record | date, meal_type | 复合索引 | | ||
| 1089 | +| idx_water_record_date | water_record | date | 单列索引 | | ||
| 1090 | +| idx_favorite_food_id | favorite | food_id | 单列索引 | | ||
| 1091 | + | ||
| 1092 | +### 7.2 核心类型定义速查 | ||
| 1093 | + | ||
| 1094 | +#### FoodItem(食物数据项) | ||
| 1095 | + | ||
| 1096 | +| 属性 | 类型 | 说明 | | ||
| 1097 | +|-----|------|------| | ||
| 1098 | +| id | number? | 主键(自动生成) | | ||
| 1099 | +| name | string | 食物名称 | | ||
| 1100 | +| category | FoodCategory | 分类枚举 | | ||
| 1101 | +| caloriesPer100g | number | 每 100g 热量 | | ||
| 1102 | +| proteinPer100g | number | 每 100g 蛋白质 | | ||
| 1103 | +| carbsPer100g | number | 每 100g 碳水 | | ||
| 1104 | +| fatPer100g | number | 每 100g 脂肪 | | ||
| 1105 | +| fiberPer100g | number | 每 100g 纤维 | | ||
| 1106 | +| icon | string | 图标文件名 | | ||
| 1107 | +| isCustom | boolean | 是否自定义 | | ||
| 1108 | + | ||
| 1109 | +#### MealRecord(饮食记录) | ||
| 1110 | + | ||
| 1111 | +| 属性 | 类型 | 说明 | | ||
| 1112 | +|-----|------|------| | ||
| 1113 | +| id | number? | 主键 | | ||
| 1114 | +| date | string | 日期 YYYY-MM-DD | | ||
| 1115 | +| mealType | MealType | 餐次枚举 | | ||
| 1116 | +| foodId | number | 食物 ID | | ||
| 1117 | +| foodName | string | 食物名称 | | ||
| 1118 | +| servings | number | 份数 | | ||
| 1119 | +| calories | number | 热量 | | ||
| 1120 | +| protein | number | 蛋白质 | | ||
| 1121 | +| carbs | number | 碳水 | | ||
| 1122 | +| fat | number | 脂肪 | | ||
| 1123 | +| createTime | number | 时间戳 | | ||
| 1124 | +| icon | string | 食物图标 | | ||
| 1125 | + | ||
| 1126 | +#### UserProfile(用户资料) | ||
| 1127 | + | ||
| 1128 | +| 属性 | 类型 | 说明 | | ||
| 1129 | +|-----|------|------| | ||
| 1130 | +| height | number \| null | 身高(cm) | | ||
| 1131 | +| weight | number \| null | 体重(kg) | | ||
| 1132 | +| age | number \| null | 年龄 | | ||
| 1133 | +| gender | GenderEnum \| null | 性别 | | ||
| 1134 | +| calorieTarget | number | 热量目标(kcal) | | ||
| 1135 | +| waterTarget | number | 饮水目标(ml) | | ||
| 1136 | + | ||
| 1137 | +#### 枚举速查 | ||
| 1138 | + | ||
| 1139 | +| 枚举名 | 值 | 说明 | | ||
| 1140 | +|-------|---|------| | ||
| 1141 | +| FoodCategory | 0-5 | GRAIN/VEGETABLE/MEAT_EGG/FRUIT/DRINK/OTHER | | ||
| 1142 | +| MealType | 0-3 | BREAKFAST/LUNCH/DINNER/SNACK | | ||
| 1143 | +| GenderEnum | '0'/'1' | MALE/FEMALE | | ||
| 1144 | +| BmiLevelEnum | string | 偏瘦/正常/偏胖/肥胖 | | ||
| 1145 | +| FoodElement | string | 热量/蛋白质/脂肪/碳水/膳食纤维 | | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +/node_modules | ||
| 2 | +/oh_modules | ||
| 3 | +/.preview | ||
| 4 | +/build | ||
| 5 | +/.cxx | ||
| 6 | +/.test | ||
| @@ -0,0 +1,33 @@ | |||
| 1 | +{ | ||
| 2 | + "apiType": "stageMode", | ||
| 3 | + "buildOption": { | ||
| 4 | + "resOptions": { | ||
| 5 | + "copyCodeResource": { | ||
| 6 | + "enable": true | ||
| 7 | + } | ||
| 8 | + } | ||
| 9 | + }, | ||
| 10 | + "buildOptionSet": [ | ||
| 11 | + { | ||
| 12 | + "name": "release", | ||
| 13 | + "arkOptions": { | ||
| 14 | + "obfuscation": { | ||
| 15 | + "ruleOptions": { | ||
| 16 | + "enable": false, | ||
| 17 | + "files": [ | ||
| 18 | + "./obfuscation-rules.txt" | ||
| 19 | + ] | ||
| 20 | + } | ||
| 21 | + } | ||
| 22 | + } | ||
| 23 | + }, | ||
| 24 | + ], | ||
| 25 | + "targets": [ | ||
| 26 | + { | ||
| 27 | + "name": "default" | ||
| 28 | + }, | ||
| 29 | + { | ||
| 30 | + "name": "ohosTest", | ||
| 31 | + } | ||
| 32 | + ] | ||
| 33 | +} | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; | ||
| 2 | + | ||
| 3 | +export default { | ||
| 4 | + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ | ||
| 5 | + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ | ||
| 6 | +}; | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +# Define project specific obfuscation rules here. | ||
| 2 | +# You can include the obfuscation configuration files in the current module's build-profile.json5. | ||
| 3 | +# | ||
| 4 | +# For more details, see the official documentation on source obfuscation. | ||
| 5 | + | ||
| 6 | +# Obfuscation options: | ||
| 7 | +# -disable-obfuscation: disable all obfuscations | ||
| 8 | +# -enable-property-obfuscation: obfuscate the property names | ||
| 9 | +# -enable-toplevel-obfuscation: obfuscate the names in the global scope | ||
| 10 | +# -compact: remove unnecessary blank spaces and all line feeds | ||
| 11 | +# -remove-log: remove all console.* statements | ||
| 12 | +# -print-namecache: print the name cache that contains the mapping from the old names to new names | ||
| 13 | +# -apply-namecache: reuse the given cache file | ||
| 14 | + | ||
| 15 | +# Keep options: | ||
| 16 | +# -keep-property-name: specifies property names that you want to keep | ||
| 17 | +# -keep-global-name: specifies names that you want to keep in the global scope | ||
| 18 | + | ||
| 19 | +-enable-property-obfuscation | ||
| 20 | +-enable-toplevel-obfuscation | ||
| 21 | +-enable-filename-obfuscation | ||
| 22 | +-enable-export-obfuscation | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | +{ | ||
| 2 | + "name": "entry", | ||
| 3 | + "version": "1.0.0", | ||
| 4 | + "description": "Please describe the basic information.", | ||
| 5 | + "main": "", | ||
| 6 | + "author": "", | ||
| 7 | + "license": "", | ||
| 8 | + "dependencies": {} | ||
| 9 | +} | ||
| 10 | + | ||
| @@ -0,0 +1,15 @@ | |||
| 1 | +{ | ||
| 2 | + "app": { | ||
| 3 | + "bundleName": "com.example.NutriLog", | ||
| 4 | + "patchVersionCode": 2000020, | ||
| 5 | + "versionCode": 1000000, | ||
| 6 | + "versionName": "1.0.0", | ||
| 7 | + "patchVersionName": "2000020" | ||
| 8 | + }, | ||
| 9 | + "module": { | ||
| 10 | + "name": "entry", | ||
| 11 | + "type": "hotreload", | ||
| 12 | + "deviceTypes": ["default"], | ||
| 13 | + "originalModuleHash": "" | ||
| 14 | + } | ||
| 15 | +} | ||
| @@ -0,0 +1,207 @@ | |||
| 1 | +/** | ||
| 2 | + * 全局常量定义 — 数据库、偏好、路由、时间、营养、校验等 | ||
| 3 | + * | ||
| 4 | + * 分区: | ||
| 5 | + * - 数据库常量(库名/版本/表名/列名/聚合别名) | ||
| 6 | + * - 偏好存储常量 | ||
| 7 | + * - 路由名称 | ||
| 8 | + * - 时间常量(毫秒换算) | ||
| 9 | + * - Toast 时长 | ||
| 10 | + * - 营养转换因子(中国营养学会推荐比例) | ||
| 11 | + * - 默认值 | ||
| 12 | + * - 分类相关 | ||
| 13 | + * - BMI 转换 | ||
| 14 | + * - 用户输入校验阈值 | ||
| 15 | + * - 其他业务常量 | ||
| 16 | + */ | ||
| 17 | + | ||
| 18 | +// ========== 数据库常量 ========== | ||
| 19 | +export const DB_NAME: string = 'Foods.db' | ||
| 20 | + | ||
| 21 | +export const DB_VERSION_INITIAL: number = 0 | ||
| 22 | + | ||
| 23 | +export const DB_VERSION_CURRENT: number = 1 | ||
| 24 | + | ||
| 25 | +export const TABLE_FOOD: string = 'food' | ||
| 26 | + | ||
| 27 | +export const TABLE_MEAL_RECORD: string = 'meal_record' | ||
| 28 | + | ||
| 29 | +export const TABLE_WATER_RECORD: string = 'water_record' | ||
| 30 | + | ||
| 31 | +export const TABLE_FAVORITE: string = 'favorite' | ||
| 32 | + | ||
| 33 | +// 数据库列名 — food 表(与 RDB 表结构一一对应) | ||
| 34 | +export const COL_ID: string = 'id' | ||
| 35 | + | ||
| 36 | +export const COL_NAME: string = 'name' | ||
| 37 | + | ||
| 38 | +export const COL_CATEGORY: string = 'category' | ||
| 39 | + | ||
| 40 | +export const COL_CALORIES_PER_100G: string = 'calories_per_100g' | ||
| 41 | + | ||
| 42 | +export const COL_PROTEIN_PER_100G: string = 'protein_per_100g' | ||
| 43 | + | ||
| 44 | +export const COL_CARBS_PER_100G: string = 'carbs_per_100g' | ||
| 45 | + | ||
| 46 | +export const COL_FAT_PER_100G: string = 'fat_per_100g' | ||
| 47 | + | ||
| 48 | +export const COL_FIBER_PER_100G: string = 'fiber_per_100g' | ||
| 49 | + | ||
| 50 | +export const COL_ICON: string = 'icon' | ||
| 51 | + | ||
| 52 | +export const COL_IS_CUSTOM: string = 'is_custom' | ||
| 53 | + | ||
| 54 | +// 数据库列名 — meal_record 表 | ||
| 55 | +export const COL_DATE: string = 'date' | ||
| 56 | + | ||
| 57 | +export const COL_MEAL_TYPE: string = 'meal_type' | ||
| 58 | + | ||
| 59 | +export const COL_FOOD_ID: string = 'food_id' | ||
| 60 | + | ||
| 61 | +export const COL_FOOD_NAME: string = 'food_name' | ||
| 62 | + | ||
| 63 | +export const COL_SERVINGS: string = 'servings' | ||
| 64 | + | ||
| 65 | +export const COL_CALORIES: string = 'calories' | ||
| 66 | + | ||
| 67 | +export const COL_PROTEIN: string = 'protein' | ||
| 68 | + | ||
| 69 | +export const COL_CARBS: string = 'carbs' | ||
| 70 | + | ||
| 71 | +export const COL_FAT: string = 'fat' | ||
| 72 | + | ||
| 73 | +export const COL_CREATE_TIME: string = 'create_time' | ||
| 74 | + | ||
| 75 | +// 数据库列名 — water_record 表 | ||
| 76 | +export const COL_AMOUNT: string = 'amount' | ||
| 77 | + | ||
| 78 | +// 数据库列名 — favorite 表 | ||
| 79 | +export const COL_FAVORITE_FOOD_ID: string = 'food_id' | ||
| 80 | + | ||
| 81 | +// is_custom 标志值(0=预设食物,1=用户自定义食物) | ||
| 82 | +export const IS_CUSTOM_TRUE: number = 1 | ||
| 83 | + | ||
| 84 | +export const IS_CUSTOM_FALSE: number = 0 | ||
| 85 | + | ||
| 86 | +// SQL 聚合查询别名 | ||
| 87 | +export const ALIAS_TOTAL_CAL: string = 'total_cal' | ||
| 88 | + | ||
| 89 | +export const ALIAS_TOTAL_PRO: string = 'total_pro' | ||
| 90 | + | ||
| 91 | +export const ALIAS_TOTAL_CARB: string = 'total_carb' | ||
| 92 | + | ||
| 93 | +export const ALIAS_TOTAL_FAT: string = 'total_fat' | ||
| 94 | + | ||
| 95 | +export const ALIAS_TOTAL_WATER: string = 'total_water' | ||
| 96 | + | ||
| 97 | +export const ALIAS_COUNT: string = 'count' | ||
| 98 | + | ||
| 99 | +export const ALIAS_TOTAL_COUNT: string = 'total_count' | ||
| 100 | + | ||
| 101 | +export const ALIAS_MIN_DATE: string = 'min_date' | ||
| 102 | + | ||
| 103 | +// ========== 偏好存储常量 ========== | ||
| 104 | +export const RECENT_FOOD_STORE_ID: string = 'recent_food_store' | ||
| 105 | + | ||
| 106 | +export const KEY_COLLECT_FOOD_IDS: string = 'collect_food_ids' | ||
| 107 | + | ||
| 108 | +// ========== 路由名称 ========== | ||
| 109 | +export const ROUTE_ADD_RECORD: string = 'addRecord' | ||
| 110 | + | ||
| 111 | +export const ROUTE_ADD_FOOD: string = 'addFood' | ||
| 112 | + | ||
| 113 | +export const ROUTE_EDIT_MEAL: string = 'editMealPage' | ||
| 114 | + | ||
| 115 | +export const INSERT_FOOD = 'insertFoodInfo' | ||
| 116 | + | ||
| 117 | +export const ROUTE_INDEX: string = 'navBar' | ||
| 118 | + | ||
| 119 | + | ||
| 120 | +export const TAB_HOME_ID = 'home' | ||
| 121 | + | ||
| 122 | +export const TAB_WEEK_ID = 'week' | ||
| 123 | + | ||
| 124 | +export const TAB_HISTORY_ID = 'history' | ||
| 125 | + | ||
| 126 | +// ========== 时间常量 ========== | ||
| 127 | +export const MS_PER_DAY: number = 86400000 | ||
| 128 | + | ||
| 129 | +export const MS_PER_HOUR: number = 3600000 | ||
| 130 | + | ||
| 131 | +export const MS_PER_WEEK: number = 604800000 | ||
| 132 | + | ||
| 133 | +export const TIMEOUT_SCROLL_MS: number = 100 | ||
| 134 | + | ||
| 135 | +export const TIMEOUT_CLEAR_MESSAGE_MS: number = 3000 | ||
| 136 | + | ||
| 137 | +// ========== Toast 时长 ========== | ||
| 138 | +export const TOAST_DURATION_SHORT: number = 1000 | ||
| 139 | + | ||
| 140 | +export const TOAST_DURATION_MEDIUM: number = 1500 | ||
| 141 | + | ||
| 142 | +export const TOAST_DURATION_LONG: number = 2000 | ||
| 143 | + | ||
| 144 | +// ========== 营养转换因子(中国营养学会推荐比例) ========== | ||
| 145 | +export const KCAL_PER_GRAM_PROTEIN: number = 4 | ||
| 146 | + | ||
| 147 | +export const KCAL_PER_GRAM_CARBS: number = 4 | ||
| 148 | + | ||
| 149 | +export const KCAL_PER_GRAM_FAT: number = 9 | ||
| 150 | + | ||
| 151 | +// 蛋白质供能比 20%、脂肪供能比 30%、碳水供能比 50% | ||
| 152 | +export const PROTEIN_CALORIE_RATIO: number = 0.20 | ||
| 153 | + | ||
| 154 | +export const FAT_CALORIE_RATIO: number = 0.30 | ||
| 155 | + | ||
| 156 | +export const CARBS_CALORIE_RATIO: number = 0.50 | ||
| 157 | + | ||
| 158 | +// ========== 默认值 ========== | ||
| 159 | +export const DEFAULT_CALORIE_TARGET: number = 1800 | ||
| 160 | + | ||
| 161 | +export const DEFAULT_MEAL_ICON: string = '' | ||
| 162 | + | ||
| 163 | +// ========== 分类相关 ========== | ||
| 164 | +export const FOOD_CATEGORY_ALL: number = 6 | ||
| 165 | + | ||
| 166 | +export const CATEGORY_TAB_OFFSET: number = 2 | ||
| 167 | + | ||
| 168 | +export const CATEGORY_TAB_MIN: number = 2 | ||
| 169 | + | ||
| 170 | +export const CATEGORY_TAB_MAX: number = 7 | ||
| 171 | + | ||
| 172 | +// ========== BMI 转换 ========== | ||
| 173 | +export const CM_PER_METER: number = 100 | ||
| 174 | + | ||
| 175 | +// ========== 用户输入校验阈值 ========== | ||
| 176 | +export const HEIGHT_MIN: number = 30 | ||
| 177 | + | ||
| 178 | +export const HEIGHT_MAX: number = 300 | ||
| 179 | + | ||
| 180 | +export const WEIGHT_MIN: number = 1 | ||
| 181 | + | ||
| 182 | +export const WEIGHT_MAX: number = 500 | ||
| 183 | + | ||
| 184 | +export const AGE_MIN: number = 1 | ||
| 185 | + | ||
| 186 | +export const AGE_MAX: number = 120 | ||
| 187 | + | ||
| 188 | +export const CALORIE_TARGET_MIN: number = 1 | ||
| 189 | + | ||
| 190 | +export const CALORIE_TARGET_MAX: number = 1800 | ||
| 191 | + | ||
| 192 | +export const CALORIE_INIT_VALUE = 1500 | ||
| 193 | + | ||
| 194 | +export const WATER_INIT_VALUE: number = 2000 | ||
| 195 | + | ||
| 196 | +export const WATER_TARGET_MIN: number = 1 | ||
| 197 | + | ||
| 198 | +export const WATER_TARGET_MAX: number = 2500 | ||
| 199 | + | ||
| 200 | +// ========== 其他业务常量 ========== | ||
| 201 | +export const MAX_SERVINGS: number = 10 | ||
| 202 | + | ||
| 203 | +export const DEFAULT_SERVINGS: number = 1 | ||
| 204 | + | ||
| 205 | +export const DEBOUNCE_DELAY_MS: number = 300 | ||
| 206 | + | ||
| 207 | +export const MAX_RECENT_FOOD_COUNT: number = 15 | ||
| @@ -0,0 +1,68 @@ | |||
| 1 | +/** | ||
| 2 | + * BMI 计算工具 — 根据身高体重计算 BMI 值并返回对应分级与颜色 | ||
| 3 | + * | ||
| 4 | + * 分级依据:中国成人 BMI 标准(偏瘦/正常/超重/肥胖) | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | +import { BMI, BmiColor, BmiLevelEnum, BmiLevelMaxValue } from '../model/BMIType'; | ||
| 8 | +import { UserProfile } from '../model/FoodType'; | ||
| 9 | +import { CM_PER_METER } from './AppConstants' | ||
| 10 | + | ||
| 11 | +// BMI 分级区间配置(与 BmiLevelMaxValue 对应) | ||
| 12 | +interface BMIType { | ||
| 13 | + min: number, | ||
| 14 | + max: number, | ||
| 15 | + level: BmiLevelEnum, | ||
| 16 | + color: string | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +const BMI_ENUM: BMIType[] = [ | ||
| 20 | + { | ||
| 21 | + level: BmiLevelEnum.LOW, | ||
| 22 | + min: 0, | ||
| 23 | + max: BmiLevelMaxValue.LOW, | ||
| 24 | + color: BmiColor.LOW | ||
| 25 | + }, | ||
| 26 | + { | ||
| 27 | + level: BmiLevelEnum.NORMAL, | ||
| 28 | + min: BmiLevelMaxValue.LOW, | ||
| 29 | + max: BmiLevelMaxValue.NORMAL, | ||
| 30 | + color: BmiColor.NORMAL | ||
| 31 | + }, | ||
| 32 | + { | ||
| 33 | + level: BmiLevelEnum.OVER, | ||
| 34 | + min: BmiLevelMaxValue.NORMAL, | ||
| 35 | + max: BmiLevelMaxValue.OVER, | ||
| 36 | + color: BmiColor.OVER | ||
| 37 | + }, | ||
| 38 | + { | ||
| 39 | + level: BmiLevelEnum.FAT, | ||
| 40 | + min: BmiLevelMaxValue.OVER, | ||
| 41 | + max: BmiLevelMaxValue.FAT, | ||
| 42 | + color: BmiColor.FAT | ||
| 43 | + }, | ||
| 44 | +]; | ||
| 45 | + | ||
| 46 | +function bmiCalculate(userProfile: UserProfile): BMI { | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + const bmi = userProfile.height ? | ||
| 50 | + (userProfile.weight || 0) / (userProfile.height * userProfile.height / (CM_PER_METER * CM_PER_METER)) : 0 | ||
| 51 | + for (let item of BMI_ENUM) { | ||
| 52 | + if (bmi >= item.min && bmi < item.max) { | ||
| 53 | + return { | ||
| 54 | + value: bmi, | ||
| 55 | + level: item.level, | ||
| 56 | + color: item.color | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + return { | ||
| 62 | + value: bmi, | ||
| 63 | + level: BmiLevelEnum.FAT, | ||
| 64 | + color: BmiColor.FAT | ||
| 65 | + } | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +export { bmiCalculate } | ||
| @@ -0,0 +1,76 @@ | |||
| 1 | +/** | ||
| 2 | + * 颜色常量映射 — 字符串上下文专用(如 LinearGradient、NutrientCard @Prop) | ||
| 3 | + * | ||
| 4 | + * UI 属性请使用 $r('app.color.xxx') 以支持深色模式自动切换, | ||
| 5 | + * 此处仅用于无法使用 ResourceRef 的字符串场景。 | ||
| 6 | + * hex 值必须与 element/color.json 中的 token 值一致。 | ||
| 7 | + */ | ||
| 8 | + | ||
| 9 | +// 颜色常量集合,按功能分类 | ||
| 10 | +class AppColorMap { | ||
| 11 | + | ||
| 12 | + // ========== 基础色 ========== | ||
| 13 | + public static readonly WHITE: string = '#FFFFFF' | ||
| 14 | + public static readonly BLACK: string = '#000000' | ||
| 15 | + public static readonly TRANSPARENT: string = '#00000000' | ||
| 16 | + | ||
| 17 | + // ========== 字体色 ========== | ||
| 18 | + public static readonly FONT_PRIMARY: string = '#333333' | ||
| 19 | + public static readonly FONT_SECONDARY: string = '#555555' | ||
| 20 | + public static readonly FONT_HINT: string = '#999999' | ||
| 21 | + public static readonly FONT_MID: string = '#666666' | ||
| 22 | + | ||
| 23 | + // ========== 灰色系 ========== | ||
| 24 | + public static readonly GRAY_LIGHT: string = '#E6E6E6' | ||
| 25 | + public static readonly GRAY_DIVIDER: string = '#D3D3D3' | ||
| 26 | + public static readonly GRAY_DISABLED_HALF: string = '#808B8B89' | ||
| 27 | + public static readonly GRAY_DISABLED_LIGHT: string = '#338B8B89' | ||
| 28 | + | ||
| 29 | + // ========== 阴影色 ========== | ||
| 30 | + public static readonly SHADOW_LIGHT: string = '#1A000000' | ||
| 31 | + public static readonly SHADOW_MEDIUM: string = '#66000000' | ||
| 32 | + public static readonly SHADOW_CARD: string = '#33FFBE78' | ||
| 33 | + | ||
| 34 | + // ========== 品牌主色 ========== | ||
| 35 | + public static readonly PRIMARY_COLOR: string = '#FF9447' | ||
| 36 | + public static readonly PRIMARY_COLOR_DEEP: string = '#FF7700' | ||
| 37 | + public static readonly PRIMARY_COLOR_LIGHT: string = '#FFB12F' | ||
| 38 | + | ||
| 39 | + // ========== 渐变色 ========== | ||
| 40 | + public static readonly GRADIENT_ORANGE: string = '#FFC238' | ||
| 41 | + public static readonly GRADIENT_ORANGE_HALF: string = '#80FEBA39' | ||
| 42 | + | ||
| 43 | + // ========== 背景色 ========== | ||
| 44 | + public static readonly PRIMARY_BACKGROUND: string = '#FFF7E6' | ||
| 45 | + public static readonly BACKGROUND_LIGHT: string = '#F8F8F8' | ||
| 46 | + public static readonly BACKGROUND_CARD: string = '#F5F5F5' | ||
| 47 | + public static readonly BACKGROUND_PINK: string = '#FFE5E5' | ||
| 48 | + | ||
| 49 | + // ========== 营养素背景色 ========== | ||
| 50 | + public static readonly NUTRITION_PROTEIN_BG: string = '#E8F5E9' | ||
| 51 | + public static readonly NUTRITION_CARBS_BG: string = '#E3F2FD' | ||
| 52 | + public static readonly NUTRITION_FAT_BG: string = '#FFF3E0' | ||
| 53 | + | ||
| 54 | + // ========== 营养素强调色 ========== | ||
| 55 | + public static readonly PROTEIN_COLOR: string = '#0028FF' | ||
| 56 | + public static readonly CARBS_COLOR: string = '#FF5900' | ||
| 57 | + public static readonly FAT_COLOR: string = '#FFE400' | ||
| 58 | + | ||
| 59 | + // ========== 状态色 ========== | ||
| 60 | + public static readonly STATUS_NORMAL: string = '#4CAF50' | ||
| 61 | + public static readonly STATUS_EXCESS: string = '#F44336' | ||
| 62 | + public static readonly STATUS_LOW: string = '#3498DB' | ||
| 63 | + public static readonly STATUS_WARNING: string = '#F08080' | ||
| 64 | + | ||
| 65 | + // ========== 图表色 ========== | ||
| 66 | + public static readonly CHART_TEAL: string = '#36C6AD' | ||
| 67 | + public static readonly CHART_CORNFLOWER: string = '#6495ED' | ||
| 68 | + public static readonly CHART_LIME: string = '#ADFF2F' | ||
| 69 | + public static readonly CHART_PURPLE: string = '#9370DB' | ||
| 70 | + public static readonly CHART_PINK: string = '#FFB6C1' | ||
| 71 | + | ||
| 72 | + // ========== 特殊背景 ========== | ||
| 73 | + public static readonly TEAL_BACKGROUND: string = '#E8F8F5' | ||
| 74 | +} | ||
| 75 | + | ||
| 76 | +export { AppColorMap } | ||
| @@ -0,0 +1,9 @@ | |||
| 1 | +/** | ||
| 2 | + * 事件总线键名 — 用于事件订阅/发布的唯一标识 | ||
| 3 | + */ | ||
| 4 | + | ||
| 5 | +// 食物列表刷新事件 | ||
| 6 | +export const EVENT_REFRESH_FOOD = 'EVENT_REFRESH_FOOD' | ||
| 7 | + | ||
| 8 | +// 历史记录刷新 | ||
| 9 | +export const EVENT_ID_HISTORY_NOW = 'EVENT_ID_HISTORY_NOW' | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | +/** | ||
| 2 | + * 日期格式化工具 — Date 对象转 'YYYY-MM-DD' 字符串 | ||
| 3 | + */ | ||
| 4 | + | ||
| 5 | +function formatDate(date: Date): string { | ||
| 6 | + const year = date.getFullYear(); | ||
| 7 | + const month = String(date.getMonth() + 1).padStart(2, '0'); | ||
| 8 | + const day = String(date.getDate()).padStart(2, '0'); | ||
| 9 | + return `${year}-${month}-${day}`; | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +export {formatDate} | ||
| @@ -0,0 +1,35 @@ | |||
| 1 | +/** | ||
| 2 | + * 日志工具类 — 封装 hilog,统一日志标签前缀 | ||
| 3 | + * | ||
| 4 | + * 使用方式: | ||
| 5 | + * - 文件顶部定义 TAG:const TAG = `${APP_TAG}.ComponentName` | ||
| 6 | + * - 调用 Logger.info(TAG, '操作成功信息') | ||
| 7 | + * - 隐私参数用 %{public}s 占位 | ||
| 8 | + */ | ||
| 9 | + | ||
| 10 | +import { hilog } from '@kit.PerformanceAnalysisKit' | ||
| 11 | + | ||
| 12 | +// hilog 域 ID(0x0000 表示应用域) | ||
| 13 | +const APP_DOMAIN: number = 0x0000 | ||
| 14 | +// 全局日志标签前缀 | ||
| 15 | +const APP_TAG: string = 'NutriLog' | ||
| 16 | + | ||
| 17 | +class Logger { | ||
| 18 | + static debug(tag: string, msg: string, ...args: Object[]): void { | ||
| 19 | + hilog.debug(APP_DOMAIN, tag, msg, ...args) | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + static info(tag: string, msg: string, ...args: Object[]): void { | ||
| 23 | + hilog.info(APP_DOMAIN, tag, msg, ...args) | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + static warn(tag: string, msg: string, ...args: Object[]): void { | ||
| 27 | + hilog.warn(APP_DOMAIN, tag, msg, ...args) | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + static error(tag: string, msg: string, ...args: Object[]): void { | ||
| 31 | + hilog.error(APP_DOMAIN, tag, msg, ...args) | ||
| 32 | + } | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +export { Logger, APP_TAG } | ||
| @@ -0,0 +1,48 @@ | |||
| 1 | +/** | ||
| 2 | + * 食物分类横向列表组件 — 显示食物大类标签,支持选中切换 | ||
| 3 | + */ | ||
| 4 | + | ||
| 5 | +import {FoodCategory} from '../model/FoodType' | ||
| 6 | + | ||
| 7 | +// 分类列表项 | ||
| 8 | +export interface FoodTypeList { | ||
| 9 | + name: string | ||
| 10 | + targetIndex: number | ||
| 11 | + value: FoodCategory | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +@Component | ||
| 15 | +export struct FoodTypeListComponent { | ||
| 16 | + @Prop param: FoodTypeList[] = []; | ||
| 17 | + @Prop currentIndex: number = 0; | ||
| 18 | + onItemClick: (item: FoodTypeList) => void = () => {}; | ||
| 19 | + | ||
| 20 | + build() { | ||
| 21 | + List({ space: 5 }) { | ||
| 22 | + ForEach(this.param, (item: FoodTypeList) => { | ||
| 23 | + ListItem() { | ||
| 24 | + Text(item.name) | ||
| 25 | + .width(60) | ||
| 26 | + .height('100%') | ||
| 27 | + .textAlign(TextAlign.Center) | ||
| 28 | + .borderRadius(10) | ||
| 29 | + .borderWidth(1) | ||
| 30 | + .borderColor($r('app.color.primary_color')) | ||
| 31 | + .backgroundColor(this.currentIndex === item.targetIndex | ||
| 32 | + ? $r('app.color.primary_color') | ||
| 33 | + : $r('app.color.white')) | ||
| 34 | + .fontColor(this.currentIndex === item.targetIndex | ||
| 35 | + ? $r('app.color.white') | ||
| 36 | + : $r('app.color.primary_color')) | ||
| 37 | + } | ||
| 38 | + .onClick(() => { | ||
| 39 | + this.onItemClick(item) | ||
| 40 | + }) | ||
| 41 | + }) | ||
| 42 | + } | ||
| 43 | + .height(50) | ||
| 44 | + .width('100%') | ||
| 45 | + .listDirection(Axis.Horizontal) | ||
| 46 | + .scrollBar(BarState.Off) | ||
| 47 | + } | ||
| 48 | +} | ||
| @@ -0,0 +1,58 @@ | |||
| 1 | +/** | ||
| 2 | + * 餐次选择器组件 — 早/午/晚/加餐四选一 | ||
| 3 | + */ | ||
| 4 | + | ||
| 5 | +import {MealType, MealTypeName} from '../model/FoodType' | ||
| 6 | + | ||
| 7 | +// 餐次列表项 | ||
| 8 | +interface MealList { | ||
| 9 | + title: string; | ||
| 10 | + targetIndex: number; | ||
| 11 | + value: MealType; | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +@Component | ||
| 15 | +export struct MealTypeSelector { | ||
| 16 | + @Prop currentIndex: number = 0; | ||
| 17 | + onMealTypeChange: (index: number) => void = () => {}; | ||
| 18 | + | ||
| 19 | + private mealList: MealList[] = [ | ||
| 20 | + { title: MealTypeName.BREAKFAST, targetIndex: 0, value: MealType.BREAKFAST }, | ||
| 21 | + { title: MealTypeName.LUNCH, targetIndex: 1, value: MealType.LUNCH }, | ||
| 22 | + { title: MealTypeName.DINNER, targetIndex: 2, value: MealType.DINNER }, | ||
| 23 | + { title:MealTypeName.SNACK, targetIndex: 3, value: MealType.SNACK }, | ||
| 24 | + ]; | ||
| 25 | + | ||
| 26 | + build() { | ||
| 27 | + Column({ space: 10 }) { | ||
| 28 | + Text('餐次选择') | ||
| 29 | + .width('90%') | ||
| 30 | + .fontSize(20) | ||
| 31 | + .fontWeight(FontWeight.Bold); | ||
| 32 | + | ||
| 33 | + Row() { | ||
| 34 | + ForEach(this.mealList, (item: MealList) => { | ||
| 35 | + Column() { | ||
| 36 | + Text(item.title) | ||
| 37 | + .width(60) | ||
| 38 | + .height(50) | ||
| 39 | + .textAlign(TextAlign.Center) | ||
| 40 | + .borderRadius(10) | ||
| 41 | + .borderWidth(1) | ||
| 42 | + .borderColor($r('app.color.primary_color')) | ||
| 43 | + .backgroundColor(this.currentIndex === item.targetIndex | ||
| 44 | + ? $r('app.color.primary_color') | ||
| 45 | + : $r('app.color.white')) | ||
| 46 | + .fontColor(this.currentIndex === item.targetIndex | ||
| 47 | + ? $r('app.color.white') | ||
| 48 | + : $r('app.color.primary_color')) | ||
| 49 | + } | ||
| 50 | + .onClick(() => this.onMealTypeChange(item.targetIndex)); | ||
| 51 | + }, (item: MealList) => item.title); | ||
| 52 | + } | ||
| 53 | + .width('90%') | ||
| 54 | + .justifyContent(FlexAlign.SpaceAround); | ||
| 55 | + } | ||
| 56 | + .width('90%') | ||
| 57 | + } | ||
| 58 | +} | ||
| @@ -0,0 +1,38 @@ | |||
| 1 | +/** | ||
| 2 | + * 营养素卡片组件 — 显示单个营养指标(图标+数值+标签+单位) | ||
| 3 | + * | ||
| 4 | + * 颜色属性为 string 类型(非 ResourceRef),用于 Gradient 等字符串场景 | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | +import { AppColorMap } from '../common/ColorUtils' | ||
| 8 | + | ||
| 9 | +@Component | ||
| 10 | +export struct NutrientCard { | ||
| 11 | + @Prop icon: string = ''; | ||
| 12 | + @Prop label: string = ''; | ||
| 13 | + @Prop value: string = ''; | ||
| 14 | + @Prop unit: string = ''; | ||
| 15 | + @Prop color: string = AppColorMap.FONT_PRIMARY; | ||
| 16 | + @Prop bgColor: string = AppColorMap.BACKGROUND_CARD; | ||
| 17 | + | ||
| 18 | + build() { | ||
| 19 | + Column({ space: 6 }) { | ||
| 20 | + if (this.icon !== '') { | ||
| 21 | + Text(this.icon).fontSize(24) | ||
| 22 | + } | ||
| 23 | + Text(this.value) | ||
| 24 | + .fontSize(20).fontWeight(FontWeight.Bold) | ||
| 25 | + .fontColor(this.color) | ||
| 26 | + Row({ space: 2 }) { | ||
| 27 | + Text(this.label).fontSize(12).fontColor($r('app.color.font_mid')) | ||
| 28 | + Text(this.unit).fontSize(11).fontColor($r('app.color.font_hint')) | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | + .width('100%').height(100) | ||
| 32 | + .justifyContent(FlexAlign.Center) | ||
| 33 | + .backgroundColor(this.bgColor) | ||
| 34 | + .borderRadius(12) | ||
| 35 | + .padding(12) | ||
| 36 | + .margin({ bottom: 12 }) | ||
| 37 | + } | ||
| 38 | +} | ||
| @@ -0,0 +1,68 @@ | |||
| 1 | +/** | ||
| 2 | + * 营养汇总卡片组件 — 显示单餐三大营养素与总热量 | ||
| 3 | + */ | ||
| 4 | + | ||
| 5 | +import {CardStyleModifier} from '../style/CardStyle' | ||
| 6 | + | ||
| 7 | +// 营养素展示项 | ||
| 8 | +interface NutritionItem { | ||
| 9 | + label: string; | ||
| 10 | + unit: string; | ||
| 11 | + value: number; | ||
| 12 | + decimals: number; | ||
| 13 | + isHighlight?: boolean; | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +@Component | ||
| 17 | +export struct NutritionSummary { | ||
| 18 | + @Prop totalCalories: number = 0; | ||
| 19 | + @Prop totalProtein: number = 0; | ||
| 20 | + @Prop totalFat: number = 0; | ||
| 21 | + @Prop totalCarbs: number = 0; | ||
| 22 | + | ||
| 23 | + private cardModifier: CardStyleModifier = new CardStyleModifier(); | ||
| 24 | + | ||
| 25 | + private getNutritionItems(): NutritionItem[] { | ||
| 26 | + return [ | ||
| 27 | + { label: '总热量', unit: 'kcal', value: this.totalCalories, decimals: 0, isHighlight: true }, | ||
| 28 | + { label: '蛋白质', unit: 'g', value: this.totalProtein, decimals: 1 }, | ||
| 29 | + { label: '脂肪', unit: 'g', value: this.totalFat, decimals: 1 }, | ||
| 30 | + { label: '碳水化合物', unit: 'g', value: this.totalCarbs, decimals: 1 } | ||
| 31 | + ]; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + @Builder | ||
| 35 | + nutritionRow(item: NutritionItem) { | ||
| 36 | + Row() { | ||
| 37 | + Text(item.label) | ||
| 38 | + .fontSize(16) | ||
| 39 | + .fontColor($r('app.color.black')) | ||
| 40 | + Blank() | ||
| 41 | + Text(`${item.value.toFixed(item.decimals)} ${item.unit}`) | ||
| 42 | + .fontSize(16) | ||
| 43 | + .fontColor(item.isHighlight ? $r('app.color.primary_color') : $r('app.color.black')) | ||
| 44 | + .fontWeight(item.isHighlight ? FontWeight.Medium : FontWeight.Normal) | ||
| 45 | + } | ||
| 46 | + .width('100%') | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + build() { | ||
| 50 | + Column({ space: 10 }) { | ||
| 51 | + Text('本餐小计') | ||
| 52 | + .width('90%') | ||
| 53 | + .fontSize(20) | ||
| 54 | + .fontWeight(FontWeight.Bold) | ||
| 55 | + | ||
| 56 | + Column({ space: 12 }) { | ||
| 57 | + ForEach(this.getNutritionItems(), (item: NutritionItem) => { | ||
| 58 | + this.nutritionRow(item) | ||
| 59 | + }) | ||
| 60 | + } | ||
| 61 | + .width('90%') | ||
| 62 | + .padding(15) | ||
| 63 | + .borderRadius(10) | ||
| 64 | + .backgroundColor($r('app.color.white')) | ||
| 65 | + } | ||
| 66 | + .attributeModifier(this.cardModifier) | ||
| 67 | + } | ||
| 68 | +} | ||
| @@ -0,0 +1,15 @@ | |||
| 1 | +/** | ||
| 2 | + * 页面标题构建器 — 居中显示页面标题文字 | ||
| 3 | + */ | ||
| 4 | + | ||
| 5 | +// 标题参数 | ||
| 6 | +interface PageTitle { | ||
| 7 | + title: string | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +@Builder | ||
| 11 | +export function PageTitleBuilder(param: PageTitle) { | ||
| 12 | + Row() { | ||
| 13 | + Text(param.title).fontSize(24).fontWeight(FontWeight.Bold) | ||
| 14 | + }.width('100%').height(40).justifyContent(FlexAlign.SpaceAround) | ||
| 15 | +} | ||
| @@ -0,0 +1,697 @@ | |||
| 1 | +/** | ||
| 2 | + * 食物初始数据 — 首次启动时导入 RDB 的预设食物列表 | ||
| 3 | + * | ||
| 4 | + * 数据来源:中国食物成分表(标准值,每 100g 含量) | ||
| 5 | + * 分类:谷物 / 蔬菜 / 肉蛋 / 水果 / 饮品 / 其他 | ||
| 6 | + */ | ||
| 7 | + | ||
| 8 | +import { FoodCategory, FoodItem } from '../model/FoodType' | ||
| 9 | + | ||
| 10 | +// ========== 谷物类 ========== | ||
| 11 | +const GRAIN_INIT_DATA: FoodItem[] = [ | ||
| 12 | + { | ||
| 13 | + name: '白米饭', | ||
| 14 | + category: FoodCategory.GRAIN, | ||
| 15 | + caloriesPer100g: 116, | ||
| 16 | + proteinPer100g: 2.6, | ||
| 17 | + carbsPer100g: 25.6, | ||
| 18 | + fatPer100g: 0.3, | ||
| 19 | + fiberPer100g: 0.3, | ||
| 20 | + icon: 'white_rice.svg', | ||
| 21 | + isCustom: false | ||
| 22 | + }, | ||
| 23 | + { | ||
| 24 | + name: '馒头', | ||
| 25 | + category: FoodCategory.GRAIN, | ||
| 26 | + caloriesPer100g: 221, | ||
| 27 | + proteinPer100g: 7.0, | ||
| 28 | + carbsPer100g: 47.0, | ||
| 29 | + fatPer100g: 1.1, | ||
| 30 | + fiberPer100g: 1.3, | ||
| 31 | + icon: 'steamed_bun.svg', | ||
| 32 | + isCustom: false | ||
| 33 | + }, | ||
| 34 | + { | ||
| 35 | + name: '面条', | ||
| 36 | + category: FoodCategory.GRAIN, | ||
| 37 | + caloriesPer100g: 110, | ||
| 38 | + proteinPer100g: 3.0, | ||
| 39 | + carbsPer100g: 24.3, | ||
| 40 | + fatPer100g: 0.2, | ||
| 41 | + fiberPer100g: 0.7, | ||
| 42 | + icon: 'noodle.svg', | ||
| 43 | + isCustom: false | ||
| 44 | + }, | ||
| 45 | + { | ||
| 46 | + name: '全麦面包', | ||
| 47 | + category: FoodCategory.GRAIN, | ||
| 48 | + caloriesPer100g: 156, | ||
| 49 | + proteinPer100g: 6.3, | ||
| 50 | + carbsPer100g: 28.0, | ||
| 51 | + fatPer100g: 2.0, | ||
| 52 | + fiberPer100g: 4.0, | ||
| 53 | + icon: 'whole_wheat_bread.svg', | ||
| 54 | + isCustom: false | ||
| 55 | + }, | ||
| 56 | + { | ||
| 57 | + name: '燕麦片', | ||
| 58 | + category: FoodCategory.GRAIN, | ||
| 59 | + caloriesPer100g: 367, | ||
| 60 | + proteinPer100g: 15.0, | ||
| 61 | + carbsPer100g: 66.9, | ||
| 62 | + fatPer100g: 8.5, | ||
| 63 | + fiberPer100g: 10.6, | ||
| 64 | + icon: 'oatmeal.svg', | ||
| 65 | + isCustom: false | ||
| 66 | + }, | ||
| 67 | + { | ||
| 68 | + name: '红薯', | ||
| 69 | + category: FoodCategory.GRAIN, | ||
| 70 | + caloriesPer100g: 86, | ||
| 71 | + proteinPer100g: 1.6, | ||
| 72 | + carbsPer100g: 20.1, | ||
| 73 | + fatPer100g: 0.1, | ||
| 74 | + fiberPer100g: 3.0, | ||
| 75 | + icon: 'sweet_potato.svg', | ||
| 76 | + isCustom: false | ||
| 77 | + }, | ||
| 78 | + { | ||
| 79 | + name: '鲜玉米', | ||
| 80 | + category: FoodCategory.GRAIN, | ||
| 81 | + caloriesPer100g: 112, | ||
| 82 | + proteinPer100g: 4.0, | ||
| 83 | + carbsPer100g: 22.8, | ||
| 84 | + fatPer100g: 1.2, | ||
| 85 | + fiberPer100g: 2.4, | ||
| 86 | + icon: 'fresh_corn.svg', | ||
| 87 | + isCustom: false | ||
| 88 | + }, | ||
| 89 | + { | ||
| 90 | + name: '小米粥', | ||
| 91 | + category: FoodCategory.GRAIN, | ||
| 92 | + caloriesPer100g: 46, | ||
| 93 | + proteinPer100g: 1.4, | ||
| 94 | + carbsPer100g: 9.9, | ||
| 95 | + fatPer100g: 0.2, | ||
| 96 | + fiberPer100g: 0.4, | ||
| 97 | + icon: 'millet_porridge.svg', | ||
| 98 | + isCustom: false | ||
| 99 | + }, | ||
| 100 | + { | ||
| 101 | + name: '油条', | ||
| 102 | + category: FoodCategory.GRAIN, | ||
| 103 | + caloriesPer100g: 386, | ||
| 104 | + proteinPer100g: 7.0, | ||
| 105 | + carbsPer100g: 51.0, | ||
| 106 | + fatPer100g: 18.0, | ||
| 107 | + fiberPer100g: 0.9, | ||
| 108 | + icon: 'fried_dough_stick.svg', | ||
| 109 | + isCustom: false | ||
| 110 | + }, | ||
| 111 | + { | ||
| 112 | + name: '炒饭', | ||
| 113 | + category: FoodCategory.GRAIN, | ||
| 114 | + caloriesPer100g: 174, | ||
| 115 | + proteinPer100g: 3.5, | ||
| 116 | + carbsPer100g: 29.0, | ||
| 117 | + fatPer100g: 4.5, | ||
| 118 | + fiberPer100g: 0.3, | ||
| 119 | + icon: 'fried_rice.svg', | ||
| 120 | + isCustom: false | ||
| 121 | + }, | ||
| 122 | + { | ||
| 123 | + name: '白粥', | ||
| 124 | + category: FoodCategory.GRAIN, | ||
| 125 | + caloriesPer100g: 46, | ||
| 126 | + proteinPer100g: 1.1, | ||
| 127 | + carbsPer100g: 9.5, | ||
| 128 | + fatPer100g: 0.1, | ||
| 129 | + fiberPer100g: 0.2, | ||
| 130 | + icon: 'rice_porridge.svg', | ||
| 131 | + isCustom: false | ||
| 132 | + }, | ||
| 133 | + { | ||
| 134 | + name: '饺子皮', | ||
| 135 | + category: FoodCategory.GRAIN, | ||
| 136 | + caloriesPer100g: 277, | ||
| 137 | + proteinPer100g: 8.0, | ||
| 138 | + carbsPer100g: 58.0, | ||
| 139 | + fatPer100g: 1.0, | ||
| 140 | + fiberPer100g: 1.5, | ||
| 141 | + icon: 'dumpling_wrapper.svg', | ||
| 142 | + isCustom: false | ||
| 143 | + } | ||
| 144 | +] | ||
| 145 | + | ||
| 146 | +// ========== 蔬菜类 ========== | ||
| 147 | +const VEGETABLE_INIT_DATA: FoodItem[] = [ | ||
| 148 | + { | ||
| 149 | + name: '炒青菜', | ||
| 150 | + category: FoodCategory.VEGETABLE, | ||
| 151 | + caloriesPer100g: 45, | ||
| 152 | + proteinPer100g: 2.0, | ||
| 153 | + carbsPer100g: 3.5, | ||
| 154 | + fatPer100g: 2.5, | ||
| 155 | + fiberPer100g: 1.8, | ||
| 156 | + icon: 'stirFriedGreenVegetables.svg', | ||
| 157 | + isCustom: false | ||
| 158 | + }, | ||
| 159 | + { | ||
| 160 | + name: '番茄', | ||
| 161 | + category: FoodCategory.VEGETABLE, | ||
| 162 | + caloriesPer100g: 15, | ||
| 163 | + proteinPer100g: 0.9, | ||
| 164 | + carbsPer100g: 2.5, | ||
| 165 | + fatPer100g: 0.2, | ||
| 166 | + fiberPer100g: 0.5, | ||
| 167 | + icon: 'tomato.svg', | ||
| 168 | + isCustom: false | ||
| 169 | + }, | ||
| 170 | + { | ||
| 171 | + name: '黄瓜', | ||
| 172 | + category: FoodCategory.VEGETABLE, | ||
| 173 | + caloriesPer100g: 15, | ||
| 174 | + proteinPer100g: 0.8, | ||
| 175 | + carbsPer100g: 2.6, | ||
| 176 | + fatPer100g: 0.1, | ||
| 177 | + fiberPer100g: 0.5, | ||
| 178 | + icon: 'cucumber.svg', | ||
| 179 | + isCustom: false | ||
| 180 | + }, | ||
| 181 | + { | ||
| 182 | + name: '西兰花', | ||
| 183 | + category: FoodCategory.VEGETABLE, | ||
| 184 | + caloriesPer100g: 36, | ||
| 185 | + proteinPer100g: 3.2, | ||
| 186 | + carbsPer100g: 4.5, | ||
| 187 | + fatPer100g: 0.5, | ||
| 188 | + fiberPer100g: 1.6, | ||
| 189 | + icon: 'broccoli.svg', | ||
| 190 | + isCustom: false | ||
| 191 | + }, | ||
| 192 | + { | ||
| 193 | + name: '胡萝卜', | ||
| 194 | + category: FoodCategory.VEGETABLE, | ||
| 195 | + caloriesPer100g: 37, | ||
| 196 | + proteinPer100g: 1.0, | ||
| 197 | + carbsPer100g: 7.2, | ||
| 198 | + fatPer100g: 0.3, | ||
| 199 | + fiberPer100g: 1.0, | ||
| 200 | + icon: 'carrot.svg', | ||
| 201 | + isCustom: false | ||
| 202 | + }, | ||
| 203 | + { | ||
| 204 | + name: '土豆', | ||
| 205 | + category: FoodCategory.VEGETABLE, | ||
| 206 | + caloriesPer100g: 76, | ||
| 207 | + proteinPer100g: 2.0, | ||
| 208 | + carbsPer100g: 16.0, | ||
| 209 | + fatPer100g: 0.3, | ||
| 210 | + fiberPer100g: 0.8, | ||
| 211 | + icon: 'potato.svg', | ||
| 212 | + isCustom: false | ||
| 213 | + }, | ||
| 214 | + { | ||
| 215 | + name: '炒豆角', | ||
| 216 | + category: FoodCategory.VEGETABLE, | ||
| 217 | + caloriesPer100g: 63, | ||
| 218 | + proteinPer100g: 2.5, | ||
| 219 | + carbsPer100g: 5.8, | ||
| 220 | + fatPer100g: 3.0, | ||
| 221 | + fiberPer100g: 2.0, | ||
| 222 | + icon: 'stirFriedBeans.svg', | ||
| 223 | + isCustom: false | ||
| 224 | + }, | ||
| 225 | + { | ||
| 226 | + name: '凉拌黄瓜', | ||
| 227 | + category: FoodCategory.VEGETABLE, | ||
| 228 | + caloriesPer100g: 24, | ||
| 229 | + proteinPer100g: 0.8, | ||
| 230 | + carbsPer100g: 3.0, | ||
| 231 | + fatPer100g: 1.0, | ||
| 232 | + fiberPer100g: 0.5, | ||
| 233 | + icon: 'coldMixedCucumber.svg', | ||
| 234 | + isCustom: false | ||
| 235 | + }, | ||
| 236 | + { | ||
| 237 | + name: '白菜', | ||
| 238 | + category: FoodCategory.VEGETABLE, | ||
| 239 | + caloriesPer100g: 18, | ||
| 240 | + proteinPer100g: 1.2, | ||
| 241 | + carbsPer100g: 2.8, | ||
| 242 | + fatPer100g: 0.2, | ||
| 243 | + fiberPer100g: 0.8, | ||
| 244 | + icon: 'chineseCabbage.svg', | ||
| 245 | + isCustom: false | ||
| 246 | + }, | ||
| 247 | + { | ||
| 248 | + name: '菠菜', | ||
| 249 | + category: FoodCategory.VEGETABLE, | ||
| 250 | + caloriesPer100g: 24, | ||
| 251 | + proteinPer100g: 2.6, | ||
| 252 | + carbsPer100g: 2.8, | ||
| 253 | + fatPer100g: 0.3, | ||
| 254 | + fiberPer100g: 1.7, | ||
| 255 | + icon: 'spinach.svg', | ||
| 256 | + isCustom: false | ||
| 257 | + } | ||
| 258 | +]; | ||
| 259 | +// ========== 肉蛋类 ========== | ||
| 260 | +const MEAT_EGG_INIT_DATA: FoodItem[] = [ | ||
| 261 | + { | ||
| 262 | + name: '鸡胸肉', | ||
| 263 | + category: FoodCategory.MEAT_EGG, | ||
| 264 | + caloriesPer100g: 133, | ||
| 265 | + proteinPer100g: 26.0, | ||
| 266 | + carbsPer100g: 0.1, | ||
| 267 | + fatPer100g: 3.2, | ||
| 268 | + fiberPer100g: 0, | ||
| 269 | + icon: 'chicken_breast.svg', | ||
| 270 | + isCustom: false, | ||
| 271 | + }, | ||
| 272 | + { | ||
| 273 | + name: '红烧肉', | ||
| 274 | + category: FoodCategory.MEAT_EGG, | ||
| 275 | + caloriesPer100g: 235, | ||
| 276 | + proteinPer100g: 10.0, | ||
| 277 | + carbsPer100g: 4.9, | ||
| 278 | + fatPer100g: 19.5, | ||
| 279 | + fiberPer100g: 0, | ||
| 280 | + icon: 'braised_pork.svg', | ||
| 281 | + isCustom: false, | ||
| 282 | + }, | ||
| 283 | + { | ||
| 284 | + name: '牛腩', | ||
| 285 | + category: FoodCategory.MEAT_EGG, | ||
| 286 | + caloriesPer100g: 332, | ||
| 287 | + proteinPer100g: 14.0, | ||
| 288 | + carbsPer100g: 5.2, | ||
| 289 | + fatPer100g: 28.4, | ||
| 290 | + fiberPer100g: 0, | ||
| 291 | + icon: 'beef_brisket.svg', | ||
| 292 | + isCustom: false, | ||
| 293 | + }, | ||
| 294 | + { | ||
| 295 | + name: '水煮蛋', | ||
| 296 | + category: FoodCategory.MEAT_EGG, | ||
| 297 | + caloriesPer100g: 144, | ||
| 298 | + proteinPer100g: 13.0, | ||
| 299 | + carbsPer100g: 1.7, | ||
| 300 | + fatPer100g: 9.5, | ||
| 301 | + fiberPer100g: 0, | ||
| 302 | + icon: 'boiled_egg.svg', | ||
| 303 | + isCustom: false, | ||
| 304 | + }, | ||
| 305 | + { | ||
| 306 | + name: '煎蛋', | ||
| 307 | + category: FoodCategory.MEAT_EGG, | ||
| 308 | + caloriesPer100g: 199, | ||
| 309 | + proteinPer100g: 13.0, | ||
| 310 | + carbsPer100g: 1.0, | ||
| 311 | + fatPer100g: 15.9, | ||
| 312 | + fiberPer100g: 0, | ||
| 313 | + icon: 'fried_egg.svg', | ||
| 314 | + isCustom: false, | ||
| 315 | + }, | ||
| 316 | + { | ||
| 317 | + name: '清蒸鱼', | ||
| 318 | + category: FoodCategory.MEAT_EGG, | ||
| 319 | + caloriesPer100g: 109, | ||
| 320 | + proteinPer100g: 21.0, | ||
| 321 | + carbsPer100g: 0.2, | ||
| 322 | + fatPer100g: 2.7, | ||
| 323 | + fiberPer100g: 0, | ||
| 324 | + icon: 'steamed_fish.svg', | ||
| 325 | + isCustom: false, | ||
| 326 | + }, | ||
| 327 | + { | ||
| 328 | + name: '虾仁', | ||
| 329 | + category: FoodCategory.MEAT_EGG, | ||
| 330 | + caloriesPer100g: 87, | ||
| 331 | + proteinPer100g: 18.0, | ||
| 332 | + carbsPer100g: 1.5, | ||
| 333 | + fatPer100g: 1.0, | ||
| 334 | + fiberPer100g: 0, | ||
| 335 | + icon: 'shrimp.svg', | ||
| 336 | + isCustom: false, | ||
| 337 | + }, | ||
| 338 | + { | ||
| 339 | + name: '猪排骨', | ||
| 340 | + category: FoodCategory.MEAT_EGG, | ||
| 341 | + caloriesPer100g: 279, | ||
| 342 | + proteinPer100g: 16.0, | ||
| 343 | + carbsPer100g: 2.9, | ||
| 344 | + fatPer100g: 22.6, | ||
| 345 | + fiberPer100g: 0, | ||
| 346 | + icon: 'pork_ribs.svg', | ||
| 347 | + isCustom: false, | ||
| 348 | + }, | ||
| 349 | + { | ||
| 350 | + name: '烤鸡腿', | ||
| 351 | + category: FoodCategory.MEAT_EGG, | ||
| 352 | + caloriesPer100g: 181, | ||
| 353 | + proteinPer100g: 20.3, | ||
| 354 | + carbsPer100g: 2.1, | ||
| 355 | + fatPer100g: 10.2, | ||
| 356 | + fiberPer100g: 0, | ||
| 357 | + icon: 'roasted_chicken_leg.svg', | ||
| 358 | + isCustom: false, | ||
| 359 | + }, | ||
| 360 | + { | ||
| 361 | + name: '豆腐', | ||
| 362 | + category: FoodCategory.MEAT_EGG, | ||
| 363 | + caloriesPer100g: 81, | ||
| 364 | + proteinPer100g: 8.0, | ||
| 365 | + carbsPer100g: 3.25, | ||
| 366 | + fatPer100g: 4.0, | ||
| 367 | + fiberPer100g: 0.2, | ||
| 368 | + icon: 'tofu.svg', | ||
| 369 | + isCustom: false, | ||
| 370 | + }, | ||
| 371 | + { | ||
| 372 | + name: '鸡翅', | ||
| 373 | + category: FoodCategory.MEAT_EGG, | ||
| 374 | + caloriesPer100g: 194, | ||
| 375 | + proteinPer100g: 17.4, | ||
| 376 | + carbsPer100g: 4.6, | ||
| 377 | + fatPer100g: 11.8, | ||
| 378 | + fiberPer100g: 0, | ||
| 379 | + icon: 'chicken_wings.svg', | ||
| 380 | + isCustom: false, | ||
| 381 | + }, | ||
| 382 | + { | ||
| 383 | + name: '菲力牛排', | ||
| 384 | + category: FoodCategory.MEAT_EGG, | ||
| 385 | + caloriesPer100g: 107, | ||
| 386 | + proteinPer100g: 22.2, | ||
| 387 | + carbsPer100g: 2.4, | ||
| 388 | + fatPer100g: 0.9, | ||
| 389 | + fiberPer100g: 0, | ||
| 390 | + icon: 'filet_mignon.svg', | ||
| 391 | + isCustom: false, | ||
| 392 | + | ||
| 393 | + }, | ||
| 394 | + | ||
| 395 | +] | ||
| 396 | +// ========== 水果类 ========== | ||
| 397 | +const FRUIT_INIT_DATA: FoodItem[] = [ | ||
| 398 | + { | ||
| 399 | + name: '苹果', | ||
| 400 | + category: FoodCategory.FRUIT, | ||
| 401 | + caloriesPer100g: 52, | ||
| 402 | + proteinPer100g: 0.2, | ||
| 403 | + carbsPer100g: 12.3, | ||
| 404 | + fatPer100g: 0.2, | ||
| 405 | + fiberPer100g: 1.2, | ||
| 406 | + icon: 'apple.svg', | ||
| 407 | + isCustom: false | ||
| 408 | + }, | ||
| 409 | + { | ||
| 410 | + name: '香蕉', | ||
| 411 | + category: FoodCategory.FRUIT, | ||
| 412 | + caloriesPer100g: 93, | ||
| 413 | + proteinPer100g: 1.3, | ||
| 414 | + carbsPer100g: 21.4, | ||
| 415 | + fatPer100g: 0.2, | ||
| 416 | + fiberPer100g: 1.2, | ||
| 417 | + icon: 'banana.svg', | ||
| 418 | + isCustom: false | ||
| 419 | + }, | ||
| 420 | + { | ||
| 421 | + name: '橙子', | ||
| 422 | + category: FoodCategory.FRUIT, | ||
| 423 | + caloriesPer100g: 48, | ||
| 424 | + proteinPer100g: 0.8, | ||
| 425 | + carbsPer100g: 10.8, | ||
| 426 | + fatPer100g: 0.2, | ||
| 427 | + fiberPer100g: 0.4, | ||
| 428 | + icon: 'orange.svg', | ||
| 429 | + isCustom: false | ||
| 430 | + }, | ||
| 431 | + { | ||
| 432 | + name: '葡萄', | ||
| 433 | + category: FoodCategory.FRUIT, | ||
| 434 | + caloriesPer100g: 45, | ||
| 435 | + proteinPer100g: 0.5, | ||
| 436 | + carbsPer100g: 10.3, | ||
| 437 | + fatPer100g: 0.2, | ||
| 438 | + fiberPer100g: 0.4, | ||
| 439 | + icon: 'grape.svg', | ||
| 440 | + isCustom: false | ||
| 441 | + }, | ||
| 442 | + { | ||
| 443 | + name: '西瓜', | ||
| 444 | + category: FoodCategory.FRUIT, | ||
| 445 | + caloriesPer100g: 31, | ||
| 446 | + proteinPer100g: 0.6, | ||
| 447 | + carbsPer100g: 7.0, | ||
| 448 | + fatPer100g: 0.1, | ||
| 449 | + fiberPer100g: 0.2, | ||
| 450 | + icon: 'watermelon.svg', | ||
| 451 | + isCustom: false | ||
| 452 | + }, | ||
| 453 | + { | ||
| 454 | + name: '草莓', | ||
| 455 | + category: FoodCategory.FRUIT, | ||
| 456 | + caloriesPer100g: 32, | ||
| 457 | + proteinPer100g: 0.7, | ||
| 458 | + carbsPer100g: 6.7, | ||
| 459 | + fatPer100g: 0.2, | ||
| 460 | + fiberPer100g: 1.3, | ||
| 461 | + icon: 'strawberry.svg', | ||
| 462 | + isCustom: false | ||
| 463 | + }, | ||
| 464 | + { | ||
| 465 | + name: '猕猴桃', | ||
| 466 | + category: FoodCategory.FRUIT, | ||
| 467 | + caloriesPer100g: 56, | ||
| 468 | + proteinPer100g: 0.9, | ||
| 469 | + carbsPer100g: 12.0, | ||
| 470 | + fatPer100g: 0.5, | ||
| 471 | + fiberPer100g: 2.6, | ||
| 472 | + icon: 'kiwi.svg', | ||
| 473 | + isCustom: false | ||
| 474 | + }, | ||
| 475 | + { | ||
| 476 | + name: '梨', | ||
| 477 | + category: FoodCategory.FRUIT, | ||
| 478 | + caloriesPer100g: 44, | ||
| 479 | + proteinPer100g: 0.3, | ||
| 480 | + carbsPer100g: 10.5, | ||
| 481 | + fatPer100g: 0.1, | ||
| 482 | + fiberPer100g: 2.6, | ||
| 483 | + icon: 'pear.svg', | ||
| 484 | + isCustom: false | ||
| 485 | + }, | ||
| 486 | + { | ||
| 487 | + name: '桃', | ||
| 488 | + category: FoodCategory.FRUIT, | ||
| 489 | + caloriesPer100g: 39, | ||
| 490 | + proteinPer100g: 0.8, | ||
| 491 | + carbsPer100g: 8.7, | ||
| 492 | + fatPer100g: 0.1, | ||
| 493 | + fiberPer100g: 1.3, | ||
| 494 | + icon: 'peach.svg', | ||
| 495 | + isCustom: false | ||
| 496 | + }, | ||
| 497 | + { | ||
| 498 | + name: '芒果', | ||
| 499 | + category: FoodCategory.FRUIT, | ||
| 500 | + caloriesPer100g: 35, | ||
| 501 | + proteinPer100g: 0.6, | ||
| 502 | + carbsPer100g: 7.7, | ||
| 503 | + fatPer100g: 0.2, | ||
| 504 | + fiberPer100g: 1.3, | ||
| 505 | + icon: 'mango.svg', | ||
| 506 | + isCustom: false | ||
| 507 | + } | ||
| 508 | +]; | ||
| 509 | +// ========== 饮品类 ========== | ||
| 510 | +const DRINK_INIT_DATA: FoodItem[] = [ | ||
| 511 | + { | ||
| 512 | + name: '美式咖啡', | ||
| 513 | + category: FoodCategory.DRINK, | ||
| 514 | + caloriesPer100g: 2, | ||
| 515 | + proteinPer100g: 0.1, | ||
| 516 | + carbsPer100g: 0.4, | ||
| 517 | + fatPer100g: 0.0, | ||
| 518 | + fiberPer100g: 0.0, | ||
| 519 | + icon: 'Americano.svg', | ||
| 520 | + isCustom: false | ||
| 521 | + }, | ||
| 522 | + { | ||
| 523 | + name: '拿铁', | ||
| 524 | + category: FoodCategory.DRINK, | ||
| 525 | + caloriesPer100g: 75, | ||
| 526 | + proteinPer100g: 2.8, | ||
| 527 | + carbsPer100g: 4.8, | ||
| 528 | + fatPer100g: 3.5, | ||
| 529 | + fiberPer100g: 0.0, | ||
| 530 | + icon: 'Latte.svg', | ||
| 531 | + isCustom: false | ||
| 532 | + }, | ||
| 533 | + { | ||
| 534 | + name: '可乐', | ||
| 535 | + category: FoodCategory.DRINK, | ||
| 536 | + caloriesPer100g: 43, | ||
| 537 | + proteinPer100g: 0.0, | ||
| 538 | + carbsPer100g: 10.8, | ||
| 539 | + fatPer100g: 0.0, | ||
| 540 | + fiberPer100g: 0.0, | ||
| 541 | + icon: 'Cola.svg', | ||
| 542 | + isCustom: false | ||
| 543 | + }, | ||
| 544 | + { | ||
| 545 | + name: '奶茶', | ||
| 546 | + category: FoodCategory.DRINK, | ||
| 547 | + caloriesPer100g: 120, | ||
| 548 | + proteinPer100g: 0.5, | ||
| 549 | + carbsPer100g: 20.0, | ||
| 550 | + fatPer100g: 4.0, | ||
| 551 | + fiberPer100g: 0.0, | ||
| 552 | + icon: 'MilkTea.svg', | ||
| 553 | + isCustom: false | ||
| 554 | + }, | ||
| 555 | + { | ||
| 556 | + name: '橙汁', | ||
| 557 | + category: FoodCategory.DRINK, | ||
| 558 | + caloriesPer100g: 45, | ||
| 559 | + proteinPer100g: 0.7, | ||
| 560 | + carbsPer100g: 10.0, | ||
| 561 | + fatPer100g: 0.1, | ||
| 562 | + fiberPer100g: 0.2, | ||
| 563 | + icon: 'OrangeJuice.svg', | ||
| 564 | + isCustom: false | ||
| 565 | + }, | ||
| 566 | + { | ||
| 567 | + name: '绿茶', | ||
| 568 | + category: FoodCategory.DRINK, | ||
| 569 | + caloriesPer100g: 1, | ||
| 570 | + proteinPer100g: 0.0, | ||
| 571 | + carbsPer100g: 0.2, | ||
| 572 | + fatPer100g: 0.0, | ||
| 573 | + fiberPer100g: 0.0, | ||
| 574 | + icon: 'GreenTea.svg', | ||
| 575 | + isCustom: false | ||
| 576 | + }, | ||
| 577 | + { | ||
| 578 | + name: '豆浆', | ||
| 579 | + category: FoodCategory.DRINK, | ||
| 580 | + caloriesPer100g: 33, | ||
| 581 | + proteinPer100g: 2.0, | ||
| 582 | + carbsPer100g: 4.5, | ||
| 583 | + fatPer100g: 0.8, | ||
| 584 | + fiberPer100g: 0.0, | ||
| 585 | + icon: 'SoyMilk.svg', | ||
| 586 | + isCustom: false | ||
| 587 | + }, | ||
| 588 | + { | ||
| 589 | + name: '蜂蜜水', | ||
| 590 | + category: FoodCategory.DRINK, | ||
| 591 | + caloriesPer100g: 43, | ||
| 592 | + proteinPer100g: 0.1, | ||
| 593 | + carbsPer100g: 10.5, | ||
| 594 | + fatPer100g: 0.0, | ||
| 595 | + fiberPer100g: 0.0, | ||
| 596 | + icon: 'Honey.svg', | ||
| 597 | + isCustom: false | ||
| 598 | + } | ||
| 599 | +]; | ||
| 600 | +// ========== 其他类 ========== | ||
| 601 | +const OTHER_INIT_DATA: FoodItem[] = [ | ||
| 602 | + { | ||
| 603 | + name: '薯片', | ||
| 604 | + category: FoodCategory.OTHER, | ||
| 605 | + caloriesPer100g: 548, | ||
| 606 | + proteinPer100g: 6.5, | ||
| 607 | + carbsPer100g: 45.0, | ||
| 608 | + fatPer100g: 37.0, | ||
| 609 | + fiberPer100g: 3.0, | ||
| 610 | + icon: 'potatoChips.svg', | ||
| 611 | + isCustom: false | ||
| 612 | + }, | ||
| 613 | + { | ||
| 614 | + name: '巧克力', | ||
| 615 | + category: FoodCategory.OTHER, | ||
| 616 | + caloriesPer100g: 544, | ||
| 617 | + proteinPer100g: 7.5, | ||
| 618 | + carbsPer100g: 55.0, | ||
| 619 | + fatPer100g: 32.0, | ||
| 620 | + fiberPer100g: 2.5, | ||
| 621 | + icon: 'chocolate.svg', | ||
| 622 | + isCustom: false | ||
| 623 | + }, | ||
| 624 | + { | ||
| 625 | + name: '坚果混合', | ||
| 626 | + category: FoodCategory.OTHER, | ||
| 627 | + caloriesPer100g: 607, | ||
| 628 | + proteinPer100g: 20.0, | ||
| 629 | + carbsPer100g: 18.0, | ||
| 630 | + fatPer100g: 52.0, | ||
| 631 | + fiberPer100g: 8.0, | ||
| 632 | + icon: 'mixedNuts.svg', | ||
| 633 | + isCustom: false | ||
| 634 | + }, | ||
| 635 | + { | ||
| 636 | + name: '饼干', | ||
| 637 | + category: FoodCategory.OTHER, | ||
| 638 | + caloriesPer100g: 433, | ||
| 639 | + proteinPer100g: 8.0, | ||
| 640 | + carbsPer100g: 67.0, | ||
| 641 | + fatPer100g: 14.0, | ||
| 642 | + fiberPer100g: 2.0, | ||
| 643 | + icon: 'cookies.svg', | ||
| 644 | + isCustom: false | ||
| 645 | + }, | ||
| 646 | + { | ||
| 647 | + name: '冰淇淋', | ||
| 648 | + category: FoodCategory.OTHER, | ||
| 649 | + caloriesPer100g: 207, | ||
| 650 | + proteinPer100g: 3.5, | ||
| 651 | + carbsPer100g: 25.0, | ||
| 652 | + fatPer100g: 11.0, | ||
| 653 | + fiberPer100g: 0.0, | ||
| 654 | + icon: 'iceCream.svg', | ||
| 655 | + isCustom: false | ||
| 656 | + }, | ||
| 657 | + { | ||
| 658 | + name: '蛋糕', | ||
| 659 | + category: FoodCategory.OTHER, | ||
| 660 | + caloriesPer100g: 348, | ||
| 661 | + proteinPer100g: 6.0, | ||
| 662 | + carbsPer100g: 50.0, | ||
| 663 | + fatPer100g: 14.0, | ||
| 664 | + fiberPer100g: 1.0, | ||
| 665 | + icon: 'cake.svg', | ||
| 666 | + isCustom: false | ||
| 667 | + }, | ||
| 668 | + { | ||
| 669 | + name: '酸奶水果捞', | ||
| 670 | + category: FoodCategory.OTHER, | ||
| 671 | + caloriesPer100g: 85, | ||
| 672 | + proteinPer100g: 2.8, | ||
| 673 | + carbsPer100g: 13.0, | ||
| 674 | + fatPer100g: 2.0, | ||
| 675 | + fiberPer100g: 1.0, | ||
| 676 | + icon: 'yogurtFruitSalad.svg', | ||
| 677 | + isCustom: false | ||
| 678 | + }, | ||
| 679 | + { | ||
| 680 | + name: '三明治', | ||
| 681 | + category: FoodCategory.OTHER, | ||
| 682 | + caloriesPer100g: 256, | ||
| 683 | + proteinPer100g: 10.0, | ||
| 684 | + carbsPer100g: 28.0, | ||
| 685 | + fatPer100g: 10.0, | ||
| 686 | + fiberPer100g: 1.5, | ||
| 687 | + icon: 'sandwich.svg', | ||
| 688 | + isCustom: false | ||
| 689 | + } | ||
| 690 | +]; | ||
| 691 | + | ||
| 692 | +// 聚合全部分类数据,供 FoodRDB.init() 批量导入 | ||
| 693 | +const FOOD_INIT_DATA = | ||
| 694 | + [...GRAIN_INIT_DATA, ...VEGETABLE_INIT_DATA, ...MEAT_EGG_INIT_DATA, ...FRUIT_INIT_DATA, ...DRINK_INIT_DATA, | ||
| 695 | + ...OTHER_INIT_DATA] | ||
| 696 | + | ||
| 697 | +export { FOOD_INIT_DATA } | ||