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