一个人撸码!之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唯一标识属性。

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

源码地址

DEMO

相关推荐
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端
爱敲代码的小鱼12 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax