后面可参考下:vue系列(三)——手把手教你搭建一个vue3管理后台基础模板
以下代码项目gitee地址
可参考:vite官网 https://vitejs.cn/guide/#scaffolding-your-first-vite-project
npm init vite@latest mushan-vue3-adminnpm installnpm run dev
在index.html中的id为app中,写入
Vite + Vue
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],server: {hmr: true,port: 5174,},resolve: {alias: {'@':path.resolve(__dirname,'./src')}}
})
npm i vue-router@4 -S
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";// 路由信息
const routes = [{path: '/',name: 'home',component: ()=>import('@/views/index.vue')},{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),}
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()next()
})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
npm i path
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],resolve: {alias: {'@':path.resolve(__dirname,'./src')}}
})
与vite.config.js在同一级目录下
{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"],}},"exclude": ["node_modules","dist"],"include": ["src/**/*"]
}
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'import router from '@/router'const app = createApp(App)
app.mount('#app')
app.use(router)
npm install element-plus --save
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'import router from '@/router'const app = createApp(App)
app.mount('#app')
app.use(router)
app.use(ElementPlus)
可参考:Vue3中的pinia使用(收藏版)
npm install pinia --save
import { createPinia } from 'pinia'const pinia = createPinia()export default pinia
import { defineStore } from 'pinia'export const useCounter = defineStore('counter',{state: () => ({count:99}),getters: {},actions: {}
})
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'import router from '@/router'
import pinia from '@/store'const app = createApp(App)app.use(router)
app.use(pinia)
app.use(ElementPlus)app.mount('#app')
{{ counterStore.count }}你好
可参考:Vue3使用axios的配置教程
npm install axios --save
import axios from 'axios'
import Messager from './messager'; // 在下面封装了const instance = axios.create({baseURL: 'http://127.0.0.1:8080/api',timeout: 10000
})instance.interceptors.request.use((config)=>{return config;
})instance.interceptors.response.use(response=>{if(response.data.errno == 0) {return Promise.resolve(response.data.data)} else {if(response.data.errno == 501) {Messager.error('请重新登录')window.location.href = '/login'} else {Messager.error(response.data.errmsg)return Promise.reject(new Error(response.data.errmsg))}}
})export default instance
import request from '@/utils/request'export function getCaptchaImage() {return request({url: 'captchaImage',})
}export function login(data) {return request({method:'post',url: 'user/login',data})
}
验证码
npm i nprogress -S
import Nprogress from 'nprogress'
import 'nprogress/nprogress.css'const nprogress = Nprogress.configure({easing: 'ease', // 动画方式speed: 1000, // 递增进度条的速度showSpinner: false, // 是否显示加载icotrickleSpeed: 200, // 自动递增间隔minimum: 0.3, // 更改启动时使用的最小百分比parent: 'body', //指定进度条的父容器
})export default nprogress
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";// 路由信息
const routes = [{path: '/',name: 'home',component: ()=>import('@/views/index.vue')},{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),}
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()next()
})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
下载iconfont相关资源到本地,添加到assets/iconfont目录下
import { createApp } from 'vue'
import './style.css'import '@/assets/iconfont/iconfont.css' // 引入iconfont的css文件import App from './App.vue'
import { ElMessage } from "element-plus";
const Messager = {ok(msg){ElMessage.success(msg)},error(msg) {ElMessage.error(msg)}
}
export default Messager
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";// 路由信息
const routes = [{path: '/',name: 'home',component: ()=>import('@/views/index.vue')},{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),}
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()next()
})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
登录
![]()
登录
将登录获取的token存入localStorage
import { defineStore } from 'pinia'import { login } from '@/api/loginApi'function retrieveLocalToken() {return localStorage.getItem('token') || ''
}export default defineStore('user',{state: () => {return {token: retrieveLocalToken() // 当刷新页面时, 这个会加载一次}},getters: {},actions: {doLogin(data) {return new Promise((resolve, reject) => {login(data).then(res=>{this.token = res // 同样先存入到pinia中localStorage.setItem('token', res)console.log('login',res);resolve(data)}).catch(err=>{reject(err)})})}}
})
import request from '@/utils/request'export function getCaptchaImage() {return request({url: 'captchaImage',})
}export function login(data) {return request({method:'post',url: 'user/login',data})
}
登录成功之后,会跳到主页,主页大概如下布局,可以先参考vue3 + elment-plus实现后台布局的静态页面布局,然后把它划分成不同的组件,不同组件的数据共享通过pinia这个store来管理。
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";// 路由信息
const routes = [{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),},{path: '/',name: 'home',component: ()=>import('@/layout/index.vue')},
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()next()
})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
Layout组件引入Sider和Main组件
将组件的共享数据存入pinia
import { defineStore } from 'pinia'export default defineStore('layout', {state: ()=> {return {isExpand: true, // 侧边栏是否展开}},getters: {},actions: {// 切换侧边栏toggleSider() {console.log('切换侧边栏', this.isExpand);this.isExpand = !this.isExpand}}
})
isExpand是存放在pinia中的数据
PSCOOL管理系统
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 9
- 9
- 9
- 9
![]()
个人中心 退出登录
系统管理 用户管理 添加用户
123456789
{{ activity.content }}
这一步,我们将获得如下的效果
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";// 路由信息
const routes = [{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),},{path: '/',name: 'layout',redirect:'/home',component: ()=>import('@/layout/index.vue'),children: [{path: 'home',name: 'home',component: ()=>import('@/views/Home.vue'),},{path: 'user',name: 'user',component: ()=>import('@/views/sys/user.vue'),},{path: 'role',name: 'role',component: ()=>import('@/views/sys/role.vue'),},{path: 'menu',name: 'menu',component: ()=>import('@/views/sys/menu.vue'),}]},// 匹配404页面{path:'/:pathMatch(.*)*',name: 'notFound',component: ()=>import('@/views/404/NotFound.vue')},
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()next()
})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
PSCOOL管理系统
主页 系统管理用户管理 角色管理 菜单管理 多级菜单test-1 test-2test-2-1 test-2-2
Home.vue可作为其它组件放入到Main.vue组件的main-body中的路由出口的模板,这样就不会让右边整体出现垂直滚动条(如下图),其它组件可以自定义布局方式,
{{ activity.content }}
这里就展示简单的返回下
页面找丢了。。。
返回
不同用户登录进来,需要根据当前用户拥有的菜单显示在侧边栏,并且动态添加路由到router中。也就是说,用户一登陆完成,我们就应该请求后台去拿到用户拥有的所有菜单,组装侧边栏菜单,并且要动态的添加路由。
我们需要做如下的事情,但是在做下面的事情之前,我们先调整一下我们的菜单,确保这样是可用的,然后再接入后台数据。
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";// 路由信息
const routes = [{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),},{path: '/',name: 'layout',component: ()=>import('@/layout/index.vue'),children: [{path: '/home', // 最好以/开头, 如果不以/开头,那么路由到这个组件就需要拼接上父路径name: 'home',component: ()=>import('@/views/Home.vue'),},{path: '/sys/user',name: 'user',component: ()=>import('@/views/sys/user.vue'),},{path: '/sys/role',name: 'role',component: ()=>import('@/views/sys/role.vue'),},{path: '/sys/menu',name: 'menu',component: ()=>import('@/views/sys/menu.vue'),},{path: '/test/test_1',name: 'test_1',component: ()=>import('@/views/test/test_1.vue'),},{path: '/test/test2/test_2_1',name: 'test_2_1',component: ()=>import('@/views/test/test2/test_2_1.vue'),},{path: '/test/test2/test_2_2',name: 'test_2_2',component: ()=>import('@/views/test/test2/test_2_2.vue'),},]},{path:'/:pathMatch(.*)*',name: 'notFound',component: ()=>import('@/views/404/NotFound.vue')},
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()next()
})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
这里我们只需要对着路由写index的路径即可。还有注意的是,如果用户是直接在地址栏输入的路径而跳转的话,我们也需要让对应的菜单高亮,我们监听路由即可。
PSCOOL管理系统
主页 系统管理用户管理 角色管理 菜单管理 多级菜单test_1 test_2test_2_1 test_2_2
{"errno": 0,"errmsg": "成功","data": [{"id": 1,"parentId": 0,"title":"主页","icon":"iconfont icon-home-line","url":"/home","menuType": "C","component":"@/views/Home.vue"},{"id": 2,"parentId": 0,"title":"系统设置","icon":"iconfont icon-shezhi","url":"/sys","menuType": "M","component":"","children": [{"id": 3,"parentId": 2,"title":"用户管理","icon":"iconfont icon-yonghuguanli","url":"/sys/user","menuType": "C","component":"@/views/sys/user.vue"},{"id": 4,"parentId": 2,"title":"角色管理","icon":"iconfont icon-jiaoseguanli","url":"/sys/role","menuType":"C","component":"@/views/sys/role.vue"},{"id": 5,"parentId": 2,"title":"菜单管理","icon":"iconfont icon-icon_caidanguanli","url":"/sys/menu","menuType":"C","component":"@/views/sys/menu.vue"}]},{"id": 6,"parentId": 0,"title":"多级菜单","icon":"iconfont icon-graphcool","url":"/test","component":"","menuType":"M","children": [{"id": 7,"parentId": 6,"title":"test_1","icon":"iconfont icon-graphcool","url":"/test/test_1","menuType":"C","component":"@/views/test/test_1.vue"},{"id": 8,"parentId": 2,"title":"test_2","icon":"iconfont icon-graphcool","url":"/test/test_2","menuType":"M","component":"","children":[{"id": 9,"parentId": 8,"title":"test_2_1","icon":"iconfont icon-graphcool","menuType":"C","url":"/test/test_2/test_2_1","component":"@/views/test/test_2_1.vue"},{"id": 10,"parentId": 8,"title":"test_2_2","icon":"iconfont icon-graphcool","menuType":"C","url":"/test/test_2/test_2_2","component":"@/views/test/test_2_2.vue"}]}]}]
}
{"errno": 0,"errmsg": "成功","data": [{"path": "/home","name": "home","component": "@/views/Home.vue"},{"path": "/sys/user","name": "user","component": "@/views/sys/user.vue"},{"path": "/sys/role","name": "role","component": "@/views/sys/role.vue"},{"path": "/sys/menu","name": "menu","component": "@/views/sys/menu.vue"},{"path": "/test/test_1","name": "test_1","component": "@/views/test/test_1.vue"},{"path": "/test/test_2/test_2_1","name": "test_2_1","component": "@/views/test/test2/test_2_1.vue"},{"path": "/test/test_2/test_2_2","name": "test_2_2","component": "@/views/test/test2/test_2_2.vue"}]
}
import request from '@/utils/request'export function getCaptchaImage() {return request({url: 'captchaImage',})
}export function login(data) {return request({method:'post',url: 'user/login',data})
}export function getMenus() { // 获取菜单return request({method:'get',url: 'test/getMenus'})
}export function getRoutes() { // 获取路由return request({method:'get',url: 'test/getRoutes'})
}
因为需要添加请求头,才能访问获取菜单路由接口
import axios from 'axios'
import Messager from './messager';
import pinia from '@/store'
import useUser from '@/store/user'const instance = axios.create({baseURL: 'http://127.0.0.1:8080/api',timeout: 10000
})instance.interceptors.request.use((config)=>{// debuggerlet userStore = useUser()if(userStore.token) {console.log('userStore.token',userStore.token);config.headers['Authorization'] = userStore.token}return config;
})instance.interceptors.response.use(response=>{if(response.data.errno == 0) {return Promise.resolve(response.data.data)} else {if(response.data.errno == 501) {Messager.error('请重新登录')window.location.href = '/login'} else {Messager.error(response.data.errmsg)return Promise.reject(new Error(response.data.errmsg))}}
})export default instance
将原本配置的静态路由删掉,这部分路由由后端返回,并添加前置守卫逻辑
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
import Messager from '@/utils/messager';import useMenu from '@/store/menu'import pinia from '@/store'
import useUser from '@/store/user'
const userStore = useUser(pinia) // 此处不能像在组件里使用userStore一样直接useUser()调用,而是要先引入pinia,再传入。参考: https://blog.csdn.net/qq_21473443/article/details/126405859
console.log('router->userStore',userStore);const menuStore = useMenu(pinia)// 路由信息
const routes = [{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),},{path: '/',name: 'layout',component: ()=>import('@/layout/index.vue'),children: []},{path:'/:pathMatch(.*)*',name: 'notFound',component: ()=>import('@/views/404/NotFound.vue')},
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()// debuggerlet token = userStore.tokenif(!token) {if(to.path == '/login') {next()} else {next('/login')}} else {if(!menuStore.routesMenusLoaded) {menuStore.loadRoutesMenus().then(res=>{next()}).catch(err=>{// 加载出错,跳回到登录页去userStore.clearUserInfo()next('/login')})} else {if(to.path == '/login') {Messager.warn('你已登录!')next('/home')} else {next()}}}})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
import { defineStore } from 'pinia'import { login } from '@/api/loginApi'function retrieveLocalToken() {console.log('read token'); return localStorage.getItem('token') || ''
}
function clearLocalToken() {return localStorage.clear('token')
}export default defineStore('user',{state: () => {return {token: retrieveLocalToken() // 当刷新页面时, 这个会加载一次}},getters: {},actions: {doLogin(data) {return new Promise((resolve, reject) => {login(data).then(res=>{this.token = res // 同样先存入到pinia中localStorage.setItem('token', res)resolve(data)}).catch(err=>{reject(err)})})},clearUserInfo() {this.token = nullclearLocalToken()}}
})
创建menu.js用来存储后台返回的数据
import { defineStore } from 'pinia'
import {getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'export default defineStore('menu', {state: ()=> {return {routesMenusLoaded: false, // 路由菜单是否已加载menus: [], // 菜单routes: [] // 路由}},getters: {},actions: {loadRoutesMenus() {return new Promise(async (resolve,reject)=>{try {let menus = await getMenus()let routes = await getRoutes()// 保存路由this.routes = routes// 保存菜单this.menus = menus// 动态加载路由routes.forEach(route=>{router.addRoute('layout', {path: route.path,name: route.name,component: ()=>import(route.component.replace('@',"../")) // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧})})console.log(router.getRoutes(),'finished');resolve()} catch (err) {reject(err)}})}}
})
PSCOOL管理系统
{{ menu.title }} 'nested-sub-menu': menu.parentId != 0}">{{ menu.title }}
上面犯了一个错误,我把404路由作为静态路由,直接给放到了router里面了,这样404的路由就排在了前面,它不是精确匹配,导致我刷新页面的时候,直接就跳404页面了,所以把404的路由改到获取完后端的全部路由数据之后
import { createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
import Messager from '@/utils/messager';import useMenu from '@/store/menu'import pinia from '@/store'
import useUser from '@/store/user'
const userStore = useUser(pinia) // 此处不能像在组件里使用userStore一样直接useUser()调用,而是要先引入pinia,再传入。参考: https://blog.csdn.net/qq_21473443/article/details/126405859
console.log('router->userStore',userStore);const menuStore = useMenu(pinia)// 路由信息
const routes = [{path: "/login",name: "login",component: () => import('@/views/login/index.vue'),},{path: '/',name: 'layout',component: ()=>import('@/layout/index.vue'),children: []}
]const router = createRouter({history: createWebHistory(),routes
});function existRoutePath(path) {let routes = router.getRoutes()let routePathArr = []routes.forEach((route) => {routePathArr.push(route.path)})return routePathArr.indexOf(path)
}router.beforeEach((to,from,next)=>{nprogress.start()// console.log(router.getRoutes(),existRoutePath(to.path),'router hasRoute-3',to);// debuggerlet token = userStore.tokenif(!token) {if(to.path == '/login') {next()} else {next('/login')}} else {if(!menuStore.routesMenusLoaded) {// console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-1',to);menuStore.loadRoutesMenus().then(res=>{// console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-2',to);next({...to})}).catch(err=>{// 加载出错,跳回到登录页去userStore.clearUserInfo()next('/login')})} else {if(to.path == '/login') {Messager.warn('你已登录!')next('/home')} else {// console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-4');next()}}}})router.afterEach((to,from,next)=>{nprogress.done()
})// 导出路由
export default router;
import { defineStore } from 'pinia'
import {getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'export default defineStore('menu', {state: ()=> {return {routesMenusLoaded: false, // 路由菜单是否已加载menus: [], // 菜单routes: [] // 路由}},getters: {},actions: {loadRoutesMenus() {return new Promise(async (resolve,reject)=>{try {let menus = await getMenus()let routes = await getRoutes()// 保存路由this.routes = routes// 保存菜单this.menus = menus// 动态加载路由routes.forEach(route=>{router.addRoute('layout', {path: route.path,name: route.name,component: ()=>import(route.component.replace('@',"../")) // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧})})router.addRoute({path:'/:pathMatch(.*)*',name: 'notFound',component: ()=>import('../views/404/NotFound.vue')})console.log(router.getRoutes(),'加载路由 finished');this.routesMenusLoaded = trueresolve()} catch (err) {reject(err)}})}}
})
npm i screenfull -S
我们需要展示当前路由的菜单的面包屑,先约定下数据,路由的name唯一且对应到菜单里的name且唯一,这样当我们切换到某一个路由的时候,就可以根据name到菜单里面递归的找到它所对应的所有父级。
{"errno": 0,"errmsg": "成功","data": [{"path": "/home","name": "home","component": "@/views/Home.vue"},{"path": "/sys/user","name": "user","component": "@/views/sys/user.vue"},{"path": "/sys/role","name": "role","component": "@/views/sys/role.vue"},{"path": "/sys/menu","name": "menu","component": "@/views/sys/menu.vue"},{"path": "/test/test_1","name": "test_1","component": "@/views/test/test_1.vue"},{"path": "/test/test_2/test_2_1","name": "test_2_1","component": "@/views/test/test2/test_2_1.vue"},{"path": "/test/test_2/test_2_2","name": "test_2_2","component": "@/views/test/test2/test_2_2.vue"}]
}
{"errno": 0,"errmsg": "成功","data": [{"id": 1,"parentId": 0,"name": "home","title":"主页","icon":"iconfont icon-home-line","url":"/home","menuType": "C","component":"@/views/Home.vue"},{"id": 2,"parentId": 0,"name": "sys","title":"系统设置","icon":"iconfont icon-shezhi","url":"/sys","menuType": "M","component":"","children": [{"id": 3,"parentId": 2,"name": "user","title":"用户管理","icon":"iconfont icon-yonghuguanli","url":"/sys/user","menuType": "C","component":"@/views/sys/user.vue"},{"id": 4,"parentId": 2,"name": "role","title":"角色管理","icon":"iconfont icon-jiaoseguanli","url":"/sys/role","menuType":"C","component":"@/views/sys/role.vue"},{"id": 5,"parentId": 2,"name": "menu","title":"菜单管理","icon":"iconfont icon-icon_caidanguanli","url":"/sys/menu","menuType":"C","component":"@/views/sys/menu.vue"}]},{"id": 6,"parentId": 0,"name": "test","title":"多级菜单","icon":"iconfont icon-graphcool","url":"/test","component":"","menuType":"M","children": [{"id": 7,"parentId": 6,"name": "test_1","title":"test_1","icon":"iconfont icon-graphcool","url":"/test/test_1","menuType":"C","component":"@/views/test/test_1.vue"},{"id": 8,"parentId": 2,"name": "test_2","title":"test_2","icon":"iconfont icon-graphcool","url":"/test/test_2","menuType":"M","component":"","children":[{"id": 9,"parentId": 8,"name": "test_2_1","title":"test_2_1","icon":"iconfont icon-graphcool","menuType":"C","url":"/test/test_2/test_2_1","component":"@/views/test/test_2_1.vue"},{"id": 10,"parentId": 8,"name": "test_2_2","title":"test_2_2","icon":"iconfont icon-graphcool","menuType":"C","url":"/test/test_2/test_2_2","component":"@/views/test/test_2_2.vue"}]}]}]
}
根据后台返回的菜单,递归出所有路由对应的带层级的菜单标题,放入路由的meta中
import { defineStore } from 'pinia'
import {getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'
import { dropdownMenuProps } from 'element-plus'function generateNameMap(menus) {const nameMap = {}menus.forEach(menu => {handleMenu(menu,nameMap,[])})return nameMap
}function handleMenu(menu,nameMap,titleArr) {titleArr.push(menu.title)nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))if(menu.children && menu.children.length > 0) {menu.children.forEach(menu => {let newTitleArr = JSON.parse(JSON.stringify(titleArr))handleMenu(menu,nameMap,newTitleArr)})}
}export default defineStore('menu', {state: ()=> {return {routesMenusLoaded: false, // 路由菜单是否已加载menus: [], // 菜单routes: [] // 路由}},getters: {},actions: {loadRoutesMenus() {return new Promise(async (resolve,reject)=>{try {let menus = await getMenus()let routes = await getRoutes()// 保存路由this.routes = routes// 保存菜单this.menus = menus// debuggerconst nameMap = generateNameMap(menus)// 动态加载路由routes.forEach(route=>{router.addRoute('layout', {path: route.path,name: route.name,meta: {titleArr: nameMap[route.name]},// 好像直接以@开头,会报错,所以这干脆替换成相对路径吧component: ()=>import(route.component.replace('@',"../")) })})router.addRoute({path:'/:pathMatch(.*)*',name: 'notFound',component: ()=>import('../views/404/NotFound.vue')})console.log(router.getRoutes(),'加载路由 finished');this.routesMenusLoaded = trueresolve()} catch (err) {reject(err)}})},}
})
监听路由变化,从路由的meta中获取缓存的面包屑数据
{{ title }}
这一步主要实现tagsView功能,tagsView中记录用户访问过的菜单,并且能够根据需要关闭它,但是主页这个tag要一直保留。
我们把tag存放在pinia里面,tagsView组件通过计算属性引用pinia里面的tags,通过监听事件触发方法调用pinia里的方法
里面有个坑:点击关闭按钮的时候,需要阻止事件冒泡,否则,不仅会触发i这个icon的关闭的事件,又会触发div的点击事件,使用@click.stop去绑定
'active':tag.isActive}]" @click="selectSpecifiedTag(tag)" v-for="(tag,index) in tags" :key="index">{{ tag.title }}
import { defineStore } from 'pinia'export default defineStore('tagsView', {state: ()=> {return {tags: [{title: '主页',name: 'home',path: '/home',isActive: false}],}},getters: {},actions: {doOnrouteChange(route) {debuggerconsole.log('doOnrouteChange->新路由', route.name);let currRouteName = route.namelet tagNameArr = []let flag = falsethis.tags.forEach(tag=>{tag.isActive = falseif(tag.name == currRouteName) {flag = truetag.isActive = true}}) if(!flag) {console.log('原先没有这个路由,现在添加tag', route.name);this.tags.push({title: route.meta.title,name: route.name,path: route.path,isActive: true})} },closeSpecifiedTag(tag){debuggerlet index = -1;for(let i=0;iif(this.tags[i].name === tag.name) {index = ibreak}}if(index > -1) {this.tags.splice(index,1)}},selectSpecifiedTag(tag) {debuggerthis.tags.forEach(t=>{t.isActive = falseif(t.name == tag.name) {t.isActive = true}}) }}
})
通过vue的directive指令方式,当用户拥有指定的权限时,才显示按钮
{"errno": 0,"errmsg": "成功","data": {"perms": ["user:list","user:add","user:remove","role:list","role:add","role:remove"]}
}
import useMenu from '@/store/menu'
import { toRaw } from '@vue/reactivity'export default {hasPerms: {mounted(el,binding) {const menuStore = useMenu()let perms1 = menuStore.permsconsole.log(el,binding,perms1);let perms2 = toRaw(perms1)let perms3 = JSON.parse(JSON.stringify(perms1))console.log(perms2.perms);console.log(perms3.perms);// 有任一指定的权限, 即可显示指定的dom, 否则移除if(!perms2.perms.some(p=>binding.value.includes(p))) {el.parentNode.removeChild(el)}},}
}
// ...省略
export function getPerms() {return request({method:'get',url: 'test/getPerms'})
}
把获取权限的部分加进去
import { defineStore } from 'pinia'
import {getMenus,getRoutes,getPerms} from '@/api/loginApi'
import router from '@/router'
import { dropdownMenuProps } from 'element-plus'function generateNameMap(menus) {const nameMap = {}menus.forEach(menu => {handleMenu(menu,nameMap,[])})return nameMap
}function handleMenu(menu,nameMap,titleArr) {titleArr.push(menu.title)nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))if(menu.children && menu.children.length > 0) {menu.children.forEach(menu => {let newTitleArr = JSON.parse(JSON.stringify(titleArr))handleMenu(menu,nameMap,newTitleArr)})}
}export default defineStore('menu', {state: ()=> {return {routesMenusLoaded: false, // 路由菜单是否已加载menus: [], // 菜单routes: [], // 路由,perms: [], // 权限}},getters: {},actions: {loadRoutesMenus() {return new Promise(async (resolve,reject)=>{try {let menus = await getMenus()let routes = await getRoutes()let perms = await getPerms()// 保存路由this.routes = routes// 保存菜单this.menus = menus// 保存权限this.perms = perms// debuggerconst nameMap = generateNameMap(menus)// 动态加载路由routes.forEach(route=>{router.addRoute('layout', {path: route.path,name: route.name,meta: {titleArr: nameMap[route.name],title: nameMap[route.name][nameMap[route.name].length-1]},// 好像直接以@开头,会报错,所以这干脆替换成相对路径吧component: ()=>import(route.component.replace('@',"../")) })})router.addRoute({path:'/:pathMatch(.*)*',name: 'notFound',component: ()=>import('../views/404/NotFound.vue')})console.log(router.getRoutes(),'加载路由 finished');this.routesMenusLoaded = trueresolve()} catch (err) {reject(err)}})},}
})
用户管理查看 添加 修改 删除
如下效果