2
0

web_app.tsx 14 KB

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