资讯详情

资讯详情

【句匠|08】HarmonyOS ArkTS 句库搜索实战:支持关键词、分类和无结果反馈

搜索页在学习类应用里经常被低估。用户真正需要的不是一个输入框而是从“我想找某个题库”“我想搜一道题里的关键词”“我从分类页点进来想看某类题”这几种入口里都能得到稳定反馈。若状态边界不清楚搜索页很容易出现三个问题还没搜索就显示无结果、分类筛选和关键词互相污染、结果为空时没有明确解释。句匠项目的SearchPage.ets是一个轻量但完整的本地搜索页。它不做云端检索不建全文索引而是基于本地BANKS、REGIONS和getQuestions()把题库名称、题目题干和分类类型收束到同一套状态keyword、categoryType、bankResults、questionResults、searched。本文基于真实源码D:\huawei\one18-11\entry\src\main\ets\pages\SearchPage.ets并结合BankDetailPage.ets的题库资料边界复盘 HarmonyOS 5.0 ArkTS 搜索页如何支持关键词、分类和无结果反馈。正文唯一复核标记com.jiaweikang.one18。本文只讨论源码可复核能力SearchParams、aboutToAppear()、doSearch()、SearchHeader()、EmptySearchHome()、NoResult()、ResultList()、QuestionResultCard()、BankDetailProfile和本地题库数据。它不声称当前版本实现了云端搜索、拼音搜索、语义搜索、服务端排序或真实用户搜索数据统计。1. 搜索页先区分“未搜索”和“无结果”搜索页最容易犯的错是把初始页面和无结果页面都写成空数组判断。句匠源码里单独用了searchedState keyword: string State categoryType: string State categoryName: string State bankResults: Bank[] [] State questionResults: Question[] [] State searched: boolean false页面主体根据searched和两个结果数组分三段渲染if (!this.searched) { this.EmptySearchHome() } else if (this.bankResults.length 0 this.questionResults.length 0) { this.NoResult() } else { this.ResultList() }这条边界很关键。初始页应该引导用户搜索空结果页应该告诉用户换关键词。两者都可能是bankResults[]和questionResults[]但用户感知完全不同。searched让页面可以明确表达“你还没搜”和“已经搜过但没有匹配”。2. 路由分类入口会自动触发搜索SearchPage支持从分类页或其他入口带参数进入。参数模型很小interface SearchParams { categoryType?: string categoryName?: string }页面出现时如果路由参数带了categoryType就把分类写入状态并把keyword设置为分类名随后执行搜索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() } }这段逻辑说明分类入口不是简单地预填输入框而是一次完整搜索。这样用户从“介词搭配”“地道表达”等分类点进来时不需要再手动点击搜索按钮。需要注意边界分类搜索真正使用的是q.type this.categoryType不是keyword文本匹配。keyword categoryName主要用于输入框展示让用户知道当前筛选来自哪个分类。3.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() this.bankResults this.categoryType.length 0 ? [] : BANKS.filter(b b.name.toLowerCase().includes(kw)) 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 }这段代码有三个明确决策决策源码表现作用空输入不算搜索清空结果并searchedfalse避免初始页误显示无结果分类模式不搜题库categoryType.length 0 ? [] : ...分类页只展示题目不混入题库卡片题目最多 20 条qResults.length 20控制本地遍历后的列表长度本地搜索没有建立索引所以结果收集上限很重要。句匠题库规模不大直接遍历BANKS - getQuestions(bank.id)足够但如果后续题量上万就应该把搜索逻辑从页面里抽到服务层或索引模块。4. 分类和关键词不能同时长期生效搜索框onChange有一段状态清理.onChange((value: string) { this.keyword value if (this.categoryType.length 0 value ! this.categoryName) { this.categoryType this.categoryName } })这解决了一个常见交互问题用户从分类入口进来后输入框显示分类名。如果用户开始手动修改输入内容页面就不应该继续按原分类筛选否则输入“receive”却还在展示“介词搭配”分类结果会让人误判搜索失效。因此源码采用的规则是分类入口进入时使用categoryType一旦用户输入内容不等于原分类名就清除分类状态回到关键词搜索。这个规则简单但有效。它避免了“分类筛选 关键词筛选”的组合复杂度。当前页面没有实现多条件联合过滤所以不应该在文章或产品文案里声称支持多维组合筛选。5. 搜索头部同时承载返回、输入、提交和分类提示SearchHeader()不是只放一个输入框它还包含返回按钮、搜索按钮和分类提示条。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) .onChange((value: string) { this.keyword value if (this.categoryType.length 0 value ! this.categoryName) { this.categoryType this.categoryName } }) .onSubmit(() { this.doSearch() })搜索按钮也直接调用同一个函数Text(搜索) .fontSize(Sizes.BODY_FONT) .fontColor(Colors.PRIMARY) .fontWeight(FontWeight.Bold) .onClick(() { this.doSearch() })键盘提交和按钮提交共用doSearch()这点要保留。否则很容易出现键盘搜索和按钮搜索结果不一致。分类提示条则只在categoryType.length 0时显示if (this.categoryType.length 0) { Row() { Text(当前分类${this.categoryName}) Blank() Text(清除) .onClick(() { this.categoryType this.categoryName this.keyword this.doSearch() }) } }清除分类后调用doSearch()由于关键词和分类都为空页面会回到未搜索状态而不是展示空结果。6. 初始页用热门地区做搜索入口未搜索时页面展示EmptySearchHome()。它使用REGIONS渲染热门搜索词Flex({ wrap: FlexWrap.Wrap }) { 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) }这类入口不是装饰它承担了两个作用让首次进入搜索页的用户知道可以搜什么给本地搜索提供可复现输入。点击地区名会写入keyword并立即搜索。源码下方还有一句说明Text(可以搜索题库名称、语法点、关键词也可以从分类页进入题型结果。)这个说明与实现基本一致但要注意精确表达源码当前题目搜索匹配的是q.stem题库搜索匹配的是b.name。它没有搜索analysis、options、chapter.name或BankDetailProfile.focusTags。7. 无结果页要给出下一步动作NoResult()使用图片和两行文案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) } }空状态的重点是避免用户困惑。由于当前搜索只覆盖题库名和题干关键词用户搜一个选项文本或解析里的词可能不会命中。无结果页不应该暗示系统故障而应该提示换关键词。如果后续扩展搜索范围建议同步调整无结果文案。例如支持解析搜索后可以提示“试试语法点、例句或解析关键词”支持拼音搜索后可以提示“支持中文、英文或拼音首字母”。8. 结果列表按题库和题目分区ResultList()先渲染题库结果再渲染题目结果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) }分区的价值是让用户知道自己搜到的是“题库入口”还是“具体题目”。题库卡片使用已有BankCard题目结果则走专门的QuestionResultCard。这种分区也保留了后续扩展空间。比如后面可以加入“章节”“收藏”“错题”分区但每个分区都应该从明确的数据源来不能把不同类型混成一个没有来源标识的列表。9. 题目结果卡片只展示必要信息QuestionResultCard()展示题干、题型、题库名和练习入口Builder QuestionResultCard(q: Question) { Column({ space: 10 }) { Text(q.stem) .fontSize(Sizes.BODY_FONT) .fontWeight(FontWeight.Medium) .fontColor(Colors.TEXT_PRIMARY) .lineHeight(21) .maxLines(3) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width(100%) Row({ space: 8 }) { Text(questionTypeLabel(q.type)) Text(this.bankName(q.bankId)) Blank() Text(练习) } } .onClick(() { router.pushUrl({ url: pages/PracticePage, params: { bankId: q.bankId, mode: random } }) }) }这里的maxLines(3)和textOverflow很必要。题干可能包含英文句子、中文说明和下划线空缺小屏上如果不限制行数结果列表会变得很难扫读。点击题目结果后进入PracticePage参数是{ bankId: q.bankId, mode: random }。这说明当前源码并不是直接打开这道题而是进入对应题库的随机练习。文章必须如实说明这一点不能写成“点击搜索结果直接定位到该题并开始作答”。如果未来要支持精确定位需要给PracticePage增加questionId参数和定位逻辑。10. 题库详情页提供分类语义但不参与搜索计算BankDetailPage.ets中定义了BankDetailProfileinterface BankDetailProfile { subtitle: string intro: string cultureNote: string focusTags: string[] sceneTags: string[] }不同题库有不同资料例如介词搭配、地道表达、真题语法等。它们用于题库详情页展示private profile(): BankDetailProfile { const profile BANK_DETAIL_PROFILES.get(this.bankId()) return profile ? profile : DEFAULT_BANK_DETAIL_PROFILE }这部分和搜索页的关系要说清楚BankDetailProfile提供题库语义展示但当前SearchPage.doSearch()并没有搜索focusTags、sceneTags或cultureNote。用户能通过题库名或题干关键词搜索而不是通过题库详情文案全文搜索。这个边界对后续迭代有指导意义。如果产品想让“地道表达”“邮件用词”“高频错词”等标签可搜索就应该把BankDetailProfile或分类数据纳入搜索数据源而不是只改 placeholder。11. 本地搜索的真实边界当前实现适合轻量题库搜索但有明确边界能力当前源码状态题库名称搜索已实现使用BANKS.filter题干关键词搜索已实现使用q.stem.toLowerCase().includes(kw)分类题型搜索已实现使用q.type categoryType结果数量限制已实现题目结果最多 20 条无结果反馈已实现NoResult()拼音/首字母搜索未实现解析/选项全文搜索未实现云端搜索/服务端排序未实现点击结果定位到具体题未实现当前进入题库随机练习把边界写清楚是技术文章和上架材料都需要遵守的基本要求。尤其是“搜索”这个词很容易被理解成全量检索源码没有做的能力不应该过度包装。12. 可复核测试清单可以按下面方式验证搜索链路场景操作预期初始进入打开搜索页不输入展示热门搜索不显示无结果空搜索输入空白后点击搜索清空结果并回到未搜索状态题库名搜索输入题库名称的一部分bankResults出现题库卡片题干关键词输入题干中存在的英文词questionResults出现题目卡片分类入口带categoryType跳转自动搜索并显示当前分类条修改分类关键词分类入口后手动改输入清除categoryType改为关键词搜索无命中输入不存在的词展示NoResult()点击题目点击题目卡片进入对应题库的随机练习页调试时建议先看searched再看两个结果数组。很多 UI 误判不是搜索逻辑错而是页面把“未搜索”和“已搜索无结果”混在一起。13. 常见问题与处理建议问题可能原因处理方向初始页显示“未找到”只按结果数组判断没有使用searched保留三段式渲染未搜索、无结果、有结果分类入口后输入关键词无效categoryType没有被清除在onChange中判断输入是否偏离categoryName搜索结果过多卡顿本地遍历没有上限保留 20 条上限题量变大后抽服务层或索引搜选项文本搜不到当前只搜q.stem若有需求扩展到 options/analysis/chapter点击题目没有定位具体题当前只传bankId和random增加questionId参数和练习页定位逻辑题库详情标签搜不到BankDetailProfile未纳入搜索源将 focusTags/sceneTags 作为可搜索字段总结句匠的SearchPage.ets没有把搜索页做成复杂系统而是用很少的状态把入口、查询、结果和反馈分开keyword负责用户输入categoryType负责分类入口searched区分未搜索和无结果bankResults与questionResults分别承载题库和题目结果。BankDetailPage.ets提供了题库语义资料但当前并不参与搜索计算这个边界同样需要明确。对 HarmonyOS ArkTS 学习应用来说这套实现的可复用点是状态边界而不是某个样式。先把搜索范围讲清楚再把未搜索、无结果、有结果分开渲染最后让点击结果进入明确的练习入口。这样即使后续扩展全文索引、分类组合筛选或具体题定位也能在现有结构上继续演进。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →