美食菜谱完整源码与主明细表通用模板(HarmonyOS 版)


实例:美食菜谱(Recipe Book)|技术:全量代码解读、操作剧本、主明细表模式抽象
一、本篇定位
前两篇分别讲了数据层与 UI 层,本篇完成三件事:
- 全量源码逐段解读——RecipeDao 与 RecipePage 从头到尾过一遍;
- 运行效果演示——从新增菜谱到食材反查的完整操作剧本;
- 模式抽象——“主明细表”(主表 + 结构性明细)通用模板,附两个改写场景。
二、完整源码: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 见中篇
}
页面组织纪律回顾
- 状态量四组:列表态、反查态(searchIng/searchResult)、表单态(f*)、抽屉态;
- 特色交互前置:反查行在分类 Tab 之上;
- UI 零 SQL:食材解析、难度映射都在页面方法,数据来自 DAO。
四、接入工程三步
Step 1:放文件
RecipeDao.ets→entry/src/main/ets/database/RecipePage.ets→entry/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 |
场景二:按食材反查 ★
- 输入"鸡蛋" → 点搜;
- 结果:“含「鸡蛋」:西红柿炒鸡蛋、紫菜蛋花汤、蛋炒饭、芒果班戟、皮蛋瘦肉粥”(去重后 5 道);
- 输入"五花肉" → “含「五花肉」:红烧肉”;
- 输入"鱼翅" → “没有找到含「鱼翅」的菜谱”。
场景三:分类筛选
点"凉菜"Tab → 只显示拍黄瓜、凉拌木耳。点"甜品"→ 只显示芒果班戟。点"全部"恢复。
场景四:查看详情
点"红烧肉"卡片 → 抽屉:
- 头部:“红烧肉 · 家常菜 · 较难 · 约 90 分钟 · 4 人份”;
- 🧺 食材清单:五花肉 500克 / 冰糖 20克 / 生抽 2勺 / 老抽 1勺 / 料酒 2勺;
- 📝 做法步骤:4 步多行;
- 💡 小贴士:“小火慢炖是关键;最后收汁别糊锅”。
场景五:新增菜谱
- 点 + → 弹窗;
- 菜名"清炒时蔬"、用时"10"、难度"简单";
- 食材多行输入:
西兰花|1颗 胡萝卜|半根 大蒜|2瓣 - 步骤:“1. 西兰花焯水\n2. 热油爆香蒜末\n3. 下菜翻炒调味”;
- 保存 → toast"✅ 菜谱已保存",列表顶部出现新卡(主料:西兰花)。
场景六:删除
打开任意菜谱 → 点"🗑 删除此菜谱"→ 确认 → toast"🗑 已删除"。
场景七:数据持久性
杀应用重开 → 数据仍在。
六、模式抽象:主明细表通用模板
6.1 模型
┌───────────────────────────────────────────────┐
│ 主表(如 recipe) │
│ id / 主属性... / 展示性长文本(steps/tips) │
│ ── 实体的"头":是什么、元信息 │
└──────────────────────┬────────────────────────┘
│ 1:N(结构性内容)
┌──────────────────────┴────────────────────────┐
│ 明细表(如 recipe_ingredient) │
│ id / 主表_id / 明细属性(name) / 修饰(amount) │
│ ── 实体的"身":由什么组成、每项多少 │
└───────────────────────────────────────────────┘
6.2 六个约定
- 主表存元信息,明细表存组成:头身分离,各自演进;
- 明细是主表实体的必要组成:没有明细,主表不完整(区别于 26 的可选时间线);
- 新增必须事务:主表 + 全部明细原子入库;
- 删除先子后父:先清明细再删主表;
- 只展示的内容不建表:步骤用 TEXT,能查询聚合的才建明细表;
- 反查从明细出发:按明细字段找主表,从表驱动 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 系列的补充应用,将回到自定义绘制主题。
更多推荐


所有评论(0)