feat: update
This commit is contained in:
@@ -80,5 +80,3 @@ export function useDebounce<T extends (...args: any[]) => any>(
|
||||
* console.log('Searching:', text);
|
||||
* }, 500);
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -90,20 +90,20 @@ export function useHaptics() {
|
||||
|
||||
/**
|
||||
* 使用示例:
|
||||
*
|
||||
*
|
||||
* function MyComponent() {
|
||||
* const haptics = useHaptics();
|
||||
*
|
||||
*
|
||||
* const handlePress = () => {
|
||||
* haptics.light();
|
||||
* // 执行其他操作
|
||||
* };
|
||||
*
|
||||
*
|
||||
* const handleSuccess = () => {
|
||||
* haptics.success();
|
||||
* // 显示成功消息
|
||||
* };
|
||||
*
|
||||
*
|
||||
* return (
|
||||
* <TouchableOpacity onPress={handlePress}>
|
||||
* <Text>Press me</Text>
|
||||
@@ -111,4 +111,3 @@ export function useHaptics() {
|
||||
* );
|
||||
* }
|
||||
*/
|
||||
|
||||
|
||||
291
src/hooks/useRequest.ts
Normal file
291
src/hooks/useRequest.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* 请求 Hook
|
||||
* 提供统一的请求状态管理
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { AxiosError } from 'axios';
|
||||
import type { RequestConfig } from '@/src/utils/network/api';
|
||||
|
||||
/**
|
||||
* 请求状态
|
||||
*/
|
||||
export interface RequestState<T> {
|
||||
data: T | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求选项
|
||||
*/
|
||||
export interface UseRequestOptions<T> extends RequestConfig {
|
||||
/** 是否立即执行 */
|
||||
immediate?: boolean;
|
||||
/** 成功回调 */
|
||||
onSuccess?: (data: T) => void;
|
||||
/** 失败回调 */
|
||||
onError?: (error: Error) => void;
|
||||
/** 完成回调(无论成功失败) */
|
||||
onFinally?: () => void;
|
||||
/** 默认数据 */
|
||||
defaultData?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 Hook
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data, loading, error, run, refresh } = useRequest(
|
||||
* () => request.get('/api/users'),
|
||||
* { immediate: true }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function useRequest<T = any>(
|
||||
requestFn: () => Promise<T>,
|
||||
options: UseRequestOptions<T> = {}
|
||||
) {
|
||||
const { immediate = false, onSuccess, onError, onFinally, defaultData = null } = options;
|
||||
|
||||
const [state, setState] = useState<RequestState<T>>({
|
||||
data: defaultData,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const requestRef = useRef(requestFn);
|
||||
requestRef.current = requestFn;
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
/**
|
||||
* 执行请求
|
||||
*/
|
||||
const run = useCallback(
|
||||
async (...args: any[]) => {
|
||||
// 取消之前的请求
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// 创建新的 AbortController
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: true,
|
||||
error: null,
|
||||
}));
|
||||
|
||||
try {
|
||||
const data = await requestRef.current();
|
||||
|
||||
setState({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
onSuccess?.(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: err,
|
||||
}));
|
||||
|
||||
onError?.(err);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
onFinally?.();
|
||||
}
|
||||
},
|
||||
[onSuccess, onError, onFinally]
|
||||
);
|
||||
|
||||
/**
|
||||
* 刷新(重新执行请求)
|
||||
*/
|
||||
const refresh = useCallback(() => {
|
||||
return run();
|
||||
}, [run]);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState({
|
||||
data: defaultData,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}, [defaultData]);
|
||||
|
||||
/**
|
||||
* 取消请求
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 立即执行
|
||||
useEffect(() => {
|
||||
if (immediate) {
|
||||
run();
|
||||
}
|
||||
|
||||
// 组件卸载时取消请求
|
||||
return () => {
|
||||
cancel();
|
||||
};
|
||||
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return {
|
||||
...state,
|
||||
run,
|
||||
refresh,
|
||||
reset,
|
||||
cancel,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求 Hook
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data, loading, loadMore, refresh, hasMore } = usePagination(
|
||||
* (page, pageSize) => request.get('/api/users', { params: { page, pageSize } })
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function usePagination<T = any>(
|
||||
requestFn: (
|
||||
page: number,
|
||||
pageSize: number
|
||||
) => Promise<{
|
||||
list: T[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
}>,
|
||||
options: {
|
||||
pageSize?: number;
|
||||
immediate?: boolean;
|
||||
onSuccess?: (data: T[]) => void;
|
||||
onError?: (error: Error) => void;
|
||||
} = {}
|
||||
) {
|
||||
const { pageSize = 20, immediate = false, onSuccess, onError } = options;
|
||||
|
||||
const [state, setState] = useState({
|
||||
data: [] as T[],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null as Error | null,
|
||||
page: 1,
|
||||
total: 0,
|
||||
hasMore: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
*/
|
||||
const load = useCallback(
|
||||
async (page: number, append = false) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: !append,
|
||||
loadingMore: append,
|
||||
error: null,
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await requestFn(page, pageSize);
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
data: append ? [...prev.data, ...result.list] : result.list,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
page,
|
||||
total: result.total,
|
||||
hasMore: result.hasMore,
|
||||
}));
|
||||
|
||||
onSuccess?.(result.list);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: err,
|
||||
}));
|
||||
|
||||
onError?.(err);
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[requestFn, pageSize, onSuccess, onError]
|
||||
);
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
const loadMore = useCallback(async () => {
|
||||
if (state.loadingMore || !state.hasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
return load(state.page + 1, true);
|
||||
}, [state.loadingMore, state.hasMore, state.page, load]);
|
||||
|
||||
/**
|
||||
* 刷新(重新加载第一页)
|
||||
*/
|
||||
const refresh = useCallback(async () => {
|
||||
return load(1, false);
|
||||
}, [load]);
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState({
|
||||
data: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
error: null,
|
||||
page: 1,
|
||||
total: 0,
|
||||
hasMore: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 立即执行
|
||||
useEffect(() => {
|
||||
if (immediate) {
|
||||
load(1, false);
|
||||
}
|
||||
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return {
|
||||
...state,
|
||||
loadMore,
|
||||
refresh,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -49,13 +49,12 @@ export function useThrottle<T extends (...args: any[]) => any>(
|
||||
|
||||
/**
|
||||
* 使用示例:
|
||||
*
|
||||
*
|
||||
* const handleScroll = useThrottle((event) => {
|
||||
* console.log('Scrolling:', event);
|
||||
* }, 200);
|
||||
*
|
||||
*
|
||||
* <ScrollView onScroll={handleScroll}>
|
||||
* ...
|
||||
* </ScrollView>
|
||||
*/
|
||||
|
||||
|
||||
14
src/index.ts
14
src/index.ts
@@ -3,8 +3,16 @@
|
||||
*/
|
||||
|
||||
// Utils
|
||||
export { default as api, request } from './utils/api';
|
||||
export {
|
||||
default as api,
|
||||
request,
|
||||
cancelAllRequests,
|
||||
cancelRequest,
|
||||
createRetryRequest,
|
||||
} from './utils/network/api';
|
||||
export type { ApiResponse, ApiError, RequestConfig } from './utils/network/api';
|
||||
export { default as Storage, STORAGE_KEYS } from './utils/storage';
|
||||
export { default as config, printConfig } from './utils/config';
|
||||
export * from './utils/date';
|
||||
|
||||
// Stores
|
||||
@@ -18,12 +26,14 @@ export * from './schemas/user';
|
||||
// Services
|
||||
export { default as authService } from './services/authService';
|
||||
export { default as userService } from './services/userService';
|
||||
export { default as appService } from './services/appService';
|
||||
|
||||
// Hooks
|
||||
export * from './hooks/useDebounce';
|
||||
export * from './hooks/useThrottle';
|
||||
export * from './hooks/useHaptics';
|
||||
export * from './hooks/useRequest';
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
export * from './types/api';
|
||||
|
||||
@@ -8,14 +8,8 @@ import { z } from 'zod';
|
||||
* 登录表单 Schema
|
||||
*/
|
||||
export const loginSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, '请输入邮箱')
|
||||
.email('请输入有效的邮箱地址'),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, '密码至少6个字符')
|
||||
.max(20, '密码最多20个字符'),
|
||||
email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
|
||||
password: z.string().min(6, '密码至少6个字符').max(20, '密码最多20个字符'),
|
||||
rememberMe: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -29,10 +23,7 @@ export const registerSchema = z
|
||||
.min(3, '用户名至少3个字符')
|
||||
.max(20, '用户名最多20个字符')
|
||||
.regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, '请输入邮箱')
|
||||
.email('请输入有效的邮箱地址'),
|
||||
email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, '密码至少6个字符')
|
||||
@@ -54,10 +45,7 @@ export const registerSchema = z
|
||||
* 忘记密码 Schema
|
||||
*/
|
||||
export const forgotPasswordSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, '请输入邮箱')
|
||||
.email('请输入有效的邮箱地址'),
|
||||
email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -70,10 +58,7 @@ export const resetPasswordSchema = z
|
||||
.min(6, '验证码为6位')
|
||||
.max(6, '验证码为6位')
|
||||
.regex(/^\d{6}$/, '验证码必须是6位数字'),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, '密码至少6个字符')
|
||||
.max(20, '密码最多20个字符'),
|
||||
password: z.string().min(6, '密码至少6个字符').max(20, '密码最多20个字符'),
|
||||
confirmPassword: z.string().min(1, '请确认密码'),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
@@ -87,10 +72,7 @@ export const resetPasswordSchema = z
|
||||
export const changePasswordSchema = z
|
||||
.object({
|
||||
oldPassword: z.string().min(1, '请输入当前密码'),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(6, '新密码至少6个字符')
|
||||
.max(20, '新密码最多20个字符'),
|
||||
newPassword: z.string().min(6, '新密码至少6个字符').max(20, '新密码最多20个字符'),
|
||||
confirmPassword: z.string().min(1, '请确认新密码'),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
@@ -127,4 +109,3 @@ export type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
|
||||
export type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;
|
||||
export type ChangePasswordFormData = z.infer<typeof changePasswordSchema>;
|
||||
export type PhoneLoginFormData = z.infer<typeof phoneLoginSchema>;
|
||||
|
||||
|
||||
@@ -21,11 +21,7 @@ export const userSchema = z.object({
|
||||
* 更新用户资料 Schema
|
||||
*/
|
||||
export const updateProfileSchema = z.object({
|
||||
nickname: z
|
||||
.string()
|
||||
.min(2, '昵称至少2个字符')
|
||||
.max(20, '昵称最多20个字符')
|
||||
.optional(),
|
||||
nickname: z.string().min(2, '昵称至少2个字符').max(20, '昵称最多20个字符').optional(),
|
||||
avatar: z.string().url('请输入有效的头像URL').optional(),
|
||||
phone: z
|
||||
.string()
|
||||
@@ -55,10 +51,7 @@ export const bindPhoneSchema = z.object({
|
||||
* 绑定邮箱 Schema
|
||||
*/
|
||||
export const bindEmailSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, '请输入邮箱')
|
||||
.email('请输入有效的邮箱地址'),
|
||||
email: z.string().min(1, '请输入邮箱').email('请输入有效的邮箱地址'),
|
||||
code: z
|
||||
.string()
|
||||
.min(6, '验证码为6位')
|
||||
@@ -73,4 +66,3 @@ export type User = z.infer<typeof userSchema>;
|
||||
export type UpdateProfileFormData = z.infer<typeof updateProfileSchema>;
|
||||
export type BindPhoneFormData = z.infer<typeof bindPhoneSchema>;
|
||||
export type BindEmailFormData = z.infer<typeof bindEmailSchema>;
|
||||
|
||||
|
||||
39
src/services/appService.ts
Normal file
39
src/services/appService.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 基础服务
|
||||
* 处理应用相关的 API 请求
|
||||
*/
|
||||
|
||||
import { request } from '@/src/utils/network/api';
|
||||
import type { User, UpdateProfileFormData } from '@/src/schemas/user';
|
||||
|
||||
/**
|
||||
* API 响应接口
|
||||
*/
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户服务类
|
||||
*/
|
||||
class AppService {
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
getPlatformData(data?: Record<string, any>): Promise<any> {
|
||||
return request.post('/v2', data, {
|
||||
headers: {
|
||||
cmdId: 371130,
|
||||
headerType: 1,
|
||||
apiName: 'getPlatformData',
|
||||
tid: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const appService = new AppService();
|
||||
export default appService;
|
||||
@@ -3,7 +3,7 @@
|
||||
* 处理登录、注册等认证相关的 API 请求
|
||||
*/
|
||||
|
||||
import { request } from '@/src/utils/api';
|
||||
import { request } from '@/src/utils/network/api';
|
||||
import type {
|
||||
LoginFormData,
|
||||
RegisterFormData,
|
||||
@@ -40,10 +40,7 @@ class AuthService {
|
||||
* 邮箱登录
|
||||
*/
|
||||
async login(data: LoginFormData): Promise<LoginResponse> {
|
||||
const response = await request.post<ApiResponse<LoginResponse>>(
|
||||
'/auth/login',
|
||||
data
|
||||
);
|
||||
const response = await request.post<ApiResponse<LoginResponse>>('/auth/login', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -51,10 +48,7 @@ class AuthService {
|
||||
* 手机号登录
|
||||
*/
|
||||
async phoneLogin(data: PhoneLoginFormData): Promise<LoginResponse> {
|
||||
const response = await request.post<ApiResponse<LoginResponse>>(
|
||||
'/auth/phone-login',
|
||||
data
|
||||
);
|
||||
const response = await request.post<ApiResponse<LoginResponse>>('/auth/phone-login', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -62,10 +56,7 @@ class AuthService {
|
||||
* 注册
|
||||
*/
|
||||
async register(data: RegisterFormData): Promise<LoginResponse> {
|
||||
const response = await request.post<ApiResponse<LoginResponse>>(
|
||||
'/auth/register',
|
||||
data
|
||||
);
|
||||
const response = await request.post<ApiResponse<LoginResponse>>('/auth/register', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -108,10 +99,9 @@ class AuthService {
|
||||
* 刷新 token
|
||||
*/
|
||||
async refreshToken(refreshToken: string): Promise<{ token: string }> {
|
||||
const response = await request.post<ApiResponse<{ token: string }>>(
|
||||
'/auth/refresh-token',
|
||||
{ refreshToken }
|
||||
);
|
||||
const response = await request.post<ApiResponse<{ token: string }>>('/auth/refresh-token', {
|
||||
refreshToken,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -131,4 +121,3 @@ class AuthService {
|
||||
// 导出单例
|
||||
export const authService = new AuthService();
|
||||
export default authService;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 处理用户信息相关的 API 请求
|
||||
*/
|
||||
|
||||
import { request } from '@/src/utils/api';
|
||||
import { request } from '@/src/utils/network/api';
|
||||
import type { User, UpdateProfileFormData } from '@/src/schemas/user';
|
||||
|
||||
/**
|
||||
@@ -50,15 +50,11 @@ class UserService {
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
|
||||
const response = await request.post<ApiResponse<{ url: string }>>(
|
||||
'/user/avatar',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await request.post<ApiResponse<{ url: string }>>('/user/avatar', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -87,4 +83,3 @@ class UserService {
|
||||
// 导出单例
|
||||
export const userService = new UserService();
|
||||
export default userService;
|
||||
|
||||
|
||||
@@ -127,12 +127,10 @@ export const useNotificationsEnabled = () =>
|
||||
useSettingsStore((state) => state.notificationsEnabled);
|
||||
|
||||
// 获取声音状态
|
||||
export const useSoundEnabled = () =>
|
||||
useSettingsStore((state) => state.soundEnabled);
|
||||
export const useSoundEnabled = () => useSettingsStore((state) => state.soundEnabled);
|
||||
|
||||
// 获取触觉反馈状态
|
||||
export const useHapticsEnabled = () =>
|
||||
useSettingsStore((state) => state.hapticsEnabled);
|
||||
export const useHapticsEnabled = () => useSettingsStore((state) => state.hapticsEnabled);
|
||||
|
||||
// 获取设置操作方法
|
||||
export const useSettingsActions = () =>
|
||||
@@ -144,4 +142,3 @@ export const useSettingsActions = () =>
|
||||
setHapticsEnabled: state.setHapticsEnabled,
|
||||
resetSettings: state.resetSettings,
|
||||
}));
|
||||
|
||||
|
||||
@@ -139,4 +139,3 @@ export const useUserActions = () =>
|
||||
logout: state.logout,
|
||||
updateUser: state.updateUser,
|
||||
}));
|
||||
|
||||
|
||||
73
src/types/api.ts
Normal file
73
src/types/api.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* API 相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应数据
|
||||
*/
|
||||
export interface PaginationResponse<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表响应数据
|
||||
*/
|
||||
export interface ListResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID 参数
|
||||
*/
|
||||
export interface IdParams {
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作参数
|
||||
*/
|
||||
export interface BatchParams {
|
||||
ids: (string | number)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索参数
|
||||
*/
|
||||
export interface SearchParams {
|
||||
keyword: string;
|
||||
filters?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传响应
|
||||
*/
|
||||
export interface UploadResponse {
|
||||
url: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用操作响应
|
||||
*/
|
||||
export interface OperationResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
@@ -78,4 +78,3 @@ declare global {
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
|
||||
144
src/utils/api.ts
144
src/utils/api.ts
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Axios API 配置
|
||||
* 统一管理 HTTP 请求
|
||||
*/
|
||||
|
||||
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
// API 基础配置
|
||||
const API_CONFIG = {
|
||||
baseURL: process.env.EXPO_PUBLIC_API_URL || 'https://api.example.com',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
// 创建 axios 实例
|
||||
const api = axios.create(API_CONFIG);
|
||||
|
||||
/**
|
||||
* 请求拦截器
|
||||
* 在请求发送前添加 token 等信息
|
||||
*/
|
||||
api.interceptors.request.use(
|
||||
async (config) => {
|
||||
try {
|
||||
// 从本地存储获取 token
|
||||
const token = await AsyncStorage.getItem('auth_token');
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 打印请求信息(开发环境)
|
||||
if (__DEV__) {
|
||||
console.log('📤 API Request:', {
|
||||
method: config.method?.toUpperCase(),
|
||||
url: config.url,
|
||||
data: config.data,
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error('Request interceptor error:', error);
|
||||
return config;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Request error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 响应拦截器
|
||||
* 统一处理响应和错误
|
||||
*/
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 打印响应信息(开发环境)
|
||||
if (__DEV__) {
|
||||
console.log('📥 API Response:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
});
|
||||
}
|
||||
|
||||
// 返回响应数据
|
||||
return response.data;
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
// 打印错误信息
|
||||
console.error('❌ API Error:', {
|
||||
url: error.config?.url,
|
||||
status: error.response?.status,
|
||||
message: error.message,
|
||||
data: error.response?.data,
|
||||
});
|
||||
|
||||
// 处理不同的错误状态码
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,清除 token 并跳转到登录页
|
||||
await AsyncStorage.removeItem('auth_token');
|
||||
// TODO: 导航到登录页
|
||||
// router.replace('/login');
|
||||
break;
|
||||
|
||||
case 403:
|
||||
// 禁止访问
|
||||
console.error('Access forbidden');
|
||||
break;
|
||||
|
||||
case 404:
|
||||
// 资源不存在
|
||||
console.error('Resource not found');
|
||||
break;
|
||||
|
||||
case 500:
|
||||
// 服务器错误
|
||||
console.error('Server error');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error('Unknown error');
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求已发送但没有收到响应
|
||||
console.error('No response received');
|
||||
} else {
|
||||
// 请求配置出错
|
||||
console.error('Request configuration error');
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
export const request = {
|
||||
get: <T = any>(url: string, config?: AxiosRequestConfig) =>
|
||||
api.get<T, T>(url, config),
|
||||
|
||||
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||
api.post<T, T>(url, data, config),
|
||||
|
||||
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||
api.put<T, T>(url, data, config),
|
||||
|
||||
delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
|
||||
api.delete<T, T>(url, config),
|
||||
|
||||
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||
api.patch<T, T>(url, data, config),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
0
src/utils/common.ts
Normal file
0
src/utils/common.ts
Normal file
130
src/utils/config.ts
Normal file
130
src/utils/config.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 应用配置工具
|
||||
* 统一管理环境变量和配置
|
||||
*/
|
||||
|
||||
import Constants from 'expo-constants';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
/**
|
||||
* 环境类型
|
||||
*/
|
||||
export type Environment = 'development' | 'staging' | 'production';
|
||||
|
||||
/**
|
||||
* 获取当前环境
|
||||
*/
|
||||
export const getEnvironment = (): Environment => {
|
||||
if (__DEV__) {
|
||||
return 'development';
|
||||
}
|
||||
|
||||
// 可以通过环境变量或其他方式判断 staging 环境
|
||||
const env = process.env.EXPO_PUBLIC_ENV;
|
||||
if (env === 'staging') {
|
||||
return 'staging';
|
||||
}
|
||||
|
||||
return 'production';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 API 基础 URL
|
||||
*/
|
||||
export const getApiBaseUrl = (): string => {
|
||||
// 1. 优先使用环境变量
|
||||
const envApiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||||
if (envApiUrl) {
|
||||
return envApiUrl;
|
||||
}
|
||||
|
||||
// 2. 根据环境返回不同的 URL
|
||||
const env = getEnvironment();
|
||||
|
||||
switch (env) {
|
||||
case 'development':
|
||||
// 开发环境
|
||||
if (Platform.OS === 'web') {
|
||||
// Web 平台使用相对路径(会被 webpack devServer 代理)
|
||||
return '/api';
|
||||
} else {
|
||||
// iOS/Android 使用本机 IP
|
||||
// ⚠️ 重要:需要替换为你的本机 IP 地址
|
||||
// 查看本机 IP:
|
||||
// - Windows: ipconfig
|
||||
// - Mac/Linux: ifconfig
|
||||
// - 或者使用 Metro Bundler 显示的 IP
|
||||
return 'http://192.168.1.100:3000/api';
|
||||
}
|
||||
|
||||
case 'staging':
|
||||
// 预发布环境
|
||||
return 'https://staging-api.yourdomain.com/api';
|
||||
|
||||
case 'production':
|
||||
// 生产环境
|
||||
return 'https://api.yourdomain.com/api';
|
||||
|
||||
default:
|
||||
return '/api';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 API 超时时间
|
||||
*/
|
||||
export const getApiTimeout = (): number => {
|
||||
const timeout = process.env.EXPO_PUBLIC_API_TIMEOUT;
|
||||
return timeout ? Number(timeout) : 10000;
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用配置
|
||||
*/
|
||||
export const config = {
|
||||
// 环境
|
||||
env: getEnvironment(),
|
||||
isDev: __DEV__,
|
||||
|
||||
// API 配置
|
||||
api: {
|
||||
baseURL: getApiBaseUrl(),
|
||||
timeout: getApiTimeout(),
|
||||
},
|
||||
|
||||
// 应用信息
|
||||
app: {
|
||||
name: process.env.EXPO_PUBLIC_APP_NAME || 'RN Demo',
|
||||
version: process.env.EXPO_PUBLIC_APP_VERSION || '1.0.0',
|
||||
bundleId: Constants.expoConfig?.ios?.bundleIdentifier || '',
|
||||
packageName: Constants.expoConfig?.android?.package || '',
|
||||
vk: 'fT6phq0wkOPRlAoyToidAnkogUV7ttGo',
|
||||
nc: 1,
|
||||
aseqId: '7',
|
||||
},
|
||||
|
||||
// 平台信息
|
||||
platform: {
|
||||
os: Platform.OS,
|
||||
version: Platform.Version,
|
||||
isWeb: Platform.OS === 'web',
|
||||
isIOS: Platform.OS === 'ios',
|
||||
isAndroid: Platform.OS === 'android',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 打印配置信息(仅开发环境)
|
||||
*/
|
||||
export const printConfig = () => {
|
||||
if (__DEV__) {
|
||||
console.log('📋 App Configuration:', {
|
||||
environment: config.env,
|
||||
apiBaseURL: config.api.baseURL,
|
||||
platform: config.platform.os,
|
||||
version: config.app.version,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -216,4 +216,3 @@ export const nowInSeconds = (): number => {
|
||||
};
|
||||
|
||||
export default dayjs;
|
||||
|
||||
|
||||
589
src/utils/network/api.ts
Normal file
589
src/utils/network/api.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
/**
|
||||
* 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 } 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,
|
||||
// params: config.params,
|
||||
// data: config.data,
|
||||
// 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 (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 = 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;
|
||||
@@ -178,4 +178,3 @@ class Storage {
|
||||
}
|
||||
|
||||
export default Storage;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user