import { useRef, useMemo } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { IconArrowUp, IconArrowDown, IconEdit, IconTrash, IconCopy } from '@tabler/icons-react'; import TableSkeleton from './TableSkeleton.jsx'; import EmptyState from './EmptyState.jsx'; import Tooltip from './Tooltip.jsx'; /** * Виртуализированная таблица для больших списков данных (1000+ элементов) * Рендерит только видимые строки для оптимальной производительности * * @param {Object} props * @param {Array} props.columns - Массив описаний колонок * @param {Array} props.items - Массив данных для отображения * @param {string} props.itemKey - Ключ уникального идентификатора * @param {boolean} props.loading - Состояние загрузки * @param {string} props.sortField - Текущее поле сортировки * @param {string} props.sortOrder - Направление сортировки * @param {function} props.onSort - Обработчик сортировки * @param {Set} props.selectedItems - Выбранные элементы * @param {function} props.onSelectItem - Обработчик выбора * @param {function} props.onSelectAll - Обработчик выбора всех * @param {function} props.onEdit - Обработчик редактирования * @param {function} props.onDelete - Обработчик удаления * @param {function} props.onCopy - Обработчик копирования * @param {number} props.rowHeight - Высота строки в пикселях (default: 48) * @param {number} props.containerHeight - Высота контейнера (default: 600) * @param {number} props.overscan - Количество дополнительных строк (default: 5) * @param {Object} props.emptyState - Настройки пустого состояния * @param {boolean} props.selectable - Включить чекбоксы (default: true) */ function VirtualizedTable({ columns = [], items = [], itemKey = 'id', loading = false, sortField, sortOrder = 'asc', onSort, selectedItems = new Set(), onSelectItem, onSelectAll, onDeselectAll, onEdit, onDelete, onCopy, rowHeight = 48, containerHeight = 600, overscan = 5, emptyState, selectable = true, actions = [], }) { const parentRef = useRef(null); const hasActions = onEdit || onDelete || onCopy || actions.length > 0; const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => rowHeight, overscan, }); const virtualItems = virtualizer.getVirtualItems(); const totalSize = virtualizer.getTotalSize(); // Рендер заголовка const renderHeader = () => ( {selectable && ( 0 && items.every(i => selectedItems.has(i[itemKey]))} onChange={(e) => e.target.checked ? onSelectAll?.() : onDeselectAll?.()} title="Выбрать все" /> )} {columns.map((col) => { const isSortable = col.sortable !== false && onSort; const isActive = sortField === col.key; return ( onSort(col.key) : undefined} style={col.width ? { width: col.width } : undefined} >
{col.icon && } {col.title} {isSortable && isActive && ( {sortOrder === 'asc' ? : } )}
); })} {hasActions && Действия} ); // Рендер действий const renderActions = (item) => ( <> {onEdit && ( )} {onCopy && ( )} {actions.map((action, idx) => ( ))} {onDelete && ( )} ); // Загрузка if (loading) { return ; } // Пустое состояние if (items.length === 0) { return (
{emptyState ? ( ) : ( )}
); } return (
{renderHeader()}
{virtualItems.map((virtualRow) => { const item = items[virtualRow.index]; const id = item[itemKey]; const isSelected = selectedItems.has(id); return ( {selectable && ( )} {columns.map((col) => ( ))} {hasActions && ( )} ); })}
onSelectItem?.(id)} /> {col.render ? col.render(item[col.key], item) : item[col.key]} {renderActions(item)}
Показано {items.length} записей {items.length > 100 && ( Виртуализация активна )}
); } export default VirtualizedTable;