Files
goods/admin-frontend/src/App.tsx
T
sulaimaannaasif6866 51304d782a
CI / Go (api) (pull_request) Successful in 9s
CI / Python (ingestion) (pull_request) Successful in 10s
CI / Migrations (postgres) (pull_request) Successful in 14s
feat(admin): 商品编辑页增加上一个/下一个导航
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-21 05:50:20 +00:00

212 lines
6.9 KiB
TypeScript

import { useEffect, useState } from "react";
import { api, clearToken, getToken } from "./api";
import Login from "./components/Login";
import ProductList from "./components/ProductList";
import ProductDetail from "./components/ProductDetail";
import SubmissionsPage from "./components/SubmissionsPage";
import ApiKeysPage from "./components/ApiKeysPage";
import CategoriesPage from "./components/CategoriesPage";
import BrandsPage from "./components/BrandsPage";
import StatsPage from "./components/StatsPage";
import AuditLogPage from "./components/AuditLogPage";
import { BarChart3, FolderTree, Inbox, KeyRound, LogOut, Package, ScrollText, Tag } from "lucide-react";
type Tab =
| "overview"
| "products"
| "submissions"
| "categories"
| "brands"
| "audit"
| "keys";
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" });
const [navIds, setNavIds] = useState<string[]>([]);
useEffect(() => {
if (!authed) return;
api
.listSubmissions("pending", 1, 1)
.then((r) => setPending(r.pending))
.catch(() => undefined);
}, [authed]);
useEffect(() => {
if (!getToken()) {
setChecking(false);
return;
}
api
.me()
.then((r) => {
setUsername(r.username);
setAuthed(true);
})
.catch(() => clearToken())
.finally(() => setChecking(false));
}, []);
function onLoggedIn(name: string) {
setUsername(name);
setAuthed(true);
setView({ name: "list" });
}
function logout() {
clearToken();
setAuthed(false);
setUsername("");
}
if (checking) {
return (
<div className="flex h-full items-center justify-center text-gray-500">
</div>
);
}
if (!authed) return <Login onLoggedIn={onLoggedIn} />;
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-6">
<div className="flex items-center gap-2 text-lg font-semibold text-gray-800">
<Package className="h-5 w-5 text-emerald-600" />
·
</div>
<nav className="flex items-center gap-1 text-sm">
<button
onClick={() => setTab("overview")}
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 ${
tab === "overview"
? "bg-emerald-50 text-emerald-700"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<BarChart3 className="h-4 w-4" />
</button>
<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>
<button
onClick={() => {
setTab("categories");
setView({ name: "list" });
}}
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 ${
tab === "categories"
? "bg-emerald-50 text-emerald-700"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<FolderTree className="h-4 w-4" />
</button>
<button
onClick={() => setTab("brands")}
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 ${
tab === "brands"
? "bg-emerald-50 text-emerald-700"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<Tag className="h-4 w-4" />
</button>
<button
onClick={() => setTab("audit")}
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 ${
tab === "audit"
? "bg-emerald-50 text-emerald-700"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<ScrollText className="h-4 w-4" />
</button>
<button
onClick={() => setTab("keys")}
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 ${
tab === "keys"
? "bg-emerald-50 text-emerald-700"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<KeyRound className="h-4 w-4" /> API
</button>
</nav>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<span>{username}</span>
<button
onClick={logout}
className="flex items-center gap-1 rounded px-2 py-1 text-gray-500 hover:bg-gray-100 hover:text-gray-800"
>
<LogOut className="h-4 w-4" /> 退
</button>
</div>
</header>
<main className="flex-1 overflow-auto p-6">
{tab === "overview" ? (
<StatsPage />
) : tab === "audit" ? (
<AuditLogPage />
) : tab === "keys" ? (
<ApiKeysPage />
) : tab === "categories" ? (
<CategoriesPage />
) : tab === "brands" ? (
<BrandsPage />
) : tab === "submissions" ? (
<SubmissionsPage onPending={setPending} />
) : view.name === "list" ? (
<ProductList
onOpen={(id, ids) => {
setNavIds(ids);
setView({ name: "detail", id });
}}
/>
) : (
<ProductDetail
id={view.id}
ids={navIds}
onNavigate={(id) => setView({ name: "detail", id })}
onBack={() => setView({ name: "list" })}
/>
)}
</main>
</div>
);
}