likes
comments
collection
share

写一个uniapp nvue与微信小程序通用的Tab组件(带下划线滑动变形动画)

作者站长头像
站长
· 阅读数 46

展示风格

1.按宽度自动展开(autoGrow、middleBorder) 写一个uniapp nvue与微信小程序通用的Tab组件(带下划线滑动变形动画) 2.顺序展示 写一个uniapp nvue与微信小程序通用的Tab组件(带下划线滑动变形动画) 3.超出宽度点击滑动自动居中 写一个uniapp nvue与微信小程序通用的Tab组件(带下划线滑动变形动画)

组件

<template>
  <view class="tab-wrap">
    <scroll-view
      scroll-with-animation
      :scroll-left="scrollLeft"
      enable-flex
      scroll-x
      :show-scrollbar="false"
      class="tab-scroll"
      :style="{ position, top }"
    >
      <view ref="tabs" class="tabs">
        <view
          v-for="(item, index) in options"
          :key="item.value"
          class="tab"
          :class="{
            autoGrow,
            middleBorder,
            active: item.value == value
          }"
          :style="{ padding: `0 ${space}px` }"
          @click="change(item, index)"
        >
          <text class="label" :class="'label' + index" :ref="`label${index}`">{{ item.label }}</text>
        </view>
      </view>
      <!-- 下划线 -->
      <view
        ref="underline"
        class="underline"
        :class="{ animationFlag }"
        :style="{
          left: underlineLeft + 'px',
          width: underlineWidth + 'px',
          bottom: underlineBottom
        }"
      ></view>
    </scroll-view>
    <!-- 占位元素,高度等于Tab的高度,防止脱离文档流高度塌陷,父组件使用时就不需要设置margin或者padding了 -->
    <view v-if="position == 'fixed'" class="placeholder"></view>
  </view>
</template>

<script>
  export default {
    props: {
      // 选中值
      value: {
        type: [Number, String],
        default: ''
      },
      // 选项
      options: {
        type: Array,
        default: () => []
        // [{label: '推荐', value: 1}]
      },
      // 定位
      position: {
        type: String,
        default: ''
      },
      // 脱离文档流后,距离顶部距离
      top: {
        type: String,
        default: '0'
      },
      // 自动展开
      autoGrow: {
        type: Boolean,
        default: false
      },
      // 下划线到底部的距离
      underlineBottom: {
        type: String,
        default: '0'
      },
      // item中间的边框
      middleBorder: {
        type: Boolean,
        default: false
      },
      // 每一项item两侧的padding(px)
      space: {
        type: Number,
        default: 15
      }
    },
    data() {
      return {
        scrollLeft: '', // scroll-view的scrollLeft
        underlineLeft: 0, // 下划线距离左侧的距离
        underlineWidth: 0, // 下划线宽度
        tabsWidth: 0, // tabs的总宽度
        animationFlag: false // 动画开关,首次设置下划线时,动画为false
      }
    },
    watch: {
      // 监听options,有数据后延迟100ms获取Dom(会存在options是请求后端接口获取的)
      options: {
        handler(options) {
          if (this.options.length) {
            // 延迟100ms获取Dom
            const timer = setTimeout(() => {
              // 获取Dom信息完成后,将和value对应的tab选中
              const item = options.find(({ value }) => value == this.value)
              this.getDom().then(() => {
                // 设置下划线的样式
                this.setLineStyle(item)
                // 设置scroll-view的scrollLeft
                this.setScrollLeft(item)
              })
              clearTimeout(timer)
            }, 100)
          }
        },
        immediate: true,
        deep: true
      }
    },
    methods: {
      // 点击Tab
      change(item) {
        // 打开动画开关
        this.animationFlag = true
        // 修改v-model的值
        this.$emit('input', item.value)
        // 设置下划线的样式
        this.setLineStyle(item)
        // 设置scroll-view的scrollLeft
        this.setScrollLeft(item)
        // 向父组件出发change事件
        this.$emit('change', item)
      },
      // 获取Tab的Dom信息
      getDom() {
        const tabPromiseList = this.options.map((item, index) => {
          return this.getRect(`label${index}`).then((size) => {
            // 一个tab的宽度为tab的实际宽度+两侧的padding
            this.tabsWidth += size.width + this.space * 2
            // 将Dom信息放回options中
            Object.assign(item, size)
          })
        })
        return Promise.all(tabPromiseList)
      },
      // 封装小程序和nvue App通用的获取Dom的方法
      getRect(el) {
        let p = null
        // #ifdef APP-NVUE
        const dom = uni.requireNativePlugin('dom')
        p = new Promise((resolve) => {
          // this.$refs[el] 有多个的话是数组,否则是对象
          dom.getComponentRect(this.$refs[el][0] || this.$refs[el], ({ size }) => resolve(size))
        })
        // #endif

        // #ifdef MP-WEIXIN
        p = new Promise((resolve) => {
          uni.createSelectorQuery().in(this).select(`.${el}`).boundingClientRect().exec(([res]) => {
            resolve(res)
          })
        })
        // #endif
        return p
      },
      // 设置下划线的样式
      setLineStyle({ left, width }) {
        // 下划线距离左侧的距离
        this.underlineLeft = left
        // 下划线宽度
        this.underlineWidth = width
      },
      // 设置scroll-view的scrollLeft
      setScrollLeft({ left, width }) {
        // 屏幕宽度
        const windowWidth = uni.getSystemInfoSync().safeArea.width
        // scrollLeft的最大值为整个scroll-view宽度 - tabs组件的宽度
        const maxScrollLeft = this.tabsWidth - windowWidth
        // 取两者最大值,不然App端会出现最后一个tab偏移到中间
        const scrollLeft = Math.min(left - windowWidth / 2 + width / 2, maxScrollLeft)
        // 小于0时要设置为0,不能为负数,不然App端会出现第一个tab偏移到中间
        this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft
      }
    }
  }
</script>

<style lang="scss" scoped>
  .tab-wrap {
    .tab-scroll {
      left: 0;
      right: 0;
      background: #fff;
      z-index: 1;
      .tabs {
        /* #ifdef MP-WEIXIN */
        display: flex;
        /* #endif */
        flex-direction: row;
        align-items: center;
        .tab {
          /* #ifdef MP-WEIXIN */
          display: flex;
          /* #endif */
          align-items: center;
          justify-content: center;
          height: 100rpx;
          position: relative;
          .label {
            font-size: 30rpx;
            color: #666;
            transition-property: color;
            transition-timing-function: ease;
            transition-duration: 300ms;
          }
          &.active .label {
            color: #ea5514;
          }
          &.autoGrow {
            flex: 1;
          }
          &.middleBorder {
            border-right: 1px solid #eee;
          }
        }
      }
      .underline {
        position: absolute;
        height: 2px;
        border-radius: 100rpx;
        background: #ea5514;
        z-index: 2;
        transition-property: left, width;
        transition-timing-function: ease;
        &.animationFlag {
          transition-duration: 300ms;
        }
      }
    }
    .placeholder {
      height: 100rpx;
    }
  }
</style>

使用

<template>
  <m-tab :options="tabs" v-model='tabValue'/>
</template>

<script>
  export default {
    data () {
      return {
        tabValue: 1,
        tabs: [
          {label: '推荐', value: 1},
          {label: '爱心捐助', value: 2},
          {label: '话费充值', value: 3},
          {label: '电子产品', value: 4},
          {label: '数码产品', value: 5},
          {label: '厨房用品', value: 6},
          {label: '办公用品', value: 7},
          {label: '玩具', value: 8},
          {label: '户外运动', value: 9},
          {label: '美食小吃', value: 10}
        ]
      }
    }
  }
</script>

<style lang='scss' scoped></style>
转载自:https://juejin.cn/post/7197694584108056635
评论
请登录