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:
Supra4E8C
2025-12-07 11:32:31 +08:00
parent 8e4132200d
commit 450964fb1a
144 changed files with 14223 additions and 21647 deletions

215
src/services/api/client.ts Normal file
View 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();