基于HarmonyOS的情侣美食管理应用开发实战(一)- 项目设计与角色管理-1
📖 系列文章目录
- (一)项目设计与角色管理(本文)
- (二)菜谱管理与订单系统
- (三)美食相册与设置功能
- (四)数据库优化与问题解决
…
一、项目背景与需求分析
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)
CREATETABLEIFNOTEXISTScouple_config(idINTEGERPRIMARYKEYAUTOINCREMENT,space_nameTEXTNOTNULL,-- 空间名称chef_nameTEXTNOTNULL,-- 主厨昵称diner_nameTEXTNOTNULL,-- 食客昵称create_timeINTEGERNOTNULL-- 创建时间)2.2.2 菜谱表(recipe)
CREATETABLEIFNOTEXISTSrecipe(idINTEGERPRIMARYKEYAUTOINCREMENT,nameTEXTNOTNULL,-- 菜谱名称typeINTEGERNOTNULL,-- 菜谱类型(0-荤菜 1-素菜 2-汤品 3-主食 4-甜点)imageTEXT,-- 菜谱图片ingredientsTEXT,-- 食材清单stepsTEXT,-- 制作步骤create_timeINTEGERNOTNULL-- 创建时间)2.2.3 订单表(order_record)
CREATETABLEIFNOTEXISTSorder_record(idINTEGERPRIMARYKEYAUTOINCREMENT,recipe_idINTEGERNOTNULL,-- 菜谱IDrecipe_nameTEXTNOTNULL,-- 菜谱名称recipe_imageTEXT,-- 菜谱图片statusINTEGERDEFAULT0,-- 订单状态(0-待接单 1-制作中 2-已完成)remarkTEXT,-- 备注信息order_timeINTEGERNOTNULL,-- 下单时间accept_timeINTEGER,-- 接单时间complete_timeINTEGER,-- 完成时间FOREIGNKEY(recipe_id)REFERENCESrecipe(id)ONDELETECASCADE)2.2.4 美食相册表(gallery_img)
CREATETABLEIFNOTEXISTSgallery_img(idINTEGERPRIMARYKEYAUTOINCREMENT,imageTEXTNOTNULL,-- 图片路径descriptionTEXT,-- 图片描述create_timeINTEGERNOTNULL-- 创建时间)三、角色管理功能实现
3.1 启动页设计
启动页负责:
- 初始化数据库
- 检查用户角色状态
- 路由到对应页面
// SplashPage.ets 核心逻辑asyncaboutToAppear(){// 初始化数据库awaitRdbUtil.init(this.context)// 检查角色状态constrole=awaitPreferencesUtil.get('current_role','')if(role==='chef'){// 主厨角色 -> 跳转主厨首页router.replaceUrl({url:'pages/ChefHomePage'})}elseif(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 数据保存逻辑
asyncselectRole(role:string){// 1. 构建配置数据constvalues:relationalStore.ValuesBucket={space_name:this.spaceName.trim(),chef_name:this.chefName.trim(),diner_name:this.dinerName.trim(),create_time:Date.now()}// 2. 检查是否已有配置constpredicates=newrelationalStore.RdbPredicates('couple_config')constresultSet=awaitRdbUtil.query(predicates)consthasExisting=resultSet.rowCount>0resultSet.close()// 3. 保存到数据库if(hasExisting){awaitRdbUtil.update('couple_config',values,predicates)}else{awaitRdbUtil.insert('couple_config',values)}// 4. 保存角色到偏好设置awaitPreferencesUtil.put('current_role',role)// 5. 跳转到对应首页consttargetPage=role==='chef'?'pages/ChefHomePage':'pages/DinerHomePage'router.replaceUrl({url:targetPage})}3.3 角色持久化机制
3.3.1 为什么需要持久化?
用户选择角色后,希望下次打开应用时自动进入对应页面,无需重复选择。这需要将角色信息持久化存储。
3.3.2 持久化方案
使用Preferences存储角色信息:
// PreferencesUtil.etsexportclassPreferencesUtil{privatestaticpreferences:preferences.Preferences|null=null// 初始化staticasyncinit(context:Context){PreferencesUtil.preferences=awaitpreferences.getPreferences(context,'my_preferences')}// 保存数据staticasyncput(key:string,value:string){awaitPreferencesUtil.preferences?.put(key,value)awaitPreferencesUtil.preferences?.flush()}// 读取数据staticasyncget(key:string,defaultValue:string){returnawaitPreferencesUtil.preferences?.get(key,defaultValue)asstring}}四、首页功能实现
4.1 主厨首页
主厨首页包含四个功能模块:
- 菜谱管理
- 订单管理
- 美食相册
- 设置
// ChefHomePage.ets 核心结构@Entry@Componentstruct ChefHomePage{@StatecoupleConfig:CoupleConfig={id:0,space_name:'',chef_name:'未命名',diner_name:'未命名',create_time:0}asyncaboutToAppear(){awaitthis.loadCoupleConfig()}asyncloadCoupleConfig(){constpredicates=newrelationalStore.RdbPredicates('couple_config')constresultSet=awaitRdbUtil.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 问题:角色选择后显示"未命名"
问题描述:
选择角色并提交后,跳转到首页,左上角显示"主厨:未命名"或"食客:未命名",但数据库中已有数据。
问题分析:
- 数据库表结构与代码模型不匹配
- 原表缺少
space_name字段 - 数据插入失败但未报错
解决方案:
步骤1:修正数据库表结构
-- 修改前CREATETABLEIFNOTEXISTScouple_config(idINTEGERPRIMARYKEYAUTOINCREMENT,chef_nameTEXTNOTNULL,diner_nameTEXTNOTNULL,chef_avatarTEXT,diner_avatarTEXT,create_timeINTEGERNOTNULL,update_timeINTEGERNOTNULL)-- 修改后CREATETABLEIFNOTEXISTScouple_config(idINTEGERPRIMARYKEYAUTOINCREMENT,space_nameTEXTNOTNULL,-- ✅ 新增字段chef_nameTEXTNOTNULL,diner_nameTEXTNOTNULL,create_timeINTEGERNOTNULL)步骤2:添加强制重建数据库逻辑
// RdbUtil.etsstaticasyncinit(context:Context):Promise<void>{// 删除旧数据库强制重建(开发阶段)try{awaitrelationalStore.deleteRdbStore(context,CommonConstant.DB_NAME)LogUtil.info(RdbUtil.TAG,'已删除旧数据库,将重新创建')}catch(err){LogUtil.info(RdbUtil.TAG,'数据库不存在或删除失败,继续创建新数据库')}// 创建新数据库RdbUtil.rdbStore=awaitrelationalStore.getRdbStore(context,config)awaitRdbUtil.createTables()}5.2 问题:清除缓存后角色丢失
问题描述:
用户清除应用缓存后,preferences被清除,但数据库数据还在,导致角色丢失但配置仍存在。
解决方案:
在角色选择页加载已有配置,自动填充表单:
// RoleSelectPage.etsasyncloadExistingConfig(){constpredicates=newrelationalStore.RdbPredicates('couple_config')constresultSet=awaitRdbUtil.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()}六、总结
本篇文章介绍了情侣美食管理应用的整体架构设计和角色管理功能的实现。主要内容包括:
- 项目架构:清晰的目录结构,模块化设计
- 数据库设计:四张核心表,满足业务需求
- 角色管理:启动页、角色选择页、持久化机制
- 问题解决:数据库表结构修正、强制重建机制
下一篇文章将详细介绍菜谱管理和订单系统的实现,包括:
- 菜谱的增删改查
- 图片上传与展示
- 订单流程管理
- 状态同步机制