Files
doc_ai_frontend/src/lib/api.ts
2026-03-06 14:30:30 +08:00

157 lines
3.7 KiB
TypeScript

/**
* HTTP 客户端封装
* 统一处理请求/响应拦截、错误处理、Token 管理
*/
import { API_BASE_URL } from '../config/env';
import type { ApiResponse } from '../types/api';
// Token 存储键名
const TOKEN_KEY = 'texpixel_token';
const TOKEN_EXPIRES_KEY = 'texpixel_token_expires';
const USER_EMAIL_KEY = 'texpixel_user_email';
/**
* Token 管理工具
*/
export const tokenManager = {
getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
},
setToken(token: string, expiresAt: number, email?: string): void {
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(TOKEN_EXPIRES_KEY, expiresAt.toString());
if (email) {
localStorage.setItem(USER_EMAIL_KEY, email);
}
},
removeToken(): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(TOKEN_EXPIRES_KEY);
localStorage.removeItem(USER_EMAIL_KEY);
},
getEmail(): string | null {
return localStorage.getItem(USER_EMAIL_KEY);
},
isTokenValid(): boolean {
const token = this.getToken();
const expiresAt = localStorage.getItem(TOKEN_EXPIRES_KEY);
if (!token || !expiresAt) return false;
// 提前 5 分钟判定为过期
const expiresTimestamp = parseInt(expiresAt, 10) * 1000;
return Date.now() < expiresTimestamp - 5 * 60 * 1000;
},
getExpiresAt(): number | null {
const expiresAt = localStorage.getItem(TOKEN_EXPIRES_KEY);
return expiresAt ? parseInt(expiresAt, 10) : null;
},
};
/**
* 自定义 API 错误类
*/
export class ApiError extends Error {
constructor(
public code: number,
message: string,
public requestId?: string
) {
super(message);
this.name = 'ApiError';
}
}
/**
* 请求配置类型
*/
interface RequestConfig extends RequestInit {
skipAuth?: boolean;
successCodes?: number[];
}
/**
* 发起 HTTP 请求
*/
async function request<T>(
endpoint: string,
config: RequestConfig = {}
): Promise<ApiResponse<T>> {
const { skipAuth = false, successCodes = [200], headers: customHeaders, ...restConfig } = config;
const headers: HeadersInit = {
'Content-Type': 'application/json',
...customHeaders,
};
// 自动添加 Authorization header
if (!skipAuth) {
const token = tokenManager.getToken();
if (token) {
(headers as Record<string, string>)['Authorization'] = token;
}
}
const url = `${API_BASE_URL}${endpoint}`;
try {
const response = await fetch(url, {
...restConfig,
headers,
});
const data: ApiResponse<T> = await response.json();
// 统一处理业务错误
if (!successCodes.includes(data.code)) {
throw new ApiError(data.code, data.message, data.request_id);
}
return data;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
// 网络错误或其他错误
throw new ApiError(-1, '网络错误,请检查网络连接');
}
}
/**
* HTTP 方法快捷函数
*/
export const http = {
get<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>> {
return request<T>(endpoint, { ...config, method: 'GET' });
},
post<T>(endpoint: string, body?: unknown, config?: RequestConfig): Promise<ApiResponse<T>> {
return request<T>(endpoint, {
...config,
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
});
},
put<T>(endpoint: string, body?: unknown, config?: RequestConfig): Promise<ApiResponse<T>> {
return request<T>(endpoint, {
...config,
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
});
},
delete<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>> {
return request<T>(endpoint, { ...config, method: 'DELETE' });
},
};
export default http;