feat: 重构前端为企业 Admin 后台布局,引入 React Router 路由

- 引入 React Router v7 (Declarative mode) 实现 SPA 路由
- 重构 Layout 为 Header + 侧边栏 + 内容区的企业 Admin 布局
- 新增侧边栏菜单组件,支持折叠/展开,状态持久化到 localStorage
- 新增示例页面:仪表盘、用户管理、系统设置、404
- 菜单配置与路由统一为单一数据源 (menu.tsx)
- Vite code splitting 新增 vendor-router 组
- 更新 DEVELOPMENT.md 和 README.md 文档
This commit is contained in:
2026-05-20 19:06:14 +08:00
parent 5aed73523e
commit 4caf502908
32 changed files with 981 additions and 140 deletions

View File

@@ -1,13 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { Layout, Menu, RadioGroup, Space } from "tdesign-react";
import type { HealthResponse } from "../shared/api";
import { useLocation } from "react-router";
import { ChevronLeftIcon, ChevronRightIcon } from "tdesign-icons-react";
import { Button, Layout, RadioGroup } from "tdesign-react";
import { APP } from "../shared/app";
import { Sidebar } from "./components/Sidebar";
import { useSidebarCollapsed } from "./hooks/use-sidebar-collapsed";
import { type ThemePreference, useThemePreference } from "./hooks/use-theme-preference";
import { MENU_ITEMS } from "./menu";
import { AppRoutes } from "./routes";
const { Aside, Content, Header } = Layout;
const { Content, Header } = Layout;
const THEME_OPTIONS = [
{ label: "系统", value: "system" },
{ label: "明亮", value: "light" },
@@ -16,12 +20,8 @@ const THEME_OPTIONS = [
export function App() {
const { preference: themePreference, setPreference: setThemePreference } = useThemePreference();
const { data: health } = useQuery({
queryFn: fetchHealth,
queryKey: ["health"],
refetchInterval: 30000,
staleTime: 5000,
});
const { collapsed, toggleCollapsed } = useSidebarCollapsed();
const location = useLocation();
useEffect(() => {
document.title = APP.title;
@@ -32,43 +32,40 @@ export function App() {
setThemePreference(value);
};
const currentPath = location.pathname;
const currentItem = MENU_ITEMS.find((item) => item.path === currentPath);
const pageTitle = currentItem?.label ?? APP.title;
return (
<Layout className="dashboard">
<Header>
<Menu.HeadMenu
logo={
<span className="dashboard-brand">
<span className="dashboard-logo">{APP.title}</span>
</span>
}
operations={
<div className="dashboard-header-controls">
<RadioGroup
onChange={handleThemeChange}
options={THEME_OPTIONS.map((option) => ({ label: option.label, value: option.value }))}
theme="button"
value={themePreference}
variant="default-filled"
/>
</div>
}
/>
</Header>
<Content>
<div className="dashboard-content">
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<h2>使 {APP.title}</h2>
<p> /health API </p>
{health && <pre className="health-response">{JSON.stringify(health, null, 2)}</pre>}
</Space>
<Layout className="app-layout">
<Header className="app-header">
<div className="app-header-left">
<Button className="app-sidebar-toggle" onClick={toggleCollapsed} shape="square" variant="text">
{collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</Button>
<span className="app-brand">{APP.title}</span>
<span className="app-page-title">{pageTitle}</span>
</div>
</Content>
<div className="app-header-right">
<RadioGroup
onChange={handleThemeChange}
options={THEME_OPTIONS.map((option) => ({ label: option.label, value: option.value }))}
theme="button"
value={themePreference}
variant="default-filled"
/>
</div>
</Header>
<Layout>
<Aside className="app-sidebar" width={collapsed ? "80px" : "232px"}>
<Sidebar collapsed={collapsed} />
</Aside>
<Layout>
<Content className="app-content">
<AppRoutes />
</Content>
</Layout>
</Layout>
</Layout>
);
}
async function fetchHealth(): Promise<HealthResponse> {
const response = await fetch("/health");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<HealthResponse>;
}