feat: 公开首页(搜索+商品详情) + 好心人投稿 + 后台审核收纳
- 公开前端 SPA(根路径 /):首页大搜索框、检索结果、只读商品详情、贡献档案表单 - 公开写入端点 POST /api/public/submissions(无需登录,基础频率限流),投稿进入 submission 待审核队列,不直接写 product - 迁移 0006:submission 投稿表 + community 来源(trust=0.50) - 后台审核队列:列表(待审核/已通过/已驳回) → 查看投稿 → 通过(创建/补全商品 + 记 source=community + 字段级溯源 + 审计 + 重算质量分) / 驳回(记原因) - 公开只读 api 服务内嵌公开 SPA;Dockerfile.prod 增加 node 构建阶段 + 内嵌 dist Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { useState } from "react";
|
||||
import { Search, PlusCircle } from "lucide-react";
|
||||
import { api } from "../api";
|
||||
import type { ProductSummary } from "../types";
|
||||
|
||||
export default function Home({
|
||||
onOpen,
|
||||
onContribute,
|
||||
}: {
|
||||
onOpen: (id: string) => void;
|
||||
onContribute: () => void;
|
||||
}) {
|
||||
const [q, setQ] = useState("");
|
||||
const [items, setItems] = useState<ProductSummary[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [searched, setSearched] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function run(e?: React.FormEvent) {
|
||||
e?.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await api.search(q.trim(), 1, 30);
|
||||
setItems(res.items);
|
||||
setTotal(res.total);
|
||||
setSearched(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "搜索失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="text-center py-10">
|
||||
<h1 className="text-3xl font-bold text-gray-800">商品档案公共库</h1>
|
||||
<p className="mt-2 text-gray-500">
|
||||
输入商品名称或条码,检索客观、可溯源的商品资料。人人可查,人人可贡献。
|
||||
</p>
|
||||
<form onSubmit={run} className="mt-6 max-w-2xl mx-auto flex gap-2">
|
||||
<div className="flex-1 flex items-center gap-2 bg-white border rounded-lg px-3 shadow-sm focus-within:ring-2 focus-within:ring-emerald-400">
|
||||
<Search className="w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
autoFocus
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="例如:可乐、Nutella、5449000000996"
|
||||
className="flex-1 py-3 outline-none bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="px-6 rounded-lg bg-emerald-600 text-white font-medium hover:bg-emerald-700 disabled:opacity-60"
|
||||
>
|
||||
{loading ? "检索中…" : "检索"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="max-w-2xl mx-auto bg-red-50 text-red-700 text-sm rounded-md px-4 py-2">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searched && (
|
||||
<div className="mt-2">
|
||||
<div className="text-sm text-gray-500 mb-2">
|
||||
共 {total} 条结果{q ? `(关键词:${q})` : ""}
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<div className="bg-white border rounded-lg p-8 text-center text-gray-500">
|
||||
<p>没有找到相关商品。</p>
|
||||
<button
|
||||
onClick={onContribute}
|
||||
className="mt-3 inline-flex items-center gap-1.5 text-emerald-700 hover:underline"
|
||||
>
|
||||
<PlusCircle className="w-4 h-4" /> 你知道这个商品?去贡献档案
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="bg-white border rounded-lg divide-y">
|
||||
{items.map((p) => (
|
||||
<li key={p.id}>
|
||||
<button
|
||||
onClick={() => onOpen(p.id)}
|
||||
className="w-full text-left px-4 py-3 hover:bg-gray-50 flex items-center justify-between gap-4"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-gray-800">{p.name}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{p.brand || "未知品牌"}
|
||||
{p.gtin ? ` · ${p.gtin}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">{p.category_path || ""}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user