diff --git a/sundynix-admin/package-lock.json b/sundynix-admin/package-lock.json
index f48c6a2..71df158 100644
--- a/sundynix-admin/package-lock.json
+++ b/sundynix-admin/package-lock.json
@@ -9,7 +9,8 @@
"version": "0.1.0",
"dependencies": {
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.1.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
@@ -1470,6 +1471,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2215,6 +2229,44 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
+ "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
+ "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.17.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -2356,6 +2408,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
diff --git a/sundynix-admin/package.json b/sundynix-admin/package.json
index 7f7ed77..8dc3fe9 100644
--- a/sundynix-admin/package.json
+++ b/sundynix-admin/package.json
@@ -10,7 +10,8 @@
},
"dependencies": {
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.1.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
diff --git a/sundynix-admin/src/App.tsx b/sundynix-admin/src/App.tsx
index 00650a2..5bc802e 100644
--- a/sundynix-admin/src/App.tsx
+++ b/sundynix-admin/src/App.tsx
@@ -1,80 +1,11 @@
-import { useEffect, useState } from "react";
-import { ModelsPage } from "./ModelsPage";
-import { gatewayOnline } from "./api";
-
-type Tab = "models" | "datasources" | "tenants" | "guardrails";
-const NAV: Array<{ key: Tab; label: string; ready?: boolean }> = [
- { key: "models", label: "模型", ready: true },
- { key: "datasources", label: "数据源" },
- { key: "tenants", label: "租户" },
- { key: "guardrails", label: "护栏" },
-];
-
-function Soon({ title, desc }: { title: string; desc: string }) {
- return (
-
-
{title}
-
{desc}
-
规划中
-
- );
-}
+import { HashRouter } from "react-router-dom";
+import { AppShell } from "./shell/AppShell";
+// 用 HashRouter:纯静态托管/桌面内嵌都能深链,无需服务端路由配置。
export default function App() {
- const [tab, setTab] = useState("models");
- const [online, setOnline] = useState(false);
- useEffect(() => {
- const ping = () => gatewayOnline().then(setOnline);
- ping();
- const id = setInterval(ping, 4000);
- return () => clearInterval(id);
- }, []);
-
return (
-
-
-
-
-
- {NAV.find((n) => n.key === tab)?.label}
-
- {tab === "models" && }
- {tab === "datasources" && (
-
- )}
- {tab === "tenants" && (
-
- )}
- {tab === "guardrails" && (
-
- )}
-
-
+
+
+
);
}
diff --git a/sundynix-admin/src/components/Soon.tsx b/sundynix-admin/src/components/Soon.tsx
new file mode 100644
index 0000000..0247af2
--- /dev/null
+++ b/sundynix-admin/src/components/Soon.tsx
@@ -0,0 +1,10 @@
+// 规划中页面占位(路由目标,后续替换为真实页面即可)。
+export function Soon({ title, desc }: { title: string; desc: string }) {
+ return (
+
+
{title}
+
{desc}
+
规划中
+
+ );
+}
diff --git a/sundynix-admin/src/ModelsPage.tsx b/sundynix-admin/src/pages/ModelsPage.tsx
similarity index 99%
rename from sundynix-admin/src/ModelsPage.tsx
rename to sundynix-admin/src/pages/ModelsPage.tsx
index 54c7ea8..0e6fdd0 100644
--- a/sundynix-admin/src/ModelsPage.tsx
+++ b/sundynix-admin/src/pages/ModelsPage.tsx
@@ -7,7 +7,7 @@ import {
testModel,
type Model,
type ModelInput,
-} from "./api";
+} from "../api";
const EMPTY: ModelInput = {
provider: "openai-compatible",
diff --git a/sundynix-admin/src/routes.tsx b/sundynix-admin/src/routes.tsx
new file mode 100644
index 0000000..05e2d1f
--- /dev/null
+++ b/sundynix-admin/src/routes.tsx
@@ -0,0 +1,63 @@
+import { lazy, type ReactNode } from "react";
+import { Soon } from "./components/Soon";
+
+// 路由注册表 —— 控制台的单一事实源:导航 + 内容都从这里派生。
+// 新增页面 = 在此加一条;real 页面用 lazy 懒加载(代码分割)。
+const ModelsPage = lazy(() => import("./pages/ModelsPage").then((m) => ({ default: m.ModelsPage })));
+
+export interface RouteDef {
+ path: string;
+ label: string;
+ group: string;
+ ready?: boolean;
+ element: ReactNode;
+}
+
+export const routes: RouteDef[] = [
+ {
+ path: "/models",
+ label: "模型",
+ group: "配置",
+ ready: true,
+ element: ,
+ },
+ {
+ path: "/datasources",
+ label: "数据源",
+ group: "配置",
+ element: (
+
+ ),
+ },
+ {
+ path: "/tenants",
+ label: "租户",
+ group: "平台",
+ element: ,
+ },
+ {
+ path: "/guardrails",
+ label: "护栏",
+ group: "平台",
+ element: ,
+ },
+];
+
+export const defaultPath = "/models";
+
+// 派生分组导航(保持注册顺序)。
+export function navGroups(): Array<{ group: string; items: RouteDef[] }> {
+ const out: Array<{ group: string; items: RouteDef[] }> = [];
+ for (const r of routes) {
+ let g = out.find((x) => x.group === r.group);
+ if (!g) {
+ g = { group: r.group, items: [] };
+ out.push(g);
+ }
+ g.items.push(r);
+ }
+ return out;
+}
diff --git a/sundynix-admin/src/shell/AppShell.tsx b/sundynix-admin/src/shell/AppShell.tsx
new file mode 100644
index 0000000..fe17a94
--- /dev/null
+++ b/sundynix-admin/src/shell/AppShell.tsx
@@ -0,0 +1,67 @@
+import { Suspense, useEffect, useState } from "react";
+import { NavLink, Routes, Route, Navigate, useLocation } from "react-router-dom";
+
+import { routes, navGroups, defaultPath } from "../routes";
+import { gatewayOnline } from "../api";
+
+// 控制台外壳:导航与内容均由路由注册表派生(动态路由)。
+export function AppShell() {
+ const [online, setOnline] = useState(false);
+ const loc = useLocation();
+ const current = routes.find((r) => r.path === loc.pathname);
+
+ useEffect(() => {
+ const ping = () => gatewayOnline().then(setOnline);
+ ping();
+ const id = setInterval(ping, 4000);
+ return () => clearInterval(id);
+ }, []);
+
+ return (
+
+
+
+
+ {current?.label ?? ""}
+ 加载中… }>
+
+ {routes.map((r) => (
+
+ ))}
+ } />
+
+
+
+
+ );
+}