本文使用 uni-app 实现左侧分类 + 右侧商品列表联动滚动效果,支持:

  • 点击左侧自动定位右侧对应分类
  • 滚动右侧自动切换左侧选中项
  • 动态计算滚动高度,适配不同屏幕
  • 运动装备商城实战演示
  • 双向联动实现

    • 左侧点击:通过 scroll-into-view 定位右侧对应 ID
    • 右侧滚动:监听 scroll 事件,对比 topArr 自动切换左侧下标
  • 动态高度计算

    • 使用 uni.createSelectorQuery() 获取容器真实高度
    • 自动适配不同手机屏幕,避免写死高度
  • 吸顶标题

    • 右侧分类标题使用 position:sticky 实现吸顶效果

以下是代码示例

1.template 结构

<template>
	<view class="container">
		<view class="scroll-panel" id="scroll-panel">
			<view class="list-box">
				<!-- 左侧分类 -->
				<view class="left">
					<scroll-view scroll-y :style="{ height: scrollHeight + 'px' }"
						:scroll-into-view="leftIntoView">
						<view class="item" v-for="(item, index) in leftArray" :key="index"
							:class="{ active: index == leftIndex }" 
							:id="'left-' + index" 
							:data-index="index"
							@tap="leftTap">
							<view class="activelink"></view>
							<text class="item-name">{{ item }}</text>
						</view>
					</scroll-view>
				</view>

				<!-- 右侧商品列表 -->
				<view class="main">
					<scroll-view scroll-y 
						:style="{ height: scrollHeight + 'px' }" 
						@scroll="mainScroll"
						:scroll-into-view="scrollInto" 
						scroll-with-animation>
						
						<view class="item main-item" v-for="(item, index) in mainArray" :key="index"
							:id="'item-' + index">
							<view class="title">
								<view>{{ item.title }}</view>
							</view>
							<view class="goods" v-for="(item2, index2) in item.list" :key="index2">
								<view class="orderlist-list">
									<view class="list-left">
										<image :src="item2.imgUrl" mode="aspectFill"></image>
									</view>
									<view class="list-right">
										<view class="list-name">{{item2.name}}</view>
										<view style="display: flex;" v-if="item2.calorie">
											<view class="list-meuns">{{item2.calorie}}</view>
											<view style="margin-left: 10rpx;">千卡/小时</view>
										</view>
										<view class="price" v-if="item2.price">¥{{item2.price}}</view>
									</view>
								</view>
							</view>
						</view>
						<view class="fill-last" :style="{ height: fillHeight + 'px' }"></view>
					</scroll-view>
				</view>
			</view>
		</view>
	</view>
</template>

2. script 逻辑

<script>
	export default {
		data() {
			return {
				scrollHeight: 400,
				scrollTopSize: 0,
				fillHeight: 0, // 填充高度,用于最后一项低于滚动区域时使用
				leftArray: [], // 左侧分类数组
				mainArray: [], // 右侧商品数组
				topArr: [],
				leftIndex: 0,
				scrollInto: '',
				apiData: [{
				    category: 1,
				    typeName: "有氧运动",
				    commVos: [{
				        commId: 1001,
				        name: "专业瑜伽垫防滑减震",
				        calorie: 200,
				        imgUrl: "", 
				        price: 89,
				        type: null
				    }, {
				        commId: 1002,
				        name: "智能计数跳绳中考专用",
				        calorie: 400,
				        imgUrl: "",
				        price: 59,
				        type: null
				    }, {
				        commId: 1003,
				        name: "静音无绳跳绳减肥专用",
				        calorie: 380,
				        imgUrl: "",
				        price: 39,
				        type: null
				    }]
				}, {
				    category: 2,
				    typeName: "力量训练",
				    commVos: [{
				        commId: 2001,
				        name: "可调节哑铃男士家用",
				        calorie: 500,
				        imgUrl: "",
				        price: 299,
				        type: null
				    },
				    {
				        commId: 2002,
				        name: "弹力带健身阻力带",
				        calorie: 350,
				        imgUrl: "",
				        price: 49,
				        type: null
				    },
				    {
				        commId: 2003,
				        name: "俯卧撑训练板多功能",
				        calorie: 450,
				        imgUrl: "",
				        price: 89,
				        type: null
				    }]
				}, {
				    category: 3,
				    typeName: "户外装备",
				    commVos: [{
				        commId: 3001,
				        name: "专业登山杖超轻伸缩",
				        calorie: 300,
				        imgUrl: "",
				        price: 129,
				        type: null
				    },{
				        commId: 3002,
				        name: "速干运动T恤透气",
				        calorie: null,
				        imgUrl: "",
				        price: 79,
				        type: null
				    },{
				        commId: 3003,
				        name: "运动水壶大容量1.5L",
				        calorie: null,
				        imgUrl: "",
				        price: 69,
				        type: null
				    }]
				}, {
				    category: 4,
				    typeName: "护具配件",
				    commVos: [{
				        commId: 4001,
				        name: "运动护膝专业跑步篮球",
				        calorie: null,
				        imgUrl: "",
				        price: 59,
				        type: null
				    },{
				        commId: 4002,
				        name: "运动护腕吸汗透气",
				        calorie: null,
				        imgUrl: "",
				        price: 29,
				        type: null
				    },{
				        commId: 4003,
				        name: "运动头带止汗导汗",
				        calorie: null,
				        imgUrl: "",
				        price: 19,
				        type: null
				    }]
				}]
			};
		},
		computed: {
			/* 计算左侧滚动位置定位 */
			leftIntoView() {
				return `left-${this.leftIndex > 3 ? this.leftIndex - 3 : 0}`;
			}
		},
		mounted() {
			this.$nextTick(() => {
				setTimeout(() => {
					this.initScrollView().then(() => {
						this.getListData();
					});
				}, 200);
			});
		},
		methods: {
			/* 初始化滚动区域 */
			initScrollView() {
				return new Promise((resolve, reject) => {
					let view = uni.createSelectorQuery().select('#scroll-panel');
					view.boundingClientRect(res => {
						// console.log(res.top, res.right, res.bottom, res.left, res.width, res.height);
						this.scrollTopSize = res.top; //元素顶部到视口顶部的距离
						this.scrollHeight = res.height; //元素的高度
						this.$nextTick(() => {
							resolve();
						});
					}).exec();

				});
			},
			/* 获取列表数据 */
			getListData() {
				new Promise((resolve, reject) => {
					uni.showLoading({
						title: '加载中...'
					});
					setTimeout(() => {
						const leftArray = this.apiData.map(item => item.typeName);
						const mainArray = this.apiData.map(item => ({
							title: item.typeName,
							list: item.commVos
						}));
						
						resolve({ left: leftArray, main: mainArray });
						uni.hideLoading();
					}, 1000);
				}).then(res => {
					this.leftArray = res.left;
					this.mainArray = res.main;
					this.$nextTick(() => {
						this.getElementTop();
					});
				}).catch(err => {
					uni.hideLoading();
					uni.showToast({
						title: '数据加载失败',
						icon: 'none'
					});
					console.error('数据请求失败:', err);
				});
			},
			/* 获取元素顶部信息 */
			getElementTop() {
				new Promise((resolve, reject) => {
					let view = uni.createSelectorQuery().selectAll('.main-item');
					view.boundingClientRect(data => {
						resolve(data);
					}).exec();
				}).then(res => {
					let topArr = res.map(item => {
						return item.top - this.scrollTopSize;
					});
					this.topArr = topArr;
					let last = res[res.length - 1]?.height || 0;
					console.log('===last>', last);
					if (last - 20 < this.scrollHeight) {
						this.fillHeight = this.scrollHeight - last + 20;
						console.log('========>', this.fillHeight);
					}
				});
			},
			/* 主区域滚动监听 */
			mainScroll(e) {
				console.log('==>', e.detail.scrollTop, e);
				let top = e.detail.scrollTop;
				let index = 0;
				console.log('->>>>>>>>', this.topArr);
				/* 查找当前滚动距离 */
				for (let i = this.topArr.length - 1; i >= 0; i--) {
					console.log('===111', i);
					if (top + 2 >= this.topArr[i]) {
						console.log('===222', this.topArr[i], i);
						index = i;
						console.log('====7888', index);
						break;
					}
				}
				this.leftIndex = index < 0 ? 0 : index;
			},
			leftTap(e) {
				let index = e.currentTarget.dataset.index;
				console.log('===+++++>', index);
				this.scrollInto = `item-${index}`;
				this.leftIndex = index;
				console.log('===+++++>', this.scrollInto);
			}
		}
	};
</script>

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐