diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx index b739c59..16db1f3 100644 --- a/web/src/pages/Svod2Page/Svod2Page.jsx +++ b/web/src/pages/Svod2Page/Svod2Page.jsx @@ -1,8 +1,9 @@ import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded'; import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material'; -import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; +import { MRT_TableHead, MaterialReactTable, useMaterialReactTable } from 'material-react-table'; import { MRT_Localization_RU } from 'material-react-table/locales/ru'; import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; @@ -13,7 +14,7 @@ import useDenomination from '../../components/common/useDenomination'; import { useSvod } from '../SvodPage/SvodContext'; import { SvodFilters } from '../SvodPage/SvodFilters'; import { EMPTY_FACET_MAP, getSvod2RowId, isPlaceholderRow, shouldShowFound } from './svod2Paging'; -import { SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles'; +import { SVOD2_LAYOUT_OPTIONS, SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles'; import { useSvod2Pages } from './useSvod2Pages'; const SHEETS = [ @@ -273,17 +274,20 @@ const getColumnConfig = (sheet) => { return COLUMN_CONFIG_CACHE.get(sheet); }; -const scaleNumericColumns = (columns, divisor) => columns.map((column) => { - const isText = ['name', 'org_name', 'smeta_type', 'smeta_direction', 'section_code', 'contract_number'].includes(column.id); - const children = column.columns ? scaleNumericColumns(column.columns, divisor) : undefined; - if (isText || children) return { ...column, ...(children ? { columns: children } : {}) }; - return { ...column, Cell: ({ cell }) => formatValue(cell.getValue(), divisor) }; -}); +const scaleNumericColumns = (columns, divisor) => + columns.map((column) => { + const isText = ['name', 'org_name', 'smeta_type', 'smeta_direction', 'section_code', 'contract_number'].includes(column.id); + const children = column.columns ? scaleNumericColumns(column.columns, divisor) : undefined; + if (isText || children) return { ...column, ...(children ? { columns: children } : {}) }; + return { ...column, Cell: ({ cell }) => formatValue(cell.getValue(), divisor) }; + }); export default function Svod2Page() { const navigate = useNavigate(); const { getSheetFilters, updateSheetFilters } = useSvod(); const [sheet, setSheet] = useState('GO'); + // MRT: заголовок рисуем сами через портал (см. комментарий у enableTableHead). + const [tableElement, setTableElement] = useState(null); const denominationStorageKey = `svod2_${sheet}_denomination`; const { denomination, setDenomination: handleDenominationChange, divisor } = useDenomination(denominationStorageKey); const [columnFiltersBySheet, setColumnFiltersBySheet] = useState(() => Object.fromEntries(SHEETS.map(({ value }) => [value, []]))); @@ -356,6 +360,12 @@ export default function Svod2Page() { enablePagination: false, enableRowVirtualization: true, enableColumnVirtualization: true, + // Заголовок MRT при виртуализации колонок рисует строки по индексам листовых колонок, + // и группы (colSpan) разъезжаются. Поэтому штатный thead отключён, а тот же MRT_TableHead + // без columnVirtualizer (все заголовки сразу) вставляется в
порталом. + enableTableHead: false, + muiTableProps: { ref: setTableElement }, + ...SVOD2_LAYOUT_OPTIONS, columnVirtualizerOptions: getSvod2ColumnVirtualizerOptions, enableTopToolbar: false, enableBottomToolbar: false, @@ -410,7 +420,12 @@ export default function Svod2Page() { for (const element of elements ?? []) measured = Math.max(measured, Math.round(element.offsetHeight)); if (measured > 0 && measured !== rowHeight) setRowHeight(measured); }); - let content = ; + let content = ( + <> + + {tableElement && createPortal(, tableElement)} + + ); if (!hasCurrentData && !loading && !error) { content = ( @@ -453,7 +468,12 @@ export default function Svod2Page() { onChange={(_event, value) => setSheet(value)} sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}> {SHEETS.map((item) => ( - + ))} (grid), а должен идти до tbody. + '& thead': { position: 'sticky', top: 0, zIndex: 10, order: -1 }, '& td': { + // Распорки виртуализации колонок (td с inline width) не должны сжиматься во flex-строке. + flexShrink: 0, backgroundColor: '#ffffff', borderRight: '0.0625rem solid #edf0f2', fontSize: '0.8125rem', @@ -70,6 +73,7 @@ export const SVOD2_TABLE_CONTAINER_SX = { '& td.svod2-group-end': { borderRight: GROUP_END_BORDER }, '& td[data-pinned="true"]': { zIndex: 2 }, '& th': { + flexShrink: 0, position: 'sticky', top: 0, boxShadow: 'none', @@ -86,6 +90,8 @@ export const SVOD2_TABLE_CONTAINER_SX = { '& th.svod2-band-1': { backgroundColor: '#f1f7f3' }, '& th.svod2-group-end': { borderRight: GROUP_END_BORDER }, '& th[data-pinned="true"]': { zIndex: 5 }, + // enableColumnPinning добавляет закреплённым ячейкам подложку ::before (z-index -1), она перекрывает чередование фона. + '& td[data-pinned="true"]::before, & th[data-pinned="true"]::before': { display: 'none' }, '& .svod2-badge': { display: 'inline-block', padding: '0.25rem 0.625rem', @@ -122,3 +128,10 @@ export const SVOD2_TABLE_CONTAINER_SX = { whiteSpace: 'nowrap', }, }; + +// Виртуализация колонок требует grid-no-grow (без растяжения ячеек) и enableColumnPinning +// (иначе MRT не учитывает закреплённые колонки в левом отступе виртуализатора). +export const SVOD2_LAYOUT_OPTIONS = { + layoutMode: 'grid-no-grow', + enableColumnPinning: true, +}; diff --git a/web/src/pages/Svod2Page/svod2Styles.test.js b/web/src/pages/Svod2Page/svod2Styles.test.js index 13823cf..d30f3c7 100644 --- a/web/src/pages/Svod2Page/svod2Styles.test.js +++ b/web/src/pages/Svod2Page/svod2Styles.test.js @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + SVOD2_LAYOUT_OPTIONS, SVOD2_TABLE_CONTAINER_SX, createColumnBandMap, createColumnClassMap, @@ -71,3 +72,15 @@ test('заглушка свода 2 — прозрачный текст в ст assert.ok(rule, 'нет правила заглушки'); assert.equal(rule.color, 'transparent'); }); + +test('column virtualization layout uses grid-no-grow and column pinning', () => { + assert.equal(SVOD2_LAYOUT_OPTIONS.layoutMode, 'grid-no-grow'); + assert.equal(SVOD2_LAYOUT_OPTIONS.enableColumnPinning, true); +}); + +test('container styles keep spacers unshrunk, hide pinned overlay and order portal thead first', () => { + assert.equal(SVOD2_TABLE_CONTAINER_SX['& td'].flexShrink, 0); + assert.equal(SVOD2_TABLE_CONTAINER_SX['& th'].flexShrink, 0); + assert.equal(SVOD2_TABLE_CONTAINER_SX['& thead'].order, -1); + assert.equal(SVOD2_TABLE_CONTAINER_SX['& td[data-pinned="true"]::before, & th[data-pinned="true"]::before'].display, 'none'); +});