|
|
|
|
/**
|
|
|
|
|
* Axios API 配置
|
|
|
|
|
* 统一管理 HTTP 请求
|
|
|
|
|
*
|
|
|
|
|
* 功能特性:
|
|
|
|
|
* - 自动添加 Token
|
|
|
|
|
* - Token 自动刷新
|
|
|
|
|
* - 请求重试机制
|
|
|
|
|
* - 请求取消功能
|
|
|
|
|
* - 统一错误处理
|
|
|
|
|
* - 请求/响应日志
|
|
|
|
|
* - Loading 状态管理
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import axios, {
|
|
|
|
|
AxiosError,
|
|
|
|
|
AxiosRequestConfig,
|
|
|
|
|
AxiosResponse,
|
|
|
|
|
InternalAxiosRequestConfig,
|
|
|
|
|
CancelTokenSource,
|
|
|
|
|
AxiosRequestHeaders,
|
|
|
|
|
} from 'axios';
|
|
|
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
|
import { router } from 'expo-router';
|
|
|
|
|
import { config } from '../config';
|
|
|
|
|
import { transformRequest, parseResponse } from './helper';
|
|
|
|
|
import { cloneDeep, pick, includes } from 'lodash-es';
|
|
|
|
|
import md5 from 'md5';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* API 响应数据结构
|
|
|
|
|
*/
|
|
|
|
|
export interface ApiResponse<T = any> {
|
|
|
|
|
code: number;
|
|
|
|
|
message: string;
|
|
|
|
|
data: T;
|
|
|
|
|
success?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* API 错误响应
|
|
|
|
|
*/
|
|
|
|
|
export interface ApiError {
|
|
|
|
|
code: number;
|
|
|
|
|
message: string;
|
|
|
|
|
errors?: Record<string, string[]>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 请求配置扩展
|
|
|
|
|
*/
|
|
|
|
|
export interface RequestConfig extends AxiosRequestConfig {
|
|
|
|
|
/** 是否显示 loading */
|
|
|
|
|
showLoading?: boolean;
|
|
|
|
|
/** 是否显示错误提示 */
|
|
|
|
|
showError?: boolean;
|
|
|
|
|
/** 是否重试 */
|
|
|
|
|
retry?: boolean;
|
|
|
|
|
/** 重试次数 */
|
|
|
|
|
retryCount?: number;
|
|
|
|
|
/** 是否需要 token */
|
|
|
|
|
requiresAuth?: boolean;
|
|
|
|
|
/** 自定义错误处理 */
|
|
|
|
|
customErrorHandler?: (error: AxiosError<ApiError>) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// API 基础配置
|
|
|
|
|
const API_CONFIG = {
|
|
|
|
|
baseURL: config.api.baseURL,
|
|
|
|
|
timeout: config.api.timeout,
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
|
|
|
Accept: 'application/json, application/xml, text/play, text/html, *.*',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 创建 axios 实例
|
|
|
|
|
const api = axios.create(API_CONFIG);
|
|
|
|
|
|
|
|
|
|
// 请求队列(用于取消请求)
|
|
|
|
|
const pendingRequests = new Map<string, CancelTokenSource>();
|
|
|
|
|
|
|
|
|
|
// 是否正在刷新 token
|
|
|
|
|
let isRefreshing = false;
|
|
|
|
|
|
|
|
|
|
// 刷新 token 时的请求队列
|
|
|
|
|
let refreshSubscribers: Array<(token: string) => void> = [];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成请求唯一标识
|
|
|
|
|
*/
|
|
|
|
|
function generateRequestKey(config: InternalAxiosRequestConfig): string {
|
|
|
|
|
const cmdId = config.headers.cmdId || config.url;
|
|
|
|
|
const data = cloneDeep(config.method === 'post' ? config.data : config.params);
|
|
|
|
|
return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加请求到队列
|
|
|
|
|
*/
|
|
|
|
|
function addPendingRequest(config: InternalAxiosRequestConfig): void {
|
|
|
|
|
const requestKey = generateRequestKey(config);
|
|
|
|
|
|
|
|
|
|
// 如果已存在相同请求,取消之前的请求
|
|
|
|
|
if (pendingRequests.has(requestKey)) {
|
|
|
|
|
const source = pendingRequests.get(requestKey);
|
|
|
|
|
source?.cancel('重复请求已取消');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建新的取消令牌
|
|
|
|
|
const source = axios.CancelToken.source();
|
|
|
|
|
config.cancelToken = source.token;
|
|
|
|
|
pendingRequests.set(requestKey, source);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从队列中移除请求
|
|
|
|
|
*/
|
|
|
|
|
function removePendingRequest(config: InternalAxiosRequestConfig | AxiosRequestConfig): void {
|
|
|
|
|
const requestKey = generateRequestKey(config as InternalAxiosRequestConfig);
|
|
|
|
|
pendingRequests.delete(requestKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 订阅 token 刷新
|
|
|
|
|
*/
|
|
|
|
|
function subscribeTokenRefresh(callback: (token: string) => void): void {
|
|
|
|
|
refreshSubscribers.push(callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 通知所有订阅者 token 已刷新
|
|
|
|
|
*/
|
|
|
|
|
function onTokenRefreshed(token: string): void {
|
|
|
|
|
refreshSubscribers.forEach((callback) => callback(token));
|
|
|
|
|
refreshSubscribers = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 刷新 token
|
|
|
|
|
*/
|
|
|
|
|
async function refreshAccessToken(): Promise<string | null> {
|
|
|
|
|
try {
|
|
|
|
|
const refreshToken = await AsyncStorage.getItem('refresh_token');
|
|
|
|
|
|
|
|
|
|
if (!refreshToken) {
|
|
|
|
|
throw new Error('No refresh token');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 调用刷新 token 接口
|
|
|
|
|
const response = await axios.post<ApiResponse<{ token: string; refreshToken: string }>>(
|
|
|
|
|
`${config.api.baseURL}/auth/refresh-token`,
|
|
|
|
|
{ refreshToken }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { token, refreshToken: newRefreshToken } = response.data.data;
|
|
|
|
|
|
|
|
|
|
// 保存新的 token
|
|
|
|
|
await AsyncStorage.setItem('auth_token', token);
|
|
|
|
|
await AsyncStorage.setItem('refresh_token', newRefreshToken);
|
|
|
|
|
|
|
|
|
|
return token;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// 刷新失败,清除所有 token
|
|
|
|
|
await AsyncStorage.multiRemove(['auth_token', 'refresh_token']);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 请求拦截器
|
|
|
|
|
* 在请求发送前添加 token 等信息
|
|
|
|
|
*/
|
|
|
|
|
api.interceptors.request.use(
|
|
|
|
|
async (config: InternalAxiosRequestConfig) => {
|
|
|
|
|
try {
|
|
|
|
|
// 添加到请求队列(防止重复请求)
|
|
|
|
|
addPendingRequest(config);
|
|
|
|
|
|
|
|
|
|
const { apiName } = config.headers;
|
|
|
|
|
|
|
|
|
|
const { headers, data } = transformRequest(pick(config, ['headers', 'data']));
|
|
|
|
|
|
|
|
|
|
config.headers = {
|
|
|
|
|
...headers,
|
|
|
|
|
...(__DEV__ ? { apiName } : {}),
|
|
|
|
|
} as AxiosRequestHeaders;
|
|
|
|
|
|
|
|
|
|
config.data = data;
|
|
|
|
|
|
|
|
|
|
if (Number(config.headers.cmdId) !== 381120) {
|
|
|
|
|
config.url = '/v2/';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if (__DEV__ && apiName) {
|
|
|
|
|
// config.url = `${config.url}?${apiName}`;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // 从本地存储获取 token
|
|
|
|
|
// const token = await AsyncStorage.getItem('auth_token');
|
|
|
|
|
//
|
|
|
|
|
// // 添加 token 到请求头
|
|
|
|
|
// if (token && config.headers) {
|
|
|
|
|
// config.headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 添加请求时间戳(用于计算请求耗时)
|
|
|
|
|
(config as any).metadata = { startTime: Date.now() };
|
|
|
|
|
|
|
|
|
|
// 打印请求信息(开发环境)
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
console.log('📤 API Request:', {
|
|
|
|
|
method: config.method?.toUpperCase(),
|
|
|
|
|
url: config.url,
|
|
|
|
|
baseURL: config.baseURL,
|
|
|
|
|
fullURL: `${config.baseURL}${config.url}`,
|
|
|
|
|
params: config.params,
|
|
|
|
|
headers: config.headers,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('❌ Request interceptor error:', error);
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
(error) => {
|
|
|
|
|
console.error('❌ Request error:', error);
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 响应拦截器
|
|
|
|
|
* 统一处理响应和错误
|
|
|
|
|
*/
|
|
|
|
|
api.interceptors.response.use(
|
|
|
|
|
async (response: AxiosResponse) => {
|
|
|
|
|
// 从请求队列中移除
|
|
|
|
|
removePendingRequest(response.config);
|
|
|
|
|
|
|
|
|
|
// 计算请求耗时
|
|
|
|
|
const duration = Date.now() - (response.config as any).metadata?.startTime;
|
|
|
|
|
|
|
|
|
|
const resData: any = await parseResponse(response);
|
|
|
|
|
|
|
|
|
|
// 打印响应信息(开发环境)
|
|
|
|
|
// if (__DEV__) {
|
|
|
|
|
// console.log('📥 API Response:', {
|
|
|
|
|
// url: response.config.url,
|
|
|
|
|
// status: response.status,
|
|
|
|
|
// duration: `${duration}ms`,
|
|
|
|
|
// data: response.data,
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 统一处理响应数据格式
|
|
|
|
|
// const apiResponse = response.data as ApiResponse;
|
|
|
|
|
|
|
|
|
|
// 如果后端返回的数据结构包含 code 和 data
|
|
|
|
|
// if (apiResponse && typeof apiResponse === 'object' && 'code' in apiResponse) {
|
|
|
|
|
// // 检查业务状态码
|
|
|
|
|
// if (apiResponse.code !== 0 && apiResponse.code !== 200) {
|
|
|
|
|
// // 业务错误
|
|
|
|
|
// const error = new Error(apiResponse.message || '请求失败') as any;
|
|
|
|
|
// error.code = apiResponse.code;
|
|
|
|
|
// error.response = response;
|
|
|
|
|
// return Promise.reject(error);
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// // 返回 data 字段
|
|
|
|
|
// return apiResponse.data;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 直接返回响应数据
|
|
|
|
|
// return response.data;
|
|
|
|
|
return Promise.resolve(resData);
|
|
|
|
|
},
|
|
|
|
|
async (error: AxiosError<ApiError>) => {
|
|
|
|
|
// 从请求队列中移除
|
|
|
|
|
if (error.config) {
|
|
|
|
|
removePendingRequest(error.config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果是取消的请求,直接返回
|
|
|
|
|
if (axios.isCancel(error)) {
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
console.log('🚫 Request cancelled:', error.message);
|
|
|
|
|
}
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const originalRequest = error.config as RequestConfig & { _retry?: boolean };
|
|
|
|
|
|
|
|
|
|
// 打印错误信息
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
console.error('❌ API Error:', {
|
|
|
|
|
method: error.config?.method,
|
|
|
|
|
cmdId: error.config?.headers?.cmdId,
|
|
|
|
|
status: error.response?.status,
|
|
|
|
|
message: error.message,
|
|
|
|
|
data: error.response?.data,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (includes([500, 502, 503], error.response?.status) && includes(['371130'], `${error.config?.headers?.cmdId}`)) {
|
|
|
|
|
router.replace('/maintenance' as any);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理不同的错误状态码
|
|
|
|
|
if (error.response) {
|
|
|
|
|
const { status, data } = error.response;
|
|
|
|
|
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 401: {
|
|
|
|
|
// Token 过期,尝试刷新
|
|
|
|
|
if (!originalRequest._retry) {
|
|
|
|
|
if (isRefreshing) {
|
|
|
|
|
// 如果正在刷新,将请求加入队列
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
subscribeTokenRefresh((token: string) => {
|
|
|
|
|
if (originalRequest.headers) {
|
|
|
|
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
}
|
|
|
|
|
resolve(api(originalRequest));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
originalRequest._retry = true;
|
|
|
|
|
isRefreshing = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const newToken = await refreshAccessToken();
|
|
|
|
|
|
|
|
|
|
if (newToken) {
|
|
|
|
|
// Token 刷新成功
|
|
|
|
|
isRefreshing = false;
|
|
|
|
|
onTokenRefreshed(newToken);
|
|
|
|
|
|
|
|
|
|
// 重试原请求
|
|
|
|
|
if (originalRequest.headers) {
|
|
|
|
|
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
|
|
|
}
|
|
|
|
|
return api(originalRequest);
|
|
|
|
|
} else {
|
|
|
|
|
// Token 刷新失败,跳转到登录页
|
|
|
|
|
isRefreshing = false;
|
|
|
|
|
await AsyncStorage.multiRemove(['auth_token', 'refresh_token']);
|
|
|
|
|
|
|
|
|
|
// 跳转到登录页
|
|
|
|
|
if (router.canGoBack()) {
|
|
|
|
|
router.replace('/(auth)/login' as any);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (refreshError) {
|
|
|
|
|
isRefreshing = false;
|
|
|
|
|
await AsyncStorage.multiRemove(['auth_token', 'refresh_token']);
|
|
|
|
|
|
|
|
|
|
// 跳转到登录页
|
|
|
|
|
if (router.canGoBack()) {
|
|
|
|
|
router.replace('/(auth)/login' as any);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Promise.reject(refreshError);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 403:
|
|
|
|
|
// 禁止访问
|
|
|
|
|
console.error('❌ 403: 没有权限访问该资源');
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 404:
|
|
|
|
|
// 资源不存在
|
|
|
|
|
console.error('❌ 404: 请求的资源不存在');
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 422:
|
|
|
|
|
// 表单验证错误
|
|
|
|
|
console.error('❌ 422: 表单验证失败', data);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 429:
|
|
|
|
|
// 请求过于频繁
|
|
|
|
|
console.error('❌ 429: 请求过于频繁,请稍后再试');
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 500:
|
|
|
|
|
// 服务器错误
|
|
|
|
|
console.error('❌ 500: 服务器内部错误');
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 502:
|
|
|
|
|
// 网关错误
|
|
|
|
|
console.error('❌ 502: 网关错误');
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 503:
|
|
|
|
|
// 服务不可用
|
|
|
|
|
console.error('❌ 503: 服务暂时不可用');
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
console.error(`❌ ${status}: 未知错误`);
|
|
|
|
|
}
|
|
|
|
|
} else if (error.request) {
|
|
|
|
|
// 请求已发送但没有收到响应
|
|
|
|
|
console.error('❌ 网络错误: 请检查网络连接');
|
|
|
|
|
} else {
|
|
|
|
|
// 请求配置出错
|
|
|
|
|
console.error('❌ 请求配置错误:', error.message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 自定义错误处理
|
|
|
|
|
if (originalRequest?.customErrorHandler) {
|
|
|
|
|
originalRequest.customErrorHandler(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取消所有待处理的请求
|
|
|
|
|
*/
|
|
|
|
|
export function cancelAllRequests(message = '请求已取消'): void {
|
|
|
|
|
pendingRequests.forEach((source) => {
|
|
|
|
|
source.cancel(message);
|
|
|
|
|
});
|
|
|
|
|
pendingRequests.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取消指定 URL 的请求
|
|
|
|
|
*/
|
|
|
|
|
export function cancelRequest(url: string): void {
|
|
|
|
|
pendingRequests.forEach((source, key) => {
|
|
|
|
|
if (key.includes(url)) {
|
|
|
|
|
source.cancel('请求已取消');
|
|
|
|
|
pendingRequests.delete(key);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 通用请求方法(增强版)
|
|
|
|
|
*/
|
|
|
|
|
export const request = {
|
|
|
|
|
/**
|
|
|
|
|
* GET 请求
|
|
|
|
|
*/
|
|
|
|
|
get: <T = any>(url: string, config?: RequestConfig) => api.get<T, T>(url, config),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* POST 请求
|
|
|
|
|
*/
|
|
|
|
|
post: <T = any>(url: string, data?: any, config?: RequestConfig) =>
|
|
|
|
|
api.post<T, T>(url, data, config),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* PUT 请求
|
|
|
|
|
*/
|
|
|
|
|
put: <T = any>(url: string, data?: any, config?: RequestConfig) =>
|
|
|
|
|
api.put<T, T>(url, data, config),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DELETE 请求
|
|
|
|
|
*/
|
|
|
|
|
delete: <T = any>(url: string, config?: RequestConfig) => api.delete<T, T>(url, config),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* PATCH 请求
|
|
|
|
|
*/
|
|
|
|
|
patch: <T = any>(url: string, data?: any, config?: RequestConfig) =>
|
|
|
|
|
api.patch<T, T>(url, data, config),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 上传文件
|
|
|
|
|
*/
|
|
|
|
|
upload: <T = any>(
|
|
|
|
|
url: string,
|
|
|
|
|
file: File | Blob,
|
|
|
|
|
onProgress?: (progress: number) => void,
|
|
|
|
|
config?: RequestConfig
|
|
|
|
|
) => {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
|
|
|
|
return api.post<T, T>(url, formData, {
|
|
|
|
|
...config,
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'multipart/form-data',
|
|
|
|
|
...config?.headers,
|
|
|
|
|
},
|
|
|
|
|
onUploadProgress: (progressEvent) => {
|
|
|
|
|
if (onProgress && progressEvent.total) {
|
|
|
|
|
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
|
|
|
onProgress(progress);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 下载文件
|
|
|
|
|
*/
|
|
|
|
|
download: async (
|
|
|
|
|
url: string,
|
|
|
|
|
filename?: string,
|
|
|
|
|
onProgress?: (progress: number) => void,
|
|
|
|
|
config?: RequestConfig
|
|
|
|
|
) => {
|
|
|
|
|
const response: any = await api.get(url, {
|
|
|
|
|
...config,
|
|
|
|
|
responseType: 'blob',
|
|
|
|
|
onDownloadProgress: (progressEvent) => {
|
|
|
|
|
if (onProgress && progressEvent.total) {
|
|
|
|
|
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
|
|
|
onProgress(progress);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 创建下载链接
|
|
|
|
|
const blob = new Blob([response]);
|
|
|
|
|
const downloadUrl = window.URL.createObjectURL(blob);
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.href = downloadUrl;
|
|
|
|
|
link.download = filename || 'download';
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
window.URL.revokeObjectURL(downloadUrl);
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 并发请求
|
|
|
|
|
*/
|
|
|
|
|
all: <T = any>(requests: Promise<T>[]) => Promise.all(requests),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 串行请求
|
|
|
|
|
*/
|
|
|
|
|
series: async <T = any>(requests: (() => Promise<T>)[]): Promise<T[]> => {
|
|
|
|
|
const results: T[] = [];
|
|
|
|
|
for (const request of requests) {
|
|
|
|
|
const result = await request();
|
|
|
|
|
results.push(result);
|
|
|
|
|
}
|
|
|
|
|
return results;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建带重试的请求
|
|
|
|
|
*/
|
|
|
|
|
export function createRetryRequest<T = any>(
|
|
|
|
|
requestFn: () => Promise<T>,
|
|
|
|
|
maxRetries = 3,
|
|
|
|
|
retryDelay = 1000
|
|
|
|
|
): Promise<T> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
let retries = 0;
|
|
|
|
|
|
|
|
|
|
const attempt = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await requestFn();
|
|
|
|
|
resolve(result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
retries++;
|
|
|
|
|
|
|
|
|
|
if (retries < maxRetries) {
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
console.log(`🔄 Retrying request (${retries}/${maxRetries})...`);
|
|
|
|
|
}
|
|
|
|
|
setTimeout(attempt, retryDelay * retries);
|
|
|
|
|
} else {
|
|
|
|
|
reject(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
attempt();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default api;
|