From 20563335296dbed4d9b702fcaeee21a90b55241e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=A1=D0=B0?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B8=D0=BD?= Date: Mon, 13 Jul 2026 17:54:53 +0300 Subject: [PATCH] =?UTF-8?q?fix(tables):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B?= =?UTF-8?q?=20=D0=B8=20showSizeChanger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Размер таблицы сохраняется в localStorage, опции 10/20/50/100 на всех list-страницах. Co-authored-by: Cursor --- src/pages/admins/AdminListPage.tsx | 18 +++----- src/pages/audit/AuditPage.tsx | 18 +++----- src/pages/banned-words/BannedWordsPage.tsx | 18 +++----- src/pages/calendars/CalendarListPage.tsx | 18 +++----- src/pages/events/EventListPage.tsx | 18 +++----- src/pages/reports/ReportListPage.tsx | 21 ++++----- src/pages/reviews/ReviewListPage.tsx | 21 ++++----- .../subscriptions/SubscriptionListPage.tsx | 18 +++----- src/pages/tickets/TicketListPage.tsx | 21 ++++----- src/pages/users/UserListPage.tsx | 21 ++++----- src/utils/tablePagination.ts | 44 +++++++++++++++++++ 11 files changed, 122 insertions(+), 114 deletions(-) create mode 100644 src/utils/tablePagination.ts diff --git a/src/pages/admins/AdminListPage.tsx b/src/pages/admins/AdminListPage.tsx index d87f0f8..b2a2661 100644 --- a/src/pages/admins/AdminListPage.tsx +++ b/src/pages/admins/AdminListPage.tsx @@ -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({ limit: 20, offset: 0, sort: 'email', order: 'asc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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 = { @@ -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)} /> {/* Модальное окно создания */} diff --git a/src/pages/audit/AuditPage.tsx b/src/pages/audit/AuditPage.tsx index e423a69..22331be 100644 --- a/src/pages/audit/AuditPage.tsx +++ b/src/pages/audit/AuditPage.tsx @@ -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({ limit: 20, offset: 0, sort: 'timestamp', order: 'desc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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)} /> ); diff --git a/src/pages/banned-words/BannedWordsPage.tsx b/src/pages/banned-words/BannedWordsPage.tsx index 4131adb..2b90edb 100644 --- a/src/pages/banned-words/BannedWordsPage.tsx +++ b/src/pages/banned-words/BannedWordsPage.tsx @@ -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({ limit: 20, offset: 0 }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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)} /> ); diff --git a/src/pages/calendars/CalendarListPage.tsx b/src/pages/calendars/CalendarListPage.tsx index d076a4d..30a3339 100644 --- a/src/pages/calendars/CalendarListPage.tsx +++ b/src/pages/calendars/CalendarListPage.tsx @@ -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({ limit: 20, offset: 0, sort: 'created_at', order: 'desc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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 = [ @@ -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)} /> { const navigate = useNavigate(); - const [params, setParams] = useState({ limit: 20, offset: 0, sort: 'id', order: 'asc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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 = { @@ -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)} /> { const navigate = useNavigate(); - const [params, setParams] = useState({ limit: 20, offset: 0, sort: 'created_at', order: 'desc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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) => ( - {status} + {formatStatusLabel(status)} ), }, @@ -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)} /> { const navigate = useNavigate(); - const [params, setParams] = useState({ limit: 20, offset: 0, sort: 'created_at', order: 'desc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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) => ( - {status} + {formatStatusLabel(status)} ), }, @@ -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)} /> {/* Модальное окно индивидуального редактирования */} diff --git a/src/pages/subscriptions/SubscriptionListPage.tsx b/src/pages/subscriptions/SubscriptionListPage.tsx index 0a6ee17..b9ee178 100644 --- a/src/pages/subscriptions/SubscriptionListPage.tsx +++ b/src/pages/subscriptions/SubscriptionListPage.tsx @@ -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({ limit: 20, offset: 0 }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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)} /> { const navigate = useNavigate(); - const [params, setParams] = useState({ limit: 20, offset: 0, sort: 'last_seen', order: 'desc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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 = [ @@ -78,7 +80,7 @@ const TicketListPage: React.FC = () => { status === 'open' ? 'red' : status === 'in_progress' ? 'blue' : status === 'resolved' ? 'green' : 'default'; - return {status}; + return {formatStatusLabel(status)}; }, }, { @@ -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)} /> ); diff --git a/src/pages/users/UserListPage.tsx b/src/pages/users/UserListPage.tsx index d051f35..eada6d5 100644 --- a/src/pages/users/UserListPage.tsx +++ b/src/pages/users/UserListPage.tsx @@ -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({ limit: 20, offset: 0, sort: 'id', order: 'asc' }); + const [params, setParams] = useState({ 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 | SorterResult[] ) => { 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 = [ @@ -202,7 +204,7 @@ const UserListPage: React.FC = () => { sorter: true, render: (status: string) => ( - {status} + {formatStatusLabel(status)} ), }, @@ -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)} /> {/* Модальное окно редактирования */} diff --git a/src/utils/tablePagination.ts b/src/utils/tablePagination.ts new file mode 100644 index 0000000..2e641dc --- /dev/null +++ b/src/utils/tablePagination.ts @@ -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( + 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, + }; +}