首先我们先来看一下 Element UI 里面的 Tabs 是怎么样的
<template>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="User" name="first">User</el-tab-pane>
<el-tab-pane label="Config" name="second">Config</el-tab-pane>
<el-tab-pane label="Role" name="third">Role</el-tab-pane>
<el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
</el-tabs>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { TabsPaneContext } from 'element-plus'
const activeName = ref('first')
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
</script>
首先, 我们来分析一下细节:
1. tabs 组件由 el-tabs 和 el-tab-pane 组件来共同完成
2. 在父组件中对 tabs 组件传入了一个 v-model 数据 activeName
3. 在父组件中监听了 tabs 组件抛出的自定义事件 @tab-click
4. 对 el-tab-pane 组件传入了 label 和 name 数据
为什么需要两个组件来共同完成?
因为 tabs 主要用于规定 tab 栏的数据显示和操作, 而 tab-pane 组件主要用于规定 tabs 组件显示什么内容.
在父组件中为什么要传入一个 v-model 数据?
因为我们对 tab 栏进行点击的时候, 我们需要给用户一个反馈; 也就是高亮的效果.
所以, 父组件传入的 v-model 数据就是用来修改 tab 栏中按钮的高亮效果的数据.
然后, 用户点击不同的 tab 栏按钮, 下面对应的数据也需要发生对应的变化(通过 v-model 数据来控制显示和隐藏)
在父组件中为什么要去监听 @tab-click 自定义事件?
因为 tabs 组件是一个上下结构, 上面是按钮, 下面就是按钮所对应的数据.
这个数据是需要发送请求获取过来的, 所以, 当用户在点击不同的按钮的时候, 我们既要去修改高亮数据; 也要去修改发送请求后端所需的参数数据.
给 el-tab-pane 组件中的 label 和 name 参数起到了什么作用?
label 数据主要是给到按钮的, 因为 tab 栏需要显示不同的按钮数据; 这一个数据是由外部来决定的.
name 数据也是给到按钮的, 因为我们需要给每一个按钮一个唯一值; 因为我们需要通过这一个唯一值去修改按钮的高亮效果.
好了根据我的说明, 你有没有一点点的明白了; 现在我们也就来逐步的将代码部分展现.
第一步:
1. 首先创建 tabs 和 tab-panel 组件
在 tabs 组件中需要去规定结构和 tab 栏的数据
<script>
export default {
name: 'Tabs',
render () {
// return出去什么, 引用页面就会显示什么
return 'tabs'
}
}
</script>
<style scoped lang="less">
.tabs {
background: #fff;
> nav {
height: 60px;
line-height: 60px;
display: flex;
border-bottom: 1px solid #f5f5f5;
> a {
width: 110px;
border-right: 1px solid #f5f5f5;
text-align: center;
font-size: 16px;
&.active {
border-top: 2px solid @xtxColor;
height: 60px;
background: #fff;
line-height: 56px;
}
}
}
}
</style>
tab-panel 组件主要是负责接收外部传入的数据, 丢到默认插槽中
<template>
<div class="tabs-panel">
<slot />
</div>
</template>
<script>
export default {
name: 'TabsPanel',
}
</script>
第二步:
1. 规定 tabs 组件结构
2. tab-panel 组件接收 name 和 label 数据
3. 在父组件中进行应用
<script>
export default {
name: 'Tabs',
render () {
// 规定结构会使用到jsx语法
// 定义tab栏结构
const nav = <nav>
<a href="javascript:;">选项卡一</a>
<a href="javascript:;">选项卡二</a>
<a href="javascript:;">选项卡三</a>
</nav>
// 定义内容数据
const content = <div>
<div>内容一</div>
<div>内容二</div>
<div>内容三</div>
</div>
return <div class="tabs">{[nav, content]}</div>
}
}
</script>
<style scoped lang="less">
.tabs {
background: #fff;
> nav {
height: 60px;
line-height: 60px;
display: flex;
border-bottom: 1px solid #f5f5f5;
> a {
width: 110px;
border-right: 1px solid #f5f5f5;
text-align: center;
font-size: 16px;
&.active {
border-top: 2px solid @xtxColor;
height: 60px;
background: #fff;
line-height: 56px;
}
}
}
}
</style>
<template>
<div class="tabs-panel">
<slot />
</div>
</template>
<script>
export default {
name: 'TabsPanel',
props: {
label: {
type: String,
default: ''
},
name: {
type: [String, Number],
default: ''
}
}
}
</script>
<template>
<div class="member-order">
<Tabs>
<TabsPanel label="选项卡一" name="first"></TabsPanel>
<TabsPanel label="选项卡二" name="second"></TabsPanel>
<TabsPanel label="选项卡三" name="third"></TabsPanel>
</Tabs>
</div>
</template>
第三步:
1. 根据 tab-panel 组件接收到的 label 数据去动态的渲染 tabs 组件的 nav 数据
通过 $slots.default 方法获取插槽数据, 此方法调用会返回一个数组; 数据里面包含若干个对象, 每一个对象就是每一个 tab-panel
<script>
export default {
name: 'Tabs',
render () {
const panels = this.$slots.default()
const nav = <nav>{
panels.map((item, index) => {
return <a href="javascript:;">{ item.props.label }</a>
})
}</nav>
return <div class="tabs">{[nav, panels]}</div>
}
}
</script>
<style scoped lang="less">
.tabs {
background: #fff;
> nav {
height: 60px;
line-height: 60px;
display: flex;
border-bottom: 1px solid #f5f5f5;
> a {
width: 110px;
border-right: 1px solid #f5f5f5;
text-align: center;
font-size: 16px;
&.active {
border-top: 2px solid @xtxColor;
height: 60px;
background: #fff;
line-height: 56px;
}
}
}
}
</style>
<template>
<div class="member-order">
<Tabs>
<TabsPanel label="选项卡一" name="first">选项一</TabsPanel >
<TabsPanel label="选项卡二" name="second">选项二</TabsPanel >
<TabsPanel label="选项卡三" name="third">选项三</TabsPanel >
</Tabs>
</div>
</template>
虽然效果还是一样的, 但是 nav 里面的数据是动态的了
第四步:
1. 兼容对 tab-panel 组件书写死数据的情况(上面已经实现, 父组件传入的就是死数据)和对 tab-panel 进行 v-for 遍历的情况
这两种情况, 使用 $slots.default 方法获取的数据是不一样的
所以我们可以根据 $slots.default 方法返回的数组中每一个对象中的 type 值来进行判断
到底传入的是静态数据, 还是 v-for 遍历的动态数据(使用 v-for 是因为数据来源是后端)
<script>
export default {
name: 'Tabs',
render () {
const panels = this.$slots.default()
const dynamicPanels = []
panels.forEach(item => {
// 提取静态的组件内容
if (item.type.name === 'TabsPanel') {
dynamicPanels.push(item)
} else {
// 提取动态的组件内容
item.children.forEach(i => {
dynamicPanels.push(i)
})
}
})
const nav = <nav>{
dynamicPanels.map((item, index) => {
return <a href="javascript:;">{ item.props.label }</a>
})
}</nav>
return <div class="tabs">{[nav, panels]}</div>
}
}
</script>
<style scoped lang="less">
.tabs {
background: #fff;
> nav {
height: 60px;
line-height: 60px;
display: flex;
border-bottom: 1px solid #f5f5f5;
> a {
width: 110px;
border-right: 1px solid #f5f5f5;
text-align: center;
font-size: 16px;
&.active {
border-top: 2px solid @xtxColor;
height: 60px;
background: #fff;
line-height: 56px;
}
}
}
}
</style>
那么现在父组件中既可以使用静态的数据, 也可以使用 v-for 遍历的的动态数据了
<template>
<div class="member-order">
<Tabs>
<TabsPanel label="选项卡零" name="zero">选项零</TabsPanel >
<TabsPanel v-for="item in 4" :key="item" :label="`选项卡${item}`" :name="`name${item}`">内容{{item}}</TabsPanel >
</Tabs>
</div>
</template>
第五步:
1. 实现 tab 栏的交互(也就是完成 tab 栏的点击高亮, 和点击不同的按钮显示对应的内容数据)
首先是在父组件中传入 v-model 的数据
<template>
<div class="member-order">
<Tabs v-model="activeName">
<TabsPanel label="选项卡零" name="zero">选项零</TabsPanel >
<TabsPanel v-for="item in 4" :key="item" :label="`选项卡${item}`" :name="`name${item}`">内容{{item}}</TabsPanel >
</Tabs>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'MemberOrder',
setup () {
const activeName = ref('zero')
return { activeName }
}
}
</script>
然后在 tabs 组件中进行接收, 给 nav 标签中的每一个 a 标签添加的点击事件
点击事件触发的同时去修改父组件的 v-model 数据
再通过 provide 给 tab-panel 组件传入当前高亮的按钮信息, v-show 控制内容的显示隐藏(通过 provide的原因是, tab 和 tab-panel 不是父子关系)
<script>
import { provide } from 'vue'
import { useVModel } from '@vueuse/core'
export default {
name: 'Tabs',
props: {
modelValue: {
type: [String, Number],
default: ''
}
},
setup (props, { emit }) {
const activeName = useVModel(props, 'modelValue', emit)
// 定义点击事件的事件处理函数
const clickCheckName = (name) => {
activeName.value = name
}
provide('activeName', activeName)
return { activeName, clickCheckName }
},
render () {
const panels = this.$slots.default()
const dynamicPanels = []
panels.forEach(item => {
if (item.type.name === 'TabsPanel') {
dynamicPanels.push(item)
} else {
item.children.forEach(i => {
dynamicPanels.push(i)
})
}
})
const nav = <nav>{
dynamicPanels.map((item, index) => {
return <a onClick={() => this.clickCheckName(item.props.name)} class={{ active: item.props.name === this.modelValue }} href="javascript:;">{ item.props.label }</a>
})
}</nav>
return <div class="tabs">{[nav, panels]}</div>
}
}
</script>
<style scoped lang="less">
.tabs {
background: #fff;
> nav {
height: 60px;
line-height: 60px;
display: flex;
border-bottom: 1px solid #f5f5f5;
> a {
width: 110px;
border-right: 1px solid #f5f5f5;
text-align: center;
font-size: 16px;
&.active {
border-top: 2px solid @xtxColor;
height: 60px;
background: #fff;
line-height: 56px;
}
}
}
}
</style>
<template>
<div class="tabs-panel" v-show="name === activeName">
<slot />
</div>
</template>
<script>
import { inject } from 'vue'
export default {
name: 'TabsPanel',
props: {
label: {
type: String,
default: ''
},
name: {
type: [String, Number],
default: ''
}
},
setup () {
const activeName = inject('activeName')
return { activeName }
}
}
</script>
第六步:
1. 实现 @tab-click 自定事件
也就是在点击 a 标签的时候, 传出父组件发送请求所需的数据
<script>
import { provide } from 'vue'
import { useVModel } from '@vueuse/core'
export default {
name: 'Tabs',
props: {
modelValue: {
type: [String, Number],
default: ''
}
},
setup (props, { emit }) {
const activeName = useVModel(props, 'modelValue', emit)
// 定义点击事件的事件处理函数
const clickCheckName = (name, index) => {
activeName.value = name
emit('tab-click', { name, index })
}
provide('activeName', activeName)
return { activeName, clickCheckName }
},
render () {
const panels = this.$slots.default()
const dynamicPanels = []
panels.forEach(item => {
if (item.type.name === 'TabsPanel') {
dynamicPanels.push(item)
} else {
item.children.forEach(i => {
dynamicPanels.push(i)
})
}
})
const nav = <nav>{
dynamicPanels.map((item, index) => {
return <a onClick={() => this.clickCheckName(item.props.name, index)} class={{ active: item.props.name === this.modelValue }} href="javascript:;">{ item.props.label }</a>
})
}</nav>
return <div class="tabs">{[nav, panels]}</div>
}
}
</script>
<style scoped lang="less">
.tabs {
background: #fff;
> nav {
height: 60px;
line-height: 60px;
display: flex;
border-bottom: 1px solid #f5f5f5;
> a {
width: 110px;
border-right: 1px solid #f5f5f5;
text-align: center;
font-size: 16px;
&.active {
border-top: 2px solid @xtxColor;
height: 60px;
background: #fff;
line-height: 56px;
}
}
}
}
</style>
emit 出去之后, 在父组件中进行监听
<template>
<div class="member-order">
<Tabs v-model="activeName" @tab-click="changeTab">
<TabsPanel label="选项卡零" name="zero">选项零</TabsPanel >
<TabsPanel v-for="item in 4" :key="item" :label="`选项卡${item}`" :name="`name${item}`">内容{{item}}</TabsPanel >
</Tabs>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'MemberOrder',
setup () {
const activeName = ref('zero')
const changeTab = (obj) => {
console.log(obj)
}
return { activeName, changeTab }
}
}
</script>
这样父组件中就可以拿着后端返回的数据进行 v-for 渲染, 然后每一次点击的时候; 子组件返回父组件所需的数据, 显示不同的组件内容