Разработка админ-панели EventHubFrontAdmin v1.0 #1

This commit is contained in:
2026-05-23 20:43:21 +03:00
parent 36c95b71a4
commit f5961c5529
68 changed files with 10374 additions and 3 deletions
+281
View File
@@ -0,0 +1,281 @@
import React, { useEffect, useState } from 'react';
import { Layout, Menu, Button, theme, notification, Avatar, Dropdown, Space, Typography, Badge } from 'antd';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { useAdminWebSocket } from '../hooks/useAdminWebSocket';
import {
DashboardOutlined,
UserOutlined,
CalendarOutlined,
WarningOutlined,
StarOutlined,
StopOutlined,
BugOutlined,
DollarOutlined,
TeamOutlined,
AuditOutlined,
LogoutOutlined,
SettingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from '@ant-design/icons';
const { Header, Sider, Content } = Layout;
const { Text } = Typography;
const AdminLayout: React.FC = () => {
useAdminWebSocket();
const navigate = useNavigate();
const location = useLocation();
const { user, logout } = useAuthStore();
const { token: { colorBgContainer } } = theme.useToken();
const [collapsed, setCollapsed] = useState(false);
// Обработка WebSocket-уведомлений
useEffect(() => {
const handler = (event: CustomEvent) => {
const msg = event.detail;
if (msg.type === 'report_created') {
const { report_id, target_type, target_id, reason } = msg.data || {};
notification.info({
title: 'Новая жалоба',
description: (
<span>
Жалоба{' '}
{report_id ? (
<a
href={`/reports/${report_id}`}
onClick={(e) => {
e.preventDefault();
navigate(`/reports/${report_id}`);
}}
style={{ fontWeight: 600 }}
>
#{report_id}
</a>
) : (
'#?'
)}{' '}
на {target_type || 'неизвестный тип'}{' '}
{target_id ? (
<a
href={`/${target_type}s/${target_id}`}
onClick={(e) => {
e.preventDefault();
navigate(`/${target_type}s/${target_id}`);
}}
style={{ fontWeight: 600 }}
>
{target_id}
</a>
) : (
'?'
)}{' '}
{reason ? `(${reason})` : ''}
</span>
),
placement: 'topRight',
});
} else if (msg.type === 'ticket_created') {
const { ticket_id } = msg.data || {};
notification.info({
title: 'Новый тикет',
description: (
<span>
Тикет{' '}
{ticket_id ? (
<a
href={`/tickets/${ticket_id}`}
onClick={(e) => {
e.preventDefault();
navigate(`/tickets/${ticket_id}`);
}}
style={{ fontWeight: 600 }}
>
#{ticket_id}
</a>
) : (
'без ID'
)}{' '}
создан
</span>
),
placement: 'topRight',
});
}
};
window.addEventListener('admin-ws-message', handler as EventListener);
return () => window.removeEventListener('admin-ws-message', handler as EventListener);
}, [navigate]);
const menuItems = [
{ key: '/dashboard', icon: <DashboardOutlined />, label: 'Дашборд' },
{ key: '/users', icon: <UserOutlined />, label: 'Пользователи', roles: ['superadmin', 'admin'] },
{ key: '/events', icon: <CalendarOutlined />, label: 'События', roles: ['superadmin', 'admin'] },
{ key: '/reports', icon: <WarningOutlined />, label: 'Жалобы', roles: ['superadmin', 'admin', 'moderator'] },
{ key: '/reviews', icon: <StarOutlined />, label: 'Отзывы', roles: ['superadmin', 'admin', 'moderator'] },
{ key: '/banned-words', icon: <StopOutlined />, label: 'Бан-слова', roles: ['superadmin', 'admin'] },
{ key: '/tickets', icon: <BugOutlined />, label: 'Тикеты', roles: ['superadmin', 'admin', 'support'] },
{ key: '/subscriptions', icon: <DollarOutlined />, label: 'Подписки', roles: ['superadmin', 'admin'] },
{ key: '/admins', icon: <TeamOutlined />, label: 'Администраторы', roles: ['superadmin'] },
{ key: '/audit', icon: <AuditOutlined />, label: 'Аудит', roles: ['superadmin'] },
];
const filteredMenu = menuItems.filter(
(item) => !item.roles || (user && item.roles.includes(user.role))
);
const handleLogout = async () => {
await logout();
navigate('/login');
};
const getDisplayName = () => {
const nick = user?.nickname;
const email = user?.email;
if (nick && nick !== '-' && nick !== 'undefined') return nick;
if (email && email !== '-' && email !== 'undefined') return email;
return user?.id ?? '—';
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider
collapsible
collapsed={collapsed}
onCollapse={setCollapsed}
trigger={null}
style={{
position: 'relative', // чтобы абсолютный блок считался от него
display: 'flex',
flexDirection: 'column',
height: '100vh',
position: 'sticky',
top: 0,
left: 0,
}}
>
<div style={{ height: 32, margin: 16, color: 'white', textAlign: 'center', fontWeight: 'bold' }}>
{collapsed ? 'EH' : import.meta.env.VITE_APP_TITLE}
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={filteredMenu}
onClick={({ key }) => navigate(key)}
style={{
flex: 1,
overflowY: 'auto',
overflowX: 'hidden',
marginBottom: 60, // отступ, чтобы меню не заходило под нижний блок
}}
/>
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
display: 'flex',
alignItems: 'center',
justifyContent: collapsed ? 'center' : 'space-between',
padding: '12px 24px',
borderTop: '1px solid rgba(255,255,255,0.1)',
background: '#001529', // цвет сайдбара Ant Design
flexShrink: 0,
}}
>
<Dropdown
menu={{
items: [
{
key: 'profile',
icon: <SettingOutlined />,
label: 'Мой профиль',
onClick: () => navigate('/profile'),
},
{ type: 'divider' },
{
key: 'logout',
icon: <LogoutOutlined />,
label: 'Выйти',
onClick: handleLogout,
},
],
}}
trigger={['click']}
placement="topLeft"
>
<Button
type="text"
style={{
color: 'white',
display: 'flex',
alignItems: 'center',
padding: '4px 0',
border: 'none',
width: collapsed ? 'auto' : '100%',
}}
>
<Avatar icon={<UserOutlined />} src={user?.avatar_url} size="small" />
{!collapsed && (
<Text
style={{
color: 'white',
marginLeft: 8,
maxWidth: 100,
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
ellipsis
>
{getDisplayName()}
</Text>
)}
</Button>
</Dropdown>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
color: 'white',
fontSize: '16px',
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
/>
</div>
</Sider>
<Layout>
<Header
style={{
padding: '0 24px',
background: colorBgContainer,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Space size="large">
<Badge status="success" text="Онлайн: 12" />
<Badge status="processing" text="Заказов сегодня: 5" />
<Badge status="warning" text="Тикетов: 2" />
</Space>
<div>{/* Резерв */}</div>
</Header>
<Content style={{ margin: 24 }}>
<Outlet />
</Content>
</Layout>
</Layout>
);
};
export default AdminLayout;