From a3e53c8cfcb6374dc1be34b9606297e2ebf979c9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 17 Oct 2026 12:08:22 +0400 Subject: [PATCH] =?UTF-8?q?fix(DocumentReview):=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=D1=8F=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20=D0=9E=D0=A1=D0=92=20?= =?UTF-8?q?=E2=80=94=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B0=D1=8F=20=D0=9E?= =?UTF-8?q?=D0=A1=D0=92=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D1=80=D0=BE=D0=BD=D1=8F=D0=B5=D1=82=20=D0=B2=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=BA=D1=83=20(#38,=20AURORA-1665)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Таблица строк ОСВ получила virtual (antd 6): в DOM только видимые строки. Высота области прокрутки считается числом (osvBodyHeight) и обновляется при изменении окна. В спеку document-review-ui добавлено требование про виртуализацию. Co-Authored-By: Claude Opus 5.5 --- openspec/specs/document-review-ui/spec.md | 12 ++++++++++++ src/widgets/DocumentReview/OsvDocument.tsx | 17 ++++++++++++++++- src/widgets/DocumentReview/osvTableSize.test.ts | 17 +++++++++++++++++ src/widgets/DocumentReview/osvTableSize.ts | 13 +++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/widgets/DocumentReview/osvTableSize.test.ts create mode 100644 src/widgets/DocumentReview/osvTableSize.ts diff --git a/openspec/specs/document-review-ui/spec.md b/openspec/specs/document-review-ui/spec.md index 4191a47..b8d1cc4 100644 --- a/openspec/specs/document-review-ui/spec.md +++ b/openspec/specs/document-review-ui/spec.md @@ -79,3 +79,15 @@ column_id, amount}`, ячейка достроенной строки (`id = nul #### Scenario: Подтверждение ОСВ - **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK` - **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION` + +### Requirement: Виртуализация таблицы ОСВ-документа +Таблица строк ОСВ-документа SHALL быть виртуализирована: в DOM находятся только видимые строки, остальные не +отрисовываются. Строки по-прежнему загружаются целиком, поиск по контрагенту/ИНН и фильтр по субсчёту — клиентские; +серверной пагинации нет. Высота области прокрутки считается числом по высоте окна и пересчитывается при его изменении. +Строка «Итого по документу» закреплена внизу и выровнена по колонкам, двухъярусная шапка и колонка «Субсчёт» +закреплены, горизонтальная прокрутка сохраняется. ОСВ любого размера (десятки тысяч строк) SHALL открываться без +падения вкладки. + +#### Scenario: Большая ОСВ +- **WHEN** аналитик открывает ОСВ-документ на ~29 000 строк +- **THEN** документ открывается, число строк в DOM не зависит от размера документа, прокрутка до конца, поиск и фильтр по субсчёту работают, «Итого по документу» совпадает с суммой по всему документу diff --git a/src/widgets/DocumentReview/OsvDocument.tsx b/src/widgets/DocumentReview/OsvDocument.tsx index bb0636b..003a5ed 100644 --- a/src/widgets/DocumentReview/OsvDocument.tsx +++ b/src/widgets/DocumentReview/OsvDocument.tsx @@ -9,6 +9,7 @@ import { formatAmount } from '../../utils/money'; import { confirmButtonStyle } from './ReviewCard'; import { confirmErrorInfo } from './confirmError'; import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows'; +import { osvBodyHeight } from './osvTableSize'; const money = (key: AmountKey, title: string) => ({ key, @@ -48,6 +49,17 @@ interface ConfirmProblem { failures: FailedCheckOut[]; } +/** Высота области прокрутки числом, пересчитывается при изменении размера окна. */ +function useBodyHeight(): number { + const [height, setHeight] = useState(() => osvBodyHeight(window.innerHeight)); + useEffect(() => { + const onResize = () => setHeight(osvBodyHeight(window.innerHeight)); + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, []); + return height; +} + export function OsvDocument({ document, onChanged }: { document: DocumentOut; onChanged: () => void }) { const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); @@ -66,6 +78,7 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on }; }, [document.id]); + const bodyHeight = useBodyHeight(); const [query, setQuery] = useState(''); const [account, setAccount] = useState(); const table = useMemo(() => flattenEntries(entries), [entries]); @@ -143,6 +156,8 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on /> )} + // virtual: в DOM только видимые строки (ОСВ на десятки тысяч строк иначе роняет вкладку); нужны числовые scroll.x и scroll.y + virtual bordered size='small' rowKey='key' @@ -151,7 +166,7 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on dataSource={rows} pagination={false} locale={{ emptyText: entries.length > 0 ? 'По заданным фильтрам строк нет' : 'В документе нет строк' }} - scroll={{ y: 'calc(100vh - 340px)', x: table.hasDepartments ? 1540 : 1380 }} + scroll={{ y: bodyHeight, x: table.hasDepartments ? 1540 : 1380 }} summary={() => entries.length > 0 && ( diff --git a/src/widgets/DocumentReview/osvTableSize.test.ts b/src/widgets/DocumentReview/osvTableSize.test.ts new file mode 100644 index 0000000..856f3ca --- /dev/null +++ b/src/widgets/DocumentReview/osvTableSize.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { OSV_MIN_BODY_HEIGHT, osvBodyHeight } from './osvTableSize'; + +describe('osvBodyHeight', () => { + it('вычитает высоту обвязки из высоты окна', () => { + expect(osvBodyHeight(1000)).toBe(660); + }); + it('округляет вниз до целого', () => { + expect(osvBodyHeight(1000.7)).toBe(660); + }); + it('в низком окне не опускается ниже минимума', () => { + expect(osvBodyHeight(400)).toBe(OSV_MIN_BODY_HEIGHT); + }); + it('на нечисловом значении возвращает минимум', () => { + expect(osvBodyHeight(Number.NaN)).toBe(OSV_MIN_BODY_HEIGHT); + }); +}); diff --git a/src/widgets/DocumentReview/osvTableSize.ts b/src/widgets/DocumentReview/osvTableSize.ts new file mode 100644 index 0000000..c98ec69 --- /dev/null +++ b/src/widgets/DocumentReview/osvTableSize.ts @@ -0,0 +1,13 @@ +/** Высота тела таблицы не меньше этой, даже в низком окне. */ +export const OSV_MIN_BODY_HEIGHT = 240; +/** Сколько высоты окна занимает всё над и под телом таблицы: шапка страницы, кнопки, поиск, шапка и «Итого» таблицы. */ +export const OSV_CHROME_HEIGHT = 340; + +/** + * Высота области прокрутки таблицы ОСВ числом (виртуальной таблице antd нужен числовой scroll.y, calc() не подходит). + * Повторяет прежнее `calc(100vh - 340px)`, но не ниже минимума; мусорное значение окна даёт минимум. + */ +export function osvBodyHeight(viewportHeight: number): number { + if (!Number.isFinite(viewportHeight)) return OSV_MIN_BODY_HEIGHT; + return Math.max(OSV_MIN_BODY_HEIGHT, Math.floor(viewportHeight - OSV_CHROME_HEIGHT)); +} -- 2.47.2