需求分析

  • 支持横向导航竖向导航;
  • 支持导航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

 

Logo

前往低代码交流专区

更多推荐