From 399234a8277e451c2998441b8a0b3630f2366205 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 18:12:41 +0400 Subject: [PATCH 1/3] =?UTF-8?q?perf(web):=20=D1=81=D0=B2=D0=BE=D0=B4=202?= =?UTF-8?q?=20=E2=80=94=20=D0=B2=D0=B8=D1=80=D1=82=D1=83=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=BE=D0=BA=20=D0=B8=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8=20=D1=8F?= =?UTF-8?q?=D1=87=D0=B5=D0=B5=D0=BA=20=D0=BE=D0=B4=D0=BD=D0=B8=D0=BC=20CSS?= =?UTF-8?q?-=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- web/src/pages/Svod2Page/Svod2Page.jsx | 133 +++----------------- web/src/pages/Svod2Page/svod2Styles.js | 117 +++++++++++++++++ web/src/pages/Svod2Page/svod2Styles.test.js | 67 ++++++++++ 3 files changed, 198 insertions(+), 119 deletions(-) create mode 100644 web/src/pages/Svod2Page/svod2Styles.js create mode 100644 web/src/pages/Svod2Page/svod2Styles.test.js diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx index 76bd120..932a483 100644 --- a/web/src/pages/Svod2Page/Svod2Page.jsx +++ b/web/src/pages/Svod2Page/Svod2Page.jsx @@ -6,11 +6,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; +import { getColumnVirtualizerOptions } from '../../components/RealtimeTable/utils/columnLayoutUtils'; import { PrimaryButton } from '../../components/common/Buttons/Buttons'; import DenominationSelect from '../../components/common/DenominationSelect'; import useDenomination from '../../components/common/useDenomination'; import { useSvod } from '../SvodPage/SvodContext'; import { SvodFilters } from '../SvodPage/SvodFilters'; +import { SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles'; const SHEETS = [ { value: 'GO', label: 'ССП' }, @@ -27,10 +29,9 @@ const ROW_VIRTUALIZER_OPTIONS = { estimateSize: () => ROW_HEIGHT, measureElement: (element) => element?.offsetHeight || ROW_HEIGHT, }; -const COLUMN_BAND_COLORS = { - head: ['#f8f9fa', '#f1f7f3'], - body: ['#ffffff', '#f8fbf9'], -}; + +const COLUMN_VIRTUALIZER_OVERSCAN = 3; +const getSvod2ColumnVirtualizerOptions = (args) => ({ ...getColumnVirtualizerOptions(args), overscan: COLUMN_VIRTUALIZER_OVERSCAN }); const valueAt = (row, path) => path.reduce((value, key) => value?.[key], row.data); @@ -73,11 +74,6 @@ const CARRYOVER_HEADERS = { 2: 'Корректировка из 2 квартала в 3–4 кварталы', 3: 'Корректировка из 3 квартала в 4 квартал', }; -const ESTIMATE_TYPE_STYLES = { - АХР: { backgroundColor: '#ecf9f0', borderColor: '#a7d9b8', color: '#08783e' }, - Операц: { backgroundColor: '#eff6ff', borderColor: '#93c5fd', color: '#1d4ed8' }, - КВ: { backgroundColor: '#fff7ed', borderColor: '#fdba74', color: '#c2410c' }, -}; const createCorrectionColumns = (quarter) => ({ id: `q${quarter}.correction`, @@ -165,28 +161,7 @@ const createColumns = (sheet) => [ Cell: ({ cell }) => { const value = cell.getValue(); if (!value) return '—'; - const colors = ESTIMATE_TYPE_STYLES[value] || { - backgroundColor: '#f3f4f6', - borderColor: '#d1d5db', - color: '#4b5563', - }; - return ( - - {value} - - ); + return {value}; }, }), textColumn('smeta_direction', 'Направление', ['header', 'smeta_direction'], 10, { enableColumnFilter: true, filterVariant: 'select' }), @@ -196,28 +171,7 @@ const createColumns = (sheet) => [ Cell: ({ cell }) => { const value = cell.getValue(); if (!value) return '—'; - return ( - - {value} - - ); + return {value}; }, }), textColumn('name', 'Наименование статьи', ['header', 'name'], 20), @@ -295,30 +249,6 @@ const createColumns = (sheet) => [ createActualColumns(4), ]; -const createColumnBandMap = (columns) => { - const bands = new Map(); - const addColumn = (column, band) => { - bands.set(column.id, band); - column.columns?.forEach((childColumn) => addColumn(childColumn, band)); - }; - columns.forEach((column, index) => addColumn(column, index % 2)); - return bands; -}; - -const createColumnGroupEndSet = (columns) => { - const groupEnds = new Set(); - const getLastLeafId = (column) => { - if (!column.columns?.length) return column.id; - return getLastLeafId(column.columns.at(-1)); - }; - columns.forEach((column) => { - if (!column.columns?.length) return; - groupEnds.add(column.id); - groupEnds.add(getLastLeafId(column)); - }); - return groupEnds; -}; - const disableGroupColumnFilters = (columns) => columns.map((column) => { if (!column.columns?.length) return column; @@ -336,8 +266,7 @@ const getColumnConfig = (sheet) => { const columns = disableGroupColumnFilters(createColumns(sheet)); COLUMN_CONFIG_CACHE.set(sheet, { columns, - columnBands: createColumnBandMap(columns), - columnGroupEnds: createColumnGroupEndSet(columns), + columnClassNames: createColumnClassMap(columns), }); } @@ -460,7 +389,7 @@ export default function Svod2Page() { const normalizedSearch = search.trim().toLowerCase(); return normalizedSearch ? rows.filter((row) => matchesSearch(row, normalizedSearch)) : rows; }, [hasCurrentData, rows, search]); - const { columns: rawColumns, columnBands, columnGroupEnds } = useMemo(() => getColumnConfig(sheet), [sheet]); + const { columns: rawColumns, columnClassNames } = useMemo(() => getColumnConfig(sheet), [sheet]); const columns = useMemo(() => scaleNumericColumns(rawColumns, divisor), [rawColumns, divisor]); const table = useMaterialReactTable({ columns, @@ -475,6 +404,8 @@ export default function Svod2Page() { enableHiding: false, enablePagination: false, enableRowVirtualization: true, + enableColumnVirtualization: true, + columnVirtualizerOptions: getSvod2ColumnVirtualizerOptions, enableTopToolbar: false, enableBottomToolbar: false, enableSorting: false, @@ -497,15 +428,7 @@ export default function Svod2Page() { flexDirection: 'column', }, }, - muiTableContainerProps: { - sx: { - flex: 1, - overflow: 'auto', - minHeight: 0, - height: '100%', - '& thead': { position: 'sticky', top: 0, zIndex: 10 }, - }, - }, + muiTableContainerProps: { sx: SVOD2_TABLE_CONTAINER_SX }, muiTableHeadProps: { sx: { '& tr:not(:last-of-type) > th > .MuiCollapse-root': { @@ -513,37 +436,9 @@ export default function Svod2Page() { }, }, }, - muiTableHeadCellProps: ({ column }) => ({ - sx: { - position: 'sticky', - top: 0, - boxShadow: 'none', - backgroundColor: COLUMN_BAND_COLORS.head[columnBands.get(column.id) ?? 0], - borderRight: columnGroupEnds.has(column.id) ? '0.1875rem solid #9eafa3' : '0.0625rem solid #e5e7eb', - color: '#4a5565', - fontSize: '0.75rem', - fontWeight: 700, - lineHeight: 1.2, - px: '0.75rem', - py: '0.5rem', - whiteSpace: 'nowrap', - opacity: 1, - zIndex: column.getIsPinned() ? 5 : undefined, - }, - }), + muiTableHeadCellProps: ({ column }) => ({ className: columnClassNames.get(column.id) }), muiTableHeadRowProps: { sx: { boxShadow: 'none' } }, - muiTableBodyCellProps: ({ column }) => ({ - sx: { - backgroundColor: COLUMN_BAND_COLORS.body[columnBands.get(column.id) ?? 0], - borderRight: columnGroupEnds.has(column.id) ? '0.1875rem solid #9eafa3' : '0.0625rem solid #edf0f2', - fontSize: '0.8125rem', - px: '0.75rem', - py: '0.5rem', - whiteSpace: 'nowrap', - opacity: 1, - zIndex: column.getIsPinned() ? 2 : undefined, - }, - }), + muiTableBodyCellProps: ({ column }) => ({ className: columnClassNames.get(column.id) }), localization: { ...MRT_Localization_RU, noRecordsToDisplay: 'Нет данных для выбранных параметров', diff --git a/web/src/pages/Svod2Page/svod2Styles.js b/web/src/pages/Svod2Page/svod2Styles.js new file mode 100644 index 0000000..09fffd9 --- /dev/null +++ b/web/src/pages/Svod2Page/svod2Styles.js @@ -0,0 +1,117 @@ +// Чистые функции и статичные стили свода 2. Без JSX, чтобы тестироваться через node --test. + +export const createColumnBandMap = (columns) => { + const bands = new Map(); + const addColumn = (column, band) => { + bands.set(column.id, band); + column.columns?.forEach((childColumn) => addColumn(childColumn, band)); + }; + columns.forEach((column, index) => addColumn(column, index % 2)); + return bands; +}; + +export const createColumnGroupEndSet = (columns) => { + const groupEnds = new Set(); + const getLastLeafId = (column) => { + if (!column.columns?.length) return column.id; + return getLastLeafId(column.columns.at(-1)); + }; + columns.forEach((column) => { + if (!column.columns?.length) return; + groupEnds.add(column.id); + groupEnds.add(getLastLeafId(column)); + }); + return groupEnds; +}; + +export const getSvod2CellClassName = (columnId, bands, groupEnds) => { + const classNames = []; + if (bands.get(columnId) === 1) classNames.push('svod2-band-1'); + if (groupEnds.has(columnId)) classNames.push('svod2-group-end'); + return classNames.length ? classNames.join(' ') : undefined; +}; + +// columnId -> className, считается один раз на лист, а не на каждую ячейку. +export const createColumnClassMap = (columns) => { + const bands = createColumnBandMap(columns); + const groupEnds = createColumnGroupEndSet(columns); + const classMap = new Map(); + for (const columnId of bands.keys()) classMap.set(columnId, getSvod2CellClassName(columnId, bands, groupEnds)); + return classMap; +}; + +const ESTIMATE_TYPE_CLASSES = { + АХР: 'svod2-badge-ahr', + Операц: 'svod2-badge-oper', + КВ: 'svod2-badge-kv', +}; + +export const getEstimateTypeClassName = (value) => `svod2-badge ${ESTIMATE_TYPE_CLASSES[value] || 'svod2-badge-default'}`; + +const GROUP_END_BORDER = '0.1875rem solid #9eafa3'; + +// Одно правило на контейнере вместо sx в каждой ячейке и заголовке. +// Специфичность (класс контейнера + элемент) выше, чем у sx самой MRT-ячейки. +export const SVOD2_TABLE_CONTAINER_SX = { + flex: 1, + overflow: 'auto', + minHeight: 0, + height: '100%', + '& thead': { position: 'sticky', top: 0, zIndex: 10 }, + '& td': { + backgroundColor: '#ffffff', + borderRight: '0.0625rem solid #edf0f2', + fontSize: '0.8125rem', + padding: '0.5rem 0.75rem', + whiteSpace: 'nowrap', + opacity: 1, + }, + '& td.svod2-band-1': { backgroundColor: '#f8fbf9' }, + '& td.svod2-group-end': { borderRight: GROUP_END_BORDER }, + '& td[data-pinned="true"]': { zIndex: 2 }, + '& th': { + position: 'sticky', + top: 0, + boxShadow: 'none', + backgroundColor: '#f8f9fa', + borderRight: '0.0625rem solid #e5e7eb', + color: '#4a5565', + fontSize: '0.75rem', + fontWeight: 700, + lineHeight: 1.2, + padding: '0.5rem 0.75rem', + whiteSpace: 'nowrap', + opacity: 1, + }, + '& th.svod2-band-1': { backgroundColor: '#f1f7f3' }, + '& th.svod2-group-end': { borderRight: GROUP_END_BORDER }, + '& th[data-pinned="true"]': { zIndex: 5 }, + '& .svod2-badge': { + display: 'inline-block', + padding: '0.25rem 0.625rem', + border: '0.0625rem solid', + borderRadius: '0.5rem', + fontWeight: 600, + lineHeight: 1.35, + whiteSpace: 'nowrap', + }, + '& .svod2-badge-ahr': { backgroundColor: '#ecf9f0', borderColor: '#a7d9b8', color: '#08783e' }, + '& .svod2-badge-oper': { backgroundColor: '#eff6ff', borderColor: '#93c5fd', color: '#1d4ed8' }, + '& .svod2-badge-kv': { backgroundColor: '#fff7ed', borderColor: '#fdba74', color: '#c2410c' }, + '& .svod2-badge-default': { backgroundColor: '#f3f4f6', borderColor: '#d1d5db', color: '#4b5563' }, + '& .svod2-org-badge': { + display: 'inline-block', + maxWidth: '100%', + padding: '0.25rem 0.625rem', + border: '0.0625rem solid #a7d9b8', + borderRadius: '0.5rem', + backgroundColor: '#ecf9f0', + color: '#08783e', + fontWeight: 600, + lineHeight: 1.35, + textTransform: 'uppercase', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, +}; diff --git a/web/src/pages/Svod2Page/svod2Styles.test.js b/web/src/pages/Svod2Page/svod2Styles.test.js new file mode 100644 index 0000000..ffdb98c --- /dev/null +++ b/web/src/pages/Svod2Page/svod2Styles.test.js @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + SVOD2_TABLE_CONTAINER_SX, + createColumnBandMap, + createColumnClassMap, + createColumnGroupEndSet, + getEstimateTypeClassName, + getSvod2CellClassName, +} from './svod2Styles.js'; + +const columns = [ + { id: 'a' }, + { id: 'g1', columns: [{ id: 'g1.x' }, { id: 'g1.sub', columns: [{ id: 'g1.sub.y' }, { id: 'g1.sub.z' }] }] }, + { id: 'b' }, + { id: 'g2', columns: [{ id: 'g2.x' }, { id: 'g2.y' }] }, +]; + +test('bands alternate by top-level column and are inherited by children', () => { + const bands = createColumnBandMap(columns); + assert.equal(bands.get('a'), 0); + assert.equal(bands.get('g1'), 1); + assert.equal(bands.get('g1.sub.z'), 1); + assert.equal(bands.get('b'), 0); + assert.equal(bands.get('g2.y'), 1); +}); + +test('group ends contain the group id and its last leaf only', () => { + const ends = createColumnGroupEndSet(columns); + assert.deepEqual([...ends].sort(), ['g1', 'g1.sub.z', 'g2', 'g2.y']); +}); + +test('cell class name combines band and group-end', () => { + const bands = createColumnBandMap(columns); + const ends = createColumnGroupEndSet(columns); + assert.equal(getSvod2CellClassName('a', bands, ends), undefined); + assert.equal(getSvod2CellClassName('g1.x', bands, ends), 'svod2-band-1'); + assert.equal(getSvod2CellClassName('g1.sub.z', bands, ends), 'svod2-band-1 svod2-group-end'); + assert.equal(getSvod2CellClassName('unknown', bands, ends), undefined); +}); + +test('class map is precomputed for every column id', () => { + const map = createColumnClassMap(columns); + assert.equal(map.size, 10); + assert.equal(map.get('g2.y'), 'svod2-band-1 svod2-group-end'); + assert.equal(map.get('a'), undefined); +}); + +test('estimate type class falls back to default', () => { + assert.equal(getEstimateTypeClassName('АХР'), 'svod2-badge svod2-badge-ahr'); + assert.equal(getEstimateTypeClassName('Прочее'), 'svod2-badge svod2-badge-default'); +}); + +test('container styles define cell, band, group-end and pinned rules', () => { + for (const key of [ + '& td', + '& th', + '& td.svod2-band-1', + '& th.svod2-band-1', + '& td.svod2-group-end', + '& th.svod2-group-end', + '& td[data-pinned="true"]', + '& th[data-pinned="true"]', + ]) { + assert.ok(SVOD2_TABLE_CONTAINER_SX[key], key); + } +}); -- 2.47.2 From b834f1008d7522959ed17905259c401a6bd78760 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 12:07:38 +0400 Subject: [PATCH 2/3] =?UTF-8?q?fix(svod2):=20grid-no-grow=20=D0=B8=20enabl?= =?UTF-8?q?eColumnPinning=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B8=D1=80=D1=82?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/pages/Svod2Page/Svod2Page.jsx | 23 ++++++++++++++------- web/src/pages/Svod2Page/svod2Styles.js | 7 +++++++ web/src/pages/Svod2Page/svod2Styles.test.js | 6 ++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx index 932a483..c129adf 100644 --- a/web/src/pages/Svod2Page/Svod2Page.jsx +++ b/web/src/pages/Svod2Page/Svod2Page.jsx @@ -12,7 +12,7 @@ import DenominationSelect from '../../components/common/DenominationSelect'; import useDenomination from '../../components/common/useDenomination'; import { useSvod } from '../SvodPage/SvodContext'; import { SvodFilters } from '../SvodPage/SvodFilters'; -import { SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles'; +import { SVOD2_LAYOUT_OPTIONS, SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles'; const SHEETS = [ { value: 'GO', label: 'ССП' }, @@ -273,12 +273,13 @@ 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) }; + }); const matchesSearch = (row, query) => { const searchableData = { @@ -405,6 +406,7 @@ export default function Svod2Page() { enablePagination: false, enableRowVirtualization: true, enableColumnVirtualization: true, + ...SVOD2_LAYOUT_OPTIONS, columnVirtualizerOptions: getSvod2ColumnVirtualizerOptions, enableTopToolbar: false, enableBottomToolbar: false, @@ -487,7 +489,12 @@ export default function Svod2Page() { onChange={(_event, value) => setSheet(value)} sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}> {SHEETS.map((item) => ( - + ))} { assert.ok(SVOD2_TABLE_CONTAINER_SX[key], key); } }); + +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); +}); -- 2.47.2 From 4cb0862ca5c20f5be49417990dff56a4710e73c9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 12:18:41 +0400 Subject: [PATCH 3/3] =?UTF-8?q?fix(svod2):=20=D0=B7=D0=B0=D0=B3=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D0=BE=D0=BA=20=D0=B1=D0=B5=D0=B7=20=D0=B2?= =?UTF-8?q?=D0=B8=D1=80=D1=82=D1=83=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA,=20?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B=20=D0=BD=D0=B0=D0=B4=20?= =?UTF-8?q?=D1=81=D0=B2=D0=BE=D0=B8=D0=BC=D0=B8=20=D0=BA=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D0=BA=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MRT при виртуализации колонок рисует каждую строку заголовка по индексам листовых колонок, из-за чего двухуровневые группы разъезжались. Штатный thead отключён (enableTableHead: false), тот же MRT_TableHead без columnVirtualizer вставляется порталом в : все заголовки сразу, стили и встроенные фильтры MRT сохраняются. Тело по-прежнему виртуализировано по строкам и колонкам. Стили: order -1 для портального thead, flexShrink 0 у td/th (распорки виртуализации сжимались и сдвигали тело на 2-8 px), скрыта подложка ::before закреплённых ячеек (перекрывала чередование фона). Co-Authored-By: Claude Opus 5.5 --- web/src/pages/Svod2Page/Svod2Page.jsx | 17 +++++++++++++++-- web/src/pages/Svod2Page/svod2Styles.js | 8 +++++++- web/src/pages/Svod2Page/svod2Styles.test.js | 7 +++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx index c129adf..16bf2f6 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, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; @@ -298,6 +299,8 @@ export default function Svod2Page() { const navigate = useNavigate(); const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = 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 [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState); @@ -406,6 +409,11 @@ 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, @@ -446,7 +454,12 @@ export default function Svod2Page() { noRecordsToDisplay: 'Нет данных для выбранных параметров', }, }); - let content = ; + let content = ( + <> + + {tableElement && createPortal(, tableElement)} + + ); if (!hasCurrentData && !loading && !error) { content = ( diff --git a/web/src/pages/Svod2Page/svod2Styles.js b/web/src/pages/Svod2Page/svod2Styles.js index cd56cbe..c92bd9a 100644 --- a/web/src/pages/Svod2Page/svod2Styles.js +++ b/web/src/pages/Svod2Page/svod2Styles.js @@ -57,8 +57,11 @@ export const SVOD2_TABLE_CONTAINER_SX = { overflow: 'auto', minHeight: 0, height: '100%', - '& thead': { position: 'sticky', top: 0, zIndex: 10 }, + // order: -1 — thead вставлен порталом в конец
(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', diff --git a/web/src/pages/Svod2Page/svod2Styles.test.js b/web/src/pages/Svod2Page/svod2Styles.test.js index 80fc8cc..2752a60 100644 --- a/web/src/pages/Svod2Page/svod2Styles.test.js +++ b/web/src/pages/Svod2Page/svod2Styles.test.js @@ -71,3 +71,10 @@ 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'); +}); -- 2.47.2