Commit c1286178 by User

全局登录弹窗,及外部axios请求拦截

parent 07405b7b
<!doctype html> <!doctype html>
<html lang="zh-cmn-Hans"> <html lang="zh-cmn-Hans">
<head> <head>
<meta name="buildTime" content="2025-06-16 14:15:36"> <meta name="buildTime" content="2025-06-17 09:17:01">
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" /> <link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<title>VueDashboard</title> <title>VueDashboard</title>
<script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-C1MVrFI5.js"></script> <script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-M4Qam_R_.js"></script>
<link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-BLjiwC98.css"> <link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-B2SFJ6Fn.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
......
<script setup lang="ts"> <script setup lang="ts">
import { createTextVNode, defineComponent } from 'vue'; import { createTextVNode, defineComponent, ref } from 'vue';
import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui'; import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui';
import ReauthModal from './reauth-modal.vue';
defineOptions({ defineOptions({
name: 'AppProvider' name: 'AppProvider'
}); });
const showReauthModal = ref(false);
const ContextHolder = defineComponent({ const ContextHolder = defineComponent({
name: 'ContextHolder', name: 'ContextHolder',
setup() { setup() {
...@@ -14,6 +17,16 @@ const ContextHolder = defineComponent({ ...@@ -14,6 +17,16 @@ const ContextHolder = defineComponent({
window.$dialog = useDialog(); window.$dialog = useDialog();
window.$message = useMessage(); window.$message = useMessage();
window.$notification = useNotification(); window.$notification = useNotification();
// 全局重新认证函数
window.$reauth = {
show: () => {
showReauthModal.value = true;
},
hide: () => {
showReauthModal.value = false;
}
};
} }
register(); register();
...@@ -21,6 +34,14 @@ const ContextHolder = defineComponent({ ...@@ -21,6 +34,14 @@ const ContextHolder = defineComponent({
return () => createTextVNode(); return () => createTextVNode();
} }
}); });
function handleReauthSuccess() {
showReauthModal.value = false;
}
function handleReauthCancel() {
showReauthModal.value = false;
}
</script> </script>
<template> <template>
...@@ -30,6 +51,7 @@ const ContextHolder = defineComponent({ ...@@ -30,6 +51,7 @@ const ContextHolder = defineComponent({
<NMessageProvider> <NMessageProvider>
<ContextHolder /> <ContextHolder />
<slot></slot> <slot></slot>
<ReauthModal v-model:show="showReauthModal" @success="handleReauthSuccess" @cancel="handleReauthCancel" />
</NMessageProvider> </NMessageProvider>
</NNotificationProvider> </NNotificationProvider>
</NDialogProvider> </NDialogProvider>
......
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { NButton, NCard, NForm, NFormItem, NIcon, NInput, NModal, NText } from 'naive-ui';
import { $t } from '@/locales';
import { useAuthStore } from '@/store/modules/auth';
import { useReauthStore } from '@/store/modules/reauth';
defineOptions({
name: 'ReauthModal'
});
interface Props {
show: boolean;
}
interface Emits {
(e: 'update:show', value: boolean): void;
(e: 'success'): void;
(e: 'cancel'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const authStore = useAuthStore();
const reauthStore = useReauthStore();
const formRef = ref<FormInst | null>(null);
const loading = ref(false);
const visible = computed({
get: () => props.show,
set: (value: boolean) => emit('update:show', value)
});
const formData = reactive({
userName: '',
password: ''
});
const rules: FormRules = {
userName: {
required: true,
message: $t('form.userName.required'),
trigger: 'blur'
},
password: {
required: true,
message: $t('form.pwd.required'),
trigger: 'blur'
}
};
async function handleSubmit() {
if (!formRef.value) return;
await formRef.value.validate();
loading.value = true;
try {
await authStore.login(formData.userName, formData.password, false);
// 重置表单
formData.userName = '';
formData.password = '';
// 标记重新认证成功
reauthStore.reauthSuccess();
emit('success');
visible.value = false;
window.$message?.success($t('page.login.common.loginSuccess'));
} catch (error) {
console.error('Re-authentication failed:', error);
// 标记重新认证失败
reauthStore.reauthFailed();
window.$message?.error($t('common.error'));
} finally {
loading.value = false;
}
}
function handleCancel() {
// 重置表单
formData.userName = '';
formData.password = '';
// 标记重新认证失败
reauthStore.reauthFailed();
emit('cancel');
visible.value = false;
// 如果用户取消,则执行登出
authStore.resetStore();
}
</script>
<template>
<NModal v-model:show="visible" :mask-closable="false" :closable="false" :auto-focus="false">
<NCard
style="width: 400px"
:title="$t('page.login.common.loginOrRegister')"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<div class="mb-4">
<NText type="warning">
{{ $t('request.tokenExpired') }}
</NText>
</div>
<NForm
ref="formRef"
:model="formData"
:rules="rules"
size="large"
:show-label="false"
@submit.prevent="handleSubmit"
>
<NFormItem path="userName">
<NInput
v-model:value="formData.userName"
:placeholder="$t('page.login.common.userNamePlaceholder')"
:input-props="{ autocomplete: 'username' }"
>
<template #prefix>
<NIcon :size="18" color="var(--text-color-3)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</NIcon>
</template>
</NInput>
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="formData.password"
type="password"
show-password-on="mousedown"
:placeholder="$t('page.login.common.passwordPlaceholder')"
:input-props="{ autocomplete: 'current-password' }"
@keydown.enter="handleSubmit"
>
<template #prefix>
<NIcon :size="18" color="var(--text-color-3)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"
/>
</svg>
</NIcon>
</template>
</NInput>
</NFormItem>
</NForm>
<template #footer>
<div class="flex justify-end space-x-3">
<NButton @click="handleCancel">
{{ $t('common.cancel') }}
</NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
</div>
</template>
</NCard>
</NModal>
</template>
<style scoped>
.space-x-3 > * + * {
margin-left: 0.75rem;
}
</style>
...@@ -3,5 +3,6 @@ export enum SetupStoreId { ...@@ -3,5 +3,6 @@ export enum SetupStoreId {
Theme = 'theme-store', Theme = 'theme-store',
Auth = 'auth-store', Auth = 'auth-store',
Route = 'route-store', Route = 'route-store',
Tab = 'tab-store' Tab = 'tab-store',
Reauth = 'reauth-store'
} }
...@@ -55,7 +55,7 @@ const local: App.I18n.Schema = { ...@@ -55,7 +55,7 @@ const local: App.I18n.Schema = {
logoutWithModal: '请求失败后弹出模态框再登出用户', logoutWithModal: '请求失败后弹出模态框再登出用户',
logoutWithModalMsg: '用户状态失效,请重新登录', logoutWithModalMsg: '用户状态失效,请重新登录',
refreshToken: '请求的token已过期,刷新token', refreshToken: '请求的token已过期,刷新token',
tokenExpired: 'token已过期' tokenExpired: '登录已过期,请重新登录'
}, },
theme: { theme: {
themeSchema: { themeSchema: {
...@@ -173,7 +173,7 @@ const local: App.I18n.Schema = { ...@@ -173,7 +173,7 @@ const local: App.I18n.Schema = {
page: { page: {
login: { login: {
common: { common: {
loginOrRegister: '登录 / 注册', loginOrRegister: '重新登录',
userNamePlaceholder: '请输入用户名', userNamePlaceholder: '请输入用户名',
phonePlaceholder: '请输入手机号', phonePlaceholder: '请输入手机号',
codePlaceholder: '请输入验证码', codePlaceholder: '请输入验证码',
......
...@@ -12,6 +12,7 @@ import { setupDayjs, setupIconifyOffline, setupLoading, setupNProgress } from '. ...@@ -12,6 +12,7 @@ import { setupDayjs, setupIconifyOffline, setupLoading, setupNProgress } from '.
import { setupStore } from './store'; import { setupStore } from './store';
import { setupRouter } from './router'; import { setupRouter } from './router';
import { setupI18n } from './locales'; import { setupI18n } from './locales';
import { setupGlobalAxiosInterceptor, setupGlobalFetchInterceptor } from './utils/global-axios-interceptor';
import App from './App.vue'; import App from './App.vue';
async function setupApp() { async function setupApp() {
...@@ -24,6 +25,11 @@ async function setupApp() { ...@@ -24,6 +25,11 @@ async function setupApp() {
setupStore(app); setupStore(app);
await setupRouter(app); await setupRouter(app);
setupI18n(app); setupI18n(app);
// 设置全局401错误拦截器
setupGlobalAxiosInterceptor();
setupGlobalFetchInterceptor();
app.use(VueGridLayout); app.use(VueGridLayout);
app.use(MateChat); app.use(MateChat);
// 全局注册 wangeditor 组件 // 全局注册 wangeditor 组件
......
import type { AxiosResponse } from 'axios'; import type { AxiosResponse } from 'axios';
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios'; import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
import { useAuthStore } from '@/store/modules/auth'; import { useAuthStore } from '@/store/modules/auth';
import { useReauthStore } from '@/store/modules/reauth';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service'; import { getServiceBaseURL } from '@/utils/service';
...@@ -112,6 +113,24 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt ...@@ -112,6 +113,24 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
backendErrorCode = String(error.response?.data?.code || ''); backendErrorCode = String(error.response?.data?.code || '');
} }
// handle 401 unauthorized error - show reauth modal
if (error.response?.status === 401) {
const reauthStore = useReauthStore();
console.warn('401 Unauthorized - showing reauth modal');
// 如果还没有在重新认证中,则开始重新认证流程
if (!reauthStore.isReauthenticating) {
reauthStore.startReauth();
window.$reauth?.show();
}
// 将失败的请求添加到队列中,等待重新认证后重试
return new Promise((resolve, reject) => {
reauthStore.addFailedRequest(error.config!, resolve, reject);
});
}
// the error message is displayed in the modal // the error message is displayed in the modal
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || []; const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(backendErrorCode)) { if (modalLogoutCodes.includes(backendErrorCode)) {
......
import { ref } from 'vue';
import { defineStore } from 'pinia';
import type { AxiosRequestConfig } from 'axios';
import { SetupStoreId } from '@/enum';
interface FailedRequest {
config: AxiosRequestConfig;
resolve: (value: any) => void;
reject: (reason: any) => void;
}
export const useReauthStore = defineStore(SetupStoreId.Reauth, () => {
const isReauthenticating = ref(false);
const failedRequestsQueue = ref<FailedRequest[]>([]);
/**
* 添加失败的请求到队列
*/
function addFailedRequest(config: AxiosRequestConfig, resolve: (value: any) => void, reject: (reason: any) => void) {
failedRequestsQueue.value.push({ config, resolve, reject });
}
/**
* 开始重新认证流程
*/
function startReauth() {
isReauthenticating.value = true;
}
/**
* 重新认证成功,重新发送所有失败的请求
*/
function reauthSuccess() {
isReauthenticating.value = false;
// 重新发送所有失败的请求
failedRequestsQueue.value.forEach(async ({ config, resolve, reject }) => {
try {
// 动态导入axios来重新发送请求
const axios = await import('axios');
// 更新token
const token = localStorage.getItem('token');
if (token && config.headers) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 重新发送请求
const response = await axios.default.request(config);
resolve(response);
} catch (error) {
reject(error);
}
});
failedRequestsQueue.value = [];
}
/**
* 重新认证失败,拒绝所有失败的请求
*/
function reauthFailed() {
isReauthenticating.value = false;
// 拒绝所有失败的请求
failedRequestsQueue.value.forEach(({ reject }) => {
reject(new Error('Authentication failed'));
});
failedRequestsQueue.value = [];
}
/**
* 重置状态
*/
function reset() {
isReauthenticating.value = false;
failedRequestsQueue.value = [];
}
return {
isReauthenticating,
failedRequestsQueue,
addFailedRequest,
startReauth,
reauthSuccess,
reauthFailed,
reset
};
});
...@@ -61,6 +61,7 @@ declare module 'vue' { ...@@ -61,6 +61,7 @@ declare module 'vue' {
NTooltip: typeof import('naive-ui')['NTooltip'] NTooltip: typeof import('naive-ui')['NTooltip']
NWatermark: typeof import('naive-ui')['NWatermark'] NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default'] PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReauthModal: typeof import('./../components/common/reauth-modal.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default'] ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
......
...@@ -12,6 +12,11 @@ declare global { ...@@ -12,6 +12,11 @@ declare global {
$message?: import('naive-ui').MessageProviderInst; $message?: import('naive-ui').MessageProviderInst;
/** Notification instance */ /** Notification instance */
$notification?: import('naive-ui').NotificationProviderInst; $notification?: import('naive-ui').NotificationProviderInst;
/** Reauth instance */
$reauth?: {
show: () => void;
hide: () => void;
};
uiGlobalConfig: { uiGlobalConfig: {
InternalCode: string; InternalCode: string;
[key: string]: any; [key: string]: any;
......
import axios from 'axios';
import type { AxiosError, AxiosResponse } from 'axios';
import { useReauthStore } from '@/store/modules/reauth';
/**
* 处理401错误的通用函数
*/
export function handle401Error(error: AxiosError): Promise<any> {
const reauthStore = useReauthStore();
console.warn('Global 401 Unauthorized - showing reauth modal');
// 如果还没有在重新认证中,则开始重新认证流程
if (!reauthStore.isReauthenticating) {
reauthStore.startReauth();
window.$reauth?.show();
}
// 将失败的请求添加到队列中,等待重新认证后重试
return new Promise((resolve, reject) => {
reauthStore.addFailedRequest(error.config!, resolve, reject);
});
}
/**
* 设置全局axios拦截器
*/
export function setupGlobalAxiosInterceptor() {
// 响应拦截器
axios.interceptors.response.use(
(response: AxiosResponse) => {
// 正常响应直接返回
return response;
},
(error: AxiosError) => {
// 检查是否为401错误
if (error.response?.status === 401) {
return handle401Error(error);
}
// 其他错误正常抛出
return Promise.reject(error);
}
);
console.log('✅ Global axios interceptor for 401 handling has been set up');
}
/**
* 为指定的axios实例添加401拦截器
*/
export function addAxios401Interceptor(axiosInstance: any) {
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
return handle401Error(error);
}
return Promise.reject(error);
}
);
}
/**
* 创建一个包含401处理的axios实例
*/
export function createAxiosWithAuth(config?: any) {
const instance = axios.create(config);
addAxios401Interceptor(instance);
return instance;
}
/**
* 手动触发401处理(用于fetch或其他HTTP客户端)
*/
export function trigger401Handler(requestConfig?: any) {
const reauthStore = useReauthStore();
console.warn('Manual 401 trigger - showing reauth modal');
if (!reauthStore.isReauthenticating) {
reauthStore.startReauth();
window.$reauth?.show();
}
// 如果有请求配置,也加入队列
if (requestConfig) {
return new Promise((resolve, reject) => {
reauthStore.addFailedRequest(requestConfig, resolve, reject);
});
}
}
/**
* 全局fetch拦截器(实验性)
*/
export function setupGlobalFetchInterceptor() {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
try {
const response = await originalFetch(...args);
// 检查401错误
if (response.status === 401) {
console.warn('Global fetch 401 Unauthorized - showing reauth modal');
const reauthStore = useReauthStore();
if (!reauthStore.isReauthenticating) {
reauthStore.startReauth();
window.$reauth?.show();
}
// 为fetch创建一个promise队列处理
return new Promise((resolve, reject) => {
const requestConfig = {
url: args[0],
options: args[1]
};
reauthStore.addFailedRequest(requestConfig, resolve, reject);
});
}
return response;
} catch (error) {
throw error;
}
};
console.log('✅ Global fetch interceptor for 401 handling has been set up');
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment