fix(DocumentReview): виртуализация таблицы ОСВ — большая ОСВ больше не роняет вкладку (#38, AURORA-1665)
Таблица строк ОСВ получила virtual (antd 6): в DOM только видимые строки. Высота области прокрутки считается числом (osvBodyHeight) и обновляется при изменении окна. В спеку document-review-ui добавлено требование про виртуализацию. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1237105af2
commit
a3e53c8cfc
@ -79,3 +79,15 @@ column_id, amount}`, ячейка достроенной строки (`id = nul
|
|||||||
#### Scenario: Подтверждение ОСВ
|
#### Scenario: Подтверждение ОСВ
|
||||||
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
|
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
|
||||||
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`
|
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`
|
||||||
|
|
||||||
|
### Requirement: Виртуализация таблицы ОСВ-документа
|
||||||
|
Таблица строк ОСВ-документа SHALL быть виртуализирована: в DOM находятся только видимые строки, остальные не
|
||||||
|
отрисовываются. Строки по-прежнему загружаются целиком, поиск по контрагенту/ИНН и фильтр по субсчёту — клиентские;
|
||||||
|
серверной пагинации нет. Высота области прокрутки считается числом по высоте окна и пересчитывается при его изменении.
|
||||||
|
Строка «Итого по документу» закреплена внизу и выровнена по колонкам, двухъярусная шапка и колонка «Субсчёт»
|
||||||
|
закреплены, горизонтальная прокрутка сохраняется. ОСВ любого размера (десятки тысяч строк) SHALL открываться без
|
||||||
|
падения вкладки.
|
||||||
|
|
||||||
|
#### Scenario: Большая ОСВ
|
||||||
|
- **WHEN** аналитик открывает ОСВ-документ на ~29 000 строк
|
||||||
|
- **THEN** документ открывается, число строк в DOM не зависит от размера документа, прокрутка до конца, поиск и фильтр по субсчёту работают, «Итого по документу» совпадает с суммой по всему документу
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { formatAmount } from '../../utils/money';
|
|||||||
import { confirmButtonStyle } from './ReviewCard';
|
import { confirmButtonStyle } from './ReviewCard';
|
||||||
import { confirmErrorInfo } from './confirmError';
|
import { confirmErrorInfo } from './confirmError';
|
||||||
import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows';
|
import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows';
|
||||||
|
import { osvBodyHeight } from './osvTableSize';
|
||||||
|
|
||||||
const money = (key: AmountKey, title: string) => ({
|
const money = (key: AmountKey, title: string) => ({
|
||||||
key,
|
key,
|
||||||
@ -48,6 +49,17 @@ interface ConfirmProblem {
|
|||||||
failures: FailedCheckOut[];
|
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 }) {
|
export function OsvDocument({ document, onChanged }: { document: DocumentOut; onChanged: () => void }) {
|
||||||
const [entries, setEntries] = useState<EntryOut[]>([]);
|
const [entries, setEntries] = useState<EntryOut[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -66,6 +78,7 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
|
|||||||
};
|
};
|
||||||
}, [document.id]);
|
}, [document.id]);
|
||||||
|
|
||||||
|
const bodyHeight = useBodyHeight();
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [account, setAccount] = useState<string | undefined>();
|
const [account, setAccount] = useState<string | undefined>();
|
||||||
const table = useMemo(() => flattenEntries(entries), [entries]);
|
const table = useMemo(() => flattenEntries(entries), [entries]);
|
||||||
@ -143,6 +156,8 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Table<OsvRow>
|
<Table<OsvRow>
|
||||||
|
// virtual: в DOM только видимые строки (ОСВ на десятки тысяч строк иначе роняет вкладку); нужны числовые scroll.x и scroll.y
|
||||||
|
virtual
|
||||||
bordered
|
bordered
|
||||||
size='small'
|
size='small'
|
||||||
rowKey='key'
|
rowKey='key'
|
||||||
@ -151,7 +166,7 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
|
|||||||
dataSource={rows}
|
dataSource={rows}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
locale={{ emptyText: entries.length > 0 ? 'По заданным фильтрам строк нет' : 'В документе нет строк' }}
|
locale={{ emptyText: entries.length > 0 ? 'По заданным фильтрам строк нет' : 'В документе нет строк' }}
|
||||||
scroll={{ y: 'calc(100vh - 340px)', x: table.hasDepartments ? 1540 : 1380 }}
|
scroll={{ y: bodyHeight, x: table.hasDepartments ? 1540 : 1380 }}
|
||||||
summary={() =>
|
summary={() =>
|
||||||
entries.length > 0 && (
|
entries.length > 0 && (
|
||||||
<Table.Summary fixed>
|
<Table.Summary fixed>
|
||||||
|
|||||||
17
src/widgets/DocumentReview/osvTableSize.test.ts
Normal file
17
src/widgets/DocumentReview/osvTableSize.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
src/widgets/DocumentReview/osvTableSize.ts
Normal file
13
src/widgets/DocumentReview/osvTableSize.ts
Normal file
@ -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));
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user