7 changed files with 337 additions and 0 deletions
@ -0,0 +1,29 @@ |
|||||
|
{ |
||||
|
"name": "@vben/access", |
||||
|
"version": "5.6.0", |
||||
|
"homepage": "https://github.com/vbenjs/vue-vben-admin", |
||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git", |
||||
|
"directory": "packages/effects/permissions" |
||||
|
}, |
||||
|
"license": "MIT", |
||||
|
"type": "module", |
||||
|
"sideEffects": [ |
||||
|
"**/*.css" |
||||
|
], |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./src/index.ts", |
||||
|
"default": "./src/index.ts" |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@vben/preferences": "workspace:*", |
||||
|
"@vben/stores": "workspace:*", |
||||
|
"@vben/types": "workspace:*", |
||||
|
"@vben/utils": "workspace:*", |
||||
|
"vue": "catalog:" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
<!-- |
||||
|
Access control component for fine-grained access control. |
||||
|
TODO: 可以扩展更完善的功能: |
||||
|
1. 支持多个权限码,只要有一个权限码满足即可 或者 多个权限码全部满足 |
||||
|
2. 支持多个角色,只要有一个角色满足即可 或者 多个角色全部满足 |
||||
|
3. 支持自定义权限码和角色的判断逻辑 |
||||
|
--> |
||||
|
<script lang="ts" setup> |
||||
|
import { computed } from 'vue'; |
||||
|
|
||||
|
import { useAccess } from './use-access'; |
||||
|
|
||||
|
interface Props { |
||||
|
/** |
||||
|
* Specified codes is visible |
||||
|
* @default [] |
||||
|
*/ |
||||
|
codes?: string[]; |
||||
|
|
||||
|
/** |
||||
|
* 通过什么方式来控制组件,如果是 role,则传入角色,如果是 code,则传入权限码 |
||||
|
* @default 'role' |
||||
|
*/ |
||||
|
type?: 'code' | 'role'; |
||||
|
} |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'AccessControl', |
||||
|
}); |
||||
|
|
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
codes: () => [], |
||||
|
type: 'role', |
||||
|
}); |
||||
|
|
||||
|
const { hasAccessByCodes, hasAccessByRoles } = useAccess(); |
||||
|
|
||||
|
const hasAuth = computed(() => { |
||||
|
const { codes, type } = props; |
||||
|
return type === 'role' ? hasAccessByRoles(codes) : hasAccessByCodes(codes); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<slot v-if="!codes"></slot> |
||||
|
<slot v-else-if="hasAuth"></slot> |
||||
|
</template> |
||||
@ -0,0 +1,156 @@ |
|||||
|
import type { Component, DefineComponent } from 'vue'; |
||||
|
|
||||
|
import type { |
||||
|
AccessModeType, |
||||
|
GenerateMenuAndRoutesOptions, |
||||
|
RouteRecordRaw, |
||||
|
} from '@vben/types'; |
||||
|
|
||||
|
import { defineComponent, h } from 'vue'; |
||||
|
|
||||
|
import { |
||||
|
cloneDeep, |
||||
|
generateMenus, |
||||
|
generateRoutesByBackend, |
||||
|
generateRoutesByFrontend, |
||||
|
isFunction, |
||||
|
isString, |
||||
|
mapTree, |
||||
|
} from '@vben/utils'; |
||||
|
|
||||
|
async function generateAccessible( |
||||
|
mode: AccessModeType, |
||||
|
options: GenerateMenuAndRoutesOptions, |
||||
|
) { |
||||
|
const { router } = options; |
||||
|
|
||||
|
options.routes = cloneDeep(options.routes); |
||||
|
// 生成路由
|
||||
|
const accessibleRoutes = await generateRoutes(mode, options); |
||||
|
|
||||
|
const root = router.getRoutes().find((item) => item.path === '/'); |
||||
|
|
||||
|
// 获取已有的路由名称列表
|
||||
|
const names = root?.children?.map((item) => item.name) ?? []; |
||||
|
|
||||
|
// 动态添加到router实例内
|
||||
|
accessibleRoutes.forEach((route) => { |
||||
|
if (root && !route.meta?.noBasicLayout) { |
||||
|
// 为了兼容之前的版本用法,如果包含子路由,则将component移除,以免出现多层BasicLayout
|
||||
|
// 如果你的项目已经跟进了本次修改,移除了所有自定义菜单首级的BasicLayout,可以将这段if代码删除
|
||||
|
if (route.children && route.children.length > 0) { |
||||
|
delete route.component; |
||||
|
} |
||||
|
// 根据router name判断,如果路由已经存在,则不再添加
|
||||
|
if (names?.includes(route.name)) { |
||||
|
// 找到已存在的路由索引并更新,不更新会造成切换用户时,一级目录未更新,homePath 在二级目录导致的404问题
|
||||
|
const index = root.children?.findIndex( |
||||
|
(item) => item.name === route.name, |
||||
|
); |
||||
|
if (index !== undefined && index !== -1 && root.children) { |
||||
|
root.children[index] = route; |
||||
|
} |
||||
|
} else { |
||||
|
root.children?.push(route); |
||||
|
} |
||||
|
} else { |
||||
|
router.addRoute(route); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (root) { |
||||
|
if (root.name) { |
||||
|
router.removeRoute(root.name); |
||||
|
} |
||||
|
router.addRoute(root); |
||||
|
} |
||||
|
|
||||
|
// 生成菜单
|
||||
|
const accessibleMenus = generateMenus(accessibleRoutes, options.router); |
||||
|
|
||||
|
return { accessibleMenus, accessibleRoutes }; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Generate routes |
||||
|
* @param mode |
||||
|
* @param options |
||||
|
*/ |
||||
|
async function generateRoutes( |
||||
|
mode: AccessModeType, |
||||
|
options: GenerateMenuAndRoutesOptions, |
||||
|
) { |
||||
|
const { forbiddenComponent, roles, routes } = options; |
||||
|
|
||||
|
let resultRoutes: RouteRecordRaw[] = routes; |
||||
|
switch (mode) { |
||||
|
case 'backend': { |
||||
|
resultRoutes = await generateRoutesByBackend(options); |
||||
|
break; |
||||
|
} |
||||
|
case 'frontend': { |
||||
|
resultRoutes = await generateRoutesByFrontend( |
||||
|
routes, |
||||
|
roles || [], |
||||
|
forbiddenComponent, |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
case 'mixed': { |
||||
|
const [frontend_resultRoutes, backend_resultRoutes] = await Promise.all([ |
||||
|
generateRoutesByFrontend(routes, roles || [], forbiddenComponent), |
||||
|
generateRoutesByBackend(options), |
||||
|
]); |
||||
|
|
||||
|
resultRoutes = [...frontend_resultRoutes, ...backend_resultRoutes]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 调整路由树,做以下处理: |
||||
|
* 1. 对未添加redirect的路由添加redirect |
||||
|
* 2. 将懒加载的组件名称修改为当前路由的名称(如果启用了keep-alive的话) |
||||
|
*/ |
||||
|
resultRoutes = mapTree(resultRoutes, (route) => { |
||||
|
// 重新包装component,使用与路由名称相同的name以支持keep-alive的条件缓存。
|
||||
|
if ( |
||||
|
route.meta?.keepAlive && |
||||
|
isFunction(route.component) && |
||||
|
route.name && |
||||
|
isString(route.name) |
||||
|
) { |
||||
|
const originalComponent = route.component as () => Promise<{ |
||||
|
default: Component | DefineComponent; |
||||
|
}>; |
||||
|
route.component = async () => { |
||||
|
const component = await originalComponent(); |
||||
|
if (!component.default) return component; |
||||
|
return defineComponent({ |
||||
|
name: route.name as string, |
||||
|
setup(props, { attrs, slots }) { |
||||
|
return () => h(component.default, { ...props, ...attrs }, slots); |
||||
|
}, |
||||
|
}); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// 如果有redirect或者没有子路由,则直接返回
|
||||
|
if (route.redirect || !route.children || route.children.length === 0) { |
||||
|
return route; |
||||
|
} |
||||
|
const firstChild = route.children[0]; |
||||
|
|
||||
|
// 如果子路由不是以/开头,则直接返回,这种情况需要计算全部父级的path才能得出正确的path,这里不做处理
|
||||
|
if (!firstChild?.path || !firstChild.path.startsWith('/')) { |
||||
|
return route; |
||||
|
} |
||||
|
|
||||
|
route.redirect = firstChild.path; |
||||
|
return route; |
||||
|
}); |
||||
|
|
||||
|
return resultRoutes; |
||||
|
} |
||||
|
|
||||
|
export { generateAccessible }; |
||||
@ -0,0 +1,42 @@ |
|||||
|
/** |
||||
|
* Global authority directive |
||||
|
* Used for fine-grained control of component permissions |
||||
|
* @Example v-access:role="[ROLE_NAME]" or v-access:role="ROLE_NAME" |
||||
|
* @Example v-access:code="[ROLE_CODE]" or v-access:code="ROLE_CODE" |
||||
|
*/ |
||||
|
import type { App, Directive, DirectiveBinding } from 'vue'; |
||||
|
|
||||
|
import { useAccess } from './use-access'; |
||||
|
|
||||
|
function isAccessible( |
||||
|
el: Element, |
||||
|
binding: DirectiveBinding<string | string[]>, |
||||
|
) { |
||||
|
const { accessMode, hasAccessByCodes, hasAccessByRoles } = useAccess(); |
||||
|
|
||||
|
const value = binding.value; |
||||
|
|
||||
|
if (!value) return; |
||||
|
const authMethod = |
||||
|
accessMode.value === 'frontend' && binding.arg === 'role' |
||||
|
? hasAccessByRoles |
||||
|
: hasAccessByCodes; |
||||
|
|
||||
|
const values = Array.isArray(value) ? value : [value]; |
||||
|
|
||||
|
if (!authMethod(values)) { |
||||
|
el?.remove(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const mounted = (el: Element, binding: DirectiveBinding<string | string[]>) => { |
||||
|
isAccessible(el, binding); |
||||
|
}; |
||||
|
|
||||
|
const authDirective: Directive = { |
||||
|
mounted, |
||||
|
}; |
||||
|
|
||||
|
export function registerAccessDirective(app: App) { |
||||
|
app.directive('access', authDirective); |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
export { default as AccessControl } from './access-control.vue'; |
||||
|
export * from './accessible'; |
||||
|
export * from './directive'; |
||||
|
export * from './use-access'; |
||||
@ -0,0 +1,53 @@ |
|||||
|
import { computed } from 'vue'; |
||||
|
|
||||
|
import { preferences, updatePreferences } from '@vben/preferences'; |
||||
|
import { useAccessStore, useUserStore } from '@vben/stores'; |
||||
|
|
||||
|
function useAccess() { |
||||
|
const accessStore = useAccessStore(); |
||||
|
const userStore = useUserStore(); |
||||
|
const accessMode = computed(() => { |
||||
|
return preferences.app.accessMode; |
||||
|
}); |
||||
|
|
||||
|
/** |
||||
|
* 基于角色判断是否有权限 |
||||
|
* @description: Determine whether there is permission,The role is judged by the user's role |
||||
|
* @param roles |
||||
|
*/ |
||||
|
function hasAccessByRoles(roles: string[]) { |
||||
|
const userRoleSet = new Set(userStore.userRoles); |
||||
|
const intersection = roles.filter((item) => userRoleSet.has(item)); |
||||
|
return intersection.length > 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 基于权限码判断是否有权限 |
||||
|
* @description: Determine whether there is permission,The permission code is judged by the user's permission code |
||||
|
* @param codes |
||||
|
*/ |
||||
|
function hasAccessByCodes(codes: string[]) { |
||||
|
const userCodesSet = new Set(accessStore.accessCodes); |
||||
|
|
||||
|
const intersection = codes.filter((item) => userCodesSet.has(item)); |
||||
|
return intersection.length > 0; |
||||
|
} |
||||
|
|
||||
|
async function toggleAccessMode() { |
||||
|
updatePreferences({ |
||||
|
app: { |
||||
|
accessMode: |
||||
|
preferences.app.accessMode === 'frontend' ? 'backend' : 'frontend', |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
accessMode, |
||||
|
hasAccessByCodes, |
||||
|
hasAccessByRoles, |
||||
|
toggleAccessMode, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export { useAccess }; |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/tsconfig", |
||||
|
"extends": "@vben/tsconfig/web.json", |
||||
|
"include": ["src"], |
||||
|
"exclude": ["node_modules"] |
||||
|
} |
||||
Loading…
Reference in new issue