feat: request && login && router【e6939e22】(不包括 login.vue 和 request.ts)

pull/62/head
YunaiV 2025-03-20 12:34:02 +08:00
parent 83f6a0fbf7
commit 3c3886e345
15 changed files with 193 additions and 85 deletions

View File

@ -1,15 +1,20 @@
import { baseRequestClient, requestClient } from '#/api/request'; import { baseRequestClient, requestClient } from '#/api/request';
import type { AuthPermissionInfo } from '@vben/types';
export namespace AuthApi { export namespace AuthApi {
/** 登录接口参数 */ /** 登录接口参数 */
export interface LoginParams { export interface LoginParams {
password?: string; password?: string;
username?: string; username?: string;
captchaVerification?: string;
} }
/** 登录接口返回值 */ /** 登录接口返回值 */
export interface LoginResult { export interface LoginResult {
accessToken: string; accessToken: string;
refreshToken: string;
userId: number;
expiresTime: number;
} }
export interface RefreshTokenResult { export interface RefreshTokenResult {
@ -22,11 +27,11 @@ export namespace AuthApi {
* *
*/ */
export async function loginApi(data: AuthApi.LoginParams) { export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data); return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data);
} }
/** /**
* accessToken * accessToken
*/ */
export async function refreshTokenApi() { export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', { return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
@ -43,9 +48,18 @@ export async function logoutApi() {
}); });
} }
// /**
// * 获取用户权限码
// */
// export async function getAccessCodesApi() {
// return requestClient.get<string[]>('/auth/codes');
// }
/** /**
* *
*/ */
export async function getAccessCodesApi() { export function getAuthPermissionInfoApi() {
return requestClient.get<string[]>('/auth/codes'); return requestClient.get<AuthPermissionInfo>(
'/system/auth/get-permission-info',
);
} }

View File

@ -1,3 +1,2 @@
export * from './auth'; export * from './auth';
export * from './menu';
export * from './user'; export * from './user';

View File

@ -1,10 +0,0 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View File

@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
* *
*/ */
export async function getUserInfoApi() { export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info'); return requestClient.get<UserInfo>('/system/user/profile/get');
} }

View File

@ -67,6 +67,10 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
config.headers.Authorization = formatToken(accessStore.accessToken); config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale; config.headers['Accept-Language'] = preferences.app.locale;
config.headers['tenant-id'] = 1
// TODO @芋艿:优化一下
// config.headers['tenant-id'] =
// tenantEnable && tenantId ? tenantId : undefined;
return config; return config;
}, },
}); });

View File

@ -8,6 +8,9 @@ import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({ export const overridesPreferences = defineOverridesPreferences({
// overrides // overrides
app: { app: {
/** 后端路由模式 */
accessMode: 'backend',
name: import.meta.env.VITE_APP_TITLE, name: import.meta.env.VITE_APP_TITLE,
enableRefreshToken: false, // TODO @芋艿:后续跟进下
}, },
}); });

View File

@ -6,16 +6,15 @@ import type {
import { generateAccessible } from '@vben/access'; import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts'; 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'); const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) { async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const accessStore = useAccessStore();
const layoutMap: ComponentRecordType = { const layoutMap: ComponentRecordType = {
BasicLayout, BasicLayout,
@ -25,11 +24,9 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
return await generateAccessible(preferences.app.accessMode, { return await generateAccessible(preferences.app.accessMode, {
...options, ...options,
fetchMenuListAsync: async () => { fetchMenuListAsync: async () => {
message.loading({ // 由于 yudao 通过 accessStore 读取,所以不在进行 message.loading 提示
content: `${$t('common.loadingMenu')}...`, const accessMenus = accessStore.accessMenus;
duration: 1.5, return convertServerMenuToRouteRecordStringComponent(accessMenus);
});
return await getAllMenusApi();
}, },
// 可以指定没有权限跳转403页面 // 可以指定没有权限跳转403页面
forbiddenComponent, forbiddenComponent,

View File

@ -9,6 +9,8 @@ import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { generateAccess } from './access'; 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()); let userInfo = userStore.userInfo;
const userRoles = userInfo.roles ?? []; 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({ const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles, roles: userRoles,
router, router,
@ -107,6 +121,7 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes); accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true); accessStore.setIsAccessChecked(true);
userStore.setUserRoles(userRoles);
const redirectPath = (from.query.redirect ?? const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH (to.path === DEFAULT_HOME_PATH
? userInfo.homePath || DEFAULT_HOME_PATH ? userInfo.homePath || DEFAULT_HOME_PATH

View File

@ -1,4 +1,4 @@
import type { Recordable, UserInfo } from '@vben/types'; import type { AuthPermissionInfo, Recordable, UserInfo} from '@vben/types';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -9,7 +9,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { getAuthPermissionInfoApi, loginApi, logoutApi} from '#/api';
import { $t } from '#/locales'; import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
@ -32,22 +32,22 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
try { try {
loginLoading.value = true; loginLoading.value = true;
const { accessToken } = await loginApi(params); const { accessToken, refreshToken } = await loginApi(params);
// 如果成功获取到 accessToken // 如果成功获取到 accessToken
if (accessToken) { if (accessToken) {
accessStore.setAccessToken(accessToken); accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中 // 获取用户信息并存储到 userStore、accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([ // TODO @芋艿:清理掉 accessCodes 相关的逻辑
fetchUserInfo(), // const [fetchUserInfoResult, accessCodes] = await Promise.all([
getAccessCodesApi(), // fetchUserInfo(),
]); // // getAccessCodesApi(),
// ]);
const fetchUserInfoResult = await fetchUserInfo();
userInfo = fetchUserInfoResult; userInfo = fetchUserInfoResult.user;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) { if (accessStore.loginExpired) {
accessStore.setLoginExpired(false); accessStore.setLoginExpired(false);
@ -95,10 +95,16 @@ export const useAuthStore = defineStore('auth', () => {
} }
async function fetchUserInfo() { async function fetchUserInfo() {
let userInfo: null | UserInfo = null; // 加载
userInfo = await getUserInfoApi(); let authPermissionInfo: AuthPermissionInfo | null = null;
userStore.setUserInfo(userInfo); authPermissionInfo = await getAuthPermissionInfoApi();
return userInfo; // userStore
userStore.setUserInfo(authPermissionInfo.user);
userStore.setUserRoles(authPermissionInfo.roles);
// accessStore
accessStore.setAccessMenus(authPermissionInfo.menus);
accessStore.setAccessCodes(authPermissionInfo.permissions);
return authPermissionInfo;
} }
function $reset() { function $reset() {

View File

@ -49,25 +49,12 @@ const formSchema = computed((): VbenFormSchema[] => {
componentProps: { componentProps: {
placeholder: $t('authentication.usernameTip'), 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', fieldName: 'username',
label: $t('authentication.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', component: 'VbenInputPassword',
@ -76,14 +63,10 @@ const formSchema = computed((): VbenFormSchema[] => {
}, },
fieldName: 'password', fieldName: 'password',
label: $t('authentication.password'), label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }), rules: z
}, .string()
{ .min(1, { message: $t('authentication.passwordTip') })
component: markRaw(SliderCaptcha), .default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
fieldName: 'captcha',
rules: z.boolean().refine((value) => value, {
message: $t('authentication.verifyRequiredTip'),
}),
}, },
]; ];
}); });

View File

@ -85,7 +85,7 @@ export const useAccessStore = defineStore('core-access', {
}, },
persist: { persist: {
// 持久化 // 持久化
pick: ['accessToken', 'refreshToken', 'accessCodes'], pick: ['accessToken', 'refreshToken', 'accessCodes'], // TODO @芋艿accessCodes 不持久化
}, },
state: (): AccessState => ({ state: (): AccessState => ({
accessCodes: [], accessCodes: [],

View File

@ -11,7 +11,7 @@ interface BasicUserInfo {
*/ */
realName: string; realName: string;
/** /**
* * TODO add by
*/ */
roles?: string[]; roles?: string[];
/** /**

View File

@ -1,20 +1,43 @@
import type { BasicUserInfo } from '@vben-core/typings'; import type { BasicUserInfo } from '@vben-core/typings';
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
/** 用户信息 */ /** 用户信息 */
interface UserInfo extends BasicUserInfo { interface UserInfo extends BasicUserInfo {
/**
*
*/
desc: string;
/** /**
* *
*/ */
homePath: string; homePath: string;
/**
* accessToken
*/
token: string;
} }
export type { UserInfo }; /** 权限信息 */
interface AuthPermissionInfo {
user: UserInfo;
roles: string[];
permissions: string[];
menus: AppRouteRecordRaw[];
}
/** 路由元信息 */
interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
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 };

View File

@ -1,8 +1,9 @@
import type { Router, RouteRecordRaw } from 'vue-router'; 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 * routes
@ -78,4 +79,76 @@ async function generateMenus(
return finalMenus; 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 };

View File

@ -30,7 +30,8 @@ async function generateRoutesByBackend(
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap); const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
return routes; // add by 芋艿:合并静态路由和动态路由
return [...options.routes, ...routes];
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return []; return [];