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:
@@ -3,16 +3,28 @@ import { api, clearToken, getToken } from "./api";
|
||||
import Login from "./components/Login";
|
||||
import ProductList from "./components/ProductList";
|
||||
import ProductDetail from "./components/ProductDetail";
|
||||
import { LogOut, Package } from "lucide-react";
|
||||
import SubmissionsPage from "./components/SubmissionsPage";
|
||||
import { Inbox, LogOut, Package } from "lucide-react";
|
||||
|
||||
type Tab = "products" | "submissions";
|
||||
type View = { name: "list" } | { name: "detail"; id: string };
|
||||
|
||||
export default function App() {
|
||||
const [authed, setAuthed] = useState(false);
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [username, setUsername] = useState("");
|
||||
const [tab, setTab] = useState<Tab>("products");
|
||||
const [pending, setPending] = useState<number | null>(null);
|
||||
const [view, setView] = useState<View>({ name: "list" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!authed) return;
|
||||
api
|
||||
.listSubmissions("pending", 1, 1)
|
||||
.then((r) => setPending(r.pending))
|
||||
.catch(() => undefined);
|
||||
}, [authed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
setChecking(false);
|
||||
@@ -53,9 +65,41 @@ export default function App() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<header className="flex items-center justify-between bg-white px-6 py-3 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-lg font-semibold text-gray-800">
|
||||
<Package className="h-5 w-5 text-emerald-600" />
|
||||
OpenGoods 商品档案后台
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2 text-lg font-semibold text-gray-800">
|
||||
<Package className="h-5 w-5 text-emerald-600" />
|
||||
OpenGoods 商品档案后台
|
||||
</div>
|
||||
<nav className="flex items-center gap-1 text-sm">
|
||||
<button
|
||||
onClick={() => {
|
||||
setTab("products");
|
||||
setView({ name: "list" });
|
||||
}}
|
||||
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 ${
|
||||
tab === "products"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
: "text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Package className="h-4 w-4" /> 商品档案
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("submissions")}
|
||||
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 ${
|
||||
tab === "submissions"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
: "text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Inbox className="h-4 w-4" /> 待审核投稿
|
||||
{pending != null && pending > 0 && (
|
||||
<span className="ml-1 text-xs bg-amber-500 text-white rounded-full px-1.5">
|
||||
{pending}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||
<span>{username}</span>
|
||||
@@ -68,13 +112,12 @@ export default function App() {
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
{view.name === "list" ? (
|
||||
{tab === "submissions" ? (
|
||||
<SubmissionsPage onPending={setPending} />
|
||||
) : view.name === "list" ? (
|
||||
<ProductList onOpen={(id) => setView({ name: "detail", id })} />
|
||||
) : (
|
||||
<ProductDetail
|
||||
id={view.id}
|
||||
onBack={() => setView({ name: "list" })}
|
||||
/>
|
||||
<ProductDetail id={view.id} onBack={() => setView({ name: "list" })} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user