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 搜索系统的完整实现方法,包括:

  1. 搜索系统架构设计:了解了搜索系统的前端组件、后端服务、数据处理等核心组成部分

  2. 搜索功能实现:掌握了基础搜索、高级搜索、实时搜索、历史搜索等核心功能的实现

  3. 关键词处理:学会了分词技术、关键词提取、关键词高亮、同义词处理等技巧

  4. 结果排序策略:掌握了相关性排序、时间排序、热度排序、自定义排序等方法

  5. 搜索体验优化:应用了搜索建议、搜索历史、无结果处理、错误处理等提升用户体验的技巧

  6. 性能优化策略:实现了防抖处理、搜索缓存、预加载、分批加载等性能优化措施

搜索系统是许多应用的核心功能之一,它不仅可以帮助用户快速找到所需内容,还能提高用户的使用效率和满意度。在实际项目中,你可以根据具体需求对本教程中的实现进行扩展和优化,构建更加完善的搜索系统。

学习目标

  • 掌握 uni-app 搜索系统的完整实现方法
  • 理解搜索系统的架构设计和数据流程
  • 学会使用关键词处理和结果排序技术
  • 掌握搜索性能优化和用户体验提升技巧
  • 实现实时搜索、历史搜索、搜索建议等功能
  • 构建高效、准确、用户友好的搜索系统

通过本教程的学习,你已经具备了开发高质量搜索系统的能力,可以在实际项目中灵活应用这些知识,为用户提供更好的搜索体验。

« 上一篇 uni-app 收藏系统 下一篇 » uni-app 推荐系统