uniapp带搜索的下拉框组件封装

343 阅读4分钟

组件使用使用

<template>
  <view class="container">
    <view class="title">带搜索功能的下拉框示例</view>
    
    <view class="dropdown-wrapper">
      <SearchableDropdown
        :options="options"
        :value="selectedValue"
        placeholder="请选择城市"
        searchPlaceholder="搜索城市..."
        @change="handleChange"
        @search="handleSearch"
        @loadMore="handleLoadMore"
      />
    </view>
    
    <view class="result" v-if="selectedValue">
      你选择的城市是:{{ selectedCity }}
    </view>
  </view>
</template>

<script>
import SearchableDropdown from '@/components/SearchableDropdown.vue';

export default {
  components: {
    SearchableDropdown
  },
  data() {
    return {
      selectedValue: null,
      options: [
        { value: 'beijing', label: '北京' },
        { value: 'shanghai', label: '上海' },
        { value: 'guangzhou', label: '广州' },
        { value: 'shenzhen', label: '深圳' },
        { value: 'hangzhou', label: '杭州' },
        { value: 'nanjing', label: '南京' },
        { value: 'chengdu', label: '成都' },
        { value: 'chongqing', label: '重庆' },
        { value: 'wuhan', label: '武汉' },
        { value: 'xi_an', label: '西安' }
      ]
    };
  },
  computed: {
    selectedCity() {
      const city = this.options.find(item => item.value === this.selectedValue);
      return city ? city.label : '';
    }
  },
  methods: {
    handleChange(value) {
      console.log('选中值:', value);
      this.selectedValue = value;
    },
    
    handleSearch(keyword) {
      console.log('搜索关键词:', keyword);
      
      // 这里可以根据关键词进行远程搜索
      // this.fetchOptions(keyword);
    },
    
    handleLoadMore(keyword) {
      console.log('加载更多:', keyword);
      
      // 模拟加载更多数据
      setTimeout(() => {
        const newOptions = [
          { value: 'qingdao', label: '青岛' },
          { value: 'suzhou', label: '苏州' },
          { value: 'xiamen', label: '厦门' }
        ];
        
        // 通知组件加载完成并添加新数据
        this.$refs.dropdown.finishLoadMore(newOptions);
      }, 1000);
    }
  }
};
</script>

<style>
.container {
  padding: 20px;
}

.title {
  font-size: 20px;
  font-weight: bold;
  margin-bottom: 20px;
  text-align: center;
}

.dropdown-wrapper {
  margin-bottom: 20px;
  padding: 0 20px;
}

.result {
  padding: 10px 20px;
  background-color: #f5f7fa;
  border-radius: 4px;
  font-size: 16px;
  color: #333;
}
</style>

如果是使用符合uniapp esaycom 规范的组件, 直接 放到 compontents中即可,会自动引入

<template>
  <view class="container">
    <view class="title">带搜索功能的下拉框示例</view>
    
    <view class="dropdown-wrapper">
      <SearchableDropdown
        :options="options"
        :value="selectedValue"
        placeholder="请选择城市"
        searchPlaceholder="搜索城市..."
        @change="handleChange"
        @search="handleSearch"
        @loadMore="handleLoadMore"
      />
    </view>
    
    <view class="result" v-if="selectedValue">
      你选择的城市是:{{ selectedCity }}
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      selectedValue: null,
      options: [
        { value: 'beijing', label: '北京' },
        { value: 'shanghai', label: '上海' },
        { value: 'guangzhou', label: '广州' },
        { value: 'shenzhen', label: '深圳' },
        { value: 'hangzhou', label: '杭州' },
        { value: 'nanjing', label: '南京' },
        { value: 'chengdu', label: '成都' },
        { value: 'chongqing', label: '重庆' },
        { value: 'wuhan', label: '武汉' },
        { value: 'xi_an', label: '西安' }
      ]
    };
  },
  computed: {
    selectedCity() {
      const city = this.options.find(item => item.value === this.selectedValue);
      return city ? city.label : '';
    }
  },
  methods: {
    handleChange(value) {
      console.log('选中值:', value);
      this.selectedValue = value;
    },
    
    handleSearch(keyword) {
      console.log('搜索关键词:', keyword);
      
      // 这里可以根据关键词进行远程搜索
      // this.fetchOptions(keyword);
    },
    
    handleLoadMore(keyword) {
      console.log('加载更多:', keyword);
      
      // 模拟加载更多数据
      setTimeout(() => {
        const newOptions = [
          { value: 'qingdao', label: '青岛' },
          { value: 'suzhou', label: '苏州' },
          { value: 'xiamen', label: '厦门' }
        ];
        
        // 通知组件加载完成并添加新数据
        this.$refs.dropdown.finishLoadMore(newOptions);
      }, 1000);
    }
  }
};
</script>

<style>
.container {
  padding: 20px;
}

.title {
  font-size: 20px;
  font-weight: bold;
  margin-bottom: 20px;
  text-align: center;
}

.dropdown-wrapper {
  margin-bottom: 20px;
  padding: 0 20px;
}

.result {
  padding: 10px 20px;
  background-color: #f5f7fa;
  border-radius: 4px;
  font-size: 16px;
  color: #333;
}
</style>

组件封装如下

SearchableDropdown.vue

<template>
  <view class="searchable-dropdown">
    <!-- 下拉框头部 -->
    <view class="dropdown-header" @click="toggleDropdown">
      <view class="selected-value">
        {{ selectedLabel || placeholder }}
      </view>
      <view class="arrow-icon">
        <text class="fa" :class="isOpen ? 'fa-chevron-up' : 'fa-chevron-down'"></text>
      </view>
    </view>
    
    <!-- 下拉内容区域 -->
    <view class="dropdown-content" v-show="isOpen">
      <!-- 搜索框 -->
      <view class="search-container">
        <text class="fa fa-search search-icon"></text>
        <input 
          type="text" 
          class="search-input" 
          :placeholder="searchPlaceholder"
          v-model="searchValue"
          @input="handleSearch"
          @confirm="handleSearchConfirm"
          @focus="setInputFocus(true)"
          @blur="setInputFocus(false)"
        />
        <text 
          class="clear-icon fa fa-times-circle" 
          v-show="searchValue && isInputFocused" 
          @click="clearSearch"
        ></text>
      </view>
      
      <!-- 选项列表 -->
      <scroll-view 
        class="options-container" 
        scroll-y 
        :style="{ maxHeight: maxHeight }"
        @scrolltolower="onScrollToLower"
      >
        <view 
          class="option-item" 
          :class="{ 'option-selected': option.value === selectedValue }"
          v-for="option in filteredOptions" 
          :key="option.value"
          @click="selectOption(option)"
        >
          <view class="option-label">{{ option.label }}</view>
          <view class="option-check" v-if="option.value === selectedValue">
            <text class="fa fa-check"></text>
          </view>
        </view>
        
        <!-- 加载更多提示 -->
        <view class="loading-more" v-show="isLoadingMore">
          <text class="fa fa-spinner fa-spin"></text>
          <text>加载更多...</text>
        </view>
        
        <!-- 无结果提示 -->
        <view class="no-result" v-show="!hasOptions">
          <text>{{ noResultText }}</text>
        </view>
      </scroll-view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'SearchableDropdown',
  props: {
    // 选项列表,格式:[{ value: '1', label: '选项1' }]
    options: {
      type: Array,
      default: () => []
    },
    
    // 当前选中的值
    value: {
      type: [String, Number, Object],
      default: null
    },
    
    // 占位文本
    placeholder: {
      type: String,
      default: '请选择'
    },
    
    // 搜索框占位文本
    searchPlaceholder: {
      type: String,
      default: '搜索...'
    },
    
    // 无结果文本
    noResultText: {
      type: String,
      default: '没有找到匹配项'
    },
    
    // 下拉框最大高度
    maxHeight: {
      type: String,
      default: '200px'
    },
    
    // 是否启用加载更多
    enableLoadMore: {
      type: Boolean,
      default: false
    },
    
    // 自定义搜索函数
    customSearch: {
      type: Function,
      default: null
    },
    
    // 是否显示搜索框
    showSearch: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      isOpen: false,           // 下拉框是否打开
      searchValue: '',         // 搜索框值
      filteredOptions: [],     // 过滤后的选项
      isLoadingMore: false,    // 是否正在加载更多
      isInputFocused: false,   // 搜索框是否聚焦
      selectedValue: this.value // 当前选中值的本地副本
    };
  },
  computed: {
    // 当前选中项的标签
    selectedLabel() {
      if (this.selectedValue === null || this.selectedValue === undefined) {
        return '';
      }
      
      const selectedOption = this.options.find(
        option => option.value === this.selectedValue
      );
      
      return selectedOption ? selectedOption.label : '';
    },
    
    // 是否有选项
    hasOptions() {
      return this.filteredOptions.length > 0;
    }
  },
  watch: {
    // 监听外部传入的value变化
    value(newVal) {
      this.selectedValue = newVal;
      this.filterOptions();
    },
    
    // 监听选项列表变化
    options() {
      this.filterOptions();
    }
  },
  created() {
    // 初始化过滤选项
    this.filterOptions();
  },
  methods: {
    // 切换下拉框打开/关闭状态
    toggleDropdown() {
      if (this.isOpen) {
        this.closeDropdown();
      } else {
        this.openDropdown();
      }
    },
    
    // 打开下拉框
    openDropdown() {
      this.isOpen = true;
      this.filterOptions();
      
      // 触发打开事件
      this.$emit('open');
      
      // 添加点击外部关闭下拉框的事件
      setTimeout(() => {
        document.addEventListener('click', this.handleOutsideClick);
      }, 0);
    },
    
    // 关闭下拉框
    closeDropdown() {
      this.isOpen = false;
      this.isInputFocused = false;
      
      // 触发关闭事件
      this.$emit('close');
      
      // 移除点击外部关闭下拉框的事件
      document.removeEventListener('click', this.handleOutsideClick);
    },
    
    // 处理点击外部关闭下拉框
    handleOutsideClick(event) {
      const dropdown = this.$el;
      if (!dropdown.contains(event.target)) {
        this.closeDropdown();
      }
    },
    
    // 过滤选项
    filterOptions() {
      if (this.customSearch && typeof this.customSearch === 'function') {
        // 使用自定义搜索函数
        this.filteredOptions = this.customSearch(this.options, this.searchValue);
      } else {
        // 默认搜索逻辑:根据label模糊匹配
        this.filteredOptions = this.options.filter(option => {
          if (!this.searchValue) return true;
          
          const label = option.label.toString().toLowerCase();
          const search = this.searchValue.toLowerCase();
          
          return label.includes(search);
        });
      }
    },
    
    // 处理搜索输入
    handleSearch() {
      this.filterOptions();
      
      // 触发搜索事件
      this.$emit('search', this.searchValue);
    },
    
    // 处理搜索确认
    handleSearchConfirm() {
      // 触发搜索确认事件
      this.$emit('searchConfirm', this.searchValue);
    },
    
    // 清除搜索内容
    clearSearch() {
      this.searchValue = '';
      this.filterOptions();
    },
    
    // 设置搜索框聚焦状态
    setInputFocus(focused) {
      this.isInputFocused = focused;
    },
    
    // 选择选项
    selectOption(option) {
      this.selectedValue = option.value;
      this.closeDropdown();
      
      // 触发选中事件
      this.$emit('change', option.value, option);
    },
    
    // 滚动到底部加载更多
    onScrollToLower() {
      if (this.enableLoadMore && !this.isLoadingMore) {
        this.isLoadingMore = true;
        
        // 触发加载更多事件
        this.$emit('loadMore', this.searchValue);
      }
    },
    
    // 完成加载更多(由父组件调用)
    finishLoadMore(newOptions = []) {
      // 添加新选项
      this.options = [...this.options, ...newOptions];
      
      // 重新过滤选项
      this.filterOptions();
      
      // 重置加载状态
      this.isLoadingMore = false;
    }
  },
  beforeDestroy() {
    // 确保移除事件监听
    document.removeEventListener('click', this.handleOutsideClick);
  }
};
</script>

<style lang="scss" scoped>
.searchable-dropdown {
  position: relative;
  width: 100%;
  
  .dropdown-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 40px;
    padding: 0 12px;
    border: 1px solid #e5e5e5;
    border-radius: 4px;
    background-color: #fff;
    cursor: pointer;
    
    .selected-value {
      flex: 1;
      font-size: 14px;
      color: #333;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    
    .arrow-icon {
      margin-left: 8px;
      color: #999;
      transition: transform 0.2s ease;
      
      &.fa-chevron-up {
        transform: rotate(180deg);
      }
    }
    
    &:hover {
      border-color: #007AFF;
    }
  }
  
  .dropdown-content {
    position: absolute;
    top: 45px;
    left: 0;
    right: 0;
    z-index: 100;
    border: 1px solid #e5e5e5;
    border-radius: 4px;
    background-color: #fff;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    
    // 下拉动画
    animation: fadeIn 0.2s ease;
    
    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(-10px); }
      to { opacity: 1; transform: translateY(0); }
    }
  }
  
  .search-container {
    display: flex;
    align-items: center;
    height: 40px;
    padding: 0 12px;
    border-bottom: 1px solid #e5e5e5;
    
    .search-icon {
      margin-right: 8px;
      color: #999;
    }
    
    .search-input {
      flex: 1;
      height: 100%;
      font-size: 14px;
      color: #333;
      
      &::placeholder {
        color: #999;
      }
    }
    
    .clear-icon {
      margin-left: 8px;
      color: #999;
      cursor: pointer;
      
      &:hover {
        color: #666;
      }
    }
  }
  
  .options-container {
    max-height: 200px;
    overflow-y: auto;
    
    // 自定义滚动条样式
    ::-webkit-scrollbar {
      width: 4px;
      height: 4px;
    }
    
    ::-webkit-scrollbar-track {
      background: #f1f1f1;
      border-radius: 2px;
    }
    
    ::-webkit-scrollbar-thumb {
      background: #c1c1c1;
      border-radius: 2px;
    }
    
    ::-webkit-scrollbar-thumb:hover {
      background: #a8a8a8;
    }
  }
  
  .option-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 40px;
    padding: 0 12px;
    font-size: 14px;
    color: #333;
    cursor: pointer;
    
    &:hover {
      background-color: #f5f7fa;
    }
    
    .option-selected {
      background-color: #e6f4ff;
      color: #007AFF;
    }
    
    .option-check {
      color: #007AFF;
    }
  }
  
  .loading-more {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 40px;
    font-size: 14px;
    color: #999;
  }
  
  .no-result {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 40px;
    font-size: 14px;
    color: #999;
  }
}
</style>