Files
WonderQ-Project/WonderQ-Admin-UI-Vue/src/api/client.ts
T

106 lines
4.5 KiB
TypeScript

import type { ApiErrorResponse, ApiResponse, AuthPayload, AdminProfile, Dashboard, Lead, SiteConfig, WanfaCategory, ConciergeAdvisor } from "@/types";
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "";
let accessToken: string | null = null;
let refreshPromise: Promise<AuthPayload> | null = null;
export class ApiRequestError extends Error {
constructor(
message: string,
readonly statusCode: number,
readonly errorCode?: string,
readonly details?: unknown,
) {
super(message);
this.name = "ApiRequestError";
}
}
export class AuthExpiredError extends ApiRequestError {
constructor(message = "登录已失效,请重新登录") {
super(message, 401, "AUTH_EXPIRED");
this.name = "AuthExpiredError";
}
}
export function setAccessToken(token: string | null) {
accessToken = token;
}
export function getAccessToken() {
return accessToken;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
async function parseJson(response: Response): Promise<unknown> {
return response.json().catch(() => null);
}
function unwrap<T>(payload: unknown, statusCode: number): T {
if (!isRecord(payload) || typeof payload.code !== "number" || payload.code !== statusCode || typeof payload.msg !== "string" || !Object.prototype.hasOwnProperty.call(payload, "data")) {
throw new ApiRequestError("接口响应格式不正确", statusCode, "API_PROTOCOL_ERROR");
}
if (statusCode < 200 || statusCode >= 300) {
if (payload.data !== null) throw new ApiRequestError("接口响应格式不正确", statusCode, "API_PROTOCOL_ERROR");
const error = payload as unknown as ApiErrorResponse;
throw new ApiRequestError(error.msg, statusCode, error.errorCode, error.details);
}
if (payload.msg !== "success") throw new ApiRequestError("接口响应格式不正确", statusCode, "API_PROTOCOL_ERROR");
return payload.data as T;
}
async function rawRequest(path: string, options: RequestInit = {}): Promise<Response> {
const headers = new Headers(options.headers);
if (!(options.body instanceof FormData)) headers.set("Content-Type", "application/json");
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
return fetch(`${API_BASE}${path}`, { ...options, headers, credentials: "include" });
}
export async function refreshAccessToken(): Promise<AuthPayload> {
const response = await rawRequest("/api/admin/auth/refresh", { method: "POST" });
const payload = unwrap<AuthPayload>(await parseJson(response), response.status);
setAccessToken(payload.accessToken || payload.token);
return payload;
}
async function refreshOnce() {
if (!refreshPromise) {
refreshPromise = refreshAccessToken().finally(() => {
refreshPromise = null;
});
}
return refreshPromise;
}
export async function request<T>(path: string, options: RequestInit = {}, retry = true): Promise<T> {
const response = await rawRequest(path, options);
if (response.status === 401 && retry && !path.includes("/auth/login") && !path.includes("/auth/refresh") && !path.includes("/auth/logout")) {
try {
await refreshOnce();
return request<T>(path, options, false);
} catch {
setAccessToken(null);
throw new AuthExpiredError();
}
}
return unwrap<T>(await parseJson(response), response.status);
}
export const login = (email: string, password: string) => request<AuthPayload>("/api/admin/auth/login", { method: "POST", body: JSON.stringify({ email, password }) }, false);
export const getProfile = () => request<AdminProfile>("/api/admin/system/profile");
export const logout = () => request<{ ok: boolean }>("/api/admin/auth/logout", { method: "POST" }, false);
export const getDashboard = () => request<Dashboard>("/api/admin/dashboard");
export const getSiteConfig = () => request<SiteConfig>("/api/admin/site-config");
export const getWanfa = async () => (await request<{ categories: WanfaCategory[] }>("/api/admin/wanfa/categories")).categories;
export const getConcierge = async () => (await request<{ advisors: ConciergeAdvisor[] }>("/api/admin/concierge/advisors")).advisors;
export const getLeads = () => request<{ items: Lead[] }>("/api/admin/leads?take=50");
export const updateLeadStatus = (leadId: string, status: string) =>
request<Lead>(`/api/admin/leads/${leadId}/status`, {
method: "PATCH",
body: JSON.stringify({ status }),
});
export const getSystemResource = (resource: string) => request<{ items: unknown[] }>(`/api/admin/system/${resource}`);