104 行
3.0 KiB
TypeScript
104 行
3.0 KiB
TypeScript
export const API_BASE =
|
||
process.env.NEXT_PUBLIC_API_BASE ??
|
||
(process.env.NODE_ENV === "development" ? "http://localhost:8080" : "/admin139");
|
||
|
||
function uiText(zhText: string, enText: string): string {
|
||
if (typeof window === "undefined") return zhText;
|
||
const lang = window.localStorage.getItem("csp.ui.language");
|
||
return lang === "en" ? enText : zhText;
|
||
}
|
||
|
||
type ApiEnvelope<T> =
|
||
| { ok: true; data?: T;[k: string]: unknown }
|
||
| { ok: false; error?: string;[k: string]: unknown };
|
||
|
||
export async function apiFetch<T>(
|
||
path: string,
|
||
init?: RequestInit,
|
||
token?: string
|
||
): Promise<T> {
|
||
const headers = new Headers(init?.headers);
|
||
if (token) headers.set("Authorization", `Bearer ${token}`);
|
||
if (init?.body && !headers.has("Content-Type")) {
|
||
headers.set("Content-Type", "application/json");
|
||
}
|
||
|
||
const method = (init?.method ?? "GET").toUpperCase();
|
||
const retryable = method === "GET" || method === "HEAD";
|
||
|
||
let resp: Response;
|
||
try {
|
||
resp = await fetch(`${API_BASE}${path}`, {
|
||
...init,
|
||
headers,
|
||
cache: "no-store",
|
||
});
|
||
} catch (err) {
|
||
if (!retryable) {
|
||
throw new Error(
|
||
uiText(
|
||
`网络请求失败,请检查后端服务或代理连接(${err instanceof Error ? err.message : String(err)})`,
|
||
`Network request failed. Please check backend/proxy connectivity (${err instanceof Error ? err.message : String(err)}).`
|
||
)
|
||
);
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||
try {
|
||
resp = await fetch(`${API_BASE}${path}`, {
|
||
...init,
|
||
headers,
|
||
cache: "no-store",
|
||
});
|
||
} catch (retryErr) {
|
||
throw new Error(
|
||
uiText(
|
||
`网络请求失败,请检查后端服务或代理连接(${retryErr instanceof Error ? retryErr.message : String(retryErr)
|
||
})`,
|
||
`Network request failed. Please check backend/proxy connectivity (${retryErr instanceof Error ? retryErr.message : String(retryErr)
|
||
}).`
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
const text = await resp.text();
|
||
let payload: unknown = null;
|
||
if (text) {
|
||
try {
|
||
payload = JSON.parse(text) as unknown;
|
||
} catch {
|
||
payload = text;
|
||
}
|
||
}
|
||
|
||
if (!resp.ok) {
|
||
const msg =
|
||
typeof payload === "object" && payload !== null && "error" in payload
|
||
? String((payload as { error?: unknown }).error ?? `HTTP ${resp.status}`)
|
||
: `HTTP ${resp.status}`;
|
||
throw new Error(msg);
|
||
}
|
||
|
||
if (typeof payload === "object" && payload !== null && "ok" in payload) {
|
||
const env = payload as ApiEnvelope<T>;
|
||
if (!env.ok) {
|
||
throw new Error(env.error ?? "request failed");
|
||
}
|
||
if ("data" in env) return (env.data as T) ?? ({} as T);
|
||
return payload as T;
|
||
}
|
||
|
||
return payload as T;
|
||
}
|
||
|
||
export interface RatingHistoryItem {
|
||
type: string;
|
||
created_at: number;
|
||
change: number;
|
||
note: string;
|
||
}
|
||
|
||
export async function listRatingHistory(limit: number = 100): Promise<RatingHistoryItem[]> {
|
||
return apiFetch<RatingHistoryItem[]>(`/api/v1/me/rating-history?limit=${limit}`);
|
||
}
|