212 lines
6.9 KiB
TypeScript
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>
|
|
);
|
|
}
|