feat: Dynamically get the menu from the back end
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -171,10 +171,7 @@
|
|||||||
"packages/@vben-core/shared/design-tokens/src/**/*.css"
|
"packages/@vben-core/shared/design-tokens/src/**/*.css"
|
||||||
],
|
],
|
||||||
|
|
||||||
"i18n-ally.localesPaths": [
|
"i18n-ally.localesPaths": ["packages/locales/src/langs"],
|
||||||
"packages/locales/src/langs",
|
|
||||||
"packages/@core/shared/i18n/src/langs"
|
|
||||||
],
|
|
||||||
"i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"],
|
"i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"],
|
||||||
"i18n-ally.sourceLanguage": "en",
|
"i18n-ally.sourceLanguage": "en",
|
||||||
"i18n-ally.displayLanguage": "zh-CN",
|
"i18n-ally.displayLanguage": "zh-CN",
|
||||||
|
6
apps/backend-mock/http/menu.http
Normal file
6
apps/backend-mock/http/menu.http
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@port = 5320
|
||||||
|
@type = application/json
|
||||||
|
@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwicm9sZXMiOlsiYWRtaW4iXSwidXNlcm5hbWUiOiJ2YmVuIiwiaWF0IjoxNzE5ODkwMTEwLCJleHAiOjE3MTk5NzY1MTB9.eyAFsQ2Jk_mAQGvrEL1jF9O6YmLZ_PSYj5aokL6fCuU
|
||||||
|
GET http://localhost:{{port}}/api/menu/getAll HTTP/1.1
|
||||||
|
content-type: {{ type }}
|
||||||
|
Authorization: {{ token }}
|
@@ -36,8 +36,8 @@
|
|||||||
"typeorm": "^0.3.20"
|
"typeorm": "^0.3.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.3.2",
|
"@nestjs/cli": "^10.4.0",
|
||||||
"@nestjs/schematics": "^10.1.1",
|
"@nestjs/schematics": "^10.1.2",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^20.14.9",
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.1.4",
|
||||||
|
@@ -7,6 +7,7 @@ import Joi from 'joi';
|
|||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { DatabaseModule } from './modules/database/database.module';
|
import { DatabaseModule } from './modules/database/database.module';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
import { HealthModule } from './modules/health/health.module';
|
||||||
|
import { MenuModule } from './modules/menu/menu.module';
|
||||||
import { UsersModule } from './modules/users/users.module';
|
import { UsersModule } from './modules/users/users.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -34,6 +35,7 @@ import { UsersModule } from './modules/users/users.module';
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
|
MenuModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
9
apps/backend-mock/src/models/dto/user.dto.ts
Normal file
9
apps/backend-mock/src/models/dto/user.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class CreateUserDto {
|
||||||
|
id: number;
|
||||||
|
password: string;
|
||||||
|
realName: string;
|
||||||
|
roles: string[];
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CreateUserDto };
|
62
apps/backend-mock/src/modules/menu/menu.controller.ts
Normal file
62
apps/backend-mock/src/modules/menu/menu.controller.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { sleep } from '@/utils';
|
||||||
|
import { Controller, Get, HttpCode, HttpStatus, Request } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('menu')
|
||||||
|
export class MenuController {
|
||||||
|
/**
|
||||||
|
* 获取用户所有菜单
|
||||||
|
*/
|
||||||
|
@Get('getAll')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getAll(@Request() req: Request) {
|
||||||
|
// 模拟请求延迟
|
||||||
|
await sleep(1000);
|
||||||
|
// 请求用户的id
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// TODO: 改为表方式获取
|
||||||
|
const dashboardMenus = [
|
||||||
|
{
|
||||||
|
component: 'BasicLayout',
|
||||||
|
meta: {
|
||||||
|
order: -1,
|
||||||
|
title: 'page.dashboard.title',
|
||||||
|
},
|
||||||
|
name: 'Dashboard',
|
||||||
|
path: '/',
|
||||||
|
redirect: '/analytics',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Analytics',
|
||||||
|
path: '/analytics',
|
||||||
|
component: '/dashboard/analytics/index',
|
||||||
|
meta: {
|
||||||
|
affixTab: true,
|
||||||
|
title: 'page.dashboard.analytics',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Workspace',
|
||||||
|
path: '/workspace',
|
||||||
|
component: '/dashboard/workspace/index',
|
||||||
|
meta: {
|
||||||
|
title: 'page.dashboard.workspace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const MOCK_MENUS = [
|
||||||
|
{
|
||||||
|
menus: [...dashboardMenus],
|
||||||
|
userId: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menus: [...dashboardMenus],
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return MOCK_MENUS.find((item) => item.userId === userId)?.menus ?? [];
|
||||||
|
}
|
||||||
|
}
|
10
apps/backend-mock/src/modules/menu/menu.module.ts
Normal file
10
apps/backend-mock/src/modules/menu/menu.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MenuController } from './menu.controller';
|
||||||
|
import { MenuService } from './menu.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [MenuController],
|
||||||
|
providers: [MenuService],
|
||||||
|
})
|
||||||
|
export class MenuModule {}
|
4
apps/backend-mock/src/modules/menu/menu.service.ts
Normal file
4
apps/backend-mock/src/modules/menu/menu.service.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MenuService {}
|
@@ -1,3 +1,4 @@
|
|||||||
|
import type { CreateUserDto } from '@/models/dto/user.dto';
|
||||||
import type { Repository } from 'typeorm';
|
import type { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { UserEntity } from '@/models/entity/user.entity';
|
import { UserEntity } from '@/models/entity/user.entity';
|
||||||
@@ -12,7 +13,7 @@ export class UsersService {
|
|||||||
private usersRepository: Repository<UserEntity>,
|
private usersRepository: Repository<UserEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(user: UserEntity): Promise<UserEntity> {
|
async create(user: CreateUserDto): Promise<UserEntity> {
|
||||||
user.password = await bcrypt.hash(user.password, 10); // 密码哈希
|
user.password = await bcrypt.hash(user.password, 10); // 密码哈希
|
||||||
return this.usersRepository.save(user);
|
return this.usersRepository.save(user);
|
||||||
}
|
}
|
||||||
|
5
apps/backend-mock/src/utils/index.ts
Normal file
5
apps/backend-mock/src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sleep };
|
@@ -31,10 +31,10 @@
|
|||||||
"@vben-core/stores": "workspace:*",
|
"@vben-core/stores": "workspace:*",
|
||||||
"@vben/chart-ui": "workspace:*",
|
"@vben/chart-ui": "workspace:*",
|
||||||
"@vben/constants": "workspace:*",
|
"@vben/constants": "workspace:*",
|
||||||
"@vben/hooks": "workspace:*",
|
|
||||||
"@vben/icons": "workspace:*",
|
"@vben/icons": "workspace:*",
|
||||||
"@vben/layouts": "workspace:*",
|
"@vben/layouts": "workspace:*",
|
||||||
"@vben/locales": "workspace:*",
|
"@vben/locales": "workspace:*",
|
||||||
|
"@vben/access": "workspace:*",
|
||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/universal-ui": "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';
|
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 { 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 { openWindow } from '@vben/utils';
|
||||||
import { Notification, UserDropdown } from '@vben/widgets';
|
import { Notification, UserDropdown } from '@vben/widgets';
|
||||||
import { preferences } from '@vben-core/preferences';
|
import { preferences } from '@vben-core/preferences';
|
||||||
|
import { useRequest } from '@vben-core/request';
|
||||||
import { useAccessStore } from '@vben-core/stores';
|
import { useAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
|
import { getUserInfo } from '#/apis';
|
||||||
|
|
||||||
// https://avatar.vercel.sh/vercel.svg?text=Vaa
|
// https://avatar.vercel.sh/vercel.svg?text=Vaa
|
||||||
// https://avatar.vercel.sh/1
|
// https://avatar.vercel.sh/1
|
||||||
// https://avatar.vercel.sh/nextjs
|
// https://avatar.vercel.sh/nextjs
|
||||||
@@ -80,6 +83,14 @@ const menus = computed(() => [
|
|||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { runAsync: runGetUserInfo } = useRequest(getUserInfo, {
|
||||||
|
manual: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
runGetUserInfo().then((userInfo) => {
|
||||||
|
accessStore.setUserInfo(userInfo);
|
||||||
|
});
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
accessStore.$reset();
|
accessStore.$reset();
|
||||||
router.replace('/auth/login');
|
router.replace('/auth/login');
|
||||||
|
@@ -3,16 +3,14 @@ import type { Router } from 'vue-router';
|
|||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { startProgress, stopProgress } from '@vben/utils';
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
import { generatorMenus, generatorRoutes } from '@vben-core/helpers';
|
|
||||||
import { preferences } from '@vben-core/preferences';
|
import { preferences } from '@vben-core/preferences';
|
||||||
import { useAccessStore } from '@vben-core/stores';
|
import { useAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
import { useTitle } from '@vueuse/core';
|
import { useTitle } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { generateAccess } from '#/forward/access';
|
||||||
import { dynamicRoutes, essentialsRouteNames } from '#/router/routes';
|
import { dynamicRoutes, essentialsRouteNames } from '#/router/routes';
|
||||||
|
|
||||||
const forbiddenPage = () => import('#/views/_essential/fallback/forbidden.vue');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用守卫配置
|
* 通用守卫配置
|
||||||
* @param router
|
* @param router
|
||||||
@@ -96,22 +94,16 @@ function setupAccessGuard(router: Router) {
|
|||||||
// 当前登录用户拥有的角色标识列表
|
// 当前登录用户拥有的角色标识列表
|
||||||
const userRoles = accessStore.getUserRoles;
|
const userRoles = accessStore.getUserRoles;
|
||||||
|
|
||||||
const accessibleRoutes = await generatorRoutes(
|
// 生成菜单和路由
|
||||||
dynamicRoutes,
|
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||||
userRoles,
|
roles: userRoles,
|
||||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
router,
|
||||||
// 则会在菜单中显示,但是访问会被重定向到403
|
// 则会在菜单中显示,但是访问会被重定向到403
|
||||||
// 这里可以指定403页面
|
routes: dynamicRoutes,
|
||||||
forbiddenPage,
|
});
|
||||||
);
|
|
||||||
// 动态添加到router实例内
|
|
||||||
accessibleRoutes.forEach((route) => router.addRoute(route));
|
|
||||||
|
|
||||||
// 生成菜单
|
|
||||||
const menus = await generatorMenus(accessibleRoutes, router);
|
|
||||||
|
|
||||||
// 保存菜单信息和路由信息
|
// 保存菜单信息和路由信息
|
||||||
accessStore.setAccessMenus(menus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
const redirectPath = (from.query.redirect ?? to.path) as string;
|
const redirectPath = (from.query.redirect ?? to.path) as string;
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
|
|||||||
hideInTab: true,
|
hideInTab: true,
|
||||||
title: '404',
|
title: '404',
|
||||||
},
|
},
|
||||||
name: 'Fallback',
|
name: 'FallbackNotFound',
|
||||||
path: '/:path(.*)*',
|
path: '/:path(.*)*',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -15,14 +15,108 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
name: 'Demos',
|
name: 'Demos',
|
||||||
path: '/demos',
|
path: '/demos',
|
||||||
redirect: '/demos/fallback/403',
|
redirect: '/demos/access/frontend',
|
||||||
children: [
|
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: {
|
meta: {
|
||||||
icon: 'mdi:lightbulb-error-outline',
|
icon: 'mdi:lightbulb-error-outline',
|
||||||
title: $t('page.demos.fallback.title'),
|
title: $t('page.demos.fallback.title'),
|
||||||
},
|
},
|
||||||
name: 'FallbackLayout',
|
name: 'Fallback',
|
||||||
path: '/fallback',
|
path: '/fallback',
|
||||||
redirect: '/fallback/403',
|
redirect: '/fallback/403',
|
||||||
children: [
|
children: [
|
||||||
|
@@ -2,7 +2,7 @@ import type { InitStoreOptions } from '@vben-core/stores';
|
|||||||
|
|
||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
|
|
||||||
import { initStore, useAccessStore, useTabsStore } from '@vben-core/stores';
|
import { initStore, useAccessStore, useTabbarStore } from '@vben-core/stores';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 初始化pinia
|
* @zh_CN 初始化pinia
|
||||||
@@ -13,4 +13,4 @@ async function setupStore(app: App, options: InitStoreOptions) {
|
|||||||
app.use(pinia);
|
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>
|
@@ -48,8 +48,8 @@
|
|||||||
"eslint-plugin-unicorn": "^54.0.0",
|
"eslint-plugin-unicorn": "^54.0.0",
|
||||||
"eslint-plugin-unused-imports": "^4.0.0",
|
"eslint-plugin-unused-imports": "^4.0.0",
|
||||||
"eslint-plugin-vitest": "^0.5.4",
|
"eslint-plugin-vitest": "^0.5.4",
|
||||||
"eslint-plugin-vue": "^9.26.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"globals": "^15.7.0",
|
"globals": "^15.8.0",
|
||||||
"jsonc-eslint-parser": "^2.4.0",
|
"jsonc-eslint-parser": "^2.4.0",
|
||||||
"vue-eslint-parser": "^9.4.3"
|
"vue-eslint-parser": "^9.4.3"
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
"check:type": "turbo run typecheck",
|
"check:type": "turbo run typecheck",
|
||||||
"clean": "vsh clean",
|
"clean": "vsh clean",
|
||||||
"commit": "czg",
|
"commit": "czg",
|
||||||
"dev": "turbo run dev --parallel",
|
"dev": "cross-env TURBO_UI=1 turbo run dev --parallel",
|
||||||
"docs:dev": "pnpm -F @vben/website run docs:dev",
|
"docs:dev": "pnpm -F @vben/website run docs:dev",
|
||||||
"format": "vsh lint --format",
|
"format": "vsh lint --format",
|
||||||
"lint": "vsh lint",
|
"lint": "vsh lint",
|
||||||
@@ -63,12 +63,12 @@
|
|||||||
"@vben/vsh": "workspace:*",
|
"@vben/vsh": "workspace:*",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cspell": "^8.9.1",
|
"cspell": "^8.10.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
"rimraf": "^5.0.7",
|
"rimraf": "^5.0.7",
|
||||||
"taze": "^0.14.0",
|
"taze": "^0.14.1",
|
||||||
"turbo": "^2.0.6",
|
"turbo": "^2.0.6",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"unbuild": "^2.0.0",
|
"unbuild": "^2.0.0",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"packageManager": "pnpm@9.4.0",
|
"packageManager": "pnpm@9.4.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@ant-design/colors": "^7.0.2",
|
"@ant-design/colors": "^7.1.0",
|
||||||
"@ctrl/tinycolor": "^4.1.0",
|
"@ctrl/tinycolor": "^4.1.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"vue": "^3.4.31"
|
"vue": "^3.4.31"
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
export * from './find-menu-by-path';
|
export * from './find-menu-by-path';
|
||||||
export * from './flatten-object';
|
export * from './flatten-object';
|
||||||
export * from './generator-menus';
|
|
||||||
export * from './generator-routes';
|
|
||||||
export * from './merge-route-modules';
|
export * from './merge-route-modules';
|
||||||
export * from './nested-object';
|
export * from './nested-object';
|
||||||
|
@@ -2,6 +2,7 @@ import type { Preferences } from './types';
|
|||||||
|
|
||||||
const defaultPreferences: Preferences = {
|
const defaultPreferences: Preferences = {
|
||||||
app: {
|
app: {
|
||||||
|
accessMode: 'frontend',
|
||||||
aiAssistant: true,
|
aiAssistant: true,
|
||||||
authPageLayout: 'panel-right',
|
authPageLayout: 'panel-right',
|
||||||
colorGrayMode: false,
|
colorGrayMode: false,
|
||||||
|
@@ -9,6 +9,8 @@ import type {
|
|||||||
|
|
||||||
type BreadcrumbStyleType = 'background' | 'normal';
|
type BreadcrumbStyleType = 'background' | 'normal';
|
||||||
|
|
||||||
|
type accessModeType = 'allow-all' | 'backend' | 'frontend';
|
||||||
|
|
||||||
type NavigationStyleType = 'plain' | 'rounded';
|
type NavigationStyleType = 'plain' | 'rounded';
|
||||||
|
|
||||||
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
|
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
|
||||||
@@ -16,6 +18,8 @@ type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
|
|||||||
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
|
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
|
||||||
|
|
||||||
interface AppPreferences {
|
interface AppPreferences {
|
||||||
|
/** 权限模式 */
|
||||||
|
accessMode: accessModeType;
|
||||||
/** 是否开启vben助手 */
|
/** 是否开启vben助手 */
|
||||||
aiAssistant: boolean;
|
aiAssistant: boolean;
|
||||||
/** 登录注册页面布局 */
|
/** 登录注册页面布局 */
|
||||||
@@ -208,4 +212,5 @@ export type {
|
|||||||
ThemeModeType,
|
ThemeModeType,
|
||||||
ThemePreferences,
|
ThemePreferences,
|
||||||
TransitionPreferences,
|
TransitionPreferences,
|
||||||
|
accessModeType,
|
||||||
};
|
};
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
export * from './access';
|
export * from './access';
|
||||||
export * from './tabs';
|
export * from './tabbar';
|
||||||
|
@@ -3,7 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router';
|
|||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { useTabsStore } from './tabs';
|
import { useTabbarStore } from './tabbar';
|
||||||
|
|
||||||
describe('useAccessStore', () => {
|
describe('useAccessStore', () => {
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -18,7 +18,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds a new tab', () => {
|
it('adds a new tab', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const tab: any = {
|
const tab: any = {
|
||||||
fullPath: '/home',
|
fullPath: '/home',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -31,7 +31,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds a new tab if it does not exist', () => {
|
it('adds a new tab if it does not exist', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const newTab: any = {
|
const newTab: any = {
|
||||||
fullPath: '/new',
|
fullPath: '/new',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -43,7 +43,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates an existing tab instead of adding a new one', () => {
|
it('updates an existing tab instead of adding a new one', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const initialTab: any = {
|
const initialTab: any = {
|
||||||
fullPath: '/existing',
|
fullPath: '/existing',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -59,7 +59,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes all tabs', async () => {
|
it('closes all tabs', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
store.tabs = [
|
store.tabs = [
|
||||||
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
|
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
|
||||||
] as any;
|
] as any;
|
||||||
@@ -72,7 +72,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns all tabs including affix tabs', () => {
|
it('returns all tabs including affix tabs', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
store.tabs = [
|
store.tabs = [
|
||||||
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
|
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
|
||||||
] as any;
|
] as any;
|
||||||
@@ -86,7 +86,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes a non-affix tab', () => {
|
it('closes a non-affix tab', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const tab: any = {
|
const tab: any = {
|
||||||
fullPath: '/closable',
|
fullPath: '/closable',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -99,7 +99,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not close an affix tab', () => {
|
it('does not close an affix tab', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const affixTab: any = {
|
const affixTab: any = {
|
||||||
fullPath: '/affix',
|
fullPath: '/affix',
|
||||||
meta: { affixTab: true },
|
meta: { affixTab: true },
|
||||||
@@ -112,14 +112,14 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns all cache tabs', () => {
|
it('returns all cache tabs', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
store.cacheTabs.add('Home');
|
store.cacheTabs.add('Home');
|
||||||
store.cacheTabs.add('About');
|
store.cacheTabs.add('About');
|
||||||
expect(store.getCacheTabs).toEqual(['Home', 'About']);
|
expect(store.getCacheTabs).toEqual(['Home', 'About']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns all tabs, including affix tabs', () => {
|
it('returns all tabs, including affix tabs', () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const normalTab: any = {
|
const normalTab: any = {
|
||||||
fullPath: '/normal',
|
fullPath: '/normal',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -139,7 +139,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to a specific tab', async () => {
|
it('navigates to a specific tab', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' };
|
const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' };
|
||||||
|
|
||||||
await store._goToTab(tab, router);
|
await store._goToTab(tab, router);
|
||||||
@@ -152,7 +152,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes multiple tabs by paths', async () => {
|
it('closes multiple tabs by paths', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
store.addTab({
|
store.addTab({
|
||||||
fullPath: '/home',
|
fullPath: '/home',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -179,7 +179,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes all tabs to the left of the specified tab', async () => {
|
it('closes all tabs to the left of the specified tab', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
store.addTab({
|
store.addTab({
|
||||||
fullPath: '/home',
|
fullPath: '/home',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -207,7 +207,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes all tabs except the specified tab', async () => {
|
it('closes all tabs except the specified tab', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
store.addTab({
|
store.addTab({
|
||||||
fullPath: '/home',
|
fullPath: '/home',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -235,7 +235,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes all tabs to the right of the specified tab', async () => {
|
it('closes all tabs to the right of the specified tab', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const targetTab: any = {
|
const targetTab: any = {
|
||||||
fullPath: '/home',
|
fullPath: '/home',
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -263,7 +263,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes the tab with the specified key', async () => {
|
it('closes the tab with the specified key', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const keyToClose = '/about';
|
const keyToClose = '/about';
|
||||||
store.addTab({
|
store.addTab({
|
||||||
fullPath: '/home',
|
fullPath: '/home',
|
||||||
@@ -293,7 +293,7 @@ describe('useAccessStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('refreshes the current tab', async () => {
|
it('refreshes the current tab', async () => {
|
||||||
const store = useTabsStore();
|
const store = useTabbarStore();
|
||||||
const currentTab: any = {
|
const currentTab: any = {
|
||||||
fullPath: '/dashboard',
|
fullPath: '/dashboard',
|
||||||
meta: { name: 'Dashboard' },
|
meta: { name: 'Dashboard' },
|
@@ -62,7 +62,7 @@ interface TabsState {
|
|||||||
/**
|
/**
|
||||||
* @zh_CN 访问权限相关
|
* @zh_CN 访问权限相关
|
||||||
*/
|
*/
|
||||||
const useTabsStore = defineStore('tabs', {
|
const useTabbarStore = defineStore('tabbar', {
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
* Close tabs in bulk
|
* Close tabs in bulk
|
||||||
@@ -395,7 +395,7 @@ const useTabsStore = defineStore('tabs', {
|
|||||||
// 解决热更新问题
|
// 解决热更新问题
|
||||||
const hot = import.meta.hot;
|
const hot = import.meta.hot;
|
||||||
if (hot) {
|
if (hot) {
|
||||||
hot.accept(acceptHMRUpdate(useTabsStore, hot));
|
hot.accept(acceptHMRUpdate(useTabbarStore, hot));
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useTabsStore };
|
export { useTabbarStore };
|
@@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^7.0.2",
|
"@ant-design/colors": "^7.1.0",
|
||||||
"@ctrl/tinycolor": "4.1.0"
|
"@ctrl/tinycolor": "4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -36,4 +36,8 @@
|
|||||||
.outline-box:not(.outline-box-active):hover::after {
|
.outline-box:not(.outline-box-active):hover::after {
|
||||||
@apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
|
@apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-box {
|
||||||
|
@apply bg-card text-card-foreground border-border rounded-xl border shadow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { generateUUID } from './hash';
|
|
||||||
|
|
||||||
describe('generateUUID', () => {
|
|
||||||
it('should return a string', () => {
|
|
||||||
const uuid = generateUUID();
|
|
||||||
expect(typeof uuid).toBe('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be length 32', () => {
|
|
||||||
const uuid = generateUUID();
|
|
||||||
expect(uuid.length).toBe(36);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have the correct format', () => {
|
|
||||||
const uuid = generateUUID();
|
|
||||||
const uuidRegex =
|
|
||||||
/^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i;
|
|
||||||
expect(uuidRegex.test(uuid)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* 生成一个UUID(通用唯一标识符)。
|
|
||||||
*
|
|
||||||
* UUID是一种用于软件构建的标识符,其目的是能够生成一个唯一的ID,以便在全局范围内标识信息。
|
|
||||||
* 此函数用于生成一个符合version 4的UUID,这种UUID是随机生成的。
|
|
||||||
*
|
|
||||||
* 生成的UUID的格式为:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
||||||
* 其中,x是任意16进制数字,y是一个16进制数字,取值范围为[8, b]。
|
|
||||||
*
|
|
||||||
* @returns {string} 生成的UUID。
|
|
||||||
*/
|
|
||||||
function generateUUID(): string {
|
|
||||||
let d = Date.now();
|
|
||||||
if (
|
|
||||||
typeof performance !== 'undefined' &&
|
|
||||||
typeof performance.now === 'function'
|
|
||||||
) {
|
|
||||||
d += performance.now(); // use high-precision timer if available
|
|
||||||
}
|
|
||||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll(
|
|
||||||
/[xy]/g,
|
|
||||||
(c) => {
|
|
||||||
const r = Math.trunc((d + Math.random() * 16) % 16);
|
|
||||||
d = Math.floor(d / 16);
|
|
||||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { generateUUID };
|
|
@@ -1,7 +1,6 @@
|
|||||||
export * from './cn';
|
export * from './cn';
|
||||||
export * from './diff';
|
export * from './diff';
|
||||||
export * from './dom';
|
export * from './dom';
|
||||||
export * from './hash';
|
|
||||||
export * from './inference';
|
export * from './inference';
|
||||||
export * from './letter';
|
export * from './letter';
|
||||||
export * from './merge';
|
export * from './merge';
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/hooks",
|
"name": "@vben/access",
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
"directory": "packages/hooks"
|
"directory": "packages/business/permissions"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm unbuild",
|
"build": "pnpm vite build",
|
||||||
"stub": "pnpm unbuild --stub"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
@@ -32,12 +32,19 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"default": "./dist/index.mjs"
|
"default": "./dist/index.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.31"
|
"@vben-core/preferences": "workspace:*",
|
||||||
|
"@vben-core/stores": "workspace:*",
|
||||||
|
"@vben-core/toolkit": "workspace:*",
|
||||||
|
"@vben/locales": "workspace:*",
|
||||||
|
"vue": "^3.4.31",
|
||||||
|
"vue-router": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vben/types": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
1
packages/business/access/postcss.config.mjs
Normal file
1
packages/business/access/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config/postcss';
|
26
packages/business/access/src/authority.vue
Normal file
26
packages/business/access/src/authority.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!--
|
||||||
|
Access control component for fine-grained access control.
|
||||||
|
-->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Specified role is visible
|
||||||
|
* - When the permission mode is 'frontend', the value can be a role value.
|
||||||
|
* - When the permission mode is 'backend', the value can be a code permission value.
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
value?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Authority',
|
||||||
|
});
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot></slot>
|
||||||
|
</template>
|
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { generatorMenus } from './generator-menus'; // 替换为您的实际路径
|
import { generateMenus } from './generate-menus'; // 替换为您的实际路径
|
||||||
import {
|
import {
|
||||||
type RouteRecordRaw,
|
type RouteRecordRaw,
|
||||||
type Router,
|
type Router,
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
|
|
||||||
// Nested route setup to test child inclusion and hideChildrenInMenu functionality
|
// Nested route setup to test child inclusion and hideChildrenInMenu functionality
|
||||||
|
|
||||||
describe('generatorMenus', () => {
|
describe('generateMenus', () => {
|
||||||
// 模拟路由数据
|
// 模拟路由数据
|
||||||
const mockRoutes = [
|
const mockRoutes = [
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ describe('generatorMenus', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const menus = await generatorMenus(mockRoutes, mockRouter as any);
|
const menus = await generateMenus(mockRoutes, mockRouter as any);
|
||||||
expect(menus).toEqual(expectedMenus);
|
expect(menus).toEqual(expectedMenus);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ describe('generatorMenus', () => {
|
|||||||
},
|
},
|
||||||
] as RouteRecordRaw[];
|
] as RouteRecordRaw[];
|
||||||
|
|
||||||
const menus = await generatorMenus(mockRoutesWithMeta, mockRouter as any);
|
const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any);
|
||||||
expect(menus).toEqual([
|
expect(menus).toEqual([
|
||||||
{
|
{
|
||||||
badge: undefined,
|
badge: undefined,
|
||||||
@@ -108,7 +108,7 @@ describe('generatorMenus', () => {
|
|||||||
},
|
},
|
||||||
] as RouteRecordRaw[];
|
] as RouteRecordRaw[];
|
||||||
|
|
||||||
const menus = await generatorMenus(mockRoutesWithParams, mockRouter as any);
|
const menus = await generateMenus(mockRoutesWithParams, mockRouter as any);
|
||||||
expect(menus).toEqual([
|
expect(menus).toEqual([
|
||||||
{
|
{
|
||||||
badge: undefined,
|
badge: undefined,
|
||||||
@@ -139,12 +139,12 @@ describe('generatorMenus', () => {
|
|||||||
},
|
},
|
||||||
] as RouteRecordRaw[];
|
] as RouteRecordRaw[];
|
||||||
|
|
||||||
const menus = await generatorMenus(
|
const menus = await generateMenus(
|
||||||
mockRoutesWithRedirect,
|
mockRoutesWithRedirect,
|
||||||
mockRouter as any,
|
mockRouter as any,
|
||||||
);
|
);
|
||||||
expect(menus).toEqual([
|
expect(menus).toEqual([
|
||||||
// Assuming your generatorMenus function excludes redirect routes from the menu
|
// Assuming your generateMenus function excludes redirect routes from the menu
|
||||||
{
|
{
|
||||||
badge: undefined,
|
badge: undefined,
|
||||||
badgeType: undefined,
|
badgeType: undefined,
|
||||||
@@ -191,7 +191,7 @@ describe('generatorMenus', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should generate menu list with correct order', async () => {
|
it('should generate menu list with correct order', async () => {
|
||||||
const menus = await generatorMenus(routes, router);
|
const menus = await generateMenus(routes, router);
|
||||||
const expectedMenus = [
|
const expectedMenus = [
|
||||||
{
|
{
|
||||||
badge: undefined,
|
badge: undefined,
|
||||||
@@ -224,7 +224,7 @@ describe('generatorMenus', () => {
|
|||||||
|
|
||||||
it('should handle empty routes', async () => {
|
it('should handle empty routes', async () => {
|
||||||
const emptyRoutes: any[] = [];
|
const emptyRoutes: any[] = [];
|
||||||
const menus = await generatorMenus(emptyRoutes, router);
|
const menus = await generateMenus(emptyRoutes, router);
|
||||||
expect(menus).toEqual([]);
|
expect(menus).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
@@ -1,4 +1,4 @@
|
|||||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben/types';
|
||||||
import type { RouteRecordRaw, Router } from 'vue-router';
|
import type { RouteRecordRaw, Router } from 'vue-router';
|
||||||
|
|
||||||
import { mapTree } from '@vben-core/toolkit';
|
import { mapTree } from '@vben-core/toolkit';
|
||||||
@@ -7,7 +7,7 @@ import { mapTree } from '@vben-core/toolkit';
|
|||||||
* 根据 routes 生成菜单列表
|
* 根据 routes 生成菜单列表
|
||||||
* @param routes
|
* @param routes
|
||||||
*/
|
*/
|
||||||
async function generatorMenus(
|
async function generateMenus(
|
||||||
routes: RouteRecordRaw[],
|
routes: RouteRecordRaw[],
|
||||||
router: Router,
|
router: Router,
|
||||||
): Promise<MenuRecordRaw[]> {
|
): Promise<MenuRecordRaw[]> {
|
||||||
@@ -70,4 +70,4 @@ async function generatorMenus(
|
|||||||
return menus;
|
return menus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { generatorMenus };
|
export { generateMenus };
|
@@ -0,0 +1,87 @@
|
|||||||
|
import type {
|
||||||
|
ComponentRecordType,
|
||||||
|
RouteRecordStringComponent,
|
||||||
|
} from '@vben/types';
|
||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import type { GeneratorMenuAndRoutesOptions } from '../types';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { mapTree } from '@vben-core/toolkit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态生成路由 - 后端方式
|
||||||
|
*/
|
||||||
|
async function generateRoutesByBackend(
|
||||||
|
options: GeneratorMenuAndRoutesOptions,
|
||||||
|
): Promise<RouteRecordRaw[]> {
|
||||||
|
const { fetchMenuListAsync, layoutMap, pageMap } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const menuRoutes = await fetchMenuListAsync?.();
|
||||||
|
if (!menuRoutes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizePageMap: ComponentRecordType = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(pageMap)) {
|
||||||
|
normalizePageMap[normalizeViewPath(key)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
|
||||||
|
return routes;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertRoutes(
|
||||||
|
routes: RouteRecordStringComponent[],
|
||||||
|
layoutMap: ComponentRecordType,
|
||||||
|
pageMap: ComponentRecordType,
|
||||||
|
): RouteRecordRaw[] {
|
||||||
|
return mapTree(routes, (node) => {
|
||||||
|
const route = node as unknown as RouteRecordRaw;
|
||||||
|
const { component, name } = node;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
console.error('route name is required', route);
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout转换
|
||||||
|
if (component && layoutMap[component]) {
|
||||||
|
route.component = layoutMap[component];
|
||||||
|
// 页面组件转换
|
||||||
|
} else if (component) {
|
||||||
|
const normalizePath = normalizeViewPath(component);
|
||||||
|
route.component =
|
||||||
|
pageMap[
|
||||||
|
normalizePath.endsWith('.vue')
|
||||||
|
? normalizePath
|
||||||
|
: `${normalizePath}.vue`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 国际化转化
|
||||||
|
if (route.meta?.title) {
|
||||||
|
route.meta.title = $t(route.meta.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return route;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeViewPath(path: string): string {
|
||||||
|
// 去除相对路径前缀
|
||||||
|
const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, '');
|
||||||
|
|
||||||
|
// 确保路径以 '/' 开头
|
||||||
|
const viewPath = normalizedPath.startsWith('/')
|
||||||
|
? normalizedPath
|
||||||
|
: `/${normalizedPath}`;
|
||||||
|
|
||||||
|
return viewPath.replace(/^\/views/, '');
|
||||||
|
}
|
||||||
|
export { generateRoutesByBackend };
|
@@ -2,7 +2,11 @@ import type { RouteRecordRaw } from 'vue-router';
|
|||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { generatorRoutes, hasAuthority, hasVisible } from './generator-routes';
|
import {
|
||||||
|
generateRoutesByFrontend,
|
||||||
|
hasAuthority,
|
||||||
|
hasVisible,
|
||||||
|
} from './generate-routes-frontend';
|
||||||
|
|
||||||
// Mock 路由数据
|
// Mock 路由数据
|
||||||
const mockRoutes = [
|
const mockRoutes = [
|
||||||
@@ -58,9 +62,11 @@ describe('hasVisible', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generatorRoutes', () => {
|
describe('generateRoutesByFrontend', () => {
|
||||||
it('should filter routes based on authority and visibility', async () => {
|
it('should filter routes based on authority and visibility', async () => {
|
||||||
const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
|
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
|
||||||
|
'user',
|
||||||
|
]);
|
||||||
// The user should have access to /dashboard/stats, but it should be filtered out because it's not visible
|
// The user should have access to /dashboard/stats, but it should be filtered out because it's not visible
|
||||||
expect(generatedRoutes).toEqual([
|
expect(generatedRoutes).toEqual([
|
||||||
{
|
{
|
||||||
@@ -77,7 +83,9 @@ describe('generatorRoutes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle routes without children', async () => {
|
it('should handle routes without children', async () => {
|
||||||
const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
|
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
|
||||||
|
'user',
|
||||||
|
]);
|
||||||
expect(generatedRoutes).toEqual(
|
expect(generatedRoutes).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -88,7 +96,7 @@ describe('generatorRoutes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty roles array', async () => {
|
it('should handle empty roles array', async () => {
|
||||||
const generatedRoutes = await generatorRoutes(mockRoutes, []);
|
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, []);
|
||||||
expect(generatedRoutes).toEqual(
|
expect(generatedRoutes).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
// Only routes without authority should be included
|
// Only routes without authority should be included
|
||||||
@@ -115,7 +123,7 @@ describe('generatorRoutes', () => {
|
|||||||
{ meta: {}, path: '/path2' }, // Empty meta
|
{ meta: {}, path: '/path2' }, // Empty meta
|
||||||
{ meta: { authority: ['admin'] }, path: '/path3' }, // Only authority
|
{ meta: { authority: ['admin'] }, path: '/path3' }, // Only authority
|
||||||
];
|
];
|
||||||
const generatedRoutes = await generatorRoutes(
|
const generatedRoutes = await generateRoutesByFrontend(
|
||||||
routesWithMissingMeta as RouteRecordRaw[],
|
routesWithMissingMeta as RouteRecordRaw[],
|
||||||
['admin'],
|
['admin'],
|
||||||
);
|
);
|
@@ -2,26 +2,26 @@ import type { RouteRecordRaw } from 'vue-router';
|
|||||||
|
|
||||||
import { filterTree, mapTree } from '@vben-core/toolkit';
|
import { filterTree, mapTree } from '@vben-core/toolkit';
|
||||||
/**
|
/**
|
||||||
* 动态生成路由
|
* 动态生成路由 - 前端方式
|
||||||
*/
|
*/
|
||||||
async function generatorRoutes(
|
async function generateRoutesByFrontend(
|
||||||
routes: RouteRecordRaw[],
|
routes: RouteRecordRaw[],
|
||||||
roles: string[],
|
roles: string[],
|
||||||
forbiddenPage?: RouteRecordRaw['component'],
|
forbiddenComponent?: RouteRecordRaw['component'],
|
||||||
): Promise<RouteRecordRaw[]> {
|
): Promise<RouteRecordRaw[]> {
|
||||||
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
||||||
const finalRoutes = filterTree(routes, (route) => {
|
const finalRoutes = filterTree(routes, (route) => {
|
||||||
return hasVisible(route) && hasAuthority(route, roles);
|
return hasVisible(route) && hasAuthority(route, roles);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!forbiddenPage) {
|
if (!forbiddenComponent) {
|
||||||
return finalRoutes;
|
return finalRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有禁止访问的页面,将禁止访问的页面替换为403页面
|
// 如果有禁止访问的页面,将禁止访问的页面替换为403页面
|
||||||
return mapTree(finalRoutes, (route) => {
|
return mapTree(finalRoutes, (route) => {
|
||||||
if (menuHasVisibleWithForbidden(route)) {
|
if (menuHasVisibleWithForbidden(route)) {
|
||||||
route.component = forbiddenPage;
|
route.component = forbiddenComponent;
|
||||||
}
|
}
|
||||||
return route;
|
return route;
|
||||||
});
|
});
|
||||||
@@ -60,4 +60,4 @@ function menuHasVisibleWithForbidden(route: RouteRecordRaw) {
|
|||||||
return !!route.meta?.menuVisibleWithForbidden;
|
return !!route.meta?.menuVisibleWithForbidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { generatorRoutes, hasAuthority, hasVisible };
|
export { generateRoutesByFrontend, hasAuthority, hasVisible };
|
@@ -0,0 +1,76 @@
|
|||||||
|
import type { accessModeType } from '@vben-core/preferences';
|
||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import type { GeneratorMenuAndRoutesOptions } from '../types';
|
||||||
|
|
||||||
|
import { generateMenus } from './generate-menus';
|
||||||
|
import { generateRoutesByBackend } from './generate-routes-backend';
|
||||||
|
import { generateRoutesByFrontend } from './generate-routes-frontend';
|
||||||
|
|
||||||
|
async function generateMenusAndRoutes(
|
||||||
|
mode: accessModeType,
|
||||||
|
options: GeneratorMenuAndRoutesOptions,
|
||||||
|
) {
|
||||||
|
const { router } = options;
|
||||||
|
// 生成路由
|
||||||
|
const accessibleRoutes = await generateRoutes(mode, options);
|
||||||
|
|
||||||
|
// 动态添加到router实例内
|
||||||
|
accessibleRoutes.forEach((route) => router.addRoute(route));
|
||||||
|
|
||||||
|
// 生成菜单
|
||||||
|
const accessibleMenus = await generateMenus1(mode, accessibleRoutes, options);
|
||||||
|
|
||||||
|
return { accessibleMenus, accessibleRoutes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate routes
|
||||||
|
* @param mode
|
||||||
|
*/
|
||||||
|
async function generateRoutes(
|
||||||
|
mode: accessModeType,
|
||||||
|
options: GeneratorMenuAndRoutesOptions,
|
||||||
|
) {
|
||||||
|
const { forbiddenComponent, roles, routes } = options;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
// 允许所有路由访问,不做任何过滤处理
|
||||||
|
case 'allow-all': {
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
case 'frontend': {
|
||||||
|
return await generateRoutesByFrontend(
|
||||||
|
routes,
|
||||||
|
roles || [],
|
||||||
|
forbiddenComponent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'backend': {
|
||||||
|
return await generateRoutesByBackend(options);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateMenus1(
|
||||||
|
mode: accessModeType,
|
||||||
|
routes: RouteRecordRaw[],
|
||||||
|
options: GeneratorMenuAndRoutesOptions,
|
||||||
|
) {
|
||||||
|
const { router } = options;
|
||||||
|
switch (mode) {
|
||||||
|
case 'allow-all':
|
||||||
|
case 'frontend':
|
||||||
|
case 'backend': {
|
||||||
|
return await generateMenus(routes, router);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generateMenusAndRoutes };
|
4
packages/business/access/src/index.ts
Normal file
4
packages/business/access/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as Authority } from './authority.vue';
|
||||||
|
export * from './generate-menu-and-routes';
|
||||||
|
export type * from './types';
|
||||||
|
export * from './use-access';
|
17
packages/business/access/src/types.ts
Normal file
17
packages/business/access/src/types.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type {
|
||||||
|
ComponentRecordType,
|
||||||
|
RouteRecordStringComponent,
|
||||||
|
} from '@vben/types';
|
||||||
|
import type { RouteRecordRaw, Router } from 'vue-router';
|
||||||
|
|
||||||
|
interface GeneratorMenuAndRoutesOptions {
|
||||||
|
fetchMenuListAsync?: () => Promise<RouteRecordStringComponent[]>;
|
||||||
|
forbiddenComponent?: RouteRecordRaw['component'];
|
||||||
|
layoutMap?: ComponentRecordType;
|
||||||
|
pageMap?: ComponentRecordType;
|
||||||
|
roles?: string[];
|
||||||
|
router: Router;
|
||||||
|
routes: RouteRecordRaw[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { GeneratorMenuAndRoutesOptions };
|
28
packages/business/access/src/use-access.ts
Normal file
28
packages/business/access/src/use-access.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { preferences } from '@vben-core/preferences';
|
||||||
|
import { useAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
|
function useAccess() {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const currentAccessMode = computed(() => {
|
||||||
|
return preferences.app.accessMode;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更改账号角色
|
||||||
|
* @param roles
|
||||||
|
*/
|
||||||
|
async function changeRoles(roles: string[]): Promise<void> {
|
||||||
|
if (preferences.app.accessMode !== 'frontend') {
|
||||||
|
throw new Error(
|
||||||
|
'The current access mode is not frontend, so the role cannot be changed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
accessStore.setUserRoles(roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { changeRoles, currentAccessMode };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useAccess };
|
1
packages/business/access/tailwind.config.mjs
Normal file
1
packages/business/access/tailwind.config.mjs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config';
|
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"extends": "@vben/tsconfig/library.json",
|
"extends": "@vben/tsconfig/web.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["@vben/types/global"]
|
||||||
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
3
packages/business/access/vite.config.mts
Normal file
3
packages/business/access/vite.config.mts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineConfig } from '@vben/vite-config';
|
||||||
|
|
||||||
|
export default defineConfig();
|
@@ -5,7 +5,7 @@ import type EchartsUI from './echarts-ui.vue';
|
|||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { computed, nextTick, watch } from 'vue';
|
import { computed, nextTick, watch } from 'vue';
|
||||||
|
|
||||||
import { usePreferences } from '@vben-core/preferences';
|
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
tryOnUnmounted,
|
tryOnUnmounted,
|
||||||
@@ -91,9 +91,24 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
|
|||||||
chartInstance.dispose();
|
chartInstance.dispose();
|
||||||
initCharts();
|
initCharts();
|
||||||
renderEcharts(cacheOptions);
|
renderEcharts(cacheOptions);
|
||||||
|
resize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => preferences.sidebar.collapsed,
|
||||||
|
() => preferences.sidebar.extraCollapse,
|
||||||
|
() => preferences.sidebar.hidden,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
// 折叠动画200ms
|
||||||
|
setTimeout(() => {
|
||||||
|
resize();
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tryOnUnmounted(() => {
|
tryOnUnmounted(() => {
|
||||||
// 销毁实例,释放资源
|
// 销毁实例,释放资源
|
||||||
chartInstance?.dispose();
|
chartInstance?.dispose();
|
||||||
|
@@ -3,14 +3,14 @@ import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
|||||||
|
|
||||||
import { preferences, usePreferences } from '@vben-core/preferences';
|
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||||
import { Spinner } from '@vben-core/shadcn-ui';
|
import { Spinner } from '@vben-core/shadcn-ui';
|
||||||
import { storeToRefs, useTabsStore } from '@vben-core/stores';
|
import { storeToRefs, useTabbarStore } from '@vben-core/stores';
|
||||||
|
|
||||||
import { IFrameRouterView } from '../../iframe';
|
import { IFrameRouterView } from '../../iframe';
|
||||||
import { useContentSpinner } from './use-content-spinner';
|
import { useContentSpinner } from './use-content-spinner';
|
||||||
|
|
||||||
defineOptions({ name: 'LayoutContent' });
|
defineOptions({ name: 'LayoutContent' });
|
||||||
|
|
||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabbarStore();
|
||||||
const { keepAlive } = usePreferences();
|
const { keepAlive } = usePreferences();
|
||||||
const { spinning } = useContentSpinner();
|
const { spinning } = useContentSpinner();
|
||||||
|
|
||||||
|
@@ -19,14 +19,14 @@ import {
|
|||||||
MdiPin,
|
MdiPin,
|
||||||
MdiPinOff,
|
MdiPinOff,
|
||||||
} from '@vben-core/iconify';
|
} from '@vben-core/iconify';
|
||||||
import { storeToRefs, useAccessStore, useTabsStore } from '@vben-core/stores';
|
import { storeToRefs, useAccessStore, useTabbarStore } from '@vben-core/stores';
|
||||||
import { filterTree } from '@vben-core/toolkit';
|
import { filterTree } from '@vben-core/toolkit';
|
||||||
|
|
||||||
function useTabs() {
|
function useTabs() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabbarStore();
|
||||||
const { accessMenus } = storeToRefs(accessStore);
|
const { accessMenus } = storeToRefs(accessStore);
|
||||||
|
|
||||||
const currentActive = computed(() => {
|
const currentActive = computed(() => {
|
||||||
|
@@ -6,12 +6,12 @@ import { useRoute } from 'vue-router';
|
|||||||
|
|
||||||
import { preferences } from '@vben-core/preferences';
|
import { preferences } from '@vben-core/preferences';
|
||||||
import { Spinner } from '@vben-core/shadcn-ui';
|
import { Spinner } from '@vben-core/shadcn-ui';
|
||||||
import { useTabsStore } from '@vben-core/stores';
|
import { useTabbarStore } from '@vben-core/stores';
|
||||||
|
|
||||||
defineOptions({ name: 'IFrameRouterView' });
|
defineOptions({ name: 'IFrameRouterView' });
|
||||||
|
|
||||||
const spinningList = ref<boolean[]>([]);
|
const spinningList = ref<boolean[]>([]);
|
||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabbarStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const enableTabbar = computed(() => preferences.tabbar.enable);
|
const enableTabbar = computed(() => preferences.tabbar.enable);
|
||||||
|
@@ -107,7 +107,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="m-5">
|
<div class="m-5">
|
||||||
<div class="bg-card border-border rounded-md border p-5 shadow">
|
<div class="card-box p-5">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-foreground text-2xl font-semibold leading-7">
|
<h3 class="text-foreground text-2xl font-semibold leading-7">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
@@ -135,7 +135,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-card border-border mt-6 rounded-md border p-5">
|
<div class="card-box mt-6 p-5">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="text-foreground text-lg">生产环境依赖</h5>
|
<h5 class="text-foreground text-lg">生产环境依赖</h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +154,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-card border-border mt-6 rounded-md border p-5">
|
<div class="card-box mt-6 p-5">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="text-foreground text-lg">开发环境依赖</h5>
|
<h5 class="text-foreground text-lg">开发环境依赖</h5>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -23,9 +23,7 @@ const defaultValue = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="card-box w-full px-4 pb-5 pt-3 shadow">
|
||||||
class="bg-card border-border w-full rounded-xl border px-4 pb-5 pt-3 shadow"
|
|
||||||
>
|
|
||||||
<Tabs :default-value="defaultValue">
|
<Tabs :default-value="defaultValue">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<template v-for="tab in tabs" :key="tab.label">
|
<template v-for="tab in tabs" :key="tab.label">
|
||||||
|
@@ -14,7 +14,7 @@ withDefaults(defineProps<Props>(), {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-card border-border rounded-xl p-4 py-6 shadow lg:flex">
|
<div class="card-box p-4 py-6 lg:flex">
|
||||||
<VbenAvatar :src="avatar" class="size-20" />
|
<VbenAvatar :src="avatar" class="size-20" />
|
||||||
<div
|
<div
|
||||||
v-if="$slots.title || $slots.description"
|
v-if="$slots.title || $slots.description"
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
import { defineBuildConfig } from 'unbuild';
|
|
||||||
|
|
||||||
export default defineBuildConfig({
|
|
||||||
clean: true,
|
|
||||||
declaration: true,
|
|
||||||
entries: ['src/index'],
|
|
||||||
});
|
|
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
@@ -1,6 +1,16 @@
|
|||||||
page:
|
page:
|
||||||
demos:
|
demos:
|
||||||
title: Demos
|
title: Demos
|
||||||
|
access:
|
||||||
|
title: Access Control
|
||||||
|
frontend-control: Front-end Control
|
||||||
|
# menuVisibleWithForbidden: Menu is visible
|
||||||
|
backend-control: Backend Control
|
||||||
|
page: Page visit
|
||||||
|
button: Button control
|
||||||
|
loading-menu: In the loading menu
|
||||||
|
access-test-1: Access test page 1
|
||||||
|
access-test-2: Access test page 2
|
||||||
nested:
|
nested:
|
||||||
title: Nested Menu
|
title: Nested Menu
|
||||||
menu1: Menu 1
|
menu1: Menu 1
|
||||||
|
@@ -1,6 +1,16 @@
|
|||||||
page:
|
page:
|
||||||
demos:
|
demos:
|
||||||
title: 演示
|
title: 演示
|
||||||
|
access:
|
||||||
|
title: 访问控制
|
||||||
|
frontend-control: 前端控制
|
||||||
|
# menuVisibleWithForbidden: 菜单可见
|
||||||
|
backend-control: 后端控制
|
||||||
|
page: 页面访问
|
||||||
|
button: 按钮控制
|
||||||
|
access-test-1: 权限测试页1
|
||||||
|
access-test-2: 权限测试页2
|
||||||
|
|
||||||
nested:
|
nested:
|
||||||
title: 嵌套菜单
|
title: 嵌套菜单
|
||||||
menu1: 菜单 1
|
menu1: 菜单 1
|
||||||
@@ -40,6 +50,7 @@ common:
|
|||||||
confirm: 确认
|
confirm: 确认
|
||||||
not-data: 暂无数据
|
not-data: 暂无数据
|
||||||
refresh: 刷新
|
refresh: 刷新
|
||||||
|
loading-menu: 加载菜单中
|
||||||
|
|
||||||
fallback:
|
fallback:
|
||||||
page-not-found: 哎呀!未找到页面
|
page-not-found: 哎呀!未找到页面
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
export type * from './router';
|
||||||
export type * from './ui';
|
export type * from './ui';
|
||||||
export type * from './user';
|
export type * from './user';
|
||||||
export type * from '@vben-core/typings';
|
export type * from '@vben-core/typings';
|
||||||
|
13
packages/types/src/router.ts
Normal file
13
packages/types/src/router.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
// 定义递归类型以将 RouteRecordRaw 的 component 属性更改为 string
|
||||||
|
type RouteRecordStringComponent<T = string> = {
|
||||||
|
children?: RouteRecordStringComponent<T>[];
|
||||||
|
component: T;
|
||||||
|
} & Omit<RouteRecordRaw, 'children' | 'component'>;
|
||||||
|
|
||||||
|
type ComponentRecordType = Record<string, () => Promise<Component>>;
|
||||||
|
|
||||||
|
export type { ComponentRecordType, RouteRecordStringComponent };
|
594
pnpm-lock.yaml
generated
594
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,12 @@
|
|||||||
"stub": {},
|
"stub": {},
|
||||||
"dev": {
|
"dev": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": [""],
|
"outputs": [],
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"@vben/backend#dev": {
|
||||||
|
"outputs": [],
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
},
|
},
|
||||||
|
@@ -104,6 +104,10 @@
|
|||||||
"name": "@vben-core/tabs-ui",
|
"name": "@vben-core/tabs-ui",
|
||||||
"path": "packages/@core/ui-kit/tabs-ui",
|
"path": "packages/@core/ui-kit/tabs-ui",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "@vben/access",
|
||||||
|
"path": "packages/business/access",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "@vben/chart-ui",
|
"name": "@vben/chart-ui",
|
||||||
"path": "packages/business/chart-ui",
|
"path": "packages/business/chart-ui",
|
||||||
@@ -124,10 +128,6 @@
|
|||||||
"name": "@vben/constants",
|
"name": "@vben/constants",
|
||||||
"path": "packages/constants",
|
"path": "packages/constants",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "@vben/hooks",
|
|
||||||
"path": "packages/hooks",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "@vben/icons",
|
"name": "@vben/icons",
|
||||||
"path": "packages/icons",
|
"path": "packages/icons",
|
||||||
|
Reference in New Issue
Block a user