一、功能说明

实现 商品分类左右联动 效果:

  • 点击左侧分类 → 右侧自动滚动到对应分类内容;

  • 滚动右侧内容 → 左侧自动高亮当前分类。

二、核心结构

<scroll-view class="menuLeft" scroll-y>
  <view v-for="(item,index) in menuList" 
        @click="changeCurrent(index)" 
        :class="{currentMenu: currentIndex==index}">
    {{item.name}}
  </view>
</scroll-view>

<scroll-view class="menuRight" scroll-y 
             :scroll-top="scrollTop" 
             @scroll="scrollRight">
  <view class="menuDetailBox" 
        v-for="(item,index) in menuList" :key="index">
    <view class="title">{{item.name}}</view>
    <view v-for="menu in item.children" class="menu">
      {{menu.name}}
    </view>
  </view>
</scroll-view>

三、主要逻辑

1️⃣ 初始化高度计算(onLoad)

获取右侧每个分类区块高度,并计算累计值:

query.selectAll('.menuDetailBox').boundingClientRect(data => {
  this.newData = data.map((item, i) => {
    item.offsetHeight = data.slice(0, i + 1)
      .reduce((sum, cur) => sum + cur.height, 0)
    return item
  })
}).exec()

2️⃣ 点击左侧分类

根据预计算的高度滚动右侧内容:

changeCurrent(index) {
  this.currentIndex = index
  this.scrollTop = this.newData[index - 1]?.offsetHeight || 0
}

3️⃣ 右侧滚动时联动左侧

监听滚动距离,动态更新左侧高亮:

scrollRight(e) {
  const top = e.detail.scrollTop
  for (let i = 0; i < this.newData.length; i++) {
    if (top < this.newData[i].offsetHeight) {
      this.currentIndex = i
      break
    }
  }
}

四、样式要点

  • 左侧固定宽度 200rpx,右侧自适应。

  • 当前分类高亮(加粗 / 红条标识)。

  • 右侧滚动视图需开启 scroll-y="true"

.currentMenu { font-weight: 700; }
.line::before { background-color: red; }

五、总结

  • 使用 scrollTop + 高度累计实现左右同步滚动。

  • 左右两栏通过 currentIndex 实现联动高亮。

  • 简单可靠,适用于电商、分类导航场景。

完整代码

<template>

	<view class="menuBox">
		<view class="menuContent">
			<scroll-view scroll-y="true" class="menuLeft">
				<view class="menuType" @click="changeCurrent(index)" v-for="(item,index) in menuList"
					:class="{currentMenu:currentIndex==index}">
					{{item.name}}
				</view>
				<view class="line" :style="{top:currentIndex*90 +'rpx'}"></view>
			</scroll-view>

			<scroll-view :scroll-top="scrollTop" @scroll="scrollRight" class="menuRight" scroll-y="true">
				<div class="menuDetailBox" v-for="(item,index) in menuList" :key="index">
					<div class="title">{{item.name}}</div>
					<div class="menu" v-for="(menu,index) in item.children" :key="index">
						<div class="d2">
							123
						</div>
						<div class="menuName">
							{{menu.name}}
						</div>
					</div>
				</div>
			</scroll-view>


		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				newData:[],  // 拿到右侧所有的商品列表元素
				scrollTop: 0,
				currentIndex: 0,
				menuList: [{
						"id": 1,
						"name": "手机数码",
						"children": [{
								"id": 101,
								"name": "手机"
							},
							{
								"id": 105,
								"name": "数码相机"
							}
						]
					},
					{
						"id": 2,
						"name": "家用电器",
						"children": [{
								"id": 201,
								"name": "电视机"
							},
							{
								"id": 204,
								"name": "冰箱"
							},
							{
								"id": 205,
								"name": "吸尘器"
							}
						]
					},
					{
						"id": 3,
						"name": "服饰鞋包",
						"children": [{
								"id": 301,
								"name": "男装"
							},
							{
								"id": 302,
								"name": "女装"
							},
							{
								"id": 303,
								"name": "运动鞋"
							},
							{
								"id": 304,
								"name": "箱包"
							},
							{
								"id": 305,
								"name": "配饰"
							}
						]
					},
					{
						"id": 4,
						"name": "美妆个护",
						"children": [{
								"id": 401,
								"name": "面部护肤"
							},
							{
								"id": 402,
								"name": "彩妆"
							},
							{
								"id": 403,
								"name": "洗发护发"
							},
							{
								"id": 404,
								"name": "身体护理"
							},
							{
								"id": 405,
								"name": "香水"
							}
						]
					},
					{
						"id": 5,
						"name": "母婴玩具",
						"children": [{
								"id": 501,
								"name": "奶粉"
							},
							{
								"id": 502,
								"name": "尿不湿"
							},
							{
								"id": 503,
								"name": "婴儿用品"
							},
							{
								"id": 504,
								"name": "儿童玩具"
							},
							{
								"id": 505,
								"name": "孕妇用品"
							}
						]
					},
					{
						"id": 6,
						"name": "食品饮料",
						"children": [{
								"id": 601,
								"name": "休闲零食"
							},
							{
								"id": 602,
								"name": "粮油调味"
							},
							{
								"id": 603,
								"name": "饮料酒水"
							},
							{
								"id": 604,
								"name": "冲调速食"
							},
							{
								"id": 605,
								"name": "生鲜水果"
							}
						]
					},
					{
						"id": 7,
						"name": "家居家装",
						"children": [{
								"id": 701,
								"name": "家具"
							},
							{
								"id": 702,
								"name": "灯具"
							},
							{
								"id": 703,
								"name": "床上用品"
							},
							{
								"id": 704,
								"name": "厨房用品"
							},
							{
								"id": 705,
								"name": "装饰摆件"
							}
						]
					},
					{
						"id": 8,
						"name": "运动户外",
						"children": [{
								"id": 801,
								"name": "跑步装备"
							},
							{
								"id": 802,
								"name": "健身器材"
							},
							{
								"id": 803,
								"name": "户外鞋服"
							},
							{
								"id": 804,
								"name": "骑行装备"
							},
							{
								"id": 805,
								"name": "垂钓用品"
							}
						]
					},
					{
						"id": 9,
						"name": "汽车用品",
						"children": [{
								"id": 901,
								"name": "机油"
							},
							{
								"id": 902,
								"name": "轮胎"
							},
							{
								"id": 903,
								"name": "车载电器"
							},
							{
								"id": 904,
								"name": "清洁用品"
							},
							{
								"id": 905,
								"name": "维修配件"
							}
						]
					},
					{
						"id": 10,
						"name": "图书文娱",
						"children": [{
								"id": 1001,
								"name": "文学小说"
							},
							{
								"id": 1002,
								"name": "少儿读物"
							},
							{
								"id": 1003,
								"name": "教育考试"
							},
							{
								"id": 1004,
								"name": "艺术设计"
							},
							{
								"id": 1005,
								"name": "音像制品"
							}
						]
					}
				]

			}
		},
		methods: {
			changeCurrent(index) {
				this.currentIndex = index
				this.scrollTop = this.newData[index-1]?.offsetHeight || 0
			},
			scrollRight(e) {
				// console.log(e.detail.scrollTop,'scrollRight');
				const query = uni.createSelectorQuery().in(this);
				for (let i = 0; i < this.newData.length; i++) {
					let el = this.newData[i]
					if (e.detail.scrollTop < el.offsetHeight) {
						this.currentIndex = i
						return true
					}
				}
			}
		},
		onLoad() {
			uni.setNavigationBarTitle({
				title: '你好'
			})
			const query = uni.createSelectorQuery().in(this);
			query.selectAll('.menuDetailBox').boundingClientRect(data => {
				console.log('所有分类项:', data);
				this.newData = data.map((item, index) => {
					function fgfg(total) {
						let sum = 0
						for (var i = 0; i <= total; i++) {
							sum += data[i].height
						}
						return sum
					}
					item.offsetHeight = fgfg(index)
					return item
				})
				
			}).exec();
			
			
		}
	}
</script>

<style lang="scss" scoped>
	.menuBox {
		height: calc(100vh);
		display: flex;
		flex-direction: column;
		border: 1px solid #ccc;
		margin: 0 10rpx;
		.menuContent {
			height: calc(100vh);
			overflow: auto;
			position: relative;
			display: flex;
			position: sticky;
			top: 0rpx;
			.menuLeft {
				float: left;
				position: relative;
				width: 200rpx;
				background-color: #f5f5f5;

				.menuType {
					transition: all 0.4s;
					position: relative;
					z-index: 999999999;
					height: 90rpx;
					display: flex;
					align-items: center;
					padding-left: 20rpx;

					&.currentMenu {
						font-weight: 700;
					}
				}

			}

			.menuRight {
				flex: 1;
				background-color: #fff;

				.menuDetailBox {
					.title {
						height: 60rpx;
						display: flex;
						align-items: center;
					}

					.menu {
						background-color: skyblue;
						height: 100rpx;
						display: flex;
						justify-content: center;

						&:not(:nth-last-of-type(1)) {
							margin-bottom: 20rpx;
						}

						.d2 {
							width: 60rpx;
							height: 60rpx;
						}

						.menuName {
							flex: 1;
							margin-left: 20rpx;
						}

					}
				}
			}
		}
	}

	.line {
		position: absolute;
		left: 0;
		top: 0;
		height: 90rpx;
		width: 100%;
		background-color: #fff;
		z-index: 999;
		transition: all 0.4s;

		&::before {
			content: '';
			position: absolute;
			left: 0;
			top: 0;
			height: 90rpx;
			width: 4rpx;
			background-color: red;
			z-index: 999;
			transition: all 0.4s
		}
	}
</style>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐