微信小程序分类-左侧导航与右侧内容联动

左侧导航与右侧内容联动
一、首先是点击左侧导航,我们右侧内容需要滑动至相应的位置。

思路是:点击左侧某一项,获取该元素的id,也就是左侧view的id,然后动态传到右侧内容的scroll-into-view,scroll-into-view的值为某个子元素的id,
这里的子元素说的是右侧的view,我们将左侧view的id和右侧view的id设置为同一个值时,这样当左侧id发生变化时,scroll-into-view会帮助我们将右侧view进行相应的变化。
如图:

 

 

数据不同,data-id='a{ {item.id}}'替换成你数据的参数id(左侧栏数据的id)
二、接下来是右侧滚动,左侧对应选中,也就是高亮。

思路是:既然是滚动,我们需要用到scroll-view中的bindscroll属性(绑定滚动事件,和bindtap点击事件类似,点击触发bindtap监听,滚动触发bindscroll监听),
每次右侧滚动时,我们需要计算当前滚动的高度,当滚动超过左侧分类商品在右侧中占据的高度时,我们将动态修改左侧导航的id。
实际上当数据都显示的时候,我们计算每一项分类在右侧占据的高度,当滚动超过这个高度时,我们就修改左侧导航的id,这样我们就实现了高亮。
如图:

需要注意的是以上的红色圈部分,两个图中的红色圈id要一样
计算右侧占据高度的方法是通过wx.createSelectorQuery()。
我的数据是{ {good}}这个,每个人的数据不一样,但方法是相通的,按照我的文字和图进行操作是一定可以实现的。

三、最后一点问题  

我们在判断滚动的高度是否超出时,最好给最初的scrollTop加上10,因为如果为0,当我点击第二个分类时,滑动可能还没有超出第一个分类占据的高度,那么高亮就会发生计算错误;还有就时如果分类中后面的商品过少,滑动时高亮也没有发生变化,所以最好时在最后的一个分类上做一些处理,比如加上占据高度,如图。


源码部分
js:

data: {

winHeight: 0,

good: [],

contentActive: '', // 内容栏id

navActive: 0, // 导航栏选中id

heightArr: [],

containerH: 0,

},

onLoad: function () {

// 获取窗口可用高度

wx.getSystemInfo({

success: res => {

this.setData({

winHeight: res.windowHeight

});

}

});

// 在请求网络数据之后里面---------

this.setData({

good: res

})

this.setHeightArr();

});

// 在请求网络数据之后里面写以上-------

},

 

setHeightArr:function(){

let query = wx.createSelectorQuery().in(this);

let heightArr = [];

let s = 0;

query.selectAll('.vertical-list').boundingClientRect((react) => {

react.forEach((res) => {

s += res.height;

heightArr.push(s)

});

this.setData({

heightArr: heightArr

})

});

query.select('.content').boundingClientRect((res) => {

// 计算容器高度

this.setData({

containerH: res.height

})

}).exec();

},

 

onScroll(e) {

let scrollTop = e.detail.scrollTop;

scrollTop += 10;

let scrollArr = this.data.heightArr;

if (scrollTop >= scrollArr[scrollArr.length - 1] - this.data.containerH) {

return

} else {

for (let i = 0; i < scrollArr.length; i++) {

if (scrollTop >= 0 && scrollTop < scrollArr[0]) {

console.log('==============aaa' + scrollTop + "==" + scrollArr[0]);

this.setData({

navActive: 0

})

} else if (scrollTop >= scrollArr[i - 1] && scrollTop < scrollArr[i]) {

console.log('==============bbb' + scrollTop + "==" + scrollArr[i]);

this.setData({

navActive: i

})

}

}

}

},

 

chooseType: function(e) {

let id = e.currentTarget.dataset.id;

let index = e.currentTarget.dataset.index;

console.log('=============id' + id + "--" + index);

this.setData({

toView: id,

navActive: index

})

},

 

wxml:
<view class="book-box">

<view class="swiper-vertical-tab">

<scroll-view class='swipter-nav' scroll-y="true" style="height:{ {winHeight}}px;">

<view wx:for="{ {good}}" wx:key="item" class="{ {index===navActive ? 'active' : ''}}" bindtap="chooseType" data-id='a{ {item.id}}' data-index='{ {index}}'>

{ {item.foodType}}

</view> 

</scroll-view>

</view>

 

<view class="content content-class">

<scroll-view class="swiper-vertical-box" scroll-y="true" style="height:{ {winHeight}}px;" scroll-into-view="{ {toView}}" scroll-with-animation="true"

<think>我们参考了用户提供的多个引用,特别关注如何实现微信小程序左侧导航右侧内容联动效果。根据引用[4],核心思路是:点击左侧导航项时,获取该项的id,并将该id赋值给右侧滚动容器的scroll-into-view属性(该属性值应为右侧内容区某子元素的id),从而实现右侧内容滚动到对应位置。同时,当右侧内容滚动时,需要监听滚动事件,当某个标题区域进入视口时,将左侧导航栏的对应项高亮显示。下面将分步骤详细说明实现方式:###1.页面结构设计(WXML)在页面上,我们采用左右布局:-左侧:使用固定高度的可滚动区域(scroll-view)展示导航菜单-右侧:使用scroll-view(可垂直滚动)展示多个内容分类区域,每个分类区域都有一个唯一的id(左侧导航项的id对应)```html<viewclass="container"><!--左侧导航--><scroll-viewclass="left-nav"scroll-y><viewwx:for="{{navList}}"wx:key="index"class="nav-item{{activeIndex==index?'active':''}}"bindtap="onNavTap"data-index="{{index}}"data-id="{{item.id}}">{{item.text}}</view></scroll-view><!--右侧内容--><scroll-viewclass="right-content"scroll-yscroll-with-animationscroll-into-view="{{scrollIntoId}}"bindscroll="onContentScroll"><viewwx:for="{{contentList}}"wx:key="index"><viewid="{{item.id}}"class="content-section"><textclass="content-title">{{item.title}}</text><!--具体内容--></view></view></scroll-view></view>```###2.样式设置(WXSS)设置左侧导航栏固定高度并允许滚动,右侧内容区域同样设置高度(例如100vh)并滚动。注意左右布局使用flex布局。```css.container{display:flex;height:100vh;}.left-nav{width:200rpx;/*宽度根据需要调整*/height:100%;background-color:#f5f5f5;overflow-y:auto;}.nav-item{height:100rpx;display:flex;align-items:center;justify-content:center;}.nav-item.active{color:red;/*高亮样式*/}.right-content{flex:1;height:100%;overflow-y:auto;}.content-section{padding:20rpx;}.content-title{font-size:32rpx;font-weight:bold;}```###3.数据设置(JS)在页面的data中初始化导航列表(navList)和内容列表(contentList),并设置activeIndex(当前选中的导航索引)和scrollIntoId(右侧滚动目标id)。```javascriptPage({data:{activeIndex:0,//当前选中的导航索引scrollIntoId:'',//右侧滚动目标idnavList:[{id:'section0',text:'分类0'},{id:'section1',text:'分类1'},//...其他导航项],contentList:[{id:'section0',title:'分类0标题',...},{id:'section1',title:'分类1标题',...},//...其他内容分类]},//点击左侧导航onNavTap(e){constindex=e.currentTarget.dataset.index;constid=e.currentTarget.dataset.id;this.setData({activeIndex:index,scrollIntoId:id});},//右侧内容滚动时触发onContentScroll(e){//计算当前滚动位置,判断哪个分类区域进入视口//这里可以使用IntersectionObserver(更精确)或者根据滚动位置和每个分类区域的高度进行计算//下面提供一种基于区域高度的计算方法constquery=this.createSelectorQuery();query.selectAll('.content-section').boundingClientRect().exec(res=>{if(res[0]){constsections=res[0];//获取当前滚动位置(从事件对象中获取滚动高度,但事件中不直接提供,所以用boundingClientRect)//也可以监听scroll事件,记录滚动位置,但这里我们换一种方式:计算每个区域的顶部位置和视口位置//通过绑定在scroll-view上的scroll-top值,但事件对象e中只有detail.scrollLeft/scrollTop(注意:基础库2.9.0才开始支持scroll事件返回scrollTop)//因此,我们使用每个区域的boundingClientRect.top和视口高度来判断//找出第一个top>=0(接近顶部)的区域,但实际可以判断在视口中的区域,这里我们判断每个区域距离顶部的距离,然后比较//注意:因为内容区域滚动,所以每个区域相对于视口的top是变化的,当某个区域顶部接近视口顶部(可加上偏移)时,我们就认为它是当前区域letcurrentIndex=0;constscrollTop=e.detail.scrollTop;//获取滚动条位置,但注意:在低版本中可能不支持,需要兼容//如果e.detail中没有scrollTop,则采用另一种方法:用页面滚动位置计算//这里我们假设小程序基础库>=2.9.0//如果不支持,可以使用wx.pageScrollTo等方法,但我们这里使用另一种思路:不用scroll事件中的scrollTop,而是用section的位置//因为我们已经获取了所有区域的位置信息,我们可以遍历所有区域,找到最接近顶部的那个//获取屏幕高度constwindowHeight=wx.getSystemInfoSync().windowHeight;letminDiff=Infinity;for(leti=0;i< sections.length;i++){constsection=sections[i];//计算该区域顶部到滚动容器顶部的距离(注意:boundingClientRect的top是相对于视口顶部的距离,所以为负表示在视口上方)//我们这里使用:计算section顶部相对于容器顶部的距离(即原始位置)滚动容器已滚动的高度(scrollTop)之间的关系//实际中,每个区域在容器中的位置(top)等于其boundingClientRect.top+scrollTop//但boundingClientRect是相对于当前视口的,所以当没有滚动时,它的top就是该区域在视口中的位置,而滚动后,它的top=初始位置-scrollTop//因此,我们想要得到该区域在容器中的位置(相对容器顶部),可以这样计算://先获取容器的位置(距离视口顶部的距离),然后每个区域的top(相对于容器顶部)=section.top-containerTop+scrollTop//但这样复杂了,我们可以直接使用section.dataset.top(在加载时记录每个区域的初始top)或者用boundingClientRect.top+scrollTop(但scrollTop未知)//因此,这里我们可以不用scrollTop,而是根据每个区域相对于视口的位置以及一个阈值(如:当区域的顶部距离视口顶部小于某个值,且下一个区域还没有进入视口)就认为它是当前区域//简单方法:当区域的顶部位置小于一个阈值(例如100)并且区域底部位置大于0,则认为是当前区域//但更简单的方法:使用区域内固定标题,然后观察标题是否在视口内//这里我们采用:找出第一个在视口中出现的区域(即区域顶部在视口内)if(section.top<=0&&section.bottom>0){//注意:boundingClientRect.top是相对于视口顶部的,所以当区域顶部在视口顶部之上(即已经滚动过去)且底部还在视口内,那么它部分可见//我们取这个区域的索引currentIndex=i;break;//找到第一个部分可见的区域就停止}}//或者我们使用阈值法:找到距离顶部最近(且小于一定值)的区域//但上面遍历已经找到了,所以直接设置this.setData({activeIndex:currentIndex});}});},//注意:上面的onContentScroll方法在低版本基础库可能无法获取scrollTop,并且boundingClientRect计算复杂,性能要求高,因此实际生产环境可以使用IntersectionObserver//下面介绍使用IntersectionObserver实现右侧滚动时更新左侧导航高亮onReady:function(){//创建观察器this.observer=wx.createIntersectionObserver(this);//观察所有内容区域(section)//每次触发,找出比例最大的一个this.observer.relativeToViewport({top:0,bottom:0}).observe('.content-section',(res)=>{//当有多个区域进入视口时,会多次触发//需要找出最接近顶部的一个,可以通过res.intersectionRatio(相交比例)和res.boundingClientRect.top(距离视口顶部位置)来判断//这里我们使用一个数组记录当前进入视口的区域,然后定时或利用防抖找出最合适的一个(但这样复杂)//所以实际中,我们通常使用上面滚动事件的方法(但更可靠的方式是使用scrollView的scroll事件获取scrollTop,然后根据每个区域的初始位置(在页面加载时获取)来计算当前是哪个区域)});//由于IntersectionObserver的复杂性和兼容性,这里我们继续使用scroll事件的方式,并稍作改进//在页面加载时,记录每个区域的位置信息constquery=this.createSelectorQuery();query.selectAll('.content-section').boundingClientRect();query.selectViewport().scrollOffset();query.exec(res=>{if(res[0]){this.sectionPosition=res[0].map(section=>section.top);//注意:这里获取的是相对于视口的位置,但我们需要的是相对于整个滚动容器的位置(所以还需要加上滚动容器当时的scrollTop)//但是,在页面加载时,我们默认滚动容器处于顶部,所以这个top就是距离容器顶部的位置//因此,我们保存起来,并记录滚动容器的初始位置//同时,我们获取了当前滚动容器的scrollTop(在第二个结果中)constscrollTop=res[1].scrollTop;//获取容器初始的scrollTop(应该是0)//所以此时,每个区域在容器内的位置就是section.top(相对于容器顶部)=section.top(相对于视口)+scrollTop(但这里scrollTop=0,所以就是section.top)//但当我们滚动后,这个位置是不变的(固定值),所以可以记录this.sectionTops=this.sectionPosition.map(top=>top);}});//重新编写onContentScroll方法,利用记录的sectionTops//由于上面的例子中已经写了onContentScroll,我们可以换一个名字,这里为了简洁,我们采用另一种方法,不重复获取区域位置,而是提前获取}});//改进的onContentScroll方法(使用预先记录的区域位置)Page({//...其他数据onContentScroll(e){constscrollTop=e.detail.scrollTop;//如果没有记录区域位置,则尝试获取if(!this.sectionTops){//若未记录,则立即记录一次(可能会重复记录,但无妨)constquery=this.createSelectorQuery();query.selectAll('.content-section').boundingClientRect().exec(res=>{if(res[0]){this.sectionTops=res[0].map(item=>item.top+scrollTop);//注意:这里记录的top是相对于滚动容器顶部的绝对位置(在容器中)this.updateActiveIndexByScroll(scrollTop);}});}else{this.updateActiveIndexByScroll(scrollTop);}},updateActiveIndexByScroll(scrollTop){//遍历区域位置,找到第一个区域顶部(大于等于scrollTop)的区域,它可能是当前区域,但更准确的是找距离最近的一个//因为scrollTop是容器的滚动位置,那么容器的顶部(可视区顶部)在容器总内容中的位置就是scrollTop//也就是说,某个区域在容器中的位置(sectionTops)表示该区域顶部距离整个容器顶部的距离//那么,当前滚动位置scrollTop,就表示容器顶部(可视区顶部)位于整个容器内容中的哪个位置//因此,可视区顶部刚好经过某个区域时,该区域的顶部位置(sectionTops[i])应该小于等于scrollTop+阈值(如50),并且该区域的底部位置(sectionTops[i]+height)大于scrollTop//由于我们没有记录高度,所以只根据顶部位置,找到第一个顶部位置大于scrollTop的区域,那么前一个区域就是当前区域if(!this.sectionTops||this.sectionTops.length===0)return;letactiveIndex=0;for(leti=0;i< this.sectionTops.length;i++){//如果该区域的顶部位置大于等于当前滚动位置(scrollTop)加上一个偏移(例如50px),那么这个区域还没有完全进入视口,前面的区域就是当前高亮区域//注意:这里偏移值可以取0,但为了提前切换,可以取一个较小的值if(this.sectionTops[i]>=scrollTop+50){//说明当前区域还没有滚动到阈值位置,那么高亮前一个activeIndex=i>0?i-1:0;break;}elseif(i===this.sectionTops.length-1){//最后一个activeIndex=i;}}this.setData({activeIndex});}});```###4.注意事项-**性能优化**:在滚动事件中频繁触发查询和计算,需使用防抖(throttle)限制频率,或使用IntersectionObserver(小程序从基础库2.0.0开始支持)。-**高亮同步**:避免在点击左侧导航时触发右侧滚动事件,从而再次触发左侧高亮变化。可以通过设置标志位避免。-**懒加载**:如果内容数据量很大,可使用虚拟列表(recycle-view)或懒加载。###5.完整实现建议实际开发中,推荐使用IntersectionObserver来实现,因为它更高效。但需要注意,每个内容区域(作为观察对象)设置阈值(threshold)为[0,1],然后选择第一个进入视口的区域(或者相交比例最大的区域)为当前区域,更新左侧导航高亮。综上所述,实现的核心在于点击导航时通过scroll-into-view控制右侧内容滚动,而滚动内容时通过计算当前视口区域来更新导航高亮。相关问题:1.如何在小程序中实现类似美团外卖的分类菜单左右联动?2.微信小程序使用scroll-view时如何优化滚动性能?3.在小程序中,如何获取元素的位置信息?4.如何避免滚动事件过于频繁导致的性能问题?</think> ### 微信小程序左侧导航右侧内容联动实现详解 #### 核心思路分析(参考引用[4]) 1. **视图联动原理** - 左右两侧使用相同的标识符(`id`) - 点击左侧菜单时,将`id`赋值给右侧容器的`scroll-into-view`属性 - 右侧滚动时,通过位置计算当前可视区域对应的`id`,反哺左侧高亮 #### 完整实现方案 ##### 1. WXML结构(参考引用[5]) ```html <view class="container"> <!-- 左侧导航 --> <scroll-view class="left-nav" scroll-y> <view wx:for="{{navList}}" wx:key="id" class="nav-item {{activeId == item.id ? 'active' : ''}}" bindtap="handleNavClick" data-id="{{item.id}}" > {{item.text}} </view> </scroll-view> <!-- 右侧内容 --> <scroll-view class="right-content" scroll-y scroll-into-view="{{currentAnchor}}" bindscroll="handleContentScroll" > <view wx:for="{{contentList}}" wx:key="id"> <view id="{{item.id}}" class="section"> <text class="section-title">{{item.title}}</text> <!-- 具体内容 --> </view> </view> </scroll-view> </view> ``` ##### 2. WXSS样式(参考引用[3]) ```css /* 容器布局 */ .container { display: flex; height: 100vh; } /* 左侧导航样式 */ .left-nav { width: 200rpx; background: #f5f5f5; position: fixed; height: 100%; } .nav-item { padding: 30rpx; text-align: center; border-left: 4rpx solid transparent; } .nav-item.active { border-left-color: #e64340; background: #fff; font-weight: bold; } /* 右侧内容样式 */ .right-content { flex: 1; margin-left: 200rpx; /* 等于导航宽度 */ } .section { padding: 20rpx; border-bottom: 1rpx solid #eee; } .section-title { font-size: 32rpx; font-weight: bold; display: block; padding: 20rpx 0; } ``` ##### 3. JS逻辑实现(核心联动) ```javascript Page({ data: { navList: [ { id: 'food', text: '美食' }, { id: 'fruit', text: '水果' }, // ...其他分类 ], contentList: [ { id: 'food', title: '美食专区' }, { id: 'fruit', title: '水果专区' }, // ...其他内容 ], currentAnchor: '', // 当前锚点ID activeId: 'food', // 当前激活导航ID sectionTops: [] // 记录各区域顶部位置 }, onReady() { // 初始化获取各区域位置 this.calcSectionPositions() }, // 计算各区域顶部位置 calcSectionPositions() { const query = wx.createSelectorQuery() this.data.contentList.forEach(item => { query.select(`#${item.id}`).boundingClientRect() }) query.exec(res => { const tops = res.map(item => item.top) this.setData({ sectionTops: tops }) }) }, // 左侧导航点击(参考引用[4]) handleNavClick(e) { const id = e.currentTarget.dataset.id this.setData({ currentAnchor: id, activeId: id }) }, // 右侧滚动事件(双向联动关键) handleContentScroll(e) { const scrollTop = e.detail.scrollTop const { sectionTops } = this.data // 计算当前可视区域对应的ID for (let i = sectionTops.length - 1; i >= 0; i--) { if (scrollTop >= sectionTops[i] - 50) { // 50rpx偏移量优化体验 const currentId = this.data.contentList[i].id if (this.data.activeId !== currentId) { this.setData({ activeId: currentId }) } break } } } }) ``` #### 4. 优化技巧 1. **性能优化** - 使用`throttle`限制`scroll`事件频率 ```javascript handleContentScroll: throttle(function(e) { // 原有逻辑 }, 300) ``` 2. **体验优化**(参考引用[1]) - 添加`scroll-with-animation`属性实现平滑滚动 - 点击导航时添加过渡效果 - 初始加载数据懒加载 3. **兼容处理** - 不同屏幕尺寸适配 - 考虑胶囊按钮高度(通过`wx.getMenuButtonBoundingClientRect()`) #### 场景应用 此方案适用于: - 外卖点餐分类(美团/饿了么样式) - 超市商品分类浏览 - 图书目录导航 - 垂直领域内容聚合平台 > 此实现参考了微信小程序官方文档[^2]和社区最佳实践[^1][^4],通过标识符双向绑定实现精准联动,避免了`scroll-view`嵌套导致的兼容性问题。 --- ### 相关问题 1. 如何实现左右联动区域的高度自适应? 2. 当内容区域有图片加载时如何避免位置计算错误? 3. 微信小程序中是否有更高效的替代`scroll-view`的方案? 4. 如何实现二级分类菜单左右联动? 5. 在Taro等跨端框架中如何实现相同效果?
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值