小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
TIP 👉 生于优患,死于安乐。《孟子·告于下》
前言
在我们日常项目开发中,我们经常会写一些选项卡,所以封装了这款tab项组件。标签页
Tabs组件属性
1. value
- 选中标签页值(即选中TabPane的name值)
- 值为字符串类型
- 非必填默认为第一个TabPane的name
2. lazy
- 未显示的内容面板是否延迟渲染
- 值为布尔类型
- 默认为false
样式要求
- 组件外面需要包裹可以相对定位的元素,增加样式:
position: relative
TabPane组件属性
1. name
- 标签页英文名称
- 值为字符串类型
- 必填
2. label
- 标签页导航标签显示的文字
- 值为字符串类型
- 必填
3. scroll
- 是否有滚动条
- 值为布尔类型
- 默认值为:true
示例
<template>
<div class="tabs-wrap">
<Tabs v-model="curTab" :lazy="true">
<TabPane name="list1" label="列表1"><TabPaneList1></TabPaneList1></TabPane>
<TabPane name="list2" label="列表2"><TabPaneList2></TabPaneList2></TabPane>
<TabPane name="list3" label="列表3"><TabPaneList3></TabPaneList3></TabPane>
</Tabs>
</div>
</template>
<script>
import { Tabs, TabPane } from '@/components/m/tabs'
import TabPaneList1 from './TabPaneList1.vue'
import TabPaneList2 from './TabPaneList2.vue'
import TabPaneList3 from './TabPaneList3.vue'
export default {
name: 'TabsDemo',
components: {
Tabs,
TabPane,
TabPaneList1,
TabPaneList2,
TabPaneList3
},
data () {
return {
// 当前选中的标签
curTab: ''
}
}
}
</script>
<style lang="scss" scoped>
.tabs-wrap {
position: absolute;
top: $app-title-bar-height;
bottom: 0;
left: 0;
right: 0;
}
</style>
实现tabs.vue
<template>
<div class="tabs">
<div class="tab-list" ref="tabWrap">
<div class="tab-item"
v-for="info in tabInfos"
:key="info.name"
:class="{active: info.name === curValue}"
@click="handleClickTab(info.name)">
{{info.label}}
</div>
<div v-if="tabLineX" class="tab-line" :style="{ transform: `translateX(${tabLineX}px) translateX(-50%)`}"></div>
</div>
<div class="tab-pane-wrap">
<slot></slot>
</div>
</div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
name: 'Tabs',
props: {
// 选中标签页值(页签的英文名)
value: String,
// 未显示的内容面板是否延迟渲染
lazy: {
type: Boolean,
default: false
}
},
data () {
return {
// 组件实例的唯一ID
id: generateUUID(),
// 当前选中的标签页值(标签页的英文名)
curValue: this.value,
// 标签页信息数组
tabInfos: [],
// 标签页面板vue实例数组
panes: [],
// 表示当前标签页的蓝线x坐标位置
tabLineX: 0
}
},
watch: {
value (val) {
this.curValue = val
},
curValue (val) {
this.$eventBus.$emit('CHANGE_TAB' + this.id, val)
this.$emit('change', val)
this.calcTabLineX()
}
},
computed: {
// 当前选中标签页索引
curIndex () {
let index = -1
for (let i = 0; i < this.tabInfos.length; i++) {
if (this.tabInfos[i].name === this.curValue) {
index = i
break
}
}
return index
}
},
mounted () {
this.calcPaneInstances()
},
beforeDestroy () {
this.$eventBus.$off('CHANGE_TAB' + this.id)
},
methods: {
// 计算标签页面板实例信息
calcPaneInstances () {
if (this.$slots.default) {
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'TabPane')
const panes = paneSlots.map(({ componentInstance }) => componentInstance)
const tabInfos = paneSlots.map(({ componentInstance }) => {
console.log(componentInstance.name, componentInstance)
return {
name: componentInstance.name,
label: componentInstance.label
}
})
this.tabInfos = tabInfos
this.panes = panes
if (!this.curValue) {
if (tabInfos.length > 0) {
this.curValue = tabInfos[0].name
}
} else {
this.$eventBus.$emit('CHANGE_TAB' + this.id, this.curValue)
this.calcTabLineX()
}
}
},
// 标签页点击事件处理方法
handleClickTab (val) {
this.curValue = val
},
// 计算表标识表示当前标签页蓝线的位置
calcTabLineX () {
let wrapWidth = this.$refs.tabWrap.getBoundingClientRect().width
let tabWidth = this.panes.length > 0 ? wrapWidth / this.panes.length : wrapWidth
let positionX = tabWidth * (this.curIndex + 0.5)
this.tabLineX = positionX
}
}
}
</script>
<style lang="scss" scoped>
.tabs {
display: flex;
flex-direction: column;
height: 100%;
.tab-list {
position: relative;
flex: none;
display: flex;
background-color: #FFF;
.tab-item {
flex: 1;
line-height: 80px;
text-align: center;
color: #8E8E8E;
&.active {
position: relative;
color: $base-color;
}
}
.tab-line {
position: absolute;
bottom: 0;
left: 0;
z-index: 1;
width: 4em;
height: 4px;
background-color: $base-color;
transition-duration: 0.3s;
}
}
.tab-pane-wrap {
flex: 1;
}
}
</style>
感谢评论区大佬的点拨。