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