feat: request && login && router【e6939e22】(不包括 login.vue 和 request.ts)
parent
83f6a0fbf7
commit
3c3886e345
|
@ -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<AuthApi.LoginResult>('/auth/login', data);
|
||||
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新accessToken
|
||||
* 刷新 accessToken
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
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() {
|
||||
return requestClient.get<string[]>('/auth/codes');
|
||||
}
|
||||
export function getAuthPermissionInfoApi() {
|
||||
return requestClient.get<AuthPermissionInfo>(
|
||||
'/system/auth/get-permission-info',
|
||||
);
|
||||
}
|
|
@ -1,3 +1,2 @@
|
|||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
|
|
|
@ -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');
|
||||
}
|
|
@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
|
|||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/user/info');
|
||||
return requestClient.get<UserInfo>('/system/user/profile/get');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 @芋艿:后续跟进下
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
@ -85,7 +85,7 @@ export const useAccessStore = defineStore('core-access', {
|
|||
},
|
||||
persist: {
|
||||
// 持久化
|
||||
pick: ['accessToken', 'refreshToken', 'accessCodes'],
|
||||
pick: ['accessToken', 'refreshToken', 'accessCodes'], // TODO @芋艿:accessCodes 不持久化
|
||||
},
|
||||
state: (): AccessState => ({
|
||||
accessCodes: [],
|
||||
|
|
|
@ -11,7 +11,7 @@ interface BasicUserInfo {
|
|||
*/
|
||||
realName: string;
|
||||
/**
|
||||
* 用户角色
|
||||
* 用户角色(TODO 已废弃,add by 芋艿)
|
||||
*/
|
||||
roles?: string[];
|
||||
/**
|
||||
|
|
|
@ -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<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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 [];
|
||||
|
|
Loading…
Reference in New Issue