fix(tables): сохранение размера страницы и showSizeChanger

Размер таблицы сохраняется в localStorage, опции 10/20/50/100 на всех list-страницах.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 17:54:53 +03:00
parent a1bd11afee
commit 2056333529
11 changed files with 122 additions and 114 deletions
+7 -11
View File
@@ -5,10 +5,13 @@ import { useNavigate } from 'react-router-dom';
import { useAdmins, useCreateAdmin, useUpdateAdmin, useDeleteAdmin, useAdmin } from '../../hooks/useAdmins';
import { Admin, AdminListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
const TABLE_KEY = 'admins';
const AdminListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<AdminListParams>({ limit: 20, offset: 0, sort: 'email', order: 'asc' });
const [params, setParams] = useState<AdminListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'email', order: 'asc' });
const { data, isLoading } = useAdmins(params);
const createAdmin = useCreateAdmin();
const updateAdmin = useUpdateAdmin();
@@ -91,13 +94,11 @@ const AdminListPage: React.FC = () => {
sorter: SorterResult<Admin> | SorterResult<Admin>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
const roleColors: Record<string, string> = {
@@ -207,12 +208,7 @@ const AdminListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
{/* Модальное окно создания */}
+7 -11
View File
@@ -7,11 +7,14 @@ import { useUser } from '../../hooks/useUsers';
import { useEvent } from '../../hooks/useEvents';
import { AuditRecord, AuditListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
const { RangePicker } = DatePicker;
const TABLE_KEY = 'audit';
const AuditPage: React.FC = () => {
const [params, setParams] = useState<AuditListParams>({ limit: 20, offset: 0, sort: 'timestamp', order: 'desc' });
const [params, setParams] = useState<AuditListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'timestamp', order: 'desc' });
const { data, isLoading } = useAudit(params);
const { data: admins, isLoading: loadingAdmins } = useAdmins({ limit: 1000, offset: 0 });
@@ -71,13 +74,11 @@ const AuditPage: React.FC = () => {
sorter: SorterResult<AuditRecord> | SorterResult<AuditRecord>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
const handleDateChange = (dates: any) => {
@@ -226,12 +227,7 @@ const AuditPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
</div>
);
+7 -11
View File
@@ -8,9 +8,12 @@ import { useAdmin } from '../../hooks/useAdmins';
import { BannedWord } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import dayjs from 'dayjs';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
const TABLE_KEY = 'banned-words';
const BannedWordsPage: React.FC = () => {
const [params, setParams] = useState<BannedWordListParams>({ limit: 20, offset: 0 });
const [params, setParams] = useState<BannedWordListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0 });
const { data, isLoading } = useBannedWords(params);
const addWord = useAddBannedWord();
const removeWord = useRemoveBannedWord();
@@ -31,13 +34,11 @@ const BannedWordsPage: React.FC = () => {
sorter: SorterResult<BannedWord> | SorterResult<BannedWord>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
// Обработчик поиска
@@ -126,12 +127,7 @@ const BannedWordsPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
</div>
);
+7 -11
View File
@@ -5,10 +5,13 @@ import { Link, useNavigate } from 'react-router-dom';
import { useCalendars, useUpdateCalendar, useDeleteCalendar, useCalendar, useCalendarStats } from '../../hooks/useCalendars';
import { Calendar, CalendarListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
const TABLE_KEY = 'calendars';
const CalendarListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<CalendarListParams>({ limit: 20, offset: 0, sort: 'created_at', order: 'desc' });
const [params, setParams] = useState<CalendarListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'created_at', order: 'desc' });
const { data, isLoading } = useCalendars(params);
const { data: stats } = useCalendarStats();
const updateCalendar = useUpdateCalendar();
@@ -101,13 +104,11 @@ const CalendarListPage: React.FC = () => {
sorter: SorterResult<Calendar> | SorterResult<Calendar>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...prev,
sort: s.field as string,
order: s.order === 'ascend' ? 'asc' : s.order === 'descend' ? 'desc' : undefined,
offset: ((pagination.current || 1) - 1) * (pagination.pageSize || prev.limit || 20),
limit: pagination.pageSize || prev.limit,
}));
}, pagination, TABLE_KEY));
};
const columns: ColumnsType<Calendar> = [
@@ -270,12 +271,7 @@ const CalendarListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
<Modal
+7 -11
View File
@@ -6,10 +6,13 @@ import { useEvents, useUpdateEvent, useDeleteEvent, useEvent, useEventStats } fr
import { Event, EventListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import dayjs from 'dayjs';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
const TABLE_KEY = 'events';
const EventListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<EventListParams>({ limit: 20, offset: 0, sort: 'id', order: 'asc' });
const [params, setParams] = useState<EventListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'id', order: 'asc' });
const { data, isLoading } = useEvents(params);
const { data: stats } = useEventStats();
const updateEvent = useUpdateEvent();
@@ -111,13 +114,11 @@ const EventListPage: React.FC = () => {
sorter: SorterResult<Event> | SorterResult<Event>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
const typeColors: Record<string, string> = {
@@ -265,12 +266,7 @@ const EventListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
<Modal
+9 -12
View File
@@ -8,10 +8,14 @@ import { useEvent } from '../../hooks/useEvents';
import { useAdmin } from '../../hooks/useAdmins';
import { Report, ReportListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
import { formatStatusLabel } from '../../utils/statusLabels';
const TABLE_KEY = 'reports';
const ReportListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<ReportListParams>({ limit: 20, offset: 0, sort: 'created_at', order: 'desc' });
const [params, setParams] = useState<ReportListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'created_at', order: 'desc' });
const { data, isLoading } = useReports(params);
const { data: stats } = useReportStats();
const updateReport = useUpdateReport();
@@ -40,13 +44,11 @@ const ReportListPage: React.FC = () => {
sorter: SorterResult<Report> | SorterResult<Report>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
// Цвета для типа цели
@@ -141,7 +143,7 @@ const ReportListPage: React.FC = () => {
sorter: true,
render: (status: string) => (
<Tag color={status === 'pending' ? 'orange' : status === 'reviewed' ? 'green' : 'default'}>
{status}
{formatStatusLabel(status)}
</Tag>
),
},
@@ -256,12 +258,7 @@ const ReportListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
<Modal
+9 -12
View File
@@ -7,10 +7,14 @@ import { useUser } from '../../hooks/useUsers';
import { useEvent } from '../../hooks/useEvents';
import { Review, ReviewListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
import { formatStatusLabel } from '../../utils/statusLabels';
const TABLE_KEY = 'reviews';
const ReviewListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<ReviewListParams>({ limit: 20, offset: 0, sort: 'created_at', order: 'desc' });
const [params, setParams] = useState<ReviewListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'created_at', order: 'desc' });
const { data, isLoading } = useReviews(params);
const { data: stats } = useReviewStats();
const updateReview = useUpdateReview();
@@ -97,13 +101,11 @@ const ReviewListPage: React.FC = () => {
sorter: SorterResult<Review> | SorterResult<Review>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
const isBadValue = (val: any) => val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
@@ -205,7 +207,7 @@ const ReviewListPage: React.FC = () => {
sorter: true,
render: (status: string) => (
<Tag color={status === 'visible' ? 'green' : status === 'hidden' ? 'orange' : 'red'}>
{status}
{formatStatusLabel(status)}
</Tag>
),
},
@@ -305,12 +307,7 @@ const ReviewListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
{/* Модальное окно индивидуального редактирования */}
@@ -7,9 +7,12 @@ import { useUser } from '../../hooks/useUsers';
import { Subscription, SubscriptionListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import dayjs from 'dayjs';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
const TABLE_KEY = 'subscriptions';
const SubscriptionListPage: React.FC = () => {
const [params, setParams] = useState<SubscriptionListParams>({ limit: 20, offset: 0 });
const [params, setParams] = useState<SubscriptionListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0 });
const { data, isLoading } = useSubscriptions(params);
const { data: stats } = useSubscriptionStats();
const updateSubscription = useUpdateSubscription();
@@ -77,13 +80,11 @@ const SubscriptionListPage: React.FC = () => {
sorter: SorterResult<Subscription> | SorterResult<Subscription>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
const isBadValue = (val: any) => val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
@@ -253,12 +254,7 @@ const SubscriptionListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
<Modal
+9 -12
View File
@@ -6,10 +6,14 @@ import { useTickets, useDeleteTicket, useTicketStats } from '../../hooks/useTick
import { useAdmin } from '../../hooks/useAdmins';
import { Ticket, TicketListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
import { formatStatusLabel } from '../../utils/statusLabels';
const TABLE_KEY = 'tickets';
const TicketListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<TicketListParams>({ limit: 20, offset: 0, sort: 'last_seen', order: 'desc' });
const [params, setParams] = useState<TicketListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'last_seen', order: 'desc' });
const { data, isLoading } = useTickets(params);
const deleteTicket = useDeleteTicket();
const { data: stats } = useTicketStats();
@@ -33,13 +37,11 @@ const TicketListPage: React.FC = () => {
sorter: SorterResult<Ticket> | SorterResult<Ticket>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
const columns: ColumnsType<Ticket> = [
@@ -78,7 +80,7 @@ const TicketListPage: React.FC = () => {
status === 'open' ? 'red' :
status === 'in_progress' ? 'blue' :
status === 'resolved' ? 'green' : 'default';
return <Tag color={color}>{status}</Tag>;
return <Tag color={color}>{formatStatusLabel(status)}</Tag>;
},
},
{
@@ -154,12 +156,7 @@ const TicketListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
</div>
);
+9 -12
View File
@@ -5,10 +5,14 @@ import { useNavigate } from 'react-router-dom';
import { useUsers, useUpdateUser, useDeleteUser, useUser, useUserStats } from '../../hooks/useUsers';
import { User, UserListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { getSavedPageSize, getTablePagination, mergeTablePagination } from '../../utils/tablePagination';
import { formatStatusLabel } from '../../utils/statusLabels';
const TABLE_KEY = 'users';
const UserListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<UserListParams>({ limit: 20, offset: 0, sort: 'id', order: 'asc' });
const [params, setParams] = useState<UserListParams>({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'id', order: 'asc' });
const { data, isLoading } = useUsers(params);
const { data: stats } = useUserStats();
const updateUser = useUpdateUser();
@@ -155,13 +159,11 @@ const UserListPage: React.FC = () => {
sorter: SorterResult<User> | SorterResult<User>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
setParams(prev => mergeTablePagination({
...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,
}));
}, pagination, TABLE_KEY));
};
const columns: ColumnsType<User> = [
@@ -202,7 +204,7 @@ const UserListPage: React.FC = () => {
sorter: true,
render: (status: string) => (
<Tag color={status === 'active' ? 'green' : status === 'frozen' ? 'orange' : 'red'}>
{status}
{formatStatusLabel(status)}
</Tag>
),
},
@@ -282,12 +284,7 @@ const UserListPage: React.FC = () => {
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
pagination={getTablePagination(params, data?.total)}
/>
{/* Модальное окно редактирования */}
+44
View File
@@ -0,0 +1,44 @@
export const TABLE_PAGE_SIZE_OPTIONS = [10, 20, 50, 100] as const;
export const DEFAULT_TABLE_PAGE_SIZE = 20;
const STORAGE_PREFIX = 'admin.table.';
export function getSavedPageSize(tableKey: string, fallback = DEFAULT_TABLE_PAGE_SIZE): number {
const raw = localStorage.getItem(`${STORAGE_PREFIX}${tableKey}.pageSize`);
const size = raw ? Number(raw) : fallback;
return (TABLE_PAGE_SIZE_OPTIONS as readonly number[]).includes(size) ? size : fallback;
}
export function savePageSize(tableKey: string, size: number): void {
localStorage.setItem(`${STORAGE_PREFIX}${tableKey}.pageSize`, String(size));
}
export function getTablePagination(
params: { offset?: number; limit?: number },
total?: number
) {
const pageSize = params.limit || DEFAULT_TABLE_PAGE_SIZE;
return {
total,
current: Math.floor((params.offset || 0) / pageSize) + 1,
pageSize,
showSizeChanger: true,
pageSizeOptions: [...TABLE_PAGE_SIZE_OPTIONS],
};
}
export function mergeTablePagination<T extends { offset?: number; limit?: number }>(
prev: T,
pagination: { current?: number; pageSize?: number },
tableKey: string
): T {
const pageSize = pagination.pageSize || prev.limit || DEFAULT_TABLE_PAGE_SIZE;
if (pagination.pageSize && pagination.pageSize !== prev.limit) {
savePageSize(tableKey, pageSize);
}
return {
...prev,
offset: ((pagination.current ?? 1) - 1) * pageSize,
limit: pageSize,
};
}