feat: add multi-source breed encyclopedia and two-hour refresh sessions
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user