feat: add multi-source breed encyclopedia and two-hour refresh sessions
ci-cd / verify (push) Successful in 1m59s
ci-cd / deploy (push) Skipped

This commit is contained in:
2026-10-05 13:38:24 +08:00
parent 607d709100
commit 9d7cf720a5
73 changed files with 25771 additions and 264 deletions
+28 -10
View File
@@ -1,5 +1,8 @@
import { NavLink, Navigate, useNavigate, useLocation, useParams } from "react-router-dom";
import Breeds from "@/components/admin/Breeds";
import { useEffect, useState, type FormEvent } from "react";
import {
Dog,
CalendarDays,
CheckCheck,
Files,
@@ -37,17 +40,21 @@ const icons = {
overview: LayoutDashboard,
users: Users,
pets: PawPrint,
breeds: Dog,
walks: Route,
meetups: CalendarDays,
tasks: CheckCheck,
files: Files,
};
type Tab = Section | "overview" | "breeds";
export default function App() {
const routerNavigate = useNavigate(), location = useLocation(), { section } = useParams();
const tab: Tab = section && section in labels ? section as Tab : "overview";
useEffect(() => { setDetail(null); setConfirm(null); setError(""); setNotice(""); setBusy(false); }, [location.pathname]);
const [token, setAuth] = useState(getToken()),
[username, setUsername] = useState("sundynix"),
[password, setPassword] = useState("");
const [tab, setTab] = useState<Section | "overview">("overview"),
[revision, refresh] = useState(0),
const [revision, refresh] = useState(0),
[stats, setStats] = useState<Stats | null>(null);
const [error, setError] = useState(""),
[busy, setBusy] = useState(false),
@@ -66,8 +73,10 @@ export default function App() {
setConfirm(null);
setError("登录已过期,请重新登录");
};
const sync = () => setAuth(getToken());
window.addEventListener("storage", sync);
window.addEventListener("admin-auth-expired", expired);
return () => window.removeEventListener("admin-auth-expired", expired);
return () => { window.removeEventListener("admin-auth-expired", expired); window.removeEventListener("storage", sync); };
}, []);
useEffect(() => {
if (!token || tab !== "overview") return;
@@ -94,7 +103,7 @@ export default function App() {
setError("");
try {
const value = await api.login(username.trim(), password);
setToken(value.token);
setToken(value);
setAuth(value.token);
setPassword("");
} catch (e) {
@@ -104,6 +113,7 @@ export default function App() {
}
}
function logout() {
void api.logout().catch(() => {});
clearToken();
setAuth("");
setPassword("");
@@ -113,9 +123,9 @@ export default function App() {
setError("");
setNotice("");
}
function navigate(section: Section | "overview") {
function navigate(section: Tab) {
setBusy(false);
setTab(section);
routerNavigate(`/admin/${section}`);
setError("");
setNotice("");
}
@@ -148,6 +158,12 @@ export default function App() {
? "启用用户"
: "停用用户"
: "取消约遛活动";
if (section !== "login" && !(section && Object.hasOwn(labels, section))) return <Navigate to="/admin/overview" replace />;
if (!token && section !== "login") return <Navigate to="/admin/login" replace state={{ from: location.pathname }} />;
if (token && section === "login") {
const from = location.state?.from;
return <Navigate to={typeof from === "string" && /^\/admin\/(overview|users|pets|breeds|walks|meetups|tasks|files)$/.test(from) ? from : "/admin/overview"} replace />;
}
if (!token)
return (
<main className="login-page">
@@ -231,20 +247,20 @@ export default function App() {
</div>
<p className="side-caption">陪伴工作台</p>
<nav aria-label="管理导航">
{(Object.keys(labels) as (Section | "overview")[]).map((key) => {
{(Object.keys(labels) as Tab[]).map((key) => {
const Icon = icons[key];
return (
<button
<NavLink
to={`/admin/${key}`}
key={key}
className={`nav-item ${tab === key ? "active" : ""}`}
aria-current={tab === key ? "page" : undefined}
title={labels[key]}
onClick={() => navigate(key)}
>
<Icon size={19} />
<span>{labels[key]}</span>
{tab === key && <span className="nav-dot" />}
</button>
</NavLink>
);
})}
</nav>
@@ -318,6 +334,8 @@ export default function App() {
)}
{tab === "overview" ? (
<Overview stats={stats} navigate={navigate} />
) : tab === "breeds" ? (
<Breeds revision={revision} />
) : (
<Records
key={tab}