HarmonyOS ArkTS 实战:实现一个菜谱大全与美食记录应用

项目效果

本文使用 HarmonyOS 和 ArkTS 实现一个精美的菜谱大全与美食记录应用。支持菜谱分类浏览、搜索、食材清单、步骤详解、收藏、美食记录、烹饪计时等功能,橙色渐变主题,美食卡片设计。

项目使用 DevEco Studio 开发,适配 API 24 及以上版本。

运行效果

菜谱大全与美食记录应用

功能介绍

  • 菜谱分类浏览(家常菜、川菜、粤菜、甜品、汤羹等)
  • 热门菜谱推荐轮播
  • 菜谱搜索
  • 食材清单展示
  • 分步骤烹饪详解
  • 收藏喜欢的菜谱
  • 烹饪计时器
  • 美食打卡记录
  • 我的收藏列表
  • 做菜历史记录
  • 难度和时间标签
  • 菜谱分享
  • 今日推荐
  • 食材用量单位转换
  • 购物清单生成

定义数据结构

interface Recipe {
  id: number;
  name: string;
  cover: string;
  category: string;
  difficulty: string;
  cookTime: number;
  servings: number;
  description: string;
  ingredients: { name: string; amount: string }[];
  steps: { order: number; content: string; image: string }[];
  tips: string;
  isCollected: boolean;
  rating: number;
}

interface FoodRecord {
  id: number;
  recipeId: number;
  recipeName: string;
  date: string;
  rating: number;
  note: string;
}

初始化页面状态

@State private currentTab: number = 0;
@State private currentCategory: string = '推荐';
@State private searchKeyword: string = '';
@State private showSearch: boolean = false;
@State private selectedRecipe: Recipe | null = null;
@State private showTimer: boolean = false;
@State private timerSeconds: number = 0;
@State private timerRunning: boolean = false;

@State private categories: string[] = ['推荐', '家常菜', '川菜', '粤菜', '甜品', '汤羹', '早餐', '快手菜'];

@State private recipes: Recipe[] = [
  {
    id: 1, name: '红烧肉', cover: '🍖', category: '家常菜', difficulty: '中等',
    cookTime: 90, servings: 3, description: '肥而不腻,入口即化的经典红烧肉',
    ingredients: [
      { name: '五花肉', amount: '500g' },
      { name: '冰糖', amount: '30g' },
      { name: '生抽', amount: '2勺' },
      { name: '老抽', amount: '1勺' },
      { name: '料酒', amount: '2勺' },
      { name: '葱姜', amount: '适量' },
    ],
    steps: [
      { order: 1, content: '五花肉切块,冷水下锅加料酒焯水,捞出洗净', image: '🥩' },
      { order: 2, content: '锅中放油加冰糖,小火炒出糖色', image: '🍬' },
      { order: 3, content: '放入五花肉翻炒上色,加葱姜爆香', image: '🍳' },
      { order: 4, content: '加生抽老抽料酒,加热水没过肉块', image: '💧' },
      { order: 5, content: '大火烧开转小火炖1小时,最后大火收汁', image: '🔥' },
    ],
    tips: '一定要加热水,肉质才会软嫩;糖色要小火慢炒不要糊',
    isCollected: true, rating: 4.8
  },
  {
    id: 2, name: '番茄炒蛋', cover: '🍅', category: '快手菜', difficulty: '简单',
    cookTime: 10, servings: 2, description: '国民家常菜,酸甜可口',
    ingredients: [
      { name: '番茄', amount: '2个' },
      { name: '鸡蛋', amount: '3个' },
      { name: '白糖', amount: '1勺' },
      { name: '盐', amount: '适量' },
    ],
    steps: [
      { order: 1, content: '鸡蛋打散加少许盐,番茄切块', image: '🥚' },
      { order: 2, content: '热油炒鸡蛋,盛出备用', image: '🍳' },
      { order: 3, content: '炒番茄出汁,加糖加盐', image: '🍅' },
      { order: 4, content: '倒入鸡蛋翻炒均匀出锅', image: '🍽️' },
    ],
    tips: '鸡蛋要炒嫩一点,番茄要炒出沙才好吃',
    isCollected: false, rating: 4.9
  },
  {
    id: 3, name: '麻婆豆腐', cover: '🌶️', category: '川菜', difficulty: '中等',
    cookTime: 20, servings: 2, description: '麻辣鲜香,下饭神器',
    ingredients: [
      { name: '嫩豆腐', amount: '1块' },
      { name: '肉末', amount: '100g' },
      { name: '豆瓣酱', amount: '1勺' },
      { name: '花椒粉', amount: '适量' },
    ],
    steps: [], tips: '豆腐要焯水去豆腥味,最后勾芡',
    isCollected: false, rating: 4.7
  },
];

@State private foodRecords: FoodRecord[] = [];

核心功能方法

private toggleCollect(recipeId: number): void {
  this.recipes = this.recipes.map(r => {
    if (r.id === recipeId) return { ...r, isCollected: !r.isCollected };
    return r;
  });
}

private startTimer(minutes: number): void {
  this.timerSeconds = minutes * 60;
  this.timerRunning = true;
  this.showTimer = true;
  const timer = setInterval(() => {
    if (this.timerSeconds > 0) {
      this.timerSeconds--;
    } else {
      clearInterval(timer);
      this.timerRunning = false;
    }
  }, 1000);
}

private getFilteredRecipes(): Recipe[] {
  let list = this.recipes;
  if (this.currentCategory !== '推荐') {
    list = list.filter(r => r.category === this.currentCategory);
  }
  if (this.searchKeyword) {
    list = list.filter(r => r.name.includes(this.searchKeyword));
  }
  return list;
}

@Builder 可复用组件

@Builder
RecipeCard(recipe: Recipe) {
  Column({ space: 8 }) {
    Stack({ alignContent: Alignment.TopEnd }) {
      Text(recipe.cover)
        .fontSize(64)
        .width('100%')
        .height(140)
        .textAlign(TextAlign.Center)
        .backgroundColor('#FFF7ED')
        .borderRadius({ topLeft: 12, topRight: 12 })
      Text(recipe.isCollected ? '❤️' : '🤍')
        .fontSize(20)
        .padding(8)
        .onClick(() => this.toggleCollect(recipe.id))
    }
    .width('100%')
    
    Column({ space: 4 }) {
      Text(recipe.name)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1F2937')
        .width('100%')
      Row({ space: 8 }) {
        Text(recipe.difficulty)
          .fontSize(11)
          .fontColor('#EA580C')
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .backgroundColor('#FFF7ED')
          .borderRadius(4)
        Text(`${recipe.cookTime}分钟`)
          .fontSize(11)
          .fontColor('#6B7280')
        Text(`${recipe.rating}`)
          .fontSize(11)
          .fontColor('#F59E0B')
      }
    }
    .width('100%')
    .padding(12)
    .alignItems(HorizontalAlign.Start)
  }
  .width('100%')
  .backgroundColor(Color.White)
  .borderRadius(12)
  .shadow({ radius: 8, color: '#00000008', offsetY: 2 })
  .onClick(() => this.selectedRecipe = recipe)
}

@Builder
IngredientItem(name: string, amount: string) {
  Row() {
    Text(name)
      .fontSize(14)
      .fontColor('#374151')
    Blank()
    Text(amount)
      .fontSize(14)
      .fontColor('#6B7280')
  }
  .width('100%')
  .padding({ top: 10, bottom: 10 })
  Divider().color('#F3F4F6')
}

完整build()页面布局

build() {
  Stack({ alignContent: Alignment.Bottom }) {
    Column() {
      // 顶部
      Row() {
        Text('美食菜谱')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1F2937')
        Blank()
        Text('🔍')
          .fontSize(22)
          .onClick(() => this.showSearch = !this.showSearch)
      }
      .width('100%')
      .padding(20)
      
      if (this.showSearch) {
        TextInput({ placeholder: '搜索菜谱', text: this.searchKeyword })
          .width('90%')
          .height(40)
          .backgroundColor('#F3F4F6')
          .borderRadius(12)
          .margin({ bottom: 12 })
          .onChange((v: string) => this.searchKeyword = v)
      }
      
      // 分类
      Scroll(Axis.Horizontal) {
        Row({ space: 12 }) {
          ForEach(this.categories, (cat: string) => {
            Text(cat)
              .fontSize(14)
              .fontColor(this.currentCategory === cat ? Color.White : '#6B7280')
              .padding({ left: 16, right: 16, top: 8, bottom: 8 })
              .backgroundColor(this.currentCategory === cat ? '#EA580C' : '#F3F4F6')
              .borderRadius(20)
              .onClick(() => this.currentCategory = cat)
          })
        }
        .padding({ left: 20, right: 20 })
      }
      .scrollBar(BarState.Off)
      
      // 菜谱列表
      Scroll() {
        Grid() {
          ForEach(this.getFilteredRecipes(), (recipe: Recipe) => {
            GridItem() { this.RecipeCard(recipe) }
          })
        }
        .columnsTemplate('1fr 1fr')
        .columnsGap(12)
        .rowsGap(12)
        .width('100%')
        .padding({ left: 20, right: 20, bottom: 80 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F9FAFB')
    
    // 菜谱详情弹窗
    if (this.selectedRecipe) {
      Column() {
        Blank()
        Scroll() {
          Column({ space: 16 }) {
            Stack({ alignContent: Alignment.TopStart }) {
              Text(this.selectedRecipe.cover)
                .fontSize(100)
                .width('100%')
                .height(200)
                .textAlign(TextAlign.Center)
                .backgroundColor('#FFF7ED')
              Text('←')
                .fontSize(24)
                .fontColor(Color.White)
                .padding(16)
                .onClick(() => this.selectedRecipe = null)
            }
            .width('100%')
            
            Column({ space: 12 }) {
              Text(this.selectedRecipe.name)
                .fontSize(22)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1F2937')
              
              Row({ space: 12 }) {
                Text(`⏱️ ${this.selectedRecipe.cookTime}分钟`)
                  .fontSize(13)
                  .fontColor('#6B7280')
                Text(`👥 ${this.selectedRecipe.servings}人份`)
                  .fontSize(13)
                  .fontColor('#6B7280')
                Text(`📊 ${this.selectedRecipe.difficulty}`)
                  .fontSize(13)
                  .fontColor('#6B7280')
              }
              
              Text(this.selectedRecipe.description)
                .fontSize(14)
                .fontColor('#6B7280')
              
              Text('食材清单')
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
                .fontColor('#1F2937')
                .width('100%')
              
              Column() {
                ForEach(this.selectedRecipe.ingredients, (ing: { name: string; amount: string }) => {
                  this.IngredientItem(ing.name, ing.amount)
                })
              }
              .width('100%')
              .padding(16)
              .backgroundColor(Color.White)
              .borderRadius(12)
              
              Text('烹饪步骤')
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
                .fontColor('#1F2937')
                .width('100%')
              
              Button('⏱️ 开始计时')
                .width('100%')
                .height(44)
                .backgroundColor('#EA580C')
                .onClick(() => this.startTimer(this.selectedRecipe!.cookTime))
            }
            .width('100%')
            .padding(20)
          }
        }
        .width('100%')
        .height('85%')
        .backgroundColor(Color.White)
        .borderRadius({ topLeft: 24, topRight: 24 })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(0,0,0,0.5)')
    }
  }
  .width('100%')
  .height('100%')
}

页面设计说明

主题色采用橙色渐变#EA580C→#FB923C,橙色代表美食、温暖、食欲,非常适合菜谱应用。两列网格布局展示菜谱卡片,大图+标题+标签的经典设计。详情页从底部弹出,食材清单、步骤详解清晰明了。

SDK配置

API 24,compatibleSdkVersion: “6.1.1(24)”

运行项目

将代码复制到 entry/src/main/ets/pages/Index.ets 即可运行。

项目总结

实现了菜谱浏览、分类筛选、搜索、详情展示、收藏、烹饪计时等功能。掌握了网格布局、底部弹窗详情、横向滚动分类、计时器等。后续可加入视频教程、用户上传、营养成分计算、菜单规划、食材购买、社区分享、AI推荐菜谱等功能。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐