diff --git a/apps/web-antd/src/api/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index 71d9f9943..b225591f7 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -1,15 +1,20 @@ import { baseRequestClient, requestClient } from '#/api/request'; +import type { AuthPermissionInfo } from '@vben/types'; export namespace AuthApi { /** 登录接口参数 */ export interface LoginParams { password?: string; username?: string; + captchaVerification?: string; } /** 登录接口返回值 */ export interface LoginResult { accessToken: string; + refreshToken: string; + userId: number; + expiresTime: number; } export interface RefreshTokenResult { @@ -22,11 +27,11 @@ export namespace AuthApi { * 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/auth/login', data); + return requestClient.post('/system/auth/login', data); } /** - * 刷新accessToken + * 刷新 accessToken */ export async function refreshTokenApi() { return baseRequestClient.post('/auth/refresh', { @@ -43,9 +48,18 @@ export async function logoutApi() { }); } +// /** +// * 获取用户权限码 +// */ +// export async function getAccessCodesApi() { +// return requestClient.get('/auth/codes'); +// } + /** - * 获取用户权限码 + * 获取权限信息 */ -export async function getAccessCodesApi() { - return requestClient.get('/auth/codes'); -} +export function getAuthPermissionInfoApi() { + return requestClient.get( + '/system/auth/get-permission-info', + ); +} \ No newline at end of file diff --git a/apps/web-antd/src/api/core/index.ts b/apps/web-antd/src/api/core/index.ts index 28a5aef47..a0032eb01 100644 --- a/apps/web-antd/src/api/core/index.ts +++ b/apps/web-antd/src/api/core/index.ts @@ -1,3 +1,2 @@ export * from './auth'; -export * from './menu'; export * from './user'; diff --git a/apps/web-antd/src/api/core/menu.ts b/apps/web-antd/src/api/core/menu.ts deleted file mode 100644 index 9ef60b11c..000000000 --- a/apps/web-antd/src/api/core/menu.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RouteRecordStringComponent } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户所有菜单 - */ -export async function getAllMenusApi() { - return requestClient.get('/menu/all'); -} diff --git a/apps/web-antd/src/api/core/user.ts b/apps/web-antd/src/api/core/user.ts index 7e28ea848..43d1fca40 100644 --- a/apps/web-antd/src/api/core/user.ts +++ b/apps/web-antd/src/api/core/user.ts @@ -6,5 +6,5 @@ import { requestClient } from '#/api/request'; * 获取用户信息 */ export async function getUserInfoApi() { - return requestClient.get('/user/info'); + return requestClient.get('/system/user/profile/get'); } diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index 288dddd09..e5e2ef439 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -67,6 +67,10 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { config.headers.Authorization = formatToken(accessStore.accessToken); config.headers['Accept-Language'] = preferences.app.locale; + config.headers['tenant-id'] = 1 + // TODO @芋艿:优化一下 + // config.headers['tenant-id'] = + // tenantEnable && tenantId ? tenantId : undefined; return config; }, }); diff --git a/apps/web-antd/src/preferences.ts b/apps/web-antd/src/preferences.ts index b2e9ace43..282dc73b1 100644 --- a/apps/web-antd/src/preferences.ts +++ b/apps/web-antd/src/preferences.ts @@ -8,6 +8,9 @@ import { defineOverridesPreferences } from '@vben/preferences'; export const overridesPreferences = defineOverridesPreferences({ // overrides app: { + /** 后端路由模式 */ + accessMode: 'backend', name: import.meta.env.VITE_APP_TITLE, + enableRefreshToken: false, // TODO @芋艿:后续跟进下 }, }); diff --git a/apps/web-antd/src/router/access.ts b/apps/web-antd/src/router/access.ts index 3a48be237..9d37b9142 100644 --- a/apps/web-antd/src/router/access.ts +++ b/apps/web-antd/src/router/access.ts @@ -6,16 +6,15 @@ import type { import { generateAccessible } from '@vben/access'; import { preferences } from '@vben/preferences'; -import { message } from 'ant-design-vue'; - -import { getAllMenusApi } from '#/api'; import { BasicLayout, IFrameView } from '#/layouts'; -import { $t } from '#/locales'; +import { useAccessStore } from '@vben/stores'; +import { convertServerMenuToRouteRecordStringComponent } from '@vben/utils'; const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); async function generateAccess(options: GenerateMenuAndRoutesOptions) { const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); + const accessStore = useAccessStore(); const layoutMap: ComponentRecordType = { BasicLayout, @@ -25,11 +24,9 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { return await generateAccessible(preferences.app.accessMode, { ...options, fetchMenuListAsync: async () => { - message.loading({ - content: `${$t('common.loadingMenu')}...`, - duration: 1.5, - }); - return await getAllMenusApi(); + // 由于 yudao 通过 accessStore 读取,所以不在进行 message.loading 提示 + const accessMenus = accessStore.accessMenus; + return convertServerMenuToRouteRecordStringComponent(accessMenus); }, // 可以指定没有权限跳转403页面 forbiddenComponent, diff --git a/apps/web-antd/src/router/guard.ts b/apps/web-antd/src/router/guard.ts index cbb5235ec..436d3cd1b 100644 --- a/apps/web-antd/src/router/guard.ts +++ b/apps/web-antd/src/router/guard.ts @@ -9,6 +9,8 @@ import { accessRoutes, coreRouteNames } from '#/router/routes'; import { useAuthStore } from '#/store'; import { generateAccess } from './access'; +import { message } from 'ant-design-vue'; +import { $t } from '@vben/locales'; /** * 通用守卫配置 @@ -92,10 +94,22 @@ function setupAccessGuard(router: Router) { // 生成路由表 // 当前登录用户拥有的角色标识列表 - const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); - const userRoles = userInfo.roles ?? []; + let userInfo = userStore.userInfo; + if (!userInfo) { + // addy by 芋艿:由于 yudao 是 fetchUserInfo 统一加载用户 + 权限信息,所以将 fetchMenuListAsync + const loading = message.loading({ + content: `${$t('common.loadingMenu')}...`, + }); + try { + userInfo = (await authStore.fetchUserInfo()).user; + } finally { + loading(); + } + } + const userRoles = userStore.userRoles ?? []; // 生成菜单和路由 + debugger; const { accessibleMenus, accessibleRoutes } = await generateAccess({ roles: userRoles, router, @@ -107,6 +121,7 @@ function setupAccessGuard(router: Router) { accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); + userStore.setUserRoles(userRoles); const redirectPath = (from.query.redirect ?? (to.path === DEFAULT_HOME_PATH ? userInfo.homePath || DEFAULT_HOME_PATH diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts index 9d64d2058..b5f54faae 100644 --- a/apps/web-antd/src/store/auth.ts +++ b/apps/web-antd/src/store/auth.ts @@ -1,4 +1,4 @@ -import type { Recordable, UserInfo } from '@vben/types'; +import type { AuthPermissionInfo, Recordable, UserInfo} from '@vben/types'; import { ref } from 'vue'; import { useRouter } from 'vue-router'; @@ -9,7 +9,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; -import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; +import { getAuthPermissionInfoApi, loginApi, logoutApi} from '#/api'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { @@ -32,22 +32,22 @@ export const useAuthStore = defineStore('auth', () => { let userInfo: null | UserInfo = null; try { loginLoading.value = true; - const { accessToken } = await loginApi(params); + const { accessToken, refreshToken } = await loginApi(params); // 如果成功获取到 accessToken if (accessToken) { accessStore.setAccessToken(accessToken); + accessStore.setRefreshToken(refreshToken); - // 获取用户信息并存储到 accessStore 中 - const [fetchUserInfoResult, accessCodes] = await Promise.all([ - fetchUserInfo(), - getAccessCodesApi(), - ]); + // 获取用户信息并存储到 userStore、accessStore 中 + // TODO @芋艿:清理掉 accessCodes 相关的逻辑 + // const [fetchUserInfoResult, accessCodes] = await Promise.all([ + // fetchUserInfo(), + // // getAccessCodesApi(), + // ]); + const fetchUserInfoResult = await fetchUserInfo(); - userInfo = fetchUserInfoResult; - - userStore.setUserInfo(userInfo); - accessStore.setAccessCodes(accessCodes); + userInfo = fetchUserInfoResult.user; if (accessStore.loginExpired) { accessStore.setLoginExpired(false); @@ -95,10 +95,16 @@ export const useAuthStore = defineStore('auth', () => { } async function fetchUserInfo() { - let userInfo: null | UserInfo = null; - userInfo = await getUserInfoApi(); - userStore.setUserInfo(userInfo); - return userInfo; + // 加载 + let authPermissionInfo: AuthPermissionInfo | null = null; + authPermissionInfo = await getAuthPermissionInfoApi(); + // userStore + userStore.setUserInfo(authPermissionInfo.user); + userStore.setUserRoles(authPermissionInfo.roles); + // accessStore + accessStore.setAccessMenus(authPermissionInfo.menus); + accessStore.setAccessCodes(authPermissionInfo.permissions); + return authPermissionInfo; } function $reset() { diff --git a/apps/web-antd/src/views/_core/authentication/login.vue b/apps/web-antd/src/views/_core/authentication/login.vue index 099e4c8c0..e033384cf 100644 --- a/apps/web-antd/src/views/_core/authentication/login.vue +++ b/apps/web-antd/src/views/_core/authentication/login.vue @@ -49,25 +49,12 @@ const formSchema = computed((): VbenFormSchema[] => { componentProps: { placeholder: $t('authentication.usernameTip'), }, - dependencies: { - trigger(values, form) { - if (values.selectAccount) { - const findUser = MOCK_USER_OPTIONS.find( - (item) => item.value === values.selectAccount, - ); - if (findUser) { - form.setValues({ - password: '123456', - username: findUser.value, - }); - } - } - }, - triggerFields: ['selectAccount'], - }, fieldName: 'username', label: $t('authentication.username'), - rules: z.string().min(1, { message: $t('authentication.usernameTip') }), + rules: z + .string() + .min(1, { message: $t('authentication.usernameTip') }) + .default(import.meta.env.VITE_APP_DEFAULT_USERNAME), }, { component: 'VbenInputPassword', @@ -76,14 +63,10 @@ const formSchema = computed((): VbenFormSchema[] => { }, fieldName: 'password', label: $t('authentication.password'), - rules: z.string().min(1, { message: $t('authentication.passwordTip') }), - }, - { - component: markRaw(SliderCaptcha), - fieldName: 'captcha', - rules: z.boolean().refine((value) => value, { - message: $t('authentication.verifyRequiredTip'), - }), + rules: z + .string() + .min(1, { message: $t('authentication.passwordTip') }) + .default(import.meta.env.VITE_APP_DEFAULT_PASSWORD), }, ]; }); diff --git a/packages/stores/src/modules/access.ts b/packages/stores/src/modules/access.ts index 53ff7f2c1..e0202e567 100644 --- a/packages/stores/src/modules/access.ts +++ b/packages/stores/src/modules/access.ts @@ -85,7 +85,7 @@ export const useAccessStore = defineStore('core-access', { }, persist: { // 持久化 - pick: ['accessToken', 'refreshToken', 'accessCodes'], + pick: ['accessToken', 'refreshToken', 'accessCodes'], // TODO @芋艿:accessCodes 不持久化 }, state: (): AccessState => ({ accessCodes: [], diff --git a/packages/stores/src/modules/user.ts b/packages/stores/src/modules/user.ts index 9d374335d..5d3631a2f 100644 --- a/packages/stores/src/modules/user.ts +++ b/packages/stores/src/modules/user.ts @@ -11,7 +11,7 @@ interface BasicUserInfo { */ realName: string; /** - * 用户角色 + * 用户角色(TODO 已废弃,add by 芋艿) */ roles?: string[]; /** diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index 44dc5b4ab..baba7278f 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -1,20 +1,43 @@ import type { BasicUserInfo } from '@vben-core/typings'; +import type { RouteMeta, RouteRecordRaw } from 'vue-router'; /** 用户信息 */ interface UserInfo extends BasicUserInfo { - /** - * 用户描述 - */ - desc: string; + /** * 首页地址 */ homePath: string; - /** - * accessToken - */ - token: string; } -export type { UserInfo }; +/** 权限信息 */ +interface AuthPermissionInfo { + + user: UserInfo; + roles: string[]; + permissions: string[]; + menus: AppRouteRecordRaw[]; + +} + +/** 路由元信息 */ +interface AppRouteRecordRaw extends Omit { + + children?: AppRouteRecordRaw[]; + component?: any; + componentName?: string; + components?: any; + fullPath?: string; + icon?: string; + keepAlive?: boolean; + meta: RouteMeta; + name: string; + parentId?: number; + props?: any; + sort?: number; + visible?: boolean; + +} + +export type { UserInfo, AuthPermissionInfo, AppRouteRecordRaw }; diff --git a/packages/utils/src/helpers/generate-menus.ts b/packages/utils/src/helpers/generate-menus.ts index 6f0d9474b..82acf8265 100644 --- a/packages/utils/src/helpers/generate-menus.ts +++ b/packages/utils/src/helpers/generate-menus.ts @@ -1,8 +1,9 @@ import type { Router, RouteRecordRaw } from 'vue-router'; -import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings'; +import type { ExRouteRecordRaw, MenuRecordRaw, RouteRecordStringComponent } from '@vben-core/typings'; -import { filterTree, mapTree } from '@vben-core/shared/utils'; +import { filterTree, mapTree, isHttpUrl } from '@vben-core/shared/utils'; +import type { AppRouteRecordRaw } from '@vben/types'; // TODO @芋艿:这里的报错,解决 /** * 根据 routes 生成菜单列表 @@ -78,4 +79,76 @@ async function generateMenus( return finalMenus; } -export { generateMenus }; +/** + * 转换后端菜单数据为路由数据 + * @param menuList 后端菜单数据 + * @param parent 父级菜单 + * @returns 路由数据 + */ +function convertServerMenuToRouteRecordStringComponent( + menuList: AppRouteRecordRaw[], + parent = '', +): RouteRecordStringComponent[] { + const menus: RouteRecordStringComponent[] = []; + menuList.forEach((menu) => { + // 处理顶级链接菜单 + if (isHttpUrl(menu.path) && menu.parentId === 0) { + const urlMenu: RouteRecordStringComponent = { + component: 'IFrameView', + meta: { + hideInMenu: !menu.visible, + icon: menu.icon, + link: menu.path, + orderNo: menu.sort, + title: menu.name, + }, + name: menu.name, + path: `/${menu.path}/index`, + }; + menus.push(urlMenu); + return; + } else if (menu.children && menu.parentId === 0) { + menu.component = 'BasicLayout'; + } else if (!menu.children) { + menu.component = menu.component as string; + } + if (menu.component === 'Layout') { + menu.component = 'BasicLayout'; + } + + if (menu.children && menu.parentId !== 0) { + menu.component = ''; + } + + // path + if (parent) { + menu.path = `${parent}/${menu.path}`; + } + + if (!menu.path.startsWith('/')) { + menu.path = `/${menu.path}`; + } + + const buildMenu: RouteRecordStringComponent = { + component: menu.component, + meta: { + hideInMenu: !menu.visible, + icon: menu.icon, + keepAlive: menu.keepAlive, + orderNo: menu.sort, + title: menu.name, + }, + name: menu.name, + path: menu.path, + }; + + if (menu.children && menu.children.length > 0) { + buildMenu.children = convertServerMenuToRouteRecordStringComponent(menu.children, menu.path); + } + + menus.push(buildMenu); + }); + return menus; +} + +export { generateMenus, convertServerMenuToRouteRecordStringComponent }; diff --git a/packages/utils/src/helpers/generate-routes-backend.ts b/packages/utils/src/helpers/generate-routes-backend.ts index 9ecd09879..5b0ab66da 100644 --- a/packages/utils/src/helpers/generate-routes-backend.ts +++ b/packages/utils/src/helpers/generate-routes-backend.ts @@ -30,7 +30,8 @@ async function generateRoutesByBackend( const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap); - return routes; + // add by 芋艿:合并静态路由和动态路由 + return [...options.routes, ...routes]; } catch (error) { console.error(error); return [];