一.配置权限路由
- 创建一个router文件夹,在router文件夹里创建一个index.ts文件,在这里主要放的是路由守卫的逻辑判断,代码如下:
import { routes } from './router' import { createRouter, createWebHashHistory } from 'vue-router' const router = createRouter({ routes, history: createWebHashHistory(), }) router.beforeEach((to, from,next) => { let isToken = localStorage.getItem("token") let isDeptToken = sessionStorage.getItem("deptToken") /**有token或者在login页面下通行*/ if ( (isToken && isDeptToken) || to.path === '/login' ) { next(); } else { next('/login'); } const title = to.meta.title as string sessionStorage.setItem('beforePath', from.fullPath) sessionStorage.setItem('authCode', JSON.stringify(to.meta.authCode)) document.title = title ? `${title} - 值ERP` : '值ERP' return true }), router.afterEach((to) => { const route: any = { path: to.path, meta: to.meta, params: to.params, query: to.query } sessionStorage.setItem('route', JSON.stringify(route)) }) export { router }
2.在router文件夹下面创建一个router.ts文件,此文件主要是用来配置公共路由和没有权限限制的路由。代码如下:
import { RouteRecordRaw } from 'vue-router' /**配置路由*/ const routes: Array<RouteRecordRaw>= [ { path:'/', redirect:'/menu' }, { path:'/login', name:'login', meta:{ title:'登录' }, component: () => import( '@/design/login/index.vue' ) }, { name: 'notFound', path: '/:pathMatch(.*)*', meta: { title: 'notFound' }, component: () => import('@/design/error/notFound.vue'), }, ] export {routes}
3.在router文件夹下面创建一个auth.ts,此文件主要配置侧边栏的路由,后面我们获取到的有权限的路由就会动态添加到这里的children里面。代码如下:
export const authRouter : any = { path:'/', name:'layout', meta:{title:'系统设置'}, component: () => import( '@/design/layout/index.vue' ), redirect:'/menu', children : [] }
二、进行权限路由的匹配
- 我们需要创建一个api文件夹,在api文件夹下面创建一个login文件夹,在login文件夹下面创建一个index.ts文件。这里主要是进行权限路由的匹配。(这个文件创建在哪里可以自己决定,我这里 是因为多处用到且获取部门的时候获取的权限,所以选择放在这里,大家可以根据自己项目自己决定)。代码如下:
注意:authList 对象必须是 权限的唯一标识 必须是唯一值
authList 是一个包含所有权限路由的对象
import { http } from '@/utils'
import { authRouter } from '@/router/auth'
import { router } from '@/router/index'
/**这里是一个包含所有权限路由的对象,用后端返回的路由的唯一标识做为键
原理是用 路由的唯一标识来匹配当前角色 所拥有的权限 从而找到 当前角色应该看
到的菜单内容。
*/
const authList = {
"menu": {
name: 'menu',
path: '/menu',
meta:{},
component: () => import('@/views/menu/index.vue')
},
"dept": {
name: 'dept',
path: '/dept',
meta:{},
component: () => import('@/views/department/index.vue')
},
"approve": {
name: 'approve',
path: '/approve',
meta:{},
component: () => import('@/views/approve/index.vue')
},
"notice": {
name: 'notice',
path: '/notice',
meta:{},
component: () => import('@/views/notice/index.vue')
},
"dictionary": {
name: 'dictionary',
path: '/dictionary',
meta:{},
component: () => import('@/views/dictionary/index.vue')
},
}
/**生成权限菜单列表 */
export const menuArr = (list) => {
const bbb = list.filter((item) => {
return item.type < 2
})
const initTree = (parent_id: number): Array<any> => {
const result = bbb.filter(item => item.pId == parent_id)
const data = result.map(item => ({
title: item.title,
icon: item.icon,
key: item.id,
sort:item.sort,
path: item.assembly ? authList[item.assembly]?.path : null,
children: initTree(item.id)
}))
data.sort((a, b) => {
return a.sort - b.sort;
});
return data
}
// 首先调用initTree方法查找所有parent_id为-1的(-1认为是第一级)
const tree = initTree(1)
return tree
}
/**生成权限路由列表 */
export const routeArr = (data) => {
const initTree = (parent_id: number): Array<any> => {
const result = data.filter(item => item.pId == parent_id)
return result.map(item => ({
...item,
children: initTree(item.id)
}))
}
data.filter((item) => {
return item.type == 1
}).forEach((item) => {
const aa = item.assembly ? authList[item.assembly] : null
const tree = initTree(item.id)
if(aa){
aa.meta.title = item.title
aa.meta.key = item.id
aa.meta.parentKey = item.pId
aa.meta.authCode = tree.map((item) => {
return item.authCode
})
}
})
return Object.values(authList)
}
/**获取权限原始列表 */
export async function getAuthMenu( dept_id : number ){
const res = await http.post(`/User/login-dept/${dept_id}`)
sessionStorage.setItem('deptToken',res.deptToken)
const list = menuArr(res.menus)
sessionStorage.setItem("menus",JSON.stringify(list))
const routeList = routeArr(res.menus)
authRouter.children = routeList
router.addRoute(authRouter)
return routeList
}
三、实现侧边栏菜单栏的渲染
- 在你的渲染侧边栏的组件里取出经过路由权限匹配生成的菜单,然后渲染到组件上。代码如下:
我这里用的是antdesign,大家可以根据自己的实际情况做更改
<template>
<div class="slider">
<div class="flex py-6 pl-[60px]"><span class="text-white text-2xl">ERP</span></div>
<a-menu class="menu" id="menu" mode="inline" theme="dark"
@click="handleClick"
v-model:openKeys="state.openKeys"
v-model:selectedKeys="state.selectedKeys"
>
<template v-for="v in authList">
<template v-if="!v.children || v.children.length == 0">
<a-menu-item :key="v.key" :path="v.path" >
<span>{{ v.title }}</span>
</a-menu-item>
</template>
<template v-else >
<a-sub-menu :key="v.key" :title="v.title">
<a-menu-item v-for="chi in v.children" class="menu-item" :key="chi.key" :path="chi.path" >
<span>{{ chi.title }}</span>
</a-menu-item>
</a-sub-menu>
</template>
</template>
</a-menu>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { app } from '@/utils'
/**我把处理过的菜单 存到了本地 所以我从本地取出 */
const authList = JSON.parse(sessionStorage.getItem('menus') || '')
const state = ref({
collapsed: false,
selectedKeys: [app.route.meta.key],
openKeys: [app.route.meta.parentKey],
});
/**切换菜单 */
const handleClick = ({ item }) => {
state.value.selectedKeys = [item.key]
app.router.push(item.path);
};
</script>
<style scoped lang='scss'>
.slider{
height: 100vh;
}
:deep(.ant-menu-dark .ant-menu-item-selected){
background-color: rgba(235,241,255,0.5);
}
:deep(.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item:not(.ant-menu-item-selected):hover){
background-color: rgba(235,241,255,0.2);
}
</style>
四、具体使用(获取后端不同角色返回的不同权限)
这里我是在登录之后,选择部门的时候。会有不同的权限数据(此时的数据是扁平化的,如果此时的数据已经是处理过的完美的json数据,那么 在api文件里 就不需要对数据进行处理了,可以直接进行匹配。)。那么,我在这里需要根据不同的部门id,调用api文件里的getAuthMenu方法进行初始化权限菜单(getAuthMenu方法的具体逻辑请看标题二)。我的login.vue文件里,代码如下:
<template>
<div class="login flex flex-col">
<div class="m-6 text-2xl font-normal" style="color: #333333;">
ERP管理系统
</div>
<div class="flex items-center h-full justify-center ">
<div class=" w-[40%] h-[78%]">
<img src="@/assets/images/login/picture_icon.png" alt="" class="w-full h-full">
</div>
<div class="w-[25%] h-[60%] ml-32 bg-white shadow-lg rounded-xl relative">
<div class="flex flex-col items-center justify-around h-full" v-if="!store.loginInfo.token" >
<div class="flex flex-col items-center justify-center">
<div class="text-lg font-medium">钉钉扫码登录</div>
<div class="w-[72px] h-[2px] bg-[#306FFF] my-2"></div>
</div>
<div class="">
<div id="self_defined_element" class="self-defined-classname absolute z-10 ml-4 mt-3 flex justify-center items-center">
<a-spin :spinning="true"></a-spin>
</div>
<img src="@/assets/images/login/fillet_corne_icon.png" alt="" class="w-[330px] h-[330px] relative">
</div>
<div class="text-[#cccccc]">
钉钉扫码进入
</div>
</div>
<div class="flex flex-col items-center mt-4" v-if="store.loginInfo.token">
<div class="flex flex-col items-center">
<div class="text-lg font-medium">请选择你的部门</div>
<div class="w-[72px] h-[2px] bg-[#306FFF] my-2"></div>
</div>
<div class="btn flex flex-col">
<a-card
:class="['w-[320px] h-[60px] mt-8 relative', item.disable?'cursor-not-allowed':'cursor-pointer hover:border-[#306FFF]']"
v-for="item in departmentList"
@click="Enter(item)"
>
<span :class="['text-base absolute top-4', item.disable?'text-[#cccccc] ':'']">
{{ item.title }}
</span>
<img src="@/assets/images/login/entrance_icon.png" class="w-[20px] h-[20px] absolute right-4 top-5">
</a-card>
</div>
<div class="absolute bottom-6 text-base">
<a-button type="link" @click="reLogin">重新扫码登录</a-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { app, http } from "@/utils"
import { getDept } from './index'
import { useLogin } from '@/store/index'
import { message } from 'ant-design-vue'
import { getAuthMenu } from '@/api'
import { router } from '@/router/index'
import { authRouter } from '@/router/auth'
const store = useLogin()
const departmentList = ref()
const spinning = ref<boolean>(true);
/**选择部门进入 */
const Enter = ( department : any ) => {
sessionStorage.setItem('deptId',JSON.stringify(department.id))
if( department.disable ){
message.warning('抱歉,您无法进入该部门');
return ;
}
getAuthMenu( department.id ).then( data => {
authRouter.children = data
router.addRoute(authRouter)
app.router.push(`${data[0].path}`)
})
}
/**重新登录 */
const reLogin = () => {
localStorage.removeItem('token')
app.router.go(0)
}
/**获取钉钉第三方登录的二维码 */
const scanCode = () => {
console.log(window.location.origin);
// @ts-ignore
window.DTFrameLogin(
{
id: 'self_defined_element',
width: 300,
height: 300,
},
{
scope: 'openid',
prompt: 'consent',
response_type: 'code',
client_id: 'dingdr49nwgpgfsuibds',
redirect_uri: encodeURIComponent(window.location.origin),
}, loginResult => {
const {redirectUrl, authCode, state} = loginResult
http.post("/User/scan-code-login" , { authCode })
.then( (res ) => {
localStorage.setItem("token" , res.token )
localStorage.setItem("userInfo" , JSON.stringify(res))
store.addInfo(res.token)
getDept()
})
}, errorMsg => {
// 这里一般需要展示登录失败的具体原因
console.log(`Login Error: ${errorMsg}`)
}
)
spinning.value = false;
}
/**第三方钉钉扫码登录 */
onMounted( () => {
scanCode()
})
if( localStorage.getItem('token') ){
getDept()
}
/**订阅状态发生变化 */
store.$subscribe((mutation, state) => {
departmentList.value = store.loginInfo.deptList
})
</script>
<style scoped lang='scss'>
.login{
width: 100vw;
height: 100vh;
background-image: url('@/assets/images/login/Background image_icon.png');
background-size: 100vw 100vh;
}
.self-defined-classname {
width: 300px;
height: 300px;
}
</style>
五、如何解决刷新权限路由丢失的问题
- 可以在项目初始化的进行权限路由的匹配添加
- 具体实现,在main.ts里面再此调用api文件的getAuthMenu进行权限路由的添加。
- main.ts文件的代码如下:
import './style.css' import dayjs from 'dayjs' import 'dayjs/locale/zh-cn' import App from './App.vue' import { createApp } from 'vue' import { router } from './router' import antdv from 'ant-design-vue' import { createPinia } from 'pinia' import { auth } from './directive/auth' import { authRouter } from '@/router/auth' import { getAuthMenu } from '@/api' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.use(antdv) app.directive('auth', auth) dayjs.locale('zh-cn') /** 因为是异步的 所以必须放在bootstrap里面*/ async function bootstrap(){ if(sessionStorage.getItem('deptId')){ const dept_id = JSON.parse(sessionStorage.getItem('deptId') || '' ) const isToken = localStorage.getItem("token") const isDeptToken = sessionStorage.getItem("deptToken") if ( (isToken && isDeptToken) ) { const data = await getAuthMenu( dept_id ) authRouter.children = data router.addRoute(authRouter) } } app.use(router) app.mount('#app') } bootstrap()