第十一集: 從零開始實現( tab切換組件 )
本集定位:
我們先來聊聊 tab 切換的意義, 不管是手機還是pc, 屏幕的大小是有限的, 人眼睛看到的範圍也是有限的, 人們看信息的時候並不喜歡'跳轉'這種操作, 或是我們要查某個知識點, 進入網站之後, 看了幾眼沒有需要的相關信息也就理所當然的退出去繼續搜索了, 而有時某些我們想要的知識點可能在網站的底部, 但人們是有瀏覽習慣的, 這就需要在第一眼看到的區域裏面, 儘可能多的展示'關鍵詞'與'關鍵信息', tab正是解決了如何'擴大'有限的空間這一問題.
tab組件與其他組件不同, 他需要至少兩個組件來配合完成功能,寫三個組件使用起來很討人厭, 只寫一個組件, 不管是語義化還是書寫方式上都太差了, 參考element的設計本次我們也是採用的雙組件,編寫上他與單一的組件不同的地方就是, 它涉及到兩個組件之間的通訊問題.
1:需求分析
- 兩部分組成, 上部是標題的展示, 下部根據選中狀態進行展示內容
- 標題要有明確的激活狀態
- 爲了性能, 內容展示不可以使用v-if
- 像這種包裹型的組件, 不允許干擾用戶的任何操作, 比如不可以有.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>
容器組件他還要接收參數
- label 也就是tab顯示的標籤名 (給用戶看的)
- 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));
}
解釋一下:
- this.$slots : 得到這個父級容器內的所有插槽元素的一個對象, 例如:v-slot:foo 中的內容將會在 vm.$slots.foo 中被找到, default 屬性包括了所有沒有被包含在具名插槽中的節點,或 v-slot:default 的內容。
- 上面循環this.$slots.default 獲取到的每一個item就是'節點元素',爲什麼打上'', 因爲這個節點是被vue處理過的, 並不是傳統意義上的節點;
- componentOptions: 顧名思義,這個組件的一些配置項, 比如listeners未接收的事件, tag標籤名, propsData, 而propsData裏面包含了我們需要的name 以及 label, 但是他需要 componentOptions.propsData.name纔可以取到值.
- 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: 樣式的設計
- 完善樣式, 比如tab的激活狀態, 激活動畫
- tab的不同樣式, 不同風格
- 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
大家繼續一起學習,一起進步, 早日實現自我價值!!
下一集準備聊聊'評分組件', 也就是選擇小星星的那個, 做起來很有意思的組件,我挺喜歡的.