// API base derives from Vite's BASE_URL (/ping/) so it matches the nginx prefix. const API_BASE = `${import.meta.env.BASE_URL}api`; const TOKEN_KEY = "opengoods_admin_token"; export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } export function setToken(token: string) { localStorage.setItem(TOKEN_KEY, token); } export function clearToken() { localStorage.removeItem(TOKEN_KEY); } export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; } } async function request(path: string, options: RequestInit = {}): Promise { const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; const token = getToken(); if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); if (res.status === 401) { clearToken(); throw new ApiError(401, "登录已过期,请重新登录"); } const text = await res.text(); const data = text ? JSON.parse(text) : null; if (!res.ok) { const msg = data?.error?.message || `请求失败 (${res.status})`; throw new ApiError(res.status, msg); } return data as T; } export const api = { login: (username: string, password: string) => request<{ token: string; username: string }>("/login", { method: "POST", body: JSON.stringify({ username, password }), }), me: () => request<{ username: string }>("/me"), listProducts: ( q: string, page: number, size: number, sort?: string, order?: string, ) => request<{ items: import("./types").ProductRow[]; page: number; size: number; total: number; completeness_fields: string[]; }>( `/products?q=${encodeURIComponent(q)}&page=${page}&size=${size}` + (sort ? `&sort=${sort}&order=${order || "asc"}` : ""), ), getProduct: (id: string) => request(`/products/${id}`), createProduct: (body: unknown) => request("/products", { method: "POST", body: JSON.stringify(body), }), updateProduct: (id: string, body: unknown) => request(`/products/${id}`, { method: "PUT", body: JSON.stringify(body), }), listAudit: (id: string) => request<{ items: import("./types").AuditEntry[] }>(`/products/${id}/audit`), addImage: (id: string, url: string, kind: string) => request(`/products/${id}/images`, { method: "POST", body: JSON.stringify({ url, kind }), }), deleteImage: (id: string, imageId: string) => request<{ status: string }>(`/products/${id}/images/${imageId}`, { method: "DELETE", }), addMsrp: (id: string, body: unknown) => request(`/products/${id}/msrp`, { method: "POST", body: JSON.stringify(body), }), deleteMsrp: (id: string, msrpId: string) => request<{ status: string }>(`/products/${id}/msrp/${msrpId}`, { method: "DELETE", }), addBarcode: (id: string, body: unknown) => request(`/products/${id}/barcodes`, { method: "POST", body: JSON.stringify(body), }), deleteBarcode: (id: string, barcodeId: string) => request<{ status: string }>(`/products/${id}/barcodes/${barcodeId}`, { method: "DELETE", }), setPrimaryBarcode: (id: string, barcodeId: string) => request( `/products/${id}/barcodes/${barcodeId}/primary`, { method: "POST" }, ), listBrands: () => request<{ items: import("./types").Brand[] }>("/brands"), createBrand: (name: string) => request("/brands", { method: "POST", body: JSON.stringify({ name }), }), updateBrand: (id: string, name: string) => request(`/brands/${id}`, { method: "PUT", body: JSON.stringify({ name }), }), mergeBrands: (id: string, targetId: string) => request(`/brands/${id}/merge`, { method: "POST", body: JSON.stringify({ target_id: targetId }), }), deleteBrand: (id: string) => request<{ status: string }>(`/brands/${id}`, { method: "DELETE" }), listKindFields: (kind: string) => request<{ items: import("./types").KindField[]; kind: string }>( `/kind-fields?kind=${encodeURIComponent(kind)}`, ), listCategories: () => request<{ items: import("./types").Category[] }>("/categories"), createCategory: (body: import("./types").CategoryInput) => request("/categories", { method: "POST", body: JSON.stringify(body), }), updateCategory: (id: string, body: import("./types").CategoryInput) => request(`/categories/${id}`, { method: "PUT", body: JSON.stringify(body), }), deleteCategory: (id: string) => request<{ status: string }>(`/categories/${id}`, { method: "DELETE" }), listSubmissions: (status: string, page: number, size: number) => request<{ items: import("./types").SubmissionRow[]; page: number; size: number; total: number; pending: number; }>(`/submissions?status=${encodeURIComponent(status)}&page=${page}&size=${size}`), getSubmission: (id: string) => request(`/submissions/${id}`), approveSubmission: (id: string) => request(`/submissions/${id}/approve`, { method: "POST", }), rejectSubmission: (id: string, note: string) => request<{ status: string }>(`/submissions/${id}/reject`, { method: "POST", body: JSON.stringify({ note }), }), listApiKeys: () => request<{ items: import("./types").ApiKey[] }>("/keys"), createApiKey: (body: { name: string; owner_email?: string; tier?: string; rate_limit_per_min?: number; quota_total?: number; }) => request<{ key: string; item: import("./types").ApiKey; warning: string }>( "/keys", { method: "POST", body: JSON.stringify(body) }, ), revokeApiKey: (id: string) => request<{ status: string }>(`/keys/${id}`, { method: "DELETE" }), stats: () => request("/stats"), auditLog: (page: number, size: number) => request<{ items: import("./types").AuditLogRow[]; page: number; size: number; total: number; }>(`/audit?page=${page}&size=${size}`), bulkProducts: (body: { ids: string[]; action: "status" | "category"; status?: string; category_id?: string | null; }) => request<{ status: string; affected: number }>("/products/bulk", { method: "POST", body: JSON.stringify(body), }), };