本文基于提供的代码实现一个左右联动的滚动组件,以下是详细的代码解析与实现原理说明:
javascript
<!--
双栏联动滚动组件 - 技术解析
功能特性:
1. 左侧导航栏与右侧内容区双向联动
2. 自适应容器高度
3. 平滑滚动定位
4. 动态内容位置计算
-->
<template>
<view class="container">
<!-- 外层容器 -->
<view class="nav-container" id="navContainer">
<!-- 左侧导航 ScrollView -->
<scroll-view
:scroll-y="true"
:style="{ height: containerHeight + 'px' }"
class="nav-sidebar"
:scroll-into-view="leftScrollId"
scroll-with-animation
>
<!-- 导航项循环渲染 -->
<view
v-for="(item, index) in leftData"
:key="index"
:id="'navItem-' + index"
:class="['nav-item', { active: currentIndex === index }]"
@tap="handleNavClick(index)"
>
{{ item }}
</view>
</scroll-view>
<!-- 右侧内容 ScrollView -->
<scroll-view
:scroll-y="true"
:style="{ height: containerHeight + 'px' }"
class="content-main"
:scroll-into-view="rightScrollId"
@scroll="handleContentScroll"
scroll-with-animation
>
<!-- 内容区块循环渲染 -->
<view
v-for="(section, sIndex) in rightData"
:key="sIndex"
:id="'content-' + sIndex"
class="content-section"
>
<view class="section-title">{{ section.title }}</view>
<view
v-for="(para, pIndex) in section.content"
:key="pIndex"
class="content-para"
>
{{ para }}
</view>
</view>
<view :style="{ height: fillHeight + 'px' }"></view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
// 组件参数定义
props: {
leftData: {
// 左侧导航数据
type: Array,
default: () => ['章节1', '章节2', '章节3', '章节4', '章节5', '章节6'],
},
rightData: {
// 右侧内容数据
type: Array,
default: () => [
{
title: '章节1',
content: ['内容1'],
},
{
title: '章节2',
content: ['内容1'],
},
{
title: '章节3',
content: ['内容1'],
},
{
title: '章节4',
content: ['内容1'],
},
{
title: '章节5',
content: ['内容1'],
},
],
},
},
// 组件状态管理
data() {
return {
containerTop: 0, //容器距离顶部距离
containerHeight: 500, // 容器动态高度
currentIndex: 0, // 当前激活索引
sectionPositions: [], // 章节位置缓存数组
positionsReady: false, // 位置计算完成标志
fillHeight: 50, // 填充盒子的高度,内容滚动最后一项增加高度方便滚动
}
},
// 计算属性
computed: {
// 左侧导航自动定位ID(保持选中项在可视区)
leftScrollId() {
return `navItem-${Math.max(this.currentIndex - 2, 0)}` // 提前2项滚动
},
// 右侧内容自动定位ID
rightScrollId() {
return `content-${this.currentIndex}`
},
},
// 生命周期钩子
mounted() {
this.initContainer()
.then(() => this.calcSectionPositions())
.catch(console.error)
},
// 组件方法
methods: {
/**
* 初始化容器尺寸
* 使用 Promise 保证高度计算完成
*/
initContainer() {
return new Promise((resolve) => {
uni.createSelectorQuery()
.in(this)
.select('#navContainer')
.boundingClientRect((res) => {
this.containerTop = res.top //距离父元素顶部高度
this.containerHeight = res.height
resolve()
})
.exec()
})
},
/**
* 计算内容区块位置
* 使用 uni API 获取元素位置信息
*/
calcSectionPositions() {
uni.createSelectorQuery()
.in(this)
.selectAll('.content-section')
.boundingClientRect((res) => {
// 缓存各章节顶部位置
this.sectionPositions = res.map((item) => item.top - this.containerTop)
this.positionsReady = true
let lastHeight = res[res.length - 1].height
console.log(this.containerHeight, 8454545)
//如果滚动显示的区域大于右侧单个元素的高度就要加入填充高度让元素滚动的时候 左侧的标签可以正常切换
if (lastHeight- 20 < this.containerHeight ) {
this.fillHeight = this.containerHeight - last + 20
}
})
.exec()
},
/**
* 导航点击处理
* @param {number} index - 点击的导航索引
*/
handleNavClick(index) {
this.currentIndex = index // 更新当前索引
},
/**
* 内容滚动处理
* @param {Object} e - 滚动事件对象
*/
handleContentScroll(e) {
if (!this.positionsReady) return
const scrollTop = e.detail.scrollTop
const positions = this.sectionPositions
// 二分查找算法优化(当前使用顺序查找)
let current = this.currentIndex
while (current < positions.length && positions[current] < scrollTop + 50) {
current++
}
this.currentIndex = Math.max(current - 1, 0)
},
},
}
</script>
<!-- 样式设计说明 -->
<style>
/* 容器布局 */
.container {
height: 20vh; /* 全屏高度 */
background: #ffffff;
}
.nav-container {
display: flex; /* 弹性布局 */
height: 100%;
}
/* 左侧导航样式 */
.nav-sidebar {
width: 200rpx; /* 固定宽度 */
background: #f5f7fa; /* 浅色背景 */
border-right: 1px solid #e4e7ed;
}
.nav-item {
padding: 24rpx;
transition: all 0.3s; /* 平滑过渡效果 */
}
.nav-item.active {
color: #409eff; /* 主题色 */
background: #ecf5ff; /* 激活背景 */
}
/* 右侧内容样式 */
.content-main {
flex: 1; /* 剩余空间填充 */
padding: 32rpx;
}
.section-title {
font-size: 36rpx; /* 标题字号 */
font-weight: 600;
}
.content-para {
background: #fafafa; /* 段落背景 */
border-radius: 8rpx;
}
</style>
技术实现要点
1. 双向滚动联动机制
- 导航 → 内容 :通过
scroll-into-view
绑定计算属性,点击时自动定位 - 内容 → 导航:监听滚动事件,计算当前可见章节并更新激活状态
2. 性能优化设计
- 位置信息缓存:预先计算章节位置,避免频繁查询DOM
- 节流处理:滚动事件默认自带节流,保证性能
- 异步计算:使用 Promise 链保证初始化顺序
3. 自适应布局
- 动态高度计算:通过 uni API 获取容器实际高度
- Flex 布局:实现左右栏自适应排列
4. 扩展性考虑
- 组件化设计:通过 props 接收数据,方便复用
- 样式可配置:通过 class 控制样式,易于主题定制
使用示例
javascript
<template>
<dual-scroll
:left-data="categories"
:right-data="contents"
/>
</template>
<script>
// 示例数据结构
const categories = ['水果', '蔬菜', '肉类']
const contents = [
{
title: '水果',
content: ['苹果', '香蕉', '橙子']
},
{
title: '蔬菜',
content: ['白菜', '萝卜', '番茄']
}
]
</script>