乒乓球口袋教练 HarmonyOS 学习应用(05):搜索与分类组合过滤

发布时间:2026/7/30 11:53:57

乒乓球口袋教练 HarmonyOS 学习应用(05):搜索与分类组合过滤 一、把关键词和专项筛选放进同一个查询入口课程多起来以后用户常常先输入“反手”再把范围切到相持模块。若关键词和模块按钮分别在页面各自过滤列表条数、课程标签和点击后的详情很容易不一致。本项目把两个条件交给 SearchService.searchkeyword 先 trim 并转小写moduleId 决定课程范围再对标题、描述、关键词和子分类标签做同一轮匹配。// 简单全字段匹配标题 描述 关键词 子分类标签 export class SearchService { static search(keyword: string, moduleId?: string): SearchHit[] { const kw: string keyword.trim().toLowerCase(); return COURSES .filter(c moduleId undefined || moduleId all || c.moduleId moduleId) .filter(c SearchService.matches(c, kw)) .mapSearchHit((c: Course): SearchHit { const m: LearnModule | undefined findModule(c.moduleId); return { course: c, moduleTitle: m?.title ?? } as SearchHit; }); } private static matches(c: Course, kw: string): boolean { if (kw.length 0) return true; if (c.title.toLowerCase().indexOf(kw) 0) return true; if (c.desc.toLowerCase().indexOf(kw) 0) return true; if (c.subCategoryLabel.indexOf(kw) 0) return true; for (const k of c.keywords) { if (k.toLowerCase().indexOf(kw) 0) return true; } return false; }二、空关键词并不等于跳过模块边界模块条件的 all 不是删除筛选逻辑而是显式表示“允许所有 moduleId”。因此空关键词时仍会返回当前模块的完整课程集合输入词后也不会意外跨到发球或战术模块。这个约束让顶部筛选按钮和搜索框是两个输入却只有一个可解释的结果集合。参与者输入输出或约束模型或配置稳定标识、模式或模块字段给出可追溯的工程事实服务或系统能力经过归一化的请求返回明确结果或失败原因页面回读后的结果只渲染不保存第二份事实} private hits(): SearchHit[] { return SearchService.search(this.keyword, this.moduleFilter); } Builder resultSection() { Column() { if (this.hits().length 0) { EmptyState({ emoji: , title: 未找到相关内容, subtitle: 换个关键词试试或在分类里浏览 }); } else { this.resultTitle(); Column({ space: AppSizes.s2 }) { ForEach(this.hits(), (h: SearchHit) { this.hitRow(h); }, (h: SearchHit) h.course.id); } .padding({ left: AppSizes.s4, right: AppSizes.s4 });三、命中结果为何要带回匹配字段SearchHit 不只保存课程对象还保留 matchedFields。页面可以提示用户命中的是标题、描述还是关键词而不是把所有结果都说成标题匹配。对于“旋转”这类可能出现在描述中的词这比只调用 title.includes 更接近用户的真实预期也让后续高亮有稳定来源。Column({ space: AppSizes.s2 }) { ForEach(this.hits(), (h: SearchHit) { this.hitRow(h); }, (h: SearchHit) h.course.id); } .padding({ left: AppSizes.s4, right: AppSizes.s4 }); } } .width(100%); } Builder resultTitle() { Row() { Text(搜索结果 this.hits().length ) .fontSize(AppText.fsTitle) .fontWeight(AppText.fwSemi) .fontColor(this.palette.textPrimary) .layoutWeight(1); } .width(100%) .padding({ left: AppSizes.s4, right: AppSizes.s4, top: AppSizes.s2, bottom: AppSizes.s2 }); }四、搜索页和课程详情怎样交接点击命中项后SearchPage 传递的是 course.id课程详情再按 id 查回模型。这样搜索结果不会把一份旧课程对象塞进路由也不会因为返回搜索页而保留已经失效的对象。找不到 id 时应留在可解释的结果或空态不能拼一个默认课程继续播放。情况容易出现的错误本文采用的处理数据或配置缺项伪造默认成功状态停在可解释的失败或空态页面重进使用上一页残留对象从模型、服务或系统重新回读重复动作再写一遍相同业务事实由稳定入口或回调收敛import { packagedVideoFileName } from ./CourseVideoAssets; export interface Course { id: string; moduleId: string; subCategory: string; subCategoryLabel: string; title: string; desc: string; thumbKey: string; videoSrc?: string; durationSec: number; order: number; intro: Block[]; keyPoints: Block[]; mistakes: Block[]; training: Block[]; keywords: string[]; externalVideoRefs?: ExternalVideoRef[]; } export interface SubCategory { id: string;五、筛选回归要观察哪些具体结果回归时先在全部范围搜索一个只出现在关键词中的动作术语再切到不含该术语的专项确认结果归零随后清空关键词确认当前专项课程恢复最后点击一条命中课程、返回并重进搜索页检查标题、专项标签和命中字段仍来自同一条查询链路。验收阶段实际动作回读重点前置确认启动正确 bundle 或打开目标页标题、入口与模块身份主题操作执行搜索、切换、完成或跳转服务/系统返回的结果重进检查返回、重启或切换范围后再进入事实没有依赖旧页面残留六、实现边界与维护顺序搜索词、专项模块和课程字段在同一条查询链路中收敛避免页面分别筛选后出现标题、分类与结果数量互相矛盾。 新增需求时应先补齐模型、配置或服务合同再调整页面入口把同一个判断复制到多个组件短期看似方便后续会使结果无法回读。ArkTS 状态管理的基础机制可参考 HarmonyOS 官方文档。七、继续扩展时的约束搜索条件的归一化必须早于排序。先把关键词转换为可比较的值再确定模块范围最后计算匹配字段结果才能稳定复现。若把排序写在页面中用户从课程详情返回后可能看到同一关键词却是另一种顺序测试也无法知道变化来自课程数据还是渲染时机。对于不存在的 moduleId服务不应该宽容地退回全部课程那会让路由或按钮传错参数时看起来仍有结果。明确返回空集合配合页面空态更容易暴露调用链错误。真正需要全量搜索时调用方显式传 all使阅读代码的人一眼能看懂扩大范围是有意的。后续若加入历史搜索或远程建议词历史项只保存用户输入不缓存旧 SearchHit。重新打开历史词时仍需经过当前 SearchService这样课程下架、关键词修改和模块调整才能立刻反映到结果中。结果数量为何必须从命中集合计算搜索页标题中的数量直接来自 hits().length而不是由输入框事件维护一个计数器。前者会随关键词、模块和课程模型重算后者容易在清空输入、返回页面或课程数据变化后留下旧数字。对于用户而言数量、每一行课程和命中说明应始终是同一集合的三个投影。匹配规则改变时怎样验收增加一个关键词字段后要准备标题不含该词但 keywords 含该词的课程确认它被召回再准备同词但不同模块的课程确认模块筛选排除它。两组数据分别覆盖匹配能力和范围能力不能只用一条“搜索成功”的截图混在一起。

相关新闻