一个人撸码!之vue3+vite+element-plus后台管理(标签页组件)

一个后台管理常常需要一个标签页来管理已经打开的页面,这里我们单独写一个组件来展示标签页数组。


该标签页组件只做展示不涉及操作数据。标签页数组可记录已打开的数组,还能定义什么页面需要缓存,是一个重要的功能呢。

首先,建立一个TagList.vue组件,里面代码如下

```js

<template>

<div

    class="tag-list-cp-container"

    ref="TagListRef">

    <div

        class="left"

        @wheel="handleScroll">

        <el-scrollbar

            ref="ElScrollbarRef"

            height="100%">

            <draggable

                class="scrollbar-container"

                item-key="sign"

                v-model="tagListTrans">

                <template #item="{element}">

                    <div

                        :class="{

                            'item':true,

                            'active':dataContainer.activeSign==element.sign,

                        }"

                        @click="handleClick(element)"

                        @contextmenu.prevent="e=>{

                            handleClickContext(e,element);

                        }">

                        <SvgIcon

                            class="sign icon-sign"

                            v-if="element.showTagIcon && element.iconName"

                            :style="'width: 15px;min-width:15px;height: 15px;'"

                            :name="element.iconName"></SvgIcon>

                        <div

                            class="sign"

                            v-else-if="dataContainer.activeSign==element.sign">

                        </div>

                        {{element.title}}

                        <div

                            v-if="!element.fixed"

                            @click.stop="handleRemove(element)"

                            class="bt">

                            <SvgIcon

                                :style="'width:12px;height:12px;'"

                                name="times"></SvgIcon>

                        </div>

                        <div

                            v-if="element.isCache"

                            class="cache"></div>

                    </div>

                </template>

            </draggable>

        </el-scrollbar>

    </div>

    <div class="bt-list">

        <div

            class="bt"

            @click="handleOptionClick(5)">

            <SvgIcon

                :style="'width:15px;height:15px;'"

                name="redo"></SvgIcon>

        </div>

        <div

            class="bt"

            @click="handleToLeft()">

            <SvgIcon

                :style="'width:15px;height:15px;'"

                name="arrow-left"></SvgIcon>

        </div>

        <div

            class="bt"

            @click="handleToRight()">

            <SvgIcon

                :style="'width:15px;height:15px;'"

                name="arrow-right"></SvgIcon>

        </div>

    </div>

    <div

        ref="RightOptionRef"

        class="right">

        <div

            @click="()=>{

                dataContainer.show_1 = !dataContainer.show_1;

            }"

            class="bt">

            <SvgIcon

                :style="'width:20px;height:20px;'"

                name="icon-drag"></SvgIcon>

        </div>

        <div

            v-if="dataContainer.show_1"

            class="bt-list-container">

            <div

                v-if="dataContainer.tagList.length>1"

                class="item"

                @click="handleOptionClick(1)">

                <SvgIcon

                    :style="'width:16px;height:16px;color:#f86464;'"

                    name="times"></SvgIcon>

                关闭当前标签页

            </div>

            <div

                v-if="dataContainer.tagList.length>1"

                class="item"

                @click="handleOptionClick(2)">

                <SvgIcon

                    :style="'width:16px;height:16px;color:#f86464;'"

                    name="borderverticle-fill"></SvgIcon>

                关闭其他标签页

            </div>

            <div

                v-if="dataContainer.tagList.length>1"

                class="item"

                @click="handleOptionClick(3)">

                <SvgIcon

                    :style="'width:16px;height:16px;color:#f86464;'"

                    name="arrow-left"></SvgIcon>

                关闭左边标签页

            </div>

            <div

                v-if="dataContainer.tagList.length>1"

                class="item"

                @click="handleOptionClick(4)">

                <SvgIcon

                    :style="'width:16px;height:16px;color:#f86464;'"

                    name="arrow-right"></SvgIcon>

                关闭右边标签页

            </div>

            <div

                class="item re-bt"

                @click="handleOptionClick(5)">

                <SvgIcon

                    :style="'width:16px;height:16px;color:#0072E5;'"

                    name="redo"></SvgIcon>

                刷新当前标签页

            </div>

            <div

                class="item"

                @click="handleOptionClick(6)">

                <SvgIcon

                    :style="'width:16px;height:16px;color:#0072E5;'"

                    name="expand-alt"></SvgIcon>

                视图全屏(Esc键退出)

            </div>

        </div>

    </div>

    <div

        v-if="dataContainer.show"

        :style="{

            '--location-x':`${dataContainer.location.x || 0}px`,

            '--location-y':`${dataContainer.location.y || 0}px`,

        }"

        class="bt-list-container">

        <div

            class="item"

            @click="handleSwitchCache()">

            <SvgIcon

                :style="'width:16px;height:16px;'"

                name="switch"></SvgIcon>

            切换缓存状态

        </div>

        <div

            class="item"

            @click="handleSwitchFixed()">

            <SvgIcon

                :style="'width:16px;height:16px;'"

                name="nail"></SvgIcon>

            切换固定状态

        </div>

        <div

            class="item re-bt"

            @click="handleRefresh()">

            <SvgIcon

                :style="'width:16px;height:16px;color:#0072E5;'"

                name="redo"></SvgIcon>

            刷新此标签页

        </div>

        <div

            class="item"

            @click="handleOptionClick(6)">

            <SvgIcon

                :style="'width:16px;height:16px;color:#0072E5;'"

                name="expand-alt"></SvgIcon>

            视图全屏

        </div>

    </div>

</div>

</template>

<script>

/*

* 标签切换按钮组件

* 由外部指定数据

*/

import {

    defineComponent,ref,reactive,

    computed,onMounted,watch,toRef,

    onUnmounted,

    nextTick,

} from "vue";

import SvgIcon from "@/components/svgIcon/index.vue";

import draggable from 'vuedraggable';

export default {

    name: 'TagList',

    components: {

        SvgIcon,

        draggable,

    },

    props:{

        /**

        * 所显示的标签列表

        *  */

        /**

        * 一个tag例子的属性介绍

        */

        // {

        //    title:'标签一',  //标签标题

        //    sign:'/main/index',  //唯一标识

        //    fullPath:'/main/index',  //跳转地址,完整地址

        //    isCache:true,  //该标签页面是否缓存

        //    fixed:false,  //是否固定,不可删除

        // }

        tagList:{

            type:Array,

            default:()=>{

                return [];

            },

        },

        /** 当前活动的唯一标识 */

        activeSign:{

            type:[Number,String],

            default:0,

        },

    },

    emits:[

        'onChange','onClick','onRemove','onOptionClick','onSwitchCache','onSwitchFixed',

        'onRefresh',

    ],

    setup(props,{emit}){

        const ElScrollbarRef = ref(null);

        const TagListRef = ref(null);

        const RightOptionRef = ref(null);

        const dataContainer = reactive({

            tagList:toRef(props,'tagList'),

            activeSign:toRef(props,'activeSign'),

            show:false,

            location:{},

            show_1:false,

        });

        const otherDataContainer = {

            activeItem:null,

        };

        /** 用来排序转换的数组,由外部确定是否转换 */

        const tagListTrans = computed({

            get(){

                return dataContainer.tagList;

            },

            set(value){

                emit('onChange',value);

            },

        });

        /** 标签点击事件,向外部抛出 */

        function handleClick(item){

            emit('onClick',item);

        }

        /** 标签删除事件 */

        function handleRemove(item){

            emit('onRemove',item);

        }

        /** 操作事件 */

        function handleOptionClick(type){

            emit('onOptionClick',type);

        }

        /**

        * 鼠标滚动事件

        * 横向滚动标签页

        *  */

        function handleScroll(e){

            if(!ElScrollbarRef.value) return;

            /** shift + 鼠标滚轮可以横向滚动 */

            if(e.shiftKey) return;

            let el = ElScrollbarRef.value.wrapRef;

            let scrollLeft = el.scrollLeft;

            if(e.deltaY < 0){

                scrollLeft = scrollLeft - 30;

            }else{

                scrollLeft = scrollLeft + 30;

            }

            el.scrollLeft = scrollLeft;

        }

        /**

        * 自动滚动到相应标签

        * 防止标签没在视区

        */

        function autoScroll(){

            nextTick(()=>{

                if(!ElScrollbarRef.value) return;

                let el = ElScrollbarRef.value.wrapRef;

                let target = el.querySelector('.item.active');

                if(!target) return;

                let rect = el.getBoundingClientRect();

                let rect_1 = target.getBoundingClientRect();

                if(rect_1.x < rect.x){

                    // 表示在左边遮挡

                    let scroll = rect.x - rect_1.x;

                    el.scrollLeft = el.scrollLeft - scroll - 5;

                }

                if((rect_1.x + rect_1.width) > (rect.x + rect.width)){

                    // 表示在右边遮挡

                    let scroll = rect_1.x - (rect.x + rect.width);

                    el.scrollLeft = el.scrollLeft + scroll + rect_1.width + 5;

                }

            });

        }

        watch(toRef(props,'activeSign'),()=>{

            autoScroll();

        });

        onMounted(()=>{

            autoScroll();

        });

        /** 鼠标右击,展示自定义右击面板 */

        function handleClickContext(e,item){

            if(!TagListRef.value) return;

            let el = TagListRef.value;

            let el_1 = e.target;

            let rect = el.getBoundingClientRect();

            let rect_1 = el_1.getBoundingClientRect();

            let location = {

                x:rect_1.x - rect.x,

                y:rect_1.y - rect.y + rect_1.height,

            };

            dataContainer.location = location;

            dataContainer.show = true;

            otherDataContainer.activeItem = item;

        }

        /** 初始化隐藏事件 */

        function initHiddenEvent(){

            function callbackFn(e){

                dataContainer.show = false;

            }

            document.addEventListener('click', callbackFn);

            onUnmounted(()=>{

                document.removeEventListener('click', callbackFn);

            });

        }

        initHiddenEvent();

        /**

        * 切换缓存状态

        * 由外部实现

        *  */

        function handleSwitchCache(){

            if(!otherDataContainer.activeItem) return;

            emit('onSwitchCache',otherDataContainer.activeItem);

        }

        /**

        * 切换固定状态

        * 由外部实现

        *  */

        function handleSwitchFixed(){

            if(!otherDataContainer.activeItem) return;

            emit('onSwitchFixed',otherDataContainer.activeItem);

        }

        /**

        * 刷新标签页

        * 由外部实现

        *  */

        function handleRefresh(){

            if(!otherDataContainer.activeItem) return;

            emit('onRefresh',otherDataContainer.activeItem);

        }

        /** 跳转到右侧 */

        function handleToRight(){

            let index = dataContainer.tagList.findIndex(item=>{

                return item.sign == dataContainer.activeSign;

            });

            if(index == -1) return;

            let target = dataContainer.tagList[index + 1];

            if(!target) return;

            handleClick(target);

        }

        /** 跳转到左侧 */

        function handleToLeft(){

            let index = dataContainer.tagList.findIndex(item=>{

                return item.sign == dataContainer.activeSign;

            });

            if(index == -1) return;

            let target = dataContainer.tagList[index - 1];

            if(!target) return;

            handleClick(target);

        }

        /** 初始化隐藏事件 */

        function initHiddenEvent_1(){

            function callbackFn(e){

                if(!RightOptionRef.value) return;

                if(!e || !e.target) return;

                if(RightOptionRef.value.contains(e.target)) return;

                dataContainer.show_1 = false;

            }

            document.addEventListener('click', callbackFn);

            onUnmounted(()=>{

                document.removeEventListener('click', callbackFn);

            });

        }

        initHiddenEvent_1();

        return {

            dataContainer,

            handleClick,

            handleRemove,

            handleOptionClick,

            tagListTrans,

            handleScroll,

            ElScrollbarRef,

            handleClickContext,

            TagListRef,

            handleSwitchCache,

            handleSwitchFixed,

            handleRefresh,

            handleToRight,

            handleToLeft,

            RightOptionRef,

        };

    },

}

</script>

<style scoped lang="scss">

.tag-list-cp-container {

    height: 100%;

    width: 100%;

    padding: 0;

    box-sizing: border-box;

    display: flex;

    flex-direction: row;

    justify-content: space-between;

    align-items: center;

    color: var(--text-color);

    >.left{

        flex: 1 1 0;

        width: 0;

        height: 100%;

        :deep(.el-scrollbar__bar){

            &.is-horizontal{

                height: 5px !important;

                opacity: 0.5;

            }

        }

        :deep(.el-scrollbar__view){

            height: 100%;

        }

        :deep(.scrollbar-container){

            display: flex;

            flex-direction: row;

            justify-content: flex-start;

            align-items: center;

            width: fit-content;

            height: 100%;

            .item{

                cursor: pointer;

                display: flex;

                flex-direction: row;

                justify-content: center;

                align-items: center;

                padding: 5px 8px;

                box-sizing: border-box;

                margin-left: 5px;

                font-size: 13px;

                height: 30px;

                width: max-content;

                border-radius: 3px;

                color: #606266;

                position: relative;

                transition: all 0.2s;

                &:last-child{

                    margin-right: 5px;

                }

                &.active{

                    background-color: #5240ff30;

                    color: #5240ff;

                    font-weight: bold;

                    box-shadow: inset 0 1px 4px #00000034;

                    // border:1px solid rgb(196, 196, 196);

                }

                &:hover{

                    background-color: #5240ff30;

                    color: #5240ff;

                }

                >.sign{

                    width: 10px;

                    height: 10px;

                    border-radius: 50%;

                    background-color: #5240ff;

                    margin-right: 5px;

                    &.icon-sign{

                        background-color: transparent;

                    }

                }

                >.bt{

                    width: fit-content;

                    height: fit-content;

                    display: flex;

                    flex-direction: row;

                    justify-content: center;

                    align-items: center;

                    margin-left: 5px;

                }

                >.cache{

                    width: 30%;

                    max-width: 30px;

                    min-width: 15px;

                    height: 3px;

                    border-radius: 999px;

                    background-color: #5340ff34;

                    position: absolute;

                    bottom: 0;

                }

            }

        }

    }

    >.bt-list{

        display: flex;

        flex-direction: row;

        align-items: center;

        padding: 0 10px;

        box-sizing: border-box;

        border-left: 1px solid var(--border-color);

        box-shadow: inset 0 1px 4px #00000010;

        height: 100%;

        >*{

            margin: 0 10px 0 0;

            &:last-child{

                margin: 0;

            }

        }

        >.bt{

            cursor: pointer;

            transition: all 0.2s;

            height: 100%;

            display: flex;

            flex-direction: row;

            align-items: center;

            justify-content: center;

            &:hover{

                color: #5240ff;

            }

        }

    }

    >.right{

        width: 40px;

        height: 100%;

        border-left: 1px solid var(--border-color);

        box-sizing: border-box;

        display: flex;

        flex-direction: row;

        justify-content: center;

        align-items: center;

        position: relative;

        box-shadow: inset 0 1px 4px #00000010;

        >.bt{

            width: 100%;

            height: 100%;

            display: flex;

            flex-direction: row;

            justify-content: center;

            align-items: center;

            cursor: pointer;

            transition: all 0.2s;

            &:hover{

                color: #5240ff;

            }

        }

        >.bt-list-container{

            width: max-content;

            min-width: 150px;

            position: absolute;

            z-index: 9;

            top: calc(100% + 0px);

            right: 5px;

            background-color: rgb(255, 255, 255);

            box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.5);

            padding: 10px 0;

            box-sizing: border-box;

            border-radius: 2px;

            overflow: hidden;

            transition: opacity 0.2s;

            font-size: 15px;

            >.item{

                cursor: pointer;

                width: auto;

                min-width: max-content;

                transition: all 0.2s;

                padding: 13px 15px;

                box-sizing: border-box;

                display: block;

                color: #6b7386;

                text-align: left;

                display: flex;

                flex-direction: row;

                align-items: center;

                justify-content: flex-start;

                >*{

                    margin-right: 10px;

                }

                &:hover{

                    box-shadow: inset 0 1px 4px #0000001f;

                    background-color: #fef0f0;

                    color: #f56c6c;

                }

                &.re-bt{

                    background-color: rgba(194, 224, 255, 0.5);

                    color: #0072E5;

                    &:hover{

                        background-color: rgba(194, 224, 255, 0.5);

                        color: #0072E5;

                    }

                }

            }

        }

    }

    >.bt-list-container{

        width: max-content;

        min-width: 150px;

        position: absolute;

        z-index: 9;

        top: var(--location-y);

        left: var(--location-x);

        background-color: rgb(255, 255, 255);

        box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.5);

        padding: 10px 0;

        box-sizing: border-box;

        border-radius: 2px;

        overflow: hidden;

        opacity: 1;

        transition: opacity 0.2s;

        font-size: 15px;

        >.item{

            cursor: pointer;

            width: auto;

            min-width: max-content;

            transition: all 0.2s;

            padding: 13px 15px;

            box-sizing: border-box;

            display: block;

            color: #6b7386;

            text-align: left;

            display: flex;

            flex-direction: row;

            align-items: center;

            justify-content: flex-start;

            >*{

                margin-right: 10px;

            }

            &:hover{

                box-shadow: inset 0 1px 4px #0000001f;

                background-color: #fef0f0;

                color: #f56c6c;

            }

            &.re-bt{

                background-color: rgba(194, 224, 255, 0.5);

                color: #0072E5;

                &:hover{

                    background-color: rgba(194, 224, 255, 0.5);

                    color: #0072E5;

                }

            }

        }

    }

}

</style>

```

这里我们使用了el-scrollbar组件来管理滚动容器,SvgIcon来管理icon的展示,vuedraggable来管理拖拽排序。

该组件接受的数据源为 tagList,activeSign。

tagList:标签的数组。

activeSign:当前活动的标签的sign字符串,每个标签是一个对象,对象有sign唯一标识属性。

组件核心思想:该组件使用外部数据源保证组件灵活性,自身集合多种操作但不处理,抛出给外部处理。只做数据的展示。

[源码地址](https://github.com/wurencaideli/dumogu-admin/blob/master/web/src/layout/main/components/TagList.vue)

[DEMO](https://admin.dumogu.top/main/index)

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容