依赖uView组件库 记得引入
功能:
索引显示联系人 自动区分排序
我使用的联系人数据格式下方代码有
选择联系人底部出现头像 点击底部头像取消选择 选择确认返回上一页并返回选择的联系人数据
如需需要更多功能自行修改谢谢
效果图:
测试IOS和安卓端
主要代码构成:
选择联系人页面(是页面不是组件 通过点击跳转页面进入 记得把这个页面的顶部导航栏隐藏)
<!-- 联系人 -->
<template>
<view class="selectCity">
<!-- 自定义顶部 -->
<u-navbar class="navbar" title="选择联系人" leftIcon="" leftText="取消" leftIconColor="#fff" :titleStyle="titleStyle" bgColor="#0BB28D" autoBack placeholder></u-navbar>
<!-- 列表 -->
<scroll-view
class="v_1q8htg"
:style="{ height: `calc(90vh - ${navHeight} + env(safe-area-inset-bottom))` }"
:scroll-y="true"
:scroll-into-view="scrollIntoView"
:enable-back-to-top="true"
:scroll-with-animation="true"
>
<p id="TOP"></p>
<view v-for="(item, key, index) in SortList" :key="index">
<view v-if="item && item.length" class="cityName" :id="key == '#' ? 'special' : key">{
{ key }}</view>
<view class="cityItemContent">
<view class="cityItemContentItem" v-for="(element, e_index) in item" :key="e_index" @click="checkClick(element, e_index, key)">
<view class="choice" :style="{ backgroundColor: element['check'] ? '#2b85e4' : '#fff', border: element['check'] ? 'none' : '1rpx solid #ccc' }">
<u-icon name="checkbox-mark" color="#fff" size="50rpx"></u-icon>
</view>
<view class="cityItemContentItemAvatar">
<image :src="element[avatarFieldName]"></image>
</view>
<view class="cityItemContentItemInfo" :style="isAvatar === true ? 'margin-left:20rpx' : ''">
<view class="cityItemContentItemInfoName">{
{ element[fieldName] }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!--索引-->
<view class="liu-scroll-right">
<image class="liu-scroll-right-top" src="@/static/top.png" @click.stop="scrollIntoView = 'TOP'"></image>
<view
:class="{ 'liu-scroll-right-name': true, 'liu-scroll-right-select': item == scrollIntoView }"
v-for="(item, index) in scrollRightList"
:key="index"
@click.stop="chooseType(item)"
>
{
{ item }}
</view>
</view>
<!-- 底部显示选择人的头像 -->
<view class="selectPeople">
<view class="selectPeople_a">
<scroll-view class="scroll-view_H" scroll-x="true" :scroll-into-view="scrollIndex">
<view :id="'scroll' + (index + 1)" class="scroll-view-item_H" v-for="(item, index) in checkboxValue1" :key="index" @click="deleteContact(item, index)">
<image class="scroll-view-item_H_img" :src="item.img" mode=""></image>
</view>
</scroll-view>
</view>
<view class="selectPeople_b">
<button class="selectPeople_btn" size="mini" type="primary" @click="sendContact">确定{
{ checkboxValue1.length != 0 ? '·' + checkboxValue1.length : '' }}</button>
</view>
</view>
<!-- </view> -->
</view>
</template>
<script>
import { pinyinUtil } from '@/utils/pinyinUtil.js';
export default {
name: 'contacts',
data() {
return {
isAvatar: true, //是否需要头像
titleStyle: {
color: '#fff'
},
fieldName: 'name', //数据列表中的字段名
avatarFieldName: 'img', //头像字段名
SortList: {}, //排序后的数据
scrollIntoView: '', //scroll-view滚动到的位置
clickLetter: 'A', //点击的字母
checkboxValue1: [], //选择的数据
navHeight: '',
checked: [], //
scrollRightList: [],
scrollLeftObj: {},
oldObj: {}, // 处理数据
scrollIndex: '' //设置ID滚动到该元素
};
},
onShow() {
// 获取联系人数据
let dataList = uni.getStorageSync('avatars');
this.cleanData(dataList);
console.log(dataList);
/*
联系人格式:
[
{
"id": 37,
"name": "李飞杰",
"img": "https://..............25180526A005.jpg"
},
{
"id": 37,
"name": "李飞杰",
"img": "https://..............25180526A005.jpg"
},
]
*/
},
mounted() {
this.styleInit();
},
methods: {
styleInit() {
let that = this;
let query = uni.createSelectorQuery().in(this);
query
.select('.navbar')
.boundingClientRect((data) => {
console.log(data);
that.navHeight = data.height + 'px';
})
.exec();
},
// 定义一个函数,用于生成包含26个英文字母和一个'#'的数组
getLetter() {
let list = []; // 初始化一个空数组
for (var i = 0; i < 26; i++) {
// 循环26次,从A到Z
list.push(String.fromCharCode(65 + i)); // 将ASCII码为65(A)加上当前循环的索引值对应的字符(字母)推入list
}
list.push('#'); // 将'#'字符添加到list的末尾
return list; // 返回生成的数组
},
// 定义一个函数,用于清理并分组数据
cleanData(list) {
// 重置滚动右侧的列表,为26个英文字母和一个'#'
this.scrollRightList = this.getLetter();
// 初始化一个空数组,用于存储按首字母分组的数据
let newList = [];
// 遍历传入的数据列表
list.forEach((res) => {
// 获取名字的首字母(或拼音首字母),并去除前后空格
let initial = pinyinUtil.getFirstLetter(res.name.trim());
// 取首字母的第一个字符
let firsfirs = initial ? initial.substring(0, 1) : ''; // 注意:这里可能存在一个拼写错误,应为'first'而不是'firsfirs'
// 如果newList中没有当前首字母对应的数组,则创建一个空数组
if (!newList[firsfirs]) newList[firsfirs] = [];
// 将当前数据添加到对应首字母的数组中
newList[firsfirs].push({
['id']: res['id'] || '', // 使用方括号语法,确保属性名始终被视为字符串(即使它们是变量)
['name']: res['name'] || '',
['img']: res['img'] || '',
['check']: false
});
});
// 遍历滚动右侧的列表(即26个英文字母和一个'#')
this.scrollRightList.forEach((t) => {
// 如果newList中存在当前字母对应的数组,则将该数组添加到scrollLeftObj中
if (newList[t]) {
this.$set(this.scrollLeftObj, t, newList[t]); // 使用Vue的$set方法确保属性是响应式的
} else {
// 如果不存在,则在scrollLeftObj中为该字母创建一个空数组
this.$set(this.scrollLeftObj, t, []);
}
});
// 查找并处理剩余的数据(即首字母不在26个英文字母范围内的数据)
let surplusList = [];
for (var i in newList) {
// 注意:这里使用for...in循环可能不是最佳实践,因为它会遍历对象的所有可枚举属性,包括原型链上的属性
let han = this.scrollRightList.find((v) => {
return v == i; // 查找滚动右侧的列表中是否包含当前首字母
});
if (!han) surplusList.push(newList[i]); // 如果不包含,则将数据添加到surplusList中
}
// 将剩余的数据添加到scrollLeftObj的'#'属性中
surplusList.forEach((item) => {
this.scrollLeftObj['#'] = this.scrollLeftObj['#'].concat(item);
});
// 保存scrollLeftObj的当前状态为oldObj,可能是为了后续对比或恢复使用
this.SortList = JSON.parse(JSON.stringify(this.scrollLeftObj));
// console.log(this.SortList);
},
chooseType(item) {
console.log(item);
this.clickLetter = item;
this.isLetter = true;
setTimeout(() => {
this.isLetter = false;
}, 700);
if (item.firstletter == '#') {
this.scrollIntoView = 'special';
} else {
this.scrollIntoView = item;
}
},
// 选择联系人
checkClick(i, j, z) {
// 使用that来保存当前Vue实例的引用,以便在回调函数中使用
let that = this;
// 多选逻辑开始
// 遍历第z个分类的SortList数组
this.SortList[z].forEach((item, index) => {
// 如果传入的索引j与当前遍历的索引index相等
if (j == index) {
// 切换当前项的选择状态(check属性)
item.check = !item.check;
// 如果当前项被选中(check为true)
if (item.check == true) {
// 将当前项添加到checkboxValue1数组中
that.checkboxValue1.push(item);
} else {
// 如果当前项未被选中,则从checkboxValue1数组中移除
// 注意:这里使用filter方法创建了一个新数组,而不是直接修改原数组
that.checkboxValue1 = that.checkboxValue1.filter((items) => items !== item);
}
}
});
// 延迟等待DOM更新(Vue的$nextTick确保DOM更新完成后执行回调函数)
// 这通常用于确保在DOM更新后再进行DOM操作或获取更新后的DOM状态
this.$nextTick(() => {
that.scrollIndex = 'scroll' + that.checkboxValue1.length;
});
},
deleteContact(contactToDelete) {
// 使用that来保存当前Vue实例的引用,以便在函数内部使用this
let that = this;
that.checkboxValue1 = that.checkboxValue1.filter((item) => item.id !== contactToDelete.id);
// 遍历SortList对象中的所有分类键(例如"A", "B", "C"等)
for (let categoryKey in that.SortList) {
// hasOwnProperty方法用于检查对象自身(不包括原型链)是否具有指定的属性
// 这确保我们不会遍历到SortList对象继承的属性或方法
if (that.SortList.hasOwnProperty(categoryKey)) {
// 使用map方法遍历SortList中当前分类下的所有联系人对象
// map方法会创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
// 这里我们检查每个联系人的id是否与contactToDelete.id相等
that.SortList[categoryKey] = that.SortList[categoryKey].map((item) => {
// 如果当前联系人的id与要删除的联系人的id相同
if (item.id === contactToDelete.id) {
// 设置该联系人的check属性为false,表示该联系人已被取消选中
item.check = false;
}
// 返回当前遍历到的联系人对象,无论是否修改了它的check属性
// 这确保了map方法返回的新数组包含了所有原始元素,但某些元素的check属性可能已被修改
return item;
});
}
}
},
sendContact() {
let pages = getCurrentPages(); // 获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。
let nowPage = pages[pages.length - 1]; //当前页页面实例
let prevPage = pages[pages.length - 2]; //上一页页面实例
prevPage.$vm.otherFun(this.checkboxValue1); // 給上一頁綁定方法otherFun,傳參object
uni.navigateBack({
delta: 1
});
}
}
};
</script>
<style lang="less" scoped>
.selectCity {
width: 100%;
height: 100%;
overflow: hidden;
.v_1q8htg {
position: relative;
background-color: #fff;
// max-height: 100vh;
// padding: 0 30rpx;
// overflow: hidden;
padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS 设备 */
padding-bottom: env(safe-area-inset-bottom); /* 兼容 iPhone X 及以上设备 */
.cityName {
width: 100%;
height: 60rpx;
line-height: 60rpx;
padding: 0 30rpx;
text-align: left;
font-size: 28rpx;
color: #999;
background-color: #f1f3f5;
}
.cityItemContent {
width: 100%;
box-sizing: border-box;
padding: 0 30rpx;
.baseline {
border-bottom: 1px solid #f8f9fa;
}
.cityItemContentItem {
width: 100%;
display: flex;
align-items: center;
height: 120rpx;
text-align: left;
font-size: 28rpx;
color: #999;
background-color: #fff;
.choice {
width: 50rpx;
height: 50rpx;
background-color: #fff;
border-radius: 50%;
border: 1rpx solid #ccc;
margin-right: 20rpx;
}
.cityItemContentItemAvatar {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
margin: 20rpx 0;
background-color: #f1f3f5;
overflow: hidden;
image {
width: 100%;
height: 100%;
// border-radius: 50%;
}
}
.cityItemContentItemInfo {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
.cityItemContentItemInfoName {
font-size: 28rpx;
color: #333;
}
.cityItemContentItemInfoText {
font-size: 24rpx;
color: #999;
}
}
}
}
}
.liu-scroll-right {
position: fixed;
right: 0rpx;
top: 50%;
transform: translateY(-47%);
z-index: 888 !important;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.liu-scroll-right-top {
width: 32rpx;
height: 32rpx;
margin-right: 14rpx;
z-index: 888 !important;
}
.liu-scroll-right-name {
width: 32rpx;
padding-right: 14rpx;
height: 28rpx;
font-size: 22rpx;
color: #333333;
line-height: 22rpx;
margin-top: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.liu-scroll-right-select {
padding: 0;
margin-right: 14rpx;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #2991ff;
color: #ffffff;
}
}
// 底部选择人样式
.selectPeople {
position: fixed;
bottom: 0;
width: 100vw;
height: 10vh;
z-index: 999;
padding: 0 20rpx;
padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS 设备 */
padding-bottom: env(safe-area-inset-bottom); /* 兼容 iPhone X 及以上设备 */
background-color: #f1f3f5;
border-top: 1rpx solid #ccc;
.selectPeople_a {
float: left;
width: 70%;
height: 100%;
.scroll-view_H {
white-space: nowrap;
width: 100%;
height: 100%;
.scroll-view-item_H {
position: relative;
display: inline-block;
width: 80rpx;
height: 100%;
margin: 0 10rpx;
.scroll-view-item_H_img {
position: absolute;
top: 50%;
transform: translate(0, -50%);
width: 80rpx;
height: 80rpx;
background-color: #fff;
border-radius: 10rpx;
}
// line-height: 300rpx;
// text-align: center;
// font-size: 36rpx;
}
}
// overflow: hidden;
}
.selectPeople_b {
float: right;
width: 25%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.selectPeople_btn {
font-weight: 600;
}
}
}
}
</style>
pinyinUtil方法
var dict = {}
//拼音首字母字典文件
const pinyin_dict_firstletter = {
all: "YDYQSXMWZSSXJBYMGCCZQPSSQBYCDSCDQ