mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 16:46:19 +08:00
feat: Dynamically get the menu from the back end
This commit is contained in:
@@ -31,10 +31,10 @@
|
||||
"@vben-core/stores": "workspace:*",
|
||||
"@vben/chart-ui": "workspace:*",
|
||||
"@vben/constants": "workspace:*",
|
||||
"@vben/hooks": "workspace:*",
|
||||
"@vben/icons": "workspace:*",
|
||||
"@vben/layouts": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/universal-ui": "workspace:*",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 5.3 KiB |
@@ -1 +1,2 @@
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
|
12
apps/web-antd/src/apis/modules/menu.ts
Normal file
12
apps/web-antd/src/apis/modules/menu.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/forward';
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
async function getAllMenus() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/getAll');
|
||||
}
|
||||
|
||||
export { getAllMenus };
|
@@ -19,5 +19,3 @@ async function getUserInfo() {
|
||||
}
|
||||
|
||||
export { getUserInfo, userLogin };
|
||||
|
||||
export * from './user';
|
||||
|
40
apps/web-antd/src/forward/access.ts
Normal file
40
apps/web-antd/src/forward/access.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { GeneratorMenuAndRoutesOptions } from '@vben/access';
|
||||
import type { ComponentRecordType } from '@vben/types';
|
||||
|
||||
import { generateMenusAndRoutes } from '@vben/access';
|
||||
import { $t } from '@vben/locales';
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { getAllMenus } from '#/apis';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
|
||||
const forbiddenPage = () => import('#/views/_essential/fallback/forbidden.vue');
|
||||
|
||||
async function generateAccess(options: GeneratorMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
IFrameView,
|
||||
};
|
||||
|
||||
return await generateMenusAndRoutes(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
message.loading({
|
||||
content: `${$t('common.loading-menu')}...`,
|
||||
duration: 1.5,
|
||||
});
|
||||
return await getAllMenus();
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
forbiddenComponent: forbiddenPage,
|
||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
layoutMap,
|
||||
pageMap,
|
||||
});
|
||||
}
|
||||
|
||||
export { generateAccess };
|
@@ -10,8 +10,11 @@ import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
import { Notification, UserDropdown } from '@vben/widgets';
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import { useRequest } from '@vben-core/request';
|
||||
import { useAccessStore } from '@vben-core/stores';
|
||||
|
||||
import { getUserInfo } from '#/apis';
|
||||
|
||||
// https://avatar.vercel.sh/vercel.svg?text=Vaa
|
||||
// https://avatar.vercel.sh/1
|
||||
// https://avatar.vercel.sh/nextjs
|
||||
@@ -80,6 +83,14 @@ const menus = computed(() => [
|
||||
const accessStore = useAccessStore();
|
||||
const router = useRouter();
|
||||
|
||||
const { runAsync: runGetUserInfo } = useRequest(getUserInfo, {
|
||||
manual: true,
|
||||
});
|
||||
|
||||
runGetUserInfo().then((userInfo) => {
|
||||
accessStore.setUserInfo(userInfo);
|
||||
});
|
||||
|
||||
function handleLogout() {
|
||||
accessStore.$reset();
|
||||
router.replace('/auth/login');
|
||||
|
@@ -3,16 +3,14 @@ import type { Router } from 'vue-router';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { $t } from '@vben/locales';
|
||||
import { startProgress, stopProgress } from '@vben/utils';
|
||||
import { generatorMenus, generatorRoutes } from '@vben-core/helpers';
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import { useAccessStore } from '@vben-core/stores';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
import { generateAccess } from '#/forward/access';
|
||||
import { dynamicRoutes, essentialsRouteNames } from '#/router/routes';
|
||||
|
||||
const forbiddenPage = () => import('#/views/_essential/fallback/forbidden.vue');
|
||||
|
||||
/**
|
||||
* 通用守卫配置
|
||||
* @param router
|
||||
@@ -96,22 +94,16 @@ function setupAccessGuard(router: Router) {
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userRoles = accessStore.getUserRoles;
|
||||
|
||||
const accessibleRoutes = await generatorRoutes(
|
||||
dynamicRoutes,
|
||||
userRoles,
|
||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
// 生成菜单和路由
|
||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||
roles: userRoles,
|
||||
router,
|
||||
// 则会在菜单中显示,但是访问会被重定向到403
|
||||
// 这里可以指定403页面
|
||||
forbiddenPage,
|
||||
);
|
||||
// 动态添加到router实例内
|
||||
accessibleRoutes.forEach((route) => router.addRoute(route));
|
||||
|
||||
// 生成菜单
|
||||
const menus = await generatorMenus(accessibleRoutes, router);
|
||||
routes: dynamicRoutes,
|
||||
});
|
||||
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(menus);
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
const redirectPath = (from.query.redirect ?? to.path) as string;
|
||||
|
||||
|
@@ -15,7 +15,7 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
hideInTab: true,
|
||||
title: '404',
|
||||
},
|
||||
name: 'Fallback',
|
||||
name: 'FallbackNotFound',
|
||||
path: '/:path(.*)*',
|
||||
};
|
||||
|
||||
|
@@ -15,14 +15,108 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
redirect: '/demos/fallback/403',
|
||||
redirect: '/demos/access/frontend',
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:shield-key-outline',
|
||||
title: $t('page.demos.access.title'),
|
||||
},
|
||||
name: 'Access',
|
||||
path: '/access',
|
||||
redirect: '/access/frontend',
|
||||
children: [
|
||||
{
|
||||
name: 'AccessFrontend',
|
||||
path: 'frontend',
|
||||
meta: {
|
||||
icon: 'mdi:table-key',
|
||||
title: $t('page.demos.access.frontend-control'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'AccessFrontendPageControl',
|
||||
path: 'page-control',
|
||||
component: () =>
|
||||
import('#/views/demos/access/frontend/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:page-previous-outline',
|
||||
title: $t('page.demos.access.page'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessFrontendButtonControl',
|
||||
path: 'button-control',
|
||||
component: () =>
|
||||
import('#/views/demos/access/frontend/button-control.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('page.demos.access.button'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessFrontendTest1',
|
||||
path: 'access-test-1',
|
||||
component: () =>
|
||||
import('#/views/demos/access/frontend/access-test-1.vue'),
|
||||
meta: {
|
||||
authority: ['admin'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('page.demos.access.access-test-1'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessFrontendTest2',
|
||||
path: 'access-test-2',
|
||||
component: () =>
|
||||
import('#/views/demos/access/frontend/access-test-2.vue'),
|
||||
meta: {
|
||||
authority: ['user'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('page.demos.access.access-test-2'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'AccessBackend',
|
||||
path: 'backend',
|
||||
component: () => import('#/views/demos/access/backend/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:cloud-key-outline',
|
||||
title: $t('page.demos.access.backend-control'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'AccessBackendPageControl',
|
||||
path: 'page-control',
|
||||
component: () =>
|
||||
import('#/views/demos/access/frontend/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:page-previous-outline',
|
||||
title: $t('page.demos.access.page'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessBackendButtonControl',
|
||||
path: 'button-control',
|
||||
component: () =>
|
||||
import('#/views/demos/access/frontend/button-control.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('page.demos.access.button'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:lightbulb-error-outline',
|
||||
title: $t('page.demos.fallback.title'),
|
||||
},
|
||||
name: 'FallbackLayout',
|
||||
name: 'Fallback',
|
||||
path: '/fallback',
|
||||
redirect: '/fallback/403',
|
||||
children: [
|
||||
|
@@ -2,7 +2,7 @@ import type { InitStoreOptions } from '@vben-core/stores';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
import { initStore, useAccessStore, useTabsStore } from '@vben-core/stores';
|
||||
import { initStore, useAccessStore, useTabbarStore } from '@vben-core/stores';
|
||||
|
||||
/**
|
||||
* @zh_CN 初始化pinia
|
||||
@@ -13,4 +13,4 @@ async function setupStore(app: App, options: InitStoreOptions) {
|
||||
app.use(pinia);
|
||||
}
|
||||
|
||||
export { setupStore, useAccessStore, useTabsStore };
|
||||
export { setupStore, useAccessStore, useTabbarStore };
|
||||
|
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/universal-ui';
|
||||
|
||||
defineOptions({ name: 'AccessBackendButtonControl' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="comming-soon" />
|
||||
</template>
|
9
apps/web-antd/src/views/demos/access/backend/index.vue
Normal file
9
apps/web-antd/src/views/demos/access/backend/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/universal-ui';
|
||||
|
||||
defineOptions({ name: 'AccessFrontend' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="comming-soon" />
|
||||
</template>
|
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/universal-ui';
|
||||
|
||||
defineOptions({ name: 'AccessFrontendAccessTest1' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 Admin 角色可见"
|
||||
status="comming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/universal-ui';
|
||||
|
||||
defineOptions({ name: 'AccessFrontendAccessTest2' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 User 角色可见"
|
||||
status="comming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/universal-ui';
|
||||
|
||||
defineOptions({ name: 'AccessFrontendButtonControl' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="comming-soon" />
|
||||
</template>
|
45
apps/web-antd/src/views/demos/access/frontend/index.vue
Normal file
45
apps/web-antd/src/views/demos/access/frontend/index.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { useAccess } from '@vben/access';
|
||||
import { useAccessStore } from '@vben-core/stores';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'AccessBackend' });
|
||||
|
||||
const { currentAccessMode } = useAccess();
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
function roleButtonType(role: string) {
|
||||
return accessStore.getUserRoles.includes(role) ? 'primary' : 'default';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<div class="card-box p-5">
|
||||
<h1 class="text-xl font-semibold">前端页面访问演示</h1>
|
||||
<div class="text-foreground/80 mt-2">
|
||||
由于刷新的时候会请求用户信息接口,会根据接口重置角色信息,所以刷新后界面会恢复原样。如果不需要,可以注释对应的代码。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="currentAccessMode === 'frontend'">
|
||||
<div class="card-box mt-5 p-5 font-semibold">
|
||||
当前权限模式:
|
||||
<span class="text-primary mx-4">{{ currentAccessMode }}</span>
|
||||
<Button type="primary">切换权限模式</Button>
|
||||
</div>
|
||||
|
||||
<div class="card-box mt-5 p-5 font-semibold">
|
||||
当前用户角色:
|
||||
<span class="text-primary mx-4">{{ accessStore.getUserRoles }}</span>
|
||||
<Button :type="roleButtonType('admin')"> 切换为 Admin 角色 </Button>
|
||||
<Button :type="roleButtonType('user')" class="mx-4">
|
||||
切换为 User 角色
|
||||
</Button>
|
||||
|
||||
<div class="text-foreground/80 mt-2">角色后请查看左侧菜单变化</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
Reference in New Issue
Block a user