UniAppx安卓app实现左侧导航与右侧内容联动滚动

发布时间:2026/7/6 3:11:10
UniAppx安卓app实现左侧导航与右侧内容联动滚动 UniAppx安卓app实现左侧导航与右侧内容联动滚动在移动端开发中左侧导航 右侧滚动内容是电商类 APP 分类页面的经典交互模式。本文将基于实际项目代码详细介绍如何在 UniAppx uts uvue 环境下实现这一功能。功能概述实现的核心功能包括左侧导航菜单垂直排列的菜单项点击可切换到对应内容区域右侧滚动内容可垂直滚动的内容区域包含多个独立模块滚动联动右侧内容滚动时自动高亮左侧对应菜单点击左侧菜单时右侧内容平滑滚动项目结构packageA/ └── stepperScroller/ ├── index.uvue # 主组件逻辑 └── index.scss # 样式文件模板结构设计模板采用 Flex 布局实现左右分栏。左侧是一个滚动视图容器内部通过 v-for 循环渲染菜单项每个菜单项绑定点击事件。右侧也是一个滚动视图容器包含四个内容模块每个模块有唯一的 id 用于定位。关键属性说明scroll-top绑定滚动位置实现点击导航时的平滑滚动scroll-with-animation开启滚动动画scroll监听滚动事件实现导航联动核心数据结构组件使用 ref 和 reactive 管理状态定义了两个 TypeScript 接口StepperScrollerItem 和 offSetItem。其中 offSetHeight 数组是实现联动的核心数据存储每个内容块相对于滚动容器顶部的距离。数据初始化时包含四个菜单项首页、分类、购物车、我的。滚动容器信息变量三个关键变量用于计算滚动边界和判断滚动位置containerClientHeight可视区域高度containerScrollHeight内容总高度maxScrollTop最大滚动距离初始化计算偏移量在 onMounted 钩子中通过 nextTick 确保 DOM 渲染完成后再进行计算获取滚动容器的可视高度使用 uni.getElementById 获取容器元素通过 getBoundingClientRect 获取元素位置信息遍历 offSetHeight 数组计算每个内容块的偏移量 top通过 rect.top 减去容器顶部位置得到相对偏移累加每个内容块的高度计算内容总高度计算最大可滚动距离即内容总高度减去可视区域高度确保值不为负数滚动监听与导航联动handleScroll 函数处理滚动事件根据滚动位置判断应该激活哪个菜单项获取当前滚动位置 scrollTop检测是否滚动到底部通过 scrollTop 可视高度 内容总高度来判断如果滚动到底部激活最后一个菜单项否则从后往前遍历偏移量数组找到第一个偏移量小于等于当前滚动位置的元素将其索引设为当前激活项只有当新索引与当前索引不同时才更新避免不必要的更新点击导航与内容滚动switchNav 函数处理点击导航事件更新当前激活索引通过 nextTick 确保 DOM 更新完成后再执行滚动获取目标内容块的偏移量作为滚动目标位置处理最后一项的边界情况如果是最后一项且偏移量超过最大可滚动距离则将目标位置设为最大滚动距离更新 scrollTop 触发滚动样式设计使用 SCSS 嵌套语法实现清晰的布局结构container 使用 flex-direction: row 实现左右分栏menu 固定宽度 24%content 使用 flex: 1 占据剩余空间所有 flex 容器都显式声明 flex-direction确保安卓兼容性内容区域包含四个不同高度和颜色的模块用于演示安卓兼容性注意事项在 UniApp 安卓端开发时需要注意以下几点flex-direction 必须显式声明安卓端对 flex 布局的默认行为支持不一致建议显式声明方向避免使用 calc()部分安卓设备不支持 CSS calc() 函数建议使用百分比或固定值getBoundingClientRect() 返回值在安卓端可能需要类型断言优化建议1. 性能优化滚动事件触发频率很高建议添加节流优化限制滚动事件处理的频率。2. 动态内容支持如果内容是动态加载的需要在数据更新后重新计算偏移量可通过 watch 监听数据变化。3. 防抖处理点击导航后暂时禁用滚动监听避免动画过程中触发导航切换导致状态混乱。总结通过以上实现我们完成了一个完整的左侧导航 右侧内容联动滚动功能。核心要点包括使用 getBoundingClientRect() 计算元素位置维护偏移量数组实现滚动位置与导航的映射处理边界情况确保滚动行为的正确性使用 nextTick 确保 DOM 操作的时机正确这种实现方式具有良好的兼容性和可扩展性可以轻松应用于各种需要导航联动的场景。完整代码index.uvuetemplateviewclasscontainerscroll-viewclassmenuviewclickswitchNav(index)classmenu-itemv-for(item, index) in stepperScrollerData:keyindextext:classcurrent index ? active_text : normal_text{{ item.title }}/text/view/scroll-viewscroll-viewidcontent1:scroll-topdata.scrollTopclasscontentdirectionvertical:scroll-with-animationtruescrollhandleScrollviewiddemo1classdemo1第一段--内容{{stepperScrollerData[0].title}}/viewviewiddemo2classdemo2第二段--内容{{stepperScrollerData[1].title}}/viewviewiddemo3classdemo3第三段--内容{{stepperScrollerData[2].title}}/viewviewiddemo4classdemo4第四段--内容{{stepperScrollerData[3].title}}/view/scroll-view/view/templatescriptsetuplangutsimport{ref,onMounted,reactive,nextTick}fromvue;type StepperScrollerItem{title:string;}type offSetItem{top:number;id:string;}constcurrentref(0);constdatareactive({scrollTop:0,currentScrollTop:0,});constoffSetHeightrefArrayoffSetItem([{top:0,id:demo1},{top:0,id:demo2},{top:0,id:demo3},{top:0,id:demo4}]);conststepperScrollerDatarefStepperScrollerItem[]([{title:首页},{title:分类},{title:购物车},{title:我的}]);letcontainerClientHeight0;letcontainerScrollHeight0;letmaxScrollTop0;consthandleScroll(e:UniScrollEvent){constscrollTope.detail.scrollTopasnumber;data.currentScrollTopscrollTop;letnewIndex0;constisAtBottomscrollTopcontainerClientHeightcontainerScrollHeight-1;if(isAtBottomoffSetHeight.value.length0){newIndexoffSetHeight.value.length-1;}else{for(letioffSetHeight.value.length-1;i0;i--){if(scrollTopoffSetHeight.value[i].top){newIndexi;break;}}}if(newIndex!current.value){current.valuenewIndex;}};constswitchNav(index:number){current.valueindex;nextTick((){lettargetTopoffSetHeight.value[index].top;if(indexoffSetHeight.value.length-1targetTopmaxScrollTop){targetTopmaxScrollTop;}data.scrollToptargetTop;});};onMounted((){nextTick((){constcontentuni.getElementById(content1);letcontainerTop0;letcontainerHeight0;if(content!null){constrectcontent.getBoundingClientRect();containerToprect.top;containerClientHeightrect.height;}offSetHeight.value.forEach((item,index){constelementuni.getElementById(item.id);if(element!null){constrectelement.getBoundingClientRect();offSetHeight.value[index].toprect.top-(containerTopasnumber)(data.currentScrollTopasnumber);}});lettotalHeight0;offSetHeight.value.forEach((item,idx){constelementuni.getElementById(item.id);if(element!null){constrectelement.getBoundingClientRect();totalHeightrect.height;}});containerScrollHeighttotalHeight;maxScrollTopcontainerScrollHeight-containerClientHeight;if(maxScrollTop0)maxScrollTop0;console.log(可视高度,containerClientHeight);console.log(内容总高度,containerScrollHeight);console.log(最大滚动距离,maxScrollTop);console.log(偏移量数组,offSetHeight.value);});});/scriptstylescopedlangscsssrc./index.scss/styleindex.scss.container { width: 100%; height: 100%; display: flex; flex-direction: row; align-items: center; justify-content: space-between; background-color: #f5f5f5; box-sizing: border-box; padding: 8px; .menu { width: 24%; height: 100%; margin-right: 16px; background-color: #fff; .menu-item { padding: 16px 0; display: flex; flex-direction: row; align-items: center; justify-content: center; color: red; .active_text { color: red; } .normal_text { color: #000; } } } .content { flex: 1; height: 100%; background-color: #fff; .demo1 { height: 300px; background-color: yellow; width: 100%; } .demo2 { height: 600px; background-color: red; width: 100%; } .demo3 { height: 400px; background-color: green; width: 100%; } .demo4 { height: 500px; background-color: blue; width: 100%; } } }