实现一个优雅的城市选择器组件 - Uniapp实战
本文将详细介绍如何使用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);// 处理选择的城市}
}