element-ui 的tabs组件再实际开发过程中有部分场景满足不了开发需求,因此需要重新实现一套
// index.vue
<script>
import tabNav from './tab-nav.vue' // 引入tab导航页组件
export default {
name: 'pmsTabs',
components: { tabNav }, // 注册之
props: {
// 父组件用v-model传参,子组件须用value接参,方可接到v-model="activeName"绑定的activeName的值
value: null, // 接收到的值即为当前高亮的是哪一项
// 传递一个函数,作为tab切换的钩子函数
beforeLeave: {
// 切换标签之前的钩子,若返回 false 或者返回 Promise 且被 reject,则阻止切换
type: Function,
default: () => {
return true // 默认为true,始终允许切换tab
},
},
props: Object, // 下拉菜单props
// 是否禁用
disabled: {
type: Boolean,
default: false
}
},
data () {
return {
tabItemArr: [], // 用于传递给tabNav组件信息数据的数组
activeName: this.value, // 高亮的是哪个tab标签页选项卡
}
},
watch: {
value (val) {
this.activeName = val // 更新后重新赋值
}
},
provide () {
return {
tabsProps: this.props || {}
}
},
mounted () {
/**
* 计算收集tab页内容信息,将需要用到的信息存在tabItemArr数组中
* 并传递给tabNav组件,tabNav组件根据tabItemArr信息去v-for渲染有哪些
* */
this.calcTabItemInstances()
},
updated () {
this.calcTabItemInstances()
},
methods: {
calcTabItemInstances () {
// 重点方法
// 获取使用的地方的my-tab标签中间的内容
if (this.$slots.default) {
// 收集my-tab标签中间的插槽内容数组
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'pmsTabPane')
// 然后把这些数据交给tab-nav动态渲染
const panes = paneSlots.map(({ componentInstance }) => componentInstance)
if (!(panes.length === this.tabItemArr.length && panes.every((pane, index) => pane === this.tabItemArr[index]))) {
this.tabItemArr = panes
}
} else {
this.tabItemArr = [] // 没传递就置为空,当然需要规范使用组件,规范传递相关参数
}
},
handleTabClick (tabItem) {
this.$emit('tabClick', tabItem) // 通知父元素点击的是谁,是哪个tab-nav
let newTabName = tabItem.name // 获取传出来的最新的name名字
this.setCurrentName(newTabName) // 执行更新方法
},
handleTabDropClick (tabItem, index, dropItem) {
this.$emit('drop-click', tabItem, index, dropItem)
},
handleTabUpdateName (index, val) {
this.$emit('tab-update', index, val)
},
handleTabAdd () {
this.$emit('tab-add')
},
// 考虑到可能有异步操作,所以加上async await(比如在切换tab标签页之前,做一个问询)
async setCurrentName (newTabName) {
let oldTabName = this.activeName // 要更新了,所以当下的就变成旧的了
let res = await this.beforeLeave(newTabName, oldTabName)
if (res) {
this.$emit('input', newTabName) // 更新父组件的v-model绑定的值
this.activeName = newTabName // 自身也更新一下
}
},
},
render (h) {
// 准备参数,以便把参数传递给tab-nav组件
const navData = {
props: {
tabItemArr: this.tabItemArr, // 内容区相关信息数组
activeName: this.activeName, // 当前高亮的是哪一项
onTabClick: this.handleTabClick, // 点击某一tab项的回调
onDropClick: this.handleTabDropClick, // 点击某一tab项下拉回调
onTabUpdateName: this.handleTabUpdateName, // 点击更新名称
onTabAddClick: this.handleTabAdd, // 新增tab
disabled: this.disabled
},
}
return (
<div class="tab-Box">
<tab-nav {...navData}></tab-nav>
<div class="my-tab-content-item-box">{this.$slots.default}</div>
</div>
)
/**
* 注意:<div class="my-tab-content-item-box">{this.$slots.default}</div>写法,正常会把所有的都渲染出来
* 所以我们在myTabContent组件中再加一个判断(v-show="isActiveToShowContent"),看看当前高亮的名字是否和组件的名字一致,
* 一致才渲染.这样的话,同一时刻,根据myTabContent组件的name属性,只会对应渲染一个
* */
},
}
</script>
// tab-nav.vue
<template>
<div class="my-tab-nav-item-box">
<div class="my-tab-nav-item-wrap">
<div :class="[
'my-tab-nav-item',
tabItem.name === activeName ? 'highLight' : '',
tabItem.disabled ? 'isForbiddenItem' : '',
]"
v-for="(tabItem, index) in tabItemArr"
:key="index"
@click="changeActiveName(tabItem)">
<!-- 编辑状态 -->
<div v-if="isRename && currentName ===tabItem.name">
<el-input v-model="reNameVal"
:ref="'reName_'+tabItem._uid"
@blur="reNameBlur"
:maxlength="maxLength"
class="tab-nav-rename-input"></el-input>
<span class="el-icon-check"
@click="updateNameClick(index,tabItem)"></span>
</div>
<!-- 展示状态 -->
<div v-else>
<span>
{{ tabItem.label }}
</span>
<el-dropdown v-if="!disabled">
<span class="el-dropdown-link">
<i class="
el-icon-more"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(item,i) in dorpList"
:key="i"
@click.native="dropdownClick(tabItem,index,item)"
:disabled="item.label === '删除' && tabItemArr.length === 1">{{item.label}}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
<div class="my-tab-nav-add">
<el-button icon="el-icon-plus"
type="text"
:disabled="disabled"
@click="addTabHandle">{{addLabel}}</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'TabNav',
props: {
// 源自于内容区的数组数据,非常重要
tabItemArr: {
type: Array,
default: () => [],
},
// 当前激活的名字
activeName: {
type: String,
default: '',
},
// 接收点击选项卡函数,在点击tab选项卡的时候,通过此函数传递出去
onTabClick: {
type: Function,
},
// 接受点击下拉函数,再点击下拉菜单时,通过此函数传递
onDropClick: {
type: Function
},
// 重命名时触发
onTabUpdateName: {
type: Function
},
// 新增选项卡
onTabAddClick: {
type: Function
},
disabled: Boolean
},
inject: ['tabsProps'],
data () {
return {
// 默认下拉
defaultDrop: [
{
label: '拷贝'
},
{
label: '重命名'
},
{
label: '删除'
}
],
// 是否进入编辑状态
isRename: false,
// 当前选中tab
currentName: '',
// 重命名值
reNameVal: '',
// 定时器
timer: null
}
},
computed: {
// drop下拉菜单
dorpList () {
if (this.tabsProps && this.tabsProps.dropdownList) {
return this.tabsProps.dropdownList
} else {
return this.defaultDrop
}
},
// 更新框最大长度
maxLength () {
if (this.tabsProps && this.tabsProps.maxLength) {
return this.tabsProps.maxLength
} else {
return '20'
}
},
// 新增按钮名称
addLabel () {
if (this.tabsProps && this.tabsProps.addLabel) {
return this.tabsProps.addLabel
} else {
return '新增分组'
}
},
},
methods: {
// 标签切换
changeActiveName (tabItem) {
// 自己点自己就不让执行了
if (tabItem.name === this.activeName) {
return
}
// 如果包含禁用项disabled属性(即处于禁用状态),也不让执行(搭配.isForbiddenItem类名)
if (tabItem.disabled) {
return
}
this.isRename = false
this.currentName = ''
this.onTabClick(tabItem)
},
// 下拉菜单点击
dropdownClick (tabItem, index, row) {
// 有自定义的回调
if (row.func) {
row.func(tabItem, index, row.label)
} else {
this.onDropClick(tabItem, index, row.label)
}
if (row.label === '重命名') {
this.isRename = true
this.currentName = tabItem.name
this.reNameVal = tabItem.label
this.$nextTick(() => {
const refVal = `reName_${tabItem._uid}`
this.$refs[refVal][0] && this.$refs[refVal][0].focus()
})
}
},
// 失去焦点退出编辑状态
reNameBlur () {
this.timer = setTimeout(() => {
this.isRename = false
}, 200)
},
// 点击更新名称
updateNameClick (index) {
this.onTabUpdateName(index, this.reNameVal)
},
addTabHandle () {
this.onTabAddClick()
}
},
// 清除定时器
beforeDestroy () {
clearTimeout(this.timer)
this.timer = null
}
}
</script>
<style lang="less" scoped>
.my-tab-nav-item-box {
width: 100%;
border-bottom: 1px solid #e9e9e9;
display: flex;
align-items: center;
.my-tab-nav-item {
display: inline-block;
height: 28px;
line-height: 28px;
font-size: 14px;
font-weight: 500;
color: #303133;
margin-right: 2px;
cursor: pointer;
border: 1px solid #dcdae3;
padding: 0 12px;
border-bottom: 0;
border-radius: 4px 4px 0 0;
background: #fafafa;
.el-icon-more {
display: none;
transform: rotate(90deg);
font-size: 12px;
}
.tab-nav-rename-input {
width: 100px;
}
}
// 非禁用时鼠标悬浮样式,注意这里not的使用
.my-tab-nav-item:not(.isForbiddenItem):hover {
color: #ff8c00;
}
// 高亮项样式
.highLight {
color: #ff8c00;
background: #ffffff;
.el-icon-more {
display: inline-block;
}
}
// 禁用项样式
.isForbiddenItem {
cursor: not-allowed;
color: #aaa;
}
.my-tab-nav-item-wrap {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding-bottom: 2px;
&::-webkit-scrollbar {
/*滚动条整体样式*/
width: 8px; /*高宽分别对应横竖滚动条的尺寸*/
height: 4px;
}
&::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 4px;
-webkit-box-shadow: inset 0 0 8px rgba(118, 83, 214, 0.2);
background: #bba5ef;
}
&::-webkit-scrollbar-track {
/*滚动条里面轨道*/
-webkit-box-shadow: inset 0 0 8px rgba(118, 83, 214, 0);
border-radius: 4px;
}
&.is_grey {
&::-webkit-scrollbar-thumb {
background: #e6e6e6;
}
}
}
.my-tab-nav-add {
flex-shrink: 0;
margin-left: 10px;
}
}
</style>
// tab-pane
<template>
<div class="my-tab-content-item"
v-show="isActiveToShowContent">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'pmsTabPane',
componentName: 'pmsTabPane',
props: {
label: String, // 标签名
name: String, // 每个下方内容区都有自己的name名字
disabled: {
// 是否禁用这一项
type: Boolean,
default: false, // 默认不禁用
},
},
computed: {
// 控制根据高亮的tab显示对应标签页内容
isActiveToShowContent () {
let activeName = this.$parent.value // 比如当前高亮的是 sunwukong
let currentName = this.name // this.name的值有很多个,有:sunwukong、zhubajie、shaheshang...
// 谁等于,就显示谁
return activeName === currentName
},
},
watch: {
label () {
this.$parent.$forceUpdate()
}
}
}
</script>
<style>
.my-tab-content-item {
padding: 12px;
}
</style>
用法
<pms-tabs v-model="detailItem.groupActiveName"
:disabled="isDetail"
:props="pmsTabsProps"
@drop-click="(tab, i, drop) => tabsDropClick(detailItem,tab, i, drop,detailIndex)"
@tab-update="(index, val) => tabsUpdate(detailItem,index, val)"
@tab-add="tabsAdd(detailItem)">
<pms-tab-pane v-for="(item,i) in detailItem.quotaGroupList"
:key="'group_'+i"
:label="item.groupName"
:name="item.groupName">
<el-button>{{item.groupName}}</el-button>
</pms-tab-pane>
</pms-tabs>