【笔下生辉|06】HarmonyOS ArkTS 素材搜索实战:实现关键词过滤、空结果与跳转

发布时间:2026/10/27 1:40:34
【笔下生辉|06】HarmonyOS ArkTS 素材搜索实战:实现关键词过滤、空结果与跳转 题库类 HarmonyOS 应用做搜索页时最容易把事情做散入口页只负责跳转搜索页又同时关心输入框、分类筛选、题库列表、题目列表、空结果提示和练习入口。等页面越来越复杂后一个常见问题会出现用户从“热门地区”点进来能搜到内容从“题型分类”点进来却没有状态清空关键词后列表还残留点击题目进入练习时题型和起始题目又丢了。笔下生辉的SearchPage.ets没有把搜索包装成一个很重的服务层而是把页面边界收得比较明确它从router.getParams()接收可选分类参数用keyword、categoryType、categoryName、searched管住页面状态再基于本地BANKS和getQuestions()做题库与题目的过滤。搜索结果分成三种渲染未搜索态、空结果态、结果列表态题目卡片点击后通过router.pushUrl()把bankId、mode、questionType和startQuestionId交给PracticePage。本文基于本地工程D:\huawei\one17-11的真实源码编写包名标记为com.jiaweikang.one17。这里先说明边界当前源码没有服务端全文检索、拼音检索、模糊排序、AI 推荐或倒排索引它是一个面向本地题库数据的 ArkTS 页面级搜索实现。文章只讨论源码可以复核的关键词过滤、分类入口、空结果、结果卡片和练习跳转。本文会解决五个工程问题如何让搜索页同时支持普通关键词入口和题型分类入口。如何把“未搜索”和“搜索后无结果”区分开避免空白页。如何在本地BANKS和题目集合中完成题库/题目两类过滤。如何限制题目结果数量避免长列表影响移动端体验。如何从搜索结果稳定跳到练习页并携带起始题目。一、搜索页先定义状态边界SearchPage的状态并不多但每个字段都有明确职责。它没有把输入框内容、分类来源、结果列表和页面是否搜索过混在一起而是拆成几个独立状态。State keyword: string State categoryType: string State categoryName: string State bankResults: Bank[] [] State questionResults: Question[] [] State searched: boolean false StorageLink(topAvoidAreaHeightPx) topAvoidAreaHeightPx: number 0 StorageLink(navigationIndicatorHeightPx) navigationIndicatorHeightPx: number 0这组状态可以分成三类状态职责页面影响keyword用户输入或入口预填关键词驱动题库名和题干搜索categoryType/categoryName从分类页带来的题型约束决定是否按题型过滤bankResults/questionResults/searched搜索结果和渲染分支决定首页、空结果、结果列表searched是这个页面里很关键的布尔值。很多搜索页只看results.length这样会把“用户还没有搜索”和“用户搜索后没有结果”都渲染成同一种空页面。源码里用searched区分这两种状态未搜索时展示热门搜索和说明搜索后无结果才展示空结果图和提示。二、路由参数让分类入口直接进入搜索结果搜索页不只从搜索框进入也会从首页分类、题型分类页进入。源码定义了一个很窄的参数接口只接收题型分类所需的两个字段。interface SearchParams { categoryType?: string categoryName?: string }页面出现时读取路由参数。如果携带categoryType说明这是分类入口不需要用户再点一次搜索按钮页面会把分类名称写入keyword并立即执行doSearch()。aboutToAppear(): void { const params router.getParams() as SearchParams | undefined if (params params.categoryType) { this.categoryType params.categoryType this.categoryName params.categoryName || params.categoryType this.keyword this.categoryName this.doSearch() } }这段代码解决的是入口一致性问题。比如CategoryPage.ets里进入搜索页时传入router.pushUrl({ url: pages/SearchPage, params: { categoryType: cat.type, categoryName: cat.name } })搜索页收到参数后不会把它当成普通关键词而是进入“题型过滤”模式。这样分类页负责选择题型搜索页负责展示题型下的题目两边职责不会重复。三、空输入要回到未搜索态doSearch()的第一段处理空输入。这里不能简单把结果设为空因为空输入有两种语义一种是用户还没开始搜索另一种是用户清除了已有条件。源码选择把它们都收敛到未搜索态。private doSearch(): void { if (this.keyword.trim().length 0 this.categoryType.length 0) { this.bankResults [] this.questionResults [] this.searched false return } this.searched true const kw this.keyword.trim().toLowerCase() // 后续过滤逻辑 }这段逻辑有两个防御点场景如果不处理当前源码行为用户打开搜索页未输入页面可能显示“未找到内容”searchedfalse展示热门搜索用户清除分类和关键词旧结果可能残留清空两类结果并回到首页这个处理对移动端体验很重要。搜索页打开后如果直接展示“未找到相关内容”用户会误以为题库为空清除条件后如果保留旧结果用户又会误以为清除没有生效。四、关键词搜索先过滤题库名当前源码的普通关键词搜索分成两类结果题库结果和题目结果。题库结果只在非分类模式下出现因为分类入口已经限定了题型不再需要展示题库名匹配。const kw this.keyword.trim().toLowerCase() this.bankResults this.categoryType.length 0 ? [] : BANKS.filter(b b.name.toLowerCase().includes(kw))这个判断让页面避免出现混乱的结果组合模式categoryType题库结果普通关键词搜索空按bank.name匹配题型分类入口非空不展示题库结果题库匹配使用的是toLowerCase().includes(kw)。这说明当前实现是基础字符串包含匹配不是分词检索。写文章和发布材料时不能把它描述为“智能搜索”或“语义搜索”否则就和源码不一致。五、题目搜索遍历本地题库并限制数量题目结果来自getQuestions(bank.id)。源码会遍历所有题库再遍历每个题库的题目。普通搜索按题干包含关键词匹配分类搜索按题目类型匹配。const qResults: Question[] [] for (const bank of BANKS) { const qs getQuestions(bank.id) for (const q of qs) { if ((this.categoryType.length 0 q.type this.categoryType) || (this.categoryType.length 0 q.stem.toLowerCase().includes(kw))) { qResults.push(q) if (qResults.length 20) break } } if (qResults.length 20) break } this.questionResults qResults这段逻辑里有一个实际工程取舍题目结果最多保留 20 条。对于本地数据量不大的题库应用这种线性遍历足够直接同时限制数量可以避免搜索页一次性渲染过长列表。如果后续题库规模变大优化方向也很清晰规模变化当前实现可能的问题可演进方案题库几十条线性遍历可接受保持页面级过滤题库几千条搜索触发可能卡顿抽出 SearchService 并做缓存支持远程题库本地getQuestions不够Repository 层封装接口支持多字段排序只有包含匹配增加权重和排序字段当前文章只按第一种边界展开因为源码就是本地题库搜索。六、输入框变化要清理分类状态搜索页允许分类入口预填关键词但用户一旦手动修改输入框就不能继续保留旧分类约束。源码在TextInput.onChange里处理了这个边界。TextInput({ placeholder: 搜索题型、题库、题目, text: this.keyword }) .layoutWeight(1) .height(42) .fontSize(Sizes.BODY_FONT) .fontColor(Colors.INPUT_TEXT) .placeholderColor(Colors.INPUT_PLACEHOLDER) .caretColor(Colors.PRIMARY) .backgroundColor(Colors.INPUT_BG) .padding(0) .onChange((value: string) { this.keyword value if (this.categoryType.length 0 value ! this.categoryName) { this.categoryType this.categoryName } }) .onSubmit(() { this.doSearch() })这里的判断很具体如果当前是分类模式并且用户输入已经不等于原来的分类名称就清除分类条件。否则用户以为自己在搜“作文”实际页面还在按旧题型过滤结果会非常难解释。搜索按钮和键盘提交都调用同一个doSearch()Text(搜索) .fontSize(Sizes.BODY_FONT) .fontColor(Colors.PRIMARY) .fontWeight(FontWeight.Bold) .height(Sizes.TOUCH_TARGET) .padding({ left: 4, right: 4 }) .onClick(() { this.doSearch() })这能保证触屏点击和键盘提交得到一致结果。对于鸿蒙电脑、平板外接键盘、2in1 设备这个细节比只做按钮点击更稳。七、分类标签提供可撤销的过滤上下文当categoryType存在时搜索头部会展示当前分类并提供“清除”动作。这个小区域让用户知道自己不是在做普通关键词搜索。if (this.categoryType.length 0) { Row() { Text(当前分类${this.categoryName}) .fontSize(Sizes.CAPTION_FONT) .fontColor(Colors.PRIMARY) Blank() Text(清除) .fontSize(Sizes.CAPTION_FONT) .fontColor(Colors.TEXT_HINT) .onClick(() { this.categoryType this.categoryName this.keyword this.doSearch() }) } .width(100%) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor(Colors.PRIMARY_LIGHT) .borderRadius(14) }这里的“清除”不是只清空 UI 文案而是同时清空三处状态categoryType、categoryName、keyword。随后调用doSearch()因为空输入且无分类页面会回到未搜索态。这个动作闭环比单纯隐藏标签更可靠。八、三种页面状态分别渲染页面主体的渲染分支很清楚。它不把空首页和空结果页合并也不在结果列表内部再塞复杂判断。build() { Column() { this.SearchHeader() if (!this.searched) { this.EmptySearchHome() } else if (this.bankResults.length 0 this.questionResults.length 0) { this.NoResult() } else { this.ResultList() } } .width(100%) .height(100%) .justifyContent(FlexAlign.Start) .alignItems(HorizontalAlign.Start) .backgroundColor(Colors.BACKGROUND) }三个状态对应不同用户心理渲染分支用户状态页面目标EmptySearchHome()还没搜索给热门地区和搜索说明NoResult()搜过但没有结果提示换关键词ResultList()有结果展示题库和题目这类页面状态设计可以直接复用到其他本地资料检索场景比如诗词素材、写作模板、错题记录、学习历史等。关键不是状态数量多而是每个状态都能解释用户下一步该做什么。九、热门搜索从地区数据进入普通关键词模式未搜索态展示REGIONS用户点击地区名称后把地区名写入keyword并执行搜索。ForEach(REGIONS, (region: Region) { Text(region.name) .fontSize(Sizes.BODY_FONT) .fontColor(Colors.TEXT_SECONDARY) .height(38) .padding({ left: 16, right: 16 }) .backgroundColor(Colors.SURFACE) .borderRadius(19) .margin({ right: 10, bottom: 10 }) .onClick(() { this.keyword region.name this.doSearch() }) }, (region: Region) region.id)这不是分类搜索因为它没有写入categoryType。地区名进入的是普通关键词过滤链路会匹配题库名和题干。这样热门搜索和输入框搜索复用同一套doSearch()避免热门入口单独维护一份结果生成逻辑。十、结果列表把题库和题目分区展示结果列表用Scroll包裹先展示题库再展示题目。底部留出安全区避免最后一条结果被系统手势区域遮挡。Builder ResultList() { Scroll() { Column({ space: 14 }) { if (this.bankResults.length 0) { this.ResultTitle(题库 (${this.bankResults.length})) ForEach(this.bankResults, (bank: Bank) { Column() { BankCard({ bank: bank }) } .padding({ left: Sizes.PADDING_LARGE, right: Sizes.PADDING_LARGE }) }, (bank: Bank) bank.id) } if (this.questionResults.length 0) { this.ResultTitle(题目 (${this.questionResults.length})) ForEach(this.questionResults, (q: Question) { this.QuestionResultCard(q) }, (q: Question) q.id) } Blank().height(24 this.bottomSafePadding()) } .padding({ top: 8 }) } .layoutWeight(1) .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) }这段代码有三个适配点适配点源码做法作用长结果Scroll()小屏仍可浏览全部结果系统底部区域bottomSafePadding()防止最后一项不可点结果区分ResultTitle()题库和题目不混在一起BankCard自己负责题库卡片样式和跳转搜索页只负责把匹配到的Bank传进去。这样组件边界是干净的。十一、题目卡片点击进入指定练习题目搜索的最终目标不是只展示文字而是让用户从某道题开始练习。QuestionResultCard点击后把题目上下文传给PracticePage。.onClick(() { router.pushUrl({ url: pages/PracticePage, params: { bankId: q.bankId, mode: random, questionType: q.type, startQuestionId: q.id } }) })这四个参数各自有作用参数来源作用bankId当前题目所属题库让练习页读取同一题库mode固定为random进入普通练习模式questionTypeq.type让练习页可按题型约束startQuestionIdq.id从用户点击的题开始如果只传bankId用户点了搜索结果中的某题进入练习页后可能看不到这道题如果不传questionType分类搜索入口的上下文会丢失。当前源码把这两个信息都传过去搜索页和练习页的链路就闭合了。十二、空结果页不应该只是空白搜索后没有题库和题目结果时源码展示空状态图片和两行提示。Builder NoResult() { Column({ space: 12 }) { Image($r(app.media.img_empty_default)) .width(120) .height(120) .objectFit(ImageFit.Contain) .opacity(0.6) Text(未找到相关内容) .fontSize(Sizes.BODY_FONT) .fontColor(Colors.TEXT_HINT) Text(换个关键词试试) .fontSize(Sizes.CAPTION_FONT) .fontColor(Colors.TEXT_HINT) } .layoutWeight(1) .width(100%) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) }空状态的价值不是装饰而是给用户反馈搜索已经执行只是没有匹配。这里结合前面的searched状态避免了“打开页面就显示无结果”的误判。十三、安全区处理保证搜索页在不同设备上可用SearchPage通过StorageLink读取顶部和底部避让高度并转换成 vp。private topSafePadding(): number { return Math.max(0, this.getUIContext().px2vp(this.topAvoidAreaHeightPx)) } private bottomSafePadding(): number { return Math.max(Sizes.BOTTOM_NAV_MIN_PADDING, this.getUIContext().px2vp(this.navigationIndicatorHeightPx)) }搜索头部用顶部安全区.padding({ left: Sizes.PADDING_LARGE, right: Sizes.PADDING_LARGE, top: 10 this.topSafePadding(), bottom: 10 })结果列表底部用底部安全区Blank().height(24 this.bottomSafePadding())这说明搜索页不是只按固定手机窗口写死。对于 HarmonyOS 5.0 的手机、折叠屏、平板或鸿蒙电脑窗口顶部状态栏和底部导航/手势区域都可能影响可点击区域。源码用统一尺寸常量和安全区高度处理能降低结果项被遮挡的风险。十四、本地复核清单复核这个搜索页时不需要先看文章描述直接从源码路径检查即可。复核点源码位置预期分类入口参数aboutToAppear()categoryType存在时预填并搜索空输入处理doSearch()开头清空结果并searchedfalse题库过滤BANKS.filter非分类模式按题库名匹配题目过滤getQuestions(bank.id)循环分类按q.type关键词按q.stem数量限制qResults.length 20最多展示 20 条题目空结果NoResult()搜索后无结果才显示练习跳转QuestionResultCard传bankId/mode/questionType/startQuestionId建议手工验证四条路径从首页搜索入口进入未输入时应看到热门搜索。输入一个能匹配题库名的关键词结果区应出现题库分区。从分类页进入搜索页应自动按题型展示题目并显示当前分类条。搜索一个不存在的关键词应进入空结果页而不是保留旧结果。十五、常见问题与修复方向问题可能原因修复方向清空输入后仍显示旧结果没有在空输入分支清空结果保留bankResults[]和questionResults[]分类搜索后修改关键词仍按旧题型过滤onChange没清理categoryType输入变化时判断是否偏离categoryName点击搜索结果进入练习不是目标题未传startQuestionId跳转参数带上q.id空结果和未搜索态混淆只用结果长度判断页面增加searched状态底部结果不可点击列表没有底部安全区添加bottomSafePadding()总结SearchPage.ets的重点不是算法复杂度而是搜索页的状态闭环。它用keyword接收普通输入用categoryType承接题型入口用searched区分未搜索和无结果用BANKS/getQuestions()完成本地过滤再用router.pushUrl()把用户点击的题目带到PracticePage。对 HarmonyOS 题库应用来说这种实现适合第一版本地资料搜索页面状态少、入口清楚、结果可解释、空状态明确、跳转参数完整。后续如果要扩展到更大题库或远程检索再把过滤逻辑抽到SearchService或 Repository 层会更稳但在当前源码边界内SearchPage已经完成了关键词过滤、空结果与练习跳转的核心链路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询