uni-app 搜索系统
核心知识点
1. 搜索系统架构
- 前端组件:搜索框、搜索历史、搜索建议、搜索结果
- 后端服务:搜索 API、数据索引、结果排序
- 数据处理:关键词提取、分词处理、模糊匹配
- 性能优化:搜索缓存、防抖处理、预加载
2. 搜索功能实现
- 基础搜索:关键词匹配、精确搜索、模糊搜索
- 高级搜索:多条件筛选、范围搜索、组合搜索
- 实时搜索:输入时实时显示搜索建议
- 历史搜索:记录用户搜索历史、快速重用
3. 关键词处理
- 分词技术:中文分词、英文分词、混合分词
- 关键词提取:从输入中提取有效关键词
- 关键词高亮:在搜索结果中高亮显示关键词
- 同义词处理:支持同义词、近义词搜索
4. 结果排序策略
- 相关性排序:根据关键词匹配程度排序
- 时间排序:按时间先后顺序排序
- 热度排序:按点击率、关注度排序
- 自定义排序:根据业务需求定制排序规则
5. 搜索体验优化
- 搜索建议:输入时提供相关搜索建议
- 搜索历史:记录并展示用户搜索历史
- 无结果处理:提供相关推荐或提示
- 错误处理:处理搜索失败的情况
实用案例
实现应用内搜索功能
1. 搜索框组件
<template>
<view class="search-bar">
<view class="search-input-container">
<uni-icons type="search" size="20" color="#999" class="search-icon" />
<input
v-model="keyword"
type="text"
placeholder="搜索"
placeholder-class="placeholder"
class="search-input"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@confirm="handleSearch"
/>
<uni-icons
v-if="keyword"
type="closeempty"
size="20"
color="#999"
class="clear-icon"
@click="clearInput"
/>
</view>
<text class="cancel-btn" @click="handleCancel">取消</text>
</view>
<!-- 搜索历史 -->
<view v-if="showHistory && !keyword" class="search-history">
<view class="history-header">
<text class="history-title">搜索历史</text>
<uni-icons type="trash" size="20" color="#999" @click="clearHistory" />
</view>
<view class="history-tags">
<view
v-for="(item, index) in searchHistory"
:key="index"
class="history-tag"
@click="selectHistory(item)"
>
{{ item }}
</view>
</view>
</view>
<!-- 搜索建议 -->
<view v-if="showSuggestions && keyword && suggestions.length > 0" class="search-suggestions">
<view
v-for="(item, index) in suggestions"
:key="index"
class="suggestion-item"
@click="selectSuggestion(item)"
>
<uni-icons type="search" size="16" color="#999" class="suggestion-icon" />
<text class="suggestion-text">{{ item }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
keyword: '',
showHistory: false,
showSuggestions: false,
suggestions: [],
searchHistory: [],
inputTimer: null
};
},
onLoad() {
this.loadSearchHistory();
},
methods: {
loadSearchHistory() {
const history = uni.getStorageSync('searchHistory');
if (history) {
this.searchHistory = history;
}
},
saveSearchHistory(keyword) {
if (!keyword) return;
// 去重并限制数量
let history = this.searchHistory.filter(item => item !== keyword);
history.unshift(keyword);
if (history.length > 10) {
history = history.slice(0, 10);
}
this.searchHistory = history;
uni.setStorageSync('searchHistory', history);
},
clearHistory() {
uni.showModal({
title: '确认清除',
content: '确定要清除所有搜索历史吗?',
success: (res) => {
if (res.confirm) {
this.searchHistory = [];
uni.removeStorageSync('searchHistory');
}
}
});
},
handleInput() {
// 防抖处理
clearTimeout(this.inputTimer);
this.inputTimer = setTimeout(() => {
if (this.keyword) {
this.getSuggestions();
this.showSuggestions = true;
} else {
this.showSuggestions = false;
}
}, 300);
},
handleFocus() {
this.showHistory = true;
this.showSuggestions = false;
},
handleBlur() {
// 延迟隐藏,以便点击历史或建议
setTimeout(() => {
this.showHistory = false;
this.showSuggestions = false;
}, 200);
},
handleSearch() {
if (!this.keyword.trim()) return;
this.saveSearchHistory(this.keyword.trim());
this.showHistory = false;
this.showSuggestions = false;
// 触发搜索事件
this.$emit('search', this.keyword.trim());
},
handleCancel() {
this.$emit('cancel');
},
clearInput() {
this.keyword = '';
this.showSuggestions = false;
},
selectHistory(item) {
this.keyword = item;
this.handleSearch();
},
selectSuggestion(item) {
this.keyword = item;
this.handleSearch();
},
getSuggestions() {
// 模拟获取搜索建议
// 实际项目中应该调用后端 API
const allSuggestions = [
'uni-app 开发',
'uni-app 教程',
'uni-app 组件',
'uni-app 性能优化',
'uni-app 跨平台',
'uni-app 插件',
'uni-app 云开发',
'uni-app 小程序'
];
this.suggestions = allSuggestions.filter(item =>
item.toLowerCase().includes(this.keyword.toLowerCase())
);
}
}
};
</script>
<style scoped>
.search-bar {
display: flex;
align-items: center;
padding: 10rpx 20rpx;
background-color: #F5F5F5;
}
.search-input-container {
flex: 1;
display: flex;
align-items: center;
background-color: #FFFFFF;
border-radius: 20rpx;
padding: 0 20rpx;
margin-right: 20rpx;
}
.search-icon {
margin-right: 10rpx;
}
.search-input {
flex: 1;
height: 60rpx;
font-size: 28rpx;
color: #333;
}
.placeholder {
color: #999;
}
.clear-icon {
margin-left: 10rpx;
}
.cancel-btn {
font-size: 28rpx;
color: #007AFF;
}
.search-history {
position: absolute;
top: 80rpx;
left: 0;
right: 0;
background-color: #FFFFFF;
border-top: 1rpx solid #EEEEEE;
padding: 20rpx;
z-index: 999;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.history-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.history-tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.history-tag {
padding: 10rpx 20rpx;
background-color: #F5F5F5;
border-radius: 20rpx;
font-size: 24rpx;
color: #666;
}
.search-suggestions {
position: absolute;
top: 80rpx;
left: 0;
right: 0;
background-color: #FFFFFF;
border-top: 1rpx solid #EEEEEE;
padding: 10rpx 0;
z-index: 999;
}
.suggestion-item {
display: flex;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.suggestion-icon {
margin-right: 15rpx;
}
.suggestion-text {
font-size: 28rpx;
color: #333;
}
</style>2. 搜索结果页面
<template>
<view class="search-result">
<!-- 搜索框 -->
<search-bar
@search="handleSearch"
@cancel="handleCancel"
/>
<!-- 搜索结果 -->
<view v-if="hasSearched" class="result-container">
<!-- 加载中 -->
<view v-if="loading" class="loading-state">
<uni-icons type="spinner" size="30" color="#007AFF" animation="spin" />
<text class="loading-text">搜索中...</text>
</view>
<!-- 有结果 -->
<view v-else-if="results.length > 0" class="result-list">
<view
v-for="(item, index) in results"
:key="index"
class="result-item"
@click="navigateToDetail(item)"
>
<view class="item-content">
<text class="item-title" v-html="highlightKeyword(item.title)"></text>
<text class="item-desc" v-html="highlightKeyword(item.description)"></text>
<view class="item-meta">
<text class="item-category">{{ item.category }}</text>
<text class="item-time">{{ formatDate(item.createdAt) }}</text>
</view>
</view>
<uni-icons type="arrowright" size="20" color="#999" />
</view>
<!-- 加载更多 -->
<view v-if="hasMore" class="load-more" @click="loadMore">
<text>加载更多</text>
</view>
</view>
<!-- 无结果 -->
<view v-else class="empty-state">
<uni-icons type="search" size="60" color="#CCCCCC" />
<text class="empty-text">未找到相关内容</text>
<text class="empty-hint">尝试其他关键词或搜索条件</text>
<view class="hot-search">
<text class="hot-title">热门搜索</text>
<view class="hot-tags">
<view
v-for="(tag, index) in hotSearchTags"
:key="index"
class="hot-tag"
@click="handleHotSearch(tag)"
>
{{ tag }}
</view>
</view>
</view>
</view>
</view>
<!-- 初始状态 -->
<view v-else class="initial-state">
<uni-icons type="search" size="80" color="#CCCCCC" />
<text class="initial-text">请输入关键词搜索</text>
<view class="hot-search">
<text class="hot-title">热门搜索</text>
<view class="hot-tags">
<view
v-for="(tag, index) in hotSearchTags"
:key="index"
class="hot-tag"
@click="handleHotSearch(tag)"
>
{{ tag }}
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import SearchBar from '@/components/search-bar.vue';
export default {
components: {
SearchBar
},
data() {
return {
keyword: '',
results: [],
loading: false,
hasSearched: false,
hasMore: true,
page: 1,
pageSize: 10,
hotSearchTags: [
'uni-app 教程',
'性能优化',
'组件开发',
'跨平台',
'云开发',
'小程序'
]
};
},
methods: {
handleSearch(keyword) {
this.keyword = keyword;
this.page = 1;
this.results = [];
this.hasMore = true;
this.hasSearched = true;
this.loading = true;
this.searchData().finally(() => {
this.loading = false;
});
},
handleCancel() {
uni.navigateBack();
},
handleHotSearch(tag) {
this.handleSearch(tag);
},
searchData() {
// 模拟搜索请求
return new Promise((resolve) => {
setTimeout(() => {
// 模拟搜索结果
const mockResults = [];
for (let i = 0; i < this.pageSize; i++) {
mockResults.push({
id: (this.page - 1) * this.pageSize + i + 1,
title: `${this.keyword} 相关内容 ${(this.page - 1) * this.pageSize + i + 1}`,
description: `这是关于 ${this.keyword} 的详细描述,包含了相关的信息和内容`,
category: '教程',
createdAt: new Date().toISOString()
});
}
if (this.page === 1) {
this.results = mockResults;
} else {
this.results = [...this.results, ...mockResults];
}
// 模拟没有更多数据
if (this.page >= 3) {
this.hasMore = false;
}
resolve();
}, 1000);
});
},
loadMore() {
if (this.loading || !this.hasMore) return;
this.page++;
this.loading = true;
this.searchData().finally(() => {
this.loading = false;
});
},
navigateToDetail(item) {
uni.navigateTo({ url: `/pages/detail?id=${item.id}` });
},
highlightKeyword(text) {
if (!text || !this.keyword) return text;
const regex = new RegExp(`(${this.keyword})`, 'gi');
return text.replace(regex, '<span style="color: #FF6600;">$1</span>');
},
formatDate(dateString) {
const date = new Date(dateString);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
}
}
};
</script>
<style scoped>
.search-result {
min-height: 100vh;
background-color: #F5F5F5;
}
.result-container {
padding: 20rpx;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #666;
}
.result-list {
background-color: #FFFFFF;
border-radius: 10rpx;
overflow: hidden;
}
.result-item {
display: flex;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.item-content {
flex: 1;
}
.item-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 24rpx;
color: #666;
line-height: 1.4;
margin-bottom: 15rpx;
}
.item-meta {
display: flex;
justify-content: space-between;
font-size: 22rpx;
color: #999;
}
.load-more {
text-align: center;
padding: 30rpx 0;
color: #666;
font-size: 28rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
background-color: #FFFFFF;
border-radius: 10rpx;
}
.empty-text {
margin-top: 20rpx;
font-size: 30rpx;
color: #666;
}
.empty-hint {
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
.initial-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 150rpx 0;
}
.initial-text {
margin-top: 30rpx;
font-size: 32rpx;
color: #666;
}
.hot-search {
margin-top: 50rpx;
width: 100%;
padding: 0 40rpx;
}
.hot-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.hot-tags {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
}
.hot-tag {
padding: 15rpx 25rpx;
background-color: #F5F5F5;
border-radius: 25rpx;
font-size: 26rpx;
color: #666;
}
</style>3. 搜索 API 服务
// api/search.js
import request from './request';
export default {
// 基础搜索
async search(keyword, params = {}) {
return request({
url: '/api/search',
method: 'GET',
params: {
keyword,
...params
}
});
},
// 高级搜索
async advancedSearch(params) {
return request({
url: '/api/search/advanced',
method: 'POST',
data: params
});
},
// 获取搜索建议
async getSuggestions(keyword) {
return request({
url: '/api/search/suggestions',
method: 'GET',
params: {
keyword
}
});
},
// 获取热门搜索
async getHotSearch() {
return request({
url: '/api/search/hot',
method: 'GET'
});
},
// 获取搜索历史
async getSearchHistory() {
return request({
url: '/api/search/history',
method: 'GET'
});
},
// 清除搜索历史
async clearSearchHistory() {
return request({
url: '/api/search/history/clear',
method: 'POST'
});
},
// 搜索统计
async trackSearch(keyword, resultCount) {
return request({
url: '/api/search/track',
method: 'POST',
data: {
keyword,
resultCount
}
});
}
};4. 搜索结果高亮组件
<template>
<text v-html="highlightedText"></text>
</template>
<script>
export default {
props: {
text: {
type: String,
default: ''
},
keyword: {
type: String,
default: ''
},
color: {
type: String,
default: '#FF6600'
}
},
computed: {
highlightedText() {
if (!this.text || !this.keyword) return this.text;
const regex = new RegExp(`(${this.keyword})`, 'gi');
return this.text.replace(regex, `<span style="color: ${this.color};">$1</span>`);
}
}
};
</script>实用技巧
1. 搜索性能优化
- 防抖处理:避免频繁输入导致的重复请求
- 缓存策略:缓存搜索结果,减少重复请求
- 预加载:提前加载可能的搜索结果
- 分批加载:使用分页加载,避免一次性加载过多数据
2. 搜索体验提升
- 实时建议:输入时提供相关搜索建议
- 历史记录:记录用户搜索历史,方便快速重用
- 热门搜索:展示热门搜索关键词,引导用户搜索
- 无结果处理:提供相关推荐或搜索建议
3. 关键词处理技巧
- 分词优化:使用专业的分词库提高搜索准确性
- 同义词扩展:支持同义词、近义词搜索
- 关键词提取:从输入中提取有效关键词
- 拼写纠错:自动纠正拼写错误
4. 结果排序策略
- 相关性排序:根据关键词匹配程度排序
- 时间排序:按时间先后顺序排序
- 热度排序:按点击率、关注度排序
- 个性化排序:根据用户偏好定制排序
5. 高级搜索功能
- 多条件筛选:支持按分类、价格、时间等筛选
- 范围搜索:支持价格范围、时间范围等搜索
- 组合搜索:支持多个关键词组合搜索
- 模糊搜索:支持部分匹配、拼音搜索等
总结
通过本教程的学习,你已经掌握了 uni-app 搜索系统的完整实现方法,包括:
搜索系统架构设计:了解了搜索系统的前端组件、后端服务、数据处理等核心组成部分
搜索功能实现:掌握了基础搜索、高级搜索、实时搜索、历史搜索等核心功能的实现
关键词处理:学会了分词技术、关键词提取、关键词高亮、同义词处理等技巧
结果排序策略:掌握了相关性排序、时间排序、热度排序、自定义排序等方法
搜索体验优化:应用了搜索建议、搜索历史、无结果处理、错误处理等提升用户体验的技巧
性能优化策略:实现了防抖处理、搜索缓存、预加载、分批加载等性能优化措施
搜索系统是许多应用的核心功能之一,它不仅可以帮助用户快速找到所需内容,还能提高用户的使用效率和满意度。在实际项目中,你可以根据具体需求对本教程中的实现进行扩展和优化,构建更加完善的搜索系统。
学习目标
- 掌握 uni-app 搜索系统的完整实现方法
- 理解搜索系统的架构设计和数据流程
- 学会使用关键词处理和结果排序技术
- 掌握搜索性能优化和用户体验提升技巧
- 实现实时搜索、历史搜索、搜索建议等功能
- 构建高效、准确、用户友好的搜索系统
通过本教程的学习,你已经具备了开发高质量搜索系统的能力,可以在实际项目中灵活应用这些知识,为用户提供更好的搜索体验。