HarmonyOS开发:商品搜索功能
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对搜索相关能力做了以下更新:
-
TextInput增强:TextInput新增了
searchIcon和searchButton属性,搜索框不需要自己拼凑了。还支持输入时实时回调,不需要手动监听onChange。 -
全文搜索能力:关系型数据库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])
-
搜索建议组件:新增
SearchBar组件,内置搜索建议、历史记录、热词展示能力,不需要自己实现。 -
防抖工具:
@kit.LangKit新增了debounce和throttle工具函数,搜索建议的防抖不需要自己写定时器了。 -
搜索结果高亮:Text组件新增了
highlight属性,搜索结果中的关键词可以高亮显示。
总结
搜索的核心是体验和性能的平衡。响应要快(防抖300ms)、结果要准(服务端排序)、交互要顺(筛选排序即时响应)。
核心记住三点:
- 搜索建议必须防抖,300ms是最佳间隔,太快浪费请求,太慢用户等不及
- 搜索历史最多20条,重复搜索提到最前面,不要无限增长
- 空结果要有引导,"换个关键词试试"比一片空白强100倍
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐ 搜索逻辑不复杂,但防抖、筛选、排序细节多 |
| 使用频率 | ⭐⭐⭐⭐⭐ 电商App50%的流量来自搜索 |
| 重要程度 | ⭐⭐⭐⭐⭐ 搜索体验直接影响转化率 |
搜索结果3秒才出来——这不是bug,这是用户流失。
- 点赞
- 收藏
- 关注作者
评论(0)