已开启
提交 NutriLog(智能饮食记录与营养管理)APPDEMO #474
chn9528创建于 28 天前
提交 NutriLog(智能饮食记录与营养管理)APPDEMO #474
已开启
chn9528创建于 28 天前
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+ ![img.png](img.png)
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+| ![首页](docs/home.jpg) | ![食物库](docs/food_library.jpeg) | ![统计](docs/statistics.jpeg) |
51+ 
52+| 记录 | 个人中心 | 添加记录 |
53+|:---:|:---:|:---:|
54+| ![记录](docs/history.jpeg) | ![个人中心](docs/profile.jpeg) | ![添加记录](docs/add_record.jpeg) |
55+ 
56+| 添加自定义食物 |
57+|:------------------------------:|
58+| ![添加自定义食物](docs/add_food.jpeg) |
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+![首页 — 今日营养总览](home.jpg)
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+![食物库 — 浏览/搜索/收藏](food_library.jpeg)
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+![统计 — 周报图表](statistics.jpeg)
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+![记录 — 日历与饮食记录](history.jpeg)
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+![个人中心 — 用户信息与目标](profile.jpeg)
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+![添加记录](add_record.jpeg)
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+![添加自定义食物](add_food.jpeg)
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 }