uniapp的分类数据左右联动的实现

4,706 阅读2分钟

分类数据的左右联动是商城类app\小程序的常见使用的模式,如下图oppo商城所示:

1638839492684.gif 左边的一级菜单栏跟随着右边分类的数据同步滑动,右边的分类数据也会跟着左边的分类滑动展现。

使用到的核心组件有 scroll-view、使用的api有 uni.createSelectorQuery()、以及async\await语法。 完成的效果展示:

1638840954042.gif

步骤一 分类页面的布局和样式

这一步比较简单,所以不详细展开讲了。需要注意的是,为了隐藏滑动出现的滚动条,我们需要在app.vue中声明以下的样式,在其他页面中声明不生效:

<style>
    ::-webkit-scrollbar {
        display: none;
        width: 0 !important;
        height: 0 !important;
        -webkit-appearance: none;
        background: transparent;
    }
</style>

<template>
    <view class="content">
        <view class="search-box">
            <icon type="search" size="13" style="margin-right: 12rpx;"></icon>
            <text>搜索</text>
    </view>
    <view class="category-wrap">
            <scroll-view scroll-y scroll-with-animation class="category-bar">
                <view v-for="(item,index) in cateData" :key="index" class="category-item"
                    :class="[current === index ? 'current' : '']">
                    <text class="line-1">{{item.name}}</text>
                </view>
        </scroll-view>
        <scroll-view scroll-y scroll-with-animation scroll-with-animation class="category-content">
            <view style="padding: 16rpx;background-color: #f9f9f9;">
                    <view class="category-right-item" v-for="(item , index) in cateData" :key="index">
                        <view class="item-title">
                                <text>{{item.name}}</text>
                        </view>
                        <view class="item-container">
                            <view class="thumbnail-box" v-for="(item1, index1) in item.foods" :key="index1">
                                <image class="item-cate-image" :src="item1.icon"></image>
                                <view class="item-cate-name">{{item1.name}}</view>
                            </view>
                        </view>
                    </view>
                </view>
            </scroll-view>
        </view>
    </view>
</template>

<script>
    import cateData from '@/cateData.js'
    export default {
        data() {
            return {
                cateData: cateData, //分类数据结构
                current: 0,         //左边分类栏当前的选中的项
                scrollLeftTop: 0,   //左边分类栏项的高度
                cateItemHeight: 0,  //右边栏项的高度
                cateBarHeight: 0,   //左边分类栏的总高度 
                rightScrollArr: [], //右边栏每项高度组成的数组
                scrollRightTop: 0   //当前右边栏滚动的高度
            }
        },
        onReady() {

        },
        methods: {

        }
    }
</script>

<style lang="scss">
    page {
        font-size: 28rpx;
        color: #303133;
    }

    .content {
        display: flex;
        flex-direction: column;
        height: calc(100vh - var(--window-top));
    }

    .category-wrap {
        flex: 1;
        display: flex;
        overflow: hidden;
    }

    .category-bar {
        width: 170rpx;
        background-color: #f6f6f6;

        .category-item {
            text-align: center;
            height: 92rpx;
            line-height: 92rpx;

            &.current {
                color: #e93b3d;
                background-color: #FFFFFF;
            }
        }
    }

    .search-box {
        display: flex;
        align-items: center;
        margin: 24rpx;
        background-color: #eaeaea;
        border-radius: 50rpx;
        padding: 10rpx 16rpx;
        font-size: 26rpx;
        color: #909399;
    }

    .line-1 {
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
    }

    .category-right-item {
        margin-bottom: 30rpx;
        background-color: #fff;
        padding: 16rpx;
        border-radius: 8rpx;
    }

    .item-title {
        font-size: 26rpx;
        font-weight: bold;
        color: #303133;
        text-align: center;
    }

    .item-container {
        display: flex;
        flex-wrap: wrap;
    }

    .thumbnail-box {
        width: 33.33%;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        margin-top: 20rpx;
    }

    .item-cate-name {
        font-weight: normal;
        font-size: 24rpx;
        color: #303133;
    }

    .item-cate-image {
        width: 120rpx;
        height: 120rpx;
    }
</style>

微信截图_20211207102032.png

步骤二 在页面渲染完成后,计算出右边的分类数据的每一项距离顶部的距离

获取右边数据每个item到顶部的距离后放入数组中,数组的第一个元素为0。注意,我们需要在onReady生命周期内调用它。

onReady() {
    this.getCateItemTop()
},
methods: {
    getCateItemTop() {
        let query = uni.createSelectorQuery();
        query.selectAll('.category-right-item').boundingClientRect((rects) => {
                rects.forEach((rect) => {
                    this.rightScrollArr.push(rect.top - rects[0].top)
                })
        }).exec(function() {

        })
    },
}

步骤三 给左边的分类增加点击事件switchCate

这里使用了async\await语法,目的使switchCate成为异步函数,将函数中的getCateItemTop方法移出js任务执行队列而不影响后面的代码的执行。

async switchCate(index) {
        if (!this.rightScrollArr.length == 0) {
            await this.getCateItemTop();
        }
        if (index === this.current) return;
        //将右边的scroll高度重设
        this.scrollRightTop = this.rightScrollArr[index];
        this.cateDataStatus(index);
},
//设置 左边分类的滚动状态
cateDataStatus(index) {
   //当前选择的项
    this.current = index;
    // 如果为0,意味着尚未初始化,调用getElRect获取目标元素的高度
    if (this.cateBarHeight == 0 || this.cateItemHeight == 0) {
        this.getElRect('.category-bar', 'cateBarHeight');
        this.getElRect('.category-item', 'cateItemHeight');
    }
    //将该项垂直居中,算法: 左边scroll的scrollTop = 当前项的高度 - 分类栏的一半高度
    this.scrollLeftTop = index * this.cateItemHeight + this.cateItemHeight / 2 -    this.cateBarHeight / 2;
},
//获取一个目标元素的高度
getElRect(elClass, dataVal) {
    const query = uni.createSelectorQuery();
    query.select(elClass).fields({
            size: true
    }, res => {
            this[dataVal] = res.height;
    }).exec(function() {

    })
},

步骤四 给右边的滚动条添加滚动事件

在右边的scroll-view组件上添加@scroll="rightScroll",声明rightScroll方法

async rightScroll(e) {
    if (this.rightScrollArr.length == 0) {
        await this.getCateItemTop();
    }
    if (!this.cateBarHeight) {
        await this.getElRect('.category-bar', 'cateBarHeight')
    }
    // scrollHeight的值等于滚动位置加上左边栏的高度一半是为了始终拿的是中间显示的右边项做对比
    let scrollHeight = e.detail.scrollTop + this.cateBarHeight / 2;
    for (let i = 0; i < this.rightScrollArr.length; i++) {
        let height1 = this.rightScrollArr[i];
        let height2 = this.rightScrollArr[i + 1];
            //如果不存在height2,意味着数据循环已经到了最后一个,设置左边菜单为最后一项即可。
            if (!height2 || scrollHeight >= height1 && scrollHeight < height2) {
                this.cateDataStatus(i)
                return;
            }
    }
}