在这里插入图片描述
在这里插入图片描述

实例:美食菜谱(Recipe Book)|技术:全量代码解读、操作剧本、主明细表模式抽象

一、本篇定位

前两篇分别讲了数据层与 UI 层,本篇完成三件事:

  1. 全量源码逐段解读——RecipeDao 与 RecipePage 从头到尾过一遍;
  2. 运行效果演示——从新增菜谱到食材反查的完整操作剧本;
  3. 模式抽象——“主明细表”(主表 + 结构性明细)通用模板,附两个改写场景。

二、完整源码:RecipeDao.ets

/**
 * 美食菜谱数据访问对象(DAO)
 * 对应文章:30_美食菜谱/30-1 建表与数据层设计、30-2 页面UI与操作实现、30-3 完整代码与运行效果
 * 核心能力:菜谱主表 + 食材明细从表(一对多)、LIKE 按食材反查菜谱、步骤文本、分类筛选
 */
import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

/** 菜谱实体,对应 recipe 表结构 */
export interface Recipe {
  id: number;
  name: string;
  category: string;
  difficulty: number;    // 1易 2中 3难
  timeCost: number;      // 分钟
  servings: number;      // 几人份
  steps: string;         // \n 分隔
  tips: string;
  createdAt: number;
}

/** 食材明细实体,对应 recipe_ingredient 表结构 */
export interface Ingredient {
  id: number;
  recipeId: number;
  name: string;
  amount: string;
}

/** 菜谱视图:菜谱 + 食材列表 + 主料提取 */
export interface RecipeView {
  recipe: Recipe;
  ingredients: Ingredient[];
  mainIngredient: string;
}

/** 按食材反查的结果条目 */
export interface IngredientMatch {
  recipeId: number;
  recipeName: string;
  category: string;
  ingredient: string;
}

const DOMAIN = 0x0001;
const TAG = 'RecipeDao';

/** 菜谱分类常量 */
export const RECIPE_CATEGORIES: string[] = ['家常菜', '汤羹', '甜品', '主食', '凉菜', '早餐'];

export class RecipeDao {
  private static readonly TABLE = 'recipe';
  private static readonly INGREDIENT_TABLE = 'recipe_ingredient';
  private static store?: relationalStore.RdbStore;
  // getStore / 行映射 / CRUD / 反查 / 统计 / 种子数据(见上篇逐段解读)
}

逐段解读总表

代码段 职责 关键决策
实体接口 Recipe / Ingredient / RecipeView / IngredientMatch View 封装主从组装
RECIPE_CATEGORIES 分类常量 页面 Tab 驱动
getStore 单例 + 建 2 表 + 3 索引 ing_name 索引服务反查
insertRecipe 事务:主表 + 循环插明细 一道菜原子入库
deleteRecipe 先删食材再删菜谱 杜绝孤儿
queryIngredients 按菜谱查食材 详情组装
queryRecipeViews 主从组装 + 主料提取 N+1 可接受
searchByIngredient 从表 LIKE + JOIN 主表 反查方向反转
summary COUNT DISTINCT + AVG 库规模统计
initSeedData 10 道菜 + 食材明细 反查多命中演示

三、完整源码:RecipePage.ets

/**
 * 美食菜谱页面
 * 对应文章:30_美食菜谱/30-1 ~ 30-3
 * 功能:分类筛选 + 菜谱卡片列表 + 菜谱详情(食材+步骤+贴士)+ 新增菜谱 + 按食材反查
 */
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { RecipeDao, Recipe, RecipeView, Ingredient, RECIPE_CATEGORIES } from '../../database/RecipeDao';

@Entry
@Component
struct RecipePage {
  @State views: RecipeView[] = [];
  @State filter: string = '';
  @State summaryText: string = '';
  @State detailVisible: boolean = false;
  @State current: RecipeView | null = null;
  @State formVisible: boolean = false;
  @State fName: string = '';
  @State fCategory: string = '家常菜';
  @State fDifficulty: number = 1;
  @State fTimeCost: string = '';
  @State fSteps: string = '';
  @State fIngredients: string = '';
  @State searchIng: string = '';
  @State searchResult: string = '';
  private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;

  aboutToAppear(): void {
    this.refresh();
  }
  // refresh/loadList/onSearchIngredient/onAdd/openDetail/onDelete/difficultyText 见中篇
}

页面组织纪律回顾

  1. 状态量四组:列表态、反查态(searchIng/searchResult)、表单态(f*)、抽屉态;
  2. 特色交互前置:反查行在分类 Tab 之上;
  3. UI 零 SQL:食材解析、难度映射都在页面方法,数据来自 DAO。

四、接入工程三步

Step 1:放文件

  • RecipeDao.etsentry/src/main/ets/database/
  • RecipePage.etsentry/src/main/ets/pages/samples/

Step 2:注册路由(main_pages.json)

"pages/samples/RecipePage",

Step 3:首页入口(Index.ets)

Button('🍳 30 美食菜谱')
  .fontSize(14)
  .width('80%')
  .onClick(() => {
    this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/RecipePage' });
  })

五、运行效果演示(操作剧本)

场景一:首次打开

步骤 界面表现 数据来源
1 标题栏:“🍳 美食菜谱”,统计:“10 道菜 · 6 类 · 平均 30 分钟 · 食材 40 种” summary
2 反查行:输入框 + 紫色"搜"按钮
3 分类 Tab:全部 + 家常菜/汤羹/甜品/主食/凉菜/早餐(可横滑) RECIPE_CATEGORIES
4 列表 10 道菜:每卡含菜名、紫色分类徽标、难度/用时/人份、主料 queryRecipeViews

场景二:按食材反查 ★

  1. 输入"鸡蛋" → 点搜;
  2. 结果:“含「鸡蛋」:西红柿炒鸡蛋、紫菜蛋花汤、蛋炒饭、芒果班戟、皮蛋瘦肉粥”(去重后 5 道);
  3. 输入"五花肉" → “含「五花肉」:红烧肉”;
  4. 输入"鱼翅" → “没有找到含「鱼翅」的菜谱”。

场景三:分类筛选

点"凉菜"Tab → 只显示拍黄瓜、凉拌木耳。点"甜品"→ 只显示芒果班戟。点"全部"恢复。

场景四:查看详情

点"红烧肉"卡片 → 抽屉:

  • 头部:“红烧肉 · 家常菜 · 较难 · 约 90 分钟 · 4 人份”;
  • 🧺 食材清单:五花肉 500克 / 冰糖 20克 / 生抽 2勺 / 老抽 1勺 / 料酒 2勺;
  • 📝 做法步骤:4 步多行;
  • 💡 小贴士:“小火慢炖是关键;最后收汁别糊锅”。

场景五:新增菜谱

  1. 点 + → 弹窗;
  2. 菜名"清炒时蔬"、用时"10"、难度"简单";
  3. 食材多行输入:
    西兰花|1颗
    胡萝卜|半根
    大蒜|2瓣
    
  4. 步骤:“1. 西兰花焯水\n2. 热油爆香蒜末\n3. 下菜翻炒调味”;
  5. 保存 → toast"✅ 菜谱已保存",列表顶部出现新卡(主料:西兰花)。

场景六:删除

打开任意菜谱 → 点"🗑 删除此菜谱"→ 确认 → toast"🗑 已删除"。

场景七:数据持久性

杀应用重开 → 数据仍在。

六、模式抽象:主明细表通用模板

6.1 模型

┌───────────────────────────────────────────────┐
│  主表(如 recipe)                              │
│  id / 主属性... / 展示性长文本(steps/tips)      │
│  ── 实体的"头":是什么、元信息                   │
└──────────────────────┬────────────────────────┘
                       │ 1:N(结构性内容)
┌──────────────────────┴────────────────────────┐
│  明细表(如 recipe_ingredient)                 │
│  id / 主表_id / 明细属性(name) / 修饰(amount)   │
│  ── 实体的"身":由什么组成、每项多少             │
└───────────────────────────────────────────────┘

6.2 六个约定

  1. 主表存元信息,明细表存组成:头身分离,各自演进;
  2. 明细是主表实体的必要组成:没有明细,主表不完整(区别于 26 的可选时间线);
  3. 新增必须事务:主表 + 全部明细原子入库;
  4. 删除先子后父:先清明细再删主表;
  5. 只展示的内容不建表:步骤用 TEXT,能查询聚合的才建明细表;
  6. 反查从明细出发:按明细字段找主表,从表驱动 JOIN。

6.3 两个改写场景

场景 A:购物清单/订单

菜谱实例 订单改写
recipe order(订单:单号/时间/总额)
recipe_ingredient order_item(商品:名称/数量/单价/小计)
反查食材 反查"哪些订单含某商品"

(注:09 订单明细已实现此场景——30 是从"商品结构"角度再讲一次范式)

场景 B:配方成分(饮品/烘焙)

菜谱实例 配方改写
recipe formula(配方:名称/类型/产出量)
recipe_ingredient formula_ingredient(成分:原料/克数)
反查食材 “有哪些配方用牛奶”

场景 C:组装清单(家居/DIY)

菜谱实例 组装改写
recipe product(产品:名称/型号/说明书)
recipe_ingredient part(零件:名称/编号/数量)

三个场景的共同点:"实体 + 组成明细"结构。掌握 30,任何"由多个部分组成的事物"(菜、订单、配方、产品)都能建模。

七、三篇内容总对照

篇章 核心知识点 一句话总结
30-1 建表与数据层设计 主明细表、事务批量、LIKE 反查 组成明细独立成表,反查方向反转
30-2 页面UI与操作实现 反查交互、三段式详情、食材解析 特色功能前置,表单轻量
30-3 完整代码与运行效果 全量源码、操作剧本、明细模板 主表存头明细存身,改表名即复用

八、FAQ(三篇汇总补遗)

Q1:26-30 五个主从表实例如何总结?
A:五种子从表语义:26 事件(状态时间线)、27 交易(借阅)、28 交易+计划(保养)、29 审计(数量日志)、30 结构(食材明细)。从"记录发生了什么"到"构成是什么"——从表语义随业务需求演进,选对语义就选对了表设计。

Q2:反查为什么不用谓词 IN 而用 LIKE?
A:IN ('鸡蛋') 是精确匹配,LIKE '%鸡蛋%' 是模糊匹配。“鸡蛋清”、"鸡蛋"都能命中——家里买的是"鸡蛋"这种模糊食材概念,模糊匹配更贴合。精确场景(标准 SKU)用 IN/equalTo。

Q3:食材明细能排序吗(按重要性)?
A:可以,ORDER BY id 保留输入顺序(第一行=主料)。若要"按名称排序",ORDER BY name输入顺序即业务顺序(主料在前)是合理约定。

Q4:新增菜谱的分类怎么选?
A:本实例表单分类写死"家常菜"(fCategory 默认值),用户无法在弹窗改分类——教学简化。生产可加分类选择器(胶囊单选,同难度)。分类选择器与难度选择器同款实现

Q5:统计的"食材 40 种"为什么不精确?
A:COUNT(*) 计明细行含重复。改为 COUNT(DISTINCT name) 即去重后"真实食材种类"。文中已说明,读者可自行修正——统计口径要服务于文案语义

Q6:这实例还能扩展什么?
A:① 步骤配图(steps 表化 + image_path);② 收藏与"我的菜单"(收藏表);③ 食材价格统计(购物预算,关联 03 记账本);④ 多人协作(家庭共享菜谱库)。

九、结语

三篇文章完整走完了美食菜谱的"设计 → 实现 → 复用"闭环:

  • -1 教你主明细建模:结构性内容独立成表、事务批量插入、从表驱动反查;
  • -2 教你特色交互:食材反查前置、三段式详情、多行文本解析;
  • -3 教你模式复用:主表存头明细存身,改表名即覆盖订单、配方、组装清单。

从单表 CRUD 到五种主从表语义,这套工程的数据库能力已覆盖个人应用 95% 的建模场景*。下一篇开始进入 Canvas 系列的补充应用,将回到自定义绘制主题。


Logo

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

更多推荐