HarmonyOS菜谱——搜索功能的实现方案与TextInput交互

HarmonyOS菜谱——搜索功能的实现方案与TextInput交互
适用读者想在 ArkTS 里实现搜索过滤功能的开发者。本文从菜谱 App 的搜索框 UI 出发分析有 UI 无逻辑的现状给出完整的搜索实现方案拆解 TextInput 的交互细节。完整效果一、当前搜索框的代码结构// Index.etsStatesearchText:stringRow(){SymbolGlyph($r(sys.symbol.magnifyingglass)).fontSize(16).fontColor([T3]).margin({left:12})TextInput({placeholder:搜索菜谱...,text:this.searchText}).layoutWeight(1).height(40).backgroundColor(transparent).fontSize(14).onChange((v:string){this.searchTextv})}.width(100%).height(40).backgroundColor(#F0EEF4).borderRadius(20).padding({left:16,right:16}).margin({left:16,right:16,bottom:16})搜索框的 UI 已经完成——放大镜图标 输入框圆角灰色背景。但onChange只是把输入内容同步到State searchText没有实际的过滤逻辑。用户输入红烧后下方的菜谱列表仍然显示全部 8 道菜不会筛选出红烧肉。二、TextInput 的三个关键属性placeholder占位提示文字TextInput({placeholder:搜索菜谱...,text:this.searchText})placeholder 在输入框为空时显示用户开始输入后消失。它告诉用户这里可以输入什么——搜索菜谱…比请输入更具体用户一看就知道搜索的目标是菜谱。踩坑记录最初 placeholder 写了请输入关键词用户反馈不知道搜什么。改成搜索菜谱…后用户明确知道搜索范围是菜谱名称和描述。text双向绑定的输入内容text:this.searchTexttext 属性绑定 State 变量——实现双向绑定。用户输入时onChange把新值赋给this.searchTextthis.searchText变化时TextInput 的显示内容自动更新。为什么需要双向绑定搜索场景下输入框需要实时反映当前搜索词——用户可能中途修改搜索词修改后列表需要重新过滤。双向绑定保证输入框内容和过滤逻辑用的是同一个值。onChange实时监听输入变化.onChange((v:string){this.searchTextv})onChange 在每次输入变化时触发——用户每打一个字都会触发。参数v是当前输入框的完整内容不是增量。onChange vs onSubmit 的区别事件触发时机适用场景onChange每次输入变化实时搜索过滤onSubmit点击键盘确认键提交搜索跳转搜索结果页菜谱 App 的搜索框需要实时过滤——用户打一个字就筛选一次不需要点确认。所以用 onChange 而不是 onSubmit。为什么不用 backgroundColor 而是外层 Row 提供背景TextInput({...}).backgroundColor(transparent)TextInput 背景透明外层 Row 提供灰色背景。如果给 TextInput 加背景色会和 Row 的背景叠加——出现两层颜色。透明背景让 TextInput 融入外层 Row视觉上是一个整体。三、搜索过滤的完整实现方案方案一在 Index 里加过滤逻辑StatesearchText:string// 新增过滤后的菜谱数组getFilteredRecipes():Recipe[]{if(this.searchText.trim().length0)returnRECIPESconstkeywordthis.searchText.trim().toLowerCase()constresult:Recipe[][]for(leti:number0;iRECIPES.length;i){if(RECIPES[i].name.toLowerCase().includes(keyword)||RECIPES[i].desc.toLowerCase().includes(keyword)){result.push(RECIPES[i])}}returnresult}build(){// ... 搜索框// 改动ForEach 从 RECIPES 改为 getFilteredRecipes()ForEach(this.getFilteredRecipes(),(r:Recipe){Row(){/* 菜谱卡片 */}})}方案分析优点缺点改动最小——只改 ForEach 的数据源每次 build 都重新计算过滤结果不需要新增 State 变量搜索关键词变化时整个 build 方法重新执行toLowerCase()的作用——忽略大小写搜索。用户输入红和红全角都能匹配红烧肉。includes检查关键词是否出现在菜名或描述里。trim().length 0的判断——搜索框为空时显示全部菜谱。用户清空搜索框后列表恢复为全部 8 道菜。方案二用 State 存储过滤结果StatesearchText:stringStatefilteredRecipes:Recipe[]RECIPES// 新增// 搜索框 onChange 改动.onChange((v:string){this.searchTextvthis.updateFiltered()})privateupdateFiltered():void{if(this.searchText.trim().length0){this.filteredRecipesRECIPESreturn}constkeywordthis.searchText.trim().toLowerCase()constresult:Recipe[][]for(leti:number0;iRECIPES.length;i){if(RECIPES[i].name.toLowerCase().includes(keyword)||RECIPES[i].desc.toLowerCase().includes(keyword)){result.push(RECIPES[i])}}this.filteredRecipesresult}build(){// ForEach 改为 this.filteredRecipesForEach(this.filteredRecipes,(r:Recipe){...})}方案分析优点缺点过滤结果缓存在 State 里需要新增一个 State 变量build 方法不需要每次重新计算需要手动调用 updateFiltered()方案二比方案一更优——过滤结果只在搜索词变化时计算不是每次 build 都计算。当其他 State 变化比如收藏按钮切换导致 build 重新执行时方案一会重新过滤即使搜索词没变方案二不会。方案对比维度方案一方案二代码改动量小改 ForEach 数据源中加 State 方法性能每次 build 重新计算只在搜索词变化时计算适用场景数据量小 50 条数据量大 50 条推荐度原型阶段可用生产环境推荐菜谱 App 只有 8 道菜方案一完全够用。但如果菜谱增加到几百道方案二的性能优势就体现出来了。四、搜索匹配的扩展维度当前方案只匹配菜名和描述。可以扩展的匹配维度匹配维度实现方式匹配例子菜名r.name.includes(keyword)“红烧” 匹配 “红烧肉”描述r.desc.includes(keyword)“酸辣” 匹配 “酸辣脆爽”分类r.category.includes(keyword)“烘焙” 匹配 “戚风蛋糕”category烘焙食材遍历r.ingredients“鸡蛋” 匹配 “番茄鸡蛋面” 和 “戚风蛋糕”难度r.difficulty.includes(keyword)“简单” 匹配所有简单菜谱按食材搜索的实现privateupdateFiltered():void{if(this.searchText.trim().length0){this.filteredRecipesRECIPESreturn}constkeywordthis.searchText.trim().toLowerCase()constresult:Recipe[][]for(leti:number0;iRECIPES.length;i){constr:RecipeRECIPES[i]// 匹配菜名if(r.name.toLowerCase().includes(keyword)){result.push(r);continue}// 匹配描述if(r.desc.toLowerCase().includes(keyword)){result.push(r);continue}// 匹配食材for(letj:number0;jr.ingredients.length;j){if(r.ingredients[j].name.toLowerCase().includes(keyword)){result.push(r);break}}}this.filteredRecipesresult}continue的作用——匹配成功后跳过后续检查。菜名匹配了就不需要再检查描述和食材减少不必要的遍历。踩坑记录最初没有continue一道菜可能被重复加入结果数组菜名匹配一次、描述又匹配一次。加了continue后每道菜最多只被 push 一次。搜索鸡蛋的结果示例菜谱匹配原因匹配字段番茄鸡蛋面食材含鸡蛋ingredients[2].name戚风蛋糕食材含鸡蛋ingredients[1].name用户输入鸡蛋后列表只显示这两道菜——因为只有它们的食材里有鸡蛋。这种按食材搜索的体验比只搜菜名更实用——用户可能记得想用鸡蛋做菜但不记得具体菜名。五、搜索无结果的空状态当搜索词没有匹配结果时当前显示空白区域。需要加空状态提示if(this.filteredRecipes.length0){Column(){Text().fontSize(48).opacity(0.2).margin({bottom:12})Text(没有找到相关菜谱).fontSize(14).fontColor(T2)Text(试试其他关键词).fontSize(12).fontColor(T3).margin({top:4})}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center)}空状态文案试试其他关键词比暂无数据更有帮助。前者暗示换个词可能找到后者暗示这里什么都没有。踩坑记录最初空状态加在了this.recipes.length 0的判断里分类页的空状态逻辑但 Index 的搜索空状态需要单独判断this.filteredRecipes.length 0。两个空状态的触发条件不同——分类页是该分类没有菜谱搜索是没有匹配的菜谱。六、搜索与分类的联动当前搜索和分类是独立的——搜索框输入红烧会过滤全部 8 道菜不会限制在当前分类内。如果需要在当前分类内搜索需要叠加两个过滤条件// 假设 Index 也有分类筛选当前没有但如果加了privateupdateFiltered():void{letresult:Recipe[]RECIPES// 先按分类过滤if(this.selectedCat.length0){resultgetRecipesByCat(this.selectedCat)}// 再按搜索词过滤if(this.searchText.trim().length0){constkeywordthis.searchText.trim().toLowerCase()constfiltered:Recipe[][]for(leti:number0;iresult.length;i){if(result[i].name.toLowerCase().includes(keyword)||result[i].desc.toLowerCase().includes(keyword)){filtered.push(result[i])}}resultfiltered}this.filteredRecipesresult}先分类后搜索的顺序——缩小范围再精确匹配。先按分类过滤出子集比如家常菜3 道再在子集里搜索关键词。如果反过来先搜索再分类搜索范围是全部 8 道菜效率更低。当前 Index 没有分类筛选功能分类是跳转到 CategoryPage所以不需要叠加过滤。但如果以后 Index 加了分类 Tab这个叠加逻辑就需要了。七、搜索性能的优化方向当前的 O(n) 遍历每次搜索词变化都遍历全部 8 道菜——对每道菜检查 name 和 desc 是否包含关键词。在 8 道菜的规模下性能完全不是问题。数据量增大后的优化方案优化方案原理适用场景防抖debounce用户停止输入 300ms 后才执行搜索输入速度快、数据量大索引预处理菜谱数据建关键词→菜谱的映射数据量 100 条分页搜索结果分页加载结果 20 条防抖的实现思路用户每打一个字不立即搜索而是等 300ms 没有新输入后才执行。避免红烧肉三个字触发三次搜索“红→红烧→红烧肉”。当前不需要优化——8 道菜的搜索耗时可以忽略。过早优化是万恶之源。只有当用户反馈搜索卡顿时才需要考虑优化。八、TextInput 的其他交互事件onSubmit键盘确认键TextInput({...}).onSubmit((){// 用户点击键盘的搜索/确认键时触发// 适合跳转到搜索结果页})onSubmit 适合提交式搜索——用户输入完后点确认跳转到搜索结果页。菜谱 App 用的是实时过滤式搜索不需要 onSubmit。但如果搜索结果需要展示更多信息比如搜索结果页有筛选、排序onSubmit 跳转更合适。onFocus / onBlur焦点获取和失去TextInput({...}).onFocus((){/* 输入框获得焦点键盘弹出 */}).onBlur((){/* 输入框失去焦点键盘收起 */})onFocus 适合展开搜索模式——用户点击搜索框时页面切换到搜索模式隐藏其他内容只显示搜索结果。菜谱 App 没有这个需求但很多电商/内容 App 有。三个事件的选择场景事件触发时机适用场景onChange每次输入变化实时过滤菜谱 App 用这个onSubmit点击键盘确认提交搜索跳转结果页onFocus点击输入框展开搜索模式隐藏其他内容九、完整搜索功能的代码清单如果要把菜谱 App 的搜索功能从有 UI 无逻辑升级为完整可用需要改动以下位置文件改动说明Index.ets加State filteredRecipes存储过滤结果Index.ets加updateFiltered()方法过滤逻辑Index.etsonChange 里调updateFiltered()搜索词变化时重新过滤Index.etsForEach 改为this.filteredRecipes渲染过滤结果Index.ets加搜索空状态 if-else无结果时提示RecipeData.ets可选加searchRecipes(keyword)把过滤逻辑抽到数据层改动量评估约 30 行代码。在 8 道菜的规模下搜索功能的实现成本很低。如果数据量增大需要考虑防抖和索引优化。