需求分析
- 支持橫向導航豎嚮導航;
- 支持導航click展開下拉列表;
- 支持下拉列表click展開二級下拉列表,可任意層級;
- 配有展開動畫;
方法實現
1、定義組件:
在html中定義
t-nav組件定義爲最外層導航欄包裹器,nav-item組件包裹每一項導航名字,並且每個nav-item組件都聲明一個name,用以標識每一項導航。sub-nav組件包裹含有下拉列表的導航項。
<t-nav class="box" :selected.sync="selected" vertical style="width: 200px;">
<nav-item name="home">首頁</nav-item>
<sub-nav name="about">
<template slot="title">關於</template>
<nav-item name="culture">企業文化</nav-item>
<nav-item name="developers">開發團隊</nav-item>
<nav-item name="contacts">聯繫電話</nav-item>
<sub-nav name="phone">
<template slot="title">聯繫方式</template>
<nav-item name="cm">移動</nav-item>
<nav-item name="cu">聯通</nav-item>
<nav-item name="cn">電信</nav-item>
</sub-nav>
</sub-nav>
<nav-item name="hire">招聘</nav-item>
</t-nav>
2、基礎功能邏輯 :
t-nav組件定義slot插槽,主要負責nav-item和sub-nav組件的傳遞。vertical代表上面html中:selected的初始選中值。
<template>
<div class="nav-content" :class="{vertical}">
<slot></slot>
</div>
</template>
nav-item組件定義導航每一項,可點擊,點擊後選中值爲當前項,並展示相關樣式。
<template>
<div class="nav-item-content" :class="{selected}" @click="onClick">
<slot></slot>
</div>
</template>
sub-item組件定義一組下拉導航,可點擊,根據橫向導航或是豎嚮導航展示不同下拉狀態。
<template>
<div class="sub-nav-content" :class="{active, vertical}" v-click-outside="close">
<span class="sub-nav-content-label" @click="onClick">
<slot name="title"></slot>
<span class="sub-nav-content-icon" :class="{open, vertical}">
<t-icon name="right"></t-icon>
</span>
</span>
<template v-if="vertical">
<transition @enter="enter" @leave="leave" @after-leave="afterLeave" @after-enter="afterEnter">
<div class="sub-nav-content-popover" v-show="open" :class="{vertical}">
<slot></slot>
</div>
</transition>
</template>
<template v-else>
<div class="sub-nav-content-popover" v-show="open">
<slot></slot>
</div>
</template>
</div>
</template>
3、支持橫向導航豎嚮導航
我們在t-nav組件標籤上定義 vertical,有vertical則表示豎嚮導航,無則表示橫向導航。然後將vertical傳給nav組件和sub-nav組件,通過判斷更改組件內部flex排列樣式即可,這裏css反而比較重點也比較難寫。
t-nav組件:
<t-nav class="box" :selected.sync="selected" vertical style="width: 200px;">
nav組件:
vertical是true則CSS展示flex-direction: column。
<template>
<div class="nav-content" :class="{vertical}">
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
@import "../../styles/var";
.nav-content {
display: flex;
border-bottom: 1px solid $grey;
color: $color;
cursor: default;
&.vertical {
flex-direction: column;
border: 1px solid $grey;
}
}
</style>
sub-nav組件:
通過vertical判斷展示哪段html,即橫向或是豎向,並展示相應CSS。
<style lang="scss" scoped>
@import '../../styles/var';
.sub-nav-content {
position: relative;
&:not(.vertical) {
&.active {
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
border-bottom: 2px solid $blue;
width: 100%;
}
}
}
}
</style>
4、支持導航click展開下拉列表
通過sub-nav下拉導航組件來展示,v-show="open" 是否顯示下拉列表。爲什麼用v-show,而不是v-if。因爲v-if從false到true的狀態會觸發生命週期鉤子:created、updated。v-if並不是能一開始跟隨父組件一起經歷所有的鉤子事件,而是單獨從false狀態到true狀態時觸發的。因此在數據傳遞的過程中並不是同步的,會出現BUG。
<template v-else>
<div class="sub-nav-content-popover" v-show="open">
<slot></slot>
</div>
</template>
5、支持下拉列表click展開二級下拉列表,可任意層級
原理是通過sub-nav組件與nav-item組件嵌套即可實現,但sub-nav下面的nav-item與t-nav下面的sub-nav不屬於同級,層級比較深,相當於sub-nav下面的nav-item可能是層級很深的子孫組件,存在跨組件通訊的問題。
怎麼解決這個問題?使用依賴注入provide/inject,將父級的數據注入到其底下所有的子子孫孫當中,這樣無論子孫組件層級多深,我們都可以調用到父級的數據並操作。
<t-nav class="box" :selected.sync="selected" vertical style="width: 200px;">
<sub-nav name="about">
<sub-nav name="phone">
<template slot="title">聯繫方式</template>
<nav-item name="cm">移動</nav-item>
<nav-item name="cu">聯通</nav-item>
<nav-item name="cn">電信</nav-item>
<sub-nav name="phone">
<template slot="title">聯繫方式</template>
<nav-item name="cm">移動</nav-item>
<nav-item name="cu">聯通</nav-item>
<nav-item name="cn">電信</nav-item>
</sub-nav>
</sub-nav>
</sub-nav>
</t-nav>
父組件t-nav:
export default {
name: 't-nav',
provide() { //依賴
return {
root: this,
vertical: this.vertical
}
},
}
子孫組件nav-item:
export default {
name: 't-nav-item',
inject: ['root'], //注入
mounted() {
this.root.addItem(this)
},
}
子孫組件sub-nav:
export default {
inject: ['root', 'vertical'], //注入
computed: {
active() {
return this.root.namePath.indexOf(this.name) > -1
},
},
}
6、配有展開動畫
下拉的展開動畫,通過vue中的transiton動畫組件即可完成。但這裏我們特意用了transition的動畫鉤子實現的,就是定義動畫開始、動畫結束等等相關時調用鉤子來做一些操作,這在寫複雜的動畫樣式時非常有用。
<transition @enter="enter" @leave="leave" @after-leave="afterLeave" @after-enter="afterEnter">
<div class="sub-nav-content-popover" v-show="open" :class="{vertical}">
<slot></slot>
</div>
</transition>
methods: {
enter(el, done) {
let {height} = el.getBoundingClientRect()
el.style.height = 0
el.getBoundingClientRect()
el.style.height = `${height}px`
el.addEventListener('transitionend', ()=>{
done()
})
},
afterEnter(el) {
el.style.height = 'auto'
},
leave(el, done) {
let {height} = el.getBoundingClientRect()
el.style.height = `${height}px`
el.getBoundingClientRect()
el.style.height = 0
el.addEventListener('transitionend', ()=>{
done()
})
},
afterLeave(el) {
el.style.height = 'auto'
},
}
&-popover {
position: absolute;
top: 100%;
left: 0;
border: 1px solid black;
white-space: nowrap;
background: white;
margin-top: 4px;
box-shadow: 0 0 3px fade_out(black, 0.8);
border-radius: $border-radius;
font-size: $font-size;
color: $light-color;
min-width: 8em;
&.vertical {
position: static;
border-radius: 0;
border: none;
box-shadow: none;
transition: height .3s;
overflow: hidden;
}
}
這裏鉤子的使用時,要注意瀏覽器的對於樣式的多次修改會執行合併操作,瀏覽器只會執行最後一個樣式。所以當我們在短時間內要執行多個樣式的時候,需要在樣式中間做點動作,讓瀏覽器無法合併樣式操作。
比如:
el.style.height = `${height}px`
el.getBoundingClientRect()
el.style.height = 0