uniapp小程序组件scrollview滚动、定位

392 阅读7分钟
<template>
	<view class="u-wrap">
		<view class="u-search-box" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
			<view class="u-search-inner">
				<u-icon name="search" color="#909399" :size="28"></u-icon>
				<text class="u-search-text">搜索</text>
			</view>
		</view>
		<view class="u-menu-wrap">
			<scroll-view scroll-y scroll-with-animation class="u-tab-view menu-scroll-view" :scroll-top="scrollTop"
				:scroll-into-view="itemId">
				<view v-for="(item, index) in tabbar" :key="index" class="u-tab-item"
					:class="[current == index ? 'u-tab-item-active' : '']" @tap.stop="swichMenu(index)">
					<text class="u-line-1">{{ item.name }}</text>
				</view>
			</scroll-view>
			<scroll-view :scroll-top="scrollRightTop" scroll-y scroll-with-animation class="right-box menuHeight"
				@scroll="rightScroll">
				<view class="page-view">
					<view class="class-item menuList" :id="'item' + index" v-for="(item, index) in tabbar" :key="index">
						<view class="item-title">
							<text>{{ item.name }}</text>
						</view>
						<view class="item-container">
							<view v-if="item.name == '价格范围'" class="roomPrice">
								<view class="priceMsg search-item">
									<text :style="{ color: price >= 20 ? '#3DC5FB' : 'black' }">0</text>
									<text :style="{ color: price >= 20 ? '#3DC5FB' : 'black' }">¥100</text>
									<text :style="{ color: price >= 40 ? '#3DC5FB' : 'black' }">¥200</text>
									<text :style="{ color: price >= 60 ? '#3DC5FB' : 'black' }">¥300</text>
									<text :style="{ color: price >= 80 ? '#3DC5FB' : 'black' }">¥400</text>
									<text :style="{ color: price >= 100 ? '#3DC5FB' : 'black' }">¥500+</text>
								</view>
								<u-slider v-model="price" step="20" @change="changePrice"></u-slider>
							</view>
							<view v-if="item.name == '房源户型' && searchObj" class="roomType search-item">
								<view v-for="item in housType" :key="item">
									<view
										:class="{ 'typeList-activ': searchObj.housTypeVal == item.val, typeList: true }"
										@click="choiceHouseType(item.val)">
										{{ item.name }}
									</view>
								</view>
							</view>
							<view v-if="item.name == '出租类型' && searchObj" class="roomType search-item">
								<view :class="{ 'typeList-activ': searchObj.houseType.rentMode == 1, typeList: true }"
									@click="searchObj.houseType.rentMode = 1">
									<text>独享整套</text>
								</view>
								<view :class="{ 'typeList-activ': searchObj.houseType.rentMode == 2, typeList: true }"
									@click="searchObj.houseType.rentMode = 2">
									<text>单间出租</text>
								</view>
							</view>
							<view v-if="item.name == '设施' && searchObj" class="roomType search-item">
								<view
									:class="{ 'typeList-activ': searchObj.otherArr.includes(item.id), typeList: true }"
									v-for="item in facilitiesArr" :key="item.id" @click="appendOtherList(item.id)">
									{{ item.name }}
								</view>
								<view :class="{ 'typeList-activ': searchObj.otherArr.includes(1049), menuList: true }"
									@click="appendOtherList(1049)">
									洗发水/沐浴露
								</view>
							</view>
							<view v-if="item.name == '服务' && searchObj" class="roomType search-item">
								<view v-for="item in serveArr" :key="item.id"
									:class="{ 'typeList-activ': searchObj.otherArr.includes(item.id), menuList: true }"
									@click="appendOtherList(item.id)">
									{{ item.name }}
								</view>
							</view>
							<view v-if="item.name == '用途' && searchObj" class="roomType search-item">
								<view @click="searchRequreArr(item.id)" :class="{
                                        'typeList-activ': addrequeryArr.includes(item.id) || searchObj.addrequeryArr.includes(item.id),
                                        typeList: true,
                                    }" v-for="item in requireArr" :key="item.id">
									{{ item.name }}
								</view>
							</view>
						</view>
					</view>
				</view>
			</scroll-view>
		</view>
		<view class="detail-btn">
			<view>
				<u-button @click="cancellation" :custom-style="{
                        width: '187.5rpx',
                        height: '97.22rpx',
                        borderRadius: '18rpx',
                    }">取消</u-button>
			</view>
			<view>
				<u-button :custom-style="{
                        width: '458.33rpx',
                        height: '97.22rpx',
                        background: '#ff960c',
                        borderRadius: '18rpx',
                        color: '#fff',
                    }" @click="sendInfo">确定</u-button>
			</view>
		</view>
	</view>
</template>
<script setup>
	import {
		ref,
		onMounted,
		nextTick
	} from 'vue';
	import {
		onLoad,
		onShow,
		onReady
	} from '@dcloudio/uni-app';
	// import mallmenu from './mallMenu.vue';
	import {
		getCurrentInstance
	} from 'vue';

	const props = defineProps({
		searchObj: {
			type:Object,
			default:()=>{
				return {
					priceRange:[100],
					housTypeVal:1,
					houseType:{
						rentMode:1
					},
					otherArr:[1035,1047,1049,1029,1041],
					addrequeryArr:[1068,1069]
					
				}
			}
		},
	});

	const menuCompoments = ref(null); // 得到组件本身实例
	const currentInstance = ref(); // 组件实例

	const emit = defineEmits(['closePopup', 'sendSearObj']);

	const scrollTop = ref(0); // 用于控制左侧菜单滚动位置的响应式变量。
	const oldScrollTop = ref(0); //  用于存储右侧内容区上次的滚动位置。
	const current = ref(0); // 表示当前激活的菜单项的索引。
	const menuHeight = ref(0); //左侧菜单的总高度。
	const menuItemHeight = ref(0); //单个菜单项的高度。
	const itemId = ref(''); //用于右侧内容滚动定位的元素 ID。

	const arr = ref([]); //存储每个右侧内容项顶部距离的数组。
	const scrollRightTop = ref(0); //控制右侧内容区滚动位置的响应式变量。
	const timer = ref(null); //用于函数节流的定时器变量。
	const {
		safeAreaInsets
	} = uni.getSystemInfoSync();
	const price = ref(20); // 价格默认值
	const searchObj = ref(); // 搜索条件对象
	const housTypeVal = ref(); // 控制房源户型条件
	const addrequeryArr = ref([1078,1069]); // 需要添加的数组id列表
	const tabbar = ref([{
		name: '价格范围'
	}, {
		name: '房源户型'
	}, {
		name: '出租类型'
	}, {
		name: '设施'
	}, {
		name: '服务'
	}, {
		name: '用途'
	}]);
	// 房源户型
	const housType = ref([{
			name: '1室',
			val: 1
		},
		{
			name: '1室1厅',
			val: 1.1
		},
		{
			name: '2室',
			val: 2
		},
		{
			name: '4室',
			val: 4
		},
		{
			name: '2室1厅',
			val: 2.1
		},
		{
			name: '3室',
			val: 3
		},
	]);
	// 用途数组
	const requireArr = ref([{
			id: 1068,
			name: '接待婴儿'
		},
		{
			id: 1069,
			name: '接待儿童'
		},
		{
			id: 1070,
			name: '接待老人'
		},
		{
			id: 1071,
			name: '接待外宾'
		},
		{
			id: 1072,
			name: '允许吸烟'
		},
		{
			id: 1073,
			name: '携带宠物'
		},
		{
			id: 1074,
			name: '允许做饭'
		},
		{
			id: 1075,
			name: '允许聚会'
		},
		{
			id: 1076,
			name: '商业拍摄'
		},
	]);
	// 设施数组
	const facilitiesArr = ref([{
			id: 1035,
			name: '电梯'
		},
		{
			id: 1047,
			name: '暖气'
		},
		{
			id: 1031,
			name: '电视'
		},
		{
			id: 1036,
			name: '厨房'
		},
		{
			id: 1037,
			name: '书房'
		},
		{
			id: 1038,
			name: '阳台'
		},
		{
			id: 1048,
			name: '浴巾'
		},
		{
			id: 1050,
			name: '电吹风'
		},
	]);
	// 服务数组
	const serveArr = ref([{
			id: 1029,
			name: '无线网络'
		},
		{
			id: 1032,
			name: '独立卫浴'
		},
		{
			id: 1041,
			name: '提供餐饮'
		},
		{
			id: 1039,
			name: '免费停车位'
		},
		{
			id: 1040,
			name: '付费停车位'
		},
		{
			id: 1027,
			name: '可洗热水澡'
		},
		{
			id: 1042,
			name: '支团建会议'
		},
	]);

	onMounted(() => {
		currentInstance.value = getCurrentInstance(); // 获取组件实例
		getMenuItemTop();
		if (props.searchObj) searchObj.value = props.searchObj;
		dateEcho();
	});

	/*
	当用户点击左侧菜单项时调用。它首先确保所有右侧内容项的顶部距离已被计算和存储。然后,如果选中的菜单项与当前激活的菜单项不同,
	它会更新 scrollRightTop 来滚动右侧内容区,使选中的内容项可见,并调用 leftMenuStatus 来更新左侧菜单的状态。
	*/
	const swichMenu = async (index) => {
		if (arr.value.length === 0) {
			await getMenuItemTop();
		}
		if (index === current.value) return;
		scrollRightTop.value = oldScrollTop.value;
		nextTick(() => {
			scrollRightTop.value = arr.value[index];
			current.value = index;

			leftMenuStatus(index);
		});
	};
	/*
	此函数用于获取指定元素的高度。它创建一个选择器查询(createSelectorQuery),
	获取特定类的元素高度,并将这个值保存在指定的响应式变量中。这对于计算滚动位置非常重要。
	*/
	const getElRect = async (elClass, dataVal) => {
		return new Promise((resolve) => {
			const query = uni.createSelectorQuery().in(currentInstance.value);
			query
				.select('.' + elClass)
				.boundingClientRect((rect) => {
					if (!rect) {
						setTimeout(() => {
							getElRect(elClass, dataVal);
						}, 10);
						return;
					}
					if (dataVal == 'menuHeight') menuHeight.value = rect.height;
					if (dataVal == 'menuItemHeight') menuItemHeight.value = rect.height;

					dataVal = rect.height;
					resolve();
				})
				.exec();
		});
	};

	const observer = () => {
		tabbar.value.map((val, index) => {
			let observer = uni.createIntersectionObserver(currentInstance.value);
			// 检测右边scroll-view的id为itemxx的元素与right-box的相交状态
			// 如果跟.right-box底部相交,就动态设置左边栏目的活动状态
			observer
				.relativeTo('.right-box', {
					top: 0,
				})
				.observe('#item' + index, (res) => {
					if (res.intersectionRatio > 0) {
						let id = res.id.substring(4);
						leftMenuStatus(id);
					}
				});
		});
	};

	//根据当前选中的菜单项索引来更新左侧菜单的状态。此方法计算 scrollTop 以确保选中的菜单项在左侧菜单中垂直居中。
	// menuHeight:左侧菜单总高度   menuItemHeight:单个菜单的高度
	const leftMenuStatus = async (index) => {
		current.value = index;
		if (menuHeight.value === 0 || menuItemHeight.value === 0) {
			await getElRect('menu-scroll-view', 'menuHeight');
			await getElRect('u-tab-item', 'menuItemHeight');
		}
		// scrollTop:控制左侧菜单滚动位置
		scrollTop.value = index * menuItemHeight.value + menuItemHeight.value / 2 - menuHeight.value / 2;
	};
	// 获取并存储右侧每个内容项的顶部位置。这对于后续根据滚动位置判断当前激活的菜单项非常关键。
	const getMenuItemTop = () => {
		return new Promise((resolve) => {
			let selectorQuery = uni.createSelectorQuery().in(currentInstance.value);
			setTimeout(() => {
				selectorQuery
					.selectAll('.search-item')
					.boundingClientRect((rects) => {
						if (!rects.length) {
							setTimeout(() => {
								getMenuItemTop();
							}, 10);
							return;
						}
						rects.forEach((rect) => {
							arr.value.push(rect.top - rects[0].top);
							resolve();
						});
					})
					.exec();
			}, 100);
		});
	};
	/*
	当用户滚动右侧内容区时触发。它首先确保所有必要的尺寸信息都已经获取。然后使用一个简单的节流机制
	(通过 timer)来限制事件处理的频率。该方法根据当前滚动位置更新左侧菜单的选中状态。
	*/
	const rightScroll = async (e) => {
		oldScrollTop.value = e.detail.scrollTop;
		if (arr.value.length === 0) {
			await getMenuItemTop();
		}
		if (timer.value) return;
		if (!menuHeight.value) {
			await getElRect('menu-scroll-view', 'menuHeight');
		}
		setTimeout(() => {
			timer.value = null;
			let scrollHeight = e.detail.scrollTop;

			for (let i = 0; i < arr.value.length; i++) {
				let height1 = Math.floor(arr.value[i]);
				let height2 = Math.floor(arr.value[i + 1]);
				if (scrollHeight >= height1 && scrollHeight < height2) {
					leftMenuStatus(i);
					return;
				}
				if (!height2) {
					leftMenuStatus(5);
				}
			}
		}, 10);
	};
	// 用于接受数据进行回显
	const dateEcho = () => {
		price.value = props.searchObj.priceRange[1] / 500;
	};
	// 搜索金额变化
	const changePrice = () => {
		searchObj.value.priceRange = [0, (price.value / 2) * 1000];
		if (price.value === 100) searchObj.value.priceRange = [0, 9999];
	};
	// 点击取消
	const cancellation = () => {
		emit('closePopup', false);
	};
	// 点击确定
	const sendInfo = () => {
		emit('sendSearObj', searchObj.value);
	};
	// 选择房源户型
	const choiceHouseType = (val) => {
		housTypeVal.value = val;
		searchObj.value.houseType.bedRoomCount = +val.toFixed(0);
		searchObj.value.housTypeVal = val;
	};
	// 选择其它数组
	const searchRequreArr = (val) => {
		if (!addrequeryArr.value.includes(val)) {
			addrequeryArr.value.push(val);
		} else {
			addrequeryArr.value = addrequeryArr.value.filter((item) => item !== val);
		}
		searchObj.value.requireArr = addrequeryArr.value.map((item) => ({
			id: String(item),
			checked: '1',
		}));
		searchObj.value.addrequeryArr = addrequeryArr.value;
	};
	// 点击其它按钮,将id存放到数组中
	const appendOtherList = (val) => {
		if (!searchObj.value.otherArr.includes(val)) {
			searchObj.value.otherArr.push(val);
		} else {
			searchObj.value.otherArr = searchObj.value.otherArr.filter((item) => item !== val);
		}
	};
</script>

<style lang="scss" scoped>
	.u-wrap {
		height: calc(100vh);
		/* #ifdef H5 */
		height: calc(100vh - var(--window-top));
		/* #endif */
		display: flex;
		flex-direction: column;
		box-sizing: border-box;
		padding-top: 20rpx;
		background-color: #f6f6f6;
	}

	.u-search-box {
		display: none;
	}

	.u-menu-wrap {
		flex: 1;
		display: flex;
		overflow: hidden;
	}

	.u-search-inner {
		background-color: rgb(234, 234, 234);
		border-radius: 100rpx;
		display: flex;
		align-items: center;
		padding: 10rpx 16rpx;
	}

	.u-search-text {
		font-size: 26rpx;
		color: $u-tips-color;
		margin-left: 10rpx;
	}

	.u-tab-view {
		width: 200rpx;
		height: 100%;
	}

	.u-tab-item {
		height: 110rpx;
		background: #f6f6f6;
		box-sizing: border-box;
		display: flex;
		align-items: center;
		justify-content: center;
		font-size: 26rpx;
		color: #444;
		font-weight: 400;
		line-height: 1;
	}

	.u-tab-item-active {
		position: relative;
		color: #000;
		font-size: 30rpx;
		font-weight: 600;
		background: #fff;
	}

	.u-tab-item-active::before {
		content: '';
		position: absolute;
		border-left: 4px solid $u-primary;
		height: 32rpx;
		left: 0;
		top: 39rpx;
	}

	.u-tab-view {
		height: 100%;
	}

	.right-box {
		background-color: rgb(250, 250, 250);
	}

	.page-view {
		padding: 16rpx;
	}

	.class-item {
		margin-bottom: 30rpx;
		background-color: #fff;
		padding: 16rpx;
		border-radius: 8rpx;
	}

	.class-item:last-child {
		min-height: 100vh;
	}

	.item-title {
		font-size: 26rpx;
		color: $u-main-color;
		font-weight: bold;
		margin-bottom: 20rpx;
	}

	.item-menu-name {
		font-weight: normal;
		font-size: 24rpx;
		color: $u-main-color;
	}

	.item-container {
		display: flex;
		flex-wrap: wrap;

		.roomPrice {
			width: 100%;

			.priceMsg {
				display: flex;
				justify-content: space-between;
				font-size: 25rpx;
				margin-left: 34.72rpx;
			}
		}
	}

	.thumb-box {
		width: 33.333333%;
		display: flex;
		align-items: center;
		justify-content: center;
		flex-direction: column;
		margin-top: 20rpx;
	}

	.item-menu-image {
		width: 120rpx;
		height: 120rpx;
	}

	.roomType {
		display: flex;
		flex-wrap: wrap;

		.typeList {
			width: 152.78rpx;
			height: 69.44rpx;
			margin: 0 20.83rpx 13.89rpx 0;
			display: flex;
			justify-content: center;
			align-items: center;
			background-color: #f0f1f3;
			font-size: 14px;
			font-weight: 300;
		}

		.menuList {
			box-sizing: border-box;
			margin: 0 20.83rpx 13.89rpx 0;
			display: flex;
			justify-content: center;
			align-items: center;
			background-color: #f0f1f3;
			font-size: 14px;
			font-weight: 300;
			padding: 22.92rpx;
		}

		.typeList-activ {
			background-image: url(https://rizuwang-1316974425.cos.ap-beijing.myqcloud.com/mobilePics/home/background.png);
			background-repeat: no-repeat;
			background-position-x: 5px;
			background-size: 22.22rpx 22.22rpx;
			background-position: bottom right;
			background-color: #dcf5ff;
		}
	}

	.detail-btn {
		margin-top: 50rpx;
		margin-bottom: 20rpx;
		display: flex;
		justify-content: space-around;
	}
</style>