diff --git a/vue-vben-admin/packages/effects/access/package.json b/vue-vben-admin/packages/effects/access/package.json new file mode 100644 index 0000000..e9549ca --- /dev/null +++ b/vue-vben-admin/packages/effects/access/package.json @@ -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:" + } +} diff --git a/vue-vben-admin/packages/effects/access/src/access-control.vue b/vue-vben-admin/packages/effects/access/src/access-control.vue new file mode 100644 index 0000000..219608e --- /dev/null +++ b/vue-vben-admin/packages/effects/access/src/access-control.vue @@ -0,0 +1,47 @@ + + + + diff --git a/vue-vben-admin/packages/effects/access/src/accessible.ts b/vue-vben-admin/packages/effects/access/src/accessible.ts new file mode 100644 index 0000000..eb90814 --- /dev/null +++ b/vue-vben-admin/packages/effects/access/src/accessible.ts @@ -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 }; diff --git a/vue-vben-admin/packages/effects/access/src/directive.ts b/vue-vben-admin/packages/effects/access/src/directive.ts new file mode 100644 index 0000000..35d9d51 --- /dev/null +++ b/vue-vben-admin/packages/effects/access/src/directive.ts @@ -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, +) { + 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) => { + isAccessible(el, binding); +}; + +const authDirective: Directive = { + mounted, +}; + +export function registerAccessDirective(app: App) { + app.directive('access', authDirective); +} diff --git a/vue-vben-admin/packages/effects/access/src/index.ts b/vue-vben-admin/packages/effects/access/src/index.ts new file mode 100644 index 0000000..392aa53 --- /dev/null +++ b/vue-vben-admin/packages/effects/access/src/index.ts @@ -0,0 +1,4 @@ +export { default as AccessControl } from './access-control.vue'; +export * from './accessible'; +export * from './directive'; +export * from './use-access'; diff --git a/vue-vben-admin/packages/effects/access/src/use-access.ts b/vue-vben-admin/packages/effects/access/src/use-access.ts new file mode 100644 index 0000000..939cdbe --- /dev/null +++ b/vue-vben-admin/packages/effects/access/src/use-access.ts @@ -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 }; diff --git a/vue-vben-admin/packages/effects/access/tsconfig.json b/vue-vben-admin/packages/effects/access/tsconfig.json new file mode 100644 index 0000000..ce1a891 --- /dev/null +++ b/vue-vben-admin/packages/effects/access/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +}