diff --git a/.vscode/settings.json b/.vscode/settings.json index 9de1c1cd..6ddee36e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -171,10 +171,7 @@ "packages/@vben-core/shared/design-tokens/src/**/*.css" ], - "i18n-ally.localesPaths": [ - "packages/locales/src/langs", - "packages/@core/shared/i18n/src/langs" - ], + "i18n-ally.localesPaths": ["packages/locales/src/langs"], "i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"], "i18n-ally.sourceLanguage": "en", "i18n-ally.displayLanguage": "zh-CN", diff --git a/apps/backend-mock/http/menu.http b/apps/backend-mock/http/menu.http new file mode 100644 index 00000000..f2d7df4d --- /dev/null +++ b/apps/backend-mock/http/menu.http @@ -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 }} diff --git a/apps/backend-mock/package.json b/apps/backend-mock/package.json index 1aa77bbc..1b8f8b94 100644 --- a/apps/backend-mock/package.json +++ b/apps/backend-mock/package.json @@ -36,8 +36,8 @@ "typeorm": "^0.3.20" }, "devDependencies": { - "@nestjs/cli": "^10.3.2", - "@nestjs/schematics": "^10.1.1", + "@nestjs/cli": "^10.4.0", + "@nestjs/schematics": "^10.1.2", "@types/express": "^4.17.21", "@types/node": "^20.14.9", "nodemon": "^3.1.4", diff --git a/apps/backend-mock/src/app.module.ts b/apps/backend-mock/src/app.module.ts index c04aad43..b3d5403a 100644 --- a/apps/backend-mock/src/app.module.ts +++ b/apps/backend-mock/src/app.module.ts @@ -7,6 +7,7 @@ import Joi from 'joi'; import { AuthModule } from './modules/auth/auth.module'; import { DatabaseModule } from './modules/database/database.module'; import { HealthModule } from './modules/health/health.module'; +import { MenuModule } from './modules/menu/menu.module'; import { UsersModule } from './modules/users/users.module'; @Module({ @@ -34,6 +35,7 @@ import { UsersModule } from './modules/users/users.module'; AuthModule, UsersModule, DatabaseModule, + MenuModule, ], }) export class AppModule {} diff --git a/apps/backend-mock/src/models/dto/user.dto.ts b/apps/backend-mock/src/models/dto/user.dto.ts new file mode 100644 index 00000000..e5f5c60a --- /dev/null +++ b/apps/backend-mock/src/models/dto/user.dto.ts @@ -0,0 +1,9 @@ +class CreateUserDto { + id: number; + password: string; + realName: string; + roles: string[]; + username: string; +} + +export { CreateUserDto }; diff --git a/apps/backend-mock/src/modules/menu/menu.controller.ts b/apps/backend-mock/src/modules/menu/menu.controller.ts new file mode 100644 index 00000000..d5effc3a --- /dev/null +++ b/apps/backend-mock/src/modules/menu/menu.controller.ts @@ -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 ?? []; + } +} diff --git a/apps/backend-mock/src/modules/menu/menu.module.ts b/apps/backend-mock/src/modules/menu/menu.module.ts new file mode 100644 index 00000000..36c3331b --- /dev/null +++ b/apps/backend-mock/src/modules/menu/menu.module.ts @@ -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 {} diff --git a/apps/backend-mock/src/modules/menu/menu.service.ts b/apps/backend-mock/src/modules/menu/menu.service.ts new file mode 100644 index 00000000..fa43f7e3 --- /dev/null +++ b/apps/backend-mock/src/modules/menu/menu.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MenuService {} diff --git a/apps/backend-mock/src/modules/users/users.service.ts b/apps/backend-mock/src/modules/users/users.service.ts index 87bfa715..67765f08 100644 --- a/apps/backend-mock/src/modules/users/users.service.ts +++ b/apps/backend-mock/src/modules/users/users.service.ts @@ -1,3 +1,4 @@ +import type { CreateUserDto } from '@/models/dto/user.dto'; import type { Repository } from 'typeorm'; import { UserEntity } from '@/models/entity/user.entity'; @@ -12,7 +13,7 @@ export class UsersService { private usersRepository: Repository, ) {} - async create(user: UserEntity): Promise { + async create(user: CreateUserDto): Promise { user.password = await bcrypt.hash(user.password, 10); // 密码哈希 return this.usersRepository.save(user); } diff --git a/apps/backend-mock/src/utils/index.ts b/apps/backend-mock/src/utils/index.ts new file mode 100644 index 00000000..af68a896 --- /dev/null +++ b/apps/backend-mock/src/utils/index.ts @@ -0,0 +1,5 @@ +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export { sleep }; diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 85e0ce5b..e4fb24a7 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -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:*", diff --git a/apps/web-antd/public/favicon.ico b/apps/web-antd/public/favicon.ico index d92e0b8c..fcf9818e 100644 Binary files a/apps/web-antd/public/favicon.ico and b/apps/web-antd/public/favicon.ico differ diff --git a/apps/web-antd/src/apis/modules/index.ts b/apps/web-antd/src/apis/modules/index.ts index e5abc856..c35b99fb 100644 --- a/apps/web-antd/src/apis/modules/index.ts +++ b/apps/web-antd/src/apis/modules/index.ts @@ -1 +1,2 @@ +export * from './menu'; export * from './user'; diff --git a/apps/web-antd/src/apis/modules/menu.ts b/apps/web-antd/src/apis/modules/menu.ts new file mode 100644 index 00000000..b6aa0323 --- /dev/null +++ b/apps/web-antd/src/apis/modules/menu.ts @@ -0,0 +1,12 @@ +import type { RouteRecordStringComponent } from '@vben/types'; + +import { requestClient } from '#/forward'; + +/** + * 获取用户所有菜单 + */ +async function getAllMenus() { + return requestClient.get('/menu/getAll'); +} + +export { getAllMenus }; diff --git a/apps/web-antd/src/apis/modules/user.ts b/apps/web-antd/src/apis/modules/user.ts index ac9b5b10..42eb7025 100644 --- a/apps/web-antd/src/apis/modules/user.ts +++ b/apps/web-antd/src/apis/modules/user.ts @@ -19,5 +19,3 @@ async function getUserInfo() { } export { getUserInfo, userLogin }; - -export * from './user'; diff --git a/apps/web-antd/src/forward/access.ts b/apps/web-antd/src/forward/access.ts new file mode 100644 index 00000000..8599e314 --- /dev/null +++ b/apps/web-antd/src/forward/access.ts @@ -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 }; diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 4471822e..a59de871 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -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'); diff --git a/apps/web-antd/src/router/guard.ts b/apps/web-antd/src/router/guard.ts index 06c6e66a..96c8dfc4 100644 --- a/apps/web-antd/src/router/guard.ts +++ b/apps/web-antd/src/router/guard.ts @@ -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; diff --git a/apps/web-antd/src/router/routes/_essentials.ts b/apps/web-antd/src/router/routes/_essentials.ts index 484a6cb0..ef92bfdc 100644 --- a/apps/web-antd/src/router/routes/_essentials.ts +++ b/apps/web-antd/src/router/routes/_essentials.ts @@ -15,7 +15,7 @@ const fallbackNotFoundRoute: RouteRecordRaw = { hideInTab: true, title: '404', }, - name: 'Fallback', + name: 'FallbackNotFound', path: '/:path(.*)*', }; diff --git a/apps/web-antd/src/router/routes/modules/demos.ts b/apps/web-antd/src/router/routes/modules/demos.ts index af976225..de631d38 100644 --- a/apps/web-antd/src/router/routes/modules/demos.ts +++ b/apps/web-antd/src/router/routes/modules/demos.ts @@ -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: [ diff --git a/apps/web-antd/src/store/index.ts b/apps/web-antd/src/store/index.ts index da5990b1..8711a3ee 100644 --- a/apps/web-antd/src/store/index.ts +++ b/apps/web-antd/src/store/index.ts @@ -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 }; diff --git a/apps/web-antd/src/views/demos/access/backend/button-control.vue b/apps/web-antd/src/views/demos/access/backend/button-control.vue new file mode 100644 index 00000000..75c76a44 --- /dev/null +++ b/apps/web-antd/src/views/demos/access/backend/button-control.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-antd/src/views/demos/access/backend/index.vue b/apps/web-antd/src/views/demos/access/backend/index.vue new file mode 100644 index 00000000..47ef44e3 --- /dev/null +++ b/apps/web-antd/src/views/demos/access/backend/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-antd/src/views/demos/access/frontend/access-test-1.vue b/apps/web-antd/src/views/demos/access/frontend/access-test-1.vue new file mode 100644 index 00000000..1309fa57 --- /dev/null +++ b/apps/web-antd/src/views/demos/access/frontend/access-test-1.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/web-antd/src/views/demos/access/frontend/access-test-2.vue b/apps/web-antd/src/views/demos/access/frontend/access-test-2.vue new file mode 100644 index 00000000..114ec30b --- /dev/null +++ b/apps/web-antd/src/views/demos/access/frontend/access-test-2.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/web-antd/src/views/demos/access/frontend/button-control.vue b/apps/web-antd/src/views/demos/access/frontend/button-control.vue new file mode 100644 index 00000000..49a0dfcc --- /dev/null +++ b/apps/web-antd/src/views/demos/access/frontend/button-control.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-antd/src/views/demos/access/frontend/index.vue b/apps/web-antd/src/views/demos/access/frontend/index.vue new file mode 100644 index 00000000..f4010f1b --- /dev/null +++ b/apps/web-antd/src/views/demos/access/frontend/index.vue @@ -0,0 +1,45 @@ + + + diff --git a/internal/lint-configs/eslint-config/package.json b/internal/lint-configs/eslint-config/package.json index 8de6bb77..296c5833 100644 --- a/internal/lint-configs/eslint-config/package.json +++ b/internal/lint-configs/eslint-config/package.json @@ -48,8 +48,8 @@ "eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-unused-imports": "^4.0.0", "eslint-plugin-vitest": "^0.5.4", - "eslint-plugin-vue": "^9.26.0", - "globals": "^15.7.0", + "eslint-plugin-vue": "^9.27.0", + "globals": "^15.8.0", "jsonc-eslint-parser": "^2.4.0", "vue-eslint-parser": "^9.4.3" } diff --git a/package.json b/package.json index b4fdd6b9..1c0ab70a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "check:type": "turbo run typecheck", "clean": "vsh clean", "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", "format": "vsh lint --format", "lint": "vsh lint", @@ -63,12 +63,12 @@ "@vben/vsh": "workspace:*", "@vue/test-utils": "^2.4.6", "cross-env": "^7.0.3", - "cspell": "^8.9.1", + "cspell": "^8.10.0", "husky": "^9.0.11", "is-ci": "^3.0.1", "jsdom": "^24.1.0", "rimraf": "^5.0.7", - "taze": "^0.14.0", + "taze": "^0.14.1", "turbo": "^2.0.6", "typescript": "^5.5.3", "unbuild": "^2.0.0", @@ -83,7 +83,7 @@ "packageManager": "pnpm@9.4.0", "pnpm": { "overrides": { - "@ant-design/colors": "^7.0.2", + "@ant-design/colors": "^7.1.0", "@ctrl/tinycolor": "^4.1.0", "clsx": "^2.1.1", "vue": "^3.4.31" diff --git a/packages/@core/forward/helpers/src/index.ts b/packages/@core/forward/helpers/src/index.ts index d96c46e3..b26d3b4b 100644 --- a/packages/@core/forward/helpers/src/index.ts +++ b/packages/@core/forward/helpers/src/index.ts @@ -1,6 +1,4 @@ export * from './find-menu-by-path'; export * from './flatten-object'; -export * from './generator-menus'; -export * from './generator-routes'; export * from './merge-route-modules'; export * from './nested-object'; diff --git a/packages/@core/forward/preferences/src/config.ts b/packages/@core/forward/preferences/src/config.ts index e110c655..b1a433f7 100644 --- a/packages/@core/forward/preferences/src/config.ts +++ b/packages/@core/forward/preferences/src/config.ts @@ -2,6 +2,7 @@ import type { Preferences } from './types'; const defaultPreferences: Preferences = { app: { + accessMode: 'frontend', aiAssistant: true, authPageLayout: 'panel-right', colorGrayMode: false, diff --git a/packages/@core/forward/preferences/src/types.ts b/packages/@core/forward/preferences/src/types.ts index 9ff29a2a..1c0db358 100644 --- a/packages/@core/forward/preferences/src/types.ts +++ b/packages/@core/forward/preferences/src/types.ts @@ -9,6 +9,8 @@ import type { type BreadcrumbStyleType = 'background' | 'normal'; +type accessModeType = 'allow-all' | 'backend' | 'frontend'; + type NavigationStyleType = 'plain' | 'rounded'; 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'; interface AppPreferences { + /** 权限模式 */ + accessMode: accessModeType; /** 是否开启vben助手 */ aiAssistant: boolean; /** 登录注册页面布局 */ @@ -208,4 +212,5 @@ export type { ThemeModeType, ThemePreferences, TransitionPreferences, + accessModeType, }; diff --git a/packages/@core/forward/stores/src/modules/index.ts b/packages/@core/forward/stores/src/modules/index.ts index 514f01e6..4f50c936 100644 --- a/packages/@core/forward/stores/src/modules/index.ts +++ b/packages/@core/forward/stores/src/modules/index.ts @@ -1,2 +1,2 @@ export * from './access'; -export * from './tabs'; +export * from './tabbar'; diff --git a/packages/@core/forward/stores/src/modules/tabs.test.ts b/packages/@core/forward/stores/src/modules/tabbar.test.ts similarity index 91% rename from packages/@core/forward/stores/src/modules/tabs.test.ts rename to packages/@core/forward/stores/src/modules/tabbar.test.ts index 9cb17ce6..0516032f 100644 --- a/packages/@core/forward/stores/src/modules/tabs.test.ts +++ b/packages/@core/forward/stores/src/modules/tabbar.test.ts @@ -3,7 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router'; import { createPinia, setActivePinia } from 'pinia'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useTabsStore } from './tabs'; +import { useTabbarStore } from './tabbar'; describe('useAccessStore', () => { const router = createRouter({ @@ -18,7 +18,7 @@ describe('useAccessStore', () => { }); it('adds a new tab', () => { - const store = useTabsStore(); + const store = useTabbarStore(); const tab: any = { fullPath: '/home', meta: {}, @@ -31,7 +31,7 @@ describe('useAccessStore', () => { }); it('adds a new tab if it does not exist', () => { - const store = useTabsStore(); + const store = useTabbarStore(); const newTab: any = { fullPath: '/new', meta: {}, @@ -43,7 +43,7 @@ describe('useAccessStore', () => { }); it('updates an existing tab instead of adding a new one', () => { - const store = useTabsStore(); + const store = useTabbarStore(); const initialTab: any = { fullPath: '/existing', meta: {}, @@ -59,7 +59,7 @@ describe('useAccessStore', () => { }); it('closes all tabs', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); store.tabs = [ { fullPath: '/home', meta: {}, name: 'Home', path: '/home' }, ] as any; @@ -72,7 +72,7 @@ describe('useAccessStore', () => { }); it('returns all tabs including affix tabs', () => { - const store = useTabsStore(); + const store = useTabbarStore(); store.tabs = [ { fullPath: '/home', meta: {}, name: 'Home', path: '/home' }, ] as any; @@ -86,7 +86,7 @@ describe('useAccessStore', () => { }); it('closes a non-affix tab', () => { - const store = useTabsStore(); + const store = useTabbarStore(); const tab: any = { fullPath: '/closable', meta: {}, @@ -99,7 +99,7 @@ describe('useAccessStore', () => { }); it('does not close an affix tab', () => { - const store = useTabsStore(); + const store = useTabbarStore(); const affixTab: any = { fullPath: '/affix', meta: { affixTab: true }, @@ -112,14 +112,14 @@ describe('useAccessStore', () => { }); it('returns all cache tabs', () => { - const store = useTabsStore(); + const store = useTabbarStore(); store.cacheTabs.add('Home'); store.cacheTabs.add('About'); expect(store.getCacheTabs).toEqual(['Home', 'About']); }); it('returns all tabs, including affix tabs', () => { - const store = useTabsStore(); + const store = useTabbarStore(); const normalTab: any = { fullPath: '/normal', meta: {}, @@ -139,7 +139,7 @@ describe('useAccessStore', () => { }); it('navigates to a specific tab', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' }; await store._goToTab(tab, router); @@ -152,7 +152,7 @@ describe('useAccessStore', () => { }); it('closes multiple tabs by paths', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); store.addTab({ fullPath: '/home', meta: {}, @@ -179,7 +179,7 @@ describe('useAccessStore', () => { }); it('closes all tabs to the left of the specified tab', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); store.addTab({ fullPath: '/home', meta: {}, @@ -207,7 +207,7 @@ describe('useAccessStore', () => { }); it('closes all tabs except the specified tab', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); store.addTab({ fullPath: '/home', meta: {}, @@ -235,7 +235,7 @@ describe('useAccessStore', () => { }); it('closes all tabs to the right of the specified tab', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); const targetTab: any = { fullPath: '/home', meta: {}, @@ -263,7 +263,7 @@ describe('useAccessStore', () => { }); it('closes the tab with the specified key', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); const keyToClose = '/about'; store.addTab({ fullPath: '/home', @@ -293,7 +293,7 @@ describe('useAccessStore', () => { }); it('refreshes the current tab', async () => { - const store = useTabsStore(); + const store = useTabbarStore(); const currentTab: any = { fullPath: '/dashboard', meta: { name: 'Dashboard' }, diff --git a/packages/@core/forward/stores/src/modules/tabs.ts b/packages/@core/forward/stores/src/modules/tabbar.ts similarity index 98% rename from packages/@core/forward/stores/src/modules/tabs.ts rename to packages/@core/forward/stores/src/modules/tabbar.ts index eb0eda12..2946191a 100644 --- a/packages/@core/forward/stores/src/modules/tabs.ts +++ b/packages/@core/forward/stores/src/modules/tabbar.ts @@ -62,7 +62,7 @@ interface TabsState { /** * @zh_CN 访问权限相关 */ -const useTabsStore = defineStore('tabs', { +const useTabbarStore = defineStore('tabbar', { actions: { /** * Close tabs in bulk @@ -395,7 +395,7 @@ const useTabsStore = defineStore('tabs', { // 解决热更新问题 const hot = import.meta.hot; if (hot) { - hot.accept(acceptHMRUpdate(useTabsStore, hot)); + hot.accept(acceptHMRUpdate(useTabbarStore, hot)); } -export { useTabsStore }; +export { useTabbarStore }; diff --git a/packages/@core/shared/colorful/package.json b/packages/@core/shared/colorful/package.json index f7c429d6..ee89b50e 100644 --- a/packages/@core/shared/colorful/package.json +++ b/packages/@core/shared/colorful/package.json @@ -36,7 +36,7 @@ } }, "dependencies": { - "@ant-design/colors": "^7.0.2", + "@ant-design/colors": "^7.1.0", "@ctrl/tinycolor": "4.1.0" } } diff --git a/packages/@core/shared/design/src/tailwind.css b/packages/@core/shared/design/src/tailwind.css index 5bd546f0..c7030039 100644 --- a/packages/@core/shared/design/src/tailwind.css +++ b/packages/@core/shared/design/src/tailwind.css @@ -36,4 +36,8 @@ .outline-box:not(.outline-box-active):hover::after { @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; + } } diff --git a/packages/@core/shared/toolkit/src/hash.test.ts b/packages/@core/shared/toolkit/src/hash.test.ts deleted file mode 100644 index f01cf6ae..00000000 --- a/packages/@core/shared/toolkit/src/hash.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/@core/shared/toolkit/src/hash.ts b/packages/@core/shared/toolkit/src/hash.ts deleted file mode 100644 index 6265c688..00000000 --- a/packages/@core/shared/toolkit/src/hash.ts +++ /dev/null @@ -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 }; diff --git a/packages/@core/shared/toolkit/src/index.ts b/packages/@core/shared/toolkit/src/index.ts index f49beebc..018871f8 100644 --- a/packages/@core/shared/toolkit/src/index.ts +++ b/packages/@core/shared/toolkit/src/index.ts @@ -1,7 +1,6 @@ export * from './cn'; export * from './diff'; export * from './dom'; -export * from './hash'; export * from './inference'; export * from './letter'; export * from './merge'; diff --git a/packages/hooks/package.json b/packages/business/access/package.json similarity index 63% rename from packages/hooks/package.json rename to packages/business/access/package.json index 322261f1..181659ac 100644 --- a/packages/hooks/package.json +++ b/packages/business/access/package.json @@ -1,18 +1,18 @@ { - "name": "@vben/hooks", + "name": "@vben/access", "version": "5.0.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/hooks" + "directory": "packages/business/permissions" }, "license": "MIT", "type": "module", "scripts": { - "build": "pnpm unbuild", - "stub": "pnpm unbuild --stub" + "build": "pnpm vite build", + "prepublishOnly": "npm run build" }, "files": [ "dist" @@ -32,12 +32,19 @@ "publishConfig": { "exports": { ".": { - "types": "./dist/index.d.ts", "default": "./dist/index.mjs" } } }, "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:*" } } diff --git a/packages/business/access/postcss.config.mjs b/packages/business/access/postcss.config.mjs new file mode 100644 index 00000000..3d807045 --- /dev/null +++ b/packages/business/access/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/packages/business/access/src/authority.vue b/packages/business/access/src/authority.vue new file mode 100644 index 00000000..4c74d448 --- /dev/null +++ b/packages/business/access/src/authority.vue @@ -0,0 +1,26 @@ + + + + diff --git a/packages/@core/forward/helpers/src/generator-menus.test.ts b/packages/business/access/src/generate-menu-and-routes/generate-menus.test.ts similarity index 89% rename from packages/@core/forward/helpers/src/generator-menus.test.ts rename to packages/business/access/src/generate-menu-and-routes/generate-menus.test.ts index 59adea6d..d550fe1e 100644 --- a/packages/@core/forward/helpers/src/generator-menus.test.ts +++ b/packages/business/access/src/generate-menu-and-routes/generate-menus.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { generatorMenus } from './generator-menus'; // 替换为您的实际路径 +import { generateMenus } from './generate-menus'; // 替换为您的实际路径 import { type RouteRecordRaw, type Router, @@ -10,7 +10,7 @@ import { // Nested route setup to test child inclusion and hideChildrenInMenu functionality -describe('generatorMenus', () => { +describe('generateMenus', () => { // 模拟路由数据 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); }); @@ -82,7 +82,7 @@ describe('generatorMenus', () => { }, ] as RouteRecordRaw[]; - const menus = await generatorMenus(mockRoutesWithMeta, mockRouter as any); + const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any); expect(menus).toEqual([ { badge: undefined, @@ -108,7 +108,7 @@ describe('generatorMenus', () => { }, ] as RouteRecordRaw[]; - const menus = await generatorMenus(mockRoutesWithParams, mockRouter as any); + const menus = await generateMenus(mockRoutesWithParams, mockRouter as any); expect(menus).toEqual([ { badge: undefined, @@ -139,12 +139,12 @@ describe('generatorMenus', () => { }, ] as RouteRecordRaw[]; - const menus = await generatorMenus( + const menus = await generateMenus( mockRoutesWithRedirect, mockRouter as any, ); 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, badgeType: undefined, @@ -191,7 +191,7 @@ describe('generatorMenus', () => { }); it('should generate menu list with correct order', async () => { - const menus = await generatorMenus(routes, router); + const menus = await generateMenus(routes, router); const expectedMenus = [ { badge: undefined, @@ -224,7 +224,7 @@ describe('generatorMenus', () => { it('should handle empty routes', async () => { const emptyRoutes: any[] = []; - const menus = await generatorMenus(emptyRoutes, router); + const menus = await generateMenus(emptyRoutes, router); expect(menus).toEqual([]); }); }); diff --git a/packages/@core/forward/helpers/src/generator-menus.ts b/packages/business/access/src/generate-menu-and-routes/generate-menus.ts similarity index 93% rename from packages/@core/forward/helpers/src/generator-menus.ts rename to packages/business/access/src/generate-menu-and-routes/generate-menus.ts index 432df3ff..9ec9a792 100644 --- a/packages/@core/forward/helpers/src/generator-menus.ts +++ b/packages/business/access/src/generate-menu-and-routes/generate-menus.ts @@ -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 { mapTree } from '@vben-core/toolkit'; @@ -7,7 +7,7 @@ import { mapTree } from '@vben-core/toolkit'; * 根据 routes 生成菜单列表 * @param routes */ -async function generatorMenus( +async function generateMenus( routes: RouteRecordRaw[], router: Router, ): Promise { @@ -70,4 +70,4 @@ async function generatorMenus( return menus; } -export { generatorMenus }; +export { generateMenus }; diff --git a/packages/business/access/src/generate-menu-and-routes/generate-routes-backend.ts b/packages/business/access/src/generate-menu-and-routes/generate-routes-backend.ts new file mode 100644 index 00000000..735e2dec --- /dev/null +++ b/packages/business/access/src/generate-menu-and-routes/generate-routes-backend.ts @@ -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 { + 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 }; diff --git a/packages/@core/forward/helpers/src/generator-routes.test.ts b/packages/business/access/src/generate-menu-and-routes/generate-routes-frontend.test.ts similarity index 87% rename from packages/@core/forward/helpers/src/generator-routes.test.ts rename to packages/business/access/src/generate-menu-and-routes/generate-routes-frontend.test.ts index 51bac936..5e1f56d4 100644 --- a/packages/@core/forward/helpers/src/generator-routes.test.ts +++ b/packages/business/access/src/generate-menu-and-routes/generate-routes-frontend.test.ts @@ -2,7 +2,11 @@ import type { RouteRecordRaw } from 'vue-router'; import { describe, expect, it } from 'vitest'; -import { generatorRoutes, hasAuthority, hasVisible } from './generator-routes'; +import { + generateRoutesByFrontend, + hasAuthority, + hasVisible, +} from './generate-routes-frontend'; // Mock 路由数据 const mockRoutes = [ @@ -58,9 +62,11 @@ describe('hasVisible', () => { }); }); -describe('generatorRoutes', () => { +describe('generateRoutesByFrontend', () => { 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 expect(generatedRoutes).toEqual([ { @@ -77,7 +83,9 @@ describe('generatorRoutes', () => { }); it('should handle routes without children', async () => { - const generatedRoutes = await generatorRoutes(mockRoutes, ['user']); + const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [ + 'user', + ]); expect(generatedRoutes).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -88,7 +96,7 @@ describe('generatorRoutes', () => { }); it('should handle empty roles array', async () => { - const generatedRoutes = await generatorRoutes(mockRoutes, []); + const generatedRoutes = await generateRoutesByFrontend(mockRoutes, []); expect(generatedRoutes).toEqual( expect.arrayContaining([ // Only routes without authority should be included @@ -115,7 +123,7 @@ describe('generatorRoutes', () => { { meta: {}, path: '/path2' }, // Empty meta { meta: { authority: ['admin'] }, path: '/path3' }, // Only authority ]; - const generatedRoutes = await generatorRoutes( + const generatedRoutes = await generateRoutesByFrontend( routesWithMissingMeta as RouteRecordRaw[], ['admin'], ); diff --git a/packages/@core/forward/helpers/src/generator-routes.ts b/packages/business/access/src/generate-menu-and-routes/generate-routes-frontend.ts similarity index 83% rename from packages/@core/forward/helpers/src/generator-routes.ts rename to packages/business/access/src/generate-menu-and-routes/generate-routes-frontend.ts index 5e787566..22957152 100644 --- a/packages/@core/forward/helpers/src/generator-routes.ts +++ b/packages/business/access/src/generate-menu-and-routes/generate-routes-frontend.ts @@ -2,26 +2,26 @@ import type { RouteRecordRaw } from 'vue-router'; import { filterTree, mapTree } from '@vben-core/toolkit'; /** - * 动态生成路由 + * 动态生成路由 - 前端方式 */ -async function generatorRoutes( +async function generateRoutesByFrontend( routes: RouteRecordRaw[], roles: string[], - forbiddenPage?: RouteRecordRaw['component'], + forbiddenComponent?: RouteRecordRaw['component'], ): Promise { // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限 const finalRoutes = filterTree(routes, (route) => { return hasVisible(route) && hasAuthority(route, roles); }); - if (!forbiddenPage) { + if (!forbiddenComponent) { return finalRoutes; } // 如果有禁止访问的页面,将禁止访问的页面替换为403页面 return mapTree(finalRoutes, (route) => { if (menuHasVisibleWithForbidden(route)) { - route.component = forbiddenPage; + route.component = forbiddenComponent; } return route; }); @@ -60,4 +60,4 @@ function menuHasVisibleWithForbidden(route: RouteRecordRaw) { return !!route.meta?.menuVisibleWithForbidden; } -export { generatorRoutes, hasAuthority, hasVisible }; +export { generateRoutesByFrontend, hasAuthority, hasVisible }; diff --git a/packages/business/access/src/generate-menu-and-routes/index.ts b/packages/business/access/src/generate-menu-and-routes/index.ts new file mode 100644 index 00000000..6a84c4f0 --- /dev/null +++ b/packages/business/access/src/generate-menu-and-routes/index.ts @@ -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 }; diff --git a/packages/business/access/src/index.ts b/packages/business/access/src/index.ts new file mode 100644 index 00000000..ecdb9c8b --- /dev/null +++ b/packages/business/access/src/index.ts @@ -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'; diff --git a/packages/business/access/src/types.ts b/packages/business/access/src/types.ts new file mode 100644 index 00000000..9030910c --- /dev/null +++ b/packages/business/access/src/types.ts @@ -0,0 +1,17 @@ +import type { + ComponentRecordType, + RouteRecordStringComponent, +} from '@vben/types'; +import type { RouteRecordRaw, Router } from 'vue-router'; + +interface GeneratorMenuAndRoutesOptions { + fetchMenuListAsync?: () => Promise; + forbiddenComponent?: RouteRecordRaw['component']; + layoutMap?: ComponentRecordType; + pageMap?: ComponentRecordType; + roles?: string[]; + router: Router; + routes: RouteRecordRaw[]; +} + +export type { GeneratorMenuAndRoutesOptions }; diff --git a/packages/business/access/src/use-access.ts b/packages/business/access/src/use-access.ts new file mode 100644 index 00000000..f4ad0a22 --- /dev/null +++ b/packages/business/access/src/use-access.ts @@ -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 { + 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 }; diff --git a/packages/business/access/tailwind.config.mjs b/packages/business/access/tailwind.config.mjs new file mode 100644 index 00000000..f17f556f --- /dev/null +++ b/packages/business/access/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/packages/hooks/tsconfig.json b/packages/business/access/tsconfig.json similarity index 51% rename from packages/hooks/tsconfig.json rename to packages/business/access/tsconfig.json index f6860a32..b5f44daf 100644 --- a/packages/hooks/tsconfig.json +++ b/packages/business/access/tsconfig.json @@ -1,6 +1,9 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@vben/tsconfig/library.json", + "extends": "@vben/tsconfig/web.json", + "compilerOptions": { + "types": ["@vben/types/global"] + }, "include": ["src"], "exclude": ["node_modules"] } diff --git a/packages/business/access/vite.config.mts b/packages/business/access/vite.config.mts new file mode 100644 index 00000000..9a5f448e --- /dev/null +++ b/packages/business/access/vite.config.mts @@ -0,0 +1,3 @@ +import { defineConfig } from '@vben/vite-config'; + +export default defineConfig(); diff --git a/packages/business/chart-ui/src/echarts/use-echarts.ts b/packages/business/chart-ui/src/echarts/use-echarts.ts index d6ab10c4..a0478df8 100644 --- a/packages/business/chart-ui/src/echarts/use-echarts.ts +++ b/packages/business/chart-ui/src/echarts/use-echarts.ts @@ -5,7 +5,7 @@ import type EchartsUI from './echarts-ui.vue'; import type { Ref } from 'vue'; import { computed, nextTick, watch } from 'vue'; -import { usePreferences } from '@vben-core/preferences'; +import { preferences, usePreferences } from '@vben-core/preferences'; import { tryOnUnmounted, @@ -91,9 +91,24 @@ function useEcharts(chartRef: Ref) { chartInstance.dispose(); initCharts(); renderEcharts(cacheOptions); + resize(); } }); + watch( + [ + () => preferences.sidebar.collapsed, + () => preferences.sidebar.extraCollapse, + () => preferences.sidebar.hidden, + ], + () => { + // 折叠动画200ms + setTimeout(() => { + resize(); + }, 200); + }, + ); + tryOnUnmounted(() => { // 销毁实例,释放资源 chartInstance?.dispose(); diff --git a/packages/business/layouts/src/basic/content/content.vue b/packages/business/layouts/src/basic/content/content.vue index 4dd00e24..ea367ad2 100644 --- a/packages/business/layouts/src/basic/content/content.vue +++ b/packages/business/layouts/src/basic/content/content.vue @@ -3,14 +3,14 @@ import type { RouteLocationNormalizedLoaded } from 'vue-router'; import { preferences, usePreferences } from '@vben-core/preferences'; 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 { useContentSpinner } from './use-content-spinner'; defineOptions({ name: 'LayoutContent' }); -const tabsStore = useTabsStore(); +const tabsStore = useTabbarStore(); const { keepAlive } = usePreferences(); const { spinning } = useContentSpinner(); diff --git a/packages/business/layouts/src/basic/tabbar/use-tabs.ts b/packages/business/layouts/src/basic/tabbar/use-tabs.ts index 31df286a..5d4896be 100644 --- a/packages/business/layouts/src/basic/tabbar/use-tabs.ts +++ b/packages/business/layouts/src/basic/tabbar/use-tabs.ts @@ -19,14 +19,14 @@ import { MdiPin, MdiPinOff, } 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'; function useTabs() { const router = useRouter(); const route = useRoute(); const accessStore = useAccessStore(); - const tabsStore = useTabsStore(); + const tabsStore = useTabbarStore(); const { accessMenus } = storeToRefs(accessStore); const currentActive = computed(() => { diff --git a/packages/business/layouts/src/iframe/iframe-router-view.vue b/packages/business/layouts/src/iframe/iframe-router-view.vue index 2adb2e06..80a3abcb 100644 --- a/packages/business/layouts/src/iframe/iframe-router-view.vue +++ b/packages/business/layouts/src/iframe/iframe-router-view.vue @@ -6,12 +6,12 @@ import { useRoute } from 'vue-router'; import { preferences } from '@vben-core/preferences'; import { Spinner } from '@vben-core/shadcn-ui'; -import { useTabsStore } from '@vben-core/stores'; +import { useTabbarStore } from '@vben-core/stores'; defineOptions({ name: 'IFrameRouterView' }); const spinningList = ref([]); -const tabsStore = useTabsStore(); +const tabsStore = useTabbarStore(); const route = useRoute(); const enableTabbar = computed(() => preferences.tabbar.enable); diff --git a/packages/business/universal-ui/src/about/about.vue b/packages/business/universal-ui/src/about/about.vue index dcb962dc..ec1e884a 100644 --- a/packages/business/universal-ui/src/about/about.vue +++ b/packages/business/universal-ui/src/about/about.vue @@ -107,7 +107,7 @@ const devDependenciesItems = Object.keys(devDependencies).map((key) => ({