赞
踩
这里记录下自己在Vue3+vite的项目实现菜单栏功能和Tab页功能,不使用ts语法,方便以后直接使用。这里承接自己的博客Vue3+vite搭建基础架构(10)— 使用less和vite-plugin-vue-setup-extend这篇博客,在该博客项目的基础上增加菜单栏功能和Tab页功能实现。
删除掉src文件夹下的style.css和compoments文件夹下的HelloWorld.vue以及assets文件夹下的vue.svg图片,这三个都是项目创建完成后自带的,因为用不到所以删除掉。
删除views下面home文件夹下的index.vue代码,因为这个里面代码是以前用来测试依赖的代码,所以把代码清空,保留为一个空文件。
代码如下:
<!--home首页代码-->
<template>
<div>我是首页</div>
</template>
<script setup name="home">
</script>
<style lang="less" scoped>
</style>
在src下面新建styles文件夹用来存放全局样式。common.less用来存放html标签样式。element-plus.less用来存放ElementPlus组件里面的标签样式。然后在main.js里面引入2个样式文件,让它们全局生效。
common.less里面代码如下:
//body全局样式设计 body{ font-size: 14px;//字体大小 margin: 0px; padding: 0px; } //a标签全局样式 a { color: #1B68B6;//字体颜色 text-decoration: none;//去掉下划线 cursor: pointer;//鼠标放上去手型 //鼠标放上去颜色 /*&:hover { color: #1B68B6; } //鼠标点击时颜色 &:active{ color: #1B68B6; } //鼠标点击后获取焦点样式 &:focus { color: #1B68B6; }*/ }
element-plus.less目前代码为空。
在store文件夹下的modules文件夹下的userStore.js文件修改代码为如下:
//使用pinia来管理全局状态 import { defineStore } from "pinia" /*defineStore 是需要传参数的,其中第一个参数是id,就是一个唯一的值, 简单点说就可以理解成是一个命名空间. 第二个参数就是一个对象,里面有三个模块需要处理,第一个是 state, 第二个是 getters, 第三个是 actions。 */ //声明了一个useUserStore方法 const useUserStore = defineStore('user', { //准备state——用于存储数据 state: () => { return { //当前激活菜单的index activeMenu: '', //绑定值,选中选项卡的name editableTabsValue: '', //tab标签选项卡内容 editableTabs: [], //tab页路由地址及参数 tabRouterList: [] } }, //使用persist插件对state里面属性进行缓存 persist: { enabled: true,//开启缓存,默认缓存所有state里面的属性,默认key为defineStore里面的id值,这里id值为user,所以默认key为user //自定义持久化参数,指定以下state里面的属性进行缓存,未指定的不进行缓存 strategies: [ { // 自定义key key: 'activeMenu', // 自定义存储方式,默认sessionStorage storage: sessionStorage, // 指定要持久化的数据 paths: ['activeMenu'] }, { key: 'editableTabsValue', storage: sessionStorage, paths: ['editableTabsValue'] }, { key: 'editableTabs', storage: sessionStorage, paths: ['editableTabs'] }, { key: 'tabRouterList', storage: sessionStorage, paths: ['tabRouterList'] } ] }, getters: { }, //准备actions——用于响应组件中的动作和用于操作数据(state),pinia中只有state、getter、action,抛弃了Vuex中的Mutation actions: { /** * 修改state中数据的方法 * @param name 需要修改的属性名 * @param value 修改值 */ updateState([name, value]) { this[name] = value }, //动态添加tab标签,item为当前点击的菜单项 addTab(item) { const newTab = { title: item.meta.title, name: item.url, iconClass: item.meta.icon, } // 判断当前editableTabs中是否存在该tab标签 if (this.editableTabs.findIndex(item => item.title === newTab.title) === -1) { this.editableTabs.push(newTab); this.editableTabsValue = newTab.name; this.activeMenu = newTab.name; } }, //移除tab标签 removeTab(targetName) { let tabs = this.editableTabs let activeName = this.editableTabsValue if (activeName === targetName) { tabs.forEach((tab, index) => { if (tab.name === targetName) { let nextTab = tabs[index + 1] || tabs[index - 1] if (nextTab) { activeName = nextTab.name } } }) } this.activeMenu = activeName this.editableTabsValue = activeName this.editableTabs = tabs.filter(tab => tab.name !== targetName) this.tabRouterList = this.tabRouterList.filter(item => item.path !== targetName) } } }) export default useUserStore
views文件下layout文件夹下的layout.vue布局代码如下:
<template> <div> <el-container> <!--侧边栏,height: 100vh;设置高度为视口高度--> <el-aside style="width: 200px;height: 100vh;"> <SliderBar></SliderBar> </el-aside> <el-container> <!--头部--> <el-header> <Navbar></Navbar> </el-header> <!--主体内容--> <el-main> <!--主体内容--> <AppMain> </AppMain> </el-main> </el-container> </el-container> </div> </template> <script> import { Navbar, SliderBar, AppMain } from './components/index.js' export default { name: "layout", components: { Navbar, SliderBar, AppMain } } </script> <style scoped> </style>
views文件下layout文件夹下的components文件夹下sliderBar文件夹下的sliderBar.vue代码如下:
<!--通用布局侧边栏内容--> <template> <el-row> <el-col> <div class="header"> <!--系统logo,随便找一个图片示例用--> <SvgIcon iconClass="systemManagement" /> <span class="icon-text">后台管理系统</span> </div> <!--router表示为启动路由模式,路由模式下index为你的页面路由路径--> <!--通过设置default-active属性点击tab页时,自动选中左边菜单栏选项--> <div> <el-menu active-text-color="#1B68B6" background-color="#FFFFFF" :default-active="store.activeMenu" text-color="#333333" @select="handleSelect" :router="true" class="menu-items" > <!--引用菜单树组件将路由的菜单栏循环显示出来--> <MenuTree :menuList="menuTreeList"/> </el-menu> </div> </el-col> </el-row> </template> <script setup name="SliderBar"> //引入菜单列表组件 import MenuTree from "./menuTree.vue" //引入全局状态里面的关于菜单栏列表数据和相关方法 import useUserStore from "@/store/modules/userStore" //使用useUserStore里面的属性 const store = useUserStore() //菜单激活回调函数,当tab页已经打开的情况下,再次点击菜单项,对应的tab页也跟着切换 function handleSelect(key) { store.updateState(["editableTabsValue", key]) store.updateState(["activeMenu", key]) } //菜单树列表,这里模拟后端接口请求返回的数据,示例数据如下: const menuTreeList = [ { id: 1, url: "/test-management1",//该url要与路由文件里面的path值要一致 level: 1,//菜单等级 meta: { title: "测试管理1", icon: "systemManagement" }, children: [] //子菜单 }, { id: 2, url: "/system-management", level: 1, meta: { title: "系统管理", icon: "systemManagement" }, children: [ { id: 3, url: "/user-management", level: 2, meta: { title: "用户管理", icon: "userManagement" }, children: [] }, { id: 4, url: "/role-management", level: 2, meta: { title: "角色管理", icon: "roleManagement" }, children: [] }, { id: 5, url: "/permission-management", level: 2, meta: { title: "权限管理", icon: "permissionManagement" }, children: [] }, { id: 6, url: "/password-management", level: 2, meta: { title: "密码管理", icon: "systemManagement" }, children: [] }, ], }, { id: 7, url: "/test-management2", level: 1, meta: { title: "测试管理2", icon: "systemManagement" }, children: [] }, { id: 8, url: "/test-management3", level: 1, meta: { title: "测试管理3", icon: "systemManagement" }, children: [ { id: 9, url: "/test-management4", level: 2, meta: { title: "测试管理4", icon: "systemManagement" }, children: [ { id: 10, url: "/test-management5", level: 3, meta: { title: "测试管理5", icon: "systemManagement" }, children: [] } ] } ] } ] </script> <style lang="less" scoped> .header { height: 64px; display: flex; align-items: center; //垂直居中 justify-content: left; //水平居左 //logo样式 .svg-icon { width: 64px; height: 32px; } .icon-text { font-size: 16px; color: #1b68b6; margin-left: -5px; } } //普通菜单悬浮样式 :deep(.el-menu-item:hover) { background-color: #E8EFF7;//背景颜色 color: #1B68B6;//字体颜色 } //子菜单悬浮样式,子菜单的图标颜色需要修改svg图片里面的fill值,由fill="#333333"改为fill="currentColor"后,图标悬浮样式颜色才会一起变化 :deep(.el-sub-menu__title:hover) { background-color: #E8EFF7;//背景颜色 color: #1B68B6;//字体颜色 } //菜单被选中的样式 :deep(.el-menu .el-menu-item.is-active) { background-color: #E8EFF7; //背景颜色 color: #1B68B6; //字体颜色 border-right: 3px solid #1B68B6;//右边框颜色 } //子菜单被选中的样式 :deep(.el-sub-menu.is-active .el-sub-menu__title){ color: #1B68B6; //字体颜色 } //菜单栏样式 .menu-items { height: 100%; //设置高度为父容器高度 border-right: none;//去掉菜单栏右边框 } </style>
views文件下layout文件夹下的components文件夹下sliderBar文件夹下的menuTree.vue代码如下:
<!--菜单树列表--> <template> <!--将菜单列表循环出来--> <template v-for="item in menuList"> <!--判断菜单里面是否有子菜单--> <el-sub-menu :key="item.id" :index="item.url" v-if="item.children.length" > <template #title> <el-icon><SvgIcon :iconClass="item.meta.icon"></SvgIcon></el-icon> <span>{{ item.meta.title }}</span> </template> <!--调用自身循环显示子菜单--> <MenuTree :menuList="item.children" /> </el-sub-menu> <!--菜单节点--> <el-menu-item v-else :key="item.id" :index="item.url" @click="store.addTab(item)" > <el-icon><SvgIcon :iconClass="item.meta.icon"></SvgIcon></el-icon> <span>{{ item.meta.title }}</span> </el-menu-item> </template> </template> <script setup name="MenuTree"> //引入全局状态里面的关于菜单栏列表数据和相关方法 import useUserStore from "@/store/modules/userStore" const store = useUserStore() //定义属性给组件接收 const props = defineProps({ //菜单栏属性 menuList: { type: Array,//类型为数组 //默认值为空数组 default() { return [] } } }) </script> <style scoped> </style>
在views文件夹下新建菜单树列表里面对应的页面文件,每个页面文件加上如下一句代码,用来表示不同页面内容。
src文件下router文件夹下的index.js代码如下:
//引入router路由做页面请求 import { createRouter,createWebHashHistory } from 'vue-router' /* Layout通用组件 */ import Layout from '../views/layout/layout' const routes = [ {path: '/404', component: () => import('@/views/404')}, //必须要把组件放在Layout的children里面,才能在侧边栏的右侧显示页面内容,否则不加载通用架构直接在当前空白页面渲染内容,如:404页面 { path: '', component: Layout, redirect: '/home', children: [ { path: 'home', name: 'home', component: () => import('@/views/home/index'), meta: {title: '首页', icon: 'home'} }, { path: 'test-management1', name: 'test-management1', component: () => import('@/views/test-management1/index'), meta: {title: '测试管理1', icon: 'systemManagement'} }, { path: 'user-management', name: 'user-management', component: () => import('@/views/system-management/user-management'), meta: {title: '用户管理', icon: 'userManagement'} }, { path: 'role-management', name: 'role-management', component: () => import('@/views/system-management/role-management'), meta: {title: '角色管理', icon: 'roleManagement'} }, { path: 'permission-management', name: 'permission-management', component: () => import('@/views/system-management/permission-management'), meta: {title: '权限管理', icon: 'permissionManagement'} }, { path: 'password-management', name: 'password-management', component: () => import('@/views/system-management/password-management'), meta: {title: '密码管理', icon: 'systemManagement'} }, { path: 'test-management2', name: 'test-management2', component: () => import('@/views/test-management2/index'), meta: {title: '测试管理2', icon: 'systemManagement'} }, { path: 'test-management5', name: 'test-management5', component: () => import('@/views/test-management5/index'), meta: {title: '测试管理5', icon: 'systemManagement'} } ] } ] // 3. 创建路由实例并传递 `routes` 配置 const router = createRouter({ // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。 history: createWebHashHistory(), routes, // `routes: routes` 的缩写 }) //路由前置守卫 router.beforeEach((to, from, next) => { //路由发生变化修改页面title if (to.meta.title) { document.title = to.meta.title } next() }) //导出路由 export default router
启动项目后,浏览器结果如下:
点击不同的菜单栏选项,页面内容也会相应的变化,这种算是单页面,activeMenu也会相应的变化,之所以要把这个写到session storage里面,是为了防止页面刷新时,点击的高亮菜单选项消失问题。
views文件下layout文件夹下的components文件夹下的navbar.vue代码如下:
<!--通用布局头部内容--> <template> <el-row> <el-col :span="20"> <el-tabs v-model="store.editableTabsValue" type="border-card" closable @tab-remove="handleTabRemove" @tab-click="handleTabClick" v-if="store.editableTabs.length !== 0"> <el-tab-pane v-for="item in store.editableTabs" :key="item.name" :name="item.name" :label="item.title"> <!-- 右键菜单开始:自定义标签页显示名称,保证每个标签页都能实现右键菜单 --> <template #label> <el-dropdown trigger="contextmenu" :id="item.name" @visible-change="handleChange($event, item.name)" ref="dropdownRef"> <span style="font-size: 16px;color: #909399;" :class="store.editableTabsValue === item.name ? 'label' : ''"> <SvgIcon :iconClass="item.iconClass"></SvgIcon>{{ item.title }} </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item @click="closeCurrent(item.name)"> <el-icon> <Close /> </el-icon>关闭当前标签页 </el-dropdown-item> <el-dropdown-item @click="closeLeft(item.name)" v-if="show(item.name, 'left')"> <el-icon> <DArrowLeft /> </el-icon>关闭左侧标签页 </el-dropdown-item> <el-dropdown-item @click="closeRight(item.name)" v-if="show(item.name, 'right')"> <el-icon> <DArrowRight /> </el-icon>关闭右侧标签页 </el-dropdown-item> <el-dropdown-item @click="closeOther(item.name)" v-if="store.editableTabs.length > 1"> <el-icon> <Operation /> </el-icon>关闭其他标签页 </el-dropdown-item> <el-dropdown-item @click="closeAll()"> <el-icon> <Minus /> </el-icon>关闭全部标签页 </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </template> <!-- 右键菜单结束 --> </el-tab-pane> </el-tabs> </el-col> <el-col :span="4"> <div class="header"> <!-- 用户信息 --> <!--trigger="click"通过点击下标触发--> <div style="cursor: pointer;"> <el-dropdown trigger="click"> <span> {{ username }} <SvgIcon iconClass="arrowDown" /> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item @click="logout"> 退出登录 </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </div> </el-col> </el-row> </template> <script setup name="navbar"> //引入全局状态里面的关于菜单栏列表数据和相关方法 import useUserStore from '@/store/modules/userStore' import { useRouter, useRoute } from "vue-router" import { onMounted, ref, computed } from 'vue' import { Close, DArrowLeft, DArrowRight, Operation, Minus } from '@element-plus/icons-vue' //接手全局状态里面的属性和方法 const store = useUserStore(); //使用路由相当于$router,系统路由方法 const router = useRouter() //使用路由相当于$route,点击菜单栏时当前点击的路由页面里面的属性值 const route = useRoute() //用户名 const username = '超级管理员' //触发右键菜单标签页为第一个时,不展示【关闭左侧标签页】 //触发右键菜单标签页为最后一个时,不展示【关闭右侧标签页】 const show = (name, type) => { const index = store.editableTabs.findIndex((item) => name === item.name) return type === 'left' ? index !== 0 : index !== store.editableTabs.length - 1 } //右键菜单ref const dropdownRef = ref() //在触发右键菜单后,关闭其他tab页上的右键菜单 const handleChange = (visible, name) => { if (!visible) return dropdownRef.value.forEach((item) => { if (item.id === name) return item.handleClose() }) } //关闭当前tab页 const closeCurrent = (targetName) => { handleTabRemove(targetName) } //关闭左侧tab页 const closeLeft = (targetName) => { //查找当前点击的tab页所在位置 let currentIndex = store.editableTabs.findIndex( (item) => item.name === targetName ) //查找当前激活标签页index const activeIndex = store.editableTabs.findIndex((item) => item.name === store.editableTabsValue) //关闭左侧tab页 store.editableTabs.splice(0, currentIndex) //删除对应的左侧历史路由 store.tabRouterList.splice(0, currentIndex) //如果当前关闭点击的tab页包含激活的tab页,则将激活tab页重置为当前点击的tab if (activeIndex < currentIndex) { //将当前激活的tab页改为当前点击的 store.updateState(['editableTabsValue', targetName]) //将激活菜单改为当前点击的 store.updateState(['activeMenu', targetName]) //路由跳转到当前点击的tab页 //查询当前点击的tab页缓存路由参数 let result = store.tabRouterList.find(item => item.path === targetName); //路由跳转且带上对应tab页的参数 router.push({ path: targetName, query: result.query }) } } //关闭右侧tab页 const closeRight = (targetName) => { //查找当前点击的tab页所在位置 let currentIndex = store.editableTabs.findIndex( (item) => item.name === targetName ) //查找当前激活标签页index const activeIndex = store.editableTabs.findIndex((item) => item.name === store.editableTabsValue) //关闭右侧tab页 store.editableTabs.splice(currentIndex + 1) //删除对应的右侧历史路由 store.tabRouterList.splice(currentIndex + 1) //如果当前关闭点击的tab页包含激活的tab页,则将激活tab页重置为当前点击的tab if (activeIndex > currentIndex) { //将当前激活的tab页改为当前点击的 store.updateState(['editableTabsValue', targetName]) //将激活菜单改为当前点击的 store.updateState(['activeMenu', targetName]) //路由跳转到当前点击的tab页 //查询当前点击的tab页缓存路由参数 let result = store.tabRouterList.find(item => item.path === targetName); //路由跳转且带上对应tab页的参数 router.push({ path: targetName, query: result.query }) } } //关闭其他tab页 const closeOther = (targetName) => { //查找当前点击的tab页所在位置 let currentIndex = store.editableTabs.findIndex( (item) => item.name === targetName ) //关闭其他标签页 store.editableTabs = [store.editableTabs[currentIndex]] //删除除当前点击外的历史路由 store.tabRouterList = [store.tabRouterList[currentIndex]] //如果当前点击的不等于当前激活的 if (targetName !== store.editableTabsValue) { //将当前激活的tab页改为当前点击的 store.updateState(['editableTabsValue', targetName]) //将激活菜单改为当前点击的 store.updateState(['activeMenu', targetName]) //路由跳转到当前点击的tab页 //查询当前点击的tab页缓存路由参数 let result = store.tabRouterList.find(item => item.path === targetName); //路由跳转且带上对应tab页的参数 router.push({ path: targetName, query: result.query }) } } //关闭全部tab页 const closeAll = () => { //清空tabs数组 store.editableTabs.length = 0 //清空历史路由 store.tabRouterList.length = 0 //当前选中tab页设置为空 store.updateState(['editableTabsValue', '']) //当前激活菜单设置为空 store.updateState(['activeMenu', '']) //跳转到首页 router.push('home') } //处理tab标签x按钮的移除 function handleTabRemove(targetName) { //如果editableTabs列表不为空数组 if (store.editableTabs.length > 0) { //如果当前所在的tab页路由地址与移除的tab页名一样,则移到前面一个tab页且路由跳转 if (route.path === targetName) { let tabs = store.editableTabs tabs.forEach((tab, index) => { if (tab.name === targetName) { //获取当前tab的后一个或者前一个 let nextTab = tabs[index + 1] || tabs[index - 1] //如果有值就移到它上面,没有就是最后一个跳转到首页 if (nextTab) { //根据name属性进行查询当前tab页的缓存路由参数 let result = store.tabRouterList.find(item => item.path === nextTab.name); //路由跳转且带上对应tab页的参数 router.push({ path: nextTab.name, query: result.query }) } else { // 更改tab标签绑定值,选中选项卡的name store.updateState(['editableTabsValue', '']) // 更改当前激活的菜单 store.updateState(['activeMenu', '']) //当删除的是最后一个tab页的时候,跳转到首页 router.push('home') } } }) //从editableTabs中移除当前tab标签 store.removeTab(targetName) } else { //从editableTabs中移除当前tab标签 store.removeTab(targetName) } } } //tab标签被选中时触发的事件 function handleTabClick(tab) { store.updateState(['activeMenu', tab.props.name]) store.updateState(['editableTabsValue', tab.props.name]) // 判断当前url地址和即将跳转的是否一致,不一致进行跳转,防止跳转多次 if (tab.props.name !== route.path) { // 根据name属性进行查询 let result = store.tabRouterList.find(item => item.path === tab.props.name); //路由跳转且带上对应tab页的参数 router.push({ path: tab.props.name, query: result.query }) } } //退出登录方法 function logout() { } </script> <style lang="less" scoped> //设置高度 :deep(.el-tabs__nav-scroll) { height: 60px; } //去掉el-tabs的边框 :deep(.el-tabs) { border: none; } .header { height: 62px; position: absolute; right: 30px; top: 0px; z-index: 1; //不设置这个,el-down点击出不来,被tab标签页长度挡住了 display: flex; align-items: center; //垂直居中 } //tab标签页里面字体设置 .label { color: #1B68B6 !important; //激活标签页高亮 font-size: 16px; } :deep(.el-tabs__item) { &:hover { span { color: #1B68B6 !important; //鼠标移到标签页高亮 } } } </style>
views文件下layout文件夹下的components文件夹下的appMain.vue代码如下:
<!--通用布局页面主体内容--> <template> <!-- 路由视图对象 --> <router-view v-slot="{ Component }"> <!--include主要解决关闭tab页时,同时销毁该组件,防止再次重新打开时数据还在的情况。注意:组件name名必须和路由name名一致,否则会导致组件不缓存的情况。--> <keep-alive :include="tabsNames"> <component :is="Component"></component> </keep-alive> </router-view> </template> <script setup name="AppMain"> import useUserStore from "@/store/modules/userStore" import { computed } from "vue" const store = useUserStore() //将路由里面的name取出来作为一个数组 const tabsNames = computed(() => store.tabRouterList.map((item) => item.name)) </script> <style scoped> </style>
这里之所以不把appMain.vue的路由视图对象写到navbar.vue里面的el-tabs里面,是因为写到el-tabs里面后,当你打开多个tab页的时候,当你向后端发送请求时,打开了多少个tab页,就会重复发送多少个后端接口请求,所以这里将它拆开写,el-tabs里面内容实际是个空的。
通过element-plus里面的样式让它内边距为0,看着内容像是放在了el-tabs里面。如下:
//让el-tabs__content不显示内容
.el-tabs--border-card>.el-tabs__content{
padding: 0px;
}
styles文件夹下的element-plus.less样式代码如下:
//移除头部间距,让宽度100%占满 .el-header{ --el-header-padding:0px; min-width: 1000px; } //未打开tab页右边背景色 .el-tabs__nav-scroll{ background: #FFFFFF; } //设置内容背景颜色 .el-main{ background: #F2F6FB; min-width: 1000px; } //让el-tabs__content不显示内容 .el-tabs--border-card>.el-tabs__content{ padding: 0px; } //Tabs标签页全局样式 .el-tabs__item { height: 64px; font-size: 16px; background: #ffffff; //未选中Tabs页背景颜色 } //Tabs标签页选中样式 .el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active { color: #1b68b6; //选中Tabs标签页后字体颜色 background-color: #e3edf7; //选中Tabs标签页后背景颜色 }
浏览器结果如下,点击多个菜单栏打开多个tab页结果:
点击tab页的同时,菜单栏也来到对应的选项,如下:
鼠标放到tab页上面的字体上面,然后鼠标右键菜单,会显示对应的关闭菜单下拉选项,如下:
到这里tab页相关的代码就写完了,但是有几个问题需要注意,第一个问题就是你在浏览器上面直接输入路由地址,它不会打开或者跳转到对应tab页上面。第2个问题就是在实际开发中,如果页面跳转需要携带参数过去,当你切换点击tab页的时候,参数会丢失问题。对于参数会丢失问题,所以才在userStore.js里面写了一个tabRouterList属性来存储历史路由及参数。
在router文件夹下面的index.js文件,将代码修改为如下:
//引入router路由做页面请求 import { createRouter,createWebHashHistory } from 'vue-router' /* Layout通用组件 */ import Layout from '../views/layout/layout' //引入pinia里面的state属性和方法 import useUserStore from "@/store/modules/userStore" const routes = [ {path: '/404', component: () => import('@/views/404')}, //必须要把组件放在Layout的children里面,才能在侧边栏的右侧显示页面内容,否则不加载通用架构直接在当前空白页面渲染内容,如:404页面 { path: '', component: Layout, redirect: '/home', children: [ { path: 'home', name: 'home', component: () => import('@/views/home/index'), meta: {title: '首页', icon: 'home'} }, { path: 'test-management1', name: 'test-management1', component: () => import('@/views/test-management1/index'), meta: {title: '测试管理1', icon: 'systemManagement'} }, { path: 'user-management', name: 'user-management', component: () => import('@/views/system-management/user-management'), meta: {title: '用户管理', icon: 'userManagement'} }, { path: 'role-management', name: 'role-management', component: () => import('@/views/system-management/role-management'), meta: {title: '角色管理', icon: 'roleManagement'} }, { path: 'permission-management', name: 'permission-management', component: () => import('@/views/system-management/permission-management'), meta: {title: '权限管理', icon: 'permissionManagement'} }, { path: 'password-management', name: 'password-management', component: () => import('@/views/system-management/password-management'), meta: {title: '密码管理', icon: 'systemManagement'} }, { path: 'test-management2', name: 'test-management2', component: () => import('@/views/test-management2/index'), meta: {title: '测试管理2', icon: 'systemManagement'} }, { path: 'test-management5', name: 'test-management5', component: () => import('@/views/test-management5/index'), meta: {title: '测试管理5', icon: 'systemManagement'} } ] } ] // 3. 创建路由实例并传递 `routes` 配置 const router = createRouter({ // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。 history: createWebHashHistory(), routes, // `routes: routes` 的缩写 }) //黑名单,在该黑名单里面的路由将不会动态加载tab页 const blackList=['/404','/home'] const handleToParams = (to) => { const route = { fullPath: to.fullPath, meta: to.meta, name: to.name, params: to.params, path: to.path, query: to.query, } return route } function handleRouteInEditableTabs(to,store) { //判断当前路由的标题是否已经在editableTabs里,如果不在则动态添加tab页 const indexInEditableTabs = store.editableTabs.findIndex( (item) => item.title === to.meta.title ) //当前路由的标题已经在editableTabs里 if (indexInEditableTabs !== -1) { //判断tabRouterList是否已经存在相同的路由 const indexInTabRouterList = store.tabRouterList.findIndex( (item) => item.name === to.name ) //当前路由的name已经在tabRouterList里面 if (indexInTabRouterList !== -1) { //根据当前路由名称找到对应的历史路由 let result = store.tabRouterList.find(item => item.name === to.name) //在name相同但是路由的query参数不一样,则替换为这个最新的(将对象转为string字符串比较,即可判断2个对象属性与值是否完全一样) let queryMatched=JSON.stringify(result.query) === JSON.stringify(to.query) //如果为false,则替换当前路由参数 if (!queryMatched) { //若存在,则从原始数组中移除该对象 store.tabRouterList = store.tabRouterList.filter( (item) => item.name !== to.name ) //重新添加这个新路由 store.tabRouterList.push(handleToParams(to)) } } else { //点击菜单栏时,如果不在则添加该路由 store.tabRouterList.push(handleToParams(to)) } } else { //判断该路由是否在黑名单里面,不在则动态添加tab页 if (!blackList.includes(to.path)) { //如果不在editableTabs里面,那么就在editableTabs里面添加这个tab页 store.editableTabs.push({ title: to.meta.title, name: to.path, iconClass: to.meta.icon, }) //点击页面中的某个按钮进行页面跳转的时候,如果不在则添加该路由里面部分字段 store.tabRouterList.push(handleToParams(to)) } } } //路由前置守卫 router.beforeEach((to, from, next) => { //如果没有匹配到路由,则跳转到404页面 if (to.matched.length === 0) { next("/404") } else { //路由发生变化修改页面title document.title = to.meta.title //使用pinia里面的全局状态属性 const store = useUserStore() //更改tab标签绑定值,选中选项卡的name store.updateState(["editableTabsValue", to.path]) //更改当前激活的菜单 store.updateState(["activeMenu", to.path]) //动态添加tab页及tab页切换时参数也跟着切换 handleRouteInEditableTabs(to,store) next() } }) //导出路由 export default router
浏览器结果如下,在浏览器直接输入相应路由,会自动跳转到对应的tab,如下:
输入不存在的路由会直接跳转到404页面,如下:
从用户管理携带参数跳转到角色管理,测试如下:
views文件夹下面的system-management文件夹下的user-management.vue代码如下:
<template> <div> 用户管理页面 <el-button type="primary" @click="toRoleManagement(1)"> 跳转到角色管理 </el-button> </div> </template> <script setup name="user-management"> import { useRouter } from "vue-router" //使用router跳转路由 const router=useRouter() const toRoleManagement = (id) => { //跳转到邮单详情里面 router.push({ path: 'role-management', query: { id: id } }) } </script> <style scoped> </style>
views文件夹下面的system-management文件夹下的role-management.vue代码如下:
<template> <div>角色管理页面</div> </template> <script setup name="role-management"> import {onActivated} from 'vue' import { useRoute } from "vue-router" //使用route接受路由传过来的参数 const route=useRoute() //每次页面初始化时或者在邮件管理页面点击邮件详情时执行该方法 onActivated(()=>{ const id=route.query.id console.info("接受到的id=",id) }) </script> <style scoped> </style>
浏览器结果如下:
点击跳转到角色管理按钮结果如下:
然后再次点击用户管理tab页,如下:
再次点击角色管理tab页,发现参数依旧在没有消失。问题解决。
到这里Vue3+vite搭建基础架构的所有代码就结束了。只需要根据实际需求添加对应页面即可。这里附上该示例项目的所有代码地址,如果有需要,自行下载即可。
Vue3+vite搭建基础架构代码链接:https://download.csdn.net/download/weixin_48040732/88855369
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。