diff --git a/client/admin/components/ErrorPage.tsx b/client/admin/components/ErrorPage.tsx
new file mode 100644
index 0000000..fe2ff51
--- /dev/null
+++ b/client/admin/components/ErrorPage.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { useRouteError } from 'react-router';
+import { Alert, Button } from 'antd';
+import { useTheme } from '../hooks_sys.tsx';
+
+export const ErrorPage = () => {
+ const { isDark } = useTheme();
+ const error = useRouteError() as any;
+ const errorMessage = error?.statusText || error?.message || '未知错误';
+
+ return (
+
+
+
发生错误
+
+ {error.stack}
+
+ ) : null
+ }
+ className="mb-4"
+ />
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/client/admin/layouts/MainLayout.tsx b/client/admin/layouts/MainLayout.tsx
new file mode 100644
index 0000000..4a831c1
--- /dev/null
+++ b/client/admin/layouts/MainLayout.tsx
@@ -0,0 +1,222 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ Outlet,
+ useLocation,
+} from 'react-router';
+import {
+ Layout, Button, Space, Badge, Avatar, Dropdown, Typography, Input, Menu,
+} from 'antd';
+import {
+ MenuFoldOutlined,
+ MenuUnfoldOutlined,
+ BellOutlined,
+ VerticalAlignTopOutlined,
+ UserOutlined
+} from '@ant-design/icons';
+import { useAuth, useTheme } from '../hooks_sys.tsx';
+import { useMenu, useMenuSearch, type MenuItem } from '../menu.tsx';
+
+const { Header, Sider, Content } = Layout;
+
+/**
+ * 主布局组件
+ * 包含侧边栏、顶部导航和内容区域
+ */
+export const MainLayout = () => {
+ const { user } = useAuth();
+ const { isDark } = useTheme();
+ const [showBackTop, setShowBackTop] = useState(false);
+ const location = useLocation();
+
+ // 使用菜单hook
+ const {
+ menuItems,
+ userMenuItems,
+ openKeys,
+ collapsed,
+ setCollapsed,
+ handleMenuClick: handleRawMenuClick,
+ onOpenChange
+ } = useMenu();
+
+ // 处理菜单点击
+ const handleMenuClick = (key: string) => {
+ const item = findMenuItem(menuItems, key);
+ if (item && 'label' in item) {
+ handleRawMenuClick(item);
+ }
+ };
+
+ // 查找菜单项
+ const findMenuItem = (items: MenuItem[], key: string): MenuItem | null => {
+ for (const item of items) {
+ if (!item) continue;
+ if (item.key === key) return item;
+ if (item.children) {
+ const found = findMenuItem(item.children, key);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+
+ // 使用菜单搜索hook
+ const {
+ searchText,
+ setSearchText,
+ filteredMenuItems
+ } = useMenuSearch(menuItems);
+
+ // 获取当前选中的菜单项
+ const selectedKey = useMemo(() => {
+ const findSelectedKey = (items: MenuItem[]): string | null => {
+ for (const item of items) {
+ if (!item) continue;
+ if (item.path === location.pathname) return item.key || null;
+ if (item.children) {
+ const childKey = findSelectedKey(item.children);
+ if (childKey) return childKey;
+ }
+ }
+ return null;
+ };
+
+ return findSelectedKey(menuItems) || '';
+ }, [location.pathname, menuItems]);
+
+ // 检测滚动位置,控制回到顶部按钮显示
+ useEffect(() => {
+ const handleScroll = () => {
+ setShowBackTop(window.pageYOffset > 300);
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
+ // 回到顶部
+ const scrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ };
+
+
+ // 应用名称 - 从CONFIG中获取或使用默认值
+ const appName = window.CONFIG?.APP_NAME || '应用Starter';
+
+ return (
+
+
+
+
+ {collapsed ? '应用' : appName}
+
+
+ {/* 菜单搜索框 */}
+ {!collapsed && (
+
+ setSearchText(e.target.value)}
+ />
+
+ )}
+
+
+ {/* 菜单列表 */}
+
+
+
+
+ : }
+ onClick={() => setCollapsed(!collapsed)}
+ className="w-16 h-16"
+ />
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ {user?.nickname || user?.username}
+
+
+
+
+
+
+
+
+
+
+
+ {/* 回到顶部按钮 */}
+ {showBackTop && (
+ }
+ size="large"
+ onClick={scrollToTop}
+ style={{
+ position: 'fixed',
+ right: 30,
+ bottom: 30,
+ zIndex: 1000,
+ boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
+ }}
+ />
+ )}
+
+
+
+ );
+};
diff --git a/client/admin/menu.tsx b/client/admin/menu.tsx
new file mode 100644
index 0000000..bbcecb2
--- /dev/null
+++ b/client/admin/menu.tsx
@@ -0,0 +1,199 @@
+import React from 'react';
+import { useNavigate } from 'react-router';
+import type { MenuProps } from 'antd';
+import {
+ UserOutlined,
+ DashboardOutlined,
+ TeamOutlined,
+ SettingOutlined,
+ FileOutlined,
+ MessageOutlined,
+ InfoCircleOutlined,
+ BarChartOutlined,
+ EnvironmentOutlined,
+ MoonOutlined,
+ SunOutlined
+} from '@ant-design/icons';
+import { useTheme } from './hooks_sys.tsx';
+
+export interface MenuItem {
+ key: string;
+ label: string;
+ icon?: React.ReactNode;
+ children?: MenuItem[];
+ path?: string;
+ permission?: string;
+}
+
+/**
+ * 菜单搜索 Hook
+ * 封装菜单搜索相关逻辑
+ */
+export const useMenuSearch = (menuItems: MenuItem[]) => {
+ const [searchText, setSearchText] = React.useState('');
+
+ // 过滤菜单项
+ const filteredMenuItems = React.useMemo(() => {
+ if (!searchText) return menuItems;
+
+ const filterItems = (items: MenuItem[]): MenuItem[] => {
+ return items
+ .map(item => {
+ // 克隆对象避免修改原数据
+ const newItem = { ...item };
+ if (newItem.children) {
+ newItem.children = filterItems(newItem.children);
+ }
+ return newItem;
+ })
+ .filter(item => {
+ // 保留匹配项或其子项匹配的项
+ const match = item.label.toLowerCase().includes(searchText.toLowerCase());
+ if (match) return true;
+ if (item.children?.length) return true;
+ return false;
+ });
+ };
+
+ return filterItems(menuItems);
+ }, [menuItems, searchText]);
+
+ // 清除搜索
+ const clearSearch = () => {
+ setSearchText('');
+ };
+
+ return {
+ searchText,
+ setSearchText,
+ filteredMenuItems,
+ clearSearch
+ };
+};
+
+export const useMenu = () => {
+ const { isDark, toggleTheme } = useTheme();
+ const navigate = useNavigate();
+ const [collapsed, setCollapsed] = React.useState(false);
+ const [openKeys, setOpenKeys] = React.useState([]);
+
+ // 基础菜单项配置
+ const menuItems: MenuItem[] = [
+ {
+ key: 'dashboard',
+ label: '控制台',
+ icon: ,
+ path: '/admin/dashboard'
+ },
+ {
+ key: 'users',
+ label: '用户管理',
+ icon: ,
+ path: '/admin/users',
+ permission: 'user:manage'
+ },
+ {
+ key: 'settings',
+ label: '系统设置',
+ icon: ,
+ children: [
+ {
+ key: 'theme-settings',
+ label: '主题设置',
+ path: '/admin/theme-settings',
+ permission: 'system:settings'
+ },
+ {
+ key: 'system-settings',
+ label: '系统配置',
+ path: '/admin/settings',
+ permission: 'system:settings'
+ }
+ ]
+ },
+ {
+ key: 'content',
+ label: '内容管理',
+ icon: ,
+ children: [
+ {
+ key: 'know-info',
+ label: '知识库',
+ path: '/admin/know-info',
+ permission: 'content:manage'
+ },
+ {
+ key: 'file-library',
+ label: '文件库',
+ path: '/admin/file-library',
+ permission: 'content:manage'
+ }
+ ]
+ },
+ {
+ key: 'messages',
+ label: '消息中心',
+ icon: ,
+ path: '/admin/messages',
+ permission: 'message:view'
+ },
+ {
+ key: 'charts',
+ label: '数据图表',
+ icon: ,
+ path: '/admin/chart-dashboard',
+ permission: 'chart:view'
+ },
+ {
+ key: 'maps',
+ label: '地图',
+ icon: ,
+ path: '/admin/map-dashboard',
+ permission: 'map:view'
+ }
+ ];
+
+ // 用户菜单项
+ const userMenuItems: MenuProps['items'] = [
+ {
+ key: 'profile',
+ label: '个人资料',
+ icon:
+ },
+ {
+ key: 'theme',
+ label: isDark ? '切换到亮色模式' : '切换到暗色模式',
+ icon: isDark ? : ,
+ onClick: () => toggleTheme()
+ },
+ {
+ key: 'logout',
+ label: '退出登录',
+ icon: ,
+ danger: true
+ }
+ ];
+
+ // 处理菜单点击
+ const handleMenuClick = (item: MenuItem) => {
+ if (item.path) {
+ navigate(item.path);
+ }
+ };
+
+ // 处理菜单展开变化
+ const onOpenChange = (keys: string[]) => {
+ const latestOpenKey = keys.find(key => openKeys.indexOf(key) === -1);
+ setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
+ };
+
+ return {
+ menuItems,
+ userMenuItems,
+ openKeys,
+ collapsed,
+ setCollapsed,
+ handleMenuClick,
+ onOpenChange
+ };
+};
\ No newline at end of file
diff --git a/client/admin/routes.tsx b/client/admin/routes.tsx
new file mode 100644
index 0000000..d603e58
--- /dev/null
+++ b/client/admin/routes.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { createBrowserRouter, Navigate } from 'react-router';
+import { ProtectedRoute } from './components_protected_route.tsx';
+import { MainLayout } from './layouts/MainLayout.tsx';
+import { ErrorPage } from './components/ErrorPage.tsx';
+import { DashboardPage } from './pages_dashboard.tsx';
+import { UsersPage } from './pages_users.tsx';
+import { FileLibraryPage } from './pages_file_library.tsx';
+import { KnowInfoPage } from './pages_know_info.tsx';
+import { MessagesPage } from './pages_messages.tsx';
+import { SettingsPage } from './pages_settings.tsx';
+import { ThemeSettingsPage } from './pages_theme_settings.tsx';
+import { ChartDashboardPage } from './pages_chart.tsx';
+import { LoginMapPage } from './pages_map.tsx';
+import { LoginPage } from './pages_login_reg.tsx';
+
+export const router = createBrowserRouter([
+ {
+ path: '/',
+ element:
+ },
+ {
+ path: '/admin/login',
+ element:
+ },
+ {
+ path: '/admin',
+ element: (
+
+
+
+ ),
+ children: [
+ {
+ index: true,
+ element:
+ },
+ {
+ path: 'dashboard',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'users',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'settings',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'theme-settings',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'chart-dashboard',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'map-dashboard',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'know-info',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'file-library',
+ element: ,
+ errorElement:
+ },
+ {
+ path: 'messages',
+ element: ,
+ errorElement:
+ },
+ ],
+ },
+]);
\ No newline at end of file
diff --git a/client/admin/web_app.tsx b/client/admin/web_app.tsx
index f27c546..c16feb0 100644
--- a/client/admin/web_app.tsx
+++ b/client/admin/web_app.tsx
@@ -1,76 +1,16 @@
-import React, { useState, useEffect} from 'react';
+import React from 'react';
import { createRoot } from 'react-dom/client';
-import {
- createBrowserRouter,
- RouterProvider,
- Outlet,
- useNavigate,
- useLocation,
- Navigate,
- useParams,
- useRouteError
-} from 'react-router';
-import {
- Layout, Menu, Button, Table, Space,
- Form, Input, Select, message, Modal,
- Card, Spin, Row, Col, Breadcrumb, Avatar,
- Dropdown, ConfigProvider, theme, Typography,
- Switch, Badge, Image, Upload, Divider, Descriptions,
- Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
-} from 'antd';
-import {
- MenuFoldOutlined,
- MenuUnfoldOutlined,
- UserOutlined,
- DashboardOutlined,
- TeamOutlined,
- SettingOutlined,
- LogoutOutlined,
- BellOutlined,
- BookOutlined,
- FileOutlined,
- PieChartOutlined,
- VerticalAlignTopOutlined,
- CloseOutlined,
- SearchOutlined
-} from '@ant-design/icons';
-import {
- QueryClient,
- QueryClientProvider,
-} from '@tanstack/react-query';
+import { RouterProvider } from 'react-router';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import dayjs from 'dayjs';
import weekday from 'dayjs/plugin/weekday';
import localeData from 'dayjs/plugin/localeData';
import 'dayjs/locale/zh-cn';
-import type {
- GlobalConfig
-} from '../share/types.ts';
-
-import {
- AuthProvider,
- useAuth,
- ThemeProvider,
- useTheme,
-} from './hooks_sys.tsx';
-
-import {
- DashboardPage
-} from './pages_dashboard.tsx';
-import {
- UsersPage
-} from './pages_users.tsx';
-import {
- FileLibraryPage
-} from './pages_file_library.tsx';
-import { KnowInfoPage } from './pages_know_info.tsx';
-import { MessagesPage } from './pages_messages.tsx';
-import {SettingsPage } from './pages_settings.tsx';
-import {ThemeSettingsPage} from './pages_theme_settings.tsx'
-import { ChartDashboardPage } from './pages_chart.tsx';
-import { LoginMapPage } from './pages_map.tsx';
-import { LoginPage } from './pages_login_reg.tsx';
-import { ProtectedRoute } from './components_protected_route.tsx';
+import { AuthProvider } from './hooks_sys.tsx';
+import { ThemeProvider } from './hooks_sys.tsx';
+import { router } from './routes.tsx';
+import type { GlobalConfig } from '../share/types.ts';
// 配置 dayjs 插件
dayjs.extend(weekday);
@@ -79,12 +19,9 @@ dayjs.extend(localeData);
// 设置 dayjs 语言
dayjs.locale('zh-cn');
-const { Header, Sider, Content } = Layout;
-
// 创建QueryClient实例
const queryClient = new QueryClient();
-
// 声明全局配置对象类型
declare global {
interface Window {
@@ -92,460 +29,8 @@ declare global {
}
}
-
-// 主布局组件
-const MainLayout = () => {
- const [collapsed, setCollapsed] = useState(false);
- const { user, logout } = useAuth();
- const { isDark, toggleTheme } = useTheme();
- const navigate = useNavigate();
- const location = useLocation();
- const [openKeys, setOpenKeys] = useState([]);
- const [showBackTop, setShowBackTop] = useState(false);
- const [searchText, setSearchText] = useState('');
- const [filteredMenuItems, setFilteredMenuItems] = useState([]);
-
- // 检测滚动位置,控制回到顶部按钮显示
- useEffect(() => {
- const handleScroll = () => {
- setShowBackTop(window.pageYOffset > 300);
- };
-
- window.addEventListener('scroll', handleScroll);
- return () => window.removeEventListener('scroll', handleScroll);
- }, []);
-
- // 回到顶部
- const scrollToTop = () => {
- window.scrollTo({
- top: 0,
- behavior: 'smooth'
- });
- };
-
- // 菜单项配置
- const menuItems = [
- {
- key: '/dashboard',
- icon: ,
- label: '仪表盘',
- },
- {
- key: '/analysis',
- icon: ,
- label: '数据分析',
- children: [
- {
- key: '/chart-dashboard',
- label: '图表统计',
- },
- {
- key: '/map-dashboard',
- label: '地图概览',
- },
- ],
- },
- {
- key: '/files',
- icon: ,
- label: '文件管理',
- children: [
- {
- key: '/file-library',
- label: '文件库',
- },
- ],
- },
- {
- key: '/know-info',
- icon: ,
- label: '知识库',
- },
- {
- key: '/users',
- icon: ,
- label: '用户管理',
- },
- {
- key: '/messages',
- icon: ,
- label: '消息管理',
- },
- {
- key: '/settings_group',
- icon: ,
- label: '系统设置',
- children: [
- {
- key: '/theme-settings',
- label: '主题设置',
- },
- {
- key: '/settings',
- label: '基本设置',
- },
- ],
- },
- ];
-
- // 初始化filteredMenuItems
- useEffect(() => {
- setFilteredMenuItems(menuItems);
- }, []);
-
- // 搜索菜单项
- const handleSearch = (value: string) => {
- setSearchText(value);
-
- if (!value.trim()) {
- setFilteredMenuItems(menuItems);
- return;
- }
-
- // 搜索功能 - 过滤菜单项
- const filtered = menuItems.reduce((acc: any[], item) => {
- // 检查主菜单项是否匹配
- const mainItemMatch = item.label.toString().toLowerCase().includes(value.toLowerCase());
-
- // 如果有子菜单,检查子菜单中是否有匹配项
- if (item.children) {
- const matchedChildren = item.children.filter(child =>
- child.label.toString().toLowerCase().includes(value.toLowerCase())
- );
-
- if (matchedChildren.length > 0) {
- // 如果有匹配的子菜单,创建包含匹配子菜单的副本
- acc.push({
- ...item,
- children: matchedChildren
- });
- return acc;
- }
- }
-
- // 如果主菜单项匹配,添加整个项
- if (mainItemMatch) {
- acc.push(item);
- }
-
- return acc;
- }, []);
-
- setFilteredMenuItems(filtered);
- };
-
- // 清除搜索
- const clearSearch = () => {
- setSearchText('');
- setFilteredMenuItems(menuItems);
- };
-
- const handleMenuClick = ({ key }: { key: string }) => {
- navigate(`/admin${key}`);
- // 如果有搜索文本,清除搜索
- if (searchText) {
- clearSearch();
- }
- };
-
- // 处理登出
- const handleLogout = async () => {
- await logout();
- navigate('/admin/login');
- };
-
- // 处理菜单展开/收起
- const onOpenChange = (keys: string[]) => {
- // 当侧边栏折叠时不保存openKeys状态
- if (!collapsed) {
- setOpenKeys(keys);
- }
- };
-
- // 当侧边栏折叠状态改变时,控制菜单打开状态
- useEffect(() => {
- if (collapsed) {
- setOpenKeys([]);
- } else {
- // 找到当前路径所属的父菜单
- const currentPath = location.pathname.replace('/admin', '');
- const parentKeys = menuItems
- .filter(item => item.children && item.children.some(child => child.key === currentPath))
- .map(item => item.key);
-
- // 仅展开当前所在的菜单组
- if (parentKeys.length > 0) {
- setOpenKeys(parentKeys);
- } else {
- // 初始时可以根据需要设置要打开的菜单组
- setOpenKeys([]);
- }
- }
- }, [collapsed, location.pathname]);
-
- // 用户下拉菜单项
- const userMenuItems = [
- {
- key: 'profile',
- label: '个人信息',
- icon:
- },
- {
- key: 'theme',
- label: isDark ? '切换到亮色模式' : '切换到暗色模式',
- icon: ,
- onClick: toggleTheme
- },
- {
- key: 'logout',
- label: '退出登录',
- icon: ,
- onClick: handleLogout
- }
- ];
-
- // 应用名称 - 从CONFIG中获取或使用默认值
- const appName = window.CONFIG?.APP_NAME || '应用Starter';
-
- return (
-
-
-
-
- {collapsed ? '应用' : appName}
-
-
-
- {/* 搜索框 - 仅在展开状态下显示 */}
- {!collapsed && (
-
- handleSearch(e.target.value)}
- suffix={
- searchText ?
- }
- onClick={clearSearch}
- /> :
-
- }
- />
-
- )}
-
-
-
-
-
-
- : }
- onClick={() => setCollapsed(!collapsed)}
- className="w-16 h-16"
- />
-
-
-
- }
- />
-
-
-
-
- }
- />
-
- {user?.nickname || user?.username}
-
-
-
-
-
-
-
-
-
-
-
- {/* 回到顶部按钮 */}
- {showBackTop && (
- }
- size="large"
- onClick={scrollToTop}
- style={{
- position: 'fixed',
- right: 30,
- bottom: 30,
- zIndex: 1000,
- boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
- }}
- />
- )}
-
-
-
- );
-};
-
-
-
-// 错误页面组件
-const ErrorPage = () => {
- const { isDark } = useTheme();
- const error = useRouteError() as any;
- const errorMessage = error?.statusText || error?.message || '未知错误';
-
-
- return (
-
-
-
发生错误
-
- {error.stack}
-
- ) : null
- }
- className="mb-4"
- />
-
-
-
-
-
-
- );
-};
-
// 应用入口组件
const App = () => {
- // 路由配置
- const router = createBrowserRouter([
- {
- path: '/',
- element:
- },
- {
- path: '/admin/login',
- element:
- },
- {
- path: '/admin',
- element: (
-
-
-
- ),
- children: [
- {
- index: true,
- element:
- },
- {
- path: 'dashboard',
- element: ,
- errorElement:
- },
- {
- path: 'users',
- element: ,
- errorElement:
- },
- {
- path: 'settings',
- element: ,
- errorElement:
- },
- {
- path: 'theme-settings',
- element: ,
- errorElement:
- },
- {
- path: 'chart-dashboard',
- element: ,
- errorElement:
- },
- {
- path: 'map-dashboard',
- element: ,
- errorElement:
- },
- {
- path: 'know-info',
- element: ,
- errorElement:
- },
- {
- path: 'file-library',
- element: ,
- errorElement:
- },
- {
- path: 'messages',
- element: ,
- errorElement:
- },
- ],
- },
- ]);
return
};
@@ -560,4 +45,3 @@ root.render(
);
-
diff --git a/client/share/types.ts b/client/share/types.ts
index 014432e..9223c99 100644
--- a/client/share/types.ts
+++ b/client/share/types.ts
@@ -33,6 +33,7 @@ export interface User {
role: string;
avatar?: string;
password?: string;
+ permissions?: string[];
}
export interface MenuItem {