353 lines
9.6 KiB
TypeScript
353 lines
9.6 KiB
TypeScript
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||
|
||
// 基础配置
|
||
export const BASE_URL = 'https://yifan.action-ai.cn/api/v1/yifan';
|
||
|
||
// export const BASE_URL = 'http://192.168.1.24:8001/api/v1/yifan';
|
||
// export const BASE_URL = 'http://localhost:8001/api/v1/yifan';
|
||
|
||
const TIMEOUT = 60000; // 60秒超时
|
||
|
||
// 开发模式配置(简化版本,避免 TypeScript 类型问题)
|
||
const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost';
|
||
const enableDebugLog = isDev; // 开发环境启用详细日志
|
||
|
||
// 导入登录相关工具
|
||
import { navigateToLogin, clearToken } from './auth';
|
||
import { showLoading as uniShowLoading, hideLoading as uniHideLoading, showToast as uniShowToast } from './uni-compat';
|
||
|
||
// 响应数据类型
|
||
interface ApiResponse<T = any> {
|
||
code: number;
|
||
msg: string;
|
||
data: T;
|
||
}
|
||
|
||
// 请求配置类型
|
||
export interface RequestConfig {
|
||
url: string;
|
||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||
data?: object;
|
||
params?: object;
|
||
headers?: object;
|
||
showLoading?: boolean;
|
||
showError?: boolean;
|
||
timeout?: number;
|
||
/** 为 false 时,业务/HTTP 鉴权类错误不执行 clearToken + 跳转登录(用于仅更新资料等场景) */
|
||
clearAuthOnError?: boolean;
|
||
}
|
||
|
||
// 创建 axios 实例
|
||
const axiosInstance = axios.create({
|
||
baseURL: BASE_URL,
|
||
timeout: TIMEOUT,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
// 禁用自动 JSON 转换,我们将手动处理以避免解析错误
|
||
transformResponse: [(data) => {
|
||
// 如果数据为空,直接返回
|
||
if (!data) return data;
|
||
|
||
// 如果已经是对象,直接返回
|
||
if (typeof data === 'object') return data;
|
||
|
||
// 如果是字符串,手动解析
|
||
if (typeof data === 'string') {
|
||
try {
|
||
// 清理 BOM 和其他不可见字符
|
||
const cleanData = data.trim().replace(/^\uFEFF/, '');
|
||
|
||
// 检查是否是 HTML
|
||
if (cleanData.startsWith('<') || cleanData.startsWith('<!DOCTYPE')) {
|
||
throw new Error('服务器返回了 HTML 页面');
|
||
}
|
||
|
||
// 尝试解析 JSON
|
||
if (cleanData.startsWith('{') || cleanData.startsWith('[')) {
|
||
return JSON.parse(cleanData);
|
||
}
|
||
|
||
return cleanData;
|
||
} catch (e) {
|
||
console.error('transformResponse JSON 解析失败:', e);
|
||
// 返回原始数据,让后续处理
|
||
return data;
|
||
}
|
||
}
|
||
|
||
return data;
|
||
}],
|
||
});
|
||
|
||
// 请求拦截器
|
||
axiosInstance.interceptors.request.use(
|
||
(config) => {
|
||
// 获取 token
|
||
const token = localStorage.getItem('token');
|
||
if (token) {
|
||
try {
|
||
// 尝试解析 JSON(如果 token 是 JSON 字符串格式存储的)
|
||
const parsedToken = JSON.parse(token);
|
||
config.headers.Authorization = `Bearer ${parsedToken}`;
|
||
} catch (e) {
|
||
// 如果不是 JSON,直接使用原始值
|
||
config.headers.Authorization = `Bearer ${token}`;
|
||
}
|
||
}
|
||
return config;
|
||
},
|
||
(error) => {
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
|
||
// 响应拦截器(统一处理)
|
||
axiosInstance.interceptors.response.use(
|
||
(response: AxiosResponse) => {
|
||
|
||
|
||
// transformResponse 已经处理了 JSON 解析
|
||
// 这里只做最后的检查
|
||
if (typeof response.data === 'string') {
|
||
const cleanData = response.data.trim();
|
||
|
||
// 如果是 HTML,拒绝
|
||
if (cleanData.startsWith('<') || cleanData.startsWith('<!DOCTYPE')) {
|
||
console.error('收到 HTML 响应:', cleanData.substring(0, 200));
|
||
return Promise.reject(new Error('服务器返回了 HTML 页面'));
|
||
}
|
||
|
||
// 如果是 JSON 字符串但未被解析,尝试解析
|
||
if (cleanData.startsWith('{') || cleanData.startsWith('[')) {
|
||
try {
|
||
response.data = JSON.parse(cleanData);
|
||
} catch (e) {
|
||
console.error('响应拦截器 JSON 解析失败:', e);
|
||
return Promise.reject(new Error('服务器返回了无效的 JSON 格式'));
|
||
}
|
||
}
|
||
}
|
||
|
||
return response;
|
||
},
|
||
(error) => {
|
||
if (enableDebugLog) {
|
||
console.error('响应拦截器错误:', error);
|
||
}
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
|
||
// 封装请求方法
|
||
const request = <T = any>(config: RequestConfig): Promise<T> => {
|
||
const {
|
||
url,
|
||
method = 'GET',
|
||
data,
|
||
params,
|
||
headers = {},
|
||
showLoading = false,
|
||
showError = true,
|
||
timeout = TIMEOUT,
|
||
clearAuthOnError = true,
|
||
} = config;
|
||
|
||
// 显示加载
|
||
if (showLoading) {
|
||
uniShowLoading({ title: '加载中...', mask: true });
|
||
}
|
||
|
||
const axiosConfig: AxiosRequestConfig = {
|
||
url,
|
||
method,
|
||
data,
|
||
params,
|
||
headers,
|
||
timeout,
|
||
};
|
||
|
||
return axiosInstance
|
||
.request(axiosConfig)
|
||
.then((res: AxiosResponse) => {
|
||
if (showLoading) {
|
||
uniHideLoading();
|
||
}
|
||
|
||
// axios 已经通过响应拦截器处理了数据
|
||
// 这里 res.data 应该已经是解析后的对象
|
||
if (res.data === null || res.data === undefined) {
|
||
console.warn('响应数据为空');
|
||
return null as T;
|
||
}
|
||
|
||
const response = res.data as ApiResponse<T> & { status_code?: number; success?: boolean };
|
||
|
||
// 业务状态码检查
|
||
if (
|
||
response.code === undefined ||
|
||
response.code === 200 ||
|
||
response.code === 0 ||
|
||
response.success === true
|
||
) {
|
||
return response.data !== undefined
|
||
? response.data
|
||
: (response as unknown as T);
|
||
} else {
|
||
// 检查是否是 token / 认证相关错误(无论当前是否有 token,都统一跳转登录)
|
||
const msg = response.msg || '';
|
||
const bizCode = response.code;
|
||
const statusCode = (response as any).status_code;
|
||
if (
|
||
clearAuthOnError &&
|
||
(bizCode === 401 ||
|
||
bizCode === 10401 || // 业务约定:10401 代表认证失败
|
||
bizCode === 10001 ||
|
||
statusCode === 401 ||
|
||
msg.includes('token') ||
|
||
msg.includes('登录') ||
|
||
msg.includes('认证失败'))
|
||
) {
|
||
clearToken();
|
||
setTimeout(() => {
|
||
navigateToLogin();
|
||
}, 500);
|
||
}
|
||
|
||
if (showError) {
|
||
uniShowToast({ title: response.msg || '请求失败', icon: 'none' });
|
||
}
|
||
|
||
const err: any = new Error(response.msg || '请求失败');
|
||
err.msg = response.msg || '请求失败';
|
||
err.code = response.code;
|
||
throw err;
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
if (showLoading) {
|
||
uniHideLoading();
|
||
}
|
||
|
||
let msg = '请求失败';
|
||
|
||
// 记录详细错误信息用于调试
|
||
console.error('请求错误详情:', {
|
||
url: config.url,
|
||
method: config.method,
|
||
error: error.message,
|
||
response: error.response?.data,
|
||
status: error.response?.status,
|
||
});
|
||
|
||
if (error.response) {
|
||
// 服务器返回了错误状态码
|
||
const { status, data } = error.response;
|
||
const dataMessage =
|
||
typeof data === 'string' ? data : data?.msg || data?.message || data?.detail;
|
||
|
||
switch (status) {
|
||
case 401:
|
||
msg = dataMessage || '未授权,请重新登录';
|
||
if (clearAuthOnError) {
|
||
clearToken();
|
||
setTimeout(() => {
|
||
navigateToLogin();
|
||
}, 500);
|
||
}
|
||
break;
|
||
case 403:
|
||
msg = dataMessage || '拒绝访问';
|
||
if (
|
||
clearAuthOnError &&
|
||
(data?.code === 401 ||
|
||
data?.code === 10401 ||
|
||
data?.msg?.includes('token') ||
|
||
data?.msg?.includes('登录') ||
|
||
data?.msg?.includes('认证失败'))
|
||
) {
|
||
clearToken();
|
||
setTimeout(() => {
|
||
navigateToLogin();
|
||
}, 500);
|
||
}
|
||
break;
|
||
case 404:
|
||
msg = dataMessage || '请求地址不存在';
|
||
break;
|
||
case 500:
|
||
msg = dataMessage || '服务器内部错误';
|
||
break;
|
||
case 502:
|
||
msg = '网关错误';
|
||
break;
|
||
case 503:
|
||
msg = '服务暂时不可用';
|
||
break;
|
||
case 504:
|
||
msg = '网关超时';
|
||
break;
|
||
default:
|
||
msg = dataMessage || `请求失败 (${status})`;
|
||
}
|
||
} else if (error.request) {
|
||
// 请求已发出但没有收到响应
|
||
msg = '网络错误,请检查网络连接';
|
||
} else {
|
||
// 其他错误(包括 JSON 解析错误)
|
||
msg = error.message || '请求失败';
|
||
|
||
// 特殊处理 JSON 解析错误
|
||
if (msg.includes('JSON') || msg.includes('parse')) {
|
||
msg = '服务器响应格式错误';
|
||
}
|
||
}
|
||
|
||
if (showError) {
|
||
uniShowToast({ title: msg, icon: 'none' });
|
||
}
|
||
|
||
const err: any = new Error(msg);
|
||
err.msg = msg;
|
||
err.originalError = error;
|
||
throw err;
|
||
});
|
||
};
|
||
|
||
// 导出便捷方法
|
||
export const http = {
|
||
get<T = any>(
|
||
url: string,
|
||
params?: object,
|
||
options?: Partial<RequestConfig>
|
||
): Promise<T> {
|
||
return request<T>({ url, method: 'GET', params, ...options });
|
||
},
|
||
|
||
post<T = any>(
|
||
url: string,
|
||
data?: object,
|
||
options?: Partial<RequestConfig>
|
||
): Promise<T> {
|
||
return request<T>({ url, method: 'POST', data, ...options });
|
||
},
|
||
|
||
put<T = any>(
|
||
url: string,
|
||
data?: object,
|
||
options?: Partial<RequestConfig>
|
||
): Promise<T> {
|
||
return request<T>({ url, method: 'PUT', data, ...options });
|
||
},
|
||
|
||
delete<T = any>(
|
||
url: string,
|
||
params?: object,
|
||
options?: Partial<RequestConfig>
|
||
): Promise<T> {
|
||
return request<T>({ url, method: 'DELETE', params, ...options });
|
||
},
|
||
};
|
||
|
||
export default http;
|