perf: improve the logic related to login expiration
This commit is contained in:
23
apps/backend-mock/src/modules/mock/mock.controller.ts
Normal file
23
apps/backend-mock/src/modules/mock/mock.controller.ts
Normal 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}`,
|
||||
});
|
||||
}
|
||||
}
|
@@ -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],
|
||||
})
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export * from './menu';
|
||||
export * from './mock';
|
||||
export * from './user';
|
||||
|
10
apps/web-antd/src/apis/modules/mock.ts
Normal file
10
apps/web-antd/src/apis/modules/mock.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requestClient } from '#/forward';
|
||||
|
||||
/**
|
||||
* 模拟人意状态码
|
||||
*/
|
||||
async function getMockStatus(status: string) {
|
||||
return requestClient.get('/mock/status', { params: { status } });
|
||||
}
|
||||
|
||||
export { getMockStatus };
|
@@ -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();
|
||||
|
||||
// 其他配置的请求方法
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,11 @@
|
||||
},
|
||||
"fallback": {
|
||||
"title": "缺省页"
|
||||
},
|
||||
"features": {
|
||||
"title": "功能",
|
||||
"hideChildrenInMenu": "隐藏菜单子项",
|
||||
"loginExpired": "登录过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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({
|
||||
|
@@ -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',
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>children</div>
|
||||
</template>
|
@@ -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>
|
@@ -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>
|
Reference in New Issue
Block a user