实现一个优雅的城市选择器组件 - Uniapp实战

发布于:2025-09-11 ⋅ 阅读:(21) ⋅ 点赞:(0)

本文将详细介绍如何使用Uniapp实现一个功能完善的城市选择器组件,包含字母索引、搜索功能和良好的用户体验。
组件概述

这个城市选择器组件主要包含以下功能:

  • 按字母分组显示城市列表
  • 右侧字母索引快速导航
  • 城市搜索功能
  • 平滑滚动和动画效果
  • 触摸交互反馈

核心代码实现

模板结构

<template>
  <view class="city-selector">
    <!-- 触发按钮 -->
    <view class="select-trigger" @click="showSelector">
      <text>{{ selectedCity || '选择城市' }}</text>
      <uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
    </view>
    
    <!-- 城市选择弹窗 -->
    <uni-popup ref="popup" type="bottom" :safe-area="false">
      <view class="city-popup">
        <!-- 搜索框 -->
        <view class="search-box">
          <uni-icons type="search" size="18" color="#999"></uni-icons>
          <input 
            class="search-input" 
            placeholder="搜索城市" 
            v-model="searchText"
            @input="onSearch"
          />
          <text class="cancel-btn" @click="closePopup">取消</text>
        </view>
        
        <!-- 城市列表 -->
        <scroll-view 
          class="city-list" 
          scroll-y 
          :scroll-into-view="scrollToId"
          :scroll-with-animation="true"
        >
          <!-- 搜索结果 -->
          <view v-if="searchText" class="search-result">
            <view 
              v-for="(city, index) in filteredCities" 
              :key="index"
              class="city-item"
              @click="selectCity(city)"
            >
              {{ city }}
            </view>
            <view v-if="filteredCities.length === 0" class="no-result">
              未找到相关城市
            </view>
          </view>
          
          <!-- 按字母分组列表 -->
          <view v-else>
            <view 
              v-for="(group, index) in cityData" 
              :key="group.letter"
              :id="'group-' + group.letter"
              class="city-group"
            >
              <view class="group-title">{{ group.letter }}</view>
              <view 
                v-for="(city, cityIndex) in group.cities" 
                :key="cityIndex"
                class="city-item"
                @click="selectCity(city)"
              >
                {{ city }}
              </view>
            </view>
          </view>
        </scroll-view>
        
        <!-- 字母索引栏 -->
        <view class="index-bar" v-if="!searchText">
          <view 
            v-for="(item, index) in indexList" 
            :key="index"
            class="index-item"
            @touchstart="onIndexTouchStart(item.letter)"
            @touchmove="onIndexTouchMove"
            @touchend="onIndexTouchEnd"
          >
            {{ item.letter }}
          </view>
        </view>
        
        <!-- 当前选中字母提示 -->
        <view class="index-tip" v-if="currentIndexTip">
          {{ currentIndexTip }}
        </view>
      </view>
    </uni-popup>
  </view>
</template>

脚本部分

<script>
export default {
  data() {
    return {
      selectedCity: '',
      searchText: '',
      scrollToId: '',
      currentIndexTip: '',
      indexList: [],
      cityData: [],
      filteredCities: [],
      firstWordList: [
        "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", 
        "N", "P", "Q", "R", "S", "T", "W", "X", "Y", "Z"
      ],
      areaNameList: [
        ["阿拉善盟", "鞍山市", "安庆市", "安阳市", "安顺市", "阿里地区", "安康市", "澳门", "阿拉尔市"],
        ["北京市", "保定市", "包头市", "本溪市", "白山市", "白城市", "蚌埠市", "滨州市", "北海市", "百色市", "巴中市", "保山市", "宝鸡市", "白银市", "北区", "毕节市", "北屯市"],
        // 其他城市数据...
      ]
    };
  },
  mounted() {
    this.processCityData();
  },
  methods: {
    // 处理城市数据
    processCityData() {
      this.cityData = [];
      this.indexList = [];
      
      this.firstWordList.forEach((letter, index) => {
        const cities = this.areaNameList[index] || [];
        if (cities.length > 0) {
          this.cityData.push({
            letter,
            cities
          });
          this.indexList.push({
            letter
          });
        }
      });
    },
    
    // 显示选择器
    showSelector() {
      this.$refs.popup.open();
      this.searchText = '';
      this.filteredCities = [];
    },
    
    // 关闭弹窗
    closePopup() {
      this.$refs.popup.close();
    },
    
    // 搜索城市
    onSearch() {
      if (!this.searchText) {
        this.filteredCities = [];
        return;
      }
      
      const keyword = this.searchText.toLowerCase();
      this.filteredCities = [];
      
      this.cityData.forEach(group => {
        group.cities.forEach(city => {
          if (city.toLowerCase().includes(keyword)) {
            this.filteredCities.push(city);
          }
        });
      });
    },
    
    // 选择城市
    selectCity(city) {
      this.selectedCity = city;
      this.closePopup();
      this.$emit('select', city);
    },
    
    // 字母索引触摸开始
    onIndexTouchStart(letter) {
      this.currentIndexTip = letter;
      this.scrollToId = `group-${letter}`;
    },
    
    // 字母索引触摸移动
    onIndexTouchMove(e) {
      if (!this.indexList.length) return;
      
      const query = uni.createSelectorQuery().in(this);
      query.select('.index-bar').boundingClientRect(data => {
        const barTop = data.top;
        query.select('.index-bar').node(res => {
          const touchY = e.touches[0].clientY;
          const index = Math.floor((touchY - barTop) / (data.height / this.indexList.length));
          
          if (index >= 0 && index < this.indexList.length) {
            const letter = this.indexList[index].letter;
            this.currentIndexTip = letter;
            this.scrollToId = `group-${letter}`;
          }
        }).exec();
      }).exec();
    },
    
    // 字母索引触摸结束
    onIndexTouchEnd() {
      setTimeout(() => {
        this.currentIndexTip = '';
      }, 500);
    }
  }
};
</script>

样式部分

<style lang="scss" scoped>
.city-selector {
  .select-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    background-color: #f5f5f5;
    border-radius: 6px;
    font-size: 14px;
  }
}

.city-popup {
  height: 70vh;
  background-color: #fff;
  border-top-left-radius: 16px;
  border-top-right-radius: 16px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  
  .search-box {
    display: flex;
    align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid #eee;
    
    .search-input {
      flex: 1;
      height: 36px;
      padding: 0 12px;
      margin: 0 10px;
      background-color: #f5f5f5;
      border-radius: 18px;
      font-size: 14px;
    }
    
    .cancel-btn {
      color: #007aff;
      font-size: 14px;
    }
  }
  
  .city-list {
    flex: 1;
    
    .search-result {
      padding: 0 16px;
    }
    
    .city-group {
      .group-title {
        padding: 8px 16px;
        background-color: #f5f5f5;
        color: #666;
        font-size: 14px;
      }
      
      .city-item {
        padding: 12px 16px;
        border-bottom: 1px solid #f0f0f0;
        font-size: 16px;
        
        &:active {
          background-color: #f0f0f0;
        }
      }
    }
    
    .no-result {
      padding: 20px;
      text-align: center;
      color: #999;
    }
  }
  
  .index-bar {
    position: absolute;
    right: 0;
    top: 60px;
    bottom: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 0 8px;
    
    .index-item {
      font-size: 10px;
      color: #007aff;
      text-align: center;
      padding: 1px 0;
    }
  }
  
  .index-tip {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 60px;
    height: 60px;
    background-color: rgba(0, 0, 0, 0.6);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 24px;
    font-weight: bold;
  }
}
</style>

使用说明

  • 在页面中引入组件:
<city-selector @select="onCitySelect"></city-selector>
  • 监听选择事件:
methods: {
  onCitySelect(city) {
    console.log('选择的城市:', city);
    // 处理选择的城市
  }
}

网站公告

今日签到

点亮在社区的每一天
去签到