Files
xingrin/frontend/lib/api-client.ts

277 lines
8.3 KiB
TypeScript
Raw Normal View History

2025-12-12 18:04:57 +08:00
/**
* 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 '发生未知错误';
};