最近接到一个注册支持各国手机号的需求,需要进行区号的选择,于是我想到了索引列表,但是我们的项目是 uniapp 项目,找了一下,没有现成的组件,一番挠头,经过查资料,最终实现如下组件。
- 再 components 文件夹下创建 citySelect.vue 组件,代码如下:
javascript
<template>
<view>
<uni-popup
ref="popup"
background-color="#fff"
border-radius="20px 20px 0 0"
>
<view style="height: 88vh">
<view class="wrapper">
<view class="header">
<!-- 这里可以换成一个图片 -->
<text class="icon-close" @click="close">X</text>
</view>
<scroll-view
class="calendar-list"
scroll-y="true"
:scroll-into-view="scrollIntoId"
@scroll="onScroll"
>
<!-- 最近访问的城市 -->
<view id="hot">
<view class="hot_wrapper" v-if="Visit.length > 0">
<view class="hot_city hot_city_zuijin">
<view
class="hot_city_one toright"
v-for="(item, index) in Visit"
:key="index"
v-show="index < 2"
@click="back_city(item)"
>
{{ item.cityName }}(+{{ item.code }})
</view>
</view>
</view>
</view>
<!-- 城市列表 -->
<view v-for="(item, index) in list" :id="getId(index)" :key="index">
<view class="letter-header" v-if="item[0]">{{
getId(index)
}}</view>
<view
class="city-div"
v-for="(city, i) in item"
:key="i"
@click="back_city(city)"
>
<text class="city">{{ city.cityName }} (+{{ city.code }})</text>
</view>
</view>
</scroll-view>
<!-- 右侧字母栏 -->
<view
class="letters"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<view
v-for="item in letter"
:key="item"
class="letters-item"
@click.stop="scrollTo(item)"
:class="{ 'letters-item--active': selectedLetter === item }"
>
{{ item }}
</view>
</view>
<!-- 选中之后字母 -->
<view class="mask" v-if="showMask && selectedLetter">
<view class="mask-r">{{ selectedLetter }}</view>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import cityAreaList from "@/components/city";
import pinyin from "pinyin";
export default {
data() {
return {
letter: [],
scrollIntoId: "",
list: [],
showMask: false,
Visit: [], // 最近访问
selectedLetter: "", // 存储当前选中的字母
letterOffsets: [], // 存储每个字母区的偏移量
touchActive: false, // 标记是否正在触摸
};
},
created() {
this.getPhonePrefix();
// 获取存储的最近访问
uni.getStorage({
key: "Visit_key",
success: (res) => {
this.Visit = res.data || [];
},
});
},
mounted() {
// 初始化后计算每个字母区的偏移量
this.$nextTick(() => {
this.calculateLetterOffsets();
});
},
watch: {
list: {
handler(newVal) {
if (newVal && newVal.length > 0) {
this.$nextTick(() => {
this.calculateLetterOffsets();
});
}
},
deep: true,
immediate: true,
},
},
methods: {
getPhonePrefix() {
// 这里可以换成你的获取城市区域的接口
setTimeout(() => {
this.citys = cityAreaList.map((v) => ({
cityName: v.areaName,
code: v.areaCode,
py: this.getFirstLetter(v.areaName),
}));
// 初始化字母列表和城市列表
const mu = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'w', 'x', 'y', 'z'];
this.letter = mu.map((item) => item.toUpperCase());
this.list = Array.from({ length: mu.length }, () => []);
mu.forEach((item, i) => {
this.citys.forEach((city) => {
if (city.py && city.py.substring(0, 1).toLowerCase() === item) {
this.list[i].push(city);
}
});
});
});
},
getFirstLetter(chineseText) {
const pinyinArray = pinyin(chineseText, {
style: pinyin.STYLE_FIRST_LETTER,
});
return pinyinArray.flat().join("").toLowerCase();
},
toggle() {
this.$refs.popup.open("bottom");
},
close() {
this.$refs.popup.close();
},
getId(index) {
return this.letter[index];
},
scrollTo(letterItem) {
this.showMask = true;
this.selectedLetter = letterItem; // 更新选中的字母
setTimeout(() => {
this.showMask = false;
}, 300);
const index = this.letter.findIndex((v) => v == letterItem);
if (!this.list[index]?.length) return;
this.scrollIntoId = letterItem;
},
query(source, text) {
return source.filter((item) => {
return Object.values(item).some((val) =>
String(val).toLowerCase().includes(text.toLowerCase())
);
});
},
back_city(item) {
if (item) {
this.$emit("back_city", item);
this.Visit.unshift(item); // unshift 把数据插入到首位,与 push 相反
const distinctArr = [...new Set(this.Visit)]; // 数组去重
this.Visit = distinctArr;
// 存储最近访问的城市
uni.setStorage({
key: "Visit_key",
data: this.Visit,
});
} else {
this.$emit("back_city", "no");
}
},
// 计算每个字母区的偏移量
calculateLetterOffsets() {
this.letterOffsets = [];
// 获取所有字母区块
this.letter.forEach((letter) => {
const sectionElement = this.$el.querySelector(`#${letter}`);
if (sectionElement && sectionElement.offsetHeight) {
this.letterOffsets.push({
id: letter,
offset: sectionElement.offsetTop,
});
}
});
// 按偏移量排序
this.letterOffsets.sort((a, b) => a.offset - b.offset);
},
// 监听滚动事件
onScroll(event) {
if (this.touchActive) return; // 如果正在触摸,不触发自动高亮
const scrollTop = event.detail.scrollTop;
// 查找当前滚动位置对应的字母
let matchedLetter = null;
for (let i = this.letterOffsets.length - 1; i >= 0; i--) {
if (scrollTop >= this.letterOffsets[i].offset) {
matchedLetter = this.letterOffsets[i].id;
break;
}
}
// 如果没有匹配的字母,则取消高亮
if (matchedLetter) {
if (this.selectedLetter !== matchedLetter) {
this.selectedLetter = matchedLetter;
}
} else {
if (this.selectedLetter !== "") {
this.selectedLetter = "";
}
}
},
// 触摸开始
onTouchStart(event) {
this.touchActive = true;
this.handleTouch(event);
},
// 触摸移动
onTouchMove(event) {
event.preventDefault(); // 阻止默认行为,防止页面滚动
this.handleTouch(event);
},
// 触摸结束
onTouchEnd(event) {
this.touchActive = false;
},
// 处理触摸事件,计算对应的字母
handleTouch(event) {
const touches = event.touches;
if (touches.length > 0) {
const touch = touches[0];
const lettersElement = this.$el.querySelector(".letters");
const lettersRect = lettersElement.getBoundingClientRect();
const relativeY = touch.clientY - lettersRect.top;
const lettersHeight = lettersRect.height;
const totalItems = this.letter.length + 1;
const itemHeight = lettersHeight / totalItems;
const index = Math.floor(relativeY / itemHeight);
if (index >= 0 && index < totalItems) {
const targetLetter = this.letter[index - 1];
this.scrollTo(targetLetter);
}
}
},
},
};
</script>
<style scoped>
.wrapper {
position: fixed;
z-index: 999999;
background: #ffffff;
height: 100%;
width: 100%;
top: 0px;
left: 0px;
border-radius: 20px 20px 0 0;
}
.mask {
position: absolute;
bottom: 0upx;
top: 83upx;
left: 0upx;
right: 0upx;
width: 750upx;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0);
}
.mask-r {
height: 120upx;
width: 120upx;
border-radius: 60upx;
display: flex;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
font-size: 40upx;
color: #ffffff;
}
.header {
height: 85upx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20upx;
box-sizing: border-box;
}
.calendar-list {
position: absolute;
top: 83upx;
bottom: 0upx;
width: 100%;
background-color: #ffffff;
}
.letters {
position: absolute;
right: 30upx;
bottom: 0px;
width: 50upx;
top: 290upx;
color: #2f9bfe;
text-align: center;
font-size: 24upx;
display: flex;
flex-direction: column;
align-items: center;
/* 增加触摸区域 */
height: fit-content;
}
.letters-item {
margin-bottom: 5upx;
cursor: pointer;
transition: background-color 0.2s;
width: 32upx;
height: 32upx;
line-height: 32upx;
text-align: center;
border-radius: 50%;
}
.letters-item--active {
background-color: #2f9bfe;
color: #ffffff;
}
.letter-header {
height: 45upx;
font-size: 22upx;
color: #333333;
padding-left: 24upx;
box-sizing: border-box;
display: flex;
align-items: center;
background-color: #ebedef;
}
.city-div {
width: 660upx;
height: 85upx;
margin-left: 24upx;
border-bottom-width: 0.5upx;
border-bottom-color: #ebedef;
border-bottom-style: solid;
display: flex;
align-items: center;
margin-right: 35upx;
}
.city {
font-size: 28upx;
color: #000000;
padding-left: 30upx;
}
.hot_wrapper {
width: 100%;
padding-top: 10upx;
box-sizing: border-box;
margin-bottom: 26upx;
}
.hot_city {
width: 100%;
height: 60upx;
padding-left: 55upx;
padding-right: 70upx;
box-sizing: border-box;
display: flex;
justify-content: space-between;
}
.hot_city_one {
width: auto;
min-width: 130upx;
padding: 0 8upx;
background-color: #f5f5f5;
border-radius: 10upx;
font-size: 24upx;
color: #333333;
display: flex;
justify-content: center;
align-items: center;
}
.hot_city_zuijin {
display: flex;
justify-content: flex-start;
}
.toright {
margin-right: 25upx;
}
.icon-close {
width: 50upx;
height: 50upx;
position: absolute;
top: 30upx;
right: 5upx;
}
.uni-popup {
z-index: 1000;
}
</style>
- 为了方便演示,我直接再 components 文件夹下放了一个 city.js 文件,用来存放城市区号的 json 数据,如下:
javascript
export default [
{ "areaName": "中国", "areaCode": "86" },
{ "areaName": "日本", "areaCode": "81" },
{ "areaName": "韩国", "areaCode": "82" },
{ "areaName": "朝鲜", "areaCode": "850" },
{ "areaName": "蒙古国", "areaCode": "976" },
{ "areaName": "越南", "areaCode": "84" },
{ "areaName": "老挝", "areaCode": "856" },
{ "areaName": "柬埔寨", "areaCode": "855" },
{ "areaName": "泰国", "areaCode": "66" },
{ "areaName": "缅甸", "areaCode": "95" },
{ "areaName": "马来西亚", "areaCode": "60" },
{ "areaName": "新加坡", "areaCode": "65" },
{ "areaName": "印度尼西亚", "areaCode": "62" },
{ "areaName": "菲律宾", "areaCode": "63" },
{ "areaName": "文莱", "areaCode": "673" },
{ "areaName": "东帝汶", "areaCode": "670" },
{ "areaName": "印度", "areaCode": "91" },
{ "areaName": "巴基斯坦", "areaCode": "92" },
{ "areaName": "孟加拉国", "areaCode": "880" },
{ "areaName": "斯里兰卡", "areaCode": "94" },
{ "areaName": "尼泊尔", "areaCode": "977" },
{ "areaName": "不丹", "areaCode": "975" },
{ "areaName": "马尔代夫", "areaCode": "960" },
{ "areaName": "伊朗", "areaCode": "98" },
{ "areaName": "伊拉克", "areaCode": "964" },
{ "areaName": "叙利亚", "areaCode": "963" },
{ "areaName": "约旦", "areaCode": "962" },
{ "areaName": "沙特阿拉伯", "areaCode": "966" },
{ "areaName": "阿联酋", "areaCode": "971" },
{ "areaName": "卡塔尔", "areaCode": "974" },
{ "areaName": "阿曼", "areaCode": "968" },
{ "areaName": "以色列", "areaCode": "972" },
{ "areaName": "土耳其", "areaCode": "90" },
{ "areaName": "黎巴嫩", "areaCode": "961" },
{ "areaName": "也门", "areaCode": "967" },
{ "areaName": "塞浦路斯", "areaCode": "357" },
{ "areaName": "巴勒斯坦", "areaCode": "970" },
{ "areaName": "哈萨克斯坦", "areaCode": "7" },
{ "areaName": "乌兹别克斯坦", "areaCode": "998" },
{ "areaName": "土库曼斯坦", "areaCode": "993" },
{ "areaName": "吉尔吉斯斯坦", "areaCode": "996" },
{ "areaName": "塔吉克斯坦", "areaCode": "992" },
{ "areaName": "英国", "areaCode": "44" },
{ "areaName": "法国", "areaCode": "33" },
{ "areaName": "德国", "areaCode": "49" },
{ "areaName": "荷兰", "areaCode": "31" },
{ "areaName": "比利时", "areaCode": "32" },
{ "areaName": "瑞士", "areaCode": "41" },
{ "areaName": "奥地利", "areaCode": "43" },
{ "areaName": "爱尔兰", "areaCode": "353" },
{ "areaName": "卢森堡", "areaCode": "352" },
{ "areaName": "挪威", "areaCode": "47" },
{ "areaName": "瑞典", "areaCode": "46" },
{ "areaName": "丹麦", "areaCode": "45" },
{ "areaName": "芬兰", "areaCode": "358" },
{ "areaName": "冰岛", "areaCode": "354" },
{ "areaName": "法罗群岛", "areaCode": "298" },
{ "areaName": "俄罗斯", "areaCode": "7" },
{ "areaName": "波兰", "areaCode": "48" },
{ "areaName": "捷克", "areaCode": "420" },
{ "areaName": "斯洛伐克", "areaCode": "421" },
{ "areaName": "匈牙利", "areaCode": "36" },
{ "areaName": "罗马尼亚", "areaCode": "40" },
{ "areaName": "保加利亚", "areaCode": "359" },
{ "areaName": "希腊", "areaCode": "30" },
{ "areaName": "意大利", "areaCode": "39" },
{ "areaName": "西班牙", "areaCode": "34" },
{ "areaName": "葡萄牙", "areaCode": "351" },
{ "areaName": "克罗地亚", "areaCode": "385" },
{ "areaName": "斯洛文尼亚", "areaCode": "386" },
{ "areaName": "塞尔维亚", "areaCode": "381" },
{ "areaName": "黑山", "areaCode": "382" },
{ "areaName": "阿尔巴尼亚", "areaCode": "355" },
{ "areaName": "马耳他", "areaCode": "356" },
{ "areaName": "圣马力诺", "areaCode": "378" },
{ "areaName": "梵蒂冈", "areaCode": "379" },
{ "areaName": "摩纳哥", "areaCode": "377" },
{ "areaName": "安道尔", "areaCode": "376" },
{ "areaName": "列支敦士登", "areaCode": "423" },
{ "areaName": "波黑", "areaCode": "387" },
{ "areaName": "北马其顿", "areaCode": "389" },
{ "areaName": "摩尔多瓦", "areaCode": "373" },
{ "areaName": "白俄罗斯", "areaCode": "375" },
{ "areaName": "乌克兰", "areaCode": "380" },
{ "areaName": "埃及", "areaCode": "20" },
{ "areaName": "摩洛哥", "areaCode": "212" },
{ "areaName": "阿尔及利亚", "areaCode": "213" },
{ "areaName": "突尼斯", "areaCode": "216" },
{ "areaName": "利比亚", "areaCode": "218" },
{ "areaName": "苏丹", "areaCode": "249" },
{ "areaName": "南苏丹", "areaCode": "211" },
{ "areaName": "尼日利亚", "areaCode": "234" },
{ "areaName": "加纳", "areaCode": "233" },
{ "areaName": "科特迪瓦", "areaCode": "225" },
{ "areaName": "塞内加尔", "areaCode": "221" },
{ "areaName": "喀麦隆", "areaCode": "237" },
{ "areaName": "刚果(布)", "areaCode": "242" },
{ "areaName": "刚果(金)", "areaCode": "243" },
{ "areaName": "肯尼亚", "areaCode": "254" },
{ "areaName": "坦桑尼亚", "areaCode": "255" },
{ "areaName": "埃塞俄比亚", "areaCode": "251" },
{ "areaName": "索马里", "areaCode": "252" },
{ "areaName": "乌干达", "areaCode": "256" },
{ "areaName": "卢旺达", "areaCode": "250" },
{ "areaName": "莫桑比克", "areaCode": "258" },
{ "areaName": "马达加斯加", "areaCode": "261" },
{ "areaName": "南非", "areaCode": "27" },
{ "areaName": "博茨瓦纳", "areaCode": "267" },
{ "areaName": "纳米比亚", "areaCode": "264" },
{ "areaName": "津巴布韦", "areaCode": "263" },
{ "areaName": "赞比亚", "areaCode": "260" },
{ "areaName": "毛里求斯", "areaCode": "230" },
{ "areaName": "塞舌尔", "areaCode": "248" },
{ "areaName": "圣赫勒拿", "areaCode": "290" },
{ "areaName": "厄立特里亚", "areaCode": "291" },
{ "areaName": "吉布提", "areaCode": "253" },
{ "areaName": "乍得", "areaCode": "235" },
{ "areaName": "尼日尔", "areaCode": "227" },
{ "areaName": "布基纳法索", "areaCode": "226" },
{ "areaName": "几内亚", "areaCode": "224" },
{ "areaName": "贝宁", "areaCode": "229" },
{ "areaName": "多哥", "areaCode": "228" },
{ "areaName": "冈比亚", "areaCode": "220" },
{ "areaName": "佛得角", "areaCode": "238" },
{ "areaName": "赤道几内亚", "areaCode": "240" },
{ "areaName": "加蓬", "areaCode": "241" },
{ "areaName": "圣多美和普林西比", "areaCode": "239" },
{ "areaName": "马拉维", "areaCode": "265" },
{ "areaName": "莱索托", "areaCode": "266" },
{ "areaName": "斯威士兰", "areaCode": "268" },
{ "areaName": "科摩罗", "areaCode": "269" },
{ "areaName": "留尼汪", "areaCode": "262" },
{ "areaName": "马约特", "areaCode": "262" },
{ "areaName": "英属印度洋领地", "areaCode": "246" },
{ "areaName": "阿森松岛", "areaCode": "247" },
{ "areaName": "美国", "areaCode": "1" },
{ "areaName": "加拿大", "areaCode": "1" },
{ "areaName": "墨西哥", "areaCode": "52" },
{ "areaName": "古巴", "areaCode": "53" },
{ "areaName": "海地", "areaCode": "509" },
{ "areaName": "牙买加", "areaCode": "1-876" },
{ "areaName": "巴哈马", "areaCode": "1-242" },
{ "areaName": "巴巴多斯", "areaCode": "1-246" },
{ "areaName": "安提瓜和巴布达", "areaCode": "1-268" },
{ "areaName": "格林纳达", "areaCode": "1-473" },
{ "areaName": "圣卢西亚", "areaCode": "1-758" },
{ "areaName": "特立尼达和多巴哥", "areaCode": "1-868" },
{ "areaName": "波多黎各", "areaCode": "1-787/939" },
{ "areaName": "开曼群岛", "areaCode": "1-345" },
{ "areaName": "百慕大", "areaCode": "1-441" },
{ "areaName": "美属维尔京群岛", "areaCode": "1-340" },
{ "areaName": "英属维尔京群岛", "areaCode": "1-284" },
{ "areaName": "蒙特塞拉特", "areaCode": "1-664" },
{ "areaName": "荷属圣马丁", "areaCode": "1-721" },
{ "areaName": "特克斯和凯科斯群岛", "areaCode": "1-649" },
{ "areaName": "圣基茨和尼维斯", "areaCode": "1-869" },
{ "areaName": "圣文森特和格林纳丁斯", "areaCode": "1-784" },
{ "areaName": "多米尼克", "areaCode": "1-767" },
{ "areaName": "安圭拉", "areaCode": "1-264" },
{ "areaName": "多米尼加", "areaCode": "1-809/829/849" },
{ "areaName": "关岛", "areaCode": "1-671" },
{ "areaName": "北马里亚纳群岛", "areaCode": "1-670" },
{ "areaName": "美属萨摩亚", "areaCode": "1-684" },
{ "areaName": "阿根廷", "areaCode": "54" },
{ "areaName": "巴西", "areaCode": "55" },
{ "areaName": "智利", "areaCode": "56" },
{ "areaName": "哥伦比亚", "areaCode": "57" },
{ "areaName": "秘鲁", "areaCode": "51" },
{ "areaName": "委内瑞拉", "areaCode": "58" },
{ "areaName": "玻利维亚", "areaCode": "591" },
{ "areaName": "厄瓜多尔", "areaCode": "593" },
{ "areaName": "巴拉圭", "areaCode": "595" },
{ "areaName": "乌拉圭", "areaCode": "598" },
{ "areaName": "圭亚那", "areaCode": "592" },
{ "areaName": "苏里南", "areaCode": "597" },
{ "areaName": "法属圭亚那", "areaCode": "594" },
{ "areaName": "澳大利亚", "areaCode": "61" },
{ "areaName": "新西兰", "areaCode": "64" },
{ "areaName": "巴布亚新几内亚", "areaCode": "675" },
{ "areaName": "斐济", "areaCode": "679" },
{ "areaName": "所罗门群岛", "areaCode": "677" },
{ "areaName": "瓦努阿图", "areaCode": "678" },
{ "areaName": "汤加", "areaCode": "676" },
{ "areaName": "萨摩亚", "areaCode": "685" },
{ "areaName": "基里巴斯", "areaCode": "686" },
{ "areaName": "密克罗尼西亚联邦", "areaCode": "691" },
{ "areaName": "马绍尔群岛", "areaCode": "692" },
{ "areaName": "瑙鲁", "areaCode": "674" },
{ "areaName": "帕劳", "areaCode": "680" },
{ "areaName": "图瓦卢", "areaCode": "688" },
{ "areaName": "纽埃", "areaCode": "683" },
{ "areaName": "库克群岛", "areaCode": "682" },
{ "areaName": "法属波利尼西亚", "areaCode": "689" },
{ "areaName": "新喀里多尼亚", "areaCode": "687" },
{ "areaName": "托克劳", "areaCode": "690" },
{ "areaName": "皮特凯恩群岛", "areaCode": "64" }
]
- 最后,再页面中进行组件的使用,代码如下:
javascript
<template>
<view>
<view class="nationalAreaCode">
<text @click="$refs.popupRef.toggle('bottom')">
+{{nationalAreaCode}}
<uni-icons type="down" size="30" style="display:inline-block;font-size:28rpx"></uni-icons>
</text>
</view>
<citySelect @back_city="back_city" ref="popupRef"/>
</view>
</template>
<script>
import citySelect from '../../components/citySelect.vue';
export default {
components: { citySelect },
data() {
return {
nationalAreaCode: '86',
}
},
methods: {
back_city(e) {
if (e !== 'no') {
this.nationalAreaCode = e.code
this.$refs.popupRef.close();
} else {
this.$refs.popupRef.close();
}
},
}
}
</script>
<style scoped>
.nationalAreaCode{
padding: 40rpx;
background: #fff;
}
</style>
最终效果如下:
