uni-app 实现左右联动分类列表(运动装备商城示例)
·
本文使用 uni-app 实现左侧分类 + 右侧商品列表联动滚动效果,支持:
- 点击左侧自动定位右侧对应分类
- 滚动右侧自动切换左侧选中项
- 动态计算滚动高度,适配不同屏幕
- 运动装备商城实战演示
-
双向联动实现
- 左侧点击:通过
scroll-into-view定位右侧对应 ID - 右侧滚动:监听
scroll事件,对比topArr自动切换左侧下标
- 左侧点击:通过
-
动态高度计算
- 使用
uni.createSelectorQuery()获取容器真实高度 - 自动适配不同手机屏幕,避免写死高度
- 使用
-
吸顶标题
- 右侧分类标题使用
position:sticky实现吸顶效果
- 右侧分类标题使用
以下是代码示例
1.template 结构
<template>
<view class="container">
<view class="scroll-panel" id="scroll-panel">
<view class="list-box">
<!-- 左侧分类 -->
<view class="left">
<scroll-view scroll-y :style="{ height: scrollHeight + 'px' }"
:scroll-into-view="leftIntoView">
<view class="item" v-for="(item, index) in leftArray" :key="index"
:class="{ active: index == leftIndex }"
:id="'left-' + index"
:data-index="index"
@tap="leftTap">
<view class="activelink"></view>
<text class="item-name">{{ item }}</text>
</view>
</scroll-view>
</view>
<!-- 右侧商品列表 -->
<view class="main">
<scroll-view scroll-y
:style="{ height: scrollHeight + 'px' }"
@scroll="mainScroll"
:scroll-into-view="scrollInto"
scroll-with-animation>
<view class="item main-item" v-for="(item, index) in mainArray" :key="index"
:id="'item-' + index">
<view class="title">
<view>{{ item.title }}</view>
</view>
<view class="goods" v-for="(item2, index2) in item.list" :key="index2">
<view class="orderlist-list">
<view class="list-left">
<image :src="item2.imgUrl" mode="aspectFill"></image>
</view>
<view class="list-right">
<view class="list-name">{{item2.name}}</view>
<view style="display: flex;" v-if="item2.calorie">
<view class="list-meuns">{{item2.calorie}}</view>
<view style="margin-left: 10rpx;">千卡/小时</view>
</view>
<view class="price" v-if="item2.price">¥{{item2.price}}</view>
</view>
</view>
</view>
</view>
<view class="fill-last" :style="{ height: fillHeight + 'px' }"></view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
2. script 逻辑
<script>
export default {
data() {
return {
scrollHeight: 400,
scrollTopSize: 0,
fillHeight: 0, // 填充高度,用于最后一项低于滚动区域时使用
leftArray: [], // 左侧分类数组
mainArray: [], // 右侧商品数组
topArr: [],
leftIndex: 0,
scrollInto: '',
apiData: [{
category: 1,
typeName: "有氧运动",
commVos: [{
commId: 1001,
name: "专业瑜伽垫防滑减震",
calorie: 200,
imgUrl: "",
price: 89,
type: null
}, {
commId: 1002,
name: "智能计数跳绳中考专用",
calorie: 400,
imgUrl: "",
price: 59,
type: null
}, {
commId: 1003,
name: "静音无绳跳绳减肥专用",
calorie: 380,
imgUrl: "",
price: 39,
type: null
}]
}, {
category: 2,
typeName: "力量训练",
commVos: [{
commId: 2001,
name: "可调节哑铃男士家用",
calorie: 500,
imgUrl: "",
price: 299,
type: null
},
{
commId: 2002,
name: "弹力带健身阻力带",
calorie: 350,
imgUrl: "",
price: 49,
type: null
},
{
commId: 2003,
name: "俯卧撑训练板多功能",
calorie: 450,
imgUrl: "",
price: 89,
type: null
}]
}, {
category: 3,
typeName: "户外装备",
commVos: [{
commId: 3001,
name: "专业登山杖超轻伸缩",
calorie: 300,
imgUrl: "",
price: 129,
type: null
},{
commId: 3002,
name: "速干运动T恤透气",
calorie: null,
imgUrl: "",
price: 79,
type: null
},{
commId: 3003,
name: "运动水壶大容量1.5L",
calorie: null,
imgUrl: "",
price: 69,
type: null
}]
}, {
category: 4,
typeName: "护具配件",
commVos: [{
commId: 4001,
name: "运动护膝专业跑步篮球",
calorie: null,
imgUrl: "",
price: 59,
type: null
},{
commId: 4002,
name: "运动护腕吸汗透气",
calorie: null,
imgUrl: "",
price: 29,
type: null
},{
commId: 4003,
name: "运动头带止汗导汗",
calorie: null,
imgUrl: "",
price: 19,
type: null
}]
}]
};
},
computed: {
/* 计算左侧滚动位置定位 */
leftIntoView() {
return `left-${this.leftIndex > 3 ? this.leftIndex - 3 : 0}`;
}
},
mounted() {
this.$nextTick(() => {
setTimeout(() => {
this.initScrollView().then(() => {
this.getListData();
});
}, 200);
});
},
methods: {
/* 初始化滚动区域 */
initScrollView() {
return new Promise((resolve, reject) => {
let view = uni.createSelectorQuery().select('#scroll-panel');
view.boundingClientRect(res => {
// console.log(res.top, res.right, res.bottom, res.left, res.width, res.height);
this.scrollTopSize = res.top; //元素顶部到视口顶部的距离
this.scrollHeight = res.height; //元素的高度
this.$nextTick(() => {
resolve();
});
}).exec();
});
},
/* 获取列表数据 */
getListData() {
new Promise((resolve, reject) => {
uni.showLoading({
title: '加载中...'
});
setTimeout(() => {
const leftArray = this.apiData.map(item => item.typeName);
const mainArray = this.apiData.map(item => ({
title: item.typeName,
list: item.commVos
}));
resolve({ left: leftArray, main: mainArray });
uni.hideLoading();
}, 1000);
}).then(res => {
this.leftArray = res.left;
this.mainArray = res.main;
this.$nextTick(() => {
this.getElementTop();
});
}).catch(err => {
uni.hideLoading();
uni.showToast({
title: '数据加载失败',
icon: 'none'
});
console.error('数据请求失败:', err);
});
},
/* 获取元素顶部信息 */
getElementTop() {
new Promise((resolve, reject) => {
let view = uni.createSelectorQuery().selectAll('.main-item');
view.boundingClientRect(data => {
resolve(data);
}).exec();
}).then(res => {
let topArr = res.map(item => {
return item.top - this.scrollTopSize;
});
this.topArr = topArr;
let last = res[res.length - 1]?.height || 0;
console.log('===last>', last);
if (last - 20 < this.scrollHeight) {
this.fillHeight = this.scrollHeight - last + 20;
console.log('========>', this.fillHeight);
}
});
},
/* 主区域滚动监听 */
mainScroll(e) {
console.log('==>', e.detail.scrollTop, e);
let top = e.detail.scrollTop;
let index = 0;
console.log('->>>>>>>>', this.topArr);
/* 查找当前滚动距离 */
for (let i = this.topArr.length - 1; i >= 0; i--) {
console.log('===111', i);
if (top + 2 >= this.topArr[i]) {
console.log('===222', this.topArr[i], i);
index = i;
console.log('====7888', index);
break;
}
}
this.leftIndex = index < 0 ? 0 : index;
},
leftTap(e) {
let index = e.currentTarget.dataset.index;
console.log('===+++++>', index);
this.scrollInto = `item-${index}`;
this.leftIndex = index;
console.log('===+++++>', this.scrollInto);
}
}
};
</script>
更多推荐
所有评论(0)