feat: update

This commit is contained in:
2025-11-05 17:24:55 +08:00
parent 61252cdf36
commit ce324c9bb5
42 changed files with 2078 additions and 448 deletions

View File

@@ -80,5 +80,3 @@ export function useDebounce<T extends (...args: any[]) => any>(
* console.log('Searching:', text);
* }, 500);
*/

View File

@@ -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
View 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,
};
}

View File

@@ -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>
*/

View File

@@ -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';

View File

@@ -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>;

View File

@@ -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>;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
}));

View File

@@ -139,4 +139,3 @@ export const useUserActions = () =>
logout: state.logout,
updateUser: state.updateUser,
}));

73
src/types/api.ts Normal file
View 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;
}

View File

@@ -78,4 +78,3 @@ declare global {
}
export {};

View File

@@ -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
View File

130
src/utils/config.ts Normal file
View 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;

View File

@@ -216,4 +216,3 @@ export const nowInSeconds = (): number => {
};
export default dayjs;

589
src/utils/network/api.ts Normal file
View 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;

View File

@@ -178,4 +178,3 @@ class Storage {
}
export default Storage;