第十一集: 從零開始實現一套pc端vue的ui組件庫( tab切換組件 )

第十一集: 從零開始實現( tab切換組件 )

本集定位:
咱們先來聊聊 tab 切換的意義, 不論是手機仍是pc, 屏幕的大小是有限的, 人眼睛看到的範圍也是有限的, 人們看信息的時候並不喜歡'跳轉'這種操做, 或是咱們要查某個知識點, 進入網站以後, 看了幾眼沒有須要的相關信息也就理所固然的退出去繼續搜索了, 而有時某些咱們想要的知識點可能在網站的底部, 但人們是有瀏覽習慣的, 這就須要在第一眼看到的區域裏面, 儘量多的展現'關鍵詞'與'關鍵信息', tab正是解決了如何'擴大'有限的空間這一問題. css

tab組件與其餘組件不一樣, 他須要至少兩個組件來配合完成功能,寫三個組件使用起來很討人厭, 只寫一個組件, 不論是語義化仍是書寫方式上都太差了, 參考element的設計本次咱們也是採用的雙組件,編寫上他與單一的組件不一樣的地方就是, 它涉及到兩個組件之間的通信問題.vue

1:需求分析

  1. 兩部分組成, 上部是標題的展現, 下部根據選中狀態進行展現內容
  2. 標題要有明確的激活狀態
  3. 爲了性能, 內容展現不可使用v-if
  4. 像這種包裹型的組件, 不容許干擾用戶的任何操做, 好比不能夠有.stop修飾符

使用方法應以下git

我以cc-tab爲包裹組件的父級標籤
cc-tab-pane爲每個展現內容的標籤程序員

<cc-tab v-model="activeName">
      <cc-tab-pane label="1號" name="one">1號的內容</cc-tab-pane>
      <cc-tab-pane label="2號" name="two">2號的內容</cc-tab-pane>
      <cc-tab-pane label="3號" name="three">3號的內容</cc-tab-pane>
 </cc-tab>

預期效果:
圖片描述github

2:基礎的搭建

vue-cc-ui/src/components/Tab/index.js數組

import Tab from './main/tab.vue'
import TabPane from './main/tab-pane.vue'

Tab.install = function(Vue) {
  Vue.component(Tab.name, Tab);
  Vue.component(TabPane.name, TabPane);
};

export default Tab

容器組件
vue-cc-ui/src/components/Tab/main/tab.vue函數

<template>
  <div class="cc-tab" >
    // 畢竟會不少標籤, ul li的語義化固然是最好的;
    // 好比3個標題, 你用3個div, 可是使用ul li 就要4個標籤, 優缺點都是有的.
    <ul class="cc-tab-nav" >
      <li v-for="item in navList" >
          標籤名
      </li>
     </ul>
     // 這裏展現內容
    <slot />
  </div>
</template>

vue-cc-ui/src/components/Tab/main/tab-pane.vue
只負責展現與提供組件的參數給容器佈局

<template>
  <div>
// 展現的內容咱們直接寫在標籤裏面, 因此slot就夠了
    <slot></slot>
  </div>
</template>

容器組件他還要接收參數性能

  1. label 也就是tab顯示的標籤名 (給用戶看的)
  2. name 也就是當點擊時, 此標籤的id (給開發用的)

這兩個分開設置還有一個緣由, 就是label能夠是重複的, 由於他不是惟一標識, name不可重複學習

props: {
    label: {
      type: String,
      required: true
    },
    name: {
      type: String,
      required: true
    }
  },

3:基礎功能

一. 咱們先把導航功能作出來, 讓標題顯示出來
在父級的容器裏面:

// 我的比較推薦的代碼規範
// mounted 與 created 這種鉤子, 放在最底部
// 由於他 不會常常變更, 他只是負責啓動代碼
// 他要符合單一職責, 不容許有具體的邏輯判斷
// 他啓動的函數, 若是有關初始化的, 必須以'init'做爲開頭
mounted() {
    this.initNav();
  }

initNav

initNav() {
// 僅負責對每一項的處理
      this.layer(item => {
        let result = {
          label: item.label,
          name: item.name,
          icon: item.icon
        };
        // 放入咱們的導航數組裏面
        this.navList.push(result);
      });
    },
    // 原理與map, reduce, 這類函數同樣, 
    // 每一步操做 都會吐給用戶
    layer(task) {
      this.$slots.default.map(item => task(item.componentInstance));
    }

解釋一下:

  1. this.$slots : 獲得這個父級容器內的全部插槽元素的一個對象, 例如:v-slot:foo 中的內容將會在 vm.$slots.foo 中被找到, default 屬性包括了全部沒有被包含在具名插槽中的節點,或 v-slot:default 的內容。
  2. 上面循環this.$slots.default 獲取到的每個item就是'節點元素',爲何打上'', 由於這個節點是被vue處理過的, 並非傳統意義上的節點;
  3. componentOptions: 顧名思義,這個組件的一些配置項, 好比listeners未接收的事件, tag標籤名, propsData, 而propsData裏面包含了咱們須要的name 以及 label, 可是他須要 componentOptions.propsData.name才能夠取到值.
  4. componentInstance: 組件狀態, 其身上有組件的this上面的參數 能夠直接獲取到 props傳入的值, 好比componentInstance.name 就會取到傳入的name, 上面爲何選他? 就是由於他只要'.'一次就能夠取到值了, 程序員的本性

上面咱們獲得了一個用戶傳入子組件的配置彙總, 咱們能夠循環展現他

<div class="cc-tab">
    <ul class="cc-tab-nav">
      <li v-for="item in navList"
          :key='item.name'
          // 當
          :class="{ 'is-active':item.name === value }"
          // 這個點擊事件就要通知子組件, 到底顯示誰
          @click="handClick($event,item.name)"
          >
          // 像這種內容的展現, 寫上標籤代碼佈局上更舒服
        <template>
        // 展現他的標籤名
          {{item.label}}
        </template>
      </li>
    </ul>
    <slot />
  </div>

handClick, 點擊事件負責把用戶的操做給父級看, 畢竟咱們綁定了v-model因此給個input事件,
tab-click是用戶接受的事件

handClick(e, name) {
      this.$emit("input", name);
      this.$emit("tab-click", e);
      // 這裏的更改選擇項須要用 宏任務, 不然測試的時候有顯示不正確的bug
      setTimeout(() => this.initSeleced(), 0);
    },

initSeleced 一個專門作選擇的方法

// 一句話的事
 initSeleced() {
    // 利用咱們以前定義好的循環函數
    // item就是每個子組件, 這些子組件數據是映射的, 因此能夠進行修改
    // 當子組件的value與激活的name相同時, 組件的展現被激活
      this.layer(item => (item.showItem = item.name == this.value));
    },

子組件

<template>
// 畢竟用戶反覆切換tab的可能性是存在的, show的效率更高一些
  <div v-show="showItem">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "ccTabPane",
  props: {
    label: {
      type: String,
      required: true
    },
    name: {
      type: String,
      required: true
    },
    icon: {
      type: String
    }
  },
  data() {
    return {
    // 默認固然是false, 不顯示
      showItem:false
    };
  }
};
</script>

如今咱們把核心功能寫完了, 但不要忘記小小的細節.
初始化選擇

mounted() {
    this.initNav();
    // 初始階段也要激活一下用戶選擇tab欄
    this.initSeleced();
  }

4: 樣式的設計

  1. 完善樣式, 好比tab的激活狀態, 激活動畫
  2. tab的不一樣樣式, 不一樣風格
  3. icon的添加

/vue-cc-ui/src/style/Tab.scss

@import './common/var.scss';
@import './common/mixin.scss';
@import './common/extend.scss';

@include b(tab) {
    @include brother(nav) {
    // 總體的title佈局就是不換行的橫向佈局
        display: flex;
        flex-wrap: nowrap;
        text-align: center;
        // 提供一條淺色的橫線
        border-bottom: 1px solid #eee;
        margin-bottom: 10px;
        &>li {
        // 主要就是每個標籤的樣式
            cursor: pointer;
            display: flex;
            position: relative;
            align-items: center;
            border-bottom: none;
            background-color: white;
            padding: 10px 20px;
            transition: all 0.2s;
            &:hover {
            // 給個有好的反饋
                transform: scale(0.8)
            };
                &::after {
                // 這個就是下面的選中橫線, 平時縮放爲0, 使用的時候再出現
                    content: '';
                    position: absolute;
                    left: 6px;
                    bottom: 0;
                    right: 6px;
                    transform: scale(0);
                    transition: all 0.2s;
                }
            @include when(active) {
            // 被激活的時候, 會字體變色, 會浮現出橫線
                color: $--color-nomal;
                &::after {
                    border-bottom: 2px solid $--color-nomal;
                    transform: scale(1);
                }
            }
        }
    }
}

添加icon

// 我就簡寫了
<li v-for="item in navList"
          :key='item.name'
          :class="{ 'is-active':item.name === value }"
          @click="handClick($event,item.name)"
          >
          // 傳入name就出現, 不然不出現
        <ccIcon v-if="item.icon"
                :name='item.icon'
                // 有一個被激活的顏色
                // 這裏還能夠這麼寫 (item.name === value)||'#409EFF'
                // 可是三元這裏比較靈活, 之後可能會改變默認顏色
                :color="item.name === value?'#409EFF':''" 
                />
        <template>
          {{item.label}}
        </template>
      </li>

其餘的類型的tab, 把標籤包裹起來

效果圖:

圖片描述
圖片描述

容許用戶選擇找這種樣式

<ul class="cc-tab-nav"
        :class="{ 'is-card':type=='card' }"
        >

相關樣式也要兼容

@include when(card) {
            &::after {
                display: none
            }
            &>li {
                border-bottom: none;
                border: 1px solid #eee;
                &:hover {
                    transform: scale(1)
                }
            };
            &>li+li {
                border-left: none
            };
            &>.is-active {
                border-bottom: none;
                &::after {
                    content: '';
                    position: absolute;
                    border-bottom: 2px solid white;
                    left: 0;
                    right: 0;
                    bottom: -1px;
                }
            };
            &>:nth-last-child(1) {
                border-top-right-radius: 7px;
            };
            &>:nth-child(1) {
                border-top-left-radius: 7px;
            };
        }

上面的寫法有個技巧就是下面這段
用戶有可能只有一個tab, 你可能會問, 只有一個幹麼要作tab?? 我只能說, 怎麼玩是你的事, 我只負責實現.
因此在只有一項的時候, 就不能只彎曲他的左上角, 還要讓他的右上角也是有弧度的

// 這兩個選擇器完美解決了問題
// 只有一個的時候, 它既是第一個也是最後一個
&>:nth-last-child(1) {
    border-top-right-radius: 7px;
};
&>:nth-child(1) {
    border-top-left-radius: 7px;
};

至此tab的功能已經作完, 總的來講這個tab組件算是cc-ui組件中比較好寫的一個了.

end
你們繼續一塊兒學習,一塊兒進步, 早日實現自我價值!!
下一集準備聊聊'評分組件', 也就是選擇小星星的那個, 作起來頗有意思的組件,我挺喜歡的.

本套ui的github地址:github
我的技術博客: 連接

相關文章
相關標籤/搜索