| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import { ProLayout } from '@ant-design/pro-components';
- import { Button, Dropdown, Select, Skeleton, message } from 'antd';
- import type { MenuProps } from 'antd';
- import {
- Bot,
- ChevronDown,
- ClipboardList,
- Gift,
- LayoutDashboard,
- LogOut,
- MessageSquareMore,
- MapPinned,
- Settings,
- Star,
- Users,
- } from 'lucide-react';
- import { Outlet, history, useLocation } from '@umijs/max';
- import { useEffect, useState } from 'react';
- import {
- apiRequest,
- getSelectedBotId,
- jsonOptions,
- setCsrfToken,
- setSelectedBotId,
- } from '@/services/api';
- import { BotAccessContext } from '@/contexts/BotAccess';
- import type { AdminUser, BotProfile } from '@/types';
- const menuItems = [
- { path: '/dashboard', name: '概览', icon: <LayoutDashboard size={18} /> },
- { path: '/chats', name: '群组管理', icon: <Users size={18} /> },
- { path: '/giveaways', name: '抽奖管理', icon: <Gift size={18} /> },
- { path: '/points', name: '积分管理', icon: <Star size={18} /> },
- { path: '/directory', name: '师生与附近', icon: <MapPinned size={18} /> },
- { path: '/audit', name: '操作日志', icon: <ClipboardList size={18} /> },
- { path: '/settings', name: '系统设置', icon: <Settings size={18} /> },
- ];
- export default function AdminLayout() {
- const location = useLocation();
- const [user, setUser] = useState<AdminUser | null>(null);
- const [bots, setBots] = useState<BotProfile[]>([]);
- const [selectedBot, setSelectedBot] = useState(getSelectedBotId());
- useEffect(() => {
- apiRequest<AdminUser>('/auth/me')
- .then((current) => {
- setCsrfToken(current.csrf_token);
- if (current.must_change_password) {
- history.replace('/change-password');
- return;
- }
- setUser(current);
- })
- .catch((error) => message.error(error.message));
- }, []);
- useEffect(() => {
- const loadBots = () => {
- apiRequest<{ items: BotProfile[] }>('/bots')
- .then(({ items }) => {
- setBots(items);
- const current = getSelectedBotId();
- if (current && items.some((item) => item.bot_id === current)) {
- setSelectedBot(current);
- return;
- }
- const first = items.find((item) => item.enabled) || items[0];
- const next = first?.bot_id || '';
- setSelectedBot(next);
- setSelectedBotId(next);
- })
- .catch(() => setBots([]));
- };
- loadBots();
- window.addEventListener('bot-profiles-changed', loadBots);
- return () => window.removeEventListener('bot-profiles-changed', loadBots);
- }, []);
- const selectedProfile = bots.find((item) => item.bot_id === selectedBot) || null;
- const rolePermissions = selectedProfile?.permissions;
- const hasPermission = (permission: string) =>
- rolePermissions === undefined || rolePermissions.includes(permission);
- const hasAnyPermission = (permissions: string[]) =>
- rolePermissions === undefined ||
- permissions.some((permission) => rolePermissions.includes(permission));
- const chatPermissions = [
- 'chat.profile',
- 'chat.members',
- 'chat.permissions',
- 'chat.announcements',
- 'chat.invites',
- 'chat.rules',
- 'chat.notes',
- 'automation.manage',
- ];
- const visibleMenuItems = menuItems.filter((item) => {
- if (item.path === '/chats') return hasAnyPermission(chatPermissions);
- if (item.path === '/points') return hasPermission('points.manage');
- if (item.path === '/giveaways') return hasPermission('giveaways.manage');
- if (item.path === '/directory') return hasPermission('teacher_directory.manage');
- return true;
- });
- useEffect(() => {
- if (!selectedProfile) return;
- const denied =
- (location.pathname.startsWith('/points') &&
- !hasPermission('points.manage')) ||
- (location.pathname.startsWith('/giveaways') &&
- !hasPermission('giveaways.manage')) ||
- (location.pathname.startsWith('/directory') &&
- !hasPermission('teacher_directory.manage')) ||
- (location.pathname.startsWith('/chats') &&
- !hasAnyPermission(chatPermissions));
- if (denied) history.replace('/dashboard');
- }, [selectedBot, bots, location.pathname]);
- const accountMenu: MenuProps['items'] = [
- {
- key: 'settings',
- icon: <Settings size={16} />,
- label: '账号设置',
- onClick: () => history.push('/settings'),
- },
- {
- key: 'logout',
- danger: true,
- icon: <LogOut size={16} />,
- label: '退出登录',
- onClick: async () => {
- await apiRequest('/auth/logout', jsonOptions('POST'));
- sessionStorage.removeItem('admin_csrf');
- history.replace('/login');
- },
- },
- ];
- if (!user) {
- return <Skeleton active style={{ padding: 24 }} />;
- }
- return (
- <BotAccessContext.Provider
- value={{ bot: selectedProfile, hasPermission, hasAnyPermission }}
- >
- <ProLayout
- title="Telegram 群管"
- logo={<Bot size={24} color="#fff" aria-hidden="true" />}
- location={{ pathname: location.pathname }}
- menu={{ request: async () => visibleMenuItems }}
- layout="mix"
- fixedHeader
- fixSiderbar
- siderWidth={224}
- breakpoint="lg"
- token={{
- header: { colorBgHeader: '#ffffff', colorTextMenu: '#17212b' },
- sider: { colorMenuBackground: '#17212b' },
- pageContainer: { paddingInlinePageContainerContent: 20 },
- }}
- menuItemRender={(item, dom) => (
- <button
- type="button"
- onClick={() => item.path && history.push(item.path)}
- style={{
- width: '100%',
- padding: 0,
- color: 'inherit',
- textAlign: 'left',
- background: 'transparent',
- border: 0,
- cursor: 'pointer',
- }}
- >
- {dom}
- </button>
- )}
- avatarProps={{
- src: undefined,
- icon: <MessageSquareMore size={18} />,
- title: (
- <Dropdown menu={{ items: accountMenu }} trigger={['click']}>
- <Button type="text" icon={<ChevronDown size={15} />} iconPosition="end">
- {user.username}
- </Button>
- </Dropdown>
- ),
- render: (_, dom) => dom,
- }}
- actionsRender={() => [
- <Select
- key="bot-selector"
- aria-label="当前机器人"
- value={selectedBot || undefined}
- placeholder="选择机器人"
- style={{ width: 200, maxWidth: '34vw' }}
- options={bots.map((item) => ({
- value: item.bot_id,
- label: item.identity?.username ? `@${item.identity.username}` : item.label,
- }))}
- onChange={(value) => {
- setSelectedBot(value);
- setSelectedBotId(value);
- }}
- />,
- ]}
- >
- <main className="page-content" id="main-content">
- <Outlet />
- </main>
- </ProLayout>
- </BotAccessContext.Provider>
- );
- }
|