系列文章目录
这是个人学习Vue过程中的经验与问题小结
手搓VUE-动态菜单(Vue/VueRouter/ElementPlus)
前言
闲着没事就想着用Vue搭一个后台管理系统练练手,项目结构就不说了,那是后端的事情,这里只讲前端一些功能模块的实现
一、功能需求
根据用户点击导航菜单进入对应页面,在顶部标签栏生成一个标签,通过点击标签可以实现在不同页面之间切换,并可以关闭页面
二、步骤
1.必要依赖
# Vue3官方路由库
npm add vue-router
# 小菠萝状态管理(缓存)
npm add pinia pinia-plugin-persistedstate
# Element组件库
npm add element-plus
2.实现思路
- 路由跳转时通过
vue-router
导航守卫将跳转页面的路由数据存入缓存 - 标签模块根据缓存中的路由数据渲染组件
- 标签绑定点击事件跳转到对应路由
3.动起手来
- 定义一个缓存空间,增加响应的
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,
},
})
- 使用
router.beforeEach
注册一个全局前置守卫,激活addTab
方法
// 路由守卫
router.beforeEach((to) => {
const tabs = useTabs()
// 添加新标签
tabs.addTab(JSON.parse(JSON.stringify(to)))
})
- 使用
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>
- 实现刷新功能之前在
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>
三、遇到的坑
由于导航标签这个模块在空间维度上比较复杂,所以开发的过程中遇到了很多意料之外情理之中的问题,下面我把主要需要注意的几点整理了一下:
keep-alive
组件缓存需要注意的include
是存入的组件名,Vue3的setup
语法糖中,组件名需要额外写一个script
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Page',
})
</script>
tabs
标签可以锚定很多类型,这边为了方便,默认吧path
作为唯一值- 页面间切换时,可以灵活使用
onActivated
来进行更多操作 - 当前标签关闭后的跳转方式有很多种,可以通过
router.back()
来返回上一个页面或者通过标签index
获取临近标签跳转,这边为了方便省脑,直接跳转到最后一个标签,如果使用router.back()
方法需要注意返回的页面是否在tabViews
中,需要额外在router.beforeEach()
中增加一个拦截并再次router.back()
,经过测试发现当页面数量较多时,会多次router.back()
程序开销较大,不推荐使用 - 这里把页面跳转方法
router.push()
换成了router.replace()
是为了防止使用浏览器返回功能自动打开不存在于tabViews
中的页面,从而产生意料之外的BUG