取消背景图

This commit is contained in:
2026-06-30 17:10:51 +08:00
parent ef9d9e8a74
commit 0c63512a29
39 changed files with 5931 additions and 661 deletions
+21 -14
View File
@@ -1,6 +1,7 @@
/**
* Real API client for connecting to the FastAPI backend.
* Handles auth tokens, request/response encryption, snake_case→camelCase conversion.
* Supports both JSON bodies and multipart FormData bodies.
*/
import { encrypt, decrypt, isCryptoAvailable } from './crypto';
@@ -24,6 +25,7 @@ function toCamel(s: string): string {
function keysToCamel(obj: unknown): unknown {
if (Array.isArray(obj)) return obj.map(keysToCamel);
if (obj !== null && typeof obj === 'object') {
if (obj instanceof File || obj instanceof Blob || obj instanceof FormData) return obj;
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [toCamel(k), keysToCamel(v)])
);
@@ -54,31 +56,35 @@ async function tryDecrypt(data: string): Promise<string | null> {
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { method = 'GET', body, auth = true, encryptBody = USE_ENCRYPTION } = options;
const isFormData = typeof FormData !== 'undefined' && body instanceof FormData;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
const headers: Record<string, string> = {};
if (!isFormData) headers['Content-Type'] = 'application/json';
if (auth) {
const token = getToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
}
let bodyStr: string | undefined;
let requestBody: BodyInit | undefined;
if (body !== undefined) {
const json = JSON.stringify(body);
if (encryptBody) {
headers['X-Encrypted'] = 'true';
bodyStr = JSON.stringify({ data: await encrypt(json) });
if (isFormData) {
requestBody = body as FormData;
} else {
bodyStr = json;
const json = JSON.stringify(body);
if (encryptBody) {
headers['X-Encrypted'] = 'true';
requestBody = JSON.stringify({ data: await encrypt(json) });
} else {
requestBody = json;
}
}
}
const res = await fetch(`${BASE_URL}/api${path}`, {
method,
headers,
body: bodyStr,
body: requestBody,
});
if (res.status === 204) return undefined as T;
@@ -96,7 +102,7 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
// If the response is encrypted, decrypt it
const hasEncryptedData = parsed && typeof parsed.data === 'string';
if (hasEncryptedData && encryptBody) {
if (hasEncryptedData && encryptBody && !isFormData) {
const decrypted = await tryDecrypt(parsed.data);
if (decrypted !== null) {
parsed = JSON.parse(decrypted);
@@ -107,7 +113,8 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
// Handle error responses
if (!res.ok) {
const msg = parsed?.detail || `请求失败 (${res.status})`;
const rawMsg = parsed?.detail || parsed?.message || `请求失败 (${res.status})`;
const msg = typeof rawMsg === 'string' ? rawMsg : JSON.stringify(rawMsg);
if (res.status === 401) {
clearToken();
// Only redirect to login if not already on login page
@@ -125,7 +132,7 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
// Convenience methods
export const api = {
get: <T>(path: string, auth = true) => apiRequest<T>(path, { auth }),
post: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'POST', body, auth }),
put: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'PUT', body, auth }),
post: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'POST', body, auth, encryptBody: body instanceof FormData ? false : USE_ENCRYPTION }),
put: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'PUT', body, auth, encryptBody: body instanceof FormData ? false : USE_ENCRYPTION }),
delete: <T>(path: string, auth = true) => apiRequest<T>(path, { method: 'DELETE', auth }),
};