uniapp 模仿企业微信实现选择联系人功能

依赖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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值