fix(router): add SPA fallback and tab URL sync

This commit is contained in:
lsy committed 2026-02-21 21:44:13 +08:00
1 parent 32f5ed8d6b
commit 00730bb375
2 files changed
+68 -3

No files matched your search

+48 -1
View File
@@ -40,6 +40,31 @@ if (!root) {
throw new Error("Missing #app root");
}
const normalizePath = (path: string) => {
if (path.length <= 1) {
return "/";
}
return path.replace(/\/+$/, "") || "/";
};
const tabToPath: Record<TabId, string> = {
dashboard: "/",
channels: "/channels",
models: "/models",
tokens: "/tokens",
usage: "/usage",
settings: "/settings",
};
const pathToTab: Record<string, TabId> = {
"/": "dashboard",
"/channels": "channels",
"/models": "models",
"/tokens": "tokens",
"/usage": "usage",
"/settings": "settings",
};
/**
* Renders the admin console application.
*
@@ -50,7 +75,13 @@ const App = () => {
const [token, setToken] = useState<string | null>(() =>
localStorage.getItem("admin_token"),
);
const [activeTab, setActiveTab] = useState<TabId>("dashboard");
const [activeTab, setActiveTab] = useState<TabId>(() => {
if (typeof window === "undefined") {
return "dashboard";
}
const normalized = normalizePath(window.location.pathname);
return pathToTab[normalized] ?? "dashboard";
});
const [loading, setLoading] = useState(false);
const [notice, setNotice] = useState("");
const [data, setData] = useState<AdminData>(initialData);
@@ -161,6 +192,17 @@ const App = () => {
}
}, [token, activeTab, loadTab]);
useEffect(() => {
const handlePopState = () => {
const normalized = normalizePath(window.location.pathname);
setActiveTab(pathToTab[normalized] ?? "dashboard");
};
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
useEffect(() => {
if (!data.settings) {
return;
@@ -227,6 +269,11 @@ const App = () => {
}, []);
const handleTabChange = useCallback((tabId: TabId) => {
const nextPath = tabToPath[tabId];
const normalized = normalizePath(window.location.pathname);
if (normalized !== nextPath) {
history.pushState(null, "", nextPath);
}
setActiveTab(tabId);
}, []);
+20 -2
View File
@@ -111,7 +111,7 @@ app.route("/api/group", newapiGroupRoutes);
app.route("/v1", proxyRoutes);
app.notFound((c) => {
app.notFound(async (c) => {
const path = c.req.path;
if (
path === "/api" ||
@@ -124,7 +124,25 @@ app.notFound((c) => {
const assets = (
c.env as { ASSETS?: { fetch: (input: Request) => Promise<Response> } }
).ASSETS;
return assets ? assets.fetch(c.req.raw) : c.text("Not Found", 404);
if (!assets) {
return c.text("Not Found", 404);
}
const res = await assets.fetch(c.req.raw);
if (res.status !== 404) {
return res;
}
const accept = c.req.header("accept") ?? "";
const isHtml = accept.includes("text/html");
const isFile = path.includes(".");
if (!isHtml || isFile) {
return res;
}
const url = new URL(c.req.url);
url.pathname = "/index.html";
return assets.fetch(new Request(url.toString(), c.req.raw));
});
export default app;