HarmonyOS7 搜索页最容易做成半成品:历史、热词、结果页这次一次补齐

📅 2026/7/1 18:15:21
HarmonyOS7 搜索页最容易做成半成品:历史、热词、结果页这次一次补齐
文章目录前言搜索栏组件输入联想与防抖搜索搜索历史PersistentStorage 持久化搜索落地页历史 热词搜索结果页商品列表 筛选排序空状态处理搜索执行的入口几点实用建议前言搜索是电商 App 的核心入口之一。用户明确知道自己要什么的时候搜索比一层层翻分类快得多。这篇我们把搜索模块做完整搜索栏交互、历史记录、热词推荐、搜索结果页外加筛选排序和空状态处理。搜索栏组件搜索栏要支持两个场景首页顶部的简洁版点击跳转到搜索页和搜索页顶部的完整版输入框 取消按钮。我把它们做成了同一个组件通过参数区分模式。// entry/src/main/ets/components/SearchBar.etsComponentexportstruct SearchBar{Propplaceholder:string搜索商品PropisFocusMode:booleanfalseStateinputValue:stringonSearch?:(keyword:string)voidonCancel?:()voidonFocus?:()voidonInputChange?:(value:string)voidbuild(){Row({space:10}){// 搜索输入框Row(){Image($r(app.media.ic_search)).width(18).height(18).margin({left:12})TextInput({text:this.inputValue,placeholder:this.placeholder}).layoutWeight(1).height(36).backgroundColor(Color.Transparent).fontSize(14).placeholderColor(#BBBBBB).onChange((value:string){this.inputValuevaluethis.onInputChange?.(value)}).onSubmit((){if(this.inputValue.trim().length0){this.onSearch?.(this.inputValue.trim())}})// 有输入内容时显示清除按钮if(this.inputValue.length0){Image($r(app.media.ic_clear)).width(18).height(18).margin({right:8}).onClick((){this.inputValuethis.onInputChange?.()})}}.layoutWeight(1).height(36).backgroundColor(#F5F5F5).borderRadius(18)// 聚焦模式下显示取消按钮if(this.isFocusMode){Text(取消).fontSize(15).fontColor(#666666).onClick((){this.onCancel?.()})}}.width(100%).height(56).padding({left:12,right:12}).backgroundColor(Color.White)}}首页的搜索栏是非聚焦模式点击整个区域跳转到搜索页。搜索页用聚焦模式输入框自动弹起键盘。输入联想与防抖搜索用户边输入边出联想词体验很好但直接每次输入都请求太浪费。加个防抖停顿 300ms 再发请求// 在搜索页中使用防抖Componentstruct SearchPage{Statekeyword:stringStatesuggestions:string[][]privatedebounceTimer:number-1build(){Column(){SearchBar({isFocusMode:true,onInputChange:(value:string){this.keywordvaluethis.debounceSearch(value)},onSearch:(kw:string){this.doSearch(kw)},onCancel:(){// 返回首页}})if(this.suggestions.length0this.keyword.length0){this.SuggestionList()}else{this.SearchLanding()// 搜索落地页历史 热词}}}privatedebounceSearch(value:string){// 清除上一次定时器if(this.debounceTimer!-1){clearTimeout(this.debounceTimer)}if(value.trim().length0){this.suggestions[]return}this.debounceTimersetTimeout((){this.fetchSuggestions(value)},300)}privateasyncfetchSuggestions(keyword:string){// 调用联想词接口try{// this.suggestions await ProductRepository.getSuggestions(keyword)// mock 数据this.suggestions[keyword 新鲜,keyword 礼盒装,keyword 包邮,keyword 当季,]}catch(e){this.suggestions[]}}}clearTimeoutsetTimeout是经典的防抖实现。ArkTS 里这两个方法是全局可用的不需要额外引入。300ms 的延迟体感上刚好——用户连续打字不会频繁请求停下来又很快出结果。搜索历史PersistentStorage 持久化搜索历史需要持久化存储关了 App 再打开还在。HarmonyOS 提供了PersistentStorage专门做这个事。// lib_core/src/main/ets/utils/SearchHistoryManager.etsconstHISTORY_KEYsearch_historyconstMAX_HISTORY15exportclassSearchHistoryManager{// 初始化时从持久化存储读取staticinit(){PersistentStorage.persistPropstring[](HISTORY_KEY,[])}// 获取历史列表staticgetHistory():string[]{returnAppStorage.getstring[](HISTORY_KEY)??[]}// 添加搜索记录去重 放最前面staticaddHistory(keyword:string){lethistorythis.getHistory()// 去重如果已有先删掉旧的constindexhistory.indexOf(keyword)if(index0){history.splice(index,1)}// 插入到头部history.unshift(keyword)// 限制最大数量if(history.lengthMAX_HISTORY){historyhistory.slice(0,MAX_HISTORY)}AppStorage.setstring[](HISTORY_KEY,history)}// 清空历史staticclearHistory(){AppStorage.setstring[](HISTORY_KEY,[])}}思路很简单用PersistentStorage.persistProp把搜索历史和磁盘绑定之后通过AppStorage读写就行。每次搜索的时候调一下addHistory自动去重、自动限制条数。在 EntryAbility 的onCreate里记得调SearchHistoryManager.init()不然第一次打开 App 读不到历史。搜索落地页历史 热词用户点进搜索页但还没输入的时候展示搜索历史 热门搜索。这个页面我管它叫「搜索落地页」BuilderSearchLanding(){Scroll(){Column({space:20}){// 搜索历史if(this.historyList.length0){Column(){Row(){Text(搜索历史).fontSize(16).fontWeight(FontWeight.Medium)Blank()Image($r(app.media.ic_delete)).width(20).height(20).onClick((){// 弹出确认弹窗后清空AlertDialog.show({title:提示,message:确认清空搜索历史,primaryButton:{value:取消,action:(){}},secondaryButton:{value:清空,action:(){SearchHistoryManager.clearHistory()this.historyList[]}}})})}.width(100%).padding({bottom:12})// 历史标签用 Flex 换行排列Flex({wrap:FlexWrap.Wrap}){ForEach(this.historyList,(item:string){Text(item).fontSize(13).fontColor(#666666).padding({left:12,right:12,top:6,bottom:6}).backgroundColor(#F5F5F5).borderRadius(16).margin({right:8,bottom:8}).onClick((){this.doSearch(item)})},(item:string)item)}}.width(100%).padding({left:16,right:16})}// 热门搜索Column(){Text(热门搜索).fontSize(16).fontWeight(FontWeight.Medium).width(100%).padding({bottom:12})ForEach(this.hotKeywords,(item:HotKeyword,index:number){Row(){Text(${index1}).fontSize(15).fontWeight(FontWeight.Bold).fontColor(index3?#FF6B35:#999999).width(24)Text(item.keyword).fontSize(14).fontColor(#333333).layoutWeight(1)if(item.isHot){Text(热).fontSize(10).fontColor(Color.White).backgroundColor(#FF4D4F).borderRadius(4).padding({left:4,right:4,top:2,bottom:2})}}.width(100%).height(44).onClick((){this.doSearch(item.keyword)})},(item:HotKeyword)item.keyword)}.width(100%).padding({left:16,right:16})}.width(100%).padding({top:12})}}历史标签用Flex的Wrap模式做流式布局标签多了自动换行每个标签是一个胶囊形状圆角 16vp。热门搜索用列表形式前三名数字用主题色高亮。有的热词后面带个红色的「热」标签这个效果用一个小 Text 加红色背景就搞定了。搜索结果页商品列表 筛选排序用户提交搜索后进入结果页。上面是筛选排序栏下面是商品列表BuilderSearchResultView(){Column(){// 排序栏Row(){ForEach(this.sortOptions,(option:SortOption){Column({space:2}){Text(option.label).fontSize(14).fontColor(this.currentSortoption.value?#FF6B35:#666666).fontWeight(this.currentSortoption.value?FontWeight.Medium:FontWeight.Normal)if(this.currentSortoption.value){Rect().width(20).height(2).fill(#FF6B35).borderRadius(1)}}.layoutWeight(1).height(44).justifyContent(FlexAlign.Center).onClick((){this.currentSortoption.valuethis.doSearch(this.keyword)})},(option:SortOption)option.value)}.width(100%).backgroundColor(Color.White)// 搜索结果列表if(this.resultList.length0!this.isSearching){this.EmptyState()}else{List(){ForEach(this.resultList,(item:ProductItem){ListItem(){this.ProductListItem(item)}},(item:ProductItem)item.id)}.width(100%).layoutWeight(1)}}}搜索结果用普通的 List 就行不需要瀑布流——搜索结果强调信息对比等高卡片更容易横向比较价格。空状态处理搜索没有结果的时候不能给用户看空白页。做一个友好的空状态BuilderEmptyState(){Column({space:12}){Image($r(app.media.ic_empty_search)).width(120).height(120).margin({top:80})Text(没有找到相关商品).fontSize(16).fontColor(#999999)Text(换个关键词试试或者看看热门推荐).fontSize(13).fontColor(#CCCCCC)Button(看看热门推荐).fontSize(14).fontColor(#FF6B35).backgroundColor(Color.Transparent).borderRadius(20).border({width:1,color:#FF6B35}).margin({top:20}).onClick((){// 跳转到推荐页})}.width(100%).alignItems(HorizontalAlign.Center)}空状态三要素一个插图、一句主文案、一个行动按钮。别小看这个页面很多用户搜不到东西就流失了一个好的空状态能把人拉回来。搜索执行的入口搜索动作触发时记得保存历史privatedoSearch(keyword:string){this.keywordkeywordthis.inputValuekeyword SearchHistoryManager.addHistory(keyword)this.historyListSearchHistoryManager.getHistory()this.suggestions[]// 清除联想this.fetchResults()// 拉取搜索结果}几点实用建议防抖时间别太短。200ms 以下用户感知不到延迟请求照样频繁。300-500ms 是比较舒适的区间。搜索历史条数要限制。我设的 15 条太多了用户翻着也累占存储也没必要。热词要后端可控。热门搜索是运营位一定从后端拉取别写死在客户端。后端可以按时间段、按活动灵活配置。空状态的推荐内容要真实。别用固定数据糊弄调一下推荐接口让用户真的能从这里找到东西。搜索模块做完首页的核心交互就差不多了。下一篇我们做分类页面——左边一级分类、右边二级分类、联动滚动是电商 App 里实现起来最有意思的一个页面。