鸿蒙跨端智能菜谱推荐系统开发指南 原创

进修的泡芙
发布于 2025-6-22 16:51
浏览
0收藏

鸿蒙跨端智能菜谱推荐系统开发指南

一、系统架构设计

基于HarmonyOS的AI能力和分布式技术,构建智能菜谱推荐系统:
食材识别层:通过图像识别和OCR技术识别冰箱内食材

菜谱匹配层:根据现有食材匹配适合的菜谱

推荐展示层:展示推荐菜谱和烹饪步骤

跨端同步层:多设备间同步食材数据和菜谱推荐

!https://example.com/harmony-recipe-system-arch.png

二、核心代码实现
食材识别服务

// IngredientService.ets
import ai from ‘@ohos.ai’;
import ocr from ‘@ohos.ai.ocr’;
import distributedData from ‘@ohos.distributedData’;
import { Ingredient, Recipe } from ‘./RecipeTypes’;

class IngredientService {
private static instance: IngredientService = null;
private modelManager: ai.ModelManager;
private ocrEngine: ocr.OCREngine;
private dataManager: distributedData.DataManager;
private listeners: IngredientListener[] = [];

private constructor() {
this.initModelManager();
this.initOCREngine();
this.initDataManager();
public static getInstance(): IngredientService {

if (!IngredientService.instance) {
  IngredientService.instance = new IngredientService();

return IngredientService.instance;

private initModelManager(): void {

try {
  this.modelManager = ai.createModelManager(getContext());
  
  // 加载食材识别模型
  this.modelManager.loadModel({
    modelName: 'ingredient_recognition',
    modelPath: 'resources/rawfile/ingredient.model',
    callback: (err, data) => {
      if (err) {
        console.error('加载食材模型失败:', JSON.stringify(err));

}

  });
  
  // 加载菜谱推荐模型
  this.modelManager.loadModel({
    modelName: 'recipe_recommendation',
    modelPath: 'resources/rawfile/recipe.model',
    callback: (err, data) => {
      if (err) {
        console.error('加载菜谱模型失败:', JSON.stringify(err));

}

  });

catch (err) {

  console.error('初始化模型管理器失败:', JSON.stringify(err));

}

private initOCREngine(): void {
try {
const context = getContext() as common.Context;
this.ocrEngine = ocr.createOCREngine(context);
catch (err) {

  console.error('初始化OCR引擎失败:', JSON.stringify(err));

}

private initDataManager(): void {
this.dataManager = distributedData.createDataManager({
bundleName: ‘com.example.recipe’,
area: distributedData.Area.GLOBAL,
isEncrypted: true
});

this.dataManager.registerDataListener('ingredient_sync', (data) => {
  this.handleSyncData(data);
});

public async recognizeIngredients(imageData: ArrayBuffer): Promise<Ingredient[]> {

try {
  // 使用AI模型识别食材
  const modelInput = {
    data: imageData,
    width: 224,
    height: 224,
    format: 'RGB'
  };
  
  const modelOutput = await this.modelManager.runModel({
    modelName: 'ingredient_recognition',
    input: modelInput
  });
  
  // 使用OCR识别包装上的文字信息
  const ocrResult = await this.ocrEngine.recognize({
    image: imageData,
    language: 'zh',
    options: {
      detectDirection: true,
      detectLanguage: true

});

  // 合并识别结果
  const ingredients: Ingredient[] = modelOutput.result.items.map(item => {
    return {
      id: Date.now().toString() + '_' + item.name,
      name: item.name,
      type: item.type,
      quantity: this.extractQuantity(ocrResult.text, item.name),
      unit: this.extractUnit(ocrResult.text, item.name),
      imageData: imageData,
      timestamp: Date.now()
    };
  });
  
  // 同步食材信息
  this.syncIngredients(ingredients);
  
  return ingredients;

catch (err) {

  console.error('食材识别失败:', JSON.stringify(err));
  throw err;

}

private extractQuantity(text: string, ingredientName: string): string {
// 简单提取数量信息(实际应用中需要更复杂的逻辑)
const regex = new RegExp(${ingredientName}\d+\.?\d, ‘i’);
const matches = regex.exec(text);
return matches ? matches[1] : ‘适量’;
private extractUnit(text: string, ingredientName: string): string {

// 简单提取单位信息
const unitRegex = /(gkg ml l 个 只 片

块)/g;
const matches = unitRegex.exec(text);
return matches ? matches[0] : ‘’;
public async recommendRecipes(ingredients: Ingredient[]): Promise<Recipe[]> {

try {
  const input = {
    ingredients: ingredients.map(i => i.name),
    count: 5 // 推荐5个菜谱
  };
  
  const output = await this.modelManager.runModel({
    modelName: 'recipe_recommendation',
    input: input
  });
  
  return output.result.recipes.map(recipe => ({
    ...recipe,
    matchScore: this.calculateMatchScore(recipe, ingredients)
  })).sort((a, b) => b.matchScore - a.matchScore);

catch (err) {

  console.error('菜谱推荐失败:', JSON.stringify(err));
  throw err;

}

private calculateMatchScore(recipe: Recipe, ingredients: Ingredient[]): number {
// 计算菜谱与现有食材的匹配度
const requiredIngredients = recipe.ingredients;
const availableNames = ingredients.map(i => i.name);

const matched = requiredIngredients.filter(ri => 
  availableNames.includes(ri.name)
).length;

return matched / requiredIngredients.length;

private syncIngredients(ingredients: Ingredient[]): void {

this.dataManager.syncData('ingredient_sync', {
  type: 'ingredient_update',
  data: ingredients,
  timestamp: Date.now()
});

private handleSyncData(data: any): void {

if (!data || data.type !== 'ingredient_update') return;

this.notifyListeners(data.data);

private notifyListeners(ingredients: Ingredient[]): void {

this.listeners.forEach(listener => {
  listener.onIngredientsUpdated?.(ingredients);
});

public addListener(listener: IngredientListener): void {

if (!this.listeners.includes(listener)) {
  this.listeners.push(listener);

}

public removeListener(listener: IngredientListener): void {
this.listeners = this.listeners.filter(l => l !== listener);
}

interface IngredientListener {
onIngredientsUpdated?(ingredients: Ingredient[]): void;
export const ingredientService = IngredientService.getInstance();

主界面实现

// MainScreen.ets
import { ingredientService } from ‘./IngredientService’;
import { cameraService } from ‘./CameraService’;
import { Ingredient, Recipe } from ‘./RecipeTypes’;

@Component
export struct MainScreen {
@State hasPermission: boolean = false;
@State isProcessing: boolean = false;
@State ingredients: Ingredient[] = [];
@State recommendedRecipes: Recipe[] = [];
@State selectedRecipe: Recipe | null = null;
@State showCamera: boolean = false;

aboutToAppear() {
this.checkPermissions();
ingredientService.addListener({
onIngredientsUpdated: (ingredients) => {
this.handleIngredientsUpdated(ingredients);
});

aboutToDisappear() {

ingredientService.removeListener({
  onIngredientsUpdated: (ingredients) => {
    this.handleIngredientsUpdated(ingredients);

});

build() {

Column() {
  // 标题栏
  Row() {
    Text('智能菜谱推荐')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .layoutWeight(1)
    
    Button(this.hasPermission ? '添加食材' : '授权')
      .width(100)
      .onClick(() => {
        if (this.hasPermission) {
          this.showCamera = true;

else {

          this.requestPermissions();

})

.padding(10)

  .width('100%')
  
  // 食材列表
  if (this.ingredients.length > 0) {
    Column() {
      Text('当前食材')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 10 })
      
      Grid() {
        ForEach(this.ingredients, (ingredient) => {
          GridItem() {
            Column() {
              Image(ingredient.imageData)
                .width(60)
                .height(60)
                .objectFit(ImageFit.Contain)
                .margin({ bottom: 5 })
              
              Text(ingredient.name)
                .fontSize(14)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              
              Text({ingredient.quantity}{ingredient.unit})
                .fontSize(12)
                .fontColor('#666666')

.padding(5)

            .width('100%')
            .alignItems(HorizontalAlign.Center)

.height(120)

        })

.columnsTemplate(‘1fr 1fr 1fr 1fr’)

      .rowsGap(10)
      .columnsGap(10)
      .height(200)
      .margin({ bottom: 20 })

.width(‘100%’)

// 推荐菜谱

  if (this.recommendedRecipes.length > 0) {
    Column() {
      Text('推荐菜谱')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 10 })
      
      List({ space: 10 }) {
        ForEach(this.recommendedRecipes, (recipe) => {
          ListItem() {
            Column() {
              Row() {
                Image(recipe.image)
                  .width(80)
                  .height(80)
                  .objectFit(ImageFit.Cover)
                  .borderRadius(8)
                  .margin({ right: 15 })
                
                Column() {
                  Text(recipe.name)
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .margin({ bottom: 5 })
                  
                  Text(匹配度: ${(recipe.matchScore * 100).toFixed(0)}%)
                    .fontSize(14)
                    .fontColor('#409EFF')
                  
                  Text(所需食材: ${recipe.ingredients.length}种)
                    .fontSize(14)
                    .fontColor('#666666')

.alignItems(HorizontalAlign.Start)

                .layoutWeight(1)

if (this.selectedRecipe?.id === recipe.id) {

                Column() {
                  Divider()
                    .margin({ vertical: 10 })
                  
                  Text('所需食材:')
                    .fontSize(14)
                    .fontColor('#666666')
                    .margin({ bottom: 5 })
                  
                  Row() {
                    ForEach(recipe.ingredients, (ing) => {
                      Chip({
                        label: {ing.name} {ing.amount},
                        selected: this.ingredients.some(i => i.name === ing.name),
                        selectIcon: false
                      })
                      .margin({ right: 5, bottom: 5 })
                    })

.wrap(true)

                  Divider()
                    .margin({ vertical: 10 })
                  
                  Text('烹饪步骤:')
                    .fontSize(14)
                    .fontColor('#666666')
                    .margin({ bottom: 5 })
                  
                  ForEach(recipe.steps, (step, index) => {
                    Row() {
                      Text(${index + 1}.)
                        .fontSize(14)
                        .fontWeight(FontWeight.Bold)
                        .width(30)
                      
                      Text(step)
                        .fontSize(14)
                        .layoutWeight(1)

.margin({ bottom: 5 })

                  })

.padding(10)

                .backgroundColor('#F9F9F9')
                .borderRadius(8)

}

            .padding(10)
            .width('100%')
            .backgroundColor('#FFFFFF')
            .border({ width: 1, color: '#E0E0E0' })
            .borderRadius(8)
            .onClick(() => {
              this.selectedRecipe = 
                this.selectedRecipe?.id === recipe.id ? null : recipe;
            })

})

.height(‘60%’)

.width(‘100%’)

else if (this.ingredients.length === 0) {

    Column() {
      Text('暂无食材信息')
        .fontSize(18)
        .margin({ bottom: 10 })
      
      Text('点击"添加食材"按钮拍摄冰箱内食材')
        .fontSize(16)
        .fontColor('#666666')

.padding(20)

    .width('90%')
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
    .margin({ top: 50 })

else {

    Column() {
      Progress({})
        .width(50)
        .height(50)
        .margin({ bottom: 10 })
      
      Text('正在为您推荐菜谱...')
        .fontSize(16)

.width(‘100%’)

    .height('60%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)

}

.width('100%')
.height('100%')
.padding(20)

// 相机界面
if (this.showCamera) {
  Stack() {
    // 相机预览
    Surface({
      id: 'camera_preview',
      type: SurfaceType.SURFACE_TEXTURE,
      width: '100%',
      height: '100%'
    })
    .onAppear(() => {
      cameraService.startPreview('camera_preview');
    })
    
    // 操作按钮
    Column() {
      Row() {
        Button('取消')
          .width(100)
          .height(50)
          .fontSize(18)
          .onClick(() => {
            this.showCamera = false;
            cameraService.stopPreview();
          })
        
        Blank()
          .layoutWeight(1)
        
        Button('识别')
          .width(100)
          .height(50)
          .fontSize(18)
          .onClick(async () => {
            this.isProcessing = true;
            this.showCamera = false;
            
            try {
              const imageData = await cameraService.captureFrame();
              const ingredients = await ingredientService.recognizeIngredients(imageData);
              
              this.ingredients = [...this.ingredients, ...ingredients];
              this.recommendedRecipes = await ingredientService.recommendRecipes(this.ingredients);

catch (err) {

              console.error('食材识别失败:', JSON.stringify(err));
              prompt.showToast({ message: '食材识别失败,请重试' });

finally {

              this.isProcessing = false;
              cameraService.stopPreview();

})

.width(‘100%’)

      .padding(10)
      .backgroundColor('rgba(0,0,0,0.5)')

.width(‘100%’)

    .position({ x: 0, y: '80%' })

.width(‘100%’)

  .height('100%')
  .position({ x: 0, y: 0 })

}

private async checkPermissions(): Promise<void> {
try {
const permissions = [
‘ohos.permission.USE_AI’,
‘ohos.permission.CAMERA’,
‘ohos.permission.DISTRIBUTED_DATASYNC’
];

  const result = await abilityAccessCtrl.verifyPermissions(
    getContext(),
    permissions
  );
  
  this.hasPermission = result.every(perm => perm.granted);

catch (err) {

  console.error('检查权限失败:', JSON.stringify(err));
  this.hasPermission = false;

}

private async requestPermissions(): Promise<void> {
try {
const permissions = [
‘ohos.permission.USE_AI’,
‘ohos.permission.CAMERA’,
‘ohos.permission.DISTRIBUTED_DATASYNC’
];

  const result = await abilityAccessCtrl.requestPermissionsFromUser(
    getContext(), 
    permissions
  );
  
  this.hasPermission = result.grantedPermissions.length === permissions.length;
  
  if (!this.hasPermission) {
    prompt.showToast({ message: '授权失败,无法使用食材识别功能' });

} catch (err) {

  console.error('请求权限失败:', JSON.stringify(err));
  this.hasPermission = false;

}

private handleIngredientsUpdated(ingredients: Ingredient[]): void {
this.ingredients = ingredients;
this.updateRecommendations();
private async updateRecommendations(): Promise<void> {

if (this.ingredients.length > 0) {
  this.recommendedRecipes = await ingredientService.recommendRecipes(this.ingredients);

else {

  this.recommendedRecipes = [];

}

// 封装Chip组件

@Component
struct Chip {
private label: string;
private selected: boolean;
private selectIcon: boolean;

build() {
Row() {
if (this.selectIcon) {
Image(this.selected ? r(‘app.media.ic_check’) : r(‘app.media.ic_add’))
.width(12)
.height(12)
.margin({ right: 5 })
Text(this.label)

    .fontSize(12)

.padding(5)

.borderRadius(15)
.backgroundColor(this.selected ? '#E1F5FE' : '#EEEEEE')
.border({ 
  width: 1, 
  color: this.selected ? '#039BE5' : '#E0E0E0' 
})

}

类型定义

// RecipeTypes.ets
export interface Ingredient {
id: string;
name: string;
type: string;
quantity: string;
unit: string;
imageData: ArrayBuffer;
timestamp: number;
export interface RecipeIngredient {

name: string;
amount: string;
export interface Recipe {

id: string;
name: string;
image: string;
ingredients: RecipeIngredient[];
steps: string[];
cookingTime: number; // 分钟
difficulty: ‘简单’ ‘中等’
‘困难’;
matchScore: number; // 0-1

三、项目配置与权限
权限配置

// module.json5
“module”: {

"requestPermissions": [

“name”: “ohos.permission.USE_AI”,

    "reason": "使用AI模型识别食材和推荐菜谱"
  },

“name”: “ohos.permission.CAMERA”,

    "reason": "拍摄食材照片"
  },

“name”: “ohos.permission.DISTRIBUTED_DATASYNC”,

    "reason": "同步食材数据和菜谱"

],

"abilities": [

“name”: “MainAbility”,

    "type": "page",
    "visible": true
  },

“name”: “CameraAbility”,

    "type": "service",
    "backgroundModes": ["camera"]

]

}

四、总结与扩展

本智能菜谱推荐系统实现了以下核心功能:
智能食材识别:通过图像识别和OCR技术自动识别冰箱内食材

个性化菜谱推荐:根据现有食材匹配最适合的菜谱

详细烹饪指导:提供完整的食材清单和分步烹饪说明

跨设备同步:家庭成员间共享食材数据和菜谱推荐

扩展方向:
智能购物清单:根据推荐菜谱自动生成缺少食材的购物清单

营养分析:计算菜谱的营养成分和热量

口味偏好学习:根据用户反馈优化推荐算法

视频教程:集成烹饪视频教程

智能家电控制:与智能厨电联动,自动设置烹饪参数

社交分享:分享菜谱和烹饪成果到社交平台

通过HarmonyOS的AI能力和分布式技术,我们构建了一个智能化的菜谱推荐系统,帮助用户充分利用现有食材,减少食物浪费,并在家庭成员间实现饮食偏好的无缝同步。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐