微信小程序仿点餐二级联动页面
作为page使用
wxml代码
html
<view class="wrap">
<!-- 左侧分类 -->
<scroll-view class="left" scroll-y scroll-with-animation scroll-top="{{leftScrollTop}}">
<view wx:for="{{categoryList}}" wx:key="id" id="left-{{index}}" class="left-item {{currentIndex === index ? 'active' : ''}}" bindtap="selectCategory" data-index="{{index}}">
{{item.name}}
</view>
</scroll-view>
<!-- 右侧商品 -->
<scroll-view class="right" scroll-y scroll-with-animation scroll-top="{{rightScrollTop}}" bindscroll="onScroll">
<view wx:for="{{categoryList}}" wx:key="id" class="section">
<view class="section-title">{{item.name}}</view>
<view wx:for="{{item.goods}}" wx:for-item="good" wx:key="id" class="goods-item">
<view class="info">
<text class="name">{{good.name}}</text>
<text class="price">¥{{good.price}}</text>
</view>
</view>
</view>
</scroll-view>
</view>wxss代码
css
/* 整页 100% 高度 */
page{height:100%}
.wrap{
display:flex;
height:100%;
}
/* 左侧 */
.left{
width:200rpx;
background:#f7f7f7;
}
.left-item{
padding:30rpx 20rpx;
text-align:center;
font-size:28rpx;
color:#333;
}
.left-item.active{
background:#fff;
color:#ff5f00;
font-weight:bold;
}
/* 右侧 */
.right{
flex:1;
background:#fff;
}
.section-title{
padding:20rpx;
background:#f5f5f5;
font-size:30rpx;
color:#333;
}
.goods-item{
display:flex;
padding:20rpx;
border-bottom:1rpx solid #eee;
}
.pic{
width:120rpx;
height:120rpx;
margin-right:20rpx;
border-radius:8rpx;
}
.info{
display:flex;
flex-direction:column;
justify-content:center;
}
.name{
font-size:30rpx;
color:#333;
}
.price{
font-size:28rpx;
color:#ff5f00;
margin-top:10rpx;
}js代码
js
let timer = null; // 用于存储节流定时器
let scrollTop = 0;
Page({
data: {
categoryList: [],
currentIndex: 0,
rightScrollTop: 0,
leftScrollTop: 0,
heightArr: [], // 右侧各分类绝对高度
lockScroll: false, // 防止 onScroll 在手动滚动期间干扰
jumpNum: 0, // 点击左侧按钮,让目标标签滚动到距离上方第几个格子,0代表第0个格子,也就是直接滚动到最上方
},
onLoad() {
// 这个高度可以做个回调函数,每次点击左侧或者滑动右侧都计算一下高度,返回了数据之后在执行后面的,因为我这里数据都是死数据,所以没有动态获取,理论是动态获取的
this.setData({
categoryList: this.mockData()
}, this.calcHeight);
},
/* 点击左侧分类:右侧跳到对应分类顶部,左侧滚到可视中间 */
async selectCategory(e) {
const index = e.currentTarget.dataset.index;
/* 1. 右侧滚到分类顶部 */
this.setData({
currentIndex: index,
lockScroll: true
});
// 左侧跟随滚动
this.scrollLeft(index)
await this.sleep(300)
this.setData({
rightScrollTop: this.data.heightArr[this.data.currentIndex],
})
await this.sleep(1000)
this.setData({
lockScroll: false
})
},
scrollLeft(index) {
wx.createSelectorQuery()
.in(this)
.select('.left')
.scrollOffset(res => {
const currentScrollTop = res.scrollTop;
wx.createSelectorQuery()
.in(this)
.select(`#left-${index}`) // 关键:这里改成真正的 class
.boundingClientRect(rect => {
if (!rect) return;
const h = rect.height; // 单行真实高度
const need = currentScrollTop + rect.top - this.data.jumpNum * h; // 向上多留两个
this.setData({
leftScrollTop: Math.max(0, need)
});
})
.exec();
})
.exec();
},
// 防抖函数
sleep(ms) {
clearTimeout(timer);
timer = null
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
resolve();
}, ms);
});
},
/* 右侧滚动时联动左侧高亮(带锁)*/
async onScroll(e) {
if(this.data.lockScroll) return
scrollTop = e.detail.scrollTop;
await this.sleep(800)
const arr = this.data.heightArr;
let current = 0;
for (let i = 0; i < arr.length; i++) {
if (scrollTop >= arr[i] - 50) current = i;
else break;
}
if (current !== this.data.currentIndex) {
this.setData({
currentIndex: current
});
// 左侧跟随滚动
this.scrollLeft(current)
}
},
/* 计算右侧每个分类到顶部绝对高度 */
calcHeight() {
const query = wx.createSelectorQuery().in(this);
let acc = 0;
let arr = [0];
query.selectAll('.section').boundingClientRect(rects => {
rects.forEach(item => {
acc += item.height;
arr.push(acc);
});
this.setData({
heightArr: arr
});
}).exec();
},
/* 30 个分类 Demo */
mockData() {
const names = Array.from({
length: 30
}, (_, i) => `分类${i + 1}`);
return names.map((n, idx) => ({
id: idx + 1,
name: n,
goods: Array.from({
length: 6
}).map((_, i) => ({
id: `${idx}-${i}`,
name: `${n}-商品${i + 1}`,
price: (Math.random() * 30 + 5).toFixed(2),
}))
}));
}
});作为组件使用
组件相关代码
组件的wxml文件
html
<view class="wrap">
<!-- 左侧分类 -->
<scroll-view class="left" scroll-y scroll-with-animation scroll-top="{{leftScrollTop}}">
<view wx:for="{{categoryList}}" wx:key="id" class="left-item left-{{index}} {{currentIndex === index ? 'active' : ''}}" bindtap="selectCategory" data-index="{{index}}">
{{item.name}}
</view>
</scroll-view>
<!-- 右侧商品 -->
<scroll-view class="right" scroll-y scroll-with-animation scroll-top="{{rightScrollTop}}" bindscroll="onScroll">
<view wx:for="{{categoryList}}" wx:key="id" class="section">
<view class="section-title">{{item.name}}</view>
<view wx:for="{{item.goods}}" wx:for-item="good" wx:key="id" class="goods-item">
<view class="info">
<text class="name">{{good.name}}</text>
<text class="price">¥{{good.price}}</text>
</view>
</view>
</view>
</scroll-view>
</view>组件的wxss文件
css
/* 整页 100% 高度 */
page{height:100%}
.wrap{
display:flex;
height:100%;
}
/* 左侧 */
.left{
width:200rpx;
background:#f7f7f7;
}
.left-item{
padding:30rpx 20rpx;
text-align:center;
font-size:28rpx;
color:#333;
}
.left-item.active{
background:#fff;
color:#ff5f00;
font-weight:bold;
}
/* 右侧 */
.right{
flex:1;
background:#fff;
}
.section-title{
padding:20rpx;
background:#f5f5f5;
font-size:30rpx;
color:#333;
}
.goods-item{
display:flex;
padding:20rpx;
border-bottom:1rpx solid #eee;
}
.pic{
width:120rpx;
height:120rpx;
margin-right:20rpx;
border-radius:8rpx;
}
.info{
display:flex;
flex-direction:column;
justify-content:center;
}
.name{
font-size:30rpx;
color:#333;
}
.price{
font-size:28rpx;
color:#ff5f00;
margin-top:10rpx;
}组件的js文件
js
// components/testDemo/testDemo.js
let timer = null; // 用于存储节流定时器
let flag = true; // 节流阀
let scrollTop = 0;
let timer2 = null;
Component({
/**
* 组件的属性列表
*/
properties: {
categoryList: {
type: Array,
value: []
} // 数据列表
},
/**
* 组件的初始数据
*/
data: {
currentIndex: 0,
rightScrollTop: 0,
leftScrollTop: 0,
heightArr: [], // 右侧各分类绝对高度
lockScroll: false, // 点击左侧时禁止右侧滚动事件
jumpNum: 3, // 点击左侧按钮,让目标标签滚动到距离上方第几个格子,0代表第0个格子,也就是直接滚动到最上方
},
lifetimes: {
ready() {
console.log(JSON.stringify(this.data.categoryList))
// 这个高度可以做个回调函数,每次点击左侧或者滑动右侧都计算一下高度,返回了数据之后在执行后面的,因为我这里数据都是死数据,所以没有动态获取,理论是动态获取的
this.calcHeight()
}
},
/**
* 组件的方法列表
*/
methods: {
/* 点击左侧分类:右侧跳到对应分类顶部,左侧滚到可视中间 */
selectCategory(e) {
const index = e.currentTarget.dataset.index;
/* 1. 右侧滚到分类顶部 */
this.setData({
currentIndex: index,
lockScroll: true
});
// 左侧跟随滚动
this.scrollLeft(index)
await this.sleep(300)
this.setData({
rightScrollTop: this.data.heightArr[this.data.currentIndex],
})
await this.sleep(1000)
this.setData({
lockScroll:false
})
},
scrollLeft(index) {
wx.createSelectorQuery()
.in(this)
.select('.left')
.scrollOffset(res => {
const currentScrollTop = res.scrollTop;
wx.createSelectorQuery()
.in(this)
.select(`.left-${index}`) // 关键:这里改成真正的 class
.boundingClientRect(rect => {
if (!rect) return;
const h = rect.height; // 单行真实高度
const need = currentScrollTop + rect.top - this.data.jumpNum * h; // 向上多留两个
this.setData({
leftScrollTop: Math.max(0, need)
});
})
.exec();
})
.exec();
},
// 防抖函数
sleep(ms) {
clearTimeout(timer);
timer = null
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
resolve();
}, ms);
});
},
/* 右侧滚动时联动左侧高亮(带锁)*/
async onScroll(e) {
if(this.data.lockScroll) return
scrollTop = e.detail.scrollTop;
await this.sleep(800)
const arr = this.data.heightArr;
let current = 0;
for (let i = 0; i < arr.length; i++) {
if (scrollTop >= arr[i] - 50) current = i;
else break;
}
if (current !== this.data.currentIndex) {
this.setData({
currentIndex: current
});
/* 让左侧跟着滚动 */
this.scrollLeft(current)
}
},
/* 计算右侧每个分类到顶部绝对高度 */
calcHeight() {
const query = wx.createSelectorQuery().in(this);
let acc = 0;
let arr = [0];
query.selectAll('.section').boundingClientRect(rects => {
rects.forEach(item => {
acc += item.height;
arr.push(acc);
});
this.setData({
heightArr: arr
});
}).exec();
},
}
})页面使用相关
页面的json文件
json
{
"usingComponents": {
"test_demo":"/components/testDemo/testDemo"
}
}页面的wxml文件
html
<test_demo categoryList="{{categoryList}}"></test_demo>页面的js文件
js
Page({
data: {
categoryList: [],
},
onLoad() {
this.setData({
categoryList: this.mockData()
});
},
/* 30 个分类 Demo */
mockData() {
const names = Array.from({
length: 30
}, (_, i) => `分类${i + 1}`);
return names.map((n, idx) => ({
id: idx + 1,
name: n,
goods: Array.from({
length: 6
}).map((_, i) => ({
id: `${idx}-${i}`,
name: `${n}-商品${i + 1}`,
price: (Math.random() * 30 + 5).toFixed(2)
}))
}));
}
});无css文件
