Tabs组件使用方式

  1. <template>
  2. <h1>示例1</h1>
  3. <Tabs>
  4. <Tab title="导航1">内容1</Tab>
  5. <Tab title="导航2">内容2</Tab>
  6. </Tabs>
  7. </template>

如何判断子组件的类型?

将context.slots打印出来,context.slots.default()得到的就是子组件数组

  1. <template>
  2. <div>Tabs组件</div>
  3. <component :is="defaults[0]"/>
  4. <component :is="defaults[1]"/>
  5. </template>
  6. <script lang="ts">
  7. import Tab from './Tab.vue'
  8. export default {
  9. setup(props,context){
  10. const defaults = context.slots.default()
  11. defaults.forEach(tag => {
  12. if(tag.type !== Tab) {
  13. throw new Error('Tabs的子标签必须为Tab')
  14. }
  15. })
  16. return {
  17. defaults
  18. }
  19. }
  20. }
  21. </script>

渲染嵌套的插槽

props属性中可以获取到tab组件的props

  1. <template>
  2. <div>Tabs组件</div>
  3. <div v-for="title in titles" :key="title">{{title}}</div>
  4. <component v-for="(tab,index) of defaults" :is="tab" :key="index"/>
  5. </template>
  6. <script lang="ts">
  7. import Tab from './Tab.vue'
  8. export default {
  9. setup(props,context){
  10. const defaults = context.slots.default()
  11. defaults.forEach(tag => {
  12. if(tag.type !== Tab) {
  13. throw new Error('Tabs的子标签必须为Tab')
  14. }
  15. })
  16. const titles = defaults.map(tab => tab.props.title)
  17. return {
  18. defaults,
  19. titles
  20. }
  21. }
  22. }
  23. </script>

实现tab切换

  1. <template>
  2. <h1>示例1</h1>
  3. <Tabs v-model:selected="title">
  4. <Tab title="导航1">内容1</Tab>
  5. <Tab title="导航2">内容2</Tab>
  6. </Tabs>
  7. </template>
  8. <script lang="ts">
  9. import { ref } from 'vue'
  10. import Tab from '../lib/Tab.vue'
  11. import Tabs from '../lib/Tabs.vue'
  12. export default {
  13. components: {
  14. Tab, Tabs
  15. },
  16. setup(){
  17. const title = ref('导航1')
  18. return {
  19. title
  20. }
  21. }
  22. }
  23. </script>

component需要绑定key才能感知到数据更新重新渲染

  1. <template>
  2. <div class="pika-tabs">
  3. <div class="pika-tabs-nav" >
  4. <div class="pika-tabs-nav-item" :class="{selected: title === selected}" v-for="title in titles" :key="title" @click="changeTab(title)">{{title}}</div>
  5. </div>
  6. <div class="pika-tabs-content">
  7. <component class="pika-tabs-content-item" :is="current" :key="selected"/>
  8. </div>
  9. </div>
  10. </template>
  11. <script lang="ts">
  12. import { computed } from 'vue'
  13. import Tab from './Tab.vue'
  14. export default {
  15. props:{
  16. selected: String
  17. },
  18. setup(props,context){
  19. const defaults = context.slots.default()
  20. defaults.forEach(tag => {
  21. if(tag.type !== Tab) {
  22. throw new Error('Tabs的子标签必须为Tab')
  23. }
  24. })
  25. const changeTab = (value) => {
  26. context.emit('update:selected', value)
  27. }
  28. const titles = defaults.map(tab => tab.props.title)
  29. const current = computed(() => defaults.find(item => item.props.title === props.selected))
  30. return {
  31. defaults,
  32. titles,
  33. changeTab,
  34. current
  35. }
  36. }
  37. }
  38. </script>

title下面添加动态指示条

在title下面添加指示条
添加一个div, 绝对定位

  1. <div class="pika-tabs-nav" >
  2. // ...
  3. <div class="pika-tabs-nav-indicator"></div>
  4. </div>

获取宽度

要获取到title的宽度,需要先用ref获取到title所在的DOM
参考文档

  1. <template>
  2. <div class="pika-tabs">
  3. <div class="pika-tabs-nav" >
  4. <div class="pika-tabs-nav-item"
  5. :ref="el => { if (el) divs[index] = el }"
  6. :class="{selected: title === selected}"
  7. v-for="(title,index) in titles"
  8. :key="index"
  9. @click="changeTab(title)">{{title}}</div>
  10. <div class="pika-tabs-nav-indicator"></div>
  11. </div>
  12. // ...
  13. </div>
  14. </template>
  15. <script lang="ts">
  16. import { computed,onMounted,ref } from 'vue'
  17. // ...
  18. export default {
  19. // ...
  20. setup(props,context){
  21. const divs = ref([])
  22. onMounted(()=>{
  23. console.log(...divs.value)
  24. })
  25. // ...
  26. return {
  27. // ...
  28. divs
  29. }
  30. }
  31. }
  32. </script>

获取DOM节点的宽度, 使用Node.getBoundingClientRect()方法
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置

  1. <template>
  2. <div class="pika-tabs">
  3. // ...
  4. <div ref="indicator" class="pika-tabs-nav-indicator"></div>
  5. </div>
  6. // ...
  7. </div>
  8. </template>
  9. <script lang="ts">
  10. import { computed,onMounted,onUpdated, ref } from 'vue'
  11. export default {
  12. props:{
  13. selected: String
  14. },
  15. setup(props,context){
  16. const defaults = context.slots.default()
  17. const navItems = ref([])
  18. const indicator = ref(null)
  19. onMounted(()=>{
  20. const divs = navItems.value
  21. const result = divs.filter(item => item.classList.contains('selected'))[0]
  22. const {width} = result.getBoundingClientRect()
  23. indicator.value.style.width = width + 'px'
  24. })
  25. onUpdated(()=> {
  26. // 同上
  27. })
  28. // ...
  29. }
  30. }
  31. </script>

计算left

  1. setup(props,context){
  2. const defaults = context.slots.default()
  3. const navItems = ref([])
  4. const indicator = ref(null)
  5. const container = ref(null)
  6. const x = () => {
  7. const divs = navItems.value
  8. const result = divs.filter(item => item.classList.contains('selected'))[0]
  9. const {width, left: left2} = result.getBoundingClientRect()
  10. indicator.value.style.width = width + 'px'
  11. const {left: left1} = container.value.getBoundingClientRect()
  12. indicator.value.style.left = (left2 - left1) + 'px'
  13. }
  14. onMounted(x)
  15. onUpdated(x)
  16. }

优化

onMounted和onUpdated可以使用watchEffect代替,注意watchEffect会在mounted前执行,导致获取不到DOM节点,所以需要放在onMounted里

  1. onMounted(()=>{
  2. watchEffect(() => {
  3. console.log(selectedItem.value)
  4. const {width, left: left2} = selectedItem.value.getBoundingClientRect()
  5. indicator.value.style.width = width + 'px'
  6. const {left: left1} = container.value.getBoundingClientRect()
  7. indicator.value.style.left = (left2 - left1) + 'px'
  8. })
  9. })