第十一集: 从零开始实现( 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
大家继续一起学习,一起进步, 早日实现自我价值!!
下一集准备聊聊'评分组件', 也就是选择小星星的那个, 做起来很有意思的组件,我挺喜欢的.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。