本文使用 uni-app 实现左侧分类 右侧商品列表联动滚动效果支持点击左侧自动定位右侧对应分类滚动右侧自动切换左侧选中项动态计算滚动高度适配不同屏幕运动装备商城实战演示双向联动实现左侧点击通过scroll-into-view定位右侧对应 ID右侧滚动监听scroll事件对比topArr自动切换左侧下标动态高度计算使用uni.createSelectorQuery()获取容器真实高度自动适配不同手机屏幕避免写死高度吸顶标题右侧分类标题使用position:sticky实现吸顶效果以下是代码示例1.template 结构template view classcontainer view classscroll-panel idscroll-panel view classlist-box !-- 左侧分类 -- view classleft scroll-view scroll-y :style{ height: scrollHeight px } :scroll-into-viewleftIntoView view classitem v-for(item, index) in leftArray :keyindex :class{ active: index leftIndex } :idleft- index :data-indexindex tapleftTap view classactivelink/view text classitem-name{{ item }}/text /view /scroll-view /view !-- 右侧商品列表 -- view classmain scroll-view scroll-y :style{ height: scrollHeight px } scrollmainScroll :scroll-into-viewscrollInto scroll-with-animation view classitem main-item v-for(item, index) in mainArray :keyindex :iditem- index view classtitle view{{ item.title }}/view /view view classgoods v-for(item2, index2) in item.list :keyindex2 view classorderlist-list view classlist-left image :srcitem2.imgUrl modeaspectFill/image /view view classlist-right view classlist-name{{item2.name}}/view view styledisplay: flex; v-ifitem2.calorie view classlist-meuns{{item2.calorie}}/view view stylemargin-left: 10rpx;千卡/小时/view /view view classprice v-ifitem2.price¥{{item2.price}}/view /view /view /view /view view classfill-last :style{ height: fillHeight px }/view /scroll-view /view /view /view /view /template2. 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
uni-app 实现左右联动分类列表(运动装备商城示例)
本文使用 uni-app 实现左侧分类 右侧商品列表联动滚动效果支持点击左侧自动定位右侧对应分类滚动右侧自动切换左侧选中项动态计算滚动高度适配不同屏幕运动装备商城实战演示双向联动实现左侧点击通过scroll-into-view定位右侧对应 ID右侧滚动监听scroll事件对比topArr自动切换左侧下标动态高度计算使用uni.createSelectorQuery()获取容器真实高度自动适配不同手机屏幕避免写死高度吸顶标题右侧分类标题使用position:sticky实现吸顶效果以下是代码示例1.template 结构template view classcontainer view classscroll-panel idscroll-panel view classlist-box !-- 左侧分类 -- view classleft scroll-view scroll-y :style{ height: scrollHeight px } :scroll-into-viewleftIntoView view classitem v-for(item, index) in leftArray :keyindex :class{ active: index leftIndex } :idleft- index :data-indexindex tapleftTap view classactivelink/view text classitem-name{{ item }}/text /view /scroll-view /view !-- 右侧商品列表 -- view classmain scroll-view scroll-y :style{ height: scrollHeight px } scrollmainScroll :scroll-into-viewscrollInto scroll-with-animation view classitem main-item v-for(item, index) in mainArray :keyindex :iditem- index view classtitle view{{ item.title }}/view /view view classgoods v-for(item2, index2) in item.list :keyindex2 view classorderlist-list view classlist-left image :srcitem2.imgUrl modeaspectFill/image /view view classlist-right view classlist-name{{item2.name}}/view view styledisplay: flex; v-ifitem2.calorie view classlist-meuns{{item2.calorie}}/view view stylemargin-left: 10rpx;千卡/小时/view /view view classprice v-ifitem2.price¥{{item2.price}}/view /view /view /view /view view classfill-last :style{ height: fillHeight px }/view /scroll-view /view /view /view /view /template2. 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