当着面试官手写tab组件,我说......

86 阅读1分钟

今天分享一个tab组件,那废话不多说直接进入正题!首先拿到一个需求之后,不要着急之后先去写代码,先去分析主要功能也就是我们的props,以及划分组件模块等;

image.png

先根据这张图来分析这个组件的基本结构,首先是通过点击上方标题来渲染下面内容。类似下面这种用法结构。

<fuzujian v-model="active"> <zizujian title="标签 1">内容 1</zizujian> <zizujian title="标签 2">内容 2</zizujian> <zizujian title="标签 3">内容 3</zizujian> <zizujian title="标签 4">内容 4</zizujian> </fuzujian>

首先我们需要获取每个子组件的title,在父组件进行渲染,如何获取呢?(注意这里使用Vue2实现) 可以使用this.$children这个api,获取所有子组件实例,它是个数组然后获取每个子组件对应的title。

得到所有的title之后进行渲染然后绑定点击事件,这时候我们根据点击的索引来切换下方内容以及小滑条的切换。

image.png

那我们如何来切换下方内容呢,我们可以这样做

showActive() {
  this.$nextTick(() => {
    this.$children.some((childrenItem, i) => {
      childrenItem.show = this.currentIndex === i;
    })
  })
},

遍历我们的this.$children,当前索引===子组件Item索引就让它显示,反之则隐藏。

),

getLineOffset: debounce(function () {
  const titles = this.$refs.titleRef;
  const title = titles[+this.currentIndex];
  if (titles && title) {
    this.offsetLine = title.offsetLeft + title.offsetWidth / 2;
  }
}, 100),

offsetLeft:返回当前元素相对于 offsetParent 节点左边界的偏移像素值。 offsetWidth 返回该元素的像素宽度,宽度包含内边距(padding)和边框(border),不包含外边距(margin),是一个整数,单位是像素 px。

实际到这里我们的组件最基本的功能已经实现了,注意是最基本的,前端开发前面的是逻辑,后面是细节,首先父组件使用v-model绑定了索引,也就是我们的组件一进来就要显示索引的内容以及小状态条显示正确的地方,这是就要使用watch监听v-model绑定的索引,一进来就调用上述两个方法即可。

但是还不够,我们还有细节要进行处理,我们去改变浏览器的窗口的时候,小滑条不会随着变化的,所以我们要去监听窗口改变事件,也就是我们的resize事件,只要窗口改变我们去触发相应事件。

mounted() {
  window.addEventListener('resize', this.debouncedGetLineOffset, false)
  this.$on('hooks:beforeDestroy', () => {
    window.removeEventListener('resize', this.debouncedGetLineOffset, false)
  })
},

到这里一个基本tab组件的完成,刚才是最基本,现在是基本,因为我们还有较多功能我们没有实现。

tabs代码实现:

<template>
  <div>
    <div ref="tabRef" :style="tabsTitleStyle" class="vux_tab-title">
      <div v-for="(item,index) in tabsTitleList" :key="item.title" ref="titleRef"
           :style="[activeTitleStyle(item,index),disabledStyle(item,index)]" class="vux_tab-title item"
           @click="handleTitleItemClick(item,index)">
        <span>{{ item.title }}</span>
        <badge v-if="item.dot" :text="item.dot"></badge>
      </div>
      <div :style="lineStyle" class="vux_tab-title line"></div>
    </div>
    <div style="width: 100%;">
      <slot>
      </slot>
    </div>
  </div>
</template>
<script>
import {debounce} from "@/packages/tabs/src/tool";

export default {
  name: "vuxTabs",
  data() {
    return {
      tabsTitleList: [],
      offsetLine: 0,
      currentIndex: this.active,
      width: 0,
      height: 0
    }
  },
  props: {
    active: {
      type: [Number, String]
    },
    color: {
      type: String,
      default: '#0068ff'
    },
    background: {
      type: String,
      default: 'white'
    },
    duration: {
      type: [Number, String], default: 0.5
    },
    lineWidth: {
      type: [Number, String], default: 40
    },
    lineHeight: {
      type: [Number, String], default: 2
    },
    ellipsis: {
      type: Boolean
    },
    swipeable: {
      type: Boolean,
      default: true
    },
    titleActiveColor: {
      type: String,
      default: '#333'
    },
    titleActiveSize: {
      type: [Number, String], default: 14
    },
    titleInactiveColor: {
      type: String,
      default: '#666'
    },
    beforeChange: {
      type: Function
    },
    scrollspy: Boolean
  },
  model: {
    prop: 'active',
  },
  mounted() {
    this.$nextTick(() => {
      this.tabsTitleList = this.$children;
      this.initActiveLine()
    })
    window.addEventListener('resize', this.debouncedGetLineOffset, false)
    this.$on('hooks:beforeDestroy', () => {
      window.removeEventListener('resize', this.debouncedGetLineOffset, false)
    })
  },
  activated() {
    this.initActiveLine()
  },
  watch: {
    active(val) {
      this.currentIndex = val;
    },
    currentIndex(newVal, oldVal) {
      if (newVal !== oldVal) {
        this.updateCurrentIndex(newVal);
        this.initActiveLine()
      }

    }
  },
  computed: {
    lineStyle() {
      return {
        // width: this.width ? this.width + 'px' : this.lineWidth + 'px',
        // height: this.height ? this.height + 'px' : this.lineHeight + 'px',
        height: this.lineHeight + 'px',
        width: this.lineWidth + 'px',
        backgroundColor: this.backgroundColor,
        transform: ` translateX(${this.offsetLine}px)  translateX(-50%)`,
        transitionDuration: `${this.duration}s`

      }
    },
    activeTitleStyle() {
      return function (item, index) {
        return index === this.currentIndex ? {
          color: this.titleActiveColor,
          fontWeight: '500',
          fontSize: `${this.titleActiveSize}px`
        } : {
          color: this.titleInactiveColor
        };
      }
    },
    disabledStyle() {
      return function (item, index) {
        if (item.disabled) {
          return {
            opacity: 0.6,
            cursor: 'not-allowed'
          }
        }

      };
    },
    tabsTitleStyle() {
      return {
        background: this.background
      }
    }

  },
  methods: {
    initActiveLine() {
      this.showActive();
      this.debouncedGetLineOffset();
    },
    updateCurrentIndex(index) {
      this.$emit('input', index)
      this.$emit('change', index)
    },
    showActive() {
      this.$nextTick(() => {
        this.$children.some((childrenItem, i) => {
          childrenItem.show = this.currentIndex === i;
        })
      })
    },
    handleTitleItemClick(item, index) {
      if (item.disabled) {
        return
      }
      if (this.currentIndex === index) {
        return;
      }
      this.$emit('click', {
        title: item.title,
        name: item.name,
        index: index,
      })
      this.updateCurrentIndex(index)

    },
    debouncedGetLineOffset() {
      this.$nextTick(() => {
        const titles = this.$refs.titleRef;
        const title = titles[+this.currentIndex];
        if (titles && title) {
          this.offsetLine = title.offsetLeft + title.offsetWidth / 2;
        }
      })
    }

  },


}
</script>

<style scoped>

</style>
<style lang="less" scoped>
.vux_tab-title {
  position: relative;
  display: flex;
  box-sizing: border-box;
  height: 100%;
  background-color: #fff;
  overflow-y: hidden;
  overflow-x: auto;
  //隐藏滚动条
  &::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
    -webkit-overflow-scrolling: touch;
    -overflow-scrolling: touch;
  }


  &.item {
    position: relative;
    display: flex;
    flex: 1 0 auto;
    min-width: 0;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    padding: 0 16px;
    font-size: 14px;
    line-height: 40px;
    cursor: pointer;

  }

  &.line {
    position: absolute;
    background-color: #187ef9;
    //opacity: 0.1;
    bottom: 0px;
    left: 0;
    z-index: 1;
    border-radius: 4px;
  }


}

</style>

tab代码实现

<template>
  <div v-show="show"
       style="width: 100%;"
       v-touch:swipeleft="leftSlide"
       v-touch:swiperight="rightSlide">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "vuxTab",
  data() {
    return {
      show: false
    }
  },
  props: {
    title: {
      type: [String, Number]
    },
    disabled: {
      type: Boolean
    },
    dot: {
      type: Number
    },
    name: {
      //唯一标识符
      type: [String, Number]
    },
    url: {
      type: String,
    },
    to: {
      type: String,
    },
  },

  computed: {

    currentIndex: {
      get() {
        return this.$parent.currentIndex

      },
      set(val) {
        this.$parent.updateCurrentIndex(val)
      }
    },
    tabsTitleList() {
      return this.$parent.tabsTitleList
    }
    ,
    swipeable() {
      return this.$parent.swipeable
    },
  },
  watch: {
    // currentIndex(newVal, oldVal) {
    //   if (newVal !== oldVal) {
    //     this.$parent.updateCurrentIndex(newVal)
    //   }
    // }
  },
  methods: {
    leftSlide() {
      if (!this.swipeable) {
        return;
      }
      if (this.currentIndex === this.tabsTitleList.length - 1) {
        return
      }
      ++this.currentIndex;
    },
    rightSlide() {
      if (!this.swipeable) {
        return;
      }
      if (this.currentIndex === 0) {
        return
      }
      --this.currentIndex;


    }
  }
}
</script>

<style scoped>

</style>