本文将详细介绍如何使用Uniapp实现一个功能完善的城市选择器组件,包含字母索引、搜索功能和良好的用户体验。
这个城市选择器组件主要包含以下功能:
- 按字母分组显示城市列表
- 右侧字母索引快速导航
- 城市搜索功能
- 平滑滚动和动画效果
- 触摸交互反馈
核心代码实现
模板结构
xml
<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>
脚本部分
xml
<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>
样式部分
xml
<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>
使用说明
- 在页面中引入组件:
xml
<city-selector @select="onCitySelect"></city-selector>
- 监听选择事件:
xml
methods: {
onCitySelect(city) {
console.log('选择的城市:', city);
// 处理选择的城市
}
}