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

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

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

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

1:需求分析

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

使用方法應如下

我以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>

預期效果:
圖片描述

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
個人技術博客: 鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章