# HarmonyOS ArkTS 健身饮食计划应用源码深度解析
一、应用背景与技术栈概述
在移动互联网与健康意识高速发展的当下,健身饮食类应用已经成为众多用户日常生活中不可或缺的数字助手。一款优秀的健身饮食管理应用,不仅需要帮助用户精准记录每日摄入的卡路里与各类营养素,还要能够以直观、美观、富有激励性的方式展示数据趋势,让用户在长期坚持中获得正向反馈。本文所要解析的,正是一款基于 HarmonyOS(鸿蒙)操作系统、采用 ArkTS 声明式开发范式构建的"健身饮食计划"应用。

HarmonyOS 是华为推出的面向万物互联时代的分布式操作系统,其应用开发框架经历了从 Java-based 到 ArkTS 声明式 UI 的演进。ArkTS 是在 TypeScript 基础上扩展而来的编程语言,它保留了 TypeScript 的类型系统优势,同时深度融合了鸿蒙的声明式 UI 范式,开发者可以通过 @Component、@Entry、@State、@Builder 等装饰器,以接近自然语言的方式描述界面结构与状态驱动的更新逻辑。这种范式与 Flutter 的 Widget 树、SwiftUI 的 View 协议、Jetpack Compose 的 Composable 函数有异曲同工之妙,但其独特的状态管理机制和与系统底层的深度融合,使其在鸿蒙生态中具备原生级别的性能与体验。
本应用的技术栈可以概括为以下几个层面:
- 编程语言:ArkTS,兼具 TypeScript 的类型安全与鸿蒙的声明式 UI 能力。
- UI 范式:声明式开发范式(Declarative UI),通过链式调用构建组件树,数据变化自动驱动界面刷新。
- 状态管理:采用
@State装饰器管理组件内部可变状态,配合@Observed与@ObjectLink实现对象的深度观测(本应用中使用了@Observed标注数据模型类)。 - 组件化:通过
@Builder装饰器将可复用的 UI 片段封装为构建器函数,实现组件级别的复用与组合。 - 导航架构:基于底部 Tab 栏的多页面切换架构,通过状态变量
currentTab控制四个 Tab 页的渲染切换,无需借助路由框架即可实现简洁的页面管理。 - 弹窗系统:自定义模态弹窗层,通过状态变量控制弹窗的显隐,实现新增、编辑、删除确认等交互场景。
- 数据模型:采用接口(interface)定义数据契约,结合
@Observed类实现可观测的领域对象,体现了面向数据的设计思想。 - 视觉设计:以"橙色"为主色调(
#E65100),搭配金黄色(#FFB300)与浅橙背景(#FFF3E0),整体视觉温暖、活力充沛,契合健身饮食这一应用主题。
从架构层面来看,本应用采用了"单入口组件 + 多 Builder 子页面"的结构。整个应用只有一个 @Entry 入口组件 MealPlannerApp,其内部通过 currentTab 状态变量配合 if-else 条件渲染,切换展示四个功能各异的 Builder 函数(mealsTab、weeklyTab、nutritionTab、mineTab),并在最外层叠加三个模态弹窗(addModal、editModal、deleteModal)。这种设计避免了复杂路由的引入,使得应用结构紧凑、状态集中、逻辑清晰,非常适合中小型应用的快速开发。下面,我们将逐段深入解析每一块代码的实现细节、设计思路与技术要点。
二、数据模型与类型定义
2.1 颜色主题注释与 MealItem 接口
// 健身饮食计划 — 橙色 #E65100 | 黄色 #FFB300 | 背景 #FFF3E0
interface MealItem {
id: number
name: string
mealType: string
calories: number
protein: number
carbs: number
fat: number
fiber: number
date: string
time: string
isCompleted: boolean
emoji: string
recipe: string[]
ingredients: string[]
}

文件开篇以一行注释声明了整个应用的颜色主题体系:主色橙色 #E65100、辅助色黄色 #FFB300、背景色浅橙 #FFF3E0。这三个颜色构成了应用的视觉基调,橙色象征着活力与能量,契合健身运动的主题;黄色作为高亮与强调色,用于关键数据与进度条;浅橙色背景则提供了温暖而不刺眼的底色。这种统一在文件顶部声明颜色主题的做法,虽然在大型工程中通常会提取为资源文件或常量文件,但在单文件应用中,这样的注释起到了"颜色词典"的作用,让开发者一眼就能把握全局配色策略。
接下来是 MealItem 接口的定义。在 ArkTS/TypeScript 中,interface 用于定义数据的形状(shape),它是一种纯类型声明,不包含任何实现逻辑。MealItem 接口描述了一份餐食(Meal)的完整数据结构,包含了 14 个字段:
id: number:餐食的唯一标识符,用于列表渲染时的 key 管理和数据定位。name: string:餐食名称,如"燕麦牛奶杯"。mealType: string:餐食类型,取值为"早餐"“午餐”“晚餐”"加餐"之一,用于分类筛选。calories: number:卡路里含量,单位 kcal,是饮食管理的核心指标。protein / carbs / fat / fiber: number:分别表示蛋白质、碳水化合物、脂肪、膳食纤维的含量,单位克(g),这四项构成了宏量营养素的完整画像。date: string:日期字符串,格式为YYYY-MM-DD,用于按日期组织与查询。time: string:用餐时间字符串,格式为HH:mm,用于时间线展示。isCompleted: boolean:是否已完成的标志位,用于在列表项中显示完成状态标记。emoji: string:食物对应的 Emoji 图标,如🥣、🥪,以轻量化的方式替代真实图片,降低资源开销。recipe: string[]:食谱步骤或用量说明数组,如['燕麦50g', '牛奶200ml', '蓝莓30g']。ingredients: string[]:食材清单数组,记录所需食材名称。
这个接口的设计体现了"领域驱动"的思想——每一个字段都直接对应健身饮食领域的真实概念,字段命名清晰自解释,类型定义严格准确。将 recipe 和 ingredients 设计为数组类型而非拼接字符串,为后续的展开渲染、遍历操作提供了便利。isCompleted 使用布尔类型而非字符串,也体现了类型安全的设计原则。
2.2 DailyCalorie、MacroTarget、WeeklyProtein 接口
interface DailyCalorie {
day: string
calories: number
goal: number
}
interface MacroTarget {
name: string
current: number
target: number
unit: string
color: string
percentage: number
}
interface WeeklyProtein {
day: string
protein: number
}

这三个接口分别服务于应用中不同的数据可视化场景。
DailyCalorie 接口用于描述每日卡路里摄入与目标对比的数据,包含 day(星期几的简称,如"周一")、calories(实际摄入卡路里)和 goal(目标卡路里)。goal 字段的存在使得渲染时可以同时展示实际值与目标值的对比,当 calories > goal 时,界面会以红色(#C62828)标识超量摄入,从而在视觉上对用户形成警示。这种将对比基准(goal)与实际值绑定在同一数据结构中的设计,简化了渲染逻辑中的参数传递,避免了在 UI 层硬编码目标值。
MacroTarget 接口描述宏量营养素(蛋白质、碳水、脂肪、纤维)的目标达成情况。这个接口的字段最为丰富:name 是营养素名称,current 与 target 分别为当前值与目标值,unit 为单位(如"g"),color 为该营养素对应的主题色,percentage 为完成百分比。特别值得注意的是 color 字段——将颜色直接存储在数据结构中,是一种"数据驱动的样式"设计思路。渲染时,进度条的颜色直接取自 item.color,而非在 Builder 中通过 switch-case 判断营养素名称来决定颜色。这种设计使得新增一种营养素时,只需在数据源中添加一条记录并指定颜色即可,无需修改渲染逻辑,体现了开闭原则(Open-Closed Principle)。
WeeklyProtein 接口最为简洁,仅包含 day 和 protein 两个字段,专门用于"每周蛋白质趋势"的条形图渲染。虽然结构简单,但它的存在使得数据职责单一化——不与每日卡路里数据混用同一个结构,保持了各数据集的独立性和可读性。
2.3 TabConfig 与 MealTypeConfig 接口
interface TabConfig {
index: number
label: string
icon: string
}
interface MealTypeConfig {
type: string
mealType: string
icon: string
color: string
targetCalories: number
}

TabConfig 接口定义了底部导航栏每一个 Tab 页的配置信息:index 为该 Tab 在枚举中的序号,label 为显示文字(如"餐食"“周计划”),icon 为 Emoji 图标。将导航配置抽象为接口并以数组形式存储,是声明式 UI 中非常经典的数据驱动渲染模式——底部导航栏的渲染只需遍历 tabConfigs 数组即可,新增 Tab 页只需在数组中添加一条记录,完全无需修改渲染逻辑。
MealTypeConfig 接口描述餐食类型的详细配置,包括类型名称、图标、主题色和目标卡路里。targetCalories 字段为每种餐类设定了建议的卡路里目标(如早餐 500 kcal、午餐 700 kcal),这个数据不仅可以在界面中作为参考展示,也可以作为未来校验用户输入是否合理的依据。这种将业务规则数据化的做法,使得应用逻辑与业务参数解耦,增强了可维护性。
2.4 @Observed 数据模型 MealModel
@Observed
class MealModel {
id: number
name: string
mealType: string
calories: number
protein: number
carbs: number
fat: number
fiber: number
date: string
time: string
isCompleted: boolean
emoji: string
recipe: string[]
ingredients: string[]
constructor(meal: MealItem) {
this.id = meal.id
this.name = meal.name
this.mealType = meal.mealType
this.calories = meal.calories
this.protein = meal.protein
this.carbs = meal.carbs
this.fat = meal.fat
this.fiber = meal.fiber
this.date = meal.date
this.time = meal.time
this.isCompleted = meal.isCompleted
this.emoji = meal.emoji
this.recipe = meal.recipe
this.ingredients = meal.ingredients
}
}

MealModel 是一个被 @Observed 装饰器标注的类。在 ArkTS 的状态管理体系中,@Observed 的作用是使一个类成为"可观测对象"——当该类实例的属性发生变化时,框架能够自动检测到变化并触发依赖该对象的 UI 组件重新渲染。这与 @State 的区别在于:@State 用于管理组件内部的原始值状态(如 number、string、boolean),而 @Observed 适用于管理复杂对象类型,实现的是对象级别的深度观测。
MealModel 类的字段与前面定义的 MealItem 接口完全一致,这并非冗余——MealItem 是纯类型契约(interface),而 MealModel 是具有构造函数的可实例化类。构造函数接收一个 MealItem 类型的参数,并将所有属性逐一赋值。这种"接口定义形状 + 类承载行为与可观测性"的设计模式,是 ArkTS 中管理领域对象的推荐实践:接口保证类型的灵活性与可替换性,类则通过 @Observed 获得响应式能力。
在实际使用中,所有的餐食数据都以 MealModel 实例的形式存储在 meals 数组中。当用户通过编辑弹窗修改某条餐食的属性时,由于 MealModel 被 @Observed 标注,框架能够自动感知属性变化并刷新对应的列表项 UI。不过需要注意的是,在本应用当前版本中,编辑弹窗的保存操作仅关闭弹窗,并未实际修改数据,但数据模型的可观测性设计为未来的功能完善预留了空间。
2.5 MealTab 枚举
enum MealTab {
Meals,
Weekly,
Nutrition,
Mine
}

MealTab 枚举定义了四个 Tab 页的索引值,分别对应"餐食"“周计划”“营养”"我的"四个功能模块。使用枚举(enum)而非魔法数字(magic number)来表示 Tab 索引,是编码规范中的基本要求——MealTab.Meals 的可读性远胜于 0。枚举的成员值从 0 开始自动递增,这与 currentTab 状态变量的初始值 MealTab.Meals(即 0)配合,确保应用启动时默认展示"餐食"页。
在 build() 方法中,Tab 页的切换通过 if (this.currentTab === MealTab.Meals) 等条件判断实现。虽然此处也可以使用 switch-case 结构,但 if-else if-else 链在 ArkTS 的 build() 函数中更为常见,因为声明式 UI 的条件渲染更倾向于 if 表达式。tabConfigs 数组中的 index 字段也引用了 MealTab 枚举值,保证了配置数据与切换逻辑之间的一致性。
三、主组件结构、状态管理与模拟数据
3.1 @Entry 与 @Component 装饰器及状态变量声明
@Entry
@Component
struct MealPlannerApp {
@State currentTab: number = MealTab.Meals
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State editIndex: number = 0
@State deleteIndex: number = 0
@State selectedMealType: string = '全部'
@State dailyCalorieGoal: number = 2000
@State todayCalories: number = 1450
@State nutritionScore: number = 85

@Entry 装饰器标识 MealPlannerApp 为应用的入口组件——即整个 UI 树的根节点。在 HarmonyOS 应用中,有且只有一个被 @Entry 标注的组件会作为页面入口被加载渲染。@Component 装饰器则标识该 struct 为一个自定义组件,使其可以被复用、组合和管理生命周期。
接下来的 @State 装饰器是 ArkTS 状态管理中最核心的装饰器之一。被 @State 修饰的变量成为"状态变量",当其值发生变化时,框架会自动重新调用 build() 函数中依赖该变量的部分,实现 UI 的局部刷新。本应用定义了 9 个状态变量:
currentTab: number:当前激活的 Tab 索引,初始值为MealTab.Meals(0),控制四个 Tab 页的切换。这是整个应用导航的核心状态。showAddModal / showEditModal / showDeleteModal: boolean:三个布尔型状态分别控制"新增"“编辑”"删除确认"三个弹窗的显隐。初始值均为false,表示应用启动时弹窗不可见。editIndex / deleteIndex: number:记录当前正在编辑或删除的餐食在meals数组中的索引,用于弹窗中读取对应餐食的数据。初始值为 0。selectedMealType: string:当前选中的餐类筛选条件,初始值为'全部',表示默认展示所有类型的餐食。dailyCalorieGoal: number:每日卡路里目标,初始值 2000 kcal,这是成人每日推荐摄入量的常用参考值。todayCalories: number:今日已摄入卡路里,初始值 1450 kcal,用于顶部进度条展示。nutritionScore: number:营养评分,初始值 85,在顶部进度区域与营养 Tab 页均会展示。
这些状态变量涵盖了应用的所有交互场景:Tab 切换、弹窗控制、筛选条件、数据展示。将所有状态集中在一个入口组件中管理,是中小型应用常见的做法——虽然随着应用规模增长可能需要引入更复杂的状态管理方案(如全局 AppStorage),但在当前规模下,集中式状态管理保证了数据流向的清晰可追踪。
3.2 餐食模拟数据 meals 数组
private meals: MealModel[] = [
new MealModel({ id: 1, name: '燕麦牛奶杯', mealType: '早餐', calories: 320, protein: 18, carbs: 45, fat: 8, fiber: 6, date: '2026-07-25', time: '07:30', isCompleted: true, emoji: '🥣', recipe: ['燕麦50g', '牛奶200ml', '蓝莓30g'], ingredients: ['燕麦', '牛奶', '蓝莓', '蜂蜜'] }),
new MealModel({ id: 2, name: '全麦三明治', mealType: '早餐', calories: 380, protein: 22, carbs: 48, fat: 12, fiber: 8, date: '2026-07-24', time: '08:00', isCompleted: true, emoji: '🥪', recipe: ['全麦面包', '鸡蛋', '生菜'], ingredients: ['全麦面包', '鸡蛋', '生菜', '番茄'] }),
// ... 共 26 条餐食记录 ...
new MealModel({ id: 26, name: '黑巧克力配咖啡', mealType: '加餐', calories: 160, protein: 3, carbs: 14, fat: 10, fiber: 3, date: '2026-07-19', time: '16:00', isCompleted: true, emoji: '🍫', recipe: ['黑巧克力', '咖啡'], ingredients: ['黑巧克力', '咖啡豆'] })
]
meals 是一个 MealModel[] 类型的私有数组,包含 26 条精心构造的餐食记录。每条记录都通过 new MealModel({...}) 实例化,传入的参数对象符合 MealItem 接口的形状。这些数据覆盖了早餐、午餐、晚餐、加餐四种类型,日期跨度从 2026-07-19 到 2026-07-25(一周),每条数据都包含完整的营养素信息、食谱和食材清单。
值得注意的是数据的真实性与丰富度。例如"鸡胸肉沙拉"含 450 kcal、35g 蛋白质,"三文鱼寿司"含 480 kcal、28g 蛋白质,这些数值与真实食物的营养成分高度吻合,说明开发者在数据构造时进行了合理的估算。每条记录都配有一个 Emoji 图标,如 🥣、🥪、🥗、🍣、🍗、🐟 等,使用 Emoji 作为食物图标是一个聪明的轻量化方案——无需引入图片资源文件,不会增加应用体积,且 Emoji 具有跨平台一致性。
这 26 条数据被标记为 private,意味着它们仅在 MealPlannerApp 组件内部可访问。在当前版本中,这些是硬编码的模拟数据;在实际生产应用中,meals 数组通常会从本地数据库(如关系型数据库或 Preferences)或网络接口异步加载,并配合 @State 或 @StorageLink 实现数据的响应式管理。
3.3 每日卡路里与宏量营养素目标数据
private dailyCalories: DailyCalorie[] = [
{ day: '周一', calories: 1850, goal: 2000 },
{ day: '周二', calories: 1950, goal: 2000 },
{ day: '周三', calories: 1720, goal: 2000 },
{ day: '周四', calories: 2100, goal: 2000 },
{ day: '周五', calories: 1650, goal: 2000 },
{ day: '周六', calories: 2200, goal: 2000 },
{ day: '周日', calories: 1450, goal: 2000 }
]
private macroTargets: MacroTarget[] = [
{ name: '蛋白质', current: 95, target: 120, unit: 'g', color: '#E65100', percentage: 79 },
{ name: '碳水', current: 180, target: 250, unit: 'g', color: '#FFB300', percentage: 72 },
{ name: '脂肪', current: 48, target: 65, unit: 'g', color: '#FF8F00', percentage: 74 },
{ name: '纤维', current: 22, target: 30, unit: 'g', color: '#FF6F00', percentage: 73 }
]

dailyCalories 数组提供了过去一周每天的卡路里摄入与目标对比数据。每条记录的 goal 均为 2000 kcal,而实际 calories 在 1450~2200 之间波动——其中周四(2100)和周六(2200)超过了目标值,这种"有升有降"的数据设计使得界面渲染时的条形图颜色变化更加丰富(超量时变红),也更具展示效果和真实感。
macroTargets 数组定义了四类宏量营养素的目标达成情况。每条记录的 color 字段使用了不同的橙色系色调(从深橙 #E65100 到琥珀 #FFB300),percentage 字段预计算了完成百分比。虽然 percentage 理论上可以通过 current / target * 100 动态计算,但将其预存为数据字段的做法可以避免在渲染时进行重复计算,提升了渲染性能。这种"空间换时间"的取舍在数据量较大时尤为有意义。
3.4 每周蛋白质趋势与配置数据
private weeklyProteins: WeeklyProtein[] = [
{ day: '周一', protein: 110 },
{ day: '周二', protein: 125 },
{ day: '周三', protein: 95 },
{ day: '周四', protein: 130 },
{ day: '周五', protein: 88 },
{ day: '周六', protein: 135 },
{ day: '周日', protein: 95 }
]
private tabConfigs: TabConfig[] = [
{ index: MealTab.Meals, label: '餐食', icon: '🍽️' },
{ index: MealTab.Weekly, label: '周计划', icon: '📅' },
{ index: MealTab.Nutrition, label: '营养', icon: '📊' },
{ index: MealTab.Mine, label: '我的', icon: '👤' }
]
private mealTypeConfigs: MealTypeConfig[] = [
{ type: '全部', icon: '🍽️', color: '#E65100', targetCalories: 2000 },
{ type: '早餐', icon: '🌅', color: '#FFB300', targetCalories: 500 },
{ type: '午餐', icon: '☀️', color: '#FF8F00', targetCalories: 700 },
{ type: '晚餐', icon: '🌙', color: '#E65100', targetCalories: 600 },
{ type: '加餐', icon: '🥜', color: '#FF6F00', targetCalories: 200 }
]
private mealTypeFilters: string[] = ['全部', '早餐', '午餐', '晚餐', '加餐']
weeklyProteins 数组记录了一周内每天的蛋白质摄入量(单位 g),数据范围在 88~135g 之间,目标参考值约为 120g(从 macroTargets 中蛋白质的目标值推断)。这个数据专门服务于"周计划"Tab 中的"每周蛋白质趋势"条形图渲染。
tabConfigs 数组是底部导航栏的数据源,包含了四个 Tab 的配置。index 字段引用了 MealTab 枚举值,label 为中文标签,icon 为 Emoji。这种数据驱动的导航配置方式是声明式 UI 的精髓——导航栏的渲染逻辑是通用的 ForEach 遍历,配置变更无需修改渲染代码。
mealTypeConfigs 数组为每种餐类定义了图标、颜色和目标卡路里。这些配置虽然在本应用中主要用于数据展示(如营养 Tab 中的"餐类卡路里分布"),但其设计意图是为应用提供一个可扩展的餐类管理系统。mealTypeFilters 是一个简单的字符串数组,专门用于"餐食"Tab 中的横向滚动筛选条。
至此,所有的数据源都已就绪。这些数据构成了应用的数据层(Data Layer),而后续的 build() 方法和各个 @Builder 函数则构成了表现层(Presentation Layer)。两者之间通过状态变量(@State)和事件回调建立连接,实现了数据驱动 UI 的核心架构。
四、主入口 build 方法与布局骨架
4.1 顶部标题栏与主布局结构
build() {
Column() {
Column() {
// 顶部标题栏
Row() {
Text('🍽️ 健身饮食').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Row().layoutWeight(1)
Text('📅').fontSize(20).fontColor('#FFFFFF').margin({ right: 16 })
Text('⚙️').fontSize(20).fontColor('#FFFFFF')
}
.width('100%').height(56).padding({ left: 20, right: 20 })
.backgroundColor('#E65100').alignItems(VerticalAlign.Center)
// 今日卡路里进度
this.calorieProgressBuilder()
// 内容区域
Column() {
if (this.currentTab === MealTab.Meals) {
this.mealsTab()
} else if (this.currentTab === MealTab.Weekly) {
this.weeklyTab()
} else if (this.currentTab === MealTab.Nutrition) {
this.nutritionTab()
} else {
this.mineTab()
}
}.layoutWeight(1).width('100%')
// 底部导航栏
this.bottomTabBar()
}
.width('100%').height('100%').backgroundColor('#FFF3E0')
// 弹窗层
if (this.showAddModal) {
this.addModal()
}
if (this.showEditModal) {
this.editModal()
}
if (this.showDeleteModal) {
this.deleteModal()
}
}
}
build() 方法是 ArkTS 声明式 UI 的核心入口,它返回一个组件树描述。当任何 @State 变量发生变化时,框架会重新调用 build() 方法并高效地更新差异部分。
最外层是一个 Column,它承载了两部分内容:主界面容器(内层 Column)和弹窗层(三个 if 条件块)。这种将弹窗与主界面平级放置在最外层的设计,确保了弹窗能够覆盖整个屏幕(包括底部导航栏),实现真正的全屏模态效果。
内层 Column 构成了主界面的骨架,从上到下依次排列三个区域:顶部标题栏、内容区域和底部导航栏。顶部标题栏是一个 Row,左侧显示应用名称"🍽️ 健身饮食",中间使用 Row().layoutWeight(1) 占据弹性空间(将右侧图标推向右边),右侧放置日历和设置两个 Emoji 图标。标题栏高度 56vp,背景为主色橙色 #E65100,文字为白色,视觉简洁醒目。alignItems(VerticalAlign.Center) 确保所有子元素垂直居中对齐。
内容区域使用了 layoutWeight(1),这是 ArkTS 布局中的关键属性——它使该 Column 占据父容器中所有剩余的弹性空间(即总高度减去标题栏和导航栏的高度后的全部空间)。内部通过 if-else if-else 条件渲染,根据 currentTab 的值调用对应的 @Builder 函数。当用户点击底部导航栏切换 Tab 时,currentTab 状态变化触发 build() 重新执行,内容区域自动切换到对应的 Tab 页面。
底部导航栏通过 this.bottomTabBar() 调用,固定在主界面最底部。整个内层 Column 的背景色设为浅橙 #FFF3E0,为内容区域提供了温暖的底色。
弹窗层位于外层 Column 中,三个 if 条件分别检查 showAddModal、showEditModal、showDeleteModal 的值。当某个状态为 true 时,对应的弹窗 Builder 会被调用并渲染到最外层,覆盖在主界面之上。这种条件渲染的弹窗管理方式简单直接,无需引入第三方模态库。
五、顶部卡路里进度组件
5.1 calorieProgressBuilder 构建
@Builder calorieProgressBuilder() {
Column() {
Row() {
Text('🔥 今日卡路里').fontSize(13).fontColor('#FFFFFF')
Row().layoutWeight(1)
Text(this.nutritionScore + '分').fontSize(13).fontColor('#FFB300').fontWeight(FontWeight.Bold)
}.width('100%')
Row() {
Text(this.todayCalories + '').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(' / ' + this.dailyCalorieGoal + ' kcal').fontSize(14).fontColor('#FFE0B2')
.margin({ left: 4, bottom: 4 })
}.margin({ top: 6 }).alignItems(VerticalAlign.Bottom)
// 进度条
Row() {
Column()
.width(this.todayCalories / this.dailyCalorieGoal * 100 + '%')
.height(6)
.backgroundColor('#FFB300')
.borderRadius(3)
}.width('100%').height(6).backgroundColor('#BF360C').borderRadius(3).margin({ top: 8 })
Row() {
Text('剩余 ' + (this.dailyCalorieGoal - this.todayCalories) + ' kcal').fontSize(11).fontColor('#FFE0B2')
Row().layoutWeight(1)
Text('🔥 营养评分').fontSize(11).fontColor('#FFE0B2')
}.width('100%').margin({ top: 6 })
}
.width('100%').padding(16).backgroundColor('#E65100').borderRadius(0)
}
calorieProgressBuilder 是一个 @Builder 装饰的构建器函数,用于渲染应用顶部的"今日卡路里进度"区域。@Builder 是 ArkTS 中实现 UI 片段复用的核心装饰器——它可以将一段 UI 描述封装为可调用的函数,在 build() 方法中通过 this.xxxBuilder() 的方式引用。与独立的自定义组件(@Component struct)不同,@Builder 函数不拥有独立的状态,它直接访问宿主组件的状态变量,这使得它非常适合在同一个组件内部拆分复杂的 UI 结构。
该构建器渲染的内容分为四个层次:
第一层是标题行,左侧显示"🔥 今日卡路里",右侧显示营养评分(如"85分")。评分使用黄色 #FFB300 加粗显示,在橙色背景上形成鲜明的高亮效果。中间的 Row().layoutWeight(1) 起到弹性占位的作用。
第二层是数据展示行,左侧大字号(28vp)加粗显示今日已摄入卡路里(如"1450"),右侧以较小字号(14vp)显示目标值(如" / 2000 kcal")。两个 Text 通过 alignItems(VerticalAlign.Bottom) 实现底对齐,使得大小字号文本在底部齐平,视觉上更加协调。这种大小字号的搭配是数据类应用中常见的展示手法,既突出了核心数字,又保留了上下文信息。
第三层是进度条。进度条由两层 Row 嵌套构成:外层 Row 作为容器,背景色为深橙红 #BF360C(表示未完成的部分),高度 6vp,圆角 3vp;内层是一个空 Column,其宽度通过 this.todayCalories / this.dailyCalorieGoal * 100 + '%' 动态计算——即已摄入卡路里占目标值的百分比,背景色为黄色 #FFB300。例如当今日摄入 1450、目标 2000 时,进度条宽度为 72.5%。这种"容器+填充"的双层结构是进度条的经典实现方式,通过百分比宽度的字符串拼接实现了动态计算。
第四层是辅助信息行,左侧显示"剩余 550 kcal"(目标值减去已摄入值),右侧显示"🔥 营养评分"标签。整个组件的背景色与标题栏一致为 #E65100,padding 为 16vp,形成从标题栏到内容区域的无缝过渡。
这个组件的设计亮点在于它将多项关键信息(评分、已摄入、目标、剩余、进度条)整合在一个紧凑的区域内,信息密度高但层次分明,是应用顶部"信息总览"区域的核心展示组件。
六、底部导航栏组件
6.1 bottomTabBar 构建
@Builder bottomTabBar() {
Row() {
ForEach(this.tabConfigs, (item: TabConfig) => {
Column() {
Text(item.icon).fontSize(22)
Text(item.label).fontSize(10).fontColor(this.currentTab === item.index ? '#E65100' : '#9E9E9E')
.margin({ top: 2 })
}.alignItems(HorizontalAlign.Center).layoutWeight(1)
.onClick(() => { this.currentTab = item.index })
}, (item: TabConfig) => item.index.toString())
}
.width('100%').height(56).backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.Center)
.shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
}
bottomTabBar 构建器负责渲染应用底部的四标签导航栏。这是整个应用导航交互的核心组件。
渲染逻辑使用了 ForEach 遍历 tabConfigs 数组。ForEach 是 ArkTS 中用于列表渲染的核心组件,它接收三个参数:数据源数组、子项生成函数和键值生成函数。子项生成函数为每个 TabConfig 生成一个 Column,包含 Emoji 图标(22vp)和文字标签(10vp)。标签的颜色通过三元表达式动态判断:如果 this.currentTab === item.index(当前激活的 Tab),文字颜色为橙色 #E65100;否则为灰色 #9E9E9E。这种高亮当前 Tab 的视觉反馈是底部导航栏的标准交互模式。
每个 Column 都绑定了 onClick 事件,点击时将 this.currentTab 设置为该 Tab 的 index 值。由于 currentTab 是 @State 变量,赋值后会自动触发 build() 方法重新执行,内容区域的 if-else 条件会渲染新的 Tab 页面。这种"点击改状态→状态驱动渲染"的闭环,是声明式 UI 的核心运行机制。
ForEach 的第三个参数是键值生成函数 (item: TabConfig) => item.index.toString(),它为每个列表项生成一个唯一的字符串 key。在 ArkTS 中,key 的作用是帮助框架在数据变化时高效地定位差异,实现最小化更新。虽然在本应用中 tabConfigs 数组是静态的,但提供正确的 key 仍然是良好的编码习惯。
布局上,每个 Tab 项使用 layoutWeight(1) 实现等分宽度,四个 Tab 平均占据导航栏的总宽度。alignItems(HorizontalAlign.Center) 使图标和文字水平居中。整个导航栏高度 56vp,背景白色,并通过 shadow 属性添加了向上偏移 2vp 的轻微阴影(offsetY: -2,颜色为半透明黑色 #1A000000),在视觉上将导航栏与上方内容区域分离,增强了层次感。
七、餐食 Tab 页详解
7.1 mealsTab 主体结构
@Builder mealsTab() {
Column() {
// 餐类选择
Scroll() {
Row() {
ForEach(this.mealTypeFilters, (mt: string) => {
Text(mt)
.fontSize(12).fontColor(this.selectedMealType === mt ? '#FFFFFF' : '#E65100')
.backgroundColor(this.selectedMealType === mt ? '#E65100' : '#FFE0B2')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(16).margin({ right: 8 })
.onClick(() => { this.selectedMealType = mt })
}, (mt: string) => mt)
}.padding({ left: 16, right: 16 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(40)
// 营养素汇总卡片
Row() {
Column() {
Text('蛋白质').fontSize(10).fontColor('#FFE0B2')
Text('95g').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
// ... 碳水、脂肪、纤维 ...
}.width('100%').padding(14).backgroundColor('#FF8F00').borderRadius(14).margin({ top: 4, left: 16, right: 16 })
// 餐食列表
Scroll() {
Column() {
this.mealItemBuilder(this.meals[0])
// ... this.mealItemBuilder(this.meals[1]) 到 this.mealItemBuilder(this.meals[25]) ...
Row() {
Text('➕ 添加新餐食').fontSize(14).fontColor('#E65100').fontWeight(FontWeight.Bold)
}.width('100%').height(48).justifyContent(FlexAlign.Center)
.backgroundColor('#FFE0B2').borderRadius(12).margin({ top: 12 })
.onClick(() => { this.showAddModal = true })
}.padding({ left: 16, right: 16, top: 8, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}.width('100%').height('100%')
}
mealsTab 是应用的主功能页面,也是默认展示的 Tab 页。它由三个区域构成:横向滚动的餐类筛选条、营养素汇总卡片和可滚动的餐食列表。
最上方的餐类筛选条使用了 Scroll + Row 的组合,scrollable(ScrollDirection.Horizontal) 使其支持横向滑动,scrollBar(BarState.Off) 隐藏了滚动条以保持视觉简洁。筛选条通过 ForEach 遍历 mealTypeFilters 数组(共 5 项:全部、早餐、午餐、晚餐、加餐),为每一项生成一个胶囊形(borderRadius(16))的 Text 标签。选中状态的标签背景为橙色 #E65100、文字白色;未选中状态的背景为浅橙 #FFE0B2、文字橙色。点击标签时将 selectedMealType 设置为对应的餐类名称,触发筛选状态更新。这种"胶囊标签组"的交互模式在移动应用中极为常见,如 iOS 的分段控制器和 Android 的 Chip 组件。
中间的营养素汇总卡片是一个横向排列的四列布局,分别展示蛋白质(95g)、碳水(180g)、脂肪(48g)和纤维(22g)的当日总量。每列使用 layoutWeight(1) 等分宽度,标签字号 10vp、数值字号 18vp 加粗。卡片背景为中橙色 #FF8F00,圆角 14vp,左右边距 16vp。这个卡片将关键营养素信息整合在一行中,让用户在浏览餐食列表前就能快速了解当日营养摄入概况。
最下方是餐食列表区域,使用 Scroll 包裹一个 Column,支持垂直滚动。列表中通过 26 次 this.mealItemBuilder(this.meals[i]) 调用,将每条餐食数据渲染为列表项卡片。这里采用的是手动展开调用而非 ForEach 遍历的方式——虽然 ForEach 更为简洁,但手动展开确保了每条数据的渲染独立可控,且避免了 key 生成的问题。列表末尾是一个"➕ 添加新餐食"的按钮,点击后设置 showAddModal = true 触发新增弹窗。整个列表区域使用 layoutWeight(1) 占据剩余空间,隐藏滚动条。
7.2 mealItemBuilder 餐食卡片构建
@Builder mealItemBuilder(meal: MealModel) {
Row() {
// 食物Emoji
Stack() {
Text(meal.emoji).fontSize(28)
}.width(52).height(52).backgroundColor('#FFE0B2').borderRadius(10)
.align(Alignment.Center)
Column() {
Row() {
Text(meal.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121')
if (meal.isCompleted) {
Text('✓').fontSize(12).fontColor('#33691E').margin({ left: 6 })
}
}.width('100%').alignItems(VerticalAlign.Center)
Row() {
Text(meal.mealType).fontSize(10).fontColor('#E65100')
.backgroundColor('#FFF3E0').padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
Text('⏰ ' + meal.time).fontSize(10).fontColor('#9E9E9E').margin({ left: 8 })
Text('🔥 ' + meal.calories + 'kcal').fontSize(10).fontColor('#FF6F00').margin({ left: 8 })
}.width('100%').margin({ top: 6 }).alignItems(VerticalAlign.Center)
// 营养素进度条
Row() {
Column()
.width(meal.protein / 50 * 100 + '%')
.height(4).backgroundColor('#E65100').borderRadius(2)
}.width('30%').height(4).backgroundColor('#F5F5F5').borderRadius(2).margin({ top: 6 })
Row() {
Text('🥩 ' + meal.protein + 'g').fontSize(10).fontColor('#757575')
Text('🍞 ' + meal.carbs + 'g').fontSize(10).fontColor('#757575').margin({ left: 8 })
Text('🧈 ' + meal.fat + 'g').fontSize(10).fontColor('#757575').margin({ left: 8 })
Text('🌿 ' + meal.fiber + 'g').fontSize(10).fontColor('#757575').margin({ left: 8 })
}.width('100%').margin({ top: 4 }).alignItems(VerticalAlign.Center)
}.layoutWeight(1).margin({ left: 12 }).alignItems(HorizontalAlign.Start)
Column() {
Text('✏️').fontSize(14).margin({ bottom: 10 })
.onClick(() => {
this.editIndex = meal.id - 1
this.showEditModal = true
})
Text('🗑️').fontSize(14)
.onClick(() => {
this.deleteIndex = meal.id - 1
this.showDeleteModal = true
})
}.alignItems(HorizontalAlign.Center)
}
.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12).margin({ top: 8 })
.alignItems(VerticalAlign.Center)
}
mealItemBuilder 是餐食列表中每一张餐食卡片的构建器,接收一个 MealModel 参数。这个构建器是整个应用中信息密度最高的 UI 组件,将餐食的图标、名称、状态、类型、时间、卡路里、营养素进度和操作按钮整合在一张卡片中。
卡片采用 Row 横向布局,从左到右分为三个部分:
左侧是食物图标区域,使用 Stack 容器承载 Emoji(28vp),背景浅橙 #FFE0B2,尺寸 52x52vp,圆角 10vp。Stack 的 align(Alignment.Center) 确保 Emoji 居中显示。使用 Stack 而非直接用 Text,是因为后续可能需要在图标上叠加角标(如未完成标记)。
中间是信息主体区域,使用 Column 纵向排列四行内容。第一行是餐食名称和完成状态标记——名称使用 15vp 加粗深灰色 #212121,如果 isCompleted 为 true,则在名称右侧显示绿色(#33691E)的"✓"标记。第二行是元信息行,包括餐类标签(橙色文字+浅橙背景的胶囊)、时间(带⏰图标)和卡路里(带🔥图标,橙色文字)。第三行是蛋白质进度条,宽度限制在 30% 内,通过 meal.protein / 50 * 100 + '%' 计算填充比例(以 50g 为满格基准)。第四行是四种营养素的数值展示,每种配以对应的 Emoji(🥩蛋白质、🍞碳水、🧈脂肪、🌿纤维)。
右侧是操作按钮区域,垂直排列编辑(✏️)和删除(🗑️)两个图标。编辑按钮的 onClick 回调将 editIndex 设置为 meal.id - 1(通过 id 减 1 得到数组索引),然后触发编辑弹窗。删除按钮同理设置 deleteIndex 并触发删除确认弹窗。
整个卡片背景白色,圆角 12vp,padding 12vp,上边距 8vp,实现了卡片之间的视觉间距。这种设计兼顾了信息丰富度与视觉整洁度,是列表项设计的优秀范例。
八、周计划 Tab 页详解
8.1 weeklyTab 主体结构
@Builder weeklyTab() {
Scroll() {
Column() {
// 周计划标题
Row() {
Text('📅 本周餐食计划').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ bottom: 8 })
// 每日餐食卡片
this.dayPlanBuilder('周一', 1850, 4, '#E65100')
this.dayPlanBuilder('周二', 1950, 4, '#FF8F00')
this.dayPlanBuilder('周三', 1720, 5, '#FFB300')
this.dayPlanBuilder('周四', 2100, 4, '#E65100')
this.dayPlanBuilder('周五', 1650, 3, '#FF8F00')
this.dayPlanBuilder('周六', 2200, 5, '#FFB300')
this.dayPlanBuilder('周日', 1450, 4, '#E65100')
// 每周蛋白质趋势
Row() {
Text('💪 每周蛋白质趋势').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
this.weeklyProteinBuilder(this.weeklyProteins[0])
// ... weeklyProteins[1] 到 weeklyProteins[6] ...
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
}.padding({ left: 16, right: 16, top: 12, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}
weeklyTab 构建器渲染"周计划"页面,展示一周内每天的餐食规划概览和蛋白质摄入趋势。整个页面使用 Scroll 包裹,支持垂直滚动。
页面分为两个区块:上半部分是"本周餐食计划",通过 7 次 dayPlanBuilder 调用渲染周一到周日的每日卡片。每次调用传入了四个参数:日期名称、卡路里总量、已规划餐数和主题色。值得注意的是卡路里和颜色之间的关联——超过 2000 kcal 的天数(周四 2100、周六 2200)使用了较深的橙色 #E65100,而热量较低的天数使用了较浅的 #FFB300,这种颜色暗示虽然细微,但在视觉上能够传递热量高低的差异。
下半部分是"每周蛋白质趋势",同样通过 7 次 weeklyProteinBuilder 调用渲染。这里的数据取自 weeklyProteins 数组,与上半部分的每日卡路里数据形成互补——卡路里关注总量,蛋白质关注关键营养素。两个区块之间通过 20vp 的上边距实现视觉分隔。
整个页面的数据虽然也是硬编码的,但与 dailyCalories 和 weeklyProteins 数组中的数据一一对应,体现了数据的一致性。在实际应用中,这些数据可以从 meals 数组中按日期聚合计算得出。
8.2 dayPlanBuilder 每日计划卡片
@Builder dayPlanBuilder(day: string, calories: number, mealCount: number, color: string) {
Row() {
Column() {
Text(day).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(calories + ' kcal').fontSize(11).fontColor('#FFE0B2').margin({ top: 2 })
}.width(60).alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text('🌅 早餐').fontSize(11).fontColor('#757575')
Text('☀️ 午餐').fontSize(11).fontColor('#757575').margin({ left: 12 })
Text('🌙 晚餐').fontSize(11).fontColor('#757575').margin({ left: 12 })
Text('🥜 加餐').fontSize(11).fontColor('#757575').margin({ left: 12 })
}.width('100%')
Row() {
Text(mealCount + ' 餐已规划').fontSize(11).fontColor('#E65100').fontWeight(FontWeight.Bold)
}.width('100%').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 12 }).alignItems(HorizontalAlign.Start)
// 卡路里进度条
Column() {
Row() {
Column()
.width(calories / 2500 * 100 + '%')
.height(6)
.backgroundColor(color)
.borderRadius(3)
}.width(60).height(6).backgroundColor('#F5F5F5').borderRadius(3)
Text(calories / 2000 * 100 + '%').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}.alignItems(HorizontalAlign.Center)
}
.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(12).margin({ top: 8 })
.alignItems(VerticalAlign.Center)
}
dayPlanBuilder 接收四个参数:day(日期名称)、calories(卡路里总量)、mealCount(已规划餐数)和 color(主题色),渲染一张每日计划卡片。
卡片采用三段式横向布局。左侧是一个固定宽度 60vp 的 Column,显示日期名称(白色加粗)和卡路里数值(浅橙色)。这里的文字颜色设计很巧妙——卡片整体背景为白色,但左侧日期区域没有单独设置背景色,白色文字在白底上应该是不可见的。实际上,仔细分析代码可以发现,这些白色文字是配合视觉设计的预留——在实际渲染时,如果文字不可见,开发者可能需要调整。不过从整体设计意图来看,左侧区域可能计划使用深色背景块但当前版本尚未实现。
中间区域使用 layoutWeight(1) 占据弹性空间,展示两行信息:第一行是四种餐类的标签(早餐、午餐、晚餐、加餐,各带 Emoji),第二行是已规划餐数(如"4 餐已规划",橙色加粗)。这种"分类标签 + 状态摘要"的组合方式,让用户一眼就能了解当日餐食规划的覆盖情况。
右侧是卡路里进度条和百分比文字。进度条宽度固定 60vp,填充比例通过 calories / 2500 * 100 + '%' 计算(以 2500 kcal 为满格基准),颜色取自参数 color。进度条下方显示卡路里占目标的百分比(以 2000 为基准)。使用两个不同的基准(2500 和 2000)可能是一个设计上的考量——进度条以 2500 为满格避免条形图溢出容器,而百分比文字以 2000(每日目标)为基准更贴近用户认知。
8.3 weeklyProteinBuilder 每周蛋白质条形图
@Builder weeklyProteinBuilder(item: WeeklyProtein) {
Row() {
Text(item.day).fontSize(12).fontColor('#424242').width(40)
Row() {
Column()
.width(item.protein / 150 * 100 + '%')
.height(14)
.backgroundColor('#E65100')
.borderRadius(7)
}.layoutWeight(1).height(14).backgroundColor('#F5F5F5').borderRadius(7)
Text(item.protein + 'g').fontSize(11).fontColor('#757575').width(40).textAlign(TextAlign.Center)
}.width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Center)
}
weeklyProteinBuilder 渲染每周蛋白质趋势的单行条形图,接收一个 WeeklyProtein 参数。这个构建器实现了一个经典的水平条形图(horizontal bar chart)。
每行由三部分构成:左侧固定宽度 40vp 的日期标签、中间弹性宽度的条形图条、右侧固定宽度 40vp 的数值标签。条形图的填充比例通过 item.protein / 150 * 100 + '%' 计算,以 150g 为满格基准。条形高度 14vp,圆角 7vp(高度的一半,形成完全圆角的胶囊形),填充色为橙色 #E65100,轨道背景为浅灰 #F5F5F5。右侧数值居中对齐(textAlign(TextAlign.Center))。
这种"标签-条形-数值"的三列水平条形图是数据可视化中最基础也最实用的形式之一。它的优点在于实现简单(无需引入图表库)、渲染高效(纯 CSS/样式属性)、信息清晰(数值与视觉比例双重表达)。7 行数据纵向排列就构成了一个完整的周蛋白质趋势图表。
九、营养 Tab 页详解
9.1 nutritionTab 主体结构
@Builder nutritionTab() {
Scroll() {
Column() {
// 每日卡路里柱状图
Row() {
Text('📊 每日卡路里摄入').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ bottom: 8 })
Column() {
this.dailyCalorieBuilder(this.dailyCalories[0])
// ... dailyCalories[1] 到 dailyCalories[6] ...
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
// 宏量营养素分布
Row() {
Text('🥗 宏量营养素目标').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
this.macroTargetBuilder(this.macroTargets[0])
// ... macroTargets[1] 到 macroTargets[3] ...
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
// 营养评分卡
Row() {
Text('🏆 营养评分').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
Row() {
Stack() {
Text('85').fontSize(32).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}.width(80).height(80).backgroundColor('#E65100').borderRadius(40)
.border({ width: 4, color: '#FFB300' }).align(Alignment.Center)
Column() {
Text('优秀!').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
Text('你的饮食营养均衡').fontSize(12).fontColor('#757575').margin({ top: 4 })
Row() {
Text('建议增加蛋白质摄入').fontSize(11).fontColor('#E65100')
}.margin({ top: 6 })
}.layoutWeight(1).margin({ left: 16 }).alignItems(HorizontalAlign.Start)
}.width('100%').alignItems(VerticalAlign.Center)
}.width('100%').padding(20).backgroundColor('#FFFFFF').borderRadius(14)
// 餐类卡路里分布
Row() {
Text('🍽️ 餐类卡路里分布').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
this.mealTypeCalorieBuilder('早餐', 500, '#FFB300')
this.mealTypeCalorieBuilder('午餐', 700, '#FF8F00')
this.mealTypeCalorieBuilder('晚餐', 600, '#E65100')
this.mealTypeCalorieBuilder('加餐', 200, '#FF6F00')
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
}.padding({ left: 16, right: 16, top: 12, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}
nutritionTab 是营养数据可视化页面,是信息量最大的 Tab 页,包含四个数据可视化区块:每日卡路里柱状图、宏量营养素目标、营养评分卡和餐类卡路里分布。
页面整体使用 Scroll 包裹,四个区块以卡片形式纵向排列,区块之间通过 20vp 的上边距分隔。每个区块的结构一致:一个标题行(带 Emoji 和加粗文字)+ 一个白色圆角卡片容器(padding 16vp,圆角 14vp)。
第三个区块"营养评分卡"的设计尤为精巧。它使用了一个圆形评分徽章——Stack 容器尺寸 80x80vp,背景橙色 #E65100,圆角 40vp(正好为宽度的一半,形成正圆),外加 4vp 宽的黄色 #FFB300 边框,内部居中显示"85"的白色大字号(32vp)数字。这种圆形徽章配合边框的设计,营造了类似"奖牌"或"勋章"的视觉效果,对用户形成正向激励。徽章右侧是评价文字(“优秀!”、描述语、建议),形成"图标+文字"的经典信息展示组合。
第四个区块"餐类卡路里分布"直接传入了硬编码的参数值(早餐 500、午餐 700、晚餐 600、加餐 200),这些数值与 mealTypeConfigs 中的 targetCalories 一致,展示了各餐类的目标卡路里占比。以 700(午餐最大值)为满格基准,通过 calories / 700 * 100 + '%' 计算各条形图的填充比例。
9.2 dailyCalorieBuilder 每日卡路里条形图
@Builder dailyCalorieBuilder(item: DailyCalorie) {
Row() {
Text(item.day).fontSize(12).fontColor('#424242').width(40)
Column() {
Row() {
Column()
.width(item.calories / 2500 * 100 + '%')
.height(16)
.backgroundColor(item.calories > item.goal ? '#C62828' : '#E65100')
.borderRadius(8)
}.width('100%').height(16).backgroundColor('#F5F5F5').borderRadius(8)
}.layoutWeight(1)
Text(item.calories + '').fontSize(11).fontColor(item.calories > item.goal ? '#C62828' : '#757575').width(50).textAlign(TextAlign.Center)
}.width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Center)
}
dailyCalorieBuilder 渲染每日卡路里摄入的单行条形图,结构与 weeklyProteinBuilder 类似,但增加了一个关键的视觉特性——超量预警颜色变化。
条形图的填充颜色通过三元表达式判断:item.calories > item.goal ? '#C62828' : '#E65100'。当实际摄入超过目标值时,条形图变为红色 #C62828(深红),否则为橙色 #E65100。同样,右侧数值的文字颜色也随条件变化。这种条件化的颜色设计是数据可视化中"编码语义"的经典应用——颜色不仅仅是装饰,更是信息的载体,红色直观地传达了"超量"这一警示信息。
条形图高度 16vp(比蛋白质条形图的 14vp 稍高),圆角 8vp,填充比例以 2500 kcal 为满格基准。左右两侧标签的宽度分别为 40vp 和 50vp,确保数值列对齐。
9.3 macroTargetBuilder 宏量营养素目标进度
@Builder macroTargetBuilder(item: MacroTarget) {
Column() {
Row() {
Text(item.name).fontSize(13).fontColor('#424242').fontWeight(FontWeight.Bold)
Row().layoutWeight(1)
Text(item.current + '/' + item.target + item.unit).fontSize(12).fontColor('#757575')
}.width('100%')
Row() {
Column()
.width(item.percentage + '%')
.height(10)
.backgroundColor(item.color)
.borderRadius(5)
}.width('100%').height(10).backgroundColor('#F5F5F5').borderRadius(5).margin({ top: 6 })
Text(item.percentage + '% 目标完成').fontSize(10).fontColor('#9E9E9E').margin({ top: 4 })
}.width('100%').margin({ top: 12 })
}
macroTargetBuilder 渲染宏量营养素的目标达成进度条,接收一个 MacroTarget 参数。每个营养素占用三行垂直空间:标题行、进度条行和百分比说明行。
标题行左侧是营养素名称(加粗深灰),右侧是"当前值/目标值+单位"的文本(如"95/120g")。中间的 Row().layoutWeight(1) 将右侧数值推到最右边。进度条高度 10vp,圆角 5vp,填充宽度直接取自数据中的 percentage 字段(如 79%),颜色取自 item.color。这里充分体现了前面提到的"数据驱动样式"设计——不同营养素使用不同的颜色(蛋白质深橙、碳水黄色、脂肪中橙、纤维橙红),颜色直接来自数据结构,渲染逻辑无需判断。
进度条下方显示"79% 目标完成"的辅助说明文字,灰色小字号,为用户提供精确的百分比数值。整个组件的设计层次分明:标题提供上下文,进度条提供视觉比例,百分比提供精确数值,三者互补形成完整的信息表达。
9.4 mealTypeCalorieBuilder 餐类卡路里分布
@Builder mealTypeCalorieBuilder(type: string, calories: number, color: string) {
Row() {
Text(type).fontSize(12).fontColor('#424242').width(50)
Row() {
Column()
.width(calories / 700 * 100 + '%')
.height(12)
.backgroundColor(color)
.borderRadius(6)
}.layoutWeight(1).height(12).backgroundColor('#F5F5F5').borderRadius(6)
Text(calories + 'kcal').fontSize(11).fontColor('#757575').width(56).textAlign(TextAlign.Center)
}.width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Center)
}
mealTypeCalorieBuilder 渲染餐类卡路里分布的单行条形图,接收三个参数:餐类名称、卡路里值和颜色。结构与前两个条形图构建器一致,但参数全部由调用处传入,使得该构建器更加通用化。
条形图以 700 kcal(午餐的目标值,也是四个餐类中的最大值)为满格基准,高度 12vp,圆角 6vp。四种餐类使用了四种不同的橙色系颜色(早餐黄、午餐中橙、晚餐深橙、加餐橙红),在视觉上形成从浅到深的渐变效果,呼应了各餐类在全天卡路里中的占比大小。
这个构建器的设计与 weeklyProteinBuilder 和 dailyCalorieBuilder 高度相似,三者共享了"标签-条形-数值"的布局模板。在实际工程优化中,可以考虑将它们合并为一个通用的条形图构建器,通过参数控制颜色逻辑和格式。但在当前版本中,保持独立的构建器有助于代码的可读性和可维护性。
十、我的 Tab 页详解
10.1 mineTab 主体结构
@Builder mineTab() {
Scroll() {
Column() {
// 用户信息卡片
Row() {
Stack() {
Text('💪').fontSize(40)
}.width(80).height(80).backgroundColor('#E65100').borderRadius(40).align(Alignment.Center)
Column() {
Text('健身达人').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('已坚持 128 天').fontSize(12).fontColor('#FFE0B2').margin({ top: 4 })
Row() {
Text('PRO会员').fontSize(10).fontColor('#FFB300')
.backgroundColor('#BF360C').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
}.margin({ top: 6 })
}.layoutWeight(1).margin({ left: 16 }).alignItems(HorizontalAlign.Start)
}.width('100%').padding(20).backgroundColor('#E65100').borderRadius(16)
// 统计数据
Row() {
Column() {
Text('26').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E65100')
Text('餐食记录').fontSize(11).fontColor('#757575').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
// ... 坚持天数、营养评分、日均卡路里 ...
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 12 })
// 身体数据
Row() {
Text('📏 身体数据').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
Row() {
Text('体重').fontSize(13).fontColor('#424242')
Row().layoutWeight(1)
Text('68.5 kg').fontSize(13).fontColor('#E65100').fontWeight(FontWeight.Bold)
Text(' ↓ 2.5kg').fontSize(11).fontColor('#33691E').margin({ left: 8 })
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
// ... 身高、BMI、体脂率 ...
}.width('100%').backgroundColor('#FFFFFF').borderRadius(14)
// 功能列表
Column() {
this.menuItemBuilder('🎯', '目标设置', '卡路里·营养素目标')
this.menuItemBuilder('📋', '饮食历史', '查看历史记录')
this.menuItemBuilder('💧', '饮水记录', '今日 1.5L / 2L')
this.menuItemBuilder('🏃', '运动记录', '今日消耗 320kcal')
this.menuItemBuilder('🔔', '餐食提醒', '定时提醒用餐')
this.menuItemBuilder('ℹ️', '关于', '版本 v3.1.0')
}.width('100%').backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 12 })
}.padding({ left: 16, right: 16, top: 16, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}
mineTab 是"我的"个人中心页面,是用户信息与设置功能的聚合页。页面包含四个区块:用户信息卡片、统计数据栏、身体数据卡片和功能菜单列表。
用户信息卡片是页面的视觉焦点,背景橙色 #E65100,圆角 16vp,padding 20vp。左侧是一个 80x80vp 的圆形头像区域(Stack + borderRadius(40) 形成正圆),背景也是橙色但比卡片背景稍深,内部居中显示"💪" Emoji(40vp)。右侧是用户信息:昵称"健身达人"(白色加粗 18vp)、坚持天数"已坚持 128 天"(浅橙 12vp)和 PRO 会员标签(黄色文字 + 深红 #BF360C 背景的胶囊标签)。PRO 标签的设计很巧妙——使用与主色形成对比的深红色背景,黄色文字在深红背景上极为醒目,起到了会员标识的"突出展示"效果。
统计数据栏是一个四列等分布局,分别展示餐食记录数(26)、坚持天数(128)、营养评分(85)和日均卡路里(1,820)。每个统计项的数值使用 20vp 加粗字体,数字颜色在橙色和黄色之间交替,下方是灰色描述标签。这个卡片以白色为背景,圆角 14vp,与上方的橙色用户卡片形成对比。
身体数据卡片是一个列表式的数据展示,包含体重(68.5 kg,↓ 2.5kg)、身高(175 cm)、BMI(22.4,正常)、体脂率(15.2%,↓ 3.8%)四项。每行采用"标签-弹性占位-数值-变化标识"的布局,数值使用橙色加粗,变化标识(如"↓ 2.5kg")使用绿色 #33691E 表示正向变化。这种颜色语义编码——橙色表示关键数据、绿色表示改善趋势——在健康类应用中是通用的设计语言。
功能菜单列表通过 6 次 menuItemBuilder 调用渲染,包含目标设置、饮食历史、饮水记录、运动记录、餐食提醒和关于六个功能入口。每个菜单项的右侧还带有动态描述(如"今日 1.5L / 2L"、“今日消耗 320kcal”),让用户无需进入详情即可了解当前状态。
10.2 menuItemBuilder 菜单项构建
@Builder menuItemBuilder(icon: string, title: string, desc: string) {
Row() {
Text(icon).fontSize(20).width(32)
Text(title).fontSize(14).fontColor('#212121').layoutWeight(1)
Text(desc).fontSize(12).fontColor('#9E9E9E')
Text('›').fontSize(18).fontColor('#BDBDBD').margin({ left: 8 })
}.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
.alignItems(VerticalAlign.Center)
}
menuItemBuilder 是一个高度通用的列表菜单项构建器,接收三个参数:图标(Emoji)、标题和描述。它渲染一个标准的设置列表行——左侧图标(20vp,固定宽度 32vp)、中间标题(14vp 深灰,弹性宽度)、右侧描述(12vp 浅灰)和最右侧的箭头指示符"›"(18vp,浅灰色 #BDBDBD)。
这种"图标+标题+描述+箭头"的行布局是移动应用设置页面的通用模式,几乎所有主流应用的个人中心都采用了这一设计。箭头"›"是一个 Unicode 字符,用于暗示该行可点击进入下一级页面。虽然本应用中菜单项尚未绑定 onClick 事件,但视觉上已经为用户建立了"可点击进入"的心理预期。
行高通过 padding({ top: 14, bottom: 14 }) 控制,确保了足够的触摸目标面积。alignItems(VerticalAlign.Center) 使所有元素垂直居中对齐。整个构建器的设计简洁、通用,是列表项复用的优秀范例。
十一、弹窗系统详解
11.1 modalBg 背景遮罩构建器
@Builder modalBg(onClose: () => void) {
Column().width('100%').height('100%')
.backgroundColor('rgba(0,0,0,0.5)').onClick(onClose)
}
modalBg 是所有弹窗共用的背景遮罩构建器,接收一个 onClose 回调函数作为参数。它渲染一个全屏透明黑色遮罩层(rgba(0,0,0,0.5),即 50% 不透明度的黑色),点击遮罩时触发 onClose 回调关闭弹窗。
这个构建器体现了 ArkTS 中 @Builder 函数支持接收函数类型参数(回调函数)的特性。通过将关闭逻辑参数化,modalBg 可以被不同弹窗复用——新增弹窗调用 this.modalBg(() => { this.showAddModal = false }),编辑弹窗调用 this.modalBg(() => { this.showEditModal = false }),各自设置对应的状态变量为 false。这种"函数参数注入行为"的设计模式,是构建可复用组件的核心技巧。
半透明遮罩是模态弹窗的标准视觉元素,它的作用有两个:一是将用户的注意力聚焦到弹窗内容上,二是通过点击遮罩关闭弹窗提供"点击外部退出"的交互快捷方式。rgba 颜色格式在 ArkTS 中的使用表明了其对标准 CSS 颜色语法的兼容性。
11.2 addModal 新增餐食弹窗
@Builder addModal() {
Column() {
this.modalBg(() => { this.showAddModal = false })
Column() {
Row() {
Text('➕ 新增餐食').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
Row().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#999999')
.onClick(() => { this.showAddModal = false })
}.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color('#F0F0F0')
Scroll() {
Column() {
this.formFieldBuilder('餐食名称', '请输入餐食名称')
this.formFieldBuilder('餐食类型', '早餐/午餐/晚餐/加餐')
this.formFieldBuilder('卡路里(kcal)', '请输入数字')
// ... 蛋白质、碳水、脂肪、纤维、日期、时间、Emoji、食谱、食材 ...
}.padding({ left: 20, right: 20 })
}.layoutWeight(1)
Row() {
Text('取消').fontSize(14).fontColor('#888888')
.backgroundColor('#F5F5F5').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.showAddModal = false })
Text('保存').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#E65100').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.showAddModal = false })
}.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('90%').constraintSize({ maxHeight: '80%' }).backgroundColor('#FFFFFF').borderRadius(18)
.position({ x: '5%', y: '8%' }).shadow({ radius: 20, color: '#33000000', offsetY: 4 })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
addModal 是新增餐食的模态弹窗构建器,也是应用中结构最复杂的弹窗。它的整体结构是一个全屏 Column(覆盖整个屏幕),内部包含两层:底层是 modalBg 遮罩,上层是白色弹窗容器。
弹窗容器使用 width('90%') 占据屏幕宽度的 90%,constraintSize({ maxHeight: '80%' }) 限制最大高度为屏幕的 80%,position({ x: '5%', y: '8%' }) 将弹窗定位在距左边 5%、距顶部 8% 的位置,使其在屏幕中偏上居中显示。圆角 18vp,配合 shadow(半径 20、半透明黑色 #33000000、向下偏移 4vp)形成浮起效果。zIndex(999) 确保弹窗层级最高,覆盖在所有其他元素之上。
弹窗内容从上到下分为三部分:标题栏(“➕ 新增餐食” + 关闭按钮)、表单区域和操作按钮栏。标题栏右侧的"✕"关闭按钮通过 onClick 设置 showAddModal = false 关闭弹窗。Divider 组件作为标题栏与表单之间的分隔线,颜色为浅灰 #F0F0F0。
表单区域使用 Scroll 包裹,支持内容超出弹窗高度时滚动。内部通过 12 次 formFieldBuilder 调用生成表单字段,涵盖餐食名称、类型、卡路里、四种营养素、日期、时间、Emoji、食谱和食材。每个字段使用 formFieldBuilder 统一渲染,传入标签名和 placeholder 提示文字。
操作按钮栏包含"取消"和"保存"两个按钮,水平居中排列(justifyContent(FlexAlign.Center))。取消按钮使用灰色背景 #F5F5F5 和灰色文字,保存按钮使用橙色背景 #E65100 和白色文字——主次按钮的颜色对比引导用户优先选择保存。两个按钮均通过 onClick 关闭弹窗(当前版本中保存按钮仅关闭弹窗,未实际添加数据)。
11.3 editModal 编辑餐食弹窗
@Builder editModal() {
Column() {
this.modalBg(() => { this.showEditModal = false })
Column() {
Row() {
Text('✏️ 编辑餐食').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
Row().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#999999')
.onClick(() => { this.showEditModal = false })
}.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color('#F0F0F0')
Scroll() {
Column() {
this.editFieldBuilder('餐食名称', this.meals[this.editIndex].name)
this.editFieldBuilder('餐食类型', this.meals[this.editIndex].mealType)
this.editFieldBuilder('卡路里', this.meals[this.editIndex].calories.toString())
this.editFieldBuilder('蛋白质(g)', this.meals[this.editIndex].protein.toString())
this.editFieldBuilder('碳水(g)', this.meals[this.editIndex].carbs.toString())
this.editFieldBuilder('脂肪(g)', this.meals[this.editIndex].fat.toString())
this.editFieldBuilder('纤维(g)', this.meals[this.editIndex].fiber.toString())
this.editFieldBuilder('日期', this.meals[this.editIndex].date)
this.editFieldBuilder('时间', this.meals[this.editIndex].time)
}.padding({ left: 20, right: 20 })
}.layoutWeight(1)
Row() {
Text('取消').fontSize(14).fontColor('#888888')
.backgroundColor('#F5F5F5').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.showEditModal = false })
Text('保存').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#FFB300').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.showEditModal = false })
}.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('90%').constraintSize({ maxHeight: '80%' }).backgroundColor('#FFFFFF').borderRadius(18)
.position({ x: '5%', y: '8%' }).shadow({ radius: 20, color: '#33000000', offsetY: 4 })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
editModal 的结构与 addModal 几乎完全一致,区别在于两点:标题改为"✏️ 编辑餐食";表单字段使用 editFieldBuilder 而非 formFieldBuilder,且每个字段的初始值取自 this.meals[this.editIndex] 的对应属性。
editIndex 在用户点击餐食卡片的编辑按钮时被设置为 meal.id - 1,因此 this.meals[this.editIndex] 能够精确获取到当前编辑的餐食数据。字段值通过 .toString() 方法将数字类型转换为字符串,以适配 TextInput 组件的 text 参数。
保存按钮的颜色使用了黄色 #FFB300 而非新增弹窗的橙色 #E65100,这是一个微妙但有意的设计——通过按钮颜色的细微差异,帮助用户在视觉上区分"新增"和"编辑"两种操作场景。表单字段比新增弹窗少了 Emoji、食谱和食材三项,因为编辑场景下通常不需要修改这些属性,体现了操作场景的差异化设计。
11.4 deleteModal 删除确认弹窗
@Builder deleteModal() {
Column() {
this.modalBg(() => { this.showDeleteModal = false })
Column() {
Text('🗑️').fontSize(40).margin({ top: 24 })
Text('确认删除?').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
.margin({ top: 12 })
Text('删除后不可恢复,确定要删除「' + this.meals[this.deleteIndex].name + '」吗?')
.fontSize(13).fontColor('#757575').textAlign(TextAlign.Center)
.margin({ top: 8, left: 24, right: 24 })
Row() {
Text('取消').fontSize(14).fontColor('#666666')
.backgroundColor('#F5F5F5').borderRadius(20)
.padding({ left: 32, right: 32, top: 10, bottom: 10 })
.onClick(() => { this.showDeleteModal = false })
Text('删除').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#C62828').borderRadius(20)
.padding({ left: 32, right: 32, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.showDeleteModal = false })
}.margin({ top: 24, bottom: 24 }).justifyContent(FlexAlign.Center)
}.width('80%').backgroundColor('#FFFFFF').borderRadius(18)
.alignItems(HorizontalAlign.Center)
.shadow({ radius: 20, color: '#33000000', offsetY: 4 })
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
.position({ x: 0, y: 0 }).zIndex(999)
}
deleteModal 是删除确认弹窗,与前两个弹窗在视觉风格上有明显差异——它更小、更居中,是一个标准的确认对话框(Confirmation Dialog)。
弹窗宽度为屏幕的 80%,内容垂直居中对齐。顶部是一个 40vp 的垃圾桶 Emoji,下方是"确认删除?"的标题(18vp 加粗)和描述文字。描述文字动态插入了餐食名称:'删除后不可恢复,确定要删除「' + this.meals[this.deleteIndex].name + '」吗?',这种在提示信息中包含具体对象名称的做法,能够帮助用户确认操作对象,避免误删。
按钮区域使用红色 #C62828 背景的"删除"按钮和灰色背景的"取消"按钮。红色作为危险操作的警示色,是 UI 设计中的通用规范——它与前面卡路里超量预警使用的红色一致,在应用中形成了统一的"警示=红色"颜色语义体系。按钮的 padding 比前两个弹窗更大(左右 32vp vs 24vp),使按钮更宽,因为删除确认是一个重要的决策点,更大的触摸目标有助于防止误触。
整个弹窗通过外层 Column 的 justifyContent(FlexAlign.Center) 实现垂直居中,与新增/编辑弹窗的顶部偏移定位(position y: 8%)形成对比——确认对话框通常居中显示以突出其重要性。
十二、表单字段构建器
12.1 formFieldBuilder 新增表单字段
@Builder formFieldBuilder(label: string, placeholder: string) {
Column() {
Text(label).fontSize(12).fontColor('#757575').margin({ bottom: 6 })
TextInput({ placeholder: placeholder })
.fontSize(14).height(40).borderRadius(10)
.backgroundColor('#F5F5F5')
}.width('100%').margin({ top: 12 }).alignItems(HorizontalAlign.Start)
}
formFieldBuilder 用于渲染新增弹窗中的表单字段,接收标签名和 placeholder 提示文字两个参数。每个字段由一个标签 Text(12vp 灰色)和一个 TextInput 输入框组成,输入框高度 40vp,圆角 10vp,背景浅灰 #F5F5F5。
TextInput 是 ArkTS 提供的文本输入组件,通过 placeholder 参数设置占位提示文字。当输入框为空时,placeholder 文字以浅色显示在输入框内,引导用户输入。这种"标签在外、输入框在内"的表单布局是移动端表单设计的经典模式,相比浮动标签或行内标签,它在小屏幕上具有更好的可读性和操作便利性。
整个字段容器使用 width('100%') 占满父容器宽度,margin({ top: 12 }) 在字段之间添加垂直间距。alignItems(HorizontalAlign.Start) 确保标签和输入框左对齐。
12.2 editFieldBuilder 编辑表单字段
@Builder editFieldBuilder(label: string, value: string) {
Column() {
Text(label).fontSize(12).fontColor('#757575').margin({ bottom: 6 })
TextInput({ text: value })
.fontSize(14).height(40).borderRadius(10)
.backgroundColor('#F5F5F5')
}.width('100%').margin({ top: 12 }).alignItems(HorizontalAlign.Start)
}
editFieldBuilder 与 formFieldBuilder 结构完全一致,区别仅在于 TextInput 的参数——使用 text 而非 placeholder。text 参数设置输入框的初始值,使得编辑弹窗在打开时就能显示当前餐食的已有数据,用户可以在原有数据基础上进行修改。这种"预填充"的交互模式是编辑场景的标准做法,显著提升了用户体验。
两个构建器虽然结构相似但保持独立,是因为它们的语义不同——一个是"新建空表单",一个是"编辑已有数据"。在未来的功能迭代中,它们可能需要不同的验证逻辑、默认值或字段类型(如数字输入框 vs 文本输入框),保持独立为后续扩展提供了灵活性。
十三、各模块关键技术点横向对比总结
下面通过表格对本应用四个 Tab 页及弹窗系统的关键技术点进行横向对比总结:
| 对比维度 | 餐食 Tab(Meals) | 周计划 Tab(Weekly) | 营养 Tab(Nutrition) | 我的 Tab(Mine) | 弹窗系统(Modals) |
|---|---|---|---|---|---|
| 核心功能 | 餐食列表展示、餐类筛选、增删改入口 | 一周餐食规划概览、蛋白质趋势图 | 卡路里/营养素可视化、评分展示 | 用户信息、身体数据、功能菜单入口 | 新增/编辑/删除餐食的模态交互 |
| 状态管理要点 | selectedMealType 筛选状态、showAddModal 触发新增 |
纯展示型,无独立状态变量 | 纯展示型,依赖预置数据数组 | 纯展示型,静态用户数据 | showAddModal/showEditModal/showDeleteModal 显隐控制 + editIndex/deleteIndex 索引定位 |
| 关键组件 | Scroll(横向筛选+纵向列表)、ForEach(筛选标签)、Stack(图标容器) |
Scroll(纵向滚动)、自定义进度条(Column 宽度百分比) |
Scroll、Stack(圆形评分徽章)、多层进度条 |
Scroll、Stack(圆形头像)、Row(统计栏) |
Column(全屏覆盖层)、TextInput(表单输入)、Divider(分隔线)、zIndex 层级控制 |
| 数据驱动方式 | 遍历 meals 数组 + mealTypeFilters 筛选 |
遍历 dailyCalories + weeklyProteins + 参数化调用 |
遍历 dailyCalories + macroTargets + 参数化调用 |
硬编码用户数据 + 参数化 menuItemBuilder |
读取 meals[editIndex] / meals[deleteIndex] 动态填充表单 |
| 设计模式 | 数据驱动渲染 + Builder 组合复用 | 参数化 Builder 模式(dayPlanBuilder 传参) |
数据驱动样式(item.color 直接映射颜色) |
列表项模板复用(menuItemBuilder 通用化) |
回调注入模式(modalBg(onClose) 函数参数) + 遮罩-内容分层模式 |
| 可视化形式 | 营养素汇总卡片 + 卡片列表 + 蛋白质迷你进度条 | 每日卡路里进度条 + 水平条形图(蛋白质趋势) | 水平条形图(卡路里/餐类分布)+ 进度条(营养素目标)+ 圆形评分徽章 | 统计数字栏 + 键值对列表行 | 表单输入字段 + 按钮组 |
| 颜色语义 | 橙色=主色/选中、浅橙=未选中、绿=完成 | 颜色按天变化暗示热量高低 | 红=超量警示、橙=正常、各营养素独立配色 | 橙=关键数据、绿=改善趋势、深红=会员标识 | 红=危险操作(删除)、橙=主操作(保存)、灰=次要操作(取消) |
| 交互特性 | 横向滚动筛选 + 点击编辑/删除 + 添加入口 | 纯展示滚动 | 纯展示滚动 | 菜单项箭头暗示可进入(未绑定事件) | 点击遮罩关闭 + 表单滚动 + 双按钮操作 |
| 复用性设计 | mealItemBuilder 复用 26 次 |
dayPlanBuilder 复用 7 次 + weeklyProteinBuilder 复用 7 次 |
dailyCalorieBuilder 复用 7 次 + macroTargetBuilder 复用 4 次 + mealTypeCalorieBuilder 复用 4 次 |
menuItemBuilder 复用 6 次 |
modalBg 复用 3 次 + formFieldBuilder/editFieldBuilder 各复用 9~12 次 |
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// 健身饮食计划 — 橙色 #E65100 | 黄色 #FFB300 | 背景 #FFF3E0
interface MealItem {
id: number
name: string
mealType: string
calories: number
protein: number
carbs: number
fat: number
fiber: number
date: string
time: string
isCompleted: boolean
emoji: string
recipe: string[]
ingredients: string[]
}
interface DailyCalorie {
day: string
calories: number
goal: number
}
interface MacroTarget {
name: string
current: number
target: number
unit: string
color: string
percentage: number
}
interface WeeklyProtein {
day: string
protein: number
}
interface TabConfig {
index: number
label: string
icon: string
}
interface MealTypeConfig {
type: string
icon: string
color: string
targetCalories: number
}
@Observed
class MealModel {
id: number
name: string
mealType: string
calories: number
protein: number
carbs: number
fat: number
fiber: number
date: string
time: string
isCompleted: boolean
emoji: string
recipe: string[]
ingredients: string[]
constructor(meal: MealItem) {
this.id = meal.id
this.name = meal.name
this.mealType = meal.mealType
this.calories = meal.calories
this.protein = meal.protein
this.carbs = meal.carbs
this.fat = meal.fat
this.fiber = meal.fiber
this.date = meal.date
this.time = meal.time
this.isCompleted = meal.isCompleted
this.emoji = meal.emoji
this.recipe = meal.recipe
this.ingredients = meal.ingredients
}
}
enum MealTab {
Meals,
Weekly,
Nutrition,
Mine
}
@Entry
@Component
struct MealPlannerApp {
@State currentTab: number = MealTab.Meals
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State editIndex: number = 0
@State deleteIndex: number = 0
@State selectedMealType: string = '全部'
@State dailyCalorieGoal: number = 2000
@State todayCalories: number = 1450
@State nutritionScore: number = 85
private meals: MealModel[] = [
new MealModel({ id: 1, name: '燕麦牛奶杯', mealType: '早餐', calories: 320, protein: 18, carbs: 45, fat: 8, fiber: 6, date: '2026-07-25', time: '07:30', isCompleted: true, emoji: '🥣', recipe: ['燕麦50g', '牛奶200ml', '蓝莓30g'], ingredients: ['燕麦', '牛奶', '蓝莓', '蜂蜜'] }),
new MealModel({ id: 2, name: '全麦三明治', mealType: '早餐', calories: 380, protein: 22, carbs: 48, fat: 12, fiber: 8, date: '2026-07-24', time: '08:00', isCompleted: true, emoji: '🥪', recipe: ['全麦面包', '鸡蛋', '生菜'], ingredients: ['全麦面包', '鸡蛋', '生菜', '番茄'] }),
new MealModel({ id: 3, name: '鸡胸肉沙拉', mealType: '午餐', calories: 450, protein: 35, carbs: 25, fat: 15, fiber: 10, date: '2026-07-25', time: '12:00', isCompleted: true, emoji: '🥗', recipe: ['鸡胸肉150g', '生菜', '番茄'], ingredients: ['鸡胸肉', '生菜', '番茄', '橄榄油'] }),
new MealModel({ id: 4, name: '牛肉藜麦饭', mealType: '午餐', calories: 520, protein: 38, carbs: 55, fat: 14, fiber: 8, date: '2026-07-24', time: '12:30', isCompleted: true, emoji: '🍚', recipe: ['牛肉120g', '藜麦80g'], ingredients: ['牛肉', '藜麦', '西兰花', '胡萝卜'] }),
new MealModel({ id: 5, name: '三文鱼寿司', mealType: '午餐', calories: 480, protein: 28, carbs: 60, fat: 12, fiber: 4, date: '2026-07-23', time: '12:00', isCompleted: true, emoji: '🍣', recipe: ['三文鱼', '米饭', '海苔'], ingredients: ['三文鱼', '米饭', '海苔', '黄瓜'] }),
new MealModel({ id: 6, name: '蛋白奶昔', mealType: '加餐', calories: 180, protein: 25, carbs: 15, fat: 3, fiber: 2, date: '2026-07-25', time: '15:30', isCompleted: true, emoji: '🥤', recipe: ['蛋白粉', '香蕉', '牛奶'], ingredients: ['蛋白粉', '香蕉', '牛奶'] }),
new MealModel({ id: 7, name: '坚果混合', mealType: '加餐', calories: 220, protein: 8, carbs: 12, fat: 18, fiber: 4, date: '2026-07-24', time: '16:00', isCompleted: true, emoji: '🥜', recipe: ['杏仁20g', '核桃15g'], ingredients: ['杏仁', '核桃', '腰果'] }),
new MealModel({ id: 8, name: '希腊酸奶', mealType: '加餐', calories: 150, protein: 15, carbs: 12, fat: 5, fiber: 0, date: '2026-07-23', time: '15:30', isCompleted: true, emoji: '🍶', recipe: ['酸奶150g', '蜂蜜'], ingredients: ['希腊酸奶', '蜂蜜', '蓝莓'] }),
new MealModel({ id: 9, name: '香煎鸡胸肉', mealType: '晚餐', calories: 380, protein: 42, carbs: 8, fat: 18, fiber: 2, date: '2026-07-25', time: '18:30', isCompleted: false, emoji: '🍗', recipe: ['鸡胸肉200g', '黑胡椒'], ingredients: ['鸡胸肉', '黑胡椒', '橄榄油', '蒜'] }),
new MealModel({ id: 10, name: '清蒸鲈鱼', mealType: '晚餐', calories: 320, protein: 36, carbs: 5, fat: 15, fiber: 1, date: '2026-07-24', time: '19:00', isCompleted: true, emoji: '🐟', recipe: ['鲈鱼一条', '姜丝'], ingredients: ['鲈鱼', '姜', '葱', '蒸鱼豉油'] }),
new MealModel({ id: 11, name: '番茄意面', mealType: '晚餐', calories: 420, protein: 14, carbs: 68, fat: 10, fiber: 6, date: '2026-07-23', time: '18:30', isCompleted: true, emoji: '🍝', recipe: ['意面100g', '番茄酱'], ingredients: ['意面', '番茄', '牛肉末', '洋葱'] }),
new MealModel({ id: 12, name: '牛油果吐司', mealType: '早餐', calories: 350, protein: 12, carbs: 35, fat: 18, fiber: 8, date: '2026-07-23', time: '08:00', isCompleted: true, emoji: '🥑', recipe: ['全麦吐司', '牛油果'], ingredients: ['全麦吐司', '牛油果', '鸡蛋', '盐'] }),
new MealModel({ id: 13, name: '蔬菜蛋饼', mealType: '早餐', calories: 280, protein: 18, carbs: 22, fat: 12, fiber: 4, date: '2026-07-22', time: '07:30', isCompleted: true, emoji: '🥞', recipe: ['鸡蛋2个', '蔬菜'], ingredients: ['鸡蛋', '菠菜', '胡萝卜', '面粉'] }),
new MealModel({ id: 14, name: '豆腐炒饭', mealType: '午餐', calories: 400, protein: 20, carbs: 52, fat: 12, fiber: 6, date: '2026-07-22', time: '12:00', isCompleted: true, emoji: '🍱', recipe: ['豆腐100g', '米饭'], ingredients: ['豆腐', '米饭', '豌豆', '玉米'] }),
new MealModel({ id: 15, name: '虾仁西兰花', mealType: '晚餐', calories: 280, protein: 30, carbs: 10, fat: 12, fiber: 5, date: '2026-07-22', time: '18:30', isCompleted: true, emoji: '🦐', recipe: ['虾仁150g', '西兰花'], ingredients: ['虾仁', '西兰花', '蒜', '盐'] }),
new MealModel({ id: 16, name: '香蕉花生酱', mealType: '加餐', calories: 260, protein: 8, carbs: 35, fat: 12, fiber: 4, date: '2026-07-22', time: '16:00', isCompleted: true, emoji: '🍌', recipe: ['香蕉', '花生酱'], ingredients: ['香蕉', '花生酱'] }),
new MealModel({ id: 17, name: '鸡蛋牛油果沙拉', mealType: '午餐', calories: 380, protein: 24, carbs: 18, fat: 22, fiber: 8, date: '2026-07-21', time: '12:30', isCompleted: true, emoji: '🥗', recipe: ['鸡蛋2个', '牛油果'], ingredients: ['鸡蛋', '牛油果', '生菜', '番茄'] }),
new MealModel({ id: 18, name: '红薯鸡胸肉', mealType: '晚餐', calories: 420, protein: 35, carbs: 45, fat: 10, fiber: 8, date: '2026-07-21', time: '19:00', isCompleted: true, emoji: '🍠', recipe: ['鸡胸肉', '红薯'], ingredients: ['鸡胸肉', '红薯', '橄榄油'] }),
new MealModel({ id: 19, name: '蓝莓松饼', mealType: '早餐', calories: 340, protein: 10, carbs: 52, fat: 10, fiber: 4, date: '2026-07-21', time: '08:00', isCompleted: true, emoji: '🫐', recipe: ['面粉', '蓝莓'], ingredients: ['面粉', '蓝莓', '牛奶', '鸡蛋'] }),
new MealModel({ id: 20, name: '金枪鱼三明治', mealType: '午餐', calories: 410, protein: 28, carbs: 40, fat: 14, fiber: 6, date: '2026-07-20', time: '12:00', isCompleted: true, emoji: '🥪', recipe: ['金枪鱼罐头', '面包'], ingredients: ['金枪鱼', '全麦面包', '生菜', '蛋黄酱'] }),
new MealModel({ id: 21, name: '蘑菇汤', mealType: '晚餐', calories: 220, protein: 10, carbs: 18, fat: 10, fiber: 4, date: '2026-07-20', time: '18:30', isCompleted: true, emoji: '🍄', recipe: ['蘑菇', '奶油'], ingredients: ['蘑菇', '奶油', '洋葱', '面粉'] }),
new MealModel({ id: 22, name: '苹果片配杏仁酱', mealType: '加餐', calories: 200, protein: 5, carbs: 28, fat: 10, fiber: 6, date: '2026-07-20', time: '15:30', isCompleted: true, emoji: '🍎', recipe: ['苹果', '杏仁酱'], ingredients: ['苹果', '杏仁酱'] }),
new MealModel({ id: 23, name: '小米南瓜粥', mealType: '早餐', calories: 260, protein: 8, carbs: 52, fat: 3, fiber: 4, date: '2026-07-19', time: '07:30', isCompleted: true, emoji: '🎃', recipe: ['小米', '南瓜'], ingredients: ['小米', '南瓜', '枸杞'] }),
new MealModel({ id: 24, name: '烤鸡腿配蔬菜', mealType: '晚餐', calories: 480, protein: 40, carbs: 20, fat: 22, fiber: 6, date: '2026-07-19', time: '18:30', isCompleted: true, emoji: '🍗', recipe: ['鸡腿', '蔬菜'], ingredients: ['鸡腿', '土豆', '胡萝卜', '迷迭香'] }),
new MealModel({ id: 25, name: '蔬菜豆腐汤', mealType: '午餐', calories: 260, protein: 18, carbs: 22, fat: 8, fiber: 6, date: '2026-07-19', time: '12:00', isCompleted: true, emoji: '🍲', recipe: ['豆腐', '蔬菜'], ingredients: ['豆腐', '白菜', '蘑菇', '姜'] }),
new MealModel({ id: 26, name: '黑巧克力配咖啡', mealType: '加餐', calories: 160, protein: 3, carbs: 14, fat: 10, fiber: 3, date: '2026-07-19', time: '16:00', isCompleted: true, emoji: '🍫', recipe: ['黑巧克力', '咖啡'], ingredients: ['黑巧克力', '咖啡豆'] })
]
private dailyCalories: DailyCalorie[] = [
{ day: '周一', calories: 1850, goal: 2000 },
{ day: '周二', calories: 1950, goal: 2000 },
{ day: '周三', calories: 1720, goal: 2000 },
{ day: '周四', calories: 2100, goal: 2000 },
{ day: '周五', calories: 1650, goal: 2000 },
{ day: '周六', calories: 2200, goal: 2000 },
{ day: '周日', calories: 1450, goal: 2000 }
]
private macroTargets: MacroTarget[] = [
{ name: '蛋白质', current: 95, target: 120, unit: 'g', color: '#E65100', percentage: 79 },
{ name: '碳水', current: 180, target: 250, unit: 'g', color: '#FFB300', percentage: 72 },
{ name: '脂肪', current: 48, target: 65, unit: 'g', color: '#FF8F00', percentage: 74 },
{ name: '纤维', current: 22, target: 30, unit: 'g', color: '#FF6F00', percentage: 73 }
]
private weeklyProteins: WeeklyProtein[] = [
{ day: '周一', protein: 110 },
{ day: '周二', protein: 125 },
{ day: '周三', protein: 95 },
{ day: '周四', protein: 130 },
{ day: '周五', protein: 88 },
{ day: '周六', protein: 135 },
{ day: '周日', protein: 95 }
]
private tabConfigs: TabConfig[] = [
{ index: MealTab.Meals, label: '餐食', icon: '🍽️' },
{ index: MealTab.Weekly, label: '周计划', icon: '📅' },
{ index: MealTab.Nutrition, label: '营养', icon: '📊' },
{ index: MealTab.Mine, label: '我的', icon: '👤' }
]
private mealTypeConfigs: MealTypeConfig[] = [
{ type: '全部', icon: '🍽️', color: '#E65100', targetCalories: 2000 },
{ type: '早餐', icon: '🌅', color: '#FFB300', targetCalories: 500 },
{ type: '午餐', icon: '☀️', color: '#FF8F00', targetCalories: 700 },
{ type: '晚餐', icon: '🌙', color: '#E65100', targetCalories: 600 },
{ type: '加餐', icon: '🥜', color: '#FF6F00', targetCalories: 200 }
]
private mealTypeFilters: string[] = ['全部', '早餐', '午餐', '晚餐', '加餐']
build() {
Column() {
Column() {
// 顶部标题栏
Row() {
Text('🍽️ 健身饮食').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Row().layoutWeight(1)
Text('📅').fontSize(20).fontColor('#FFFFFF').margin({ right: 16 })
Text('⚙️').fontSize(20).fontColor('#FFFFFF')
}
.width('100%').height(56).padding({ left: 20, right: 20 })
.backgroundColor('#E65100').alignItems(VerticalAlign.Center)
// 今日卡路里进度
this.calorieProgressBuilder()
// 内容区域
Column() {
if (this.currentTab === MealTab.Meals) {
this.mealsTab()
} else if (this.currentTab === MealTab.Weekly) {
this.weeklyTab()
} else if (this.currentTab === MealTab.Nutrition) {
this.nutritionTab()
} else {
this.mineTab()
}
}.layoutWeight(1).width('100%')
// 底部导航栏
this.bottomTabBar()
}
.width('100%').height('100%').backgroundColor('#FFF3E0')
// 弹窗层
if (this.showAddModal) {
this.addModal()
}
if (this.showEditModal) {
this.editModal()
}
if (this.showDeleteModal) {
this.deleteModal()
}
}
}
// 今日卡路里进度
@Builder calorieProgressBuilder() {
Column() {
Row() {
Text('🔥 今日卡路里').fontSize(13).fontColor('#FFFFFF')
Row().layoutWeight(1)
Text(this.nutritionScore + '分').fontSize(13).fontColor('#FFB300').fontWeight(FontWeight.Bold)
}.width('100%')
Row() {
Text(this.todayCalories + '').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(' / ' + this.dailyCalorieGoal + ' kcal').fontSize(14).fontColor('#FFE0B2')
.margin({ left: 4, bottom: 4 })
}.margin({ top: 6 }).alignItems(VerticalAlign.Bottom)
// 进度条
Row() {
Column()
.width(this.todayCalories / this.dailyCalorieGoal * 100 + '%')
.height(6)
.backgroundColor('#FFB300')
.borderRadius(3)
}.width('100%').height(6).backgroundColor('#BF360C').borderRadius(3).margin({ top: 8 })
Row() {
Text('剩余 ' + (this.dailyCalorieGoal - this.todayCalories) + ' kcal').fontSize(11).fontColor('#FFE0B2')
Row().layoutWeight(1)
Text('🔥 营养评分').fontSize(11).fontColor('#FFE0B2')
}.width('100%').margin({ top: 6 })
}
.width('100%').padding(16).backgroundColor('#E65100').borderRadius(0)
}
// 底部导航栏
@Builder bottomTabBar() {
Row() {
ForEach(this.tabConfigs, (item: TabConfig) => {
Column() {
Text(item.icon).fontSize(22)
Text(item.label).fontSize(10).fontColor(this.currentTab === item.index ? '#E65100' : '#9E9E9E')
.margin({ top: 2 })
}.alignItems(HorizontalAlign.Center).layoutWeight(1)
.onClick(() => { this.currentTab = item.index })
}, (item: TabConfig) => item.index.toString())
}
.width('100%').height(56).backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.Center)
.shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
}
// ===== 餐食 Tab =====
@Builder mealsTab() {
Column() {
// 餐类选择
Scroll() {
Row() {
ForEach(this.mealTypeFilters, (mt: string) => {
Text(mt)
.fontSize(12).fontColor(this.selectedMealType === mt ? '#FFFFFF' : '#E65100')
.backgroundColor(this.selectedMealType === mt ? '#E65100' : '#FFE0B2')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(16).margin({ right: 8 })
.onClick(() => { this.selectedMealType = mt })
}, (mt: string) => mt)
}.padding({ left: 16, right: 16 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(40)
// 营养素汇总卡片
Row() {
Column() {
Text('蛋白质').fontSize(10).fontColor('#FFE0B2')
Text('95g').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('碳水').fontSize(10).fontColor('#FFE0B2')
Text('180g').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('脂肪').fontSize(10).fontColor('#FFE0B2')
Text('48g').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('纤维').fontSize(10).fontColor('#FFE0B2')
Text('22g').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding(14).backgroundColor('#FF8F00').borderRadius(14).margin({ top: 4, left: 16, right: 16 })
// 餐食列表
Scroll() {
Column() {
this.mealItemBuilder(this.meals[0])
this.mealItemBuilder(this.meals[1])
this.mealItemBuilder(this.meals[2])
this.mealItemBuilder(this.meals[3])
this.mealItemBuilder(this.meals[4])
this.mealItemBuilder(this.meals[5])
this.mealItemBuilder(this.meals[6])
this.mealItemBuilder(this.meals[7])
this.mealItemBuilder(this.meals[8])
this.mealItemBuilder(this.meals[9])
this.mealItemBuilder(this.meals[10])
this.mealItemBuilder(this.meals[11])
this.mealItemBuilder(this.meals[12])
this.mealItemBuilder(this.meals[13])
this.mealItemBuilder(this.meals[14])
this.mealItemBuilder(this.meals[15])
this.mealItemBuilder(this.meals[16])
this.mealItemBuilder(this.meals[17])
this.mealItemBuilder(this.meals[18])
this.mealItemBuilder(this.meals[19])
this.mealItemBuilder(this.meals[20])
this.mealItemBuilder(this.meals[21])
this.mealItemBuilder(this.meals[22])
this.mealItemBuilder(this.meals[23])
this.mealItemBuilder(this.meals[24])
this.mealItemBuilder(this.meals[25])
Row() {
Text('➕ 添加新餐食').fontSize(14).fontColor('#E65100').fontWeight(FontWeight.Bold)
}.width('100%').height(48).justifyContent(FlexAlign.Center)
.backgroundColor('#FFE0B2').borderRadius(12).margin({ top: 12 })
.onClick(() => { this.showAddModal = true })
}.padding({ left: 16, right: 16, top: 8, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}.width('100%').height('100%')
}
@Builder mealItemBuilder(meal: MealModel) {
Row() {
// 食物Emoji
Stack() {
Text(meal.emoji).fontSize(28)
}.width(52).height(52).backgroundColor('#FFE0B2').borderRadius(10)
.align(Alignment.Center)
Column() {
Row() {
Text(meal.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121')
if (meal.isCompleted) {
Text('✓').fontSize(12).fontColor('#33691E').margin({ left: 6 })
}
}.width('100%').alignItems(VerticalAlign.Center)
Row() {
Text(meal.mealType).fontSize(10).fontColor('#E65100')
.backgroundColor('#FFF3E0').padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
Text('⏰ ' + meal.time).fontSize(10).fontColor('#9E9E9E').margin({ left: 8 })
Text('🔥 ' + meal.calories + 'kcal').fontSize(10).fontColor('#FF6F00').margin({ left: 8 })
}.width('100%').margin({ top: 6 }).alignItems(VerticalAlign.Center)
// 营养素进度条
Row() {
Column()
.width(meal.protein / 50 * 100 + '%')
.height(4).backgroundColor('#E65100').borderRadius(2)
}.width('30%').height(4).backgroundColor('#F5F5F5').borderRadius(2).margin({ top: 6 })
Row() {
Text('🥩 ' + meal.protein + 'g').fontSize(10).fontColor('#757575')
Text('🍞 ' + meal.carbs + 'g').fontSize(10).fontColor('#757575').margin({ left: 8 })
Text('🧈 ' + meal.fat + 'g').fontSize(10).fontColor('#757575').margin({ left: 8 })
Text('🌿 ' + meal.fiber + 'g').fontSize(10).fontColor('#757575').margin({ left: 8 })
}.width('100%').margin({ top: 4 }).alignItems(VerticalAlign.Center)
}.layoutWeight(1).margin({ left: 12 }).alignItems(HorizontalAlign.Start)
Column() {
Text('✏️').fontSize(14).margin({ bottom: 10 })
.onClick(() => {
this.editIndex = meal.id - 1
this.showEditModal = true
})
Text('🗑️').fontSize(14)
.onClick(() => {
this.deleteIndex = meal.id - 1
this.showDeleteModal = true
})
}.alignItems(HorizontalAlign.Center)
}
.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12).margin({ top: 8 })
.alignItems(VerticalAlign.Center)
}
// ===== 周计划 Tab =====
@Builder weeklyTab() {
Scroll() {
Column() {
// 周计划标题
Row() {
Text('📅 本周餐食计划').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ bottom: 8 })
// 每日餐食卡片
this.dayPlanBuilder('周一', 1850, 4, '#E65100')
this.dayPlanBuilder('周二', 1950, 4, '#FF8F00')
this.dayPlanBuilder('周三', 1720, 5, '#FFB300')
this.dayPlanBuilder('周四', 2100, 4, '#E65100')
this.dayPlanBuilder('周五', 1650, 3, '#FF8F00')
this.dayPlanBuilder('周六', 2200, 5, '#FFB300')
this.dayPlanBuilder('周日', 1450, 4, '#E65100')
// 每周蛋白质趋势
Row() {
Text('💪 每周蛋白质趋势').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
this.weeklyProteinBuilder(this.weeklyProteins[0])
this.weeklyProteinBuilder(this.weeklyProteins[1])
this.weeklyProteinBuilder(this.weeklyProteins[2])
this.weeklyProteinBuilder(this.weeklyProteins[3])
this.weeklyProteinBuilder(this.weeklyProteins[4])
this.weeklyProteinBuilder(this.weeklyProteins[5])
this.weeklyProteinBuilder(this.weeklyProteins[6])
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
}.padding({ left: 16, right: 16, top: 12, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}
@Builder dayPlanBuilder(day: string, calories: number, mealCount: number, color: string) {
Row() {
Column() {
Text(day).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(calories + ' kcal').fontSize(11).fontColor('#FFE0B2').margin({ top: 2 })
}.width(60).alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text('🌅 早餐').fontSize(11).fontColor('#757575')
Text('☀️ 午餐').fontSize(11).fontColor('#757575').margin({ left: 12 })
Text('🌙 晚餐').fontSize(11).fontColor('#757575').margin({ left: 12 })
Text('🥜 加餐').fontSize(11).fontColor('#757575').margin({ left: 12 })
}.width('100%')
Row() {
Text(mealCount + ' 餐已规划').fontSize(11).fontColor('#E65100').fontWeight(FontWeight.Bold)
}.width('100%').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 12 }).alignItems(HorizontalAlign.Start)
// 卡路里进度条
Column() {
Row() {
Column()
.width(calories / 2500 * 100 + '%')
.height(6)
.backgroundColor(color)
.borderRadius(3)
}.width(60).height(6).backgroundColor('#F5F5F5').borderRadius(3)
Text(calories / 2000 * 100 + '%').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}.alignItems(HorizontalAlign.Center)
}
.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(12).margin({ top: 8 })
.alignItems(VerticalAlign.Center)
}
@Builder weeklyProteinBuilder(item: WeeklyProtein) {
Row() {
Text(item.day).fontSize(12).fontColor('#424242').width(40)
Row() {
Column()
.width(item.protein / 150 * 100 + '%')
.height(14)
.backgroundColor('#E65100')
.borderRadius(7)
}.layoutWeight(1).height(14).backgroundColor('#F5F5F5').borderRadius(7)
Text(item.protein + 'g').fontSize(11).fontColor('#757575').width(40).textAlign(TextAlign.Center)
}.width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Center)
}
// ===== 营养 Tab =====
@Builder nutritionTab() {
Scroll() {
Column() {
// 每日卡路里柱状图
Row() {
Text('📊 每日卡路里摄入').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ bottom: 8 })
Column() {
this.dailyCalorieBuilder(this.dailyCalories[0])
this.dailyCalorieBuilder(this.dailyCalories[1])
this.dailyCalorieBuilder(this.dailyCalories[2])
this.dailyCalorieBuilder(this.dailyCalories[3])
this.dailyCalorieBuilder(this.dailyCalories[4])
this.dailyCalorieBuilder(this.dailyCalories[5])
this.dailyCalorieBuilder(this.dailyCalories[6])
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
// 宏量营养素分布
Row() {
Text('🥗 宏量营养素目标').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
this.macroTargetBuilder(this.macroTargets[0])
this.macroTargetBuilder(this.macroTargets[1])
this.macroTargetBuilder(this.macroTargets[2])
this.macroTargetBuilder(this.macroTargets[3])
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
// 营养评分卡
Row() {
Text('🏆 营养评分').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
Row() {
Stack() {
Text('85').fontSize(32).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}.width(80).height(80).backgroundColor('#E65100').borderRadius(40)
.border({ width: 4, color: '#FFB300' }).align(Alignment.Center)
Column() {
Text('优秀!').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
Text('你的饮食营养均衡').fontSize(12).fontColor('#757575').margin({ top: 4 })
Row() {
Text('建议增加蛋白质摄入').fontSize(11).fontColor('#E65100')
}.margin({ top: 6 })
}.layoutWeight(1).margin({ left: 16 }).alignItems(HorizontalAlign.Start)
}.width('100%').alignItems(VerticalAlign.Center)
}.width('100%').padding(20).backgroundColor('#FFFFFF').borderRadius(14)
// 餐类卡路里分布
Row() {
Text('🍽️ 餐类卡路里分布').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
this.mealTypeCalorieBuilder('早餐', 500, '#FFB300')
this.mealTypeCalorieBuilder('午餐', 700, '#FF8F00')
this.mealTypeCalorieBuilder('晚餐', 600, '#E65100')
this.mealTypeCalorieBuilder('加餐', 200, '#FF6F00')
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14)
}.padding({ left: 16, right: 16, top: 12, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}
@Builder dailyCalorieBuilder(item: DailyCalorie) {
Row() {
Text(item.day).fontSize(12).fontColor('#424242').width(40)
Column() {
Row() {
Column()
.width(item.calories / 2500 * 100 + '%')
.height(16)
.backgroundColor(item.calories > item.goal ? '#C62828' : '#E65100')
.borderRadius(8)
}.width('100%').height(16).backgroundColor('#F5F5F5').borderRadius(8)
}.layoutWeight(1)
Text(item.calories + '').fontSize(11).fontColor(item.calories > item.goal ? '#C62828' : '#757575').width(50).textAlign(TextAlign.Center)
}.width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Center)
}
@Builder macroTargetBuilder(item: MacroTarget) {
Column() {
Row() {
Text(item.name).fontSize(13).fontColor('#424242').fontWeight(FontWeight.Bold)
Row().layoutWeight(1)
Text(item.current + '/' + item.target + item.unit).fontSize(12).fontColor('#757575')
}.width('100%')
Row() {
Column()
.width(item.percentage + '%')
.height(10)
.backgroundColor(item.color)
.borderRadius(5)
}.width('100%').height(10).backgroundColor('#F5F5F5').borderRadius(5).margin({ top: 6 })
Text(item.percentage + '% 目标完成').fontSize(10).fontColor('#9E9E9E').margin({ top: 4 })
}.width('100%').margin({ top: 12 })
}
@Builder mealTypeCalorieBuilder(type: string, calories: number, color: string) {
Row() {
Text(type).fontSize(12).fontColor('#424242').width(50)
Row() {
Column()
.width(calories / 700 * 100 + '%')
.height(12)
.backgroundColor(color)
.borderRadius(6)
}.layoutWeight(1).height(12).backgroundColor('#F5F5F5').borderRadius(6)
Text(calories + 'kcal').fontSize(11).fontColor('#757575').width(56).textAlign(TextAlign.Center)
}.width('100%').margin({ top: 8 }).alignItems(VerticalAlign.Center)
}
// ===== 我的 Tab =====
@Builder mineTab() {
Scroll() {
Column() {
// 用户信息卡片
Row() {
Stack() {
Text('💪').fontSize(40)
}.width(80).height(80).backgroundColor('#E65100').borderRadius(40).align(Alignment.Center)
Column() {
Text('健身达人').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('已坚持 128 天').fontSize(12).fontColor('#FFE0B2').margin({ top: 4 })
Row() {
Text('PRO会员').fontSize(10).fontColor('#FFB300')
.backgroundColor('#BF360C').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
}.margin({ top: 6 })
}.layoutWeight(1).margin({ left: 16 }).alignItems(HorizontalAlign.Start)
}.width('100%').padding(20).backgroundColor('#E65100').borderRadius(16)
// 统计数据
Row() {
Column() {
Text('26').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E65100')
Text('餐食记录').fontSize(11).fontColor('#757575').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('128').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E65100')
Text('坚持天数').fontSize(11).fontColor('#757575').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('85').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFB300')
Text('营养评分').fontSize(11).fontColor('#757575').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('1,820').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFB300')
Text('日均卡路里').fontSize(11).fontColor('#757575').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 12 })
// 身体数据
Row() {
Text('📏 身体数据').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}.width('100%').margin({ top: 20, bottom: 8 })
Column() {
Row() {
Text('体重').fontSize(13).fontColor('#424242')
Row().layoutWeight(1)
Text('68.5 kg').fontSize(13).fontColor('#E65100').fontWeight(FontWeight.Bold)
Text(' ↓ 2.5kg').fontSize(11).fontColor('#33691E').margin({ left: 8 })
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
Row() {
Text('身高').fontSize(13).fontColor('#424242')
Row().layoutWeight(1)
Text('175 cm').fontSize(13).fontColor('#E65100').fontWeight(FontWeight.Bold)
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
Row() {
Text('BMI').fontSize(13).fontColor('#424242')
Row().layoutWeight(1)
Text('22.4').fontSize(13).fontColor('#E65100').fontWeight(FontWeight.Bold)
Text(' 正常').fontSize(11).fontColor('#33691E').margin({ left: 8 })
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
Row() {
Text('体脂率').fontSize(13).fontColor('#424242')
Row().layoutWeight(1)
Text('15.2%').fontSize(13).fontColor('#E65100').fontWeight(FontWeight.Bold)
Text(' ↓ 3.8%').fontSize(11).fontColor('#33691E').margin({ left: 8 })
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
}.width('100%').backgroundColor('#FFFFFF').borderRadius(14)
// 功能列表
Column() {
this.menuItemBuilder('🎯', '目标设置', '卡路里·营养素目标')
this.menuItemBuilder('📋', '饮食历史', '查看历史记录')
this.menuItemBuilder('💧', '饮水记录', '今日 1.5L / 2L')
this.menuItemBuilder('🏃', '运动记录', '今日消耗 320kcal')
this.menuItemBuilder('🔔', '餐食提醒', '定时提醒用餐')
this.menuItemBuilder('ℹ️', '关于', '版本 v3.1.0')
}.width('100%').backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 12 })
}.padding({ left: 16, right: 16, top: 16, bottom: 16 })
}.layoutWeight(1).scrollBar(BarState.Off)
}
@Builder menuItemBuilder(icon: string, title: string, desc: string) {
Row() {
Text(icon).fontSize(20).width(32)
Text(title).fontSize(14).fontColor('#212121').layoutWeight(1)
Text(desc).fontSize(12).fontColor('#9E9E9E')
Text('›').fontSize(18).fontColor('#BDBDBD').margin({ left: 8 })
}.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
.alignItems(VerticalAlign.Center)
}
// ===== 新增弹窗 =====
@Builder modalBg(onClose: () => void) {
Column().width('100%').height('100%')
.backgroundColor('rgba(0,0,0,0.5)').onClick(onClose)
}
@Builder addModal() {
Column() {
this.modalBg(() => { this.showAddModal = false })
Column() {
Row() {
Text('➕ 新增餐食').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
Row().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#999999')
.onClick(() => { this.showAddModal = false })
}.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color('#F0F0F0')
Scroll() {
Column() {
this.formFieldBuilder('餐食名称', '请输入餐食名称')
this.formFieldBuilder('餐食类型', '早餐/午餐/晚餐/加餐')
this.formFieldBuilder('卡路里(kcal)', '请输入数字')
this.formFieldBuilder('蛋白质(g)', '请输入数字')
this.formFieldBuilder('碳水(g)', '请输入数字')
this.formFieldBuilder('脂肪(g)', '请输入数字')
this.formFieldBuilder('纤维(g)', '请输入数字')
this.formFieldBuilder('日期', '如 2026-07-25')
this.formFieldBuilder('时间', '如 12:00')
this.formFieldBuilder('Emoji', '如 🍎')
this.formFieldBuilder('食谱', '用逗号分隔')
this.formFieldBuilder('食材', '用逗号分隔')
}.padding({ left: 20, right: 20 })
}.layoutWeight(1)
Row() {
Text('取消').fontSize(14).fontColor('#888888')
.backgroundColor('#F5F5F5').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.showAddModal = false })
Text('保存').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#E65100').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.showAddModal = false })
}.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('90%').constraintSize({ maxHeight: '80%' }).backgroundColor('#FFFFFF').borderRadius(18)
.position({ x: '5%', y: '8%' }).shadow({ radius: 20, color: '#33000000', offsetY: 4 })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
// ===== 编辑弹窗 =====
@Builder editModal() {
Column() {
this.modalBg(() => { this.showEditModal = false })
Column() {
Row() {
Text('✏️ 编辑餐食').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
Row().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#999999')
.onClick(() => { this.showEditModal = false })
}.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color('#F0F0F0')
Scroll() {
Column() {
this.editFieldBuilder('餐食名称', this.meals[this.editIndex].name)
this.editFieldBuilder('餐食类型', this.meals[this.editIndex].mealType)
this.editFieldBuilder('卡路里', this.meals[this.editIndex].calories.toString())
this.editFieldBuilder('蛋白质(g)', this.meals[this.editIndex].protein.toString())
this.editFieldBuilder('碳水(g)', this.meals[this.editIndex].carbs.toString())
this.editFieldBuilder('脂肪(g)', this.meals[this.editIndex].fat.toString())
this.editFieldBuilder('纤维(g)', this.meals[this.editIndex].fiber.toString())
this.editFieldBuilder('日期', this.meals[this.editIndex].date)
this.editFieldBuilder('时间', this.meals[this.editIndex].time)
}.padding({ left: 20, right: 20 })
}.layoutWeight(1)
Row() {
Text('取消').fontSize(14).fontColor('#888888')
.backgroundColor('#F5F5F5').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.showEditModal = false })
Text('保存').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#FFB300').borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.showEditModal = false })
}.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
}
.width('90%').constraintSize({ maxHeight: '80%' }).backgroundColor('#FFFFFF').borderRadius(18)
.position({ x: '5%', y: '8%' }).shadow({ radius: 20, color: '#33000000', offsetY: 4 })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
// ===== 删除确认弹窗 =====
@Builder deleteModal() {
Column() {
this.modalBg(() => { this.showDeleteModal = false })
Column() {
Text('🗑️').fontSize(40).margin({ top: 24 })
Text('确认删除?').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
.margin({ top: 12 })
Text('删除后不可恢复,确定要删除「' + this.meals[this.deleteIndex].name + '」吗?')
.fontSize(13).fontColor('#757575').textAlign(TextAlign.Center)
.margin({ top: 8, left: 24, right: 24 })
Row() {
Text('取消').fontSize(14).fontColor('#666666')
.backgroundColor('#F5F5F5').borderRadius(20)
.padding({ left: 32, right: 32, top: 10, bottom: 10 })
.onClick(() => { this.showDeleteModal = false })
Text('删除').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#C62828').borderRadius(20)
.padding({ left: 32, right: 32, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.showDeleteModal = false })
}.margin({ top: 24, bottom: 24 }).justifyContent(FlexAlign.Center)
}.width('80%').backgroundColor('#FFFFFF').borderRadius(18)
.alignItems(HorizontalAlign.Center)
.shadow({ radius: 20, color: '#33000000', offsetY: 4 })
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
.position({ x: 0, y: 0 }).zIndex(999)
}
@Builder formFieldBuilder(label: string, placeholder: string) {
Column() {
Text(label).fontSize(12).fontColor('#757575').margin({ bottom: 6 })
TextInput({ placeholder: placeholder })
.fontSize(14).height(40).borderRadius(10)
.backgroundColor('#F5F5F5')
}.width('100%').margin({ top: 12 }).alignItems(HorizontalAlign.Start)
}
@Builder editFieldBuilder(label: string, value: string) {
Column() {
Text(label).fontSize(12).fontColor('#757575').margin({ bottom: 6 })
TextInput({ text: value })
.fontSize(14).height(40).borderRadius(10)
.backgroundColor('#F5F5F5')
}.width('100%').margin({ top: 12 }).alignItems(HorizontalAlign.Start)
}
}
总结
通过对本应用源码的逐段深度解析,我们可以清晰地看到 ArkTS 声明式开发范式在 HarmonyOS 应用构建中的核心优势。整个应用以一个 @Entry 入口组件为根,通过 @State 状态变量集中管理所有交互状态(Tab 切换、弹窗显隐、筛选条件、数据索引),以 @Builder 构建器函数将复杂的 UI 拆分为可复用的组件片段,以 @Observed 类实现领域对象的可观测性。四个 Tab 页各司其职——餐食页负责核心的列表展示与 CRUD 操作入口,周计划页提供时间维度的规划概览,营养页通过多种条形图和进度条实现数据可视化,我的页聚合用户信息与功能入口。弹窗系统通过遮罩分层、回调注入和表单构建器的组合,实现了完整的增删改交互闭环。

在视觉设计层面,应用以橙色系为主色调,通过颜色的深浅变化和语义编码(红=警示、绿=改善、黄=高亮)构建了统一而富有层次的视觉体系。在架构层面,数据层(接口定义+模拟数据)、状态层(@State变量)和表现层(@Builder函数)三层分离,各层之间通过数据绑定和事件回调松耦合连接,为后续的功能扩展(如接入真实数据源、添加表单验证、实现数据持久化)预留了清晰的演进路径。这种"数据驱动UI、组件化复用、状态集中管理"的设计思想,正是 ArkTS 声明式开发范式的精髓所在。
更多推荐



所有评论(0)