文章目录

  • 前言
  • 一、项目地址
  • 二、项目思路描述
  • 三、操作步骤
    • 3.1拦截路由
      • 静态路由
      • 后端动态路由
    • 3.2登录表单验证
    • 3.3permission.js文件
    • 3.4取后台路由并处理
    • 3.5 layout布局
      • 整体布局
      • 菜单栏
      • 头部实现
    • 3.6其他功能
  • 四、效果图

前言

在开发后台管理类项目中发现,前端菜单栏路由是由后端返回的动态路由,为了更好的了解实现的过程,所以研究了一下,写一下开发的思路。项目中主要使用的技术栈是vue3 、element-plus、vite、vuex


一、项目地址

项目源码请访问:github项目地址

二、项目思路描述

1.后台返回一个json格式的路由表,我这里直接写死了数据,使用Promise返回,大家可参考,也可以自己造;
2.因为后端传回来的都是字符串格式的,但是前端这里需要的是一个组件对象,所以要写个方法遍历一下,将字符串转换为组件对象;
3.利用vue-router的beforeEach、addRoutes、cookie、localStorage来配合上边两步实现效果;
4.左侧菜单栏根据拿到转换好的路由列表进行展示;

三、操作步骤

3.1拦截路由

在拦截路由之前,我们还得先定义好静态路由,然后从后端取到动态路由之后,进行合并。

静态路由

静态路由主要是登录页面和重定向页面等。代码如下

import {createRouter,createWebHistory } from 'vue-router';
import Layout from '@/layout'//静态路由
export const constantRoutes = [{name:"/",path: '/',//根目录路由为/component: Layout,//指定使用Layout组件布局redirect: '/',//重定向至/home页面hidden:true,children: [{//子菜单信息path: '/',//路径name: 'home',component: () => import('@/views/home'),//指定组件meta: { title: '首页', access: 0, affix: true }}]},{path: '/login',name: 'login',hidden: true,component: () => import('@/views/login')}
];const router =createRouter({history:createWebHistory(),routes:constantRoutes,//使用浏览器的回退或者前进时,重新返回时保留页面滚动位置,跳转页面的话,不触发。scrollBehavior(to,from,savePosition){if(savePosition){return savePosition;}else{return {top:0};}}
});export default router;

后端动态路由

这个路由在实际项目中是通过后端接口返回,在日常的开发中可以根据后端格式写手动写死,前后端联调的时候换成接口就行了。


//获取后台路由(动态路由)
export function getRouters(){return new Promise((resolve,reject)=>{const menuList = [{"name": "Tool","path": "/tool",// "redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统工具","icon": "tool","noCache": false,"link": null},"children": [{"name": "Build","path": "build","component": "tool/build/index","meta": {"title": "表单构建","icon": "build","noCache": false,"link": 'build'}}, {"name": "Gen","path": "gen","component": "tool/gen/index","meta": {"title": "代码生成","icon": "code","noCache": false,"link": null}}, {"name": "Swagger","path": "swagger","component": "tool/swagger/index","meta": {"title": "系统接口","icon": "swagger","noCache": false,"link": null}}]},{"name":"","path":"","hidden":false,"redirect":"/organization","component":"Layout",// "alwaysShow":"true","meta":{"title":"组织管理","icon":"tool","nocache":false,"link":null},"children":[{"name":"Organization","path":"organization","hidden":false,"component":"organization/index","meta":{"title":"组织管理","icon":"tool","nocache":false,"link":"organization"}}]},{"name":"","path":"","hidden":false,"redirect":"noRedirect","component":"Layout",// "alwaysShow":"true","meta":{"title":"部门管理","icon":"tool","nocache":false,"link":''},"children":[{"name":"Department","path":"department","hidden":false,"component":"department/index","meta":{"title":"部门管理","icon":"tool","nocache":false,"link":"department"}}]},{"name":"","path":"","hidden":false,"redirect":"noRedirect","component":"Layout",// "alwaysShow":"true","meta":{"title":"岗位管理","icon":"tool","nocache":false,"link":''},"children":[{"name":"Station","path":"station","hidden":false,"component":"station/index","meta":{"title":"岗位管理","icon":"tool","nocache":false,"link":"station"}}]},{"name":"","path":"","hidden":false,"redirect":"noRedirect","component":"Layout",// "alwaysShow":"true","meta":{"title":"应用管理","icon":"tool","nocache":false,"link":''},"children":[{"name":"Application","path":"application","hidden":false,"component":"application/index","meta":{"title":"应用管理","icon":"tool","nocache":false,"link":"application"}}]},{"name":"","path":"","hidden":false,"redirect":"noRedirect","component":"Layout",// "alwaysShow":"true","meta":{"title":"菜单管理","icon":"tool","nocache":false,"link":''},"children":[{"name":"Menulist","path":"menulist","hidden":false,"component":"menuList/index","meta":{"title":"菜单管理","icon":"tool","nocache":false,"link":"menulist"}}]},{"name":"DayRecord","path":"/dayRecord","hidden":false,"redirect":"noRedirect","component":"Layout","alwaysShow":"true","meta":{"title":"日志管理","icon":"tool","nocache":false,"link":'dayRecord'},"children":[{"name":"LoginRecord","path":"loginRecord","hidden":false,"component":"dayRecord/loginRecord/index","meta":{"title":"登录日志","icon":"tool","nocache":false,"link":"loginRecord"}},{"name":"HandleRecord","path":"handleRecord","hidden":false,"component":"dayRecord/handleRecord/index","meta":{"title":"操作日志","icon":"tool","nocache":false,"link":"handleRecord"}},]},{"name":"","path":"","hidden":false,"redirect":"noRedirect","component":"Layout",// "alwaysShow":"true","meta":{"title":"应用组管理","icon":"tool","nocache":false,"link":''},"children":[{"name":"AppGroup","path":"appGroup","hidden":false,"component":"appGroup/index","meta":{"title":"应用组管理","icon":"tool","nocache":false,"link":"appGroup"}},{"name":"AssignUser","path":"assignUser/:id","hidden":true,"component":"appGroup/assignUser/index","meta":{title:"分配用户","link":"assignUser/:id"}}]},{"name":"","path":"","hidden":false,"redirect":"noRedirect","component":"Layout",// "alwaysShow":"true","meta":{"title":"用户管理","icon":"tool","nocache":false,"link":''},"children":[{"name":"UserList","path":"userList","hidden":false,"component":"user/index","meta":{"title":"用户管理","icon":"tool","nocache":false,"link":"userList"}}]},]resolve(menuList);})
}

3.2登录表单验证

登录页面主要涉及密码的处理,使用的加密方法是crypto-js然后登录之后设置对应的token到浏览器作为判断是否登录的条件,同时前端可以加一个token过期时间。

<template><div class="app-container"><div class="app-content"><div class="app-image">产业大脑项目</div><el-form ref="ruleFormRef" :model="ruleForm" status-icon :rules="rules" label-width="120px" class="demo-ruleForm"><el-form-item label="用户账号" prop="username"><el-input v-model="ruleForm.username" type="username" autocomplete="off" /></el-form-item><el-form-item label="用户密码" prop="password"><el-input v-model="ruleForm.password" type="password" autocomplete="off"/></el-form-item><el-form-item label="验证码" prop="code"><el-input v-model="ruleForm.code" /></el-form-item><el-form-item prop="remember"><el-checkbox v-model="ruleForm.remember">记住密码</el-checkbox></el-form-item><el-form-item><el-button type="primary" @click="submitForm()">登录</el-button><el-button @click="resetForm()">没有注册?,去注册</el-button></el-form-item></el-form></div></div>
</template>
<script setup>
import {toRefs,getCurrentInstance,reactive} from 'vue';
import {useStore} from "vuex";
import {useRouter} from "vue-router";
import Cookies from 'js-cookie';
import {encrypt} from "../../utils/jsencrypt.js";const store = useStore();
const router = useRouter();
const {proxy} = getCurrentInstance();
const data = reactive({ruleForm:{username:'',password:"",code:'',uuid:1,remember:false},rules:{username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],}
});function submitForm(){proxy.$refs['ruleFormRef'].validate(valid=>{if(valid){if(ruleForm.value.remember){// Cookies.set('username',ruleForm.value.username,{exprise:30})// Cookies.set('password',encrypt(ruleForm.value.password),{exprise:30});// Cookies.set('remember',ruleForm.value.remember,{exprise:30});}else{//移除Cookies.remove('username');Cookies.remove('password');Cookies.remove('remember');}ruleForm.value.password = encrypt(ruleForm.value.password);//调用 store里面actions登录方法store.dispatch('Login',ruleForm.value).then(res=>{console.log(res);if(res.code === 200){router.push('/');}else{router.push('/login');// proxy.resetForm('ruleFormRef');不起作用,不知道为啥。}});// store.dispatch('GenerateRoutes').then(accessRoutes=>{})}})
}
function resetForm(){}
const {rules,ruleForm} = toRefs(data);</script><style lang="scss" scoped>.app-container{width: 100%;height: 100%;.app-content{position: relative;width: 30%;height: 200px;margin: 0 auto;padding-top:100px;.app-image{position: absolute;top: 42px;left: 230px;font-weight: 700;color: blue;}}}</style>

3.3permission.js文件

该文件要引入到main.js文件中使用,可以看到在permission中也调用了store中的处理后端路由的方法,这个跟login中的不同的点就是,登录是处理路由和用户信息来实现登录,而permission.js中则是用来使用路由拦截的方式,如果没有登录而是在地址栏直接输入对应的地址的话,可以进行拦截判断是否合法登录,获取当前的用户信息和路由权限,如果没有权限获取没有用户信息和错误就会退出登录。重新登录

import router from './router'
import store from './store'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'NProgress.configure({ showSpinner: false });//白名单,不进行拦截处理
const whiteList = ['/login', '/auth-redirect', '/bind', '/register'];//路由拦截
router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) {to.meta.title && store.dispatch('setTitle', to.meta.title)/* has token*/if (to.path === '/login') {console.log(111)next({ path: '' })NProgress.done()} else {if (store.getters.roles.length === 0) {// isRelogin.show = true// 判断当前用户是否已拉取完user_info信息store.dispatch('GetInfo').then(() => {// isRelogin.show = falsestore.dispatch('GenerateRoutes').then(accessRoutes => {// 根据roles权限生成可访问的路由表accessRoutes.forEach(route => {if (!isHttp(route.path)) {router.addRoute(route) // 动态添加可访问路由表}})next({ ...to, replace: true }) // hack方法 确保addRoutes已完成})}).catch(err => {//捕捉错误,退出登录store.dispatch('LogOut').then(() => {ElMessage.error(err)next({ path: '/' })})})} else {next()}}} else {// 没有tokenif (whiteList.indexOf(to.path) !== -1) {// 在免登录白名单,直接进入next()} else {next(`/login`) // 否则全部重定向到登录页NProgress.done()}}
})router.afterEach(() => {NProgress.done()
})

3.4取后台路由并处理

处理后端路由,通过调用vuex里的GenerateRoutes方法,处理对应的路由信息,因为方法稍微负责,在这里就不细说了。

import { constantRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout'
// 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue')const permission = {state: {routes: [],addRoutes: [],defaultRoutes: [],topbarRouters: [],sidebarRouters: []},mutations: {SET_ROUTES: (state, routes) => {state.addRoutes = routesstate.routes = constantRoutes.concat(routes)},SET_DEFAULT_ROUTES: (state, routes) => {state.defaultRoutes = constantRoutes.concat(routes)},SET_TOPBAR_ROUTES: (state, routes) => {state.topbarRouters = routes},SET_SIDEBAR_ROUTERS: (state, routes) => {state.sidebarRouters = routes;},},actions: {// 生成路由GenerateRoutes({ commit }) {return new Promise(resolve => {getRouters().then((res) => {console.log(res);const sdata = JSON.parse(JSON.stringify(res))const rdata = JSON.parse(JSON.stringify(res))const defaultData = JSON.parse(JSON.stringify(res))const sidebarRoutes = filterAsyncRouter(sdata)const rewriteRoutes = filterAsyncRouter(rdata, false, true)const defaultRoutes = filterAsyncRouter(defaultData)commit('SET_ROUTES', rewriteRoutes)commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))commit('SET_DEFAULT_ROUTES', sidebarRoutes)commit('SET_TOPBAR_ROUTES', defaultRoutes)resolve(rewriteRoutes);})})}}
}// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {return asyncRouterMap.filter(route => {if (type && route.children) {route.children = filterChildren(route.children)}if (route.component) {// Layout ParentView 组件特殊处理if (route.component === 'Layout') {route.component = Layout} else if (route.component === 'ParentView') {// route.component = ParentViewconsole.log('ParentView')} else if (route.component === 'InnerLink') {// route.component = InnerLinkconsole.log('InnerLink')} else {route.component = loadView(route.component)}}if (route.children != null && route.children && route.children.length) {route.children = filterAsyncRouter(route.children, route, type)} else {delete route['children']delete route['redirect']}return true})
}function filterChildren(childrenMap, lastRouter = false) {var children = []childrenMap.forEach((el, index) => {if (el.children && el.children.length) {if (el.component === 'ParentView' && !lastRouter) {el.children.forEach(c => {c.path = el.path + '/' + c.pathif (c.children && c.children.length) {children = children.concat(filterChildren(c.children, c))return}children.push(c)})return}}if (lastRouter) {el.path = lastRouter.path + '/' + el.path}children = children.concat(el)})return children
}
//这一步是取出来view里面的文件找到对应文件的懒加载函数,并执行。
export const loadView = (view) => {let res;for (const path in modules) {const dir = path.split('views/')[1].split('.vue')[0];if (dir === view) {res = () => modules[path]();}}return res;
}export default permission

3.5 layout布局

layout布局是侧边栏菜单显示的重要的一步。

整体布局

//路径:src/layout/index.vue
<template><el-container class="app-wrapper" ><el-aside :width="asideWidth" class="sidebar-container"><div style="text-align:center;margin-top: 15px;color: #FFFFFF;width: 100%;height: 80px"><el-icon><Avatar /></el-icon><div v-if="$store.getters.sidebarType" style="margin-left: 10px">后台管理系统</div></div><Menu /></el-aside><el-container class="container" :class="{hidderContainer:!$store.getters.sidebarType}"><el-header class="header-container"><Header/></el-header><el-main style="background: #fff;margin: 0 15px"><router-view></router-view></el-main></el-container></el-container>
</template><script setup>
import Menu from './Menu'
import {computed, ref} from 'vue'
import Header from './header/index'
import {useStore} from "vuex";
const store = useStore();const asideWidth = computed(()=>{return store.getters.sidebarType === true? '180px' : '67px'
})
</script><style lang="scss" scoped>
.app-wrapper{width: 100vw;height:100vh;background: #f5f5f5;margin: unset;
}
.app-container {position: relative;width: 100%;height: 100%;
}
.container {width: calc(100% - $sideBarWidth);height: 100%;position: fixed;top: 0;right: 0;z-index: 9;transition: all 0.28s;.header-container{height: 50px;line-height: 50px;background: #fff;margin-bottom: 15px;padding: 0 10px;::v-deep .el-breadcrumb{line-height: unset;}}&.hidderContainer {width: calc(100% - $hideSideBarWidth);}
}
::v-deep .el-header {padding: 0;
}
::v-deep .el-sub-menu .el-menu-item{min-width: unset;
}.el-aside {height: 100vh;overflow-y: auto;-ms-overflow-style: none; /* Edge */scrollbar-width: none; /* Firefox */&::-webkit-scrollbar {display: none; /* WebKit */}
}
</style>

菜单栏

//路径:src/layout/menu.vue
<template><el-menuactive-text-color="#ffd04b"background-color="#545c64"class="el-menu-vertical-demo":default-active="defaultRouter"text-color="#fff"routerunique-opened:collapse="!$store.getters.sidebarType"><el-sub-menu :index="(index+1).toString()" v-for="(item,index) in menusList" :key="index"><template #title><el-icon><component :is="iconList[index]"></component></el-icon><span>{{ item.meta.title }}</span></template><el-menu-item:index= "item.path + '/' + it.path"v-for="(it,index) in item.children":key="index"@click="savePath(item.path,it.path)"><template #title><el-icon><component :is="icon[index]"></component></el-icon><span>{{ it.meta.title }}</span></template></el-menu-item></el-sub-menu></el-menu>
</template><script setup>
import { getRouters } from '@/api/menu'
import { ref } from 'vue'
const iconList = ref(['user','setting','shop','tickets','pie-chart','Bell','checked','chicken','coin']);
const icon = ref(['menu','Edit','Files','folder','fold']);
const defaultRouter = ref(sessionStorage.getItem('path')|| '/tool/build');
const menusList = ref([]);
const initMenusList = async () => {menusList.value = await getRouters()
}function savePath(x,y){console.log(x,y);sessionStorage.setItem('path',`${x}/${y}`);
}
initMenusList()
</script><style lang="scss" scoped></style>

头部实现

//路径:src/layout/header/index
<template><div class="nav"><hamburger/><bread-crumb/><div class="nav-right"><avatar/></div></div>
</template><script setup>
import Hamburger from './components/hamburger'
import BreadCrumb from "./components/breadCrumb";
import avatar from './components/avatar'
// 自定义图标</script><style lang="scss" scoped>
.nav{height: 50px;line-height: 50px;display: flex;align-items: center;justify-content: center;.nav-right{flex: 1;display: flex;justify-content: flex-end;align-items: center;}
}</style>

3.6其他功能

其他功能可以从github项目中下载,查看具体的代码。

四、效果图

vue3后台管理项目,后端返回动态路由相关推荐

  1. Vue2+elementUi后台管理项目总结

    前言 该项目是一款对公司员工及商品管理的后台系统,主要实现功能:公司角色的增删改查,和商品的增删改查,项目的主要模块有,登录,主页,员工管理,权限管理,商品管理,该项目的亮点是权限管理,不同角色登录进 ...

  2. SSM 电影后台管理项目

    SSM 电影后台管理项目 概述 通过对数据库中一张表的CRUD,将相应的操作结果渲染到页面上. 笔者通过这篇博客还原了项目(当然有一些隐藏的坑),然后将该项目上传到了Github.Gitee,在末尾会 ...

  3. 电商项目总结java_Vue 电商后台管理项目阶段性总结(推荐)

    一.阶段总结 该项目偏向前端更多一点,后端 API 服务是已经搭建好了的,我们只需要用既可以,(但是作为一个 全栈开发人员,它的数据库的表设计,Restful API 的设计是我们要着重学习的!!!) ...

  4. 全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis)

    全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis) 众所周知,作为一个后端新手学习者,通过项目来学习,增长项目经验,是一 ...

  5. 尚硅谷尚品汇_后台管理项目

    vueProject_尚品汇后台管理 项目源码 文章目录 vueProject_尚品汇后台管理 login/out模块 product模块 login/out模块 .env.development . ...

  6. 从0到1完成一个Vue后台管理项目(九、引入Breadcrumb面包屑,更改bug)

    往期 从0到1完成一个Vue后台管理项目(一.创建项目) 从0到1完成一个Vue后台管理项目(二.使用element-ui) 从0到1完成一个Vue后台管理项目(三.使用SCSS/LESS,安装图标库 ...

  7. 为element ui+Vue搭建的后台管理项目添加图标

    问题:使用element UI 及Vue 2.0搭建一个后台管理项目,想要在页面中为其添加对勾及叉的图标. 解决方案:问题涉及到为页面添加图标.有两种解决方案. (1)Element官网提供了Icon ...

  8. vue考试系统后台管理项目-登录、记住密码功能

    考试系统后台管理项目介绍: 技术选型:Vue2.0+Elemenu-ui 项目功能介绍: 账户信息模块:菜单权限.角色权限设置.角色权限分配.账号设置.公司分组 考试管理模块:新增/编辑/删除考试试题 ...

  9. 一个基于 Go+Vue 实现的 openLDAP 后台管理项目

    [公众号回复 "1024",免费领取程序员赚钱实操经验] 大家好,我是章鱼猫. 今天给大家推荐的这个开源你项目来自于读者的投稿.还挺不错的,分享给大家. 这个开源项目是基 于Go+ ...

最新文章

  1. 2022-2028年小型风电产业投资及前景预测报告
  2. 人工智能70年商业变现艰难,新基建能否催生规模化落地?
  3. 如何成为java高手
  4. 新增数组_数组链表和List部分理解总结
  5. css图片居中_网页元素居中的n种方法
  6. Shiro配置cookie以及共享Session和Session失效问题
  7. WordPress后台定制-为WooCommerce产品增加自定义字段
  8. 【每日一具10】磁力资源搜索助手特别版
  9. IDEA 不检查语法错误问题
  10. 一个exe可执行程序的生与死
  11. 【jQuery进阶】子菜单插件Slight Submenu
  12. jquery bootstrap-select多选组件使用指南
  13. Excel中的Countif和Countifs
  14. 浏览器突然不能上网,DNS问题
  15. Zabbix内网监控外网阿里云主机
  16. poi实现word文档转pdf格式
  17. ImportError: No module named 'win32api'
  18. 记录自己三天速成django+html制作国内疫情可视化平台的过程(二)
  19. flexray unknown message
  20. window脚本介绍

热门文章

  1. Python 的nonlocal使用
  2. windows 7 下安装windows 8
  3. [附源码]SSM计算机毕业设计闲置物品交易管理系统JAVA
  4. java精确除法运算(BigDecimal)
  5. MySQL的三种注释方法
  6. 求出生年份 linux_从您出生之前我就一直从事Linux
  7. ElasticSearch查询大于10000条的数据
  8. [附源码]计算机毕业设计JAVA面向企业人力资源管理网上智能考勤系统
  9. 简单几步把LOGO变字体
  10. sqlserver sql语句查询数据库端口号