Files
tiku-backend.net/Tiku.PlatformAdmin.Web/src/api/http.ts
wangziqi 589ecd06f0 fix: 修复前端界面边界条件问题
- rowKey 使用 ?? 替代 ||,避免合法值 0 被当作 falsy
- parseJson 增加非字符串类型防御检查
- toTree 增加递归深度限制防止循环引用栈溢出
- saveBatchNodes 增加空名称数组检查
- localStorage.setItem 捕获 QuotaExceededError
- 路径参数缺失时显式抛出错误而非静默替换
2026-07-30 17:08:03 +08:00

110 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { OperationInput, PlatformOperation } from './types';
import { tokenStore, type TokenPair } from './token-store';
const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '');
let refreshPromise: Promise<TokenPair | null> | null = null;
export class ApiError extends Error {
constructor(
message: string,
readonly status: number,
readonly details?: unknown,
) {
super(message);
}
}
async function parseResponse(response: Response): Promise<unknown> {
if (response.status === 204) return null;
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('json')) return response.json();
const text = await response.text();
return text || null;
}
async function refreshTokens(): Promise<TokenPair | null> {
const tokens = tokenStore.get();
if (!tokens?.refreshToken) return null;
const response = await fetch(`${apiBaseUrl}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: tokens.refreshToken }),
});
if (!response.ok) {
tokenStore.set(null);
return null;
}
const next = (await response.json()) as TokenPair;
tokenStore.set(next);
return next;
}
async function authorizedFetch(url: string, init: RequestInit, retry = true): Promise<Response> {
const accessToken = tokenStore.get()?.accessToken;
const headers = new Headers(init.headers);
if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`);
const response = await fetch(`${apiBaseUrl}${url}`, { ...init, headers });
if (response.status !== 401 || !retry) return response;
refreshPromise ??= refreshTokens().finally(() => { refreshPromise = null; });
const refreshed = await refreshPromise;
return refreshed ? authorizedFetch(url, init, false) : response;
}
function appendQuery(url: URL, query: Record<string, unknown>) {
for (const [key, value] of Object.entries(query)) {
if (value === undefined || value === null || value === '') continue;
if (Array.isArray(value)) value.forEach((item) => url.searchParams.append(key, String(item)));
else url.searchParams.set(key, String(value));
}
}
export async function apiRequest<T = unknown>(
operation: PlatformOperation,
input: OperationInput = {},
signal?: AbortSignal,
): Promise<T> {
const pathValues = (input.path || {}) as Record<string, unknown>;
let route = operation.path.replace(/\{([^}]+)\}/g, (_, name: string) => {
const value = pathValues[name];
if (value === undefined || value === null) throw new Error(`缺少必填路径参数:${name}`);
return encodeURIComponent(String(value));
});
const url = new URL(route, window.location.origin);
appendQuery(url, ((input.query || {}) as Record<string, unknown>));
route = `${url.pathname}${url.search}`;
const headers: Record<string, string> = {};
const init: RequestInit = { method: operation.method, headers, signal };
if (input.body !== undefined && operation.method !== 'GET') {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(input.body);
}
const response = await authorizedFetch(route, init);
const payload = await parseResponse(response);
if (!response.ok) {
const problem = payload as { detail?: string; title?: string } | null;
throw new ApiError(problem?.detail || problem?.title || `请求失败HTTP ${response.status}`, response.status, payload);
}
return payload as T;
}
export async function authRequest<T>(path: string, body?: unknown): Promise<T> {
const response = await fetch(`${apiBaseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
const payload = await parseResponse(response);
if (!response.ok) {
const problem = payload as { detail?: string; title?: string } | null;
throw new ApiError(problem?.detail || problem?.title || `请求失败HTTP ${response.status}`, response.status, payload);
}
return payload as T;
}
export async function getCurrentUser<T>(): Promise<T> {
const response = await authorizedFetch('/api/me', { method: 'GET' });
const payload = await parseResponse(response);
if (!response.ok) throw new ApiError('登录会话已失效', response.status, payload);
return payload as T;
}