第十一集: 从零开始实现一套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
我的技术博客: 连接

相关文章
相关标签/搜索