只管目前大多数 UI 框架都有 tab 组件,然而有时候并不能满足需要,或者须要对组件进行二次开发,思考到总总起因,还是决定本人亲手写一个算了。

Element.getBoundingClientRect()
实现其实不难,这里只需应用 getBoundingClientRect 这个函数就能够,依据文档的介绍,该办法返回元素的大小及其绝对于视窗的地位。

看图后应该不难理解,图中 0,0 指定是浏览器中窗口的左上角,因而应用该函数后元素返回的 top、bottom、left、right 都是绝对窗口左上角的地位。

设计剖析

因为 tab 的数量不是固定的,很有可能超出元素边界,所以须要用外层来包含 tablist,并且是 overflow: hidden,滚动成果通过扭转 css 中的 translate 来实现。

<template>    <div>        <button class="cat-button" type="button" @click="addTab">增加标签</button>    </div>    <div class="cat-tabbar">        <div class="cat-tabbar__arrow" @click="scrollTabbar('prev')" v-if="showArrow">后退</div>        <div ref="containerElement" class="cat-tabbar__container">            <ul ref="tabbarElement" class="cat-tabbar__list">                <template v-for="(item, index) in data" :key="index">                    <li :class="['cat-tabbar__item', activeName === item.name && 'is-active']" @click="changeTab(index)">                        <div class="tab-text">{{item.title}}</div>                        <div class="tab-close" v-if="data.length > 1">                            <span @click.stop="removeTab(index)">X</span>                        </div>                    </li>                </template>            </ul>        </div>        <div class="cat-tabbar__arrow" @click="scrollTabbar('next')" v-if="showArrow">后退</div>    </div></template>

实现剖析

如何计算滚动地位?只有通过 getBoundingClientRect 获得各元素的地位后,再判断 tab 是否超出父级元素的边界,而后依据以后选中的 tab 地位计算出向前或向后须要滚动多少像素。

<script lang="ts">    import { defineComponent, ref, reactive } from "vue";    export default defineComponent({        name: "CatTabs",        setup() {            const containerElement = ref(),                tabbarElement = ref(),                showArrow = ref(false),                activeName = ref("home");            const data = reactive([                {                    name: "home",                    title: "首页"                }            ]);            const addTab = () => {                const tabName = new Date().getTime().toString();                data.push({                    name: tabName,                    title: "标签长一点-" + (data.length + 1)                })                activeName.value = tabName;                scrollTab();            }            // 抉择标签            const changeTab = (index: number) => {                activeName.value = data[index].name;                scrollTab();            }            // 移除标签            const removeTab = (index: number) => {                data.splice(index, 1);                const lastTab = data[data.length - 1];                activeName.value = lastTab.name;                scrollTab();            }            // 滚动标签            const scrollTab = () => {                setTimeout(() => {                    const el = {                        container: containerElement.value,                        tabbar: tabbarElement.value,                        activeTab: containerElement.value.querySelector("li.is-active"),                        lastTab: containerElement.value.querySelector("li:last-child")                    }                    if (el.tabbar.scrollWidth > el.container.clientWidth) {                        showArrow.value = true;                        // 期待箭头元素呈现后再计算,不然可能呈现计算误差                        setTimeout(() => {                            const rect = {                                container: el.container.getBoundingClientRect(), // 外层容器                                tabbar: el.tabbar.getBoundingClientRect(), // 标签栏                                activeTab: el.activeTab?.getBoundingClientRect(), // 标签栏中被选中的标签                                lastTab: el.lastTab?.getBoundingClientRect() // 标签栏中最初一个标签                            }                            if (rect.activeTab && rect.lastTab) {                                let tabbarOffset = rect.container.left - rect.tabbar.left, // 计算标签栏偏移容器间隔                                    activeTabOffsetLeft = rect.container.left - rect.activeTab.left, // 计算标签偏移容器右边的间隔                                    activeTabOffsetRight = rect.activeTab.right - rect.container.right; // 计算标签偏移容器左边的间隔                                // 计算最初一个标签和容器最左边之间的间隔                                const lastOffset = rect.container.right - rect.lastTab.right;                                if (activeTabOffsetLeft < lastOffset) {                                    activeTabOffsetLeft += lastOffset - activeTabOffsetLeft;                                }                                // 判断标签是否超出父元素左边界                                if (activeTabOffsetLeft > 0) {                                    const scrollX = tabbarOffset - activeTabOffsetLeft;                                    el.tabbar.style.transform = "translate3d(-" + scrollX + "px,0,0)";                                }                                // 判断标签是否超出父元素右边界                                if (activeTabOffsetRight > 0) {                                    const scrollX = tabbarOffset + activeTabOffsetRight;                                    el.tabbar.style.transform = "translate3d(-" + scrollX + "px,0,0)";                                }                            }                        }, 0)                    } else {                        showArrow.value = false;                        el.tabbar.style.transform = "translate3d(0,0,0)";                    }                }, 0)            }            // 滚动标签栏            const scrollTabbar = (direction: "prev" | "next") => {                const el = {                    container: containerElement.value,                    tabbar: tabbarElement.value,                    lastTab: containerElement.value.querySelector("li:last-child")                }                const rect = {                    container: el.container.getBoundingClientRect(), // 外层容器                    tabbar: el.tabbar.getBoundingClientRect(), // 标签栏                    lastTab: el.lastTab?.getBoundingClientRect() // 标签栏中最初一个标签                }                if (rect.lastTab) {                    const barOffsetLeft = rect.container.left - rect.tabbar.left, // 计算标签栏偏移容器间隔                        barOffsetRight = rect.lastTab.right - rect.container.right; // 计算标签偏移容器左边的间隔                    // 判断标签栏是否超出父元素左边界(后退)                    if (direction === "prev" && barOffsetLeft > 0) {                        let scrollX = 0;                        // 每次滚动的最长距离为容器宽度                        if (barOffsetLeft > el.container.clientWidth) {                            scrollX = barOffsetLeft - el.container.clientWidth;                        }                        el.tabbar.style.transform = "translate3d(-" + scrollX + "px,0,0)";                    }                    // 判断标签栏是否超出父元素右边界(后退)                    if (direction === "next" && barOffsetRight > 0) {                        let scrollX = barOffsetLeft + barOffsetRight;                        // 每次滚动的最长距离为容器宽度                        if (barOffsetRight > el.container.clientWidth) {                            scrollX = barOffsetLeft + el.container.clientWidth;                        }                        el.tabbar.style.transform = "translate3d(-" + scrollX + "px,0,0)";                    }                }            }            return {                containerElement,                tabbarElement,                showArrow,                activeName,                changeTab,                removeTab,                scrollTabbar,                data,                addTab            };        },    });</script>

less款式

.cat-tabbar {    display: flex;    align-items: center;    width: 100%;    border-top: 1px solid #f2f2f2;    padding-top: 8px;    &__arrow {        display:flex;        align-items:center;        height: 40px;        line-height: 0;        padding: 0 16px;        cursor: pointer;        color: #fff;        background-color: #000;        &:hover {            color: dodgerblue;        }    }    &__container {        flex: 1;        overflow: hidden;    }    &__list {        display: flex;        white-space: nowrap;        transition: transform 200ms;    }    &__item {        display: flex;        align-items: stretch;        height: 40px;        cursor: pointer;        border-top-left-radius: 4px;        border-top-right-radius: 4px;        .tab-text {            display: flex;            justify-content: center;            align-items: center;            padding-left: 20px;            color: #777;            // 既是最初一个,又是第一个            &:last-child:first-child {                padding-right: 20px;            }        }        .tab-close {            display: flex;            justify-content: center;            align-items: center;            width: 40px;            height: 100%;            color: #313b46;            span {                font-size: 12px;                transition: color 200ms;            }            span:hover {                color: red;            }        }    }    &__item.is-active {        background-color: dodgerblue;        .tab-text {            color: #fff;        }        .tab-close {            color: #fff;        }    }}