手搓VUE-Tabs标签页(Vue/VueRouter/ElementPlus)

系列文章目录

这是个人学习Vue过程中的经验与问题小结
手搓VUE-动态菜单(Vue/VueRouter/ElementPlus)



前言

闲着没事就想着用Vue搭一个后台管理系统练练手,项目结构就不说了,那是后端的事情,这里只讲前端一些功能模块的实现


一、功能需求

根据用户点击导航菜单进入对应页面,在顶部标签栏生成一个标签,通过点击标签可以实现在不同页面之间切换,并可以关闭页面

二、步骤

1.必要依赖

# Vue3官方路由库
npm add vue-router
# 小菠萝状态管理(缓存)
npm add pinia pinia-plugin-persistedstate
# Element组件库
npm add element-plus

2.实现思路

  1. 路由跳转时通过vue-router导航守卫将跳转页面的路由数据存入缓存
  2. 标签模块根据缓存中的路由数据渲染组件
  3. 标签绑定点击事件跳转到对应路由

3.动起手来

  1. 定义一个缓存空间,增加响应的action方法
import { defineStore } from 'pinia'
import { RouteLocationNormalized } from 'vue-router'
import { nextTick } from 'vue'
import { isEmpty } from 'lodash'
import { TabPaneName } from 'element-plus'
import { router } from '@/router'

const name = 'tabs'

export const useTabs = defineStore(name, {
    state: () => ({
        tabViews: [] as RouteLocationNormalized[],
        refreshFlag: true,
    }),
    actions: {
        // 增加标签
        addTab(view: RouteLocationNormalized) {
            // 判断是否需要添加标签
            if (!view.meta.addTab) return
            // 判断标签是否已添加
            const index = this.tabViews.findIndex((e) => e.path == view.path)
            if (index == -1) {
                // 标签不存在,添加
                this.tabViews.push(view)
            }
        },
        refreshView() {
            this.refreshFlag = false
            nextTick(() => {
                this.refreshFlag = true
            })
        },
        // 移除标签
        removeTab(tabName: TabPaneName) {
            this.tabViews = this.tabViews.filter((e) => e.path != tabName)
            this.setActiveTab()
        },
        // 移除多个标签
        removeTabs(retainViews: RouteLocationNormalized[]) {
            this.tabViews = retainViews
            this.setActiveTab()
        },
        // 设置选中标签
        setActiveTab() {
            // 在标签列表中找当前地址
            const filter = this.tabViews.filter(
                (e) => e.path == router.currentRoute.value.path
            )
            if (isEmpty(filter)) {
                // 当前地址不在标签列表中,跳转到最后一个标签
                router.replace(this.tabViews[this.tabViews.length - 1])
            }
        },
    },
    persist: {
        key: name,
    },
})

  1. 使用router.beforeEach注册一个全局前置守卫,激活addTab方法
// 路由守卫
router.beforeEach((to) => {
    const tabs = useTabs()
    // 添加新标签
    tabs.addTab(JSON.parse(JSON.stringify(to)))
})
  1. 使用el-tabs封装一个组件,添加标签点击跳转功能及关闭功能
<script lang="ts" setup>
import type { TabPaneName, TabsPaneContext } from 'element-plus'
import { router } from '@/router'
import { useTabs } from '@/stores/tabs'

const tabs = useTabs()

// 标签点击事件
const handleTabClick = (pane: TabsPaneContext) => {
    router.replace(pane.paneName as string)
}

// 标签关闭事件
const handleTabRemove = (name: TabPaneName) => {
    tabs.removeTab(name)
}

// 重新加载事件
const handleRefresh = () => {
    tabs.refreshView()
}

// 关闭其他事件
const handleCloseOther = (index: number) => {
    tabs.removeTabs(tabs.tabViews.splice(index, index + 1))
}

// 关闭左侧标签事件
const handleCloseLeft = (index: number) => {
    tabs.removeTabs(tabs.tabViews.splice(index, tabs.tabViews.length))
}

// 关闭右侧标签事件
const handleCloseRight = (index: number) => {
    tabs.removeTabs(tabs.tabViews.splice(0, index + 1))
}

// 标签全部关闭事件
const handleCloseAll = () => {
    tabs.removeTabs([])
}
</script>

<template>
    <el-tabs
        v-model="$route.path"
        type="card"
        closable
        @tab-click="handleTabClick"
        @tab-remove="handleTabRemove"
    >
        <el-tab-pane
            v-for="(route, index) in tabs.tabViews"
            :key="index"
            :name="route.path"
        >
            <template #label>
                <el-dropdown trigger="contextmenu">
                    <span>
                        {{ route.meta.title || '无标题' }}
                    </span>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item
                                :disabled="route.path != $route.path"
                                @click="handleRefresh()"
                            >
                                重新加载
                            </el-dropdown-item>
                            <el-dropdown-item
                                :disabled="tabs.tabViews.length == 1"
                                @click="handleCloseOther(index)"
                            >
                                关闭其他
                            </el-dropdown-item>
                            <el-dropdown-item
                                :disabled="index == 0"
                                @click="handleCloseLeft(index)"
                            >
                                关闭左侧
                            </el-dropdown-item>
                            <el-dropdown-item
                                :disabled="index == tabs.tabViews.length - 1"
                                @click="handleCloseRight(index)"
                            >
                                关闭右侧
                            </el-dropdown-item>
                            <el-dropdown-item divided @click="handleCloseAll">
                                全部关闭
                            </el-dropdown-item>
                        </el-dropdown-menu>
                    </template>
                </el-dropdown>
            </template>
        </el-tab-pane>
    </el-tabs>
</template>
  1. 实现刷新功能之前在tabs中定义了一个refreshFlag参数,用于控制组件刷新
<script setup lang="ts">
import { computed } from 'vue'
import { useTabs } from '@/stores/tabs'

const tabs = useTabs()

const includeList = computed(() =>
    tabs.tabViews.map((e) => e.meta.componentName as string)
)
</script>

<template>
    <router-view v-slot="{ Component, route }">
        <keep-alive :include="includeList">
            <component
                :is="Component"
                v-if="tabs.refreshFlag"
                :key="route.path"
            />
        </keep-alive>
    </router-view>
</template>

三、遇到的坑

由于导航标签这个模块在空间维度上比较复杂,所以开发的过程中遇到了很多意料之外情理之中的问题,下面我把主要需要注意的几点整理了一下:

  1. keep-alive组件缓存需要注意的include是存入的组件名,Vue3的setup语法糖中,组件名需要额外写一个script
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
    name: 'Page',
})
</script>
  1. tabs标签可以锚定很多类型,这边为了方便,默认吧path作为唯一值
  2. 页面间切换时,可以灵活使用onActivated来进行更多操作
  3. 当前标签关闭后的跳转方式有很多种,可以通过router.back()来返回上一个页面或者通过标签index获取临近标签跳转,这边为了方便省脑,直接跳转到最后一个标签,如果使用router.back()方法需要注意返回的页面是否在tabViews中,需要额外在router.beforeEach()中增加一个拦截并再次router.back(),经过测试发现当页面数量较多时,会多次router.back()程序开销较大,不推荐使用
  4. 这里把页面跳转方法router.push()换成了router.replace()是为了防止使用浏览器返回功能自动打开不存在于tabViews中的页面,从而产生意料之外的BUG
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值