Files
goods/public-frontend/src/components/ApiDocs.tsx
T
oyaegeli98668 5bfb2d3a5f
CI / Go (api) (pull_request) Failing after 15s
CI / Python (ingestion) (pull_request) Successful in 7s
CI / Migrations (postgres) (pull_request) Failing after 19s
chore: 站名改为「天工商品档案公共仓」(公开站 + 后台标题/页头/页脚/API 文案)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 05:37:23 +00:00

280 lines
9.5 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 { useState } from "react";
import { Check, Copy } from "lucide-react";
const ORIGIN = typeof window !== "undefined" ? window.location.origin : "https://goods.tangshasha.com";
const BASE = `${ORIGIN}/api/v1`;
function CopyBtn({ text }: { text: string }) {
const [done, setDone] = useState(false);
return (
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(text);
setDone(true);
setTimeout(() => setDone(false), 1200);
} catch {
/* clipboard unavailable */
}
}}
className="text-gray-400 hover:text-gray-600"
title="复制"
>
{done ? <Check className="w-4 h-4 text-emerald-600" /> : <Copy className="w-4 h-4" />}
</button>
);
}
function Code({ children }: { children: string }) {
return (
<div className="relative group">
<pre className="bg-gray-900 text-gray-100 text-xs rounded-md p-3 overflow-x-auto whitespace-pre">
{children}
</pre>
<div className="absolute top-2 right-2 opacity-70 group-hover:opacity-100">
<CopyBtn text={children} />
</div>
</div>
);
}
function Method({ m }: { m: string }) {
const color = m === "GET" ? "bg-sky-100 text-sky-700" : "bg-emerald-100 text-emerald-700";
return <span className={`text-xs font-mono font-semibold rounded px-1.5 py-0.5 ${color}`}>{m}</span>;
}
type Param = { name: string; required?: boolean; desc: string };
function Endpoint({
method,
path,
title,
desc,
params,
example,
response,
}: {
method: string;
path: string;
title: string;
desc: string;
params?: Param[];
example: string;
response: string;
}) {
return (
<div className="bg-white border rounded-lg p-5">
<div className="flex items-center gap-2 flex-wrap">
<Method m={method} />
<code className="text-sm text-gray-800 font-mono break-all">{path}</code>
<span className="ml-auto" />
<CopyBtn text={`${ORIGIN}${path}`} />
</div>
<div className="mt-2 font-medium text-gray-800">{title}</div>
<p className="text-sm text-gray-500 mt-0.5">{desc}</p>
{params && params.length > 0 && (
<table className="mt-3 w-full text-sm">
<thead className="text-gray-400 text-left">
<tr>
<th className="font-medium pr-4 pb-1"></th>
<th className="font-medium pr-4 pb-1"></th>
<th className="font-medium pb-1"></th>
</tr>
</thead>
<tbody className="align-top">
{params.map((p) => (
<tr key={p.name}>
<td className="pr-4 py-0.5 font-mono text-gray-700">{p.name}</td>
<td className="pr-4 py-0.5 text-gray-500">{p.required ? "是" : "否"}</td>
<td className="py-0.5 text-gray-600">{p.desc}</td>
</tr>
))}
</tbody>
</table>
)}
<div className="mt-3 text-xs text-gray-400 mb-1"></div>
<Code>{example}</Code>
<div className="mt-3 text-xs text-gray-400 mb-1"></div>
<Code>{response}</Code>
</div>
);
}
export default function ApiDocs() {
return (
<div className="space-y-5">
<div className="bg-white border rounded-lg p-5">
<h1 className="text-2xl font-bold text-gray-800">API </h1>
<p className="mt-2 text-gray-600 text-sm leading-relaxed">
<strong></strong> REST API
/Nutri-Score
JSONUTF-8/
</p>
<div className="mt-3 text-sm text-gray-700">
<div>
<code className="font-mono bg-gray-100 rounded px-1.5 py-0.5">{BASE}</code>
</div>
<ul className="mt-2 list-disc pl-5 text-gray-600 space-y-1">
<li> API Key / Token GET </li>
<li>
<code className="font-mono">page</code> 1
<code className="font-mono">size</code> 20 100
</li>
<li>
<code className="font-mono">404</code>
<code className="font-mono">{` {"error":{"code","message","request_id"}}`}</code>
</li>
<li></li>
</ul>
</div>
</div>
<Endpoint
method="GET"
path="/healthz"
title="健康检查"
desc="服务存活探针。"
example={`curl ${ORIGIN}/healthz`}
response={`{ "status": "ok" }`}
/>
<Endpoint
method="GET"
path="/api/v1/products/barcode/{gtin}"
title="按条码查询商品"
desc="按 GTIN(条码)精确查询单个商品档案。"
params={[{ name: "gtin", required: true, desc: "商品条码(路径参数),如 5449000000996" }]}
example={`curl ${BASE}/products/barcode/5449000000996`}
response={`{
"id": "…",
"gtin": "5449000000996",
"name": "可口可乐 经典原味",
"brand": "Coca-Cola",
"category_path": "food.beverages.carbonated",
"net_content_value": 33,
"net_content_unit": "cl",
"country_of_origin": "Algeria",
"quality_score": 0.81
}`}
/>
<Endpoint
method="GET"
path="/api/v1/products/search"
title="搜索商品"
desc="按名称模糊搜索,可按品类过滤,支持分页。"
params={[
{ name: "q", desc: "关键词(名称/条码),留空返回全部" },
{ name: "category", desc: "品类编码过滤,如 food.beverages" },
{ name: "page", desc: "页码,默认 1" },
{ name: "size", desc: "每页条数,默认 20,最大 100" },
]}
example={`curl "${BASE}/products/search?q=nutella&page=1&size=20"`}
response={`{
"items": [
{ "id": "…", "gtin": "3017624010701",
"name": "Nutella", "brand": "Ferrero",
"category_path": "food.snacks.chocolate" }
],
"page": 1, "size": 20, "total": 1
}`}
/>
<Endpoint
method="GET"
path="/api/v1/products/{id}"
title="商品详情"
desc="按商品 UUID 获取完整档案(含配料、营养、添加剂、图片、MSRP 等)。"
params={[{ name: "id", required: true, desc: "商品 UUID(路径参数)" }]}
example={`curl ${BASE}/products/{id}`}
response={`{
"id": "…", "name": "…", "brand": "…",
"ingredients_text": "…",
"nutriments": { "energy_kcal": 42, "sugars_g": 10.6 },
"nutrition_basis": "per_100g",
"nutri_score": "E",
"images": [], "msrp": [],
"quality_score": 0.81
}`}
/>
<Endpoint
method="GET"
path="/api/v1/products/{id}/nutriments"
title="商品营养成分"
desc="仅返回该商品的营养字段。"
params={[{ name: "id", required: true, desc: "商品 UUID(路径参数)" }]}
example={`curl ${BASE}/products/{id}/nutriments`}
response={`{
"basis": "per_100g",
"values": { "energy_kcal": 42, "sugars_g": 10.6, "salt_g": 0 }
}`}
/>
<Endpoint
method="GET"
path="/api/v1/products/{id}/msrp"
title="厂商建议零售价快照"
desc="官方建议零售价历史快照,仅供参考,不含任何购买入口。"
params={[{ name: "id", required: true, desc: "商品 UUID(路径参数)" }]}
example={`curl ${BASE}/products/{id}/msrp`}
response={`{
"items": [
{ "amount": 3.5, "currency": "CNY", "region": "CN", "effective_date": "2025-01-01" }
],
"disclaimer": "厂商建议零售价历史快照,仅供参考,不构成购买建议…"
}`}
/>
<Endpoint
method="GET"
path="/api/v1/brands"
title="品牌列表"
desc="分页列出全部品牌。"
params={[
{ name: "page", desc: "页码,默认 1" },
{ name: "size", desc: "每页条数,默认 20,最大 100" },
]}
example={`curl "${BASE}/brands?page=1&size=20"`}
response={`{
"items": [ { "id": "…", "name": "Ferrero" } ],
"page": 1, "size": 20, "total": 4
}`}
/>
<Endpoint
method="GET"
path="/api/v1/categories"
title="品类树"
desc="返回完整品类层级(编码 + 名称)。"
example={`curl ${BASE}/categories`}
response={`{
"items": [
{ "id": "…", "code": "food", "name": "食品饮料", "parent_id": null }
]
}`}
/>
<Endpoint
method="GET"
path="/api/v1/sources/{id}"
title="数据来源"
desc="按 ID 查询单个数据来源(含许可、信任权重)。"
params={[{ name: "id", required: true, desc: "来源 UUID(路径参数)" }]}
example={`curl ${BASE}/sources/{id}`}
response={`{
"id": "…", "name": "openfoodfacts",
"license": "ODbL", "trust_weight": 0.7
}`}
/>
<div className="text-xs text-gray-400 leading-relaxed">
/
OpenFoodFacts ODbL
</div>
</div>
);
}