"冰箱里有鸡蛋和土豆,今天能做什么?"这是菜谱应用中非常实用的一类入口。它不要求用户先知道菜名,而是从已有食材反推候选菜谱。看起来只要把食材关键词和菜谱文本做一次匹配即可,真正落地时却会遇到数据覆盖、别名归一、主辅料权重、缺失食材解释、排序稳定性和空结果等问题。
本文基于"健康菜谱助手"1.0.5 的真实 HarmonyOS ArkTS 工程展开。FridgeRecipePage.ets 已经提供 21 个常用食材、默认选中的鸡蛋与土豆、自定义食材输入,以及最多 12 条候选菜谱;项目总菜谱数是 60,但页面内的 recipeIngredientIndex 只覆盖 recipeId 1 到 20。换句话说,剩余 40 道菜没有结构化食材索引,只能偶然通过菜名、分类、描述或标签中的文字被命中。
冰箱推荐不是搜索框换了一个外观,而是把"我有什么、菜谱需要什么、还缺什么、为什么排在前面"计算成一份可解释结果。
HEALTHY-RECIPE-ONE7-FRIDGE-MATCH-20260730

本文将完成以下内容:
- 还原当前食材选择、文本匹配和候选排序链路。
- 解释 60 道菜与 20 条食材索引之间的数据覆盖缺口。
- 设计食材归一、别名映射和去重规则。
- 把"命中几项"升级为覆盖率、主料命中和缺失清单。
- 修复
duration.localeCompare()带来的字符串排序错误。 - 给出纯本地、可复核、无需联网的推荐模型。
- 覆盖空选择、无结果、重复输入和多设备布局测试。
一、真实页面目前做了什么
页面只维护两个可变字段:
ts
@State selectedIngredients: string[] = ['鸡蛋', '土豆']
@State customIngredient: string = ''
常用食材按蛋奶、肉类、水产、蔬菜和主食组织,共 21 项:
ts
private commonIngredients: FridgeIngredient[] = [
{ name: '鸡蛋', group: '蛋奶' },
{ name: '豆腐', group: '蛋奶' },
{ name: '牛奶', group: '蛋奶' },
{ name: '鸡胸肉', group: '肉类' },
// ...共 21 项
{ name: '面条', group: '主食' }
]
点击标签时使用不可变数组更新,能触发 ArkUI 状态刷新:
ts
toggleIngredient(name: string): void {
const index = this.selectedIngredients.indexOf(name)
if (index >= 0) {
const next = [...this.selectedIngredients]
next.splice(index, 1)
this.selectedIngredients = next
} else {
this.selectedIngredients = [...this.selectedIngredients, name]
}
}
这个交互基础是正确的。问题主要出在候选数据和评分规则,而不是标签本身。
二、当前推荐算法的完整链路
页面把菜名、菜系、描述、标签和食材索引拼成一个字符串:
ts
getMatchCount(recipe: RecipeModel): number {
const searchableText = `${recipe.name} ${recipe.category} ${recipe.description} `
+ `${recipe.tags.join(' ')} ${this.getIngredientText(recipe.id)}`
let count = 0
for (const ingredient of this.selectedIngredients) {
if (searchableText.indexOf(ingredient) >= 0) {
count++
}
}
return count
}
随后按命中数量降序、时长字符串升序排列:
ts
source.sort((a: RecipeModel, b: RecipeModel) => {
const scoreDiff = this.getMatchCount(b) - this.getMatchCount(a)
if (scoreDiff !== 0) {
return scoreDiff
}
return a.duration.localeCompare(b.duration)
})
有选择时保留命中数大于 0 的前 12 条;没有选择时直接返回排序后的前 8 条。这条链路足够演示"点食材后列表会变化",却不能解释一条菜为什么排在另一条前面。
三、先量化真实数据缺口
源码中的数量关系如下:
| 数据 | 数量 | 覆盖情况 |
|---|---|---|
RECIPE_LIST |
60 道菜 | 完整列表 |
| 常用食材 | 21 项 | 只用于输入 |
recipeIngredientIndex |
20 条 | 只覆盖 recipeId 1--20 |
| 候选上限 | 12 条 | 有食材时 |
| 空选择上限 | 8 条 | 无食材时 |
后 40 道菜的 getIngredientText() 都返回空字符串:
ts
getIngredientText(recipeId: number): string {
for (const item of this.recipeIngredientIndex) {
if (item.recipeId === recipeId) {
return item.ingredients.join('、')
}
}
return ''
}
例如"油焖大虾"是 recipeId 26。用户选择"大虾"时,这道菜没有结构化索引;只有当名称、描述或标签恰好出现"大虾"时才可能命中。推荐是否成功取决于文案措辞,而不是菜谱真实所需食材。
数据覆盖率只有:
text
20 / 60 = 33.3%
这应当作为上线前的硬检查,而不是由 UI 静默掩盖。

四、推荐模型要先从字符串变成结构化数据
建议把食材需求放到独立模型中:
ts
export type IngredientRole = 'primary' | 'secondary' | 'seasoning'
export interface RecipeIngredientRequirement {
ingredientId: string
displayName: string
role: IngredientRole
required: boolean
amount?: string
aliases?: string[]
}
export interface RecipeIngredientProfile {
recipeId: number
ingredients: RecipeIngredientRequirement[]
}
主料、辅料和调味料不能完全等权。用户有鸡胸肉,对宫保鸡丁的意义显然高于用户有大葱;盐、油、生抽等基础调味料也不应让每道菜都获得大量分数。
一个实际条目可以写成:
ts
const GONG_BAO_PROFILE: RecipeIngredientProfile = {
recipeId: 1,
ingredients: [
{ ingredientId: 'chicken_breast', displayName: '鸡胸肉', role: 'primary', required: true },
{ ingredientId: 'peanut', displayName: '花生米', role: 'secondary', required: true },
{ ingredientId: 'dried_chili', displayName: '干辣椒', role: 'secondary', required: false },
{ ingredientId: 'scallion', displayName: '大葱', role: 'seasoning', required: false },
{ ingredientId: 'ginger', displayName: '生姜', role: 'seasoning', required: false }
]
}
这样算法不再从一整段自然语言中猜食材。
五、食材输入必须归一化
用户可能输入"西红柿""番茄""小番茄",也可能输入带空格的" 鸡蛋 "。当前代码只执行 trim(),重复判断仍然是精确字符串比较:
ts
if (this.selectedIngredients.indexOf(name) < 0) {
this.selectedIngredients = [...this.selectedIngredients, name]
}
可以建立本地别名字典:
ts
const INGREDIENT_ALIASES: Record<string, string> = {
'西红柿': '番茄',
'小番茄': '番茄',
'圣女果': '番茄',
'马铃薯': '土豆',
'洋芋': '土豆',
'鸡肉丁': '鸡胸肉',
'虾仁': '大虾'
}
function normalizeIngredientName(input: string): string {
const compact = input.trim().replace(/\s+/g, '')
return INGREDIENT_ALIASES[compact] ?? compact
}
归一后再判断重复:
ts
addCustomIngredient(): void {
const normalized = normalizeIngredientName(this.customIngredient)
if (!normalized) {
promptAction.showToast({ message: '先输入一种食材' })
return
}
if (!this.selectedIngredients.includes(normalized)) {
this.selectedIngredients = [...this.selectedIngredients, normalized]
}
this.customIngredient = ''
}
别名表必须可审查,不能把不等价食材强行合并。例如"鱼头"不能直接等同于"草鱼","鸡腿"也不应默认等同于"鸡胸肉"。
六、把用户输入转成集合
评分时反复调用 indexOf 会让语义散落。可以在计算开始时生成规范集合:
ts
function buildPantrySet(selected: string[]): Set<string> {
const pantry = new Set<string>()
selected.forEach((item) => {
const normalized = normalizeIngredientName(item)
if (normalized) pantry.add(normalized)
})
return pantry
}
Set 同时解决去重和 O(1) 查找。对 21 个常用食材而言性能不是瓶颈,但统一数据形态能减少规则分叉。
七、定义一份可解释的 MatchResult
不要让 UI 只拿到一个数字。推荐函数应返回命中、缺失和解释:
ts
export interface IngredientMatch {
ingredientId: string
name: string
role: IngredientRole
}
export interface RecipeMatchResult {
recipe: RecipeModel
score: number
coverage: number
matched: IngredientMatch[]
missingRequired: IngredientMatch[]
missingOptional: IngredientMatch[]
reason: string
durationMinutes: number
}
这样卡片可以展示:
text
已具备 3/4 项核心食材
命中:土豆、茄子、青椒
还缺:大蒜
预计 15 分钟
这比"3项匹配"更能帮助用户做决定。
八、给主料、辅料和调味料不同权重
可以使用简单透明的本地规则:
ts
const ROLE_WEIGHT: Record<IngredientRole, number> = {
primary: 5,
secondary: 2,
seasoning: 0.5
}
function scoreIngredient(
item: RecipeIngredientRequirement,
pantry: Set<string>
): number {
return pantry.has(item.displayName) ? ROLE_WEIGHT[item.role] : 0
}
随后增加完整度和缺失主料惩罚:
ts
function calculateScore(
profile: RecipeIngredientProfile,
pantry: Set<string>
): number {
let score = 0
let missingPrimary = 0
for (const item of profile.ingredients) {
if (pantry.has(item.displayName)) {
score += ROLE_WEIGHT[item.role]
} else if (item.required && item.role === 'primary') {
missingPrimary++
}
}
return score - missingPrimary * 4
}
该规则不是"AI 黑盒",每一分都能解释。对于纯本地菜谱应用,这种透明度往往比复杂模型更重要。
九、覆盖率应该只计算相关食材
如果把盐、油、葱姜蒜都算入分母,覆盖率会被调味料稀释。可以只对 required 项计算:
ts
function calculateCoverage(
profile: RecipeIngredientProfile,
pantry: Set<string>
): number {
const required = profile.ingredients.filter((item) => item.required)
if (required.length === 0) return 0
const matched = required.filter((item) => pantry.has(item.displayName)).length
return matched / required.length
}
UI 展示时转成百分比,但排序保留原始浮点值。推荐条件可以定义为:
- 至少命中一个主料;或
- required 覆盖率达到 50%;或
- 没有主料的轻食菜谱命中两个辅料。
规则应写进测试,不要藏在多个页面判断里。
十、修复时长字符串排序
当前代码使用:
ts
a.duration.localeCompare(b.duration)
duration 的真实值覆盖 15 至 180 分钟,并包含 120分钟、180分钟 等数字分钟格式。字符串按字符顺序比较时,120分钟 会排在 20分钟 前,因为字符 '1' 小于 '2'。这与"更快完成"完全不同。
最小修复是解析数字:
ts
function parseDurationMinutes(duration: string): number {
const hourMatch = duration.match(/(\d+(?:\.\d+)?)\s*小时/)
const minuteMatch = duration.match(/(\d+)\s*分钟/)
const hours = hourMatch ? Number(hourMatch[1]) * 60 : 0
const minutes = minuteMatch ? Number(minuteMatch[1]) : 0
const total = hours + minutes
return Number.isFinite(total) && total > 0 ? total : Number.MAX_SAFE_INTEGER
}
当前数据都是分钟格式,也可以在 RecipeModel 中直接保存 durationMinutes: number,展示层再格式化。结构化数值优于每次排序时解析字符串。
十一、稳定排序需要完整优先级
推荐列表建议按以下顺序比较:
- 缺失必需主料数量升序。
- required 覆盖率降序。
- 评分降序。
- 烹饪分钟数升序。
- recipeId 升序,作为稳定兜底。
ts
function compareMatches(a: RecipeMatchResult, b: RecipeMatchResult): number {
const missing = a.missingRequired.length - b.missingRequired.length
if (missing !== 0) return missing
const coverage = b.coverage - a.coverage
if (coverage !== 0) return coverage
const score = b.score - a.score
if (score !== 0) return score
const duration = a.durationMinutes - b.durationMinutes
if (duration !== 0) return duration
return a.recipe.id - b.recipe.id
}
最后的 recipeId 保证相同条件下每次打开顺序一致,便于用户形成预期,也便于截图测试。
十二、不要让"未选择食材"偷偷使用推荐排序
当前空选择时仍对全部菜谱排序,只是所有匹配分都为 0,然后按错误的时长字符串取前 8 条。页面文案写"先展示常用快手菜",算法却没有"常用"数据,也没有正确的数值时长。
可以选择一种明确策略:
- 展示标签包含"快手菜"的菜谱,再按数值时长排序;
- 展示用户历史中常做的菜;
- 展示固定编辑精选;
- 只显示空态,引导至少选择一种食材。
如果当前产品没有历史统计,建议采用第一种:
ts
function getQuickRecipes(recipes: RecipeModel[]): RecipeModel[] {
return recipes
.filter((recipe) => recipe.tags.includes('快手菜') || parseDurationMinutes(recipe.duration) <= 20)
.sort((a, b) => parseDurationMinutes(a.duration) - parseDurationMinutes(b.duration))
.slice(0, 8)
}
这与"常用快手菜"的用户文案一致,也能被验证。
十三、60 道菜的食材索引必须闭合
推荐质量的第一门槛不是算法,而是数据完整。可以在开发阶段加入检查:
ts
function validateIngredientProfiles(
recipes: RecipeModel[],
profiles: RecipeIngredientProfile[]
): string[] {
const issues: string[] = []
const profileIds = new Set(profiles.map((item) => item.recipeId))
recipes.forEach((recipe) => {
if (!profileIds.has(recipe.id)) {
issues.push(`recipeId=${recipe.id} ${recipe.name} 缺少食材索引`)
}
})
profiles.forEach((profile) => {
if (!recipes.some((recipe) => recipe.id === profile.recipeId)) {
issues.push(`recipeId=${profile.recipeId} 没有对应菜谱`)
}
if (!profile.ingredients.some((item) => item.role === 'primary')) {
issues.push(`recipeId=${profile.recipeId} 缺少主料`)
}
})
return issues
}
基线应该是 60 个菜谱、60 个 profile、0 个孤立 ID。索引未补齐前,页面可以给旧的 20 道菜显示推荐结果,对其余菜谱明确标记"食材数据待完善",而不是假装算法覆盖完整。
十四、把计算移出 ArkUI build
当前 ForEach(this.getMatchedRecipes()) 在 UI 构建过程中执行排序,而卡片内部又多次调用 getMatchCount(recipe)。状态刷新时,同一批字符串会被重复拼接和扫描。
更清晰的做法是在选择变化时重算:
ts
@State matchResults: RecipeMatchResult[] = []
private refreshMatches(): void {
const pantry = buildPantrySet(this.selectedIngredients)
this.matchResults = this.recommendationService
.recommend(pantry)
.slice(0, 12)
}
toggleIngredient(name: string): void {
// 更新 selectedIngredients
this.refreshMatches()
}
build() 只消费 matchResults。推荐逻辑可以脱离 HarmonyOS UI 单独测试,页面也不会在每个卡片中重复计算。
十五、输入和结果状态要完整
页面至少需要以下状态:
ts
type RecommendationState =
'idle' | 'calculating' | 'content' | 'empty' | 'error'
虽然当前算法完全本地,计算几乎瞬时完成,但 empty 仍非常重要。用户输入"榴莲"后如果没有任何菜谱,不能只留下空白区域:
ts
if (this.state === 'empty') {
Column({ space: 10 }) {
Text('暂时没有匹配菜谱')
Text('可以减少一种食材,或换成番茄、鸡蛋等常用食材')
Button('清空重选').onClick(() => this.clearSelected())
}
}
错误态用于数据索引加载失败或结构校验失败;不要用"暂无结果"掩盖程序错误。
十六、卡片要解释推荐,不只显示分数
当前卡片显示菜名、N项匹配、完整食材字符串、时长、难度和热量。结构化结果后,建议把最有决策价值的信息放在前两行:
text
地三鲜 75% 已具备
已有:土豆、茄子、青椒
还缺:大蒜 15 分钟 · 简单
可使用以下字段生成文案:
ts
function buildReason(result: RecipeMatchResult): string {
const matched = result.matched.map((item) => item.name).slice(0, 3).join('、')
if (result.missingRequired.length === 0) {
return `核心食材已齐:${matched}`
}
const missing = result.missingRequired.map((item) => item.name).slice(0, 2).join('、')
return `已有 ${matched},还缺 ${missing}`
}
不要使用"智能推荐""最适合"等无法验证的绝对表述。页面只应陈述本地规则算出的事实。

十七、服务层如何组织
建议拆成三个关注点:
17.1 IngredientNormalizer
负责空格清理、别名映射和去重,不知道任何页面信息。
17.2 RecipeIngredientRepository
按 recipeId 提供食材 profile,并在启动或测试阶段执行 60/60 完整性检查。
17.3 FridgeRecommendationService
接收 pantry 集合,输出 RecipeMatchResult[]。它只做匹配、评分、解释和排序,不负责导航详情页。
接口可以保持很小:
ts
export interface FridgeRecommendationService {
recommend(selectedIngredients: string[]): RecipeMatchResult[]
getQuickRecipes(limit: number): RecipeMatchResult[]
}
页面继续负责 router.pushUrl(),服务层不依赖 ArkUI 路由。
十八、隐私与离线边界
冰箱食材是用户生活信息,但当前功能完全可以本地完成:
- 不上传食材列表。
- 不需要账号和云端画像。
- 不请求相机、相册或定位权限。
- 不把输入词发送到第三方推荐接口。
- 若未来保存历史,需要在隐私政策中说明本地存储、清除方式和保存范围。
当前项目已经声明 INTERNET 权限,但这个页面本身不需要网络。文章和应用说明不应把本地规则描述成云端 AI;若未来接入联网模型,要重新评估权限、隐私、失败降级和费用。
十九、多设备布局
手机上适合"输入区---已选食材---结果列表"的纵向结构。平板与 2in1 可以采用左右分栏:左侧固定食材选择,右侧滚动候选,避免用户每次改一项都上下往返。
| 设备宽度 | 建议布局 | 关键约束 |
|---|---|---|
| 小于 600vp | 单列 | 输入框与加入按钮不挤压,标签可换行 |
| 600--960vp | 左 2/5、右 3/5 | 两侧独立滚动,结果卡片限宽 |
| 大于 960vp | 左侧固定 360--420vp | 支持鼠标、键盘焦点和窗口缩放 |
已选食材文案当前最多两行并省略,选择较多时用户无法确认全部内容。可以改成可折叠标签区或横向滚动 Chip 列表,而不是把所有名称拼成一个 Text。
二十、测试矩阵
| 编号 | 输入 | 预期 |
|---|---|---|
| F01 | 默认鸡蛋、土豆 | 返回包含对应主辅料的候选,并显示命中原因 |
| F02 | 西红柿 | 归一为番茄,不产生两个重复标签 |
| F03 | 鸡蛋 |
去除空格并与已有鸡蛋去重 |
| F04 | 只选大葱 | 调味料不能让大量菜谱获得高分 |
| F05 | 选鸡胸肉 | 宫保鸡丁主料命中,排名高于只命中葱姜的菜 |
| F06 | 选土豆、茄子、青椒 | 地三鲜展示高覆盖率和缺少大蒜 |
| F07 | 输入榴莲 | 显示空态与重选入口,不留白 |
| F08 | 清空选择 | 展示真正的快手菜,不使用字符串时长排序 |
| F09 | 比较 120 与 20 分钟 | 20 分钟候选排序更靠前 |
| F10 | recipeId 21--60 | 每道菜都有结构化食材 profile |
| F11 | 重复点击标签 | 状态稳定切换,不出现重复食材 |
| F12 | 选择 15 项以上 | 已选区域可查看全部项,布局不溢出 |
| F13 | 相同分数 | 按覆盖率、时长、recipeId 稳定排序 |
| F14 | profile 缺主料 | 完整性检查报告明确错误 |
| F15 | 打开结果 | recipeId 正确传给详情页 |
二十一、可复核的验证步骤
21.1 数据覆盖
- 读取
RECIPE_LIST的全部 60 个 ID。 - 读取全部
RecipeIngredientProfile。 - 比较 ID 集合,确认无缺失、无重复、无孤立 profile。
- 确认每道菜至少有一个 primary 食材。
- 记录基线:60 道菜、60 个 profile、覆盖率 100%。
21.2 排序验证
构造三条相同评分、不同覆盖率和时长的数据,运行 compareMatches()。验证缺主料更少者优先,覆盖率更高者优先,最后才比较数值时长;重复执行顺序必须一致。
21.3 解释一致性
对每条结果检查:matched 与用户 pantry 有交集,missingRequired 不在 pantry 中,coverage 等于命中 required 数除以 required 总数,reason 只引用实际字段。
21.4 UI 验证
在手机竖屏、横屏、平板和 2in1 窗口下选择 0、2、10 和 21 项食材。检查输入框、Chip、已选区域、空态、结果卡和底部安全区。深色模式下检查标签选中态、次要文字和缺失食材提示的对比度。
二十二、常见问题与定位
22.1 明明有大虾,却不推荐油焖大虾
原因: 食材索引只覆盖 recipeId 1--20,油焖大虾的 recipeId 是 26。
处理: 为 21--60 补齐 profile,并把 60/60 覆盖作为构建检查。
22.2 120 分钟的菜排在 20 分钟前
原因: 使用 localeCompare() 比较展示字符串。
处理: 在模型中保存数值分钟,或先用 parseDurationMinutes() 转换。
22.3 只选葱姜却出现很多"高匹配"
原因: 主料、辅料与调味料每命中一项都加 1 分。
处理: 按 role 加权,并对缺失主料设置惩罚或过滤条件。
22.4 番茄和西红柿被当成两个食材
原因: 当前去重只比较原始字符串。
处理: 先归一再去重,所有 Chip 和自定义输入共用同一函数。
22.5 没有结果时页面空白
原因: 结果数组为空但没有 empty 状态。
处理: 显示原因、调整建议和清空入口;数据异常应进入 error 而不是 empty。
22.6 相同条件下顺序偶尔变化
原因: 比较器没有最终稳定键,或 UI 构建中重复计算。
处理: 末级比较 recipeId,并在状态变化时一次性生成结果。
二十三、落地顺序
- 先把 duration 改为数值排序,消除明确错误。
- 建立
RecipeIngredientProfile,补齐 60 道菜的食材数据。 - 加入归一与别名字典,统一常用标签和自定义输入。
- 输出
RecipeMatchResult,让 UI 展示命中与缺失。 - 将算法移到服务层并补齐 F01--F15 测试。
- 最后调整平板分栏和已选食材交互。
每一步都能独立验证,不需要一次性重写整个页面。
二十四、源码事实与建议实现的边界
本文既复盘当前页面,也给出推荐模型的演进方案。为避免把方案误写成已经上线的能力,下面按源码快照逐项区分。当前事实可从 FridgeRecipePage.ets、MockData.ets 与 RecipeModel.ets 直接复核。
| 检查项 | 当前源码事实 | 本文建议,不代表已经落地 |
|---|---|---|
| 菜谱总量 | RECIPE_LIST 有 60 条菜谱 |
建立 60 个结构化 RecipeIngredientProfile |
| 常用食材 | 页面定义 21 个 Chip,默认选中鸡蛋和土豆 | 为别名、分类和展示顺序建立可测试配置 |
| 食材索引 | recipeIngredientIndex 只有 20 条,仅覆盖 recipeId 1--20 |
补齐 21--60,并把 60/60 覆盖作为硬检查 |
| 自定义输入 | 只执行 trim(),再按原始字符串精确去重 |
先做别名归一和空白压缩,再用集合去重 |
| 匹配规则 | 在菜名、分类、描述、标签和索引文本中执行 indexOf(),每命中一个选择项加 1 |
使用主料、辅料、调味料角色和透明权重 |
| 排序规则 | 先按命中数降序,再用 duration.localeCompare() 比较展示字符串 |
使用数值分钟、覆盖率、缺失主料和 recipeId 稳定排序 |
| 结果数量 | 有选择时过滤零命中并取前 12;空选择时取排序后的前 8 | 空选择展示明确的快手菜、历史菜或编辑精选 |
| 推荐解释 | 卡片显示"N项匹配"和索引食材字符串,不计算缺失清单 | 输出 RecipeMatchResult,展示命中、缺失、覆盖率和理由 |
| 计算位置 | build() 中调用 getMatchedRecipes(),卡片渲染时再次计算命中数 |
选择变化时由服务层一次性生成结果,UI 只渲染 |
| 网络与隐私 | FridgeRecipePage 没有网络调用,也不请求相机、相册或定位;工程整体声明了 INTERNET 权限 |
若未来接入联网模型,重新核对权限、隐私、失败降级和费用说明 |
| 构建与设备验证 | 本文读取并统计了本地源码,没有声称执行安装包构建或真机测试 | 在 phone、tablet、2in1 上验证长 Chip、空态、分栏和键鼠焦点 |
因此,"可解释评分""60/60 食材 profile""别名归一"和"服务层缓存"都属于建议实现。它们只有在代码落地、硬检查通过并完成设备验证后,才能写成当前应用能力。
二十五、总结
当前冰箱食材推荐页已经具备完整的交互外壳:21 个常用食材、可输入自定义项、选中态、清空操作和最多 12 条候选卡片。限制它实用性的,是 60 道菜只有 20 条食材索引、文本包含匹配无法区分主辅料、时长采用字符串排序,以及结果只显示命中数量而不说明缺什么。
一个可靠的本地推荐器不需要神秘模型,但需要四项工程基础:
- 60 道菜与 60 份结构化食材 profile 完整闭合。
- 输入经过别名归一和去重。
- 评分区分主料、辅料、调味料并使用数值时长。
- 每条候选都能说明已具备、仍缺少和排序理由。
当推荐结果能被用户理解、被测试复算、被开发者定位时,"冰箱有什么"才真正从关键词筛选升级为一个可信的日常决策工具。
参考资料
- HarmonyOS ArkUI 概述
- HarmonyOS ArkTS 概述
- HarmonyOS 数据管理概述
- 工程源码:
FridgeRecipePage.ets、MockData.ets、RecipeModel.ets
AI 辅助声明:本文由 AI 辅助完成源码统计、结构校对与配图设计,所有当前能力判断均以文中列出的本地 ArkTS 源码为依据,并由作者复核后发布;建议算法、测试矩阵和多设备方案不代表工程已经构建或上线。