
鸿蒙跨端智能菜谱推荐系统开发指南 原创
鸿蒙跨端智能菜谱推荐系统开发指南
一、系统架构设计
基于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能力和分布式技术,我们构建了一个智能化的菜谱推荐系统,帮助用户充分利用现有食材,减少食物浪费,并在家庭成员间实现饮食偏好的无缝同步。
