鸿蒙NEXT开发实战:基于ArkUI实现菜谱搜索分类应用
一、项目概述
1.1 项目简介
本项目基于HarmonyOS NEXT(API 20+)、ArkTS及ArkUI声明式框架开发一款轻量化菜谱展示应用。应用主打轻量化、简洁实用的特点,适配鸿蒙原生交互规范,支持菜谱分类筛选、关键词模糊搜索、菜谱详情查看、多页面路由跳转等核心功能,非常适合鸿蒙开发入门开发者学习列表渲染、数据过滤、页面传参、组件布局等核心知识点。
项目摒弃复杂第三方组件,全部使用系统原生组件开发,代码精简易读、可直接运行,适配移动端自适应布局,卡片式UI设计符合主流App视觉风格。
1.2 应用核心场景
-
新手烹饪学习:用户可查看完整的食材清单、详细烹饪步骤、制作时长与难度
-
精准筛选菜谱:支持菜系分类筛选、菜名/食材双维度关键词搜索
-
详情沉浸式查看:独立详情页展示完整菜谱信息,支持页面返回交互
1.3 技术栈说明
-
系统版本:HarmonyOS NEXT(API 20及以上)
-
开发语言:ArkTS
-
UI框架:ArkUI 声明式UI
-
核心技术:页面路由、数据过滤、实时搜索、卡片布局、滚动容器、条件渲染
二、核心技术知识点详解
2.1 鸿蒙页面路由与参数传递
多页面应用的核心基础就是页面路由,鸿蒙通过内置router模块实现页面跳转、参数传递、页面返回等能力。本项目主要实现首页跳转到菜谱详情页,并携带菜谱唯一标识与名称参数。
核心路由方法说明:
-
pushUrl:保留当前页面,跳转至新页面(常用详情页跳转)
-
back:关闭当前页面,返回上一级页面
-
getParams:接收上一个页面传递的参数
-
replaceUrl:替换当前页面,无返回历史记录(适合登录页跳转)
2.2 TextInput实时搜索组件
TextInput是鸿蒙原生单行输入组件,本项目用于实现顶部搜索栏,通过onChange事件监听输入内容,实现实时模糊搜索,无需点击搜索按钮,输入即筛选数据。同时通过圆角、内边距、背景色属性实现现代化搜索框样式。
2.3 Scroll滚动容器组件
Scroll组件为页面提供局部滚动能力,区别于页面全局滚动,可指定固定滚动区域,适配分类标签、长文本内容的滚动需求,有效解决内容溢出屏幕的布局问题。
2.4 多条件数据过滤逻辑
项目核心业务逻辑,整合分类筛选和关键词搜索双条件过滤。支持根据菜系分类精准筛选,同时支持菜名、食材的双向模糊匹配,并且统一大小写匹配规则,避免大小写导致的搜索失效问题。
2.5 数组与字符串高阶处理
基于ArkTS数组方法filter、some、find实现数据筛选与查找,结合字符串toLowerCase、includes方法实现不区分大小写的模糊匹配,是鸿蒙列表类项目的通用核心写法。
2.6 多形式条件渲染
适配不同业务场景的渲染方式:if-else整体布局渲染、三元运算符文本状态渲染、短路运算局部组件渲染,解决空数据、加载状态、异常状态的页面展示问题。
三、项目页面结构设计
3.1 首页结构(Index页面)
顶部标题栏 + 全局搜索框 + 菜系分类标签栏 + 菜谱卡片列表,层级清晰,交互逻辑自上而下,是典型的列表展示类App布局结构。
3.2 详情页结构(RecipeDetail页面)
导航返回栏 + 菜谱封面图 + 基础信息区(分类/难度/时长) + 食材清单区 + 分步烹饪步骤区,完整展示菜谱全量信息。
四、完整项目源码实现
本项目包含两个核心页面:首页(Index.ets)、菜谱详情页(RecipeDetail.ets),附带完整数据模型、业务逻辑、样式布局,可直接新建鸿蒙项目替换代码运行。
4.1 全局数据模型定义(可直接写在页面顶部)
// 菜谱数据模型
interface Recipe {
id: number;
name: string;
category: string;
ingredients: string[];
steps: string[];
cookTime: number;
difficulty: string;
image: string;
}
4.2 首页完整代码(Index.ets)
import router from ‘@ohos.router’;
// 菜系分类数组
const CATEGORY_LIST: string[] = [‘全部’, ‘家常菜’, ‘川菜’, ‘粤菜’, ‘湘菜’];
@Entry
@Component
struct Index {
// 搜索关键词
@State searchKeyword: string = '';
// 选中分类
@State selectedCategory: string = '全部';
// 菜谱数据源
@State recipeList: Recipe[] = [];
// 页面初始化加载数据
aboutToAppear() {
this.recipeList = [
{
id: 1,
name: '家常红烧肉',
category: '家常菜',
ingredients: ['五花肉500g', '生姜3片', '大葱2根', '八角2个', '桂皮1小块', '冰糖适量', '生抽2勺', '老抽1勺', '料酒1勺'],
steps: ['五花肉切块,冷水下锅焯水去除血沫', '锅中少油,放入冰糖小火炒出枣红色糖色', '下入五花肉快速翻炒均匀上色', '加入葱姜八角桂皮翻炒出香味', '加入生抽、老抽、料酒调味,倒入热水没过食材', '大火烧开转小火慢炖60分钟,最后大火收汁即可'],
cookTime: 90,
difficulty: '中等',
image: '红烧肉'
},
{
id: 2,
name: '宫保鸡丁',
category: '川菜',
ingredients: ['鸡胸肉300g', '熟花生米50g', '干辣椒5个', '花椒10粒', '葱姜蒜适量', '黄瓜半根', '胡萝卜半根'],
steps: ['鸡胸肉切丁,加少许盐、料酒、淀粉腌制15分钟', '冷锅少油,小火炸熟花生米捞出备用', '锅底留油,爆香花椒、干辣椒、葱姜蒜', '下入鸡丁大火翻炒至变色熟透', '加入胡萝卜、黄瓜丁翻炒断生', '调入料汁,最后加入花生米翻炒均匀出锅'],
cookTime: 30,
difficulty: '简单',
image: '宫保鸡丁'
},
{
id: 3,
name: '白切鸡',
category: '粤菜',
ingredients: ['三黄鸡1只', '生姜适量', '葱适量', '香菜少许', '生抽', '香油'],
steps: ['三黄鸡处理干净,冷水下锅,加入葱姜', '大火煮开后转小火浸煮20分钟', '关火焖10分钟后捞出过冰水', '鸡肉切块摆盘,调制生抽香油料汁', '淋上料汁,撒上香菜即可食用'],
cookTime: 40,
difficulty: '简单',
image: '白切鸡'
},
{
id: 4,
name: '剁椒鱼头',
category: '湘菜',
ingredients: ['胖头鱼鱼头1个', '剁椒2勺', '生姜大蒜适量', '葱花少许', '蒸鱼豉油', '食用油'],
steps: ['鱼头处理干净,对半切开摆盘', '铺上切好的姜片蒜末,铺满剁椒', '水开后上锅大火蒸15分钟', '倒掉盘中多余汁水,淋上蒸鱼豉油', '撒上葱花,浇上热油激发出香味即可'],
cookTime: 50,
difficulty: '中等',
image: '剁椒鱼头'
}
]
}
// 多条件过滤菜谱数据
private filterRecipeData(): Recipe[] {
let resultList = this.recipeList;
// 分类筛选
if (this.selectedCategory !== '全部') {
resultList = resultList.filter(item => item.category === this.selectedCategory)
}
// 关键词模糊搜索(菜名+食材,不区分大小写)
if (this.searchKeyword.trim() !== '') {
const key = this.searchKeyword.trim().toLowerCase();
resultList = resultList.filter(item => {
const nameMatch = item.name.toLowerCase().includes(key);
const ingredientMatch = item.ingredients.some(ing => ing.toLowerCase().includes(key));
return nameMatch || ingredientMatch;
})
}
return resultList;
}
// 根据难度获取对应颜色
private getDifficultyColor(diff: string): string {
switch (diff) {
case '简单':
return '#10b981';
case '中等':
return '#f59e0b';
default:
return '#ef4444';
}
}
// 跳转菜谱详情页
private goRecipeDetail(item: Recipe) {
router.pushUrl({
url: 'pages/RecipeDetail',
params: {
recipeId: item.id,
recipeName: item.name
}
})
}
// 分类标签组件
@Builder CategoryItem(title: string) {
Text(title)
.fontSize(14)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.selectedCategory === title ? '#2563eb' : '#f1f5f9')
.fontColor(this.selectedCategory === title ? '#fff' : '#333')
.onClick(() => {
this.selectedCategory = title;
})
}
build() {
Column() {
// 顶部标题栏
Text('美食菜谱大全')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
.padding({ left: 16, top: 20, bottom: 16 })
// 搜索框
TextInput({
placeholder: '搜索菜谱、食材...',
text: this.searchKeyword
})
.width('92%')
.height(44)
.borderRadius(22)
.backgroundColor('#ffffff')
.padding({ left: 16, right: 16 })
.shadow({ radius: 4, color: '#00000010' })
.onChange((val: string) => {
this.searchKeyword = val;
})
// 分类滚动栏
Scroll() {
Row() {
ForEach(CATEGORY_LIST, (item: string) => {
this.CategoryItem(item)
.margin({ right: 10 })
})
}
.padding({ left: 16, right: 16, top: 16 })
}
.width('100%')
.scrollable(ScrollDirection.Horizontal)
// 菜谱列表
List() {
const filterList = this.filterRecipeData();
if (filterList.length === 0) {
ListItem() {
Text('暂无匹配菜谱')
.fontSize(16)
.fontColor('#999')
.margin({ top: 100 })
.width('100%')
.textAlign(TextAlign.Center)
}
} else {
ForEach(filterList, (item: Recipe) => {
ListItem() {
// 菜谱卡片
Column() {
// 卡片顶部图片区域
Column() {
Text(item.image)
.fontSize(28)
.fontColor('#fff')
}
.width('100%')
.height(160)
.borderRadius({ topLeft: 12, topRight: 12 })
.backgroundColor('#f97316')
.justifyContent(FlexAlign.Center)
// 卡片信息区域
Column() {
Text(item.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
Row() {
Text(item.category)
.fontSize(12)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor('#f1f5f9')
.borderRadius(4)
.fontColor('#64748b')
Text(item.difficulty)
.fontSize(12)
.fontColor(this.getDifficultyColor(item.difficulty))
.margin({ left: 10 })
Text(`${item.cookTime}分钟`)
.fontSize(12)
.fontColor('#666')
.margin({ left: 10 })
}
.width('100%')
.margin({ top: 8 })
Text(`共${item.ingredients.length}种食材`)
.fontSize(13)
.fontColor('#999')
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(12)
}
.width('92%')
.backgroundColor('#fff')
.borderRadius(12)
.shadow({ radius: 6, color: '#00000015' })
.margin({ top: 12 })
.onClick(() => this.goRecipeDetail(item))
}
})
}
}
.layoutWeight(1)
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#f8f9fa')
}
}
4.3 详情页完整代码(RecipeDetail.ets)
import router from ‘@ohos.router’;
@Entry
@Component
struct RecipeDetail {
@State recipeId: number = 0;
@State recipeName: string = '';
// 模拟完整菜谱数据,实际项目可通过id请求/查询数据
@State recipeDetail: Recipe | null = null;
// 根据ID匹配菜谱数据
private getRecipeDetailById(id: number): Recipe | undefined {
const list: Recipe[] = [
{
id: 1,
name: '家常红烧肉',
category: '家常菜',
ingredients: ['五花肉500g', '生姜3片', '大葱2根', '八角2个', '桂皮1小块', '冰糖适量', '生抽2勺', '老抽1勺', '料酒1勺'],
steps: ['五花肉切块,冷水下锅焯水去除血沫', '锅中少油,放入冰糖小火炒出枣红色糖色', '下入五花肉快速翻炒均匀上色', '加入葱姜八角桂皮翻炒出香味', '加入生抽、老抽、料酒调味,倒入热水没过食材', '大火烧开转小火慢炖60分钟,最后大火收汁即可'],
cookTime: 90,
difficulty: '中等',
image: '红烧肉'
},
{
id: 2,
name: '宫保鸡丁',
category: '川菜',
ingredients: ['鸡胸肉300g', '熟花生米50g', '干辣椒5个', '花椒10粒', '葱姜蒜适量', '黄瓜半根', '胡萝卜半根'],
steps: ['鸡胸肉切丁,加少许盐、料酒、淀粉腌制15分钟', '冷锅少油,小火炸熟花生米捞出备用', '锅底留油,爆香花椒、干辣椒、葱姜蒜', '下入鸡丁大火翻炒至变色熟透', '加入胡萝卜、黄瓜丁翻炒断生', '调入料汁,最后加入花生米翻炒均匀出锅'],
cookTime: 30,
difficulty: '简单',
image: '宫保鸡丁'
},
{
id: 3,
name: '白切鸡',
category: '粤菜',
ingredients: ['三黄鸡1只', '生姜适量', '葱适量', '香菜少许', '生抽', '香油'],
steps: ['三黄鸡处理干净,冷水下锅,加入葱姜', '大火煮开后转小火浸煮20分钟', '关火焖10分钟后捞出过冰水', '鸡肉切块摆盘,调制生抽香油料汁', '淋上料汁,撒上香菜即可食用'],
cookTime: 40,
difficulty: '简单',
image: '白切鸡'
},
{
id: 4,
name: '剁椒鱼头',
category: '湘菜',
ingredients: ['胖头鱼鱼头1个', '剁椒2勺', '生姜大蒜适量', '葱花少许', '蒸鱼豉油', '食用油'],
steps: ['鱼头处理干净,对半切开摆盘', '铺上切好的姜片蒜末,铺满剁椒', '水开后上锅大火蒸15分钟', '倒掉盘中多余汁水,淋上蒸鱼豉油', '撒上葱花,浇上热油激发出香味即可'],
cookTime: 50,
difficulty: '中等',
image: '剁椒鱼头'
}
]
return list.find(item => item.id === id)
}
// 页面加载接收路由参数
aboutToAppear() {
const params = router.getParams() as Record<string, Object>;
if (params) {
this.recipeId = params.recipeId as number;
this.recipeName = params.recipeName as string;
this.recipeDetail = this.getRecipeDetailById(this.recipeId) || null;
}
}
// 返回上一页
private backPrevPage() {
router.back();
}
build() {
Column() {
// 顶部导航栏
Row() {
Text('←')
.fontSize(22)
.onClick(() => this.backPrevPage())
Text(this.recipeName)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ left: 12 })
}
.width('100%')
.padding({ left: 16, top: 10, bottom: 10 })
.alignItems(VerticalAlign.Center)
// 详情主体内容
if (this.recipeDetail) {
Scroll() {
Column() {
// 菜谱封面
Column() {
Text(this.recipeDetail.image)
.fontSize(32)
.fontColor('#fff')
}
.width('92%')
.height(180)
.borderRadius(12)
.backgroundColor('#f97316')
.justifyContent(FlexAlign.Center)
// 基础信息
Column() {
Row() {
Text(`分类:${this.recipeDetail.category}`)
.fontSize(14)
.fontColor('#666')
Text(`难度:${this.recipeDetail.difficulty}`)
.fontSize(14)
.fontColor('#666')
.margin({ left: 20 })
}
Text(`制作时长:${this.recipeDetail.cookTime}分钟`)
.fontSize(14)
.fontColor('#666')
.width('100%')
.margin({ top: 8 })
}
.width('92%')
.margin({ top: 16 })
// 食材清单
Column() {
Text('🥬 食材清单')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
ForEach(this.recipeDetail.ingredients, (ing: string) => {
Text(`• ${ing}`)
.fontSize(14)
.width('100%')
.margin({ top: 6 })
.fontColor('#333')
})
}
.width('92%')
.margin({ top: 20 })
// 烹饪步骤
Column() {
Text('🍳 烹饪步骤')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
ForEach(this.recipeDetail.steps, (step: string, index: number) => {
Column() {
Text(`步骤 ${index + 1}`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#2563eb')
.width('100%')
Text(step)
.fontSize(14)
.fontColor('#444')
.width('100%')
.margin({ top: 4 })
}
.width('100%')
.margin({ top: 12 })
})
}
.width('92%')
.margin({ top: 20, bottom: 30 })
}
.width('100%')
}
.layoutWeight(1)
} else {
Text('数据加载中...')
.fontSize(16)
.fontColor('#999')
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.backgroundColor('#f8f9fa')
}
}



五、常见问题排查与解决方案
5.1 搜索无匹配结果
问题原因:大小写敏感、仅匹配单一字段、空格干扰
解决方案:统一转换小写、去除首尾空格、同时匹配菜名和食材字段,代码中已内置该兼容逻辑。
5.2 详情页参数获取失败
问题原因:传递非基础类型参数、未做空值判断
解决方案:仅传递数字、字符串基础类型,接收参数时增加空值校验,避免报错。
5.3 分类切换列表不刷新
问题原因:列表绑定原始数据源,未绑定过滤后的数据
解决方案:List组件遍历过滤后的新数组,而非原始静态数组,实时响应筛选变化。
六、项目扩展优化方向
-
功能扩展:新增菜谱收藏、一键分享、购物清单生成、烹饪计时器、视频教程嵌入
-
体验优化:新增搜索历史记录、热门搜索、输入防抖、空状态优化
-
性能优化:将ForEach替换为LazyForEach实现懒加载,适配大数据量菜谱列表
-
UI优化:添加网络图片加载、骨架屏、卡片点击动画效果
七、项目总结
本教程从零实现了一款完整的鸿蒙菜谱应用,覆盖了ArkUI开发中页面路由传参、实时搜索筛选、多条件数据过滤、卡片式布局、滚动容器、条件渲染等高频核心知识点。项目代码规范、逻辑清晰、无冗余,适配HarmonyOS NEXT最新API,所有代码可直接编译运行。
本项目的搜索、筛选、列表渲染逻辑可通用适配商城、资讯、工具类等绝大多数鸿蒙列表型应用,是鸿蒙入门进阶的绝佳实战案例。
更多推荐


所有评论(0)