web_app.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. import React, { useState, useEffect} from 'react';
  2. import { createRoot } from 'react-dom/client';
  3. import {
  4. createBrowserRouter,
  5. RouterProvider,
  6. Outlet,
  7. useNavigate,
  8. useLocation,
  9. Navigate,
  10. useParams,
  11. useRouteError
  12. } from 'react-router';
  13. import {
  14. Layout, Menu, Button, Table, Space,
  15. Form, Input, Select, message, Modal,
  16. Card, Spin, Row, Col, Breadcrumb, Avatar,
  17. Dropdown, ConfigProvider, theme, Typography,
  18. Switch, Badge, Image, Upload, Divider, Descriptions,
  19. Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
  20. } from 'antd';
  21. import {
  22. MenuFoldOutlined,
  23. MenuUnfoldOutlined,
  24. UserOutlined,
  25. DashboardOutlined,
  26. TeamOutlined,
  27. SettingOutlined,
  28. LogoutOutlined,
  29. BellOutlined,
  30. BookOutlined,
  31. FileOutlined,
  32. PieChartOutlined,
  33. VerticalAlignTopOutlined,
  34. CloseOutlined,
  35. SearchOutlined
  36. } from '@ant-design/icons';
  37. import {
  38. QueryClient,
  39. QueryClientProvider,
  40. } from '@tanstack/react-query';
  41. import dayjs from 'dayjs';
  42. import weekday from 'dayjs/plugin/weekday';
  43. import localeData from 'dayjs/plugin/localeData';
  44. import 'dayjs/locale/zh-cn';
  45. import type {
  46. GlobalConfig
  47. } from '../share/types.ts';
  48. import {
  49. AuthProvider,
  50. useAuth,
  51. ThemeProvider,
  52. useTheme,
  53. } from './hooks_sys.tsx';
  54. import {
  55. DashboardPage
  56. } from './pages_dashboard.tsx';
  57. import {
  58. UsersPage
  59. } from './pages_users.tsx';
  60. import {
  61. FileLibraryPage
  62. } from './pages_file_library.tsx';
  63. import { KnowInfoPage } from './pages_know_info.tsx';
  64. import { MessagesPage } from './pages_messages.tsx';
  65. import {SettingsPage } from './pages_settings.tsx';
  66. import {ThemeSettingsPage} from './pages_theme_settings.tsx'
  67. import { ChartDashboardPage } from './pages_chart.tsx';
  68. import { LoginMapPage } from './pages_map.tsx';
  69. import { LoginPage } from './pages_login_reg.tsx';
  70. import { ProtectedRoute } from './components_protected_route.tsx';
  71. // 配置 dayjs 插件
  72. dayjs.extend(weekday);
  73. dayjs.extend(localeData);
  74. // 设置 dayjs 语言
  75. dayjs.locale('zh-cn');
  76. const { Header, Sider, Content } = Layout;
  77. // 创建QueryClient实例
  78. const queryClient = new QueryClient();
  79. // 声明全局配置对象类型
  80. declare global {
  81. interface Window {
  82. CONFIG?: GlobalConfig;
  83. }
  84. }
  85. // 主布局组件
  86. const MainLayout = () => {
  87. const [collapsed, setCollapsed] = useState(false);
  88. const { user, logout } = useAuth();
  89. const { isDark, toggleTheme } = useTheme();
  90. const navigate = useNavigate();
  91. const location = useLocation();
  92. const [openKeys, setOpenKeys] = useState<string[]>([]);
  93. const [showBackTop, setShowBackTop] = useState(false);
  94. const [searchText, setSearchText] = useState('');
  95. const [filteredMenuItems, setFilteredMenuItems] = useState<any[]>([]);
  96. // 检测滚动位置,控制回到顶部按钮显示
  97. useEffect(() => {
  98. const handleScroll = () => {
  99. setShowBackTop(window.pageYOffset > 300);
  100. };
  101. window.addEventListener('scroll', handleScroll);
  102. return () => window.removeEventListener('scroll', handleScroll);
  103. }, []);
  104. // 回到顶部
  105. const scrollToTop = () => {
  106. window.scrollTo({
  107. top: 0,
  108. behavior: 'smooth'
  109. });
  110. };
  111. // 菜单项配置
  112. const menuItems = [
  113. {
  114. key: '/dashboard',
  115. icon: <DashboardOutlined />,
  116. label: '仪表盘',
  117. },
  118. {
  119. key: '/analysis',
  120. icon: <PieChartOutlined />,
  121. label: '数据分析',
  122. children: [
  123. {
  124. key: '/chart-dashboard',
  125. label: '图表统计',
  126. },
  127. {
  128. key: '/map-dashboard',
  129. label: '地图概览',
  130. },
  131. ],
  132. },
  133. {
  134. key: '/files',
  135. icon: <FileOutlined />,
  136. label: '文件管理',
  137. children: [
  138. {
  139. key: '/file-library',
  140. label: '文件库',
  141. },
  142. ],
  143. },
  144. {
  145. key: '/know-info',
  146. icon: <BookOutlined />,
  147. label: '知识库',
  148. },
  149. {
  150. key: '/users',
  151. icon: <TeamOutlined />,
  152. label: '用户管理',
  153. },
  154. {
  155. key: '/messages',
  156. icon: <BellOutlined />,
  157. label: '消息管理',
  158. },
  159. {
  160. key: '/settings_group',
  161. icon: <SettingOutlined />,
  162. label: '系统设置',
  163. children: [
  164. {
  165. key: '/theme-settings',
  166. label: '主题设置',
  167. },
  168. {
  169. key: '/settings',
  170. label: '基本设置',
  171. },
  172. ],
  173. },
  174. ];
  175. // 初始化filteredMenuItems
  176. useEffect(() => {
  177. setFilteredMenuItems(menuItems);
  178. }, []);
  179. // 搜索菜单项
  180. const handleSearch = (value: string) => {
  181. setSearchText(value);
  182. if (!value.trim()) {
  183. setFilteredMenuItems(menuItems);
  184. return;
  185. }
  186. // 搜索功能 - 过滤菜单项
  187. const filtered = menuItems.reduce((acc: any[], item) => {
  188. // 检查主菜单项是否匹配
  189. const mainItemMatch = item.label.toString().toLowerCase().includes(value.toLowerCase());
  190. // 如果有子菜单,检查子菜单中是否有匹配项
  191. if (item.children) {
  192. const matchedChildren = item.children.filter(child =>
  193. child.label.toString().toLowerCase().includes(value.toLowerCase())
  194. );
  195. if (matchedChildren.length > 0) {
  196. // 如果有匹配的子菜单,创建包含匹配子菜单的副本
  197. acc.push({
  198. ...item,
  199. children: matchedChildren
  200. });
  201. return acc;
  202. }
  203. }
  204. // 如果主菜单项匹配,添加整个项
  205. if (mainItemMatch) {
  206. acc.push(item);
  207. }
  208. return acc;
  209. }, []);
  210. setFilteredMenuItems(filtered);
  211. };
  212. // 清除搜索
  213. const clearSearch = () => {
  214. setSearchText('');
  215. setFilteredMenuItems(menuItems);
  216. };
  217. const handleMenuClick = ({ key }: { key: string }) => {
  218. navigate(`/admin${key}`);
  219. // 如果有搜索文本,清除搜索
  220. if (searchText) {
  221. clearSearch();
  222. }
  223. };
  224. // 处理登出
  225. const handleLogout = async () => {
  226. await logout();
  227. navigate('/admin/login');
  228. };
  229. // 处理菜单展开/收起
  230. const onOpenChange = (keys: string[]) => {
  231. // 当侧边栏折叠时不保存openKeys状态
  232. if (!collapsed) {
  233. setOpenKeys(keys);
  234. }
  235. };
  236. // 当侧边栏折叠状态改变时,控制菜单打开状态
  237. useEffect(() => {
  238. if (collapsed) {
  239. setOpenKeys([]);
  240. } else {
  241. // 找到当前路径所属的父菜单
  242. const currentPath = location.pathname.replace('/admin', '');
  243. const parentKeys = menuItems
  244. .filter(item => item.children && item.children.some(child => child.key === currentPath))
  245. .map(item => item.key);
  246. // 仅展开当前所在的菜单组
  247. if (parentKeys.length > 0) {
  248. setOpenKeys(parentKeys);
  249. } else {
  250. // 初始时可以根据需要设置要打开的菜单组
  251. setOpenKeys([]);
  252. }
  253. }
  254. }, [collapsed, location.pathname]);
  255. // 用户下拉菜单项
  256. const userMenuItems = [
  257. {
  258. key: 'profile',
  259. label: '个人信息',
  260. icon: <UserOutlined />
  261. },
  262. {
  263. key: 'theme',
  264. label: isDark ? '切换到亮色模式' : '切换到暗色模式',
  265. icon: <SettingOutlined />,
  266. onClick: toggleTheme
  267. },
  268. {
  269. key: 'logout',
  270. label: '退出登录',
  271. icon: <LogoutOutlined />,
  272. onClick: handleLogout
  273. }
  274. ];
  275. // 应用名称 - 从CONFIG中获取或使用默认值
  276. const appName = window.CONFIG?.APP_NAME || '应用Starter';
  277. return (
  278. <Layout style={{ minHeight: '100vh' }}>
  279. <Sider
  280. trigger={null}
  281. collapsible
  282. collapsed={collapsed}
  283. width={240}
  284. className="custom-sider"
  285. style={{
  286. overflow: 'auto',
  287. height: '100vh',
  288. position: 'fixed',
  289. left: 0,
  290. top: 0,
  291. bottom: 0,
  292. zIndex: 100,
  293. }}
  294. >
  295. <div className="p-4">
  296. <Typography.Title level={2} className="text-xl font-bold truncate">
  297. {collapsed ? '应用' : appName}
  298. </Typography.Title>
  299. </div>
  300. {/* 搜索框 - 仅在展开状态下显示 */}
  301. {!collapsed && (
  302. <div style={{ padding: '0 16px 16px' }}>
  303. <Input
  304. placeholder="搜索菜单..."
  305. value={searchText}
  306. onChange={(e) => handleSearch(e.target.value)}
  307. suffix={
  308. searchText ?
  309. <Button
  310. type="text"
  311. size="small"
  312. icon={<CloseOutlined />}
  313. onClick={clearSearch}
  314. /> :
  315. <SearchOutlined />
  316. }
  317. />
  318. </div>
  319. )}
  320. <Menu
  321. theme={isDark ? "light" : "light"}
  322. mode="inline"
  323. selectedKeys={[location.pathname.replace('/admin', '')]}
  324. openKeys={openKeys}
  325. onOpenChange={onOpenChange}
  326. items={filteredMenuItems}
  327. onClick={handleMenuClick}
  328. />
  329. </Sider>
  330. <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
  331. <Header className="p-0 flex justify-between items-center"
  332. style={{
  333. position: 'sticky',
  334. top: 0,
  335. zIndex: 99,
  336. boxShadow: '0 1px 4px rgba(0,21,41,0.08)',
  337. }}
  338. >
  339. <Button
  340. type="text"
  341. icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
  342. onClick={() => setCollapsed(!collapsed)}
  343. className="w-16 h-16"
  344. />
  345. <Space size="middle" className="mr-4">
  346. <Badge count={5} offset={[0, 5]}>
  347. <Button
  348. type="text"
  349. icon={<BellOutlined />}
  350. />
  351. </Badge>
  352. <Dropdown menu={{ items: userMenuItems }}>
  353. <Space className="cursor-pointer">
  354. <Avatar
  355. src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
  356. icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
  357. />
  358. <span>
  359. {user?.nickname || user?.username}
  360. </span>
  361. </Space>
  362. </Dropdown>
  363. </Space>
  364. </Header>
  365. <Content className="m-6" style={{ overflow: 'initial' }}>
  366. <div className="site-layout-content p-6 rounded-lg">
  367. <Outlet />
  368. </div>
  369. {/* 回到顶部按钮 */}
  370. {showBackTop && (
  371. <Button
  372. type="primary"
  373. shape="circle"
  374. icon={<VerticalAlignTopOutlined />}
  375. size="large"
  376. onClick={scrollToTop}
  377. style={{
  378. position: 'fixed',
  379. right: 30,
  380. bottom: 30,
  381. zIndex: 1000,
  382. boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
  383. }}
  384. />
  385. )}
  386. </Content>
  387. </Layout>
  388. </Layout>
  389. );
  390. };
  391. // 错误页面组件
  392. const ErrorPage = () => {
  393. const { isDark } = useTheme();
  394. const error = useRouteError() as any;
  395. const errorMessage = error?.statusText || error?.message || '未知错误';
  396. return (
  397. <div className="flex flex-col items-center justify-center min-h-screen p-4"
  398. style={{ color: isDark ? '#fff' : 'inherit' }}
  399. >
  400. <div className="max-w-3xl w-full">
  401. <h1 className="text-2xl font-bold mb-4">发生错误</h1>
  402. <Alert
  403. type="error"
  404. message={error?.message || '未知错误'}
  405. description={
  406. error?.stack ? (
  407. <pre className="text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded">
  408. {error.stack}
  409. </pre>
  410. ) : null
  411. }
  412. className="mb-4"
  413. />
  414. <div className="flex gap-4">
  415. <Button
  416. type="primary"
  417. onClick={() => window.location.reload()}
  418. >
  419. 重新加载
  420. </Button>
  421. <Button
  422. onClick={() => window.location.href = '/admin'}
  423. >
  424. 返回首页
  425. </Button>
  426. </div>
  427. </div>
  428. </div>
  429. );
  430. };
  431. // 应用入口组件
  432. const App = () => {
  433. // 路由配置
  434. const router = createBrowserRouter([
  435. {
  436. path: '/',
  437. element: <Navigate to="/admin" replace />
  438. },
  439. {
  440. path: '/admin/login',
  441. element: <LoginPage />
  442. },
  443. {
  444. path: '/admin',
  445. element: (
  446. <ProtectedRoute>
  447. <MainLayout />
  448. </ProtectedRoute>
  449. ),
  450. children: [
  451. {
  452. index: true,
  453. element: <Navigate to="/admin/dashboard" />
  454. },
  455. {
  456. path: 'dashboard',
  457. element: <DashboardPage />,
  458. errorElement: <ErrorPage />
  459. },
  460. {
  461. path: 'users',
  462. element: <UsersPage />,
  463. errorElement: <ErrorPage />
  464. },
  465. {
  466. path: 'settings',
  467. element: <SettingsPage />,
  468. errorElement: <ErrorPage />
  469. },
  470. {
  471. path: 'theme-settings',
  472. element: <ThemeSettingsPage />,
  473. errorElement: <ErrorPage />
  474. },
  475. {
  476. path: 'chart-dashboard',
  477. element: <ChartDashboardPage />,
  478. errorElement: <ErrorPage />
  479. },
  480. {
  481. path: 'map-dashboard',
  482. element: <LoginMapPage />,
  483. errorElement: <ErrorPage />
  484. },
  485. {
  486. path: 'know-info',
  487. element: <KnowInfoPage />,
  488. errorElement: <ErrorPage />
  489. },
  490. {
  491. path: 'file-library',
  492. element: <FileLibraryPage />,
  493. errorElement: <ErrorPage />
  494. },
  495. {
  496. path: 'messages',
  497. element: <MessagesPage />,
  498. errorElement: <ErrorPage />
  499. },
  500. ],
  501. },
  502. ]);
  503. return <RouterProvider router={router} />
  504. };
  505. // 渲染应用
  506. const root = createRoot(document.getElementById('root') as HTMLElement);
  507. root.render(
  508. <QueryClientProvider client={queryClient}>
  509. <ThemeProvider>
  510. <AuthProvider>
  511. <App />
  512. </AuthProvider>
  513. </ThemeProvider>
  514. </QueryClientProvider>
  515. );