Files
xingrin/frontend/lib/api-client.ts
2025-12-12 18:04:57 +08:00

277 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 '发生未知错误';
};