基于HarmonyOS的情侣美食管理应用开发实战(一)- 项目设计与角色管理

📖 系列文章目录

  • (一)项目设计与角色管理(本文)
  • (二)菜谱管理与订单系统
  • (三)美食相册与设置功能
  • (四)数据库优化与问题解决

一、项目背景与需求分析

1.1 项目灵感

在情侣日常生活中,经常面临这样的场景:

  • 主厨不知道今天该做什么菜
  • 食客想吃什么但不知道如何表达
  • 做过的美食没有记录,下次想做时找不到
  • 想要记录一起品尝美食的美好时光

基于这些痛点,我开发了一款情侣美食管理应用,帮助情侣更好地规划、记录和分享美食时光。

1.2 核心功能

功能模块 主厨角色 食客角色
角色管理 选择成为主厨,管理菜谱 选择成为食客,浏览点餐
菜谱管理 添加、编辑、删除菜谱 浏览菜谱、查看详情
订单管理 接单、完成订单 点餐、查看订单状态
美食相册 上传美食照片 浏览、点赞美食照片
设置页面 修改个人信息、清除数据 修改个人信息、清除数据

1.3 技术选型

  • 开发框架:HarmonyOS ArkUI(声明式UI)
  • 开发语言:ArkTS(TypeScript扩展)
  • 数据存储:关系型数据库(RDB)
  • 状态管理:@State、@Prop、@Link装饰器
  • UI设计:HarmonyOS设计规范

二、项目架构设计

2.1 目录结构

entry/src/main/ets/
├── common/                 # 公共工具类
│   ├── CommonConstant.ets  # 常量定义
│   ├── LogUtil.ets         # 日志工具
│   ├── RdbUtil.ets         # 数据库工具
│   └── PreferencesUtil.ets # 偏好设置工具
├── model/                  # 数据模型
│   ├── Recipe.ets          # 菜谱模型
│   ├── Order.ets           # 订单模型
│   ├── GalleryImage.ets    # 相册图片模型
│   └── CoupleConfig.ets    # 情侣配置模型
├── pages/                  # 页面
│   ├── SplashPage.ets      # 启动页
│   ├── RoleSelectPage.ets  # 角色选择页
│   ├── ChefHomePage.ets    # 主厨首页
│   ├── DinerHomePage.ets   # 食客首页
│   ├── RecipeManagePage.ets # 菜谱管理页
│   ├── OrderManagePage.ets # 订单管理页
│   ├── GalleryPage.ets     # 美食相册页
│   └── SettingsPage.ets    # 设置页
└── entryability/
    └── EntryAbility.ets    # 应用入口

2.2 数据库设计

2.2.1 情侣配置表(couple_config)
CREATE TABLE IF NOT EXISTS couple_config (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  space_name TEXT NOT NULL,      -- 空间名称
  chef_name TEXT NOT NULL,       -- 主厨昵称
  diner_name TEXT NOT NULL,      -- 食客昵称
  create_time INTEGER NOT NULL   -- 创建时间
)
2.2.2 菜谱表(recipe)
CREATE TABLE IF NOT EXISTS recipe (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,            -- 菜谱名称
  type INTEGER NOT NULL,         -- 菜谱类型(0-荤菜 1-素菜 2-汤品 3-主食 4-甜点)
  image TEXT,                    -- 菜谱图片
  ingredients TEXT,              -- 食材清单
  steps TEXT,                    -- 制作步骤
  create_time INTEGER NOT NULL   -- 创建时间
)
2.2.3 订单表(order_record)
CREATE TABLE IF NOT EXISTS order_record (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  recipe_id INTEGER NOT NULL,    -- 菜谱ID
  recipe_name TEXT NOT NULL,     -- 菜谱名称
  recipe_image TEXT,             -- 菜谱图片
  status INTEGER DEFAULT 0,      -- 订单状态(0-待接单 1-制作中 2-已完成)
  remark TEXT,                   -- 备注信息
  order_time INTEGER NOT NULL,   -- 下单时间
  accept_time INTEGER,           -- 接单时间
  complete_time INTEGER,         -- 完成时间
  FOREIGN KEY (recipe_id) REFERENCES recipe(id) ON DELETE CASCADE
)
2.2.4 美食相册表(gallery_img)
CREATE TABLE IF NOT EXISTS gallery_img (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  image TEXT NOT NULL,           -- 图片路径
  description TEXT,              -- 图片描述
  create_time INTEGER NOT NULL   -- 创建时间
)

三、角色管理功能实现

3.1 启动页设计

启动页负责:

  1. 初始化数据库
  2. 检查用户角色状态
  3. 路由到对应页面
// SplashPage.ets 核心逻辑
async aboutToAppear() {
  // 初始化数据库
  await RdbUtil.init(this.context)
  
  // 检查角色状态
  const role = await PreferencesUtil.get('current_role', '')
  
  if (role === 'chef') {
    // 主厨角色 -> 跳转主厨首页
    router.replaceUrl({ url: 'pages/ChefHomePage' })
  } else if (role === 'diner') {
    // 食客角色 -> 跳转食客首页
    router.replaceUrl({ url: 'pages/DinerHomePage' })
  } else {
    // 无角色 -> 跳转角色选择页
    router.replaceUrl({ url: 'pages/RoleSelectPage' })
  }
}

3.2 角色选择页面

在这里插入图片描述

角色选择页面功能:

  • 输入空间名称(情侣专属空间)
  • 输入主厨昵称
  • 输入食客昵称
  • 选择角色(主厨/食客)
  • 保存配置并跳转
3.2.1 页面布局
// 核心UI结构
Column() {
  // 标题
  Text('创建美食空间')
    .fontSize(28)
    .fontWeight(FontWeight.Bold)
  
  // 表单输入
  TextInput({ placeholder: '请输入空间名称' })
    .onChange((value) => this.spaceName = value)
  
  TextInput({ placeholder: '请输入主厨昵称' })
    .onChange((value) => this.chefName = value)
  
  TextInput({ placeholder: '请输入食客昵称' })
    .onChange((value) => this.dinerName = value)
  
  // 角色选择
  Row() {
    Button('我是主厨')
      .onClick(() => this.selectRole('chef'))
    
    Button('我是食客')
      .onClick(() => this.selectRole('diner'))
  }
}
3.2.2 数据保存逻辑
async selectRole(role: string) {
  // 1. 构建配置数据
  const values: relationalStore.ValuesBucket = {
    space_name: this.spaceName.trim(),
    chef_name: this.chefName.trim(),
    diner_name: this.dinerName.trim(),
    create_time: Date.now()
  }
  
  // 2. 检查是否已有配置
  const predicates = new relationalStore.RdbPredicates('couple_config')
  const resultSet = await RdbUtil.query(predicates)
  const hasExisting = resultSet.rowCount > 0
  resultSet.close()
  
  // 3. 保存到数据库
  if (hasExisting) {
    await RdbUtil.update('couple_config', values, predicates)
  } else {
    await RdbUtil.insert('couple_config', values)
  }
  
  // 4. 保存角色到偏好设置
  await PreferencesUtil.put('current_role', role)
  
  // 5. 跳转到对应首页
  const targetPage = role === 'chef' ? 'pages/ChefHomePage' : 'pages/DinerHomePage'
  router.replaceUrl({ url: targetPage })
}

3.3 角色持久化机制

3.3.1 为什么需要持久化?

用户选择角色后,希望下次打开应用时自动进入对应页面,无需重复选择。这需要将角色信息持久化存储。

3.3.2 持久化方案

使用 Preferences 存储角色信息:

// PreferencesUtil.ets
export class PreferencesUtil {
  private static preferences: preferences.Preferences | null = null
  
  // 初始化
  static async init(context: Context) {
    PreferencesUtil.preferences = 
      await preferences.getPreferences(context, 'my_preferences')
  }
  
  // 保存数据
  static async put(key: string, value: string) {
    await PreferencesUtil.preferences?.put(key, value)
    await PreferencesUtil.preferences?.flush()
  }
  
  // 读取数据
  static async get(key: string, defaultValue: string) {
    return await PreferencesUtil.preferences?.get(key, defaultValue) as string
  }
}

四、首页功能实现

4.1 主厨首页

主厨首页包含四个功能模块:

  • 菜谱管理
  • 订单管理
  • 美食相册
  • 设置
// ChefHomePage.ets 核心结构
@Entry
@Component
struct ChefHomePage {
  @State coupleConfig: CoupleConfig = {
    id: 0,
    space_name: '',
    chef_name: '未命名',
    diner_name: '未命名',
    create_time: 0
  }
  
  async aboutToAppear() {
    await this.loadCoupleConfig()
  }
  
  async loadCoupleConfig() {
    const predicates = new relationalStore.RdbPredicates('couple_config')
    const resultSet = await RdbUtil.query(predicates)
    
    if (resultSet.goToFirstRow()) {
      this.coupleConfig = {
        id: resultSet.getLong(resultSet.getColumnIndex('id')),
        space_name: resultSet.getString(resultSet.getColumnIndex('space_name')),
        chef_name: resultSet.getString(resultSet.getColumnIndex('chef_name')),
        diner_name: resultSet.getString(resultSet.getColumnIndex('diner_name')),
        create_time: resultSet.getLong(resultSet.getColumnIndex('create_time'))
      }
    }
    resultSet.close()
  }
  
  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text(`主厨:${this.coupleConfig.chef_name}`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
        
        Blank()
        
        Text(this.coupleConfig.space_name)
          .fontSize(16)
          .fontColor('#666666')
      }
      .width('100%')
      .padding(16)
      
      // 功能模块网格
      Grid() {
        GridItem() {
          this.MenuItem('菜谱管理', $r('app.media.ic_recipe'))
        }
        .onClick(() => {
          router.pushUrl({ url: 'pages/RecipeManagePage' })
        })
        
        GridItem() {
          this.MenuItem('订单管理', $r('app.media.ic_order'))
        }
        .onClick(() => {
          router.pushUrl({ url: 'pages/OrderManagePage' })
        })
        
        GridItem() {
          this.MenuItem('美食相册', $r('app.media.ic_gallery'))
        }
        .onClick(() => {
          router.pushUrl({ url: 'pages/GalleryPage' })
        })
        
        GridItem() {
          this.MenuItem('设置', $r('app.media.ic_settings'))
        }
        .onClick(() => {
          router.pushUrl({ url: 'pages/SettingsPage' })
        })
      }
      .columnsTemplate('1fr 1fr')
      .rowsTemplate('1fr 1fr')
    }
  }
}

4.2 食客首页

食客首页与主厨首页类似,但功能权限不同:

  • 菜谱浏览(只读)
  • 点餐下单
  • 美食相册
  • 设置

五、遇到的问题与解决方案

5.1 问题:角色选择后显示"未命名"

问题描述:
选择角色并提交后,跳转到首页,左上角显示"主厨:未命名"或"食客:未命名",但数据库中已有数据。

问题分析:

  1. 数据库表结构与代码模型不匹配
  2. 原表缺少 space_name 字段
  3. 数据插入失败但未报错

解决方案:

步骤1:修正数据库表结构
-- 修改前
CREATE TABLE IF NOT EXISTS couple_config (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  chef_name TEXT NOT NULL,
  diner_name TEXT NOT NULL,
  chef_avatar TEXT,
  diner_avatar TEXT,
  create_time INTEGER NOT NULL,
  update_time INTEGER NOT NULL
)

-- 修改后
CREATE TABLE IF NOT EXISTS couple_config (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  space_name TEXT NOT NULL,      -- ✅ 新增字段
  chef_name TEXT NOT NULL,
  diner_name TEXT NOT NULL,
  create_time INTEGER NOT NULL
)
步骤2:添加强制重建数据库逻辑
// RdbUtil.ets
static async init(context: Context): Promise<void> {
  // 删除旧数据库强制重建(开发阶段)
  try {
    await relationalStore.deleteRdbStore(context, CommonConstant.DB_NAME)
    LogUtil.info(RdbUtil.TAG, '已删除旧数据库,将重新创建')
  } catch (err) {
    LogUtil.info(RdbUtil.TAG, '数据库不存在或删除失败,继续创建新数据库')
  }
  
  // 创建新数据库
  RdbUtil.rdbStore = await relationalStore.getRdbStore(context, config)
  await RdbUtil.createTables()
}

5.2 问题:清除缓存后角色丢失

问题描述:
用户清除应用缓存后,preferences被清除,但数据库数据还在,导致角色丢失但配置仍存在。

解决方案:
在角色选择页加载已有配置,自动填充表单:

// RoleSelectPage.ets
async loadExistingConfig() {
  const predicates = new relationalStore.RdbPredicates('couple_config')
  const resultSet = await RdbUtil.query(predicates)
  
  if (resultSet.goToFirstRow()) {
    this.spaceName = resultSet.getString(resultSet.getColumnIndex('space_name'))
    this.chefName = resultSet.getString(resultSet.getColumnIndex('chef_name'))
    this.dinerName = resultSet.getString(resultSet.getColumnIndex('diner_name'))
    LogUtil.info('RoleSelectPage', '已加载历史配置')
  }
  resultSet.close()
}

六、总结

本篇文章介绍了情侣美食管理应用的整体架构设计和角色管理功能的实现。主要内容包括:

  1. 项目架构:清晰的目录结构,模块化设计
  2. 数据库设计:四张核心表,满足业务需求
  3. 角色管理:启动页、角色选择页、持久化机制
  4. 问题解决:数据库表结构修正、强制重建机制

下一篇文章将详细介绍菜谱管理和订单系统的实现,包括:

  • 菜谱的增删改查
  • 图片上传与展示
  • 订单流程管理
  • 状态同步机制

Logo

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

更多推荐