mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 02:30:51 +08:00
feat: initialize new React application structure with TypeScript, ESLint, and Prettier configurations, while removing legacy files and adding new components and pages for enhanced functionality
This commit is contained in:
215
src/services/api/client.ts
Normal file
215
src/services/api/client.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Axios API 客户端
|
||||
* 替代原项目 src/core/api-client.js
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import type { ApiClientConfig, ApiError } from '@/types';
|
||||
import {
|
||||
BUILD_DATE_HEADER_KEYS,
|
||||
MANAGEMENT_API_PREFIX,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
VERSION_HEADER_KEYS
|
||||
} from '@/utils/constants';
|
||||
|
||||
class ApiClient {
|
||||
private instance: AxiosInstance;
|
||||
private apiBase: string = '';
|
||||
private managementKey: string = '';
|
||||
|
||||
constructor() {
|
||||
this.instance = axios.create({
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 API 配置
|
||||
*/
|
||||
setConfig(config: ApiClientConfig): void {
|
||||
this.apiBase = this.normalizeApiBase(config.apiBase);
|
||||
this.managementKey = config.managementKey;
|
||||
|
||||
if (config.timeout) {
|
||||
this.instance.defaults.timeout = config.timeout;
|
||||
} else {
|
||||
this.instance.defaults.timeout = REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 API Base URL
|
||||
*/
|
||||
private normalizeApiBase(base: string): string {
|
||||
let normalized = base.trim();
|
||||
|
||||
// 移除尾部的 /v0/management
|
||||
normalized = normalized.replace(/\/?v0\/management\/?$/i, '');
|
||||
|
||||
// 移除尾部斜杠
|
||||
normalized = normalized.replace(/\/+$/, '');
|
||||
|
||||
// 添加协议
|
||||
if (!/^https?:\/\//i.test(normalized)) {
|
||||
normalized = `http://${normalized}`;
|
||||
}
|
||||
|
||||
return `${normalized}${MANAGEMENT_API_PREFIX}`;
|
||||
}
|
||||
|
||||
private readHeader(headers: Record<string, any>, keys: string[]): string | null {
|
||||
const normalized = Object.fromEntries(
|
||||
Object.entries(headers || {}).map(([key, value]) => [key.toLowerCase(), value as string | undefined])
|
||||
);
|
||||
for (const key of keys) {
|
||||
const match = normalized[key.toLowerCase()];
|
||||
if (match) return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求/响应拦截器
|
||||
*/
|
||||
private setupInterceptors(): void {
|
||||
// 请求拦截器
|
||||
this.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 设置 baseURL
|
||||
config.baseURL = this.apiBase;
|
||||
|
||||
// 添加认证头
|
||||
if (this.managementKey) {
|
||||
config.headers.Authorization = `Bearer ${this.managementKey}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(this.handleError(error))
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const headers = response.headers as Record<string, string | undefined>;
|
||||
const version = this.readHeader(headers, VERSION_HEADER_KEYS);
|
||||
const buildDate = this.readHeader(headers, BUILD_DATE_HEADER_KEYS);
|
||||
|
||||
// 触发版本更新事件(后续通过 store 处理)
|
||||
if (version || buildDate) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('server-version-update', {
|
||||
detail: { version: version || null, buildDate: buildDate || null }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => Promise.reject(this.handleError(error))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理
|
||||
*/
|
||||
private handleError(error: any): ApiError {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const responseData = error.response?.data as any;
|
||||
const message = responseData?.error || responseData?.message || error.message || 'Request failed';
|
||||
const apiError = new Error(message) as ApiError;
|
||||
apiError.name = 'ApiError';
|
||||
apiError.status = error.response?.status;
|
||||
apiError.code = error.code;
|
||||
apiError.details = responseData;
|
||||
apiError.data = responseData;
|
||||
|
||||
// 401 未授权 - 触发登出事件
|
||||
if (error.response?.status === 401) {
|
||||
window.dispatchEvent(new Event('unauthorized'));
|
||||
}
|
||||
|
||||
return apiError;
|
||||
}
|
||||
|
||||
const fallback = new Error(error?.message || 'Unknown error occurred') as ApiError;
|
||||
fallback.name = 'ApiError';
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
*/
|
||||
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.get<T>(url, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
*/
|
||||
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.post<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.put<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH 请求
|
||||
*/
|
||||
async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.patch<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
*/
|
||||
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.delete<T>(url, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始响应(用于下载等场景)
|
||||
*/
|
||||
async getRaw(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse> {
|
||||
return this.instance.get(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 FormData
|
||||
*/
|
||||
async postForm<T = any>(url: string, formData: FormData, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.post<T>(url, formData, {
|
||||
...config,
|
||||
headers: {
|
||||
...(config?.headers || {}),
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保留对 axios.request 的访问,便于下载等场景
|
||||
*/
|
||||
async requestRaw(config: AxiosRequestConfig): Promise<AxiosResponse> {
|
||||
return this.instance.request(config);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const apiClient = new ApiClient();
|
||||
Reference in New Issue
Block a user