联动的demo


<template>
  <view class="class">
    <!-- 一级分类 -->
    <scroll-view :scroll-top="maxTitle.scrollTop" scroll-y scroll-with-animation class="class-left no-scrollbar">
      <view @click="maxTitleTab(item.id, index)" v-for="(item, index) in maxTitle.list" :key="index" :class="{ checked: maxTitle.current === index }" class="item">
        <view class="line-overflow">{{ item.name }}</view>
      </view>
    </scroll-view>
    <view class="class-right">
      <!-- 二级分类 -->
      <scroll-view :scroll-left="minTitle.scrollLeft" scroll-x scroll-with-animation style="width: 100%" class="no-scrollbar">
        <view class="class-nav">
          <view @click="minTitleTab(item.id, index)" v-for="(item, index) in minTitle.list" :key="index" :class="{ checked: minTitle.current === index }" class="min-title-item item">{{
            item.name
          }}</view>
        </view>
      </scroll-view>
      <scroll-view @scroll="onScroll" :scroll-top="listModule.scrollTop" scroll-with-animation scroll-y enable-back-to-top class="content-scroll">
        <block v-for="(item, index) in listModule.list" :key="index">
          <view v-if="item.products.length" class="list-modele class-model">
            <view class="class-name">{{ item.cname }}</view>
            <view class="class-content">
              <view @click="toDetail(e.productId)" v-for="(e, i) in item.products" :key="i" class="product-item">
                <view class="logo">
                  <image :src="e.littleImage" lazy-load mode="aspectFill" />
                </view>
                <view class="content">
                  <view class="product-name line-overflow">{{ e.onlyName }}</view>
                  <view class="tip-view">
                    <view v-for="(tag, a) in e.tags" :key="a" class="tip">{{ tag }}</view>
                  </view>
                  <view class="remain">销量 {{ e.saleAmount || 0 }} · 库存 {{ e.stockAmount || 0 }}</view>
                  <view class="sale-money">
                    <text class="money">{{ e.salePriceYuan || '0.00' }}</text>
                    <text class="old-money" style="font-size: 13px">¥{{ e.originPriceYuan || '0.00' }}</text>
                    <!-- <text v-if="e.agentMoney" class="return-money">{{ e.agentMoney }}</text> -->
                  </view>
                </view>
                <text v-if="e.isSoldOut === 1" class="status">已售罄</text>
                <view v-else @click.stop="joinCart(`#join-cart${index}-${i}`, e, index, i)" class="join-cart" :id="`join-cart${index}-${i}`">
                  <image mode="widthFix" src="/static/images/home/join-cart.png"></image>
                  <view v-if="joinCartLoading[`${index}-${i}`]" @click.stop class="cart-loading"></view>
                </view>
              </view>
            </view>
          </view>
        </block>
      </scroll-view>
    </view>

    <!-- 购物车动画 -->
    <cart-animate ref="cartAnimate"></cart-animate>
  </view>
</template>

<script>
import cartAnimate from '@/component/cartAnimate.vue'

import { setCartNum, getProductCategories, getProductPageCategory, cartAdd } from '@/service/consumer'

export default {
  name: 'class',
  components: {
    cartAnimate
  },
  data() {
    return {
      httpErr: false,
      maxTitle: {
        list: [],
        scrollTop: 0,
        current: 0
      }, // 一级分类
      minTitle: {
        list: [], // 数据
        current: 0, // 选中索引
        domtArr: [], // dom信息
        scrollLeft: 0
      }, // 二级标题
      listModule: {
        list: [],
        heightArr: [],
        scrollTop: 0
      }, // 分类数据
      iscartAnimate: true, // 允许加入购物车
      joinCartLoading: {} // 购物车加载
    }
  },
  onLoad() {
    this.getClassTitle()
  },
  onShow() {
    if (this.httpErr) this.getClassTitle() // onLoad 错误继续请求
    setCartNum() // 设置购物车数量
  },
  methods: {
    // 竖向滚动
    onScroll(e) {
      const scrollTop = e.detail.scrollTop
      let minTitleCurrent = 0
      this.listModule.heightArr.forEach((item, index) => {
        if (parseInt(scrollTop) >= parseInt(item)) minTitleCurrent = index
      })
      this.minTitle.current = minTitleCurrent

      this.minTitleScroll(minTitleCurrent) // 二级分类滚动
    },
    // 去详情
    toDetail(productId = '') {
      uni.navigateTo({
        url: `/pages/consumer/home/detail?productId=${productId}`
      })
    },
    // 二级菜单点击
    minTitleTab(id, index) {
      this.minTitle.current = index
      this.minTitleScroll(index) // 二级分类滚动

      const scrollTop = this.listModule.scrollTop
      let curScrollTop = this.listModule.heightArr[index]
      scrollTop === curScrollTop && curScrollTop++ // 解决小程序兼容性
      this.listModule.scrollTop = curScrollTop
    },
    // 一级菜单点击
    maxTitleTab(id, index) {
      this.minTitle.list = this.maxTitle.list[index].sons || []
      this.maxTitle.current = index
      this.getListModule(id)
      this.minTitle.current = 0 // 二级分类重置
      this.minTitle.scrollLeft = 0 // 二级分类滚动位置重置
      this.listModule.scrollTop = 0 // 竖向滚动重置
      this.getMinTitleItemDom() //  获取二级标题dom信息集合

      if (index >= 2) {
        this.maxTitle.scrollTop = (index - 1) * uni.upx2px(126)
      } else {
        this.maxTitle.scrollTop = 0
      }
    },
    // 加入购物车
    joinCart(joinCartDom, row = {}, index = 0, i = 0) {
      if (!this.iscartAnimate)
        return uni.showToast({
          title: '别着急,慢慢来',
          duration: 1000,
          icon: 'none'
        })
      this.$set(this.joinCartLoading, `${index}-${i}`, true) // 当前加入loading显示
      this.iscartAnimate = false
      const { agentId, productId, productItemId } = row
      const body = {
        agentId,
        productId,
        productItemId,
        quantity: 1
      }
      cartAdd(body)
        .then(() => {
          this.$refs.cartAnimate.joinCart(joinCartDom) // 执行购物车动画
        })
        .finally(() => {
          this.iscartAnimate = true
          this.$set(this.joinCartLoading, `${index}-${i}`, false) // 当前加入loading关闭
          setCartNum() // 设置购物车数量
        })
    },
    // 获取分类列表
    getListModule(id = '') {
      this.listModule.list = []
      getProductPageCategory({ concurrentId: id }).then((res = {}) => {
        const { records = [] } = res
        this.listModule.list = records

        // 计算高度
        this.$nextTick(() => {
          uni
            .createSelectorQuery()
            .selectAll('.list-modele')
            .boundingClientRect((rect) => {
              let height = 0
              const domArr = []
              rect.forEach((item) => {
                domArr.push(height)
                height += item.height
              })
              this.listModule.heightArr = domArr
              console.log(this.listModule.heightArr, 'listModule')
            })
            .exec()
        })
      })
    },
    // 获取分类
    getClassTitle() {
      getProductCategories()
        .then((res = {}) => {
          const { records = [] } = res
          this.maxTitle.list = records
          console.log(this.maxTitle)
          // 默认列表渲染
          if (records.length) {
            this.minTitle.list = records[0].sons || []
            this.getListModule(records[0].id)
          }
        })
        .catch(() => {
          this.httpErr = true
        })
    },
    // 二级标题滚动
    minTitleScroll(index = 0) {
      if (index >= 2) {
        const domtArr = this.minTitle.domtArr
        const uselessLeft = domtArr[0].left // 无用的left
        this.minTitle.scrollLeft = domtArr[index].left - uselessLeft - (domtArr[index - 1].width + uni.upx2px(20))
        console.log('scrollLeft', this.minTitle.scrollLeft)
      } else {
        this.minTitle.scrollLeft = 0
      }
    },
    // 获取二级标题dom信息集合
    getMinTitleItemDom() {
      this.$nextTick(() => {
        uni
          .createSelectorQuery()
          .selectAll('.min-title-item')
          .boundingClientRect((rect) => {
            console.log('获取二级标题dom信息集合', rect)
            this.minTitle.domtArr = rect
          })
          .exec()
      })
    }
  }
}
</script>

<style lang="scss" scoped>
::-webkit-scrollbar {
  width: 0;
  height: 0;
  color: transparent;
}
.class {
  height: 100%;
  display: flex;
  &-left {
    margin-top: 20rpx;
    width: 170rpx;
    height: calc(100% - 20rpx);
    background: hsl(0, 0%, 96%);
    .item {
      position: relative;
      width: 100%;
      height: 126rpx;
      padding: 0 15rpx;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: bold;
      font-size: 26rpx;
      transition: background-color 0.3s;
      &::after {
        content: '';
        position: absolute;
        top: 50%;
        left: 0;
        width: 4rpx;
        height: 0;
        background: $main-color;
        transform: translateY(-50%);
        transition: height 0.3s;
      }
    }
    .checked {
      background: #fff;
      &::after {
        // content: '';
        height: 50rpx;
      }
    }
  }
  &-right {
    flex: 1;
    height: 100%;
    padding-top: 20rpx;
    padding-left: 30rpx;
    box-sizing: border-box;
    overflow: hidden;
    .class-nav {
      padding-bottom: 30rpx;
      display: inline-block;
      white-space: nowrap;
      .item {
        margin-right: 20rpx;
        min-width: 80rpx;
        height: 50rpx;
        border-radius: 5rpx;
        background: #f4f4f4;
        padding: 0 20rpx;
        display: inline-block;
        text-align: center;
        line-height: 50rpx;
        font-weight: 500;
        font-size: 26rpx;
        // transition: background-color 0.2s;
      }
      .checked {
        background: $main-color;
        color: #fff;
      }
    }
    .content-scroll {
      height: calc(100% - 80rpx);
    }
    .class-model {
      padding-bottom: 30rpx;
      .class-name {
        padding: 20rpx 0 0 0;
        font-weight: bold;
        font-size: 24rpx;
      }
      .class-content {
        padding-right: 30rpx;
        .product-item {
          position: relative;
          border-bottom: solid 1rpx #f2f2f2;
          padding: 50rpx 0;
          display: flex;
          .logo {
            margin-right: 30rpx;
            flex-shrink: 0;
            width: 150rpx;
            height: 150rpx;
            background: #f4f4f4;
            image {
              width: 100%;
              height: 100%;
            }
          }
          .content {
            width: calc(100% - 180rpx);
            .product-name {
              height: 46rpx;
              box-sizing: border-box;
              width: 100%;
              line-height: 1.5;
              font-weight: 500;
              font-size: 26rpx;
            }
            .tip-view {
              height: 30rpx;
              display: flex;
              align-items: center;
              .tip {
                margin-right: 10rpx;
                border-radius: 2rpx;
                background: rgba(242, 109, 48, 0.1);
                padding: 0 15rpx;
                line-height: 30rpx;
                font-weight: bold;
                font-size: 23rpx;
                color: #f26d30;
              }
            }
            .remain {
              flex-shrink: 0;
              height: 32rpx;
              padding-top: 15rpx;
              box-sizing: border-box;
              line-height: 1;
              font-size: 23rpx;
              color: #ccc;
            }
            .sale-money {
              padding-top: 6rpx;
              .money {
                position: relative;
                top: 2px;
              }
              .old-money {
                position: relative;
                top: 1px;
                padding-left: 15rpx;
                display: inline-block;
                font-size: 12px;
                color: #ccc;
                text-decoration: line-through;
              }
              .return-money {
                position: relative;
                top: -6rpx;
                margin-left: 15rpx;
                background: #f3b446;
                padding: 0 15rpx;
                display: inline-block;
                line-height: 30rpx;
                font-weight: bold;
                font-size: 12px;
                color: #fff;
              }
            }
          }
          .status {
            position: absolute;
            right: 0;
            bottom: 50rpx;
            font-size: 13px;
            color: #ccc;
          }
          .join-cart {
            position: absolute;
            right: -20rpx;
            bottom: 26rpx;
            padding: 27rpx 28rpx;
            image {
              width: 45rpx;
              height: 45rpx;
            }
            .cart-loading {
              position: absolute;
              top: 0;
              left: 0;
              width: 100%;
              height: 100%;
              background: rgba(255, 255, 255, 0.8);
              &::after {
                content: '';
                position: absolute;
                top: 0;
                right: 0;
                bottom: 0;
                left: 0;
                margin: auto;
                width: 36rpx;
                height: 36rpx;
                border-radius: 50%;
                border-bottom: solid 2rpx $main-color;
                box-sizing: border-box;
                animation: turn 1s linear infinite;
              }
            }
          }
        }
      }
    }
  }
}
</style>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351