bd1d3bde7c
- categories 接口返回 archive_kind - 新增公开只读接口 /api/v1/kind-fields?kind=X 返回字段模板 - 投稿 payload 支持通用 attributes,审核通过写入 product.attributes (JSONB 合并) - food_detail 仅在含食品数据时才 upsert (药品/3C/通用不再产生空行) - 前端 Contribute 按所选品类 archive_kind 动态渲染字段 (食品营养 / 药品 18 字段 / 3C / 通用),除商品名外均选填 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
95 lines
2.5 KiB
TypeScript
95 lines
2.5 KiB
TypeScript
import type { Category, KindField, Product, ProductSummary, SubmissionInput } from "./types";
|
|
|
|
async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(path, {
|
|
...init,
|
|
headers: { "Content-Type": "application/json", ...(init?.headers || {}) },
|
|
});
|
|
if (!res.ok) {
|
|
let msg = `请求失败 (${res.status})`;
|
|
try {
|
|
const body = await res.json();
|
|
if (body?.error?.message) msg = body.error.message;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error(msg);
|
|
}
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
export interface SearchResult {
|
|
items: ProductSummary[];
|
|
page: number;
|
|
size: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface SearchFilters {
|
|
brand?: string;
|
|
country?: string;
|
|
}
|
|
|
|
export interface Stats {
|
|
total: number;
|
|
qualified: number;
|
|
min_score: number;
|
|
}
|
|
|
|
export interface KeyResponse {
|
|
email: string;
|
|
api_key: string;
|
|
key_prefix: string;
|
|
rate_limit_per_min: number;
|
|
quota_total: number;
|
|
}
|
|
|
|
export interface AccountInfo {
|
|
email: string;
|
|
key_prefix: string;
|
|
rate_limit_per_min: number;
|
|
quota_total: number;
|
|
quota_used: number;
|
|
quota_remaining: number;
|
|
}
|
|
|
|
export const api = {
|
|
stats: () => req<Stats>(`/api/v1/stats`),
|
|
search: (q: string, page = 1, size = 20, filters: SearchFilters = {}) => {
|
|
const params = new URLSearchParams({
|
|
q,
|
|
page: String(page),
|
|
size: String(size),
|
|
});
|
|
if (filters.brand) params.set("brand", filters.brand);
|
|
if (filters.country) params.set("country", filters.country);
|
|
return req<SearchResult>(`/api/v1/products/search?${params.toString()}`);
|
|
},
|
|
product: (id: string) => req<Product>(`/api/v1/products/${id}`),
|
|
categories: () => req<{ items: Category[] }>(`/api/v1/categories`),
|
|
kindFields: (kind: string) =>
|
|
req<{ items: KindField[]; kind: string }>(
|
|
`/api/v1/kind-fields?kind=${encodeURIComponent(kind)}`,
|
|
),
|
|
submit: (input: SubmissionInput) =>
|
|
req<{ id: string; status: string }>(`/api/public/submissions`, {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
register: (email: string, password: string) =>
|
|
req<KeyResponse>(`/api/v1/register`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ email, password }),
|
|
}),
|
|
account: (email: string, password: string) =>
|
|
req<AccountInfo>(`/api/v1/account`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ email, password }),
|
|
}),
|
|
regenerate: (email: string, password: string) =>
|
|
req<KeyResponse>(`/api/v1/account/regenerate`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ email, password }),
|
|
}),
|
|
};
|