Разработка админ-панели 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
+354
View File
@@ -0,0 +1,354 @@
import React, { useState, useEffect, useRef } from 'react';
import { Table, Button, Tag, Space, Modal, Form, Select, message, Spin, Input, Tooltip } from 'antd';
import { InfoCircleOutlined, EditOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useUsers, useUpdateUser, useDeleteUser, useUser } from '../../hooks/useUsers';
import { User, UserListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
const UserListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<UserListParams>({ limit: 20, offset: 0, sort: 'id', order: 'asc' });
const { data, isLoading } = useUsers(params);
const updateUser = useUpdateUser();
const deleteUser = useDeleteUser();
// Редактирование
const [editModal, setEditModal] = useState<{ open: boolean; userId: string | null }>({
open: false,
userId: null,
});
const { data: editingUser, isLoading: loadingUser } = useUser(editModal.userId || '');
const [editForm] = Form.useForm();
const [editHasErrors, setEditHasErrors] = useState(true);
const originalUserRef = useRef<User | null>(null);
// Изменение статуса
const [statusModal, setStatusModal] = useState<{
open: boolean;
userId: string | null;
newStatus: string;
currentReason: string;
}>({
open: false,
userId: null,
newStatus: 'active',
currentReason: '',
});
const [statusForm] = Form.useForm();
// ==================== Редактирование ====================
const handleEdit = (id: string) => {
setEditModal({ open: true, userId: id });
};
const handleSaveEdit = () => {
editForm.validateFields().then((values) => {
if (!editModal.userId || !originalUserRef.current) return;
const original = originalUserRef.current;
const cleanedValues: Record<string, any> = {};
const allKeys = new Set([...Object.keys(values), ...Object.keys(original)]);
allKeys.forEach((key) => {
const newVal = values[key];
if (newVal === '' || newVal === undefined || newVal === null) {
const origVal = (original as any)[key];
if (origVal && origVal !== '-' && origVal !== '' && origVal !== undefined && origVal !== null) {
cleanedValues[key] = null;
}
return;
}
if (newVal === '-') return;
cleanedValues[key] = newVal;
});
const payload = Object.fromEntries(
Object.entries(cleanedValues).filter(([key, v]) => {
const origVal = (original as any)[key];
return JSON.stringify(v) !== JSON.stringify(origVal === '-' ? undefined : origVal);
})
);
updateUser.mutate(
{ id: editModal.userId, data: payload },
{ onSuccess: () => setEditModal({ open: false, userId: null }) }
);
});
};
useEffect(() => {
if (editingUser && editModal.open) {
originalUserRef.current = editingUser;
const clean = (val: any) => (val === '-' || val === 'undefined' ? undefined : val);
setTimeout(() => {
editForm.setFieldsValue({
email: clean(editingUser.email),
nickname: clean(editingUser.nickname),
role: clean(editingUser.role),
status: clean(editingUser.status),
reason: clean(editingUser.reason),
});
validateEditForm();
}, 0);
}
}, [editingUser, editModal.open, editForm]);
const validateEditForm = () => {
const role = editForm.getFieldValue('role');
const status = editForm.getFieldValue('status');
const hasErrors = !role || !status;
setEditHasErrors(hasErrors);
};
// ==================== Изменение статуса ====================
const handleStatusChange = (user: User) => {
if (user.status === 'deleted') return;
const newStatus = user.status === 'active' ? 'frozen' : 'active';
const currentReason = user.reason && user.reason !== '-' ? user.reason : '';
setStatusModal({
open: true,
userId: user.id,
newStatus,
currentReason,
});
};
useEffect(() => {
if (statusModal.open) {
setTimeout(() => {
statusForm.setFieldsValue({ reason: statusModal.currentReason });
}, 0);
}
}, [statusModal.open, statusModal.currentReason, statusForm]);
const handleStatusSave = () => {
statusForm.validateFields().then((values) => {
if (!statusModal.userId) return;
const payload: any = { status: statusModal.newStatus };
if (values.reason && values.reason.trim() !== '') {
payload.reason = values.reason;
}
updateUser.mutate(
{ id: statusModal.userId, data: payload },
{ onSuccess: () => setStatusModal({ open: false, userId: null, newStatus: 'active', currentReason: '' }) }
);
});
};
// ==================== Удаление ====================
const handleDelete = (id: string) => {
Modal.confirm({
title: 'Удалить пользователя?',
content: 'Это действие нельзя отменить. При необходимости предварительно укажите причину через редактирование.',
okText: 'Удалить',
okType: 'danger',
cancelText: 'Отмена',
onOk: () => deleteUser.mutate(id),
});
};
// ==================== Таблица ====================
const handleTableChange = (
pagination: any,
filters: any,
sorter: SorterResult<User> | SorterResult<User>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
...prev,
sort: s.field as string,
order: s.order === 'ascend' ? 'asc' : s.order === 'descend' ? 'desc' : undefined,
offset: ((pagination.current - 1) * pagination.pageSize) || 0,
limit: pagination.pageSize || prev.limit,
}));
};
const columns: ColumnsType<User> = [
{
title: <InfoCircleOutlined />,
key: 'detail',
width: 48,
align: 'center',
render: (_, record) => (
<Tooltip title="Открыть страницу пользователя">
<Button
icon={<InfoCircleOutlined />}
size="small"
type="link"
onClick={() => navigate(`/users/${record.id}`)}
/>
</Tooltip>
),
},
{ title: 'Email', dataIndex: 'email', key: 'email', sorter: true, ellipsis: true },
{ title: 'Ник', dataIndex: 'nickname', key: 'nickname', sorter: true, ellipsis: true },
{
title: 'Роль',
dataIndex: 'role',
key: 'role',
width: 100,
sorter: true,
render: (role: string) => {
const color = role === 'user' ? 'blue' : role === 'bot' ? 'purple' : 'default';
return <Tag color={color}>{role}</Tag>;
},
},
{
title: 'Статус',
dataIndex: 'status',
key: 'status',
width: 100,
sorter: true,
render: (status: string) => (
<Tag color={status === 'active' ? 'green' : status === 'frozen' ? 'orange' : 'red'}>
{status}
</Tag>
),
},
{ title: 'Последний вход', dataIndex: 'last_login', key: 'last_login', sorter: true },
{
title: 'Действия',
key: 'actions',
width: 120,
render: (_, record) => {
const isDeleted = record.status === 'deleted';
return (
<Space>
<Tooltip title="Редактировать">
<Button
icon={<EditOutlined />}
size="small"
onClick={() => handleEdit(record.id)}
/>
</Tooltip>
<Tooltip title={record.status === 'active' ? 'Заморозить' : 'Разморозить'}>
<Button
icon={record.status === 'active' ? <LockOutlined /> : <UnlockOutlined />}
size="small"
onClick={() => handleStatusChange(record)}
disabled={isDeleted}
/>
</Tooltip>
<Tooltip title="Удалить">
<Button
icon={<DeleteOutlined />}
size="small"
danger
onClick={() => handleDelete(record.id)}
disabled={isDeleted}
/>
</Tooltip>
</Space>
);
},
},
];
return (
<div>
<h2>Пользователи</h2>
<Table
columns={columns}
dataSource={data?.data}
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
/>
{/* Модальное окно редактирования */}
<Modal
title="Редактировать пользователя"
open={editModal.open}
onCancel={() => setEditModal({ open: false, userId: null })}
onOk={handleSaveEdit}
confirmLoading={updateUser.isPending}
destroyOnHidden
okButtonProps={{ disabled: editHasErrors }}
>
{loadingUser ? (
<div style={{ textAlign: 'center', padding: 24 }}><Spin /></div>
) : (
<Form form={editForm} layout="vertical" preserve={false} onValuesChange={validateEditForm}>
<Form.Item label="Email" name="email">
<Input />
</Form.Item>
<Form.Item label="Ник" name="nickname">
<Input />
</Form.Item>
<Form.Item
label="Роль"
name="role"
rules={[{ required: true, message: 'Выберите роль' }]}
>
<Select placeholder="Выберите роль">
<Select.Option value="user">user</Select.Option>
<Select.Option value="bot">bot</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Статус"
name="status"
rules={[{ required: true, message: 'Выберите статус' }]}
>
<Select placeholder="Выберите статус">
<Select.Option value="active">active</Select.Option>
<Select.Option value="frozen">frozen</Select.Option>
<Select.Option value="deleted">deleted</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Причина"
name="reason"
rules={[
({ getFieldValue }) => ({
validator(_, value) {
const status = getFieldValue('status');
if (status && status !== 'active' && (!value || value.trim() === '')) {
return Promise.reject(new Error('Укажите причину изменения статуса'));
}
return Promise.resolve();
},
}),
]}
>
<Input.TextArea rows={3} />
</Form.Item>
</Form>
)}
</Modal>
{/* Модальное окно изменения статуса */}
<Modal
title={`Изменить статус на «${statusModal.newStatus}»`}
open={statusModal.open}
onCancel={() => setStatusModal({ open: false, userId: null, newStatus: 'active', currentReason: '' })}
onOk={handleStatusSave}
confirmLoading={updateUser.isPending}
destroyOnHidden
>
<Form form={statusForm} layout="vertical" preserve={false}>
<Form.Item
label="Причина"
name="reason"
rules={[
{
required: statusModal.newStatus !== 'active',
message: 'Укажите причину',
},
]}
>
<Input.TextArea rows={3} placeholder="Введите причину изменения статуса" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default UserListPage;