HarmonyOS7 Search 搜索框要有空态:别把没输入和没结果混在一起
前言
搜索页最容易被写成一个输入框加一个列表。能跑,但体验很粗:用户刚进页面是空白,搜不到也是空白,网络失败可能还是空白。用户看不出来到底发生了什么。
HarmonyOS7 提供了 Search 组件,适合做搜索入口。但真正决定体验的是状态设计。没输入、搜索中、无结果、有结果、失败,这几种状态要分开写。
为什么这个问题经常被写乱

Search 搜索框要有空态 这类内容很容易被写成“代码能跑就算讲完了”,但对初学者来说,这恰恰是最不够的地方。真正让人卡住的,往往不是某个组件名记不住,而是不知道这段代码为什么要这样拆、状态为什么要这样放、以后需求变化时应该从哪里改。
所以这篇文章不只想给你一个能跑的例子,更想把背后的判断过程讲清楚。你只要把这个判断过程吃透,后面自己改页面、补需求、查问题时,心里会稳很多。
搜索页至少有三种基础状态
先不考虑网络请求,纯本地搜索也要处理三个状态。
| 状态 | 触发条件 | 页面应该显示 |
|---|---|---|
| 初始空态 | 关键词为空 | 热门词、历史搜索或推荐内容 |
| 无结果 | 有关键词但列表为空 | 带关键词的无结果提示 |
| 有结果 | 命中数据 | 结果数量和列表 |

我不建议只写“暂无数据”。这句话太模糊,用户不知道是没输入、没查到,还是页面坏了。
先把页面目标想清楚
在真正写代码之前,先别急着盯着 API。更有用的做法是先想清楚:这个页面到底想解决什么问题,用户最在意的反馈是什么,哪些状态必须一直保持一致。
当你先把这条主线想明白,再回头看组件和状态设计,很多选择都会顺理成章。对小白来说,这一步尤其重要,因为它能帮你从“照着抄”慢慢过渡到“看得懂、改得动”。

完整 ArkTS 示例
interface SearchItem {
id: number
title: string
type: string
}
@Entry
@Component
struct SearchEmptyStatePage {
@State keyword: string = ''
@State results: SearchItem[] = []
private allItems: SearchItem[] = [
{ id: 1, title: 'HarmonyOS7 ArkUI 布局', type: '文章' },
{ id: 2, title: 'HTTP 请求封装', type: '代码' },
{ id: 3, title: 'Tabs 页面切换', type: '示例' }
]
private hotWords: string[] = ['ArkUI', 'HTTP', 'Tabs']
private doSearch(value: string): void {
this.keyword = value
const word: string = value.trim().toLowerCase()
if (word.length === 0) {
this.results = []
return
}
this.results = this.allItems.filter((item: SearchItem) => {
return item.title.toLowerCase().includes(word)
})
}
build() {
Column({ space: 14 }) {
Search({ value: this.keyword, placeholder: '搜索文章、示例或接口' })
.onChange((value: string) => this.doSearch(value))
.onSubmit((value: string) => this.doSearch(value))
if (this.keyword.trim().length === 0) {
Column({ space: 10 }) {
Text('热门搜索')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
Row({ space: 8 }) {
ForEach(this.hotWords, (word: string) => {
Text(word)
.fontSize(13)
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.backgroundColor('#F0F2F5')
.borderRadius(16)
.onClick(() => this.doSearch(word))
}, (word: string) => word)
}
}
.width('100%')
} else if (this.results.length === 0) {
Column({ space: 8 }) {
Text('没有找到相关内容')
.fontSize(17)
.fontWeight(FontWeight.Medium)
Text(`换个关键词试试:${this.keyword}`)
.fontSize(13)
.fontColor('#777777')
}
.justifyContent(FlexAlign.Center)
.height(220)
.width('100%')
} else {
Text(`找到 ${this.results.length} 条结果`)
.fontSize(13)
.fontColor('#666666')
.width('100%')
List({ space: 8 }) {
ForEach(this.results, (item: SearchItem) => {
ListItem() {
Column({ space: 5 }) {
Text(item.title)
.fontSize(16)
Text(item.type)
.fontSize(12)
.fontColor('#888888')
}
.alignItems(HorizontalAlign.Start)
.padding(14)
.backgroundColor(Color.White)
.borderRadius(10)
}
}, (item: SearchItem) => item.id.toString())
}
}
}
.padding(16)
.backgroundColor('#F5F7FA')
.height('100%')
}
}
把关键代码一段段拆开
keyword 和 results 分开存。关键词为空时不展示“无结果”,因为用户还没有开始搜索;只有关键词存在且结果为空,才进入无结果态。
doSearch() 同时处理输入变化和提交。规则只有一份,热门词点击、键盘提交、输入变化都走同一个入口。
热门词点击后调用 doSearch(word),不仅能填充结果,也会同步更新搜索框里的 keyword。用户能明确看到自己当前搜的是什么。
接口搜索时怎么扩展
如果搜索来自接口,可以再加两个状态:loading 和 errorText。这两个状态不要和空列表混在一起。
@State loading: boolean = false
@State errorText: string = ''
搜索中显示加载提示,失败时显示错误文案,无结果只留给接口成功但没有命中的情况。状态边界清楚,页面就不会用一个“暂无数据”糊过去。
新手最容易踩的坑
这一类示例最容易让人产生错觉:界面出来了,就以为已经掌握了。其实真正容易出问题的地方,通常都在效果之外,比如状态有没有收拢、失败后怎么兜底、以后要扩展时会不会牵一发动全身。
所以你练这篇内容时,别只看“现在能不能跑”,还要继续看“以后好不好改”。能把这个习惯养起来,你写出来的页面会比单纯照着示例拼出来的页面稳很多。
放进真实项目还要补什么
示例代码的重点是把核心思路讲明白,所以很多工程化细节会故意省掉。真正落到项目里时,你通常还要继续补接口联动、异常处理、边界保护、资源抽离,以及和其他页面状态之间的配合。
比较稳的做法是分三步走:先把结构和职责立住,再把真实业务接进去,最后再优化视觉和交互体验。这样改出来的页面不只是“能演示”,而是真的更接近可以长期维护的业务代码。
写在最后
Search 只是入口,搜索体验靠状态撑起来。初始空态给用户方向,无结果态给用户解释,有结果态给用户继续浏览的路径。把这几种状态拆开,页面会立刻显得专业很多。
更多推荐



所有评论(0)