<template> 
 | 
  <view class="tn-tabs-class tn-tabs" :class="[backgroundColorClass]" :style="{backgroundColor: backgroundColorStyle, marginTop: $t.string.getLengthUnitValue(top, 'px')}"> 
 | 
     
 | 
    <!-- _tgetRect()对组件根节点无效,因为写了.in(this),故这里获取内层接点尺寸 --> 
 | 
    <view :id="id"> 
 | 
      <scroll-view scroll-x class="tn-tabs__scroll-view" :scroll-left="scrollLeft" scroll-with-animation> 
 | 
        <view class="tn-tabs__scroll-view__box" :class="{'tn-tabs__scroll-view--flex': !isScroll}"> 
 | 
          <!-- item --> 
 | 
          <view 
 | 
            v-for="(item, index) in list" 
 | 
            :key="index" 
 | 
            :id="'tn-tabs__scroll-view__item-' + index" 
 | 
            class="tn-tabs__scroll-view__item tn-text-ellipsis" 
 | 
            :style="[tabItemStyle(index)]" 
 | 
            @tap="clickTab(index)" 
 | 
          > 
 | 
            <tn-badge v-if="item[count] || item['count']" backgroundColor="tn-bg-red" fontColor="#FFFFFF" :absolute="true" :top="badgeOffset[0] || 0" :right="badgeOffset[1] || 0">{{ item[count] || item['count']}}</tn-badge> 
 | 
            {{ item[name] || item['name'] }} 
 | 
          </view> 
 | 
           
 | 
          <!-- 底部滑块 --> 
 | 
          <view v-if="showBar" class="tn-tabs__bar" :style="[tabBarStyle]"></view> 
 | 
        </view> 
 | 
      </scroll-view> 
 | 
    </view> 
 | 
  </view> 
 | 
</template> 
 | 
  
 | 
<script> 
 | 
  import componentsColor from '../../libs/mixin/components_color.js' 
 | 
  export default { 
 | 
    mixins: [componentsColor], 
 | 
    name: 'tn-tabs', 
 | 
    props: { 
 | 
      // 标签列表 
 | 
      list: { 
 | 
        type: Array, 
 | 
        default() { 
 | 
          return [] 
 | 
        } 
 | 
      }, 
 | 
      // 列表数据tab名称的属性 
 | 
      name: { 
 | 
        type: String, 
 | 
        default: 'name' 
 | 
      }, 
 | 
      // 列表数据微标数量的属性 
 | 
      count: { 
 | 
        type: String, 
 | 
        default: 'count' 
 | 
      }, 
 | 
      // 当前活动的tab索引 
 | 
      current: { 
 | 
        type: Number, 
 | 
        default: 0 
 | 
      }, 
 | 
      // 菜单是否可以滑动 
 | 
      isScroll: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 高度 
 | 
      height: { 
 | 
        type: Number, 
 | 
        default: 80 
 | 
      }, 
 | 
      // 距离顶部的距离(px) 
 | 
      top: { 
 | 
        type: Number, 
 | 
        default: 0 
 | 
      }, 
 | 
      // item的宽度 
 | 
      itemWidth: { 
 | 
        type: [String, Number], 
 | 
        default: 'auto' 
 | 
      }, 
 | 
      // 过渡动画时长 
 | 
      duration: { 
 | 
        type: Number, 
 | 
        default: 0.3 
 | 
      }, 
 | 
      // 选中时的颜色 
 | 
      activeColor: { 
 | 
        type: String, 
 | 
        default: '#01BEFF' 
 | 
      }, 
 | 
      // 未被选中时的颜色 
 | 
      inactiveColor: { 
 | 
        type: String, 
 | 
        default: '#080808' 
 | 
      }, 
 | 
      // 选中的item样式 
 | 
      activeItemStyle: { 
 | 
        type: Object, 
 | 
        default() { 
 | 
          return {} 
 | 
        } 
 | 
      }, 
 | 
      // 是否显示底部滑块 
 | 
      showBar: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 底部滑块的宽度 
 | 
      barWidth: { 
 | 
        type: Number, 
 | 
        default: 40 
 | 
      }, 
 | 
      // 底部滑块的高度 
 | 
      barHeight: { 
 | 
        type: Number, 
 | 
        default: 6 
 | 
      }, 
 | 
      // 自定义底部滑块的样式 
 | 
      barStyle: { 
 | 
        type: Object, 
 | 
        default() { 
 | 
          return {} 
 | 
        } 
 | 
      }, 
 | 
      // 单个tab的左右内边距 
 | 
      gutter: { 
 | 
        type: Number, 
 | 
        default: 30 
 | 
      }, 
 | 
      // 微标的偏移数[top, right] 
 | 
      badgeOffset: { 
 | 
        type: Array, 
 | 
        default() { 
 | 
          return [20, 22] 
 | 
        } 
 | 
      }, 
 | 
      // 是否加粗字体 
 | 
      bold: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      } 
 | 
    }, 
 | 
    computed: { 
 | 
      // 底部滑块样式 
 | 
      tabBarStyle() { 
 | 
        let style = { 
 | 
          width: this.$t.string.getLengthUnitValue(this.barWidth), 
 | 
          height: this.$t.string.getLengthUnitValue(this.barHeight), 
 | 
          borderRadius: `${this.barHeight / 2}rpx`, 
 | 
          backgroundColor: this.activeColor, 
 | 
          opacity: this.barMoveFirst ? 0 : 1, 
 | 
          transform: `translate(${this.scrollBarLeft}px, -100%)`, 
 | 
          transitionDuration: this.barMoveFirst ? '0s' : `${this.duration}s` 
 | 
        } 
 | 
        Object.assign(style, this.barStyle) 
 | 
        return style 
 | 
      }, 
 | 
      // tabItem样式 
 | 
      tabItemStyle() { 
 | 
        return index => { 
 | 
          let style = { 
 | 
            width: this.$t.string.getLengthUnitValue(this.itemWidth), 
 | 
            height: this.$t.string.getLengthUnitValue(this.height), 
 | 
            lineHeight: this.$t.string.getLengthUnitValue(this.height), 
 | 
            fontSize: this.fontSizeStyle || '28rpx', 
 | 
            padding: this.isScroll ? `0 ${this.gutter}rpx` : '', 
 | 
            flex: this.isScroll ? 'auto' : '1', 
 | 
            transitionDuration: `${this.duration}s` 
 | 
          } 
 | 
          if (index === this.currentIndex) { 
 | 
            if (this.bold) { 
 | 
              style.fontWeight = 'bold' 
 | 
            } 
 | 
            style.color = this.activeColor 
 | 
            Object.assign(style, this.activeItemStyle) 
 | 
          } else { 
 | 
            style.color = this.inactiveColor 
 | 
          } 
 | 
          return style 
 | 
        } 
 | 
      } 
 | 
    }, 
 | 
    data() { 
 | 
      return { 
 | 
        // id值 
 | 
        id: this.$t.uuid(), 
 | 
        // 滚动scroll-view的左边距离 
 | 
        scrollLeft: 0, 
 | 
        // 存放查询后tab菜单的节点信息 
 | 
        tabQueryInfo: [], 
 | 
        // 组件宽度 
 | 
        componentWidth: 0, 
 | 
        // 底部滑块的移动距离 
 | 
        scrollBarLeft: 0, 
 | 
        // 组件到屏幕左边的巨鹿 
 | 
        componentLeft: 0, 
 | 
        // 当前选中的itemIndex 
 | 
        currentIndex: this.current, 
 | 
        // 标记底部滑块是否第一次移动,第一次移动的时候不触发动画 
 | 
        barMoveFirst: true 
 | 
      } 
 | 
    }, 
 | 
    watch: { 
 | 
      // 监听tab的变化,重新计算tab菜单信息 
 | 
      list(newValue, oldValue) { 
 | 
        // list变化时,重置内部索引,防止出现超过数据边界的问题 
 | 
        if (newValue.length !== oldValue.length) this.currentIndex = 0 
 | 
        this.$nextTick(() => { 
 | 
          this.init() 
 | 
        }) 
 | 
      }, 
 | 
      current: { 
 | 
        handler(val) { 
 | 
          this.$nextTick(() => { 
 | 
            this.currentIndex = val 
 | 
            this.scrollByIndex() 
 | 
          }) 
 | 
        }, 
 | 
        immediate: true 
 | 
      } 
 | 
    }, 
 | 
    mounted() { 
 | 
      this.init() 
 | 
    }, 
 | 
    methods: { 
 | 
      // 初始化变量 
 | 
      async init() { 
 | 
        // 获取tabs组件的信息 
 | 
        let tabRect = await this._tGetRect('#' + this.id) 
 | 
        // 计算组件的宽度 
 | 
        this.componentLeft = tabRect.left 
 | 
        this.componentWidth = tabRect.width 
 | 
        this.getTabRect() 
 | 
      }, 
 | 
      // 点击tab菜单 
 | 
      clickTab(index) { 
 | 
        if (index === this.currentIndex) return 
 | 
        this.$emit('change', index) 
 | 
      }, 
 | 
      // 查询tab的布局信息 
 | 
      getTabRect() { 
 | 
        let query = uni.createSelectorQuery().in(this) 
 | 
        // 遍历所有的tab 
 | 
        for (let i = 0; i < this.list.length; i++) { 
 | 
          query.select(`#tn-tabs__scroll-view__item-${i}`).fields({ 
 | 
            size: true, 
 | 
            rect: true 
 | 
          }) 
 | 
        } 
 | 
        query.exec((res) => { 
 | 
          this.tabQueryInfo = res 
 | 
          // 初始滚动条和底部滑块的位置 
 | 
          this.scrollByIndex() 
 | 
        }) 
 | 
      }, 
 | 
      // 滚动scrollView,让活动的tab处于屏幕中间 
 | 
      scrollByIndex() { 
 | 
        // 当前获取tab的布局信息 
 | 
        let tabInfo = this.tabQueryInfo[this.currentIndex] 
 | 
        if (!tabInfo) return 
 | 
         
 | 
        // 活动tab的宽度 
 | 
        let tabWidth = tabInfo.width 
 | 
        // 活动item的左边到组件左边的距离 
 | 
        let offsetLeft = tabInfo.left - this.componentLeft 
 | 
        // 计算scroll-view移动的距离 
 | 
        let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2 
 | 
        this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft 
 | 
         
 | 
        // 计算当前滑块需要移动的距离,当前活动item的中点到左边的距离减去滑块宽度的一半 
 | 
        let left = tabInfo.left + tabInfo.width / 2 - this.componentLeft 
 | 
         
 | 
        // 计算当前活跃item到组件左边的距离 
 | 
        this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2 
 | 
         
 | 
        // 防止在计算时出错,所以延迟执行标记不是第一次移动 
 | 
        if (this.barMoveFirst) { 
 | 
          setTimeout(() => { 
 | 
            this.barMoveFirst = false 
 | 
          }, 100) 
 | 
        } 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
</script> 
 | 
  
 | 
<style lang="scss" scoped> 
 | 
   
 | 
  /* #ifndef APP-NVUE */ 
 | 
  ::-webkit-scrollbar { 
 | 
    display: none; 
 | 
    width: 0 !important; 
 | 
    height: 0 !important; 
 | 
    -webkit-appearance: none; 
 | 
    background: transparent; 
 | 
  } 
 | 
  /* #endif */ 
 | 
   
 | 
  /* #ifdef H5 */ 
 | 
  // 通过样式穿透,隐藏H5下,scroll-view下的滚动条 
 | 
  scroll-view ::v-deep ::-webkit-scrollbar { 
 | 
      display: none; 
 | 
      width: 0 !important; 
 | 
      height: 0 !important; 
 | 
      -webkit-appearance: none; 
 | 
      background: transparent; 
 | 
  } 
 | 
  /* #endif */ 
 | 
   
 | 
  .tn-tabs { 
 | 
    &__scroll-view { 
 | 
      position: relative; 
 | 
      width: 100%; 
 | 
      white-space: nowrap; 
 | 
       
 | 
      &__box { 
 | 
        position: relative; 
 | 
        /* #ifdef MP-TOUTIAO */ 
 | 
        white-space: nowrap; 
 | 
        /* #endif */ 
 | 
      } 
 | 
       
 | 
      &__item { 
 | 
        position: relative; 
 | 
        /* #ifndef APP-NVUE */ 
 | 
        display: inline-block; 
 | 
        /* #endif */ 
 | 
        text-align: center; 
 | 
        transition-property: background-color, color; 
 | 
      } 
 | 
       
 | 
      &--flex { 
 | 
        display: flex; 
 | 
        flex-direction: row; 
 | 
        justify-content: space-between; 
 | 
      } 
 | 
    } 
 | 
     
 | 
    &__bar { 
 | 
      position: absolute; 
 | 
      bottom: 0; 
 | 
    } 
 | 
  } 
 | 
</style> 
 |