# 基于 HarmonyOS ArkTS 的菜谱教学应用——美味厨房全模块深度解析 ## 引言 在移动互联网高速发展的今天,烹饪类应用已经成为许多人日常生活中不可或缺的工具。无论是厨房新手想要学
基于 HarmonyOS ArkTS 的菜谱教学应用——美味厨房全模块深度解析
引言
在移动互联网高速发展的今天,烹饪类应用已经成为许多人日常生活中不可或缺的工具。无论是厨房新手想要学习基本的家常菜,还是资深美食爱好者希望挑战高难度料理,一款优秀的菜谱教学应用都能为用户提供系统化、可视化的烹饪指导。本文将要深度解析的"美味厨房"应用,正是一款基于 HarmonyOS ArkTS 框架开发的菜谱教学类移动应用。它不仅仅是一个简单的菜谱列表展示工具,而是一个集成了今日推荐、菜谱库浏览、食材管理、学习进度追踪和个人中心等五大核心模块的综合性烹饪学习平台。通过精心设计的用户界面和丰富的交互功能,这款应用旨在为用户打造一个从食材准备到烹饪完成的全链路学习体验。
从技术架构的角度来看,该应用采用了 HarmonyOS 的声明式 UI 开发范式,使用 ArkTS 语言进行编写。ArkTS 是华为专为 HarmonyOS 生态设计的应用开发语言,它在 TypeScript 的基础上进行了扩展和优化,增加了 @Entry、@Component、@State、@Builder 等装饰器语法,使得开发者能够以声明式的方式构建高性能的用户界面。整个应用基于单一组件 CookingApp 构建,通过 @State 装饰器管理全局状态,通过 @Builder 装饰器封装可复用的 UI 构建函数,实现了代码的高内聚和低耦合。应用内部没有引入额外的状态管理框架或数据持久化方案,而是通过组件内部的 Mock 数据来模拟真实的业务场景,这种设计方式非常适合用于技术演示和原型开发。
在设计理念方面,该应用选择了"食欲红"(#C62828)和"暖黄"(#F9A825)作为主色调,这一配色方案充分考虑了烹饪类应用的视觉心理学需求。红色在色彩心理学中具有增进食欲、激发热情的作用,这也是为什么许多餐饮品牌都选择红色作为主色调的原因;而暖黄色则传递出温暖、活力和阳光的感觉,与红色搭配能够营造出温馨而充满活力的视觉氛围。应用整体背景采用了极浅的暖黄色(#FFFDE7),使得界面看起来干净明亮而不刺眼,卡片使用纯白色背景与浅色背景形成层次对比,整体视觉风格清新而不失专业感。每个分类和难度级别都配备了专属的颜色标识,如中餐用红色、西餐用蓝色、日料用青色,这种颜色编码系统帮助用户在海量信息中快速识别和分类。
功能层面上,这款应用涵盖了烹饪学习的多个维度。"今日推荐"模块通过精心设计的大卡片展示每日精选菜谱,配合分类标签横滑选择和热门菜谱列表,为用户提供个性化的推荐体验;"菜谱库"模块提供了搜索功能和分类网格入口,并详细展示了红烧肉的8步烹饪教程,每一步都配有标题、描述、用时和专业小贴士;"食材管理"模块帮助用户追踪冰箱中食材的保质期和新鲜状态,避免食材浪费;"学习进度"模块通过进度环、柱状图和技能等级展示,将学习成果可视化,激励用户持续学习;"个人中心"则提供了用户信息展示和各类功能入口。五个模块之间通过底部导航栏实现无缝切换,整体形成了完整的烹饪学习闭环。
从代码工程的角度审视,该应用虽然在单文件中实现了所有功能,但其代码组织结构清晰、层次分明。数据接口定义部分使用 TypeScript 的 interface 语法严格约束了数据结构;颜色配置部分采用集中式管理策略,通过 ColorToken 接口和 COLORS 常量对象统一管理17个颜色值;状态管理部分利用 ArkTS 的 @State 装饰器实现了响应式数据绑定;UI 构建部分通过 @Builder 装饰器将复杂的界面拆分为多个可维护的构建函数。这种代码组织方式既保证了功能的完整性,又确保了代码的可读性和可维护性,对于学习 HarmonyOS ArkTS 开发的开发者来说,是一个非常优秀的参考案例。
一、数据模型与接口定义分析
在任何应用开发中,数据模型的设计都是最基础也是最重要的环节之一。良好的数据模型能够清晰地表达业务逻辑,减少运行时错误,并为后续的功能扩展提供便利。该应用定义了五个核心接口:RecipeInfo、IngredientInfo、StepInfo、LearnStat 和 ColorToken,分别对应菜谱信息、食材信息、烹饪步骤、学习统计和颜色令牌。
1.1 RecipeInfo 菜谱信息接口
interface RecipeInfo {
id: number
name: string
category: string
difficulty: string
time: string
calories: number
rating: number
cookCount: number
icon: string
steps: number
isFavorite: boolean
description: string
}
RecipeInfo 接口定义了菜谱的核心数据结构,它是整个应用中最重要的数据模型之一。该接口包含了12个字段,涵盖了菜谱的各个维度信息。id 字段作为唯一标识符,类型为 number,用于在菜谱列表中区分不同的菜谱条目,同时也是编辑和删除操作的关键依据。name 和 description 分别存储菜谱名称和描述文字,为用户提供菜谱的基本介绍信息。category 字段标识菜谱所属的分类,如"中餐"、"西餐"等,这个字段与后续的 CATEGORY_MAP 配合使用,决定了分类标签的显示颜色。
在数据类型的选择上,这个接口体现了开发者的细致考量。calories、rating、cookCount、steps 和 id 使用了 number 类型,因为它们涉及数值计算和比较操作(如卡路里统计、评分排序、烹饪次数统计等),使用数值类型能够保证数据精度和操作效率。而 time 字段虽然表示时间,但使用了 string 类型(如"60分钟"),这是因为时间在此应用中主要用于展示而非计算,使用字符串可以直接显示而无需格式化转换。isFavorite 字段使用 boolean 类型表示收藏状态,这是最简洁高效的状态表示方式,使得在 UI 层面可以通过简单的三元运算符来切换收藏图标和文字。icon 字段使用 string 类型存储 emoji 表情符号,如"🥘"、"🍳"等,这是一种轻量级的图标方案,避免了引入图片资源带来的包体积增加。
1.2 IngredientInfo 食材信息接口
interface IngredientInfo {
id: number
name: string
category: string
amount: string
unit: string
expireDate: string
icon: string
status: string
}
IngredientInfo 接口定义了食材管理模块的数据结构。与菜谱信息类似,它同样以 id 作为唯一标识符。name 和 category 分别表示食材名称和分类(如"肉类"、“蔬菜”、“乳制品"等)。值得注意的是,amount 和 unit 被拆分为两个独立字段:amount 存储数量值(如"500”),unit 存储单位(如"克"、“个”、“升”),这种拆分方式比直接存储"500克"更加灵活,使得在 UI 层面可以分别对数量和单位进行样式控制,也方便未来扩展数值比较或单位换算功能。
expireDate 字段使用 string 类型存储保质期日期(如"07-28"),采用月-日的简短格式。这种设计简化了日期处理逻辑,但在实际生产环境中可能需要考虑使用完整日期格式或时间戳以支持跨年场景。status 字段表示食材的新鲜状态,取值为"新鲜"、“充足"或"即将过期”,这个字段在 UI 层面会根据不同的值显示不同的颜色标签,帮助用户快速识别需要优先使用的食材。icon 字段同样使用 emoji 来表示食材图标,如"🥩"表示肉类、"🥚"表示蛋类、"🍅"表示蔬菜等,与菜谱信息保持一致的视觉语言。
1.3 StepInfo 烹饪步骤接口
interface StepInfo {
step: number
title: string
description: string
duration: string
tip: string
}
StepInfo 接口定义了烹饪步骤的数据结构,它是菜谱教学功能的核心数据模型。step 字段为步骤序号,类型为 number,用于在 UI 中显示步骤编号(如1、2、3…),同时在列表渲染时也作为排序依据。title 字段是步骤的简短标题(如"准备食材"、“炒糖色”、“炖煮"等),让用户能够快速了解每一步的核心动作。description 字段是步骤的详细描述,提供了具体的操作指导,如"将五花肉切成3cm见方的块,冷水下锅焯水去血沫”。
duration 字段记录该步骤的预计用时(如"10分钟"、“40分钟”),帮助用户合理规划烹饪时间。tip 字段是该接口中最具教学价值的字段,它为每一步提供了专业的小贴士,如"焯水时加料酒和姜片去腥效果更好"、“一定要用热水,冷水会让肉变紧"等。这些贴士来自于实际烹饪经验的总结,对于初学者来说具有很高的参考价值。这个接口的设计体现了教学型应用的特点:不仅要告诉用户"做什么”,还要告诉用户"怎么做更好"。
1.4 LearnStat 学习统计接口
interface LearnStat {
week: string
learned: number
target: number
}
LearnStat 接口定义了学习进度的统计数据结构,结构简洁但功能明确。week 字段表示统计周期(如"第1周"、“第2周”),learned 字段记录该周期内实际学习的菜谱数量,target 字段记录该周期的目标学习数量。这三个字段共同构成了一个完整的学习进度数据单元,用于在"学习进度"模块中绘制周度学习柱状图。
这个接口虽然简单,但蕴含了数据可视化的设计思路。learned 和 target 的对比能够直观反映用户的学习完成率,当 learned 大于等于 target 时表示达标,小于时表示未达标。在柱状图展示中,learned 的值直接决定了柱子的高度(通过 stat.learned * 12 计算像素高度),而 target 则可以作为参考线(虽然当前实现中未直接使用 target 绘制参考线,但数据已经预留)。这种数据结构设计为未来扩展更丰富的进度展示功能(如达标率统计、目标完成趋势分析等)打下了基础。
1.5 ColorToken 颜色令牌接口
interface ColorToken {
primary: string;
primaryDark: string;
primaryLight: string;
accent: string;
accentDark: string;
bg: string;
cardBg: string;
textDark: string;
textLight: string;
textWhite: string;
border: string;
success: string;
warning: string;
danger: string;
blue: string;
purple: string;
teal: string;
}
ColorToken 接口是该应用颜色管理体系的基石,它定义了17个颜色令牌,涵盖了应用中所有可能用到的颜色场景。这个接口的设计思路源自设计系统(Design System)中的"颜色令牌"(Color Token)概念,即将所有颜色值抽象为具有语义的令牌名称,而不是在代码中硬编码具体的颜色值。
接口中的颜色可以分为几个功能组:主色系列(primary、primaryDark、primaryLight)定义了应用的品牌色调和渐变范围;强调色系列(accent、accentDark)提供了与主色搭配的辅助色;背景色系列(bg、cardBg、border)定义了界面的层次结构;文字色系列(textDark、textLight、textWhite)提供了不同层级的文字颜色;语义色系列(success、warning、danger)用于表示状态信息;扩展色系列(blue、purple、teal)为分类标识提供了更多颜色选择。
通过定义这个接口,开发者获得了多重好处。首先,类型安全得到了保障——任何使用 COLORS 对象的地方都会经过 TypeScript 的类型检查,拼写错误或引用不存在的颜色令牌会在编译时被发现。其次,主题切换变得可行——如果未来需要支持暗色模式或其他主题,只需要提供不同的 ColorToken 实现即可,而不需要修改 UI 代码中的颜色引用。最后,颜色的一致性得到了保证——整个应用使用统一的颜色令牌系统,避免了同一语义的颜色在不同地方使用不同色值的问题。
二、颜色与配置常量分析
2.1 COLORS 颜色配置对象
const COLORS: ColorToken = {
primary: '#C62828',
primaryDark: '#B71C1C',
primaryLight: '#FFCDD2',
accent: '#F9A825',
accentDark: '#F57F17',
bg: '#FFFDE7',
cardBg: '#FFFFFF',
textDark: '#212121',
textLight: '#757575',
textWhite: '#FFFFFF',
border: '#FFF8E1',
success: '#43A047',
warning: '#FB8C00',
danger: '#E53935',
blue: '#1E88E5',
purple: '#8E24AA',
teal: '#00897B'
};
COLORS 常量对象是 ColorToken 接口的具体实现,它将所有颜色令牌映射为实际的十六进制颜色值。这个对象被声明为 const,确保在运行时不会被意外修改,保证了颜色配置的不可变性。代码注释中标注了"方案1:标准静态类型,无断言,API24编译通过(推荐)",说明开发者经过了多种方案的对比和测试,最终选择了这种最安全、最规范的类型声明方式。
从颜色值的选择来看,这套配色方案经过了精心设计。主色 #C62828 是一种深沉的红色,属于 Material Design 色板中的 Red 800,它比纯红色更加沉稳,适合作为应用的品牌色。primaryDark(#B71C1C,Red 900)比主色更深,用于渐变效果的起始端,营造出从深到浅的视觉层次。primaryLight(#FFCDD2,Red 100)是非常浅的粉色,虽然在本应用中未直接使用,但预留了浅色主题的支持。
强调色 #F9A825(Amber 700)是一种温暖的橙黄色,与红色主色形成了经典的"红黄搭配",这是餐饮行业最常见的配色组合之一。背景色 #FFFDE7(Yellow 50)是极浅的暖黄色,几乎接近白色但带有微弱的暖色调,使得界面在保持明亮的同时不会显得过于冰冷。cardBg 使用纯白色 #FFFFFF,与背景色形成微妙的层次对比,使卡片能够从背景中"浮起"。
文字颜色采用了 Material Design 的标准文字色阶:textDark(#212121)用于主要文字,对比度高,确保可读性;textLight(#757575)用于次要文字,降低视觉权重;textWhite(#FFFFFF)用于深色背景上的文字。语义色方面,success(#43A047,Green 600)用于表示"简单"难度或"新鲜"状态,warning(#FB8C00,Orange 600)用于表示"中等"难度,danger(#E53935,Red 600)用于表示"困难"难度或"即将过期"状态。这些颜色选择符合用户的直觉认知——绿色代表安全/容易,橙色代表注意/中等,红色代表危险/困难。
2.2 CATEGORY_MAP 分类颜色映射
const CATEGORY_MAP: Record<string, string> = {
'中餐': '#C62828',
'西餐': '#1E88E5',
'日料': '#00897B',
'甜点': '#E91E63',
'烘焙': '#FB8C00',
'饮品': '#8E24AA'
}
CATEGORY_MAP 是一个分类到颜色的映射表,使用 Record<string, string> 类型定义。它为每个菜谱分类分配了专属的颜色标识:中餐对应食欲红(与主色一致)、西餐对应蓝色(传递国际化、现代感)、日料对应青色(让人联想到海洋和清新)、甜点对应粉色(甜美可爱的视觉联想)、烘焙对应橙色(温暖烘焙的氛围)、饮品对应紫色(优雅独特的气质)。
这个映射表在应用中扮演着重要的角色。在"今日推荐"模块的热门菜谱列表中,每个菜谱的分类标签背景色就是通过 CATEGORY_MAP[item.category] 动态获取的。在"菜谱库"模块的分类网格中,虽然当前实现使用 emoji 图标而非颜色区分分类,但 CATEGORY_MAP 的存在为分类判断提供了依据(代码中通过 CATEGORY_MAP[cat] ? '🍽️' : '' 来判断分类是否存在)。这种通过映射表管理分类颜色的方式,使得新增分类时只需要在映射表中添加一条记录即可,无需修改 UI 代码,具有良好的扩展性。
2.3 DIFFICULTY_MAP 难度颜色映射
const DIFFICULTY_MAP: Record<string, string> = {
'简单': COLORS.success,
'中等': COLORS.warning,
'困难': COLORS.danger
}
DIFFICULTY_MAP 是难度到颜色的映射表,与 CATEGORY_MAP 类似,但有一个重要区别:它的值直接引用了 COLORS 对象中的语义色,而不是使用硬编码的颜色值。这种设计体现了颜色令牌系统的优势——如果未来需要调整语义色的具体色值(例如将 success 从绿色改为其他颜色),只需要修改 COLORS 对象中的一处定义,所有引用 COLORS.success 的地方(包括 DIFFICULTY_MAP)都会自动更新。
难度映射的语义设计非常直观:"简单"对应绿色(容易、安全)、"中等"对应橙色(需要注意)、"困难"对应红色(具有挑战性)。这种颜色编码在热门菜谱列表中的难度标签上得到了应用——每个菜谱的难度标签背景色会根据难度值自动匹配相应的语义色,帮助用户在浏览菜谱时快速判断烹饪难度。
三、状态管理与 Mock 数据分析
3.1 组件声明与状态变量
@Entry
@Component
struct CookingApp {
@State currentTab: number = 0
@State showAddRecipe: boolean = false
@State showEditRecipe: boolean = false
@State showDeleteRecipe: boolean = false
@State selectedCategory: number = 0
@State editingRecipeId: number = 0
CookingApp 组件通过 @Entry 和 @Component 装饰器声明为应用的入口组件。@Entry 表示该组件是页面的根组件,@Component 表示这是一个自定义组件。在组件内部,使用了6个 @State 装饰的响应式状态变量来管理应用的全局状态。
@State 是 ArkTS 中最重要的状态管理装饰器之一。被 @State 修饰的变量具有响应式特性——当变量的值发生变化时,所有引用该变量的 UI 部分会自动重新渲染。这种响应式机制是声明式 UI 的核心,开发者不需要手动操作 DOM 或调用 setState,只需要修改变量值,框架会自动处理 UI 更新。
currentTab 变量记录当前选中的 Tab 索引,初始值为0(即"今日推荐"页面)。这个变量是整个应用导航的核心——build() 函数根据 currentTab 的值决定渲染哪个内容模块,底部导航栏根据 currentTab 的值高亮当前选中的 Tab 项。三个布尔型变量 showAddRecipe、showEditRecipe 和 showDeleteRecipe 分别控制三个弹窗的显示状态,初始值均为 false(隐藏状态)。当用户点击相应的操作按钮时,对应的变量被设置为 true,弹窗显示;当用户点击弹窗中的"取消"或确认按钮时,变量被重置为 false,弹窗隐藏。
selectedCategory 变量记录当前选中的分类索引,用于"今日推荐"模块的分类标签切换。初始值为0(即"全部"分类),当用户点击某个分类标签时,该变量更新为对应索引,分类标签的选中状态和列表过滤逻辑会相应更新。editingRecipeId 变量记录当前正在编辑的菜谱 ID,当用户点击菜谱列表中的"编辑"按钮时,该变量被设置为对应菜谱的 ID,然后显示编辑弹窗。这个变量的存在使得编辑弹窗能够知道正在编辑的是哪个菜谱,为未来实现真正的数据修改功能预留了接口。
3.2 分类列表与菜谱 Mock 数据
private categories: string[] = ['全部', '中餐', '西餐', '日料', '甜点', '饮品', '烘焙']
private recipes: RecipeInfo[] = [
{ id: 1, name: '红烧肉', category: '中餐', difficulty: '中等', time: '60分钟', calories: 520, rating: 4.8, cookCount: 32, icon: '🥘', steps: 8, isFavorite: true, description: '经典家常菜,肥而不腻入口即化' },
{ id: 2, name: '番茄炒蛋', category: '中餐', difficulty: '简单', time: '15分钟', calories: 180, rating: 4.9, cookCount: 156, icon: '🍳', steps: 5, isFavorite: true, description: '国民下饭菜,零失败入门菜' },
{ id: 3, name: '牛排', category: '西餐', difficulty: '中等', time: '30分钟', calories: 450, rating: 4.7, cookCount: 18, icon: '🥩', steps: 6, isFavorite: false, description: '完美五分熟,外焦里嫩多汁' },
// ... 更多菜谱数据
]
categories 数组定义了7个菜谱分类,以"全部"开头,后跟6个具体分类。这个数组在"今日推荐"模块的分类标签横滑列表和"菜谱库"模块的分类网格中被使用。在分类网格中,代码通过 this.categories.slice(1, 5) 和 this.categories.slice(5, 7) 分别获取前4个和后2个分类,实现了4+2的网格布局。
recipes 数组包含了12条菜谱 Mock 数据,每条数据都严格遵循 RecipeInfo 接口定义。这些菜谱涵盖了中餐(红烧肉、番茄炒蛋、麻婆豆腐)、西餐(牛排、意大利面)、日料(寿司拼盘、天妇罗)、甜点(提拉米苏、舒芙蕾)、饮品(珍珠奶茶、芒果西米露)和烘焙(黄油曲奇)等6个分类,确保了数据覆盖的全面性。每条菜谱数据都包含了完整的12个字段信息,如评分(4.4-4.9分)、烹饪次数(4-156次)、卡路里(150-520卡)等,这些数据为 UI 展示提供了丰富的信息维度。
Mock 数据的设计不仅考虑了数据完整性,还注重了数据的真实性和多样性。例如,难度分布既有"简单"(番茄炒蛋、珍珠奶茶等)也有"困难"(寿司拼盘、舒芙蕾),烹饪时间从15分钟(番茄炒蛋)到240分钟(提拉米苏)不等,收藏状态有已收藏和未收藏两种。这种多样化的数据设计确保了 UI 在各种数据组合下都能正确渲染,也为功能测试提供了全面的覆盖。
3.3 食材与步骤 Mock 数据
private ingredients: IngredientInfo[] = [
{ id: 1, name: '五花肉', category: '肉类', amount: '500', unit: '克', expireDate: '07-28', icon: '🥩', status: '新鲜' },
{ id: 2, name: '鸡蛋', category: '蛋类', amount: '12', unit: '个', expireDate: '08-05', icon: '🥚', status: '充足' },
{ id: 4, name: '牛奶', category: '乳制品', amount: '1', unit: '升', expireDate: '07-29', icon: '🥛', status: '即将过期' },
// ... 更多食材数据
]
private cookSteps: StepInfo[] = [
{ step: 1, title: '准备食材', description: '将五花肉切成3cm见方的块,冷水下锅焯水去血沫', duration: '10分钟', tip: '焯水时加料酒和姜片去腥效果更好' },
{ step: 2, title: '炒糖色', description: '锅中放少许油,加入冰糖小火慢炒至焦糖色', duration: '5分钟', tip: '糖色炒到冒小泡即可,不要炒过头' },
// ... 更多步骤数据
]
ingredients 数组包含12条食材数据,覆盖了肉类、蛋类、蔬菜、乳制品、海鲜、水果、主食等多种食材类型。食材状态分布合理:部分食材为"即将过期"(如牛奶、三文鱼、虾),部分为"充足"(如鸡蛋、面粉、黄油),其余为"新鲜"。这种状态分布与食材统计区域显示的数据(12种总种类、3种即将过期、8种新鲜/充足)保持一致,体现了数据的内在一致性。
cookSteps 数组包含了红烧肉的8个烹饪步骤,从"准备食材"到"装盘",完整覆盖了红烧肉的烹饪流程。每个步骤都配有详细的操作描述和专业的小贴士,如第5步"炖煮"的贴士"一定要用热水,冷水会让肉变紧",这是非常实用的烹饪技巧。步骤用时从1分钟(装盘)到40分钟(炖煮)不等,反映了实际烹饪中各步骤的时间分布。这组数据在"菜谱库"模块的步骤详解区域被完整展示,是应用教学功能的核心内容。
3.4 学习统计 Mock 数据
private learnStats: LearnStat[] = [
{ week: '第1周', learned: 3, target: 5 },
{ week: '第2周', learned: 5, target: 5 },
{ week: '第3周', learned: 4, target: 5 },
{ week: '第4周', learned: 6, target: 5 },
{ week: '第5周', learned: 7, target: 5 },
{ week: '第6周', learned: 5, target: 5 }
]
learnStats 数组包含了6周的学习统计数据,每周的目标均为5道菜,但实际学习数量在3到7之间波动。这种数据设计反映了真实的学习场景——用户的学习进度不会完全按照计划进行,会有起伏波动。第1周学习了3道(未达标),第2周学习了5道(达标),第4周和第5周分别学习了6道和7道(超额完成),这种起伏变化使得柱状图的展示更加生动和真实。
这组数据在"学习进度"模块中被用于绘制周度学习柱状图。每个柱子的高度通过 stat.learned * 12 计算得出,即将学习数量乘以12个像素作为柱子高度。同时,学习数量也会显示在柱子上方,周次标签显示在柱子下方,形成了完整的柱状图可视化效果。6周累计学习数量为30道,与总体进度环中显示的"30/50"保持一致,确保了数据在不同模块间的统一性。
四、底部导航栏实现分析
@Builder
TabBar() {
Row() {
Column() { Text('🌟').fontSize(20).opacity(this.currentTab === 0 ? 1 : 0.4); Text('推荐').fontSize(10).fontColor(this.currentTab === 0 ? COLORS.primary : COLORS.textLight).margin({ top: 2 }) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 }).onClick(() => { this.currentTab = 0 })
Column() { Text('📖').fontSize(20).opacity(this.currentTab === 1 ? 1 : 0.4); Text('菜谱库').fontSize(10).fontColor(this.currentTab === 1 ? COLORS.primary : COLORS.textLight).margin({ top: 2 }) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 }).onClick(() => { this.currentTab = 1 })
Column() { Text('🥬').fontSize(20).opacity(this.currentTab === 2 ? 1 : 0.4); Text('食材').fontSize(10).fontColor(this.currentTab === 2 ? COLORS.primary : COLORS.textLight).margin({ top: 2 }) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 }).onClick(() => { this.currentTab = 2 })
Column() { Text('📊').fontSize(20).opacity(this.currentTab === 3 ? 1 : 0.4); Text('进度').fontSize(10).fontColor(this.currentTab === 3 ? COLORS.primary : COLORS.textLight).margin({ top: 2 }) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 }).onClick(() => { this.currentTab = 3 })
Column() { Text('👤').fontSize(20).opacity(this.currentTab === 4 ? 1 : 0.4); Text('我的').fontSize(10).fontColor(this.currentTab === 4 ? COLORS.primary : COLORS.textLight).margin({ top: 2 }) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 }).onClick(() => { this.currentTab = 4 })
}.width('100%').height(54).backgroundColor(COLORS.cardBg).border({ width: { top: 1 }, color: COLORS.border })
}
TabBar 是通过 @Builder 装饰器封装的底部导航栏构建函数。@Builder 装饰器用于定义自定义构建函数,它可以将一段 UI 结构封装为可复用的代码单元。与普通函数不同,@Builder 函数中可以直接使用 ArkTS 的声明式 UI 语法(如 Row、Column、Text 等),并且可以访问组件的 @State 变量和 this 上下文。
导航栏整体采用 Row 容器横向排列5个 Tab 项,每个 Tab 项是一个 Column 容器,内含一个 emoji 图标和一行文字标签。5个 Tab 分别对应"推荐"(🌟)、“菜谱库”(📖)、“食材”(🥬)、“进度”(📊)和"我的"(👤),使用 emoji 作为图标既节省了资源开销,又增加了界面的趣味性。每个 Tab 项通过 layoutWeight(1) 等分容器宽度,确保5个 Tab 在导航栏中均匀分布。
选中状态的视觉反馈通过两个维度实现:图标透明度和文字颜色。当某个 Tab 被选中时(即 this.currentTab 等于该 Tab 的索引),图标的 opacity 设置为1(完全不透明),文字颜色设置为主色 COLORS.primary;未选中的 Tab 图标 opacity 设置为0.4(半透明),文字颜色设置为浅色 COLORS.textLight。这种双重维度的视觉反馈使得选中状态非常醒目,用户可以一目了然地知道当前位于哪个页面。
每个 Tab 项的 onClick 事件处理函数非常简洁——只需要将 this.currentTab 设置为对应的索引值即可。由于 currentTab 是 @State 变量,当其值改变时,整个组件会自动重新渲染,build() 函数会根据新的 currentTab 值渲染对应的内容模块,同时 TabBar 中各 Tab 的选中状态也会同步更新。这种"修改状态即触发 UI 更新"的响应式编程模式是 ArkTS 的核心优势之一,极大地简化了状态管理和 UI 同步的复杂度。
导航栏容器的样式设置也很考究。高度设置为54像素,背景色为白色(COLORS.cardBg),顶部边框设置为1像素的浅黄色(COLORS.border),形成了与内容区域的视觉分隔。54像素的高度在保证可点击区域足够大的同时,不会占用过多的屏幕空间,为内容区域留出了更多的展示空间。
五、今日推荐模块分析
5.1 推荐大卡片
// 今日推荐大卡片
Column() {
Row() {
Column() {
Text('🌟 今日推荐').fontSize(12).fontColor('#FFCCBC')
Text('红烧肉').fontSize(24).fontColor(COLORS.textWhite).fontWeight(FontWeight.Bold).margin({ top: 4 })
Text('经典家常菜 · 肥而不腻').fontSize(12).fontColor('#FFCCBC').margin({ top: 4 })
Row() {
Text('⭐ 4.8').fontSize(12).fontColor(COLORS.accent)
Text('🕐 60分钟').fontSize(12).fontColor('#FFCCBC').margin({ left: 12 })
Text('🔥 520卡').fontSize(12).fontColor('#FFCCBC').margin({ left: 12 })
}.margin({ top: 8 })
Text('立即学习 →').fontSize(13).fontColor(COLORS.textWhite).fontWeight(FontWeight.Bold).padding({ left: 14, right: 14, top: 6, bottom: 6 }).backgroundColor('#33FFFFFF').borderRadius(16).margin({ top: 10 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text('🥘').fontSize(64)
}.width('100%').alignItems(VerticalAlign.Center)
}.width('100%').padding(20).linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0], [COLORS.primary, 1]] }).borderRadius(20).margin({ left: 16, right: 16, top: 12 })
推荐大卡片是"今日推荐"页面最醒目的视觉元素,它采用了左文右图的布局结构。左侧是一个 Column 容器,从上到下依次排列了"今日推荐"标签、菜谱名称"红烧肉"、简短描述、评分/时间/卡路里信息行和"立即学习"按钮。右侧是一个64像素大小的 emoji 图标"🥘",作为菜谱的视觉标识。
卡片的视觉设计非常精致。整体使用了135度角的线性渐变背景,从 primaryDark(深红)渐变到 primary(红色),营造出从深到浅的立体感。圆角设置为20像素,使得卡片边缘柔和而不生硬。卡片内部的文字颜色使用了两种策略:主要文字(菜谱名称)使用纯白色,辅助文字使用浅橙色(#FFCCBC),这种暖色调的文字在红色背景上既保持了可读性,又与整体配色方案协调统一。
"立即学习"按钮的设计采用了半透明白色背景(#33FFFFFF,即白色20%透明度),配合16像素的圆角,形成了一个药丸形状的按钮。这种半透明背景在渐变色卡片上能够产生毛玻璃效果,既不喧宾夺主,又清晰可辨。评分信息行中,评分数字使用了强调色 COLORS.accent(暖黄色),与星标 emoji 形成呼应,而时间和卡路里信息使用浅橙色,保持视觉层级的一致性。
5.2 分类标签横滑
// 分类标签
Scroll() {
Row() {
ForEach(this.categories, (cat: string, index: number) => {
Text(cat).fontSize(11).fontColor(this.selectedCategory === index ? COLORS.textWhite : COLORS.primary).padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(16).backgroundColor(this.selectedCategory === index ? COLORS.primary : COLORS.cardBg).margin({ left: 4, right: 4 }).onClick(() => { this.selectedCategory = index })
})
}.padding({ left: 12, right: 12 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%').margin({ top: 10 })
分类标签区域使用了横向滚动的 Scroll 容器包裹 Row 布局,通过 ForEach 循环渲染7个分类标签。Scroll 组件设置了 scrollable(ScrollDirection.Horizontal) 实现横向滚动,同时通过 scrollBar(BarState.Off) 隐藏了滚动条,保持了界面的整洁。
每个分类标签的样式根据选中状态动态变化。选中状态下(this.selectedCategory === index),文字颜色为白色,背景色为主色红色,形成了实心标签效果;未选中状态下,文字颜色为主色红色,背景色为白色,形成了描边标签效果。这种实心与描边的对比设计,使得选中的分类标签在视觉上更加突出,用户可以清晰地知道当前选择的分类。
标签的 onClick 事件处理函数将 this.selectedCategory 设置为当前点击的索引。由于 selectedCategory 是 @State 变量,其值变化后所有标签的样式会自动重新计算和渲染。圆角设置为16像素,内边距设置为水平12像素、垂直6像素,使得标签的尺寸适中、视觉舒适。标签之间的间距通过 margin({ left: 4, right: 4 }) 设置为4像素,既保持了标签之间的间隔,又不会显得过于分散。
5.3 热门菜谱列表
// 推荐列表
Text('🔥 热门菜谱').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark).width('100%').padding({ left: 16, top: 8 })
Scroll() {
Column() {
ForEach(this.recipes, (item: RecipeInfo, index: number) => {
Row() {
Column() {
Text(item.icon).fontSize(32)
}.width(60).height(60).borderRadius(12).backgroundColor(COLORS.bg).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text(item.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark)
Text(item.difficulty).fontSize(9).fontColor(COLORS.textWhite).padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(4).backgroundColor(DIFFICULTY_MAP[item.difficulty]).margin({ left: 6 })
}
Text(item.description).fontSize(11).fontColor(COLORS.textLight).margin({ top: 3 }).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('⭐' + item.rating).fontSize(10).fontColor(COLORS.accent)
Text('🕐' + item.time).fontSize(10).fontColor(COLORS.textLight).margin({ left: 8 })
Text('🔥' + item.calories + '卡').fontSize(10).fontColor(COLORS.textLight).margin({ left: 8 })
Text('👨🍳' + item.cookCount + '次').fontSize(10).fontColor(COLORS.textLight).margin({ left: 8 })
}.margin({ top: 4 })
Row() {
Text(item.category).fontSize(9).fontColor(COLORS.textWhite).padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(4).backgroundColor(CATEGORY_MAP[item.category])
Text(item.isFavorite ? '❤️ 已收藏' : '🤍 收藏').fontSize(10).fontColor(item.isFavorite ? COLORS.danger : COLORS.textLight).margin({ left: 8 })
}.margin({ top: 4 })
}.layoutWeight(1).padding({ left: 10 }).alignItems(HorizontalAlign.Start)
Column() {
Text('编辑').fontSize(11).fontColor(COLORS.primary).padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6).backgroundColor(COLORS.bg).onClick(() => { this.editingRecipeId = item.id; this.showEditRecipe = true })
Text('删除').fontSize(11).fontColor(COLORS.danger).padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6).backgroundColor('#FFEBEE').margin({ top: 6 }).onClick(() => { this.showDeleteRecipe = true })
}.alignItems(HorizontalAlign.Center)
}.width('100%').padding(12).backgroundColor(COLORS.cardBg).borderRadius(14).margin({ left: 12, right: 12, top: 6 }).alignItems(VerticalAlign.Center)
})
}.padding({ bottom: 20 })
}.scrollBar(BarState.Off).layoutWeight(1)
热门菜谱列表是"今日推荐"模块的核心内容区域,它通过 ForEach 遍历 this.recipes 数组,为每个菜谱渲染一个信息丰富的卡片。每个卡片采用左中右三栏布局:左侧是60x60像素的 emoji 图标容器,中间是菜谱的详细信息区域,右侧是"编辑"和"删除"操作按钮。
中间区域的信息密度非常高,包含了四行内容。第一行是菜谱名称和难度标签,难度标签的背景色通过 DIFFICULTY_MAP[item.difficulty] 动态获取,实现了颜色与难度的语义映射。第二行是菜谱描述,使用了 maxLines(1) 和 textOverflow({ overflow: TextOverflow.Ellipsis }) 限制为单行显示并在溢出时显示省略号,保证了卡片高度的一致性。第三行是评分、时间、卡路里和烹饪次数四个信息项,以紧凑的横向排列方式展示,评分使用暖黄色高亮,其余信息使用浅灰色。第四行是分类标签和收藏状态,分类标签的背景色通过 CATEGORY_MAP[item.category] 获取,收藏状态根据 item.isFavorite 的值显示不同的图标和文字。
右侧操作区域包含"编辑"和"删除"两个按钮。"编辑"按钮点击后会设置 this.editingRecipeId 为当前菜谱的 ID 并显示编辑弹窗;"删除"按钮点击后会显示删除确认弹窗。两个按钮的视觉设计也体现了语义区分——"编辑"使用主色红色文字配浅黄色背景,"删除"使用危险红色文字配浅红色背景(#FFEBEE),帮助用户区分不同操作的后果。
整个列表被包裹在 Scroll 容器中,通过 layoutWeight(1) 占据剩余空间,实现了列表的可滚动浏览。滚动条被隐藏(scrollBar(BarState.Off)),保持了界面的整洁。每张卡片之间通过 margin({ top: 6 }) 保持6像素的间距,卡片本身使用14像素圆角和12像素内边距,整体视觉效果紧凑而有序。
六、菜谱库模块分析
6.1 搜索栏与新增按钮
Row() {
Row() { Text('🔍').fontSize(14); Text('搜索菜谱...').fontSize(13).fontColor(COLORS.textLight).margin({ left: 6 }) }.layoutWeight(1).height(36).padding({ left: 12 }).backgroundColor(COLORS.cardBg).borderRadius(18).alignItems(VerticalAlign.Center)
Text('➕').fontSize(18).fontColor(COLORS.textWhite).width(36).height(36).borderRadius(18).backgroundColor(COLORS.primary).textAlign(TextAlign.Center).margin({ left: 8 }).onClick(() => { this.showAddRecipe = true })
}.width('100%').padding({ left: 16, right: 16, top: 10 }).alignItems(VerticalAlign.Center)
菜谱库模块的顶部是搜索栏和新增按钮的组合区域。搜索栏是一个 layoutWeight(1) 的圆角容器(圆角18像素,形成药丸形状),内含搜索图标和占位文字"搜索菜谱…"。搜索栏的高度设置为36像素,背景色为白色,与页面背景形成微妙的对比。
新增按钮位于搜索栏右侧,是一个36x36像素的圆形按钮,背景色为主色红色,内部显示"➕"号。按钮的 onClick 事件将 this.showAddRecipe 设置为 true,触发新增菜谱弹窗的显示。搜索栏和新增按钮之间通过 margin({ left: 8 }) 保持8像素的间距,整体布局紧凑而协调。
6.2 分类网格布局
// 分类网格
Row() {
ForEach(this.categories.slice(1, 5), (cat: string, index: number) => {
Column() {
Text(CATEGORY_MAP[cat] ? '🍽️' : '').fontSize(20)
Text(cat).fontSize(10).fontColor(COLORS.textDark).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 }).backgroundColor(COLORS.cardBg).borderRadius(12).margin({ left: index === 0 ? 0 : 4, right: 4 })
})
}.width('100%').padding({ left: 12, right: 12, top: 8 })
Row() {
ForEach(this.categories.slice(5, 7), (cat: string, index: number) => {
Column() {
Text(CATEGORY_MAP[cat] ? '🍴' : '').fontSize(20)
Text(cat).fontSize(10).fontColor(COLORS.textDark).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 }).backgroundColor(COLORS.cardBg).borderRadius(12).margin({ left: index === 0 ? 0 : 4, right: 4 })
})
Column() {}.layoutWeight(1)
Column() {}.layoutWeight(1)
Column() {}.layoutWeight(1)
}.width('100%').padding({ left: 12, right: 12, top: 4 })
分类网格采用了4+2的布局方式,即第一行4个分类,第二行2个分类加3个空白占位。通过 this.categories.slice(1, 5) 获取前4个分类(中餐、西餐、日料、甜点),通过 this.categories.slice(5, 7) 获取后2个分类(烘焙、饮品),巧妙地排除了"全部"分类(索引0)。
每个分类卡片包含一个 emoji 图标和分类名称,图标通过 CATEGORY_MAP[cat] ? '🍽️' : '' 进行条件渲染——如果分类存在于映射表中则显示图标,否则显示空字符串。第一行使用"🍽️"图标,第二行使用"🍴"图标,进行了简单的视觉区分。每个卡片使用 layoutWeight(1) 等分宽度,背景色为白色,圆角12像素,内边距上下10像素。
第二行布局的一个巧妙之处在于使用了3个空的 Column() {}.layoutWeight(1) 作为占位符。由于第二行只有2个分类,但为了与第一行的4列对齐,需要补充占位列使得整体布局保持整齐。每个卡片之间的间距通过条件 margin 控制:第一个卡片 margin left 为0,其余为4像素,避免了第一个卡片与容器边缘的间距过大。
6.3 红烧肉步骤详解
// 菜谱步骤教学
Text('📖 红烧肉 · 步骤详解').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark).width('100%').padding({ left: 16, top: 12 })
Scroll() {
Column() {
ForEach(this.cookSteps, (step: StepInfo, index: number) => {
Row() {
Column() {
Text(step.step.toString()).fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textWhite)
}.width(36).height(36).borderRadius(18).backgroundColor(COLORS.primary).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text(step.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark)
Text('🕐 ' + step.duration).fontSize(10).fontColor(COLORS.accent).margin({ left: 'auto' })
}.width('100%')
Text(step.description).fontSize(12).fontColor(COLORS.textLight).margin({ top: 4 })
Row() {
Text('💡').fontSize(12)
Text(step.tip).fontSize(11).fontColor(COLORS.teal).margin({ left: 4 })
}.margin({ top: 4 }).width('100%')
}.layoutWeight(1).padding({ left: 10 }).alignItems(HorizontalAlign.Start)
}.width('100%').padding(12).backgroundColor(COLORS.cardBg).borderRadius(14).margin({ left: 12, right: 12, top: 6 }).alignItems(VerticalAlign.Top)
})
}.padding({ bottom: 20 })
}.scrollBar(BarState.Off).layoutWeight(1)
步骤详解区域是菜谱库模块的教学核心,它通过 ForEach 遍历 this.cookSteps 数组,为红烧肉的8个烹饪步骤逐一生成教学卡片。每个步骤卡片采用左右布局:左侧是一个36x36像素的圆形序号容器,右侧是步骤的详细信息区域。
序号容器使用红色背景和白色文字,圆形设计(borderRadius(18) 即宽度的一半)使得数字居中显示在一个醒目的红色圆圈中。序号文字通过 step.step.toString() 将数字转换为字符串显示。这种圆形序号设计是步骤引导类界面的经典模式,用户可以通过序号快速了解当前处于第几步以及总共有多少步。
右侧详细信息区域包含三行内容。第一行是步骤标题和用时,标题使用加粗深色字体,用时通过 margin({ left: 'auto' }) 推至行末右对齐,使用暖黄色显示,与"🕐"图标配合形成时间提示。第二行是步骤的详细描述,使用12像素的浅灰色字体,提供了具体的操作指导。第三行是小贴士区域,"💡"图标配合青色(COLORS.teal)文字,以视觉上区别于步骤描述,提示用户这是额外的技巧建议。
整个步骤列表被包裹在 Scroll 容器中,支持纵向滚动浏览。卡片之间通过 margin({ top: 6 }) 保持间距,每张卡片使用14像素圆角和12像素内边距,与热门菜谱列表的卡片风格保持一致。alignItems(VerticalAlign.Top) 确保了当步骤描述文字较长时,序号圆圈与卡片顶部对齐,而不是垂直居中,这在多行文本场景下更为美观。
七、食材管理模块分析
@Builder
IngredientContent() {
Column() {
// 食材统计
Row() {
Column() { Text('12').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.primary); Text('总种类').fontSize(10).fontColor(COLORS.textLight) }.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() { Text('3').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.danger); Text('即将过期').fontSize(10).fontColor(COLORS.textLight) }.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() { Text('8').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.success); Text('新鲜').fontSize(10).fontColor(COLORS.textLight) }.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding(14).backgroundColor(COLORS.cardBg)
Text('🥬 冰箱食材').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark).width('100%').padding({ left: 16, top: 12 })
Scroll() {
Column() {
ForEach(this.ingredients, (item: IngredientInfo, index: number) => {
Row() {
Column() {
Text(item.icon).fontSize(28)
}.width(48).height(48).borderRadius(10).backgroundColor(COLORS.bg).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column() {
Text(item.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark)
Text(item.category + ' · ' + item.amount + item.unit).fontSize(11).fontColor(COLORS.textLight).margin({ top: 3 })
Row() {
Text('保质期: ' + item.expireDate).fontSize(10).fontColor(COLORS.textLight)
Text(item.status).fontSize(9).fontColor(COLORS.textWhite).padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(4).backgroundColor(item.status === '即将过期' ? COLORS.danger : item.status === '充足' ? COLORS.blue : COLORS.success).margin({ left: 6 })
}.margin({ top: 3 })
}.layoutWeight(1).padding({ left: 10 }).alignItems(HorizontalAlign.Start)
}.width('100%').padding(12).backgroundColor(COLORS.cardBg).borderRadius(12).margin({ left: 12, right: 12, top: 6 }).alignItems(VerticalAlign.Center)
})
}.padding({ bottom: 20 })
}.scrollBar(BarState.Off).layoutWeight(1)
}.width('100%').layoutWeight(1)
}
食材管理模块由两部分组成:顶部的统计概览区域和下方的食材列表区域。统计区域使用 Row 容器横向排列三列数据,分别是"总种类"(12,红色)、“即将过期”(3,危险红色)和"新鲜"(8,成功绿色)。三个数字使用了不同的语义色,帮助用户快速理解各项数据的紧迫程度——即将过期的数字使用红色是最醒目的,提醒用户优先处理。
食材列表的每张卡片采用左图右文的布局。左侧是48x48像素的 emoji 图标容器,圆角10像素,背景为浅黄色。右侧信息区域包含三行内容:食材名称(加粗深色)、分类与数量(如"肉类 · 500克",浅灰色)和保质期与状态标签行。状态标签的颜色通过嵌套三元运算符动态决定:"即将过期"显示红色背景、“充足"显示蓝色背景、其他状态(如"新鲜”)显示绿色背景。这种基于状态值的颜色映射与统计区域的语义色保持一致,形成了统一的视觉语言。
整个模块的设计目标是帮助用户管理冰箱中的食材库存,避免食材过期浪费。通过醒目的状态标签和统计数字,用户可以快速识别需要优先使用的食材,合理安排烹饪计划。这种将食材管理与菜谱教学结合的设计理念,使得应用不仅仅是一个菜谱展示工具,更是一个实用的厨房管理助手。
八、学习进度模块分析
8.1 总体进度环
// 总体进度环
Column() {
Row() {
Text('30').fontSize(48).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
Text('/ 50').fontSize(20).fontColor(COLORS.textLight).margin({ top: 20 })
}
Text('已学菜谱数').fontSize(12).fontColor(COLORS.textLight).margin({ top: 4 })
Row() {
Column().width('60%').height(8).borderRadius(4).linearGradient({ angle: 0, colors: [[COLORS.accent, 0], [COLORS.primary, 1]] })
}.width('80%').height(8).backgroundColor(COLORS.border).borderRadius(4).margin({ top: 12 })
}.width('100%').padding(20).backgroundColor(COLORS.cardBg).borderRadius(16).margin({ left: 16, right: 16, top: 8 }).alignItems(HorizontalAlign.Center)
总体进度区域展示用户的学习完成情况。主体是一个大号的数字"30"和跟随其后的"/ 50",分别表示已学菜谱数和目标菜谱数。“30"使用了48像素的超大字号和主色红色,视觉冲击力强;”/ 50"使用20像素的浅灰色,作为辅助信息存在。"已学菜谱数"标签位于数字下方,解释了数字的含义。
进度条的设计简洁而有效。外层容器宽度80%、高度8像素、圆角4像素、背景色为浅黄色(COLORS.border),作为进度条的轨道。内层 Column 宽度60%(即30/50=60%的完成率)、高度8像素,使用0度角的线性渐变从暖黄色渐变到红色,作为进度条的填充部分。这种用百分比宽度模拟进度条的方式是纯 UI 实现进度条的常用技巧,无需引入额外的图表组件即可实现进度可视化效果。
8.2 周度学习柱状图
// 周度学习柱状图
Column() {
Text('📊 周度学习量').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark).width('100%')
Row() {
ForEach(this.learnStats, (stat: LearnStat, index: number) => {
Column() {
Text(stat.learned.toString()).fontSize(9).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
Column().width(18).height(stat.learned * 12).borderRadius(4).linearGradient({ angle: 180, colors: [[COLORS.accent, 0], [COLORS.primary, 1]] }).margin({ top: 2 })
Text(stat.week).fontSize(8).fontColor(COLORS.textLight).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}.width('100%').alignItems(VerticalAlign.Bottom).margin({ top: 12 })
}.width('100%').padding(16).backgroundColor(COLORS.cardBg).borderRadius(14).margin({ left: 16, right: 16, top: 12 })
周度学习柱状图是学习进度模块的核心可视化组件,它通过纯 ArkTS 声明式 UI 实现了柱状图效果,无需引入任何第三方图表库。ForEach 遍历6周的学习数据,每周渲染一个柱状图单元。每个单元从上到下包含三部分:学习数量数字(9像素红色加粗)、柱子(18像素宽,高度为 stat.learned * 12 像素)和周次标签(8像素浅灰色)。
柱子的高度计算公式 stat.learned * 12 将学习数量映射为像素高度——例如学习3道菜对应36像素高的柱子,学习7道菜对应84像素高的柱子。柱子使用180度角的线性渐变(从底部暖黄色到顶部红色),营造了从下到上颜色加深的视觉效果。Row 容器设置了 alignItems(VerticalAlign.Bottom),确保所有柱子底部对齐,形成了标准的柱状图底部基线。
这种纯 UI 实现柱状图的方式有其优势和局限。优势在于无需引入额外的图表库,减少了包体积和依赖;柱状图的样式完全通过 ArkTS 样式属性控制,与整体设计风格高度统一。局限在于缺少坐标轴刻度、网格线和交互提示等专业图表功能,但对于展示简单的周度学习趋势来说已经足够。整个柱状图区域被包裹在白色卡片中,圆角14像素,内边距16像素,与其他模块的卡片风格一致。
8.3 厨艺等级展示
// 技能等级
Column() {
Text('🏆 厨艺等级').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark).width('100%')
Row() {
Column() { Text('🥇').fontSize(20); Text('中餐').fontSize(10).fontColor(COLORS.textLight).margin({ top: 4 }); Text('Lv.5').fontSize(12).fontColor(COLORS.primary).fontWeight(FontWeight.Bold) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
Column() { Text('🥈').fontSize(20); Text('西餐').fontSize(10).fontColor(COLORS.textLight).margin({ top: 4 }); Text('Lv.3').fontSize(12).fontColor(COLORS.accent).fontWeight(FontWeight.Bold) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
Column() { Text('🥉').fontSize(20); Text('日料').fontSize(10).fontColor(COLORS.textLight).margin({ top: 4 }); Text('Lv.2').fontSize(12).fontColor(COLORS.teal).fontWeight(FontWeight.Bold) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
Column() { Text('⭐').fontSize(20); Text('甜点').fontSize(10).fontColor(COLORS.textLight).margin({ top: 4 }); Text('Lv.4').fontSize(12).fontColor(COLORS.purple).fontWeight(FontWeight.Bold) }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
}.width('100%').margin({ top: 8 })
}.width('100%').padding(16).backgroundColor(COLORS.cardBg).borderRadius(14).margin({ left: 16, right: 16, top: 12, bottom: 20 })
厨艺等级展示区域以四列等宽布局展示用户在不同菜系中的技能等级。每列包含三个元素:等级图标(金、银、铜牌和星星分别对应中餐、西餐、日料、甜点)、菜系名称和等级数字。等级数字使用了不同的颜色——中餐 Lv.5 用主色红色(最高等级、最突出)、甜点 Lv.4 用紫色、西餐 Lv.3 用暖黄色、日料 Lv.2 用青色,通过颜色深浅暗示了等级的高低排序。
图标的选择也体现了等级的差异:金、银、铜牌分别对应前三名,而甜点使用星星图标,虽然等级高于日料和西餐,但图标的选择暗示了甜点技能虽然不错但还未达到奖牌级别的认可。这种用 emoji 模拟奖牌系统的设计既直观又有趣,为学习进度展示增添了游戏化元素,有助于激励用户提升各项烹饪技能。
整个学习进度模块通过三种不同的可视化方式——数字进度条、柱状图和等级展示——全面呈现了用户的学习成果。数字进度条展示了总体完成率,柱状图展示了时间维度的学习趋势,等级展示展示了技能维度的分布情况。三者相互补充,构成了完整的学习进度画像。
九、个人中心模块分析
@Builder
ProfileContent() {
Scroll() {
Column() {
Column() {
Row() {
Column() { Text('👨🍳').fontSize(40) }.width(72).height(72).borderRadius(36).backgroundColor('#33FFFFFF').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column() {
Text('美食家小明').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textWhite)
Text('厨艺等级: 大厨').fontSize(12).fontColor('#FFCCBC').margin({ top: 4 })
Text('已学30道菜 · 好评率96%').fontSize(11).fontColor('#FFCCBC').margin({ top: 2 })
}.layoutWeight(1).padding({ left: 14 }).alignItems(HorizontalAlign.Start)
}.width('100%')
Row() {
Column() { Text('30').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textWhite); Text('已学').fontSize(10).fontColor('#FFCCBC') }.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() { Text('12').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textWhite); Text('收藏').fontSize(10).fontColor('#FFCCBC') }.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() { Text('156').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textWhite); Text('烹饪次数').fontSize(10).fontColor('#FFCCBC') }.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 14 }).border({ width: { top: 1 }, color: '#33FFFFFF' }).margin({ top: 14 })
}.width('100%').padding(20).linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0], [COLORS.primary, 1]] }).borderRadius(20).margin({ left: 16, right: 16, top: 16 })
Column() {
Row() { Text('❤️').fontSize(18); Text('我的收藏').fontSize(14).fontColor(COLORS.textDark).layoutWeight(1).margin({ left: 12 }); Text('12道 >').fontSize(11).fontColor(COLORS.textLight) }.width('100%').padding(14)
Row() { Text('📝').fontSize(18); Text('烹饪笔记').fontSize(14).fontColor(COLORS.textDark).layoutWeight(1).margin({ left: 12 }); Text('8篇 >').fontSize(11).fontColor(COLORS.textLight) }.width('100%').padding(14)
Row() { Text('🏆').fontSize(18); Text('我的成就').fontSize(14).fontColor(COLORS.textDark).layoutWeight(1).margin({ left: 12 }); Text('6枚 >').fontSize(11).fontColor(COLORS.textLight) }.width('100%').padding(14)
Row() { Text('🛒').fontSize(18); Text('购物清单').fontSize(14).fontColor(COLORS.textDark).layoutWeight(1).margin({ left: 12 }); Text(' >').fontSize(14).fontColor(COLORS.textLight) }.width('100%').padding(14)
Row() { Text('⚙️').fontSize(18); Text('设置').fontSize(14).fontColor(COLORS.textDark).layoutWeight(1).margin({ left: 12 }); Text(' >').fontSize(14).fontColor(COLORS.textLight) }.width('100%').padding(14)
}.width('100%').backgroundColor(COLORS.cardBg).borderRadius(14).margin({ left: 16, right: 16, top: 12, bottom: 20 })
}.width('100%')
}.scrollBar(BarState.Off).layoutWeight(1)
}
个人中心模块分为上下两部分:用户信息卡片和功能菜单列表。用户信息卡片采用了与今日推荐大卡片相同的渐变设计——135度角从 primaryDark 到 primary 的线性渐变,圆角20像素,形成了一个视觉上引人注目的头部区域。
卡片内部上半部分是头像和用户信息的横向布局。头像是一个72x72像素的圆形容器,背景使用半透明白色(#33FFFFFF),内含40像素大小的厨师 emoji。用户信息包括用户名"美食家小明"(18像素白色加粗)、厨艺等级"大厨"和成就描述"已学30道菜 · 好评率96%",后两者使用浅橙色(#FFCCBC)显示。
卡片内部下半部分是三列统计数据——“已学”(30)、“收藏”(12)和"烹饪次数"(156),通过顶部1像素的半透明白色边框线与上半部分分隔。数字使用18像素白色加粗,标签使用10像素浅橙色。这三项数据与学习进度模块的数据保持一致(已学30道菜),体现了应用内数据的统一性。
功能菜单列表是一个白色卡片容器,内部包含5行菜单项:我的收藏、烹饪笔记、我的成就、购物清单和设置。每行采用左图标、中间标题(layoutWeight(1) 占据剩余空间)、右箭头提示的布局结构。每个菜单项的右侧显示了该项的数量信息(如"12道"、“8篇”、“6枚”),部分项右侧仅显示">"箭头。这种列表布局是移动端个人中心页面的经典设计模式,用户可以快速扫描各项功能入口。
整个个人中心模块被包裹在 Scroll 容器中,支持纵向滚动。用户信息卡片的渐变设计与今日推荐卡片形成了视觉呼应,使得应用在不同模块间保持了统一的设计语言。功能菜单的图标和标题风格统一,每行之间的间距通过 padding(14) 控制,整体视觉简洁有序。
十、弹窗系统分析
10.1 新增菜谱弹窗
@Builder
AddRecipeOverlay() {
Column() {
Column() {
Text('📝 添加菜谱').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark).width('100%')
Column() {
Text('菜谱名称').fontSize(13).fontColor(COLORS.textLight).width('100%')
Row() { Text('输入菜谱名称...').fontSize(14).fontColor(COLORS.textLight) }.width('100%').height(40).padding({ left: 12 }).backgroundColor(COLORS.bg).borderRadius(8).margin({ top: 4 }).alignItems(VerticalAlign.Center)
}.width('100%').margin({ top: 16 })
Column() {
Text('分类').fontSize(13).fontColor(COLORS.textLight).width('100%')
Row() {
ForEach(this.categories.slice(1, 5), (cat: string, index: number) => {
Text(cat).fontSize(11).fontColor(index === 0 ? COLORS.textWhite : COLORS.primary).padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(10).backgroundColor(index === 0 ? COLORS.primary : COLORS.bg).margin({ right: 6, top: 4 })
})
}.width('100%').margin({ top: 4 })
}.width('100%').margin({ top: 12 })
Row() {
Column() { Text('难度').fontSize(13).fontColor(COLORS.textLight).width('100%'); Row() { Text('简单').fontSize(14).fontColor(COLORS.textDark) }.width('100%').height(40).padding({ left: 12 }).backgroundColor(COLORS.bg).borderRadius(8).margin({ top: 4 }).alignItems(VerticalAlign.Center) }.layoutWeight(1)
Column() { Text('用时').fontSize(13).fontColor(COLORS.textLight).width('100%'); Row() { Text('30分钟').fontSize(14).fontColor(COLORS.textDark) }.width('100%').height(40).padding({ left: 12 }).backgroundColor(COLORS.bg).borderRadius(8).margin({ top: 4 }).alignItems(VerticalAlign.Center) }.layoutWeight(1).margin({ left: 12 })
}.width('100%').margin({ top: 12 })
Row() {
Text('取消').fontSize(15).fontColor(COLORS.textLight).layoutWeight(1).textAlign(TextAlign.Center).padding(10).backgroundColor(COLORS.bg).borderRadius(8).onClick(() => { this.showAddRecipe = false })
Text('添加').fontSize(15).fontColor(COLORS.textWhite).layoutWeight(1).textAlign(TextAlign.Center).padding(10).backgroundColor(COLORS.primary).borderRadius(8).margin({ left: 12 }).onClick(() => { this.showAddRecipe = false })
}.width('100%').margin({ top: 20 })
}.width('85%').padding(20).backgroundColor(COLORS.cardBg).borderRadius(20).onClick(() => {})
}.width('100%').height('100%').backgroundColor('#80000000').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
新增菜谱弹窗是一个覆盖在全屏之上的模态对话框。外层 Column 设置了 width('100%') 和 height('100%'),确保覆盖整个屏幕,背景色使用半透明黑色(#80000000,即黑色50%透明度),营造出遮罩效果。通过 justifyContent(FlexAlign.Center) 和 alignItems(HorizontalAlign.Center) 将弹窗内容居中显示。
弹窗主体是一个宽度85%的白色圆角卡片(圆角20像素),内边距20像素。卡片内部从上到下依次包含:标题"添加菜谱"、菜谱名称输入区域、分类选择区域、难度和用时输入区域、取消和添加按钮。菜谱名称输入框是一个高度40像素、圆角8像素的浅黄色背景容器,内含占位文字"输入菜谱名称…"。分类选择区域使用 ForEach 渲染4个分类标签,第一个标签默认选中(红色背景白色文字)。
难度和用时并排排列,各占一半宽度,中间通过 margin({ left: 12 }) 保持间距。底部按钮区域,"取消"按钮使用浅黄色背景和浅灰色文字,"添加"按钮使用红色背景和白色文字,两个按钮通过 layoutWeight(1) 等分宽度。每个按钮的 onClick 事件都将 this.showAddRecipe 设置为 false,关闭弹窗。
值得注意的是,弹窗主体卡片设置了 onClick(() => {}) 空事件处理器。这是一个阻止事件冒泡的技巧——当用户点击弹窗主体区域时,空的事件处理器会拦截点击事件,防止事件冒泡到外层遮罩层。虽然没有为遮罩层添加关闭弹窗的点击事件,但这种设计模式为未来扩展"点击遮罩层关闭弹窗"功能预留了实现基础。
10.2 编辑菜谱弹窗
@Builder
EditRecipeOverlay() {
Column() {
Column() {
Text('✏️ 编辑菜谱').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark).width('100%')
Column() {
Text('菜谱名称').fontSize(13).fontColor(COLORS.textLight).width('100%')
Row() { Text('红烧肉').fontSize(14).fontColor(COLORS.textDark) }.width('100%').height(40).padding({ left: 12 }).backgroundColor(COLORS.bg).borderRadius(8).margin({ top: 4 }).alignItems(VerticalAlign.Center)
}.width('100%').margin({ top: 16 })
Row() {
Column() { Text('难度').fontSize(13).fontColor(COLORS.textLight).width('100%'); Row() { Text('中等').fontSize(14).fontColor(COLORS.textDark) }.width('100%').height(40).padding({ left: 12 }).backgroundColor(COLORS.bg).borderRadius(8).margin({ top: 4 }).alignItems(VerticalAlign.Center) }.layoutWeight(1)
Column() { Text('用时').fontSize(13).fontColor(COLORS.textLight).width('100%'); Row() { Text('60分钟').fontSize(14).fontColor(COLORS.textDark) }.width('100%').height(40).padding({ left: 12 }).backgroundColor(COLORS.bg).borderRadius(8).margin({ top: 4 }).alignItems(VerticalAlign.Center) }.layoutWeight(1).margin({ left: 12 })
}.width('100%').margin({ top: 12 })
Column() {
Text('描述').fontSize(13).fontColor(COLORS.textLight).width('100%')
Row() { Text('经典家常菜,肥而不腻入口即化').fontSize(14).fontColor(COLORS.textDark) }.width('100%').height(60).padding(12).backgroundColor(COLORS.bg).borderRadius(8).margin({ top: 4 })
}.width('100%').margin({ top: 12 })
Row() {
Text('取消').fontSize(15).fontColor(COLORS.textLight).layoutWeight(1).textAlign(TextAlign.Center).padding(10).backgroundColor(COLORS.bg).borderRadius(8).onClick(() => { this.showEditRecipe = false })
Text('保存').fontSize(15).fontColor(COLORS.textWhite).layoutWeight(1).textAlign(TextAlign.Center).padding(10).backgroundColor(COLORS.primary).borderRadius(8).margin({ left: 12 }).onClick(() => { this.showEditRecipe = false })
}.width('100%').margin({ top: 20 })
}.width('85%').padding(20).backgroundColor(COLORS.cardBg).borderRadius(20).onClick(() => {})
}.width('100%').height('100%').backgroundColor('#80000000').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
编辑菜谱弹窗与新增菜谱弹窗在结构上高度相似,但有几个关键区别。首先,标题改为"编辑菜谱",并使用了"✏️"图标替代"📝"。其次,各个输入框中预填了当前编辑菜谱的数据——菜谱名称显示"红烧肉"(对应 editingRecipeId 为1的菜谱),难度显示"中等",用时显示"60分钟",描述显示"经典家常菜,肥而不腻入口即化"。这些预填数据使得编辑弹窗与新增弹窗在视觉上有所区分,也体现了编辑功能的业务逻辑。
编辑弹窗还额外增加了一个"描述"输入区域,高度60像素(比名称输入框的40像素更高),用于展示和编辑菜谱的详细描述。底部按钮的文字从"添加"改为"保存",语义上更符合编辑操作。"取消"和"保存"按钮的 onClick 事件都将 this.showEditRecipe 设置为 false,关闭弹窗。
10.3 删除确认弹窗
@Builder
DeleteRecipeOverlay() {
Column() {
Column() {
Text('🗑️').fontSize(40).margin({ bottom: 12 })
Text('确认删除菜谱?').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textDark)
Text('删除后菜谱及步骤将无法恢复。').fontSize(13).fontColor(COLORS.textLight).textAlign(TextAlign.Center).margin({ top: 8 })
Row() {
Text('取消').fontSize(15).fontColor(COLORS.textLight).layoutWeight(1).textAlign(TextAlign.Center).padding(12).backgroundColor(COLORS.bg).borderRadius(8).onClick(() => { this.showDeleteRecipe = false })
Text('删除').fontSize(15).fontColor(COLORS.textWhite).layoutWeight(1).textAlign(TextAlign.Center).padding(12).backgroundColor(COLORS.danger).borderRadius(8).margin({ left: 12 }).onClick(() => { this.showDeleteRecipe = false })
}.width('100%').margin({ top: 20 })
}.width('75%').padding(24).backgroundColor(COLORS.cardBg).borderRadius(20).alignItems(HorizontalAlign.Center).onClick(() => {})
}.width('100%').height('100%').backgroundColor('#80000000').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
删除确认弹窗是三个弹窗中最简洁的一个,宽度仅为75%(比新增和编辑弹窗的85%更窄),采用了居中对话的设计风格。弹窗顶部是一个40像素大小的垃圾桶图标,下方是标题"确认删除菜谱?“和提示文字"删除后菜谱及步骤将无法恢复。”,明确告知用户删除操作的不可逆性。
底部按钮区域的"删除"按钮使用了危险红色(COLORS.danger)背景,与"取消"按钮的浅黄色背景形成强烈对比,通过颜色语义提醒用户这是一个具有破坏性的操作。按钮内边距设置为12像素(比新增和编辑弹窗的10像素更大),使得按钮的点击区域更大,减少误触概率。
三个弹窗共同构成了应用的 CRUD(创建、读取、更新、删除)操作界面。虽然当前实现中,各按钮的 onClick 事件仅关闭弹窗而未实际修改数据,但弹窗的 UI 结构和交互逻辑已经完整搭建,为接入真实的数据操作 API 提供了现成的界面基础。弹窗的遮罩层、居中布局、事件冒泡控制等设计模式都是移动端弹窗开发的最佳实践。
十一、主构建函数分析
build() {
Column() {
Row() {
Text('🍳 美味厨房').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.textWhite)
Text('🔍').fontSize(18).fontColor(COLORS.textWhite).margin({ left: 'auto' })
}.width('100%').height(50).padding({ left: 16, right: 16 }).backgroundColor(COLORS.primary).alignItems(VerticalAlign.Center)
Column() {
if (this.currentTab === 0) {
this.RecommendContent()
} else if (this.currentTab === 1) {
this.RecipeLibContent()
} else if (this.currentTab === 2) {
this.IngredientContent()
} else if (this.currentTab === 3) {
this.ProgressContent()
} else {
this.ProfileContent()
}
}.width('100%').layoutWeight(1)
this.TabBar()
if (this.showAddRecipe) { this.AddRecipeOverlay() }
if (this.showEditRecipe) { this.EditRecipeOverlay() }
if (this.showDeleteRecipe) { this.DeleteRecipeOverlay() }
}.width('100%').height('100%').backgroundColor(COLORS.bg)
}
build() 函数是 ArkTS 组件的核心入口,它定义了组件的 UI 结构。CookingApp 的 build() 函数构建了一个从上到下三段式的布局:顶部标题栏、中间内容区和底部导航栏,再加上条件渲染的弹窗层。
顶部标题栏是一个高度50像素的 Row 容器,背景色为主色红色,内含应用名称"美味厨房"(20像素白色加粗)和右侧搜索图标。搜索图标通过 margin({ left: 'auto' }) 推至行末右对齐。标题栏的左右内边距设置为16像素,确保内容不会紧贴屏幕边缘。这个标题栏是全局共享的,不会随着 Tab 切换而变化,为应用提供了统一的品牌标识。
中间内容区是一个 layoutWeight(1) 的 Column 容器,占据标题栏和导航栏之间的所有剩余空间。容器内部使用 if-else 条件语句根据 this.currentTab 的值调用不同的 @Builder 函数来渲染对应的内容模块。这种基于条件渲染的页面切换方式简单直接——当 currentTab 的值改变时,ArkTS 框架会自动重新执行 build() 函数,销毁旧的内容组件并创建新的内容组件。虽然这种方式在页面切换时没有过渡动画,但对于原型开发和简单应用来说已经足够。
底部导航栏通过 this.TabBar() 直接调用 @Builder 函数渲染。弹窗层使用三个独立的 if 条件语句控制——当对应的 @State 变量为 true 时渲染弹窗,为 false 时不渲染。由于弹窗使用全屏遮罩层覆盖在所有内容之上,且通过 justifyContent(FlexAlign.Center) 居中显示,因此弹窗的位置不受其他布局元素的影响。三个弹窗使用独立的条件判断而非 if-else 链,这意味着理论上可以同时显示多个弹窗(虽然在当前交互设计中不会出现这种情况)。
整个 build() 函数的最外层 Column 设置了 width('100%') 和 height('100%'),确保组件填满整个屏幕,背景色设置为 COLORS.bg(极浅暖黄色),为应用提供了统一的背景基调。这种三段式布局结构——固定标题栏 + 弹性内容区 + 固定导航栏——是移动端应用最经典的页面布局模式,既保证了核心导航元素的固定可见性,又为内容展示提供了最大的空间。
十二、模块功能对比表格
| 对比维度 | 今日推荐 | 菜谱库 | 食材管理 | 学习进度 | 我的 |
|---|---|---|---|---|---|
| 核心功能 | 个性化菜谱推荐与浏览 | 菜谱搜索与分类浏览、步骤教学 | 食材库存与保质期管理 | 学习数据可视化与技能展示 | 用户信息与功能入口 |
| 主要数据源 | recipes 数组 | recipes + cookSteps + categories | ingredients 数组 | learnStats 数组 | 静态用户数据 |
| 视觉亮点 | 渐变大卡片 + 分类标签横滑 | 搜索栏 + 分类网格 + 步骤详解 | 统计概览 + 状态标签 | 进度条 + 柱状图 + 等级展示 | 渐变用户卡片 + 菜单列表 |
| 交互元素 | 分类切换、编辑/删除按钮 | 搜索、新增菜谱、步骤浏览 | 食材状态查看 | 数据浏览 | 菜单项点击 |
| 数据可视化 | 卡路里、评分、烹饪次数展示 | 步骤用时展示 | 统计数字展示 | 进度条、柱状图、等级 | 统计数字展示 |
| 弹窗关联 | 编辑弹窗、删除弹窗 | 新增弹窗 | 无 | 无 | 无 |
| 滚动方式 | 纵向滚动列表 | 纵向滚动步骤列表 | 纵向滚动食材列表 | 整页纵向滚动 | 整页纵向滚动 |
| 颜色应用 | 分类色 + 难度色 | 分类色 | 状态色(红/蓝/绿) | 语义色 + 渐变色 | 渐变色 + 文字色阶 |
| 信息密度 | 高(每卡片含6+信息项) | 中高(步骤卡片含3信息项) | 中(每卡片含4信息项) | 中(图表+等级) | 中低(菜单列表) |
| 教学价值 | 菜谱发现与选择 | 详细步骤教学 | 食材管理辅助 | 学习激励与反馈 | 个人数据管理 |
| 卡片圆角 | 14px(列表)+ 20px(大卡片) | 14px(步骤)+ 12px(分类) | 12px | 14-16px | 20px(用户卡片)+ 14px(菜单) |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
总结
通过对这款基于 HarmonyOS ArkTS 开发的"美味厨房"菜谱教学应用的全面深度解析,我们可以看到,一个优秀的移动应用不仅需要功能完善,更需要在架构设计、视觉体验和代码质量上达到高度的统一。该应用以食欲红和暖黄为主色调,通过精心设计的配色方案营造出了温暖而充满活力的烹饪氛围,每一个颜色选择都经过深思熟虑——从主色的品牌识别到语义色的状态指示,从渐变效果的视觉层次到文字色阶的信息层级,形成了一套完整且自洽的视觉设计系统。
在技术架构层面,该应用充分展现了 ArkTS 声明式 UI 范式的优势。通过 @State 装饰器实现的响应式状态管理,使得 UI 更新与数据变化自动同步,开发者无需手动操作 DOM 或管理渲染周期;通过 @Builder 装饰器封装的可复用构建函数,将复杂的界面拆分为多个高内聚的模块,每个模块职责单一、边界清晰;通过 interface 定义的严格类型系统,在编译阶段就消除了大量潜在的类型错误。五个核心数据接口(RecipeInfo、IngredientInfo、StepInfo、LearnStat、ColorToken)的设计既满足了当前功能需求,又为未来扩展预留了空间。这种以类型安全为基础、以响应式为核心、以模块化为手段的架构设计,是 HarmonyOS 应用开发的最佳实践。
从功能完整性来看,该应用覆盖了烹饪学习的全链路需求。今日推荐模块通过大卡片推荐和热门列表帮助用户发现菜谱;菜谱库模块通过搜索、分类和详细步骤教学帮助用户学习烹饪;食材管理模块帮助用户追踪食材状态、减少浪费;学习进度模块通过多种可视化方式激励用户持续学习;个人中心模块提供了个人信息管理和功能入口。五个模块之间数据互通、逻辑互补,形成了一个完整的烹饪学习生态闭环。特别是菜谱库模块中的红烧肉8步教程,每步都配有专业小贴士,体现了应用的教学专业性。
柱状图通过纯 UI 组件(Column + 动态高度 + 渐变色)实现,无需引入第三方图表库;进度条通过百分比宽度模拟,简洁高效;弹窗系统通过全屏遮罩层 + 居中卡片 + 事件冒泡控制实现,符合移动端弹窗设计规范;分类颜色和难度颜色通过映射表管理,扩展性好且维护方便。这些实现技巧虽然看似简单,但每一处都体现了开发者对用户体验和代码质量的追求。对于希望深入学习 HarmonyOS ArkTS 开发的开发者来说,这款应用的代码是一个非常好的学习素材——它涵盖了接口定义、状态管理、列表渲染、条件渲染、样式绑定、事件处理、弹窗设计等 ArkTS 开发的核心知识点,同时也展示了如何在一个组件中优雅地组织复杂的多模块应用。
更多推荐
所有评论(0)