HarmonyOS开发:商品搜索功能

举报
Jack20 发表于 2026/06/26 17:20:21 2026/06/26
【摘要】 HarmonyOS开发:商品搜索功能📌 核心要点:搜索是电商的流量入口,搜索建议与热词提升体验,搜索结果筛选与排序提升转化,搜索历史管理兼顾便利与隐私。 背景与动机用户打开电商App,第一件事干什么?搜索。50%的电商订单来自搜索,搜索做得好不好,直接影响营收。搜索看起来简单——一个输入框、一个搜索按钮、一个结果列表。但真要做起来,坑一个接一个。搜索建议怎么做?用户输入"iPh"就出来"...

HarmonyOS开发:商品搜索功能

📌 核心要点:搜索是电商的流量入口,搜索建议与热词提升体验,搜索结果筛选与排序提升转化,搜索历史管理兼顾便利与隐私。

背景与动机

用户打开电商App,第一件事干什么?搜索。50%的电商订单来自搜索,搜索做得好不好,直接影响营收。

搜索看起来简单——一个输入框、一个搜索按钮、一个结果列表。但真要做起来,坑一个接一个。搜索建议怎么做?用户输入"iPh"就出来"iPhone 15 Pro Max"?热词怎么更新?搜索结果怎么排序?按相关度还是按销量?筛选条件怎么组合?价格区间+品牌+分类,组合查询怎么做?搜索历史怎么管理?存多少条?怎么清?

更别提性能。用户输入一个字就发一次请求?那"iPhone"要发6次请求,服务器扛不住。搜索结果10万条,一次性返回?内存爆了。

搜索的核心是体验和性能的平衡——响应要快、结果要准、交互要顺。

核心原理

搜索的核心流程是输入→建议→搜索→筛选→排序→展示,每一步都有优化空间。

flowchart TD
    A[用户点击搜索框] --> B[展示搜索热词+历史]
    B --> C{用户输入}
    
    C -->|输入文字| D[防抖请求搜索建议]
    D --> E[展示搜索建议列表]
    
    C -->|点击热词/历史| F[直接搜索]
    C -->|点击搜索按钮| G[执行搜索]
    E -->|点击建议项| F
    
    F --> H[请求搜索结果]
    G --> H
    
    H --> I[展示搜索结果]
    I --> J{用户操作}
    
    J -->|筛选条件| K[组合筛选查询]
    J -->|排序切换| L[重新排序]
    J -->|上滑加载| M[加载下一页]
    
    K --> I
    L --> I
    M --> N[追加结果到列表]
    
    classDef input fill:#1565C0,color:#fff,stroke:#0D47A1
    classDef suggest fill:#2E7D32,color:#fff,stroke:#1B5E20
    classDef search fill:#E65100,color:#fff,stroke:#BF360C
    classDef result fill:#6A1B9A,color:#fff,stroke:#4A148C
    
    class A,B,C,input
    class D,E,suggest
    class F,G,H,search
    class I,J,K,L,M,N,result

搜索优化策略

策略 说明
防抖请求 输入停顿300ms后才发请求,避免频繁请求
搜索建议 输入时实时展示建议词,减少输入量
搜索热词 展示热门搜索词,引导用户搜索
搜索历史 记录用户搜索历史,方便重复搜索
分页加载 搜索结果分页返回,每次20条
筛选排序 支持价格区间、品牌、分类、销量等筛选

搜索历史管理

  • 最多保存20条历史
  • 重复搜索的关键词提到最前面
  • 支持单条删除和全部清空
  • 退出登录时清空搜索历史

代码实战

基础用法:搜索页面与搜索建议

先搞定搜索页面的核心:搜索框、热词、历史、建议。

// SearchPage.ets — 搜索页面
import { router } from '@kit.ArkUI'

// 搜索建议项
interface SearchSuggestion {
  keyword: string
  type: 'history' | 'hot' | 'suggest'  // 历史/热词/建议
  hotRank?: number       // 热词排名
}

// 搜索历史管理
class SearchHistoryManager {
  private static instance: SearchHistoryManager
  private history: string[] = []
  private maxCount: number = 20

  private constructor() {
    this.loadFromLocal()
  }

  static getInstance(): SearchHistoryManager {
    if (!SearchHistoryManager.instance) {
      SearchHistoryManager.instance = new SearchHistoryManager()
    }
    return SearchHistoryManager.instance
  }

  // 添加搜索历史
  add(keyword: string): void {
    const trimmed = keyword.trim()
    if (!trimmed) return

    // 去重:如果已存在,移到最前面
    const idx = this.history.indexOf(trimmed)
    if (idx >= 0) {
      this.history.splice(idx, 1)
    }

    // 插入到最前面
    this.history.unshift(trimmed)

    // 超过最大数量,移除最旧的
    if (this.history.length > this.maxCount) {
      this.history = this.history.slice(0, this.maxCount)
    }

    this.saveToLocal()
  }

  // 删除单条
  remove(keyword: string): void {
    const idx = this.history.indexOf(keyword)
    if (idx >= 0) {
      this.history.splice(idx, 1)
      this.saveToLocal()
    }
  }

  // 清空所有
  clear(): void {
    this.history = []
    this.saveToLocal()
  }

  // 获取历史列表
  getAll(): string[] {
    return [...this.history]
  }

  // 本地持久化
  private loadFromLocal(): void {
    // 实际项目:从Preferences读取
    this.history = ['iPhone 15', '运动鞋', '蓝牙耳机', '保温杯']
  }

  private saveToLocal(): void {
    // 实际项目:写入Preferences
  }
}

@Entry
@Component
struct SearchPage {
  @State searchText: string = ''
  @State searchHistory: string[] = []
  @State hotKeywords: string[] = []
  @State suggestions: SearchSuggestion[] = []
  @State isSearching: boolean = false  // 是否在搜索结果页

  private historyManager: SearchHistoryManager = SearchHistoryManager.getInstance()
  private debounceTimer: number = -1

  aboutToAppear() {
    this.searchHistory = this.historyManager.getAll()
    this.loadHotKeywords()
  }

  build() {
    Column() {
      // 搜索栏
      Row() {
        Image($r('app.media.ic_back'))
          .width(24).height(24).fillColor('#333333')
          .onClick(() => {
            if (this.isSearching) {
              this.isSearching = false
              this.suggestions = []
            } else {
              router.back()
            }
          })

        Row() {
          Image($r('app.media.ic_search'))
            .width(16).height(16).fillColor('#999999')

          TextInput({ placeholder: '搜索商品', text: $$this.searchText })
            .layoutWeight(1)
            .height(36)
            .fontSize(15)
            .backgroundColor(Color.Transparent)
            .padding({ left: 8 })
            .onChange((value: string) => {
              this.onSearchInput(value)
            })
            .onSubmit(() => {
              this.doSearch(this.searchText)
            })
        }
        .layoutWeight(1)
        .height(36)
        .padding({ left: 12 })
        .backgroundColor('#F5F5F5')
        .borderRadius(18)
        .margin({ left: 8 })

        if (this.searchText.length > 0) {
          Text('搜索')
            .fontSize(15).fontColor('#1DA1F2')
            .margin({ left: 12 })
            .onClick(() => { this.doSearch(this.searchText) })
        }
      }
      .width('100%')
      .height(48)
      .padding({ left: 12, right: 12 })
      .backgroundColor(Color.White)

      if (this.isSearching) {
        // 搜索结果页
        this.SearchResultSection()
      } else if (this.suggestions.length > 0) {
        // 搜索建议
        this.SuggestionSection()
      } else {
        // 默认:热词+历史
        this.DefaultSection()
      }
    }
    .width('100%').height('100%').backgroundColor(Color.White)
  }

  // ========== 搜索热词+历史 ==========
  @Builder
  DefaultSection() {
    Scroll() {
      Column() {
        // 搜索历史
        if (this.searchHistory.length > 0) {
          Row() {
            Text('搜索历史')
              .fontSize(16).fontWeight(FontWeight.Medium).fontColor('#333333')
            Blank()
            Image($r('app.media.ic_delete'))
              .width(18).height(18).fillColor('#999999')
              .onClick(() => { this.clearHistory() })
          }
          .width('100%').padding({ left: 16, right: 16 })

          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.searchHistory, (keyword: string) => {
              Row() {
                Text(keyword)
                  .fontSize(13).fontColor('#666666')
                Image($r('app.media.ic_close'))
                  .width(12).height(12).fillColor('#CCCCCC')
                  .margin({ left: 4 })
                  .onClick(() => {
                    this.historyManager.remove(keyword)
                    this.searchHistory = this.historyManager.getAll()
                  })
              }
              .padding({ left: 12, right: 8, top: 6, bottom: 6 })
              .backgroundColor('#F5F5F5')
              .borderRadius(16)
              .margin({ right: 8, bottom: 8 })
              .onClick(() => { this.doSearch(keyword) })
            }, (keyword: string, index?: number) => `${keyword}_${index}`)
          }
          .padding({ left: 16, right: 16, top: 8 })
        }

        // 热门搜索
        Column() {
          Text('热门搜索')
            .fontSize(16).fontWeight(FontWeight.Medium).fontColor('#333333')
            .width('100%')
            .padding({ left: 16, right: 16, top: 16 })

          Column() {
            ForEach(this.hotKeywords, (keyword: string, index?: number) => {
              Row() {
                Text(`${(index ?? 0) + 1}`)
                  .fontSize(14).fontWeight(FontWeight.Bold)
                  .fontColor((index ?? 0) < 3 ? '#FF4444' : '#999999')
                  .width(24)

                Text(keyword)
                  .fontSize(14).fontColor('#333333')
                  .layoutWeight(1)

                Image($r('app.media.ic_fire'))
                  .width(14).height(14)
                  .fillColor((index ?? 0) < 3 ? '#FF4444' : 'transparent')
              }
              .width('100%')
              .height(40)
              .padding({ left: 16, right: 16 })
              .onClick(() => { this.doSearch(keyword) })
            }, (keyword: string, index?: number) => `${keyword}_${index}`)
          }
        }
        .margin({ top: 16 })
      }
    }
    .layoutWeight(1)
  }

  // ========== 搜索建议 ==========
  @Builder
  SuggestionSection() {
    List() {
      ForEach(this.suggestions, (item: SearchSuggestion) => {
        ListItem() {
          Row() {
            Image($r('app.media.ic_search'))
              .width(16).height(16).fillColor('#999999')

            Text(item.keyword)
              .fontSize(15).fontColor('#333333')
              .margin({ left: 8 })

            Blank()
          }
          .width('100%')
          .height(44)
          .padding({ left: 16, right: 16 })
          .onClick(() => { this.doSearch(item.keyword) })
        }
      }, (item: SearchSuggestion) => item.keyword)
    }
    .layoutWeight(1)
    .divider({ strokeWidth: 0.5, color: '#F0F0F0' })
  }

  // ========== 搜索结果 ==========
  @Builder
  SearchResultSection() {
    Column() {
      // 筛选排序栏
      Row() {
        Text('综合').fontSize(14).fontColor('#FF4444').layoutWeight(1).textAlign(TextAlign.Center)
        Text('销量').fontSize(14).fontColor('#333333').layoutWeight(1).textAlign(TextAlign.Center)
        Text('价格').fontSize(14).fontColor('#333333').layoutWeight(1).textAlign(TextAlign.Center)
        Text('筛选').fontSize(14).fontColor('#333333').layoutWeight(1).textAlign(TextAlign.Center)
      }
      .width('100%').height(40).backgroundColor(Color.White)
      .border({ width: { bottom: 0.5 }, color: '#F0F0F0' })

      // 搜索结果列表
      Text(`搜索"${this.searchText}"的结果`)
        .fontSize(13).fontColor('#999999').padding(12)

      // 实际项目中用瀑布流展示搜索结果
      Text('搜索结果展示区域')
        .fontSize(14).fontColor('#999999').padding(16)
    }
    .layoutWeight(1)
    .backgroundColor('#F5F5F5')
  }

  // ========== 搜索输入处理(防抖) ==========
  onSearchInput(value: string) {
    this.searchText = value

    // 清除之前的定时器
    if (this.debounceTimer !== -1) {
      clearTimeout(this.debounceTimer)
    }

    if (value.trim().length === 0) {
      this.suggestions = []
      return
    }

    // 300ms防抖
    this.debounceTimer = setTimeout(() => {
      this.loadSuggestions(value)
    }, 300)
  }

  // ========== 执行搜索 ==========
  doSearch(keyword: string) {
    const trimmed = keyword.trim()
    if (!trimmed) return

    this.searchText = trimmed
    this.isSearching = true
    this.suggestions = []

    // 添加搜索历史
    this.historyManager.add(trimmed)
    this.searchHistory = this.historyManager.getAll()

    // 请求搜索结果
    console.info(`[Search] 搜索: ${trimmed}`)
  }

  // ========== 加载搜索建议 ==========
  async loadSuggestions(query: string) {
    // 实际项目:const response = await http.get(`/api/search/suggest?q=${query}`)
    // 模拟建议
    this.suggestions = [
      { keyword: `${query}手机`, type: 'suggest' },
      { keyword: `${query}`, type: 'suggest' },
      { keyword: `${query}充电器`, type: 'suggest' },
      { keyword: `${query}耳机`, type: 'suggest' },
    ]
  }

  // ========== 清空历史 ==========
  clearHistory() {
    this.historyManager.clear()
    this.searchHistory = []
  }

  // ========== 加载热词 ==========
  loadHotKeywords() {
    this.hotKeywords = [
      'iPhone 15 Pro Max',
      '华为Mate 60',
      '羽绒服女',
      '空气炸锅',
      '蓝牙耳机',
      '跑步鞋男',
      '保温杯',
      '面膜',
    ]
  }
}

进阶用法:搜索结果筛选与排序

搜索结果要支持多维筛选和排序:价格区间、品牌、分类、销量、价格排序。

// SearchResultFilter.ets — 搜索结果筛选

// 筛选条件
interface SearchFilter {
  category?: string       // 分类
  brand?: string          // 品牌
  priceMin?: number       // 最低价
  priceMax?: number       // 最高价
  sortBy: 'default' | 'sales' | 'price_asc' | 'price_desc'  // 排序方式
}

// 搜索结果商品
interface SearchResultItem {
  id: string
  title: string
  price: number
  originalPrice: number
  coverUrl: string
  sales: number
  brand: string
  category: string
  tags: string[]
}

@Component
struct SearchResultFilter {
  @Prop keyword: string = ''
  @State results: SearchResultItem[] = []
  @State filter: SearchFilter = { sortBy: 'default' }
  @State currentSortIndex: number = 0
  @State showFilterPanel: boolean = false
  @State isLoading: boolean = false

  // 排序选项
  private sortOptions: string[] = ['综合', '销量', '价格']

  build() {
    Column() {
      // 排序栏
      Row() {
        ForEach(this.sortOptions, (option: string, index?: number) => {
          Column() {
            Text(option)
              .fontSize(14)
              .fontColor(this.currentSortIndex === (index ?? 0) ? '#FF4444' : '#333333')
              .fontWeight(this.currentSortIndex === (index ?? 0) ? FontWeight.Bold : FontWeight.Normal)

            // 价格排序箭头
            if (option === '价格') {
              Row() {
                Text('↑')
                  .fontSize(10)
                  .fontColor(this.filter.sortBy === 'price_asc' ? '#FF4444' : '#CCCCCC')
                Text('↓')
                  .fontSize(10)
                  .fontColor(this.filter.sortBy === 'price_desc' ? '#FF4444' : '#CCCCCC')
              }
            }
          }
          .layoutWeight(1)
          .height(40)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.onSortClick(index ?? 0)
          })
        }, (option: string, index?: number) => `${option}_${index}`)

        // 筛选按钮
        Row() {
          Image($r('app.media.ic_filter'))
            .width(16).height(16).fillColor('#333333')
          Text('筛选')
            .fontSize(14).fontColor('#333333').margin({ left: 4 })
        }
        .width(60)
        .height(40)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showFilterPanel = true
        })
      }
      .width('100%')
      .backgroundColor(Color.White)
      .border({ width: { bottom: 0.5 }, color: '#F0F0F0' })

      // 搜索结果列表
      if (this.results.length > 0) {
        List() {
          ForEach(this.results, (item: SearchResultItem) => {
            ListItem() {
              this.ResultItem(item)
            }
          }, (item: SearchResultItem) => item.id)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        .divider({ strokeWidth: 0.5, color: '#F0F0F0' })
      } else {
        Column() {
          Text('未找到相关商品')
            .fontSize(16).fontColor('#999999')
          Text('换个关键词试试')
            .fontSize(13).fontColor('#CCCCCC').margin({ top: 8 })
        }
        .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
      }
    }
    .bindSheet($$this.showFilterPanel, this.FilterPanel(), {
      height: '60%',
      dragBar: true
    })
  }

  @Builder
  ResultItem(item: SearchResultItem) {
    Row() {
      Image(item.coverUrl)
        .width(100).height(100).borderRadius(4).objectFit(ImageFit.Cover)

      Column() {
        Text(item.title)
          .fontSize(14).fontColor('#333333').maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Row() {
          ForEach(item.tags, (tag: string) => {
            Text(tag).fontSize(10).fontColor('#FF4444')
              .backgroundColor('#FFF0F0').borderRadius(2)
              .padding({ left: 4, right: 4, top: 1, bottom: 1 })
              .margin({ right: 4 })
          }, (tag: string) => tag)
        }
        .margin({ top: 4 })

        Row() {
          Text(`¥${item.price}`).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF4444')
          if (item.originalPrice > item.price) {
            Text(`¥${item.originalPrice}`).fontSize(11).fontColor('#CCCCCC')
              .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 4 })
          }
          Blank()
          Text(`${item.sales}人付款').fontSize(11).fontColor('#999999')
        }
        .width('100%').margin({ top: 8 })
      }
      .layoutWeight(1).margin({ left: 8 }).alignItems(HorizontalAlign.Start)
    }
    .width('100%').padding(12).backgroundColor(Color.White)
  }

  @Builder
  FilterPanel() {
    Column() {
      Text('筛选条件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333')
        .width('100%').padding(16)

      // 价格区间
      Column() {
        Text('价格区间').fontSize(14).fontColor('#333333').margin({ bottom: 8 })
        Row() {
          TextInput({ placeholder: '最低价' })
            .type(InputType.Number).width('40%').height(36).fontSize(14)
            .backgroundColor('#F5F5F5').borderRadius(4)
          Text('—').fontSize(14).fontColor('#999999').margin({ left: 8, right: 8 })
          TextInput({ placeholder: '最高价' })
            .type(InputType.Number).width('40%').height(36).fontSize(14)
            .backgroundColor('#F5F5F5').borderRadius(4)
        }
      }
      .width('100%').padding({ left: 16, right: 16, bottom: 16 })
      .alignItems(HorizontalAlign.Start)

      // 品牌
      Column() {
        Text('品牌').fontSize(14).fontColor('#333333').margin({ bottom: 8 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(['华为', '小米', '苹果', '三星', 'OPPO', 'vivo'], (brand: string) => {
            Text(brand).fontSize(13).fontColor('#333333')
              .backgroundColor(this.filter.brand === brand ? '#FFF0F0' : '#F5F5F5')
              .borderRadius(4).padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => { this.filter.brand = brand })
          }, (brand: string) => brand)
        }
      }
      .width('100%').padding({ left: 16, right: 16, bottom: 16 })
      .alignItems(HorizontalAlign.Start)

      // 底部按钮
      Row() {
        Button('重置').fontSize(14).fontColor('#666666')
          .backgroundColor('#F5F5F5').borderRadius(20)
          .width('40%').height(40)
          .onClick(() => { this.filter = { sortBy: 'default' } })
        Button('确定').fontSize(14).fontColor(Color.White)
          .backgroundColor('#FF4444').borderRadius(20)
          .width('40%').height(40)
          .onClick(() => {
            this.showFilterPanel = false
            this.applyFilter()
          })
      }
      .width('100%').justifyContent(FlexAlign.SpaceEvenly)
      .padding({ top: 16, bottom: 32 })
    }
    .width('100%').height('100%').backgroundColor(Color.White)
  }

  onSortClick(index: number) {
    this.currentSortIndex = index
    switch (index) {
      case 0: this.filter.sortBy = 'default'; break
      case 1: this.filter.sortBy = 'sales'; break
      case 2:
        this.filter.sortBy = this.filter.sortBy === 'price_asc' ? 'price_desc' : 'price_asc'
        break
    }
    this.applyFilter()
  }

  applyFilter() {
    // 根据筛选条件重新请求搜索结果
    console.info(`[SearchFilter] 筛选: ${JSON.stringify(this.filter)}`)
  }
}

完整示例:搜索全流程

把搜索输入、建议、热词、历史、结果、筛选串成完整搜索流程。

// FullSearchPage.ets — 完整搜索页面
import { router } from '@kit.ArkUI'

@Entry
@Component
struct FullSearchPage {
  @State searchText: string = ''
  @State searchHistory: string[] = []
  @State hotKeywords: Array<{ keyword: string; heat: number }> = []
  @State suggestions: string[] = []
  @State searchResults: SearchResultItem[] = []
  @State isSearching: boolean = false
  @State currentSort: string = 'default'
  @State showFilter: boolean = false
  @State hasMore: boolean = true
  @State isLoadingMore: boolean = false

  private historyManager: SearchHistoryManager = SearchHistoryManager.getInstance()
  private debounceTimer: number = -1

  aboutToAppear() {
    this.searchHistory = this.historyManager.getAll()
    this.loadHotKeywords()
  }

  build() {
    Column() {
      // 搜索栏
      Row() {
        Image($r('app.media.ic_back')).width(24).height(24).fillColor('#333333')
          .onClick(() => {
            if (this.isSearching) {
              this.isSearching = false
              this.searchResults = []
              this.suggestions = []
            } else {
              router.back()
            }
          })

        Row() {
          Image($r('app.media.ic_search')).width(16).height(16).fillColor('#999999')
          TextInput({ placeholder: '搜索商品', text: $$this.searchText })
            .layoutWeight(1).height(36).fontSize(15).backgroundColor(Color.Transparent)
            .padding({ left: 8 })
            .onChange((value: string) => { this.onInput(value) })
            .onSubmit(() => { this.search(this.searchText) })
        }
        .layoutWeight(1).height(36).padding({ left: 12 })
        .backgroundColor('#F5F5F5').borderRadius(18).margin({ left: 8 })

        if (this.searchText.length > 0) {
          Image($r('app.media.ic_close')).width(18).height(18).fillColor('#999999')
            .margin({ left: 8 })
            .onClick(() => { this.searchText = ''; this.suggestions = [] })
        }
        Text('搜索').fontSize(15).fontColor('#1DA1F2').margin({ left: 12 })
          .onClick(() => { this.search(this.searchText) })
      }
      .width('100%').height(48).padding({ left: 12, right: 12 }).backgroundColor(Color.White)

      if (this.isSearching) {
        // 搜索结果页
        this.ResultSection()
      } else if (this.suggestions.length > 0) {
        // 搜索建议
        this.SuggestSection()
      } else {
        // 默认页
        this.DefaultSection()
      }
    }
    .width('100%').height('100%').backgroundColor(Color.White)
  }

  @Builder DefaultSection() {
    Scroll() {
      Column() {
        if (this.searchHistory.length > 0) {
          Row() {
            Text('搜索历史').fontSize(16).fontWeight(FontWeight.Medium).fontColor('#333333')
            Blank()
            Image($r('app.media.ic_delete')).width(18).height(18).fillColor('#999999')
              .onClick(() => { this.historyManager.clear(); this.searchHistory = [] })
          }.width('100%').padding({ left: 16, right: 16 })

          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.searchHistory, (kw: string) => {
              Text(kw).fontSize(13).fontColor('#666666')
                .backgroundColor('#F5F5F5').borderRadius(16)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, bottom: 8 })
                .onClick(() => { this.search(kw) })
            }, (kw: string, index?: number) => `${kw}_${index}`)
          }.padding({ left: 16, right: 16, top: 8 })
        }

        Text('热门搜索').fontSize(16).fontWeight(FontWeight.Medium).fontColor('#333333')
          .width('100%').padding({ left: 16, top: 16 })

        Column() {
          ForEach(this.hotKeywords, (item: { keyword: string; heat: number }, index?: number) => {
            Row() {
              Text(`${(index ?? 0) + 1}`).fontSize(14).fontWeight(FontWeight.Bold)
                .fontColor((index ?? 0) < 3 ? '#FF4444' : '#999999').width(24)
              Text(item.keyword).fontSize(14).fontColor('#333333').layoutWeight(1)
            }.width('100%').height(40).padding({ left: 16, right: 16 })
            .onClick(() => { this.search(item.keyword) })
          }, (item: { keyword: string }, index?: number) => `${item.keyword}_${index}`)
        }
      }
    }.layoutWeight(1)
  }

  @Builder SuggestSection() {
    List() {
      ForEach(this.suggestions, (kw: string) => {
        ListItem() {
          Row() {
            Image($r('app.media.ic_search')).width(16).height(16).fillColor('#999999')
            Text(kw).fontSize(15).fontColor('#333333').margin({ left: 8 })
            Blank()
          }.width('100%').height(44).padding({ left: 16, right: 16 })
          .onClick(() => { this.search(kw) })
        }
      }, (kw: string, index?: number) => `${kw}_${index}`)
    }.layoutWeight(1).divider({ strokeWidth: 0.5, color: '#F0F0F0' })
  }

  @Builder ResultSection() {
    Column() {
      // 排序栏
      Row() {
        ForEach(['综合', '销量', '价格'], (opt: string, idx?: number) => {
          Text(opt).fontSize(14)
            .fontColor(this.currentSort === ['default', 'sales', 'price'][idx ?? 0] ? '#FF4444' : '#333333')
            .layoutWeight(1).textAlign(TextAlign.Center).height(40)
            .onClick(() => { this.currentSort = ['default', 'sales', 'price'][idx ?? 0] })
        }, (opt: string, idx?: number) => `${opt}_${idx}`)
        Text('筛选').fontSize(14).fontColor('#333333').layoutWeight(1).textAlign(TextAlign.Center)
          .onClick(() => { this.showFilter = true })
      }.width('100%').backgroundColor(Color.White)

      // 结果列表
      List() {
        ForEach(this.searchResults, (item: SearchResultItem) => {
          ListItem() {
            Row() {
              Image(item.coverUrl).width(100).height(100).borderRadius(4).objectFit(ImageFit.Cover)
              Column() {
                Text(item.title).fontSize(14).fontColor('#333333').maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row() {
                  Text(`¥${item.price}`).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF4444')
                  Blank()
                  Text(`${item.sales}人付款').fontSize(11).fontColor('#999999')
                }.width('100%').margin({ top: 8 })
              }.layoutWeight(1).margin({ left: 8 }).alignItems(HorizontalAlign.Start)
            }.width('100%').padding(12).backgroundColor(Color.White)
          }
        }, (item: SearchResultItem) => item.id)
      }.layoutWeight(1).scrollBar(BarState.Off)
    }.layoutWeight(1).backgroundColor('#F5F5F5')
  }

  onInput(value: string) {
    this.searchText = value
    if (this.debounceTimer !== -1) clearTimeout(this.debounceTimer)
    if (value.trim().length === 0) { this.suggestions = []; return }
    this.debounceTimer = setTimeout(() => {
      this.suggestions = [`${value}手机`, `${value}`, `${value}耳机`, `${value}充电器`]
    }, 300)
  }

  search(keyword: string) {
    this.searchText = keyword.trim()
    if (!this.searchText) return
    this.isSearching = true
    this.suggestions = []
    this.historyManager.add(this.searchText)
    this.searchHistory = this.historyManager.getAll()
    // 模拟搜索结果
    this.searchResults = Array.from({ length: 10 }, (_, i) => ({
      id: `result_${i}`, title: `${this.searchText}相关商品 ${i + 1}`,
      price: Math.round(Math.random() * 500 + 10), originalPrice: Math.round(Math.random() * 800 + 100),
      coverUrl: `https://picsum.photos/200/200?random=${i}`, sales: Math.round(Math.random() * 2000),
      brand: '品牌', category: '分类', tags: []
    }))
  }

  loadHotKeywords() {
    this.hotKeywords = [
      { keyword: 'iPhone 15 Pro Max', heat: 100 },
      { keyword: '华为Mate 60', heat: 95 },
      { keyword: '羽绒服女', heat: 88 },
      { keyword: '空气炸锅', heat: 82 },
      { keyword: '蓝牙耳机', heat: 78 },
    ]
  }
}

踩坑与注意事项

坑1:搜索建议请求太频繁

用户输入"iPhone",5个字母5次请求——服务器压力翻倍。

解决方案:防抖300ms。用户停止输入300ms后才发请求,快速输入时不会触发。

坑2:搜索结果为空时的体验

搜索"asdfgh"没有结果,页面一片空白,用户不知道怎么办。

解决方案:空结果页要有引导——“换个关键词试试”、“查看热门搜索”、“浏览分类”。

坑3:搜索历史越存越多

用户搜索了100次,历史列表100条——太长了,找不到想要的。

解决方案:最多保存20条,重复搜索的关键词提到最前面(不重复添加)。

坑4:筛选条件组合查询

用户选了"华为+1000-2000元+手机分类"三个筛选条件,查询语句怎么写?

解决方案:筛选条件用对象存储,请求时序列化为查询参数。服务端支持多条件AND查询。

坑5:搜索结果排序切换

用户从"综合"切换到"价格",结果列表应该重新请求还是本地排序?

解决方案:综合排序和销量排序需要服务端计算(因为涉及权重),价格排序可以本地排序。但为了一致性,建议所有排序都走服务端。

HarmonyOS 6适配说明

HarmonyOS 6对搜索相关能力做了以下更新:

  1. TextInput增强:TextInput新增了searchIconsearchButton属性,搜索框不需要自己拼凑了。还支持输入时实时回调,不需要手动监听onChange。

  2. 全文搜索能力:关系型数据库RDB新增了FTS5全文搜索扩展。搜索历史和本地商品数据可以用SQL全文搜索,比模糊匹配精准10倍。

// HarmonyOS 6 FTS5全文搜索
// 创建全文搜索虚拟表
rdb.executeSql('CREATE VIRTUAL TABLE products_fts USING fts5(title, category, brand)')

// 全文搜索
const results = rdb.querySql('SELECT * FROM products_fts WHERE products_fts MATCH ?', [keyword])
  1. 搜索建议组件:新增SearchBar组件,内置搜索建议、历史记录、热词展示能力,不需要自己实现。

  2. 防抖工具@kit.LangKit新增了debouncethrottle工具函数,搜索建议的防抖不需要自己写定时器了。

  3. 搜索结果高亮:Text组件新增了highlight属性,搜索结果中的关键词可以高亮显示。

总结

搜索的核心是体验和性能的平衡。响应要快(防抖300ms)、结果要准(服务端排序)、交互要顺(筛选排序即时响应)。

核心记住三点:

  • 搜索建议必须防抖,300ms是最佳间隔,太快浪费请求,太慢用户等不及
  • 搜索历史最多20条,重复搜索提到最前面,不要无限增长
  • 空结果要有引导,"换个关键词试试"比一片空白强100倍
评估维度 说明
学习难度 ⭐⭐⭐ 搜索逻辑不复杂,但防抖、筛选、排序细节多
使用频率 ⭐⭐⭐⭐⭐ 电商App50%的流量来自搜索
重要程度 ⭐⭐⭐⭐⭐ 搜索体验直接影响转化率

搜索结果3秒才出来——这不是bug,这是用户流失。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。