Разработка админ-панели EventHubFrontAdmin v1.0 #1
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user