| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- import React, { useState, useEffect, createContext, useContext } 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 zhCN from "antd/locale/zh_CN";
- import {
- MenuFoldOutlined,
- MenuUnfoldOutlined,
- UserOutlined,
- DashboardOutlined,
- TeamOutlined,
- SettingOutlined,
- LogoutOutlined,
- BellOutlined,
- BookOutlined,
- FileOutlined,
- PieChartOutlined,
- UploadOutlined,
- GlobalOutlined,
- VerticalAlignTopOutlined,
- CloseOutlined,
- SearchOutlined
- } from '@ant-design/icons';
- import {
- QueryClient,
- QueryClientProvider,
- useQuery,
- useMutation,
- useQueryClient
- } from '@tanstack/react-query';
- import axios from 'axios';
- import dayjs from 'dayjs';
- import weekday from 'dayjs/plugin/weekday';
- import localeData from 'dayjs/plugin/localeData';
- import { uploadMinIOWithPolicy } from '@d8d-appcontainer/api';
- import type { MinioUploadPolicy } from '@d8d-appcontainer/types';
- import { Line, Pie, Column } from "@ant-design/plots";
- import 'dayjs/locale/zh-cn';
- import type {
- GlobalConfig
- } from '../share/types.ts';
- import {
- EnableStatus, DeleteStatus, ThemeMode, FontSize, CompactMode
- } from '../share/types.ts';
- import { getEnumOptions } from './utils.ts';
- import {
- AuthProvider,
- useAuth,
- ThemeProvider,
- useTheme,
- } from './hooks_sys.tsx';
- import {
- DashboardPage,
- UsersPage,
- KnowInfoPage,
- FileLibraryPage
- } from './pages_sys.tsx';
- import { MessagesPage } from './pages_messages.tsx';
- import {
- SettingsPage,
- ThemeSettingsPage,
- } from './pages_settings.tsx';
- import {
- ChartDashboardPage,
- } from './pages_chart.tsx';
- import {
- LoginMapPage
- } from './pages_map.tsx';
- import {
- LoginPage,
- } from './pages_login_reg.tsx';
- // 配置 dayjs 插件
- dayjs.extend(weekday);
- dayjs.extend(localeData);
- // 设置 dayjs 语言
- dayjs.locale('zh-cn');
- const { Header, Sider, Content } = Layout;
- // 创建QueryClient实例
- const queryClient = new QueryClient();
- // 声明全局配置对象类型
- declare global {
- interface Window {
- CONFIG?: GlobalConfig;
- }
- }
- // 主布局组件
- 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<string[]>([]);
- const [showBackTop, setShowBackTop] = useState(false);
- const [searchText, setSearchText] = useState('');
- const [filteredMenuItems, setFilteredMenuItems] = useState<any[]>([]);
-
- // 检测滚动位置,控制回到顶部按钮显示
- 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: <DashboardOutlined />,
- label: '仪表盘',
- },
- {
- key: '/analysis',
- icon: <PieChartOutlined />,
- label: '数据分析',
- children: [
- {
- key: '/chart-dashboard',
- label: '图表统计',
- },
- {
- key: '/map-dashboard',
- label: '地图概览',
- },
- ],
- },
- {
- key: '/files',
- icon: <FileOutlined />,
- label: '文件管理',
- children: [
- {
- key: '/file-library',
- label: '文件库',
- },
- ],
- },
- {
- key: '/know-info',
- icon: <BookOutlined />,
- label: '知识库',
- },
- {
- key: '/users',
- icon: <TeamOutlined />,
- label: '用户管理',
- },
- {
- key: '/messages',
- icon: <BellOutlined />,
- label: '消息管理',
- },
- {
- key: '/settings',
- icon: <SettingOutlined />,
- 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: <UserOutlined />
- },
- {
- key: 'theme',
- label: isDark ? '切换到亮色模式' : '切换到暗色模式',
- icon: <SettingOutlined />,
- onClick: toggleTheme
- },
- {
- key: 'logout',
- label: '退出登录',
- icon: <LogoutOutlined />,
- onClick: handleLogout
- }
- ];
-
- // 应用名称 - 从CONFIG中获取或使用默认值
- const appName = window.CONFIG?.APP_NAME || '应用Starter';
-
- return (
- <Layout style={{ minHeight: '100vh' }}>
- <Sider
- trigger={null}
- collapsible
- collapsed={collapsed}
- width={240}
- className="custom-sider"
- style={{
- overflow: 'auto',
- height: '100vh',
- position: 'fixed',
- left: 0,
- top: 0,
- bottom: 0,
- zIndex: 100,
- }}
- >
- <div className="p-4">
- <Typography.Title level={2} className="text-xl font-bold truncate">
- {collapsed ? '应用' : appName}
- </Typography.Title>
- </div>
-
- {/* 搜索框 - 仅在展开状态下显示 */}
- {!collapsed && (
- <div style={{ padding: '0 16px 16px' }}>
- <Input
- placeholder="搜索菜单..."
- value={searchText}
- onChange={(e) => handleSearch(e.target.value)}
- suffix={
- searchText ?
- <Button
- type="text"
- size="small"
- icon={<CloseOutlined />}
- onClick={clearSearch}
- /> :
- <SearchOutlined />
- }
- />
- </div>
- )}
-
- <Menu
- theme={isDark ? "light" : "light"}
- mode="inline"
- selectedKeys={[location.pathname.replace('/admin', '')]}
- openKeys={openKeys}
- onOpenChange={onOpenChange}
- items={filteredMenuItems}
- onClick={handleMenuClick}
- />
- </Sider>
-
- <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
- <Header className="p-0 flex justify-between items-center"
- style={{
- position: 'sticky',
- top: 0,
- zIndex: 99,
- boxShadow: '0 1px 4px rgba(0,21,41,0.08)',
- }}
- >
- <Button
- type="text"
- icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
- onClick={() => setCollapsed(!collapsed)}
- className="w-16 h-16"
- />
-
- <Space size="middle" className="mr-4">
- <Badge count={5} offset={[0, 5]}>
- <Button
- type="text"
- icon={<BellOutlined />}
- />
- </Badge>
-
- <Dropdown menu={{ items: userMenuItems }}>
- <Space className="cursor-pointer">
- <Avatar
- src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
- icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
- />
- <span>
- {user?.nickname || user?.username}
- </span>
- </Space>
- </Dropdown>
- </Space>
- </Header>
-
- <Content className="m-6" style={{ overflow: 'initial' }}>
- <div className="site-layout-content p-6 rounded-lg">
- <Outlet />
- </div>
-
- {/* 回到顶部按钮 */}
- {showBackTop && (
- <Button
- type="primary"
- shape="circle"
- icon={<VerticalAlignTopOutlined />}
- size="large"
- onClick={scrollToTop}
- style={{
- position: 'fixed',
- right: 30,
- bottom: 30,
- zIndex: 1000,
- boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
- }}
- />
- )}
- </Content>
- </Layout>
- </Layout>
- );
- };
- // 受保护的路由组件
- const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
- const { isAuthenticated, isLoading } = useAuth();
- const navigate = useNavigate();
-
- useEffect(() => {
- // 只有在加载完成且未认证时才重定向
- if (!isLoading && !isAuthenticated) {
- navigate('/admin/login', { replace: true });
- }
- }, [isAuthenticated, isLoading, navigate]);
-
- // 显示加载状态,直到认证检查完成
- if (isLoading) {
- return (
- <div className="flex justify-center items-center h-screen">
- <div className="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12"></div>
- </div>
- );
- }
-
- // 如果未认证且不再加载中,不显示任何内容(等待重定向)
- if (!isAuthenticated) {
- return null;
- }
-
- return children;
- };
- // 错误页面组件
- const ErrorPage = () => {
- const { isDark } = useTheme();
- const error = useRouteError() as any;
- const errorMessage = error?.statusText || error?.message || '未知错误';
-
-
- return (
- <div className="flex flex-col items-center justify-center min-h-screen p-4"
- style={{ color: isDark ? '#fff' : 'inherit' }}
- >
- <div className="max-w-3xl w-full">
- <h1 className="text-2xl font-bold mb-4">发生错误</h1>
- <Alert
- type="error"
- message={error?.message || '未知错误'}
- description={
- error?.stack ? (
- <pre className="text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded">
- {error.stack}
- </pre>
- ) : null
- }
- className="mb-4"
- />
- <div className="flex gap-4">
- <Button
- type="primary"
- onClick={() => window.location.reload()}
- >
- 重新加载
- </Button>
- <Button
- onClick={() => window.location.href = '/admin'}
- >
- 返回首页
- </Button>
- </div>
- </div>
- </div>
- );
- };
- // 应用入口组件
- const App = () => {
- // 路由配置
- const router = createBrowserRouter([
- {
- path: '/',
- element: <Navigate to="/admin" replace />
- },
- {
- path: '/admin/login',
- element: <LoginPage />
- },
- {
- path: '/admin',
- element: (
- <ProtectedRoute>
- <MainLayout />
- </ProtectedRoute>
- ),
- children: [
- {
- index: true,
- element: <Navigate to="/admin/dashboard" />
- },
- {
- path: 'dashboard',
- element: <DashboardPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'users',
- element: <UsersPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'settings',
- element: <SettingsPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'theme-settings',
- element: <ThemeSettingsPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'chart-dashboard',
- element: <ChartDashboardPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'map-dashboard',
- element: <LoginMapPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'know-info',
- element: <KnowInfoPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'file-library',
- element: <FileLibraryPage />,
- errorElement: <ErrorPage />
- },
- {
- path: 'messages',
- element: <MessagesPage />,
- errorElement: <ErrorPage />
- },
- ],
- },
- ]);
- return <RouterProvider router={router} />
- };
- // 渲染应用
- const root = createRoot(document.getElementById('root') as HTMLElement);
- root.render(
- <QueryClientProvider client={queryClient}>
- <ThemeProvider>
- <AuthProvider>
- <App />
- </AuthProvider>
- </ThemeProvider>
- </QueryClientProvider>
- );
|