AdminLayout.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { ProLayout } from '@ant-design/pro-components';
  2. import { Button, Dropdown, Select, Skeleton, message } from 'antd';
  3. import type { MenuProps } from 'antd';
  4. import {
  5. Bot,
  6. ChevronDown,
  7. ClipboardList,
  8. Gift,
  9. LayoutDashboard,
  10. LogOut,
  11. MessageSquareMore,
  12. Settings,
  13. Star,
  14. Users,
  15. } from 'lucide-react';
  16. import { Outlet, history, useLocation } from '@umijs/max';
  17. import { useEffect, useState } from 'react';
  18. import {
  19. apiRequest,
  20. getSelectedBotId,
  21. jsonOptions,
  22. setCsrfToken,
  23. setSelectedBotId,
  24. } from '@/services/api';
  25. import type { AdminUser, BotProfile } from '@/types';
  26. const menuItems = [
  27. { path: '/dashboard', name: '概览', icon: <LayoutDashboard size={18} /> },
  28. { path: '/chats', name: '群组管理', icon: <Users size={18} /> },
  29. { path: '/giveaways', name: '抽奖管理', icon: <Gift size={18} /> },
  30. { path: '/points', name: '积分管理', icon: <Star size={18} /> },
  31. { path: '/audit', name: '操作日志', icon: <ClipboardList size={18} /> },
  32. { path: '/settings', name: '系统设置', icon: <Settings size={18} /> },
  33. ];
  34. export default function AdminLayout() {
  35. const location = useLocation();
  36. const [user, setUser] = useState<AdminUser | null>(null);
  37. const [bots, setBots] = useState<BotProfile[]>([]);
  38. const [selectedBot, setSelectedBot] = useState(getSelectedBotId());
  39. useEffect(() => {
  40. apiRequest<AdminUser>('/auth/me')
  41. .then((current) => {
  42. setCsrfToken(current.csrf_token);
  43. if (current.must_change_password) {
  44. history.replace('/change-password');
  45. return;
  46. }
  47. setUser(current);
  48. })
  49. .catch((error) => message.error(error.message));
  50. }, []);
  51. useEffect(() => {
  52. const loadBots = () => {
  53. apiRequest<{ items: BotProfile[] }>('/bots')
  54. .then(({ items }) => {
  55. setBots(items);
  56. const current = getSelectedBotId();
  57. if (current && items.some((item) => item.bot_id === current)) {
  58. setSelectedBot(current);
  59. return;
  60. }
  61. const first = items.find((item) => item.enabled) || items[0];
  62. const next = first?.bot_id || '';
  63. setSelectedBot(next);
  64. setSelectedBotId(next);
  65. })
  66. .catch(() => setBots([]));
  67. };
  68. loadBots();
  69. window.addEventListener('bot-profiles-changed', loadBots);
  70. return () => window.removeEventListener('bot-profiles-changed', loadBots);
  71. }, []);
  72. const accountMenu: MenuProps['items'] = [
  73. {
  74. key: 'settings',
  75. icon: <Settings size={16} />,
  76. label: '账号设置',
  77. onClick: () => history.push('/settings'),
  78. },
  79. {
  80. key: 'logout',
  81. danger: true,
  82. icon: <LogOut size={16} />,
  83. label: '退出登录',
  84. onClick: async () => {
  85. await apiRequest('/auth/logout', jsonOptions('POST'));
  86. sessionStorage.removeItem('admin_csrf');
  87. history.replace('/login');
  88. },
  89. },
  90. ];
  91. if (!user) {
  92. return <Skeleton active style={{ padding: 24 }} />;
  93. }
  94. return (
  95. <ProLayout
  96. title="Telegram 群管"
  97. logo={<Bot size={24} color="#fff" aria-hidden="true" />}
  98. location={{ pathname: location.pathname }}
  99. menu={{ request: async () => menuItems }}
  100. layout="mix"
  101. fixedHeader
  102. fixSiderbar
  103. siderWidth={224}
  104. breakpoint="lg"
  105. token={{
  106. header: { colorBgHeader: '#ffffff', colorTextMenu: '#17212b' },
  107. sider: { colorMenuBackground: '#17212b' },
  108. pageContainer: { paddingInlinePageContainerContent: 20 },
  109. }}
  110. menuItemRender={(item, dom) => (
  111. <button
  112. type="button"
  113. onClick={() => item.path && history.push(item.path)}
  114. style={{
  115. width: '100%',
  116. padding: 0,
  117. color: 'inherit',
  118. textAlign: 'left',
  119. background: 'transparent',
  120. border: 0,
  121. cursor: 'pointer',
  122. }}
  123. >
  124. {dom}
  125. </button>
  126. )}
  127. avatarProps={{
  128. src: undefined,
  129. icon: <MessageSquareMore size={18} />,
  130. title: (
  131. <Dropdown menu={{ items: accountMenu }} trigger={['click']}>
  132. <Button type="text" icon={<ChevronDown size={15} />} iconPosition="end">
  133. {user.username}
  134. </Button>
  135. </Dropdown>
  136. ),
  137. render: (_, dom) => dom,
  138. }}
  139. actionsRender={() => [
  140. <Select
  141. key="bot-selector"
  142. aria-label="当前机器人"
  143. value={selectedBot || undefined}
  144. placeholder="选择机器人"
  145. style={{ width: 200, maxWidth: '34vw' }}
  146. options={bots.map((item) => ({
  147. value: item.bot_id,
  148. label: item.identity?.username ? `@${item.identity.username}` : item.label,
  149. }))}
  150. onChange={(value) => {
  151. setSelectedBot(value);
  152. setSelectedBotId(value);
  153. }}
  154. />,
  155. ]}
  156. >
  157. <main className="page-content" id="main-content">
  158. <Outlet />
  159. </main>
  160. </ProLayout>
  161. );
  162. }