1
0

web_app.tsx 14 KB

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