左右菜单联动

发布时间 2024-01-04 15:31:33作者: 柯基与佩奇

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

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

主要利用的是 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 2s infinite ease-in-out;
  z-index: 1000;
}

.double-bounce2 {
  animation-delay: -1s;
}

@keyframes bounce {
  0%,
  100% {
    transform: scale(0);
  }
  50% {
    transform: scale(1);
  }
}
</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);
    });
  },
};