使用 UniApp 实现商品分类页面的示例代码,包含虚拟滚动和防抖处理,以解决数据量大时页面卡顿和快速滑动抖动的问题。
<template>
<view class="container">
<!-- 左侧分类 -->
<scroll-view
class="left-nav"
scroll-y
:scroll-top="leftScrollTop"
:scroll-with-animation="true"
@scroll="onLeftScroll"
>
<view
v-for="(cat, index) in categories"
:class="['nav-item', {
active: currentCat === index,
'active-animation': activeAnimation === index
}]"
@click="switchCategory(index)"
>
{{ cat.name }}
<!-- 新增选中指示条 -->
<view class="active-line" v-if="currentCat === index"></view>
</view>
</scroll-view>
<!-- 右侧商品 -->
<scroll-view
class="right-list"
scroll-y
:scroll-top="rightScrollTop"
@scroll="onRightScroll"
>
<template v-for="(cat, index) in categories" >
<!-- 分类锚点 -->
<view :id="'cat-' + index" class="category-title">{{ cat.name }}</view>
<!-- 虚拟滚动商品列表 -->
<view
v-for="item in visibleItems[index]"
:key="item.id"
class="product-item"
>
{{ item.name }}
</view>
</template>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
categories: [], // 分类数据
currentCat: 0, // 当前选中分类
catPositions: [], // 存储分类锚点位置
leftScrollTop: 0, // 左侧滚动位置
rightScrollTop: 0, // 右侧滚动位置
visibleItems: [], // 虚拟滚动可见商品
scrollTimer: null,
activeAnimation: -1, // 动画效果控制
leftNavHeight: 0, // 左侧导航高度
itemHeight: 0, // 单个分类项高度
}
},
mounted() {
this.loadData();
this.$nextTick(() => {
this.calcCatPositions();
});
},
methods: {
generateMockData() {
const categories = [];
for (let i = 0; i < 30; i++) {
const category = {
id: i,
name: '分类'+(i + 1)',
products: []
};
for (let j = 0; j < 50; j++) {
category.products.push({
id: i-j,
name: '商品'+ (i+1)-(j+1)
});
}
categories.push(category);
}
return categories;
},
// 加载数据
async loadData() {
this.categories = this.generateMockData();
this.initVirtualScroll();
},
// 初始化布局参数
async initLayoutMetrics() {
const res = await new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select('.left-nav')
.boundingClientRect(res => resolve(res))
.exec()
});
this.leftNavHeight = res.height;
const itemRes = await new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select('.nav-item')
.boundingClientRect(res => resolve(res))
.exec()
});
this.itemHeight = itemRes.height;
},
// 优化后的切换分类方法
switchCategory(index) {
if (this.currentCat === index) return;
// 触发点击动画
this.activeAnimation = index;
setTimeout(() => {
this.activeAnimation = -1;
}, 300);
this.currentCat = index;
// 平滑滚动到对应位置
this.scrollRightTo(index);
this.scrollLeftTo(index);
},
// 优化右侧滚动方法
scrollRightTo(index) {
const query = uni.createSelectorQuery().in(this);
query.select('#cat-'+index).boundingClientRect(res => {
this.rightScrollTop = res.top + this.rightScrollTop - 50; // 增加50px偏移
}).exec();
},
// 优化左侧滚动定位
scrollLeftTo(index) {
const visibleItems = Math.floor(this.leftNavHeight / this.itemHeight);
const targetPos = Math.max(
0,
index * this.itemHeight - this.leftNavHeight/2 + this.itemHeight/2
);
this.leftScrollTop = Math.min(
targetPos,
this.categories.length * this.itemHeight - this.leftNavHeight
);
},
// 优化滚动事件处理
onRightScroll(e) {
if (this.scrollTimer) clearTimeout(this.scrollTimer);
this.scrollTimer = setTimeout(() => {
const scrollTop = e.detail.scrollTop;
const index = this.findCurrentCat(scrollTop + 100); // 增加提前量
if (index !== this.currentCat) {
this.currentCat = index;
this.scrollLeftTo(index);
}
}, 150);
},
// 初始化虚拟滚动
initVirtualScroll() {
this.visibleItems = this.categories.map(() => []);
this.calcVisibleItems();
},
// 计算可见商品
calcVisibleItems() {
// 根据滚动位置计算需要渲染的商品
// 这里需要根据实际布局高度和滚动位置进行计算
// 示例简单实现,实际需要优化
this.visibleItems = this.categories.map(cat =>
cat.products.slice(0, 20) // 仅渲染前20个
);
},
// 计算分类锚点位置
calcCatPositions() {
const query = uni.createSelectorQuery().in(this);
this.categories.forEach((_, index) => {
query.select('#cat-'+index).boundingClientRect(res => {
this.catPositions[index] = res.top;
}).exec();
});
},
// 切换分类
switchCategory(index) {
this.currentCat = index;
this.scrollRightTo(index);
this.scrollLeftTo(index);
},
// 右侧滚动到指定分类
scrollRightTo(index) {
const position = this.catPositions[index];
this.rightScrollTop = position;
},
// 左侧滚动到可视区域
scrollLeftTo(index) {
const query = uni.createSelectorQuery().in(this);
query.selectAll('.nav-item').boundingClientRect(res => {
const itemHeight = res[0].height;
const visibleCount = Math.floor(this.leftNavHeight / itemHeight);
this.leftScrollTop = Math.max(0, (index - Math.floor(visibleCount/2)) * itemHeight)
}).exec();
},
// 右侧滚动事件
onRightScroll(e) {
if (this.scrollTimer) clearTimeout(this.scrollTimer);
this.scrollTimer = setTimeout(() => {
const scrollTop = e.detail.scrollTop;
// 查找当前分类
const index = this.findCurrentCat(scrollTop);
if (index !== this.currentCat) {
this.currentCat = index;
this.scrollLeftTo(index);
}
}, 100);
},
// 二分查找当前分类
findCurrentCat(scrollTop) {
let low = 0, high = this.catPositions.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (this.catPositions[mid] <= scrollTop) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return Math.max(0, high);
}
}
}
</script>
<style>
.container {
display: flex;
height: 100vh;
}
.left-nav {
width: 200rpx;
height: 100%;
background: #f5f5f5;
}
.nav-item {
padding: 30rpx;
&.active {
color: red;
background: white;
}
}
.right-list {
flex: 1;
height: 100%;
}
.category-title {
padding: 20rpx;
font-weight: bold;
background: #eee;
}
.product-item {
padding: 20rpx;
border-bottom: 1px solid #ddd;
}
/* 优化选中效果 */
.nav-item {
position: relative;
transition: all 0.3s;
&.active {
color: #e4393c;
font-weight: bold;
transform: scale(1.05);
.active-line {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 60%;
background: #e4393c;
border-radius: 3rpx;
}
}
/* 点击动画 */
&.active-animation {
animation: itemScale 0.3s ease-in-out;
}
}
@keyframes itemScale {
0% { transform: scale(1); }
50% { transform: scale(1.08); }
100% { transform: scale(1.05); }
}
</style>