左右菜单联动

1,571 阅读6分钟

对于左右菜单联动的需求是很常见的在小程序里,主要表现为:

  • 点击左侧的菜单栏,右侧会切换到对应的内容区域
  • 滑动右侧的内容,左侧会自动切换到对应的菜单项

主要利用的是 scroll-view标签,以及相关的一些API,可参考:uniapp.dcloud.net.cn/api/ui/node… 去获取当前的所有节点集合,再配合 scroll-view 的 scroll-top 属性,使其在点击左侧菜单栏的时候动态赋值右侧scroll-view 的 scroll-top 属性,从而实现点击左侧菜单栏时右侧内容区域进行滚动

基本UI结构:

<template>
	<view class="d-flex border-top border-light-secondary" style="height: 100%; box-sizing: border-box;">
		<!-- 左侧菜单栏 -->
		<scroll-view scroll-y style="flex: 1; height: 100%;" class="border-right border-light-secondary">
			<view class="border-bottom border-light-secondary py-1" hover-class="bg-light-secondary"
				v-for="(item,index) in cate" :key="item.id" @click="changeCate(index)">
				<view class="py-1 font-md text-muted text-center" :class="activeIndex == index ? 'class-active' : ''">
					{{item.name}}
				</view>
			</view>
		</scroll-view>
		<!-- 右侧数据 -->
		<scroll-view scroll-y style="flex: 3.5; height: 100%;">
			<view class="row" v-for="(item,index) in list" :key="index">
				<view class="span-8 text-center py-2" v-for="(item2,index2) in item.list" :key="index2">
					<image :src="item2.src" mode="" style="width: 120upx;height: 120upx;"></image>
					<text class="d-block">{{item2.name}}</text>
				</view>
			</view>
		</scroll-view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				// 左侧菜单栏当前选中的分类
				activeIndex: 0,
				// 左侧菜单栏分类数据
				cate: [],
				// 右侧内容
				list: [],
				// 
			}
		},
		onLoad() {
			// 模拟左侧菜单栏分类数据
			for (let i = 0; i < 20; i++) {
				this.cate.push({
					name: "分类" + i,
					id: i
				})
			}
			
			// 模拟右侧内容数据
			for (let i = 0; i < 15; i++) {
				this.list.push({
					list: [{
							src: '/static/images/demo/cate_01.png',
							name: '商品一'
						},
						{
							src: '/static/images/demo/cate_02.png',
							name: '商品一'
						},
						{
							src: '/static/images/demo/cate_06.png',
							name: '商品一'
						},
						{
							src: '/static/images/demo/cate_05.png',
							name: '商品一'
						}
					]
				})
			}
		},
		methods: {
			// 点击左侧菜单栏,当前选中项高亮--切换
			changeCate(index) {
				this.activeIndex = index;
			},
			// 
		}
	}
</script>

<style lang="scss" scoped>
	.class-active {
		border-left: 8upx solid #FD6801;
		color: #FD6801 !important;
	}
</style>

image.png

点击左侧菜单栏-右侧内容滚动到对应区域:

<template>
	<view class="d-flex border-top border-light-secondary" style="height: 100%; box-sizing: border-box;">
		<!-- 左侧菜单栏 -->
		<scroll-view scroll-y style="flex: 1; height: 100%;" class="border-right border-light-secondary">
			<view class="border-bottom border-light-secondary py-1 left-scroll-item" hover-class="bg-light-secondary"
				v-for="(item,index) in cate" :key="item.id" @click="changeCate(index)">
				<view class="py-1 font-md text-muted text-center" :class="activeIndex == index ? 'class-active' : ''">
					{{item.name}}
				</view>
			</view>
		</scroll-view>

		<!-- 右侧数据 -->
		<scroll-view scroll-y style="flex: 3.5; height: 100%;" :scroll-top="rightScrollTop"
			:scroll-with-animation="true">
			<view class="row right-scroll-item" v-for="(item,index) in list" :key="index">
				<view class="span-8 text-center py-2" v-for="(item2,index2) in item.list" :key="index2">
					<image :src="item2.src" mode="" style="width: 120upx;height: 120upx;"></image>
					<text class="d-block">{{item2.name}}</text>
				</view>
			</view>
		</scroll-view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				// 左侧菜单栏当前选中的分类
				activeIndex: 0,
				// 左侧菜单栏分类数据
				cate: [],
				// 右侧内容
				list: [],
				// 记录左侧导航里的每一个导航栏距离顶部的距离
				leftDomsTop: [],
				// 记录右侧菜单距离顶部的距离
				rightDomsTop: [],
				// 右侧内容区块滚动的距离
				rightScrollTop: 0,
			}
		},
		// 页面加载中类似于created--获取不到DOM节点
		onLoad() {
			// 模拟右侧内容数据
			this.getData();
		},
		// 页面渲染完成-可获取DOM节点,相当于mounted
		onReady() {
			const query = uni.createSelectorQuery().in(this);
			// 左侧导航栏中的每一个导航栏距离顶部距离
			query.selectAll('.left-scroll-item').boundingClientRect(data => {
				this.leftDomsTop = data.map(v => v.top);
			}).exec();
			// 右侧内容中的每一个距离顶部距离
			query.selectAll('.right-scroll-item').boundingClientRect(data => {
				this.rightDomsTop = data.map(v => v.top);
			}).exec();
		},
		methods: {
			// 获取数据
			getData() {
				// 模拟左侧菜单栏分类数据
				for (let i = 0; i < 20; i++) {
					// 左侧导航
					this.cate.push({
						name: "分类" + i,
						id: i
					})
					// 右侧内容
					this.list.push({
						list: []
					})
					for (let i = 0; i < this.list.length; i++) {
						for (let j = 0; j < 24; j++) {
							this.list[i].list.push({
								src: '/static/images/demo/cate_01.png',
								name: '分类' + i + '-商品' + j
							})
						}
					}
				}
			},
			// 点击左侧菜单栏,当前选中项高亮--切换
			changeCate(index) {
				this.activeIndex = index;
				// 右边内容scroll-view滚动到对应的区块
				this.rightScrollTop = this.rightDomsTop[index];
			},
			// 
		}
	}
</script>

<style lang="scss" scoped>
	.class-active {
		border-left: 8upx solid #FD6801;
		color: #FD6801 !important;
	}
</style>

image.png

image.png

滚动右侧内容-左侧菜单栏跟着联动到对应菜单栏项

<template>
  <view class="d-flex border-top border-light-secondary" style="height: 100%; box-sizing: border-box;">
    <!-- 左侧菜单栏 -->
    <scroll-view scroll-y style="flex: 1; height: 100%;" class="border-right border-light-secondary" id="leftScroll" :scroll-top="leftScrollTop">
      <view class="border-bottom border-light-secondary py-1 left-scroll-item" hover-class="bg-light-secondary"
        v-for="(item,index) in cate" :key="item.id" @click="changeCate(index)">
        <view class="py-1 font-md text-muted text-center" :class="activeIndex == index ? 'class-active' : ''">
          {{item.name}}
        </view>
      </view>
    </scroll-view>

    <!-- 右侧数据 -->
    <scroll-view scroll-y style="flex: 3.5; height: 100%;" :scroll-top="rightScrollTop" :scroll-with-animation="true" @scroll="onRightScroll">
      <view class="row right-scroll-item" v-for="(item,index) in list" :key="index">
        <view class="span-8 text-center py-2" v-for="(item2,index2) in item.list" :key="index2">
          <image :src="item2.src" mode="" style="width: 120upx;height: 120upx;"></image>
          <text class="d-block">{{item2.name}}</text>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        // 加载效果
        showLoading:true,
        // 左侧菜单栏当前选中的分类
        activeIndex: 0,
        // 左侧菜单栏分类数据
        cate: [],
        // 右侧内容
        list: [],
        // 记录左侧导航里的每一个导航栏距离顶部的距离
        leftDomsTop: [],
        // 记录右侧菜单距离顶部的距离
        rightDomsTop: [],
        // 右侧内容区块滚动的距离
        rightScrollTop: 0,
        leftScrollTop:0,
        cateItemHeight: 0,
      }
    },
    // 页面加载中类似于created--获取不到DOM节点
    onLoad() {
      // 模拟右侧内容数据
      this.getData();
    },
    watch: {
      async activeIndex(newValue, oldValue) {
        // 获取scroll-view高度以及scrollTop
        const query = uni.createSelectorQuery().in(this);
        // 左侧导航栏中的每一个导航栏距离顶部距离
        query.select('#leftScroll').fields({
          size: true,
          scrollOffset: true
        }, data => {
         let H = data.height;
         let ST = data.scrollTop;
         // 下边
         if((this.leftDomsTop[newValue] + this.cateItemHeight) > (H+ST)){
           return this.leftScrollTop = this.leftDomsTop[newValue] + this.cateItemHeight - H;
         }
         // 上边
         if(ST > this.cateItemHeight){
           this.leftScrollTop = this.leftDomsTop[newValue];
         }
        }).exec();
      },
    },
    // 页面渲染完成-可获取DOM节点,相当于mounted
    onReady() {
      const query = uni.createSelectorQuery().in(this);
      // 左侧导航栏中的每一个导航栏距离顶部距离
      query.selectAll('.left-scroll-item').fields({
        size: true,
        rect: true
      }, data => {
        this.leftDomsTop = data.map(v => {
          this.cateItemHeight = v.height;
          return v.top;
        });
      }).exec();
      // 右侧内容中的每一个距离顶部距离
      query.selectAll('.right-scroll-item').boundingClientRect(data => {
        this.rightDomsTop = data.map(v => v.top);
      }).exec();
    },
    methods: {
      // 获取数据
      getData() {
        // 模拟左侧菜单栏分类数据
        for (let i = 0; i < 20; i++) {
          // 左侧导航
          this.cate.push({
            name: "分类" + i,
            id: i
          })
          // 右侧内容
          this.list.push({
            list: []
          })
          for(let i = 0; i < this.list.length; i++){
            for(let j = 0; j < 24; j++){
              this.list[i].list.push({
                  src: '/static/images/demo/cate_01.png',
                  name: '分类'+i+'-商品'+j
                })
            }
          }
        }
      },
      // 点击左侧菜单栏,当前选中项高亮--切换
      changeCate(index) {
        this.activeIndex = index;
        // 右边内容scroll-view滚动到对应的区块
        this.rightScrollTop = this.rightDomsTop[index];
      },
      // 监听右侧内容滚动事件
      async onRightScroll(e) {
        // console.log(e.detail.scrollTop);
        // 匹配当前scrollTop所处的索引
        this.rightDomsTop.forEach((v,k) => {
          if(v < e.detail.scrollTop + 3){
            this.activeIndex = k;
            return false;
          }
        })
      },
      // 
    }
  }
</script>

<style lang="scss" scoped>
  .class-active {
    border-left: 8upx solid #FD6801;
    color: #FD6801 !important;
  }
</style>

image.png

image.png

优化

对上面的代码进行优化重构--因为有一些代码是重复使用的比如 const query = uni.createSelectorQuery().in(this);

<template>
  <view class="d-flex border-top border-light-secondary" style="height: 100%; box-sizing: border-box;">
    <!-- 左侧菜单栏 -->
    <scroll-view scroll-y style="flex: 1; height: 100%;" class="border-right border-light-secondary" id="leftScroll"
      :scroll-top="leftScrollTop">
      <view class="border-bottom border-light-secondary py-1 left-scroll-item" hover-class="bg-light-secondary"
        v-for="(item,index) in cate" :key="item.id" @click="changeCate(index)">
        <view class="py-1 font-md text-muted text-center" :class="activeIndex == index ? 'class-active' : ''">
          {{item.name}}
        </view>
      </view>
    </scroll-view>

    <!-- 右侧数据 -->
    <scroll-view scroll-y style="flex: 3.5; height: 100%;" :scroll-top="rightScrollTop" :scroll-with-animation="true"
      @scroll="onRightScroll">
      <view class="row right-scroll-item" v-for="(item,index) in list" :key="index">
        <view class="span-8 text-center py-2" v-for="(item2,index2) in item.list" :key="index2">
          <image :src="item2.src" mode="" style="width: 120upx;height: 120upx;"></image>
          <text class="d-block">{{item2.name}}</text>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        // 加载效果
        showLoading: true,
        // 左侧菜单栏当前选中的分类
        activeIndex: 0,
        // 左侧菜单栏分类数据
        cate: [],
        // 右侧内容
        list: [],
        // 记录左侧导航里的每一个导航栏距离顶部的距离
        leftDomsTop: [],
        // 记录右侧菜单距离顶部的距离
        rightDomsTop: [],
        // 右侧内容区块滚动的距离
        rightScrollTop: 0,
        leftScrollTop: 0,
        cateItemHeight: 0,
      }
    },
    // 页面加载中类似于created--获取不到DOM节点
    onLoad() {
      // 模拟右侧内容数据
      this.getData();
    },
    watch: {
      async activeIndex(newValue, oldValue) {
        // 获取scroll-view高度以及scrollTop
        let data = await this.getElInfo({
        	size:true,
        	scrollOffset:true
        })
        let H = data.height;
        let ST = data.scrollTop;
        // 下边
        if ((this.leftDomsTop[newValue] + this.cateItemHeight) > (H + ST)) {
          return this.leftScrollTop = this.leftDomsTop[newValue] + this.cateItemHeight - H;
        }
        // 上边
        if (ST > this.cateItemHeight) {
          this.leftScrollTop = this.leftDomsTop[newValue];
        }
      },
    },
    // 页面渲染完成-可获取DOM节点,相当于mounted
    onReady() {
      this.getElInfo({
        all: 'left',
        size: true,
        rect: true
      }).then(data => {
        this.leftDomsTop = data.map(v => {
          this.cateItemHeight = v.height;
          return v.top;
        });
      })
      
      this.getElInfo({
        all: 'right',
        size: false,
        rect: true
      }).then(data => {
        this.rightDomsTop = data.map(v => v.top);
      })
    },
    methods: {
      // 获取节点信息
      getElInfo(obj = {}) {
        return new Promise((res, rej) => {
          let option = {
            size: obj.size ? true : false,
            rect: obj.rect ? true : false,
            scrollOffset: obj.scrollOffset ? true : false
          };
          const query = uni.createSelectorQuery().in(this);
          let q = obj.all ? query.selectAll(`.${obj.all}-scroll-item`) : query.select('#leftScroll');
          q.fields(option, data => {
            res(data);
          }).exec();
        })
      },
      // 获取数据
      getData() {
        // 模拟左侧菜单栏分类数据
        for (let i = 0; i < 20; i++) {
          // 左侧导航
          this.cate.push({
            name: "分类" + i,
            id: i
          })
          // 右侧内容
          this.list.push({
            list: []
          })
          for (let i = 0; i < this.list.length; i++) {
            for (let j = 0; j < 24; j++) {
              this.list[i].list.push({
                src: '/static/images/demo/cate_01.png',
                name: '分类' + i + '-商品' + j
              })
            }
          }
        }
      },
      // 点击左侧菜单栏,当前选中项高亮--切换
      changeCate(index) {
        this.activeIndex = index;
        // 右边内容scroll-view滚动到对应的区块
        this.rightScrollTop = this.rightDomsTop[index];
      },
      // 监听右侧内容滚动事件
      async onRightScroll(e) {
        // 匹配当前scrollTop所处的索引
        this.rightDomsTop.forEach((v, k) => {
          if (v < e.detail.scrollTop + 3) {
            this.activeIndex = k;
            return false;
          }
        })
      },
      // 
    }
  }
</script>

<style lang="scss" scoped>
  .class-active {
    border-left: 8upx solid #FD6801;
    color: #FD6801 !important;
  }
</style>

给分类页匹配加载动画效果

components/common/loading/loading.vue

<template>
	<view class="position-fixed top-0 left-0 right-0 bottom-0 loading-model"
	v-if="show">
		<view class="spinner">
		  <view class="double-bounce1"></view>
		  <view class="double-bounce2"></view>
		</view>
	</view>
</template>

<script>
	export default {
		props:{
			show:{
				type:Boolean,
				default:false
			}
		}
	}
</script>

<style scoped>
.loading-model{
	background: rgba(255, 255, 255, 0.6);
	z-index: 1000;
}
.spinner {
  width: 60px;
  height: 60px;
 
  position: relative;
  margin: 300upx auto;
  z-index: 1000;
}
 
.double-bounce1, .double-bounce2 {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background-color: #FD6801;
  opacity: 0.6;
  position: absolute;
  top: 0;
  left: 0;
  animation: bounce 2.0s infinite ease-in-out;
  z-index: 1000;
}
 
.double-bounce2 {
  animation-delay: -1.0s;
}
 
 
@keyframes bounce {
  0%, 100% {
    transform: scale(0.0);
  } 50% {
    transform: scale(1.0);
  }
}
</style>

main.js

// 引入全局加载动画
import loading from '@/components/common/loading/loading.vue';
Vue.component('loading', loading)
<template>
	<view class="d-flex border-top border-light-secondary" style="height: 100%; box-sizing: border-box;">
		<loading :show="showLoading"></loading>

		<!-- 左侧菜单栏 -->
		<scroll-view scroll-y style="flex: 1; height: 100%;" class="border-right border-light-secondary" id="leftScroll"
			:scroll-top="leftScrollTop">
			<view class="border-bottom border-light-secondary py-1 left-scroll-item" hover-class="bg-light-secondary"
				v-for="(item,index) in cate" :key="item.id" @click="changeCate(index)">
				<view class="py-1 font-md text-muted text-center" :class="activeIndex == index ? 'class-active' : ''">
					{{item.name}}
				</view>
			</view>
		</scroll-view>

		<!-- 右侧数据 -->
		<scroll-view scroll-y style="flex: 3.5; height: 100%;" :scroll-top="rightScrollTop"
			:scroll-with-animation="true" @scroll="onRightScroll">
			<view class="row right-scroll-item" v-for="(item,index) in list" :key="index">
				<view class="span-8 text-center py-2" v-for="(item2,index2) in item.list" :key="index2">
					<image :src="item2.src" mode="" style="width: 120upx;height: 120upx;"></image>
					<text class="d-block">{{item2.name}}</text>
				</view>
			</view>
		</scroll-view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				// 加载效果
				showLoading: true,

				// 左侧菜单栏当前选中的分类
				activeIndex: 0,
				// 左侧菜单栏分类数据
				cate: [],
				// 右侧内容
				list: [],
				// 记录左侧导航里的每一个导航栏距离顶部的距离
				leftDomsTop: [],
				// 记录右侧菜单距离顶部的距离
				rightDomsTop: [],
				// 右侧内容区块滚动的距离
				rightScrollTop: 0,
				leftScrollTop: 0,
				cateItemHeight: 0,
			}
		},
		// 页面加载中类似于created--获取不到DOM节点
		onLoad() {
			// 模拟右侧内容数据
			this.getData();
		},
		watch: {
			async activeIndex(newValue, oldValue) {
				// 获取scroll-view高度以及scrollTop
				let data = await this.getElInfo({
					size: true,
					scrollOffset: true
				})
				let H = data.height;
				let ST = data.scrollTop;
				// 下边
				if ((this.leftDomsTop[newValue] + this.cateItemHeight) > (H + ST)) {
					return this.leftScrollTop = this.leftDomsTop[newValue] + this.cateItemHeight - H;
				}
				// 上边
				if (ST > this.cateItemHeight) {
					this.leftScrollTop = this.leftDomsTop[newValue];
				}
			},
		},
		// 页面渲染完成-可获取DOM节点,相当于mounted
		onReady() {
			this.getElInfo({
				all: 'left',
				size: true,
				rect: true
			}).then(data => {
				this.leftDomsTop = data.map(v => {
					this.cateItemHeight = v.height;
					return v.top;
				});
			})

			this.getElInfo({
				all: 'right',
				size: false,
				rect: true
			}).then(data => {
				this.rightDomsTop = data.map(v => v.top);
			})
		},
		methods: {
			// 获取节点信息
			getElInfo(obj = {}) {
				return new Promise((res, rej) => {
					let option = {
						size: obj.size ? true : false,
						rect: obj.rect ? true : false,
						scrollOffset: obj.scrollOffset ? true : false
					};
					const query = uni.createSelectorQuery().in(this);
					let q = obj.all ? query.selectAll(`.${obj.all}-scroll-item`) : query.select('#leftScroll');
					q.fields(option, data => {
						res(data);
					}).exec();
				})
			},
			// 获取数据
			getData() {
				// 模拟左侧菜单栏分类数据
				for (let i = 0; i < 20; i++) {
					// 左侧导航
					this.cate.push({
						name: "分类" + i,
						id: i
					})
					this.$nextTick(() => {
						this.showLoading = false;
					})
					// 右侧内容
					this.list.push({
						list: []
					})
					for (let i = 0; i < this.list.length; i++) {
						for (let j = 0; j < 24; j++) {
							this.list[i].list.push({
								src: '/static/images/demo/cate_01.png',
								name: '分类' + i + '-商品' + j
							})
						}
					}
				}
			},
			// 点击左侧菜单栏,当前选中项高亮--切换
			changeCate(index) {
				this.activeIndex = index;
				// 右边内容scroll-view滚动到对应的区块
				this.rightScrollTop = this.rightDomsTop[index];
			},
			// 监听右侧内容滚动事件
			async onRightScroll(e) {
				// 匹配当前scrollTop所处的索引
				this.rightDomsTop.forEach((v, k) => {
					if (v < e.detail.scrollTop + 3) {
						this.activeIndex = k;
						return false;
					}
				})
			},
			// 
		}
	}
</script>

<style lang="scss" scoped>
	.class-active {
		border-left: 8upx solid #FD6801;
		color: #FD6801 !important;
	}
</style>

实际应用

<template>
	<view style="height: 100vh;" class="d-flex flex-column">
		<!-- #ifdef MP -->
		<!-- 自定义导航 -->
		<view class="d-flex a-center" style="height: 90rpx;">
			<!-- 左边 -->
			<view style="width: 85rpx;" class="d-flex a-center j-center">
				<text class="iconfont icon-xiaoxi"></text>
			</view>
			<!-- 中间 -->
			<view class="flex-1 bg-light rounded d-flex a-center text-light-muted" style="height: 65rpx;" @click="openSearch">
				<text class="iconfont icon-sousuo mx-2"></text>
				智能积木
			</view>
			<!-- 右边 -->
			<view style="width: 85rpx;" class="d-flex a-center j-center">
				<text class="iconfont icon-richscan_icon"></text>
			</view>
		</view>
		<!-- #endif -->
		<view class="d-flex border-top border-light-secondary animated fadeIn faster" style="height: 100%;box-sizing: border-box;">
			
			<loading-plus v-if="beforeReady"></loading-plus>
			
			<!-- <loading :show="showLoading"></loading> -->
			
			<scroll-view id="leftScroll" scroll-y style="flex: 1;height: 100%;" 
			class="border-right border-light-secondary" :scroll-top="leftScrollTop">
				<view class="border-bottom border-light-secondary py-1 left-scroll-item"
				hover-class="bg-light-secondary"
				v-for="(item,index) in cate" :key="index"
				@tap="changeCate(index)">
					<view class="py-1 font-md text-muted text-center"
					:class="activeIndex === index ? 'class-active' : ''">
						{{item.name}}</view>
				</view>
			</scroll-view>
			<scroll-view scroll-y style="flex: 3.5;height: 100%;" 
			:scroll-top="rightScrollTop" :scroll-with-animation="true"
			@scroll="onRightScroll">
				<view class="row right-scroll-item" 
				v-for="(item,index) in list" 
				:key="index">
					<view class="span24-8 text-center py-2"
					v-for="(item2,index2) in item.list" :key="index2"
					 @click="openDetail(item2)">
						<image :src="item2.cover"
						style="width: 120upx;height: 120upx;"></image>
						<text class="d-block">{{item2.name}}</text>
					</view>
				</view>
			</scroll-view>
		</view>
	</view>
</template>

<script>
	import loading from "@/common/mixin/loading.js"
	export default {
		mixins:[loading],
		data() {
			return {
				showLoading:true,
				// 当前选中的分类
				activeIndex:0,
				cate:[],
				list:[],
				leftDomsTop:[],
				rightDomsTop:[],
				rightScrollTop:0,
				leftScrollTop:0,
				cateItemHeight:0
			}
		},
		watch: {
			async activeIndex(newValue, oldValue) {
				// 获取scroll-view高度,scrollTop
				let data = await this.getElInfo({
					size:true,
					scrollOffset:true
				})
				let H = data.height
				let ST = data.scrollTop
				// 下边
				if ((this.leftDomsTop[newValue]+this.cateItemHeight) > (H+ST) ) {
					 return this.leftScrollTop = this.leftDomsTop[newValue]+this.cateItemHeight - H
				}
				// 上边
				if (ST > this.cateItemHeight) {
					this.leftScrollTop = this.leftDomsTop[newValue]
				}
			}
		},
		onLoad() {
			this.getData()
		},
		methods: {
			openSearch(){
				uni.navigateTo({
					url: '../search/search',
				});
			},
			// 获取节点信息
			getElInfo(obj = {}){
				return new Promise((res,rej)=>{
					let option = {
						size:obj.size ? true : false,
						rect:obj.rect ? true : false,
						scrollOffset:obj.scrollOffset ? true : false,
					}
					const query = uni.createSelectorQuery().in(this);
					let q = obj.all ? query.selectAll(`.${obj.all}-scroll-item`):query.select('#leftScroll')
					q.fields(option,data => {
					  res(data)
					}).exec();
				})
			},
			getData(){
				/*
				cate:[{
					name:"分类1"
				},{
					name:"分类2"
				}]
				
				list:[{
					list:[...]
				},{
					list:[...]
				}]
				*/
				this.$H.get('/category/app_category').then(res=>{
					var cate = []
					var list = []
					res.forEach(v=>{
						cate.push({
							id:v.id,
							name:v.name
						})
						list.push({
							list:v.app_category_items
						})
					})
					this.cate = cate
					this.list = list
					this.$nextTick(()=>{
						this.getElInfo({
							all:'left',
							size:true,
							rect:true
						}).then(data=>{
							this.leftDomsTop = data.map(v=>{
								this.cateItemHeight = v.height
								return v.top
							})
						})
						this.getElInfo({
							all:'right',
							size:false,
							rect:true
						}).then(data=>{
							this.rightDomsTop = data.map(v=> v.top)
						})
						this.showLoading = false
					})
				})
			},
			// 点击左边分类
			changeCate(index){
				this.activeIndex = index
				// 右边scroll-view滚动到对应区块
				this.rightScrollTop = this.rightDomsTop[index]
			},
			// 监听右边滚动事件
			async onRightScroll(e){
				// 匹配当前scrollTop所处的索引
				this.rightDomsTop.forEach((v,k)=>{
					if (v < e.detail.scrollTop + 3) {
						this.activeIndex = k
						return false
					}
				})
			},
			// 打开详情页
			openDetail(item){
				/*
				{
					"id":1,
					"name":"新品",
					"cover":"https://res.vmallres.com/pimages/product/6901443331376/428_428_FAF5BBAB67C16D7426B5B1A2A38F9001DED6D011A0EE9977mp.png",
					"category_id":1,
					"goods_id":25,
					"order":50,
					"create_time":"2019-08-17 00:57:12",
					"update_time":"2019-08-17 00:57:12"
				}
				*/
				uni.navigateTo({
					url: '../detail/detail?detail='+JSON.stringify({
						id:item.goods_id,
						title:item.name
					}),
				});
			}
		}
	}
</script>

<style>
.class-active{
	border-left: 8upx solid #FD6801;color: #FD6801!important;
}
</style>

image.png

common/mixin/loading-plus.vue

<template>
	<view class="position-fixed top-0 left-0 right-0 bottom-0 bg-white font-md d-flex a-center j-center main-text-color" style="z-index: 10000;">
		加载中...
	</view>
</template>

<script>
</script>

<style>
</style>

common/mixin/loading.js

export default {
	data(){
		return {
			beforeReady:true,
		}
	},
	onReady() {
		this.$nextTick(()=>{
			setTimeout(()=>{
				this.beforeReady = false
			},500)
		})
	},
}