前言

搜索页最容易被写成一个输入框加一个列表。能跑,但体验很粗:用户刚进页面是空白,搜不到也是空白,网络失败可能还是空白。用户看不出来到底发生了什么。

HarmonyOS7 提供了 Search 组件,适合做搜索入口。但真正决定体验的是状态设计。没输入、搜索中、无结果、有结果、失败,这几种状态要分开写。

为什么这个问题经常被写乱

一张适合技术文章的手绘笔记风对比插图,主题是 HarmonyOS7 的 Search 搜索页状态区分

Search 搜索框要有空态 这类内容很容易被写成“代码能跑就算讲完了”,但对初学者来说,这恰恰是最不够的地方。真正让人卡住的,往往不是某个组件名记不住,而是不知道这段代码为什么要这样拆、状态为什么要这样放、以后需求变化时应该从哪里改。

所以这篇文章不只想给你一个能跑的例子,更想把背后的判断过程讲清楚。你只要把这个判断过程吃透,后面自己改页面、补需求、查问题时,心里会稳很多。

搜索页至少有三种基础状态

先不考虑网络请求,纯本地搜索也要处理三个状态。

状态触发条件页面应该显示
初始空态关键词为空热门词、历史搜索或推荐内容
无结果有关键词但列表为空带关键词的无结果提示
有结果命中数据结果数量和列表

一张手绘流程图风格插图,内容是 HarmonyOS7 Search 搜索页的状态判断流程。流程从 S

我不建议只写“暂无数据”。这句话太模糊,用户不知道是没输入、没查到,还是页面坏了。

先把页面目标想清楚

在真正写代码之前,先别急着盯着 API。更有用的做法是先想清楚:这个页面到底想解决什么问题,用户最在意的反馈是什么,哪些状态必须一直保持一致。

当你先把这条主线想明白,再回头看组件和状态设计,很多选择都会顺理成章。对小白来说,这一步尤其重要,因为它能帮你从“照着抄”慢慢过渡到“看得懂、改得动”。

一张手绘系统框架插图,展示 HarmonyOS7 Search 页面在 ArkTS 中的结构拆分。中

完整 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%')
  }
}

把关键代码一段段拆开

keywordresults 分开存。关键词为空时不展示“无结果”,因为用户还没有开始搜索;只有关键词存在且结果为空,才进入无结果态。

doSearch() 同时处理输入变化和提交。规则只有一份,热门词点击、键盘提交、输入变化都走同一个入口。

热门词点击后调用 doSearch(word),不仅能填充结果,也会同步更新搜索框里的 keyword。用户能明确看到自己当前搜的是什么。

接口搜索时怎么扩展

如果搜索来自接口,可以再加两个状态:loadingerrorText。这两个状态不要和空列表混在一起。

@State loading: boolean = false
@State errorText: string = ''

搜索中显示加载提示,失败时显示错误文案,无结果只留给接口成功但没有命中的情况。状态边界清楚,页面就不会用一个“暂无数据”糊过去。

新手最容易踩的坑

这一类示例最容易让人产生错觉:界面出来了,就以为已经掌握了。其实真正容易出问题的地方,通常都在效果之外,比如状态有没有收拢、失败后怎么兜底、以后要扩展时会不会牵一发动全身。

所以你练这篇内容时,别只看“现在能不能跑”,还要继续看“以后好不好改”。能把这个习惯养起来,你写出来的页面会比单纯照着示例拼出来的页面稳很多。

放进真实项目还要补什么

示例代码的重点是把核心思路讲明白,所以很多工程化细节会故意省掉。真正落到项目里时,你通常还要继续补接口联动、异常处理、边界保护、资源抽离,以及和其他页面状态之间的配合。

比较稳的做法是分三步走:先把结构和职责立住,再把真实业务接进去,最后再优化视觉和交互体验。这样改出来的页面不只是“能演示”,而是真的更接近可以长期维护的业务代码。

写在最后

Search 只是入口,搜索体验靠状态撑起来。初始空态给用户方向,无结果态给用户解释,有结果态给用户继续浏览的路径。把这几种状态拆开,页面会立刻显得专业很多。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐