鸿蒙ArkUI实战:搜索页面与实时筛选
搜索是 App 最高频的功能之一。本文用 ArkUI 构建一个完整的搜索页面,覆盖实时筛选、搜索历史管理、热门搜索排行,以及
TextInput的onSubmit事件处理。
一、我们要做什么
一个完整的搜索页面,三个区域在不同状态下交替显示:
- 默认状态 — 搜索栏为空时,显示搜索历史 + 热门搜索
- 搜索中 — 输入文字时,实时筛选结果列表
- 无结果 — 输入了文字但没匹配到任何内容,显示空状态
另外:
4. 搜索历史管理 — 执行搜索时自动记录,去重,最多保留 10 条,可单条删除、一键清除
5. 热门搜索 — 10 个热词,前 3 名红色数字 + "热"标签
二、数据准备
2.1 搜索源数据
class SearchItem {
id: number;
title: string;
category: string;
reads: number;
}
const ALL_DATA: SearchItem[] = [
new SearchItem(1, '鸿蒙ArkUI状态管理深度解析', '技术文章', 3421),
new SearchItem(2, 'DevEco Studio 6.0新特性一览', '工具', 2890),
// ... 共 15 条
];
15 条模拟数据,每条有标题、分类、阅读数。搜索结果按标题和分类字段匹配。
2.2 热门搜索
const HOT_SEARCHES: string[] = [
'鸿蒙', 'ArkUI', '状态管理', '性能优化', '分布式',
'多端部署', 'DevEco Studio', 'HarmonyOS NEXT', '元服务', '动画',
];
10 个热词,常量数组,不参与状态管理。

三、交互点1:实时筛选
TextInput({ placeholder: '搜索文章、教程、话题...', text: $$this.searchText })
.fontSize(FontSize.BODY)
.layoutWeight(1)
.height(36)
.backgroundColor(Color.Transparent)
.onChange(() => this.filterResults())
.onSubmit(() => {
this.doSearch(this.searchText); // 键盘回车 → 记录历史
})
两个事件的分工:
onChange→filterResults()— 实时筛选,每输入一个字都过滤一次onSubmit→doSearch()— 键盘回车,记录到搜索历史
为什么要分开?因为用户在输入过程中不应该触发历史记录——比如输入"鸿"看到结果了,但并不想把这个单字存为搜索词。只有按回车确认才是"真正的搜索"。
筛选逻辑
private filterResults(): void {
const kw = this.searchText.trim().toLowerCase();
if (kw.length === 0) {
this.filteredResults = []; // 空输入 → 回到默认页面
return;
}
this.filteredResults = ALL_DATA.filter((item: SearchItem) =>
item.title.toLowerCase().includes(kw) ||
item.category.toLowerCase().includes(kw)
);
}
toLowerCase() 做大小写不敏感匹配——用户搜"arkui" 能匹配到"ArkUI"。
搜索结果用 ForEach 直接在 List 中渲染,和前面文章的分页列表不同——搜索结果数量有限(最多 15 条),不需要分页。
取消按钮
if (this.searchText.length > 0) {
Text('取消')
.fontSize(FontSize.BODY)
.fontColor(AppColors.PRIMARY)
.margin({ left: Spacing.SM })
.onClick(() => this.cancelSearch())
}
只在有输入时才显示"取消"按钮——不输入时不需要取消。点击取消 → 清空搜索文字 → 清空筛选结果 → 回到历史+热榜的默认页面。

四、交互点2:搜索历史管理
4.1 添加历史
private doSearch(term: string): void {
const t = term.trim();
if (t.length === 0) return;
// 去重:如果已存在,先删除
const idx = this.searchHistory.indexOf(t);
if (idx >= 0) {
this.searchHistory.splice(idx, 1);
}
// 插入到最前面
this.searchHistory = [t].concat(this.searchHistory);
// 最多保留 10 条
if (this.searchHistory.length > 10) {
this.searchHistory = this.searchHistory.slice(0, 10);
}
this.searchText = t; // 把搜索词填入输入框
this.filterResults(); // 立即展示结果
}
四个步骤:
- 去重 — 如果"鸿蒙"已存在于历史中,先删掉旧的(用户刚才输入"鸿蒙"又搜了一次,不需要在历史里显示两次)
- 前置插入 —
[t].concat(this.searchHistory)新搜索词放在第一位 - 裁剪 — 超过 10 条就保留前 10 条,历史不会无限增长
- 同步更新 UI — 把搜索词填入输入框并展示结果
4.2 单条删除
private deleteHistoryItem(term: string): void {
const idx = this.searchHistory.indexOf(term);
if (idx >= 0) {
this.searchHistory.splice(idx, 1);
this.searchHistory = [...this.searchHistory]; // 触发 @State 更新
}
}
每条历史右侧有一个 × 按钮。splice 后 [...this.searchHistory] 展开创建新数组——又回到了待办事项文章讲过的不可变更新模式。
4.3 一键清除
private clearHistory(): void {
promptAction.showDialog({
title: '清除搜索历史',
message: '确定要清除全部搜索历史吗?',
buttons: [
{ text: '取消', color: AppColors.TEXT_TERTIARY },
{ text: '确定', color: AppColors.ERROR }
]
}).then((result) => {
if (result.index === 1) {
this.searchHistory = [];
promptAction.showToast({ message: '已清除全部历史', duration: 1500 });
}
});
}
清除是破坏性操作——需要弹窗确认。这和 ProfilePage 的退出登录确认是同一个模式。
4.4 历史列表 UI
Column() {
ForEach(this.searchHistory, (term: string) => {
Row() {
Image($r('sys.symbol.clock'))
.width(16).height(16)
.fillColor(AppColors.TEXT_DISABLED)
.margin({ right: Spacing.SM })
Text(term)
.fontSize(FontSize.BODY)
.fontColor(AppColors.TEXT_PRIMARY)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('×')
.fontSize(18)
.fontColor(AppColors.TEXT_DISABLED)
.padding({ left: Spacing.SM, right: Spacing.XS })
.onClick(() => this.deleteHistoryItem(term))
}
.width('100%').height(44)
.padding({ left: Spacing.LG, right: Spacing.LG })
.onClick(() => this.doSearch(term))
}, (term: string) => term)
}
整行可以点击(搜索该词),右侧 × 按钮单独处理删除。(term: string) => term 作为 ForEach 的 key——历史中不会有重复词(添加时已去重),所以 term 本身就是唯一的 key。
五、交互点3:热门搜索排行
ForEach(HOT_SEARCHES, (term: string, index: number) => {
Row() {
Text(`${index + 1}`) // 排名数字
.fontSize(FontSize.BODY)
.fontWeight(index < 3 ? FontWeight.Bold : FontWeight.Regular)
.fontColor(index < 3 ? AppColors.ERROR : AppColors.TEXT_TERTIARY)
.width(24)
.textAlign(TextAlign.Center)
Text(term) // 热词
.fontSize(FontSize.BODY)
.fontColor(AppColors.TEXT_PRIMARY)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
if (index < 3) { // 前三名"热"标签
Text('热')
.fontSize(10)
.fontColor(Color.White)
.backgroundColor(AppColors.ERROR)
.borderRadius(BorderRadius.SM)
.padding({ left: Spacing.XS, right: Spacing.XS, top: 1, bottom: 1 })
}
}
.width('100%').height(44)
.padding({ left: Spacing.LG, right: Spacing.LG })
.onClick(() => this.doSearch(term))
}, (term: string) => term)
排名视觉差异化:
- 前 3 名 — 红色粗体数字 + "热"标签,抓眼球
- 4-10 名 — 灰色常规数字,不抢注意力
"热"标签是红色小方块——尺寸 10、圆角 SM、白字红底。和前几篇文章的标签/Tag 保持同一视觉语言。
六、页面状态流转
初始状态(搜索栏为空)
→ 显示搜索历史(如有)+ 热门搜索
→ 点击历史/热词 → 填充搜索栏 → 展示筛选结果 → 记录历史
输入状态(搜索栏有文字)
→ 实时筛选 ALL_DATA → 匹配项展示列表
→ 匹配到 0 条 → 空状态:"未找到相关内容,换个关键词试试吧"
键盘回车
→ 记录搜索词到历史 → 去重 + 前置 + 裁剪
取消按钮
→ 清空搜索文字 → 清空结果 → 回到初始状态
七、代码结构
entry/src/main/ets/
├── common/Constants.ets
├── pages/
│ ├── Index.ets # 入口页(十个按钮)
│ ├── SearchPage.ets # 本篇核心:搜索页面(~260行)
│ └── ... # 前九篇
SearchPage 约 260 行,数据模型(SearchItem)和常量(ALL_DATA、HOT_SEARCHES)均定义在页面文件内。
八、常见面试题 / 踩坑点
8.1 onChange 中做筛选会不会太频繁?
15 条数据的数组 filter 是 O(n) 操作,在 JSCore 中 15 次比较不到 1ms。即使是 1000 条数据,filter 也能在几毫秒内完成。真正的性能瓶颈不在这里,而在 UI 渲染——15 条 ListItem 的重绘比 1000 次字符串比较重得多。
如果数据量大(10000+),可以加节流(debounce 300ms)。但 15 条就完全不需要。
8.2 搜索历史的 key 为什么用 term 自身?
因为去重逻辑保证了历史中不会有重复词。(term: string) => term 是最简 key 生成器,既保证唯一性又零计算开销。
但如果将来允许重复(比如按时间戳区分同词的不同搜索),key 就需要改为 term + timestamp。
8.3 onSubmit 在 ArkUI 中的触发条件?
TextInput.onSubmit 在用户按键盘上的回车/搜索键时触发。如果系统键盘没有显示搜索键(比如用了普通键盘),onSubmit 可能不会触发。这时用户可以通过点击历史词或热门词来触发 doSearch——保留回车的便利,但不依赖回车。
8.4 搜索历史最多 10 条是为什么?
防止无限增长。用户可能每天搜索几十次,如果不清除,几个月后历史列表会非常长——不仅占内存,用户翻很久也找不到想用的历史词。10 条是一个合理的上限,刚好在一屏内展示完。
8.5 "取消"按钮为什么不收起键盘?
ArkUI 的 TextInput 没有直接的 dismissKeyboard() 方法。点击"取消"后清空搜索内容、回到默认页面——键盘可能还开着,但不影响体验(用户看到结果已经变了)。如果一定要收起键盘,需要用到 focusControl.requestFocus(null) 或类似 API,但不同版本支持度不一。
九、运行方式
代码位于 dev/entry/src/main/ets/pages/SearchPage.ets。
用 DevEco Studio 打开 dev/ 项目,首页点击"搜索页面 — 实时筛选与历史记录"即可体验:
- 进入页面 → 搜索栏为空,显示"热门搜索"(10 个热词,前 3 红色)
- 点击任意热词 → 填充搜索栏,展示筛选结果,该词加入历史
- 点击历史中的词 → 直接搜索
- 点击历史右侧 × → 删除单条
- 点击"清除全部" → 弹窗确认 → 清空全部历史
- 手动输入"鸿蒙" → 实时显示匹配结果(标题或分类含"鸿蒙"的条目)
- 输入"xyz" → 显示空状态"未找到相关内容"
- 键盘回车 → 搜索词存入历史最前面
- 多次搜索不同词 → 历史按最近优先排列,超过 10 条自动裁剪
十、扩展方向
- 防抖/节流 — 为
onChange增加setTimeout300ms 防抖,适合数据量大的场景 - 高亮搜索词 — 搜索结果中匹配的文字用不同颜色标记(需要富文本或样式化处理)
- 拼音搜索 — 引入拼音库,输入"hm" 匹配到"鸿蒙"
- 搜索提示/联想 — 输入时同时展示搜索建议下拉(类似 Google 的 autocomplete)
- 远程搜索 — 替换本地 filter 为 HTTP 请求,
onChange中发请求并 abort 上一次未完成的请求 - 搜索历史同步 — 用 Preferences 持久化搜索历史,重启 App 不丢
- 搜索结果分页 — 远程搜索结果多了需要分页,复用 FeedPage 的分页模式
更多推荐




所有评论(0)