组件封装 – Tabs组件

  • Post author:
  • Post category:其他


首先我们先来看一下 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 渲染, 然后每一次点击的时候; 子组件返回父组件所需的数据, 显示不同的组件内容



版权声明:本文为weixin_48524561原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。