| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 | <template>  <view class="u-tabs">    <view class="u-tabs__wrapper">      <slot name="left" />      <view class="u-tabs__wrapper__scroll-view-wrapper">        <scroll-view          :scroll-x="scrollable"          :scroll-left="scrollLeft"          scroll-with-animation          enable-flex          class="u-tabs__wrapper__scroll-view white-space"          :show-scrollbar="false"          ref="u-tabs__wrapper__scroll-view"        >          <view class="u-tabs__wrapper__nav" ref="u-tabs__wrapper__nav">            <view              class="u-tabs__wrapper__nav__item"              v-for="(item, index) in list"              :key="index"              @tap="clickHandler(item, index)"              :ref="`u-tabs__wrapper__nav__item-${index}`"              :style="[addStyle(itemStyle), { flex: scrollable ? '' : 1 }]"              :class="[                `u-tabs__wrapper__nav__item-${index}`,                item.disabled && 'u-tabs__wrapper__nav__item--disabled',              ]"            >              <text                :class="[item.disabled && 'u-tabs__wrapper__nav__item__text--disabled']"                class="u-tabs__wrapper__nav__item__text"                :style="[textStyle(index)]"                >{{ item[keyName] }}</text              >            </view>            <!-- #ifdef APP-NVUE -->            <view              class="u-tabs__wrapper__nav__line"              ref="u-tabs__wrapper__nav__line"              :style="[                {                  width: addUnit(lineWidth),                  height: addUnit(lineHeight),                  background: lineColor ? lineColor : 'var(--ui-BG-Main)',                  backgroundSize: lineBgSize,                },              ]"            ></view>            <!-- #endif -->            <!-- #ifndef APP-NVUE -->            <view              class="u-tabs__wrapper__nav__line"              ref="u-tabs__wrapper__nav__line"              :style="[                {                  width: addUnit(lineWidth),                  transform: `translate(${lineOffsetLeft}px)`,                  transitionDuration: `${firstTime ? 0 : duration}ms`,                  height: addUnit(lineHeight),                  background: lineColor ? lineColor : 'var(--ui-BG-Main)',                  backgroundSize: lineBgSize,                },              ]"            >            </view>            <!-- #endif -->          </view>        </scroll-view>      </view>      <slot name="right" />    </view>  </view></template><script>  import { deepMerge, addStyle, addUnit, sleep, getPx, sys } from '@/sheep/helper';  // #ifdef APP-NVUE  const animation = uni.requireNativePlugin('animation');  const dom = uni.requireNativePlugin('dom');  // #endif  /**   * Tabs 标签   * @description tabs标签组件,在标签多的时候,可以配置为左右滑动,标签少的时候,可以禁止滑动。 该组件的一个特点是配置为滚动模式时,激活的tab会自动移动到组件的中间位置。   * @tutorial https://www.uviewui.com/components/tabs.html   * @property {String | Number}	duration			滑块移动一次所需的时间,单位秒(默认 200 )   * @property {String | Number}	swierWidth			swiper的宽度(默认 '750rpx' )   * @property {String}	keyName	 从`list`元素对象中读取的键名(默认 'name' )   * @event {Function(index)} change 标签改变时触发 index: 点击了第几个tab,索引从0开始   * @event {Function(index)} click 点击标签时触发 index: 点击了第几个tab,索引从0开始   * @example <u-tabs :list="list" :is-scroll="false" :current="current" @change="change"></u-tabs>   */  export default {    name: 'su-tabs',    data() {      return {        addStyle,        addUnit,        firstTime: true,        scrollLeft: 0,        scrollViewWidth: 0,        lineOffsetLeft: 0,        tabsRect: {          left: 0,        },        innerCurrent: 0,        moving: false,      };    },    props: {      // 滑块的移动过渡时间,单位ms      duration: {        type: Number,        default: 300,      },      // tabs标签数组      list: {        type: Array,        default: [],      },      // 滑块颜色      lineColor: {        type: String,        default: '',      },      // 菜单选择中时的样式      activeStyle: {        type: [String, Object],        default() {          return {            color: '#303133',          };        },      },      // 菜单非选中时的样式      inactiveStyle: {        type: [String, Object],        default() {          return {            color: '#606266',          };        },      },      // 滑块长度      lineWidth: {        type: [String, Number],        default: 20,      },      // 滑块高度      lineHeight: {        type: [String, Number],        default: 3,      },      // 滑块背景显示大小,当滑块背景设置为图片时使用      lineBgSize: {        type: String,        default: 'cover',      },      // 菜单item的样式      itemStyle: {        type: [String, Object],        default() {          return {            height: '44px',          };        },      },      // 菜单是否可滚动      scrollable: {        type: Boolean,        default: true,      },      // 当前选中标签的索引      current: {        type: [Number, String],        default: 0,      },      // 默认读取的键名      keyName: {        type: String,        default: 'name',      },    },    watch: {      current: {        immediate: true,        handler(newValue, oldValue) {          // 内外部值不相等时,才尝试移动滑块          if (newValue !== this.innerCurrent) {            this.innerCurrent = newValue;            this.$nextTick(() => {              this.resize();            });          }        },      },      // list变化时,重新渲染list各项信息      list() {        this.$nextTick(() => {          this.resize();        });      },    },    computed: {      textStyle() {        return (index) => {          const style = {};          // 取当期是否激活的样式          const customeStyle =            index === this.innerCurrent ? addStyle(this.activeStyle) : addStyle(this.inactiveStyle);          // 如果当前菜单被禁用,则加上对应颜色,需要在此做处理,是因为nvue下,无法在style样式中通过!import覆盖标签的内联样式          if (this.list[index].disabled) {            style.color = '#c8c9cc';          }          return deepMerge(customeStyle, style);        };      },    },    async mounted() {      this.init();    },    methods: {      $uGetRect(selector, all) {        return new Promise((resolve) => {          uni.createSelectorQuery()            .in(this)            [all ? 'selectAll' : 'select'](selector)            .boundingClientRect((rect) => {              if (all && Array.isArray(rect) && rect.length) {                resolve(rect);              }              if (!all && rect) {                resolve(rect);              }            })            .exec();        });      },      setLineLeft() {        const tabItem = this.list[this.innerCurrent];        if (!tabItem) {          return;        }        // 获取滑块该移动的位置        let lineOffsetLeft = this.list          .slice(0, this.innerCurrent)          .reduce((total, curr) => total + curr.rect.width, 0);        // 获取下划线的数值px表示法        const lineWidth = getPx(this.lineWidth);        this.lineOffsetLeft = lineOffsetLeft + (tabItem.rect.width - lineWidth) / 2;        // #ifdef APP-NVUE        // 第一次移动滑块,无需过渡时间        this.animation(this.lineOffsetLeft, this.firstTime ? 0 : parseInt(this.duration));        // #endif        // 如果是第一次执行此方法,让滑块在初始化时,瞬间滑动到第一个tab item的中间        // 这里需要一个定时器,因为在非nvue下,是直接通过style绑定过渡时间,需要等其过渡完成后,再设置为false(非第一次移动滑块)        if (this.firstTime) {          setTimeout(() => {            this.firstTime = false;          }, 10);        }      },      // nvue下设置滑块的位置      animation(x, duration = 0) {        // #ifdef APP-NVUE        const ref = this.$refs['u-tabs__wrapper__nav__line'];        animation.transition(ref, {          styles: {            transform: `translateX(${x}px)`,          },          duration,        });        // #endif      },      // 点击某一个标签      clickHandler(item, index) {        // 因为标签可能为disabled状态,所以click是一定会发出的,但是change事件是需要可用的状态才发出        this.$emit('click', {          ...item,          index,        });        // 如果disabled状态,返回        if (item.disabled) return;        this.innerCurrent = index;        this.resize();        this.$emit('change', {          ...item,          index,        });      },      init() {        sleep().then(() => {          this.resize();        });      },      setScrollLeft() {        // 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息        const tabRect = this.list[this.innerCurrent];        // 累加得到当前item到左边的距离        const offsetLeft = this.list.slice(0, this.innerCurrent).reduce((total, curr) => {          return total + curr.rect.width;        }, 0);        // 此处为屏幕宽度        const windowWidth = sys().windowWidth;        // 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动        let scrollLeft =          offsetLeft -          (this.tabsRect.width - tabRect.rect.width) / 2 -          (windowWidth - this.tabsRect.right) / 2 +          this.tabsRect.left / 2;        // 这里做一个限制,限制scrollLeft的最大值为整个scroll-view宽度减去tabs组件的宽度        scrollLeft = Math.min(scrollLeft, this.scrollViewWidth - this.tabsRect.width);        this.scrollLeft = Math.max(0, scrollLeft);      },      // 获取所有标签的尺寸      resize() {        // 如果不存在list,则不处理        if (this.list.length === 0) {          return;        }        Promise.all([this.getTabsRect(), this.getAllItemRect()]).then(          ([tabsRect, itemRect = []]) => {            this.tabsRect = tabsRect;            this.scrollViewWidth = 0;            itemRect.map((item, index) => {              // 计算scroll-view的宽度,这里              this.scrollViewWidth += item.width;              // 另外计算每一个item的中心点X轴坐标              this.list[index].rect = item;            });            // 获取了tabs的尺寸之后,设置滑块的位置            this.setLineLeft();            this.setScrollLeft();          },        );      },      // 获取导航菜单的尺寸      getTabsRect() {        return new Promise((resolve) => {          this.queryRect('u-tabs__wrapper__scroll-view').then((size) => resolve(size));        });      },      // 获取所有标签的尺寸      getAllItemRect() {        return new Promise((resolve) => {          const promiseAllArr = this.list.map((item, index) =>            this.queryRect(`u-tabs__wrapper__nav__item-${index}`, true),          );          Promise.all(promiseAllArr).then((sizes) => resolve(sizes));        });      },      // 获取各个标签的尺寸      queryRect(el, item) {        // #ifndef APP-NVUE        // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html        // 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同        return new Promise((resolve) => {          this.$uGetRect(`.${el}`).then((size) => {            resolve(size);          });        });        // #endif        // #ifdef APP-NVUE        // nvue下,使用dom模块查询元素高度        // 返回一个promise,让调用此方法的主体能使用then回调        return new Promise((resolve) => {          dom.getComponentRect(item ? this.$refs[el][0] : this.$refs[el], (res) => {            resolve(res.size);          });        });        // #endif      },    },  };</script><style lang="scss" scoped>  .u-tabs {    background: #fff;    border-bottom: 2rpx solid #eee;    &__wrapper {      @include flex;      align-items: center;      &__scroll-view-wrapper {        flex: 1;        /* #ifndef APP-NVUE */        overflow: auto hidden;        /* #endif */      }      &__nav {        @include flex;        position: relative;        &__item {          padding: 0 11px;          @include flex;          align-items: center;          justify-content: center;          &--disabled {            /* #ifndef APP-NVUE */            cursor: not-allowed;            /* #endif */          }          &__text {            font-size: 14px;            color: #606266;            white-space: nowrap !important;            &--disabled {              color: #c8c9cc !important;            }          }        }        &__line {          height: 3px;          background: #3c9cff;          width: 30px;          position: absolute;          bottom: 2px;          border-radius: 100px;          transition-property: transform;          transition-duration: 300ms;        }      }    }  }</style>
 |