Vue實現簡單Tab標簽頁組件


Tab 標簽頁組件

基礎用法

默認情況下啟用第一個標簽,可以通過v-model綁定當前激活的標簽索引

<tabs v-model="active">
  <tab title="標簽 1">內容 1</tab>
  <tab title="標簽 2">內容 2</tab>
  <tab title="標簽 3">內容 3</tab>
</tabs>
export default {
  data() {
    return {
      active: 2
    };
  }
}

點擊事件

可以在tabs上綁定click事件和change事件,事件傳參為標簽對應的索引和標題

<tabs @click="handleClick"></tabs>
<tabs @change="handleChange"></tabs>

Tabs API

屬性名 類型 默認值 說明
v-model String Number 0 當前標簽的索引
color String red 標簽和底部條顏色
duration Number 0.3 動畫時間,單位秒
line-width Number tab.offsetWidth / 2 底部條寬度,單位 px
line-height Number 3 底部條高度,單位 px

Tab API

屬性名 類型 默認值 說明
title String - tab頁的標題

example

code

tab

<template>
  <div 
    v-show="isSelected"
    class="tab__pane"
    >
    <slot />
  </div>
</template>

<script>
export default {
  name: 'tab',
  props: {
    title: String,
  },
  data() {
    return {
      parent: null,
    };
  },
  computed: {
    index() {
      return this.parent.tabs.indexOf(this);
    },
    isSelected() {
      return this.index === this.parent.curActive;
    },
  },
  watch: {
    title() {
      this.parent.setLine();
    }
  },
  methods: {
    findParent(name) {
      let parent = this.$parent;
      while (parent) {
        if (parent.$options.name === name) {
          this.parent = parent;
          break;
        }
        parent = parent.$parent; // 多層嵌套
      }
    },
  },
  created() {
    this.findParent('tabs');
  },
  mounted() {
    const { tabs } = this.parent;
    const index = this.parent.$slots.default.indexOf(this.$vnode);
    tabs.splice(index === -1 ? tabs.length : index, 0, this);
  },
  beforeDestroy() {
    this.parent.tabs.splice(this.index, 1);
  },
};
</script>

<style>
.tab__pane {
}
</style>

tabs

<template>
  <div class="tabs">
    <div class="tabs__nav">
      <div
       class="tabs__line"
       :style="lineStyle"
      ></div>
      <div 
        v-for="(tab, index) in tabs" 
        :key="index" 
        ref="tabs"
        @click="onClick(index)"
        class="tab"
        :style="getTabStyle(tab, index)"
        :class="{'tab--active': index === curActive,}"
      >
        <span>{{tab.title}}</span>
      </div>
    </div>
    <slot/>
  </div>
</template>

<script>
export default {
  name: 'tabs',
  model: {
    prop: 'active',
  },
  props: {
    color: String,
    lineWidth: {
      type: Number,
      default: null,
    },
    lineHeight: {
      type: Number,
      default: 3,
    },
    duration: {
      type: Number,
      default: 0.3,
    },
    active: {
      type: [Number, String],
      default: 0,
    },
  },
  data() {
    return {
      tabs: [],
      curActive: null,
      lineStyle: {
        backgroundColor: this.color,
      },
    };
  },
  computed: {},
  watch: {
    active(val) {
      if (val !== this.curActive) {
        this.correctActive(val);
      }
    },
    color() {
      this.setLine();
    },
    tabs() {
      this.correctActive(this.curActive || this.active);
      this.setLine();
    },
    curActive() {
      this.setLine();
    },
  },
  mounted() {
    this.correctActive(this.active);
    this.isFirstLoaded = true;
    this.$nextTick(() => {
      this.isFirstLoaded = false;
    });
  },
  methods: {
    onClick(index) {
      const { title } = this.tabs[index];
      this.setCurActive(index);
      this.$emit('click', index, title);
    },
    setActive() {},
    correctActive(active) {
      active = +active;
      // console.log(active, 'active');
      const exist = this.tabs.some(tab => tab.index === active);
      const defaultActive = (this.tabs[0] || {}).index || 0;
      this.setCurActive(exist ? active : defaultActive);
    },
    setCurActive(active) {
      if (active !== this.curActive) {
        console.log(this.curActive);
        if (this.curActive !== null) {
          this.$emit('change', active, this.tabs[active].title);
        }
        this.curActive = active;
      }
    },
    setLine() {
      const animation = this.isFirstLoaded;
      this.$nextTick(() => {
        const { tabs } = this.$refs;
        if (!tabs || !tabs[this.curActive]) {
          return;
        }
        const tab = tabs[this.curActive];
        const { lineWidth, lineHeight } = this;
        /* eslint-disable no-unneeded-ternary */
        const width = lineWidth ? lineWidth : (tab.offsetWidth / 2);
        /* eslint-disable no-unneeded-ternary */
        const left = tab.offsetLeft + ((tab.offsetWidth - width) / 2);
        const lineStyle = {
          width: `${width}px`,
          backgroundColor: this.color,
          transform: `translateX(${left}px)`,
        };
        if (!animation) {
          lineStyle.transitionDuration = `${this.duration}s`;
        }
        let height = '';
        if (lineHeight) {
          height = `${lineHeight}px`;
        } else {
          height = '3px';
        }
        lineStyle.height = height;
        lineStyle.borderRadius = height;
        this.lineStyle = lineStyle;
      });
    },
    getTabStyle(tab, index) {
      if (index === this.curActive) {
        return {
          color: this.color,
          fontWeight: 'bold',
        };
      }
    },
  },
};
</script>
<style>
.tab {
  flex: 1;
  cursor: pointer;
  min-width: 0;
  padding: 0 5px;
  font-size: 14px;
  position: relative;
  color: #000;
  line-height: 50px;
  text-align: center;
  box-sizing: border-box;
  background-color: #fff;
  span {
    display: block;
  }
  &--active {
    font-weight: 500!important;
  }
}
.tabs {
  position: relative;
  &__nav {
    display: flex;
    user-select: none;
    position: relative;
    background-color: #fff;
    height: 100%;
    box-sizing: content-box;
  }
  &__line {
    z-index: 1;
    left: 0;
    bottom: 0px;
    height: 3px;
    position: absolute;
    border-radius: 3px;
    background-color: red;
  }
}
</style>

END


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM