使用Vue开发Tabs组件(二)

一个 Tabs 的组成

Tabs

-Tab Header
-Tab Panel

一、TabPanel 组件开发

TabPanel Template 基本结构

<div :class="panelCls()">
    <slot></slot>
</div>

因为每个 Tab Header label name 需要在子组件上配置,故该子组件的 props 应该有以下俩值

props: {
    label: {
        default: "",
        type: String
    },
    name: [String, Number]
}

为了判断 TabPanel 显示和隐藏,故在 data 中定义以下值

data() {
    return {
        show: true,
        currentName: this.name
    }
},

随之便引出了控制隐藏显示的样式方法

panelCls(){
    return ["tab-panel-content", {['panel-active']: this.show === true}];
}

TabPanel name label 值发生变化后, Tab Header 也需要相应的进行更新渲染,定义一个 updateNav 函数 直接去调用父类的 updateNav 函数进行更新。随后在添加上要监听的属性和基本配置得出 TabPanel 组件所有代码

<template>
  <div :class="panelCls()">
    <slot></slot>
  </div>
</template>
<script>
export default {
  data() {
    return {
      show: true,
      currentName: this.name
    };
  },
  name: "TabPanel",
  props: {
    label: {
      type: [String, Function],
      default: ""
    },
    name: [String, Number]
  },
  watch: {
    label() {
      this.updateNav();
    },
    name(val) {
      this.currentName = val;
      this.updateNav();
    }
  },
  inject: ["TabsInstance"],
  computed: {},
  methods: {
    updateNav() {
      this.TabsInstance.updateNav();
    },
    panelCls() {
      return ["tab-panel-content", {['panel-active']: this.show === true}];
    }
  },
  mounted() {
    this.updateNav();
  }
};
</script>

二、编写父类组件 Tabs

Tabs Template 基本结构

<div class="tab">
    <div class="tab-header">
        <ul class="tab-item" ref="nav">
            <li
                :class="tabCls(item)"
                v-for="(item, index) in navList"
                :key="index"
                @click="handleChange(index)"
            >
                {{item.label}}
            </li>
        </ul>
        <div class="active-bar-link bar-animated" :style="barStyle"></div>
    </div>
    <div class="tab-panel" ref="panels" :style="contentStyle">
        <slot></slot>
    </div>
</div>

在之前实现的 Html 的基础稍加改造。使用 for 循环渲染 Tab Header , class 改为函数,更好的控制选中非选中样式变动。根据上面可在JS中定义以下结构

export default {
    name: "Tabs",
    data() {
        return {
            navList: [],
            activeKey: this.value,
        };
    },
    props: {
        value: [String, Number]
    },
    watch: {},
    computed: {},
    mounted() {},
    methods: {
        handleChange(index) {
            this.activeKey = this.navList[index].name;
        },
        tabCls(item) {
            return [
                `tab-item-title`,
                {
                    [`tab-active`]: item.name === this.activeKey
                }
            ];
        },
    }
};

接下来编写很重要的一个函数 updateNav ,在子元素,挂载更新时都需要调用到

// 先获取所有面板
getPanels() {
    const TabPanels = this.$children.filter(
        item => item.$options.name === "TabPanel"
    );
    TabPanels.sort((a, b) => {
        if (a.index && b.index) {
            return a.index > b.index ? 1 : -1;
        }
    });
    return TabPanels;
},
updateNav() {
    this.navList = [];
    this.getPanels().forEach((panel, index) => {
        this.navList.push({
            label: panel.label,
            name: panel.name || index
        });
        if (!panel.currentName) panel.currentName = index;
        if (index === 0) {
            if (!this.activeKey)
                this.activeKey = panel.currentName || index;
        }
    });
    this.updateStatus();
},
updateStatus() {
    const navs = this.getPanels();
    navs.forEach(
        tab => (tab.show = tab.currentName === this.activeKey)
    );
}

最后在进行一些面板切换和选中的样式细节优化,再加上一个简单的 click 事件,调整的代码为

<template>
    <div class="tab">
        <div class="tab-header">
            <ul class="tab-item" ref="nav">
                <li
                    :class="tabCls(item)"
                    v-for="(item, index) in navList"
                    :key="index"
                    @click="handleChange(index)"
                >
                    {{item.label}}
                </li>
            </ul>
			<div class="active-bar-link bar-animated" :style="barStyle"></div>
        </div>
        <div class="tab-panel" ref="panels" :style="contentStyle">
            <slot></slot>
        </div>
    </div>
</template>
<script>
export default {
    name: "Tabs",
    data() {
        return {
            navList: [],
            activeKey: this.value,
			barWidth: 0,
			barOffset: 0
        };
    },
    props: {
        value: [String, Number]
    },
    provide() {
        return { TabsInstance: this };
    },
    watch: {
        activeKey() {
			this.updateStatus();
			this.updateBar();
        }
    },
    computed: {
		barStyle(){
			let style = {
				visibility: 'visible',
				width: `${this.barWidth}px`
			}
			style.transform = `translate3d(${this.barOffset}px, 0px, 0px)`;
			return style;
		},
		contentStyle(){
			const index = this.getTabIndex(this.activeKey);
			return {'margin-left': index>0 ?`-${index}00%`:0};
		}
	},
    mounted() {},
    methods: {
        handleChange(index) {
            this.activeKey = this.navList[index].name;
            const nav = this.navList[index];
			this.$emit('tab-click', nav.name)
        },
        tabCls(item) {
            return [
                `tab-item-title`,
                {
                    [`tab-active`]: item.name === this.activeKey
                }
            ];
        },
        getPanels() {
            const TabPanels = this.$children.filter(
                item => item.$options.name === "TabPanel"
            );
            TabPanels.sort((a, b) => {
                if (a.index && b.index) {
                    return a.index > b.index ? 1 : -1;
                }
            });
            return TabPanels;
        },
        updateNav() {
            this.navList = [];
            this.getPanels().forEach((panel, index) => {
                this.navList.push({
                    label: panel.label,
                    name: panel.name || index
                });
                if (!panel.currentName) panel.currentName = index;
                if (index === 0) {
                    if (!this.activeKey)
                        this.activeKey = panel.currentName || index;
                }
            });
			this.updateStatus();
			this.updateBar();
        },
        updateBar() {
            this.$nextTick(() => {
                const index = this.getTabIndex(this.activeKey);
				if (!this.$refs.nav) return;
				const prevTabs = this.$refs.nav.querySelectorAll('.tab-item-title');
				const tab = prevTabs[index];
				this.barWidth = tab? parseFloat(tab.offsetWidth): 0;
				if(index > 0){
					let offset = 0;
					for(let i = 0;i < index;i++){
						offset += prevTabs[i].offsetWidth
					}
					this.barOffset = offset;
				}else{
					this.barOffset = 0;
				}
            });
        },
        getTabIndex(name) {
            return this.navList.findIndex(nav => nav.name === name);
        },
        updateStatus() {
            const navs = this.Panels();
            navs.forEach(
                tab => (tab.show = tab.currentName === this.activeKey)
            );
        }
    }
};
</script>

Css 样式

html {
    height: 100%;
    padding: 0;
    margin: 0;
}

a {
    text-decoration: none;
}

a:visited {
    color: #000;
}

.clear {
    clear: both;
}

.tab {
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    color: rgba(0, 0, 0, 0.65);
    font-size: 14px;
    font-variant: tabular-nums;
    line-height: 1.5;
    list-style: none;
    -webkit-font-feature-settings: "tnum";
    font-feature-settings: "tnum";
    position: relative;
    overflow: hidden;
    zoom: 1;
}

.tab-header{
	border-bottom: 1px solid #e8e8e8;
}

.tab-item {
    list-style: none;
    padding-inline-start: 0;
	margin-block-end: 0;
}

.tab-item > .tab-item-title {
    position: relative;
    float: left;
    padding: 10px 15px;
    text-align: center;
    font-weight: 500;
    color: #000;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
        "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica,
        Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
        "Segoe UI Symbol";
}

.tab-item > .tab-item-title.tab-active {
    color: #1890ff;
}


.tab-item > .tab-item-title::before {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    height: 2px;
    width: 0;
}

.active-bar-link{
	background-color: #1890ff;
    height: 2px;
	clear: both;
}

.active-bar-link.bar-animated{
	-webkit-transition: width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1),-webkit-transform .3s cubic-bezier(.645, .045, .355, 1);
    transition: width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1),-webkit-transform .3s cubic-bezier(.645, .045, .355, 1); 
    transition: transform .3s cubic-bezier(.645, .045, .355, 1),width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1); 
    transition: transform .3s cubic-bezier(.645, .045, .355, 1),width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1),-webkit-transform .3s cubic-bezier(.645, .045, .355, 1); 
}

.tab-item > .tab-item-title.tab-active > a {
    color: #1890ff;
}

.tab-panel {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-orient: horizontal;
    -webkit-box-direction: normal;
    -ms-flex-direction: row;
    flex-direction: row;
    transition: margin-left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
    will-change: margin-left;
    width: 100%;
}

.tab-panel::before {
    display: block;
    overflow: hidden;
    content: "";
}

.tab-panel-content {
    height: 0;
    padding: 0 !important;
    opacity: 0;
    pointer-events: none;
    flex-shrink: 0;
    width: 100%;
    -webkit-transition: opacity 0.45s;
    transition: opacity 0.45s;
}

.tab-panel-content.panel-active {
    flex-shrink: 0;
    height: auto;
    width: 100%;
    opacity: 1;
    -webkit-transition: opacity 0.45s;
    transition: opacity 0.45s;
}

结语

以上一个基本的 Tabs 组件就实现了。

Tabs代码

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值