From f6a669070d1886333bfbdb7db5a6ac6f69b87d19 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Wed, 10 Jun 2026 16:09:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor(admin):=20=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E8=B7=AF=E7=94=B1=E8=A1=A8=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E5=8A=A8=E6=80=81=E8=B7=AF=E7=94=B1=20(react-router)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 控制台从 useState 切 tab + 硬编码条件渲染,改为路由注册表驱动 + 真实 URL 路由, 加页面只需在 routes.tsx 加一条,不动外壳。 - 依赖 react-router-dom v7;App=HashRouter(静态托管/桌面内嵌都能深链) - routes.tsx:路由注册表(单一事实源,导航+内容都派生);real 页面 lazy 懒加载(代码分割) - shell/AppShell:NavLink 分组导航(配置/平台) + Routes + Suspense + 健康灯,全从注册表派生 - 页面归入 pages/(ModelsPage 移入),components/Soon 占位复用 - 验证:npm build✓(ModelsPage 独立 chunk=懒加载生效);真实浏览器——默认重定向 #/models、 nav 切换改 URL hash、深链 #/guardrails 直达、浏览器后退回 #/datasources、active 高亮 Co-Authored-By: Claude Opus 4.8 --- sundynix-admin/package-lock.json | 60 +++++++++++++- sundynix-admin/package.json | 3 +- sundynix-admin/src/App.tsx | 81 ++----------------- sundynix-admin/src/components/Soon.tsx | 10 +++ sundynix-admin/src/{ => pages}/ModelsPage.tsx | 2 +- sundynix-admin/src/routes.tsx | 63 +++++++++++++++ sundynix-admin/src/shell/AppShell.tsx | 67 +++++++++++++++ 7 files changed, 208 insertions(+), 78 deletions(-) create mode 100644 sundynix-admin/src/components/Soon.tsx rename sundynix-admin/src/{ => pages}/ModelsPage.tsx (99%) create mode 100644 sundynix-admin/src/routes.tsx create mode 100644 sundynix-admin/src/shell/AppShell.tsx 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) => ( + + ))} + } /> + + + + + ); +}