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) => ( + + ))} + } /> + + + + + ); +}