perf: improve the logic related to login expiration

This commit is contained in:
vince
2024-07-11 20:11:11 +08:00
parent 8e6c1abf19
commit d62a3da009
43 changed files with 552 additions and 347 deletions

View File

@@ -0,0 +1,23 @@
import type { Response } from 'express';
import { Controller, Get, Query, Res } from '@nestjs/common';
@Controller('mock')
export class MockController {
/**
* 用于模拟任意的状态码
* @param res
*/
@Get('status')
async mockAnyStatus(
@Res() res: Response,
@Query() { status }: { status: string },
) {
res.status(Number.parseInt(status, 10)).send({
code: 1,
data: null,
error: null,
message: `code is ${status}`,
});
}
}

View File

@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { MockController } from './mock.controller';
import { MockService } from './mock.service';
@Module({
controllers: [MockController],
exports: [MockService],
providers: [MockService],
})

View File

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

View File

@@ -0,0 +1,10 @@
import { requestClient } from '#/forward';
/**
* 模拟人意状态码
*/
async function getMockStatus(status: string) {
return requestClient.get('/mock/status', { params: { status } });
}
export { getMockStatus };

View File

@@ -1,23 +1,14 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben-core/request';
import type { AxiosResponse } from '@vben-core/request';
import { RequestClient, isCancelError } from '@vben-core/request';
import { useCoreAccessStore } from '@vben-core/stores';
import { preferences } from '@vben-core/preferences';
import { RequestClient } from '@vben-core/request';
import { message } from 'ant-design-vue';
interface HttpResponse<T = any> {
/**
* 0 表示成功 其他表示失败
* 0 means success, others means fail
*/
code: number;
data: T;
message: string;
}
import { useAccessStore } from '#/store';
/**
* 创建请求实例
@@ -29,59 +20,42 @@ function createRequestClient() {
// 为每个请求携带 Authorization
makeAuthorization: () => {
return {
handler: () => {
// 这里不能用 useAccessStore因为 useAccessStore 会导致循环引用
const accessStore = useCoreAccessStore();
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `Bearer ${accessStore.refreshToken}`,
token: `Bearer ${accessStore.accessToken}`,
};
},
// 默认
key: 'Authorization',
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
accessStore.setAccessToken(null);
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.openLoginExpiredModal = true;
} else {
// 退出登录
await accessStore.logout();
}
},
};
},
makeErrorMessage: (msg) => message.error(msg),
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(msg);
});
setupRequestInterceptors(client);
return client;
}
function setupRequestInterceptors(client: RequestClient) {
client.addResponseInterceptor(
(response: AxiosResponse<HttpResponse>) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
} else {
message.error(msg);
throw new Error(msg);
}
},
(error: any) => {
if (isCancelError(error)) {
return Promise.reject(error);
}
const err: string = error?.toString?.() ?? '';
let errMsg = '';
if (err?.includes('Network Error')) {
errMsg = '网络错误。';
} else if (error?.message?.includes?.('timeout')) {
errMsg = '请求超时。';
} else {
const data = error?.response?.data;
errMsg = (data?.message || data?.error?.message) ?? '';
}
message.error(errMsg);
return Promise.reject(error);
},
);
}
const requestClient = createRequestClient();
// 其他配置的请求方法

View File

@@ -6,11 +6,11 @@ import { LOGIN_PATH } from '@vben/constants';
import { IcRoundCreditScore, MdiDriveDocument, MdiGithub } from '@vben/icons';
import {
BasicLayout,
LoginDialog,
Notification,
NotificationItem,
UserDropdown,
} from '@vben/layouts';
import { AuthenticationLoginExpiredModal } from '@vben/universal-ui';
import { openWindow } from '@vben/utils';
import { preferences } from '@vben-core/preferences';
@@ -85,7 +85,7 @@ const menus = computed(() => [
const appStore = useAppStore();
const accessStore = useAccessStore();
const { showLoginDialog, userInfo } = toRefs(accessStore);
const { openLoginExpiredModal, userInfo } = toRefs(accessStore);
const router = useRouter();
async function handleLogout() {
@@ -124,11 +124,11 @@ function handleMakeAll() {
/>
</template>
<template #dialog>
<LoginDialog
:open="showLoginDialog"
<AuthenticationLoginExpiredModal
v-model:open="openLoginExpiredModal"
password-placeholder="123456"
username-placeholder="vben"
@login="accessStore.authLogin"
@submit="accessStore.authLogin"
/>
</template>
</BasicLayout>

View File

@@ -28,7 +28,12 @@
"embedded": "Embedded",
"externalLink": "External Link"
},
"fallback": { "title": "Fallback Page" }
"fallback": { "title": "Fallback Page" },
"features": {
"title": "Features",
"hideChildrenInMenu": "Hide Menu Children",
"loginExpired": "Login Expired"
}
}
}
}

View File

@@ -30,6 +30,11 @@
},
"fallback": {
"title": "缺省页"
},
"features": {
"title": "功能",
"hideChildrenInMenu": "隐藏菜单子项",
"loginExpired": "登录过期"
}
}
}

View File

@@ -93,24 +93,9 @@ function setupAccessGuard(router: Router) {
// 生成路由表
// 当前登录用户拥有的角色标识列表
let userRoles: string[] = [];
try {
const userInfo =
accessStore.userInfo || (await accessStore.fetchUserInfo());
userRoles = userInfo.roles ?? [];
} catch (error: any) {
if (error.status === 409) {
accessStore.setShowLoginDialog(true);
} else if (error.status === 401) {
accessStore.reset();
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
}
const userInfo =
accessStore.userInfo || (await accessStore.fetchUserInfo());
userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({

View File

@@ -125,6 +125,48 @@ const routes: RouteRecordRaw[] = [
},
],
},
{
meta: {
icon: 'mdi:feature-highlight',
title: $t('page.demos.features.title'),
},
name: 'Features',
path: '/features',
redirect: '/features/hide-menu-children',
children: [
{
name: 'HideChildrenInMenuParent',
path: 'hide-children-in-menu',
component: () =>
import('#/views/demos/features/hide-menu-children/parent.vue'),
meta: {
hideChildrenInMenu: true,
icon: 'ic:round-menu',
title: 'page.demos.features.hideChildrenInMenu',
},
children: [
{
name: 'HideChildrenInMenuChildren',
path: 'hide-children-in-menu',
component: () =>
import(
'#/views/demos/features/hide-menu-children/children.vue'
),
},
],
},
{
name: 'LoginExpired',
path: 'login-expired',
component: () =>
import('#/views/demos/features/login-expired/index.vue'),
meta: {
icon: 'mdi:encryption-expiration',
title: $t('page.demos.features.loginExpired'),
},
},
],
},
{
meta: {
icon: 'mdi:lightbulb-error-outline',

View File

@@ -5,7 +5,7 @@ import type { RouteRecordRaw } from 'vue-router';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH } from '@vben/constants';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { useCoreAccessStore } from '@vben-core/stores';
import { defineStore } from 'pinia';
@@ -17,12 +17,10 @@ export const useAccessStore = defineStore('access', () => {
const router = useRouter();
const loading = ref(false);
const showLoginDialog = ref(false);
function setShowLoginDialog(value: boolean) {
showLoginDialog.value = value;
}
const openLoginExpiredModal = ref(false);
const accessToken = computed(() => coreStoreAccess.accessToken);
const refreshToken = computed(() => coreStoreAccess.refreshToken);
const userRoles = computed(() => coreStoreAccess.userRoles);
const userInfo = computed(() => coreStoreAccess.userInfo);
const accessRoutes = computed(() => coreStoreAccess.accessRoutes);
@@ -31,6 +29,10 @@ export const useAccessStore = defineStore('access', () => {
coreStoreAccess.setAccessMenus(menus);
}
function setAccessToken(token: null | string) {
coreStoreAccess.setAccessToken(token);
}
function setAccessRoutes(routes: RouteRecordRaw[]) {
coreStoreAccess.setAccessRoutes(routes);
}
@@ -70,7 +72,7 @@ export const useAccessStore = defineStore('access', () => {
coreStoreAccess.setUserInfo(userInfo);
coreStoreAccess.setAccessCodes(accessCodes);
showLoginDialog.value = false;
openLoginExpiredModal.value = false;
onSuccess
? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
@@ -85,6 +87,19 @@ export const useAccessStore = defineStore('access', () => {
};
}
async function logout() {
coreStoreAccess.$reset();
openLoginExpiredModal.value = false;
// 回登陆页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
},
});
}
async function fetchUserInfo() {
let userInfo: UserInfo | null = null;
userInfo = await getUserInfo();
@@ -102,11 +117,13 @@ export const useAccessStore = defineStore('access', () => {
authLogin,
fetchUserInfo,
loading,
logout,
openLoginExpiredModal,
refreshToken,
reset,
setAccessMenus,
setAccessRoutes,
setShowLoginDialog,
showLoginDialog,
setAccessToken,
userInfo,
userRoles,
};

View File

@@ -1,16 +1,18 @@
import { useCoreAccessStore, useCoreTabbarStore } from '@vben-core/stores';
import { useCoreTabbarStore } from '@vben-core/stores';
import { defineStore } from 'pinia';
import { useAccessStore } from './access';
export const useAppStore = defineStore('app', () => {
const coreStoreAccess = useCoreAccessStore();
const accessStore = useAccessStore();
const coreTabbarStore = useCoreTabbarStore();
/**
* 重置所有状态
*/
async function resetAppState() {
coreStoreAccess.$reset();
accessStore.$reset();
coreTabbarStore.$reset();
}

View File

@@ -0,0 +1,3 @@
<template>
<div>children</div>
</template>

View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
import { Fallback } from '@vben/universal-ui';
defineOptions({ name: 'HideMenuChildren' });
</script>
<template>
<Fallback
description="当前菜单子菜单不可见"
status="comming-soon"
title="隐藏子菜单"
/>
</template>

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import type { LoginExpiredModeType } from '@vben-core/preferences';
import { preferences, updatePreferences } from '@vben-core/preferences';
import { Button } from 'ant-design-vue';
import { getMockStatus } from '#/apis';
defineOptions({ name: 'LoginExpired' });
async function handleClick(type: LoginExpiredModeType) {
const loginExpiredMode = preferences.app.loginExpiredMode;
updatePreferences({ app: { loginExpiredMode: type } });
await getMockStatus('401');
updatePreferences({ app: { loginExpiredMode } });
}
</script>
<template>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">登录过期演示</h1>
<div class="text-foreground/80 mt-2">
401状态码转到登录页登录成功后跳转回原页面
</div>
</div>
<div class="card-box mt-5 p-5 font-semibold">
<div class="mb-3 text-lg">跳转登录页面方式</div>
<Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
</div>
<div class="card-box mt-5 p-5 font-semibold">
<div class="mb-3 text-lg">登录弹窗方式</div>
<Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
</div>
</div>
</template>