Files
admin-ui/src/utils/request.ts
2910410219 f89063af6f refactor(路由与用户管理): 优化路由处理和用户登出逻辑
- 修改用户登出时的重定向逻辑,确保用户显式返回登录页,避免保留重定向参数
- 引入默认动态路由子项,简化路由配置
- 更新后端路由初始化逻辑,确保动态路由的正确处理
- 增强代码可读性,修复部分代码风格问题
2026-04-08 13:51:43 +08:00

256 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Session } from '/@/utils/storage';
import qs from 'qs';
import { getChangedFields } from '/@/utils/diffUtils';
import { handleModuleNotEnabled } from '/@/utils/assetSubscribe';
// 标记是否正在处理 token 过期,避免重复弹窗
let isHandlingTokenExpired = false;
// 错误消息防抖:防止短时间内显示多个错误消息
let lastErrorTime = 0;
const ERROR_MESSAGE_INTERVAL = 2000; // 2秒内只显示一个错误
const showErrorMessage = (message: string) => {
const now = Date.now();
// 2秒内只显示一个错误消息不管内容是否相同
if (now - lastErrorTime < ERROR_MESSAGE_INTERVAL) {
return; // 跳过
}
lastErrorTime = now;
ElMessage.error(message);
};
// ============================================================
// Axios 实例配置
// 地址配置见 .env.development 文件
// ============================================================
// 统一服务实例端口8000- 全部模块共用
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 50000,
headers: { 'Content-Type': 'application/json' },
paramsSerializer: {
serialize(params) {
return qs.stringify(params, { allowDots: true, arrayFormat: 'brackets' });
},
},
});
// token 过期处理函数
const handleTokenExpired = () => {
if (isHandlingTokenExpired) return;
isHandlingTokenExpired = true;
ElMessageBox.alert('登录状态已过期,请重新登录', '提示', {
confirmButtonText: '确定',
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: false,
beforeClose: (action, _instance, done) => {
if (action === 'confirm') {
done();
performLogout();
}
},
})
.then(() => {
performLogout();
})
.catch(() => {
performLogout();
});
};
// 执行退出登录操作
const performLogout = () => {
Session.clear();
localStorage.clear();
isHandlingTokenExpired = false;
// Hash 路由统一回登录页,避免跳到错误地址
setTimeout(() => {
window.location.href = '/#/login';
}, 500);
};
// 请求拦截器
const requestInterceptor = (config: InternalAxiosRequestConfig) => {
// 检查 token 是否有效
const token = Session.get('token');
if (token) {
// 可以在这里添加 token 有效性检查(如果需要)
config.headers!['Authorization'] = `Bearer ${token}`;
}
// PUT 请求最小化传参处理
// 如果请求数据中包含 _originalData则自动计算差异只传递修改过的字段
if (config.method?.toLowerCase() === 'put' && config.data && typeof config.data === 'object') {
const { _originalData, ...currentData } = config.data;
if (_originalData && typeof _originalData === 'object') {
// 获取 id 字段(必须保留)
const idField = currentData.id || currentData.Id || currentData.ID;
// 计算差异
const changedFields = getChangedFields(_originalData, currentData, {
exclude: ['_originalData', 'id', 'Id', 'ID'],
});
// 如果有变化,只传递 id + 变化的字段
if (Object.keys(changedFields).length > 0) {
config.data = { id: idField, ...changedFields };
} else {
// 没有变化,只传递 id
config.data = { id: idField };
}
console.log('[最小化传参] 原始字段数:', Object.keys(currentData).length, '-> 传递字段数:', Object.keys(config.data).length);
}
}
return config;
};
const requestErrorHandler = (error: any) => {
return Promise.reject(error);
};
// 响应拦截器
const responseInterceptor = (response: AxiosResponse) => {
// 文件流响应直接返回
if (
response.config.responseType === 'blob' ||
response.headers['content-type']?.includes('application/zip') ||
response.headers['content-type']?.includes('application/octet-stream')
) {
return response;
}
const res = response.data;
const httpStatus = response.status;
const code = res?.code;
const message = res?.message;
// 检查 token 相关错误
if (
httpStatus === 401 ||
code === 401 ||
message?.includes('token') ||
message === 'token is invalid' ||
message === 'token 解析失败' ||
message?.includes('decrypt error')
) {
handleTokenExpired();
return Promise.reject(new Error('登录状态已过期'));
}
// 处理模块未开通错误 (403)
// 跳过资产SKU查询接口避免弹窗内部请求触发循环
const requestUrl = response.config.url || '';
if (code === 402 && !requestUrl.includes('/assets/asset/sku/')) {
// 获取当前路由路径
const currentPath = window.location.hash.replace('#', '') || window.location.pathname;
console.log('[request.ts] 检测到403错误当前路径:', currentPath);
handleModuleNotEnabled(currentPath);
// 直接返回,不再显示错误消息
return Promise.reject(new Error('模块未开通'));
}
// 业务逻辑错误处理排除403因为上面已处理
if (code !== undefined && code !== 0 && code !== 200 && code !== 403) {
const errorMsg = message || `请求失败(${code})`;
showErrorMessage(errorMsg);
return Promise.reject(new Error(errorMsg));
}
return res;
};
// 响应错误拦截器
const responseErrorHandler = (error: any) => {
console.error('API请求错误:', error);
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
showErrorMessage('请求超时,请检查网络连接');
return Promise.reject(new Error('请求超时'));
}
if (!error.response) {
if (error.message === 'Network Error') {
// ElMessage.error('网络连接错误,请检查网络设置');
} else {
// ElMessage.error('网络异常,请检查连接');
}
return Promise.reject(error);
}
const httpStatus = error.response.status;
// 优先使用返回数据中的 message 字段
const responseMessage = error.response.data?.message;
// 处理 HTTP 错误状态
const requestUrl = error.response.config?.url || '';
switch (httpStatus) {
case 401:
handleTokenExpired();
break;
case 402:
// 模块未开通处理跳过SKU相关接口避免循环
if (!requestUrl.includes('/assets/asset/sku/') && !requestUrl.includes('getAssetAndSku')) {
// 检查是否刚从开通页面返回5秒内不再跳转
const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime');
const now = Date.now();
if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) {
console.log('[responseErrorHandler] 刚完成开通跳过402处理');
showErrorMessage(responseMessage || '服务开通中,请稍后刷新页面');
return Promise.reject(new Error('模块开通中'));
}
const currentPath = window.location.hash.replace('#', '') || window.location.pathname;
console.log('[responseErrorHandler] 检测到HTTP 402错误当前路径:', currentPath);
handleModuleNotEnabled(currentPath);
return Promise.reject(new Error('模块未开通'));
}
showErrorMessage(responseMessage || '服务未开通');
break;
case 403:
showErrorMessage(responseMessage || '没有权限访问该资源');
break;
case 404:
showErrorMessage(responseMessage || '请求的资源不存在');
break;
case 429:
showErrorMessage(responseMessage || '请求过于频繁,请稍后再试');
handleTokenExpired();
break;
case 500:
showErrorMessage(responseMessage || '服务器内部错误');
break;
case 502:
showErrorMessage(responseMessage || '网关错误');
break;
case 503:
showErrorMessage(responseMessage || '服务不可用');
break;
default:
if (httpStatus >= 400) {
showErrorMessage(responseMessage || `请求失败(${httpStatus})`);
}
}
return Promise.reject(error);
};
// 为实例添加拦截器
service.interceptors.request.use(requestInterceptor, requestErrorHandler);
service.interceptors.response.use(responseInterceptor, responseErrorHandler);
// 导出
export default service;
export { showErrorMessage };