资讯动态

【健康菜谱助手|08】HarmonyOS ArkTS 食材百科实战:实现分类、检索和详情跳转

发布时间:2026/10/27 9:58:04 来源:尧图企业网站定制
食材百科不只是把几十条文案放进卡片。用户真正的路径通常是先判断“大虾属于哪一类”再搜索名称或营养关键词最后进入详情核对选购和储存方法。如果分类、检索与路由各自维护一套数据功能一多就会出现索引错位、重复 key、详情参数失效和空态含义不清等问题。本文基于“健康菜谱助手”1.0.5 的真实 HarmonyOS ArkTS 工程展开。IngredientWikiPage.ets当前内置蔬菜、肉类、海鲜、调料、谷物 5 个分类共 22 条食材页面支持横向分类切换和卡片展开展开后显示选购技巧、储存方法与营养价值。真实源码尚未提供搜索输入框和独立详情页因此本文会先还原现状再给出可落地的“分类 检索 详情路由”升级方案不把设计稿当成已完成代码。食材百科的唯一真源应该是带稳定 ID 的食材数据分类、搜索、展开状态和详情路由都只传递 ID不复制整份对象也不依赖随过滤变化的数组下标。HEALTHY-RECIPE-ONE7-INGREDIENT-WIKI-20260730本文将完成以下内容核对 5 个分类、22 条食材和当前折叠卡片交互。将页面内嵌数据提取为可复用、可测试的数据模块。设计名称、分类、时令、营养、选购和储存多字段检索。解决过滤后expandedItem索引错位与重复 key 风险。使用稳定ingredientId完成详情跳转与参数校验。补齐加载、空结果、无效路由、长文本和大字体状态。给出 ArkTS 代码、测试矩阵和可复核验收步骤。一、真实页面的功能边界IngredientWikiPage当前有三个状态StorageLink(appThemeColor) appThemeColor: string #4CAF50 StorageLink(appFontSize) appFontSize: number 14 State selectedCategory: number 0 State expandedItem: number -1主题色和字号来自AppStorage分类与展开项属于页面临时状态。分类条使用水平Scroll列表使用垂直Scroll。点击分类时更新selectedCategory并把expandedItem设为-1点击食材卡片则在当前索引和折叠状态之间切换。这套实现适合静态原型但当搜索结果改变列表顺序时expandedItem: number就不再可靠。二、5 个分类与 22 条真实数据真实源码中的数量如下分类数量示例蔬菜6大白菜、西兰花、番茄、胡萝卜、大蒜、辣椒肉类4猪五花肉、鸡胸肉、牛里脊、羊腿肉海鲜4鲈鱼、大虾、螃蟹、海参调料5食盐、食用油、生抽、白糖、料酒谷物3大米、玉米、红薯每条数据包含emoji、name、category、season、nutrition、tips、storage七个字段。数据虽然不多但已经能覆盖分类过滤、多字段检索和详情展示。三、当前展开卡片的工作方式列表 key 使用分类索引与名称ForEach( this.categories[this.selectedCategory].items, (item: Ingredient, index: number) { // card }, (item: Ingredient, index: number) ${this.selectedCategory}_${item.name} )展开状态则保存数组下标this.expandedItem this.expandedItem index ? -1 : index在没有搜索和排序时下标与当前列表一致。加入搜索后如果原列表第 4 项变成结果列表第 0 项旧下标会展开错误卡片。根因不是 ArkUI而是把“位置”当成了“身份”。四、现有源码没有搜索和详情路由工程检索结果显示main_pages.json注册了pages/IngredientWikiPage。没有IngredientDetailPage。IngredientWikiPage只调用router.back()。没有TextInput搜索状态和检索函数。因此本文标题中的“实现分类、检索和详情跳转”属于增量设计目标。文章后续代码会明确需要新增的模型、服务、页面与路由配置。五、先为食材补稳定 ID名称可读但不适合长期充当主键。名称可能改文案也可能在不同地区或品种下重复。模型应增加稳定 IDexport interface IngredientModel { id: string emoji: string name: string categoryId: string categoryName: string season: string nutrition: string tips: string storage: string aliases: string[] }示例 ID 可以是vegetable-tomato一旦发布就不要随中文名称变化。aliases用于“西红柿/番茄”等同义词但别把别名偷偷拼进 UI。六、分类也需要稳定身份export interface IngredientCategoryModel { id: string name: string icon: string } export const INGREDIENT_CATEGORIES: IngredientCategoryModel[] [ { id: all, name: 全部, icon: }, { id: vegetable, name: 蔬菜, icon: }, { id: meat, name: 肉类, icon: }, { id: seafood, name: 海鲜, icon: }, { id: seasoning, name: 调料, icon: }, { id: grain, name: 谷物, icon: } ]增加“全部”后搜索可以跨分类进行。页面保存selectedCategoryId而不是保存容易越界的数组索引。七、把静态数据移出页面当前 22 条食材直接写在组件私有字段中详情页无法复用。建议拆成models/IngredientModel.ets data/IngredientData.ets services/IngredientSearchService.ets pages/IngredientWikiPage.ets pages/IngredientDetailPage.ets数据模块导出只读数组和按 ID 查询函数页面不再拥有数据副本。这样列表、搜索、详情和测试共享同一事实来源。八、建立 ID 索引export class IngredientRepository { private readonly byId: Mapstring, IngredientModel new Map() constructor(private readonly items: IngredientModel[]) { for (const item of items) { this.byId.set(item.id, item) } } getAll(): IngredientModel[] { return [...this.items] } getById(id: string): IngredientModel | undefined { return this.byId.get(id) } }构造时还应检测重复 ID。出现重复时不能后值静默覆盖前值否则详情路由会指向不确定对象。九、搜索输入先做规范化中文检索至少要处理首尾空格和连续空白export function normalizeKeyword(value: string): string { return value.trim().replace(/\s/g, ).toLowerCase() }空关键词代表“不过滤”不是错误。若后续支持拼音、同义词或分词应作为独立索引能力增加不能让页面里堆叠越来越长的includes()。十、多字段可检索文本用户可能搜“维C”“冷藏”“秋季”或“高蛋白”不仅会搜名称。可以构建可检索文本export function searchableText(item: IngredientModel): string { return [ item.name, item.categoryName, item.season, item.nutrition, item.tips, item.storage, item.aliases.join( ) ].join( ).toLowerCase() }检索范围应在页面上通过占位文案说明例如“搜索食材、时令、营养或储存方法”。十一、分类与关键词要组合过滤export interface IngredientFilter { categoryId: string keyword: string } export function filterIngredients( items: IngredientModel[], filter: IngredientFilter ): IngredientModel[] { const keyword normalizeKeyword(filter.keyword) return items.filter((item: IngredientModel) { const categoryMatched filter.categoryId all || item.categoryId filter.categoryId const keywordMatched keyword.length 0 || searchableText(item).includes(keyword) return categoryMatched keywordMatched }) }这个函数不修改输入可以独立验证“海鲜 高蛋白”“全部 冷藏”等组合条件。十二、搜索结果需要稳定排序纯includes能筛选却无法让名称精确匹配排在前面。可以计算简单分值export function matchScore(item: IngredientModel, keyword: string): number { if (keyword.length 0) return 0 if (item.name.toLowerCase() keyword) return 100 if (item.name.toLowerCase().startsWith(keyword)) return 80 if (item.aliases.some((alias: string) alias.toLowerCase() keyword)) return 70 if (item.nutrition.toLowerCase().includes(keyword)) return 40 if (item.tips.toLowerCase().includes(keyword)) return 30 if (item.storage.toLowerCase().includes(keyword)) return 20 return 10 }排序时复制数组并用原始数据顺序或 ID 作为次级规则保证同分结果不会刷新跳动。十三、保存 expandedItemId 而不是 indexState private expandedItemId: string private toggleExpanded(id: string): void { this.expandedItemId this.expandedItemId id ? : id }渲染判断改为expandedItemId item.id。筛选后如果该 ID 不在结果中可以在刷新结果时清空如果仍在结果中展开状态保持稳定。十四、分类切换是否清空关键词这是产品规则不是技术必然。推荐保留关键词用户搜索“高蛋白”后切换肉类和海鲜可以快速比较不同分类。页面应显示“海鲜 · 高蛋白 · 2 条结果”让过滤条件可见。切换分类时只需要private selectCategory(id: string): void { this.selectedCategoryId id this.expandedItemId this.refreshResults() }若产品决定切换分类清空关键词也应显式调用clearKeyword()不要依赖组件重建的偶然行为。十五、输入更新要避免业务散落在 UI 树TextInput({ placeholder: 搜索食材、时令、营养或储存方法, text: this.keyword }) .onChange((value: string) { this.keyword value this.refreshResults() })22 条本地数据可以即时过滤。数据增长后可增加 150–250 ms 防抖但防抖只减少计算频率不能改变最终结果。页面退出时要清理定时器。十六、用 ViewData 表达结果状态export interface IngredientListViewData { items: IngredientModel[] resultText: string emptyTitle: string emptyDescription: string showClearKeyword: boolean }页面一次计算ViewDataArkUI 只负责渲染。不要在ForEach、空态条件和结果计数中分别调用过滤函数否则一次重建可能做三遍相同工作。十七、空结果要解释当前条件空数据至少有两种数据源为空百科内容未加载或版本异常。筛选无结果数据正常但当前分类和关键词没有匹配。筛选无结果可以显示“在海鲜中没有找到‘番茄’”并提供“清除搜索”和“查看全部”两个明确动作。不能只放一个“暂无数据”。十八、卡片展开与详情页的职责不同卡片展开适合快速扫读选购和储存摘要独立详情页适合展示完整营养说明、时令、注意事项、来源和关联菜谱。两种交互可以并存点击卡片正文展开摘要。点击明确的详情按钮进入详情页。不要让整张卡片同时承担“展开”和“跳转”否则用户无法预测点击结果。十九、详情路由只传稳定 IDprivate openDetail(item: IngredientModel): void { router.pushUrl({ url: pages/IngredientDetailPage, params: { ingredientId: item.id } }) }不要把整份IngredientModel作为路由参数。对象可能变大、版本变化也会让详情页拿到与仓库不同步的副本。ID 是跨页面契约详情页再向仓库取当前数据。二十、新详情页必须注册路由新增页面后在main_pages.json的src中加入pages/IngredientDetailPageURL 大小写、目录名和文件名必须完全一致。只创建.ets文件却不注册会在点击时才暴露路由错误。二十一、严格解析路由参数interface IngredientRouteParams { ingredientId?: string } function readIngredientId(): string { const params router.getParams() as IngredientRouteParams return typeof params.ingredientId string ? params.ingredientId.trim() : }即使 TypeScript 断言通过运行时参数仍可能缺失。详情页不能直接使用未检查字段。二十二、详情页处理三种状态type DetailState ready | missing-param | not-found interface IngredientDetailViewData { state: DetailState item?: IngredientModel }ready正常展示。missing-param没有ingredientId提供返回。not-foundID 不存在或数据版本变化说明内容不可用并允许回到百科。无效参数不能导致空白页、数组越界或非空断言崩溃。二十三、详情页读取数据的时机aboutToAppear(): void { const id readIngredientId() if (id.length 0) { this.viewData { state: missing-param } return } const item this.repository.getById(id) this.viewData item undefined ? { state: not-found } : { state: ready, item } }初始化逻辑放在生命周期方法或 ViewModel 中不要在build()期间反复读取参数并修改状态。二十四、数据契约与页面边界推荐边界如下模块责任IngredientData22 条真实食材数据IngredientRepositoryID 索引、全量读取、重复校验IngredientSearchService规范化、过滤、评分与排序IngredientWikiViewModel分类、关键词、结果和空态IngredientWikiPage搜索框、分类条、卡片和详情按钮IngredientDetailPage参数校验和完整详情依赖方向保持为页面调用 ViewModelViewModel 调用仓库和检索服务数据层不依赖 ArkUI。二十五、ForEach key 使用 item.idForEach(this.viewData.items, (item: IngredientModel) { this.IngredientCard(item) }, (item: IngredientModel) item.id)搜索、分类和排序会改变位置但不会改变 ID。稳定 key 可以避免错误复用展开状态和不必要的节点重建。二十六、分类选择不能越界原实现通过categories[selectedCategory]取数据。若远端配置、版本迁移或开发修改导致分类数组变化索引可能越界。改用categoryId后找不到分类时回退到allfunction safeCategoryId(id: string, categories: IngredientCategoryModel[]): string { return categories.some((item: IngredientCategoryModel) item.id id) ? id : all }二十七、主题色与文字对比度当前选中分类使用appThemeColor背景和白字分类标签文字则通过getThemeTextColor()取得深色变体。主题来自用户设置不能假设任意颜色与白字都有足够对比度。更稳妥的主题服务应同时产出背景色和前景色 token。展开卡片的alpha(theme, 08)很浅边框和正文仍要依赖高对比度语义色不要只靠淡色背景表示展开。二十八、字号变化与动态布局页面使用appFontSize - 2和固定fontSize(10)。如果用户把基础字号设得很小减法会让关键信息难以阅读设得很大时名称、分类和时令三个标签可能挤在一行。应为字号设置上下限并允许标签区换行或拆成两行。食材名称和详情按钮保持可达长营养描述用合理maxLines与展开状态控制。二十九、搜索框需要完整交互状态一个可用搜索框应包含输入关键词。一键清空图标。键盘搜索操作。无结果说明。当前结果数量。输入法弹出后的可滚动与安全区域。清空按钮使用熟悉的关闭图标并提供可识别描述不需要用大段说明文字占页面。三十、本地数据也要有加载与错误边界当前数据编译进应用不需要网络权限这是一个清晰优势。但未来若数据迁移到 JSON 资源或数据库页面仍应区分loading、ready、empty和error。离线不等于永远不会失败资源解析和版本迁移同样可能出错。三十一、健康信息的内容边界22 条食材文本包含营养、摄入和健康效果描述例如维生素含量、蛋白质数值和每日用量。技术页面应显示来源、更新时间和适用范围避免把静态科普文本包装成诊断或个体化建议。搜索服务只匹配文本不应根据用户关键词推断健康状况也不应上传搜索记录除非产品明确需要并完成隐私说明。三十二、测试矩阵编号场景预期I01默认进入选中全部或约定默认分类列表稳定I02选择海鲜只显示 4 条真实海鲜数据I03搜索“番茄”名称精确匹配置顶I04搜索“冷藏”匹配储存字段中的食材I05搜索“秋季”匹配时令字段I06海鲜中搜索“番茄”显示带条件说明的空态I07输入前后空格规范化后结果不变I08清空关键词恢复当前分类全部结果I09展开后改变搜索不展开错误卡片I10同名不同分类ID 和 key 仍唯一I11点击详情只传ingredientIdI12缺少路由参数显示可返回错误态I13ID 不存在显示内容不存在不崩溃I14320vp 小屏名称、标签和按钮不重叠I15大字体卡片可增长详情文本可达I16深浅色切换选中、展开和正文均可读I17连续快速切分类结果计数和空态同步I18返回百科分类与关键词按产品规则恢复三十三、可复核检索样例使用真实数据执行以下检查const seafood filterIngredients(INGREDIENTS, { categoryId: seafood, keyword: }) const coldStorage filterIngredients(INGREDIENTS, { categoryId: all, keyword: 冷藏 })seafood.length应为 4coldStorage应命中储存字段中包含“冷藏”的多条食材。再搜索“大虾”精确名称分值应高于只在描述中包含关键词的结果。三十四、验收步骤34.1 数据与分类启动页面核对 5 个业务分类共 22 条数据。检查每个 ID 唯一分类 ID 均存在。连续切换全部分类确认结果数量与数据源一致。34.2 检索分别搜索名称、时令、营养、选购和储存关键词。组合分类和关键词核对交集结果。输入空格、无结果词和超长词确认状态可解释。结果改变时检查展开项不会错位。34.3 路由点击详情按钮记录传入的稳定 ID。正常打开详情并返回确认列表状态符合产品约定。手动构造缺失 ID 和不存在 ID验证错误态。检查main_pages.json已注册且名称大小写一致。34.4 适配检查手机小屏、横屏、平板和 2in1 窗口。检查大字体、长营养文本和键盘弹出状态。检查深浅色模式、主题色和系统返回操作。三十五、常见故障定位35.1 搜索后展开了另一项检查展开状态是否仍保存数组下标。改为保存ingredientId并让ForEachkey 同样使用 ID。35.2 搜索结果数量正确但顺序跳动检查评分相同结果是否缺少稳定次级排序以及过滤函数是否原地修改源数组。35.3 点击详情没有反应确认目标页面已写入main_pages.jsonURL 与文件路径完全一致并查看router.pushUrl()的错误回调或运行日志。35.4 详情页白屏检查router.getParams()是否缺失、类型错误或 ID 不存在。不要使用未校验的非空断言。35.5 主题色下文字看不清不要默认白字适配任意自定义色。由主题层提供经过对比度检查的前景色和背景色组合。三十六、落地顺序提取 22 条食材为独立数据源并增加稳定 ID。将分类状态从索引改为categoryId展开状态改为ingredientId。实现规范化、组合过滤、评分和稳定排序纯函数。增加搜索框、结果计数和条件化空态。新建详情页、注册路由、只传 ID 并校验参数。执行 I01–I18 和多设备适配验收。这个顺序先建立数据契约再加 UI 控件能避免搜索和路由建立在不稳定索引上。三十七、源码事实与建议实现的边界为了避免把教程中的目标架构误读为现有功能下面把可直接从IngredientWikiPage.ets复核的事实与后续建议分开列出。维度当前源码事实建议实现与验证数据规模5 个分类共 22 条食材抽成独立数据模块并增加 schemaVersion数据字段每条数据只有emoji、name、category、season、nutrition、tips、storage7 个字段增加稳定ingredientId和可追溯的内容元数据数据位置数据硬编码在页面类内部由 Repository 统一读取页面不直接持有原始数据集当前交互只有分类标签切换与卡片展开/收起增加搜索输入、结果计数、空态与详情入口搜索能力没有搜索输入框、关键词状态和过滤函数增加规范化、多字段匹配、分类交集与稳定排序详情能力没有独立详情路由点击行只切换展开状态注册详情页只传递并严格校验ingredientId展开状态expandedItem保存数组下标改为保存稳定 ID避免过滤或排序后的错位列表 key分类使用名称食材使用分类索引与名称组合分类和食材都使用稳定 ID 作为 key页面状态没有显式 loading、empty、error 状态为资源解析、无结果、无效 ID 分别建模健康内容营养、选购和储存文字是页面中的静态内容标注来源、更新时间与适用边界不作诊断或个体化建议本文实际执行并可复核的是源码阅读、数据统计、文章检查和配图检查。文中的检索服务、详情路由、数据迁移与测试矩阵属于建议实现不代表当前工程已经完成构建、安装、真机运行或上线。三十八、总结真实IngredientWikiPage已有 5 个分类、22 条本地食材、主题适配和展开详情是一个可运行的百科列表原型。下一步的关键不是简单插入一个TextInput而是先把“食材身份”从数组位置升级为稳定 ID再让分类、检索、展开和详情路由共享同一数据源。需要守住六条原则食材和分类使用稳定 ID位置只用于展示。检索是可测试的纯函数同时处理分类和关键词。展开状态保存 ID过滤后不发生索引错位。详情路由只传 ID目标页重新读取并校验数据。空结果、无效参数和数据错误必须是不同状态。健康文本标注来源与边界搜索记录默认留在本地。当用户可以从“海鲜”筛到 4 条数据搜索“冷藏”找到真正匹配的储存说明再通过稳定 ID 打开详情且无效链接也不会白屏时食材百科才形成完整、可信、可维护的浏览闭环。参考资料HarmonyOS ArkUI 概述HarmonyOS ArkTS 概述HarmonyOS Ability Kit 概述HarmonyOS 深浅色适配工程源码IngredientWikiPage.ets、ThemeConstants.ets、main_pages.json

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑