From d74f5286f90e3ee81a21dd2f259190bad5a78b52 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:03:43 +0400 Subject: [PATCH] =?UTF-8?q?perf(web):=20=D1=81=D0=B2=D0=BE=D0=B4=202=20?= =?UTF-8?q?=E2=80=94=20refine=20=D0=B2=D0=BE=20=D0=B2=D1=80=D0=B5=D0=BC?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B9=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8,=20=C2=AB=D0=9D=D0=B0?= =?UTF-8?q?=D0=B9=D0=B4=D0=B5=D0=BD=D0=BE=C2=BB=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8,=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=B2=D1=8B=D1=81?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8?= 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 | 15 +++++++++++---- web/src/pages/Svod2Page/svod2Paging.js | 12 ++++++++++-- web/src/pages/Svod2Page/svod2Paging.test.js | 16 ++++++++++++++++ web/src/pages/Svod2Page/useSvod2Pages.js | 13 ++++++++++++- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx index 149cceb..b2144d2 100644 --- a/web/src/pages/Svod2Page/Svod2Page.jsx +++ b/web/src/pages/Svod2Page/Svod2Page.jsx @@ -2,7 +2,7 @@ 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_Localization_RU } from 'material-react-table/locales/ru'; -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; @@ -317,6 +317,7 @@ export default function Svod2Page() { const tableRef = useRef(null); const getScrollTop = useCallback(() => tableRef.current?.refs.tableContainerRef.current?.scrollTop ?? 0, []); + const [rowHeight, setRowHeight] = useState(ROW_HEIGHT); const { state, windowRows, padding, facetsMap, load, onScroll } = useSvod2Pages({ sheet, year, @@ -324,7 +325,7 @@ export default function Svod2Page() { queryKey: currentQueryKey, search, columnFilters, - rowHeight: ROW_HEIGHT, + rowHeight, getScrollTop, }); const { loading, error } = state; @@ -360,7 +361,7 @@ export default function Svod2Page() { enableBottomToolbar: false, enableSorting: false, getFacetedUniqueValues: (_table, columnId) => () => facetsMapRef.current.get(columnId) ?? EMPTY_FACET_MAP, - rowVirtualizerOptions: { ...ROW_VIRTUALIZER_OPTIONS, measureElement: () => ROW_HEIGHT, ...padding }, + rowVirtualizerOptions: { ...ROW_VIRTUALIZER_OPTIONS, estimateSize: () => rowHeight, measureElement: () => rowHeight, ...padding }, state: { isLoading: loading, columnFilters }, initialState: { columnPinning: { left: ['smeta_type', 'smeta_direction', 'org_name', 'name'] }, @@ -398,6 +399,12 @@ export default function Svod2Page() { }, }); tableRef.current = table; + // Единая высота строки для окна страниц: реальная высота первой строки с данными (по умолчанию ROW_HEIGHT). + useLayoutEffect(() => { + const element = tableRef.current?.refs.tableContainerRef.current?.querySelector('tbody tr:not([data-svod2-placeholder])'); + const measured = element ? Math.round(element.offsetHeight) : 0; + if (measured > 0 && measured !== rowHeight) setRowHeight(measured); + }); let content = ; if (!hasCurrentData && !loading && !error) { content = ( @@ -453,7 +460,7 @@ export default function Svod2Page() { emptyOrganizationsPlaceholder='Выберите ССП/РФ' allowSelectAll additionalFilters={}> - {shouldShowFound({ hasCurrentData, loading, facets: state.facets }) && ( + {shouldShowFound({ hasCurrentData, loading, facets: state.facets, error }) && ( {`Найдено ${state.facets.search_total.toLocaleString('ru-RU')} из ${state.facets.total.toLocaleString('ru-RU')} строк`} diff --git a/web/src/pages/Svod2Page/svod2Paging.js b/web/src/pages/Svod2Page/svod2Paging.js index 0ca4367..93c49f9 100644 --- a/web/src/pages/Svod2Page/svod2Paging.js +++ b/web/src/pages/Svod2Page/svod2Paging.js @@ -98,8 +98,16 @@ export function facetsToMaps(result) { return maps; } -export function shouldShowFound({ hasCurrentData, loading, facets }) { - return Boolean(hasCurrentData) && !loading && !!facets && facets.search_total !== facets.total; +export function shouldShowFound({ hasCurrentData, loading, facets, error = '' }) { + return Boolean(hasCurrentData) && !loading && !error && !!facets && facets.search_total !== facets.total; +} + +// Нужно ли уточнение (refine) при смене поиска/колоночных фильтров: на загруженных данных +// либо пока для текущего queryKey ещё летит первая загрузка (иначе ответы станут «текущими» по старым условиям). +export function shouldRefine({ hasCurrentData, loading, pendingQueryKey, queryKey, prevRefineKey, refineKey }) { + if (prevRefineKey === refineKey) return false; + if (hasCurrentData) return true; + return Boolean(loading) && pendingQueryKey === queryKey; } export function isPlaceholderRow(row) { diff --git a/web/src/pages/Svod2Page/svod2Paging.test.js b/web/src/pages/Svod2Page/svod2Paging.test.js index 07e9c16..ee19dc5 100644 --- a/web/src/pages/Svod2Page/svod2Paging.test.js +++ b/web/src/pages/Svod2Page/svod2Paging.test.js @@ -16,6 +16,7 @@ import { getWindowPages, getWindowRange, isPlaceholderRow, + shouldRefine, shouldShowFound, svod2PagingReducer, touchPage, @@ -279,3 +280,18 @@ test('refine поверх load: loading снимает settled нового по s = svod2PagingReducer(s, { type: 'settled', generation: 2 }); assert.equal(s.loading, false); }); + +test('«Найдено» не показывается рядом с ошибкой', () => { + assert.equal(shouldShowFound({ hasCurrentData: true, loading: false, error: 'x', facets: { total: 5, search_total: 1 } }), false); +}); +test('shouldRefine: на загруженных данных при смене условий', () => { + const base = { hasCurrentData: true, loading: false, pendingQueryKey: 'q', queryKey: 'q', prevRefineKey: 'a', refineKey: 'b' }; + assert.equal(shouldRefine(base), true); + assert.equal(shouldRefine({ ...base, refineKey: 'a' }), false); +}); +test('shouldRefine: поиск изменён во время первой загрузки (T12.7)', () => { + const base = { hasCurrentData: false, loading: true, pendingQueryKey: 'q', queryKey: 'q', prevRefineKey: 'a', refineKey: 'b' }; + assert.equal(shouldRefine(base), true); + assert.equal(shouldRefine({ ...base, pendingQueryKey: 'old' }), false); + assert.equal(shouldRefine({ ...base, loading: false }), false); +}); diff --git a/web/src/pages/Svod2Page/useSvod2Pages.js b/web/src/pages/Svod2Page/useSvod2Pages.js index 4f915a8..adbef98 100644 --- a/web/src/pages/Svod2Page/useSvod2Pages.js +++ b/web/src/pages/Svod2Page/useSvod2Pages.js @@ -17,6 +17,7 @@ import { getVisiblePage, getWindowPages, getWindowRange, + shouldRefine, svod2PagingReducer, } from './svod2Paging'; @@ -235,7 +236,17 @@ export function useSvod2Pages({ sheet, year, orgIds, queryKey, search, columnFil syncWindow(); return; } - if (previous.refineKey !== refineKey && hasCurrentData) start('refine'); + if ( + shouldRefine({ + hasCurrentData, + loading: sheetState.loading, + pendingQueryKey: sheetState.pendingQueryKey, + queryKey, + prevRefineKey: previous.refineKey, + refineKey, + }) + ) + start('refine'); }, [sheet, refineKey]); useEffect(() => {