mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-02-02 04:33:10 +08:00
277 lines
8.3 KiB
TypeScript
277 lines
8.3 KiB
TypeScript
|
|
/**
|
|||
|
|
* API 客户端配置文件
|
|||
|
|
*
|
|||
|
|
* 核心功能:
|
|||
|
|
* 1. 统一的 HTTP 请求封装
|
|||
|
|
* 2. 统一错误处理
|
|||
|
|
* 3. 请求/响应日志记录
|
|||
|
|
*
|
|||
|
|
* 命名规范说明:
|
|||
|
|
* - 前端(TypeScript/React):驼峰命名 camelCase
|
|||
|
|
* 例如:pageSize, createdAt, organizationId
|
|||
|
|
*
|
|||
|
|
* - 后端(Django/Python):下划线命名 snake_case(模型字段)
|
|||
|
|
* 例如:page_size, created_at, organization_id
|
|||
|
|
*
|
|||
|
|
* - API JSON 格式:驼峰命名 camelCase(已由后端自动转换)
|
|||
|
|
* 例如:pageSize, createdAt, organizationId
|
|||
|
|
*
|
|||
|
|
* 命名转换机制:
|
|||
|
|
* ══════════════════════════════════════════════════════════════════════
|
|||
|
|
* 【后端处理】Django REST Framework + djangorestframework-camel-case
|
|||
|
|
* ══════════════════════════════════════════════════════════════════════
|
|||
|
|
*
|
|||
|
|
* 1. 前端发送请求(camelCase):
|
|||
|
|
* { pageSize: 10, sortBy: "name" }
|
|||
|
|
*
|
|||
|
|
* 2. Django 接收并自动转换为 snake_case:
|
|||
|
|
* { page_size: 10, sort_by: "name" }
|
|||
|
|
*
|
|||
|
|
* 3. Django 处理后端逻辑(使用 snake_case 模型字段)
|
|||
|
|
*
|
|||
|
|
* 4. Django 返回数据时自动转换为 camelCase:
|
|||
|
|
* { pageSize: 10, createdAt: "2024-01-01" }
|
|||
|
|
*
|
|||
|
|
* 5. 前端直接使用(camelCase):
|
|||
|
|
* response.data.pageSize // [OK] 直接使用
|
|||
|
|
*
|
|||
|
|
* [NOTE] 关键点:命名转换由后端统一处理,前端无需转换
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import axios, { AxiosRequestConfig } from 'axios';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建 axios 实例
|
|||
|
|
* 配置基础 URL、超时时间和默认请求头
|
|||
|
|
*/
|
|||
|
|
const apiClient = axios.create({
|
|||
|
|
baseURL: '/api', // API 基础路径
|
|||
|
|
timeout: 30000, // 30秒超时
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 请求拦截器:处理请求前的准备工作
|
|||
|
|
*
|
|||
|
|
* 工作流程:
|
|||
|
|
* 1. 确保 URL 格式正确(Django 要求末尾斜杠)
|
|||
|
|
* 2. 记录请求日志(开发调试用)
|
|||
|
|
*
|
|||
|
|
* 注意事项:
|
|||
|
|
* - 命名转换由后端处理,前端无需转换
|
|||
|
|
* - 前端直接使用 camelCase 命名即可
|
|||
|
|
*/
|
|||
|
|
apiClient.interceptors.request.use(
|
|||
|
|
(config) => {
|
|||
|
|
// 只在开发环境输出调试日志
|
|||
|
|
if (process.env.NODE_ENV === 'development') {
|
|||
|
|
console.log('[REQUEST] API Request:', {
|
|||
|
|
method: config.method?.toUpperCase(),
|
|||
|
|
url: config.url,
|
|||
|
|
baseURL: config.baseURL,
|
|||
|
|
fullURL: `${config.baseURL}${config.url}`,
|
|||
|
|
data: config.data,
|
|||
|
|
params: config.params
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return config;
|
|||
|
|
},
|
|||
|
|
(error) => {
|
|||
|
|
if (process.env.NODE_ENV === 'development') {
|
|||
|
|
console.error('[ERROR] Request Error:', error);
|
|||
|
|
}
|
|||
|
|
return Promise.reject(error);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 响应拦截器:处理响应数据
|
|||
|
|
*
|
|||
|
|
* 工作流程:
|
|||
|
|
* 1. 记录响应日志(开发调试用)
|
|||
|
|
* 2. 返回响应数据(后端已转换为 camelCase)
|
|||
|
|
*
|
|||
|
|
* 注意事项:
|
|||
|
|
* - 后端已自动将 snake_case 转换为 camelCase
|
|||
|
|
* - 前端直接使用即可,无需额外转换
|
|||
|
|
*/
|
|||
|
|
apiClient.interceptors.response.use(
|
|||
|
|
(response) => {
|
|||
|
|
// 只在开发环境输出调试日志
|
|||
|
|
if (process.env.NODE_ENV === 'development') {
|
|||
|
|
console.log('[RESPONSE] API Response:', {
|
|||
|
|
status: response.status,
|
|||
|
|
statusText: response.statusText,
|
|||
|
|
url: response.config.url,
|
|||
|
|
data: response.data
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return response;
|
|||
|
|
},
|
|||
|
|
(error) => {
|
|||
|
|
// 只在开发环境输出错误日志
|
|||
|
|
if (process.env.NODE_ENV === 'development') {
|
|||
|
|
// 检查是否是 Axios 错误
|
|||
|
|
if (axios.isAxiosError(error)) {
|
|||
|
|
console.error('[ERROR] API Error:', {
|
|||
|
|
status: error.response?.status,
|
|||
|
|
statusText: error.response?.statusText,
|
|||
|
|
url: error.config?.url,
|
|||
|
|
method: error.config?.method,
|
|||
|
|
data: error.response?.data,
|
|||
|
|
message: error.message,
|
|||
|
|
code: error.code
|
|||
|
|
});
|
|||
|
|
} else if (error instanceof Error) {
|
|||
|
|
// 普通 Error 对象
|
|||
|
|
console.error('[ERROR] API Error:', error.message, error.stack);
|
|||
|
|
} else {
|
|||
|
|
// 未知错误类型
|
|||
|
|
console.error('[ERROR] API Error: Unknown error', String(error));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Promise.reject(error);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 导出默认的 axios 实例(一般不直接使用)
|
|||
|
|
export default apiClient;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 导出常用的 HTTP 方法
|
|||
|
|
*
|
|||
|
|
* 使用示例:
|
|||
|
|
*
|
|||
|
|
* 1. GET 请求:
|
|||
|
|
* api.get('/organizations', {
|
|||
|
|
* params: { pageSize: 10, sortBy: 'name' } // 使用 camelCase
|
|||
|
|
* })
|
|||
|
|
* 后端接收:page_size=10&sort_by=name(自动转换)
|
|||
|
|
*
|
|||
|
|
* 2. POST 请求:
|
|||
|
|
* api.post('/organizations/create', {
|
|||
|
|
* organizationName: 'test', // 使用 camelCase
|
|||
|
|
* createdAt: '2024-01-01'
|
|||
|
|
* })
|
|||
|
|
* 后端接收:organization_name, created_at(自动转换)
|
|||
|
|
*
|
|||
|
|
* 3. 响应数据(已经是 camelCase):
|
|||
|
|
* const response = await api.get('/organizations')
|
|||
|
|
* response.data.pageSize // [OK] 直接使用 camelCase
|
|||
|
|
* response.data.createdAt // [OK] 直接使用 camelCase
|
|||
|
|
*
|
|||
|
|
* 类型参数:
|
|||
|
|
* - T: 响应数据的类型(可选)
|
|||
|
|
* - config: axios 配置对象(可选)
|
|||
|
|
*/
|
|||
|
|
export const api = {
|
|||
|
|
/**
|
|||
|
|
* GET 请求
|
|||
|
|
* @param url - 请求路径(相对于 baseURL)
|
|||
|
|
* @param config - axios 配置,建议使用 params 传递查询参数
|
|||
|
|
* @returns Promise<AxiosResponse<T>>
|
|||
|
|
*/
|
|||
|
|
get: <T = unknown>(url: string, config?: AxiosRequestConfig) => apiClient.get<T>(url, config),
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* POST 请求
|
|||
|
|
* @param url - 请求路径(相对于 baseURL)
|
|||
|
|
* @param data - 请求体数据(会自动转换为 snake_case)
|
|||
|
|
* @param config - axios 配置(可选)
|
|||
|
|
* @returns Promise<AxiosResponse<T>>
|
|||
|
|
*/
|
|||
|
|
post: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiClient.post<T>(url, data, config),
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* PUT 请求
|
|||
|
|
* @param url - 请求路径(相对于 baseURL)
|
|||
|
|
* @param data - 请求体数据(会自动转换为 snake_case)
|
|||
|
|
* @param config - axios 配置(可选)
|
|||
|
|
* @returns Promise<AxiosResponse<T>>
|
|||
|
|
*/
|
|||
|
|
put: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiClient.put<T>(url, data, config),
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* PATCH 请求(部分更新)
|
|||
|
|
* @param url - 请求路径(相对于 baseURL)
|
|||
|
|
* @param data - 请求体数据(会自动转换为 snake_case)
|
|||
|
|
* @param config - axios 配置(可选)
|
|||
|
|
* @returns Promise<AxiosResponse<T>>
|
|||
|
|
*/
|
|||
|
|
patch: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) => apiClient.patch<T>(url, data, config),
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* DELETE 请求
|
|||
|
|
* @param url - 请求路径(相对于 baseURL)
|
|||
|
|
* @param config - axios 配置(可选)
|
|||
|
|
* @returns Promise<AxiosResponse<T>>
|
|||
|
|
*/
|
|||
|
|
delete: <T = unknown>(url: string, config?: AxiosRequestConfig) => apiClient.delete<T>(url, config),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 错误处理工具函数
|
|||
|
|
*
|
|||
|
|
* 功能:从错误对象中提取用户友好的错误消息
|
|||
|
|
*
|
|||
|
|
* 错误优先级:
|
|||
|
|
* 1. 请求取消
|
|||
|
|
* 2. 请求超时
|
|||
|
|
* 3. 后端返回的错误消息
|
|||
|
|
* 4. axios 错误消息
|
|||
|
|
* 5. 未知错误
|
|||
|
|
*
|
|||
|
|
* 使用示例:
|
|||
|
|
* try {
|
|||
|
|
* await api.get('/organizations')
|
|||
|
|
* } catch (error) {
|
|||
|
|
* const message = getErrorMessage(error)
|
|||
|
|
* toast.error(message)
|
|||
|
|
* }
|
|||
|
|
*
|
|||
|
|
* @param error - 错误对象(可以是任意类型)
|
|||
|
|
* @returns 用户友好的错误消息字符串
|
|||
|
|
*/
|
|||
|
|
export const getErrorMessage = (error: unknown): string => {
|
|||
|
|
// 请求被取消(用户主动取消或组件卸载)
|
|||
|
|
if (axios.isCancel(error)) {
|
|||
|
|
return '请求已被取消';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 类型守卫:检查是否为错误对象
|
|||
|
|
const err = error as {
|
|||
|
|
code?: string;
|
|||
|
|
response?: { data?: { message?: string; error?: string; detail?: string } };
|
|||
|
|
message?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 请求超时(超过 30 秒)
|
|||
|
|
if (err.code === 'ECONNABORTED') {
|
|||
|
|
return '请求超时,请稍后重试';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 后端返回的错误消息(支持多种格式)
|
|||
|
|
if (err.response?.data?.error) {
|
|||
|
|
return err.response.data.error;
|
|||
|
|
}
|
|||
|
|
if (err.response?.data?.message) {
|
|||
|
|
return err.response.data.message;
|
|||
|
|
}
|
|||
|
|
if (err.response?.data?.detail) {
|
|||
|
|
return err.response.data.detail;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// axios 自身的错误消息
|
|||
|
|
if (err.message) {
|
|||
|
|
return err.message;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 兜底错误消息
|
|||
|
|
return '发生未知错误';
|
|||
|
|
};
|