refactor(admin): 控制台改为路由表驱动的动态路由 (react-router)
控制台从 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 <noreply@anthropic.com>
This commit is contained in:
Generated
+59
-1
@@ -9,7 +9,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
@@ -1470,6 +1471,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -2215,6 +2229,44 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -2356,6 +2408,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
|
|||||||
@@ -1,80 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { HashRouter } from "react-router-dom";
|
||||||
import { ModelsPage } from "./ModelsPage";
|
import { AppShell } from "./shell/AppShell";
|
||||||
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 (
|
|
||||||
<div className="rounded-lg border border-dashed bg-gray-50 p-6">
|
|
||||||
<div className="mb-1 text-sm font-semibold text-gray-600">{title}</div>
|
|
||||||
<p className="text-xs leading-relaxed text-gray-400">{desc}</p>
|
|
||||||
<span className="mt-3 inline-block rounded bg-gray-200 px-2 py-0.5 text-[10px] text-gray-500">规划中</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 用 HashRouter:纯静态托管/桌面内嵌都能深链,无需服务端路由配置。
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [tab, setTab] = useState<Tab>("models");
|
|
||||||
const [online, setOnline] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const ping = () => gatewayOnline().then(setOnline);
|
|
||||||
ping();
|
|
||||||
const id = setInterval(ping, 4000);
|
|
||||||
return () => clearInterval(id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen text-gray-900">
|
<HashRouter>
|
||||||
<aside className="flex w-56 shrink-0 flex-col border-r bg-gray-50">
|
<AppShell />
|
||||||
<div className="border-b p-4">
|
</HashRouter>
|
||||||
<div className="text-sm font-bold text-gray-800">sundynix-agentix</div>
|
|
||||||
<div className="text-[11px] text-gray-400">运维控制台</div>
|
|
||||||
</div>
|
|
||||||
<nav className="flex flex-col p-2">
|
|
||||||
{NAV.map((n) => (
|
|
||||||
<button
|
|
||||||
key={n.key}
|
|
||||||
onClick={() => setTab(n.key)}
|
|
||||||
className={`flex items-center justify-between rounded px-3 py-2 text-left text-sm ${
|
|
||||||
tab === n.key ? "bg-violet-50 font-medium text-violet-700" : "text-gray-600 hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{n.label}
|
|
||||||
{!n.ready && <span className="text-[9px] text-gray-300">规划</span>}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="mt-auto flex items-center gap-2 border-t p-4 text-[11px] text-gray-500">
|
|
||||||
<span className={`h-2 w-2 rounded-full ${online ? "bg-emerald-500" : "bg-rose-500"}`} />
|
|
||||||
Gateway {online ? "在线" : "离线"}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main className="flex-1 overflow-auto p-6">
|
|
||||||
<h1 className="mb-4 text-lg font-semibold text-gray-800">
|
|
||||||
{NAV.find((n) => n.key === tab)?.label}
|
|
||||||
</h1>
|
|
||||||
{tab === "models" && <ModelsPage />}
|
|
||||||
{tab === "datasources" && (
|
|
||||||
<Soon
|
|
||||||
title="数据源(向量库 / 图库 / 全文)"
|
|
||||||
desc="配置 Milvus(:19530) / Neo4j(:7687) / Bleve 连接 + 测试连接 + 状态。后端 search.Hybrid 接真后接通(RAG 核心链)。"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab === "tenants" && (
|
|
||||||
<Soon title="租户 / 工作区" desc="多租户隔离、配额、用户与计费。垂直行业平台级复制的基座。" />
|
|
||||||
)}
|
|
||||||
{tab === "guardrails" && (
|
|
||||||
<Soon title="护栏" desc="输入/输出 Guardrail 规则(脱敏 / 免责 / 强制引用)。受监管垂直必备。" />
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 规划中页面占位(路由目标,后续替换为真实页面即可)。
|
||||||
|
export function Soon({ title, desc }: { title: string; desc: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-dashed bg-gray-50 p-6">
|
||||||
|
<div className="mb-1 text-sm font-semibold text-gray-600">{title}</div>
|
||||||
|
<p className="text-xs leading-relaxed text-gray-400">{desc}</p>
|
||||||
|
<span className="mt-3 inline-block rounded bg-gray-200 px-2 py-0.5 text-[10px] text-gray-500">规划中</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
testModel,
|
testModel,
|
||||||
type Model,
|
type Model,
|
||||||
type ModelInput,
|
type ModelInput,
|
||||||
} from "./api";
|
} from "../api";
|
||||||
|
|
||||||
const EMPTY: ModelInput = {
|
const EMPTY: ModelInput = {
|
||||||
provider: "openai-compatible",
|
provider: "openai-compatible",
|
||||||
@@ -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: <ModelsPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/datasources",
|
||||||
|
label: "数据源",
|
||||||
|
group: "配置",
|
||||||
|
element: (
|
||||||
|
<Soon
|
||||||
|
title="数据源(向量库 / 图库 / 全文)"
|
||||||
|
desc="配置 Milvus(:19530) / Neo4j(:7687) / Bleve 连接 + 测试连接 + 状态。复用模型控制面同套路(配置→NATS 下发→mcp-go 热更新)。RAG 核心链。"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/tenants",
|
||||||
|
label: "租户",
|
||||||
|
group: "平台",
|
||||||
|
element: <Soon title="租户 / 工作区" desc="多租户隔离、配额、用户与计费。垂直行业平台级复制的基座。" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/guardrails",
|
||||||
|
label: "护栏",
|
||||||
|
group: "平台",
|
||||||
|
element: <Soon title="护栏" desc="输入/输出 Guardrail 规则(脱敏 / 免责 / 强制引用)。受监管垂直必备。" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="flex h-screen w-screen text-gray-900">
|
||||||
|
<aside className="flex w-56 shrink-0 flex-col border-r bg-gray-50">
|
||||||
|
<div className="border-b p-4">
|
||||||
|
<div className="text-sm font-bold text-gray-800">sundynix-agentix</div>
|
||||||
|
<div className="text-[11px] text-gray-400">运维控制台</div>
|
||||||
|
</div>
|
||||||
|
<nav className="flex flex-col gap-1 p-2">
|
||||||
|
{navGroups().map((g) => (
|
||||||
|
<div key={g.group}>
|
||||||
|
<div className="mt-2 px-3 text-[9px] font-semibold tracking-wider text-gray-300">{g.group}</div>
|
||||||
|
{g.items.map((r) => (
|
||||||
|
<NavLink
|
||||||
|
key={r.path}
|
||||||
|
to={r.path}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex items-center justify-between rounded px-3 py-2 text-sm ${
|
||||||
|
isActive ? "bg-violet-50 font-medium text-violet-700" : "text-gray-600 hover:bg-gray-100"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{r.label}
|
||||||
|
{!r.ready && <span className="text-[9px] text-gray-300">规划</span>}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<div className="mt-auto flex items-center gap-2 border-t p-4 text-[11px] text-gray-500">
|
||||||
|
<span className={`h-2 w-2 rounded-full ${online ? "bg-emerald-500" : "bg-rose-500"}`} />
|
||||||
|
Gateway {online ? "在线" : "离线"}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="flex-1 overflow-auto p-6">
|
||||||
|
<h1 className="mb-4 text-lg font-semibold text-gray-800">{current?.label ?? ""}</h1>
|
||||||
|
<Suspense fallback={<div className="text-sm text-gray-400">加载中…</div>}>
|
||||||
|
<Routes>
|
||||||
|
{routes.map((r) => (
|
||||||
|
<Route key={r.path} path={r.path} element={r.element} />
|
||||||
|
))}
|
||||||
|
<Route path="*" element={<Navigate to={defaultPath} replace />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user