鸿蒙简易菜品热量计算应用开发指南 原创

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

鸿蒙简易菜品热量计算应用开发指南

一、系统架构设计

基于HarmonyOS的分布式能力和AI技术,我们设计了一套简易菜品热量计算系统,主要功能包括:
菜品拍照:使用设备相机拍摄菜品照片

AI识别:识别菜品成分和分量

热量估算:计算菜品总热量和营养成分

多设备同步:跨设备同步饮食记录和健康数据

健康建议:根据用户目标提供饮食建议

!https://example.com/harmony-food-calorie-arch.png

二、核心代码实现
相机服务管理

// FoodCameraService.ets
import camera from ‘@ohos.multimedia.camera’;
import image from ‘@ohos.multimedia.image’;

class FoodCameraService {
private static instance: FoodCameraService;
private cameraManager: camera.CameraManager;
private cameraInput: camera.CameraInput | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private photoOutput: camera.PhotoOutput | null = null;

private constructor() {
this.cameraManager = camera.getCameraManager();
public static getInstance(): FoodCameraService {

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

return FoodCameraService.instance;

public async initCamera(previewSurfaceId: string): Promise<void> {

const cameras = this.cameraManager.getSupportedCameras();
if (cameras.length === 0) {
  throw new Error('No camera available');

this.cameraInput = this.cameraManager.createCameraInput(cameras[0]);

await this.cameraInput.open();

const previewProfile = this.cameraManager.getSupportedOutputCapability(
  cameras[0],
  camera.ProfileMode.PROFILE_MODE_DEFAULT
).previewProfiles[0];

this.previewOutput = this.cameraManager.createPreviewOutput(
  previewProfile,
  previewSurfaceId
);

const photoProfiles = this.cameraManager.getSupportedOutputCapability(
  cameras[0],
  camera.ProfileMode.PROFILE_MODE_DEFAULT
).photoProfiles;

this.photoOutput = this.cameraManager.createPhotoOutput(
  photoProfiles[0]
);

public async takeFoodPhoto(): Promise<image.Image> {

if (!this.photoOutput) {
  throw new Error('Photo output not initialized');

const photoSettings = {

  quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
  rotation: camera.ImageRotation.ROTATION_0
};

return new Promise((resolve, reject) => {
  this.photoOutput!.capture(photoSettings, (err, image) => {
    if (err) {
      reject(err);

else {

      resolve(image);

});

});

}

export const foodCameraService = FoodCameraService.getInstance();

菜品识别服务

// FoodRecognitionService.ets
import image from ‘@ohos.multimedia.image’;
import http from ‘@ohos.net.http’;

class FoodRecognitionService {
private static instance: FoodRecognitionService;
private httpClient: http.HttpRequest;
private apiKey = ‘YOUR_FOOD_API_KEY’;

private constructor() {
this.httpClient = http.createHttp();
public static getInstance(): FoodRecognitionService {

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

return FoodRecognitionService.instance;

public async recognizeFood(image: image.Image): Promise<FoodInfo> {

const formData = new FormData();
formData.append('image', image);
formData.append('api_key', this.apiKey);

return new Promise((resolve, reject) => {
  this.httpClient.request(
    'https://food-api.example.com/identify',

method: ‘POST’,

      header: { 'Content-Type': 'multipart/form-data' },
      extraData: formData
    },
    (err, data) => {
      if (err) {
        reject(err);

else {

        const result = JSON.parse(data.result);
        resolve(this.processFoodInfo(result));

}

  );
});

private processFoodInfo(rawData: any): FoodInfo {

return {
  name: rawData.name || '未知菜品',
  ingredients: rawData.ingredients.map(ing => ({
    name: ing.name,
    amount: ing.amount,
    unit: ing.unit,
    calories: ing.calories,
    protein: ing.protein,
    fat: ing.fat,
    carbs: ing.carbs
  })),
  totalCalories: rawData.totalCalories,
  confidence: rawData.confidence
};

}

export const foodRecognitionService = FoodRecognitionService.getInstance();

多设备同步服务

// FoodSyncService.ets
import distributedData from ‘@ohos.data.distributedData’;
import deviceManager from ‘@ohos.distributedHardware.deviceManager’;

class FoodSyncService {
private static instance: FoodSyncService;
private kvManager: distributedData.KVManager;
private kvStore: distributedData.KVStore;

private constructor() {
this.initKVStore();
private async initKVStore(): Promise<void> {

const config = {
  bundleName: 'com.example.foodCalorie',
  userInfo: { userId: 'currentUser' }
};

this.kvManager = distributedData.createKVManager(config);
this.kvStore = await this.kvManager.getKVStore('food_store', {
  createIfMissing: true
});

this.kvStore.on('dataChange', (data) => {
  this.handleRemoteUpdate(data);
});

public static getInstance(): FoodSyncService {

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

return FoodSyncService.instance;

public async syncMealRecord(record: MealRecord): Promise<void> {

await this.kvStore.put(meal_${record.id}, JSON.stringify(record));

public async getMealRecords(date: string): Promise<MealRecord[]> {

const entries = await this.kvStore.getEntries('meal_');
return Array.from(entries)
  .map(([_, value]) => JSON.parse(value))
  .filter(record => record.date === date);

public async syncUserProfile(profile: UserProfile): Promise<void> {

await this.kvStore.put('user_profile', JSON.stringify(profile));

public async getUserProfile(): Promise<UserProfile | null> {

const value = await this.kvStore.get('user_profile');
return value ? JSON.parse(value) : null;

private handleRemoteUpdate(data: distributedData.ChangeInfo): void {

if (data.deviceId === deviceInfo.deviceId) return;

const key = data.key as string;
if (key.startsWith('meal_')) {
  const record = JSON.parse(data.value);
  EventBus.emit('mealRecordUpdated', record);

else if (key === ‘user_profile’) {

  const profile = JSON.parse(data.value);
  EventBus.emit('userProfileUpdated', profile);

}

export const foodSyncService = FoodSyncService.getInstance();

热量计算服务

// CalorieCalculationService.ets
class CalorieCalculationService {
private static instance: CalorieCalculationService;

private constructor() {}

public static getInstance(): CalorieCalculationService {
if (!CalorieCalculationService.instance) {
CalorieCalculationService.instance = new CalorieCalculationService();
return CalorieCalculationService.instance;

public calculateTotalCalories(foodInfo: FoodInfo): number {

return foodInfo.ingredients.reduce(
  (total, ing) => total + ing.calories,

);

public calculateDailyIntake(records: MealRecord[]): DailyIntake {

return records.reduce(
  (daily, record) => {
    daily.calories += record.totalCalories;
    daily.protein += record.totalProtein;
    daily.fat += record.totalFat;
    daily.carbs += record.totalCarbs;
    return daily;
  },

calories: 0, protein: 0, fat: 0, carbs: 0 }

);

public getHealthAdvice(dailyIntake: DailyIntake, userProfile: UserProfile): HealthAdvice {

const targetCalories = this.calculateTargetCalories(userProfile);
const remainingCalories = targetCalories - dailyIntake.calories;

return {
  remainingCalories,
  suggestion: remainingCalories > 0 

还可以摄入约${remainingCalories}大卡

已超出${-remainingCalories}大卡,

  balancedDiet: this.getBalancedDietAdvice(dailyIntake, targetCalories)
};

private calculateTargetCalories(userProfile: UserProfile): number {

// 基础代谢率(BMR)计算
let bmr = userProfile.gender === 'male'

88.362 + (13.397 userProfile.weight) + (4.799 userProfile.height) - (5.677 * userProfile.age)

447.593 + (9.247 userProfile.weight) + (3.098 userProfile.height) - (4.330 * userProfile.age);

// 根据活动水平调整
const activityFactors = {
  sedentary: 1.2,
  lightlyActive: 1.375,
  moderatelyActive: 1.55,
  veryActive: 1.725,
  extraActive: 1.9
};

bmr *= activityFactors[userProfile.activityLevel];

// 根据目标调整
if (userProfile.goal === 'lose') {
  return bmr * 0.8; // 减重建议减少20%摄入

else if (userProfile.goal === ‘gain’) {

  return bmr * 1.2; // 增重建议增加20%摄入

return bmr; // 维持体重

private getBalancedDietAdvice(dailyIntake: DailyIntake, targetCalories: number): string[] {

const advice = [];
const proteinPercent = (dailyIntake.protein * 4) / dailyIntake.calories;
const fatPercent = (dailyIntake.fat * 9) / dailyIntake.calories;
const carbsPercent = (dailyIntake.carbs * 4) / dailyIntake.calories;

if (proteinPercent < 0.1) {
  advice.push('建议增加蛋白质摄入,如鸡胸肉、鱼类、豆制品等');

else if (proteinPercent > 0.35) {

  advice.push('蛋白质摄入偏高,可适当减少');

if (fatPercent < 0.2) {

  advice.push('建议增加健康脂肪摄入,如坚果、橄榄油、牛油果等');

else if (fatPercent > 0.35) {

  advice.push('脂肪摄入偏高,可适当减少油炸食品和高脂肪食物');

if (carbsPercent < 0.45) {

  advice.push('建议增加碳水化合物摄入,如全谷物、蔬菜、水果等');

else if (carbsPercent > 0.65) {

  advice.push('碳水化合物摄入偏高,可适当减少精制糖和精制谷物');

return advice.length > 0 ? advice : [‘当前营养比例均衡,继续保持!’];

}

export const calorieService = CalorieCalculationService.getInstance();

三、主界面实现
相机识别界面

// FoodCameraView.ets
@Component
struct FoodCameraView {
@State previewSurfaceId: string = ‘’;
@State capturedImage: image.Image | null = null;
@State isRecognizing: boolean = false;

aboutToAppear() {
this.initCamera();
build() {

Stack() {
  // 相机预览
  XComponent({
    id: 'foodCameraPreview',
    type: 'surface',
    libraryname: 'libcamera.so',
    controller: this.previewController
  })
  .onLoad(() => {
    this.previewSurfaceId = this.previewController.getXComponentSurfaceId();
    foodCameraService.initCamera(this.previewSurfaceId);
  })
  .width('100%')
  .height('100%')
  
  // 拍照按钮
  Button('识别菜品')
    .onClick(() => this.captureAndRecognize())
    .position({ x: '50%', y: '90%' })
  
  // 识别状态
  if (this.isRecognizing) {
    LoadingProgress()
      .position({ x: '50%', y: '50%' })

}

private async captureAndRecognize(): Promise<void> {

this.isRecognizing = true;
this.capturedImage = await foodCameraService.takeFoodPhoto();
const foodInfo = await foodRecognitionService.recognizeFood(this.capturedImage);

// 计算总热量
const totalCalories = calorieService.calculateTotalCalories(foodInfo);

// 保存识别记录
const record: MealRecord = {
  id: generateId(),
  foodInfo,
  date: getCurrentDate(),
  time: getCurrentTime(),
  totalCalories,
  totalProtein: foodInfo.ingredients.reduce((sum, ing) => sum + ing.protein, 0),
  totalFat: foodInfo.ingredients.reduce((sum, ing) => sum + ing.fat, 0),
  totalCarbs: foodInfo.ingredients.reduce((sum, ing) => sum + ing.carbs, 0)
};

await foodSyncService.syncMealRecord(record);
EventBus.emit('foodIdentified', foodInfo);
this.isRecognizing = false;

}

function generateId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
function getCurrentDate(): string {

const now = new Date();
return {now.getFullYear()}-{now.getMonth() + 1}-${now.getDate()};
function getCurrentTime(): string {

const now = new Date();
return {now.getHours()}:{now.getMinutes()};

菜品详情界面

// FoodDetailView.ets
@Component
struct FoodDetailView {
@State foodInfo: FoodInfo | null = null;
@State healthAdvice: HealthAdvice | null = null;

aboutToAppear() {
EventBus.on(‘foodIdentified’, (info) => {
this.foodInfo = info;
this.loadHealthAdvice();
});
build() {

Column() {
  if (this.foodInfo) {
    Scroll() {
      Column() {
        // 菜品基本信息
        Text(this.foodInfo.name)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          
        Text(总热量: ${this.foodInfo.totalCalories.toFixed(0)} 大卡)
          .fontSize(18)
          .margin({ top: 8 })
        
        // 营养成分
        Text('营养成分')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 24 })
        
        Row() {
          Column() {
            Text('蛋白质')
              .fontSize(16)
            Text(${this.foodInfo.ingredients.reduce((sum, ing) => sum + ing.protein, 0).toFixed(1)}g)
              .fontSize(18)

.margin(8)

          Column() {
            Text('脂肪')
              .fontSize(16)
            Text(${this.foodInfo.ingredients.reduce((sum, ing) => sum + ing.fat, 0).toFixed(1)}g)
              .fontSize(18)

.margin(8)

          Column() {
            Text('碳水')
              .fontSize(16)
            Text(${this.foodInfo.ingredients.reduce((sum, ing) => sum + ing.carbs, 0).toFixed(1)}g)
              .fontSize(18)

.margin(8)

.justifyContent(FlexAlign.SpaceAround)

        .margin({ top: 12 })
        
        // 食材列表
        Text('食材组成')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 24 })
        
        ForEach(this.foodInfo.ingredients, (ingredient) => {
          Column() {
            Row() {
              Text(ingredient.name)
                .fontSize(16)
                .layoutWeight(1)
              
              Text({ingredient.amount}{ingredient.unit})
                .fontSize(16)
                .fontColor('#666666')
              
              Text(${ingredient.calories.toFixed(0)}大卡)
                .fontSize(16)
                .fontColor('#FF5722')

.margin({ top: 8 })

            Divider()

})

        // 健康建议
        if (this.healthAdvice) {
          Text('健康建议')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .margin({ top: 24 })
          
          Text(this.healthAdvice.suggestion)
            .fontSize(16)
            .margin({ top: 8 })
            .fontColor(this.healthAdvice.remainingCalories > 0 ? '#4CAF50' : '#FF5722')
          
          ForEach(this.healthAdvice.balancedDiet, (advice, index) => {
            Text(• ${advice})
              .fontSize(16)
              .margin({ top: index === 0 ? 12 : 4 })
          })

}

      .padding(16)

}

}

private async loadHealthAdvice(): Promise<void> {
if (this.foodInfo) {
const userProfile = await foodSyncService.getUserProfile();
if (userProfile) {
const date = getCurrentDate();
const records = await foodSyncService.getMealRecords(date);
const dailyIntake = calorieService.calculateDailyIntake(records);
this.healthAdvice = calorieService.getHealthAdvice(dailyIntake, userProfile);
}

}

饮食记录界面

// MealHistoryView.ets
@Component
struct MealHistoryView {
@State records: MealRecord[] = [];
@State selectedDate: string = getCurrentDate();
@State dailyIntake: DailyIntake = { calories: 0, protein: 0, fat: 0, carbs: 0 };

aboutToAppear() {
this.loadRecords();
EventBus.on(‘mealRecordUpdated’, () => this.loadRecords());
build() {

Column() {
  // 日期选择
  Row() {
    Button('<')
      .onClick(() => this.changeDate(-1))
    
    Text(this.selectedDate)
      .fontSize(18)
      .margin({ left: 16, right: 16 })
    
    Button('>')
      .onClick(() => this.changeDate(1))
      .enabled(this.selectedDate !== getCurrentDate())

.margin({ top: 16 })

  .justifyContent(FlexAlign.Center)
  
  // 每日总摄入
  Text(总摄入: ${this.dailyIntake.calories.toFixed(0)} 大卡)
    .fontSize(20)
    .margin({ top: 16 })
  
  // 营养比例图表
  Row() {
    Stack() {
      // 蛋白质
      Column() {
        Text('')
          .width('100%')
          .height(${Math.min(100, (this.dailyIntake.protein  4 / this.dailyIntake.calories)  100)}%)
          .backgroundColor('#4CAF50')

.width(‘30%’)

      .height('100%')
      
      // 脂肪
      Column() {
        Text('')
          .width('100%')
          .height(${Math.min(100, (this.dailyIntake.fat  9 / this.dailyIntake.calories)  100)}%)
          .backgroundColor('#FFC107')

.width(‘30%’)

      .height('100%')
      .margin({ left: '35%' })
      
      // 碳水
      Column() {
        Text('')
          .width('100%')
          .height(${Math.min(100, (this.dailyIntake.carbs  4 / this.dailyIntake.calories)  100)}%)
          .backgroundColor('#2196F3')

.width(‘30%’)

      .height('100%')
      .margin({ left: '70%' })

.height(120)

    .width('90%')
    .margin({ top: 8 })

.justifyContent(FlexAlign.Center)

  // 记录列表
  if (this.records.length === 0) {
    Text('暂无饮食记录')
      .fontSize(16)
      .margin({ top: 32 })

else {

    List({ space: 10 }) {
      ForEach(this.records, (record) => {
        ListItem() {
          MealRecordItem({ record })

})

.layoutWeight(1)

}

private async loadRecords(): Promise<void> {

this.records = await foodSyncService.getMealRecords(this.selectedDate);
this.records.sort((a, b) => b.time.localeCompare(a.time));
this.dailyIntake = calorieService.calculateDailyIntake(this.records);

private async changeDate(days: number): Promise<void> {

const date = new Date(this.selectedDate);
date.setDate(date.getDate() + days);
this.selectedDate = {date.getFullYear()}-{date.getMonth() + 1}-${date.getDate()};
await this.loadRecords();

}

@Component
struct MealRecordItem {
private record: MealRecord;

build() {
Row() {
Column() {
Text(this.record.foodInfo.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)

    Text(this.record.time)
      .fontSize(14)
      .fontColor('#666666')
      .margin({ top: 4 })

.layoutWeight(1)

  Text(${this.record.totalCalories.toFixed(0)}大卡)
    .fontSize(18)
    .fontColor('#FF5722')

.padding(12)

.onClick(() => {
  EventBus.emit('showFoodDetail', this.record.foodInfo);
})

}

四、高级功能实现
多设备协同记录

// CollaborativeMealRecording.ets
class CollaborativeMealRecording {
private static instance: CollaborativeMealRecording;

private constructor() {}

public static getInstance(): CollaborativeMealRecording {
if (!CollaborativeMealRecording.instance) {
CollaborativeMealRecording.instance = new CollaborativeMealRecording();
return CollaborativeMealRecording.instance;

public async startFamilySharing(familyId: string): Promise<void> {

const devices = await deviceManager.getTrustedDevices();
await Promise.all(devices.map(device => 
  this.inviteDeviceToFamily(device.id, familyId)
));

private async inviteDeviceToFamily(deviceId: string, familyId: string): Promise<void> {

const ability = await featureAbility.startAbility({
  bundleName: 'com.example.foodCalorie',
  abilityName: 'FamilyMealAbility',
  deviceId
});

await ability.call({
  method: 'joinFamily',
  parameters: [familyId]
});

public async shareMealRecord(record: MealRecord, familyId: string): Promise<void> {

await foodSyncService.syncMealRecord(record);

const devices = await deviceManager.getTrustedDevices();
await Promise.all(devices.map(device => 
  this.sendRecordToDevice(device.id, record, familyId)
));

private async sendRecordToDevice(deviceId: string, record: MealRecord, familyId: string): Promise<void> {

const ability = await featureAbility.startAbility({
  bundleName: 'com.example.foodCalorie',
  abilityName: 'FamilyMealAbility',
  deviceId
});

await ability.call({
  method: 'receiveMealRecord',
  parameters: [record, familyId]
});

}

export const collaborativeRecording = CollaborativeMealRecording.getInstance();

用户健康档案

// UserProfileService.ets
import preferences from ‘@ohos.data.preferences’;

class UserProfileService {
private static instance: UserProfileService;

private constructor() {}

public static getInstance(): UserProfileService {
if (!UserProfileService.instance) {
UserProfileService.instance = new UserProfileService();
return UserProfileService.instance;

public async saveUserProfile(profile: UserProfile): Promise<void> {

await foodSyncService.syncUserProfile(profile);

// 本地也保存一份
const prefs = await preferences.getPreferences('user_profile');
await prefs.put('profile', JSON.stringify(profile));
await prefs.flush();

public async getUserProfile(): Promise<UserProfile | null> {

// 先从分布式数据获取
const distributedProfile = await foodSyncService.getUserProfile();
if (distributedProfile) {
  return distributedProfile;

// 如果分布式数据没有,从本地获取

const prefs = await preferences.getPreferences('user_profile');
const localProfile = await prefs.get('profile', '');
return localProfile ? JSON.parse(localProfile) : null;

public async calculateBMI(weight: number, height: number): Promise<number> {

return weight / Math.pow(height / 100, 2);

public async getBMIStatus(bmi: number): Promise<string> {

if (bmi < 18.5) return '偏瘦';
if (bmi < 24) return '正常';
if (bmi < 28) return '超重';
return '肥胖';

}

export const userProfileService = UserProfileService.getInstance();

饮食计划提醒

// MealReminderService.ets
import reminderAgent from ‘@ohos.reminderAgent’;

class MealReminderService {
private static instance: MealReminderService;

private constructor() {}

public static getInstance(): MealReminderService {
if (!MealReminderService.instance) {
MealReminderService.instance = new MealReminderService();
return MealReminderService.instance;

public async scheduleMealReminder(mealType: string, time: string): Promise<void> {

const [hours, minutes] = time.split(':').map(Number);
const reminderTime = new Date();
reminderTime.setHours(hours, minutes, 0, 0);

const reminderRequest: reminderAgent.ReminderRequest = {
  reminderType: reminderAgent.ReminderType.REMINDER_TYPE_TIMER,
  actionButton: [{ title: '已用餐' }, { title: '稍后提醒' }],
  wantAgent: {
    pkgName: 'com.example.foodCalorie',
    abilityName: 'MealReminderAbility'
  },
  ringDuration: 60,
  snoozeTimes: 2,
  triggerTime: reminderTime.getTime(),
  repeatInterval: 24  60  60 * 1000, // 每天重复
  title: ${mealType}时间到,
  content: 该吃${mealType}了,记得记录哦!,
  expiredContent: "用餐提醒已过期"
};

await reminderAgent.publishReminder(reminderRequest);

public async cancelAllMealReminders(): Promise<void> {

const reminders = await reminderAgent.getValidReminders();
await Promise.all(reminders.map(rem => 
  reminderAgent.cancelReminder(rem.id)
));

}

export const mealReminderService = MealReminderService.getInstance();

五、总结

本简易菜品热量计算应用实现了以下核心价值:
精准识别:利用AI技术准确识别常见菜品和分量

热量计算:精确计算菜品总热量和营养成分

健康管理:根据用户目标提供个性化饮食建议

多设备协同:支持家庭成员共享饮食记录

习惯养成:通过提醒帮助用户规律饮食

扩展方向:
增加菜品DIY功能,自定义食材和分量

开发饮食计划生成功能

集成运动数据,计算热量平衡

增加社区分享功能,分享健康食谱

注意事项:
需要申请ohos.permission.CAMERA权限

菜品识别准确率受图片质量和菜品特征可见度影响

热量计算为估算值,实际可能有偏差

多设备协同需保持网络连接

健康建议仅供参考,具体饮食请咨询专业营养师

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